2025-12-05 12:27:41 +00:00
|
|
|
/**
|
|
|
|
|
* 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<Uint8Array>;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Compress a ReadableStream
|
|
|
|
|
*/
|
|
|
|
|
compressStream(
|
|
|
|
|
stream: ReadableStream<Uint8Array>,
|
|
|
|
|
algorithm: TCompressionAlgorithm,
|
|
|
|
|
level?: number
|
|
|
|
|
): ReadableStream<Uint8Array>;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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<Uint8Array> {
|
|
|
|
|
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<Uint8Array>,
|
|
|
|
|
algorithm: TCompressionAlgorithm,
|
|
|
|
|
level?: number
|
|
|
|
|
): ReadableStream<Uint8Array> {
|
|
|
|
|
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<Uint8Array>({
|
|
|
|
|
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 {
|
2025-12-05 15:38:43 +00:00
|
|
|
private _brotliSupported: boolean | null = null;
|
|
|
|
|
private _isDeno: boolean;
|
2025-12-05 12:27:41 +00:00
|
|
|
|
2025-12-05 15:38:43 +00:00
|
|
|
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;
|
2025-12-05 12:27:41 +00:00
|
|
|
}
|
|
|
|
|
}
|
2025-12-05 15:38:43 +00:00
|
|
|
return this._brotliSupported;
|
2025-12-05 12:27:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getSupportedAlgorithms(): TCompressionAlgorithm[] {
|
2025-12-05 15:38:43 +00:00
|
|
|
// CompressionStream supports gzip and deflate
|
2025-12-05 12:27:41 +00:00
|
|
|
const algorithms: TCompressionAlgorithm[] = ['gzip', 'deflate'];
|
2025-12-05 15:38:43 +00:00
|
|
|
|
|
|
|
|
// Deno has native brotli via Deno.compress
|
|
|
|
|
if (this.hasDenoBrotli()) {
|
2025-12-05 12:27:41 +00:00
|
|
|
algorithms.unshift('br');
|
|
|
|
|
}
|
2025-12-05 15:38:43 +00:00
|
|
|
|
2025-12-05 12:27:41 +00:00
|
|
|
return algorithms;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async compress(
|
|
|
|
|
data: Uint8Array,
|
|
|
|
|
algorithm: TCompressionAlgorithm,
|
|
|
|
|
_level?: number
|
|
|
|
|
): Promise<Uint8Array> {
|
|
|
|
|
if (algorithm === 'identity') {
|
|
|
|
|
return data;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-05 15:38:43 +00:00
|
|
|
// 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;
|
|
|
|
|
}
|
2025-12-05 12:27:41 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-05 15:38:43 +00:00
|
|
|
// 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);
|
|
|
|
|
}
|
2025-12-05 12:27:41 +00:00
|
|
|
|
2025-12-05 15:38:43 +00:00
|
|
|
// 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;
|
|
|
|
|
}
|
2025-12-05 12:27:41 +00:00
|
|
|
|
2025-12-05 15:38:43 +00:00
|
|
|
return result;
|
|
|
|
|
} catch {
|
|
|
|
|
// Compression failed, return original
|
|
|
|
|
return data;
|
|
|
|
|
}
|
2025-12-05 12:27:41 +00:00
|
|
|
}
|
2025-12-05 15:38:43 +00:00
|
|
|
|
|
|
|
|
// Unsupported algorithm
|
|
|
|
|
return data;
|
2025-12-05 12:27:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
compressStream(
|
|
|
|
|
stream: ReadableStream<Uint8Array>,
|
|
|
|
|
algorithm: TCompressionAlgorithm,
|
|
|
|
|
_level?: number
|
|
|
|
|
): ReadableStream<Uint8Array> {
|
|
|
|
|
if (algorithm === 'identity') {
|
|
|
|
|
return stream;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-05 15:38:43 +00:00
|
|
|
// 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;
|
2025-12-05 12:27:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
2025-12-05 15:38:43 +00:00
|
|
|
const compressionStream = new CompressionStream(algorithm);
|
2025-12-05 12:27:41 +00:00
|
|
|
return stream.pipeThrough(compressionStream as unknown as TransformStream<Uint8Array, Uint8Array>);
|
|
|
|
|
} 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;
|
|
|
|
|
}
|