@nanobpm/urban 0.51.0 → 0.53.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/README.md +47 -20
- package/dist/runtime/core/events.d.ts +135 -0
- package/dist/runtime/core/events.js +329 -0
- package/dist/runtime/core/execContext.d.ts +5 -0
- package/dist/runtime/core/extensions.d.ts +131 -0
- package/dist/runtime/core/extensions.js +137 -0
- package/dist/runtime/core/glob.d.ts +7 -6
- package/dist/runtime/core/glob.js +16 -14
- package/dist/runtime/core/host.d.ts +4 -1
- package/dist/runtime/core/lineage.d.ts +116 -0
- package/dist/runtime/core/lineage.js +186 -0
- package/dist/runtime/core/modules/agent.d.ts +17 -0
- package/dist/runtime/core/modules/agent.js +63 -0
- package/dist/runtime/core/modules/datasource.d.ts +6 -0
- package/dist/runtime/core/modules/datasource.js +8 -0
- package/dist/runtime/core/modules/deploy.d.ts +8 -5
- package/dist/runtime/core/modules/deploy.js +62 -16
- package/dist/runtime/core/modules/lineage-store.d.ts +81 -0
- package/dist/runtime/core/modules/lineage-store.js +175 -0
- package/dist/runtime/core/modules/pages.js +397 -20
- package/dist/runtime/core/modules/surfaces.js +8 -0
- package/dist/runtime/core/modules/workers.js +69 -2
- package/dist/runtime/core/phase.d.ts +168 -0
- package/dist/runtime/core/phase.js +367 -0
- package/dist/runtime/core/runtime.d.ts +17 -0
- package/dist/runtime/core/runtime.js +30 -0
- package/dist/runtime/engine/nanosdk.js +8 -2
- package/dist/runtime/index.d.ts +10 -0
- package/dist/runtime/index.js +12 -0
- package/dist/toolkit/derivers/system-brief.d.ts +62 -0
- package/dist/toolkit/derivers/system-brief.js +156 -0
- package/dist/toolkit/gen.d.ts +5 -5
- package/dist/toolkit/gen.js +20 -14
- package/dist/toolkit/index.d.ts +5 -0
- package/dist/toolkit/index.js +3 -0
- package/dist/toolkit/models.d.ts +4 -3
- package/dist/toolkit/models.js +4 -3
- package/package.json +1 -1
- package/src/runtime/core/events.test.ts +382 -0
- package/src/runtime/core/events.ts +370 -0
- package/src/runtime/core/execContext.ts +5 -0
- package/src/runtime/core/extensions.test.ts +378 -0
- package/src/runtime/core/extensions.ts +276 -0
- package/src/runtime/core/glob.ts +15 -12
- package/src/runtime/core/host.ts +4 -1
- package/src/runtime/core/lineage.test.ts +215 -0
- package/src/runtime/core/lineage.ts +297 -0
- package/src/runtime/core/modules/agent.test.ts +104 -0
- package/src/runtime/core/modules/agent.ts +76 -0
- package/src/runtime/core/modules/datasource.ts +9 -0
- package/src/runtime/core/modules/deploy.test.ts +84 -16
- package/src/runtime/core/modules/deploy.ts +59 -16
- package/src/runtime/core/modules/lineage-store.test.ts +177 -0
- package/src/runtime/core/modules/lineage-store.ts +239 -0
- package/src/runtime/core/modules/pages.test.ts +309 -5
- package/src/runtime/core/modules/pages.ts +398 -20
- package/src/runtime/core/modules/surfaces.ts +9 -0
- package/src/runtime/core/modules/workers.test.ts +136 -1
- package/src/runtime/core/modules/workers.ts +72 -6
- package/src/runtime/core/phase.test.ts +350 -0
- package/src/runtime/core/phase.ts +526 -0
- package/src/runtime/core/runtime.extensions.test.ts +151 -0
- package/src/runtime/core/runtime.ts +49 -0
- package/src/runtime/devserver.test.ts +3 -0
- package/src/runtime/engine/nanosdk.test.ts +97 -3
- package/src/runtime/engine/nanosdk.ts +8 -2
- package/src/runtime/index.ts +92 -0
- package/src/toolkit/derivers/system-brief.test.ts +150 -0
- package/src/toolkit/derivers/system-brief.ts +210 -0
- package/src/toolkit/gen.test.ts +28 -4
- package/src/toolkit/gen.ts +20 -13
- package/src/toolkit/index.ts +14 -0
- package/src/toolkit/models.ts +4 -3
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { EventBus } from "./events.ts";
|
|
4
|
+
|
|
5
|
+
test("emit — synchronous fire-and-forget notifies every listener in order", () => {
|
|
6
|
+
const bus = new EventBus();
|
|
7
|
+
const channel = bus.emit<string>("note");
|
|
8
|
+
const seen: string[] = [];
|
|
9
|
+
channel.on((m) => seen.push(`a:${m}`));
|
|
10
|
+
channel.on((m) => seen.push(`b:${m}`));
|
|
11
|
+
channel.emit("hi");
|
|
12
|
+
assert.deepEqual(seen, ["a:hi", "b:hi"]);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("emit — a throwing listener is contained; the rest still run", () => {
|
|
16
|
+
const contained: string[] = [];
|
|
17
|
+
const bus = new EventBus({ onError: (_e, info) => contained.push(info.event) });
|
|
18
|
+
const channel = bus.emit<void>("boom");
|
|
19
|
+
const seen: string[] = [];
|
|
20
|
+
channel.on(() => {
|
|
21
|
+
throw new Error("nope");
|
|
22
|
+
});
|
|
23
|
+
channel.on(() => seen.push("ran"));
|
|
24
|
+
channel.emit();
|
|
25
|
+
assert.deepEqual(seen, ["ran"]);
|
|
26
|
+
assert.deepEqual(contained, ["boom"]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("emit — a rejecting async listener is contained, not an unhandled rejection", async () => {
|
|
30
|
+
const contained: string[] = [];
|
|
31
|
+
const bus = new EventBus({ onError: (_e, info) => contained.push(info.event) });
|
|
32
|
+
const channel = bus.emit<void>("boom");
|
|
33
|
+
const seen: string[] = [];
|
|
34
|
+
channel.on(async () => {
|
|
35
|
+
throw new Error("async nope");
|
|
36
|
+
});
|
|
37
|
+
channel.on(() => seen.push("ran"));
|
|
38
|
+
channel.emit();
|
|
39
|
+
assert.deepEqual(seen, ["ran"]);
|
|
40
|
+
await Promise.resolve();
|
|
41
|
+
await Promise.resolve();
|
|
42
|
+
assert.deepEqual(contained, ["boom"]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("emit — a rejecting thenable (non-native promise) listener is contained", async () => {
|
|
46
|
+
const contained: string[] = [];
|
|
47
|
+
const bus = new EventBus({ onError: (_e, info) => contained.push(info.event) });
|
|
48
|
+
const channel = bus.emit<void>("boom");
|
|
49
|
+
const seen: string[] = [];
|
|
50
|
+
// A cross-realm / userland promise is a thenable but not `instanceof Promise`.
|
|
51
|
+
channel.on(() => ({
|
|
52
|
+
then(_resolve: (value: unknown) => void, reject: (reason: unknown) => void) {
|
|
53
|
+
reject(new Error("thenable nope"));
|
|
54
|
+
},
|
|
55
|
+
}));
|
|
56
|
+
channel.on(() => seen.push("ran"));
|
|
57
|
+
channel.emit();
|
|
58
|
+
assert.deepEqual(seen, ["ran"]);
|
|
59
|
+
await Promise.resolve();
|
|
60
|
+
await Promise.resolve();
|
|
61
|
+
assert.deepEqual(contained, ["boom"]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("emit — listeners are snapshotted; one registered mid-dispatch is not run this emission", () => {
|
|
65
|
+
const bus = new EventBus();
|
|
66
|
+
const channel = bus.emit<void>("note");
|
|
67
|
+
const seen: string[] = [];
|
|
68
|
+
channel.on(() => {
|
|
69
|
+
seen.push("first");
|
|
70
|
+
channel.on(() => seen.push("late")); // registered during dispatch
|
|
71
|
+
});
|
|
72
|
+
channel.on(() => seen.push("second"));
|
|
73
|
+
channel.emit();
|
|
74
|
+
assert.deepEqual(seen, ["first", "second"]); // "late" only runs on the next emit
|
|
75
|
+
channel.emit();
|
|
76
|
+
assert.deepEqual(seen, ["first", "second", "first", "second", "late"]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("serial — listeners are snapshotted; one registered mid-dispatch is not run this checkpoint", async () => {
|
|
80
|
+
const bus = new EventBus();
|
|
81
|
+
const channel = bus.serial<void>("checkpoint");
|
|
82
|
+
const seen: string[] = [];
|
|
83
|
+
channel.on(async () => {
|
|
84
|
+
seen.push("first");
|
|
85
|
+
channel.on(() => seen.push("late")); // registered during dispatch
|
|
86
|
+
});
|
|
87
|
+
channel.on(() => seen.push("second"));
|
|
88
|
+
await channel.run();
|
|
89
|
+
assert.deepEqual(seen, ["first", "second"]); // "late" only runs on the next run
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("serial — listeners run in registration order, each awaited", async () => {
|
|
93
|
+
const bus = new EventBus();
|
|
94
|
+
const channel = bus.serial<number>("checkpoint");
|
|
95
|
+
const order: string[] = [];
|
|
96
|
+
channel.on(async (n) => {
|
|
97
|
+
await Promise.resolve();
|
|
98
|
+
order.push(`first:${n}`);
|
|
99
|
+
});
|
|
100
|
+
channel.on(async (n) => {
|
|
101
|
+
order.push(`second:${n}`);
|
|
102
|
+
});
|
|
103
|
+
await channel.run(7);
|
|
104
|
+
assert.deepEqual(order, ["first:7", "second:7"]);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("serial — a throwing checkpoint is contained; later checkpoints still run", async () => {
|
|
108
|
+
const bus = new EventBus({ onError: () => {} });
|
|
109
|
+
const channel = bus.serial<void>("cp");
|
|
110
|
+
const order: string[] = [];
|
|
111
|
+
channel.on(() => {
|
|
112
|
+
order.push("one");
|
|
113
|
+
});
|
|
114
|
+
channel.on(async () => {
|
|
115
|
+
throw new Error("boom");
|
|
116
|
+
});
|
|
117
|
+
channel.on(() => {
|
|
118
|
+
order.push("three");
|
|
119
|
+
});
|
|
120
|
+
await channel.run();
|
|
121
|
+
assert.deepEqual(order, ["one", "three"]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("parallel — every listener gets an independent chance even when one throws", async () => {
|
|
125
|
+
const bus = new EventBus({ onError: () => {} });
|
|
126
|
+
const channel = bus.parallel<void>("flush");
|
|
127
|
+
const done: string[] = [];
|
|
128
|
+
channel.on(async () => {
|
|
129
|
+
await Promise.resolve();
|
|
130
|
+
done.push("a");
|
|
131
|
+
});
|
|
132
|
+
channel.on(async () => {
|
|
133
|
+
throw new Error("b failed");
|
|
134
|
+
});
|
|
135
|
+
channel.on(async () => {
|
|
136
|
+
await Promise.resolve();
|
|
137
|
+
done.push("c");
|
|
138
|
+
});
|
|
139
|
+
await channel.run();
|
|
140
|
+
assert.deepEqual(done.sort(), ["a", "c"]);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("waterfall — middlewares wrap next outermost-first and can transform the value", async () => {
|
|
144
|
+
const bus = new EventBus();
|
|
145
|
+
const channel = bus.waterfall<string, string>("transform");
|
|
146
|
+
channel.on(async (value, next) => `<${await next(value)}>`);
|
|
147
|
+
channel.on(async (value, next) => next(`${value}!`));
|
|
148
|
+
const out = await channel.run("hi", (v) => v.toUpperCase());
|
|
149
|
+
assert.equal(out, "<HI!>");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("waterfall — a middleware short-circuits by not calling next", async () => {
|
|
153
|
+
const bus = new EventBus();
|
|
154
|
+
const channel = bus.waterfall<string, string>("gate");
|
|
155
|
+
let baseRan = false;
|
|
156
|
+
channel.on(() => "DENIED");
|
|
157
|
+
channel.on(async (value, next) => next(value));
|
|
158
|
+
const out = await channel.run("req", () => {
|
|
159
|
+
baseRan = true;
|
|
160
|
+
return "ALLOWED";
|
|
161
|
+
});
|
|
162
|
+
assert.equal(out, "DENIED");
|
|
163
|
+
assert.equal(baseRan, false);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("waterfall — a middleware recovers a downstream throw", async () => {
|
|
167
|
+
const bus = new EventBus();
|
|
168
|
+
const channel = bus.waterfall<string, string>("recover");
|
|
169
|
+
channel.on(async (value, next) => {
|
|
170
|
+
try {
|
|
171
|
+
return await next(value);
|
|
172
|
+
} catch {
|
|
173
|
+
return "recovered";
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
const out = await channel.run("x", () => {
|
|
177
|
+
throw new Error("downstream boom");
|
|
178
|
+
});
|
|
179
|
+
assert.equal(out, "recovered");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("waterfall — a throwing middleware that never called next is contained and delegated past", async () => {
|
|
183
|
+
const contained: string[] = [];
|
|
184
|
+
const bus = new EventBus({ onError: (_e, info) => contained.push(info.event) });
|
|
185
|
+
const channel = bus.waterfall<string, string>("dispatch");
|
|
186
|
+
channel.on(() => {
|
|
187
|
+
throw new Error("plugin bug");
|
|
188
|
+
});
|
|
189
|
+
channel.on(async (value, next) => next(`${value}-seen`));
|
|
190
|
+
const out = await channel.run("v", (v) => `base:${v}`);
|
|
191
|
+
assert.equal(out, "base:v-seen");
|
|
192
|
+
assert.deepEqual(contained, ["dispatch"]);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("waterfall — next() is idempotent within a middleware: a second call re-advances nothing", async () => {
|
|
196
|
+
const bus = new EventBus();
|
|
197
|
+
const channel = bus.waterfall<string, string>("reentrant");
|
|
198
|
+
let downstreamRuns = 0;
|
|
199
|
+
// Inner middleware counts how often the chain advances past the outer one.
|
|
200
|
+
channel.on(async (value, next) => {
|
|
201
|
+
downstreamRuns += 1;
|
|
202
|
+
return next(value);
|
|
203
|
+
});
|
|
204
|
+
let firstPromise: Promise<string> | undefined;
|
|
205
|
+
let secondPromise: Promise<string> | undefined;
|
|
206
|
+
channel.on((value, next) => {
|
|
207
|
+
firstPromise = next(value);
|
|
208
|
+
// A buggy or defensive middleware calling next() twice must not run the
|
|
209
|
+
// downstream chain (or its side effects) a second time.
|
|
210
|
+
secondPromise = next(value);
|
|
211
|
+
return firstPromise;
|
|
212
|
+
});
|
|
213
|
+
const out = await channel.run("v", (v) => `base:${v}`);
|
|
214
|
+
assert.equal(out, "base:v");
|
|
215
|
+
assert.equal(downstreamRuns, 1);
|
|
216
|
+
assert.strictEqual(firstPromise, secondPromise);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("dispose ladder — a listener disposer detaches it and is idempotent", () => {
|
|
220
|
+
const bus = new EventBus();
|
|
221
|
+
const channel = bus.emit<void>("e");
|
|
222
|
+
const off = channel.on(() => {});
|
|
223
|
+
assert.equal(channel.size, 1);
|
|
224
|
+
assert.equal(bus.listenerCount, 1);
|
|
225
|
+
off();
|
|
226
|
+
assert.equal(channel.size, 0);
|
|
227
|
+
assert.equal(bus.listenerCount, 0);
|
|
228
|
+
off(); // idempotent
|
|
229
|
+
assert.equal(bus.listenerCount, 0);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("dispose ladder — bus.dispose() unwinds every registration (start→stop→start leaks nothing)", () => {
|
|
233
|
+
const bus = new EventBus();
|
|
234
|
+
const emit = bus.emit<void>("a");
|
|
235
|
+
const serial = bus.serial<void>("b");
|
|
236
|
+
const waterfall = bus.waterfall<void, void>("c");
|
|
237
|
+
|
|
238
|
+
const register = () => {
|
|
239
|
+
emit.on(() => {});
|
|
240
|
+
serial.on(() => {});
|
|
241
|
+
waterfall.on((_v, next) => next());
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
register();
|
|
245
|
+
assert.equal(bus.listenerCount, 3);
|
|
246
|
+
bus.dispose(); // stop
|
|
247
|
+
assert.equal(bus.listenerCount, 0);
|
|
248
|
+
assert.equal(emit.size, 0);
|
|
249
|
+
|
|
250
|
+
register(); // start again — no accumulation from the previous cycle
|
|
251
|
+
assert.equal(bus.listenerCount, 3);
|
|
252
|
+
bus.dispose();
|
|
253
|
+
assert.equal(bus.listenerCount, 0);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("dispose ladder — arbitrary effect() cleanups run on dispose, LIFO", () => {
|
|
257
|
+
const bus = new EventBus();
|
|
258
|
+
const order: string[] = [];
|
|
259
|
+
bus.effect(() => order.push("first"));
|
|
260
|
+
bus.effect(() => order.push("second"));
|
|
261
|
+
bus.dispose();
|
|
262
|
+
assert.deepEqual(order, ["second", "first"]);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("dispose ladder — a throwing disposer is contained; the rest still unwind", () => {
|
|
266
|
+
const contained: unknown[] = [];
|
|
267
|
+
const bus = new EventBus({ onError: (e) => contained.push(e) });
|
|
268
|
+
const order: string[] = [];
|
|
269
|
+
bus.effect(() => order.push("a"));
|
|
270
|
+
bus.effect(() => {
|
|
271
|
+
throw new Error("bad disposer");
|
|
272
|
+
});
|
|
273
|
+
bus.effect(() => order.push("c"));
|
|
274
|
+
bus.dispose();
|
|
275
|
+
assert.deepEqual(order, ["c", "a"]);
|
|
276
|
+
assert.equal(contained.length, 1);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test("effect() returns an idempotent disposer that also detaches from the ladder", () => {
|
|
280
|
+
const bus = new EventBus();
|
|
281
|
+
let cleaned = 0;
|
|
282
|
+
const off = bus.effect(() => {
|
|
283
|
+
cleaned += 1;
|
|
284
|
+
});
|
|
285
|
+
assert.equal(bus.listenerCount, 1);
|
|
286
|
+
off();
|
|
287
|
+
assert.equal(cleaned, 1);
|
|
288
|
+
assert.equal(bus.listenerCount, 0);
|
|
289
|
+
off(); // idempotent
|
|
290
|
+
assert.equal(cleaned, 1);
|
|
291
|
+
bus.dispose(); // already detached — not run again
|
|
292
|
+
assert.equal(cleaned, 1);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test("a seam cannot be redeclared under a different mode", () => {
|
|
296
|
+
const bus = new EventBus();
|
|
297
|
+
bus.emit<void>("x");
|
|
298
|
+
assert.throws(() => bus.serial<void>("x"), /already declared as "emit"/);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test("dispose ladder — an effect registered by a disposer during dispose is still unwound", () => {
|
|
302
|
+
const bus = new EventBus();
|
|
303
|
+
const order: string[] = [];
|
|
304
|
+
bus.effect(() => {
|
|
305
|
+
order.push("outer");
|
|
306
|
+
// A disposer that registers another effect mid-teardown: the ladder must
|
|
307
|
+
// keep unwinding until empty rather than dropping the late registration.
|
|
308
|
+
bus.effect(() => order.push("late"));
|
|
309
|
+
});
|
|
310
|
+
bus.dispose();
|
|
311
|
+
assert.deepEqual(order, ["outer", "late"]);
|
|
312
|
+
assert.equal(bus.listenerCount, 0);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test("scope() — rolls back only registrations made after the scope opened, LIFO, and stays idempotent under dispose", () => {
|
|
316
|
+
const bus = new EventBus();
|
|
317
|
+
const order: string[] = [];
|
|
318
|
+
const before = bus.effect(() => order.push("before"));
|
|
319
|
+
const rollback = bus.scope();
|
|
320
|
+
bus.effect(() => order.push("scoped-1"));
|
|
321
|
+
bus.effect(() => order.push("scoped-2"));
|
|
322
|
+
assert.equal(bus.listenerCount, 3);
|
|
323
|
+
rollback();
|
|
324
|
+
// Only the two effects added after the scope opened are unwound, newest-first.
|
|
325
|
+
assert.deepEqual(order, ["scoped-2", "scoped-1"]);
|
|
326
|
+
// The pre-scope registration is untouched.
|
|
327
|
+
assert.equal(bus.listenerCount, 1);
|
|
328
|
+
// A later dispose must not double-run the already-rolled-back effects.
|
|
329
|
+
order.length = 0;
|
|
330
|
+
void before;
|
|
331
|
+
bus.dispose();
|
|
332
|
+
assert.deepEqual(order, ["before"]);
|
|
333
|
+
assert.equal(bus.listenerCount, 0);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test("scope() — keeps unwinding when a rolled-back disposer registers a new effect mid-rollback (truly atomic)", () => {
|
|
337
|
+
const bus = new EventBus();
|
|
338
|
+
const order: string[] = [];
|
|
339
|
+
const before = bus.effect(() => order.push("before"));
|
|
340
|
+
const rollback = bus.scope();
|
|
341
|
+
bus.effect(() => order.push("scoped-1"));
|
|
342
|
+
// A scoped disposer that registers ANOTHER effect while it is being rolled
|
|
343
|
+
// back: that late registration is still "after" the scope point, so an atomic
|
|
344
|
+
// rollback (like dispose()) must unwind it too rather than strand it live.
|
|
345
|
+
bus.effect(() => {
|
|
346
|
+
order.push("scoped-2");
|
|
347
|
+
bus.effect(() => order.push("scoped-late"));
|
|
348
|
+
});
|
|
349
|
+
assert.equal(bus.listenerCount, 3);
|
|
350
|
+
rollback();
|
|
351
|
+
assert.deepEqual(order, ["scoped-2", "scoped-late", "scoped-1"]);
|
|
352
|
+
// The pre-scope registration survives; nothing added after the scope leaks.
|
|
353
|
+
assert.equal(bus.listenerCount, 1);
|
|
354
|
+
void before;
|
|
355
|
+
bus.dispose();
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
test("register — .on(sameListener) twice is idempotent: one ladder entry, shared disposer, no drift", () => {
|
|
359
|
+
const bus = new EventBus();
|
|
360
|
+
const channel = bus.emit<string>("e");
|
|
361
|
+
const seen: string[] = [];
|
|
362
|
+
const listener = (m: string) => seen.push(m);
|
|
363
|
+
|
|
364
|
+
const off1 = channel.on(listener);
|
|
365
|
+
const off2 = channel.on(listener);
|
|
366
|
+
// A duplicate registration must not inflate the leak metric nor the channel.
|
|
367
|
+
assert.equal(channel.size, 1);
|
|
368
|
+
assert.equal(bus.listenerCount, 1);
|
|
369
|
+
// The same disposer is handed back, so callers can't get out of sync.
|
|
370
|
+
assert.equal(off1, off2);
|
|
371
|
+
|
|
372
|
+
// The coalesced listener fires exactly once per emit.
|
|
373
|
+
channel.emit("x");
|
|
374
|
+
assert.deepEqual(seen, ["x"]);
|
|
375
|
+
|
|
376
|
+
// Disposing once fully removes it — the ladder does not strand a phantom entry.
|
|
377
|
+
off1();
|
|
378
|
+
assert.equal(channel.size, 0);
|
|
379
|
+
assert.equal(bus.listenerCount, 0);
|
|
380
|
+
off2(); // idempotent second disposer call
|
|
381
|
+
assert.equal(bus.listenerCount, 0);
|
|
382
|
+
});
|