@nodefony/http 10.0.0-beta.3 → 10.0.0-beta.4

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.
Files changed (124) hide show
  1. package/dist/_virtual/{_@oxc-project_runtime@0.153.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
  2. package/dist/_virtual/{_@oxc-project_runtime@0.153.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorateParam.js +1 -2
  3. package/dist/index.js +12 -10
  4. package/dist/nodefony/command/assetsPublishCommand.js +5 -3
  5. package/dist/nodefony/command/certificatesCommand.js +5 -3
  6. package/dist/nodefony/command/networkCommand.js +5 -3
  7. package/dist/nodefony/command/proxyGenerateCommand.js +8 -3
  8. package/dist/nodefony/config/config.js +1 -2
  9. package/dist/nodefony/config/defineModuleConfig.js +1 -2
  10. package/dist/nodefony/service/HttpAdminApi.js +2 -2
  11. package/dist/nodefony/service/ProfilerAdminApi.js +1 -2
  12. package/dist/nodefony/service/audit-logger.js +20 -13
  13. package/dist/nodefony/service/certificates.js +97 -13
  14. package/dist/nodefony/service/error-renderer.js +29 -12
  15. package/dist/nodefony/service/http-kernel.js +118 -43
  16. package/dist/nodefony/service/pretty-request-logger.js +5 -4
  17. package/dist/nodefony/service/request-logger.js +24 -16
  18. package/dist/nodefony/service/reverse-proxy.js +3 -3
  19. package/dist/nodefony/service/servers/clientError.js +22 -3
  20. package/dist/nodefony/service/servers/server-h2c.js +126 -0
  21. package/dist/nodefony/service/servers/server-http.js +5 -8
  22. package/dist/nodefony/service/servers/server-https.js +12 -24
  23. package/dist/nodefony/service/servers/server-static.js +3 -3
  24. package/dist/nodefony/service/servers/server-websocket-secure.js +10 -18
  25. package/dist/nodefony/service/servers/server-websocket.js +10 -18
  26. package/dist/nodefony/service/servers/serverShutdown.js +78 -18
  27. package/dist/nodefony/service/servers/wsHeartbeat.js +1 -2
  28. package/dist/nodefony/service/sessions/sessions-service.js +7 -14
  29. package/dist/nodefony/service/trace.js +1 -2
  30. package/dist/nodefony/service/upload/upload-service.js +5 -8
  31. package/dist/nodefony/service/x509.js +1 -2
  32. package/dist/nodefony/src/assets/collectAssets.js +1 -2
  33. package/dist/nodefony/src/assets/prebuiltUi.js +1 -2
  34. package/dist/nodefony/src/context/Context.js +23 -31
  35. package/dist/nodefony/src/context/domainMatcher.js +1 -2
  36. package/dist/nodefony/src/context/forwarded.js +1 -2
  37. package/dist/nodefony/src/context/http/HttpContext.js +7 -6
  38. package/dist/nodefony/src/context/http/Request.js +33 -29
  39. package/dist/nodefony/src/context/http/Response.js +23 -6
  40. package/dist/nodefony/src/context/http/SseStream.js +1 -2
  41. package/dist/nodefony/src/context/http/parser.js +36 -10
  42. package/dist/nodefony/src/context/http/urlFastPath.js +1 -2
  43. package/dist/nodefony/src/context/http2/Request.js +4 -8
  44. package/dist/nodefony/src/context/http2/Response.js +22 -9
  45. package/dist/nodefony/src/context/metaData.js +1 -2
  46. package/dist/nodefony/src/context/requestId.js +1 -2
  47. package/dist/nodefony/src/context/responseEnded.js +1 -2
  48. package/dist/nodefony/src/context/transport.js +45 -0
  49. package/dist/nodefony/src/context/trustProxy.js +1 -2
  50. package/dist/nodefony/src/context/websocket/Response.js +33 -6
  51. package/dist/nodefony/src/context/websocket/WebsocketContext.js +92 -17
  52. package/dist/nodefony/src/context/websocket/isWebsocketContext.js +17 -0
  53. package/dist/nodefony/src/context/websocket/websocketUrl.js +1 -2
  54. package/dist/nodefony/src/context/websocket/wsBackpressure.js +1 -2
  55. package/dist/nodefony/src/context/websocket/wsCloseReason.js +1 -2
  56. package/dist/nodefony/src/context/websocket/wsLogContent.js +1 -2
  57. package/dist/nodefony/src/cookies/cookie.js +3 -7
  58. package/dist/nodefony/src/errors/httpError.js +1 -2
  59. package/dist/nodefony/src/errors/thrownMessage.js +1 -2
  60. package/dist/nodefony/src/profiler/FrameProfile.js +1 -2
  61. package/dist/nodefony/src/profiler/Profiler.js +102 -20
  62. package/dist/nodefony/src/profiler/aggregate.js +422 -0
  63. package/dist/nodefony/src/proxy/forward.js +29 -5
  64. package/dist/nodefony/src/proxy/generateProxyConfig.js +14 -6
  65. package/dist/nodefony/src/proxy/rules.js +12 -6
  66. package/dist/nodefony/src/rateLimit/MemoryRateLimitStore.js +1 -2
  67. package/dist/nodefony/src/rateLimit/WsConnectionCounter.js +1 -2
  68. package/dist/nodefony/src/rateLimit/rateLimitFilters.js +1 -2
  69. package/dist/nodefony/src/servers/http2Limits.js +50 -0
  70. package/dist/nodefony/src/servers/kernelServers.js +12 -3
  71. package/dist/nodefony/src/servers/portBinder.js +41 -10
  72. package/dist/nodefony/src/servers/upgradeDispatch.js +86 -7
  73. package/dist/nodefony/src/session/session.js +2 -3
  74. package/dist/nodefony/src/session/sessionStoreFailure.js +1 -2
  75. package/dist/nodefony/src/session/storage/MemorySessionStorage.js +1 -2
  76. package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +1 -2
  77. package/dist/nodefony/src/session/storage/sessionFilters.js +1 -2
  78. package/dist/nodefony/src/session/storage/sessionSort.js +1 -2
  79. package/dist/types/index.d.ts +7 -3
  80. package/dist/types/nodefony/interfaces/IContext.d.ts +63 -4
  81. package/dist/types/nodefony/interfaces/IRequest.d.ts +9 -4
  82. package/dist/types/nodefony/interfaces/IRouting.d.ts +10 -0
  83. package/dist/types/nodefony/interfaces/index.d.ts +2 -2
  84. package/dist/types/nodefony/service/certificates.d.ts +55 -4
  85. package/dist/types/nodefony/service/error-renderer.d.ts +31 -0
  86. package/dist/types/nodefony/service/http-kernel.d.ts +46 -10
  87. package/dist/types/nodefony/service/servers/clientError.d.ts +16 -0
  88. package/dist/types/nodefony/service/servers/server-h2c.d.ts +51 -0
  89. package/dist/types/nodefony/service/servers/server-http.d.ts +3 -3
  90. package/dist/types/nodefony/service/servers/server-https.d.ts +4 -4
  91. package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +1 -3
  92. package/dist/types/nodefony/service/servers/server-websocket.d.ts +1 -3
  93. package/dist/types/nodefony/service/servers/serverShutdown.d.ts +30 -16
  94. package/dist/types/nodefony/src/context/Context.d.ts +13 -4
  95. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +1 -1
  96. package/dist/types/nodefony/src/context/http/Request.d.ts +20 -15
  97. package/dist/types/nodefony/src/context/http/Response.d.ts +11 -0
  98. package/dist/types/nodefony/src/context/http/parser.d.ts +18 -2
  99. package/dist/types/nodefony/src/context/http2/Request.d.ts +1 -1
  100. package/dist/types/nodefony/src/context/http2/Response.d.ts +15 -5
  101. package/dist/types/nodefony/src/context/transport.d.ts +53 -0
  102. package/dist/types/nodefony/src/context/websocket/Response.d.ts +18 -2
  103. package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +57 -7
  104. package/dist/types/nodefony/src/context/websocket/isWebsocketContext.d.ts +15 -0
  105. package/dist/types/nodefony/src/profiler/FrameProfile.d.ts +13 -12
  106. package/dist/types/nodefony/src/profiler/Profiler.d.ts +68 -15
  107. package/dist/types/nodefony/src/profiler/aggregate.d.ts +215 -0
  108. package/dist/types/nodefony/src/proxy/forward.d.ts +14 -2
  109. package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +16 -0
  110. package/dist/types/nodefony/src/proxy/rules.d.ts +3 -0
  111. package/dist/types/nodefony/src/servers/http2Limits.d.ts +37 -0
  112. package/dist/types/nodefony/src/servers/kernelServers.d.ts +10 -1
  113. package/dist/types/nodefony/src/servers/portBinder.d.ts +29 -5
  114. package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +33 -3
  115. package/docs/cookies.md +85 -78
  116. package/docs/observabilite.md +80 -80
  117. package/docs/rate-limit.md +83 -83
  118. package/docs/reverse-proxy.md +54 -54
  119. package/docs/servers.md +338 -224
  120. package/docs/session.md +179 -179
  121. package/docs/sse.md +24 -20
  122. package/docs/upload.md +120 -117
  123. package/package.json +8 -9
  124. package/dist/_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js +0 -6
@@ -4,7 +4,7 @@ import { toWsCloseReason } from "./wsCloseReason.js";
4
4
  import { RequestContext } from "nodefony";
5
5
  import http from "node:http";
6
6
  import Ws from "ws";
7
- //#region nodefony/src/context/websocket/Response.ts
7
+ // nodefony/src/context/websocket/Response.ts
8
8
  /**
9
9
  * Codes d'erreur d'écriture « le client est PARTI » : reload de page, onglet
10
10
  * fermé, HMR — la socket TCP est morte côté client pendant qu'une frame était
@@ -71,7 +71,11 @@ var WebsocketResponse = class {
71
71
  const payload = data ?? this.body;
72
72
  if (payload == null) throw new Error("no data");
73
73
  return new Promise((resolve, reject) => {
74
- if (!this.connection || this.connection.readyState !== Ws.OPEN) return reject(/* @__PURE__ */ new Error("WebSocket not open"));
74
+ if (!this.connection || this.connection.readyState === Ws.CONNECTING) return reject(/* @__PURE__ */ new Error("WebSocket not open"));
75
+ if (this.connection.readyState !== Ws.OPEN) {
76
+ this.log("WS send: client déjà déconnecté — frame perdue", "DEBUG");
77
+ return resolve(this);
78
+ }
75
79
  const { max, policy, closeAfterDrops } = readBackpressureOptions(this.context.server);
76
80
  const decision = decideSend(this.connection, max, policy, closeAfterDrops);
77
81
  if (decision !== "send") {
@@ -122,7 +126,7 @@ var WebsocketResponse = class {
122
126
  setBody(ele, encoding) {
123
127
  if (typeof ele === "string") this.body = Buffer.from(ele, encoding ?? this.encoding);
124
128
  else if (ele instanceof ArrayBuffer || ele instanceof SharedArrayBuffer) this.body = Buffer.from(ele);
125
- else if ("buffer" in ele && ele.buffer instanceof ArrayBuffer) this.body = Buffer.from(ele.buffer);
129
+ else if (ArrayBuffer.isView(ele)) this.body = Buffer.from(ele.buffer, ele.byteOffset, ele.byteLength);
126
130
  return this.body;
127
131
  }
128
132
  drop(reasonCode, description) {
@@ -176,14 +180,38 @@ var WebsocketResponse = class {
176
180
  clean() {
177
181
  this.connection = null;
178
182
  this.body = null;
183
+ this.handshakeHeaders = null;
179
184
  }
180
185
  setEncoding(encoding) {
181
186
  return this.encoding = encoding;
182
187
  }
183
- setHeader() {
188
+ /**
189
+ * En-têtes de la réponse au handshake, retenus tant que la socket n'est pas
190
+ * acceptée : ils ne partent que dans un REFUS (`refuseHandshake`) — le `101`
191
+ * de `ws` ne les porte pas. `null` tant que personne n'en pose (le cas
192
+ * nominal : aucune allocation).
193
+ */
194
+ handshakeHeaders = null;
195
+ /**
196
+ * Retient un en-tête du handshake (cf {@link handshakeHeaders}) ; sans effet
197
+ * une fois la socket ouverte, où il n'y a plus de réponse HTTP.
198
+ *
199
+ * @param name - nom de l'en-tête
200
+ * @param value - valeur
201
+ * @returns toujours `true`
202
+ */
203
+ setHeader(name, value) {
204
+ if (this.connection !== null) return true;
205
+ this.handshakeHeaders ??= {};
206
+ this.handshakeHeaders[name] = typeof value === "object" ? value.join(", ") : value;
184
207
  return true;
185
208
  }
186
- setHeaders() {
209
+ setHeaders(obj) {
210
+ if (this.connection !== null) return true;
211
+ for (const name of Object.keys(obj)) {
212
+ const value = obj[name];
213
+ if (value !== void 0) this.setHeader(name, value);
214
+ }
187
215
  return true;
188
216
  }
189
217
  addCookie(cookie) {
@@ -193,5 +221,4 @@ var WebsocketResponse = class {
193
221
  setCookies() {}
194
222
  setCookie(_cookie) {}
195
223
  };
196
- //#endregion
197
224
  export { WebsocketResponse as default, isPeerGoneError };
@@ -7,10 +7,10 @@ import { FrameProfile } from "../../profiler/FrameProfile.js";
7
7
  import { formatWsLogContent } from "./wsLogContent.js";
8
8
  import { parseWebsocketUrl } from "./websocketUrl.js";
9
9
  import { RequestContext, logColor } from "nodefony";
10
- import Ws from "ws";
11
10
  import { AsyncResource } from "node:async_hooks";
12
11
  import url, { URL } from "node:url";
13
- //#region nodefony/src/context/websocket/WebsocketContext.ts
12
+ import Ws from "ws";
13
+ // nodefony/src/context/websocket/WebsocketContext.ts
14
14
  /**
15
15
  * Appelle l'action résolue et rend TOUJOURS une promesse.
16
16
  *
@@ -54,6 +54,51 @@ function toWsCloseCode(code) {
54
54
  return 1011;
55
55
  }
56
56
  /**
57
+ * Statut HTTP d'un handshake REFUSÉ avant le `101` (RFC 6455 §4.2.2) — la
58
+ * réciproque de {@link toWsCloseCode} : une erreur levée pendant la décision
59
+ * porte un code HTTP (pare-feu, garde) ou, par héritage, un code de fermeture
60
+ * WS (origine `1008`, protocole `1002`, introuvable `4004`) ; le client qui
61
+ * lit le statut doit y reconnaître le même sens.
62
+ *
63
+ * @param code - code porté par l'erreur
64
+ * @returns un statut HTTP 4xx/5xx
65
+ */
66
+ function toHandshakeStatus(code) {
67
+ if (typeof code !== "number" || !Number.isInteger(code)) return 500;
68
+ if (code >= 400 && code <= 599) return code;
69
+ switch (code) {
70
+ case 1002:
71
+ case 1003:
72
+ case 1007: return 400;
73
+ case 1008:
74
+ case 4003: return 403;
75
+ case 4001: return 401;
76
+ case 1009: return 413;
77
+ case 1013: return 503;
78
+ case 4004: return 404;
79
+ default: return 500;
80
+ }
81
+ }
82
+ /**
83
+ * Statut HTTP que reçoit le client d'un handshake refusé — ou `null` si la
84
+ * connexion était déjà acceptée.
85
+ *
86
+ * La ligne de journal d'un refus s'écrit AVANT `refuseHandshake` : sans ce
87
+ * calcul, elle affichait le code de fermeture par défaut (`1000`) en ERROR,
88
+ * pour un `400` que le client reçoit. Même conversion que le refus
89
+ * ({@link toHandshakeStatus}), donc même chiffre. Chemin d'ERREUR seulement :
90
+ * 1 comparaison et 1 `typeof`, aucune allocation.
91
+ *
92
+ * @param context - le contexte WS de la ligne
93
+ * @param error - l'erreur qui a interrompu le handshake
94
+ * @returns le statut HTTP du refus, ou `null` après l'acceptation
95
+ */
96
+ function handshakeRefusalStatus(context, error) {
97
+ if (context.webSocketState !== "handshake") return null;
98
+ const code = error.code;
99
+ return toHandshakeStatus(typeof code === "number" ? code : void 0);
100
+ }
101
+ /**
57
102
  * Drapeau « logger le CONTENU des messages WS » — résolu **une seule fois** au
58
103
  * premier message (kernel présent). `true` hors production : le Suivi de requête
59
104
  * (Studio) affiche alors les frames RECEIVE/SEND/BROADCAST. En prod → `false` →
@@ -68,16 +113,21 @@ var WebsocketContext = class extends Context {
68
113
  rejected = false;
69
114
  teardownWired = false;
70
115
  connection = null;
116
+ /**
117
+ * Le verdict du handshake encore à rendre : posé par le noyau tant que le
118
+ * pipeline décide (pare-feu, garde de l'action), `null` dès que la socket
119
+ * est acceptée ou refusée. Tant qu'il est là, AUCUN `101` n'est parti.
120
+ */
121
+ admission = null;
71
122
  origin;
72
123
  proxy = null;
73
124
  forwarded = null;
74
125
  wsUrl = null;
75
- queryGet = {};
76
- queryRequest = {};
126
+ query = {};
77
127
  wsPath = "";
78
128
  #frameSeq = 0;
79
- constructor(scope, req, ws, type) {
80
- super(scope, type);
129
+ constructor(scope, req, ws, serverType) {
130
+ super(scope, serverType);
81
131
  this.webSocketState = "handshake";
82
132
  this.request = req;
83
133
  this.connection = ws;
@@ -89,18 +139,16 @@ var WebsocketContext = class extends Context {
89
139
  if (checker && checker.isTrusted(socketAddress) && hasForwardingHeaders(req.headers)) this.forwarded = resolveForwarded(req.headers, socketAddress, checker);
90
140
  this.remoteAddress = this.getRemoteAddress();
91
141
  this.acceptedProtocol = req.headers["sec-websocket-protocol"];
92
- this.scheme = type === "websocket-secure" ? "wss" : "ws";
142
+ this.scheme = serverType === "wss" ? "wss" : "ws";
93
143
  const incomingId = sanitizeRequestId(req.headers["x-request-id"]);
94
144
  if (incomingId) this.requestId = incomingId;
95
145
  this.wsUrl = parseWebsocketUrl(this.scheme, req.headers.host, req.url);
96
- this.queryGet = Object.fromEntries(this.wsUrl.searchParams.entries());
97
- this.queryRequest = { ...this.queryGet };
146
+ this.query = Object.fromEntries(this.wsUrl.searchParams.entries());
98
147
  this.wsPath = this.wsUrl.pathname + this.wsUrl.search;
99
148
  this.url = url.format(this.wsUrl);
100
- this.port = parseInt(this.wsUrl.port, 10) || (type === "websocket-secure" ? 443 : 80);
149
+ this.port = parseInt(this.wsUrl.port, 10) || (serverType === "wss" ? 443 : 80);
101
150
  this.request.url = this.wsUrl;
102
- this.request.queryGet = this.queryGet;
103
- this.request.query = this.queryRequest;
151
+ this.request.query = this.query;
104
152
  this.request.path = this.wsPath;
105
153
  try {
106
154
  this.originUrl = new URL(this.origin);
@@ -141,6 +189,34 @@ var WebsocketContext = class extends Context {
141
189
  return;
142
190
  }
143
191
  }
192
+ /**
193
+ * Rattache la socket que le handshake vient d'ouvrir (`101` envoyé) : le
194
+ * contexte a été construit et jugé AVANT, sans elle.
195
+ *
196
+ * @param ws - la socket acceptée
197
+ */
198
+ acceptConnection(ws) {
199
+ this.admission = null;
200
+ this.connection = ws;
201
+ this.response?.setConnection(ws);
202
+ }
203
+ /**
204
+ * Refuse le handshake par un statut HTTP, avant tout `101` (RFC 6455
205
+ * §4.2.2) — avec les en-têtes posés pendant la décision (`WWW-Authenticate`
206
+ * du pare-feu, `Retry-After`).
207
+ *
208
+ * @param status - statut HTTP du refus
209
+ * @returns `false` si le verdict était déjà rendu (rien n'est écrit)
210
+ */
211
+ refuseHandshake(status) {
212
+ const admission = this.admission;
213
+ if (admission === null) return false;
214
+ this.admission = null;
215
+ this.rejected = true;
216
+ this.webSocketState = "error";
217
+ admission.refuse(status, this.response?.handshakeHeaders ?? null);
218
+ return true;
219
+ }
144
220
  async connect() {
145
221
  if (!this.response || !this.request || !this.connection) throw new Error("Nodefony Websocket Bad request/response/connection");
146
222
  if (this.rejected) throw new Error("Nodefony Websocket rejected");
@@ -337,12 +413,12 @@ var WebsocketContext = class extends Context {
337
413
  }
338
414
  onClose(code, reason) {
339
415
  const description = reason.toString();
340
- this.log(`${logColor.cyan("URL")} : ${this.url} ${logColor.cyan("FROM")} : ${this.remoteAddress} ${logColor.cyan("ORIGIN")} : ${this.originUrl?.host} ${logColor.cyan("ID")} : ${this.requestId} ${logColor.cyan("Description")} : ${description}`, "INFO", `${this.type} ${logColor.magenta(code)} CLOSE ${this.method}`);
416
+ this.log(`${logColor.cyan("URL")} : ${this.url} ${logColor.cyan("FROM")} : ${this.remoteAddress} ${logColor.cyan("ORIGIN")} : ${this.originUrl?.host} ${logColor.cyan("ID")} : ${this.requestId} ${logColor.cyan("Description")} : ${description}`, "INFO", `${this.serverType} ${logColor.magenta(code)} CLOSE ${this.method}`);
341
417
  if (this.connection?.readyState !== Ws.CLOSED) {
342
418
  try {
343
419
  this.response?.drop(code, description);
344
420
  } catch (e) {
345
- this.log(`${logColor.cyan("URL")} : ${this.url} ${logColor.cyan("FROM")} : ${this.remoteAddress} ${logColor.cyan("ORIGIN")} : ${this.originUrl?.host} ${logColor.cyan("ID")} : ${this.requestId} ${logColor.cyan("error")} : ${e.message}`, "ERROR", `${this.type} CLOSE ${logColor.red(this.method ?? "")}`);
421
+ this.log(`${logColor.cyan("URL")} : ${this.url} ${logColor.cyan("FROM")} : ${this.remoteAddress} ${logColor.cyan("ORIGIN")} : ${this.originUrl?.host} ${logColor.cyan("ID")} : ${this.requestId} ${logColor.cyan("error")} : ${e.message}`, "ERROR", `${this.serverType} CLOSE ${logColor.red(this.method ?? "")}`);
346
422
  }
347
423
  this.fire("onClose", code, description, this.connection);
348
424
  } else this.fire("onClose", code, description, this.connection);
@@ -359,7 +435,7 @@ var WebsocketContext = class extends Context {
359
435
  */
360
436
  onConnectionError(error) {
361
437
  this.webSocketState = "error";
362
- this.log(`${logColor.cyan("URL")} : ${this.url} ${logColor.cyan("FROM")} : ${this.remoteAddress} ${logColor.cyan("ID")} : ${this.requestId} ${logColor.cyan("error")} : ${error.message}`, "ERROR", `${this.type} ${logColor.red("SOCKET ERROR")} ${this.method}`);
438
+ this.log(`${logColor.cyan("URL")} : ${this.url} ${logColor.cyan("FROM")} : ${this.remoteAddress} ${logColor.cyan("ID")} : ${this.requestId} ${logColor.cyan("error")} : ${error.message}`, "ERROR", `${this.serverType} ${logColor.red("SOCKET ERROR")} ${this.method}`);
363
439
  this.fire("onError", error, this);
364
440
  }
365
441
  setScheme() {
@@ -417,5 +493,4 @@ var WebsocketContext = class extends Context {
417
493
  this.rejected = true;
418
494
  }
419
495
  };
420
- //#endregion
421
- export { WebsocketContext as default, toWsCloseCode };
496
+ export { WebsocketContext as default, handshakeRefusalStatus, toHandshakeStatus, toWsCloseCode };
@@ -0,0 +1,17 @@
1
+ // nodefony/src/context/websocket/isWebsocketContext.ts
2
+ /**
3
+ * Dit si un contexte sert une connexion WebSocket — la garde qui rétrécit
4
+ * `ContextType` à `WebsocketContext` sans conversion.
5
+ *
6
+ * Elle lit la famille (`type`) posée à la construction du contexte, jamais
7
+ * `instanceof` : deux copies installées de `@nodefony/http` font échouer
8
+ * `instanceof` en silence, quand ce champ dit vrai pour les deux. Deux
9
+ * Une comparaison de chaînes, aucune allocation.
10
+ *
11
+ * @param context - le contexte à éprouver.
12
+ * @returns `true` si le contexte traite une connexion WebSocket (`ws` ou `wss`).
13
+ */
14
+ function isWebsocketContext(context) {
15
+ return context.type === "websocket";
16
+ }
17
+ export { isWebsocketContext };
@@ -1,4 +1,4 @@
1
- //#region nodefony/src/context/websocket/websocketUrl.ts
1
+ // nodefony/src/context/websocket/websocketUrl.ts
2
2
  /**
3
3
  * URL d'une requête d'upgrade WebSocket, telle que le pipeline la lit.
4
4
  *
@@ -17,5 +17,4 @@
17
17
  function parseWebsocketUrl(scheme, host, rawUrl) {
18
18
  return new URL(`${scheme}://${host ?? "localhost"}${rawUrl ?? "/"}`);
19
19
  }
20
- //#endregion
21
20
  export { parseWebsocketUrl };
@@ -1,4 +1,4 @@
1
- //#region nodefony/src/context/websocket/wsBackpressure.ts
1
+ // nodefony/src/context/websocket/wsBackpressure.ts
2
2
  /**
3
3
  * Relit le seuil + la politique depuis les options du `WebSocketServer`.
4
4
  *
@@ -52,5 +52,4 @@ const decideSend = (ws, max, policy, closeAfterDrops = 0) => {
52
52
  }
53
53
  return "drop";
54
54
  };
55
- //#endregion
56
55
  export { decideSend, readBackpressureOptions };
@@ -1,4 +1,4 @@
1
- //#region nodefony/src/context/websocket/wsCloseReason.ts
1
+ // nodefony/src/context/websocket/wsCloseReason.ts
2
2
  /**
3
3
  * Taille maximale, en octets UTF-8, de la raison d'une trame Close (RFC 6455
4
4
  * §5.5 : charge d'une trame de contrôle ≤ 125 octets, dont 2 pour le code).
@@ -37,5 +37,4 @@ function toWsCloseReason(reason) {
37
37
  }
38
38
  return reason.slice(0, end) + ELLIPSIS;
39
39
  }
40
- //#endregion
41
40
  export { MAX_WS_CLOSE_REASON_BYTES, toWsCloseReason };
@@ -1,4 +1,4 @@
1
- //#region nodefony/src/context/websocket/wsLogContent.ts
1
+ // nodefony/src/context/websocket/wsLogContent.ts
2
2
  /**
3
3
  * Formatage **pur** (0 état, 0 dépendance) du CONTENU d'un message WebSocket pour
4
4
  * le log de trace (Suivi de requête Studio). Séparé de `WebsocketContext` pour
@@ -64,5 +64,4 @@ function formatWsLogContent(data, cap = WS_LOG_CONTENT_CAP) {
64
64
  if (typeof s !== "string") return String(data);
65
65
  return s.length > cap ? `${s.slice(0, cap)}…` : s;
66
66
  }
67
- //#endregion
68
67
  export { WS_LOG_CONTENT_CAP, binaryByteLength, formatWsLogContent };
@@ -2,7 +2,7 @@ import { extend } from "nodefony";
2
2
  import crypto from "node:crypto";
3
3
  import { parseCookie } from "cookie";
4
4
  import MS from "ms";
5
- //#region nodefony/src/cookies/cookie.ts
5
+ // nodefony/src/cookies/cookie.ts
6
6
  const encode = encodeURIComponent;
7
7
  const decode = decodeURIComponent;
8
8
  const cookieDefaultSettings = {
@@ -20,11 +20,9 @@ function parser(strToParse) {
20
20
  }
21
21
  function cookiesParser(context) {
22
22
  let cookies = null;
23
- let co = null;
23
+ let co;
24
24
  switch (context.type) {
25
25
  case "http":
26
- case "https":
27
- case "http2":
28
26
  if (context.request.request.headers.cookie) cookies = context.request.request.headers.cookie;
29
27
  if (cookies) {
30
28
  const obj = parser(cookies);
@@ -35,8 +33,7 @@ function cookiesParser(context) {
35
33
  if (context.response) context.request.request.cookies = context.response.cookies;
36
34
  }
37
35
  break;
38
- case "websocket":
39
- case "websocket-secure": {
36
+ case "websocket": {
40
37
  const cookieHeader = context.request?.headers.cookie;
41
38
  if (cookieHeader) {
42
39
  const parsed = parser(cookieHeader);
@@ -254,5 +251,4 @@ var Cookie = class Cookie {
254
251
  return obj;
255
252
  }
256
253
  };
257
- //#endregion
258
254
  export { cookiesParser, Cookie as default, parser };
@@ -1,5 +1,5 @@
1
1
  import { clc, nodefonyError } from "nodefony";
2
- //#region nodefony/src/errors/httpError.ts
2
+ // nodefony/src/errors/httpError.ts
3
3
  const exclude = {
4
4
  context: true,
5
5
  resolver: true,
@@ -65,5 +65,4 @@ var HttpError = class extends nodefonyError {
65
65
  }
66
66
  };
67
67
  Object.defineProperty(HttpError.prototype, "toJSON", jsonHttpError);
68
- //#endregion
69
68
  export { HttpError as default };
@@ -1,4 +1,4 @@
1
- //#region nodefony/src/errors/thrownMessage.ts
1
+ // nodefony/src/errors/thrownMessage.ts
2
2
  /**
3
3
  * Message lisible d'une valeur LEVÉE (`catch`, rejet) dont on ignore le type.
4
4
  *
@@ -23,5 +23,4 @@ function thrownMessage(value) {
23
23
  case "function": return value === null ? "null" : Object.prototype.toString.call(value);
24
24
  }
25
25
  }
26
- //#endregion
27
26
  export { thrownMessage };
@@ -1,5 +1,5 @@
1
1
  import { thrownMessage } from "../errors/thrownMessage.js";
2
- //#region nodefony/src/profiler/FrameProfile.ts
2
+ // nodefony/src/profiler/FrameProfile.ts
3
3
  /**
4
4
  * Profil d'**une invocation** du pont RPC (une frame `api.request`), et non de
5
5
  * la connexion qui la porte.
@@ -92,5 +92,4 @@ var FrameProfile = class {
92
92
  if (error !== void 0 && error !== null) this.error = { message: thrownMessage(error) };
93
93
  }
94
94
  };
95
- //#endregion
96
95
  export { FrameProfile, FrameProfile as default };
@@ -1,12 +1,20 @@
1
- //#region nodefony/src/profiler/Profiler.ts
2
- /** Durée totale = (fin de la dernière phase terminée) − (début de la 1ère). */
1
+ import { profileLane } from "./aggregate.js";
2
+ // nodefony/src/profiler/Profiler.ts
3
+ /**
4
+ * Durée totale = (fin de la dernière phase) − (début de la 1ère).
5
+ *
6
+ * Une phase encore OUVERTE à la collecte (action interrompue par le départ du
7
+ * client : flux SSE, outil MCP annulé) court jusqu'à la collecte — la compter
8
+ * pour zéro publiait 0,3 ms pour un appel coupé à 3 s.
9
+ */
3
10
  function totalDuration(phases) {
4
11
  if (phases.length === 0) return null;
5
12
  const first = phases.at(0);
6
13
  if (first === void 0 || typeof first.startMs !== "number") return null;
7
14
  let end = first.startMs;
15
+ let now = null;
8
16
  for (const p of phases) {
9
- const e = p.startMs + (p.durationMs ?? 0);
17
+ const e = p.durationMs === void 0 ? now ??= performance.now() : p.startMs + p.durationMs;
10
18
  if (e > end) end = e;
11
19
  }
12
20
  const d = end - first.startMs;
@@ -14,11 +22,41 @@ function totalDuration(phases) {
14
22
  }
15
23
  /** Capacité par défaut du ring buffer (assez pour une session de debug). */
16
24
  const DEFAULT_CAP = 500;
25
+ /**
26
+ * Capacité par défaut des deux anneaux annexes : l'OUTILLAGE (console, API
27
+ * d'administration, porte MCP) et les ASSETS relayés de Vite. Un chargement
28
+ * de la console relaie à lui seul quelques centaines de modules : dans un
29
+ * tampon commun, il chassait en quelques secondes les profils de
30
+ * l'application, puis ceux des appels de la console elle-même.
31
+ */
32
+ const DEFAULT_SIDE_CAP = 100;
33
+ /**
34
+ * Collecteur des profils de requête, en un anneau par provenance
35
+ * ({@link profileLane}) : l'application, l'outillage, les assets relayés.
36
+ * Chacun évince ses propres profils — ouvrir la console ne chasse plus celui
37
+ * qu'on y consulte.
38
+ */
17
39
  var Profiler = class {
18
- _buf = /* @__PURE__ */ new Map();
19
- _cap;
20
- constructor(cap = DEFAULT_CAP) {
21
- this._cap = cap > 0 ? cap : DEFAULT_CAP;
40
+ _lanes = {
41
+ app: /* @__PURE__ */ new Map(),
42
+ tooling: /* @__PURE__ */ new Map(),
43
+ asset: /* @__PURE__ */ new Map()
44
+ };
45
+ _caps;
46
+ /** Dernier numéro d'ordre attribué (0 = aucun profil collecté). */
47
+ _seq = 0;
48
+ /**
49
+ * @param cap - capacité de l'anneau de l'application.
50
+ * @param sideCap - capacité de chacun des anneaux de l'outillage et des
51
+ * assets relayés.
52
+ */
53
+ constructor(cap = DEFAULT_CAP, sideCap = DEFAULT_SIDE_CAP) {
54
+ const side = sideCap > 0 ? sideCap : DEFAULT_SIDE_CAP;
55
+ this._caps = {
56
+ app: cap > 0 ? cap : DEFAULT_CAP,
57
+ tooling: side,
58
+ asset: side
59
+ };
22
60
  }
23
61
  /**
24
62
  * Snapshot un `Context` en fin de requête. Tolérant aux champs absents
@@ -32,8 +70,9 @@ var Profiler = class {
32
70
  const phases = ctx.phases ?? [];
33
71
  const entry = {
34
72
  requestId,
73
+ seq: ++this._seq,
35
74
  ts: Date.now(),
36
- kind: ctx.type === "websocket" || ctx.type === "websocket-secure" ? "ws" : "http",
75
+ kind: ctx.type === "websocket" ? "ws" : "http",
37
76
  method: ctx.method ?? null,
38
77
  url: ctx.url ?? "",
39
78
  scheme: ctx.scheme ?? "",
@@ -54,24 +93,30 @@ var Profiler = class {
54
93
  queries: ctx.profilerQueries && ctx.profilerQueries.length > 0 ? ctx.profilerQueries : void 0,
55
94
  security: readSecurity(ctx)
56
95
  };
57
- this._buf.delete(requestId);
58
- this._buf.set(requestId, entry);
59
- if (this._buf.size > this._cap) {
60
- const oldest = this._buf.keys().next().value;
61
- if (oldest !== void 0) this._buf.delete(oldest);
96
+ const { app, tooling, asset } = this._lanes;
97
+ app.delete(requestId);
98
+ tooling.delete(requestId);
99
+ asset.delete(requestId);
100
+ const lane = profileLane(entry.url);
101
+ const buf = this._lanes[lane];
102
+ buf.set(requestId, entry);
103
+ if (buf.size > this._caps[lane]) {
104
+ const oldest = buf.keys().next().value;
105
+ if (oldest !== void 0) buf.delete(oldest);
62
106
  }
63
107
  }
64
108
  /** Profil complet d'une requête, ou `undefined` si évincé/inconnu. */
65
109
  get(requestId) {
66
- return this._buf.get(requestId);
110
+ const { app, tooling, asset } = this._lanes;
111
+ return app.get(requestId) ?? tooling.get(requestId) ?? asset.get(requestId);
67
112
  }
68
113
  /**
69
114
  * Résumés des requêtes les plus récentes (récent → ancien), capés à `limit`.
70
115
  */
71
116
  recent(limit = 60) {
72
117
  const out = [];
73
- const entries = [...this._buf.values()];
74
- for (const e of entries.reverse()) {
118
+ const entries = this.#collectAfter(0).reverse();
119
+ for (const e of entries) {
75
120
  if (out.length >= limit) break;
76
121
  out.push({
77
122
  requestId: e.requestId,
@@ -87,13 +132,51 @@ var Profiler = class {
87
132
  }
88
133
  return out;
89
134
  }
135
+ /** Dernier numéro d'ordre attribué : le point de départ d'une lecture suivie. */
136
+ get cursor() {
137
+ return this._seq;
138
+ }
139
+ /**
140
+ * Profils collectés après `cursor`, tous anneaux confondus, dans l'ordre
141
+ * des numéros.
142
+ *
143
+ * Les pertes se comptent sans estimation : chaque numéro attribué est un
144
+ * profil collecté, donc ce qui manque entre le curseur et le dernier numéro
145
+ * a disparu avant d'être lu. Un curseur venu d'ailleurs (supérieur au
146
+ * dernier numéro) repart de zéro.
147
+ *
148
+ * @param cursor - le `cursor` rendu par l'appel précédent, ou {@link cursor}.
149
+ */
150
+ since(cursor) {
151
+ const from = cursor >= 0 && cursor <= this._seq ? cursor : 0;
152
+ const entries = this.#collectAfter(from);
153
+ return {
154
+ entries,
155
+ cursor: this._seq,
156
+ lost: this._seq - from - entries.length
157
+ };
158
+ }
90
159
  /** Nombre de profils en mémoire. */
91
160
  get size() {
92
- return this._buf.size;
161
+ const { app, tooling, asset } = this._lanes;
162
+ return app.size + tooling.size + asset.size;
93
163
  }
94
- /** Vide le ring buffer. */
164
+ /** Vide les anneaux. */
95
165
  clear() {
96
- this._buf.clear();
166
+ const { app, tooling, asset } = this._lanes;
167
+ app.clear();
168
+ tooling.clear();
169
+ asset.clear();
170
+ }
171
+ /**
172
+ * Profils de numéro supérieur à `from`, tous anneaux confondus, du plus
173
+ * ancien au plus récent. Lecture à la demande (console, outil de l'agent) :
174
+ * jamais sur le chemin d'une requête.
175
+ */
176
+ #collectAfter(from) {
177
+ const out = [];
178
+ for (const lane of Object.values(this._lanes)) for (const e of lane.values()) if (e.seq > from) out.push(e);
179
+ return out.sort((a, b) => a.seq - b.seq);
97
180
  }
98
181
  };
99
182
  /**
@@ -131,5 +214,4 @@ function readSecurity(ctx) {
131
214
  function readUser(ctx) {
132
215
  return ctx.userIdentifier ?? ctx.securityTrace?.user ?? null;
133
216
  }
134
- //#endregion
135
217
  export { Profiler, Profiler as default };