Platform Support
Platform Support
The bloqr-backend includes a platform abstraction layer that enables running in any JavaScript runtime, including:
- Deno (default — this project’s own runtime)
- Bun (supported runtime target for consumers; not adopted as this project’s own tooling)
- Node.js (via npm compatibility)
- Cloudflare Workers
- Deno Deploy
- Vercel Edge Functions
- AWS Lambda@Edge
- Web Workers (browser)
- Browsers (with server-side proxy for CORS)
The platform layer is designed to be pluggable - you can easily add or remove fetchers without modifying the core compiler.
Core Concepts
The platform layer provides:
WorkerCompiler- A platform-agnostic compiler that works without file system accessPreFetchedContentFetcher- Supply source content directly instead of fetching from URLsHttpFetcher- Standard Fetch API-based content fetching (works everywhere)CompositeFetcher- Chain multiple fetchers together (pre-fetched takes priority)PlatformDownloader- Handles!#includedirectives and conditional compilation
Local File System Access (Deno / Node.js / Bun)
FilterDownloader and FilterCompiler (the !#include/local-file-reading path used by compile() and the CLI, as opposed to the pre-fetched-content path above used for edge/browser runtimes) read local files through an injectable IFileSystem (readTextFile / writeTextFile / exists), resolved automatically by detectFileSystem() in src/downloader/ContentFetcher.ts:
- Deno —
DenoFileSystem, backed directly byDeno.readTextFile/Deno.writeTextFile/Deno.stat. - Node.js and Bun —
NodeFileSystem, backed bynode:fs/promises. One implementation covers both runtimes, since Bun implements Node’sfsmodule natively.
detectFileSystem() picks between the two by checking for the Deno global — there is no environment variable or config flag to set. ConfigurationLoader, OutputWriter, and FilterDownloader all default to detectFileSystem() when no IFileSystem is explicitly injected, so local-file sources in a filter list configuration (as opposed to http(s):// sources, which always go through the Fetch API and already worked everywhere) now resolve correctly under Deno, Node.js, and Bun alike.
The CLI (src/cli/CliApp.ts) additionally abstracts process-level operations (argv, exit codes, stdin/stdout) behind ICliProcess in src/cli/CliRuntime.ts, following the same Deno-vs-Node/Bun detection. See the CLI Reference → Runtime support for the per-runtime entry points.
Edge Runtimes (Generic)
The WorkerCompiler works in any edge runtime or serverless environment that supports the standard Fetch API. The pattern is the same across all platforms:
- Pre-fetch source content on the server (avoids CORS and network restrictions)
- Pass content to the compiler via
preFetchedContent - Configure and compile using the standard API
import { CompositeFetcher, HttpFetcher, type IConfiguration, PreFetchedContentFetcher, WorkerCompiler } from '@bloqr/compiler';
// Option 1: Use pre-fetched content (recommended for edge)async function compileWithPreFetched(sourceUrls: string[]): Promise<string[]> { // Fetch all sources const preFetched = new Map<string, string>(); for (const url of sourceUrls) { const response = await fetch(url); preFetched.set(url, await response.text()); }
const compiler = new WorkerCompiler({ preFetchedContent: preFetched }); const config: IConfiguration = { name: 'My Filter List', sources: sourceUrls.map((url) => ({ source: url })), transformations: ['Deduplicate', 'RemoveEmptyLines'], };
return compiler.compile(config);}
// Option 2: Build a custom fetcher chainfunction createCustomCompiler() { const preFetched = new PreFetchedContentFetcher( new Map([ ['local://rules', 'my-custom-rule'], ]), ); const http = new HttpFetcher(); const composite = new CompositeFetcher([preFetched, http]);
return new WorkerCompiler({ customFetcher: composite });}Cloudflare Workers
The compiler runs natively in Cloudflare Workers. A production-ready implementation is available at the repository root in the worker/ directory with a comprehensive web UI in public/.
Quick Start:
# Install dependenciesnpm install
# Run locallydeno task wrangler:dev
# Deploy to Cloudflaredeno task wrangler:deployDeployment: A wrangler.toml configuration file is provided in the repository root for easy deployment via Cloudflare’s Git integration or using wrangler deploy.
⚠️ Important: This project uses wrangler deploy for Cloudflare Workers, NOT deno deploy. While this is a Deno-based project, it deploys to Cloudflare Workers runtime. See the Cloudflare Pages Deployment Guide for Pages-specific configuration.
The production worker (worker/worker.ts) includes:
- Interactive Web UI at
/(seepublic/index.html) - API Testing Interface at
/test.html - JSON API at
POST /compile - Streaming API at
POST /compile/streamwith Server-Sent Events - Batch API at
POST /compile/batch - Async API at
POST /compile/asyncfor queue-based processing - Batch Async API at
POST /compile/batch/asyncfor queue-based batch processing - Metrics at
GET /metrics - Pre-fetched content support to bypass CORS restrictions
- Caching with KV storage
- Rate limiting
- Request body size limits (DoS protection)
- Request deduplication
- Cloudflare Queue integration for async compilation
Request Body Size Limits:
The worker validates request body sizes to prevent denial-of-service attacks via large payloads. The default limit is 1MB.
# Configure in environment (megabytes)MAX_REQUEST_BODY_MB="2" # Set to 2MB limitRequests exceeding the limit receive a 413 Payload Too Large response:
{ "error": "Request body size (2097152 bytes) exceeds maximum allowed size (1048576 bytes)"}Cloudflare Queue Support:
The worker supports asynchronous compilation through Cloudflare Queues, allowing you to:
- Offload long-running compilations to background processing
- Process batch operations without blocking
- Pre-warm the cache with popular filter lists
- Bypass rate limits for queued requests
📚 Queue Support Documentation - Complete guide for using async compilation
Tail Worker for Observability:
A Cloudflare Tail Worker is included for advanced logging and monitoring. The tail worker captures logs, exceptions, and events from the main worker in real-time.
# Deploy the tail workerdeno task wrangler:tail:deploy
# View tail worker logsdeno task wrangler:tail:logsFeatures:
- Log Persistence: Store logs in Cloudflare KV
- Error Forwarding: Send critical errors to webhooks (Slack, Discord, etc.)
- Structured Events: Format logs for external systems
- Automatic Cleanup: Configurable log retention
📚 Tail Worker Documentation - Complete guide for setup and configuration
import { type IConfiguration, WorkerCompiler } from '@bloqr/compiler';
export default { async fetch(request: Request): Promise<Response> { // Pre-fetch content on the server where there are no CORS restrictions const sourceContent = await fetch('https://example.com/filters.txt').then((r) => r.text());
const compiler = new WorkerCompiler({ preFetchedContent: { 'https://example.com/filters.txt': sourceContent, }, });
const configuration: IConfiguration = { name: 'My Filter List', sources: [ { source: 'https://example.com/filters.txt' }, ], transformations: ['Deduplicate', 'RemoveEmptyLines'], };
const result = await compiler.compile(configuration);
return new Response(result.join('\n'), { headers: { 'Content-Type': 'text/plain' }, }); },};Using the Web UI:
- Visit the root URL of your deployed worker
- Use Simple Mode for quick filter list compilation
- Use Advanced Mode for JSON configuration
- Use Test Page to test API endpoints directly
- View real-time progress with streaming compilation
Web Workers
Use WorkerCompiler in Web Workers for background compilation:
import { type IConfiguration, WorkerCompiler } from '@bloqr/compiler';
self.onmessage = async (event) => { const { configuration, preFetchedContent } = event.data;
const compiler = new WorkerCompiler({ preFetchedContent, events: { onProgress: (progress) => { self.postMessage({ type: 'progress', progress }); }, }, });
const result = await compiler.compile(configuration); self.postMessage({ type: 'complete', rules: result });};Browser Usage
For browser environments, pre-fetch all source content server-side to avoid CORS issues:
import { type IConfiguration, WorkerCompiler } from '@bloqr/compiler';
// Fetch sources through your server proxy to avoid CORSasync function fetchSources(urls: string[]): Promise<Map<string, string>> { const content = new Map<string, string>(); for (const url of urls) { const response = await fetch(`/api/proxy?url=${encodeURIComponent(url)}`); content.set(url, await response.text()); } return content;}
// Usageconst sources = await fetchSources([ 'https://example.com/filters.txt',]);
const compiler = new WorkerCompiler({ preFetchedContent: Object.fromEntries(sources),});
const configuration: IConfiguration = { name: 'Browser Compiled List', sources: [ { source: 'https://example.com/filters.txt' }, ],};
const rules = await compiler.compile(configuration);Platform API Reference
WorkerCompiler
interface WorkerCompilerOptions { // Pre-fetched content (Map or Record) preFetchedContent?: Map<string, string> | Record<string, string>;
// Custom content fetcher (for advanced use cases) customFetcher?: IContentFetcher;
// Options for the default HTTP fetcher (timeout, user agent, custom headers) httpOptions?: IHttpFetcherOptions;
// Tracing context for diagnostics and OpenTelemetry integration tracingContext?: TracingContext;
// Injectable dependencies (for testing/customization) dependencies?: WorkerCompilerDependencies;}
class WorkerCompiler { constructor(options?: WorkerCompilerOptions);
// Compile and return rules compile(configuration: IConfiguration): Promise<string[]>;
// Compile with optional benchmarking metrics compileWithMetrics( configuration: IConfiguration, benchmark?: boolean, ): Promise<WorkerCompilationResult>;}IContentFetcher
Implement this interface to create custom content fetchers:
interface IContentFetcher { canHandle(source: string): boolean; fetch(source: string): Promise<string>;}Custom Fetchers
You can implement custom fetchers for specialized use cases:
import { CompositeFetcher, HttpFetcher, type IContentFetcher, WorkerCompiler } from '@bloqr/compiler';
// Example: Redis-backed cache fetcherclass RedisCacheFetcher implements IContentFetcher { constructor(private redis: RedisClient, private ttl: number) {}
canHandle(source: string): boolean { return source.startsWith('http://') || source.startsWith('https://'); }
async fetch(source: string): Promise<string> { const cached = await this.redis.get(`filter:${source}`); if (cached) return cached;
const response = await fetch(source); const content = await response.text();
await this.redis.setex(`filter:${source}`, this.ttl, content); return content; }}
// Example: S3/R2-backed storage fetcherclass S3StorageFetcher implements IContentFetcher { constructor(private bucket: S3Bucket) {}
canHandle(source: string): boolean { return source.startsWith('s3://'); }
async fetch(source: string): Promise<string> { const key = source.replace('s3://', ''); const object = await this.bucket.get(key); return object?.text() ?? ''; }}
// Chain fetchers together - first match winsconst compiler = new WorkerCompiler({ customFetcher: new CompositeFetcher([ new RedisCacheFetcher(redis, 3600), new S3StorageFetcher(bucket), new HttpFetcher(), ]),});This pluggable architecture allows you to:
- Add caching layers (Redis, KV, memory)
- Support custom protocols (S3, R2, database)
- Implement authentication/authorization
- Add logging and metrics
- Mock sources for testing