@nodefony/http 10.0.0-beta.5 → 10.0.0-beta.7
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.158.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorateParam.js +1 -1
- package/dist/index.js +30 -26
- package/dist/nodefony/command/proxyGenerateCommand.js +2 -2
- package/dist/nodefony/config/config.js +29 -4
- package/dist/nodefony/grpc/GrpcCall.js +298 -0
- package/dist/nodefony/grpc/GrpcEndpoint.js +528 -0
- package/dist/nodefony/grpc/index.js +4 -0
- package/dist/nodefony/interfaces/IRpcCall.js +1 -0
- package/dist/nodefony/service/ProfilerAdminApi.js +5 -2
- package/dist/nodefony/service/audit-logger.js +52 -6
- package/dist/nodefony/service/error-renderer.js +47 -13
- package/dist/nodefony/service/http-kernel.js +306 -49
- package/dist/nodefony/service/pretty-request-logger.js +5 -3
- package/dist/nodefony/service/request-logger.js +21 -6
- package/dist/nodefony/service/reverse-proxy.js +8 -3
- package/dist/nodefony/service/servers/clientError.js +53 -4
- package/dist/nodefony/service/servers/server-h2c.js +6 -6
- package/dist/nodefony/service/servers/server-http.js +6 -4
- package/dist/nodefony/service/servers/server-https.js +14 -7
- package/dist/nodefony/service/servers/server-static.js +1 -6
- package/dist/nodefony/service/servers/server-websocket-secure.js +7 -11
- package/dist/nodefony/service/servers/server-websocket.js +7 -11
- package/dist/nodefony/service/servers/serverShutdown.js +43 -6
- package/dist/nodefony/service/sessions/sessions-service.js +7 -7
- 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 +12 -1
- package/dist/nodefony/src/context/http/ByteStream.js +364 -0
- package/dist/nodefony/src/context/http/HttpContext.js +139 -32
- package/dist/nodefony/src/context/http/Request.js +111 -22
- package/dist/nodefony/src/context/http/Response.js +459 -37
- package/dist/nodefony/src/context/http/SseStream.js +83 -148
- package/dist/nodefony/src/context/http/StreamLifecycle.js +582 -0
- package/dist/nodefony/src/context/http/parser.js +29 -11
- package/dist/nodefony/src/context/http/urlFastPath.js +80 -1
- package/dist/nodefony/src/context/http/webResponse.js +56 -0
- package/dist/nodefony/src/context/http2/Request.js +1 -1
- package/dist/nodefony/src/context/http2/Response.js +55 -12
- package/dist/nodefony/src/context/requestTrace.js +6 -2
- package/dist/nodefony/src/context/rpcOutcome.js +20 -0
- package/dist/nodefony/src/context/transport.js +20 -1
- package/dist/nodefony/src/context/websocket/Response.js +15 -3
- package/dist/nodefony/src/context/websocket/WebsocketContext.js +86 -18
- 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 +57 -14
- package/dist/nodefony/src/errors/httpError.js +58 -25
- package/dist/nodefony/src/grpc/cors.js +59 -0
- package/dist/nodefony/src/grpc/limits.js +16 -0
- package/dist/nodefony/src/grpc/refusal.js +271 -0
- package/dist/nodefony/src/profiler/Profiler.js +14 -1
- package/dist/nodefony/src/profiler/aggregate.js +2 -2
- package/dist/nodefony/src/proxy/forward.js +109 -9
- package/dist/nodefony/src/proxy/generateProxyConfig.js +97 -17
- package/dist/nodefony/src/proxy/rules.js +29 -14
- package/dist/nodefony/src/servers/http2Goaway.js +76 -0
- package/dist/nodefony/src/servers/http2Limits.js +41 -15
- package/dist/nodefony/src/servers/http2StreamGrace.js +100 -0
- package/dist/nodefony/src/servers/receiveDeadline.js +177 -0
- package/dist/nodefony/src/servers/upgradeDispatch.js +62 -4
- package/dist/types/index.d.ts +11 -2
- package/dist/types/nodefony/config/config.d.ts +14 -0
- package/dist/types/nodefony/grpc/GrpcCall.d.ts +142 -0
- package/dist/types/nodefony/grpc/GrpcEndpoint.d.ts +98 -0
- package/dist/types/nodefony/grpc/index.d.ts +15 -0
- package/dist/types/nodefony/interfaces/IContext.d.ts +9 -0
- package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +14 -0
- package/dist/types/nodefony/interfaces/IRequest.d.ts +2 -1
- package/dist/types/nodefony/interfaces/IRequestTrace.d.ts +10 -0
- package/dist/types/nodefony/interfaces/IRouting.d.ts +19 -0
- package/dist/types/nodefony/interfaces/IRpcCall.d.ts +83 -0
- package/dist/types/nodefony/interfaces/ISecurity.d.ts +27 -2
- package/dist/types/nodefony/interfaces/IUpload.d.ts +51 -4
- package/dist/types/nodefony/interfaces/index.d.ts +2 -1
- package/dist/types/nodefony/service/ProfilerAdminApi.d.ts +2 -1
- package/dist/types/nodefony/service/audit-logger.d.ts +33 -3
- package/dist/types/nodefony/service/error-renderer.d.ts +27 -5
- package/dist/types/nodefony/service/http-kernel.d.ts +36 -3
- package/dist/types/nodefony/service/request-logger.d.ts +10 -0
- package/dist/types/nodefony/service/servers/clientError.d.ts +3 -2
- package/dist/types/nodefony/service/servers/serverShutdown.d.ts +7 -5
- package/dist/types/nodefony/src/assets/prebuiltUi.d.ts +9 -4
- package/dist/types/nodefony/src/context/Context.d.ts +9 -1
- package/dist/types/nodefony/src/context/http/ByteStream.d.ts +109 -0
- package/dist/types/nodefony/src/context/http/HttpContext.d.ts +99 -3
- package/dist/types/nodefony/src/context/http/Request.d.ts +46 -9
- package/dist/types/nodefony/src/context/http/Response.d.ts +188 -8
- package/dist/types/nodefony/src/context/http/SseStream.d.ts +33 -8
- package/dist/types/nodefony/src/context/http/StreamLifecycle.d.ts +106 -0
- package/dist/types/nodefony/src/context/http/parser.d.ts +17 -3
- package/dist/types/nodefony/src/context/http/urlFastPath.d.ts +40 -0
- package/dist/types/nodefony/src/context/http/webResponse.d.ts +31 -0
- package/dist/types/nodefony/src/context/http2/Response.d.ts +40 -5
- package/dist/types/nodefony/src/context/requestTrace.d.ts +3 -1
- package/dist/types/nodefony/src/context/rpcOutcome.d.ts +11 -0
- package/dist/types/nodefony/src/context/transport.d.ts +17 -0
- package/dist/types/nodefony/src/context/websocket/Response.d.ts +6 -1
- package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +48 -0
- 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/errors/httpError.d.ts +29 -4
- package/dist/types/nodefony/src/grpc/cors.d.ts +32 -0
- package/dist/types/nodefony/src/grpc/limits.d.ts +14 -0
- package/dist/types/nodefony/src/grpc/refusal.d.ts +136 -0
- package/dist/types/nodefony/src/profiler/Profiler.d.ts +11 -1
- package/dist/types/nodefony/src/proxy/forward.d.ts +37 -2
- package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +15 -2
- package/dist/types/nodefony/src/proxy/rules.d.ts +20 -0
- package/dist/types/nodefony/src/servers/http2Goaway.d.ts +41 -0
- package/dist/types/nodefony/src/servers/http2Limits.d.ts +27 -14
- package/dist/types/nodefony/src/servers/http2StreamGrace.d.ts +34 -0
- package/dist/types/nodefony/src/servers/receiveDeadline.d.ts +77 -0
- package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +41 -1
- package/docs/byte-stream.md +406 -0
- package/docs/cookies.md +13 -3
- package/docs/diagnostics-channel.md +45 -1
- package/docs/index.md +8 -0
- package/docs/reverse-proxy.md +15 -3
- package/docs/servers.md +67 -43
- package/docs/session.md +3 -3
- package/docs/sse.md +27 -20
- package/docs/upload.md +43 -17
- package/package.json +38 -18
|
@@ -1,21 +1,12 @@
|
|
|
1
|
-
import
|
|
2
|
-
import Http2Response from "../http2/Response.js";
|
|
3
|
-
import { RequestContext } from "nodefony";
|
|
1
|
+
import { MAX_TIMER_MS, STREAM_STALL_TIMEOUT_MS, checkStreamTimer, openStreamLifecycle } from "./StreamLifecycle.js";
|
|
4
2
|
import http from "node:http";
|
|
5
|
-
import { AsyncResource } from "node:async_hooks";
|
|
6
3
|
// nodefony/src/context/http/SseStream.ts
|
|
7
4
|
/** Battement de cœur par défaut, en ms (§9.2.7 : « every 15 seconds or so »). */
|
|
8
5
|
const SSE_HEARTBEAT_MS = 15e3;
|
|
9
|
-
/** Délai de blocage d'écriture par défaut, en ms. */
|
|
10
|
-
const SSE_STALL_TIMEOUT_MS =
|
|
6
|
+
/** Délai de blocage d'écriture par défaut, en ms — celui de tout flux. */
|
|
7
|
+
const SSE_STALL_TIMEOUT_MS = STREAM_STALL_TIMEOUT_MS;
|
|
11
8
|
/** Taille maximale d'un événement par défaut, en octets (1 Mio). */
|
|
12
9
|
const SSE_MAX_EVENT_BYTES = 1048576;
|
|
13
|
-
/**
|
|
14
|
-
* Plus long délai qu'un minuteur sait tenir (2^31-1 ms). Au-delà, Node le
|
|
15
|
-
* ramène à 1 ms : un `retry` qui le dépasse ferait reconnecter le client en
|
|
16
|
-
* rafale, un battement qui le dépasse battrait mille fois par seconde.
|
|
17
|
-
*/
|
|
18
|
-
const MAX_TIMER_MS = 2147483647;
|
|
19
10
|
/** Commentaire de battement de cœur — ignoré par tout client conforme. */
|
|
20
11
|
const HEARTBEAT = ":\n\n";
|
|
21
12
|
/** Fin de ligne : CRLF, CR ou LF (§9.2.5). */
|
|
@@ -38,21 +29,19 @@ function singleLine(name, value) {
|
|
|
38
29
|
*
|
|
39
30
|
* `JSON.stringify` rend `undefined` pour `undefined`, une fonction ou un
|
|
40
31
|
* symbole, et son type `string` le tait : on l'élargit avant de le lire.
|
|
32
|
+
*
|
|
33
|
+
* Des octets sont refusés : un flux SSE est du texte UTF-8 (WHATWG HTML
|
|
34
|
+
* §9.2), et `JSON.stringify` les rendrait en objet (`[1,2,255]` →
|
|
35
|
+
* `{"0":1,"1":2,"2":255}`) sans erreur (ADR-0021 S8).
|
|
36
|
+
*
|
|
37
|
+
* @throws TypeError quand `data` est un `ArrayBuffer` ou une vue sur octets.
|
|
41
38
|
*/
|
|
42
39
|
function serialize(data) {
|
|
43
40
|
if (typeof data === "string") return data;
|
|
41
|
+
if (ArrayBuffer.isView(data) || data instanceof ArrayBuffer) throw new TypeError("SSE : un flux SSE ne porte que du texte UTF-8, des octets n'y passent pas — les encoder en base64 dans la charge, avec leur type MIME (ex. { mimeType: \"image/png\", data: \"<base64>\" }).");
|
|
44
42
|
const json = JSON.stringify(data);
|
|
45
43
|
return typeof json === "string" ? json : "";
|
|
46
44
|
}
|
|
47
|
-
/**
|
|
48
|
-
* Refuse une option de délai hors de ce qu'un minuteur sait tenir.
|
|
49
|
-
*
|
|
50
|
-
* @throws RangeError quand la valeur n'est ni `false` ni un entier entre 1 et 2^31-1.
|
|
51
|
-
*/
|
|
52
|
-
function checkTimer(name, value) {
|
|
53
|
-
if (value === void 0 || value === false) return;
|
|
54
|
-
if (!Number.isInteger(value) || value < 1 || value > MAX_TIMER_MS) throw new RangeError(`SSE : ${name} entier entre 1 et ${MAX_TIMER_MS} ms, ou false, attendu`);
|
|
55
|
-
}
|
|
56
45
|
/** Préfixe chaque ligne d'un texte par `prefix` — `data: ` ou `: `. */
|
|
57
46
|
function lines(prefix, text) {
|
|
58
47
|
let out = "";
|
|
@@ -65,69 +54,53 @@ function lines(prefix, text) {
|
|
|
65
54
|
* Il s'obtient par {@link openSseStream} (ou `this.renderSse()` dans un
|
|
66
55
|
* contrôleur), jamais par `new` : l'ouverture écrit les en-têtes, et un flux
|
|
67
56
|
* sans en-têtes ne se rattrape pas.
|
|
57
|
+
*
|
|
58
|
+
* Typé par une carte d'événements (`ContractServerEmit<C>` : le MÊME contrat
|
|
59
|
+
* que la socket temps réel) : `event:` est un canal de la carte et la charge est
|
|
60
|
+
* celle du canal. Deux formes se lisent sur la même carte — **multiplexé**
|
|
61
|
+
* (`send(data, { event: canal })`, plusieurs canaux sur une connexion) ou **un
|
|
62
|
+
* flux = un canal** (carte d'un seul canal, `send(data)` sans `event`). Sans
|
|
63
|
+
* carte, le flux reste libre (porte MCP, flux hors contrat). Le type n'est pas
|
|
64
|
+
* une validation, et ne coûte rien à l'exécution.
|
|
65
|
+
*
|
|
66
|
+
* @typeParam Events - carte des événements du flux.
|
|
68
67
|
*/
|
|
69
68
|
var SseStream = class {
|
|
70
|
-
|
|
71
|
-
#
|
|
72
|
-
/** La réponse dont on écoute la fermeture — en HTTP/2, l'objet de compatibilité. */
|
|
73
|
-
#response;
|
|
74
|
-
#stallTimeout;
|
|
69
|
+
/** Cycle de vie partagé avec le flux d'octets : fin, gardes, registre. */
|
|
70
|
+
#lifecycle;
|
|
75
71
|
#maxEventBytes;
|
|
76
|
-
#revalidate;
|
|
77
|
-
/**
|
|
78
|
-
* Identifiant de connexion de l'identité qui a ouvert le flux (`null` pour
|
|
79
|
-
* un anonyme) — le registre de révocation ferme par lui les flux d'une
|
|
80
|
-
* identité dont les droits ont changé.
|
|
81
|
-
*
|
|
82
|
-
* @internal
|
|
83
|
-
*/
|
|
84
|
-
identifier;
|
|
85
|
-
#closed = false;
|
|
86
72
|
#heartbeat = null;
|
|
87
|
-
/** Coupure d'un client qui ne lit plus — armée seulement sous pression. */
|
|
88
|
-
#stall = null;
|
|
89
|
-
/** Fin de vie du flux — armée seulement si `maxDuration` est posé. */
|
|
90
|
-
#lifetime = null;
|
|
91
|
-
/** Écouteurs de fermeture — `null` tant que personne n'en pose (lazy). */
|
|
92
|
-
#onClose = null;
|
|
93
73
|
/** Attente de `drain` en cours, partagée par tous les `send()` sous pression. */
|
|
94
74
|
#drain = null;
|
|
95
75
|
#releaseDrain = null;
|
|
96
|
-
#onResponseClose = () => this.#finish();
|
|
97
76
|
#onDrain = () => this.#settleDrain();
|
|
98
|
-
/**
|
|
99
|
-
* Le client ne lit plus depuis `stallTimeout` : on coupe la connexion, comme
|
|
100
|
-
* s'il était parti — la fermeture de la réponse termine le flux.
|
|
101
|
-
*/
|
|
102
|
-
#onStall = () => {
|
|
103
|
-
this.#stall = null;
|
|
104
|
-
this.#target.destroy();
|
|
105
|
-
};
|
|
106
77
|
/** @internal — passer par {@link openSseStream}. */
|
|
107
|
-
constructor(
|
|
108
|
-
this.#
|
|
109
|
-
this.#target = target;
|
|
110
|
-
this.#response = response;
|
|
111
|
-
this.#stallTimeout = settings.stallTimeout;
|
|
78
|
+
constructor(lifecycle, settings) {
|
|
79
|
+
this.#lifecycle = lifecycle;
|
|
112
80
|
this.#maxEventBytes = settings.maxEventBytes;
|
|
113
|
-
this.#
|
|
114
|
-
this.identifier = settings.identifier;
|
|
115
|
-
response.on("close", this.#onResponseClose);
|
|
81
|
+
lifecycle.onRelease(() => this.#release());
|
|
116
82
|
const heartbeat = settings.heartbeat;
|
|
117
|
-
if (
|
|
118
|
-
|
|
119
|
-
this.#lifetime.unref();
|
|
120
|
-
}
|
|
121
|
-
if (heartbeat !== false && heartbeat > 0) {
|
|
83
|
+
if (!lifecycle.closed && heartbeat !== false && heartbeat > 0) {
|
|
84
|
+
const target = lifecycle.target;
|
|
122
85
|
this.#heartbeat = setInterval(() => {
|
|
123
|
-
if (this.#drain === null)
|
|
86
|
+
if (this.#drain === null) target.write(HEARTBEAT);
|
|
124
87
|
}, heartbeat);
|
|
125
88
|
this.#heartbeat.unref();
|
|
126
89
|
}
|
|
127
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Identifiant de connexion de l'identité qui a ouvert le flux (`null` pour
|
|
93
|
+
* un anonyme) — le registre de révocation ferme par lui les flux d'une
|
|
94
|
+
* identité dont les droits ont changé.
|
|
95
|
+
*
|
|
96
|
+
* @internal
|
|
97
|
+
*/
|
|
98
|
+
get identifier() {
|
|
99
|
+
return this.#lifecycle.identifier;
|
|
100
|
+
}
|
|
128
101
|
/** Le flux est-il fermé — par le serveur (`close()`) ou par le client parti ? */
|
|
129
102
|
get closed() {
|
|
130
|
-
return this.#closed;
|
|
103
|
+
return this.#lifecycle.closed;
|
|
131
104
|
}
|
|
132
105
|
/**
|
|
133
106
|
* L'identité qui a ouvert le flux est-elle toujours valable ? Lu par le
|
|
@@ -136,7 +109,7 @@ var SseStream = class {
|
|
|
136
109
|
* @internal
|
|
137
110
|
*/
|
|
138
111
|
revalidate() {
|
|
139
|
-
return this.#
|
|
112
|
+
return this.#lifecycle.revalidate();
|
|
140
113
|
}
|
|
141
114
|
/**
|
|
142
115
|
* Envoie un événement.
|
|
@@ -147,7 +120,9 @@ var SseStream = class {
|
|
|
147
120
|
* @returns rien quand l'écriture est acceptée ; une promesse réglée au
|
|
148
121
|
* `drain` quand le client lit moins vite qu'on écrit — l'attendre borne la
|
|
149
122
|
* mémoire du serveur. Sans effet une fois le flux fermé.
|
|
150
|
-
* @throws TypeError quand `event` ou `id` contient une fin de ligne
|
|
123
|
+
* @throws TypeError quand `event` ou `id` contient une fin de ligne, ou
|
|
124
|
+
* quand `data` est fait d'octets (`Uint8Array`, `Buffer`, `ArrayBuffer`) :
|
|
125
|
+
* un flux SSE ne porte que du texte, les encoder soi-même (base64).
|
|
151
126
|
* @throws RangeError quand l'événement dépasse `maxEventBytes`.
|
|
152
127
|
*/
|
|
153
128
|
send(data, options) {
|
|
@@ -174,7 +149,7 @@ var SseStream = class {
|
|
|
174
149
|
* @throws RangeError quand `ms` n'est pas un entier entre 0 et 2^31-1.
|
|
175
150
|
*/
|
|
176
151
|
retry(ms) {
|
|
177
|
-
if (!Number.isInteger(ms) || ms < 0 || ms >
|
|
152
|
+
if (!Number.isInteger(ms) || ms < 0 || ms > 2147483647) throw new RangeError(`retry : entier entre 0 et ${MAX_TIMER_MS} ms attendu`);
|
|
178
153
|
return this.#write(`retry: ${ms}\n\n`);
|
|
179
154
|
}
|
|
180
155
|
/**
|
|
@@ -186,13 +161,7 @@ var SseStream = class {
|
|
|
186
161
|
* Un flux déjà fermé appelle l'écouteur au tour suivant.
|
|
187
162
|
*/
|
|
188
163
|
onClose(listener) {
|
|
189
|
-
|
|
190
|
-
if (this.#closed) {
|
|
191
|
-
queueMicrotask(bound);
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
this.#onClose ??= [];
|
|
195
|
-
this.#onClose.push(bound);
|
|
164
|
+
this.#lifecycle.onClose(listener);
|
|
196
165
|
}
|
|
197
166
|
/**
|
|
198
167
|
* Ferme le flux et termine la requête. Idempotent.
|
|
@@ -200,58 +169,40 @@ var SseStream = class {
|
|
|
200
169
|
* @returns la fin de la requête (hooks `onClose` du contexte compris).
|
|
201
170
|
*/
|
|
202
171
|
close() {
|
|
203
|
-
|
|
204
|
-
this.#finish();
|
|
205
|
-
const done = this.#context.close();
|
|
206
|
-
return done instanceof Promise ? done.then(() => void 0) : void 0;
|
|
172
|
+
return this.#lifecycle.close();
|
|
207
173
|
}
|
|
208
174
|
/** Écrit une trame ; une promesse seulement sous contre-pression. */
|
|
209
175
|
#write(frame) {
|
|
210
|
-
|
|
176
|
+
const lifecycle = this.#lifecycle;
|
|
177
|
+
if (lifecycle.closed) return void 0;
|
|
211
178
|
const max = this.#maxEventBytes;
|
|
212
179
|
if (frame.length * 3 > max && Buffer.byteLength(frame) > max) throw new RangeError(`SSE : événement de plus de ${max} octets refusé`);
|
|
213
|
-
|
|
180
|
+
const target = lifecycle.target;
|
|
181
|
+
if (target.write(frame)) return void 0;
|
|
214
182
|
if (this.#drain === null) {
|
|
215
183
|
this.#drain = new Promise((resolve) => {
|
|
216
184
|
this.#releaseDrain = resolve;
|
|
217
185
|
});
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
this.#stall = setTimeout(this.#onStall, this.#stallTimeout);
|
|
221
|
-
this.#stall.unref();
|
|
222
|
-
}
|
|
186
|
+
target.once("drain", this.#onDrain);
|
|
187
|
+
lifecycle.armStall();
|
|
223
188
|
}
|
|
224
189
|
return this.#drain;
|
|
225
190
|
}
|
|
226
191
|
#settleDrain() {
|
|
227
|
-
|
|
228
|
-
clearTimeout(this.#stall);
|
|
229
|
-
this.#stall = null;
|
|
230
|
-
}
|
|
192
|
+
this.#lifecycle.clearStall();
|
|
231
193
|
const release = this.#releaseDrain;
|
|
232
194
|
this.#drain = null;
|
|
233
195
|
this.#releaseDrain = null;
|
|
234
196
|
release?.();
|
|
235
197
|
}
|
|
236
|
-
/** Fin du flux :
|
|
237
|
-
#
|
|
238
|
-
|
|
239
|
-
this.#closed = true;
|
|
240
|
-
this.#response.removeListener("close", this.#onResponseClose);
|
|
241
|
-
this.#target.removeListener("drain", this.#onDrain);
|
|
198
|
+
/** Fin du flux, appelée UNE fois par le cycle de vie : battement et attentes libérés. */
|
|
199
|
+
#release() {
|
|
200
|
+
this.#lifecycle.target.removeListener("drain", this.#onDrain);
|
|
242
201
|
if (this.#heartbeat !== null) {
|
|
243
202
|
clearInterval(this.#heartbeat);
|
|
244
203
|
this.#heartbeat = null;
|
|
245
204
|
}
|
|
246
|
-
if (this.#lifetime !== null) {
|
|
247
|
-
clearTimeout(this.#lifetime);
|
|
248
|
-
this.#lifetime = null;
|
|
249
|
-
}
|
|
250
|
-
if (this.#revalidate !== null) this.#context.httpKernel?.unwatchStream(this);
|
|
251
205
|
this.#settleDrain();
|
|
252
|
-
const listeners = this.#onClose;
|
|
253
|
-
this.#onClose = null;
|
|
254
|
-
if (listeners !== null) for (const listener of listeners) listener();
|
|
255
206
|
}
|
|
256
207
|
};
|
|
257
208
|
/**
|
|
@@ -293,56 +244,40 @@ function acceptsEventStream(accept) {
|
|
|
293
244
|
* 2^31-1, ou que `maxEventBytes` n'est pas un entier positif.
|
|
294
245
|
*/
|
|
295
246
|
function openSseStream(context, options = {}) {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
247
|
+
checkStreamTimer("SSE", "heartbeat", options.heartbeat);
|
|
248
|
+
checkStreamTimer("SSE", "stallTimeout", options.stallTimeout);
|
|
249
|
+
checkStreamTimer("SSE", "maxDuration", options.maxDuration);
|
|
299
250
|
const maxEventBytes = options.maxEventBytes ?? 1048576;
|
|
300
251
|
if (!Number.isInteger(maxEventBytes) || maxEventBytes < 1) throw new RangeError("SSE : maxEventBytes entier positif attendu");
|
|
301
|
-
if (context.sended || context.finished || context.response.isHeaderSent()) throw new HttpError("SSE : la réponse est déjà partie", 500, context);
|
|
302
|
-
const kernel = context.httpKernel;
|
|
303
|
-
const nodeResponse = context.response.response;
|
|
304
|
-
if (kernel !== null && nodeResponse !== null && !kernel.acquireConnectionSlot(context.request.request, nodeResponse)) throw new HttpError("SSE : trop de connexions ouvertes depuis cette adresse", 429, context);
|
|
305
|
-
const revalidate = kernel?.firewall?.currentRevalidator?.() ?? null;
|
|
306
|
-
const identifier = revalidate === null ? null : RequestContext.getAuthenticatedUser()?.identifier ?? null;
|
|
307
252
|
const settings = {
|
|
308
253
|
heartbeat: options.heartbeat ?? 15e3,
|
|
309
|
-
|
|
310
|
-
maxEventBytes,
|
|
311
|
-
maxDuration: options.maxDuration ?? false,
|
|
312
|
-
revalidate,
|
|
313
|
-
identifier
|
|
254
|
+
maxEventBytes
|
|
314
255
|
};
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
}).then(() => startSseStream(context, options, settings));
|
|
320
|
-
return startSseStream(context, options, settings);
|
|
321
|
-
}
|
|
322
|
-
/** En-têtes, désarmement du délai, puis le flux. */
|
|
323
|
-
function startSseStream(context, options, settings) {
|
|
324
|
-
const response = context.response;
|
|
325
|
-
const nodeResponse = response.response;
|
|
326
|
-
if (nodeResponse === null) throw new HttpError("SSE : réponse introuvable", 500, context);
|
|
327
|
-
const stream = response instanceof Http2Response ? response.stream : null;
|
|
328
|
-
const h1 = nodeResponse instanceof http.ServerResponse ? nodeResponse : null;
|
|
329
|
-
let target;
|
|
330
|
-
if (stream !== null) target = stream;
|
|
331
|
-
else if (h1 !== null) target = h1;
|
|
332
|
-
else throw new HttpError("SSE : transport de réponse inconnu", 500, context);
|
|
333
|
-
response.setHeaders({
|
|
334
|
-
"Content-Type": "text/event-stream; charset=utf-8",
|
|
335
|
-
"Cache-Control": "no-cache, no-transform",
|
|
336
|
-
"X-Accel-Buffering": "no"
|
|
256
|
+
const opened = openStreamLifecycle(context, {
|
|
257
|
+
label: "SSE",
|
|
258
|
+
stallTimeout: options.stallTimeout ?? SSE_STALL_TIMEOUT_MS,
|
|
259
|
+
maxDuration: options.maxDuration ?? false
|
|
337
260
|
});
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
261
|
+
return opened instanceof Promise ? opened.then((lifecycle) => startSseStream(lifecycle, options, settings)) : startSseStream(opened, options, settings);
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* En-têtes, désarmement du délai, puis le flux. Un client parti pendant
|
|
265
|
+
* l'ouverture rend un flux déjà fermé, sans rien écrire.
|
|
266
|
+
*/
|
|
267
|
+
function startSseStream(lifecycle, options, settings) {
|
|
268
|
+
if (!lifecycle.closed) {
|
|
269
|
+
const context = lifecycle.context;
|
|
270
|
+
context.response.setHeaders({
|
|
271
|
+
"Content-Type": "text/event-stream; charset=utf-8",
|
|
272
|
+
"Cache-Control": "no-cache, no-transform",
|
|
273
|
+
"X-Accel-Buffering": "no"
|
|
274
|
+
});
|
|
275
|
+
context.writeHead(200);
|
|
276
|
+
const target = lifecycle.target;
|
|
277
|
+
if (target instanceof http.ServerResponse) target.flushHeaders();
|
|
278
|
+
lifecycle.disarmIdleTimeout();
|
|
343
279
|
}
|
|
344
|
-
const sse = new SseStream(
|
|
345
|
-
if (settings.revalidate !== null) context.httpKernel?.watchStream(sse);
|
|
280
|
+
const sse = new SseStream(lifecycle, settings);
|
|
346
281
|
if (options.retry !== void 0) sse.retry(options.retry);
|
|
347
282
|
return sse;
|
|
348
283
|
}
|