@nodefony/http 10.0.0-beta.4 → 10.0.0-beta.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/README.md +24 -0
- package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.157.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.157.0}/helpers/esm/decorateParam.js +1 -1
- package/dist/index.js +29 -25
- package/dist/nodefony/command/proxyGenerateCommand.js +2 -2
- package/dist/nodefony/config/config.js +6 -3
- package/dist/nodefony/interfaces/IRequestTrace.js +1 -0
- package/dist/nodefony/service/ProfilerAdminApi.js +17 -3
- package/dist/nodefony/service/audit-logger.js +3 -17
- package/dist/nodefony/service/error-renderer.js +6 -4
- package/dist/nodefony/service/http-kernel.js +191 -40
- package/dist/nodefony/service/pretty-request-logger.js +11 -10
- package/dist/nodefony/service/request-logger.js +1 -1
- package/dist/nodefony/service/reverse-proxy.js +7 -2
- package/dist/nodefony/service/servers/clientError.js +28 -4
- package/dist/nodefony/service/servers/server-h2c.js +5 -5
- package/dist/nodefony/service/servers/server-http.js +5 -3
- package/dist/nodefony/service/servers/server-https.js +12 -5
- package/dist/nodefony/service/servers/server-static.js +1 -6
- package/dist/nodefony/service/servers/server-websocket-secure.js +6 -10
- package/dist/nodefony/service/servers/server-websocket.js +6 -10
- package/dist/nodefony/service/servers/serverShutdown.js +63 -4
- package/dist/nodefony/service/sessions/sessions-service.js +3 -3
- package/dist/nodefony/service/upload/upload-service.js +2 -2
- package/dist/nodefony/src/assets/prebuiltUi.js +23 -5
- package/dist/nodefony/src/context/Context.js +56 -13
- package/dist/nodefony/src/context/http/ByteStream.js +359 -0
- package/dist/nodefony/src/context/http/HttpContext.js +75 -25
- package/dist/nodefony/src/context/http/Request.js +15 -6
- package/dist/nodefony/src/context/http/Response.js +354 -35
- package/dist/nodefony/src/context/http/SseStream.js +81 -146
- package/dist/nodefony/src/context/http/StreamLifecycle.js +273 -0
- package/dist/nodefony/src/context/http/urlFastPath.js +80 -1
- package/dist/nodefony/src/context/http2/Request.js +1 -1
- package/dist/nodefony/src/context/http2/Response.js +28 -5
- package/dist/nodefony/src/context/requestTrace.js +104 -0
- package/dist/nodefony/src/context/timingMode.js +21 -0
- package/dist/nodefony/src/context/transport.js +20 -1
- package/dist/nodefony/src/context/unsentResponse.js +30 -0
- package/dist/nodefony/src/context/websocket/Response.js +4 -2
- package/dist/nodefony/src/context/websocket/WebsocketContext.js +81 -20
- package/dist/nodefony/src/context/websocket/websocketUrl.js +7 -0
- package/dist/nodefony/src/context/websocket/wsBackpressure.js +15 -15
- package/dist/nodefony/src/context/websocket/wsFrameRate.js +71 -0
- package/dist/nodefony/src/context/websocket/wsLogContent.js +55 -17
- package/dist/nodefony/src/cookies/cookie.js +54 -13
- package/dist/nodefony/src/profiler/FrameProfile.js +22 -0
- package/dist/nodefony/src/profiler/Profiler.js +25 -22
- package/dist/nodefony/src/profiler/aggregate.js +26 -6
- package/dist/nodefony/src/proxy/forward.js +109 -9
- package/dist/nodefony/src/proxy/generateProxyConfig.js +6 -2
- package/dist/nodefony/src/proxy/rules.js +29 -14
- package/dist/nodefony/src/servers/upgradeDispatch.js +62 -4
- package/dist/types/index.d.ts +8 -0
- package/dist/types/nodefony/config/config.d.ts +8 -0
- package/dist/types/nodefony/interfaces/IContext.d.ts +9 -0
- package/dist/types/nodefony/interfaces/IRequestTrace.d.ts +56 -0
- package/dist/types/nodefony/interfaces/IReverseProxy.d.ts +8 -0
- package/dist/types/nodefony/interfaces/IRouting.d.ts +16 -0
- package/dist/types/nodefony/interfaces/ISecurity.d.ts +27 -2
- package/dist/types/nodefony/interfaces/index.d.ts +1 -0
- package/dist/types/nodefony/service/ProfilerAdminApi.d.ts +5 -1
- package/dist/types/nodefony/service/audit-logger.d.ts +3 -11
- package/dist/types/nodefony/service/error-renderer.d.ts +5 -4
- package/dist/types/nodefony/service/http-kernel.d.ts +65 -2
- package/dist/types/nodefony/service/servers/clientError.d.ts +3 -2
- package/dist/types/nodefony/service/servers/serverShutdown.d.ts +5 -3
- package/dist/types/nodefony/src/assets/prebuiltUi.d.ts +9 -4
- package/dist/types/nodefony/src/context/Context.d.ts +25 -1
- package/dist/types/nodefony/src/context/http/ByteStream.d.ts +109 -0
- package/dist/types/nodefony/src/context/http/HttpContext.d.ts +60 -4
- package/dist/types/nodefony/src/context/http/Request.d.ts +8 -1
- package/dist/types/nodefony/src/context/http/Response.d.ts +134 -8
- package/dist/types/nodefony/src/context/http/SseStream.d.ts +32 -7
- package/dist/types/nodefony/src/context/http/StreamLifecycle.d.ts +53 -0
- package/dist/types/nodefony/src/context/http/urlFastPath.d.ts +40 -0
- package/dist/types/nodefony/src/context/http2/Response.d.ts +17 -3
- package/dist/types/nodefony/src/context/requestTrace.d.ts +64 -0
- package/dist/types/nodefony/src/context/timingMode.d.ts +23 -0
- package/dist/types/nodefony/src/context/transport.d.ts +17 -0
- package/dist/types/nodefony/src/context/unsentResponse.d.ts +16 -0
- package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +41 -1
- package/dist/types/nodefony/src/context/websocket/websocketUrl.d.ts +4 -0
- package/dist/types/nodefony/src/context/websocket/wsBackpressure.d.ts +12 -2
- package/dist/types/nodefony/src/context/websocket/wsFrameRate.d.ts +69 -0
- package/dist/types/nodefony/src/context/websocket/wsLogContent.d.ts +16 -8
- package/dist/types/nodefony/src/cookies/cookie.d.ts +20 -2
- package/dist/types/nodefony/src/profiler/FrameProfile.d.ts +15 -0
- package/dist/types/nodefony/src/profiler/Profiler.d.ts +27 -102
- package/dist/types/nodefony/src/profiler/aggregate.d.ts +23 -106
- package/dist/types/nodefony/src/proxy/forward.d.ts +30 -2
- package/dist/types/nodefony/src/proxy/rules.d.ts +20 -0
- package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +41 -1
- package/docs/byte-stream.md +371 -0
- package/docs/cookies.md +13 -3
- package/docs/diagnostics-channel.md +303 -0
- package/docs/index.md +14 -1
- package/docs/observabilite.md +94 -16
- package/docs/reverse-proxy.md +33 -19
- package/docs/servers.md +26 -15
- package/docs/session.md +3 -3
- package/docs/sse.md +27 -20
- package/package.json +16 -16
|
@@ -21,6 +21,85 @@ const COLON = 58;
|
|
|
21
21
|
* @returns les composants découpés, ou `null` (bail-out → vrai `new URL`).
|
|
22
22
|
*/
|
|
23
23
|
function splitTarget(target) {
|
|
24
|
+
return splitTargetFast(target);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Origine fixe d'un request-target analysé SANS requête HTTP (pont
|
|
28
|
+
* `api.request`) : le chemin y est CONCATÉNÉ, jamais résolu comme relatif — un
|
|
29
|
+
* `//autre/x` reste un pathname `//autre/x`, comme le verrait la porte HTTP,
|
|
30
|
+
* au lieu de devenir une autorité.
|
|
31
|
+
*/
|
|
32
|
+
const DETACHED_ORIGIN = "http://request-target.invalid";
|
|
33
|
+
const HASH = 35;
|
|
34
|
+
const AT = 64;
|
|
35
|
+
/**
|
|
36
|
+
* Découpe un request-target en forme absolue (`http://hôte:port/chemin?q`) :
|
|
37
|
+
* la forme qu'un client envoie à un proxy, et qu'un serveur d'origine DOIT
|
|
38
|
+
* accepter (RFC 9112 §3.2.2). Il DOIT alors ignorer l'en-tête `Host` et prendre
|
|
39
|
+
* l'hôte de la cible — le recoller à `Host` fabriquait un hôte qui n'existe pas.
|
|
40
|
+
*
|
|
41
|
+
* Seuls `http` et `https` sont reconnus (RFC 6455 §4.2.1 vaut aussi pour un
|
|
42
|
+
* handshake WebSocket) ; le schéma de la cible ne remplace jamais celui de la
|
|
43
|
+
* connexion. Une autorité porteuse d'identifiants (`user@hôte`, interdite par
|
|
44
|
+
* RFC 9110 §4.2.4) n'est pas reconnue : `Host` garde la main.
|
|
45
|
+
*
|
|
46
|
+
* Chemin de chaque requête : un seul test de caractère pour la forme origine,
|
|
47
|
+
* aucune allocation.
|
|
48
|
+
*
|
|
49
|
+
* @param target - le request-target brut, tel que reçu.
|
|
50
|
+
* @returns l'autorité et la forme origine, ou `null` pour toute autre forme.
|
|
51
|
+
*/
|
|
52
|
+
function splitAbsoluteForm(target) {
|
|
53
|
+
if (target === void 0 || target.charCodeAt(0) === SLASH) return null;
|
|
54
|
+
const sep = target.indexOf("://");
|
|
55
|
+
if (sep !== 4 && sep !== 5) return null;
|
|
56
|
+
const scheme = target.slice(0, sep).toLowerCase();
|
|
57
|
+
if (scheme !== "http" && scheme !== "https") return null;
|
|
58
|
+
const start = sep + 3;
|
|
59
|
+
let end = start;
|
|
60
|
+
while (end < target.length) {
|
|
61
|
+
const code = target.charCodeAt(end);
|
|
62
|
+
if (code === SLASH || code === QMARK || code === HASH) break;
|
|
63
|
+
if (code === AT) return null;
|
|
64
|
+
end++;
|
|
65
|
+
}
|
|
66
|
+
if (end === start) return null;
|
|
67
|
+
const rest = target.slice(end);
|
|
68
|
+
return {
|
|
69
|
+
authority: target.slice(start, end),
|
|
70
|
+
target: rest.charCodeAt(0) === SLASH ? rest : `/${rest}`
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Analyse un request-target origin-form (`/chemin[?query]`) dans la forme
|
|
75
|
+
* NORMALISÉE que la porte HTTP donne au routeur (`HttpRequest`) : découpe exacte
|
|
76
|
+
* quand le parse WHATWG serait l'identité ({@link splitTarget}), vrai parse
|
|
77
|
+
* WHATWG sinon — dot-segments résolus, `\` replié, percent-encoding posé.
|
|
78
|
+
*
|
|
79
|
+
* Pour qui reçoit un chemin hors d'une requête HTTP — le pont WebSocket
|
|
80
|
+
* `api.request` — et doit router exactement comme HTTP : la même chaîne, deux
|
|
81
|
+
* transports, une seule forme.
|
|
82
|
+
*
|
|
83
|
+
* @param target - le chemin reçu ; il doit commencer par `/`.
|
|
84
|
+
* @returns pathname et search normalisés, ou `null` si `target` n'est pas un
|
|
85
|
+
* chemin origin-form analysable.
|
|
86
|
+
*/
|
|
87
|
+
function parseRequestTarget(target) {
|
|
88
|
+
if (target.charCodeAt(0) !== SLASH) return null;
|
|
89
|
+
const fast = splitTargetFast(target);
|
|
90
|
+
if (fast !== null) return fast;
|
|
91
|
+
let url;
|
|
92
|
+
try {
|
|
93
|
+
url = new URL(`${DETACHED_ORIGIN}${target}`);
|
|
94
|
+
} catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
pathname: url.pathname,
|
|
99
|
+
search: url.search
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function splitTargetFast(target) {
|
|
24
103
|
if (typeof target !== "string" || target.length === 0) return null;
|
|
25
104
|
if (target.charCodeAt(0) !== SLASH) return null;
|
|
26
105
|
let prev = 0;
|
|
@@ -123,4 +202,4 @@ function isDigitsFrom(s, from) {
|
|
|
123
202
|
}
|
|
124
203
|
return true;
|
|
125
204
|
}
|
|
126
|
-
export { isCanonicalAuthority, splitTarget };
|
|
205
|
+
export { isCanonicalAuthority, parseRequestTarget, splitAbsoluteForm, splitTarget };
|
|
@@ -10,8 +10,9 @@ const emptyHeaders = {
|
|
|
10
10
|
getHeader: () => void 0
|
|
11
11
|
};
|
|
12
12
|
/**
|
|
13
|
-
* Compose les en-têtes d'une réponse HTTP/2 : ceux
|
|
14
|
-
* ceux
|
|
13
|
+
* Compose les en-têtes d'une réponse HTTP/2 : ceux qu'un tiers a écrits sur la
|
|
14
|
+
* réponse Node, puis ceux de la réserve de la réponse, puis ceux passés à
|
|
15
|
+
* `writeHead` — chacun l'emporte sur le précédent.
|
|
15
16
|
*
|
|
16
17
|
* Les clés sont ramenées en MINUSCULES : `getHeaders()` rend `content-type`,
|
|
17
18
|
* un contrôleur passe `Content-Type` — fusionnées telles quelles, les deux
|
|
@@ -27,11 +28,13 @@ const emptyHeaders = {
|
|
|
27
28
|
* @param statusMessage - message de statut, recopié en `x-status-message`
|
|
28
29
|
* @param current - la réponse Node qui porte les en-têtes déjà posés
|
|
29
30
|
* @param extra - en-têtes passés à `writeHead` (objet, ou liste plate clé/valeur)
|
|
31
|
+
* @param stored - la réserve de la réponse, liste plate aux noms en minuscules
|
|
30
32
|
* @returns les en-têtes à transmettre à `respond()`
|
|
31
33
|
*/
|
|
32
|
-
function mergeResponseHeaders(statusMessage, current, extra) {
|
|
34
|
+
function mergeResponseHeaders(statusMessage, current, extra, stored) {
|
|
33
35
|
const merged = { "x-status-message": statusMessage };
|
|
34
36
|
for (const name of current.getHeaderNames()) merged[name] = current.getHeader(name);
|
|
37
|
+
if (stored) for (let i = 0; i + 1 < stored.length; i += 2) merged[String(stored[i])] = stored[i + 1];
|
|
35
38
|
if (Array.isArray(extra)) for (let i = 0; i + 1 < extra.length; i += 2) merged[String(extra[i]).toLowerCase()] = extra[i + 1];
|
|
36
39
|
else if (extra) for (const key in extra) {
|
|
37
40
|
const value = extra[key];
|
|
@@ -68,7 +71,8 @@ var Http2Response = class extends HttpResponse {
|
|
|
68
71
|
this.statusMessage = this.getStatusMessage();
|
|
69
72
|
this.setLength();
|
|
70
73
|
this.ensureContentTypeHeader();
|
|
71
|
-
this.
|
|
74
|
+
this.applyHeaderPolicy(this.contentTypeForPolicy());
|
|
75
|
+
this.headers = mergeResponseHeaders(this.statusMessage, this.response ?? emptyHeaders, headers, this.headerList());
|
|
72
76
|
this.headers[HTTP2_HEADER_STATUS] = this.statusCode;
|
|
73
77
|
const requestId = this.context.requestId;
|
|
74
78
|
if (requestId) this.headers["x-request-id"] = requestId;
|
|
@@ -93,6 +97,7 @@ var Http2Response = class extends HttpResponse {
|
|
|
93
97
|
const stream = this.stream;
|
|
94
98
|
if (!stream) return super.send(chunk, encoding);
|
|
95
99
|
if (stream.destroyed || !stream.writable) {
|
|
100
|
+
this.settleHeadResponse();
|
|
96
101
|
this.log("HTTP/2 stream not writable before send — skip", "DEBUG");
|
|
97
102
|
return this;
|
|
98
103
|
}
|
|
@@ -116,11 +121,29 @@ var Http2Response = class extends HttpResponse {
|
|
|
116
121
|
*/
|
|
117
122
|
end(chunk, encoding) {
|
|
118
123
|
if (this.stream) {
|
|
119
|
-
if (this.stream.writableEnded || this.stream.destroyed || this.stream.closed)
|
|
124
|
+
if (this.stream.writableEnded || this.stream.destroyed || this.stream.closed) {
|
|
125
|
+
this.settleHeadResponse();
|
|
126
|
+
return this.stream;
|
|
127
|
+
}
|
|
120
128
|
return this.stream.end(chunk, encoding || this.encoding);
|
|
121
129
|
}
|
|
122
130
|
return super.end(chunk, encoding);
|
|
123
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* Termine la réponse de compatibilité d'un HEAD, dont Node a clos le flux de
|
|
134
|
+
* lui-même au `respond()`.
|
|
135
|
+
*
|
|
136
|
+
* Sans ce geste, `Http2ServerResponse` n'émet ni `finish` ni `close` : le
|
|
137
|
+
* démontage de la requête, branché sur ce `close` (ligne d'accès, profil,
|
|
138
|
+
* hooks d'après-réponse, libération du scope DI), n'a jamais lieu. Les statuts
|
|
139
|
+
* que Node termine aussi d'office (204, 205, 304) émettent leur `close` seuls,
|
|
140
|
+
* et un flux détruit (client parti) aussi : ils ne sont pas touchés. Hors du
|
|
141
|
+
* chemin servi — appelé seulement quand le flux est déjà clos.
|
|
142
|
+
*/
|
|
143
|
+
settleHeadResponse() {
|
|
144
|
+
const raw = this.response;
|
|
145
|
+
if (raw !== null && this.context.method === "HEAD" && this.stream?.destroyed === false && !raw.writableEnded) raw.end();
|
|
146
|
+
}
|
|
124
147
|
getStatusMessage(code) {
|
|
125
148
|
if (code) {
|
|
126
149
|
if (this.response) return http.STATUS_CODES[code] || this.statusMessage;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import diagnosticsChannel from "node:diagnostics_channel";
|
|
2
|
+
// nodefony/src/context/requestTrace.ts
|
|
3
|
+
/**
|
|
4
|
+
* Nom du `TracingChannel` qui encadre chaque requête HTTP et chaque handshake
|
|
5
|
+
* WebSocket — forme `module.fonction` recommandée par la documentation de
|
|
6
|
+
* `node:diagnostics_channel` ; les sous-canaux sont
|
|
7
|
+
* `tracing:nodefony.http.request:start`, `:end`, `:asyncStart`, `:asyncEnd`,
|
|
8
|
+
* `:error`.
|
|
9
|
+
*/
|
|
10
|
+
const REQUEST_TRACE_CHANNEL = "nodefony.http.request";
|
|
11
|
+
const tracing = diagnosticsChannel.tracingChannel(REQUEST_TRACE_CHANNEL);
|
|
12
|
+
const startChannel = tracing.start;
|
|
13
|
+
const endChannel = tracing.end;
|
|
14
|
+
const asyncStartChannel = tracing.asyncStart;
|
|
15
|
+
const asyncEndChannel = tracing.asyncEnd;
|
|
16
|
+
const errorChannel = tracing.error;
|
|
17
|
+
/**
|
|
18
|
+
* Dit si quelqu'un écoute le canal — LA garde du chemin chaud : ~1,6 ns
|
|
19
|
+
* mesurées, aucune allocation. Lue une seule fois, au début de la requête :
|
|
20
|
+
* un abonné arrivé ensuite ne voit que les requêtes suivantes (règle des
|
|
21
|
+
* `TracingChannel`, qui interdit une fin sans début).
|
|
22
|
+
*
|
|
23
|
+
* @returns `true` si au moins un sous-canal a un abonné
|
|
24
|
+
*/
|
|
25
|
+
function hasRequestTraceSubscribers() {
|
|
26
|
+
return tracing.hasSubscribers;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Crée le message de la requête et le range sur son contexte. À n'appeler
|
|
30
|
+
* que sous {@link hasRequestTraceSubscribers} : c'est la seule allocation
|
|
31
|
+
* du canal, et elle n'a lieu que si quelqu'un écoute.
|
|
32
|
+
*
|
|
33
|
+
* @param context - le contexte qui vient d'être construit
|
|
34
|
+
* @param protocol - requête HTTP ou handshake WebSocket
|
|
35
|
+
* @returns le message, de forme fixe, dont les champs de résultat valent `null`
|
|
36
|
+
*/
|
|
37
|
+
function openRequestTrace(context, protocol) {
|
|
38
|
+
const trace = {
|
|
39
|
+
requestId: context.requestId,
|
|
40
|
+
protocol,
|
|
41
|
+
method: context.method,
|
|
42
|
+
scheme: context.scheme,
|
|
43
|
+
url: context.url,
|
|
44
|
+
route: null,
|
|
45
|
+
routePath: null,
|
|
46
|
+
module: null,
|
|
47
|
+
statusCode: null,
|
|
48
|
+
durationMs: null,
|
|
49
|
+
error: null
|
|
50
|
+
};
|
|
51
|
+
context.requestTrace = trace;
|
|
52
|
+
return trace;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Exécute la partie synchrone du traitement sous `start.runStores` puis
|
|
56
|
+
* publie `end` — la séquence de `TracingChannel.tracePromise`. Les stores
|
|
57
|
+
* liés au canal (`bindStore`, ce que fait un agent APM) restent actifs pour
|
|
58
|
+
* toute la suite asynchrone de la requête.
|
|
59
|
+
*
|
|
60
|
+
* @param trace - le message de la requête
|
|
61
|
+
* @param fn - la fonction qui sert la requête
|
|
62
|
+
* @param thisArg - le receveur de `fn`
|
|
63
|
+
* @param args - les arguments de `fn`
|
|
64
|
+
* @returns ce que rend `fn` — un contexte ou sa promesse
|
|
65
|
+
*/
|
|
66
|
+
function runTraced(trace, fn, thisArg, ...args) {
|
|
67
|
+
try {
|
|
68
|
+
return startChannel.runStores(trace, fn, thisArg, ...args);
|
|
69
|
+
} finally {
|
|
70
|
+
endChannel.publish(trace);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Termine la trace de la requête : remplit les champs de résultat puis
|
|
75
|
+
* publie `error` (s'il y a une exception), `asyncStart` et `asyncEnd`.
|
|
76
|
+
* Idempotent — le premier appel gagne, les suivants ne font rien : un
|
|
77
|
+
* handshake WebSocket a plusieurs sorties, une seule publie la fin.
|
|
78
|
+
*
|
|
79
|
+
* La durée est celle de l'horloge unique du contexte (`stopClock`), la même
|
|
80
|
+
* que celle de la ligne d'accès : le canal n'en mesure aucune.
|
|
81
|
+
*
|
|
82
|
+
* @param context - le contexte de la requête
|
|
83
|
+
* @param statusCode - le statut rendu au client, `null` s'il n'y en a pas eu
|
|
84
|
+
* @param error - l'exception attrapée par le pipeline, `null` ou `undefined` sinon
|
|
85
|
+
*/
|
|
86
|
+
function closeRequestTrace(context, statusCode, error) {
|
|
87
|
+
const trace = context.requestTrace;
|
|
88
|
+
if (trace === null) return;
|
|
89
|
+
context.requestTrace = null;
|
|
90
|
+
const route = context.resolver?.route ?? null;
|
|
91
|
+
trace.route = route?.name ?? null;
|
|
92
|
+
const path = route?.path;
|
|
93
|
+
trace.routePath = path === void 0 ? null : path === "" ? "/" : path;
|
|
94
|
+
trace.module = context.logModule ?? route?.module?.name ?? null;
|
|
95
|
+
trace.statusCode = statusCode;
|
|
96
|
+
trace.durationMs = context.stopClock();
|
|
97
|
+
if (error !== null && error !== void 0) {
|
|
98
|
+
trace.error = error;
|
|
99
|
+
errorChannel.publish(trace);
|
|
100
|
+
}
|
|
101
|
+
asyncStartChannel.publish(trace);
|
|
102
|
+
asyncEndChannel.publish(trace);
|
|
103
|
+
}
|
|
104
|
+
export { REQUEST_TRACE_CHANNEL, closeRequestTrace, hasRequestTraceSubscribers, openRequestTrace, runTraced };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// nodefony/src/context/timingMode.ts
|
|
2
|
+
/**
|
|
3
|
+
* Résout le mode de chronométrage d'une requête depuis `timing.enabled` et
|
|
4
|
+
* l'environnement — la SEULE règle, appelée par le constructeur du `Context`.
|
|
5
|
+
*
|
|
6
|
+
* Défaut (`null`, pas d'avis) : la durée partout, les phases hors production.
|
|
7
|
+
* La durée coûte deux lectures d'horloge, l'équivalent de quelques pourcents
|
|
8
|
+
* de la ligne d'accès qu'elle complète ; les phases coûtent une lecture par
|
|
9
|
+
* phase, un tableau et un index par requête — réservées au développement, ou
|
|
10
|
+
* à un diagnostic à chaud (`true`). `false` coupe tout, durée comprise.
|
|
11
|
+
*
|
|
12
|
+
* @param enabled - valeur de `timing.enabled` (`true`, `false`, `null`)
|
|
13
|
+
* @param environment - environnement normalisé du noyau (`"production"`…)
|
|
14
|
+
* @returns le mode de la requête — une chaîne littérale, rien d'alloué
|
|
15
|
+
*/
|
|
16
|
+
function resolveTimingMode(enabled, environment) {
|
|
17
|
+
if (enabled === false) return "off";
|
|
18
|
+
if (enabled === true) return "phases";
|
|
19
|
+
return environment === "production" ? "clock" : "phases";
|
|
20
|
+
}
|
|
21
|
+
export { resolveTimingMode };
|
|
@@ -42,4 +42,23 @@ function alpnProtocolOf(request) {
|
|
|
42
42
|
function requestAuthority(headers) {
|
|
43
43
|
return headers[":authority"] ?? headers.host;
|
|
44
44
|
}
|
|
45
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Une requête HTTP/2 nomme-t-elle deux hôtes — `:authority` ET un `Host`
|
|
47
|
+
* différent ? RFC 9113 §8.3.1 : « SHOULD treat a request as malformed ». Le
|
|
48
|
+
* serveur lit `:authority` ({@link requestAuthority}), mais un code qui lit
|
|
49
|
+
* `headers.host` verrait un autre hôte que celui que le routage et le
|
|
50
|
+
* pare-feu ont jugé.
|
|
51
|
+
*
|
|
52
|
+
* Chemin de chaque requête : une lecture d'en-tête ; la comparaison n'a lieu
|
|
53
|
+
* que si les deux sont présents (un navigateur n'envoie pas `Host` en HTTP/2).
|
|
54
|
+
*
|
|
55
|
+
* @param headers - en-têtes de la requête Node.
|
|
56
|
+
* @returns `true` quand les deux autorités diffèrent (casse ignorée).
|
|
57
|
+
*/
|
|
58
|
+
function hasConflictingAuthority(headers) {
|
|
59
|
+
const authority = headers[":authority"];
|
|
60
|
+
if (authority === void 0) return false;
|
|
61
|
+
const host = headers.host;
|
|
62
|
+
return host !== void 0 && host.toLowerCase() !== authority.toLowerCase();
|
|
63
|
+
}
|
|
64
|
+
export { alpnProtocolOf, hasConflictingAuthority, protocolNameOf, requestAuthority };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// nodefony/src/context/unsentResponse.ts
|
|
2
|
+
/**
|
|
3
|
+
* Rédige l'avertissement de développement d'une requête restée sans réponse :
|
|
4
|
+
* l'action a rendu une valeur que le framework ne sait pas envoyer, et la
|
|
5
|
+
* requête a attendu son délai d'expiration.
|
|
6
|
+
*
|
|
7
|
+
* Le message nomme l'action fautive, ce qu'elle a rendu, et le geste qui
|
|
8
|
+
* convient à ce cas — `null` est presque toujours une entité absente, qui
|
|
9
|
+
* appelle un 404.
|
|
10
|
+
*
|
|
11
|
+
* @param resolver - le resolver de la requête (route, controller, action,
|
|
12
|
+
* valeur non rendue), `null` si aucune route n'a été résolue.
|
|
13
|
+
* @param url - l'URL de la requête, repli quand aucune route n'est connue.
|
|
14
|
+
* @returns le texte de l'avertissement.
|
|
15
|
+
*/
|
|
16
|
+
function unsentResponseWarning(resolver, url) {
|
|
17
|
+
const controller = resolver?.controller?.name;
|
|
18
|
+
const actionName = resolver?.actionName;
|
|
19
|
+
const action = controller && actionName ? `${controller}.${actionName}` : null;
|
|
20
|
+
const route = resolver?.route?.name ?? null;
|
|
21
|
+
const where = action ? `L'action ${action}${route ? ` (route ${route})` : ""}` : `L'action de ${route ?? url}`;
|
|
22
|
+
const returned = resolver?.unrendered ?? null;
|
|
23
|
+
const consequence = "et n'a rien envoyé : la requête a attendu son délai d'expiration.";
|
|
24
|
+
if (returned === "null") return `${where} a rendu null ${consequence} Pour le framework, null veut dire « l'action répond elle-même ». Entité absente → lever HttpError(…, 404) ; sinon rendre la valeur, ou envoyer la réponse soi-même.`;
|
|
25
|
+
if (returned === "undefined") return `${where} n'a rien rendu ${consequence} Un « return » oublié ? Rendre la valeur (objet, chaîne, nombre, booléen, Buffer : envoyés automatiquement), ou envoyer la réponse soi-même.`;
|
|
26
|
+
if (returned?.startsWith("une instance de")) return `${where} a rendu ${returned}, qui ne s'envoie pas telle quelle, ${consequence} Rendre un objet simple (sa forme JSON), ou envoyer la réponse soi-même.`;
|
|
27
|
+
if (returned !== null) return `${where} a rendu une valeur de type ${returned} ${consequence} Rendre un objet, une chaîne, un nombre, un booléen ou un Buffer, ou envoyer la réponse soi-même.`;
|
|
28
|
+
return `${where} n'a rien envoyé : la requête a attendu son délai d'expiration. Rendre la valeur, ou envoyer la réponse soi-même.`;
|
|
29
|
+
}
|
|
30
|
+
export { unsentResponseWarning };
|
|
@@ -215,8 +215,10 @@ var WebsocketResponse = class {
|
|
|
215
215
|
return true;
|
|
216
216
|
}
|
|
217
217
|
addCookie(cookie) {
|
|
218
|
-
if (cookie instanceof Cookie)
|
|
219
|
-
|
|
218
|
+
if (cookie instanceof Cookie) {
|
|
219
|
+
cookie.assertGrammar();
|
|
220
|
+
this.cookies[cookie.name] = cookie;
|
|
221
|
+
} else throw new Error("Response addCookies not valid cookies");
|
|
220
222
|
}
|
|
221
223
|
setCookies() {}
|
|
222
224
|
setCookie(_cookie) {}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import { hasForwardingHeaders, resolveForwarded } from "../forwarded.js";
|
|
2
1
|
import Context from "../Context.js";
|
|
3
|
-
import { toWsCloseReason } from "./wsCloseReason.js";
|
|
4
2
|
import WebsocketResponse from "./Response.js";
|
|
3
|
+
import { toWsCloseReason } from "./wsCloseReason.js";
|
|
5
4
|
import { sanitizeRequestId } from "../requestId.js";
|
|
6
5
|
import { FrameProfile } from "../../profiler/FrameProfile.js";
|
|
7
|
-
import { formatWsLogContent } from "./wsLogContent.js";
|
|
6
|
+
import { WS_LOG_CONTENT_CAP, formatWsLogContent } from "./wsLogContent.js";
|
|
7
|
+
import { admitFrame, readFrameRateBound, readFrameRateCloseAfter } from "./wsFrameRate.js";
|
|
8
|
+
import { closeRequestTrace } from "../requestTrace.js";
|
|
9
|
+
import { hasForwardingHeaders, resolveForwarded } from "../forwarded.js";
|
|
8
10
|
import { parseWebsocketUrl } from "./websocketUrl.js";
|
|
9
|
-
import { RequestContext, logColor } from "nodefony";
|
|
11
|
+
import { IDENTITY_DEAD_CLOSE_CODE, NOT_FOUND_CLOSE_CODE, ORIGIN_REFUSED_CLOSE_CODE, RequestContext, frameRequestId, logColor } from "nodefony";
|
|
10
12
|
import { AsyncResource } from "node:async_hooks";
|
|
11
13
|
import url, { URL } from "node:url";
|
|
12
14
|
import Ws from "ws";
|
|
@@ -50,7 +52,7 @@ function toWsCloseCode(code) {
|
|
|
50
52
|
if (code === 1e3 || code >= 1001 && code <= 1003 || code >= 1007 && code <= 1011 || code >= 3e3 && code <= 4999) return code;
|
|
51
53
|
if (code >= 500 && code < 600) return 1011;
|
|
52
54
|
if (code === 401 || code === 403 || code === 421) return 1008;
|
|
53
|
-
if (code >= 400 && code < 500) return
|
|
55
|
+
if (code >= 400 && code < 500) return NOT_FOUND_CLOSE_CODE;
|
|
54
56
|
return 1011;
|
|
55
57
|
}
|
|
56
58
|
/**
|
|
@@ -71,11 +73,11 @@ function toHandshakeStatus(code) {
|
|
|
71
73
|
case 1003:
|
|
72
74
|
case 1007: return 400;
|
|
73
75
|
case 1008:
|
|
74
|
-
case
|
|
75
|
-
case
|
|
76
|
+
case ORIGIN_REFUSED_CLOSE_CODE: return 403;
|
|
77
|
+
case IDENTITY_DEAD_CLOSE_CODE: return 401;
|
|
76
78
|
case 1009: return 413;
|
|
77
79
|
case 1013: return 503;
|
|
78
|
-
case
|
|
80
|
+
case NOT_FOUND_CLOSE_CODE: return 404;
|
|
79
81
|
default: return 500;
|
|
80
82
|
}
|
|
81
83
|
}
|
|
@@ -109,11 +111,23 @@ var WebsocketContext = class extends Context {
|
|
|
109
111
|
request;
|
|
110
112
|
response = null;
|
|
111
113
|
acceptedProtocol;
|
|
114
|
+
/**
|
|
115
|
+
* Codec des trames BINAIRES de cette connexion, posé par la couche qui l'a négocié
|
|
116
|
+
* (le temps réel, ADR-0022 §2). Hors production, le traceur s'en sert pour
|
|
117
|
+
* afficher une trame binaire décodée plutôt que sa seule taille ; jamais lu en
|
|
118
|
+
* production. `null` = octets bruts, résumés à leur taille.
|
|
119
|
+
*/
|
|
120
|
+
frameCodec = null;
|
|
112
121
|
port;
|
|
113
122
|
rejected = false;
|
|
114
123
|
teardownWired = false;
|
|
115
124
|
connection = null;
|
|
116
125
|
/**
|
|
126
|
+
* Fenêtre de la borne de débit entrante — `null` tant que la borne est
|
|
127
|
+
* désactivée ou qu'aucune trame n'est arrivée (créée au premier usage).
|
|
128
|
+
*/
|
|
129
|
+
frameRate = null;
|
|
130
|
+
/**
|
|
117
131
|
* Le verdict du handshake encore à rendre : posé par le noyau tant que le
|
|
118
132
|
* pipeline décide (pare-feu, garde de l'action), `null` dès que la socket
|
|
119
133
|
* est acceptée ou refusée. Tant qu'il est là, AUCUN `101` n'est parti.
|
|
@@ -173,16 +187,16 @@ var WebsocketContext = class extends Context {
|
|
|
173
187
|
this.log(`PROXY WEBSOCKET REQUEST ${fwd.fromStandard ? "Forwarded (RFC 7239)" : "x-forwarded"} VIA : ${this.proxy.proxyVia}`, "DEBUG");
|
|
174
188
|
}
|
|
175
189
|
}
|
|
176
|
-
log(pci, severity, msgid, msg) {
|
|
190
|
+
log(pci, severity, msgid, msg, moduleName) {
|
|
177
191
|
if (!msgid) msgid = "WEBSOCKET CONTEXT";
|
|
178
|
-
return super.log(pci, severity, msgid, msg);
|
|
192
|
+
return super.log(pci, severity, msgid, msg, moduleName);
|
|
179
193
|
}
|
|
180
194
|
logRequest(httpError, acceptedProtocol) {
|
|
181
195
|
try {
|
|
182
196
|
const logger = this.httpKernel?.getRequestLogger();
|
|
183
197
|
if (!logger) return;
|
|
184
198
|
const entry = logger.renderWebsocket(this, httpError ?? null, acceptedProtocol ?? null);
|
|
185
|
-
const pdu = this.log(entry.text, entry.severity, entry.msgid);
|
|
199
|
+
const pdu = this.log(entry.text, entry.severity, entry.msgid, void 0, this.logModule ?? this.resolver?.route?.module?.name);
|
|
186
200
|
pdu.requestId ??= this.requestId;
|
|
187
201
|
return pdu;
|
|
188
202
|
} catch {
|
|
@@ -215,6 +229,7 @@ var WebsocketContext = class extends Context {
|
|
|
215
229
|
this.rejected = true;
|
|
216
230
|
this.webSocketState = "error";
|
|
217
231
|
admission.refuse(status, this.response?.handshakeHeaders ?? null);
|
|
232
|
+
if (this.requestTrace !== null) closeRequestTrace(this, status, this.error);
|
|
218
233
|
return true;
|
|
219
234
|
}
|
|
220
235
|
async connect() {
|
|
@@ -228,6 +243,7 @@ var WebsocketContext = class extends Context {
|
|
|
228
243
|
this.requestEnded = true;
|
|
229
244
|
this.connection.on("message", AsyncResource.bind(this.handleMessage.bind(this)));
|
|
230
245
|
this.logRequest(null, this.acceptedProtocol ?? null);
|
|
246
|
+
if (this.requestTrace !== null) closeRequestTrace(this, 101, null);
|
|
231
247
|
this.webSocketState = "connected";
|
|
232
248
|
return this.connection;
|
|
233
249
|
}
|
|
@@ -268,7 +284,7 @@ var WebsocketContext = class extends Context {
|
|
|
268
284
|
}
|
|
269
285
|
});
|
|
270
286
|
} else if (!this.isRejected()) {
|
|
271
|
-
this.reject(
|
|
287
|
+
this.reject(NOT_FOUND_CLOSE_CODE, "Not Found");
|
|
272
288
|
this.rejected = true;
|
|
273
289
|
this.webSocketState = "error";
|
|
274
290
|
}
|
|
@@ -308,6 +324,28 @@ var WebsocketContext = class extends Context {
|
|
|
308
324
|
return null;
|
|
309
325
|
}
|
|
310
326
|
/**
|
|
327
|
+
* Le contenu des messages est-il journalisé ? Hors production seulement —
|
|
328
|
+
* décidé une fois pour le processus (P8 : runtime ∈ {development, production},
|
|
329
|
+
* `resolveRuntimeEnv`).
|
|
330
|
+
*/
|
|
331
|
+
tracesMessageContent() {
|
|
332
|
+
wsContentLogging ??= this.kernel?.environment !== "production";
|
|
333
|
+
return wsContentLogging;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Traceur des trames SORTANTES écrites hors de {@link send} — le temps réel
|
|
337
|
+
* écrit droit sur la socket. À demander UNE fois par connexion : `null` en
|
|
338
|
+
* production, donc l'appelant choisit son chemin d'écriture à l'ouverture et
|
|
339
|
+
* ne paie rien par trame.
|
|
340
|
+
*
|
|
341
|
+
* @returns une fonction qui journalise une trame comme `send` (`WS SEND`),
|
|
342
|
+
* ou `null` quand le contenu des messages n'est pas journalisé.
|
|
343
|
+
*/
|
|
344
|
+
outboundTracer() {
|
|
345
|
+
if (!this.tracesMessageContent()) return null;
|
|
346
|
+
return (data) => this.logMessageContent("SEND", data);
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
311
349
|
* Logge le CONTENU d'un message WS (corrélé `requestId` via l'override `log`),
|
|
312
350
|
* gaté hors prod et borné — le Suivi de requête (Studio) le surface alors par
|
|
313
351
|
* direction. Hot path : le gate booléen court-circuite en prod AVANT toute
|
|
@@ -317,9 +355,8 @@ var WebsocketContext = class extends Context {
|
|
|
317
355
|
* @param data - charge utile (string, Buffer binaire, objet, ou null).
|
|
318
356
|
*/
|
|
319
357
|
logMessageContent(dir, data) {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
this.log(formatWsLogContent(data), "DEBUG", `WS ${dir}`);
|
|
358
|
+
if (!this.tracesMessageContent()) return;
|
|
359
|
+
this.log(formatWsLogContent(data, WS_LOG_CONTENT_CAP, this.frameCodec), "DEBUG", `WS ${dir}`);
|
|
323
360
|
}
|
|
324
361
|
/**
|
|
325
362
|
* Ouvre le profil d'**une invocation** du pont RPC (une frame `api.request`).
|
|
@@ -337,10 +374,11 @@ var WebsocketContext = class extends Context {
|
|
|
337
374
|
* zéro allocation dans ce cas.
|
|
338
375
|
*/
|
|
339
376
|
beginFrame(method, framePath) {
|
|
340
|
-
|
|
377
|
+
const profiled = this.profiling && this.httpKernel?.shouldProfile(framePath) === true;
|
|
378
|
+
if (!profiled && !this.timingEnabled) return null;
|
|
341
379
|
this.#frameSeq += 1;
|
|
342
380
|
return new FrameProfile({
|
|
343
|
-
requestId:
|
|
381
|
+
requestId: frameRequestId(this.requestId, this.#frameSeq),
|
|
344
382
|
type: this.type,
|
|
345
383
|
scheme: this.scheme,
|
|
346
384
|
method,
|
|
@@ -350,7 +388,7 @@ var WebsocketContext = class extends Context {
|
|
|
350
388
|
security: this.security ?? null,
|
|
351
389
|
securityTrace: this.securityTrace,
|
|
352
390
|
timing: this.timingEnabled,
|
|
353
|
-
queries:
|
|
391
|
+
queries: profiled
|
|
354
392
|
});
|
|
355
393
|
}
|
|
356
394
|
/**
|
|
@@ -358,7 +396,7 @@ var WebsocketContext = class extends Context {
|
|
|
358
396
|
* Profiler (no-op hors dev, ou si aucun profil n'a été ouvert).
|
|
359
397
|
*/
|
|
360
398
|
collectFrame(frame) {
|
|
361
|
-
if (frame === null || !
|
|
399
|
+
if (frame === null || !frame.profiled) return;
|
|
362
400
|
this.get("profiler")?.collect(frame);
|
|
363
401
|
}
|
|
364
402
|
/**
|
|
@@ -386,7 +424,30 @@ var WebsocketContext = class extends Context {
|
|
|
386
424
|
}
|
|
387
425
|
super.phaseEnd(name);
|
|
388
426
|
}
|
|
427
|
+
/**
|
|
428
|
+
* Applique la borne de débit entrante à une trame : `true` = l'admettre.
|
|
429
|
+
*
|
|
430
|
+
* Écartée → comptée (`frameRateRejectedTotal`) ; solde de refus atteint →
|
|
431
|
+
* connexion fermée en `1008` (RFC 6455 §7.4.1, violation de politique).
|
|
432
|
+
*
|
|
433
|
+
* @param bound - trames admises par seconde (`> 0`)
|
|
434
|
+
* @returns `true` si la trame poursuit son chemin
|
|
435
|
+
*/
|
|
436
|
+
admitInboundFrame(bound) {
|
|
437
|
+
const now = Date.now();
|
|
438
|
+
this.frameRate ??= {
|
|
439
|
+
start: now,
|
|
440
|
+
count: 0,
|
|
441
|
+
balance: 0
|
|
442
|
+
};
|
|
443
|
+
const verdict = admitFrame(this.frameRate, now, bound, readFrameRateCloseAfter(this.server));
|
|
444
|
+
if (verdict === "send") return true;
|
|
445
|
+
if (verdict === "close") this.connection?.close(1008, "frame rate");
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
389
448
|
async handleMessage(data, isBinary) {
|
|
449
|
+
const bound = readFrameRateBound(this.server);
|
|
450
|
+
if (bound > 0 && !this.admitInboundFrame(bound)) return void 0;
|
|
390
451
|
this.webSocketState = "message";
|
|
391
452
|
const message = isBinary ? data : data.toString();
|
|
392
453
|
this.logMessageContent("RECEIVE", message);
|
|
@@ -402,7 +463,7 @@ var WebsocketContext = class extends Context {
|
|
|
402
463
|
} } });
|
|
403
464
|
return callAsPromise(this.resolver, [message]);
|
|
404
465
|
} else if (!this.rejected) {
|
|
405
|
-
this.reject(
|
|
466
|
+
this.reject(NOT_FOUND_CLOSE_CODE, "Not Found");
|
|
406
467
|
this.rejected = true;
|
|
407
468
|
}
|
|
408
469
|
return;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { splitAbsoluteForm } from "../http/urlFastPath.js";
|
|
1
2
|
// nodefony/src/context/websocket/websocketUrl.ts
|
|
2
3
|
/**
|
|
3
4
|
* URL d'une requête d'upgrade WebSocket, telle que le pipeline la lit.
|
|
@@ -8,6 +9,10 @@
|
|
|
8
9
|
* un hôte de l'autre — et une route acceptée par l'une serait refusée par
|
|
9
10
|
* l'autre.
|
|
10
11
|
*
|
|
12
|
+
* Une cible en forme absolue (`http://hôte/chemin`, RFC 6455 §4.2.1) donne son
|
|
13
|
+
* hôte, et l'en-tête `Host` est ignoré (RFC 9112 §3.2.2) — la même règle que
|
|
14
|
+
* `HttpRequest.getHost`.
|
|
15
|
+
*
|
|
11
16
|
* @param scheme - `ws` ou `wss`
|
|
12
17
|
* @param host - en-tête `Host` de la requête (`localhost` s'il manque)
|
|
13
18
|
* @param rawUrl - cible brute de la requête (`/` si elle manque)
|
|
@@ -15,6 +20,8 @@
|
|
|
15
20
|
* @throws TypeError si l'hôte ou la cible ne forment pas une URL
|
|
16
21
|
*/
|
|
17
22
|
function parseWebsocketUrl(scheme, host, rawUrl) {
|
|
23
|
+
const absolute = splitAbsoluteForm(rawUrl);
|
|
24
|
+
if (absolute !== null) return new URL(`${scheme}://${absolute.authority}${absolute.target}`);
|
|
18
25
|
return new URL(`${scheme}://${host ?? "localhost"}${rawUrl ?? "/"}`);
|
|
19
26
|
}
|
|
20
27
|
export { parseWebsocketUrl };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { TRY_AGAIN_LATER_CLOSE_CODE, decideBackpressure, nextDropBalance } from "nodefony";
|
|
1
2
|
// nodefony/src/context/websocket/wsBackpressure.ts
|
|
2
3
|
/**
|
|
3
4
|
* Relit le seuil + la politique depuis les options du `WebSocketServer`.
|
|
@@ -29,27 +30,26 @@ const readBackpressureOptions = (wss) => {
|
|
|
29
30
|
* « Try Again Later » → le client peut back-off + reconnecter). NE FAIT PAS le `send()`
|
|
30
31
|
* — le caller émet uniquement si le retour vaut `"send"`.
|
|
31
32
|
*
|
|
33
|
+
* La DÉCISION est celle du cœur (`decideBackpressure` / `nextDropBalance`, partagée
|
|
34
|
+
* avec `NodefonySocket`) ; cette fonction n'en porte que l'état (compteurs sur la
|
|
35
|
+
* socket) et le geste serveur (fermeture `1013`).
|
|
36
|
+
*
|
|
32
37
|
* @param ws - la socket destinataire
|
|
33
38
|
* @param max - seuil d'octets (`<= 0` → désactivé)
|
|
34
39
|
* @param policy - action au dépassement
|
|
40
|
+
* @param closeAfterDrops - solde de refus qui ferme (`0` = jamais)
|
|
35
41
|
* @returns `"send"` (émettre) · `"drop"` (sauter cette frame) · `"close"` (socket fermée)
|
|
36
42
|
*/
|
|
37
43
|
const decideSend = (ws, max, policy, closeAfterDrops = 0) => {
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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";
|
|
44
|
+
const balance = ws._nfDropStreak ?? 0;
|
|
45
|
+
const verdict = decideBackpressure(ws.bufferedAmount ?? 0, max, policy, closeAfterDrops, balance);
|
|
46
|
+
if (verdict === "send") {
|
|
47
|
+
if (balance > 0) ws._nfDropStreak = balance - 1;
|
|
48
|
+
return verdict;
|
|
52
49
|
}
|
|
53
|
-
|
|
50
|
+
ws._nfDrops = (ws._nfDrops ?? 0) + 1;
|
|
51
|
+
ws._nfDropStreak = nextDropBalance(verdict, balance);
|
|
52
|
+
if (verdict === "close") ws.close(TRY_AGAIN_LATER_CLOSE_CODE, "backpressure");
|
|
53
|
+
return verdict;
|
|
54
54
|
};
|
|
55
55
|
export { decideSend, readBackpressureOptions };
|