@nodefony/http 10.0.0-alpha.1

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 (155) hide show
  1. package/LICENSE +544 -0
  2. package/README.md +77 -0
  3. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
  4. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
  5. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateParam.js +8 -0
  6. package/dist/index.js +108 -0
  7. package/dist/nodefony/command/assetsPublishCommand.js +102 -0
  8. package/dist/nodefony/command/certificatesCommand.js +47 -0
  9. package/dist/nodefony/command/networkCommand.js +27 -0
  10. package/dist/nodefony/command/proxyGenerateCommand.js +66 -0
  11. package/dist/nodefony/config/config.js +335 -0
  12. package/dist/nodefony/config/defineModuleConfig.js +93 -0
  13. package/dist/nodefony/interfaces/IContext.js +1 -0
  14. package/dist/nodefony/interfaces/ICookie.js +1 -0
  15. package/dist/nodefony/interfaces/IErrorRenderer.js +1 -0
  16. package/dist/nodefony/interfaces/IHttpConfig.js +1 -0
  17. package/dist/nodefony/interfaces/IHttpKernel.js +1 -0
  18. package/dist/nodefony/interfaces/IRequest.js +1 -0
  19. package/dist/nodefony/interfaces/IRequestLogger.js +1 -0
  20. package/dist/nodefony/interfaces/IResponse.js +1 -0
  21. package/dist/nodefony/interfaces/ISession.js +1 -0
  22. package/dist/nodefony/interfaces/IUpload.js +1 -0
  23. package/dist/nodefony/interfaces/index.js +1 -0
  24. package/dist/nodefony/service/HttpAdminApi.js +376 -0
  25. package/dist/nodefony/service/ProfilerAdminApi.js +73 -0
  26. package/dist/nodefony/service/audit-logger.js +159 -0
  27. package/dist/nodefony/service/certificates.js +545 -0
  28. package/dist/nodefony/service/error-renderer.js +320 -0
  29. package/dist/nodefony/service/http-kernel.js +948 -0
  30. package/dist/nodefony/service/pretty-request-logger.js +72 -0
  31. package/dist/nodefony/service/request-logger.js +54 -0
  32. package/dist/nodefony/service/servers/clientError.js +20 -0
  33. package/dist/nodefony/service/servers/server-http.js +135 -0
  34. package/dist/nodefony/service/servers/server-https.js +204 -0
  35. package/dist/nodefony/service/servers/server-static.js +192 -0
  36. package/dist/nodefony/service/servers/server-websocket-secure.js +104 -0
  37. package/dist/nodefony/service/servers/server-websocket.js +104 -0
  38. package/dist/nodefony/service/servers/serverShutdown.js +31 -0
  39. package/dist/nodefony/service/servers/wsHeartbeat.js +64 -0
  40. package/dist/nodefony/service/sessions/sessions-service.js +580 -0
  41. package/dist/nodefony/service/trace.js +72 -0
  42. package/dist/nodefony/service/upload/upload-service.js +171 -0
  43. package/dist/nodefony/src/assets/collectAssets.js +34 -0
  44. package/dist/nodefony/src/assets/prebuiltUi.js +125 -0
  45. package/dist/nodefony/src/context/Context.js +415 -0
  46. package/dist/nodefony/src/context/domainMatcher.js +88 -0
  47. package/dist/nodefony/src/context/forwarded.js +185 -0
  48. package/dist/nodefony/src/context/http/HttpContext.js +309 -0
  49. package/dist/nodefony/src/context/http/Request.js +543 -0
  50. package/dist/nodefony/src/context/http/Response.js +368 -0
  51. package/dist/nodefony/src/context/http/parser.js +188 -0
  52. package/dist/nodefony/src/context/http/urlFastPath.js +103 -0
  53. package/dist/nodefony/src/context/http2/Request.js +29 -0
  54. package/dist/nodefony/src/context/http2/Response.js +97 -0
  55. package/dist/nodefony/src/context/metaData.js +47 -0
  56. package/dist/nodefony/src/context/requestId.js +41 -0
  57. package/dist/nodefony/src/context/trustProxy.js +167 -0
  58. package/dist/nodefony/src/context/websocket/Response.js +181 -0
  59. package/dist/nodefony/src/context/websocket/WebsocketContext.js +389 -0
  60. package/dist/nodefony/src/context/websocket/wsBackpressure.js +56 -0
  61. package/dist/nodefony/src/context/websocket/wsLogContent.js +68 -0
  62. package/dist/nodefony/src/cookies/cookie.js +258 -0
  63. package/dist/nodefony/src/errors/httpError.js +69 -0
  64. package/dist/nodefony/src/profiler/FrameProfile.js +95 -0
  65. package/dist/nodefony/src/profiler/Profiler.js +139 -0
  66. package/dist/nodefony/src/proxy/generateProxyConfig.js +157 -0
  67. package/dist/nodefony/src/rateLimit/IRateLimitStore.js +1 -0
  68. package/dist/nodefony/src/rateLimit/MemoryRateLimitStore.js +146 -0
  69. package/dist/nodefony/src/rateLimit/WsConnectionCounter.js +64 -0
  70. package/dist/nodefony/src/rateLimit/rateLimitFilters.js +20 -0
  71. package/dist/nodefony/src/servers/portBinder.js +114 -0
  72. package/dist/nodefony/src/session/session.js +390 -0
  73. package/dist/nodefony/src/session/storage/MemorySessionStorage.js +185 -0
  74. package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +137 -0
  75. package/dist/nodefony/src/session/storage/sessionFilters.js +83 -0
  76. package/dist/nodefony/src/session/storage/sessionSort.js +53 -0
  77. package/dist/types/index.d.ts +83 -0
  78. package/dist/types/nodefony/command/assetsPublishCommand.d.ts +23 -0
  79. package/dist/types/nodefony/command/certificatesCommand.d.ts +17 -0
  80. package/dist/types/nodefony/command/networkCommand.d.ts +8 -0
  81. package/dist/types/nodefony/command/proxyGenerateCommand.d.ts +19 -0
  82. package/dist/types/nodefony/config/config.d.ts +197 -0
  83. package/dist/types/nodefony/config/defineModuleConfig.d.ts +39 -0
  84. package/dist/types/nodefony/interfaces/IContext.d.ts +138 -0
  85. package/dist/types/nodefony/interfaces/ICookie.d.ts +47 -0
  86. package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +55 -0
  87. package/dist/types/nodefony/interfaces/IHttpConfig.d.ts +12 -0
  88. package/dist/types/nodefony/interfaces/IHttpKernel.d.ts +10 -0
  89. package/dist/types/nodefony/interfaces/IRequest.d.ts +35 -0
  90. package/dist/types/nodefony/interfaces/IRequestLogger.d.ts +31 -0
  91. package/dist/types/nodefony/interfaces/IResponse.d.ts +39 -0
  92. package/dist/types/nodefony/interfaces/ISession.d.ts +283 -0
  93. package/dist/types/nodefony/interfaces/IUpload.d.ts +66 -0
  94. package/dist/types/nodefony/interfaces/index.d.ts +7 -0
  95. package/dist/types/nodefony/service/HttpAdminApi.d.ts +18 -0
  96. package/dist/types/nodefony/service/ProfilerAdminApi.d.ts +23 -0
  97. package/dist/types/nodefony/service/audit-logger.d.ts +143 -0
  98. package/dist/types/nodefony/service/certificates.d.ts +246 -0
  99. package/dist/types/nodefony/service/error-renderer.d.ts +74 -0
  100. package/dist/types/nodefony/service/http-kernel.d.ts +377 -0
  101. package/dist/types/nodefony/service/pretty-request-logger.d.ts +25 -0
  102. package/dist/types/nodefony/service/request-logger.d.ts +18 -0
  103. package/dist/types/nodefony/service/servers/clientError.d.ts +14 -0
  104. package/dist/types/nodefony/service/servers/server-http.d.ts +42 -0
  105. package/dist/types/nodefony/service/servers/server-https.d.ts +41 -0
  106. package/dist/types/nodefony/service/servers/server-static.d.ts +62 -0
  107. package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +29 -0
  108. package/dist/types/nodefony/service/servers/server-websocket.d.ts +29 -0
  109. package/dist/types/nodefony/service/servers/serverShutdown.d.ts +27 -0
  110. package/dist/types/nodefony/service/servers/wsHeartbeat.d.ts +46 -0
  111. package/dist/types/nodefony/service/sessions/sessions-service.d.ts +218 -0
  112. package/dist/types/nodefony/service/trace.d.ts +39 -0
  113. package/dist/types/nodefony/service/upload/upload-service.d.ts +61 -0
  114. package/dist/types/nodefony/src/assets/collectAssets.d.ts +35 -0
  115. package/dist/types/nodefony/src/assets/prebuiltUi.d.ts +99 -0
  116. package/dist/types/nodefony/src/context/Context.d.ts +195 -0
  117. package/dist/types/nodefony/src/context/domainMatcher.d.ts +67 -0
  118. package/dist/types/nodefony/src/context/forwarded.d.ts +95 -0
  119. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +85 -0
  120. package/dist/types/nodefony/src/context/http/Request.d.ts +203 -0
  121. package/dist/types/nodefony/src/context/http/Response.d.ts +68 -0
  122. package/dist/types/nodefony/src/context/http/parser.d.ts +65 -0
  123. package/dist/types/nodefony/src/context/http/urlFastPath.d.ts +52 -0
  124. package/dist/types/nodefony/src/context/http2/Request.d.ts +14 -0
  125. package/dist/types/nodefony/src/context/http2/Response.d.ts +20 -0
  126. package/dist/types/nodefony/src/context/metaData.d.ts +58 -0
  127. package/dist/types/nodefony/src/context/requestId.d.ts +28 -0
  128. package/dist/types/nodefony/src/context/trustProxy.d.ts +77 -0
  129. package/dist/types/nodefony/src/context/websocket/Response.d.ts +53 -0
  130. package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +125 -0
  131. package/dist/types/nodefony/src/context/websocket/wsBackpressure.d.ts +73 -0
  132. package/dist/types/nodefony/src/context/websocket/wsLogContent.d.ts +37 -0
  133. package/dist/types/nodefony/src/cookies/cookie.d.ts +88 -0
  134. package/dist/types/nodefony/src/errors/httpError.d.ts +15 -0
  135. package/dist/types/nodefony/src/profiler/FrameProfile.d.ts +110 -0
  136. package/dist/types/nodefony/src/profiler/Profiler.d.ts +192 -0
  137. package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +76 -0
  138. package/dist/types/nodefony/src/rateLimit/IRateLimitStore.d.ts +98 -0
  139. package/dist/types/nodefony/src/rateLimit/MemoryRateLimitStore.d.ts +40 -0
  140. package/dist/types/nodefony/src/rateLimit/WsConnectionCounter.d.ts +37 -0
  141. package/dist/types/nodefony/src/rateLimit/rateLimitFilters.d.ts +18 -0
  142. package/dist/types/nodefony/src/servers/portBinder.d.ts +102 -0
  143. package/dist/types/nodefony/src/session/session.d.ts +171 -0
  144. package/dist/types/nodefony/src/session/storage/MemorySessionStorage.d.ts +77 -0
  145. package/dist/types/nodefony/src/session/storage/RevocationGuardStorage.d.ts +81 -0
  146. package/dist/types/nodefony/src/session/storage/sessionFilters.d.ts +102 -0
  147. package/dist/types/nodefony/src/session/storage/sessionSort.d.ts +45 -0
  148. package/docs/cookies.md +365 -0
  149. package/docs/index.md +163 -0
  150. package/docs/observabilite.md +460 -0
  151. package/docs/rate-limit.md +372 -0
  152. package/docs/servers.md +935 -0
  153. package/docs/session.md +768 -0
  154. package/docs/upload.md +460 -0
  155. package/package.json +101 -0
