@blamejs/core 0.7.4 → 0.7.18
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/CHANGELOG.md +423 -395
- package/README.md +150 -149
- package/bin/blamejs.js +0 -0
- package/index.js +308 -284
- package/lib/api-key.js +660 -663
- package/lib/api-snapshot.js +338 -338
- package/lib/app-shutdown.js +385 -385
- package/lib/app.js +365 -365
- package/lib/archive.js +250 -250
- package/lib/atomic-file.js +544 -544
- package/lib/audit-chain.js +177 -177
- package/lib/audit-sign.js +344 -344
- package/lib/audit-tools.js +677 -677
- package/lib/audit.js +766 -766
- package/lib/auth/jwt.js +311 -311
- package/lib/auth/lockout.js +436 -436
- package/lib/auth/oauth.js +721 -721
- package/lib/auth/passkey.js +181 -181
- package/lib/auth/password.js +594 -594
- package/lib/backup/bundle.js +217 -217
- package/lib/backup/crypto.js +176 -176
- package/lib/backup/index.js +515 -515
- package/lib/backup/manifest.js +282 -282
- package/lib/break-glass.js +1338 -1338
- package/lib/bundler.js +441 -441
- package/lib/cache-redis.js +256 -256
- package/lib/cache.js +1206 -1206
- package/lib/canonical-json.js +115 -115
- package/lib/chain-writer.js +234 -234
- package/lib/cli-helpers.js +206 -206
- package/lib/cli.js +2334 -2334
- package/lib/cluster-provider-db.js +317 -317
- package/lib/cluster-storage.js +226 -226
- package/lib/cluster.js +703 -703
- package/lib/codepoint-class.js +196 -0
- package/lib/config-drift.js +301 -301
- package/lib/consent.js +222 -222
- package/lib/constants.js +191 -191
- package/lib/cookies.js +315 -315
- package/lib/credential-hash.js +322 -322
- package/lib/crypto.js +266 -266
- package/lib/csv.js +275 -286
- package/lib/db-declare-row-policy.js +267 -267
- package/lib/db-declare-view.js +420 -421
- package/lib/db-query.js +406 -406
- package/lib/db-schema.js +319 -319
- package/lib/db.js +1288 -1288
- package/lib/deprecate.js +222 -222
- package/lib/dev.js +335 -335
- package/lib/dual-control.js +473 -473
- package/lib/error-page.js +420 -420
- package/lib/external-db-migrate.js +441 -441
- package/lib/external-db.js +1061 -1061
- package/lib/file-type.js +273 -273
- package/lib/file-upload.js +213 -10
- package/lib/forms.js +422 -422
- package/lib/framework-error.js +293 -215
- package/lib/framework-schema.js +717 -717
- package/lib/gate-contract.js +971 -0
- package/lib/guard-all.js +405 -0
- package/lib/guard-archive.js +739 -0
- package/lib/guard-csv.js +816 -0
- package/lib/guard-email.js +744 -0
- package/lib/guard-filename.js +724 -0
- package/lib/guard-html.js +976 -0
- package/lib/guard-json.js +729 -0
- package/lib/guard-markdown.js +586 -0
- package/lib/guard-svg.js +976 -0
- package/lib/guard-xml.js +405 -0
- package/lib/guard-yaml.js +529 -0
- package/lib/handlers.js +350 -350
- package/lib/http-client-cookie-jar.js +508 -508
- package/lib/http-client.js +1195 -1195
- package/lib/i18n.js +878 -878
- package/lib/jobs.js +185 -185
- package/lib/log-stream-cloudwatch.js +369 -369
- package/lib/log-stream-local.js +146 -146
- package/lib/log-stream-otlp-grpc.js +410 -410
- package/lib/log-stream-otlp.js +286 -286
- package/lib/log-stream-syslog.js +302 -302
- package/lib/log-stream-webhook.js +199 -199
- package/lib/log-stream.js +330 -330
- package/lib/log.js +500 -500
- package/lib/mail-bounce.js +528 -528
- package/lib/mail-dkim.js +369 -362
- package/lib/mail.js +981 -962
- package/lib/metrics.js +683 -683
- package/lib/middleware/api-encrypt.js +936 -936
- package/lib/middleware/attach-user.js +157 -157
- package/lib/middleware/body-parser.js +1170 -1091
- package/lib/middleware/bot-guard.js +178 -178
- package/lib/middleware/compression.js +452 -452
- package/lib/middleware/cors.js +314 -314
- package/lib/middleware/csp-nonce.js +348 -348
- package/lib/middleware/csrf-protect.js +316 -316
- package/lib/middleware/db-role-for.js +264 -264
- package/lib/middleware/health.js +392 -392
- package/lib/middleware/index.js +79 -79
- package/lib/middleware/rate-limit.js +358 -358
- package/lib/middleware/request-id.js +61 -61
- package/lib/middleware/request-log.js +168 -168
- package/lib/middleware/require-auth.js +104 -104
- package/lib/middleware/security-headers.js +116 -116
- package/lib/middleware/sse.js +166 -166
- package/lib/migrations.js +383 -383
- package/lib/mtls-ca.js +518 -518
- package/lib/mtls-engine-default.js +481 -481
- package/lib/network-dns.js +632 -632
- package/lib/network-heartbeat.js +290 -290
- package/lib/network-nts.js +574 -574
- package/lib/network-proxy.js +265 -265
- package/lib/network-tls.js +328 -328
- package/lib/network.js +233 -233
- package/lib/notify.js +612 -612
- package/lib/ntp-check.js +229 -229
- package/lib/numeric-bounds.js +111 -91
- package/lib/object-store/azure-blob-bucket-ops.js +349 -349
- package/lib/object-store/azure-blob.js +488 -488
- package/lib/object-store/gcs-bucket-ops.js +351 -351
- package/lib/object-store/gcs.js +519 -519
- package/lib/object-store/http-put.js +153 -153
- package/lib/object-store/index.js +197 -197
- package/lib/object-store/sigv4-bucket-ops.js +1092 -1092
- package/lib/object-store/sigv4.js +903 -903
- package/lib/observability.js +151 -151
- package/lib/otel-export.js +269 -269
- package/lib/pagination.js +464 -464
- package/lib/parsers/index.js +80 -80
- package/lib/parsers/safe-env.js +642 -642
- package/lib/parsers/safe-ini.js +292 -292
- package/lib/parsers/safe-toml.js +784 -784
- package/lib/parsers/safe-xml.js +390 -390
- package/lib/parsers/safe-yaml.js +1015 -1015
- package/lib/permissions.js +708 -708
- package/lib/pqc-agent.js +87 -87
- package/lib/pqc-gate.js +279 -279
- package/lib/protobuf-encoder.js +190 -190
- package/lib/protocol-dispatcher.js +161 -161
- package/lib/pubsub-redis.js +167 -167
- package/lib/pubsub.js +429 -429
- package/lib/queue-local.js +476 -476
- package/lib/queue-redis.js +745 -745
- package/lib/queue-sqs.js +319 -319
- package/lib/queue.js +695 -695
- package/lib/redis-client.js +519 -519
- package/lib/request-helpers.js +340 -340
- package/lib/restore-bundle.js +237 -237
- package/lib/restore-rollback.js +259 -259
- package/lib/restore.js +409 -409
- package/lib/retry.js +376 -376
- package/lib/router.js +748 -748
- package/lib/safe-async.js +735 -735
- package/lib/safe-buffer.js +237 -237
- package/lib/safe-json.js +541 -541
- package/lib/safe-schema.js +1266 -1266
- package/lib/safe-url.js +159 -159
- package/lib/scheduler.js +706 -706
- package/lib/security-assert.js +373 -373
- package/lib/seeders.js +618 -618
- package/lib/session.js +478 -478
- package/lib/slug.js +269 -269
- package/lib/ssrf-guard.js +401 -401
- package/lib/static.js +184 -4
- package/lib/storage.js +471 -471
- package/lib/subject.js +281 -281
- package/lib/template.js +791 -791
- package/lib/testing.js +798 -798
- package/lib/time.js +310 -310
- package/lib/totp.js +302 -302
- package/lib/tracing.js +494 -494
- package/lib/uuid.js +132 -132
- package/lib/validate-opts.js +340 -319
- package/lib/vault/index.js +308 -308
- package/lib/vault/rotate.js +784 -784
- package/lib/vault/wrap.js +296 -296
- package/lib/vendor/noble-ciphers.cjs +9 -9
- package/lib/webhook.js +595 -595
- package/lib/websocket.js +1048 -1048
- package/package.json +77 -77
- package/sbom.cyclonedx.json +7 -7
package/lib/middleware/sse.js
CHANGED
|
@@ -1,166 +1,166 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* sse — Server-Sent Events middleware. One-way streaming from server
|
|
4
|
-
* to browser over a single HTTP response with `Content-Type:
|
|
5
|
-
* text/event-stream`. Browsers reconnect automatically with the
|
|
6
|
-
* `Last-Event-ID` header so the operator's handler can resume from
|
|
7
|
-
* the last delivered event.
|
|
8
|
-
*
|
|
9
|
-
* Use case: live dashboards, log tailing, progress updates, real-time
|
|
10
|
-
* counters. SSE is the right tool when the server pushes and the
|
|
11
|
-
* client doesn't need to send anything back. For bidirectional, use
|
|
12
|
-
* `b.websocket`.
|
|
13
|
-
*
|
|
14
|
-
* router.get("/events", b.middleware.sse(async function (channel, req) {
|
|
15
|
-
* channel.send({ id: 1, event: "tick", data: { count: 1 } });
|
|
16
|
-
* channel.send({ id: 2, event: "tick", data: { count: 2 } });
|
|
17
|
-
* // resume support — read req.headers["last-event-id"] and
|
|
18
|
-
* // resume from that point.
|
|
19
|
-
* }));
|
|
20
|
-
*
|
|
21
|
-
* `channel`:
|
|
22
|
-
* send({ id?, event?, data, retry? }) — emit one SSE message
|
|
23
|
-
* ping(comment?) — emit a comment line (keeps
|
|
24
|
-
* intermediate proxies happy)
|
|
25
|
-
* close() — end the stream
|
|
26
|
-
* onAbort(fn) — register cleanup when the
|
|
27
|
-
* client disconnects (browser
|
|
28
|
-
* tab close, network drop)
|
|
29
|
-
*
|
|
30
|
-
* Heartbeat: the middleware sends a comment line every `heartbeatMs`
|
|
31
|
-
* (default 15s) automatically so corporate proxies / Heroku-style
|
|
32
|
-
* idle-timeouts don't kill the stream. Operators with strict
|
|
33
|
-
* deployments override `heartbeatMs: false` to disable.
|
|
34
|
-
*
|
|
35
|
-
* Compression: SSE streams typically should NOT be compressed —
|
|
36
|
-
* `b.middleware.compression` skips `text/event-stream` by default.
|
|
37
|
-
*/
|
|
38
|
-
var C = require("../constants");
|
|
39
|
-
var requestHelpers = require("../request-helpers");
|
|
40
|
-
var safeBuffer = require("../safe-buffer");
|
|
41
|
-
var validateOpts = require("../validate-opts");
|
|
42
|
-
|
|
43
|
-
var DEFAULT_HEARTBEAT_MS = C.TIME.seconds(15);
|
|
44
|
-
|
|
45
|
-
function _formatEvent(msg) {
|
|
46
|
-
// RFC 6455... wait, that's WebSocket. SSE: WHATWG HTML §9.2.5.
|
|
47
|
-
// Lines: "id: <n>\n", "event: <name>\n", "data: <line>\n" (multi-line
|
|
48
|
-
// data is multiple "data: " lines), "retry: <ms>\n", blank line ends.
|
|
49
|
-
var out = "";
|
|
50
|
-
if (msg.id !== undefined && msg.id !== null) out += "id: " + safeBuffer.stripCrlf(String(msg.id)) + "\n";
|
|
51
|
-
if (msg.event) out += "event: " + safeBuffer.stripCrlf(String(msg.event)) + "\n";
|
|
52
|
-
if (msg.retry !== undefined && msg.retry !== null) {
|
|
53
|
-
if (typeof msg.retry !== "number" || !isFinite(msg.retry) || msg.retry < 0) {
|
|
54
|
-
throw new Error("sse: retry must be a non-negative finite number of milliseconds");
|
|
55
|
-
}
|
|
56
|
-
out += "retry: " + Math.floor(msg.retry) + "\n";
|
|
57
|
-
}
|
|
58
|
-
var dataStr;
|
|
59
|
-
if (msg.data === undefined || msg.data === null) dataStr = "";
|
|
60
|
-
else if (typeof msg.data === "string") dataStr = msg.data;
|
|
61
|
-
else dataStr = JSON.stringify(msg.data);
|
|
62
|
-
// Multi-line data → one `data:` line per source line (per spec).
|
|
63
|
-
var lines = dataStr.split(/\r?\n/);
|
|
64
|
-
for (var i = 0; i < lines.length; i++) out += "data: " + lines[i] + "\n";
|
|
65
|
-
out += "\n"; // dispatch
|
|
66
|
-
return out;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function create(handler, opts) {
|
|
70
|
-
if (typeof handler !== "function") {
|
|
71
|
-
throw new Error("middleware.sse: handler must be a function (channel, req) => ...");
|
|
72
|
-
}
|
|
73
|
-
opts = opts || {};
|
|
74
|
-
validateOpts(opts, ["heartbeatMs", "headers"], "middleware.sse");
|
|
75
|
-
var heartbeatMs = opts.heartbeatMs === false ? 0
|
|
76
|
-
: (opts.heartbeatMs != null ? opts.heartbeatMs : DEFAULT_HEARTBEAT_MS);
|
|
77
|
-
if (heartbeatMs !== 0 && (typeof heartbeatMs !== "number" || !isFinite(heartbeatMs) || heartbeatMs <= 0)) {
|
|
78
|
-
throw new Error("middleware.sse: heartbeatMs must be a positive finite number or false");
|
|
79
|
-
}
|
|
80
|
-
var extraHeaders = opts.headers || {};
|
|
81
|
-
|
|
82
|
-
return async function sseMiddleware(req, res) {
|
|
83
|
-
if (typeof res.writeHead !== "function" || typeof res.write !== "function") {
|
|
84
|
-
// Not an http.ServerResponse — operator wired this onto something
|
|
85
|
-
// unusual. Fail closed rather than silently dropping the handler.
|
|
86
|
-
throw new Error("middleware.sse: res does not support writeHead/write — wire SSE only on HTTP routes");
|
|
87
|
-
}
|
|
88
|
-
var headers = Object.assign({
|
|
89
|
-
"Content-Type": "text/event-stream; charset=utf-8",
|
|
90
|
-
"Cache-Control": "no-cache, no-transform",
|
|
91
|
-
"Connection": "keep-alive",
|
|
92
|
-
// Disable nginx response buffering when terminating behind it.
|
|
93
|
-
"X-Accel-Buffering": "no",
|
|
94
|
-
}, extraHeaders);
|
|
95
|
-
// Append Vary: Accept so a proxy doesn't serve a cached non-SSE
|
|
96
|
-
// response on the same URL to a future client.
|
|
97
|
-
res.writeHead(requestHelpers.HTTP_STATUS.OK, headers);
|
|
98
|
-
requestHelpers.appendVary(res, "Accept");
|
|
99
|
-
// Initial flush — some proxies hold the headers until first byte.
|
|
100
|
-
res.write(":\n\n");
|
|
101
|
-
|
|
102
|
-
var closed = false;
|
|
103
|
-
var heartbeatTimer = null;
|
|
104
|
-
var abortHandlers = [];
|
|
105
|
-
|
|
106
|
-
function _scheduleHeartbeat() {
|
|
107
|
-
if (heartbeatMs === 0) return;
|
|
108
|
-
heartbeatTimer = setTimeout(function () {
|
|
109
|
-
if (closed) return;
|
|
110
|
-
try { res.write(": heartbeat\n\n"); } catch (_e) { /* socket closed */ }
|
|
111
|
-
_scheduleHeartbeat();
|
|
112
|
-
}, heartbeatMs);
|
|
113
|
-
if (typeof heartbeatTimer.unref === "function") heartbeatTimer.unref();
|
|
114
|
-
}
|
|
115
|
-
_scheduleHeartbeat();
|
|
116
|
-
|
|
117
|
-
var channel = {
|
|
118
|
-
send: function (msg) {
|
|
119
|
-
if (closed) return false;
|
|
120
|
-
try { res.write(_formatEvent(msg || {})); return true; }
|
|
121
|
-
catch (_e) { return false; }
|
|
122
|
-
},
|
|
123
|
-
ping: function (comment) {
|
|
124
|
-
if (closed) return false;
|
|
125
|
-
var safe = comment ? safeBuffer.stripCrlf(String(comment), " ") : "ping";
|
|
126
|
-
try { res.write(": " + safe + "\n\n"); return true; }
|
|
127
|
-
catch (_e) { return false; }
|
|
128
|
-
},
|
|
129
|
-
close: function () {
|
|
130
|
-
if (closed) return;
|
|
131
|
-
closed = true;
|
|
132
|
-
if (heartbeatTimer) { clearTimeout(heartbeatTimer); heartbeatTimer = null; }
|
|
133
|
-
try { res.end(); } catch (_e) { /* already ended */ }
|
|
134
|
-
},
|
|
135
|
-
onAbort: function (fn) {
|
|
136
|
-
if (typeof fn === "function") abortHandlers.push(fn);
|
|
137
|
-
},
|
|
138
|
-
get closed() { return closed; },
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
function _onClose() {
|
|
142
|
-
if (closed) return;
|
|
143
|
-
closed = true;
|
|
144
|
-
if (heartbeatTimer) { clearTimeout(heartbeatTimer); heartbeatTimer = null; }
|
|
145
|
-
for (var i = 0; i < abortHandlers.length; i++) {
|
|
146
|
-
try { abortHandlers[i](); } catch (_e) { /* operator handler error — drop */ }
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
res.once("close", _onClose);
|
|
150
|
-
res.once("error", _onClose);
|
|
151
|
-
if (req && typeof req.once === "function") req.once("aborted", _onClose);
|
|
152
|
-
|
|
153
|
-
try {
|
|
154
|
-
await handler(channel, req);
|
|
155
|
-
} catch (e) {
|
|
156
|
-
_onClose();
|
|
157
|
-
try { res.end(); } catch (_ignored) { /* */ }
|
|
158
|
-
throw e;
|
|
159
|
-
}
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
module.exports = {
|
|
164
|
-
create: create,
|
|
165
|
-
_formatEvent: _formatEvent, // test-only export
|
|
166
|
-
};
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* sse — Server-Sent Events middleware. One-way streaming from server
|
|
4
|
+
* to browser over a single HTTP response with `Content-Type:
|
|
5
|
+
* text/event-stream`. Browsers reconnect automatically with the
|
|
6
|
+
* `Last-Event-ID` header so the operator's handler can resume from
|
|
7
|
+
* the last delivered event.
|
|
8
|
+
*
|
|
9
|
+
* Use case: live dashboards, log tailing, progress updates, real-time
|
|
10
|
+
* counters. SSE is the right tool when the server pushes and the
|
|
11
|
+
* client doesn't need to send anything back. For bidirectional, use
|
|
12
|
+
* `b.websocket`.
|
|
13
|
+
*
|
|
14
|
+
* router.get("/events", b.middleware.sse(async function (channel, req) {
|
|
15
|
+
* channel.send({ id: 1, event: "tick", data: { count: 1 } });
|
|
16
|
+
* channel.send({ id: 2, event: "tick", data: { count: 2 } });
|
|
17
|
+
* // resume support — read req.headers["last-event-id"] and
|
|
18
|
+
* // resume from that point.
|
|
19
|
+
* }));
|
|
20
|
+
*
|
|
21
|
+
* `channel`:
|
|
22
|
+
* send({ id?, event?, data, retry? }) — emit one SSE message
|
|
23
|
+
* ping(comment?) — emit a comment line (keeps
|
|
24
|
+
* intermediate proxies happy)
|
|
25
|
+
* close() — end the stream
|
|
26
|
+
* onAbort(fn) — register cleanup when the
|
|
27
|
+
* client disconnects (browser
|
|
28
|
+
* tab close, network drop)
|
|
29
|
+
*
|
|
30
|
+
* Heartbeat: the middleware sends a comment line every `heartbeatMs`
|
|
31
|
+
* (default 15s) automatically so corporate proxies / Heroku-style
|
|
32
|
+
* idle-timeouts don't kill the stream. Operators with strict
|
|
33
|
+
* deployments override `heartbeatMs: false` to disable.
|
|
34
|
+
*
|
|
35
|
+
* Compression: SSE streams typically should NOT be compressed —
|
|
36
|
+
* `b.middleware.compression` skips `text/event-stream` by default.
|
|
37
|
+
*/
|
|
38
|
+
var C = require("../constants");
|
|
39
|
+
var requestHelpers = require("../request-helpers");
|
|
40
|
+
var safeBuffer = require("../safe-buffer");
|
|
41
|
+
var validateOpts = require("../validate-opts");
|
|
42
|
+
|
|
43
|
+
var DEFAULT_HEARTBEAT_MS = C.TIME.seconds(15);
|
|
44
|
+
|
|
45
|
+
function _formatEvent(msg) {
|
|
46
|
+
// RFC 6455... wait, that's WebSocket. SSE: WHATWG HTML §9.2.5.
|
|
47
|
+
// Lines: "id: <n>\n", "event: <name>\n", "data: <line>\n" (multi-line
|
|
48
|
+
// data is multiple "data: " lines), "retry: <ms>\n", blank line ends.
|
|
49
|
+
var out = "";
|
|
50
|
+
if (msg.id !== undefined && msg.id !== null) out += "id: " + safeBuffer.stripCrlf(String(msg.id)) + "\n";
|
|
51
|
+
if (msg.event) out += "event: " + safeBuffer.stripCrlf(String(msg.event)) + "\n";
|
|
52
|
+
if (msg.retry !== undefined && msg.retry !== null) {
|
|
53
|
+
if (typeof msg.retry !== "number" || !isFinite(msg.retry) || msg.retry < 0) {
|
|
54
|
+
throw new Error("sse: retry must be a non-negative finite number of milliseconds");
|
|
55
|
+
}
|
|
56
|
+
out += "retry: " + Math.floor(msg.retry) + "\n";
|
|
57
|
+
}
|
|
58
|
+
var dataStr;
|
|
59
|
+
if (msg.data === undefined || msg.data === null) dataStr = "";
|
|
60
|
+
else if (typeof msg.data === "string") dataStr = msg.data;
|
|
61
|
+
else dataStr = JSON.stringify(msg.data);
|
|
62
|
+
// Multi-line data → one `data:` line per source line (per spec).
|
|
63
|
+
var lines = dataStr.split(/\r?\n/);
|
|
64
|
+
for (var i = 0; i < lines.length; i++) out += "data: " + lines[i] + "\n";
|
|
65
|
+
out += "\n"; // dispatch
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function create(handler, opts) {
|
|
70
|
+
if (typeof handler !== "function") {
|
|
71
|
+
throw new Error("middleware.sse: handler must be a function (channel, req) => ...");
|
|
72
|
+
}
|
|
73
|
+
opts = opts || {};
|
|
74
|
+
validateOpts(opts, ["heartbeatMs", "headers"], "middleware.sse");
|
|
75
|
+
var heartbeatMs = opts.heartbeatMs === false ? 0
|
|
76
|
+
: (opts.heartbeatMs != null ? opts.heartbeatMs : DEFAULT_HEARTBEAT_MS);
|
|
77
|
+
if (heartbeatMs !== 0 && (typeof heartbeatMs !== "number" || !isFinite(heartbeatMs) || heartbeatMs <= 0)) {
|
|
78
|
+
throw new Error("middleware.sse: heartbeatMs must be a positive finite number or false");
|
|
79
|
+
}
|
|
80
|
+
var extraHeaders = opts.headers || {};
|
|
81
|
+
|
|
82
|
+
return async function sseMiddleware(req, res) {
|
|
83
|
+
if (typeof res.writeHead !== "function" || typeof res.write !== "function") {
|
|
84
|
+
// Not an http.ServerResponse — operator wired this onto something
|
|
85
|
+
// unusual. Fail closed rather than silently dropping the handler.
|
|
86
|
+
throw new Error("middleware.sse: res does not support writeHead/write — wire SSE only on HTTP routes");
|
|
87
|
+
}
|
|
88
|
+
var headers = Object.assign({
|
|
89
|
+
"Content-Type": "text/event-stream; charset=utf-8",
|
|
90
|
+
"Cache-Control": "no-cache, no-transform",
|
|
91
|
+
"Connection": "keep-alive",
|
|
92
|
+
// Disable nginx response buffering when terminating behind it.
|
|
93
|
+
"X-Accel-Buffering": "no",
|
|
94
|
+
}, extraHeaders);
|
|
95
|
+
// Append Vary: Accept so a proxy doesn't serve a cached non-SSE
|
|
96
|
+
// response on the same URL to a future client.
|
|
97
|
+
res.writeHead(requestHelpers.HTTP_STATUS.OK, headers);
|
|
98
|
+
requestHelpers.appendVary(res, "Accept");
|
|
99
|
+
// Initial flush — some proxies hold the headers until first byte.
|
|
100
|
+
res.write(":\n\n");
|
|
101
|
+
|
|
102
|
+
var closed = false;
|
|
103
|
+
var heartbeatTimer = null;
|
|
104
|
+
var abortHandlers = [];
|
|
105
|
+
|
|
106
|
+
function _scheduleHeartbeat() {
|
|
107
|
+
if (heartbeatMs === 0) return;
|
|
108
|
+
heartbeatTimer = setTimeout(function () {
|
|
109
|
+
if (closed) return;
|
|
110
|
+
try { res.write(": heartbeat\n\n"); } catch (_e) { /* socket closed */ }
|
|
111
|
+
_scheduleHeartbeat();
|
|
112
|
+
}, heartbeatMs);
|
|
113
|
+
if (typeof heartbeatTimer.unref === "function") heartbeatTimer.unref();
|
|
114
|
+
}
|
|
115
|
+
_scheduleHeartbeat();
|
|
116
|
+
|
|
117
|
+
var channel = {
|
|
118
|
+
send: function (msg) {
|
|
119
|
+
if (closed) return false;
|
|
120
|
+
try { res.write(_formatEvent(msg || {})); return true; }
|
|
121
|
+
catch (_e) { return false; }
|
|
122
|
+
},
|
|
123
|
+
ping: function (comment) {
|
|
124
|
+
if (closed) return false;
|
|
125
|
+
var safe = comment ? safeBuffer.stripCrlf(String(comment), " ") : "ping";
|
|
126
|
+
try { res.write(": " + safe + "\n\n"); return true; }
|
|
127
|
+
catch (_e) { return false; }
|
|
128
|
+
},
|
|
129
|
+
close: function () {
|
|
130
|
+
if (closed) return;
|
|
131
|
+
closed = true;
|
|
132
|
+
if (heartbeatTimer) { clearTimeout(heartbeatTimer); heartbeatTimer = null; }
|
|
133
|
+
try { res.end(); } catch (_e) { /* already ended */ }
|
|
134
|
+
},
|
|
135
|
+
onAbort: function (fn) {
|
|
136
|
+
if (typeof fn === "function") abortHandlers.push(fn);
|
|
137
|
+
},
|
|
138
|
+
get closed() { return closed; },
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
function _onClose() {
|
|
142
|
+
if (closed) return;
|
|
143
|
+
closed = true;
|
|
144
|
+
if (heartbeatTimer) { clearTimeout(heartbeatTimer); heartbeatTimer = null; }
|
|
145
|
+
for (var i = 0; i < abortHandlers.length; i++) {
|
|
146
|
+
try { abortHandlers[i](); } catch (_e) { /* operator handler error — drop */ }
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
res.once("close", _onClose);
|
|
150
|
+
res.once("error", _onClose);
|
|
151
|
+
if (req && typeof req.once === "function") req.once("aborted", _onClose);
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
await handler(channel, req);
|
|
155
|
+
} catch (e) {
|
|
156
|
+
_onClose();
|
|
157
|
+
try { res.end(); } catch (_ignored) { /* */ }
|
|
158
|
+
throw e;
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
module.exports = {
|
|
164
|
+
create: create,
|
|
165
|
+
_formatEvent: _formatEvent, // test-only export
|
|
166
|
+
};
|