@nodefony/http 10.0.0-beta.2 → 10.0.0-beta.4
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.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
- package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorateParam.js +1 -2
- package/dist/index.js +19 -11
- package/dist/nodefony/command/assetsPublishCommand.js +5 -3
- package/dist/nodefony/command/certificatesCommand.js +5 -3
- package/dist/nodefony/command/networkCommand.js +5 -3
- package/dist/nodefony/command/proxyGenerateCommand.js +8 -3
- package/dist/nodefony/config/config.js +45 -6
- package/dist/nodefony/config/defineModuleConfig.js +2 -3
- package/dist/nodefony/interfaces/IReverseProxy.js +1 -0
- package/dist/nodefony/service/HttpAdminApi.js +2 -3
- package/dist/nodefony/service/ProfilerAdminApi.js +1 -2
- package/dist/nodefony/service/audit-logger.js +22 -16
- package/dist/nodefony/service/certificates.js +228 -19
- package/dist/nodefony/service/error-renderer.js +30 -13
- package/dist/nodefony/service/http-kernel.js +331 -82
- package/dist/nodefony/service/pretty-request-logger.js +5 -4
- package/dist/nodefony/service/request-logger.js +24 -16
- package/dist/nodefony/service/reverse-proxy.js +336 -0
- package/dist/nodefony/service/servers/clientError.js +22 -3
- package/dist/nodefony/service/servers/server-h2c.js +126 -0
- package/dist/nodefony/service/servers/server-http.js +5 -8
- package/dist/nodefony/service/servers/server-https.js +12 -24
- package/dist/nodefony/service/servers/server-static.js +7 -10
- package/dist/nodefony/service/servers/server-websocket-secure.js +18 -18
- package/dist/nodefony/service/servers/server-websocket.js +18 -18
- package/dist/nodefony/service/servers/serverShutdown.js +78 -18
- package/dist/nodefony/service/servers/wsHeartbeat.js +32 -17
- package/dist/nodefony/service/sessions/sessions-service.js +34 -20
- package/dist/nodefony/service/trace.js +1 -2
- package/dist/nodefony/service/upload/upload-service.js +5 -8
- package/dist/nodefony/service/x509.js +1 -2
- package/dist/nodefony/src/assets/collectAssets.js +1 -2
- package/dist/nodefony/src/assets/prebuiltUi.js +1 -2
- package/dist/nodefony/src/context/Context.js +51 -32
- package/dist/nodefony/src/context/domainMatcher.js +21 -4
- package/dist/nodefony/src/context/forwarded.js +3 -4
- package/dist/nodefony/src/context/http/HttpContext.js +98 -46
- package/dist/nodefony/src/context/http/Request.js +34 -38
- package/dist/nodefony/src/context/http/Response.js +39 -7
- package/dist/nodefony/src/context/http/SseStream.js +349 -0
- package/dist/nodefony/src/context/http/parser.js +36 -10
- package/dist/nodefony/src/context/http/urlFastPath.js +1 -2
- package/dist/nodefony/src/context/http2/Request.js +4 -8
- package/dist/nodefony/src/context/http2/Response.js +22 -9
- package/dist/nodefony/src/context/metaData.js +1 -2
- package/dist/nodefony/src/context/requestId.js +1 -2
- package/dist/nodefony/src/context/responseEnded.js +26 -0
- package/dist/nodefony/src/context/transport.js +45 -0
- package/dist/nodefony/src/context/trustProxy.js +1 -2
- package/dist/nodefony/src/context/websocket/Response.js +42 -6
- package/dist/nodefony/src/context/websocket/WebsocketContext.js +94 -20
- package/dist/nodefony/src/context/websocket/isWebsocketContext.js +17 -0
- package/dist/nodefony/src/context/websocket/websocketUrl.js +20 -0
- package/dist/nodefony/src/context/websocket/wsBackpressure.js +1 -2
- package/dist/nodefony/src/context/websocket/wsCloseReason.js +1 -2
- package/dist/nodefony/src/context/websocket/wsLogContent.js +1 -2
- package/dist/nodefony/src/cookies/cookie.js +3 -7
- package/dist/nodefony/src/errors/httpError.js +1 -2
- package/dist/nodefony/src/errors/thrownMessage.js +1 -2
- package/dist/nodefony/src/profiler/FrameProfile.js +1 -2
- package/dist/nodefony/src/profiler/Profiler.js +108 -30
- package/dist/nodefony/src/profiler/aggregate.js +422 -0
- package/dist/nodefony/src/proxy/forward.js +500 -0
- package/dist/nodefony/src/proxy/generateProxyConfig.js +14 -6
- package/dist/nodefony/src/proxy/rules.js +188 -0
- package/dist/nodefony/src/rateLimit/MemoryRateLimitStore.js +1 -2
- package/dist/nodefony/src/rateLimit/WsConnectionCounter.js +1 -2
- package/dist/nodefony/src/rateLimit/rateLimitFilters.js +1 -2
- package/dist/nodefony/src/servers/http2Limits.js +50 -0
- package/dist/nodefony/src/servers/kernelServers.js +12 -3
- package/dist/nodefony/src/servers/portBinder.js +41 -10
- package/dist/nodefony/src/servers/upgradeDispatch.js +128 -0
- package/dist/nodefony/src/session/session.js +30 -15
- package/dist/nodefony/src/session/sessionStoreFailure.js +1 -2
- package/dist/nodefony/src/session/storage/MemorySessionStorage.js +1 -2
- package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +1 -2
- package/dist/nodefony/src/session/storage/sessionFilters.js +1 -2
- package/dist/nodefony/src/session/storage/sessionSort.js +1 -2
- package/dist/types/index.d.ts +12 -3
- package/dist/types/nodefony/config/config.d.ts +16 -0
- package/dist/types/nodefony/interfaces/IContext.d.ts +65 -4
- package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +1 -1
- package/dist/types/nodefony/interfaces/IRequest.d.ts +9 -4
- package/dist/types/nodefony/interfaces/IReverseProxy.d.ts +67 -0
- package/dist/types/nodefony/interfaces/IRouting.d.ts +26 -0
- package/dist/types/nodefony/interfaces/ISecurity.d.ts +6 -0
- package/dist/types/nodefony/interfaces/index.d.ts +3 -2
- package/dist/types/nodefony/service/audit-logger.d.ts +2 -1
- package/dist/types/nodefony/service/certificates.d.ts +147 -5
- package/dist/types/nodefony/service/error-renderer.d.ts +32 -1
- package/dist/types/nodefony/service/http-kernel.d.ts +157 -15
- package/dist/types/nodefony/service/reverse-proxy.d.ts +156 -0
- package/dist/types/nodefony/service/servers/clientError.d.ts +16 -0
- package/dist/types/nodefony/service/servers/server-h2c.d.ts +51 -0
- package/dist/types/nodefony/service/servers/server-http.d.ts +3 -3
- package/dist/types/nodefony/service/servers/server-https.d.ts +4 -4
- package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +2 -3
- package/dist/types/nodefony/service/servers/server-websocket.d.ts +2 -3
- package/dist/types/nodefony/service/servers/serverShutdown.d.ts +30 -16
- package/dist/types/nodefony/service/servers/wsHeartbeat.d.ts +26 -0
- package/dist/types/nodefony/service/sessions/sessions-service.d.ts +19 -1
- package/dist/types/nodefony/src/context/Context.d.ts +35 -6
- package/dist/types/nodefony/src/context/domainMatcher.d.ts +11 -1
- package/dist/types/nodefony/src/context/http/HttpContext.d.ts +48 -1
- package/dist/types/nodefony/src/context/http/Request.d.ts +20 -15
- package/dist/types/nodefony/src/context/http/Response.d.ts +14 -0
- package/dist/types/nodefony/src/context/http/SseStream.d.ts +155 -0
- package/dist/types/nodefony/src/context/http/parser.d.ts +18 -2
- package/dist/types/nodefony/src/context/http2/Request.d.ts +1 -1
- package/dist/types/nodefony/src/context/http2/Response.d.ts +15 -5
- package/dist/types/nodefony/src/context/responseEnded.d.ts +20 -0
- package/dist/types/nodefony/src/context/transport.d.ts +53 -0
- package/dist/types/nodefony/src/context/websocket/Response.d.ts +18 -2
- package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +57 -7
- package/dist/types/nodefony/src/context/websocket/isWebsocketContext.d.ts +15 -0
- package/dist/types/nodefony/src/context/websocket/websocketUrl.d.ts +16 -0
- package/dist/types/nodefony/src/profiler/FrameProfile.d.ts +13 -12
- package/dist/types/nodefony/src/profiler/Profiler.d.ts +69 -15
- package/dist/types/nodefony/src/profiler/aggregate.d.ts +215 -0
- package/dist/types/nodefony/src/proxy/forward.d.ts +168 -0
- package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +16 -0
- package/dist/types/nodefony/src/proxy/rules.d.ts +88 -0
- package/dist/types/nodefony/src/servers/http2Limits.d.ts +37 -0
- package/dist/types/nodefony/src/servers/kernelServers.d.ts +10 -1
- package/dist/types/nodefony/src/servers/portBinder.d.ts +29 -5
- package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +69 -0
- package/dist/types/nodefony/src/session/session.d.ts +2 -0
- package/docs/cookies.md +85 -78
- package/docs/index.md +5 -1
- package/docs/observabilite.md +81 -81
- package/docs/rate-limit.md +83 -83
- package/docs/reverse-proxy.md +522 -0
- package/docs/servers.md +378 -230
- package/docs/session.md +180 -180
- package/docs/sse.md +287 -0
- package/docs/upload.md +120 -117
- package/package.json +10 -11
- package/dist/_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js +0 -6
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import HttpError from "../../errors/httpError.js";
|
|
2
2
|
import { hasForwardingHeaders, resolveForwarded } from "../forwarded.js";
|
|
3
|
-
import {
|
|
3
|
+
import { hostnameOf } from "../domainMatcher.js";
|
|
4
|
+
import { Parser, ParserJson, ParserQs, ParserRaw, ParserXml, acceptParser } from "./parser.js";
|
|
4
5
|
import { isCanonicalAuthority, splitTarget } from "./urlFastPath.js";
|
|
5
|
-
import { RequestContext,
|
|
6
|
+
import { RequestContext, useConfig } from "nodefony";
|
|
6
7
|
import { createHash, randomUUID } from "node:crypto";
|
|
7
8
|
import { URL } from "node:url";
|
|
8
9
|
import fs from "node:fs";
|
|
@@ -11,7 +12,7 @@ import os from "node:os";
|
|
|
11
12
|
import path from "node:path";
|
|
12
13
|
import QS from "qs";
|
|
13
14
|
import { Busboy } from "@fastify/busboy";
|
|
14
|
-
|
|
15
|
+
// nodefony/src/context/http/Request.ts
|
|
15
16
|
const reg = /(.*)[[][\]]$/u;
|
|
16
17
|
/**
|
|
17
18
|
* Supprime un fichier temporaire d'upload sans jamais faire échouer le nettoyage :
|
|
@@ -33,6 +34,7 @@ const CHARSET_ALIASES = {
|
|
|
33
34
|
"utf-16le": "utf16le"
|
|
34
35
|
};
|
|
35
36
|
const QUOTE_TRIM = /^["']|["']$/gu;
|
|
37
|
+
const NO_FILES = Object.freeze([]);
|
|
36
38
|
const parse = {
|
|
37
39
|
POST: true,
|
|
38
40
|
PUT: true,
|
|
@@ -52,15 +54,6 @@ function perfMark(field) {
|
|
|
52
54
|
const p = globalThis.__nfPerfProbe;
|
|
53
55
|
if (p && p.t0 !== 0n) p[field] += Number(process.hrtime.bigint() - p.t0);
|
|
54
56
|
}
|
|
55
|
-
/**
|
|
56
|
-
* Nom d'hôte d'une autorité brute (`host[:port]`) : tout ce qui précède le
|
|
57
|
-
* premier `:`. Même résultat que `host.split(":")[0]`, sans tableau (#508).
|
|
58
|
-
*/
|
|
59
|
-
function hostnameOf(host) {
|
|
60
|
-
if (!host) return "";
|
|
61
|
-
const colon = host.indexOf(":");
|
|
62
|
-
return colon === -1 ? host : host.slice(0, colon);
|
|
63
|
-
}
|
|
64
57
|
var HttpRequest = class {
|
|
65
58
|
context;
|
|
66
59
|
request;
|
|
@@ -112,7 +105,7 @@ var HttpRequest = class {
|
|
|
112
105
|
let u = this.#url;
|
|
113
106
|
if (u === null) {
|
|
114
107
|
u = this.getUrl(this.sUrl);
|
|
115
|
-
u.query = this.
|
|
108
|
+
u.query = this.query;
|
|
116
109
|
this.#url = u;
|
|
117
110
|
}
|
|
118
111
|
return u;
|
|
@@ -121,24 +114,28 @@ var HttpRequest = class {
|
|
|
121
114
|
this.#url = u;
|
|
122
115
|
}
|
|
123
116
|
parser = null;
|
|
124
|
-
queryPost = {};
|
|
125
|
-
queryGet = {};
|
|
126
|
-
queryFile = [];
|
|
127
|
-
query = {};
|
|
128
117
|
/**
|
|
129
|
-
* Le CORPS de la requête, parsé
|
|
130
|
-
* Fastify, NestJS)
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
118
|
+
* Le CORPS de la requête, parsé (JSON, urlencoded, XML, champs multipart) —
|
|
119
|
+
* nom universel de l'écosystème (Express, Fastify, NestJS) et celui
|
|
120
|
+
* d'`IAdminRequest.body`. Jamais un paramètre d'URL : ceux-là sont dans
|
|
121
|
+
* {@link query}, et les deux ne fusionnent pas.
|
|
122
|
+
*/
|
|
123
|
+
body = {};
|
|
124
|
+
/** Fichiers reçus, alloués au PREMIER fichier seulement (cf {@link files}). */
|
|
125
|
+
#uploads = null;
|
|
126
|
+
/**
|
|
127
|
+
* Les fichiers reçus dans un corps multipart, déjà écrits dans le répertoire
|
|
128
|
+
* temporaire — lus par `@UploadedFile()` / `@UploadedFiles()`. Les champs
|
|
129
|
+
* texte du même corps sont dans {@link body}.
|
|
136
130
|
*
|
|
137
|
-
*
|
|
131
|
+
* Lecture seule : une requête sans upload rend un vide partagé et gelé,
|
|
132
|
+
* aucune allocation.
|
|
138
133
|
*/
|
|
139
|
-
get
|
|
140
|
-
return this
|
|
134
|
+
get files() {
|
|
135
|
+
return this.#uploads ?? NO_FILES;
|
|
141
136
|
}
|
|
137
|
+
/** Les paramètres d'URL, seuls — jamais un champ du corps ({@link body}). */
|
|
138
|
+
query = {};
|
|
142
139
|
queryStringOptions;
|
|
143
140
|
charset = "utf8";
|
|
144
141
|
uploadOption = {};
|
|
@@ -193,7 +190,6 @@ var HttpRequest = class {
|
|
|
193
190
|
if (this.search) query = QS.parse(this.search.slice(1), this.queryStringOptions ?? {});
|
|
194
191
|
else query = {};
|
|
195
192
|
if (this.#url !== null) this.#url.query = query;
|
|
196
|
-
this.queryGet = query;
|
|
197
193
|
this.query = query;
|
|
198
194
|
if (PERF_PROBE_SUB) perfMark("reqQueryNs");
|
|
199
195
|
this.contentType = this.getContentType(this.request);
|
|
@@ -209,11 +205,11 @@ var HttpRequest = class {
|
|
|
209
205
|
* remplace le `once("onRequestEnd")` du ctor qui allouait 1 onceWrapper +
|
|
210
206
|
* 1 closure à CHAQUE requête.
|
|
211
207
|
*
|
|
212
|
-
* Il porte le CORPS (`
|
|
208
|
+
* Il porte le CORPS (`body`), jamais les paramètres d'URL : un seul sens de
|
|
213
209
|
* `body` dans tout le framework, celui de l'écosystème et d'`IAdminRequest`.
|
|
214
210
|
*/
|
|
215
211
|
fireRequestEnd() {
|
|
216
|
-
this.request.body = this.
|
|
212
|
+
this.request.body = this.body;
|
|
217
213
|
if (this.context.listenerCount("onRequestEnd") === 0) return false;
|
|
218
214
|
return this.context.fireAsync("onRequestEnd", this);
|
|
219
215
|
}
|
|
@@ -323,7 +319,7 @@ var HttpRequest = class {
|
|
|
323
319
|
await this.fireRequestEnd();
|
|
324
320
|
return BODY_DONE;
|
|
325
321
|
}
|
|
326
|
-
this.parser = new
|
|
322
|
+
this.parser = new ParserRaw(this);
|
|
327
323
|
return this.parser;
|
|
328
324
|
}
|
|
329
325
|
}
|
|
@@ -370,8 +366,7 @@ var HttpRequest = class {
|
|
|
370
366
|
return this.parser;
|
|
371
367
|
}
|
|
372
368
|
const { fields, files } = await this.streamMultipart(bb);
|
|
373
|
-
this.
|
|
374
|
-
this.query = extend({}, this.query, this.queryPost);
|
|
369
|
+
this.body = fields;
|
|
375
370
|
for (const pf of files) await this.createFileUpload(pf.field, pf.file);
|
|
376
371
|
this.context.requestEnded = true;
|
|
377
372
|
await this.fireRequestEnd();
|
|
@@ -486,7 +481,7 @@ var HttpRequest = class {
|
|
|
486
481
|
}
|
|
487
482
|
accepts(Type) {
|
|
488
483
|
let parts = [];
|
|
489
|
-
let subtype
|
|
484
|
+
let subtype;
|
|
490
485
|
let type = "*";
|
|
491
486
|
if (Type) parts = Type.split("/");
|
|
492
487
|
switch (parts.length) {
|
|
@@ -504,7 +499,7 @@ var HttpRequest = class {
|
|
|
504
499
|
}
|
|
505
500
|
async createFileUpload(name, file) {
|
|
506
501
|
const fileUpload = await this.context.uploadService?.createUploadFile(file, name);
|
|
507
|
-
if (fileUpload) this.
|
|
502
|
+
if (fileUpload) (this.#uploads ??= []).push(fileUpload);
|
|
508
503
|
return fileUpload;
|
|
509
504
|
}
|
|
510
505
|
getMethod() {
|
|
@@ -569,7 +564,9 @@ var HttpRequest = class {
|
|
|
569
564
|
/**
|
|
570
565
|
* Scheme effectif côté client : `Forwarded`/`X-Forwarded-*` résolu de façon
|
|
571
566
|
* canonique (this.forwarded, gated proxy de confiance) ; sinon le transport
|
|
572
|
-
* réel. HTTP/2
|
|
567
|
+
* réel. Vaut pour HTTP/2 aussi : le pseudo-en-tête `:scheme` vient du client
|
|
568
|
+
* (RFC 9113 §8.3.1) et n'est jamais cru — sur h2c, `:scheme: https` ferait
|
|
569
|
+
* passer une connexion claire pour chiffrée.
|
|
573
570
|
*/
|
|
574
571
|
resolveScheme(request) {
|
|
575
572
|
if (this.forwarded?.proto) return this.forwarded.proto;
|
|
@@ -597,9 +594,8 @@ var HttpRequest = class {
|
|
|
597
594
|
return new URL(sUrl, baseUrl);
|
|
598
595
|
}
|
|
599
596
|
log(pci, severity, msgid, msg) {
|
|
600
|
-
if (!msgid) msgid = `${this.context.
|
|
597
|
+
if (!msgid) msgid = `${this.context.serverType} REQUEST `;
|
|
601
598
|
return this.context.log(pci, severity, msgid, msg);
|
|
602
599
|
}
|
|
603
600
|
};
|
|
604
|
-
//#endregion
|
|
605
601
|
export { HttpRequest as default };
|
|
@@ -2,7 +2,7 @@ import Cookie from "../../cookies/cookie.js";
|
|
|
2
2
|
import { thenMaybe, typeOf } from "nodefony";
|
|
3
3
|
import http from "node:http";
|
|
4
4
|
import mime from "mime-types";
|
|
5
|
-
|
|
5
|
+
// nodefony/src/context/http/Response.ts
|
|
6
6
|
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
7
|
const stripAnsi = function(val) {
|
|
8
8
|
return typeof val === "string" ? val.replace(ANSI_REGEX, "") : val;
|
|
@@ -55,16 +55,32 @@ var HttpResponse = class {
|
|
|
55
55
|
constructor(response, context) {
|
|
56
56
|
this.context = context;
|
|
57
57
|
this.response = response;
|
|
58
|
-
|
|
58
|
+
const responseTimeout = this.context.httpKernel?.responseTimeout;
|
|
59
|
+
this.timeout = this.context.serverType === "https" ? responseTimeout?.https : responseTimeout?.http;
|
|
59
60
|
}
|
|
60
61
|
/**
|
|
61
62
|
* Filet posé juste avant l'émission des en-têtes : si aucun `Content-Type`
|
|
62
63
|
* n'a été choisi (ni négociation, ni `render`, ni controller), émet le défaut
|
|
63
64
|
* `this.contentType` (application/octet-stream) — comportement identique à
|
|
64
65
|
* l'ancienne pose au constructeur, sans le ping-pong set/remove/re-set.
|
|
66
|
+
*
|
|
67
|
+
* Une réponse SANS corps n'annonce aucun type : `Content-Type` décrit le
|
|
68
|
+
* contenu (RFC 9110 §8.3), et il n'y en a pas.
|
|
65
69
|
*/
|
|
66
70
|
ensureContentTypeHeader() {
|
|
67
|
-
if (this.response && !this.response.hasHeader("content-type")) this.response.setHeader("content-type", this.contentType);
|
|
71
|
+
if (this.response && !this.response.hasHeader("content-type") && !this.#isBodyless()) this.response.setHeader("content-type", this.contentType);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Vrai quand la réponse n'a pas de corps : 204 et 304 n'en ont jamais
|
|
75
|
+
* (RFC 9110 §15.3.5, §15.4.5) ; ailleurs, un corps posé et VIDE. Un corps
|
|
76
|
+
* jamais posé ne dit rien — un flux peut suivre l'en-tête — et `HEAD` rend
|
|
77
|
+
* les en-têtes du `GET` (§9.3.2) : tous deux gardent le défaut.
|
|
78
|
+
*/
|
|
79
|
+
#isBodyless() {
|
|
80
|
+
if (NO_CONTENT_LENGTH_STATUS.has(this.statusCode)) return true;
|
|
81
|
+
if (this.context.method === "HEAD") return false;
|
|
82
|
+
if (this.#text !== null) return this.#text === "" && !this.flushing;
|
|
83
|
+
return this.#buffer?.length === 0 && !this.flushing;
|
|
68
84
|
}
|
|
69
85
|
/**
|
|
70
86
|
* Corps de la réponse en octets. Un corps posé en texte n'est encodé qu'à la
|
|
@@ -138,16 +154,33 @@ var HttpResponse = class {
|
|
|
138
154
|
const serialized = [];
|
|
139
155
|
for (const cookie of cookies) {
|
|
140
156
|
const s = cookie.serialize();
|
|
141
|
-
this.
|
|
157
|
+
this.logCookie(cookie, s);
|
|
142
158
|
serialized.push(s);
|
|
143
159
|
}
|
|
144
160
|
return this.setHeader("Set-Cookie", serialized);
|
|
145
161
|
}
|
|
146
162
|
setCookie(cookie) {
|
|
147
163
|
const serialize = cookie.serialize();
|
|
148
|
-
this.
|
|
164
|
+
this.logCookie(cookie, serialize);
|
|
149
165
|
return this.setHeader("Set-Cookie", serialize);
|
|
150
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* Journalise en DEBUG un cookie posé, sa VALEUR MASQUÉE : nom et attributs
|
|
169
|
+
* seulement. Le journal de développement se lit par le plan d'administration
|
|
170
|
+
* — un identifiant de session y entrerait chez quiconque le consulte.
|
|
171
|
+
* La ligne n'est construite que si DEBUG passe le gate du syslog pour ce
|
|
172
|
+
* `msgid` (debug ciblé compris) : rien n'est alloué en production.
|
|
173
|
+
*
|
|
174
|
+
* @param cookie - le cookie posé.
|
|
175
|
+
* @param serialized - sa forme `Set-Cookie`, déjà calculée pour l'en-tête.
|
|
176
|
+
*/
|
|
177
|
+
logCookie(cookie, serialized) {
|
|
178
|
+
const msgid = `${this.context.serverType} RESPONSE `;
|
|
179
|
+
const syslog = this.context.syslog;
|
|
180
|
+
if (syslog && !syslog.severityEnabled("DEBUG", msgid)) return;
|
|
181
|
+
const attrs = serialized.indexOf("; ", cookie.name.length + 1);
|
|
182
|
+
this.log(`ADD COOKIE ==> ${cookie.name}=<masqué>${attrs === -1 ? "" : serialized.slice(attrs)}`, "DEBUG", msgid);
|
|
183
|
+
}
|
|
151
184
|
setHeader(name, value) {
|
|
152
185
|
if (this.response) {
|
|
153
186
|
if (this.flushing) {
|
|
@@ -444,9 +477,8 @@ var HttpResponse = class {
|
|
|
444
477
|
return this;
|
|
445
478
|
}
|
|
446
479
|
log(pci, severity, msgid, msg) {
|
|
447
|
-
if (!msgid) msgid = `${this.context.
|
|
480
|
+
if (!msgid) msgid = `${this.context.serverType} RESPONSE `;
|
|
448
481
|
return this.context.log(pci, severity, msgid, msg);
|
|
449
482
|
}
|
|
450
483
|
};
|
|
451
|
-
//#endregion
|
|
452
484
|
export { HttpResponse as default };
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import HttpError from "../../errors/httpError.js";
|
|
2
|
+
import Http2Response from "../http2/Response.js";
|
|
3
|
+
import { RequestContext } from "nodefony";
|
|
4
|
+
import http from "node:http";
|
|
5
|
+
import { AsyncResource } from "node:async_hooks";
|
|
6
|
+
// nodefony/src/context/http/SseStream.ts
|
|
7
|
+
/** Battement de cœur par défaut, en ms (§9.2.7 : « every 15 seconds or so »). */
|
|
8
|
+
const SSE_HEARTBEAT_MS = 15e3;
|
|
9
|
+
/** Délai de blocage d'écriture par défaut, en ms. */
|
|
10
|
+
const SSE_STALL_TIMEOUT_MS = 3e4;
|
|
11
|
+
/** Taille maximale d'un événement par défaut, en octets (1 Mio). */
|
|
12
|
+
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
|
+
/** Commentaire de battement de cœur — ignoré par tout client conforme. */
|
|
20
|
+
const HEARTBEAT = ":\n\n";
|
|
21
|
+
/** Fin de ligne : CRLF, CR ou LF (§9.2.5). */
|
|
22
|
+
const LINE_BREAK = /\r\n|\r|\n/;
|
|
23
|
+
/**
|
|
24
|
+
* Refuse un champ d'une seule ligne qui contiendrait une fin de ligne.
|
|
25
|
+
*
|
|
26
|
+
* Laisser passer un CR ou un LF dans `event` ou `id` permettrait à une valeur
|
|
27
|
+
* venue d'un utilisateur de clore l'événement et d'en FORGER un autre dans le
|
|
28
|
+
* flux — l'équivalent SSE d'une injection d'en-tête.
|
|
29
|
+
*
|
|
30
|
+
* @throws TypeError quand la valeur contient CR, LF ou NUL.
|
|
31
|
+
*/
|
|
32
|
+
function singleLine(name, value) {
|
|
33
|
+
if (/[\r\n\0]/.test(value)) throw new TypeError(`Champ SSE « ${name} » : fin de ligne ou NUL interdits`);
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Texte d'un événement : la chaîne telle quelle, sinon son JSON.
|
|
38
|
+
*
|
|
39
|
+
* `JSON.stringify` rend `undefined` pour `undefined`, une fonction ou un
|
|
40
|
+
* symbole, et son type `string` le tait : on l'élargit avant de le lire.
|
|
41
|
+
*/
|
|
42
|
+
function serialize(data) {
|
|
43
|
+
if (typeof data === "string") return data;
|
|
44
|
+
const json = JSON.stringify(data);
|
|
45
|
+
return typeof json === "string" ? json : "";
|
|
46
|
+
}
|
|
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
|
+
/** Préfixe chaque ligne d'un texte par `prefix` — `data: ` ou `: `. */
|
|
57
|
+
function lines(prefix, text) {
|
|
58
|
+
let out = "";
|
|
59
|
+
for (const line of text.split(LINE_BREAK)) out += `${prefix}${line}\n`;
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Un flux `text/event-stream` ouvert sur une réponse.
|
|
64
|
+
*
|
|
65
|
+
* Il s'obtient par {@link openSseStream} (ou `this.renderSse()` dans un
|
|
66
|
+
* contrôleur), jamais par `new` : l'ouverture écrit les en-têtes, et un flux
|
|
67
|
+
* sans en-têtes ne se rattrape pas.
|
|
68
|
+
*/
|
|
69
|
+
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;
|
|
75
|
+
#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
|
+
#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
|
+
/** Attente de `drain` en cours, partagée par tous les `send()` sous pression. */
|
|
94
|
+
#drain = null;
|
|
95
|
+
#releaseDrain = null;
|
|
96
|
+
#onResponseClose = () => this.#finish();
|
|
97
|
+
#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
|
+
/** @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;
|
|
112
|
+
this.#maxEventBytes = settings.maxEventBytes;
|
|
113
|
+
this.#revalidate = settings.revalidate;
|
|
114
|
+
this.identifier = settings.identifier;
|
|
115
|
+
response.on("close", this.#onResponseClose);
|
|
116
|
+
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) {
|
|
122
|
+
this.#heartbeat = setInterval(() => {
|
|
123
|
+
if (this.#drain === null) this.#target.write(HEARTBEAT);
|
|
124
|
+
}, heartbeat);
|
|
125
|
+
this.#heartbeat.unref();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/** Le flux est-il fermé — par le serveur (`close()`) ou par le client parti ? */
|
|
129
|
+
get closed() {
|
|
130
|
+
return this.#closed;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* L'identité qui a ouvert le flux est-elle toujours valable ? Lu par le
|
|
134
|
+
* registre de révocation du noyau HTTP.
|
|
135
|
+
*
|
|
136
|
+
* @internal
|
|
137
|
+
*/
|
|
138
|
+
revalidate() {
|
|
139
|
+
return this.#revalidate === null ? Promise.resolve(true) : this.#revalidate();
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Envoie un événement.
|
|
143
|
+
*
|
|
144
|
+
* @param data - texte envoyé tel quel, ou valeur sérialisée en JSON ; un
|
|
145
|
+
* texte de plusieurs lignes part en autant de champs `data:`.
|
|
146
|
+
* @param options - type (`event`) et identifiant (`id`) de l'événement.
|
|
147
|
+
* @returns rien quand l'écriture est acceptée ; une promesse réglée au
|
|
148
|
+
* `drain` quand le client lit moins vite qu'on écrit — l'attendre borne la
|
|
149
|
+
* mémoire du serveur. Sans effet une fois le flux fermé.
|
|
150
|
+
* @throws TypeError quand `event` ou `id` contient une fin de ligne.
|
|
151
|
+
* @throws RangeError quand l'événement dépasse `maxEventBytes`.
|
|
152
|
+
*/
|
|
153
|
+
send(data, options) {
|
|
154
|
+
let frame = "";
|
|
155
|
+
if (options?.event !== void 0) frame += `event: ${singleLine("event", options.event)}\n`;
|
|
156
|
+
if (options?.id !== void 0) frame += `id: ${singleLine("id", options.id)}\n`;
|
|
157
|
+
frame += `${lines("data: ", serialize(data))}\n`;
|
|
158
|
+
return this.#write(frame);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Envoie un commentaire — ignoré par le client, utile pour garder une
|
|
162
|
+
* connexion vivante ou marquer le flux à la lecture.
|
|
163
|
+
*
|
|
164
|
+
* @returns comme {@link send}.
|
|
165
|
+
*/
|
|
166
|
+
comment(text = "") {
|
|
167
|
+
return this.#write(`${lines(": ", text)}\n`);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Impose au client un délai de reconnexion (champ `retry:`).
|
|
171
|
+
*
|
|
172
|
+
* @param ms - délai en millisecondes, entier positif.
|
|
173
|
+
* @returns comme {@link send}.
|
|
174
|
+
* @throws RangeError quand `ms` n'est pas un entier entre 0 et 2^31-1.
|
|
175
|
+
*/
|
|
176
|
+
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`);
|
|
178
|
+
return this.#write(`retry: ${ms}\n\n`);
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Appelle `listener` à la fermeture du flux, quelle qu'en soit la cause.
|
|
182
|
+
*
|
|
183
|
+
* L'écouteur est lié au contexte asynchrone de la requête au moment de
|
|
184
|
+
* l'enregistrement : la fermeture par le client part de la SOCKET, hors de
|
|
185
|
+
* la bulle de la requête, et un `RequestContext.get()` y rendrait `undefined`.
|
|
186
|
+
* Un flux déjà fermé appelle l'écouteur au tour suivant.
|
|
187
|
+
*/
|
|
188
|
+
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);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Ferme le flux et termine la requête. Idempotent.
|
|
199
|
+
*
|
|
200
|
+
* @returns la fin de la requête (hooks `onClose` du contexte compris).
|
|
201
|
+
*/
|
|
202
|
+
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;
|
|
207
|
+
}
|
|
208
|
+
/** Écrit une trame ; une promesse seulement sous contre-pression. */
|
|
209
|
+
#write(frame) {
|
|
210
|
+
if (this.#closed) return void 0;
|
|
211
|
+
const max = this.#maxEventBytes;
|
|
212
|
+
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;
|
|
214
|
+
if (this.#drain === null) {
|
|
215
|
+
this.#drain = new Promise((resolve) => {
|
|
216
|
+
this.#releaseDrain = resolve;
|
|
217
|
+
});
|
|
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
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return this.#drain;
|
|
225
|
+
}
|
|
226
|
+
#settleDrain() {
|
|
227
|
+
if (this.#stall !== null) {
|
|
228
|
+
clearTimeout(this.#stall);
|
|
229
|
+
this.#stall = null;
|
|
230
|
+
}
|
|
231
|
+
const release = this.#releaseDrain;
|
|
232
|
+
this.#drain = null;
|
|
233
|
+
this.#releaseDrain = null;
|
|
234
|
+
release?.();
|
|
235
|
+
}
|
|
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);
|
|
242
|
+
if (this.#heartbeat !== null) {
|
|
243
|
+
clearInterval(this.#heartbeat);
|
|
244
|
+
this.#heartbeat = null;
|
|
245
|
+
}
|
|
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
|
+
this.#settleDrain();
|
|
252
|
+
const listeners = this.#onClose;
|
|
253
|
+
this.#onClose = null;
|
|
254
|
+
if (listeners !== null) for (const listener of listeners) listener();
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
/**
|
|
258
|
+
* Le client accepte-t-il un flux d'événements ? Lit l'en-tête `Accept`.
|
|
259
|
+
*
|
|
260
|
+
* Un même `POST` répond en JSON ou en flux selon ce que demande le client
|
|
261
|
+
* (porte MCP) : c'est cette fonction qui tranche, et elle seule. Une plage
|
|
262
|
+
* `q=0` est un REFUS explicite (RFC 9110 §12.4.2) ; `*\/*` ne suffit pas — un
|
|
263
|
+
* client qui ne nomme pas le flux ne sait pas le lire.
|
|
264
|
+
*
|
|
265
|
+
* @param accept - valeur de l'en-tête `Accept`, absente ou multiple.
|
|
266
|
+
* @returns `true` quand `text/event-stream` est nommé avec un poids non nul.
|
|
267
|
+
*/
|
|
268
|
+
function acceptsEventStream(accept) {
|
|
269
|
+
if (accept === void 0) return false;
|
|
270
|
+
const value = Array.isArray(accept) ? accept.join(",") : accept;
|
|
271
|
+
for (const range of value.split(",")) {
|
|
272
|
+
const [type, ...params] = range.split(";");
|
|
273
|
+
if (type?.trim().toLowerCase() !== "text/event-stream") continue;
|
|
274
|
+
const q = params.map((p) => p.trim().toLowerCase()).find((p) => p.startsWith("q="));
|
|
275
|
+
if (q === void 0 || Number.parseFloat(q.slice(2)) > 0) return true;
|
|
276
|
+
}
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Ouvre un flux d'événements sur la réponse d'un contexte HTTP.
|
|
281
|
+
*
|
|
282
|
+
* Sauve la session d'abord — ses cookies partent avec les en-têtes, qu'on ne
|
|
283
|
+
* pourra plus toucher —, puis écrit les en-têtes du flux et désarme le délai
|
|
284
|
+
* d'inactivité de la requête.
|
|
285
|
+
*
|
|
286
|
+
* @param context - le contexte de la requête, pas encore répondu.
|
|
287
|
+
* @param options - battement de cœur, délai `retry:` initial.
|
|
288
|
+
* @returns le flux ouvert, ou sa promesse quand une session est à sauver.
|
|
289
|
+
* @throws HttpError 500 quand la réponse est déjà partie.
|
|
290
|
+
* @throws HttpError 429 quand l'IP du client a déjà tous ses flux et sockets
|
|
291
|
+
* (`wsMaxConnectionsPerIp`) — refusé AVANT d'écrire quoi que ce soit.
|
|
292
|
+
* @throws RangeError quand un délai n'est ni `false` ni un entier entre 1 et
|
|
293
|
+
* 2^31-1, ou que `maxEventBytes` n'est pas un entier positif.
|
|
294
|
+
*/
|
|
295
|
+
function openSseStream(context, options = {}) {
|
|
296
|
+
checkTimer("heartbeat", options.heartbeat);
|
|
297
|
+
checkTimer("stallTimeout", options.stallTimeout);
|
|
298
|
+
checkTimer("maxDuration", options.maxDuration);
|
|
299
|
+
const maxEventBytes = options.maxEventBytes ?? 1048576;
|
|
300
|
+
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
|
+
const settings = {
|
|
308
|
+
heartbeat: options.heartbeat ?? 15e3,
|
|
309
|
+
stallTimeout: options.stallTimeout ?? 3e4,
|
|
310
|
+
maxEventBytes,
|
|
311
|
+
maxDuration: options.maxDuration ?? false,
|
|
312
|
+
revalidate,
|
|
313
|
+
identifier
|
|
314
|
+
};
|
|
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"
|
|
337
|
+
});
|
|
338
|
+
context.writeHead(200);
|
|
339
|
+
if (stream !== null) stream.setTimeout(0);
|
|
340
|
+
else if (h1 !== null) {
|
|
341
|
+
h1.flushHeaders();
|
|
342
|
+
h1.socket?.setTimeout(0);
|
|
343
|
+
}
|
|
344
|
+
const sse = new SseStream(context, target, nodeResponse, settings);
|
|
345
|
+
if (settings.revalidate !== null) context.httpKernel?.watchStream(sse);
|
|
346
|
+
if (options.retry !== void 0) sse.retry(options.retry);
|
|
347
|
+
return sse;
|
|
348
|
+
}
|
|
349
|
+
export { SSE_HEARTBEAT_MS, SSE_MAX_EVENT_BYTES, SSE_STALL_TIMEOUT_MS, SseStream, acceptsEventStream, openSseStream };
|