feat(compression): Add cross-runtime response compression (Brotli/gzip), per-route decorators, and pre-compressed static file support
This commit is contained in:
115
ts/decorators/decorators.compress.ts
Normal file
115
ts/decorators/decorators.compress.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* Compression control decorators (@Compress, @NoCompress)
|
||||
*
|
||||
* These decorators allow per-route control over compression:
|
||||
* - @Compress(options?) - Force compression with optional settings
|
||||
* - @NoCompress() - Disable compression for this route (useful for SSE, streaming)
|
||||
*/
|
||||
|
||||
import { getControllerMetadata } from './decorators.metadata.js';
|
||||
import type { IRouteCompressionOptions } from './decorators.types.js';
|
||||
|
||||
/**
|
||||
* Set compression options for a route
|
||||
*/
|
||||
function setRouteCompression(
|
||||
target: any,
|
||||
methodName: string | symbol,
|
||||
options: IRouteCompressionOptions
|
||||
): void {
|
||||
const metadata = getControllerMetadata(target.constructor);
|
||||
|
||||
let route = metadata.routes.get(methodName);
|
||||
if (!route) {
|
||||
// Create placeholder route (will be completed by @Get/@Post/etc.)
|
||||
route = {
|
||||
method: 'GET',
|
||||
path: '',
|
||||
methodName,
|
||||
interceptors: [],
|
||||
options: {},
|
||||
};
|
||||
metadata.routes.set(methodName, route);
|
||||
}
|
||||
|
||||
route.compression = options;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Compress decorator - Force compression on a route with optional settings
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @Controller('/api')
|
||||
* class ApiController {
|
||||
* @Get('/heavy-data')
|
||||
* @Compress({ level: 11 }) // Max brotli compression
|
||||
* getHeavyData() {
|
||||
* return massiveJsonData;
|
||||
* }
|
||||
*
|
||||
* @Get('/data')
|
||||
* @Compress() // Use default settings
|
||||
* getData() {
|
||||
* return jsonData;
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function Compress(options?: { level?: number }) {
|
||||
return function <This, Args extends any[], Return>(
|
||||
target: (this: This, ...args: Args) => Return,
|
||||
context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
|
||||
) {
|
||||
context.addInitializer(function (this: This) {
|
||||
setRouteCompression(this, context.name, {
|
||||
enabled: true,
|
||||
level: options?.level,
|
||||
});
|
||||
});
|
||||
return target;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoCompress decorator - Disable compression for a route
|
||||
*
|
||||
* Useful for:
|
||||
* - Server-Sent Events (SSE)
|
||||
* - Streaming responses
|
||||
* - Already-compressed content
|
||||
* - Real-time data where latency is critical
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @Controller('/api')
|
||||
* class EventController {
|
||||
* @Get('/events')
|
||||
* @NoCompress() // SSE should not be compressed
|
||||
* getEvents() {
|
||||
* return new Response(eventStream, {
|
||||
* headers: { 'Content-Type': 'text/event-stream' }
|
||||
* });
|
||||
* }
|
||||
*
|
||||
* @Get('/video/:id')
|
||||
* @NoCompress() // Already compressed media
|
||||
* getVideo() {
|
||||
* return videoStream;
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function NoCompress() {
|
||||
return function <This, Args extends any[], Return>(
|
||||
target: (this: This, ...args: Args) => Return,
|
||||
context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
|
||||
) {
|
||||
context.addInitializer(function (this: This) {
|
||||
setRouteCompression(this, context.name, {
|
||||
enabled: false,
|
||||
});
|
||||
});
|
||||
return target;
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user