@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.
Files changed (130) hide show
  1. package/README.md +24 -0
  2. package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorate.js +1 -1
  3. package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorateParam.js +1 -1
  4. package/dist/index.js +30 -26
  5. package/dist/nodefony/command/proxyGenerateCommand.js +2 -2
  6. package/dist/nodefony/config/config.js +29 -4
  7. package/dist/nodefony/grpc/GrpcCall.js +298 -0
  8. package/dist/nodefony/grpc/GrpcEndpoint.js +528 -0
  9. package/dist/nodefony/grpc/index.js +4 -0
  10. package/dist/nodefony/interfaces/IRpcCall.js +1 -0
  11. package/dist/nodefony/service/ProfilerAdminApi.js +5 -2
  12. package/dist/nodefony/service/audit-logger.js +52 -6
  13. package/dist/nodefony/service/error-renderer.js +47 -13
  14. package/dist/nodefony/service/http-kernel.js +306 -49
  15. package/dist/nodefony/service/pretty-request-logger.js +5 -3
  16. package/dist/nodefony/service/request-logger.js +21 -6
  17. package/dist/nodefony/service/reverse-proxy.js +8 -3
  18. package/dist/nodefony/service/servers/clientError.js +53 -4
  19. package/dist/nodefony/service/servers/server-h2c.js +6 -6
  20. package/dist/nodefony/service/servers/server-http.js +6 -4
  21. package/dist/nodefony/service/servers/server-https.js +14 -7
  22. package/dist/nodefony/service/servers/server-static.js +1 -6
  23. package/dist/nodefony/service/servers/server-websocket-secure.js +7 -11
  24. package/dist/nodefony/service/servers/server-websocket.js +7 -11
  25. package/dist/nodefony/service/servers/serverShutdown.js +43 -6
  26. package/dist/nodefony/service/sessions/sessions-service.js +7 -7
  27. package/dist/nodefony/service/upload/upload-service.js +2 -2
  28. package/dist/nodefony/src/assets/prebuiltUi.js +23 -5
  29. package/dist/nodefony/src/context/Context.js +12 -1
  30. package/dist/nodefony/src/context/http/ByteStream.js +364 -0
  31. package/dist/nodefony/src/context/http/HttpContext.js +139 -32
  32. package/dist/nodefony/src/context/http/Request.js +111 -22
  33. package/dist/nodefony/src/context/http/Response.js +459 -37
  34. package/dist/nodefony/src/context/http/SseStream.js +83 -148
  35. package/dist/nodefony/src/context/http/StreamLifecycle.js +582 -0
  36. package/dist/nodefony/src/context/http/parser.js +29 -11
  37. package/dist/nodefony/src/context/http/urlFastPath.js +80 -1
  38. package/dist/nodefony/src/context/http/webResponse.js +56 -0
  39. package/dist/nodefony/src/context/http2/Request.js +1 -1
  40. package/dist/nodefony/src/context/http2/Response.js +55 -12
  41. package/dist/nodefony/src/context/requestTrace.js +6 -2
  42. package/dist/nodefony/src/context/rpcOutcome.js +20 -0
  43. package/dist/nodefony/src/context/transport.js +20 -1
  44. package/dist/nodefony/src/context/websocket/Response.js +15 -3
  45. package/dist/nodefony/src/context/websocket/WebsocketContext.js +86 -18
  46. package/dist/nodefony/src/context/websocket/websocketUrl.js +7 -0
  47. package/dist/nodefony/src/context/websocket/wsBackpressure.js +15 -15
  48. package/dist/nodefony/src/context/websocket/wsFrameRate.js +71 -0
  49. package/dist/nodefony/src/context/websocket/wsLogContent.js +55 -17
  50. package/dist/nodefony/src/cookies/cookie.js +57 -14
  51. package/dist/nodefony/src/errors/httpError.js +58 -25
  52. package/dist/nodefony/src/grpc/cors.js +59 -0
  53. package/dist/nodefony/src/grpc/limits.js +16 -0
  54. package/dist/nodefony/src/grpc/refusal.js +271 -0
  55. package/dist/nodefony/src/profiler/Profiler.js +14 -1
  56. package/dist/nodefony/src/profiler/aggregate.js +2 -2
  57. package/dist/nodefony/src/proxy/forward.js +109 -9
  58. package/dist/nodefony/src/proxy/generateProxyConfig.js +97 -17
  59. package/dist/nodefony/src/proxy/rules.js +29 -14
  60. package/dist/nodefony/src/servers/http2Goaway.js +76 -0
  61. package/dist/nodefony/src/servers/http2Limits.js +41 -15
  62. package/dist/nodefony/src/servers/http2StreamGrace.js +100 -0
  63. package/dist/nodefony/src/servers/receiveDeadline.js +177 -0
  64. package/dist/nodefony/src/servers/upgradeDispatch.js +62 -4
  65. package/dist/types/index.d.ts +11 -2
  66. package/dist/types/nodefony/config/config.d.ts +14 -0
  67. package/dist/types/nodefony/grpc/GrpcCall.d.ts +142 -0
  68. package/dist/types/nodefony/grpc/GrpcEndpoint.d.ts +98 -0
  69. package/dist/types/nodefony/grpc/index.d.ts +15 -0
  70. package/dist/types/nodefony/interfaces/IContext.d.ts +9 -0
  71. package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +14 -0
  72. package/dist/types/nodefony/interfaces/IRequest.d.ts +2 -1
  73. package/dist/types/nodefony/interfaces/IRequestTrace.d.ts +10 -0
  74. package/dist/types/nodefony/interfaces/IRouting.d.ts +19 -0
  75. package/dist/types/nodefony/interfaces/IRpcCall.d.ts +83 -0
  76. package/dist/types/nodefony/interfaces/ISecurity.d.ts +27 -2
  77. package/dist/types/nodefony/interfaces/IUpload.d.ts +51 -4
  78. package/dist/types/nodefony/interfaces/index.d.ts +2 -1
  79. package/dist/types/nodefony/service/ProfilerAdminApi.d.ts +2 -1
  80. package/dist/types/nodefony/service/audit-logger.d.ts +33 -3
  81. package/dist/types/nodefony/service/error-renderer.d.ts +27 -5
  82. package/dist/types/nodefony/service/http-kernel.d.ts +36 -3
  83. package/dist/types/nodefony/service/request-logger.d.ts +10 -0
  84. package/dist/types/nodefony/service/servers/clientError.d.ts +3 -2
  85. package/dist/types/nodefony/service/servers/serverShutdown.d.ts +7 -5
  86. package/dist/types/nodefony/src/assets/prebuiltUi.d.ts +9 -4
  87. package/dist/types/nodefony/src/context/Context.d.ts +9 -1
  88. package/dist/types/nodefony/src/context/http/ByteStream.d.ts +109 -0
  89. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +99 -3
  90. package/dist/types/nodefony/src/context/http/Request.d.ts +46 -9
  91. package/dist/types/nodefony/src/context/http/Response.d.ts +188 -8
  92. package/dist/types/nodefony/src/context/http/SseStream.d.ts +33 -8
  93. package/dist/types/nodefony/src/context/http/StreamLifecycle.d.ts +106 -0
  94. package/dist/types/nodefony/src/context/http/parser.d.ts +17 -3
  95. package/dist/types/nodefony/src/context/http/urlFastPath.d.ts +40 -0
  96. package/dist/types/nodefony/src/context/http/webResponse.d.ts +31 -0
  97. package/dist/types/nodefony/src/context/http2/Response.d.ts +40 -5
  98. package/dist/types/nodefony/src/context/requestTrace.d.ts +3 -1
  99. package/dist/types/nodefony/src/context/rpcOutcome.d.ts +11 -0
  100. package/dist/types/nodefony/src/context/transport.d.ts +17 -0
  101. package/dist/types/nodefony/src/context/websocket/Response.d.ts +6 -1
  102. package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +48 -0
  103. package/dist/types/nodefony/src/context/websocket/websocketUrl.d.ts +4 -0
  104. package/dist/types/nodefony/src/context/websocket/wsBackpressure.d.ts +12 -2
  105. package/dist/types/nodefony/src/context/websocket/wsFrameRate.d.ts +69 -0
  106. package/dist/types/nodefony/src/context/websocket/wsLogContent.d.ts +16 -8
  107. package/dist/types/nodefony/src/cookies/cookie.d.ts +20 -2
  108. package/dist/types/nodefony/src/errors/httpError.d.ts +29 -4
  109. package/dist/types/nodefony/src/grpc/cors.d.ts +32 -0
  110. package/dist/types/nodefony/src/grpc/limits.d.ts +14 -0
  111. package/dist/types/nodefony/src/grpc/refusal.d.ts +136 -0
  112. package/dist/types/nodefony/src/profiler/Profiler.d.ts +11 -1
  113. package/dist/types/nodefony/src/proxy/forward.d.ts +37 -2
  114. package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +15 -2
  115. package/dist/types/nodefony/src/proxy/rules.d.ts +20 -0
  116. package/dist/types/nodefony/src/servers/http2Goaway.d.ts +41 -0
  117. package/dist/types/nodefony/src/servers/http2Limits.d.ts +27 -14
  118. package/dist/types/nodefony/src/servers/http2StreamGrace.d.ts +34 -0
  119. package/dist/types/nodefony/src/servers/receiveDeadline.d.ts +77 -0
  120. package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +41 -1
  121. package/docs/byte-stream.md +406 -0
  122. package/docs/cookies.md +13 -3
  123. package/docs/diagnostics-channel.md +45 -1
  124. package/docs/index.md +8 -0
  125. package/docs/reverse-proxy.md +15 -3
  126. package/docs/servers.md +67 -43
  127. package/docs/session.md +3 -3
  128. package/docs/sse.md +27 -20
  129. package/docs/upload.md +43 -17
  130. package/package.json +38 -18
