/** * Cross-runtime compression abstraction * * Uses: * - Node.js: zlib module (native, full brotli support) * - Deno/Bun: CompressionStream API (Web Standard) */ import * as plugins from '../plugins.js'; import type { TCompressionAlgorithm } from '../utils/index.js'; import type { Transform } from 'stream'; // ============================================================================= // Compression Provider Interface // ============================================================================= export interface ICompressionProvider { /** * Compress data to Uint8Array */ compress( data: Uint8Array, algorithm: TCompressionAlgorithm, level?: number ): Promise; /** * Compress a ReadableStream */ compressStream( stream: ReadableStream, algorithm: TCompressionAlgorithm, level?: number ): ReadableStream; /** * Get supported algorithms for this runtime */ getSupportedAlgorithms(): TCompressionAlgorithm[]; } // ============================================================================= // Node.js Compression Provider (using zlib) // ============================================================================= class NodeCompressionProvider implements ICompressionProvider { getSupportedAlgorithms(): TCompressionAlgorithm[] { return ['br', 'gzip', 'deflate']; } async compress( data: Uint8Array, algorithm: TCompressionAlgorithm, level?: number ): Promise { if (algorithm === 'identity') { return data; } return new Promise((resolve, reject) => { const buffer = Buffer.from(data); const callback = (err: Error | null, result: Buffer) => { if (err) reject(err); else resolve(new Uint8Array(result)); }; switch (algorithm) { case 'br': plugins.zlib.brotliCompress( buffer, { params: { [plugins.zlib.constants.BROTLI_PARAM_QUALITY]: level ?? 4, }, }, callback ); break; case 'gzip': plugins.zlib.gzip(buffer, { level: level ?? 6 }, callback); break; case 'deflate': plugins.zlib.deflate(buffer, { level: level ?? 6 }, callback); break; default: resolve(data); } }); } compressStream( stream: ReadableStream, algorithm: TCompressionAlgorithm, level?: number ): ReadableStream { if (algorithm === 'identity') { return stream; } // Create zlib transform stream let zlibStream: Transform; switch (algorithm) { case 'br': zlibStream = plugins.zlib.createBrotliCompress({ params: { [plugins.zlib.constants.BROTLI_PARAM_QUALITY]: level ?? 4, }, }); break; case 'gzip': zlibStream = plugins.zlib.createGzip({ level: level ?? 6 }); break; case 'deflate': zlibStream = plugins.zlib.createDeflate({ level: level ?? 6 }); break; default: return stream; } // Convert Web ReadableStream to Node stream, compress, and back const reader = stream.getReader(); return new ReadableStream({ async start(controller) { // Pipe data through zlib zlibStream.on('data', (chunk: Buffer) => { controller.enqueue(new Uint8Array(chunk)); }); zlibStream.on('end', () => { controller.close(); }); zlibStream.on('error', (err) => { controller.error(err); }); // Read from source and write to zlib try { while (true) { const { done, value } = await reader.read(); if (done) { zlibStream.end(); break; } zlibStream.write(Buffer.from(value)); } } catch (err) { controller.error(err); } }, cancel() { reader.cancel(); zlibStream.destroy(); }, }); } } // ============================================================================= // Web Standard Compression Provider (Deno/Bun/Browser) // ============================================================================= class WebStandardCompressionProvider implements ICompressionProvider { private _brotliSupported: boolean | null = null; private _isDeno: boolean; constructor() { this._isDeno = typeof (globalThis as any).Deno !== 'undefined'; } /** * Check if brotli is supported via Deno.compress API */ private hasDenoBrotli(): boolean { if (this._brotliSupported === null) { if (this._isDeno) { // Deno 1.37+ has Deno.compress/decompress with brotli support const Deno = (globalThis as any).Deno; this._brotliSupported = typeof Deno?.compress === 'function'; } else { this._brotliSupported = false; } } return this._brotliSupported; } getSupportedAlgorithms(): TCompressionAlgorithm[] { // CompressionStream supports gzip and deflate const algorithms: TCompressionAlgorithm[] = ['gzip', 'deflate']; // Deno has native brotli via Deno.compress if (this.hasDenoBrotli()) { algorithms.unshift('br'); } return algorithms; } async compress( data: Uint8Array, algorithm: TCompressionAlgorithm, _level?: number ): Promise { if (algorithm === 'identity') { return data; } // Use Deno's native brotli if available if (algorithm === 'br' && this.hasDenoBrotli()) { try { const Deno = (globalThis as any).Deno; return await Deno.compress(data, 'br'); } catch { // Fall through to return original return data; } } // Use CompressionStream for gzip/deflate if (algorithm === 'gzip' || algorithm === 'deflate') { try { const stream = new CompressionStream(algorithm); const writer = stream.writable.getWriter(); const reader = stream.readable.getReader(); // Write data and close await writer.write(data as unknown as BufferSource); await writer.close(); // Collect compressed chunks const chunks: Uint8Array[] = []; while (true) { const { done, value } = await reader.read(); if (done) break; chunks.push(value); } // Concatenate chunks const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0); const result = new Uint8Array(totalLength); let offset = 0; for (const chunk of chunks) { result.set(chunk, offset); offset += chunk.length; } return result; } catch { // Compression failed, return original return data; } } // Unsupported algorithm return data; } compressStream( stream: ReadableStream, algorithm: TCompressionAlgorithm, _level?: number ): ReadableStream { if (algorithm === 'identity') { return stream; } // Brotli streaming not supported in Web Standard (Deno.compress is not streaming) // Only gzip/deflate work with CompressionStream if (algorithm !== 'gzip' && algorithm !== 'deflate') { return stream; } try { const compressionStream = new CompressionStream(algorithm); return stream.pipeThrough(compressionStream as unknown as TransformStream); } catch { // Compression not supported, return original stream return stream; } } } // ============================================================================= // Factory & Singleton // ============================================================================= let compressionProvider: ICompressionProvider | null = null; /** * Create appropriate compression provider for the current runtime */ export function createCompressionProvider(): ICompressionProvider { // Check for Node.js (has zlib module) if (typeof process !== 'undefined' && process.versions?.node) { return new NodeCompressionProvider(); } // Check for Deno if (typeof (globalThis as any).Deno !== 'undefined') { return new WebStandardCompressionProvider(); } // Check for Bun if (typeof (globalThis as any).Bun !== 'undefined') { return new WebStandardCompressionProvider(); } // Fallback to Web Standard return new WebStandardCompressionProvider(); } /** * Get the singleton compression provider */ export function getCompressionProvider(): ICompressionProvider { if (!compressionProvider) { compressionProvider = createCompressionProvider(); } return compressionProvider; }