typedserver/ts/servertools/classes.compressor.ts
2024-02-20 17:30:46 +01:00

128 lines
3.8 KiB
TypeScript

import * as plugins from '../plugins.js';
export type TCompressionMethod = 'gzip' | 'deflate' | 'br' | 'none';
export interface ICompressionResult {
result: Buffer;
compressionMethod: TCompressionMethod;
}
export class Compressor {
private _cache: Map<string, Buffer>;
private MAX_CACHE_SIZE: number = 100 * 1024 * 1024; // 100 MB
constructor() {
this._cache = new Map<string, Buffer>();
}
private _addToCache(key: string, value: Buffer) {
this._cache.set(key, value);
this._manageCacheSize();
}
private _manageCacheSize() {
let currentSize = Array.from(this._cache.values()).reduce((acc, buffer) => acc + buffer.length, 0);
while (currentSize > this.MAX_CACHE_SIZE) {
const firstKey = this._cache.keys().next().value;
const firstValue = this._cache.get(firstKey)!;
currentSize -= firstValue.length;
this._cache.delete(firstKey);
}
}
public async compressContent(
content: Buffer,
method: 'gzip' | 'deflate' | 'br' | 'none'
): Promise<Buffer> {
const cacheKey = content.toString('base64') + method;
const cachedResult = this._cache.get(cacheKey);
if (cachedResult) {
return cachedResult;
}
return new Promise((resolve, reject) => {
const callback = (err: Error | null, result: Buffer) => {
if (err) reject(err);
else {
this._addToCache(cacheKey, result);
resolve(result);
}
};
switch (method) {
case 'gzip':
plugins.zlib.gzip(content, {
level: 1,
},callback,);
break;
case 'br':
plugins.zlib.brotliCompress(content, {}, callback);
break;
case 'deflate':
plugins.zlib.deflate(content, callback);
break;
default:
this._addToCache(cacheKey, content);
resolve(content);
}
});
}
public determineCompression(acceptEncoding: string | string[], preferredCompressionMethodsArg: TCompressionMethod[] = []) {
// Ensure acceptEncoding is a single string
const encodingString = Array.isArray(acceptEncoding)
? acceptEncoding.join(', ')
: acceptEncoding;
let compressionMethod: TCompressionMethod = 'none';
// Prioritize preferred compression methods if provided
for (const preferredMethod of preferredCompressionMethodsArg) {
if (new RegExp(`\\b${preferredMethod}\\b`).test(encodingString)) {
return preferredMethod;
}
}
// Fallback to default prioritization if no preferred method matches
if (/\bbr\b/.test(encodingString)) {
compressionMethod = 'br';
} else if (/\bgzip\b/.test(encodingString)) {
compressionMethod = 'gzip';
} else if (/\bdeflate\b/.test(encodingString)) {
compressionMethod = 'deflate';
}
return compressionMethod;
}
public async maybeCompress(requestHeaders: plugins.http.IncomingHttpHeaders, content: Buffer, preferredCompressionMethodsArg?: TCompressionMethod[]): Promise<ICompressionResult> {
const acceptEncoding = requestHeaders['accept-encoding'];
const compressionMethod = this.determineCompression(acceptEncoding, preferredCompressionMethodsArg);
const result = await this.compressContent(content, compressionMethod);
return {
result,
compressionMethod,
};
}
public createCompressionStream(method: 'gzip' | 'deflate' | 'br' | 'none') {
let compressionStream: any;
switch (method) {
case 'gzip':
compressionStream = plugins.zlib.createGzip();
case 'br':
compressionStream = plugins.zlib.createBrotliCompress({
chunkSize: 16 * 1024,
params: {
},
});
case 'deflate':
compressionStream = plugins.zlib.createDeflate();
default:
compressionStream = plugins.smartstream.createPassThrough();
}
}
}