Files
smartserve/ts/utils/utils.encoding.ts

167 lines
4.2 KiB
TypeScript

/**
* HTTP encoding utilities for Accept-Encoding parsing and compression algorithm selection
*/
// =============================================================================
// Types
// =============================================================================
/**
* Supported compression algorithms
*/
export type TCompressionAlgorithm = 'br' | 'gzip' | 'deflate' | 'identity';
/**
* Parsed encoding preference with quality value
*/
export interface IEncodingPreference {
encoding: string;
q: number;
}
// =============================================================================
// Accept-Encoding Parsing
// =============================================================================
/**
* Parse Accept-Encoding header with quality values
*
* @example
* parseAcceptEncoding('gzip, deflate, br;q=1.0, identity;q=0.5')
* // Returns: [{ encoding: 'br', q: 1.0 }, { encoding: 'gzip', q: 1.0 }, { encoding: 'deflate', q: 1.0 }, { encoding: 'identity', q: 0.5 }]
*/
export function parseAcceptEncoding(header: string | null): IEncodingPreference[] {
if (!header) {
return [{ encoding: 'identity', q: 1.0 }];
}
const preferences: IEncodingPreference[] = [];
for (const part of header.split(',')) {
const trimmed = part.trim();
if (!trimmed) continue;
const [encoding, ...params] = trimmed.split(';');
let q = 1.0;
for (const param of params) {
const [key, value] = param.trim().split('=');
if (key?.toLowerCase() === 'q' && value) {
q = parseFloat(value) || 0;
}
}
if (q > 0) {
preferences.push({ encoding: encoding.trim().toLowerCase(), q });
}
}
// Sort by quality (descending)
return preferences.sort((a, b) => b.q - a.q);
}
/**
* Select best encoding from client preferences and server support
*/
export function selectEncoding(
acceptEncoding: string | null,
supported: TCompressionAlgorithm[]
): TCompressionAlgorithm {
const preferences = parseAcceptEncoding(acceptEncoding);
for (const pref of preferences) {
if (pref.encoding === '*') {
// Wildcard - use first supported (brotli preferred)
return supported[0] ?? 'identity';
}
if (supported.includes(pref.encoding as TCompressionAlgorithm)) {
return pref.encoding as TCompressionAlgorithm;
}
}
return 'identity';
}
// =============================================================================
// Compressibility Check
// =============================================================================
/**
* Default MIME type patterns that should be compressed
*/
const DEFAULT_COMPRESSIBLE_TYPES = [
'text/',
'application/json',
'application/javascript',
'application/xml',
'application/xhtml+xml',
'application/rss+xml',
'application/atom+xml',
'application/x-javascript',
'application/ld+json',
'application/manifest+json',
'application/vnd.api+json',
'image/svg+xml',
'image/x-icon',
'font/ttf',
'font/otf',
];
/**
* MIME types that should never be compressed (already compressed)
*/
const NEVER_COMPRESS = [
'image/png',
'image/jpeg',
'image/gif',
'image/webp',
'image/avif',
'video/',
'audio/',
'application/zip',
'application/gzip',
'application/x-gzip',
'application/x-bzip2',
'application/x-7z-compressed',
'application/x-rar-compressed',
'application/wasm',
'font/woff',
'font/woff2',
];
/**
* Check if content type should be compressed
*/
export function isCompressible(
contentType: string | null,
customTypes?: string[]
): boolean {
if (!contentType) return false;
const lowerType = contentType.toLowerCase().split(';')[0].trim();
// Never compress already-compressed formats
for (const pattern of NEVER_COMPRESS) {
if (pattern.endsWith('/')) {
if (lowerType.startsWith(pattern)) return false;
} else {
if (lowerType === pattern) return false;
}
}
const types = customTypes ?? DEFAULT_COMPRESSIBLE_TYPES;
return types.some(pattern =>
pattern.endsWith('/')
? lowerType.startsWith(pattern)
: lowerType.includes(pattern)
);
}
/**
* Get default compressible types (for configuration reference)
*/
export function getDefaultCompressibleTypes(): string[] {
return [...DEFAULT_COMPRESSIBLE_TYPES];
}