-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathcompressor.ts
128 lines (112 loc) · 3.7 KB
/
compressor.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
import { ALL_METHODS } from '../interfaces';
import { Exception } from '../interfaces';
import { Message } from '../interfaces';
import { Method } from '../interfaces';
import { Middleware } from '../middleware';
import { vary } from '../ported/vary';
import * as zlib from 'zlib';
import { Inject } from 'injection-js';
import { Injectable } from 'injection-js';
import { InjectionToken } from 'injection-js';
import { Observable } from 'rxjs';
import { Optional } from 'injection-js';
import { catchError } from 'rxjs/operators';
import { defaultIfEmpty } from 'rxjs/operators';
import { filter } from 'rxjs/operators';
import { map } from 'rxjs/operators';
import { mapTo } from 'rxjs/operators';
import { mergeMap } from 'rxjs/operators';
import { of } from 'rxjs';
import { tap } from 'rxjs/operators';
import { throwError } from 'rxjs';
/**
* Compressor options
*
* @see https://nodejs.org/api/zlib.html#zlib_class_options
* @see http://zlib.net/manual.html#Constants
*/
/* eslint-disable @typescript-eslint/naming-convention */
export enum CompressorLevel {
NO_COMPRESSION = 0,
BEST_SPEED = 1,
BEST_COMPRESSION = 2,
DEFAULT_COMPRESSION = -1
}
export enum CompressorStrategy {
FILTERED = 1,
HUFFMAN_ONLY = 2,
RLE = 3,
FIXED = 4,
DEFAULT_STRATEGY = 0
}
/* eslint-enable @typescript-eslint/naming-convention */
export interface CompressorOpts {
level?: CompressorLevel;
methods?: Method[];
strategy?: CompressorStrategy;
threshold?: number;
}
export const COMPRESSOR_OPTS = new InjectionToken<CompressorOpts>(
'COMPRESSOR_OPTS'
);
export const COMPRESSOR_DEFAULT_OPTS: CompressorOpts = {
level: CompressorLevel.DEFAULT_COMPRESSION,
methods: ALL_METHODS,
strategy: CompressorStrategy.DEFAULT_STRATEGY,
threshold: 1024
};
/**
* Request logger
*/
@Injectable()
export class Compressor extends Middleware {
private opts: CompressorOpts;
constructor(@Optional() @Inject(COMPRESSOR_OPTS) opts: CompressorOpts) {
super();
this.opts = opts
? { ...COMPRESSOR_DEFAULT_OPTS, ...opts }
: COMPRESSOR_DEFAULT_OPTS;
}
posthandle(message$: Observable<Message>): Observable<Message> {
return message$.pipe(
mergeMap((message: Message): Observable<Message> => {
const { request, response } = message;
return of(message).pipe(
tap(({ response }) => vary(response, 'Accept-Encoding')),
map((_message: Message) => {
const accepts = (
String(request.headers['Accept-Encoding']) || ''
).split(/, /);
const willDeflate = accepts.includes('deflate');
const willGZIP = accepts.includes('gzip');
return { willDeflate, willGZIP };
}),
filter(({ willDeflate, willGZIP }) => {
const alreadyEncoded = !!request.headers['Content-Encoding'];
const size = Number(response.headers['Content-Length'] || '0');
return (
(!this.opts.methods ||
this.opts.methods.includes(request.method)) &&
response.body &&
!alreadyEncoded &&
(willDeflate || willGZIP) &&
size >= this.opts.threshold
);
}),
tap(({ willGZIP }) => {
// NOTE: prefer gzip to deflate
const type = willGZIP ? 'gzip' : 'deflate';
response.body = zlib[`${type}Sync`](response.body, this.opts);
response.headers['Content-Encoding'] = type;
response.headers['Content-Length'] = Buffer.byteLength(
response.body
);
}),
mapTo(message),
catchError(() => throwError(new Exception({ statusCode: 400 }))),
defaultIfEmpty(message)
);
})
);
}
}