fulmine.js 5.21.4 → 5.21.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fulmine.js",
3
- "version": "5.21.4",
3
+ "version": "5.21.6",
4
4
  "description": "Drop-in Express 5 replacement on uWebSockets.js, up to 20x faster. Same API, your middleware and framework keep working.",
5
5
  "main": "src/index.js",
6
6
  "exports": {
package/src/index.js CHANGED
@@ -17,27 +17,6 @@ See the License for the specific language governing permissions and
17
17
  limitations under the License.
18
18
  */
19
19
 
20
- // A project on pnpm owns the uWebSockets.js dependency itself, see `npx fulmine.js pnpm`, so the
21
- // one installed can drift from the one this package pins and was tested against. Said once, at
22
- // require time, where it reaches every deployment rather than only the ones that run verify.
23
- {
24
- const pinned = /#v?([\d.]+)$/.exec(require("../package.json").dependencies["uWebSockets.js"])?.[1];
25
- /** @type {string|undefined} */
26
- let installed;
27
- try {
28
- // its exports map does not expose package.json, so it is read beside the entry point
29
- const beside = require("path").join(require.resolve("uWebSockets.js"), "..", "package.json");
30
- installed = JSON.parse(require("fs").readFileSync(beside, "utf8")).version;
31
- } catch {
32
- // nothing to compare against, which is not worth a warning of its own
33
- }
34
- if (pinned && installed && installed !== pinned) {
35
- console.warn(
36
- `fulmine.js: uWebSockets.js ${installed} is installed, this version was tested with ${pinned}.\n` +
37
- " On pnpm the pin is the project's own: `npx fulmine.js pnpm` writes the tested one."
38
- );
39
- }
40
- }
41
20
  const Application = require("./application.js");
42
21
  const Router = require("./router.js");
43
22
  const Route = require("./route.js");
@@ -31,13 +31,43 @@ const parseQuery = require("./parse-query.js");
31
31
  const { kGetSafe } = require("./usage.js");
32
32
  const { AsyncResource } = require("async_hooks");
33
33
 
34
+ // node's unexported Symbol("context_frame"), where an AsyncResource keeps the store it captured,
35
+ // read off the first resource. null when node has none, then every binding keeps its scope
36
+ /** @type {symbol|null|undefined} */
37
+ let kContextFrame;
38
+ // set once an init hook was seen, never cleared: from then on every binding has its scope as before
39
+ let initHooksSeen = false;
40
+
34
41
  /**
35
- * AsyncResource.bind without node's generic wrapper: ~1.9us per call there, ~0.08 here.
42
+ * raw-body's wrap: AsyncResource.bind without node's generic wrapper (~1.9us per call there), and no
43
+ * scope without an AsyncLocalStorage store or an init hook: 2.3 to 3.5us per request in a uWS
44
+ * callback, +9 to 16% on the arena's POST. new AsyncResource("") throws only while a hook listens.
36
45
  *
37
46
  * @param {(...args: any[]) => any} fn called with at most one argument by every caller here
38
47
  * @returns {(err?: any) => any}
39
48
  */
40
49
  function bindContext(fn) {
50
+ if (!initHooksSeen) {
51
+ /** @type {AsyncResource|undefined} */
52
+ let probe;
53
+ try {
54
+ probe = new AsyncResource("");
55
+ } catch {
56
+ initHooksSeen = true;
57
+ }
58
+ if (probe !== undefined) {
59
+ if (kContextFrame === undefined) {
60
+ kContextFrame =
61
+ Object.getOwnPropertySymbols(probe).find((s) => s.description === "context_frame") ?? null;
62
+ }
63
+ if (kContextFrame !== null && /** @type {any} */ (probe)[kContextFrame] === undefined) {
64
+ return fn;
65
+ }
66
+ // a store to carry, and no init hook to have seen the empty type: the probe is the resource
67
+ const resource = probe;
68
+ return (err) => resource.runInAsyncScope(fn, undefined, err);
69
+ }
70
+ }
41
71
  const resource = new AsyncResource(fn.name || "bound-anonymous-fn");
42
72
  return (err) => resource.runInAsyncScope(fn, undefined, err);
43
73
  }
@@ -586,8 +616,14 @@ function serveStatic(root, options) {
586
616
  }
587
617
  const ext = path.extname(fullpath);
588
618
  let i = 0;
589
- // no extension on a directory; on the decoded url, as send tries it
590
- if (ext === "" && !url.endsWith("/") && options.extensions) {
619
+ // no extension on a directory; on the decoded url and only after ENOENT, as send does:
620
+ // ENOTDIR or ENAMETOOLONG report the path asked, not the last extension (fuzz seed 3561875562)
621
+ if (
622
+ /** @type {NodeJS.ErrnoException} */ (err).code === "ENOENT" &&
623
+ ext === "" &&
624
+ !url.endsWith("/") &&
625
+ options.extensions
626
+ ) {
591
627
  while (i < options.extensions.length) {
592
628
  try {
593
629
  stat = fs.statSync(fullpath + "." + options.extensions[i]);
@@ -1174,9 +1210,14 @@ const json = createBodyParser(
1174
1210
 
1175
1211
  // strict: only an object or an array is a body
1176
1212
  if (options.strict !== false) {
1177
- // exactly the four characters body-parser skips
1178
- // eslint-disable-next-line no-control-regex
1179
- const first = text.match(/^[\x20\x09\x0a\x0d]*([^\x20\x09\x0a\x0d])/)?.[1];
1213
+ // the four characters body-parser skips, space, tab, LF and CR, with a loop: the regex
1214
+ // allocated a match array per body, 56ns to 14
1215
+ let at = 0;
1216
+ let code = text.charCodeAt(0);
1217
+ while (code === 0x20 || code === 0x09 || code === 0x0a || code === 0x0d) {
1218
+ code = text.charCodeAt(++at);
1219
+ }
1220
+ const first = at < text.length ? text[at] : undefined;
1180
1221
  if (first !== "{" && first !== "[") {
1181
1222
  // a SyntaxError, as body-parser builds it
1182
1223
  return next(
package/src/node-shim.js CHANGED
@@ -15,14 +15,36 @@ limitations under the License.
15
15
  */
16
16
 
17
17
  // A uWS-shaped request and response over node's own, for supertest and http.createServer(app).
18
- // Not fast and not meant to be. Where the models disagree node's gives way: cork only runs its
19
- // callback, a status is kept until the first byte of body
18
+ // Not the fast path, app.listen() is, but about 1.5x Express on a hello world on Linux. Where the
19
+ // models disagree node's gives way: cork only runs its callback, a status is kept until the first
20
+ // byte of body
20
21
 
21
22
  const { IncomingMessage } = require("http");
23
+ const { HEADER_NAME_BUF, HEADER_VALUE_BUF } = require("./response-utils.js");
22
24
 
23
25
  /** What µWS returns for an address nobody declared. */
24
26
  const emptyAddress = new ArrayBuffer(0);
25
27
 
28
+ // The strings behind the Buffers writeHeaders hands over for the recurring names and values.
29
+ // Decoded on every response they gave node a new string to check, lowercase and hash each time
30
+ /** @type {Map<unknown, string>} */
31
+ const HEADER_TEXT = new Map();
32
+ for (const cache of [HEADER_NAME_BUF, HEADER_VALUE_BUF]) {
33
+ for (const text in cache) {
34
+ HEADER_TEXT.set(cache[text], text);
35
+ }
36
+ }
37
+
38
+ /**
39
+ * A header name or value as node's appendHeader takes it.
40
+ *
41
+ * @param {unknown} part
42
+ * @returns {string}
43
+ */
44
+ function headerText(part) {
45
+ return typeof part === "string" ? part : (HEADER_TEXT.get(part) ?? String(part));
46
+ }
47
+
26
48
  /**
27
49
  * An IP address as the four or sixteen bytes uWS hands over; unreadable comes back empty.
28
50
  *
@@ -93,6 +115,22 @@ function toArrayBuffer(chunk) {
93
115
  return /** @type {ArrayBuffer} */ (buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength));
94
116
  }
95
117
 
118
+ /**
119
+ * A body chunk as node's write takes it: a string goes as it is and node encodes it once, with
120
+ * the head, anything else is copied as toArrayBuffer does.
121
+ *
122
+ * @param {ArrayBuffer|Buffer|string} chunk
123
+ * @returns {Buffer|string}
124
+ */
125
+ function toNodeChunk(chunk) {
126
+ return typeof chunk === "string" ? chunk : Buffer.from(toArrayBuffer(chunk));
127
+ }
128
+
129
+ /** @param {Buffer|string} chunk */
130
+ function byteLength(chunk) {
131
+ return typeof chunk === "string" ? Buffer.byteLength(chunk) : chunk.length;
132
+ }
133
+
96
134
  /** uWS's HttpRequest over node's IncomingMessage. */
97
135
  class NodeHttpRequest {
98
136
  /** @type {import("http").IncomingMessage} */
@@ -241,16 +279,16 @@ class NodeHttpResponse {
241
279
  writeHeader(key, value) {
242
280
  if (!this._nodeRes.headersSent) {
243
281
  // writeHeaders hands the recurring names and values over as Buffers
244
- this._nodeRes.appendHeader(String(key), String(value));
282
+ this._nodeRes.appendHeader(headerText(key), headerText(value));
245
283
  }
246
284
  return this;
247
285
  }
248
286
 
249
287
  /** @param {ArrayBuffer|Buffer|string} chunk @returns {boolean} false when the socket is full */
250
288
  write(chunk) {
251
- const buffer = Buffer.from(toArrayBuffer(chunk));
252
- this._offset += buffer.length;
253
- return this._nodeRes.write(buffer);
289
+ const data = toNodeChunk(chunk);
290
+ this._offset += byteLength(data);
291
+ return this._nodeRes.write(data);
254
292
  }
255
293
 
256
294
  /** @param {ArrayBuffer|Buffer|string} [body] */
@@ -262,9 +300,9 @@ class NodeHttpResponse {
262
300
  this._nodeRes.end();
263
301
  return this;
264
302
  }
265
- const buffer = Buffer.from(toArrayBuffer(body));
266
- this._offset += buffer.length;
267
- this._nodeRes.end(buffer);
303
+ const data = toNodeChunk(body);
304
+ this._offset += byteLength(data);
305
+ this._nodeRes.end(data);
268
306
  return this;
269
307
  }
270
308
 
@@ -298,9 +336,9 @@ class NodeHttpResponse {
298
336
  if (!this._nodeRes.headersSent && !this._nodeRes.hasHeader("Content-Length")) {
299
337
  this._nodeRes.setHeader("Content-Length", String(totalSize));
300
338
  }
301
- const buffer = Buffer.from(toArrayBuffer(chunk));
302
- const ok = this._nodeRes.write(buffer);
303
- this._offset += buffer.length;
339
+ const data = toNodeChunk(chunk);
340
+ const ok = this._nodeRes.write(data);
341
+ this._offset += byteLength(data);
304
342
  const done = this._offset >= totalSize;
305
343
  if (done) {
306
344
  this._nodeRes.end();
package/src/request.js CHANGED
@@ -999,8 +999,9 @@ module.exports = class Request extends LazyReadable {
999
999
  /** @type {string|undefined} */
1000
1000
  let ip;
1001
1001
  if (rawIp.byteLength === 4) {
1002
- // plain, as node writes an IPv4 peer on an IPv4 socket
1003
- ip = new Uint8Array(rawIp).join(".");
1002
+ // plain, as node writes an IPv4 peer; by hand, Uint8Array join() was 420us per 1000 requests
1003
+ const bytes = new Uint8Array(rawIp);
1004
+ ip = bytes[0] + "." + bytes[1] + "." + bytes[2] + "." + bytes[3];
1004
1005
  } else if (rawIp.byteLength === 16) {
1005
1006
  const bytes = new Uint8Array(rawIp);
1006
1007
  if (isMappedIPv4(bytes)) {
@@ -34,8 +34,30 @@ const kShapeMode =
34
34
  Symbol("shapeMode");
35
35
  // names setHeader has validated and lowercased, so the constant names middleware writes per
36
36
  // request are one Map hit. Insert-only after validation, bounded; only setHeader may insert,
37
- // the never-throwing readers keep their plain toLowerCase
37
+ // the never-throwing readers have HEADER_KEYS below
38
38
  const VALIDATED_HEADER_NAMES = new Map();
39
+ // lowercase names for the header readers, apart from the map above as nothing here is validated.
40
+ // A fresh lowercase key goes through the V8 runtime on res.headers: 76ns against 6 cached, and
41
+ // helmet's removeHeader("X-Powered-By") alone was 190us per thousand requests. Bounded too
42
+ /** @type {Map<string, string>} */
43
+ const HEADER_KEYS = new Map();
44
+
45
+ /**
46
+ * The key a header lives under in res.headers, for the readers.
47
+ *
48
+ * @param {string} field the name as the caller wrote it
49
+ * @returns {string}
50
+ */
51
+ function headerKey(field) {
52
+ let key = HEADER_KEYS.get(field);
53
+ if (key === undefined) {
54
+ key = field.toLowerCase();
55
+ if (HEADER_KEYS.size < 512 && typeof field === "string") {
56
+ HEADER_KEYS.set(field, key);
57
+ }
58
+ }
59
+ return key;
60
+ }
39
61
  // The names and values that recur on every response, kept as Buffers for the uWS crossing: a Buffer
40
62
  // is memcpy'd as it is, a string pays a UTF-8 scan and copy per call. A header that is not here
41
63
  // crosses as the string it was. Names must stay lowercase, as writeHeaders receives them. The
@@ -121,6 +143,7 @@ module.exports = {
121
143
  kOutHeaders,
122
144
  kShapeMode,
123
145
  VALIDATED_HEADER_NAMES,
146
+ headerKey,
124
147
  HEADER_NAME_BUF,
125
148
  HEADER_VALUE_BUF,
126
149
  statusLine,
package/src/response.js CHANGED
@@ -57,6 +57,7 @@ const {
57
57
  kOutHeaders,
58
58
  kShapeMode,
59
59
  VALIDATED_HEADER_NAMES,
60
+ headerKey,
60
61
  HEADER_NAME_BUF,
61
62
  HEADER_VALUE_BUF,
62
63
  statusLine,
@@ -77,6 +78,10 @@ const JSON_UTF8 = "application/json; charset=utf-8";
77
78
 
78
79
  // the seeded Keep-Alive, a constant so setHeader can tell it from one the application set
79
80
  const SEEDED_KEEP_ALIVE = "timeout=10";
81
+ // the seeded connection and keep-alive as one value, every response writes them in one call:
82
+ // 572ns to 438 per head with a content-type, on node 26
83
+ const SEEDED_PAIR = "keep-alive\r\nkeep-alive: " + SEEDED_KEEP_ALIVE;
84
+ const SEEDED_PAIR_BUF = Buffer.from(SEEDED_PAIR);
80
85
  // send's ceiling for maxAge, one year
81
86
  const MAX_MAXAGE = 60 * 60 * 24 * 365 * 1000;
82
87
 
@@ -600,8 +605,11 @@ module.exports = class Response extends LazyWritable {
600
605
 
601
606
  /**
602
607
  * Writes every header set so far to uWS, the point of no return. Content-Length is kept on
603
- * totalSize instead: uWS takes the length through tryEnd or endWithoutBody. One writeHeader
604
- * per header: packing the head into one writeStatus measured slower, see issue #11.
608
+ * totalSize instead: uWS takes the length through tryEnd or endWithoutBody.
609
+ *
610
+ * From the third line on the head goes in one writeHeader, joined in the first value, as uWS
611
+ * writes `name: value\r\n` without reading it. Per head on node 26: helmet's set 3953ns to 1327,
612
+ * five lines 1013 to 531. One or two lines keep a call each, the join costs what it saves (issue #11).
605
613
  *
606
614
  * @param {boolean} utf8 unused, kept because node's equivalent takes it and the two callers
607
615
  * differ on what they know about the body
@@ -614,6 +622,16 @@ module.exports = class Response extends LazyWritable {
614
622
  // length first: the value is nearly always "keep-alive", which paid a lowercase per response
615
623
  const closing =
616
624
  typeof connection === "string" && connection.length === 5 && connection.toLowerCase() === "close";
625
+ // the node shim writes a line with node's appendHeader, that refuses the CRLF of a join
626
+ const joins = res._nodeRes === undefined;
627
+ // lines not written yet: the first two apart, from the third on in joined
628
+ let lines = 0;
629
+ let pair = false;
630
+ let firstName = "";
631
+ let firstValue = "";
632
+ let secondName = "";
633
+ let secondValue = "";
634
+ let joined = "";
617
635
  // for..in over an object a few responses delete from (204, 304, 205, a 304 file,
618
636
  // removeHeader), rare enough to keep the shape, unlike the request side
619
637
  for (const header in headers) {
@@ -637,20 +655,107 @@ module.exports = class Response extends LazyWritable {
637
655
  this.#userChunked = true;
638
656
  continue;
639
657
  }
640
- // the recurring names and values cross as cached Buffers, see HEADER_NAME_BUF
641
- const name = HEADER_NAME_BUF[header] || header;
642
- if (Array.isArray(value)) {
643
- for (const val of value) {
644
- res.writeHeader(name, HEADER_VALUE_BUF[val] || val);
658
+ // a string first, nearly every value is one
659
+ const isText = typeof value === "string";
660
+ const list = isText || !Array.isArray(value) ? null : value;
661
+ // only text is joined, the rest crosses alone as before. false is joined as "false", the
662
+ // content-type res.set stores for an unknown type
663
+ let joinable = joins && (isText || list !== null || value === false);
664
+ if (joinable && list !== null) {
665
+ for (let i = 0; i < list.length; i++) {
666
+ if (typeof list[i] !== "string") {
667
+ joinable = false;
668
+ break;
669
+ }
645
670
  }
646
- } else {
647
- res.writeHeader(name, HEADER_VALUE_BUF[value] || value);
648
671
  }
672
+ if (!joinable) {
673
+ // the lines before this one go first, so the order on the wire holds
674
+ if (lines !== 0) {
675
+ Response.#writeLines(res, lines, pair, firstName, firstValue, secondName, secondValue, joined);
676
+ lines = 0;
677
+ pair = false;
678
+ }
679
+ // the recurring names and values cross as cached Buffers, see HEADER_NAME_BUF
680
+ const name = HEADER_NAME_BUF[header] || header;
681
+ if (list !== null) {
682
+ for (const val of list) {
683
+ res.writeHeader(name, HEADER_VALUE_BUF[val] || val);
684
+ }
685
+ } else {
686
+ res.writeHeader(name, HEADER_VALUE_BUF[value] || value);
687
+ }
688
+ continue;
689
+ }
690
+ const count = list === null ? 1 : list.length;
691
+ for (let i = 0; i < count; i++) {
692
+ const text = list === null ? (value === false ? "false" : value) : list[i];
693
+ if (
694
+ lines === 1 &&
695
+ !pair &&
696
+ header === "keep-alive" &&
697
+ text === SEEDED_KEEP_ALIVE &&
698
+ firstName === "connection" &&
699
+ firstValue === "keep-alive"
700
+ ) {
701
+ // the seeded pair, one line from here on
702
+ pair = true;
703
+ continue;
704
+ }
705
+ if (lines === 0) {
706
+ firstName = header;
707
+ firstValue = text;
708
+ } else if (lines === 1) {
709
+ secondName = header;
710
+ secondValue = text;
711
+ } else if (lines === 2) {
712
+ joined =
713
+ (pair ? SEEDED_PAIR : firstValue) +
714
+ "\r\n" +
715
+ secondName +
716
+ ": " +
717
+ secondValue +
718
+ "\r\n" +
719
+ header +
720
+ ": " +
721
+ text;
722
+ } else {
723
+ joined += "\r\n" + header + ": " + text;
724
+ }
725
+ lines++;
726
+ }
727
+ }
728
+ if (lines !== 0) {
729
+ Response.#writeLines(res, lines, pair, firstName, firstValue, secondName, secondValue, joined);
649
730
  }
650
731
  this.headersSent = true;
651
732
  this.#headOut = true;
652
733
  }
653
734
 
735
+ /**
736
+ * Writes the lines of writeHeaders: one or two a call each, three or more in one call.
737
+ *
738
+ * @param {import("uWebSockets.js").HttpResponse} res
739
+ * @param {number} lines how many, the seeded pair counted as one
740
+ * @param {boolean} pair whether the first line is the seeded pair
741
+ * @param {string} firstName
742
+ * @param {string} firstValue
743
+ * @param {string} secondName
744
+ * @param {string} secondValue
745
+ * @param {string} joined the first value with the other lines after it
746
+ */
747
+ static #writeLines(res, lines, pair, firstName, firstValue, secondName, secondValue, joined) {
748
+ const name = HEADER_NAME_BUF[firstName] || firstName;
749
+ if (lines > 2) {
750
+ res.writeHeader(name, joined);
751
+ return;
752
+ }
753
+ res.writeHeader(name, pair ? SEEDED_PAIR_BUF : HEADER_VALUE_BUF[firstValue] || firstValue);
754
+ if (lines === 2) {
755
+ res.writeHeader(HEADER_NAME_BUF[secondName] || secondName, HEADER_VALUE_BUF[secondValue] || secondValue);
756
+ }
757
+ }
758
+
654
759
  /**
655
760
  * node's, called before a body when writeHead never was. Guarded here: the compression module
656
761
  * calls it on the strength of node's _header, which this response does not keep.
@@ -1473,7 +1578,7 @@ module.exports = class Response extends LazyWritable {
1473
1578
  * @returns {boolean}
1474
1579
  */
1475
1580
  hasHeader(name) {
1476
- return this.headers[name.toLowerCase()] !== undefined;
1581
+ return this.headers[headerKey(name)] !== undefined;
1477
1582
  }
1478
1583
 
1479
1584
  /**
@@ -1502,7 +1607,7 @@ module.exports = class Response extends LazyWritable {
1502
1607
  * @returns {this}
1503
1608
  */
1504
1609
  appendHeader(name, value) {
1505
- const key = name.toLowerCase();
1610
+ const key = headerKey(name);
1506
1611
  const current = this.headers[key];
1507
1612
  if (current === undefined) {
1508
1613
  return this.setHeader(name, value);
@@ -1563,7 +1668,7 @@ module.exports = class Response extends LazyWritable {
1563
1668
  this.set(header, fields[header]);
1564
1669
  }
1565
1670
  } else {
1566
- const name = field.toLowerCase();
1671
+ const name = headerKey(field);
1567
1672
  // coerced here as Express does, so res.get answers what was sent
1568
1673
  let out = Array.isArray(value) ? value.map(String) : String(value);
1569
1674
  if (name === "content-type") {
@@ -1592,7 +1697,7 @@ module.exports = class Response extends LazyWritable {
1592
1697
  * @returns {string|string[]|undefined}
1593
1698
  */
1594
1699
  get(field) {
1595
- const name = field.toLowerCase();
1700
+ const name = headerKey(field);
1596
1701
  const value = this.headers[name];
1597
1702
  // Content-Length is uWS's, measured from the body it was handed: worked out here only for
1598
1703
  // whoever asks, morgan's common and combined formats do, and kept
@@ -1631,7 +1736,7 @@ module.exports = class Response extends LazyWritable {
1631
1736
  if (this.headersSent) {
1632
1737
  throw headersSentError("remove");
1633
1738
  }
1634
- const key = field.toLowerCase();
1739
+ const key = headerKey(field);
1635
1740
  // helmet removes a header most responses never carry, and a delete is a runtime call
1636
1741
  if (key in this.headers) {
1637
1742
  delete this.headers[key];
package/src/uws.js CHANGED
@@ -21,6 +21,30 @@ limitations under the License.
21
21
  /** @type {any} */
22
22
  let uWS;
23
23
 
24
+ /**
25
+ * A project on pnpm owns the uWebSockets.js dependency itself, see `npx fulmine.js pnpm`, so the
26
+ * one installed can drift from the one this package pins and was tested against. Said once, when
27
+ * it is loaded: an app served through node's http never loads it and has nothing to hear.
28
+ */
29
+ function warnOnDrift() {
30
+ const pinned = /#v?([\d.]+)$/.exec(require("../package.json").dependencies["uWebSockets.js"])?.[1];
31
+ /** @type {string|undefined} */
32
+ let installed;
33
+ try {
34
+ // its exports map does not expose package.json, so it is read beside the entry point
35
+ const beside = require("path").join(require.resolve("uWebSockets.js"), "..", "package.json");
36
+ installed = JSON.parse(require("fs").readFileSync(beside, "utf8")).version;
37
+ } catch {
38
+ // nothing to compare against, which is not worth a warning of its own
39
+ }
40
+ if (pinned && installed && installed !== pinned) {
41
+ console.warn(
42
+ `fulmine.js: uWebSockets.js ${installed} is installed, this version was tested with ${pinned}.\n` +
43
+ " On pnpm the pin is the project's own: `npx fulmine.js pnpm` writes the tested one."
44
+ );
45
+ }
46
+ }
47
+
24
48
  /**
25
49
  * The µWS module. H3App, DeclarativeResponse and _cfg exist at runtime but are missing from the
26
50
  * .d.ts the package ships, so it is handed back loosely typed.
@@ -29,6 +53,7 @@ let uWS;
29
53
  */
30
54
  function loadUWS() {
31
55
  if (uWS === undefined) {
56
+ warnOnDrift();
32
57
  uWS = /** @type {any} */ (require("uWebSockets.js"));
33
58
  try {
34
59
  // disable Uwebsockets header