Client Libraries & Examples
Official and community client libraries for the Adblock Compiler API.
Official Clients
Python
Modern async client using httpx with full type annotations.
from __future__ import annotations
import httpx
from dataclasses import dataclass
from typing import AsyncIterator, Iterator
from collections.abc import Callable
@dataclass
class Source:
"""Filter list source configuration."""
source: str
name: str | None = None
type: str | None = None # 'adblock' or 'hosts'
transformations: list[str] | None = None
@dataclass
class 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 AdblockCompilerError(Exception):
"""Raised when compilation fails."""
pass
class AdblockCompiler:
"""Modern async/sync Python client for Adblock Compiler API."""
DEFAULT_URL = "https://adblock-compiler.jayson-knight.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 AdblockCompilerError(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 usage
if __name__ == "__main__":
import asyncio
client = AdblockCompiler()
# 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.
// Types
interface 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 errors
class AdblockCompilerError extends Error {
constructor(
message: string,
public readonly statusCode?: number,
public readonly retryAfter?: number,
) {
super(message);
this.name = 'AdblockCompilerError';
}
}
class RateLimitError extends AdblockCompilerError {
constructor(retryAfter: number) {
super(`Rate limited. Retry after ${retryAfter}s`, 429, retryAfter);
this.name = 'RateLimitError';
}
}
// Client
class AdblockCompiler {
private readonly baseUrl: string;
private readonly maxRetries: number;
private readonly retryDelayMs: number;
static readonly DEFAULT_URL = 'https://adblock-compiler.jayson-knight.workers.dev';
static readonly DEFAULT_TRANSFORMS = ['Deduplicate', 'RemoveEmptyLines'];
constructor(options: {
baseUrl?: string;
maxRetries?: number;
retryDelayMs?: number;
} = {}) {
this.baseUrl = options.baseUrl?.replace(/\/$/, '') ?? AdblockCompiler.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 AdblockCompilerError(
`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 ?? AdblockCompiler.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 AdblockCompilerError(`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 ?? AdblockCompiler.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 usage
const client = new AdblockCompiler({ maxRetries: 3 });
// With AbortController for cancellation
const 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 updates
for 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://adblock-compiler.jayson-knight.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 Adblock 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 Adblock 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 usage
func 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://adblock-compiler.jayson-knight.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 AdblockCompiler { client: Client, base_url: String, max_retries: u32, } impl Default for AdblockCompiler { fn default() -> Self { Self::new() } } impl AdblockCompiler { 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 = AdblockCompiler::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 AdblockCompiler;
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 AdblockCompilerException : Exception
{
public HttpStatusCode? StatusCode { get; }
public int? RetryAfter { get; }
public AdblockCompilerException(string message, HttpStatusCode? statusCode = null, int? retryAfter = null)
: base(message)
{
StatusCode = statusCode;
RetryAfter = retryAfter;
}
}
public class RateLimitException : AdblockCompilerException
{
public RateLimitException(int retryAfter)
: base($"Rate limited. Retry after {retryAfter}s", HttpStatusCode.TooManyRequests, retryAfter) { }
}
public sealed class AdblockCompilerClient : IDisposable
{
private const string DefaultBaseUrl = "https://adblock-compiler.jayson-knight.workers.dev";
private static readonly string[] DefaultTransformations = ["Deduplicate", "RemoveEmptyLines"];
private readonly HttpClient _httpClient;
private readonly string _baseUrl;
private readonly int _maxRetries;
public AdblockCompilerClient(
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 AdblockCompilerException("Failed to deserialize response");
if (!result.Success)
{
throw new AdblockCompilerException($"Compilation failed: {result.Error}");
}
return result;
}
catch (RateLimitException)
{
throw;
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
lastException = ex;
}
}
throw lastException ?? new AdblockCompilerException("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 usage
public static class Program
{
public static async Task Main()
{
using var client = new AdblockCompilerClient(
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 adblock_compiler.py
JavaScript/TypeScript
# No dependencies required - uses native fetch
# Works in Node.js 18+, Deno, Bun, and all modern browsers
Go
go get # No external dependencies - uses standard library
# Save as adblock/compiler.go
Rust
# 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 needed
Error 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