@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
@@ -0,0 +1,582 @@
1
+ import Http2Response from "../http2/Response.js";
2
+ import HttpError from "../../errors/httpError.js";
3
+ import { ANNOUNCE_CAP_MS } from "../../servers/http2Goaway.js";
4
+ import { REVOCATION_REVALIDATE_MS, RequestContext } from "nodefony";
5
+ import http from "node:http";
6
+ import { AsyncResource } from "node:async_hooks";
7
+ // nodefony/src/context/http/StreamLifecycle.ts
8
+ /**
9
+ * Cycle de vie d'une réponse en flux — la brique commune au flux d'événements
10
+ * (`SseStream`) et au flux d'octets, NON exportée.
11
+ *
12
+ * Elle porte ce qu'un flux doit à la requête qui l'ouvre, quel que soit son
13
+ * format :
14
+ * - la PRISE de la réponse (`sended`), avant toute attente : le noyau ne peut
15
+ * plus répondre par-dessus, et l'avertissement « réponse non envoyée » se tait ;
16
+ * - la session sauvée AVANT les en-têtes — ses cookies partent avec eux ;
17
+ * - la fermeture lue sur la RÉPONSE, écouteur posé de façon SYNCHRONE à
18
+ * l'ouverture : un client parti pendant la sauvegarde de la session est vu ;
19
+ * - le délai d'inactivité de la requête désarmé, remplacé par les gardes du
20
+ * flux : coupure d'un client qui ne lit plus (`stallTimeout`), durée de vie
21
+ * maximale (`maxDuration`) ;
22
+ * - le plafond de connexions longues par IP (`wsMaxConnectionsPerIp`, le budget
23
+ * des sockets WebSocket) et la fermeture à la révocation de l'identité ;
24
+ * - une fin unique, et des écouteurs de fin liés au contexte asynchrone.
25
+ *
26
+ * Le FORMAT (en-têtes, écriture, battement de cœur) appartient au flux qui la
27
+ * porte.
28
+ */
29
+ /**
30
+ * Plus long délai qu'un minuteur sait tenir (2^31-1 ms). Au-delà, Node le
31
+ * ramène à 1 ms : un délai qui le dépasse ferait battre ou couper le flux
32
+ * mille fois par seconde.
33
+ */
34
+ const MAX_TIMER_MS = 2147483647;
35
+ /**
36
+ * Délai de blocage par défaut d'un flux, en ms : un client qui ne lit plus
37
+ * depuis ce délai est coupé. Valeur commune aux flux d'événements, d'octets et
38
+ * aux flux RPC.
39
+ */
40
+ const STREAM_STALL_TIMEOUT_MS = 3e4;
41
+ /**
42
+ * Période du balayage de blocage ({@link StallSweep}) : la moitié du délai de
43
+ * blocage. Un client bloqué est coupé entre `stallTimeout` et
44
+ * `stallTimeout + période` après la première contre-pression vue — jamais
45
+ * avant le délai réglé.
46
+ */
47
+ const STALL_SWEEP_PERIOD_MS = STREAM_STALL_TIMEOUT_MS / 2;
48
+ /**
49
+ * Attente maximale, à l'arrêt du serveur, des flux longs qu'on vient de
50
+ * terminer (`HttpKernel.shutdownStreams`), avant le drain des connexions.
51
+ *
52
+ * Un flux qui écoute son signal se ferme en quelques millisecondes : l'attente
53
+ * finit avec lui. Elle ne dure jusqu'à son terme que pour un gestionnaire qui
54
+ * n'écoute rien — et ce temps s'AJOUTE au drain (`shutdownTimeout`), d'où une
55
+ * borne courte, celle de l'annonce du GOAWAY.
56
+ */
57
+ const SHUTDOWN_STREAMS_WAIT_MS = ANNOUNCE_CAP_MS;
58
+ /**
59
+ * Refuse une option de délai hors de ce qu'un minuteur sait tenir.
60
+ *
61
+ * @param label - préfixe du message (`SSE`, `Flux`).
62
+ * @param name - nom de l'option.
63
+ * @param value - valeur reçue ; `undefined` et `false` passent.
64
+ * @throws RangeError quand la valeur n'est ni `false` ni un entier entre 1 et 2^31-1.
65
+ *
66
+ * @internal
67
+ */
68
+ function checkStreamTimer(label, name, value) {
69
+ if (value === void 0 || value === false) return;
70
+ if (!Number.isInteger(value) || value < 1 || value > 2147483647) throw new RangeError(`${label} : ${name} entier entre 1 et ${MAX_TIMER_MS} ms, ou false, attendu`);
71
+ }
72
+ /**
73
+ * Constate le transport de la réponse d'un contexte : le flux en HTTP/2, la
74
+ * réponse en HTTP/1.1. Seule implémentation de ce constat — un flux SSE,
75
+ * d'octets ou un appel gRPC le lisent ici.
76
+ *
77
+ * @param context - le contexte de la requête.
78
+ * @returns la cible d'écriture, ou `null` quand la réponse est absente ou que
79
+ * son transport n'est pas reconnu.
80
+ *
81
+ * @internal
82
+ */
83
+ function resolveStreamTarget(context) {
84
+ const response = context.response;
85
+ const stream = response instanceof Http2Response ? response.stream : null;
86
+ if (stream !== null) return stream;
87
+ const nodeResponse = response.response;
88
+ return nodeResponse instanceof http.ServerResponse ? nodeResponse : null;
89
+ }
90
+ /**
91
+ * Désarme le délai de réponse que Nodefony arme sur toute requête
92
+ * (`HttpContext.setTimeout`) : celui du flux en HTTP/2, celui de la socket en
93
+ * HTTP/1.1. Seule implémentation de cette règle — un flux long l'appelle, et
94
+ * une réponse possédée par un tiers aussi, pour qu'à l'expiration Nodefony
95
+ * n'écrive pas un 503 par-dessus (un seul écrivain).
96
+ *
97
+ * En HTTP/1.1, la requête suivante de la même connexion réarme la socket
98
+ * (`HttpContext.setTimeout` compare `socket.timeout` à son délai).
99
+ *
100
+ * @param target - la cible d'écriture constatée par {@link resolveStreamTarget}.
101
+ *
102
+ * @internal
103
+ */
104
+ function disarmResponseTimeout(target) {
105
+ if (target instanceof http.ServerResponse) target.socket?.setTimeout(0);
106
+ else target.setTimeout(0);
107
+ }
108
+ /**
109
+ * Le cycle de vie d'UN flux ouvert sur une réponse.
110
+ *
111
+ * S'obtient par {@link openStreamLifecycle} ; le constructeur n'est appelé
112
+ * directement que par les tests.
113
+ *
114
+ * @internal
115
+ */
116
+ var StreamLifecycle = class StreamLifecycle {
117
+ context;
118
+ /** Où l'on écrit : la réponse HTTP/1.1, ou le flux HTTP/2. */
119
+ target;
120
+ /** La réponse dont on écoute la fermeture — en HTTP/2, l'objet de compatibilité. */
121
+ #response;
122
+ #stallTimeout;
123
+ #revalidate;
124
+ identifier;
125
+ #foreignWriter;
126
+ /**
127
+ * Le noyau des registres où le flux s'inscrit, pris À L'OUVERTURE : le
128
+ * nettoyage du contexte (`Context.clean`) remet `context.httpKernel` à
129
+ * `null`, et il passe à la fermeture AVANT celui du flux — relu là, le
130
+ * retrait ne se ferait pas, et le flux fermé resterait dans les registres
131
+ * (révocation, balayage, arrêt) avec son contexte.
132
+ */
133
+ #kernel;
134
+ /** Terminé par le noyau à l'arrêt du serveur — et donc inscrit à son registre. */
135
+ #endsAtShutdown;
136
+ /**
137
+ * Fin d'attente d'un flux balayé : posée sur `drain` quand le balayage voit
138
+ * une contre-pression — créée à la première, `null` avant (lazy).
139
+ */
140
+ #onSweptDrain = null;
141
+ /**
142
+ * Fenêtre laissée à un appel RPC révoqué pour se terminer à son format avant
143
+ * la destruction du transport — armée seulement à la révocation.
144
+ */
145
+ #revokeGrace = null;
146
+ #closed = false;
147
+ #cause = null;
148
+ /**
149
+ * Minuteur de la coupure d'un client qui ne lit plus — créé à la PREMIÈRE
150
+ * contre-pression, puis réarmé au reste du délai : jamais un minuteur par
151
+ * épisode (un flux d'octets en connaît un par morceau sur une socket rapide).
152
+ */
153
+ #stall = null;
154
+ /** Début de l'attente de `drain` en cours (`Date.now()`) ; `0` : aucune. */
155
+ #stallSince = 0;
156
+ /** Fin de vie du flux — armée seulement si `maxDuration` est posé. */
157
+ #lifetime = null;
158
+ /** Nettoyage du flux qui porte la brique — posé une fois, appelé en premier. */
159
+ #release = null;
160
+ /** Écouteurs de fermeture — `null` tant que personne n'en pose (lazy). */
161
+ #onClose = null;
162
+ #onResponseClose = () => this.finish();
163
+ /**
164
+ * Tour du minuteur de blocage. Le client ne lit plus depuis `stallTimeout` :
165
+ * on coupe la connexion, comme s'il était parti — la fermeture de la réponse
166
+ * termine le flux. Une attente plus récente (le client a lu entre-temps) est
167
+ * réarmée au reste de son délai ; plus d'attente du tout : le minuteur s'éteint.
168
+ */
169
+ #onStall = () => {
170
+ this.#stall = null;
171
+ const limit = this.#stallTimeout;
172
+ if (this.#stallSince === 0 || this.#closed || limit === false) return;
173
+ const waited = Date.now() - this.#stallSince;
174
+ if (waited < limit) {
175
+ this.#stall = setTimeout(this.#onStall, limit - waited);
176
+ this.#stall.unref();
177
+ return;
178
+ }
179
+ this.#cutStalled();
180
+ };
181
+ constructor(context, target, response, settings) {
182
+ this.context = context;
183
+ this.target = target;
184
+ this.#response = response;
185
+ this.#stallTimeout = settings.stallTimeout;
186
+ this.#revalidate = settings.revalidate;
187
+ this.identifier = settings.identifier;
188
+ this.#foreignWriter = settings.foreignWriter;
189
+ this.#endsAtShutdown = settings.endsAtShutdown;
190
+ const kernel = context.httpKernel;
191
+ this.#kernel = kernel;
192
+ response.on("close", this.#onResponseClose);
193
+ if (settings.maxDuration !== false) {
194
+ this.#lifetime = setTimeout(() => void this.close(), settings.maxDuration);
195
+ this.#lifetime.unref();
196
+ }
197
+ if (this.#revalidate !== null) kernel?.watchStream(this);
198
+ if (this.#endsAtShutdown) kernel?.trackStream(this);
199
+ if (this.#foreignWriter && this.#stallTimeout !== false) kernel?.sweepStall(this);
200
+ }
201
+ /** Le flux est-il fermé — par le serveur, ou par le client parti ? */
202
+ get closed() {
203
+ return this.#closed;
204
+ }
205
+ /**
206
+ * Qui a fermé le flux : le serveur, la coupure d'un client qui ne lit plus,
207
+ * ou `null` — client parti, ou flux encore ouvert. Lu par le flux qui porte
208
+ * la brique, dans son nettoyage, pour nommer la raison de la fin.
209
+ */
210
+ get cause() {
211
+ return this.#cause;
212
+ }
213
+ /** L'identité qui a ouvert le flux est-elle toujours valable ? */
214
+ revalidate() {
215
+ return this.#revalidate === null ? Promise.resolve(true) : this.#revalidate();
216
+ }
217
+ /**
218
+ * Pose le nettoyage du flux qui porte la brique (minuteurs, attentes) : il
219
+ * passe AVANT les écouteurs de l'application. Un cycle déjà fermé l'appelle
220
+ * aussitôt.
221
+ */
222
+ onRelease(release) {
223
+ if (this.#closed) {
224
+ release();
225
+ return;
226
+ }
227
+ this.#release = release;
228
+ }
229
+ /**
230
+ * Appelle `listener` à la fermeture du flux, quelle qu'en soit la cause.
231
+ *
232
+ * L'écouteur est lié au contexte asynchrone de la requête au moment de
233
+ * l'enregistrement : la fermeture par le client part de la SOCKET, hors de
234
+ * la bulle de la requête. Un flux déjà fermé appelle l'écouteur au tour
235
+ * suivant.
236
+ */
237
+ onClose(listener) {
238
+ const bound = AsyncResource.bind(listener);
239
+ if (this.#closed) {
240
+ queueMicrotask(bound);
241
+ return;
242
+ }
243
+ this.#onClose ??= [];
244
+ this.#onClose.push(bound);
245
+ }
246
+ /**
247
+ * Arme la coupure d'un client qui ne lit plus — à appeler quand une écriture
248
+ * attend `drain`. Sans effet si elle est déjà armée ou désactivée.
249
+ */
250
+ armStall() {
251
+ if (this.#stallTimeout === false || this.#stallSince !== 0 || this.#closed) return;
252
+ this.#stallSince = Date.now();
253
+ if (this.#stall !== null) return;
254
+ this.#stall = setTimeout(this.#onStall, this.#stallTimeout);
255
+ this.#stall.unref();
256
+ }
257
+ /**
258
+ * Désarme la coupure : le client a repris sa lecture. Le minuteur n'est pas
259
+ * détruit — il s'éteindra seul à son tour s'il n'y a plus d'attente, ou
260
+ * resservira à la suivante sans nouvelle allocation.
261
+ */
262
+ clearStall() {
263
+ this.#stallSince = 0;
264
+ }
265
+ /**
266
+ * Un tour du balayage de blocage, pour un flux dont un tiers tient les
267
+ * écritures ({@link StallSweep}).
268
+ *
269
+ * Une contre-pression vue (`writableNeedDrain`) ouvre l'attente et pose un
270
+ * `drain` : seul ce progrès RÉEL la ferme, jamais un tour qui verrait le
271
+ * tampon vide — un client lent qui lit voit son tampon se remplir à nouveau
272
+ * aussitôt, et serait sinon pris pour bloqué. Une attente qui dure
273
+ * `stallTimeout` coupe le client.
274
+ *
275
+ * @param now - l'heure du tour (`Date.now()`), lue une fois pour tous les flux.
276
+ */
277
+ sweepStall(now) {
278
+ const limit = this.#stallTimeout;
279
+ if (this.#closed || limit === false) return;
280
+ if (this.#stallSince !== 0) {
281
+ if (now - this.#stallSince >= limit) this.#cutStalled();
282
+ return;
283
+ }
284
+ if (!this.target.writableNeedDrain) return;
285
+ this.#stallSince = now;
286
+ this.target.once("drain", this.#onSweptDrain ??= () => {
287
+ this.#stallSince = 0;
288
+ });
289
+ }
290
+ /** Coupe un client qui ne lit plus : la fermeture de la réponse termine le flux. */
291
+ #cutStalled() {
292
+ this.#cause = "stall";
293
+ this.target.destroy();
294
+ }
295
+ /**
296
+ * Désarme le délai d'inactivité de la requête : un flux n'a pas de fin
297
+ * attendue, ses propres gardes le remplacent.
298
+ */
299
+ disarmIdleTimeout() {
300
+ disarmResponseTimeout(this.target);
301
+ }
302
+ /**
303
+ * Ferme le flux et termine la requête. Idempotent. Quand un tiers tient les
304
+ * écritures, le transport est détruit : terminer la réponse en ferait un
305
+ * second écrivain.
306
+ */
307
+ close() {
308
+ if (this.#closed) return void 0;
309
+ this.#cause = "server";
310
+ this.finish();
311
+ if (this.#foreignWriter) {
312
+ this.target.destroy();
313
+ return;
314
+ }
315
+ const done = this.context.close();
316
+ return done instanceof Promise ? done.then(() => void 0) : void 0;
317
+ }
318
+ /**
319
+ * L'identité qui a ouvert le flux est morte (révocation, droits changés) :
320
+ * le flux prend fin. Idempotent.
321
+ *
322
+ * Quand un adaptateur RPC tient les écritures, c'est LUI qui termine l'appel,
323
+ * au format de son protocole (`HttpContext.rpc.revoke`, gRPC : 16) : le
324
+ * client lit un statut, pas une coupure, et aucun second écrivain ne touche
325
+ * la réponse. Un gestionnaire qui n'entend rien — bloqué, sans message à
326
+ * produire — est détruit à la fenêtre de re-validation suivante. Sinon :
327
+ * {@link close}.
328
+ */
329
+ revoke() {
330
+ if (this.#closed) return void 0;
331
+ const rpc = this.#foreignWriter ? this.context.rpc : null;
332
+ if (rpc === null) return this.close();
333
+ if (this.#revokeGrace !== null) return void 0;
334
+ this.#cause = "server";
335
+ rpc.revoke();
336
+ this.#revokeGrace = setTimeout(StreamLifecycle.#destroyRevoked, REVOCATION_REVALIDATE_MS, this);
337
+ this.#revokeGrace.unref();
338
+ }
339
+ /**
340
+ * Le serveur s'arrête : le flux prend fin AVANT le drain des connexions
341
+ * (ADR-0026 D8). Idempotent.
342
+ *
343
+ * Quand un adaptateur RPC tient les écritures, c'est LUI qui termine l'appel,
344
+ * au format de son protocole (`HttpContext.rpc.shutdown`, gRPC : 14
345
+ * `UNAVAILABLE`) : le client le rejoue ailleurs. Un gestionnaire qui n'entend
346
+ * rien tombe avec sa connexion, détruite par le drain à `shutdownTimeout` —
347
+ * aucun minuteur de plus. Sinon : {@link close} — `EventSource` se
348
+ * reconnecte, vers un autre exemplaire.
349
+ */
350
+ shutdown() {
351
+ if (this.#closed) return void 0;
352
+ const rpc = this.#foreignWriter ? this.context.rpc : null;
353
+ if (rpc === null) return this.close();
354
+ this.#cause = "server";
355
+ rpc.shutdown();
356
+ }
357
+ /** Fenêtre écoulée : l'appel révoqué n'a pas fini, le transport est détruit. */
358
+ static #destroyRevoked(lifecycle) {
359
+ lifecycle.#revokeGrace = null;
360
+ if (lifecycle.#closed) return;
361
+ lifecycle.finish();
362
+ lifecycle.target.destroy();
363
+ }
364
+ /** Fin du flux : minuteurs, écouteurs et registre libérés, une seule fois. */
365
+ finish() {
366
+ if (this.#closed) return;
367
+ this.#closed = true;
368
+ this.#response.removeListener("close", this.#onResponseClose);
369
+ if (this.#lifetime !== null) {
370
+ clearTimeout(this.#lifetime);
371
+ this.#lifetime = null;
372
+ }
373
+ if (this.#revokeGrace !== null) {
374
+ clearTimeout(this.#revokeGrace);
375
+ this.#revokeGrace = null;
376
+ }
377
+ this.#stallSince = 0;
378
+ if (this.#stall !== null) {
379
+ clearTimeout(this.#stall);
380
+ this.#stall = null;
381
+ }
382
+ const kernel = this.#kernel;
383
+ if (this.#revalidate !== null) kernel?.unwatchStream(this);
384
+ if (this.#endsAtShutdown) kernel?.untrackStream(this);
385
+ if (this.#foreignWriter) {
386
+ kernel?.unsweepStall(this);
387
+ if (this.#onSweptDrain !== null) this.target.removeListener("drain", this.#onSweptDrain);
388
+ }
389
+ const release = this.#release;
390
+ this.#release = null;
391
+ release?.();
392
+ const listeners = this.#onClose;
393
+ this.#onClose = null;
394
+ if (listeners !== null) for (const listener of listeners) listener();
395
+ }
396
+ };
397
+ /**
398
+ * Le balayage de blocage des flux dont un tiers tient les écritures : UN
399
+ * minuteur pour tous, armé au premier flux inscrit, arrêté au dernier retiré.
400
+ *
401
+ * Pourquoi un balayage : l'adaptateur écrit et attend `drain` lui-même, la
402
+ * brique ne voit aucune écriture. Le délai d'inactivité que Node tient sur un
403
+ * flux HTTP/2 ne suffit pas : il est repoussé tant que la SESSION avance
404
+ * (`internal/http2/core#callTimeout`), si bien qu'un flux bloqué ne serait
405
+ * jamais coupé tant qu'un voisin de la même connexion est lu.
406
+ *
407
+ * Le seuil reste dans {@link StreamLifecycle.sweepStall} : une règle, une
408
+ * implémentation.
409
+ *
410
+ * Coût mesuré (300 flux HTTP/2 réels, médiane de 9 passes) : ~6 ns par flux et
411
+ * par tour ; ~60 ns pour inscrire puis retirer un flux ; 0 par message.
412
+ *
413
+ * @internal
414
+ */
415
+ var StallSweep = class {
416
+ #entries = null;
417
+ #timer = null;
418
+ #periodMs;
419
+ #tick = () => {
420
+ this.sweep(Date.now());
421
+ };
422
+ /** @param periodMs - période du balayage (défaut {@link STALL_SWEEP_PERIOD_MS}). */
423
+ constructor(periodMs = STALL_SWEEP_PERIOD_MS) {
424
+ this.#periodMs = periodMs;
425
+ }
426
+ /** Nombre de flux balayés. */
427
+ get size() {
428
+ return this.#entries?.size ?? 0;
429
+ }
430
+ /** Le minuteur est-il armé ? */
431
+ get running() {
432
+ return this.#timer !== null;
433
+ }
434
+ /** Inscrit un flux ; arme le minuteur au premier. */
435
+ add(lifecycle) {
436
+ (this.#entries ??= /* @__PURE__ */ new Set()).add(lifecycle);
437
+ if (this.#timer === null) {
438
+ this.#timer = setInterval(this.#tick, this.#periodMs);
439
+ this.#timer.unref();
440
+ }
441
+ }
442
+ /** Retire un flux (idempotent) ; arrête le minuteur quand il n'en reste aucun. */
443
+ delete(lifecycle) {
444
+ if (this.#entries === null) return;
445
+ this.#entries.delete(lifecycle);
446
+ if (this.#entries.size === 0 && this.#timer !== null) {
447
+ clearInterval(this.#timer);
448
+ this.#timer = null;
449
+ }
450
+ }
451
+ /**
452
+ * Un tour : chaque flux inscrit juge son propre blocage.
453
+ *
454
+ * @param now - l'heure du tour (`Date.now()`).
455
+ */
456
+ sweep(now) {
457
+ if (this.#entries === null) return;
458
+ for (const lifecycle of this.#entries) lifecycle.sweepStall(now);
459
+ }
460
+ };
461
+ /**
462
+ * Le registre de l'arrêt : TOUS les flux longs ouverts qui n'ont pas de fin à
463
+ * eux — SSE, flux RPC, flux d'octets vivants (ADR-0026 D8). À l'arrêt du
464
+ * serveur, le noyau les termine AVANT le drain des connexions, au lieu de les
465
+ * laisser détruire sans un mot à `shutdownTimeout`.
466
+ *
467
+ * Distinct du registre de révocation (`RevocationWatch`), qui ne tient que les
468
+ * flux à identité : y mettre les anonymes ferait travailler chaque fenêtre de
469
+ * re-validation pour rien.
470
+ *
471
+ * Coût : un ajout et un retrait par flux long, constants (44 à 77 ns mesurés
472
+ * de 0 à 10 000 flux ouverts, ADR-0026 D8) ; 0 par appel unaire, 0 par
473
+ * message.
474
+ *
475
+ * @internal
476
+ */
477
+ var ShutdownRegistry = class {
478
+ #entries = /* @__PURE__ */ new Set();
479
+ /** L'arrêt a commencé : un flux inscrit désormais est terminé au tour suivant. */
480
+ #closing = false;
481
+ /** Fin de l'attente de l'arrêt, appelée au retrait du dernier flux. */
482
+ #onEmpty = null;
483
+ /** Nombre de flux ouverts. */
484
+ get size() {
485
+ return this.#entries.size;
486
+ }
487
+ /**
488
+ * Inscrit un flux. Pendant l'arrêt, il est terminé au tour suivant — une
489
+ * fois son appel RPC relié, qui ne l'est qu'après l'ouverture.
490
+ */
491
+ add(stream) {
492
+ this.#entries.add(stream);
493
+ if (this.#closing) setImmediate(shutdownLate, stream);
494
+ }
495
+ /** Retire un flux (idempotent) ; le dernier retiré met fin à l'attente. */
496
+ delete(stream) {
497
+ if (!this.#entries.delete(stream) || this.#entries.size !== 0) return;
498
+ const done = this.#onEmpty;
499
+ this.#onEmpty = null;
500
+ done?.();
501
+ }
502
+ /**
503
+ * L'arrêt : termine chaque flux, puis attend que tous se soient retirés, au
504
+ * plus `waitMs`.
505
+ *
506
+ * @param waitMs - attente maximale des fermetures.
507
+ * @returns le nombre de flux encore ouverts à la fin de l'attente — tout de
508
+ * suite quand il n'y a rien à attendre, sinon sa promesse.
509
+ */
510
+ shutdown(waitMs) {
511
+ this.#closing = true;
512
+ for (const stream of this.#entries) stream.shutdown();
513
+ if (this.#entries.size === 0) return 0;
514
+ return new Promise((resolve) => {
515
+ const timer = setTimeout(() => {
516
+ this.#onEmpty = null;
517
+ resolve(this.#entries.size);
518
+ }, waitMs);
519
+ this.#onEmpty = () => {
520
+ clearTimeout(timer);
521
+ resolve(0);
522
+ };
523
+ });
524
+ }
525
+ };
526
+ /** Termine un flux inscrit pendant l'arrêt (rappel de `setImmediate`). */
527
+ function shutdownLate(stream) {
528
+ stream.shutdown();
529
+ }
530
+ /**
531
+ * Ouvre le cycle de vie d'un flux sur la réponse d'un contexte HTTP.
532
+ *
533
+ * Prend la réponse, pose l'écoute de sa fermeture, puis sauve la session —
534
+ * sans rien écrire : les en-têtes appartiennent au flux. Un client parti
535
+ * pendant la sauvegarde rend un cycle déjà fermé (`closed`) : le flux n'écrit
536
+ * alors rien.
537
+ *
538
+ * @param context - le contexte de la requête, pas encore répondu.
539
+ * @param options - gardes du flux et préfixe des messages.
540
+ * @returns le cycle de vie, ou sa promesse quand une session est à sauver.
541
+ * @throws HttpError 500 quand la réponse est déjà partie, ou que son
542
+ * transport n'est pas reconnu.
543
+ * @throws HttpError 429 quand l'IP du client a déjà tous ses flux et sockets
544
+ * (`wsMaxConnectionsPerIp`) — refusé AVANT d'écrire quoi que ce soit.
545
+ *
546
+ * @internal
547
+ */
548
+ function openStreamLifecycle(context, options) {
549
+ const { label } = options;
550
+ if (context.sended || context.finished || context.response.isHeaderSent()) throw new HttpError(`${label} : la réponse est déjà partie`, 500);
551
+ const nodeResponse = context.response.response;
552
+ if (nodeResponse === null) throw new HttpError(`${label} : réponse introuvable`, 500, context);
553
+ const target = resolveStreamTarget(context);
554
+ if (target === null) throw new HttpError(`${label} : transport de réponse inconnu`, 500, context);
555
+ const gone = target.destroyed;
556
+ const kernel = context.httpKernel;
557
+ if (!gone && kernel !== null && !kernel.acquireConnectionSlot(context.request.request, nodeResponse)) throw new HttpError(`${label} : trop de connexions ouvertes depuis cette adresse`, 429, context);
558
+ const revalidate = kernel?.firewall?.currentRevalidator?.() ?? null;
559
+ const identifier = revalidate === null ? null : RequestContext.getAuthenticatedUser()?.identifier ?? null;
560
+ context.sended = true;
561
+ const lifecycle = new StreamLifecycle(context, target, nodeResponse, {
562
+ stallTimeout: options.stallTimeout,
563
+ maxDuration: options.maxDuration,
564
+ revalidate,
565
+ identifier,
566
+ foreignWriter: options.foreignWriter ?? false,
567
+ endsAtShutdown: options.endsAtShutdown ?? true
568
+ });
569
+ if (gone) {
570
+ lifecycle.finish();
571
+ return lifecycle;
572
+ }
573
+ if (context.session == null) return lifecycle;
574
+ return context.saveSession().catch((e) => {
575
+ context.log(e, "ERROR", "SESSION-STORE");
576
+ return null;
577
+ }).then(() => {
578
+ if (target.destroyed) lifecycle.finish();
579
+ return lifecycle;
580
+ });
581
+ }
582
+ export { MAX_TIMER_MS, SHUTDOWN_STREAMS_WAIT_MS, STALL_SWEEP_PERIOD_MS, STREAM_STALL_TIMEOUT_MS, ShutdownRegistry, StallSweep, StreamLifecycle, checkStreamTimer, disarmResponseTimeout, openStreamLifecycle, resolveStreamTarget };
@@ -8,8 +8,8 @@ var Parser = class {
8
8
  chunks;
9
9
  received = 0;
10
10
  aborted = false;
11
- overflow = null;
12
- onOverflow = null;
11
+ failure = null;
12
+ onFailure = null;
13
13
  constructor(request) {
14
14
  this.request = request;
15
15
  this.chunks = [];
@@ -22,33 +22,51 @@ var Parser = class {
22
22
  const max = this.request.maxBodySize;
23
23
  this.received += buffer.length;
24
24
  if (max > 0 && this.received > max) {
25
- this.aborted = true;
26
- this.chunks.length = 0;
27
- this.overflow = new HttpError(`Request body exceeds maxBodySize (${max} bytes)`, 413, this.request.context);
28
- this.request.request.pause?.();
29
- if (this.onOverflow) this.onOverflow(this.overflow);
25
+ this.fail(new HttpError(`Request body exceeds maxBodySize (${max} bytes)`, 413, this.request.context));
30
26
  return buffer;
31
27
  }
32
28
  this.chunks.push(buffer);
33
29
  return buffer;
34
30
  }
35
31
  /**
32
+ * Interrompt la réception du corps : `parse()` rejette avec `error`, que
33
+ * l'interruption survienne avant ou pendant l'attente de la fin du flux.
34
+ *
35
+ * La RAM déjà bufferisée est libérée et la lecture du flux suspendue ; la
36
+ * réponse d'erreur peut encore partir. Sans effet si le corps est déjà
37
+ * entièrement reçu, ou déjà interrompu.
38
+ *
39
+ * @param error - la cause rendue au client (413 dépassement, 408 délai)
40
+ * @returns vrai si la réception a été interrompue
41
+ */
42
+ fail(error) {
43
+ const req = this.request.request;
44
+ if (this.aborted || req.readableEnded === true || req.complete === true) return false;
45
+ this.aborted = true;
46
+ this.chunks.length = 0;
47
+ this.failure = error;
48
+ req.pause?.();
49
+ if (this.onFailure) this.onFailure(error);
50
+ return true;
51
+ }
52
+ /**
36
53
  * Résout quand le flux requête est **entièrement reçu** (`end`). No-op si déjà
37
54
  * terminé (corps vide / déjà drainé) → pas de hang d'un `once("end")` tardif.
38
55
  * Indispensable avant de concaténer les chunks pour un corps à décoder
39
56
  * (JSON…) : sinon on lit un buffer encore incomplet. (formidable drainait via
40
57
  * son `await form.parse()` ; ses remplaçants doivent drainer explicitement.)
41
- * Rejette en 413 si le corps dépasse `maxBodySize` (avant ou pendant l'attente).
58
+ * Rejette avec la cause de {@link Parser.fail} (413 dépassement de
59
+ * `maxBodySize`, 408 délai de réception), avant ou pendant l'attente.
42
60
  */
43
61
  ended() {
44
- if (this.overflow) return Promise.reject(this.overflow);
62
+ if (this.failure) return Promise.reject(this.failure);
45
63
  const req = this.request.request;
46
64
  return new Promise((resolve, reject) => {
47
65
  if (req.readableEnded || req.complete) return resolve();
48
66
  const cleanup = () => {
49
67
  req.removeListener("end", onEnd);
50
68
  req.removeListener("error", onError);
51
- this.onOverflow = null;
69
+ this.onFailure = null;
52
70
  };
53
71
  const onEnd = () => {
54
72
  cleanup();
@@ -58,7 +76,7 @@ var Parser = class {
58
76
  cleanup();
59
77
  reject(e instanceof Error ? e : new Error(String(e)));
60
78
  };
61
- this.onOverflow = (err) => {
79
+ this.onFailure = (err) => {
62
80
  cleanup();
63
81
  reject(err);
64
82
  };