@@ -1,21 +1,12 @@
1
- import HttpError from "../../errors/httpError.js";
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 = 3e4;
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
- #context;
71
- #target;
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(context, target, response, settings) {
108
- this.#context = context;
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.#revalidate = settings.revalidate;
114
- this.identifier = settings.identifier;
115
- response.on("close", this.#onResponseClose);
81
+ lifecycle.onRelease(() => this.#release());
116
82
  const heartbeat = settings.heartbeat;
117
- if (settings.maxDuration !== false) {
118
- this.#lifetime = setTimeout(() => void this.close(), settings.maxDuration);
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) this.#target.write(HEARTBEAT);
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.#revalidate === null ? Promise.resolve(true) : this.#revalidate();
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 > MAX_TIMER_MS) throw new RangeError(`retry : entier entre 0 et ${MAX_TIMER_MS} ms attendu`);
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
- const bound = AsyncResource.bind(listener);
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
- if (this.#closed) return void 0;
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
- if (this.#closed) return void 0;
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
- if (this.#target.write(frame)) return void 0;
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
- this.#target.once("drain", this.#onDrain);
219
- if (this.#stallTimeout !== false) {
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
- if (this.#stall !== null) {
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 : minuteur, écouteurs et attentes libérés, une seule fois. */
237
- #finish() {
238
- if (this.#closed) return;
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
- checkTimer("heartbeat", options.heartbeat);
297
- checkTimer("stallTimeout", options.stallTimeout);
298
- checkTimer("maxDuration", options.maxDuration);
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
- stallTimeout: options.stallTimeout ?? 3e4,
310
- maxEventBytes,
311
- maxDuration: options.maxDuration ?? false,
312
- revalidate,
313
- identifier
254
+ maxEventBytes
314
255
  };
315
- context.sended = true;
316
- if (context.session != null) return context.saveSession().catch((e) => {
317
- context.log(e, "ERROR", "SESSION-STORE");
318
- return null;
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
- context.writeHead(200);
339
- if (stream !== null) stream.setTimeout(0);
340
- else if (h1 !== null) {
341
- h1.flushHeaders();
342
- h1.socket?.setTimeout(0);
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(context, target, nodeResponse, settings);
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
  }