@blamejs/core 0.6.32 → 0.6.34
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 +2 -0
- package/README.md +2 -2
- package/index.js +2 -0
- package/lib/cache.js +61 -0
- package/lib/db.js +9 -8
- package/lib/framework-schema.js +11 -10
- package/lib/log-stream-cloudwatch.js +64 -5
- package/lib/log-stream-syslog.js +297 -0
- package/lib/log-stream.js +12 -12
- package/lib/protocol-dispatcher.js +1 -1
- package/lib/pubsub-cluster.js +154 -0
- package/lib/pubsub-redis.js +160 -0
- package/lib/pubsub.js +357 -0
- package/lib/redis-client.js +46 -1
- package/lib/websocket-channels.js +131 -217
- package/package.json +1 -1
- package/sbom.cyclonedx.json +6 -6
|
@@ -4,27 +4,39 @@
|
|
|
4
4
|
*
|
|
5
5
|
* `lib/websocket.js` owns the wire protocol (RFC 6455 + 8441 frame
|
|
6
6
|
* parsing, masking, control frames). This module owns the higher-level
|
|
7
|
-
*
|
|
8
|
-
* a payload to every subscriber
|
|
9
|
-
*
|
|
7
|
+
* subscription bookkeeping: connections subscribe to named channels,
|
|
8
|
+
* publish() fans out a payload to every subscriber. Cross-node fan-out
|
|
9
|
+
* is delegated to `b.pubsub` so cache-cluster invalidation, websocket
|
|
10
|
+
* channels, and any future cross-node primitive share the same
|
|
11
|
+
* transport (cluster table, Redis PUB/SUB, or operator-supplied).
|
|
10
12
|
*
|
|
11
13
|
* Public API:
|
|
12
14
|
*
|
|
13
15
|
* var hub = b.websocketChannels.create({
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* // Pub/sub backend opts — passed through to b.pubsub.create.
|
|
17
|
+
* backend: 'local' | 'cluster' | 'redis' | { custom },
|
|
18
|
+
* // cluster opts:
|
|
19
|
+
* cluster: clusterInstance,
|
|
20
|
+
* pollIntervalMs: C.TIME.ms? — default 100ms
|
|
21
|
+
* retentionMs: C.TIME.ms? — default 60s
|
|
22
|
+
* // redis opts:
|
|
23
|
+
* redisUrl: string?
|
|
24
|
+
* redisPassword: string?
|
|
25
|
+
* redisUsername: string?
|
|
26
|
+
* redisTls: boolean?
|
|
27
|
+
* redisCa: string|Buffer?
|
|
28
|
+
* // common:
|
|
29
|
+
* topicPrefix: string? — default "ws" so independent
|
|
30
|
+
* pubsub primitives sharing a
|
|
31
|
+
* backend (cache, etc.) don't
|
|
32
|
+
* collide on channel names.
|
|
33
|
+
* audit: boolean — emit system.ws.publish on each publish
|
|
18
34
|
* });
|
|
19
35
|
*
|
|
20
36
|
* r.ws("/socket", function (conn) {
|
|
21
37
|
* hub.attach(conn); // tracks lifecycle, auto-detach on close
|
|
22
38
|
* hub.subscribe(conn, "chat:room-1");
|
|
23
39
|
* hub.subscribe(conn, "presence:user-42");
|
|
24
|
-
*
|
|
25
|
-
* conn.on("message", function (msg) {
|
|
26
|
-
* // Operator-handled inbound; hub doesn't reflect inbound back.
|
|
27
|
-
* });
|
|
28
40
|
* });
|
|
29
41
|
*
|
|
30
42
|
* await hub.publish("chat:room-1", { user: "alice", text: "hi" });
|
|
@@ -34,29 +46,11 @@
|
|
|
34
46
|
* hub.channels(); // → ["chat:room-1", ...]
|
|
35
47
|
* hub.connectionChannels(conn); // → ["chat:room-1", ...]
|
|
36
48
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* _blamejs_ws_messages. Other nodes poll the table on
|
|
43
|
-
* pollIntervalMs (default 100ms) and dispatch new rows past their
|
|
44
|
-
* last-seen id to their local subscribers.
|
|
45
|
-
* - The publishing node skips its own rows on poll (publishedBy =
|
|
46
|
-
* self) so each subscriber sees the message exactly once.
|
|
47
|
-
* - Latency-sensitive operators bring their own backend (Redis,
|
|
48
|
-
* NATS, MQTT) by passing { publishRemote, start, stop }.
|
|
49
|
-
*
|
|
50
|
-
* Single-node mode:
|
|
51
|
-
* - 'local' backend (default when opts.cluster is not wired). publish
|
|
52
|
-
* synchronously dispatches; no DB writes; no poll loop.
|
|
53
|
-
* - Operators on a single node pay zero coordination overhead.
|
|
54
|
-
*
|
|
55
|
-
* Channel naming is operator-defined — the hub treats names as opaque
|
|
56
|
-
* strings. Common conventions: "chat:room-id", "presence:user-id",
|
|
57
|
-
* "metrics:hostname". For pattern matching subscribe to multiple
|
|
58
|
-
* specific channels rather than a wildcard — wildcards complicate
|
|
59
|
-
* cross-node fan-out and are deferred.
|
|
49
|
+
* Backend semantics live in `lib/pubsub.js`. The hub here is responsible
|
|
50
|
+
* for the WebSocket-specific connection lifecycle (attach / detach),
|
|
51
|
+
* the channel-to-conn map, and serialization of payloads for the local
|
|
52
|
+
* dispatch path; the cross-node delivery is one pubsub.subscribe per
|
|
53
|
+
* channel the hub joins, with the hub's `_localDispatch` as the handler.
|
|
60
54
|
*
|
|
61
55
|
* Error policy: a connection's send() that throws (closed socket, peer
|
|
62
56
|
* gone) does NOT break dispatch to other subscribers. The throwing
|
|
@@ -65,9 +59,7 @@
|
|
|
65
59
|
*/
|
|
66
60
|
|
|
67
61
|
var lazyRequire = require("./lazy-require");
|
|
68
|
-
var
|
|
69
|
-
var C = require("./constants");
|
|
70
|
-
var safeJson = require("./safe-json");
|
|
62
|
+
var pubsub = require("./pubsub");
|
|
71
63
|
var validateOpts = require("./validate-opts");
|
|
72
64
|
var { defineClass } = require("./framework-error");
|
|
73
65
|
|
|
@@ -76,157 +68,10 @@ var logger = lazyRequire(function () { return require("./log").boot("websocket-c
|
|
|
76
68
|
|
|
77
69
|
var WebSocketChannelsError = defineClass("WebSocketChannelsError");
|
|
78
70
|
|
|
79
|
-
var DEFAULT_POLL_INTERVAL_MS = 100;
|
|
80
|
-
var DEFAULT_RETENTION_MS = C.TIME.minutes(1);
|
|
81
|
-
var DEFAULT_PRUNE_EVERY_MS = C.TIME.minutes(5);
|
|
82
|
-
|
|
83
71
|
function _err(code, message) {
|
|
84
72
|
return new WebSocketChannelsError(code, message, true);
|
|
85
73
|
}
|
|
86
74
|
|
|
87
|
-
// ---- Backends ----
|
|
88
|
-
|
|
89
|
-
function _localBackend() {
|
|
90
|
-
// Single-node: no remote fan-out. publish() goes only to local subs.
|
|
91
|
-
return {
|
|
92
|
-
name: "local",
|
|
93
|
-
publishRemote: null,
|
|
94
|
-
start: null,
|
|
95
|
-
stop: null,
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function _clusterBackend(opts) {
|
|
100
|
-
var clusterInstance = opts.cluster;
|
|
101
|
-
var pollIntervalMs = opts.pollIntervalMs || DEFAULT_POLL_INTERVAL_MS;
|
|
102
|
-
var retentionMs = opts.retentionMs || DEFAULT_RETENTION_MS;
|
|
103
|
-
var pruneEveryMs = opts.pruneEveryMs || DEFAULT_PRUNE_EVERY_MS;
|
|
104
|
-
var lastSeenId = 0;
|
|
105
|
-
var primed = false;
|
|
106
|
-
var lastPruneAt = 0;
|
|
107
|
-
var pollTimer = null;
|
|
108
|
-
var stopped = false;
|
|
109
|
-
|
|
110
|
-
function _nodeId() {
|
|
111
|
-
if (clusterInstance && typeof clusterInstance.currentNodeId === "function") {
|
|
112
|
-
return clusterInstance.currentNodeId();
|
|
113
|
-
}
|
|
114
|
-
return "single-node-local";
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
async function publishRemote(channel, payload) {
|
|
118
|
-
var serialized = JSON.stringify(payload);
|
|
119
|
-
await clusterStorage.execute(
|
|
120
|
-
"INSERT INTO _blamejs_ws_messages " +
|
|
121
|
-
"(channel, payload, publishedAt, publishedBy) VALUES (?, ?, ?, ?)",
|
|
122
|
-
[channel, serialized, Date.now(), _nodeId()]
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
async function _poll(onRemoteMessage) {
|
|
127
|
-
if (stopped) return;
|
|
128
|
-
var nodeId = _nodeId();
|
|
129
|
-
try {
|
|
130
|
-
// First poll: prime lastSeenId to the current MAX so we don't
|
|
131
|
-
// re-dispatch every historical row on startup. Tracked via the
|
|
132
|
-
// `primed` flag separately from lastSeenId — when MAX(id) is 0
|
|
133
|
-
// (empty table) the value-equality check would trip on every
|
|
134
|
-
// poll without it.
|
|
135
|
-
if (!primed) {
|
|
136
|
-
var primer = await clusterStorage.execute(
|
|
137
|
-
"SELECT COALESCE(MAX(id), 0) AS maxId FROM _blamejs_ws_messages",
|
|
138
|
-
[]
|
|
139
|
-
);
|
|
140
|
-
if (primer.rows && primer.rows[0]) {
|
|
141
|
-
lastSeenId = Number(primer.rows[0].maxId) || 0;
|
|
142
|
-
}
|
|
143
|
-
primed = true;
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
var result = await clusterStorage.execute(
|
|
147
|
-
"SELECT id, channel, payload FROM _blamejs_ws_messages " +
|
|
148
|
-
"WHERE id > ? AND publishedBy <> ? ORDER BY id ASC",
|
|
149
|
-
[lastSeenId, nodeId]
|
|
150
|
-
);
|
|
151
|
-
var rows = result.rows || [];
|
|
152
|
-
for (var i = 0; i < rows.length; i++) {
|
|
153
|
-
var row = rows[i];
|
|
154
|
-
try {
|
|
155
|
-
var payload = safeJson.parse(row.payload);
|
|
156
|
-
onRemoteMessage(row.channel, payload);
|
|
157
|
-
} catch (e) {
|
|
158
|
-
try {
|
|
159
|
-
logger().warn("malformed fan-out row id=" + row.id +
|
|
160
|
-
": " + ((e && e.message) || String(e)));
|
|
161
|
-
} catch (_e) { /* logger best-effort */ }
|
|
162
|
-
}
|
|
163
|
-
if (Number(row.id) > lastSeenId) lastSeenId = Number(row.id);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Rate-limited prune of expired rows.
|
|
167
|
-
var now = Date.now();
|
|
168
|
-
if (now - lastPruneAt >= pruneEveryMs) {
|
|
169
|
-
lastPruneAt = now;
|
|
170
|
-
await clusterStorage.execute(
|
|
171
|
-
"DELETE FROM _blamejs_ws_messages WHERE publishedAt < ?",
|
|
172
|
-
[now - retentionMs]
|
|
173
|
-
);
|
|
174
|
-
}
|
|
175
|
-
} catch (e) {
|
|
176
|
-
try {
|
|
177
|
-
logger().warn("fan-out poll failed: " + ((e && e.message) || String(e)));
|
|
178
|
-
} catch (_e) { /* logger best-effort */ }
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function start(onRemoteMessage) {
|
|
183
|
-
if (pollTimer) return;
|
|
184
|
-
stopped = false;
|
|
185
|
-
var tick = function () {
|
|
186
|
-
_poll(onRemoteMessage).then(function () {
|
|
187
|
-
if (stopped) return;
|
|
188
|
-
pollTimer = setTimeout(tick, pollIntervalMs);
|
|
189
|
-
if (typeof pollTimer.unref === "function") pollTimer.unref();
|
|
190
|
-
}, function () {
|
|
191
|
-
if (stopped) return;
|
|
192
|
-
pollTimer = setTimeout(tick, pollIntervalMs);
|
|
193
|
-
if (typeof pollTimer.unref === "function") pollTimer.unref();
|
|
194
|
-
});
|
|
195
|
-
};
|
|
196
|
-
// Prime lastSeenId immediately so the first publish on a brand-new
|
|
197
|
-
// hub doesn't race the first poll-interval tick. Without this,
|
|
198
|
-
// publishes that happen between create() and the first poll would
|
|
199
|
-
// be missed by other nodes that haven't seeded their lastSeenId yet.
|
|
200
|
-
pollTimer = setTimeout(tick, 0);
|
|
201
|
-
if (typeof pollTimer.unref === "function") pollTimer.unref();
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function stop() {
|
|
205
|
-
stopped = true;
|
|
206
|
-
if (pollTimer) { clearTimeout(pollTimer); pollTimer = null; }
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
return {
|
|
210
|
-
name: "cluster",
|
|
211
|
-
publishRemote: publishRemote,
|
|
212
|
-
start: start,
|
|
213
|
-
stop: stop,
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function _resolveBackend(opts) {
|
|
218
|
-
var requested = opts.backend;
|
|
219
|
-
if (requested && typeof requested === "object" &&
|
|
220
|
-
typeof requested.publishRemote === "function") {
|
|
221
|
-
return Object.assign({ name: "custom" }, requested);
|
|
222
|
-
}
|
|
223
|
-
if (requested === "cluster") return _clusterBackend(opts);
|
|
224
|
-
if (!requested || requested === "local") return _localBackend();
|
|
225
|
-
throw _err("UNKNOWN_BACKEND",
|
|
226
|
-
"websocketChannels: unknown backend '" + requested +
|
|
227
|
-
"' (must be 'local', 'cluster', or { publishRemote, start, stop })");
|
|
228
|
-
}
|
|
229
|
-
|
|
230
75
|
// ---- Hub ----
|
|
231
76
|
|
|
232
77
|
function create(opts) {
|
|
@@ -234,19 +79,47 @@ function create(opts) {
|
|
|
234
79
|
validateOpts(opts, [
|
|
235
80
|
"backend", "audit", "cluster",
|
|
236
81
|
"pollIntervalMs", "retentionMs", "pruneEveryMs",
|
|
82
|
+
"redisUrl", "redisPassword", "redisUsername", "redisTls",
|
|
83
|
+
"redisCa", "redisServername",
|
|
84
|
+
"topicPrefix",
|
|
237
85
|
], "b.websocket");
|
|
238
86
|
var auditOn = !!opts.audit;
|
|
239
|
-
|
|
87
|
+
|
|
88
|
+
// Pub/sub fan-out is delegated to b.pubsub. The hub owns one pubsub
|
|
89
|
+
// instance per WebSocket-channels primitive; topicPrefix defaults to
|
|
90
|
+
// "ws" so independent pubsub primitives sharing the same backend
|
|
91
|
+
// (cache cluster invalidation, app-level pubsub) don't collide on
|
|
92
|
+
// channel names.
|
|
93
|
+
var ps = pubsub.create({
|
|
94
|
+
backend: opts.backend,
|
|
95
|
+
cluster: opts.cluster,
|
|
96
|
+
pollIntervalMs: opts.pollIntervalMs,
|
|
97
|
+
retentionMs: opts.retentionMs,
|
|
98
|
+
pruneEveryMs: opts.pruneEveryMs,
|
|
99
|
+
redisUrl: opts.redisUrl,
|
|
100
|
+
redisPassword: opts.redisPassword,
|
|
101
|
+
redisUsername: opts.redisUsername,
|
|
102
|
+
redisTls: opts.redisTls,
|
|
103
|
+
redisCa: opts.redisCa,
|
|
104
|
+
redisServername: opts.redisServername,
|
|
105
|
+
// Default empty so pre-existing operators / tests that publish on
|
|
106
|
+
// raw channel names keep working unchanged. Operators sharing one
|
|
107
|
+
// pubsub backend across cache + websockets + custom primitives
|
|
108
|
+
// pass a topicPrefix to isolate.
|
|
109
|
+
topicPrefix: opts.topicPrefix || "",
|
|
110
|
+
});
|
|
111
|
+
var backendName = ps.backend();
|
|
240
112
|
|
|
241
113
|
// channel -> Set<connection>
|
|
242
114
|
var channelToConns = new Map();
|
|
115
|
+
// channel -> pubsub-token (one subscribe per channel the hub
|
|
116
|
+
// currently has any local listener on; refcounted via the local
|
|
117
|
+
// map's size).
|
|
118
|
+
var channelToToken = new Map();
|
|
243
119
|
// connection -> Set<channel> (WeakMap so dropped conns don't leak)
|
|
244
120
|
var connToChannels = new WeakMap();
|
|
245
121
|
// Tracked connection set for surface methods that need to enumerate
|
|
246
|
-
// connections (e.g. close-all-on-shutdown).
|
|
247
|
-
// set on detach. We hold strong refs intentionally — the hub is the
|
|
248
|
-
// owner of this membership for as long as the operator hasn't said
|
|
249
|
-
// detach.
|
|
122
|
+
// connections (e.g. close-all-on-shutdown).
|
|
250
123
|
var attachedConns = new Set();
|
|
251
124
|
|
|
252
125
|
function _localDispatch(channel, payload) {
|
|
@@ -266,13 +139,17 @@ function create(opts) {
|
|
|
266
139
|
return sent;
|
|
267
140
|
}
|
|
268
141
|
|
|
269
|
-
//
|
|
270
|
-
|
|
271
|
-
|
|
142
|
+
// Pubsub message arrivals dispatch to local WebSocket connections.
|
|
143
|
+
// For local-backend pubsub the handler runs synchronously inside
|
|
144
|
+
// ps.publish(), so we accumulate the per-publish delivery count via
|
|
145
|
+
// the `dispatchTallies` map keyed by channel + sequence id — the
|
|
146
|
+
// hub's publish() reads it back after ps.publish() resolves.
|
|
147
|
+
var lastDispatchCount = 0;
|
|
148
|
+
function _onPubsubMessage(payload, ev) {
|
|
149
|
+
var n = _localDispatch(ev.channel, payload);
|
|
150
|
+
lastDispatchCount += n;
|
|
272
151
|
}
|
|
273
152
|
|
|
274
|
-
if (typeof backend.start === "function") backend.start(_onRemoteMessage);
|
|
275
|
-
|
|
276
153
|
function attach(conn) {
|
|
277
154
|
if (!conn || typeof conn.send !== "function") {
|
|
278
155
|
throw _err("INVALID_CONN", "attach(conn) requires a connection with .send()");
|
|
@@ -292,7 +169,14 @@ function create(opts) {
|
|
|
292
169
|
var subs = channelToConns.get(c);
|
|
293
170
|
if (subs) {
|
|
294
171
|
subs.delete(conn);
|
|
295
|
-
if (subs.size === 0)
|
|
172
|
+
if (subs.size === 0) {
|
|
173
|
+
channelToConns.delete(c);
|
|
174
|
+
var token = channelToToken.get(c);
|
|
175
|
+
if (token) {
|
|
176
|
+
ps.unsubscribe(token);
|
|
177
|
+
channelToToken.delete(c);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
296
180
|
}
|
|
297
181
|
}
|
|
298
182
|
connToChannels.delete(conn);
|
|
@@ -306,7 +190,13 @@ function create(opts) {
|
|
|
306
190
|
if (!connToChannels.has(conn)) {
|
|
307
191
|
throw _err("NOT_ATTACHED", "subscribe: connection must be attach()-ed first");
|
|
308
192
|
}
|
|
309
|
-
if (!channelToConns.has(channel))
|
|
193
|
+
if (!channelToConns.has(channel)) {
|
|
194
|
+
channelToConns.set(channel, new Set());
|
|
195
|
+
// First local listener for this channel — open a pubsub
|
|
196
|
+
// subscription so cross-node fan-out reaches us.
|
|
197
|
+
var token = ps.subscribe(channel, _onPubsubMessage);
|
|
198
|
+
channelToToken.set(channel, token);
|
|
199
|
+
}
|
|
310
200
|
channelToConns.get(channel).add(conn);
|
|
311
201
|
connToChannels.get(conn).add(channel);
|
|
312
202
|
}
|
|
@@ -315,7 +205,14 @@ function create(opts) {
|
|
|
315
205
|
var subs = channelToConns.get(channel);
|
|
316
206
|
if (subs) {
|
|
317
207
|
subs.delete(conn);
|
|
318
|
-
if (subs.size === 0)
|
|
208
|
+
if (subs.size === 0) {
|
|
209
|
+
channelToConns.delete(channel);
|
|
210
|
+
var token = channelToToken.get(channel);
|
|
211
|
+
if (token) {
|
|
212
|
+
ps.unsubscribe(token);
|
|
213
|
+
channelToToken.delete(channel);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
319
216
|
}
|
|
320
217
|
var chans = connToChannels.get(conn);
|
|
321
218
|
if (chans) chans.delete(channel);
|
|
@@ -325,25 +222,41 @@ function create(opts) {
|
|
|
325
222
|
if (typeof channel !== "string" || channel.length === 0) {
|
|
326
223
|
throw _err("INVALID_CHANNEL", "publish: channel must be a non-empty string");
|
|
327
224
|
}
|
|
328
|
-
|
|
225
|
+
// Pre-validate JSON serializability so circular / unserializable
|
|
226
|
+
// payloads throw INVALID_PAYLOAD on the operator's await rather
|
|
227
|
+
// than disappearing into the pubsub dispatcher's per-handler
|
|
228
|
+
// try/catch (where they'd surface only as a warn-level log).
|
|
229
|
+
try { JSON.stringify(payload); }
|
|
230
|
+
catch (e) {
|
|
231
|
+
throw _err("INVALID_PAYLOAD",
|
|
232
|
+
"publish payload is not JSON-serializable: " + (e && e.message));
|
|
233
|
+
}
|
|
234
|
+
lastDispatchCount = 0;
|
|
329
235
|
var remoteSent = false;
|
|
330
|
-
|
|
236
|
+
var localCount = 0;
|
|
237
|
+
try {
|
|
238
|
+
var rv = await ps.publish(channel, payload);
|
|
239
|
+
// ps.publish invokes _onPubsubMessage synchronously for the
|
|
240
|
+
// local-backend; lastDispatchCount holds the conn-level count.
|
|
241
|
+
// For remote-only backends the handler doesn't fire until a poll
|
|
242
|
+
// tick / push event arrives — at that point the message goes to
|
|
243
|
+
// every node including this one (publishedBy filter excluded for
|
|
244
|
+
// pubsub-cluster, redis sees its own publish via its subscribe
|
|
245
|
+
// socket too). So lastDispatchCount captures the local fan-out.
|
|
246
|
+
localCount = lastDispatchCount;
|
|
247
|
+
remoteSent = (rv && rv.remote > 0);
|
|
248
|
+
} catch (e) {
|
|
331
249
|
try {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
} catch (
|
|
335
|
-
try {
|
|
336
|
-
logger().error("publishRemote failed for channel '" + channel + "': " +
|
|
337
|
-
((e && e.message) || String(e)));
|
|
338
|
-
} catch (_e) { /* logger best-effort */ }
|
|
339
|
-
}
|
|
250
|
+
logger().error("publishRemote failed for channel '" + channel + "': " +
|
|
251
|
+
((e && e.message) || String(e)));
|
|
252
|
+
} catch (_e) { /* logger best-effort */ }
|
|
340
253
|
}
|
|
341
254
|
if (auditOn) {
|
|
342
255
|
audit().safeEmit({
|
|
343
256
|
action: "system.ws.publish",
|
|
344
257
|
metadata: {
|
|
345
258
|
channel: channel,
|
|
346
|
-
backend:
|
|
259
|
+
backend: backendName,
|
|
347
260
|
localDelivered: localCount,
|
|
348
261
|
remoteSent: remoteSent,
|
|
349
262
|
},
|
|
@@ -375,18 +288,20 @@ function create(opts) {
|
|
|
375
288
|
return attachedConns.size;
|
|
376
289
|
}
|
|
377
290
|
|
|
378
|
-
function close() {
|
|
379
|
-
if (typeof backend.stop === "function") backend.stop();
|
|
291
|
+
async function close() {
|
|
380
292
|
// Drop all subscriptions. Connections are not closed — that's the
|
|
381
293
|
// operator's call (`router.closeWebSockets()` is the framework's
|
|
382
294
|
// hook for graceful shutdown).
|
|
295
|
+
for (var token of channelToToken.values()) ps.unsubscribe(token);
|
|
296
|
+
channelToToken.clear();
|
|
383
297
|
channelToConns.clear();
|
|
384
298
|
for (var conn of attachedConns) connToChannels.delete(conn);
|
|
385
299
|
attachedConns.clear();
|
|
300
|
+
await ps.close();
|
|
386
301
|
}
|
|
387
302
|
|
|
388
303
|
return {
|
|
389
|
-
backend:
|
|
304
|
+
backend: backendName,
|
|
390
305
|
attach: attach,
|
|
391
306
|
detach: detach,
|
|
392
307
|
subscribe: subscribe,
|
|
@@ -398,16 +313,15 @@ function create(opts) {
|
|
|
398
313
|
connectionChannels: connectionChannels,
|
|
399
314
|
attachedCount: attachedCount,
|
|
400
315
|
close: close,
|
|
401
|
-
// Test hook — directly inject a remote message as if the
|
|
402
|
-
// backend's
|
|
403
|
-
_injectRemoteMessage:
|
|
316
|
+
// Test hook — directly inject a remote message as if the pubsub
|
|
317
|
+
// backend's transport just received it.
|
|
318
|
+
_injectRemoteMessage: function (channel, payload) {
|
|
319
|
+
_localDispatch(channel, payload);
|
|
320
|
+
},
|
|
404
321
|
};
|
|
405
322
|
}
|
|
406
323
|
|
|
407
324
|
module.exports = {
|
|
408
325
|
create: create,
|
|
409
326
|
WebSocketChannelsError: WebSocketChannelsError,
|
|
410
|
-
// Backend factories exported for tests + advanced operator wiring.
|
|
411
|
-
_localBackend: _localBackend,
|
|
412
|
-
_clusterBackend: _clusterBackend,
|
|
413
327
|
};
|
package/package.json
CHANGED
package/sbom.cyclonedx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:731bee30-9e20-4cc1-87e9-2ca1cf7a6e31",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
8
|
+
"timestamp": "2026-05-02T20:14:03.424Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/core@0.6.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.6.34",
|
|
23
23
|
"type": "library",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.6.
|
|
25
|
+
"version": "0.6.34",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "The Node framework that owns its stack.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/core@0.6.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.6.34",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/core@0.6.
|
|
57
|
+
"ref": "@blamejs/core@0.6.34",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|