@@ -0,0 +1,389 @@
1
+ import { hasForwardingHeaders, resolveForwarded } from "../forwarded.js";
2
+ import Context from "../Context.js";
3
+ import WebsocketResponse from "./Response.js";
4
+ import { sanitizeRequestId } from "../requestId.js";
5
+ import { FrameProfile } from "../../profiler/FrameProfile.js";
6
+ import { formatWsLogContent } from "./wsLogContent.js";
7
+ import { RequestContext, logColor } from "nodefony";
8
+ import Ws from "ws";
9
+ import { AsyncResource } from "node:async_hooks";
10
+ import url, { URL } from "node:url";
11
+ //#region nodefony/src/context/websocket/WebsocketContext.ts
12
+ /**
13
+ * Coerce un code (applicatif / HTTP / WS) en code de fermeture WebSocket VALIDE
14
+ * et conforme RFC 6455 §7.4, en PRÉFÉRANT les codes standard §7.4.1 quand le
15
+ * sens existe :
16
+ * - code déjà valide émissible (1000-1003, 1007-1011, 3000-4999) → conservé ;
17
+ * - HTTP 5xx / interne / code absent → **1011** (Internal Error) ;
18
+ * - HTTP 401 / 403 / 421 → **1008** (Policy Violation) ;
19
+ * - autre `< 1000` (ex. 404, sans équivalent RFC) → **4004**, plage privée
20
+ * 4000-4999 (§7.4.2, « undefined by this protocol » → convention applicative).
21
+ *
22
+ * Évite d'émettre un code de la plage 0-999 (« not used », rejeté par `ws`) ou
23
+ * un code réservé non émissible (1004/1005/1006/1015).
24
+ *
25
+ * @param code - code source (number, undefined…).
26
+ * @returns un code de fermeture WS valide.
27
+ */
28
+ function toWsCloseCode(code) {
29
+ if (typeof code !== "number" || !Number.isInteger(code)) return 1011;
30
+ if (code === 1e3 || code >= 1001 && code <= 1003 || code >= 1007 && code <= 1011 || code >= 3e3 && code <= 4999) return code;
31
+ if (code >= 500 && code < 600) return 1011;
32
+ if (code === 401 || code === 403 || code === 421) return 1008;
33
+ if (code >= 400 && code < 500) return 4004;
34
+ return 1011;
35
+ }
36
+ /**
37
+ * Drapeau « logger le CONTENU des messages WS » — résolu **une seule fois** au
38
+ * premier message (kernel présent). `true` hors production : le Suivi de requête
39
+ * (Studio) affiche alors les frames RECEIVE/SEND/BROADCAST. En prod → `false` →
40
+ * le gate court-circuite AVANT toute allocation/concat (0 surcoût hot path).
41
+ */
42
+ let wsContentLogging = null;
43
+ var WebsocketContext = class extends Context {
44
+ request;
45
+ response = null;
46
+ acceptedProtocol;
47
+ port;
48
+ rejected = false;
49
+ teardownWired = false;
50
+ connection = null;
51
+ origin;
52
+ proxy = null;
53
+ forwarded = null;
54
+ wsUrl = null;
55
+ queryGet = {};
56
+ queryRequest = {};
57
+ wsPath = "";
58
+ #frameSeq = 0;
59
+ constructor(scope, req, ws, type) {
60
+ super(scope, type);
61
+ this.webSocketState = "handshake";
62
+ this.request = req;
63
+ this.connection = ws;
64
+ this.response = new WebsocketResponse(ws, this);
65
+ this.method = this.getMethod();
66
+ this.origin = req.headers.origin ?? "";
67
+ const checker = this.httpKernel?.getTrustProxyChecker();
68
+ const socketAddress = req.socket?.remoteAddress;
69
+ if (checker && checker.isTrusted(socketAddress) && hasForwardingHeaders(req.headers)) this.forwarded = resolveForwarded(req.headers, socketAddress, checker);
70
+ this.remoteAddress = this.getRemoteAddress();
71
+ this.acceptedProtocol = req.headers["sec-websocket-protocol"];
72
+ this.scheme = type === "websocket-secure" ? "wss" : "ws";
73
+ const incomingId = sanitizeRequestId(req.headers["x-request-id"]);
74
+ if (incomingId) this.requestId = incomingId;
75
+ const host = req.headers.host ?? "localhost";
76
+ const rawUrl = req.url ?? "/";
77
+ this.wsUrl = new URL(`${this.scheme}://${host}${rawUrl}`);
78
+ this.queryGet = Object.fromEntries(this.wsUrl.searchParams.entries());
79
+ this.queryRequest = { ...this.queryGet };
80
+ this.wsPath = this.wsUrl.pathname + this.wsUrl.search;
81
+ this.url = url.format(this.wsUrl);
82
+ this.port = parseInt(this.wsUrl.port, 10) || (type === "websocket-secure" ? 443 : 80);
83
+ this.request.url = this.wsUrl;
84
+ this.request.queryGet = this.queryGet;
85
+ this.request.query = this.queryRequest;
86
+ this.request.path = this.wsPath;
87
+ try {
88
+ this.originUrl = new URL(this.origin);
89
+ } catch {
90
+ this.originUrl = new URL(this.url);
91
+ }
92
+ this.parseCookies();
93
+ this.cookieSession = this.getCookieSession(this.getSessionCookieName());
94
+ this.domain = this.getHostName();
95
+ this.validDomain = this.isValidDomain();
96
+ this.rejected = false;
97
+ const fwd = this.forwarded;
98
+ if (fwd) {
99
+ this.proxy = {
100
+ proxyServer: req.headers["x-forwarded-server"] ?? "unknown",
101
+ proxyProto: fwd.proto ?? req.headers["x-forwarded-proto"],
102
+ proxyPort: req.headers["x-forwarded-port"],
103
+ proxyFor: fwd.forwardedFor ?? req.headers["x-forwarded-for"],
104
+ proxyHost: fwd.host ?? req.headers["x-forwarded-host"],
105
+ proxyVia: req.headers.via
106
+ };
107
+ this.log(`PROXY WEBSOCKET REQUEST ${fwd.fromStandard ? "Forwarded (RFC 7239)" : "x-forwarded"} VIA : ${this.proxy?.proxyVia}`, "DEBUG");
108
+ }
109
+ }
110
+ log(pci, severity, msgid, msg) {
111
+ if (!msgid) msgid = "WEBSOCKET CONTEXT";
112
+ return super.log(pci, severity, msgid, msg);
113
+ }
114
+ logRequest(httpError, acceptedProtocol) {
115
+ try {
116
+ const logger = this.httpKernel?.getRequestLogger();
117
+ if (!logger) return;
118
+ const entry = logger.renderWebsocket(this, httpError ?? null, acceptedProtocol ?? null);
119
+ const pdu = this.log(entry.text, entry.severity, entry.msgid);
120
+ if (pdu && pdu.requestId === void 0) pdu.requestId = this.requestId;
121
+ return pdu;
122
+ } catch {}
123
+ }
124
+ async connect() {
125
+ if (!this.response || !this.request || !this.connection) throw new Error("Nodefony Websocket Bad request/response/connection");
126
+ if (this.rejected) throw new Error("Nodefony Websocket rejected");
127
+ this.response.setConnection(this.connection);
128
+ this.connection.on("close", AsyncResource.bind(this.onClose.bind(this)));
129
+ this.connection.on("error", AsyncResource.bind(this.onConnectionError.bind(this)));
130
+ this.teardownWired = true;
131
+ await this.fireAsync("onConnect", this, this.connection);
132
+ this.requestEnded = true;
133
+ this.connection.on("message", AsyncResource.bind(this.handleMessage.bind(this)));
134
+ this.logRequest(null, this.acceptedProtocol ?? null);
135
+ this.webSocketState = "connected";
136
+ return this.connection;
137
+ }
138
+ async handle(data) {
139
+ if (this.rejected) {
140
+ this.webSocketState = "error";
141
+ throw new Error("Nodefony Websocket rejected");
142
+ }
143
+ if (!this.resolver) this.resolver = this.router?.resolve(this);
144
+ else try {
145
+ this.resolver.match(this.resolver.route, this);
146
+ } catch (e) {
147
+ if (!this.rejected) this.reject(e.code ?? void 0, e.message);
148
+ throw e;
149
+ }
150
+ await this.fireAsync("onRequest", this, this.resolver);
151
+ await this.kernel?.fireAsync("onRequest", this, this.resolver);
152
+ if (this.resolver && this.resolver.resolve) {
153
+ this.setMetaData({ nodefony: { websocket: {
154
+ state: this.webSocketState,
155
+ protocol: this.acceptedProtocol
156
+ } } });
157
+ await this.resolver.callController(data).then(async () => {
158
+ await this.saveSession().then((session) => {
159
+ if (session) this.log(`SAVE SESSION ID : ${session.id}`, "DEBUG");
160
+ }).catch((e) => {
161
+ throw e;
162
+ });
163
+ return this;
164
+ }).catch((error) => {
165
+ if (!this.rejected) {
166
+ if (this.requestEnded) throw this.close(error.code, error.message);
167
+ this.reject(error.code ?? void 0, error.message);
168
+ this.rejected = true;
169
+ this.webSocketState = "error";
170
+ throw error;
171
+ }
172
+ });
173
+ } else if (!this.rejected) {
174
+ this.reject(4004, "Not Found");
175
+ this.rejected = true;
176
+ this.webSocketState = "error";
177
+ }
178
+ return this;
179
+ }
180
+ async render(chunk, encoding) {
181
+ let data = chunk;
182
+ if (this.isJson) data = JSON.stringify(chunk);
183
+ return this.send(data, encoding);
184
+ }
185
+ async send(data, encoding) {
186
+ if (this.response) {
187
+ const payload = data ?? this.response.body;
188
+ const sink = RequestContext.get()?.renderSink;
189
+ if (sink) {
190
+ sink.body = payload;
191
+ return this.response;
192
+ }
193
+ this.logMessageContent("SEND", payload);
194
+ this.fire("onMessage", payload, this, "SEND");
195
+ this.fire("onSend", payload, this);
196
+ return this.response.send(payload, encoding);
197
+ }
198
+ throw new Error("No response found");
199
+ }
200
+ broadcast(data, encoding) {
201
+ if (this.response) {
202
+ const payload = data ?? this.response.body;
203
+ if (payload) {
204
+ this.logMessageContent("BROADCAST", payload);
205
+ this.fire("onMessage", payload, this, "BROADCAST");
206
+ this.fire("onBroadcast", payload, this);
207
+ return this.response.broadcast(payload, encoding);
208
+ }
209
+ }
210
+ return null;
211
+ }
212
+ /**
213
+ * Logge le CONTENU d'un message WS (corrélé `requestId` via l'override `log`),
214
+ * gaté hors prod et borné — le Suivi de requête (Studio) le surface alors par
215
+ * direction. Hot path : le gate booléen court-circuite en prod AVANT toute
216
+ * construction de chaîne (0 allocation / 0 concat).
217
+ *
218
+ * @param dir - sens du message du point de vue serveur.
219
+ * @param data - charge utile (string, Buffer binaire, objet, ou null).
220
+ */
221
+ logMessageContent(dir, data) {
222
+ if (wsContentLogging === null) wsContentLogging = this.kernel?.environment !== "production";
223
+ if (!wsContentLogging) return;
224
+ this.log(formatWsLogContent(data), "DEBUG", `WS ${dir}`);
225
+ }
226
+ /**
227
+ * Ouvre le profil d'**une invocation** du pont RPC (une frame `api.request`).
228
+ *
229
+ * Le contexte WS vit pour la CONNEXION : ses `phases` sont cumulatives et son
230
+ * `requestId` est unique pour toute la socket. Une frame reçoit donc son
231
+ * propre profil, identifié `<requestId de la connexion>.<n° de frame>` — le
232
+ * `id` JSON-RPC ne peut pas servir de clé : il est choisi par le client.
233
+ *
234
+ * @param method - méthode LOGIQUE de l'invocation (`GET`, `POST`…).
235
+ * @param framePath - chemin invoqué par la frame (jamais l'URL de la
236
+ * connexion) — nommé ainsi parce que `url` masquerait le module
237
+ * `node:url`, importé ici et utilisé par `connect()`.
238
+ * @returns le profil, ou `null` si profiler ET timing sont éteints (prod) —
239
+ * zéro allocation dans ce cas.
240
+ */
241
+ beginFrame(method, framePath) {
242
+ if (!this.profiling && !this.timingEnabled) return null;
243
+ this.#frameSeq += 1;
244
+ return new FrameProfile({
245
+ requestId: `${this.requestId}.${this.#frameSeq}`,
246
+ type: this.type,
247
+ scheme: this.scheme,
248
+ method,
249
+ url: framePath,
250
+ remoteAddress: this.remoteAddress ?? null,
251
+ traceparent: this.traceparent,
252
+ security: this.security ?? null,
253
+ securityTrace: this.securityTrace,
254
+ timing: this.timingEnabled,
255
+ queries: this.profiling
256
+ });
257
+ }
258
+ /**
259
+ * Enregistre le profil d'une invocation terminée dans le ring buffer du
260
+ * Profiler (no-op hors dev, ou si aucun profil n'a été ouvert).
261
+ */
262
+ collectFrame(frame) {
263
+ if (frame === null || !this.profiling) return;
264
+ this.get("profiler")?.collect(frame);
265
+ }
266
+ /**
267
+ * Les phases émises pendant une invocation du pont (`initialize` par le
268
+ * Resolver, `render` par le Controller…) vont dans le profil de **la frame**,
269
+ * pas du contexte : sans cette redirection, `Context.phases` accumulerait deux
270
+ * entrées par message pour toute la vie de la socket (timeline cumulative
271
+ * ET croissance sans borne). Hors invocation (handshake), comportement de base.
272
+ */
273
+ phaseStart(name) {
274
+ if (!this.timingEnabled) return;
275
+ const frame = RequestContext.get()?.invocation;
276
+ if (frame) {
277
+ frame.phaseStart(name);
278
+ return;
279
+ }
280
+ super.phaseStart(name);
281
+ }
282
+ phaseEnd(name) {
283
+ if (!this.timingEnabled) return;
284
+ const frame = RequestContext.get()?.invocation;
285
+ if (frame) {
286
+ frame.phaseEnd(name);
287
+ return;
288
+ }
289
+ super.phaseEnd(name);
290
+ }
291
+ async handleMessage(data, isBinary) {
292
+ this.webSocketState = "message";
293
+ const message = isBinary ? data : data.toString();
294
+ this.logMessageContent("RECEIVE", message);
295
+ if (this.response) this.response.body = Buffer.isBuffer(data) ? data : Buffer.from(data.toString());
296
+ try {
297
+ if (!this.resolver) this.resolver = this.router?.resolve(this);
298
+ else this.resolver.match(this.resolver?.route, this);
299
+ await this.fireAsync("onMessage", message, this, "RECEIVE");
300
+ if (this.resolver.resolve) {
301
+ this.setMetaData({ nodefony: { websocket: {
302
+ state: this.webSocketState,
303
+ protocol: this.acceptedProtocol
304
+ } } });
305
+ return this.resolver.callController([message]).catch((e) => {
306
+ throw e;
307
+ });
308
+ } else if (!this.rejected) {
309
+ this.reject(4004, "Not Found");
310
+ this.rejected = true;
311
+ }
312
+ } catch (e) {
313
+ this.reject(1011, "Internal Error");
314
+ throw e;
315
+ }
316
+ }
317
+ onClose(code, reason) {
318
+ const description = reason.toString();
319
+ 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}`);
320
+ if (this.connection?.readyState !== Ws.CLOSED) {
321
+ try {
322
+ this.response?.drop(code, description);
323
+ } catch (e) {
324
+ 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 ?? "")}`);
325
+ }
326
+ this.fire("onClose", code, description, this.connection);
327
+ } else this.fire("onClose", code, description, this.connection);
328
+ this.fire("onFinish", this, code, description);
329
+ this.webSocketState = "closed";
330
+ }
331
+ /**
332
+ * Listener `error` de la socket ws (OBLIGATOIRE — sans lui, un 'error' émis
333
+ * sans listener crashe le process via EventEmitter). `ws` émet 'error' PUIS
334
+ * 'close' → le teardown se fait dans {@link onClose} ; ici on logge seulement
335
+ * (pas de double-close). Erreur transport (reset TCP, frame corrompue…).
336
+ *
337
+ * @param error - erreur émise par la socket.
338
+ */
339
+ onConnectionError(error) {
340
+ this.webSocketState = "error";
341
+ 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}`);
342
+ this.fire("onError", error, this);
343
+ }
344
+ setScheme() {
345
+ return this.wsUrl?.protocol.replace(":", "");
346
+ }
347
+ getRemoteAddress() {
348
+ if (this.forwarded) return this.forwarded.clientIp;
349
+ return this.request?.socket?.remoteAddress ?? null;
350
+ }
351
+ getHost() {
352
+ return this.request?.headers.host;
353
+ }
354
+ getHostName() {
355
+ return this.wsUrl?.hostname;
356
+ }
357
+ getUserAgent() {
358
+ return this.request?.headers["user-agent"];
359
+ }
360
+ getMethod() {
361
+ return "WEBSOCKET";
362
+ }
363
+ setContextJson() {
364
+ this.isJson = true;
365
+ }
366
+ clean() {
367
+ this.request = null;
368
+ if (this.response) this.response.clean();
369
+ this.response = null;
370
+ this.connection = null;
371
+ this.container?.clean();
372
+ super.clean();
373
+ }
374
+ close(reasonCode, description) {
375
+ if (this.response) return this.response.close(toWsCloseCode(reasonCode), description);
376
+ }
377
+ drop(reasonCode, description) {
378
+ if (this.response) return this.response.drop(reasonCode, description);
379
+ }
380
+ reject(code, message) {
381
+ if (this.connection && this.connection.readyState === Ws.OPEN) {
382
+ const raw = typeof code === "string" ? parseInt(code, 10) : code;
383
+ this.connection.close(toWsCloseCode(raw), message ?? "Rejected");
384
+ }
385
+ this.rejected = true;
386
+ }
387
+ };
388
+ //#endregion
389
+ export { WebsocketContext as default, toWsCloseCode };
@@ -0,0 +1,56 @@
1
+ //#region nodefony/src/context/websocket/wsBackpressure.ts
2
+ /**
3
+ * Relit le seuil + la politique depuis les options du `WebSocketServer`.
4
+ *
5
+ * `ws` préserve nos clés custom (`maxBackpressure`/`backpressurePolicy`) dans
6
+ * `wss.options` via le spread de son constructeur. Appelé UNE fois hors de la boucle
7
+ * `broadcast()` (pas par frame).
8
+ *
9
+ * @param wss - le serveur WebSocket (ou null/undefined → backpressure désactivée)
10
+ * @returns seuil résolu (`max`, 0 = off) + politique (`policy`, défaut `"drop"`)
11
+ */
12
+ const readBackpressureOptions = (wss) => {
13
+ const opts = wss?.options ?? {};
14
+ return {
15
+ max: opts.maxBackpressure ?? 0,
16
+ policy: opts.backpressurePolicy ?? "drop",
17
+ closeAfterDrops: opts.backpressureCloseAfterDrops ?? 0
18
+ };
19
+ };
20
+ /**
21
+ * Décide si une frame doit partir vers `ws` selon la backpressure SORTANTE
22
+ * (serveur → client). Protège la RAM d'envoi du serveur quand le client est lent à
23
+ * RECEVOIR (ne lit pas assez vite → `ws.bufferedAmount` gonfle → OOM). `broadcast()`
24
+ * amplifie : un seul client lent peut, sans borne, faire tomber la diffusion entière.
25
+ *
26
+ * PERF (hot path WS) : lecture O(1) de `ws.bufferedAmount`, **0 allocation sous le
27
+ * seuil** (chemin nominal = comportement inchangé). Au-delà : incrémente `_nfDrops`
28
+ * (lazy) et, si la politique est `close`, ferme la socket (RFC 6455 close 1013
29
+ * « Try Again Later » → le client peut back-off + reconnecter). NE FAIT PAS le `send()`
30
+ * — le caller émet uniquement si le retour vaut `"send"`.
31
+ *
32
+ * @param ws - la socket destinataire
33
+ * @param max - seuil d'octets (`<= 0` → désactivé)
34
+ * @param policy - action au dépassement
35
+ * @returns `"send"` (émettre) · `"drop"` (sauter cette frame) · `"close"` (socket fermée)
36
+ */
37
+ const decideSend = (ws, max, policy, closeAfterDrops = 0) => {
38
+ const sock = ws;
39
+ if (max <= 0 || (ws.bufferedAmount ?? 0) <= max) {
40
+ if (sock._nfDropStreak) sock._nfDropStreak -= 1;
41
+ return "send";
42
+ }
43
+ sock._nfDrops = (sock._nfDrops ?? 0) + 1;
44
+ const streak = sock._nfDropStreak = (sock._nfDropStreak ?? 0) + 1;
45
+ if (policy === "close") {
46
+ ws.close(1013, "backpressure");
47
+ return "close";
48
+ }
49
+ if (closeAfterDrops > 0 && streak >= closeAfterDrops) {
50
+ ws.close(1013, "backpressure");
51
+ return "close";
52
+ }
53
+ return "drop";
54
+ };
55
+ //#endregion
56
+ export { decideSend, readBackpressureOptions };
@@ -0,0 +1,68 @@
1
+ //#region nodefony/src/context/websocket/wsLogContent.ts
2
+ /**
3
+ * Formatage **pur** (0 état, 0 dépendance) du CONTENU d'un message WebSocket pour
4
+ * le log de trace (Suivi de requête Studio). Séparé de `WebsocketContext` pour
5
+ * être testable aux limites sans serveur.
6
+ *
7
+ * 🔒 Robustesse binaire (cf doc `ws` — `socket.send(data)` accepte
8
+ * `String | Number | Object | Buffer | ArrayBuffer | TypedArray | DataView |
9
+ * Buffer[] | Blob`, et l'event `message` livre `Buffer | ArrayBuffer | Buffer[]`).
10
+ * **Toute** charge binaire est résumée `[binary N B]` — JAMAIS sérialisée
11
+ * (`JSON.stringify(new Uint8Array(...))` produirait `{"0":..,"1":..}`, énorme et
12
+ * faux). Seules string et objets « JSON » sont rendus en texte (borné).
13
+ */
14
+ /** Cap de troncature du contenu loggé (octets/chars). Borne ring + JSONL. */
15
+ const WS_LOG_CONTENT_CAP = 4096;
16
+ /**
17
+ * Taille en octets d'une charge **binaire** reconnue, ou `-1` si la valeur n'est
18
+ * pas binaire (→ à traiter en JSON/texte). Couvre tous les types binaires que
19
+ * `ws` accepte/livre : `Buffer`, `ArrayBuffer`, vues (`TypedArray`/`DataView`),
20
+ * `Blob` (Node ≥ 18), et `Buffer[]` (fragments — binaire ssi TOUS ses éléments
21
+ * le sont, sinon c'est un tableau JSON ordinaire).
22
+ *
23
+ * @param data - valeur à mesurer.
24
+ * @returns nombre d'octets, ou `-1` si non binaire.
25
+ */
26
+ function binaryByteLength(data) {
27
+ if (Buffer.isBuffer(data)) return data.length;
28
+ if (data instanceof ArrayBuffer) return data.byteLength;
29
+ if (ArrayBuffer.isView(data)) return data.byteLength;
30
+ if (typeof Blob !== "undefined" && data instanceof Blob) return data.size;
31
+ if (Array.isArray(data)) {
32
+ let total = 0;
33
+ for (const part of data) {
34
+ const n = binaryByteLength(part);
35
+ if (n < 0) return -1;
36
+ total += n;
37
+ }
38
+ return total;
39
+ }
40
+ return -1;
41
+ }
42
+ /**
43
+ * Formate une charge utile WS en chaîne **bornée** et **sûre** pour le log :
44
+ * - `string` → tronquée à `cap` (+ ellipse) ;
45
+ * - binaire (cf {@link binaryByteLength}) → `[binary N B]` (jamais de dump) ;
46
+ * - `null`/`undefined` → `""` ;
47
+ * - objet « JSON » → `JSON.stringify` compact tronqué (cycle, `bigint`, valeur
48
+ * non sérialisable → repli `String(...)`).
49
+ *
50
+ * @param data - charge utile (RECEIVE/SEND/BROADCAST).
51
+ * @param cap - longueur max avant troncature (défaut {@link WS_LOG_CONTENT_CAP}).
52
+ */
53
+ function formatWsLogContent(data, cap = WS_LOG_CONTENT_CAP) {
54
+ if (typeof data === "string") return data.length > cap ? `${data.slice(0, cap)}…` : data;
55
+ if (data === null || data === void 0) return "";
56
+ const bytes = binaryByteLength(data);
57
+ if (bytes >= 0) return `[binary ${bytes} B]`;
58
+ let s;
59
+ try {
60
+ s = JSON.stringify(data);
61
+ } catch {
62
+ return String(data);
63
+ }
64
+ if (typeof s !== "string") return String(data);
65
+ return s.length > cap ? `${s.slice(0, cap)}…` : s;
66
+ }
67
+ //#endregion
68
+ export { WS_LOG_CONTENT_CAP, binaryByteLength, formatWsLogContent };