feat(compression): Improve compression implementation (buffering and threshold), add Deno brotli support, add compression tests and dynamic route API
This commit is contained in:
@@ -164,29 +164,38 @@ class NodeCompressionProvider implements ICompressionProvider {
|
||||
// =============================================================================
|
||||
|
||||
class WebStandardCompressionProvider implements ICompressionProvider {
|
||||
private brotliSupported: boolean | null = null;
|
||||
private _brotliSupported: boolean | null = null;
|
||||
private _isDeno: boolean;
|
||||
|
||||
private checkBrotliSupport(): boolean {
|
||||
if (this.brotliSupported === null) {
|
||||
try {
|
||||
// Try to create a brotli stream - not all runtimes support it
|
||||
new CompressionStream('deflate');
|
||||
// Note: CompressionStream doesn't support 'br' in most runtimes yet
|
||||
this.brotliSupported = false;
|
||||
} catch {
|
||||
this.brotliSupported = false;
|
||||
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;
|
||||
return this._brotliSupported;
|
||||
}
|
||||
|
||||
getSupportedAlgorithms(): TCompressionAlgorithm[] {
|
||||
// CompressionStream supports gzip and deflate in most runtimes
|
||||
// Brotli support is limited
|
||||
// CompressionStream supports gzip and deflate
|
||||
const algorithms: TCompressionAlgorithm[] = ['gzip', 'deflate'];
|
||||
if (this.checkBrotliSupport()) {
|
||||
|
||||
// Deno has native brotli via Deno.compress
|
||||
if (this.hasDenoBrotli()) {
|
||||
algorithms.unshift('br');
|
||||
}
|
||||
|
||||
return algorithms;
|
||||
}
|
||||
|
||||
@@ -199,46 +208,54 @@ class WebStandardCompressionProvider implements ICompressionProvider {
|
||||
return data;
|
||||
}
|
||||
|
||||
// Map algorithm to CompressionStream format
|
||||
// Brotli falls back to gzip if not supported
|
||||
let format: CompressionFormat;
|
||||
if (algorithm === 'br') {
|
||||
format = this.checkBrotliSupport() ? ('br' as CompressionFormat) : 'gzip';
|
||||
} else {
|
||||
format = algorithm as CompressionFormat;
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const stream = new CompressionStream(format);
|
||||
const writer = stream.writable.getWriter();
|
||||
const reader = stream.readable.getReader();
|
||||
// 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 (cast for type compatibility)
|
||||
await writer.write(data as unknown as BufferSource);
|
||||
await writer.close();
|
||||
// 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);
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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(
|
||||
@@ -250,17 +267,14 @@ class WebStandardCompressionProvider implements ICompressionProvider {
|
||||
return stream;
|
||||
}
|
||||
|
||||
// Map algorithm to CompressionStream format
|
||||
let format: CompressionFormat;
|
||||
if (algorithm === 'br') {
|
||||
format = this.checkBrotliSupport() ? ('br' as CompressionFormat) : 'gzip';
|
||||
} else {
|
||||
format = algorithm as CompressionFormat;
|
||||
// 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(format);
|
||||
// Use type assertion for cross-runtime compatibility
|
||||
const compressionStream = new CompressionStream(algorithm);
|
||||
return stream.pipeThrough(compressionStream as unknown as TransformStream<Uint8Array, Uint8Array>);
|
||||
} catch {
|
||||
// Compression not supported, return original stream
|
||||
|
||||
Reference in New Issue
Block a user