Client Libraries & Examples
Client Libraries & Examples
Official and community client libraries for the Bloqr Compiler API.
Official Clients
Python
Modern async client using httpx with full type annotations.
from __future__ import annotations
import httpxfrom dataclasses import dataclassfrom typing import AsyncIterator, Iteratorfrom collections.abc import Callable
@dataclassclass Source: """Filter list source configuration.""" source: str name: str | None = None type: str | None = None # 'adblock' or 'hosts' transformations: list[str] | None = None
@dataclassclass CompileResult: """Compilation result with metrics.""" success: bool rules: list[str] rule_count: int cached: bool = False metrics: dict | None = None error: str | None = None
class BloqrCompilerError(Exception): """Raised when compilation fails.""" pass
class BloqrCompiler: """Modern async/sync Python client for Bloqr Compiler API."""
DEFAULT_URL = "https://bloqr-backend.jk-com.workers.dev" DEFAULT_TRANSFORMS = ["Deduplicate", "RemoveEmptyLines"]
def __init__( self, base_url: str = DEFAULT_URL, timeout: float = 30.0, max_retries: int = 3, ) -> None: self.base_url = base_url.rstrip("/") self.timeout = timeout self.max_retries = max_retries
def _build_payload( self, sources: list[Source | dict], name: str, transformations: list[str] | None, benchmark: bool, ) -> dict: source_list = [ s if isinstance(s, dict) else { "source": s.source, "name": s.name, "type": s.type, "transformations": s.transformations, } for s in sources ] return { "configuration": { "name": name, "sources": source_list, "transformations": transformations or self.DEFAULT_TRANSFORMS, }, "benchmark": benchmark, }
def _parse_result(self, data: dict) -> CompileResult: if not data.get("success", False): raise BloqrCompilerError(data.get("error", "Unknown error")) return CompileResult( success=True, rules=data.get("rules", []), rule_count=data.get("ruleCount", 0), cached=data.get("cached", False), metrics=data.get("metrics"), )
def compile( self, sources: list[Source | dict], name: str = "Compiled List", transformations: list[str] | None = None, benchmark: bool = False, ) -> CompileResult: """Synchronous compilation.""" payload = self._build_payload(sources, name, transformations, benchmark)
transport = httpx.HTTPTransport(retries=self.max_retries) with httpx.Client(transport=transport, timeout=self.timeout) as client: response = client.post( f"{self.base_url}/compile", json=payload, headers={"Content-Type": "application/json"}, ) response.raise_for_status() return self._parse_result(response.json())
async def compile_async( self, sources: list[Source | dict], name: str = "Compiled List", transformations: list[str] | None = None, benchmark: bool = False, ) -> CompileResult: """Asynchronous compilation.""" payload = self._build_payload(sources, name, transformations, benchmark)
transport = httpx.AsyncHTTPTransport(retries=self.max_retries) async with httpx.AsyncClient(transport=transport, timeout=self.timeout) as client: response = await client.post( f"{self.base_url}/compile", json=payload, headers={"Content-Type": "application/json"}, ) response.raise_for_status() return self._parse_result(response.json())
def compile_stream( self, sources: list[Source | dict], name: str = "Compiled List", transformations: list[str] | None = None, on_event: Callable[[str, dict], None] | None = None, ) -> Iterator[tuple[str, dict]]: """Stream compilation events using SSE.""" payload = self._build_payload(sources, name, transformations, benchmark=False)
with httpx.Client(timeout=None) as client: with client.stream( "POST", f"{self.base_url}/compile/stream", json=payload, headers={"Content-Type": "application/json"}, ) as response: response.raise_for_status() event_type = ""
for line in response.iter_lines(): if line.startswith("event: "): event_type = line[7:] elif line.startswith("data: "): import json data = json.loads(line[6:]) if on_event: on_event(event_type, data) yield event_type, data
async def compile_stream_async( self, sources: list[Source | dict], name: str = "Compiled List", transformations: list[str] | None = None, ) -> AsyncIterator[tuple[str, dict]]: """Async stream compilation events using SSE.""" payload = self._build_payload(sources, name, transformations, benchmark=False)
async with httpx.AsyncClient(timeout=None) as client: async with client.stream( "POST", f"{self.base_url}/compile/stream", json=payload, headers={"Content-Type": "application/json"}, ) as response: response.raise_for_status() event_type = ""
async for line in response.aiter_lines(): if line.startswith("event: "): event_type = line[7:] elif line.startswith("data: "): import json data = json.loads(line[6:]) yield event_type, data
# Example usageif __name__ == "__main__": import asyncio
client = BloqrCompiler()
# Synchronous compilation result = client.compile( sources=[Source(source="https://easylist.to/easylist/easylist.txt")], name="My Filter List", benchmark=True, ) print(f"Compiled {result.rule_count} rules") if result.metrics: print(f"Duration: {result.metrics['totalDurationMs']}ms")
# Async compilation async def main(): result = await client.compile_async( sources=[{"source": "https://easylist.to/easylist/easylist.txt"}], benchmark=True, ) print(f"Async compiled {result.rule_count} rules")
# Async streaming async for event_type, data in client.compile_stream_async( sources=[{"source": "https://easylist.to/easylist/easylist.txt"}], ): if event_type == "progress": print(f"Progress: {data.get('message')}") elif event_type == "result": print(f"Complete! {data['ruleCount']} rules")
asyncio.run(main())JavaScript/TypeScript
Modern TypeScript client with retry logic, AbortController support, and custom error handling.
// Typesinterface Source { source: string; name?: string; type?: 'adblock' | 'hosts'; transformations?: string[];}
interface CompileOptions { name?: string; transformations?: string[]; benchmark?: boolean; signal?: AbortSignal;}
interface CompileResult { success: boolean; rules: string[]; ruleCount: number; cached: boolean; metrics?: { totalDurationMs: number; sourceCount: number; ruleCount: number; };}
interface StreamEvent { event: 'progress' | 'result' | 'error'; data: Record<string, unknown>;}
// Custom errorsclass BloqrCompilerError extends Error { constructor( message: string, public readonly statusCode?: number, public readonly retryAfter?: number, ) { super(message); this.name = 'BloqrCompilerError'; }}
class RateLimitError extends BloqrCompilerError { constructor(retryAfter: number) { super(`Rate limited. Retry after ${retryAfter}s`, 429, retryAfter); this.name = 'RateLimitError'; }}
// Clientclass BloqrCompiler { private readonly baseUrl: string; private readonly maxRetries: number; private readonly retryDelayMs: number;
static readonly DEFAULT_URL = 'https://bloqr-backend.jk-com.workers.dev'; static readonly DEFAULT_TRANSFORMS = ['Deduplicate', 'RemoveEmptyLines'];
constructor(options: { baseUrl?: string; maxRetries?: number; retryDelayMs?: number; } = {}) { this.baseUrl = options.baseUrl?.replace(/\/$/, '') ?? BloqrCompiler.DEFAULT_URL; this.maxRetries = options.maxRetries ?? 3; this.retryDelayMs = options.retryDelayMs ?? 1000; }
private async fetchWithRetry( url: string, init: RequestInit, retries = this.maxRetries, ): Promise<Response> { let lastError: Error | undefined;
for (let attempt = 0; attempt <= retries; attempt++) { try { const response = await fetch(url, init);
if (response.status === 429) { const retryAfter = parseInt(response.headers.get('Retry-After') ?? '60', 10); throw new RateLimitError(retryAfter); }
if (!response.ok) { throw new BloqrCompilerError( `HTTP ${response.status}: ${response.statusText}`, response.status, ); }
return response; } catch (error) { lastError = error as Error;
// Don't retry on rate limits or abort if (error instanceof RateLimitError) throw error; if (init.signal?.aborted) throw error;
// Retry on network errors if (attempt < retries) { await new Promise(r => setTimeout(r, this.retryDelayMs * (attempt + 1))); } } }
throw lastError; }
async compile(sources: Source[], options: CompileOptions = {}): Promise<CompileResult> { const payload = { configuration: { name: options.name ?? 'Compiled List', sources, transformations: options.transformations ?? BloqrCompiler.DEFAULT_TRANSFORMS, }, benchmark: options.benchmark ?? false, };
const response = await this.fetchWithRetry( `${this.baseUrl}/compile`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), signal: options.signal, }, );
const result = await response.json();
if (!result.success) { throw new BloqrCompilerError(`Compilation failed: ${result.error}`); }
return result; }
async *compileStream( sources: Source[], options: Omit<CompileOptions, 'benchmark'> = {}, ): AsyncGenerator<StreamEvent> { const payload = { configuration: { name: options.name ?? 'Compiled List', sources, transformations: options.transformations ?? BloqrCompiler.DEFAULT_TRANSFORMS, }, };
const response = await this.fetchWithRetry( `${this.baseUrl}/compile/stream`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), signal: options.signal, }, );
const reader = response.body!.getReader(); const decoder = new TextDecoder(); let buffer = ''; let currentEvent = '';
try { while (true) { const { done, value } = await reader.read(); if (done) break;
buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() ?? '';
for (const line of lines) { if (line.startsWith('event: ')) { currentEvent = line.slice(7); } else if (line.startsWith('data: ')) { yield { event: currentEvent as StreamEvent['event'], data: JSON.parse(line.slice(6)), }; } } } } finally { reader.releaseLock(); } }}
// Example usageconst client = new BloqrCompiler({ maxRetries: 3 });
// With AbortController for cancellationconst controller = new AbortController();setTimeout(() => controller.abort(), 30000); // 30s timeout
try { const result = await client.compile( [{ source: 'https://easylist.to/easylist/easylist.txt' }], { name: 'My Filter List', benchmark: true, signal: controller.signal, }, );
console.log(`Compiled ${result.ruleCount} rules`); console.log(`Duration: ${result.metrics?.totalDurationMs}ms`); console.log(`Cached: ${result.cached}`);} catch (error) { if (error instanceof RateLimitError) { console.log(`Rate limited. Retry after ${error.retryAfter}s`); } else { throw error; }}
// Streaming with progress updatesfor await (const { event, data } of client.compileStream([ { source: 'https://easylist.to/easylist/easylist.txt' },])) { switch (event) { case 'progress': console.log(`Progress: ${data.message}`); break; case 'result': console.log(`Complete! ${data.ruleCount} rules`); break; case 'error': console.error(`Error: ${data.message}`); break; }}Go
Modern Go client with context support, retry logic, and proper error handling.
package adblock
import ( "bufio" "bytes" "context" "encoding/json" "errors" "fmt" "net/http" "strconv" "strings" "time")
const ( DefaultBaseURL = "https://bloqr-backend.jk-com.workers.dev" DefaultTimeout = 30 * time.Second DefaultMaxRetries = 3)
var ( ErrRateLimited = errors.New("rate limited") ErrCompilationFailed = errors.New("compilation failed"))
// Source represents a filter list source.type Source struct { Source string `json:"source"` Name string `json:"name,omitempty"` Type string `json:"type,omitempty"` Transformations []string `json:"transformations,omitempty"`}
// Metrics contains compilation performance metrics.type Metrics struct { TotalDurationMs int `json:"totalDurationMs"` SourceCount int `json:"sourceCount"` RuleCount int `json:"ruleCount"`}
// CompileResult represents the compilation response.type CompileResult struct { Success bool `json:"success"` Rules []string `json:"rules"` RuleCount int `json:"ruleCount"` Cached bool `json:"cached"` Metrics *Metrics `json:"metrics,omitempty"` Error string `json:"error,omitempty"`}
// Event represents a Server-Sent Event from streaming compilation.type Event struct { Type string Data map[string]any}
// CompileOptions configures a compilation request.type CompileOptions struct { Name string Transformations []string Benchmark bool}
// Compiler is the Bloqr Compiler API client.type Compiler struct { baseURL string client *http.Client maxRetries int}
// Option configures a Compiler.type Option func(*Compiler)
// WithBaseURL sets a custom API base URL.func WithBaseURL(url string) Option { return func(c *Compiler) { c.baseURL = strings.TrimRight(url, "/") }}
// WithTimeout sets the HTTP client timeout.func WithTimeout(d time.Duration) Option { return func(c *Compiler) { c.client.Timeout = d }}
// WithMaxRetries sets the maximum retry attempts.func WithMaxRetries(n int) Option { return func(c *Compiler) { c.maxRetries = n }}
// NewCompiler creates a new Bloqr Compiler client.func NewCompiler(opts ...Option) *Compiler { c := &Compiler{ baseURL: DefaultBaseURL, client: &http.Client{Timeout: DefaultTimeout}, maxRetries: DefaultMaxRetries, } for _, opt := range opts { opt(c) } return c}
func (c *Compiler) doWithRetry(ctx context.Context, req *http.Request) (*http.Response, error) { var lastErr error
for attempt := 0; attempt <= c.maxRetries; attempt++ { if attempt > 0 { select { case <-ctx.Done(): return nil, ctx.Err() case <-time.After(time.Duration(attempt) * time.Second): } }
resp, err := c.client.Do(req.WithContext(ctx)) if err != nil { lastErr = err continue }
if resp.StatusCode == http.StatusTooManyRequests { resp.Body.Close() retryAfter, _ := strconv.Atoi(resp.Header.Get("Retry-After")) lastErr = fmt.Errorf("%w: retry after %ds", ErrRateLimited, retryAfter) continue }
if resp.StatusCode >= 500 { resp.Body.Close() lastErr = fmt.Errorf("server error: %s", resp.Status) continue }
return resp, nil }
return nil, lastErr}
// Compile compiles filter lists and returns the result.func (c *Compiler) Compile(ctx context.Context, sources []Source, opts *CompileOptions) (*CompileResult, error) { if opts == nil { opts = &CompileOptions{} } if opts.Name == "" { opts.Name = "Compiled List" } if opts.Transformations == nil { opts.Transformations = []string{"Deduplicate", "RemoveEmptyLines"} }
payload := map[string]any{ "configuration": map[string]any{ "name": opts.Name, "sources": sources, "transformations": opts.Transformations, }, "benchmark": opts.Benchmark, }
body, err := json.Marshal(payload) if err != nil { return nil, fmt.Errorf("marshal request: %w", err) }
req, err := http.NewRequest(http.MethodPost, c.baseURL+"/compile", bytes.NewReader(body)) if err != nil { return nil, fmt.Errorf("create request: %w", err) } req.Header.Set("Content-Type", "application/json")
resp, err := c.doWithRetry(ctx, req) if err != nil { return nil, err } defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("unexpected status: %s", resp.Status) }
var result CompileResult if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, fmt.Errorf("decode response: %w", err) }
if !result.Success { return nil, fmt.Errorf("%w: %s", ErrCompilationFailed, result.Error) }
return &result, nil}
// CompileStream compiles filter lists and streams events via a channel.// The returned channel is closed when the stream ends or context is canceled.func (c *Compiler) CompileStream(ctx context.Context, sources []Source, opts *CompileOptions) (<-chan Event, <-chan error) { events := make(chan Event) errc := make(chan error, 1)
go func() { defer close(events) defer close(errc)
if opts == nil { opts = &CompileOptions{} } if opts.Name == "" { opts.Name = "Compiled List" } if opts.Transformations == nil { opts.Transformations = []string{"Deduplicate", "RemoveEmptyLines"} }
payload := map[string]any{ "configuration": map[string]any{ "name": opts.Name, "sources": sources, "transformations": opts.Transformations, }, }
body, err := json.Marshal(payload) if err != nil { errc <- fmt.Errorf("marshal request: %w", err) return }
req, err := http.NewRequest(http.MethodPost, c.baseURL+"/compile/stream", bytes.NewReader(body)) if err != nil { errc <- fmt.Errorf("create request: %w", err) return } req.Header.Set("Content-Type", "application/json")
resp, err := c.client.Do(req.WithContext(ctx)) if err != nil { errc <- err return } defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { errc <- fmt.Errorf("unexpected status: %s", resp.Status) return }
scanner := bufio.NewScanner(resp.Body) var eventType string
for scanner.Scan() { select { case <-ctx.Done(): errc <- ctx.Err() return default: }
line := scanner.Text() switch { case strings.HasPrefix(line, "event: "): eventType = strings.TrimPrefix(line, "event: ") case strings.HasPrefix(line, "data: "): var data map[string]any if err := json.Unmarshal([]byte(strings.TrimPrefix(line, "data: ")), &data); err == nil { events <- Event{Type: eventType, Data: data} } } }
if err := scanner.Err(); err != nil { errc <- err } }()
return events, errc}
// Example usagefunc main() { ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel()
client := NewCompiler( WithMaxRetries(3), WithTimeout(30*time.Second), )
// Simple compilation result, err := client.Compile(ctx, []Source{ {Source: "https://easylist.to/easylist/easylist.txt"}, }, &CompileOptions{ Name: "My Filter List", Benchmark: true, }) if err != nil { if errors.Is(err, ErrRateLimited) { fmt.Println("Rate limited, try again later") return } panic(err) }
fmt.Printf("Compiled %d rules", result.RuleCount) if result.Metrics != nil { fmt.Printf(" in %dms", result.Metrics.TotalDurationMs) } fmt.Printf(" (cached: %v)\n", result.Cached)
// Streaming compilation events, errc := client.CompileStream(ctx, []Source{ {Source: "https://easylist.to/easylist/easylist.txt"}, }, nil)
for event := range events { switch event.Type { case "progress": fmt.Printf("Progress: %v\n", event.Data["message"]) case "result": fmt.Printf("Complete! %v rules\n", event.Data["ruleCount"]) case "error": fmt.Printf("Error: %v\n", event.Data["message"]) } }
if err := <-errc; err != nil { fmt.Printf("Stream error: %v\n", err) }}Rust
Async Rust client using reqwest and tokio.
use reqwest::{Client, StatusCode};use serde::{Deserialize, Serialize};use std::time::Duration;use thiserror::Error;
const DEFAULT_BASE_URL: &str = "https://bloqr-backend.jk-com.workers.dev";
#[derive(Error, Debug)]pub enum AdblockError { #[error("HTTP error: {0}")] Http(#[from] reqwest::Error), #[error("Rate limited, retry after {0}s")] RateLimited(u64), #[error("Compilation failed: {0}")] CompilationFailed(String), #[error("Parse error: {0}")] Parse(#[from] serde_json::Error),}
#[derive(Debug, Clone, Serialize)]pub struct Source { pub source: String, #[serde(skip_serializing_if = "Option::is_none")] pub name: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] pub r#type: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] pub transformations: Option<Vec<String>>,}
impl Source { pub fn new(source: impl Into<String>) -> Self { Self { source: source.into(), name: None, r#type: None, transformations: None, } }}
#[derive(Debug, Clone, Deserialize)]#[serde(rename_all = "camelCase")]pub struct Metrics { pub total_duration_ms: u64, pub source_count: usize, pub rule_count: usize,}
#[derive(Debug, Clone, Deserialize)]#[serde(rename_all = "camelCase")]pub struct CompileResult { pub success: bool, pub rules: Vec<String>, pub rule_count: usize, #[serde(default)] pub cached: bool, pub metrics: Option<Metrics>, pub error: Option<String>,}
#[derive(Debug, Clone, Serialize)]struct CompileRequest { configuration: Configuration, benchmark: bool,}
#[derive(Debug, Clone, Serialize)]struct Configuration { name: String, sources: Vec<Source>, transformations: Vec<String>,}
pub struct BloqrCompiler { client: Client, base_url: String, max_retries: u32,}
impl Default for BloqrCompiler { fn default() -> Self { Self::new() }}
impl BloqrCompiler { pub fn new() -> Self { Self { client: Client::builder() .timeout(Duration::from_secs(30)) .build() .expect("Failed to create HTTP client"), base_url: DEFAULT_BASE_URL.to_string(), max_retries: 3, } }
pub fn with_base_url(mut self, url: impl Into<String>) -> Self { self.base_url = url.into().trim_end_matches('/').to_string(); self }
pub fn with_timeout(mut self, timeout: Duration) -> Self { self.client = Client::builder() .timeout(timeout) .build() .expect("Failed to create HTTP client"); self }
pub fn with_max_retries(mut self, retries: u32) -> Self { self.max_retries = retries; self }
pub async fn compile( &self, sources: Vec<Source>, name: Option<&str>, transformations: Option<Vec<String>>, benchmark: bool, ) -> Result<CompileResult, AdblockError> { let request = CompileRequest { configuration: Configuration { name: name.unwrap_or("Compiled List").to_string(), sources, transformations: transformations .unwrap_or_else(|| vec!["Deduplicate".into(), "RemoveEmptyLines".into()]), }, benchmark, };
let mut last_error = None;
for attempt in 0..=self.max_retries { if attempt > 0 { tokio::time::sleep(Duration::from_secs(attempt as u64)).await; }
let response = match self .client .post(format!("{}/compile", self.base_url)) .json(&request) .send() .await { Ok(resp) => resp, Err(e) => { last_error = Some(AdblockError::Http(e)); continue; } };
match response.status() { StatusCode::TOO_MANY_REQUESTS => { let retry_after = response .headers() .get("Retry-After") .and_then(|v| v.to_str().ok()) .and_then(|v| v.parse().ok()) .unwrap_or(60); last_error = Some(AdblockError::RateLimited(retry_after)); continue; } status if status.is_server_error() => { last_error = Some(AdblockError::CompilationFailed(format!( "Server error: {}", status ))); continue; } _ => {} }
let result: CompileResult = response.json().await?;
if !result.success { return Err(AdblockError::CompilationFailed( result.error.unwrap_or_else(|| "Unknown error".to_string()), )); }
return Ok(result); }
Err(last_error.unwrap_or_else(|| AdblockError::CompilationFailed("Max retries exceeded".to_string()))) }}
// Example usage#[tokio::main]async fn main() -> Result<(), AdblockError> { let client = BloqrCompiler::new() .with_max_retries(3) .with_timeout(Duration::from_secs(60));
let result = client .compile( vec![Source::new("https://easylist.to/easylist/easylist.txt")], Some("My Filter List"), None, true, ) .await?;
println!("Compiled {} rules", result.rule_count); if let Some(metrics) = &result.metrics { println!("Duration: {}ms", metrics.total_duration_ms); } println!("Cached: {}", result.cached);
Ok(())}C# / .NET
Modern C# client using HttpClient and async/await patterns.
using System.Net;using System.Net.Http.Json;using System.Runtime.CompilerServices;using System.Text.Json;using System.Text.Json.Serialization;
namespace BloqrCompiler;
public record Source( [property: JsonPropertyName("source")] string Url, [property: JsonPropertyName("name")] string? Name = null, [property: JsonPropertyName("type")] string? Type = null, [property: JsonPropertyName("transformations")] List<string>? Transformations = null);
public record Metrics( [property: JsonPropertyName("totalDurationMs")] int TotalDurationMs, [property: JsonPropertyName("sourceCount")] int SourceCount, [property: JsonPropertyName("ruleCount")] int RuleCount);
public record CompileResult( [property: JsonPropertyName("success")] bool Success, [property: JsonPropertyName("rules")] List<string> Rules, [property: JsonPropertyName("ruleCount")] int RuleCount, [property: JsonPropertyName("cached")] bool Cached = false, [property: JsonPropertyName("metrics")] Metrics? Metrics = null, [property: JsonPropertyName("error")] string? Error = null);
public record StreamEvent(string EventType, JsonElement Data);
public class BloqrCompilerException : Exception{ public HttpStatusCode? StatusCode { get; } public int? RetryAfter { get; }
public BloqrCompilerException(string message, HttpStatusCode? statusCode = null, int? retryAfter = null) : base(message) { StatusCode = statusCode; RetryAfter = retryAfter; }}
public class RateLimitException : BloqrCompilerException{ public RateLimitException(int retryAfter) : base($"Rate limited. Retry after {retryAfter}s", HttpStatusCode.TooManyRequests, retryAfter) { }}
public sealed class BloqrCompilerClient : IDisposable{ private const string DefaultBaseUrl = "https://bloqr-backend.jk-com.workers.dev"; private static readonly string[] DefaultTransformations = ["Deduplicate", "RemoveEmptyLines"];
private readonly HttpClient _httpClient; private readonly string _baseUrl; private readonly int _maxRetries;
public BloqrCompilerClient( string? baseUrl = null, TimeSpan? timeout = null, int maxRetries = 3) { _baseUrl = (baseUrl ?? DefaultBaseUrl).TrimEnd('/'); _maxRetries = maxRetries; _httpClient = new HttpClient { Timeout = timeout ?? TimeSpan.FromSeconds(30) }; }
public async Task<CompileResult> CompileAsync( IEnumerable<Source> sources, string? name = null, IEnumerable<string>? transformations = null, bool benchmark = false, CancellationToken cancellationToken = default) { var request = new { configuration = new { name = name ?? "Compiled List", sources = sources.ToList(), transformations = transformations?.ToList() ?? DefaultTransformations.ToList() }, benchmark };
Exception? lastException = null;
for (var attempt = 0; attempt <= _maxRetries; attempt++) { if (attempt > 0) { await Task.Delay(TimeSpan.FromSeconds(attempt), cancellationToken); }
try { var response = await _httpClient.PostAsJsonAsync( $"{_baseUrl}/compile", request, cancellationToken);
if (response.StatusCode == HttpStatusCode.TooManyRequests) { var retryAfter = int.TryParse( response.Headers.GetValues("Retry-After").FirstOrDefault(), out var ra) ? ra : 60; throw new RateLimitException(retryAfter); }
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<CompileResult>(cancellationToken) ?? throw new BloqrCompilerException("Failed to deserialize response");
if (!result.Success) { throw new BloqrCompilerException($"Compilation failed: {result.Error}"); }
return result; } catch (RateLimitException) { throw; } catch (OperationCanceledException) { throw; } catch (Exception ex) { lastException = ex; } }
throw lastException ?? new BloqrCompilerException("Max retries exceeded"); }
public async IAsyncEnumerable<StreamEvent> CompileStreamAsync( IEnumerable<Source> sources, string? name = null, IEnumerable<string>? transformations = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { var request = new { configuration = new { name = name ?? "Compiled List", sources = sources.ToList(), transformations = transformations?.ToList() ?? DefaultTransformations.ToList() } };
var response = await _httpClient.PostAsJsonAsync( $"{_baseUrl}/compile/stream", request, cancellationToken);
response.EnsureSuccessStatusCode();
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken); using var reader = new StreamReader(stream);
var currentEvent = "";
while (!reader.EndOfStream) { cancellationToken.ThrowIfCancellationRequested();
var line = await reader.ReadLineAsync(cancellationToken); if (string.IsNullOrEmpty(line)) continue;
if (line.StartsWith("event: ")) { currentEvent = line[7..]; } else if (line.StartsWith("data: ")) { var data = JsonSerializer.Deserialize<JsonElement>(line[6..]); yield return new StreamEvent(currentEvent, data); } } }
public void Dispose() => _httpClient.Dispose();}
// Example usagepublic static class Program{ public static async Task Main() { using var client = new BloqrCompilerClient( timeout: TimeSpan.FromSeconds(60), maxRetries: 3);
try { // Simple compilation var result = await client.CompileAsync( sources: [new Source("https://easylist.to/easylist/easylist.txt")], name: "My Filter List", benchmark: true);
Console.WriteLine($"Compiled {result.RuleCount} rules"); if (result.Metrics is not null) { Console.WriteLine($"Duration: {result.Metrics.TotalDurationMs}ms"); } Console.WriteLine($"Cached: {result.Cached}");
// Streaming compilation await foreach (var evt in client.CompileStreamAsync( sources: [new Source("https://easylist.to/easylist/easylist.txt")])) { switch (evt.EventType) { case "progress": Console.WriteLine($"Progress: {evt.Data.GetProperty("message")}"); break; case "result": Console.WriteLine($"Complete! {evt.Data.GetProperty("ruleCount")} rules"); break; case "error": Console.WriteLine($"Error: {evt.Data.GetProperty("message")}"); break; } } } catch (RateLimitException ex) { Console.WriteLine($"Rate limited. Retry after {ex.RetryAfter}s"); } }}Community Clients
Contributions welcome for additional language support:
- Ruby
- PHP
- Java
- Swift
- Kotlin
Installation
Python
pip install httpx # Modern async HTTP client# Save the client code as bloqr_compiler.pyJavaScript/TypeScript
# No dependencies required - uses native fetch# Works in Node.js 18+, Deno, Bun, and all modern browsersGo
go get # No external dependencies - uses standard library# Save as adblock/compiler.goRust
# Add to Cargo.toml[dependencies]reqwest = { version = "0.12", features = ["json"] }serde = { version = "1.0", features = ["derive"] }serde_json = "1.0"thiserror = "2.0"tokio = { version = "1", features = ["full"] }C# / .NET
# .NET 8+ required (uses native JSON and HTTP support)dotnet new console# No additional packages neededError Handling
All clients handle the following errors:
- 429 Too Many Requests: Rate limit exceeded (max 10 req/min)
- 400 Bad Request: Invalid configuration
- 500 Internal Server Error: Compilation failed
Caching
The API automatically caches compilation results for 1 hour. Check the X-Cache header:
HIT: Result served from cacheMISS: Fresh compilation
Rate Limiting
- Limit: 10 requests per minute per IP
- Window: 60 seconds (sliding)
- Response: HTTP 429 with
Retry-Afterheader
Support
- GitHub: jaypatrick/hostlistcompiler
- Issues: Submit a bug report
- API Docs: docs/api/README.md