fulmine.js 5.15.1 → 5.16.0
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/README.md +7 -0
- package/package.json +1 -1
- package/src/compression.js +46 -3
- package/src/request.js +29 -0
- package/src/types.d.ts +6 -0
- package/src/utils.js +1 -0
package/README.md
CHANGED
|
@@ -502,6 +502,13 @@ Server-Timing: route;desc="native", hdr;desc="not copied", db;dur=3.62, total;du
|
|
|
502
502
|
app.use(express.compression({ threshold: 1024 }));
|
|
503
503
|
```
|
|
504
504
|
|
|
505
|
+
One option is Fulmine's own, `encodings`: the list of what the middleware may answer with, out of `"br"`, `"gzip"` and `"deflate"`. What is not named is never used, however the client ranks it, and an uncompressed answer is always on offer. It exists because the preferred encoding is a cost decision, not only a size one: brotli compresses smaller but what it costs per response depends on the machine, and on a CPU where it runs expensive `encodings: ["gzip"]` buys the cheaper call for every client that accepts both.
|
|
506
|
+
|
|
507
|
+
```js
|
|
508
|
+
// answer gzip even to a client that also accepts br
|
|
509
|
+
app.use(express.compression({ level: 1, encodings: ["gzip"] }));
|
|
510
|
+
```
|
|
511
|
+
|
|
505
512
|
Runnable: [`examples/compression.js`](./examples/compression.js).
|
|
506
513
|
|
|
507
514
|
5. If a route answers with a JSON shape you know in advance, [express-fast-json-stringify](https://www.npmjs.com/package/express-fast-json-stringify) compiles that shape into a serializer and `res.fastJson()` replaces `res.json()`. `JSON.stringify()` has to walk an object it knows nothing about; a compiled serializer does not. It is worth reaching for, and a CPU profile says why: on a route answering 3.6KB of JSON, serialising it is about 25% of the time that is not spent waiting, ahead of the ETag at 19% and of everything the framework does to route the request and build its request and response objects.
|
package/package.json
CHANGED
package/src/compression.js
CHANGED
|
@@ -36,7 +36,23 @@ limitations under the License.
|
|
|
36
36
|
const zlib = require("zlib");
|
|
37
37
|
const bytes = require("bytes");
|
|
38
38
|
const compressible = require("compressible");
|
|
39
|
-
const {
|
|
39
|
+
const {
|
|
40
|
+
negotiateEncoding,
|
|
41
|
+
ENCODING_ANY,
|
|
42
|
+
ENCODING_BR,
|
|
43
|
+
ENCODING_GZIP,
|
|
44
|
+
ENCODING_DEFLATE,
|
|
45
|
+
memoizeByString
|
|
46
|
+
} = require("./utils.js");
|
|
47
|
+
|
|
48
|
+
// what the `encodings` option may name, and the mask each name contributes. identity is 0: an
|
|
49
|
+
// uncompressed answer is always on offer, naming it only makes the list read complete
|
|
50
|
+
const ENCODING_MASKS = new Map([
|
|
51
|
+
["br", ENCODING_BR],
|
|
52
|
+
["gzip", ENCODING_GZIP],
|
|
53
|
+
["deflate", ENCODING_DEFLATE],
|
|
54
|
+
["identity", 0]
|
|
55
|
+
]);
|
|
40
56
|
|
|
41
57
|
// Cache-Control: no-transform forbids recoding the body, which is what this does
|
|
42
58
|
const NO_TRANSFORM = /(?:^|,)\s*?no-transform\s*?(?:,|$)/;
|
|
@@ -135,6 +151,11 @@ function toBuffer(chunk, encoding) {
|
|
|
135
151
|
* @param {string} [options.enforceEncoding] what to use when the request carries no
|
|
136
152
|
* Accept-Encoding at all. Default "identity", which is to say nothing is compressed.
|
|
137
153
|
* @param {object} [options.brotli] brotli options, `params` included. The default quality is 4.
|
|
154
|
+
* @param {string[]} [options.encodings] the encodings this middleware may answer with, out of
|
|
155
|
+
* "br", "gzip" and "deflate". What is not named is never used, however the client ranks it: a
|
|
156
|
+
* server that prefers cheap gzip over brotli passes ["gzip", "deflate"]. An uncompressed answer
|
|
157
|
+
* is always on offer, and enforceEncoding stays its own explicit choice, outside this list.
|
|
158
|
+
* This option is fulmine's own, the compression module has no equivalent.
|
|
138
159
|
* @param {number} [options.level] zlib compression level, for gzip and deflate.
|
|
139
160
|
* @param {number} [options.chunkSize] zlib chunk size.
|
|
140
161
|
* @param {number} [options.memLevel] zlib memory level.
|
|
@@ -157,6 +178,22 @@ function compression(options) {
|
|
|
157
178
|
// bytes.parse reads "1kb" and hands back null for anything it cannot, an absent option
|
|
158
179
|
// included, which is where the default comes in
|
|
159
180
|
const threshold = bytes.parse(/** @type {any} */ (opts.threshold)) ?? 1024;
|
|
181
|
+
// the mask handed to the negotiation, built once here: a name nobody knows is a config
|
|
182
|
+
// mistake and throws now rather than serving the wrong bytes later
|
|
183
|
+
let allowed = ENCODING_ANY;
|
|
184
|
+
if (opts.encodings !== undefined) {
|
|
185
|
+
if (!Array.isArray(opts.encodings)) {
|
|
186
|
+
throw new TypeError("encodings must be an array of encoding names");
|
|
187
|
+
}
|
|
188
|
+
allowed = 0;
|
|
189
|
+
for (const name of opts.encodings) {
|
|
190
|
+
const mask = ENCODING_MASKS.get(name);
|
|
191
|
+
if (mask === undefined) {
|
|
192
|
+
throw new TypeError(`unknown encoding "${name}" in encodings`);
|
|
193
|
+
}
|
|
194
|
+
allowed |= mask;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
160
197
|
|
|
161
198
|
/**
|
|
162
199
|
* A whole body, compressed on this thread. Blocks the event loop for as long as it takes,
|
|
@@ -213,8 +250,14 @@ function compression(options) {
|
|
|
213
250
|
// has to carry. Most requests to most routes cannot: a client that sent no Accept-Encoding,
|
|
214
251
|
// one that refused everything, a HEAD. Those get the Vary and nothing else, since the
|
|
215
252
|
// answer still depends on the header even when this particular client did not ask.
|
|
216
|
-
|
|
217
|
-
|
|
253
|
+
// straight from the raw entries where this request keeps them: reading req.headers here
|
|
254
|
+
// built the whole object for one name. Folded, so a repeated Accept-Encoding still reads
|
|
255
|
+
// as the joined list the headers object would have shown
|
|
256
|
+
const accept =
|
|
257
|
+
typeof req._foldedHeader === "function"
|
|
258
|
+
? req._foldedHeader("accept-encoding")
|
|
259
|
+
: req.headers["accept-encoding"];
|
|
260
|
+
let chosen = negotiateEncoding(accept === undefined ? "" : accept, allowed);
|
|
218
261
|
if (accept === undefined && ENFORCEABLE.has(enforceEncoding)) {
|
|
219
262
|
chosen = enforceEncoding;
|
|
220
263
|
}
|
package/src/request.js
CHANGED
|
@@ -951,6 +951,35 @@ module.exports = class Request extends LazyReadable {
|
|
|
951
951
|
return undefined;
|
|
952
952
|
}
|
|
953
953
|
|
|
954
|
+
/**
|
|
955
|
+
* The same, with repeats folded exactly as the headers object folds them, so a reader of one
|
|
956
|
+
* name per request does not build the whole object to stay correct on a repeated header.
|
|
957
|
+
* Not for set-cookie, whose folded form is an array.
|
|
958
|
+
*
|
|
959
|
+
* @param {string} name lowercase
|
|
960
|
+
* @returns {string|undefined}
|
|
961
|
+
*/
|
|
962
|
+
_foldedHeader(name) {
|
|
963
|
+
if (this.#cachedHeaders !== null) {
|
|
964
|
+
return this.#cachedHeaders[name];
|
|
965
|
+
}
|
|
966
|
+
const entries = this.#rawHeadersEntries;
|
|
967
|
+
let value;
|
|
968
|
+
for (let i = 0, len = entries.length; i < len; i += 2) {
|
|
969
|
+
if (entries[i] === name) {
|
|
970
|
+
if (value === undefined) {
|
|
971
|
+
value = entries[i + 1];
|
|
972
|
+
} else {
|
|
973
|
+
if (discardedDuplicates.has(name)) {
|
|
974
|
+
continue;
|
|
975
|
+
}
|
|
976
|
+
value += (name === "cookie" ? "; " : ", ") + entries[i + 1];
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
return value;
|
|
981
|
+
}
|
|
982
|
+
|
|
954
983
|
/**
|
|
955
984
|
* Whether there is any point still reading the body: once the response is finished or the
|
|
956
985
|
* connection is gone, uWS has nothing left to hand over.
|
package/src/types.d.ts
CHANGED
|
@@ -50,6 +50,12 @@ declare module "fulmine.js" {
|
|
|
50
50
|
enforceEncoding?: string;
|
|
51
51
|
/** Brotli options. The default quality is 4. */
|
|
52
52
|
brotli?: BrotliOptions;
|
|
53
|
+
/**
|
|
54
|
+
* The encodings this middleware may answer with; what is not named is never used,
|
|
55
|
+
* however the client ranks it. Fulmine's own option, the compression module has no
|
|
56
|
+
* equivalent. An uncompressed answer is always on offer.
|
|
57
|
+
*/
|
|
58
|
+
encodings?: ("br" | "gzip" | "deflate" | "identity")[];
|
|
53
59
|
}
|
|
54
60
|
// what listen() decided about each route, for a test to hold on to
|
|
55
61
|
interface RouteVerdict {
|