@zero-server/sdk 0.9.0 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +460 -437
- package/index.js +414 -412
- package/lib/app.js +1172 -1172
- package/lib/auth/authorize.js +399 -399
- package/lib/auth/enrollment.js +367 -367
- package/lib/auth/index.js +57 -57
- package/lib/auth/jwt.js +731 -731
- package/lib/auth/oauth.js +362 -362
- package/lib/auth/session.js +588 -588
- package/lib/auth/trustedDevice.js +409 -409
- package/lib/auth/twoFactor.js +1150 -1150
- package/lib/auth/webauthn.js +946 -946
- package/lib/body/index.js +14 -14
- package/lib/body/json.js +109 -109
- package/lib/body/multipart.js +440 -440
- package/lib/body/raw.js +71 -71
- package/lib/body/rawBuffer.js +160 -160
- package/lib/body/sendError.js +25 -25
- package/lib/body/text.js +75 -75
- package/lib/body/typeMatch.js +41 -41
- package/lib/body/urlencoded.js +235 -235
- package/lib/cli.js +845 -845
- package/lib/cluster.js +666 -666
- package/lib/debug.js +372 -372
- package/lib/env/index.js +460 -460
- package/lib/errors.js +683 -683
- package/lib/fetch/index.js +256 -256
- package/lib/grpc/balancer.js +378 -378
- package/lib/grpc/call.js +708 -708
- package/lib/grpc/client.js +764 -764
- package/lib/grpc/codec.js +1221 -1221
- package/lib/grpc/credentials.js +398 -398
- package/lib/grpc/frame.js +262 -262
- package/lib/grpc/health.js +287 -287
- package/lib/grpc/index.js +121 -121
- package/lib/grpc/metadata.js +461 -461
- package/lib/grpc/proto.js +821 -821
- package/lib/grpc/reflection.js +590 -590
- package/lib/grpc/server.js +445 -445
- package/lib/grpc/status.js +118 -118
- package/lib/grpc/watch.js +173 -173
- package/lib/http/index.js +10 -10
- package/lib/http/request.js +727 -727
- package/lib/http/response.js +799 -799
- package/lib/lifecycle.js +557 -557
- package/lib/middleware/compress.js +230 -230
- package/lib/middleware/cookieParser.js +237 -237
- package/lib/middleware/cors.js +93 -93
- package/lib/middleware/csrf.js +136 -136
- package/lib/middleware/errorHandler.js +101 -101
- package/lib/middleware/helmet.js +175 -175
- package/lib/middleware/index.js +19 -17
- package/lib/middleware/logger.js +74 -74
- package/lib/middleware/rateLimit.js +88 -88
- package/lib/middleware/requestId.js +53 -53
- package/lib/middleware/static.js +326 -326
- package/lib/middleware/timeout.js +71 -71
- package/lib/middleware/validator.js +254 -254
- package/lib/observe/health.js +326 -326
- package/lib/observe/index.js +50 -50
- package/lib/observe/logger.js +359 -359
- package/lib/observe/metrics.js +805 -805
- package/lib/observe/tracing.js +592 -592
- package/lib/orm/adapters/json.js +290 -290
- package/lib/orm/adapters/memory.js +764 -764
- package/lib/orm/adapters/mongo.js +764 -764
- package/lib/orm/adapters/mysql.js +933 -933
- package/lib/orm/adapters/postgres.js +1144 -1144
- package/lib/orm/adapters/redis.js +1534 -1534
- package/lib/orm/adapters/sql-base.js +212 -212
- package/lib/orm/adapters/sqlite.js +858 -858
- package/lib/orm/audit.js +649 -649
- package/lib/orm/cache.js +394 -394
- package/lib/orm/geo.js +387 -387
- package/lib/orm/index.js +784 -784
- package/lib/orm/migrate.js +432 -432
- package/lib/orm/model.js +1706 -1706
- package/lib/orm/plugin.js +375 -375
- package/lib/orm/procedures.js +836 -836
- package/lib/orm/profiler.js +233 -233
- package/lib/orm/query.js +1772 -1772
- package/lib/orm/replicas.js +241 -241
- package/lib/orm/schema.js +307 -307
- package/lib/orm/search.js +380 -380
- package/lib/orm/seed/data/commerce.js +136 -136
- package/lib/orm/seed/data/internet.js +111 -111
- package/lib/orm/seed/data/locations.js +204 -204
- package/lib/orm/seed/data/names.js +338 -338
- package/lib/orm/seed/data/person.js +128 -128
- package/lib/orm/seed/data/phone.js +211 -211
- package/lib/orm/seed/data/words.js +134 -134
- package/lib/orm/seed/factory.js +178 -178
- package/lib/orm/seed/fake.js +1186 -1186
- package/lib/orm/seed/index.js +18 -18
- package/lib/orm/seed/rng.js +70 -70
- package/lib/orm/seed/seeder.js +124 -124
- package/lib/orm/seed/unique.js +68 -68
- package/lib/orm/snapshot.js +366 -366
- package/lib/orm/tenancy.js +605 -605
- package/lib/orm/views.js +350 -350
- package/lib/router/index.js +436 -436
- package/lib/sse/index.js +8 -8
- package/lib/sse/stream.js +349 -349
- package/lib/ws/connection.js +451 -451
- package/lib/ws/handshake.js +125 -125
- package/lib/ws/index.js +14 -14
- package/lib/ws/room.js +223 -223
- package/package.json +73 -73
- package/types/app.d.ts +223 -223
- package/types/auth.d.ts +520 -520
- package/types/cluster.d.ts +75 -75
- package/types/env.d.ts +80 -80
- package/types/errors.d.ts +316 -316
- package/types/fetch.d.ts +43 -43
- package/types/grpc.d.ts +432 -432
- package/types/index.d.ts +384 -384
- package/types/lifecycle.d.ts +60 -60
- package/types/middleware.d.ts +320 -320
- package/types/observe.d.ts +304 -304
- package/types/orm.d.ts +1887 -1887
- package/types/request.d.ts +109 -109
- package/types/response.d.ts +157 -157
- package/types/router.d.ts +78 -78
- package/types/sse.d.ts +78 -78
- package/types/websocket.d.ts +126 -126
|
@@ -1,230 +1,230 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module compress
|
|
3
|
-
* @description Response compression middleware using Node's built-in `zlib`.
|
|
4
|
-
* Supports gzip, deflate, and brotli (Node >= 11.7).
|
|
5
|
-
* Zero external dependencies.
|
|
6
|
-
*/
|
|
7
|
-
const zlib = require('zlib');
|
|
8
|
-
const log = require('../debug')('zero:compress');
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Default minimum response size (in bytes) to bother compressing.
|
|
12
|
-
* Responses smaller than this are sent uncompressed.
|
|
13
|
-
* @type {number}
|
|
14
|
-
*/
|
|
15
|
-
const DEFAULT_THRESHOLD = 1024;
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* MIME types that are worth compressing.
|
|
19
|
-
* Binary formats (images, video, zip) are already compressed and gain little.
|
|
20
|
-
* @type {RegExp}
|
|
21
|
-
*/
|
|
22
|
-
const COMPRESSIBLE = /^text\/|^application\/(json|javascript|xml|x-www-form-urlencoded|ld\+json|graphql|wasm)|^image\/svg\+xml/;
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Create a compression middleware.
|
|
26
|
-
*
|
|
27
|
-
* @param {object} [opts] - Configuration options.
|
|
28
|
-
* @param {number} [opts.threshold=1024] - Minimum body size in bytes to compress.
|
|
29
|
-
* @param {number} [opts.level] - Compression level (zlib.constants.Z_DEFAULT_COMPRESSION).
|
|
30
|
-
* @param {string|string[]} [opts.encoding] - Force specific encoding(s). Default: auto-negotiate.
|
|
31
|
-
* @param {Function} [opts.filter] - `(req, res) => boolean` — return false to skip compression.
|
|
32
|
-
* @returns {Function} Middleware `(req, res, next) => void`.
|
|
33
|
-
*
|
|
34
|
-
* @example
|
|
35
|
-
* const { createApp, compress } = require('@zero-server/sdk');
|
|
36
|
-
* const app = createApp();
|
|
37
|
-
* app.use(compress()); // gzip/deflate/br auto-negotiated
|
|
38
|
-
* app.use(compress({ threshold: 0 })) // compress everything
|
|
39
|
-
*/
|
|
40
|
-
function compress(opts = {})
|
|
41
|
-
{
|
|
42
|
-
const threshold = opts.threshold !== undefined ? opts.threshold : DEFAULT_THRESHOLD;
|
|
43
|
-
const level = opts.level !== undefined ? opts.level : undefined;
|
|
44
|
-
const filterFn = typeof opts.filter === 'function' ? opts.filter : null;
|
|
45
|
-
const hasBrotli = typeof zlib.createBrotliCompress === 'function';
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Choose the best encoding from the Accept-Encoding header.
|
|
49
|
-
* Parses quality values (RFC 7231) and picks the highest-priority match.
|
|
50
|
-
* Priority when equal quality: br > gzip > deflate.
|
|
51
|
-
* @private
|
|
52
|
-
* @param {string} header - HTTP header value.
|
|
53
|
-
* @returns {string|null} Best encoding name, or `null` if none acceptable.
|
|
54
|
-
*/
|
|
55
|
-
function negotiate(header)
|
|
56
|
-
{
|
|
57
|
-
if (!header) return null;
|
|
58
|
-
const encodings = { br: 0, gzip: 0, deflate: 0 };
|
|
59
|
-
const parts = header.toLowerCase().split(',');
|
|
60
|
-
for (let i = 0; i < parts.length; i++)
|
|
61
|
-
{
|
|
62
|
-
const part = parts[i].trim();
|
|
63
|
-
const semi = part.indexOf(';');
|
|
64
|
-
const name = (semi !== -1 ? part.substring(0, semi).trim() : part);
|
|
65
|
-
let q = 1;
|
|
66
|
-
if (semi !== -1)
|
|
67
|
-
{
|
|
68
|
-
const qMatch = /q\s*=\s*([0-9.]+)/.exec(part.substring(semi));
|
|
69
|
-
if (qMatch) q = parseFloat(qMatch[1]);
|
|
70
|
-
}
|
|
71
|
-
if (name in encodings) encodings[name] = q;
|
|
72
|
-
}
|
|
73
|
-
// Filter available encodings
|
|
74
|
-
if (!hasBrotli) encodings.br = 0;
|
|
75
|
-
// Pick highest quality; break ties with priority order
|
|
76
|
-
let best = null;
|
|
77
|
-
let bestQ = 0;
|
|
78
|
-
const order = hasBrotli ? ['br', 'gzip', 'deflate'] : ['gzip', 'deflate'];
|
|
79
|
-
for (let i = 0; i < order.length; i++)
|
|
80
|
-
{
|
|
81
|
-
if (encodings[order[i]] > bestQ)
|
|
82
|
-
{
|
|
83
|
-
bestQ = encodings[order[i]];
|
|
84
|
-
best = order[i];
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
return best;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Create a compression stream for the chosen encoding.
|
|
92
|
-
* @private
|
|
93
|
-
* @param {string} encoding - Content encoding.
|
|
94
|
-
* @returns {import('stream').Transform} Compression transform stream.
|
|
95
|
-
*/
|
|
96
|
-
function createStream(encoding)
|
|
97
|
-
{
|
|
98
|
-
const zlibOpts = {};
|
|
99
|
-
if (level !== undefined) zlibOpts.level = level;
|
|
100
|
-
switch (encoding)
|
|
101
|
-
{
|
|
102
|
-
case 'br':
|
|
103
|
-
return zlib.createBrotliCompress(level !== undefined
|
|
104
|
-
? { params: { [zlib.constants.BROTLI_PARAM_QUALITY]: level } }
|
|
105
|
-
: undefined);
|
|
106
|
-
case 'gzip':
|
|
107
|
-
return zlib.createGzip(zlibOpts);
|
|
108
|
-
case 'deflate':
|
|
109
|
-
return zlib.createDeflate(zlibOpts);
|
|
110
|
-
default:
|
|
111
|
-
return null;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return (req, res, next) =>
|
|
116
|
-
{
|
|
117
|
-
// Skip if client doesn't accept encoding
|
|
118
|
-
const acceptEncoding = req.headers['accept-encoding'] || '';
|
|
119
|
-
const encoding = negotiate(acceptEncoding);
|
|
120
|
-
if (!encoding) return next();
|
|
121
|
-
|
|
122
|
-
// Allow user to skip compression
|
|
123
|
-
if (filterFn && !filterFn(req, res)) return next();
|
|
124
|
-
|
|
125
|
-
// Monkey-patch the raw response's write/end to pipe through compression
|
|
126
|
-
const raw = res.raw;
|
|
127
|
-
const origWrite = raw.write.bind(raw);
|
|
128
|
-
const origEnd = raw.end.bind(raw);
|
|
129
|
-
let compressStream = null;
|
|
130
|
-
let headersWritten = false;
|
|
131
|
-
const chunks = [];
|
|
132
|
-
|
|
133
|
-
/** @private */
|
|
134
|
-
function initCompress()
|
|
135
|
-
{
|
|
136
|
-
if (compressStream) return true;
|
|
137
|
-
|
|
138
|
-
// If headers were already committed (e.g. res.sse() calls
|
|
139
|
-
// writeHead before write), we can no longer modify them.
|
|
140
|
-
if (raw.headersSent) return false;
|
|
141
|
-
|
|
142
|
-
// Check Content-Type — skip non-compressible types
|
|
143
|
-
const ct = raw.getHeader('content-type') || '';
|
|
144
|
-
if (ct && !COMPRESSIBLE.test(ct))
|
|
145
|
-
{
|
|
146
|
-
return false;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Never compress SSE streams — compression buffers
|
|
150
|
-
// the small frames and prevents real-time delivery.
|
|
151
|
-
if (ct.includes('text/event-stream'))
|
|
152
|
-
{
|
|
153
|
-
return false;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
compressStream = createStream(encoding);
|
|
157
|
-
if (!compressStream) return false;
|
|
158
|
-
|
|
159
|
-
// Remove Content-Length (we don't know compressed size ahead of time)
|
|
160
|
-
raw.removeHeader('content-length');
|
|
161
|
-
raw.removeHeader('Content-Length');
|
|
162
|
-
raw.setHeader('Content-Encoding', encoding);
|
|
163
|
-
raw.setHeader('Vary', 'Accept-Encoding');
|
|
164
|
-
log.debug('compressing with %s', encoding);
|
|
165
|
-
|
|
166
|
-
compressStream.on('data', (chunk) => origWrite(chunk));
|
|
167
|
-
compressStream.on('end', () => origEnd());
|
|
168
|
-
compressStream.on('error', (err) =>
|
|
169
|
-
{ log.error('compression error: %s', err.message); // On compression error, remove encoding header and end raw stream
|
|
170
|
-
try { raw.removeHeader('Content-Encoding'); } catch (e) { }
|
|
171
|
-
try { origEnd(); } catch (e) { }
|
|
172
|
-
});
|
|
173
|
-
return true;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
raw.write = function (chunk, enc, callback)
|
|
177
|
-
{
|
|
178
|
-
if (!headersWritten)
|
|
179
|
-
{
|
|
180
|
-
headersWritten = true;
|
|
181
|
-
const ct = raw.getHeader('content-type') || '';
|
|
182
|
-
initCompress();
|
|
183
|
-
if (compressStream)
|
|
184
|
-
{
|
|
185
|
-
compressStream.write(chunk, enc, callback);
|
|
186
|
-
return true;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
if (compressStream)
|
|
190
|
-
{
|
|
191
|
-
compressStream.write(chunk, enc, callback);
|
|
192
|
-
return true;
|
|
193
|
-
}
|
|
194
|
-
return origWrite(chunk, enc, callback);
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
raw.end = function (chunk, encoding, callback)
|
|
198
|
-
{
|
|
199
|
-
if (!headersWritten)
|
|
200
|
-
{
|
|
201
|
-
headersWritten = true;
|
|
202
|
-
|
|
203
|
-
// Check threshold — if the total body is small, skip compression
|
|
204
|
-
const totalChunk = chunk ? (Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))) : null;
|
|
205
|
-
if (totalChunk && totalChunk.length < threshold)
|
|
206
|
-
{
|
|
207
|
-
return origEnd(chunk, encoding, callback);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
if (initCompress())
|
|
211
|
-
{
|
|
212
|
-
if (chunk) compressStream.end(chunk, encoding, callback);
|
|
213
|
-
else compressStream.end(callback);
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
if (compressStream)
|
|
218
|
-
{
|
|
219
|
-
if (chunk) compressStream.end(chunk, encoding, callback);
|
|
220
|
-
else compressStream.end(callback);
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
return origEnd(chunk, encoding, callback);
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
next();
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
module.exports = compress;
|
|
1
|
+
/**
|
|
2
|
+
* @module compress
|
|
3
|
+
* @description Response compression middleware using Node's built-in `zlib`.
|
|
4
|
+
* Supports gzip, deflate, and brotli (Node >= 11.7).
|
|
5
|
+
* Zero external dependencies.
|
|
6
|
+
*/
|
|
7
|
+
const zlib = require('zlib');
|
|
8
|
+
const log = require('../debug')('zero:compress');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Default minimum response size (in bytes) to bother compressing.
|
|
12
|
+
* Responses smaller than this are sent uncompressed.
|
|
13
|
+
* @type {number}
|
|
14
|
+
*/
|
|
15
|
+
const DEFAULT_THRESHOLD = 1024;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* MIME types that are worth compressing.
|
|
19
|
+
* Binary formats (images, video, zip) are already compressed and gain little.
|
|
20
|
+
* @type {RegExp}
|
|
21
|
+
*/
|
|
22
|
+
const COMPRESSIBLE = /^text\/|^application\/(json|javascript|xml|x-www-form-urlencoded|ld\+json|graphql|wasm)|^image\/svg\+xml/;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Create a compression middleware.
|
|
26
|
+
*
|
|
27
|
+
* @param {object} [opts] - Configuration options.
|
|
28
|
+
* @param {number} [opts.threshold=1024] - Minimum body size in bytes to compress.
|
|
29
|
+
* @param {number} [opts.level] - Compression level (zlib.constants.Z_DEFAULT_COMPRESSION).
|
|
30
|
+
* @param {string|string[]} [opts.encoding] - Force specific encoding(s). Default: auto-negotiate.
|
|
31
|
+
* @param {Function} [opts.filter] - `(req, res) => boolean` — return false to skip compression.
|
|
32
|
+
* @returns {Function} Middleware `(req, res, next) => void`.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* const { createApp, compress } = require('@zero-server/sdk');
|
|
36
|
+
* const app = createApp();
|
|
37
|
+
* app.use(compress()); // gzip/deflate/br auto-negotiated
|
|
38
|
+
* app.use(compress({ threshold: 0 })) // compress everything
|
|
39
|
+
*/
|
|
40
|
+
function compress(opts = {})
|
|
41
|
+
{
|
|
42
|
+
const threshold = opts.threshold !== undefined ? opts.threshold : DEFAULT_THRESHOLD;
|
|
43
|
+
const level = opts.level !== undefined ? opts.level : undefined;
|
|
44
|
+
const filterFn = typeof opts.filter === 'function' ? opts.filter : null;
|
|
45
|
+
const hasBrotli = typeof zlib.createBrotliCompress === 'function';
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Choose the best encoding from the Accept-Encoding header.
|
|
49
|
+
* Parses quality values (RFC 7231) and picks the highest-priority match.
|
|
50
|
+
* Priority when equal quality: br > gzip > deflate.
|
|
51
|
+
* @private
|
|
52
|
+
* @param {string} header - HTTP header value.
|
|
53
|
+
* @returns {string|null} Best encoding name, or `null` if none acceptable.
|
|
54
|
+
*/
|
|
55
|
+
function negotiate(header)
|
|
56
|
+
{
|
|
57
|
+
if (!header) return null;
|
|
58
|
+
const encodings = { br: 0, gzip: 0, deflate: 0 };
|
|
59
|
+
const parts = header.toLowerCase().split(',');
|
|
60
|
+
for (let i = 0; i < parts.length; i++)
|
|
61
|
+
{
|
|
62
|
+
const part = parts[i].trim();
|
|
63
|
+
const semi = part.indexOf(';');
|
|
64
|
+
const name = (semi !== -1 ? part.substring(0, semi).trim() : part);
|
|
65
|
+
let q = 1;
|
|
66
|
+
if (semi !== -1)
|
|
67
|
+
{
|
|
68
|
+
const qMatch = /q\s*=\s*([0-9.]+)/.exec(part.substring(semi));
|
|
69
|
+
if (qMatch) q = parseFloat(qMatch[1]);
|
|
70
|
+
}
|
|
71
|
+
if (name in encodings) encodings[name] = q;
|
|
72
|
+
}
|
|
73
|
+
// Filter available encodings
|
|
74
|
+
if (!hasBrotli) encodings.br = 0;
|
|
75
|
+
// Pick highest quality; break ties with priority order
|
|
76
|
+
let best = null;
|
|
77
|
+
let bestQ = 0;
|
|
78
|
+
const order = hasBrotli ? ['br', 'gzip', 'deflate'] : ['gzip', 'deflate'];
|
|
79
|
+
for (let i = 0; i < order.length; i++)
|
|
80
|
+
{
|
|
81
|
+
if (encodings[order[i]] > bestQ)
|
|
82
|
+
{
|
|
83
|
+
bestQ = encodings[order[i]];
|
|
84
|
+
best = order[i];
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return best;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Create a compression stream for the chosen encoding.
|
|
92
|
+
* @private
|
|
93
|
+
* @param {string} encoding - Content encoding.
|
|
94
|
+
* @returns {import('stream').Transform} Compression transform stream.
|
|
95
|
+
*/
|
|
96
|
+
function createStream(encoding)
|
|
97
|
+
{
|
|
98
|
+
const zlibOpts = {};
|
|
99
|
+
if (level !== undefined) zlibOpts.level = level;
|
|
100
|
+
switch (encoding)
|
|
101
|
+
{
|
|
102
|
+
case 'br':
|
|
103
|
+
return zlib.createBrotliCompress(level !== undefined
|
|
104
|
+
? { params: { [zlib.constants.BROTLI_PARAM_QUALITY]: level } }
|
|
105
|
+
: undefined);
|
|
106
|
+
case 'gzip':
|
|
107
|
+
return zlib.createGzip(zlibOpts);
|
|
108
|
+
case 'deflate':
|
|
109
|
+
return zlib.createDeflate(zlibOpts);
|
|
110
|
+
default:
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return (req, res, next) =>
|
|
116
|
+
{
|
|
117
|
+
// Skip if client doesn't accept encoding
|
|
118
|
+
const acceptEncoding = req.headers['accept-encoding'] || '';
|
|
119
|
+
const encoding = negotiate(acceptEncoding);
|
|
120
|
+
if (!encoding) return next();
|
|
121
|
+
|
|
122
|
+
// Allow user to skip compression
|
|
123
|
+
if (filterFn && !filterFn(req, res)) return next();
|
|
124
|
+
|
|
125
|
+
// Monkey-patch the raw response's write/end to pipe through compression
|
|
126
|
+
const raw = res.raw;
|
|
127
|
+
const origWrite = raw.write.bind(raw);
|
|
128
|
+
const origEnd = raw.end.bind(raw);
|
|
129
|
+
let compressStream = null;
|
|
130
|
+
let headersWritten = false;
|
|
131
|
+
const chunks = [];
|
|
132
|
+
|
|
133
|
+
/** @private */
|
|
134
|
+
function initCompress()
|
|
135
|
+
{
|
|
136
|
+
if (compressStream) return true;
|
|
137
|
+
|
|
138
|
+
// If headers were already committed (e.g. res.sse() calls
|
|
139
|
+
// writeHead before write), we can no longer modify them.
|
|
140
|
+
if (raw.headersSent) return false;
|
|
141
|
+
|
|
142
|
+
// Check Content-Type — skip non-compressible types
|
|
143
|
+
const ct = raw.getHeader('content-type') || '';
|
|
144
|
+
if (ct && !COMPRESSIBLE.test(ct))
|
|
145
|
+
{
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Never compress SSE streams — compression buffers
|
|
150
|
+
// the small frames and prevents real-time delivery.
|
|
151
|
+
if (ct.includes('text/event-stream'))
|
|
152
|
+
{
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
compressStream = createStream(encoding);
|
|
157
|
+
if (!compressStream) return false;
|
|
158
|
+
|
|
159
|
+
// Remove Content-Length (we don't know compressed size ahead of time)
|
|
160
|
+
raw.removeHeader('content-length');
|
|
161
|
+
raw.removeHeader('Content-Length');
|
|
162
|
+
raw.setHeader('Content-Encoding', encoding);
|
|
163
|
+
raw.setHeader('Vary', 'Accept-Encoding');
|
|
164
|
+
log.debug('compressing with %s', encoding);
|
|
165
|
+
|
|
166
|
+
compressStream.on('data', (chunk) => origWrite(chunk));
|
|
167
|
+
compressStream.on('end', () => origEnd());
|
|
168
|
+
compressStream.on('error', (err) =>
|
|
169
|
+
{ log.error('compression error: %s', err.message); // On compression error, remove encoding header and end raw stream
|
|
170
|
+
try { raw.removeHeader('Content-Encoding'); } catch (e) { }
|
|
171
|
+
try { origEnd(); } catch (e) { }
|
|
172
|
+
});
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
raw.write = function (chunk, enc, callback)
|
|
177
|
+
{
|
|
178
|
+
if (!headersWritten)
|
|
179
|
+
{
|
|
180
|
+
headersWritten = true;
|
|
181
|
+
const ct = raw.getHeader('content-type') || '';
|
|
182
|
+
initCompress();
|
|
183
|
+
if (compressStream)
|
|
184
|
+
{
|
|
185
|
+
compressStream.write(chunk, enc, callback);
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (compressStream)
|
|
190
|
+
{
|
|
191
|
+
compressStream.write(chunk, enc, callback);
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
return origWrite(chunk, enc, callback);
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
raw.end = function (chunk, encoding, callback)
|
|
198
|
+
{
|
|
199
|
+
if (!headersWritten)
|
|
200
|
+
{
|
|
201
|
+
headersWritten = true;
|
|
202
|
+
|
|
203
|
+
// Check threshold — if the total body is small, skip compression
|
|
204
|
+
const totalChunk = chunk ? (Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))) : null;
|
|
205
|
+
if (totalChunk && totalChunk.length < threshold)
|
|
206
|
+
{
|
|
207
|
+
return origEnd(chunk, encoding, callback);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (initCompress())
|
|
211
|
+
{
|
|
212
|
+
if (chunk) compressStream.end(chunk, encoding, callback);
|
|
213
|
+
else compressStream.end(callback);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (compressStream)
|
|
218
|
+
{
|
|
219
|
+
if (chunk) compressStream.end(chunk, encoding, callback);
|
|
220
|
+
else compressStream.end(callback);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
return origEnd(chunk, encoding, callback);
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
next();
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
module.exports = compress;
|