@furystack/cross-node-bus 1.0.0
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 +78 -0
- package/README.md +49 -0
- package/esm/cross-node-bus-telemetry.d.ts +74 -0
- package/esm/cross-node-bus-telemetry.d.ts.map +1 -0
- package/esm/cross-node-bus-telemetry.js +28 -0
- package/esm/cross-node-bus-telemetry.js.map +1 -0
- package/esm/cross-node-bus-telemetry.spec.d.ts +2 -0
- package/esm/cross-node-bus-telemetry.spec.d.ts.map +1 -0
- package/esm/cross-node-bus-telemetry.spec.js +115 -0
- package/esm/cross-node-bus-telemetry.spec.js.map +1 -0
- package/esm/cross-node-bus.d.ts +78 -0
- package/esm/cross-node-bus.d.ts.map +1 -0
- package/esm/cross-node-bus.js +18 -0
- package/esm/cross-node-bus.js.map +1 -0
- package/esm/cross-node-bus.spec.d.ts +2 -0
- package/esm/cross-node-bus.spec.d.ts.map +1 -0
- package/esm/cross-node-bus.spec.js +123 -0
- package/esm/cross-node-bus.spec.js.map +1 -0
- package/esm/define-in-process-cross-node-bus.d.ts +26 -0
- package/esm/define-in-process-cross-node-bus.d.ts.map +1 -0
- package/esm/define-in-process-cross-node-bus.js +27 -0
- package/esm/define-in-process-cross-node-bus.js.map +1 -0
- package/esm/errors.d.ts +21 -0
- package/esm/errors.d.ts.map +1 -0
- package/esm/errors.js +30 -0
- package/esm/errors.js.map +1 -0
- package/esm/errors.spec.d.ts +2 -0
- package/esm/errors.spec.d.ts.map +1 -0
- package/esm/errors.spec.js +30 -0
- package/esm/errors.spec.js.map +1 -0
- package/esm/in-process-cross-node-bus.d.ts +58 -0
- package/esm/in-process-cross-node-bus.d.ts.map +1 -0
- package/esm/in-process-cross-node-bus.js +196 -0
- package/esm/in-process-cross-node-bus.js.map +1 -0
- package/esm/in-process-cross-node-bus.spec.d.ts +2 -0
- package/esm/in-process-cross-node-bus.spec.d.ts.map +1 -0
- package/esm/in-process-cross-node-bus.spec.js +737 -0
- package/esm/in-process-cross-node-bus.spec.js.map +1 -0
- package/esm/index.d.ts +8 -0
- package/esm/index.d.ts.map +1 -0
- package/esm/index.js +7 -0
- package/esm/index.js.map +1 -0
- package/esm/memory-broker.d.ts +74 -0
- package/esm/memory-broker.d.ts.map +1 -0
- package/esm/memory-broker.js +156 -0
- package/esm/memory-broker.js.map +1 -0
- package/esm/memory-broker.spec.d.ts +2 -0
- package/esm/memory-broker.spec.d.ts.map +1 -0
- package/esm/memory-broker.spec.js +497 -0
- package/esm/memory-broker.spec.js.map +1 -0
- package/esm/testing/create-in-process-bus-network.d.ts +49 -0
- package/esm/testing/create-in-process-bus-network.d.ts.map +1 -0
- package/esm/testing/create-in-process-bus-network.js +54 -0
- package/esm/testing/create-in-process-bus-network.js.map +1 -0
- package/esm/testing/create-in-process-bus-network.spec.d.ts +2 -0
- package/esm/testing/create-in-process-bus-network.spec.d.ts.map +1 -0
- package/esm/testing/create-in-process-bus-network.spec.js +142 -0
- package/esm/testing/create-in-process-bus-network.spec.js.map +1 -0
- package/esm/testing/index.d.ts +2 -0
- package/esm/testing/index.d.ts.map +1 -0
- package/esm/testing/index.js +2 -0
- package/esm/testing/index.js.map +1 -0
- package/esm/types.d.ts +35 -0
- package/esm/types.d.ts.map +1 -0
- package/esm/types.js +2 -0
- package/esm/types.js.map +1 -0
- package/package.json +56 -0
- package/src/cross-node-bus-telemetry.spec.ts +44 -0
- package/src/cross-node-bus-telemetry.ts +69 -0
- package/src/cross-node-bus.spec.ts +41 -0
- package/src/cross-node-bus.ts +92 -0
- package/src/define-in-process-cross-node-bus.ts +38 -0
- package/src/errors.spec.ts +32 -0
- package/src/errors.ts +38 -0
- package/src/in-process-cross-node-bus.spec.ts +428 -0
- package/src/in-process-cross-node-bus.ts +248 -0
- package/src/index.ts +7 -0
- package/src/memory-broker.spec.ts +282 -0
- package/src/memory-broker.ts +199 -0
- package/src/testing/create-in-process-bus-network.spec.ts +73 -0
- package/src/testing/create-in-process-bus-network.ts +87 -0
- package/src/testing/index.ts +1 -0
- package/src/types.ts +35 -0
|
@@ -0,0 +1,737 @@
|
|
|
1
|
+
var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
|
|
2
|
+
if (value !== null && value !== void 0) {
|
|
3
|
+
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
|
|
4
|
+
var dispose, inner;
|
|
5
|
+
if (async) {
|
|
6
|
+
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
|
|
7
|
+
dispose = value[Symbol.asyncDispose];
|
|
8
|
+
}
|
|
9
|
+
if (dispose === void 0) {
|
|
10
|
+
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
|
|
11
|
+
dispose = value[Symbol.dispose];
|
|
12
|
+
if (async) inner = dispose;
|
|
13
|
+
}
|
|
14
|
+
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
|
|
15
|
+
if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
|
|
16
|
+
env.stack.push({ value: value, dispose: dispose, async: async });
|
|
17
|
+
}
|
|
18
|
+
else if (async) {
|
|
19
|
+
env.stack.push({ async: true });
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
};
|
|
23
|
+
var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
|
|
24
|
+
return function (env) {
|
|
25
|
+
function fail(e) {
|
|
26
|
+
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
|
|
27
|
+
env.hasError = true;
|
|
28
|
+
}
|
|
29
|
+
var r, s = 0;
|
|
30
|
+
function next() {
|
|
31
|
+
while (r = env.stack.pop()) {
|
|
32
|
+
try {
|
|
33
|
+
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
|
|
34
|
+
if (r.dispose) {
|
|
35
|
+
var result = r.dispose.call(r.value);
|
|
36
|
+
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
|
|
37
|
+
}
|
|
38
|
+
else s |= 1;
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
fail(e);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
|
|
45
|
+
if (env.hasError) throw env.error;
|
|
46
|
+
}
|
|
47
|
+
return next();
|
|
48
|
+
};
|
|
49
|
+
})(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
50
|
+
var e = new Error(message);
|
|
51
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
52
|
+
});
|
|
53
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
54
|
+
import { CrossNodeBusTelemetry } from './cross-node-bus-telemetry.js';
|
|
55
|
+
import { ReplayWindowExceededError } from './errors.js';
|
|
56
|
+
import { InProcessCrossNodeBus } from './in-process-cross-node-bus.js';
|
|
57
|
+
import { MemoryBroker } from './memory-broker.js';
|
|
58
|
+
const collect = async (iterable) => {
|
|
59
|
+
const out = [];
|
|
60
|
+
for await (const message of iterable)
|
|
61
|
+
out.push(message);
|
|
62
|
+
return out;
|
|
63
|
+
};
|
|
64
|
+
describe('InProcessCrossNodeBus', () => {
|
|
65
|
+
describe('basics', () => {
|
|
66
|
+
it('exposes a non-empty nodeId by default', () => {
|
|
67
|
+
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
68
|
+
try {
|
|
69
|
+
const bus = __addDisposableResource(env_1, new InProcessCrossNodeBus(), false);
|
|
70
|
+
expect(typeof bus.nodeId).toBe('string');
|
|
71
|
+
expect(bus.nodeId.length).toBeGreaterThan(0);
|
|
72
|
+
expect(bus.nodeId.startsWith('local-')).toBe(true);
|
|
73
|
+
}
|
|
74
|
+
catch (e_1) {
|
|
75
|
+
env_1.error = e_1;
|
|
76
|
+
env_1.hasError = true;
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
__disposeResources(env_1);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
it('honors a caller-supplied nodeId', () => {
|
|
83
|
+
const env_2 = { stack: [], error: void 0, hasError: false };
|
|
84
|
+
try {
|
|
85
|
+
const bus = __addDisposableResource(env_2, new InProcessCrossNodeBus({ nodeId: 'fixed' }), false);
|
|
86
|
+
expect(bus.nodeId).toBe('fixed');
|
|
87
|
+
}
|
|
88
|
+
catch (e_2) {
|
|
89
|
+
env_2.error = e_2;
|
|
90
|
+
env_2.hasError = true;
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
__disposeResources(env_2);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
it('declares replay + assignsSequence capabilities and non-persistent', () => {
|
|
97
|
+
const env_3 = { stack: [], error: void 0, hasError: false };
|
|
98
|
+
try {
|
|
99
|
+
const bus = __addDisposableResource(env_3, new InProcessCrossNodeBus(), false);
|
|
100
|
+
expect(bus.capabilities).toEqual({ persistent: false, replay: true, assignsSequence: true });
|
|
101
|
+
}
|
|
102
|
+
catch (e_3) {
|
|
103
|
+
env_3.error = e_3;
|
|
104
|
+
env_3.hasError = true;
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
__disposeResources(env_3);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
describe('publish + subscribe', () => {
|
|
112
|
+
it('delivers self-published messages by default', async () => {
|
|
113
|
+
const env_4 = { stack: [], error: void 0, hasError: false };
|
|
114
|
+
try {
|
|
115
|
+
const bus = __addDisposableResource(env_4, new InProcessCrossNodeBus({ nodeId: 'self' }), false);
|
|
116
|
+
const handler = vi.fn();
|
|
117
|
+
const _sub = __addDisposableResource(env_4, bus.subscribe('topic', handler), false);
|
|
118
|
+
await bus.publish('topic', { n: 1 });
|
|
119
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
120
|
+
const message = handler.mock.calls[0]?.[0];
|
|
121
|
+
expect(message.originId).toBe('self');
|
|
122
|
+
expect(message.payload).toEqual({ n: 1 });
|
|
123
|
+
expect(message.seq).toBe('1');
|
|
124
|
+
}
|
|
125
|
+
catch (e_4) {
|
|
126
|
+
env_4.error = e_4;
|
|
127
|
+
env_4.hasError = true;
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
__disposeResources(env_4);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
it('multiplexes a single broker subscription across local handlers', async () => {
|
|
134
|
+
const broker = new MemoryBroker();
|
|
135
|
+
const brokerSubscribe = vi.spyOn(broker, 'subscribe');
|
|
136
|
+
try {
|
|
137
|
+
const env_5 = { stack: [], error: void 0, hasError: false };
|
|
138
|
+
try {
|
|
139
|
+
const bus = __addDisposableResource(env_5, new InProcessCrossNodeBus({ broker, nodeId: 'self' }), false);
|
|
140
|
+
const _a = __addDisposableResource(env_5, bus.subscribe('topic', () => undefined), false);
|
|
141
|
+
const _b = __addDisposableResource(env_5, bus.subscribe('topic', () => undefined), false);
|
|
142
|
+
const _c = __addDisposableResource(env_5, bus.subscribe('topic', () => undefined), false);
|
|
143
|
+
expect(brokerSubscribe).toHaveBeenCalledTimes(1);
|
|
144
|
+
}
|
|
145
|
+
catch (e_5) {
|
|
146
|
+
env_5.error = e_5;
|
|
147
|
+
env_5.hasError = true;
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
__disposeResources(env_5);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
broker[Symbol.dispose]();
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
it('releases the broker subscription when the last local handler disposes', async () => {
|
|
158
|
+
const env_6 = { stack: [], error: void 0, hasError: false };
|
|
159
|
+
try {
|
|
160
|
+
const broker = __addDisposableResource(env_6, new MemoryBroker(), false);
|
|
161
|
+
const bus = __addDisposableResource(env_6, new InProcessCrossNodeBus({ broker, nodeId: 'self' }), false);
|
|
162
|
+
const handler = vi.fn();
|
|
163
|
+
const sub = bus.subscribe('topic', handler);
|
|
164
|
+
sub[Symbol.dispose]();
|
|
165
|
+
await bus.publish('topic', null);
|
|
166
|
+
expect(handler).not.toHaveBeenCalled();
|
|
167
|
+
const handler2 = vi.fn();
|
|
168
|
+
const _sub2 = __addDisposableResource(env_6, bus.subscribe('topic', handler2), false);
|
|
169
|
+
await bus.publish('topic', null);
|
|
170
|
+
expect(handler2).toHaveBeenCalledTimes(1);
|
|
171
|
+
}
|
|
172
|
+
catch (e_6) {
|
|
173
|
+
env_6.error = e_6;
|
|
174
|
+
env_6.hasError = true;
|
|
175
|
+
}
|
|
176
|
+
finally {
|
|
177
|
+
__disposeResources(env_6);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
it('isolates one local handler error from another and from the bus', async () => {
|
|
181
|
+
const env_7 = { stack: [], error: void 0, hasError: false };
|
|
182
|
+
try {
|
|
183
|
+
const bus = __addDisposableResource(env_7, new InProcessCrossNodeBus({ nodeId: 'self' }), false);
|
|
184
|
+
const ok = vi.fn();
|
|
185
|
+
const _failing = __addDisposableResource(env_7, bus.subscribe('topic', () => {
|
|
186
|
+
throw new Error('boom');
|
|
187
|
+
}), false);
|
|
188
|
+
const _ok = __addDisposableResource(env_7, bus.subscribe('topic', ok), false);
|
|
189
|
+
await expect(bus.publish('topic', null)).resolves.toBeUndefined();
|
|
190
|
+
expect(ok).toHaveBeenCalledTimes(1);
|
|
191
|
+
}
|
|
192
|
+
catch (e_7) {
|
|
193
|
+
env_7.error = e_7;
|
|
194
|
+
env_7.hasError = true;
|
|
195
|
+
}
|
|
196
|
+
finally {
|
|
197
|
+
__disposeResources(env_7);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
it('subscribeRemoteOnly skips messages originating on this node', async () => {
|
|
201
|
+
const env_8 = { stack: [], error: void 0, hasError: false };
|
|
202
|
+
try {
|
|
203
|
+
const broker = __addDisposableResource(env_8, new MemoryBroker(), false);
|
|
204
|
+
const busA = __addDisposableResource(env_8, new InProcessCrossNodeBus({ broker, nodeId: 'a' }), false);
|
|
205
|
+
const busB = __addDisposableResource(env_8, new InProcessCrossNodeBus({ broker, nodeId: 'b' }), false);
|
|
206
|
+
const handler = vi.fn();
|
|
207
|
+
const _sub = __addDisposableResource(env_8, busA.subscribeRemoteOnly('topic', handler), false);
|
|
208
|
+
await busA.publish('topic', { from: 'a' });
|
|
209
|
+
await busB.publish('topic', { from: 'b' });
|
|
210
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
211
|
+
expect((handler.mock.calls[0]?.[0]).originId).toBe('b');
|
|
212
|
+
}
|
|
213
|
+
catch (e_8) {
|
|
214
|
+
env_8.error = e_8;
|
|
215
|
+
env_8.hasError = true;
|
|
216
|
+
}
|
|
217
|
+
finally {
|
|
218
|
+
__disposeResources(env_8);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
describe('topicPrefix + subscribeForeign', () => {
|
|
223
|
+
it('prefixes topics on the wire and isolates buses with different prefixes', async () => {
|
|
224
|
+
const env_9 = { stack: [], error: void 0, hasError: false };
|
|
225
|
+
try {
|
|
226
|
+
const broker = __addDisposableResource(env_9, new MemoryBroker(), false);
|
|
227
|
+
const svcA = __addDisposableResource(env_9, new InProcessCrossNodeBus({ broker, nodeId: 'a', topicPrefix: 'svc-a/' }), false);
|
|
228
|
+
const svcB = __addDisposableResource(env_9, new InProcessCrossNodeBus({ broker, nodeId: 'b', topicPrefix: 'svc-b/' }), false);
|
|
229
|
+
const onA = vi.fn();
|
|
230
|
+
const onB = vi.fn();
|
|
231
|
+
const _subA = __addDisposableResource(env_9, svcA.subscribe('events', onA), false);
|
|
232
|
+
const _subB = __addDisposableResource(env_9, svcB.subscribe('events', onB), false);
|
|
233
|
+
await svcA.publish('events', { from: 'a' });
|
|
234
|
+
expect(onA).toHaveBeenCalledTimes(1);
|
|
235
|
+
expect(onB).not.toHaveBeenCalled();
|
|
236
|
+
}
|
|
237
|
+
catch (e_9) {
|
|
238
|
+
env_9.error = e_9;
|
|
239
|
+
env_9.hasError = true;
|
|
240
|
+
}
|
|
241
|
+
finally {
|
|
242
|
+
__disposeResources(env_9);
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
it('subscribeForeign delivers messages from a foreign prefix on demand', async () => {
|
|
246
|
+
const env_10 = { stack: [], error: void 0, hasError: false };
|
|
247
|
+
try {
|
|
248
|
+
const broker = __addDisposableResource(env_10, new MemoryBroker(), false);
|
|
249
|
+
const svcA = __addDisposableResource(env_10, new InProcessCrossNodeBus({ broker, nodeId: 'a', topicPrefix: 'svc-a/' }), false);
|
|
250
|
+
const svcB = __addDisposableResource(env_10, new InProcessCrossNodeBus({ broker, nodeId: 'b', topicPrefix: 'svc-b/' }), false);
|
|
251
|
+
const eavesdrop = vi.fn();
|
|
252
|
+
const _foreign = __addDisposableResource(env_10, svcB.subscribeForeign('svc-a/', 'events', eavesdrop), false);
|
|
253
|
+
await svcA.publish('events', { from: 'a' });
|
|
254
|
+
expect(eavesdrop).toHaveBeenCalledTimes(1);
|
|
255
|
+
expect((eavesdrop.mock.calls[0]?.[0]).originId).toBe('a');
|
|
256
|
+
}
|
|
257
|
+
catch (e_10) {
|
|
258
|
+
env_10.error = e_10;
|
|
259
|
+
env_10.hasError = true;
|
|
260
|
+
}
|
|
261
|
+
finally {
|
|
262
|
+
__disposeResources(env_10);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
describe('replay', () => {
|
|
267
|
+
it('yields messages newer than fromSeq', async () => {
|
|
268
|
+
const env_11 = { stack: [], error: void 0, hasError: false };
|
|
269
|
+
try {
|
|
270
|
+
const bus = __addDisposableResource(env_11, new InProcessCrossNodeBus({ nodeId: 'a' }), false);
|
|
271
|
+
await bus.publish('topic', { n: 1 });
|
|
272
|
+
await bus.publish('topic', { n: 2 });
|
|
273
|
+
await bus.publish('topic', { n: 3 });
|
|
274
|
+
const yielded = await collect(bus.replay('topic', '1'));
|
|
275
|
+
expect(yielded.map((m) => m.payload.n)).toEqual([2, 3]);
|
|
276
|
+
}
|
|
277
|
+
catch (e_11) {
|
|
278
|
+
env_11.error = e_11;
|
|
279
|
+
env_11.hasError = true;
|
|
280
|
+
}
|
|
281
|
+
finally {
|
|
282
|
+
__disposeResources(env_11);
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
it('throws ReplayWindowExceededError when fromSeq is too old', () => {
|
|
286
|
+
const bus = new InProcessCrossNodeBus({ replayWindow: 2 });
|
|
287
|
+
try {
|
|
288
|
+
void bus.publish('topic', null);
|
|
289
|
+
void bus.publish('topic', null);
|
|
290
|
+
void bus.publish('topic', null);
|
|
291
|
+
expect(() => bus.replay('topic', '0')).toThrow(ReplayWindowExceededError);
|
|
292
|
+
}
|
|
293
|
+
finally {
|
|
294
|
+
bus[Symbol.dispose]();
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
describe('telemetry', () => {
|
|
299
|
+
it('emits onCrossNodePublished with byteLength after a successful publish', async () => {
|
|
300
|
+
const telemetry = new CrossNodeBusTelemetry();
|
|
301
|
+
try {
|
|
302
|
+
const env_12 = { stack: [], error: void 0, hasError: false };
|
|
303
|
+
try {
|
|
304
|
+
const bus = __addDisposableResource(env_12, new InProcessCrossNodeBus({ nodeId: 'a', telemetry }), false);
|
|
305
|
+
const events = [];
|
|
306
|
+
const _sub = __addDisposableResource(env_12, telemetry.subscribe('onCrossNodePublished', (event) => {
|
|
307
|
+
events.push(event);
|
|
308
|
+
}), false);
|
|
309
|
+
await bus.publish('topic', { hi: 'there' });
|
|
310
|
+
expect(events).toEqual([
|
|
311
|
+
{ topic: 'topic', originId: 'a', byteLength: Buffer.byteLength(JSON.stringify({ hi: 'there' })) },
|
|
312
|
+
]);
|
|
313
|
+
}
|
|
314
|
+
catch (e_12) {
|
|
315
|
+
env_12.error = e_12;
|
|
316
|
+
env_12.hasError = true;
|
|
317
|
+
}
|
|
318
|
+
finally {
|
|
319
|
+
__disposeResources(env_12);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
finally {
|
|
323
|
+
telemetry[Symbol.dispose]();
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
it('emits onCrossNodeReceived once per arrival regardless of handler count', async () => {
|
|
327
|
+
const telemetry = new CrossNodeBusTelemetry();
|
|
328
|
+
try {
|
|
329
|
+
const env_13 = { stack: [], error: void 0, hasError: false };
|
|
330
|
+
try {
|
|
331
|
+
const bus = __addDisposableResource(env_13, new InProcessCrossNodeBus({ nodeId: 'a', telemetry }), false);
|
|
332
|
+
const arrivals = vi.fn();
|
|
333
|
+
const _sub = __addDisposableResource(env_13, telemetry.subscribe('onCrossNodeReceived', arrivals), false);
|
|
334
|
+
const _h1 = __addDisposableResource(env_13, bus.subscribe('topic', () => undefined), false);
|
|
335
|
+
const _h2 = __addDisposableResource(env_13, bus.subscribe('topic', () => undefined), false);
|
|
336
|
+
const _h3 = __addDisposableResource(env_13, bus.subscribe('topic', () => undefined), false);
|
|
337
|
+
await bus.publish('topic', null);
|
|
338
|
+
expect(arrivals).toHaveBeenCalledTimes(1);
|
|
339
|
+
}
|
|
340
|
+
catch (e_13) {
|
|
341
|
+
env_13.error = e_13;
|
|
342
|
+
env_13.hasError = true;
|
|
343
|
+
}
|
|
344
|
+
finally {
|
|
345
|
+
__disposeResources(env_13);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
finally {
|
|
349
|
+
telemetry[Symbol.dispose]();
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
it('emits onCrossNodeError with phase=subscribe when a local handler throws', async () => {
|
|
353
|
+
const telemetry = new CrossNodeBusTelemetry();
|
|
354
|
+
try {
|
|
355
|
+
const env_14 = { stack: [], error: void 0, hasError: false };
|
|
356
|
+
try {
|
|
357
|
+
const bus = __addDisposableResource(env_14, new InProcessCrossNodeBus({ nodeId: 'a', telemetry }), false);
|
|
358
|
+
const errors = [];
|
|
359
|
+
const _sub = __addDisposableResource(env_14, telemetry.subscribe('onCrossNodeError', (event) => {
|
|
360
|
+
errors.push(event);
|
|
361
|
+
}), false);
|
|
362
|
+
const _failing = __addDisposableResource(env_14, bus.subscribe('topic', () => {
|
|
363
|
+
throw new Error('boom');
|
|
364
|
+
}), false);
|
|
365
|
+
await bus.publish('topic', null);
|
|
366
|
+
expect(errors).toHaveLength(1);
|
|
367
|
+
expect(errors[0]?.phase).toBe('subscribe');
|
|
368
|
+
expect(errors[0]?.topic).toBe('topic');
|
|
369
|
+
}
|
|
370
|
+
catch (e_14) {
|
|
371
|
+
env_14.error = e_14;
|
|
372
|
+
env_14.hasError = true;
|
|
373
|
+
}
|
|
374
|
+
finally {
|
|
375
|
+
__disposeResources(env_14);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
finally {
|
|
379
|
+
telemetry[Symbol.dispose]();
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
it('emits onCrossNodeError with phase=subscribe when a direct broker subscriber throws during fan-out', async () => {
|
|
383
|
+
const env_15 = { stack: [], error: void 0, hasError: false };
|
|
384
|
+
try {
|
|
385
|
+
const telemetry = new CrossNodeBusTelemetry();
|
|
386
|
+
const broker = __addDisposableResource(env_15, new MemoryBroker(), false);
|
|
387
|
+
try {
|
|
388
|
+
const env_16 = { stack: [], error: void 0, hasError: false };
|
|
389
|
+
try {
|
|
390
|
+
const bus = __addDisposableResource(env_16, new InProcessCrossNodeBus({ broker, nodeId: 'a', telemetry }), false);
|
|
391
|
+
const errors = [];
|
|
392
|
+
const _sub = __addDisposableResource(env_16, telemetry.subscribe('onCrossNodeError', (event) => {
|
|
393
|
+
errors.push(event);
|
|
394
|
+
})
|
|
395
|
+
// A direct broker subscriber bypasses the bus's `#deliver` try/catch;
|
|
396
|
+
// its throw routes through the broker's `onSubscriberError` sink.
|
|
397
|
+
, false);
|
|
398
|
+
// A direct broker subscriber bypasses the bus's `#deliver` try/catch;
|
|
399
|
+
// its throw routes through the broker's `onSubscriberError` sink.
|
|
400
|
+
const _failing = __addDisposableResource(env_16, broker.subscribe('topic', () => {
|
|
401
|
+
throw new Error('boom');
|
|
402
|
+
}), false);
|
|
403
|
+
await bus.publish('topic', null);
|
|
404
|
+
expect(errors.some((event) => event.phase === 'subscribe' && event.topic === 'topic')).toBe(true);
|
|
405
|
+
}
|
|
406
|
+
catch (e_15) {
|
|
407
|
+
env_16.error = e_15;
|
|
408
|
+
env_16.hasError = true;
|
|
409
|
+
}
|
|
410
|
+
finally {
|
|
411
|
+
__disposeResources(env_16);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
finally {
|
|
415
|
+
telemetry[Symbol.dispose]();
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
catch (e_16) {
|
|
419
|
+
env_15.error = e_16;
|
|
420
|
+
env_15.hasError = true;
|
|
421
|
+
}
|
|
422
|
+
finally {
|
|
423
|
+
__disposeResources(env_15);
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
it('emits onCrossNodeError with phase=subscribe when broker.subscribe throws on first hookup', () => {
|
|
427
|
+
const telemetry = new CrossNodeBusTelemetry();
|
|
428
|
+
const broker = new MemoryBroker();
|
|
429
|
+
try {
|
|
430
|
+
const env_17 = { stack: [], error: void 0, hasError: false };
|
|
431
|
+
try {
|
|
432
|
+
const bus = __addDisposableResource(env_17, new InProcessCrossNodeBus({ broker, nodeId: 'a', telemetry }), false);
|
|
433
|
+
const errors = [];
|
|
434
|
+
const _sub = __addDisposableResource(env_17, telemetry.subscribe('onCrossNodeError', (event) => {
|
|
435
|
+
errors.push(event);
|
|
436
|
+
}), false);
|
|
437
|
+
broker[Symbol.dispose]();
|
|
438
|
+
expect(() => bus.subscribe('topic', () => undefined)).toThrow(/disposed/);
|
|
439
|
+
expect(errors.some((event) => event.phase === 'subscribe')).toBe(true);
|
|
440
|
+
}
|
|
441
|
+
catch (e_17) {
|
|
442
|
+
env_17.error = e_17;
|
|
443
|
+
env_17.hasError = true;
|
|
444
|
+
}
|
|
445
|
+
finally {
|
|
446
|
+
__disposeResources(env_17);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
finally {
|
|
450
|
+
telemetry[Symbol.dispose]();
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
it('emits onCrossNodeError with phase=publish when the broker rejects publish', async () => {
|
|
454
|
+
const telemetry = new CrossNodeBusTelemetry();
|
|
455
|
+
const broker = new MemoryBroker();
|
|
456
|
+
try {
|
|
457
|
+
const env_18 = { stack: [], error: void 0, hasError: false };
|
|
458
|
+
try {
|
|
459
|
+
const bus = __addDisposableResource(env_18, new InProcessCrossNodeBus({ broker, nodeId: 'a', telemetry }), false);
|
|
460
|
+
const errors = [];
|
|
461
|
+
const _sub = __addDisposableResource(env_18, telemetry.subscribe('onCrossNodeError', (event) => {
|
|
462
|
+
errors.push(event);
|
|
463
|
+
}), false);
|
|
464
|
+
broker[Symbol.dispose]();
|
|
465
|
+
await expect(bus.publish('topic', null)).rejects.toThrow(/disposed/);
|
|
466
|
+
expect(errors.some((event) => event.phase === 'publish')).toBe(true);
|
|
467
|
+
}
|
|
468
|
+
catch (e_18) {
|
|
469
|
+
env_18.error = e_18;
|
|
470
|
+
env_18.hasError = true;
|
|
471
|
+
}
|
|
472
|
+
finally {
|
|
473
|
+
__disposeResources(env_18);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
finally {
|
|
477
|
+
telemetry[Symbol.dispose]();
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
it('emits onCrossNodeError with phase=replay when the underlying broker rejects', () => {
|
|
481
|
+
const env_19 = { stack: [], error: void 0, hasError: false };
|
|
482
|
+
try {
|
|
483
|
+
const telemetry = new CrossNodeBusTelemetry();
|
|
484
|
+
const broker = __addDisposableResource(env_19, new MemoryBroker({ replayWindow: 1 }), false);
|
|
485
|
+
try {
|
|
486
|
+
const env_20 = { stack: [], error: void 0, hasError: false };
|
|
487
|
+
try {
|
|
488
|
+
const bus = __addDisposableResource(env_20, new InProcessCrossNodeBus({ broker, nodeId: 'a', telemetry }), false);
|
|
489
|
+
const errors = [];
|
|
490
|
+
const _sub = __addDisposableResource(env_20, telemetry.subscribe('onCrossNodeError', (event) => {
|
|
491
|
+
errors.push(event);
|
|
492
|
+
}), false);
|
|
493
|
+
void bus.publish('topic', null);
|
|
494
|
+
void bus.publish('topic', null);
|
|
495
|
+
expect(() => bus.replay('topic', '0')).toThrow();
|
|
496
|
+
expect(errors.some((event) => event.phase === 'replay')).toBe(true);
|
|
497
|
+
}
|
|
498
|
+
catch (e_19) {
|
|
499
|
+
env_20.error = e_19;
|
|
500
|
+
env_20.hasError = true;
|
|
501
|
+
}
|
|
502
|
+
finally {
|
|
503
|
+
__disposeResources(env_20);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
finally {
|
|
507
|
+
telemetry[Symbol.dispose]();
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
catch (e_20) {
|
|
511
|
+
env_19.error = e_20;
|
|
512
|
+
env_19.hasError = true;
|
|
513
|
+
}
|
|
514
|
+
finally {
|
|
515
|
+
__disposeResources(env_19);
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
it('emits onCrossNodeWindowEvicted with displayTopic when a private broker rolls', async () => {
|
|
519
|
+
const telemetry = new CrossNodeBusTelemetry();
|
|
520
|
+
try {
|
|
521
|
+
const env_21 = { stack: [], error: void 0, hasError: false };
|
|
522
|
+
try {
|
|
523
|
+
const bus = __addDisposableResource(env_21, new InProcessCrossNodeBus({ nodeId: 'a', telemetry, replayWindow: 2 }), false);
|
|
524
|
+
const evictions = [];
|
|
525
|
+
const _sub = __addDisposableResource(env_21, telemetry.subscribe('onCrossNodeWindowEvicted', (event) => {
|
|
526
|
+
evictions.push(event);
|
|
527
|
+
})
|
|
528
|
+
// No subscriber → displayTopic falls back to wire (= 'topic' here, no prefix)
|
|
529
|
+
, false);
|
|
530
|
+
// No subscriber → displayTopic falls back to wire (= 'topic' here, no prefix)
|
|
531
|
+
await bus.publish('topic', null); // seq 1
|
|
532
|
+
await bus.publish('topic', null); // seq 2
|
|
533
|
+
await bus.publish('topic', null); // seq 3 → evicts seq 1
|
|
534
|
+
expect(evictions).toEqual([{ topic: 'topic', evictedSeq: '1', retainedCount: 2 }]);
|
|
535
|
+
}
|
|
536
|
+
catch (e_21) {
|
|
537
|
+
env_21.error = e_21;
|
|
538
|
+
env_21.hasError = true;
|
|
539
|
+
}
|
|
540
|
+
finally {
|
|
541
|
+
__disposeResources(env_21);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
finally {
|
|
545
|
+
telemetry[Symbol.dispose]();
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
it('attributes window-evicted topic to the original subscribe call when topicPrefix is set', async () => {
|
|
549
|
+
const telemetry = new CrossNodeBusTelemetry();
|
|
550
|
+
try {
|
|
551
|
+
const env_22 = { stack: [], error: void 0, hasError: false };
|
|
552
|
+
try {
|
|
553
|
+
const bus = __addDisposableResource(env_22, new InProcessCrossNodeBus({ nodeId: 'a', telemetry, replayWindow: 2, topicPrefix: 'svc-a/' }), false);
|
|
554
|
+
const _h = __addDisposableResource(env_22, bus.subscribe('events', () => undefined), false);
|
|
555
|
+
const evictions = [];
|
|
556
|
+
const _sub = __addDisposableResource(env_22, telemetry.subscribe('onCrossNodeWindowEvicted', (event) => {
|
|
557
|
+
evictions.push({ topic: event.topic });
|
|
558
|
+
}), false);
|
|
559
|
+
await bus.publish('events', null);
|
|
560
|
+
await bus.publish('events', null);
|
|
561
|
+
await bus.publish('events', null); // first eviction
|
|
562
|
+
expect(evictions).toEqual([{ topic: 'events' }]);
|
|
563
|
+
}
|
|
564
|
+
catch (e_22) {
|
|
565
|
+
env_22.error = e_22;
|
|
566
|
+
env_22.hasError = true;
|
|
567
|
+
}
|
|
568
|
+
finally {
|
|
569
|
+
__disposeResources(env_22);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
finally {
|
|
573
|
+
telemetry[Symbol.dispose]();
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
it('does not emit onCrossNodeWindowEvicted when the broker is supplied externally', async () => {
|
|
577
|
+
const env_23 = { stack: [], error: void 0, hasError: false };
|
|
578
|
+
try {
|
|
579
|
+
const telemetry = new CrossNodeBusTelemetry();
|
|
580
|
+
const broker = __addDisposableResource(env_23, new MemoryBroker({ replayWindow: 2 }), false);
|
|
581
|
+
try {
|
|
582
|
+
const env_24 = { stack: [], error: void 0, hasError: false };
|
|
583
|
+
try {
|
|
584
|
+
const bus = __addDisposableResource(env_24, new InProcessCrossNodeBus({ broker, nodeId: 'a', telemetry }), false);
|
|
585
|
+
const onEvicted = vi.fn();
|
|
586
|
+
const _sub = __addDisposableResource(env_24, telemetry.subscribe('onCrossNodeWindowEvicted', onEvicted), false);
|
|
587
|
+
await bus.publish('topic', null);
|
|
588
|
+
await bus.publish('topic', null);
|
|
589
|
+
await bus.publish('topic', null);
|
|
590
|
+
expect(onEvicted).not.toHaveBeenCalled();
|
|
591
|
+
}
|
|
592
|
+
catch (e_23) {
|
|
593
|
+
env_24.error = e_23;
|
|
594
|
+
env_24.hasError = true;
|
|
595
|
+
}
|
|
596
|
+
finally {
|
|
597
|
+
__disposeResources(env_24);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
finally {
|
|
601
|
+
telemetry[Symbol.dispose]();
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
catch (e_24) {
|
|
605
|
+
env_23.error = e_24;
|
|
606
|
+
env_23.hasError = true;
|
|
607
|
+
}
|
|
608
|
+
finally {
|
|
609
|
+
__disposeResources(env_23);
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
it('emits onCrossNodeError with phase=serialize when JSON.stringify throws', async () => {
|
|
613
|
+
const telemetry = new CrossNodeBusTelemetry();
|
|
614
|
+
try {
|
|
615
|
+
const env_25 = { stack: [], error: void 0, hasError: false };
|
|
616
|
+
try {
|
|
617
|
+
const bus = __addDisposableResource(env_25, new InProcessCrossNodeBus({ nodeId: 'a', telemetry }), false);
|
|
618
|
+
const errors = [];
|
|
619
|
+
const _sub = __addDisposableResource(env_25, telemetry.subscribe('onCrossNodeError', (event) => {
|
|
620
|
+
errors.push(event);
|
|
621
|
+
}), false);
|
|
622
|
+
const cycle = {};
|
|
623
|
+
cycle.self = cycle;
|
|
624
|
+
await bus.publish('topic', cycle);
|
|
625
|
+
expect(errors.some((event) => event.phase === 'serialize')).toBe(true);
|
|
626
|
+
}
|
|
627
|
+
catch (e_25) {
|
|
628
|
+
env_25.error = e_25;
|
|
629
|
+
env_25.hasError = true;
|
|
630
|
+
}
|
|
631
|
+
finally {
|
|
632
|
+
__disposeResources(env_25);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
finally {
|
|
636
|
+
telemetry[Symbol.dispose]();
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
});
|
|
640
|
+
describe('compareSeq', () => {
|
|
641
|
+
it('returns negative, zero, positive for a<b, a==b, a>b', () => {
|
|
642
|
+
const env_26 = { stack: [], error: void 0, hasError: false };
|
|
643
|
+
try {
|
|
644
|
+
const bus = __addDisposableResource(env_26, new InProcessCrossNodeBus(), false);
|
|
645
|
+
expect(bus.compareSeq('1', '2')).toBeLessThan(0);
|
|
646
|
+
expect(bus.compareSeq('5', '5')).toBe(0);
|
|
647
|
+
expect(bus.compareSeq('10', '2')).toBeGreaterThan(0);
|
|
648
|
+
}
|
|
649
|
+
catch (e_26) {
|
|
650
|
+
env_26.error = e_26;
|
|
651
|
+
env_26.hasError = true;
|
|
652
|
+
}
|
|
653
|
+
finally {
|
|
654
|
+
__disposeResources(env_26);
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
});
|
|
658
|
+
describe('oldestSeq', () => {
|
|
659
|
+
it('returns undefined before any publish', () => {
|
|
660
|
+
const env_27 = { stack: [], error: void 0, hasError: false };
|
|
661
|
+
try {
|
|
662
|
+
const bus = __addDisposableResource(env_27, new InProcessCrossNodeBus(), false);
|
|
663
|
+
expect(bus.oldestSeq('topic')).toBeUndefined();
|
|
664
|
+
}
|
|
665
|
+
catch (e_27) {
|
|
666
|
+
env_27.error = e_27;
|
|
667
|
+
env_27.hasError = true;
|
|
668
|
+
}
|
|
669
|
+
finally {
|
|
670
|
+
__disposeResources(env_27);
|
|
671
|
+
}
|
|
672
|
+
});
|
|
673
|
+
it('returns the oldest retained seq honoring topicPrefix', async () => {
|
|
674
|
+
const env_28 = { stack: [], error: void 0, hasError: false };
|
|
675
|
+
try {
|
|
676
|
+
const broker = __addDisposableResource(env_28, new MemoryBroker(), false);
|
|
677
|
+
const bus = __addDisposableResource(env_28, new InProcessCrossNodeBus({ broker, topicPrefix: 'svc/' }), false);
|
|
678
|
+
await bus.publish('topic', null);
|
|
679
|
+
await bus.publish('topic', null);
|
|
680
|
+
expect(bus.oldestSeq('topic')).toBe('1');
|
|
681
|
+
// direct broker access proves the prefix was applied on the wire.
|
|
682
|
+
expect(broker.oldestSeq('svc/topic')).toBe('1');
|
|
683
|
+
}
|
|
684
|
+
catch (e_28) {
|
|
685
|
+
env_28.error = e_28;
|
|
686
|
+
env_28.hasError = true;
|
|
687
|
+
}
|
|
688
|
+
finally {
|
|
689
|
+
__disposeResources(env_28);
|
|
690
|
+
}
|
|
691
|
+
});
|
|
692
|
+
it('throws after dispose', () => {
|
|
693
|
+
const bus = new InProcessCrossNodeBus();
|
|
694
|
+
bus[Symbol.dispose]();
|
|
695
|
+
expect(() => bus.oldestSeq('topic')).toThrow(/disposed/);
|
|
696
|
+
});
|
|
697
|
+
});
|
|
698
|
+
describe('disposal', () => {
|
|
699
|
+
it('rejects publish/subscribe/replay after dispose', async () => {
|
|
700
|
+
const bus = new InProcessCrossNodeBus();
|
|
701
|
+
bus[Symbol.dispose]();
|
|
702
|
+
expect(() => bus.subscribe('topic', () => undefined)).toThrow(/disposed/);
|
|
703
|
+
expect(() => bus.subscribeForeign('p/', 'topic', () => undefined)).toThrow(/disposed/);
|
|
704
|
+
expect(() => bus.replay('topic', '0')).toThrow(/disposed/);
|
|
705
|
+
await expect(bus.publish('topic', null)).rejects.toThrow(/disposed/);
|
|
706
|
+
});
|
|
707
|
+
it('is idempotent', () => {
|
|
708
|
+
const bus = new InProcessCrossNodeBus();
|
|
709
|
+
bus[Symbol.dispose]();
|
|
710
|
+
expect(() => bus[Symbol.dispose]()).not.toThrow();
|
|
711
|
+
});
|
|
712
|
+
it('disposes its private broker when it owns one', () => {
|
|
713
|
+
const bus = new InProcessCrossNodeBus();
|
|
714
|
+
const handler = vi.fn();
|
|
715
|
+
const sub = bus.subscribe('topic', handler);
|
|
716
|
+
bus[Symbol.dispose]();
|
|
717
|
+
expect(() => sub[Symbol.dispose]()).not.toThrow();
|
|
718
|
+
});
|
|
719
|
+
it('does not dispose a shared broker', () => {
|
|
720
|
+
const env_29 = { stack: [], error: void 0, hasError: false };
|
|
721
|
+
try {
|
|
722
|
+
const broker = __addDisposableResource(env_29, new MemoryBroker(), false);
|
|
723
|
+
const bus = new InProcessCrossNodeBus({ broker });
|
|
724
|
+
bus[Symbol.dispose]();
|
|
725
|
+
expect(() => broker.publish('topic', 'a', null)).not.toThrow();
|
|
726
|
+
}
|
|
727
|
+
catch (e_29) {
|
|
728
|
+
env_29.error = e_29;
|
|
729
|
+
env_29.hasError = true;
|
|
730
|
+
}
|
|
731
|
+
finally {
|
|
732
|
+
__disposeResources(env_29);
|
|
733
|
+
}
|
|
734
|
+
});
|
|
735
|
+
});
|
|
736
|
+
});
|
|
737
|
+
//# sourceMappingURL=in-process-cross-node-bus.spec.js.map
|