@nodefony/http 10.0.0-beta.6 → 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/dist/_virtual/{_@oxc-project_runtime@0.157.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.157.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorateParam.js +1 -1
- package/dist/index.js +3 -2
- package/dist/nodefony/config/config.js +25 -2
- 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/audit-logger.js +52 -6
- package/dist/nodefony/service/error-renderer.js +41 -9
- package/dist/nodefony/service/http-kernel.js +198 -19
- 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 +1 -1
- package/dist/nodefony/service/servers/clientError.js +26 -1
- package/dist/nodefony/service/servers/server-h2c.js +3 -3
- package/dist/nodefony/service/servers/server-http.js +3 -3
- package/dist/nodefony/service/servers/server-https.js +4 -4
- package/dist/nodefony/service/servers/server-websocket-secure.js +3 -3
- package/dist/nodefony/service/servers/server-websocket.js +3 -3
- package/dist/nodefony/service/servers/serverShutdown.js +14 -36
- package/dist/nodefony/service/sessions/sessions-service.js +6 -6
- package/dist/nodefony/service/upload/upload-service.js +2 -2
- package/dist/nodefony/src/context/Context.js +12 -1
- package/dist/nodefony/src/context/http/ByteStream.js +9 -4
- package/dist/nodefony/src/context/http/HttpContext.js +86 -8
- package/dist/nodefony/src/context/http/Request.js +96 -16
- package/dist/nodefony/src/context/http/Response.js +120 -17
- package/dist/nodefony/src/context/http/SseStream.js +4 -4
- package/dist/nodefony/src/context/http/StreamLifecycle.js +328 -19
- package/dist/nodefony/src/context/http/parser.js +29 -11
- package/dist/nodefony/src/context/http/webResponse.js +56 -0
- package/dist/nodefony/src/context/http2/Response.js +47 -8
- package/dist/nodefony/src/context/requestTrace.js +6 -2
- package/dist/nodefony/src/context/rpcOutcome.js +20 -0
- package/dist/nodefony/src/context/websocket/Response.js +11 -1
- package/dist/nodefony/src/context/websocket/WebsocketContext.js +15 -4
- package/dist/nodefony/src/cookies/cookie.js +3 -1
- 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/aggregate.js +2 -2
- package/dist/nodefony/src/proxy/forward.js +1 -1
- package/dist/nodefony/src/proxy/generateProxyConfig.js +91 -15
- 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/types/index.d.ts +5 -2
- package/dist/types/nodefony/config/config.d.ts +10 -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/IUpload.d.ts +51 -4
- package/dist/types/nodefony/interfaces/index.d.ts +2 -1
- package/dist/types/nodefony/service/audit-logger.d.ts +33 -3
- package/dist/types/nodefony/service/error-renderer.d.ts +22 -1
- package/dist/types/nodefony/service/http-kernel.d.ts +21 -1
- package/dist/types/nodefony/service/request-logger.d.ts +10 -0
- package/dist/types/nodefony/service/servers/serverShutdown.d.ts +2 -2
- package/dist/types/nodefony/src/context/Context.d.ts +9 -1
- package/dist/types/nodefony/src/context/http/ByteStream.d.ts +1 -1
- package/dist/types/nodefony/src/context/http/HttpContext.d.ts +56 -2
- package/dist/types/nodefony/src/context/http/Request.d.ts +38 -8
- package/dist/types/nodefony/src/context/http/Response.d.ts +61 -7
- package/dist/types/nodefony/src/context/http/SseStream.d.ts +1 -1
- package/dist/types/nodefony/src/context/http/StreamLifecycle.d.ts +53 -0
- package/dist/types/nodefony/src/context/http/parser.d.ts +17 -3
- package/dist/types/nodefony/src/context/http/webResponse.d.ts +31 -0
- package/dist/types/nodefony/src/context/http2/Response.d.ts +35 -2
- 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/websocket/Response.d.ts +6 -1
- package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +8 -0
- 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/proxy/forward.d.ts +7 -0
- package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +15 -2
- 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/docs/byte-stream.md +52 -17
- package/docs/diagnostics-channel.md +45 -1
- package/docs/servers.md +57 -44
- package/docs/upload.md +43 -17
- package/package.json +26 -6
|
@@ -3,6 +3,41 @@ import { thenMaybe, typeOf } from "nodefony";
|
|
|
3
3
|
import http, { validateHeaderName, validateHeaderValue } from "node:http";
|
|
4
4
|
import mime from "mime-types";
|
|
5
5
|
// nodefony/src/context/http/Response.ts
|
|
6
|
+
/**
|
|
7
|
+
* Plafond, en octets, d'un corps de requête HTTP/1.1 non lu que le serveur
|
|
8
|
+
* accepte de vider pour garder la connexion réutilisable — 256 Kio, le geste du
|
|
9
|
+
* serveur HTTP de Go que Connect reprend (ADR-0026, D5). Au-delà, la réponse
|
|
10
|
+
* ferme la connexion.
|
|
11
|
+
*/
|
|
12
|
+
const UNREAD_BODY_DRAIN_LIMIT = 262144;
|
|
13
|
+
/**
|
|
14
|
+
* Silence, en millisecondes, au bout duquel une connexion HTTP/1.1 fermée sur
|
|
15
|
+
* un corps non lu est détruite — 5 s, le `lingering_timeout` par défaut de
|
|
16
|
+
* nginx. La durée TOTALE reste bornée par le délai de réception de Node
|
|
17
|
+
* (`requestTimeout`), qui court tant que le corps n'est pas fini.
|
|
18
|
+
*/
|
|
19
|
+
const UNREAD_BODY_LINGER_MS = 5e3;
|
|
20
|
+
/**
|
|
21
|
+
* Fermeture ATTARDÉE (RFC 9112 §9.6) d'une connexion qui part en
|
|
22
|
+
* `connection: close` avec un corps encore en route : la réponse écrite, le
|
|
23
|
+
* serveur ferme son côté écriture et continue de LIRE — Node jette les octets
|
|
24
|
+
* du corps non consommé — jusqu'à ce que le client ferme, ou après
|
|
25
|
+
* {@link UNREAD_BODY_LINGER_MS} de silence.
|
|
26
|
+
*
|
|
27
|
+
* Sans elle, Node détruit la socket aussitôt la réponse écrite
|
|
28
|
+
* (`destroySoon`) : le client qui écrit encore son corps prend une
|
|
29
|
+
* réinitialisation (`EPIPE`, `ECONNRESET`) et ne lit jamais la réponse — un
|
|
30
|
+
* 413 sur un envoi de 5 Mo devenait une erreur réseau. Posée sur CETTE socket
|
|
31
|
+
* seulement, qui ne sert plus d'autre requête.
|
|
32
|
+
*
|
|
33
|
+
* @param socket - la socket de la connexion qui se ferme.
|
|
34
|
+
*/
|
|
35
|
+
function lingerBeforeClose(socket) {
|
|
36
|
+
socket.destroySoon = () => {
|
|
37
|
+
socket.setTimeout(UNREAD_BODY_LINGER_MS, () => socket.destroy());
|
|
38
|
+
socket.end();
|
|
39
|
+
};
|
|
40
|
+
}
|
|
6
41
|
const ANSI_REGEX = new RegExp(["[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)", "(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))"].join("|"), "g");
|
|
7
42
|
const stripAnsi = function(val) {
|
|
8
43
|
return typeof val === "string" ? val.replace(ANSI_REGEX, "") : val;
|
|
@@ -100,7 +135,7 @@ var HttpResponse = class {
|
|
|
100
135
|
flushing = false;
|
|
101
136
|
encoding = "utf-8";
|
|
102
137
|
#text = null;
|
|
103
|
-
#textEncoding = "
|
|
138
|
+
#textEncoding = "utf8";
|
|
104
139
|
#buffer = null;
|
|
105
140
|
contentType = "application/octet-stream";
|
|
106
141
|
headers = {};
|
|
@@ -127,6 +162,11 @@ var HttpResponse = class {
|
|
|
127
162
|
* le transport y est déjà, il ne s'amorce plus.
|
|
128
163
|
*/
|
|
129
164
|
#committed = false;
|
|
165
|
+
/**
|
|
166
|
+
* Méthode `setHeader` native retenue pendant que l'interception du repli
|
|
167
|
+
* statique est armée ; `null` sinon (et sur toute réponse routée).
|
|
168
|
+
*/
|
|
169
|
+
#nativeSetHeader = null;
|
|
130
170
|
constructor(response, context) {
|
|
131
171
|
this.context = context;
|
|
132
172
|
this.response = response;
|
|
@@ -387,19 +427,37 @@ var HttpResponse = class {
|
|
|
387
427
|
else list[at + 1] = value;
|
|
388
428
|
}
|
|
389
429
|
/**
|
|
430
|
+
* Range les en-têtes de corrélation du contexte — `X-Request-Id` et le
|
|
431
|
+
* `traceparent` W3C — que toute réponse renvoie, quel qu'en soit l'émetteur.
|
|
432
|
+
* Validés par Node à la remise, comme le reste de la réserve.
|
|
433
|
+
*/
|
|
434
|
+
putCorrelationHeaders() {
|
|
435
|
+
if (this.context.requestId) this.#put("x-request-id", this.context.requestId);
|
|
436
|
+
if (this.context.traceparent) this.#put("traceparent", this.context.traceparent);
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
390
439
|
* Recopie la réserve sur la réponse Node, pour un émetteur qui écrit sans
|
|
391
|
-
* passer par {@link writeHead} — fichier statique, relais vers un amont
|
|
392
|
-
* réserve se vide ; ce qui se pose ensuite y retourne, et
|
|
393
|
-
* deux au `writeHead
|
|
440
|
+
* passer par {@link writeHead} — fichier statique, relais vers un amont,
|
|
441
|
+
* adaptateur RPC. La réserve se vide ; ce qui se pose ensuite y retourne, et
|
|
442
|
+
* Node fusionne les deux au `writeHead` (l'émetteur l'emporte, nom par nom).
|
|
443
|
+
*
|
|
444
|
+
* @param contentType - type de la réponse que l'émetteur écrira, quand il
|
|
445
|
+
* est connu d'avance (adaptateur RPC). Omis : il se lit à l'extension du
|
|
446
|
+
* chemin, comme le serveur statique le fera.
|
|
394
447
|
*/
|
|
395
|
-
commitHeaders() {
|
|
448
|
+
commitHeaders(contentType) {
|
|
396
449
|
const res = this.response;
|
|
397
450
|
if (res === null || res.headersSent) return;
|
|
398
451
|
if (this.headerPolicy !== null) {
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
452
|
+
let type = contentType;
|
|
453
|
+
if (type === void 0) {
|
|
454
|
+
const pathname = this.context.request.pathname;
|
|
455
|
+
const found = pathname.endsWith("/") ? false : mime.lookup(pathname);
|
|
456
|
+
if (found !== false) type = found;
|
|
457
|
+
}
|
|
458
|
+
this.applyHeaderPolicy(type, true);
|
|
402
459
|
}
|
|
460
|
+
this.putCorrelationHeaders();
|
|
403
461
|
const list = this.#store();
|
|
404
462
|
for (let i = 0; i + 1 < list.length; i += 2) {
|
|
405
463
|
const name = list[i];
|
|
@@ -416,15 +474,22 @@ var HttpResponse = class {
|
|
|
416
474
|
* rien, sa réponse part ensuite en un seul `writeHead`. À désarmer par
|
|
417
475
|
* {@link disarmNativeWrite} quand le tiers rend la main sans avoir écrit.
|
|
418
476
|
*
|
|
419
|
-
* L'interception est une propriété PROPRE de la réponse,
|
|
420
|
-
*
|
|
477
|
+
* L'interception est une propriété PROPRE de la réponse, posée une fois avec
|
|
478
|
+
* des attributs constants — toutes les réponses interceptées partagent donc
|
|
479
|
+
* la même forme — et désarmée en lui RÉAFFECTANT la méthode native, jamais
|
|
480
|
+
* en la supprimant : un `delete` fait basculer la réponse Node en mode
|
|
481
|
+
* dictionnaire (constaté par `%HasFastProperties` sur Node 26, 200 réponses
|
|
482
|
+
* sur 200), et chaque accès suivant de `node:http` le paie — mesuré in situ
|
|
483
|
+
* sur un 404 : `writeHead` +1,1 µs, `end` +4,3 µs.
|
|
421
484
|
*/
|
|
422
485
|
commitBeforeNativeWrite() {
|
|
423
486
|
const res = this.response;
|
|
424
|
-
if (res === null || res.headersSent ||
|
|
487
|
+
if (res === null || res.headersSent || this.#nativeSetHeader !== null) return;
|
|
425
488
|
const native = res.setHeader;
|
|
489
|
+
this.#nativeSetHeader = native;
|
|
426
490
|
Object.defineProperty(res, "setHeader", {
|
|
427
491
|
configurable: true,
|
|
492
|
+
enumerable: false,
|
|
428
493
|
writable: true,
|
|
429
494
|
value: (name, value) => {
|
|
430
495
|
this.disarmNativeWrite();
|
|
@@ -433,10 +498,17 @@ var HttpResponse = class {
|
|
|
433
498
|
}
|
|
434
499
|
});
|
|
435
500
|
}
|
|
436
|
-
/**
|
|
501
|
+
/**
|
|
502
|
+
* Retire l'interception posée par {@link commitBeforeNativeWrite} : la
|
|
503
|
+
* propriété propre reçoit la méthode native — la forme de l'objet ne change
|
|
504
|
+
* pas, la réponse reste en mode rapide pour V8.
|
|
505
|
+
*/
|
|
437
506
|
disarmNativeWrite() {
|
|
507
|
+
const native = this.#nativeSetHeader;
|
|
508
|
+
if (native === null) return;
|
|
509
|
+
this.#nativeSetHeader = null;
|
|
438
510
|
const res = this.response;
|
|
439
|
-
if (res !== null
|
|
511
|
+
if (res !== null) Object.defineProperty(res, "setHeader", { value: native });
|
|
440
512
|
}
|
|
441
513
|
/**
|
|
442
514
|
* Retire un en-tête, de la réserve comme de la réponse Node.
|
|
@@ -595,7 +667,7 @@ var HttpResponse = class {
|
|
|
595
667
|
#setText(text, encoding) {
|
|
596
668
|
this.#buffer = null;
|
|
597
669
|
this.#text = text;
|
|
598
|
-
this.#textEncoding = encoding;
|
|
670
|
+
this.#textEncoding = encoding === "utf-8" ? "utf8" : encoding;
|
|
599
671
|
}
|
|
600
672
|
setLength(body) {
|
|
601
673
|
if (this.response?.headersSent) throw new Error("Headers already sended");
|
|
@@ -625,8 +697,7 @@ var HttpResponse = class {
|
|
|
625
697
|
}
|
|
626
698
|
this.statusMessage = this.getStatusMessage();
|
|
627
699
|
if (headers) this.#apply(headers);
|
|
628
|
-
|
|
629
|
-
if (this.context.traceparent) this.#put("traceparent", this.context.traceparent);
|
|
700
|
+
this.putCorrelationHeaders();
|
|
630
701
|
this.setLength();
|
|
631
702
|
this.ensureContentTypeHeader();
|
|
632
703
|
this.applyHeaderPolicy(this.contentTypeForPolicy());
|
|
@@ -750,6 +821,38 @@ var HttpResponse = class {
|
|
|
750
821
|
return this.response.end(chunk, encoding || this.encoding);
|
|
751
822
|
}
|
|
752
823
|
/**
|
|
824
|
+
* Règle le sort d'un corps de requête resté NON LU au moment de répondre —
|
|
825
|
+
* route inconnue (404, 405), refus avant l'action, 408, 413. À appeler AVANT
|
|
826
|
+
* l'écriture des en-têtes ; sans effet si le corps est entièrement reçu.
|
|
827
|
+
*
|
|
828
|
+
* HTTP/1.1 : Node vide lui-même un corps non consommé une fois la réponse
|
|
829
|
+
* écrite, pour réutiliser la connexion — sans borne. On ne le laisse faire
|
|
830
|
+
* que pour un corps DÉCLARÉ d'au plus {@link UNREAD_BODY_DRAIN_LIMIT}
|
|
831
|
+
* octets ; au-delà, ou sans longueur déclarée (`transfer-encoding`), la
|
|
832
|
+
* réponse part avec `connection: close` (RFC 9112 §9.6) et la connexion
|
|
833
|
+
* n'est pas réutilisée — fermée par étapes ({@link lingerBeforeClose}),
|
|
834
|
+
* pour que le client qui écrit encore lise la réponse. La décision se prend
|
|
835
|
+
* sur la longueur ANNONCÉE, sans attendre un octet : la réponse n'est jamais
|
|
836
|
+
* retardée par le corps.
|
|
837
|
+
*
|
|
838
|
+
* HTTP/2 surcharge cette méthode (`Http2Response`) : fermeture du flux en
|
|
839
|
+
* `NO_ERROR` une fois la réponse écrite. Une seule entrée pour les deux
|
|
840
|
+
* protocoles, partagée par tout refus qui laisse un corps non lu (ADR-0026,
|
|
841
|
+
* D5).
|
|
842
|
+
*/
|
|
843
|
+
closeUnfinishedRequest() {
|
|
844
|
+
const response = this.response;
|
|
845
|
+
if (response === null || response.headersSent) return;
|
|
846
|
+
if (this.context.request.request.complete) return;
|
|
847
|
+
const headers = this.context.request.headers;
|
|
848
|
+
if (headers["transfer-encoding"] === void 0) {
|
|
849
|
+
if (Number(headers["content-length"] ?? 0) <= 262144) return;
|
|
850
|
+
}
|
|
851
|
+
this.setHeader("connection", "close");
|
|
852
|
+
const socket = response.socket;
|
|
853
|
+
if (socket !== null) lingerBeforeClose(socket);
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
753
856
|
* Valeur d'un en-tête posé — dans la réserve, sinon sur la réponse Node (ce
|
|
754
857
|
* qu'un tiers y a écrit directement : `send`, quota).
|
|
755
858
|
*
|
|
@@ -800,4 +903,4 @@ var HttpResponse = class {
|
|
|
800
903
|
return this.context.log(pci, severity, msgid, msg);
|
|
801
904
|
}
|
|
802
905
|
};
|
|
803
|
-
export { HttpResponse as default, isContentlessStatus };
|
|
906
|
+
export { UNREAD_BODY_DRAIN_LIMIT, UNREAD_BODY_LINGER_MS, HttpResponse as default, isContentlessStatus };
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { MAX_TIMER_MS, checkStreamTimer, openStreamLifecycle } from "./StreamLifecycle.js";
|
|
1
|
+
import { MAX_TIMER_MS, STREAM_STALL_TIMEOUT_MS, checkStreamTimer, openStreamLifecycle } from "./StreamLifecycle.js";
|
|
2
2
|
import http from "node:http";
|
|
3
3
|
// nodefony/src/context/http/SseStream.ts
|
|
4
4
|
/** Battement de cœur par défaut, en ms (§9.2.7 : « every 15 seconds or so »). */
|
|
5
5
|
const SSE_HEARTBEAT_MS = 15e3;
|
|
6
|
-
/** Délai de blocage d'écriture par défaut, en ms. */
|
|
7
|
-
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;
|
|
8
8
|
/** Taille maximale d'un événement par défaut, en octets (1 Mio). */
|
|
9
9
|
const SSE_MAX_EVENT_BYTES = 1048576;
|
|
10
10
|
/** Commentaire de battement de cœur — ignoré par tout client conforme. */
|
|
@@ -255,7 +255,7 @@ function openSseStream(context, options = {}) {
|
|
|
255
255
|
};
|
|
256
256
|
const opened = openStreamLifecycle(context, {
|
|
257
257
|
label: "SSE",
|
|
258
|
-
stallTimeout: options.stallTimeout ??
|
|
258
|
+
stallTimeout: options.stallTimeout ?? SSE_STALL_TIMEOUT_MS,
|
|
259
259
|
maxDuration: options.maxDuration ?? false
|
|
260
260
|
});
|
|
261
261
|
return opened instanceof Promise ? opened.then((lifecycle) => startSseStream(lifecycle, options, settings)) : startSseStream(opened, options, settings);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import Http2Response from "../http2/Response.js";
|
|
2
2
|
import HttpError from "../../errors/httpError.js";
|
|
3
|
-
import {
|
|
3
|
+
import { ANNOUNCE_CAP_MS } from "../../servers/http2Goaway.js";
|
|
4
|
+
import { REVOCATION_REVALIDATE_MS, RequestContext } from "nodefony";
|
|
4
5
|
import http from "node:http";
|
|
5
6
|
import { AsyncResource } from "node:async_hooks";
|
|
6
7
|
// nodefony/src/context/http/StreamLifecycle.ts
|
|
@@ -32,6 +33,29 @@ import { AsyncResource } from "node:async_hooks";
|
|
|
32
33
|
*/
|
|
33
34
|
const MAX_TIMER_MS = 2147483647;
|
|
34
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
|
+
/**
|
|
35
59
|
* Refuse une option de délai hors de ce qu'un minuteur sait tenir.
|
|
36
60
|
*
|
|
37
61
|
* @param label - préfixe du message (`SSE`, `Flux`).
|
|
@@ -46,6 +70,42 @@ function checkStreamTimer(label, name, value) {
|
|
|
46
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`);
|
|
47
71
|
}
|
|
48
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
|
+
/**
|
|
49
109
|
* Le cycle de vie d'UN flux ouvert sur une réponse.
|
|
50
110
|
*
|
|
51
111
|
* S'obtient par {@link openStreamLifecycle} ; le constructeur n'est appelé
|
|
@@ -53,7 +113,7 @@ function checkStreamTimer(label, name, value) {
|
|
|
53
113
|
*
|
|
54
114
|
* @internal
|
|
55
115
|
*/
|
|
56
|
-
var StreamLifecycle = class {
|
|
116
|
+
var StreamLifecycle = class StreamLifecycle {
|
|
57
117
|
context;
|
|
58
118
|
/** Où l'on écrit : la réponse HTTP/1.1, ou le flux HTTP/2. */
|
|
59
119
|
target;
|
|
@@ -62,6 +122,27 @@ var StreamLifecycle = class {
|
|
|
62
122
|
#stallTimeout;
|
|
63
123
|
#revalidate;
|
|
64
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;
|
|
65
146
|
#closed = false;
|
|
66
147
|
#cause = null;
|
|
67
148
|
/**
|
|
@@ -95,8 +176,7 @@ var StreamLifecycle = class {
|
|
|
95
176
|
this.#stall.unref();
|
|
96
177
|
return;
|
|
97
178
|
}
|
|
98
|
-
this.#
|
|
99
|
-
this.target.destroy();
|
|
179
|
+
this.#cutStalled();
|
|
100
180
|
};
|
|
101
181
|
constructor(context, target, response, settings) {
|
|
102
182
|
this.context = context;
|
|
@@ -105,12 +185,18 @@ var StreamLifecycle = class {
|
|
|
105
185
|
this.#stallTimeout = settings.stallTimeout;
|
|
106
186
|
this.#revalidate = settings.revalidate;
|
|
107
187
|
this.identifier = settings.identifier;
|
|
188
|
+
this.#foreignWriter = settings.foreignWriter;
|
|
189
|
+
this.#endsAtShutdown = settings.endsAtShutdown;
|
|
190
|
+
const kernel = context.httpKernel;
|
|
191
|
+
this.#kernel = kernel;
|
|
108
192
|
response.on("close", this.#onResponseClose);
|
|
109
193
|
if (settings.maxDuration !== false) {
|
|
110
194
|
this.#lifetime = setTimeout(() => void this.close(), settings.maxDuration);
|
|
111
195
|
this.#lifetime.unref();
|
|
112
196
|
}
|
|
113
|
-
if (this.#revalidate !== null)
|
|
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);
|
|
114
200
|
}
|
|
115
201
|
/** Le flux est-il fermé — par le serveur, ou par le client parti ? */
|
|
116
202
|
get closed() {
|
|
@@ -177,22 +263,104 @@ var StreamLifecycle = class {
|
|
|
177
263
|
this.#stallSince = 0;
|
|
178
264
|
}
|
|
179
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
|
+
/**
|
|
180
296
|
* Désarme le délai d'inactivité de la requête : un flux n'a pas de fin
|
|
181
297
|
* attendue, ses propres gardes le remplacent.
|
|
182
298
|
*/
|
|
183
299
|
disarmIdleTimeout() {
|
|
184
|
-
|
|
185
|
-
if (target instanceof http.ServerResponse) target.socket?.setTimeout(0);
|
|
186
|
-
else target.setTimeout(0);
|
|
300
|
+
disarmResponseTimeout(this.target);
|
|
187
301
|
}
|
|
188
|
-
/**
|
|
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
|
+
*/
|
|
189
307
|
close() {
|
|
190
308
|
if (this.#closed) return void 0;
|
|
191
309
|
this.#cause = "server";
|
|
192
310
|
this.finish();
|
|
311
|
+
if (this.#foreignWriter) {
|
|
312
|
+
this.target.destroy();
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
193
315
|
const done = this.context.close();
|
|
194
316
|
return done instanceof Promise ? done.then(() => void 0) : void 0;
|
|
195
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
|
+
}
|
|
196
364
|
/** Fin du flux : minuteurs, écouteurs et registre libérés, une seule fois. */
|
|
197
365
|
finish() {
|
|
198
366
|
if (this.#closed) return;
|
|
@@ -202,12 +370,22 @@ var StreamLifecycle = class {
|
|
|
202
370
|
clearTimeout(this.#lifetime);
|
|
203
371
|
this.#lifetime = null;
|
|
204
372
|
}
|
|
373
|
+
if (this.#revokeGrace !== null) {
|
|
374
|
+
clearTimeout(this.#revokeGrace);
|
|
375
|
+
this.#revokeGrace = null;
|
|
376
|
+
}
|
|
205
377
|
this.#stallSince = 0;
|
|
206
378
|
if (this.#stall !== null) {
|
|
207
379
|
clearTimeout(this.#stall);
|
|
208
380
|
this.#stall = null;
|
|
209
381
|
}
|
|
210
|
-
|
|
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
|
+
}
|
|
211
389
|
const release = this.#release;
|
|
212
390
|
this.#release = null;
|
|
213
391
|
release?.();
|
|
@@ -217,6 +395,139 @@ var StreamLifecycle = class {
|
|
|
217
395
|
}
|
|
218
396
|
};
|
|
219
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
|
+
/**
|
|
220
531
|
* Ouvre le cycle de vie d'un flux sur la réponse d'un contexte HTTP.
|
|
221
532
|
*
|
|
222
533
|
* Prend la réponse, pose l'écoute de sa fermeture, puis sauve la session —
|
|
@@ -237,14 +548,10 @@ var StreamLifecycle = class {
|
|
|
237
548
|
function openStreamLifecycle(context, options) {
|
|
238
549
|
const { label } = options;
|
|
239
550
|
if (context.sended || context.finished || context.response.isHeaderSent()) throw new HttpError(`${label} : la réponse est déjà partie`, 500);
|
|
240
|
-
const
|
|
241
|
-
const nodeResponse = response.response;
|
|
551
|
+
const nodeResponse = context.response.response;
|
|
242
552
|
if (nodeResponse === null) throw new HttpError(`${label} : réponse introuvable`, 500, context);
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
if (stream !== null) target = stream;
|
|
246
|
-
else if (nodeResponse instanceof http.ServerResponse) target = nodeResponse;
|
|
247
|
-
else throw new HttpError(`${label} : transport de réponse inconnu`, 500, context);
|
|
553
|
+
const target = resolveStreamTarget(context);
|
|
554
|
+
if (target === null) throw new HttpError(`${label} : transport de réponse inconnu`, 500, context);
|
|
248
555
|
const gone = target.destroyed;
|
|
249
556
|
const kernel = context.httpKernel;
|
|
250
557
|
if (!gone && kernel !== null && !kernel.acquireConnectionSlot(context.request.request, nodeResponse)) throw new HttpError(`${label} : trop de connexions ouvertes depuis cette adresse`, 429, context);
|
|
@@ -255,7 +562,9 @@ function openStreamLifecycle(context, options) {
|
|
|
255
562
|
stallTimeout: options.stallTimeout,
|
|
256
563
|
maxDuration: options.maxDuration,
|
|
257
564
|
revalidate,
|
|
258
|
-
identifier
|
|
565
|
+
identifier,
|
|
566
|
+
foreignWriter: options.foreignWriter ?? false,
|
|
567
|
+
endsAtShutdown: options.endsAtShutdown ?? true
|
|
259
568
|
});
|
|
260
569
|
if (gone) {
|
|
261
570
|
lifecycle.finish();
|
|
@@ -270,4 +579,4 @@ function openStreamLifecycle(context, options) {
|
|
|
270
579
|
return lifecycle;
|
|
271
580
|
});
|
|
272
581
|
}
|
|
273
|
-
export { MAX_TIMER_MS, StreamLifecycle, checkStreamTimer, openStreamLifecycle };
|
|
582
|
+
export { MAX_TIMER_MS, SHUTDOWN_STREAMS_WAIT_MS, STALL_SWEEP_PERIOD_MS, STREAM_STALL_TIMEOUT_MS, ShutdownRegistry, StallSweep, StreamLifecycle, checkStreamTimer, disarmResponseTimeout, openStreamLifecycle, resolveStreamTarget };
|