@gjsify/diagnostics_channel 0.1.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.
@@ -0,0 +1,927 @@
1
+ // Ported from refs/node-test/parallel/test-diagnostics-channel-*.js
2
+ // Original: MIT license, Node.js contributors
3
+
4
+ import { describe, it, expect } from '@gjsify/unit';
5
+ import {
6
+ Channel,
7
+ channel,
8
+ hasSubscribers,
9
+ subscribe,
10
+ unsubscribe,
11
+ tracingChannel,
12
+ } from 'node:diagnostics_channel';
13
+
14
+ // Helper: unique channel names to avoid cross-test interference
15
+ let counter = 0;
16
+ function uid(prefix = 'test') {
17
+ return `${prefix}:${Date.now()}:${counter++}`;
18
+ }
19
+
20
+ export default async () => {
21
+ await describe('diagnostics_channel', async () => {
22
+ // ---------------------------------------------------------------
23
+ // channel() factory
24
+ // ---------------------------------------------------------------
25
+ await describe('channel() factory', async () => {
26
+ await it('should return a Channel instance', async () => {
27
+ const ch = channel(uid());
28
+ expect(ch).toBeDefined();
29
+ expect(ch instanceof Channel).toBe(true);
30
+ });
31
+
32
+ await it('should return the same channel for the same name', async () => {
33
+ const name = uid('singleton');
34
+ const a = channel(name);
35
+ const b = channel(name);
36
+ expect(a).toBe(b);
37
+ });
38
+
39
+ await it('should have a name property matching the input', async () => {
40
+ const name = uid('named');
41
+ const ch = channel(name);
42
+ expect(ch.name).toBe(name);
43
+ });
44
+
45
+ await it('should start with no subscribers', async () => {
46
+ const ch = channel(uid());
47
+ expect(ch.hasSubscribers).toBe(false);
48
+ });
49
+
50
+ await it('should support Symbol names', async () => {
51
+ const sym = Symbol('test-symbol');
52
+ const ch = channel(sym);
53
+ // Cannot use toBe with Symbol (string interpolation issue) — use strict equality
54
+ expect(ch.name === sym).toBe(true);
55
+ expect(typeof ch.name).toBe('symbol');
56
+ });
57
+
58
+ await it('should return the same channel for the same Symbol', async () => {
59
+ const sym = Symbol.for('dc-test-same');
60
+ const a = channel(sym);
61
+ const b = channel(sym);
62
+ expect(a).toBe(b);
63
+ });
64
+ });
65
+
66
+ // ---------------------------------------------------------------
67
+ // hasSubscribers (module-level)
68
+ // ---------------------------------------------------------------
69
+ await describe('hasSubscribers()', async () => {
70
+ await it('should return false for unknown channel', async () => {
71
+ expect(hasSubscribers(uid('unknown'))).toBe(false);
72
+ });
73
+
74
+ await it('should return true when channel has subscribers', async () => {
75
+ const name = uid('hassub');
76
+ subscribe(name, () => {});
77
+ expect(hasSubscribers(name)).toBe(true);
78
+ });
79
+
80
+ await it('should return false after all subscribers removed', async () => {
81
+ const name = uid('hassub-remove');
82
+ const handler = () => {};
83
+ subscribe(name, handler);
84
+ expect(hasSubscribers(name)).toBe(true);
85
+ unsubscribe(name, handler);
86
+ expect(hasSubscribers(name)).toBe(false);
87
+ });
88
+ });
89
+
90
+ // ---------------------------------------------------------------
91
+ // Channel: subscribe / unsubscribe / hasSubscribers lifecycle
92
+ // ---------------------------------------------------------------
93
+ await describe('Channel subscribe/unsubscribe lifecycle', async () => {
94
+ await it('should track hasSubscribers correctly through add/remove', async () => {
95
+ const ch = channel(uid('lifecycle'));
96
+ expect(ch.hasSubscribers).toBe(false);
97
+
98
+ const fn1 = () => {};
99
+ const fn2 = () => {};
100
+
101
+ ch.subscribe(fn1);
102
+ expect(ch.hasSubscribers).toBe(true);
103
+
104
+ ch.subscribe(fn2);
105
+ expect(ch.hasSubscribers).toBe(true);
106
+
107
+ ch.unsubscribe(fn1);
108
+ expect(ch.hasSubscribers).toBe(true);
109
+
110
+ ch.unsubscribe(fn2);
111
+ expect(ch.hasSubscribers).toBe(false);
112
+ });
113
+
114
+ await it('should return true from unsubscribe when subscriber exists', async () => {
115
+ const ch = channel(uid());
116
+ const handler = () => {};
117
+ ch.subscribe(handler);
118
+ expect(ch.unsubscribe(handler)).toBe(true);
119
+ });
120
+
121
+ await it('should return false from unsubscribe when subscriber not found', async () => {
122
+ const ch = channel(uid());
123
+ expect(ch.unsubscribe(() => {})).toBe(false);
124
+ });
125
+
126
+ await it('should return false from unsubscribe after already removed', async () => {
127
+ const ch = channel(uid());
128
+ const handler = () => {};
129
+ ch.subscribe(handler);
130
+ ch.unsubscribe(handler);
131
+ expect(ch.unsubscribe(handler)).toBe(false);
132
+ });
133
+
134
+ await it('should allow re-subscribe after unsubscribe', async () => {
135
+ const ch = channel(uid('resub'));
136
+ const handler = () => {};
137
+ ch.subscribe(handler);
138
+ ch.unsubscribe(handler);
139
+ expect(ch.hasSubscribers).toBe(false);
140
+ ch.subscribe(handler);
141
+ expect(ch.hasSubscribers).toBe(true);
142
+ });
143
+ });
144
+
145
+ // ---------------------------------------------------------------
146
+ // Channel: publish
147
+ // ---------------------------------------------------------------
148
+ await describe('Channel publish', async () => {
149
+ await it('should publish messages to subscribers', async () => {
150
+ const ch = channel(uid('pub'));
151
+ const messages: unknown[] = [];
152
+ ch.subscribe((msg: unknown) => messages.push(msg));
153
+ ch.publish('hello');
154
+ ch.publish({ data: 42 });
155
+ expect(messages.length).toBe(2);
156
+ expect(messages[0]).toBe('hello');
157
+ });
158
+
159
+ await it('should pass the channel name as second argument', async () => {
160
+ const name = uid('name-arg');
161
+ const ch = channel(name);
162
+ let receivedName: string | symbol | undefined;
163
+ ch.subscribe((_msg: unknown, n: string | symbol) => { receivedName = n; });
164
+ ch.publish('test');
165
+ expect(receivedName).toBe(name);
166
+ });
167
+
168
+ await it('should pass Symbol name as second argument', async () => {
169
+ const sym = Symbol('pub-sym');
170
+ const ch = channel(sym);
171
+ let receivedName: string | symbol | undefined;
172
+ ch.subscribe((_msg: unknown, n: string | symbol) => { receivedName = n; });
173
+ ch.publish({ key: 'val' });
174
+ // Cannot use toBe with Symbol (string interpolation issue) — use strict equality
175
+ expect(receivedName === sym).toBe(true);
176
+ });
177
+
178
+ await it('should pass message data faithfully (reference identity)', async () => {
179
+ const ch = channel(uid('data'));
180
+ let received: any = null;
181
+ ch.subscribe((msg: unknown) => { received = msg; });
182
+ const obj = { key: 'value', num: 42 };
183
+ ch.publish(obj);
184
+ expect(received).toBe(obj);
185
+ });
186
+
187
+ await it('should not call subscribers when there are none', async () => {
188
+ const ch = channel(uid('noop'));
189
+ // Should not throw
190
+ ch.publish('no-op');
191
+ });
192
+
193
+ await it('should notify all subscribers', async () => {
194
+ const ch = channel(uid('multi'));
195
+ let count = 0;
196
+ ch.subscribe(() => { count++; });
197
+ ch.subscribe(() => { count++; });
198
+ ch.subscribe(() => { count++; });
199
+ ch.publish('msg');
200
+ expect(count).toBe(3);
201
+ });
202
+
203
+ await it('should not notify unsubscribed handlers', async () => {
204
+ const ch = channel(uid('unsub'));
205
+ const messages: unknown[] = [];
206
+ const handler = (msg: unknown) => messages.push(msg);
207
+ ch.subscribe(handler);
208
+ ch.publish('a');
209
+ ch.unsubscribe(handler);
210
+ ch.publish('b');
211
+ expect(messages.length).toBe(1);
212
+ });
213
+ });
214
+
215
+ // ---------------------------------------------------------------
216
+ // Unsubscribe during publish (copy-on-write safety)
217
+ // ---------------------------------------------------------------
218
+ await describe('unsubscribe during publish', async () => {
219
+ await it('should not crash when a subscriber unsubscribes during publish', async () => {
220
+ const name = uid('sync-unsub');
221
+ let secondCalled = false;
222
+
223
+ const handler1 = () => {
224
+ unsubscribe(name, handler1);
225
+ };
226
+ const handler2 = () => {
227
+ secondCalled = true;
228
+ };
229
+
230
+ subscribe(name, handler1);
231
+ subscribe(name, handler2);
232
+
233
+ // Must not throw
234
+ channel(name).publish('data');
235
+ expect(secondCalled).toBe(true);
236
+ });
237
+ });
238
+
239
+ // ---------------------------------------------------------------
240
+ // subscribe/unsubscribe module-level functions
241
+ // ---------------------------------------------------------------
242
+ await describe('subscribe/unsubscribe module-level functions', async () => {
243
+ await it('should subscribe and receive messages via module functions', async () => {
244
+ const name = uid('modsub');
245
+ const messages: unknown[] = [];
246
+ const handler = (msg: unknown) => messages.push(msg);
247
+ subscribe(name, handler);
248
+ channel(name).publish('test');
249
+ expect(messages.length).toBe(1);
250
+ unsubscribe(name, handler);
251
+ });
252
+
253
+ await it('unsubscribe should return true when found', async () => {
254
+ const name = uid('modret');
255
+ const handler = () => {};
256
+ subscribe(name, handler);
257
+ expect(unsubscribe(name, handler)).toBe(true);
258
+ });
259
+
260
+ await it('unsubscribe should return false when not found', async () => {
261
+ const name = uid('modnotfound');
262
+ expect(unsubscribe(name, () => {})).toBe(false);
263
+ });
264
+ });
265
+
266
+ // ---------------------------------------------------------------
267
+ // Error handling in subscribers
268
+ // ---------------------------------------------------------------
269
+ await describe('subscriber error handling', async () => {
270
+ await it('should continue notifying remaining subscribers when one throws', async () => {
271
+ const ch = channel(uid('err'));
272
+ let secondCalled = false;
273
+ const expectedError = new Error('subscriber error ' + uid('err'));
274
+
275
+ // On Node.js, Channel.publish catches subscriber errors and re-throws
276
+ // them asynchronously, so we need to catch the uncaught exception.
277
+ // On GJS, our implementation does the same via Promise.resolve().then().
278
+ const errorHandler = (err: unknown) => {
279
+ if (err === expectedError) return; // suppress expected error
280
+ };
281
+
282
+ // Install handler if process is available (Node.js)
283
+ const proc = typeof process !== 'undefined' ? process : undefined;
284
+ if (proc && typeof proc.on === 'function') {
285
+ proc.on('uncaughtException', errorHandler);
286
+ }
287
+
288
+ ch.subscribe(() => { throw expectedError; });
289
+ ch.subscribe(() => { secondCalled = true; });
290
+
291
+ // Publish should not throw synchronously
292
+ ch.publish('data');
293
+ expect(secondCalled).toBe(true);
294
+
295
+ // Clean up the handler after a tick so the async re-throw is caught
296
+ await new Promise<void>((resolve) => {
297
+ // Use setTimeout to let the async error fire first
298
+ setTimeout(() => {
299
+ if (proc && typeof proc.removeListener === 'function') {
300
+ proc.removeListener('uncaughtException', errorHandler);
301
+ }
302
+ resolve();
303
+ }, 10);
304
+ });
305
+ });
306
+ });
307
+
308
+ // ---------------------------------------------------------------
309
+ // TracingChannel: creation and structure
310
+ // ---------------------------------------------------------------
311
+ await describe('TracingChannel creation', async () => {
312
+ await it('should create a tracing channel with all sub-channels', async () => {
313
+ const tc = tracingChannel(uid('trace'));
314
+ expect(tc).toBeDefined();
315
+ expect(tc.start).toBeDefined();
316
+ expect(tc.end).toBeDefined();
317
+ expect(tc.asyncStart).toBeDefined();
318
+ expect(tc.asyncEnd).toBeDefined();
319
+ expect(tc.error).toBeDefined();
320
+ });
321
+
322
+ await it('should have the expected methods and properties', async () => {
323
+ const tc = tracingChannel(uid('inst'));
324
+ expect(typeof tc.traceSync).toBe('function');
325
+ expect(typeof tc.tracePromise).toBe('function');
326
+ expect(typeof tc.subscribe).toBe('function');
327
+ expect(typeof tc.unsubscribe).toBe('function');
328
+ });
329
+
330
+ await it('should name sub-channels with tracing: prefix', async () => {
331
+ const name = uid('naming');
332
+ const tc = tracingChannel(name);
333
+ expect(tc.start.name).toBe(`tracing:${name}:start`);
334
+ expect(tc.end.name).toBe(`tracing:${name}:end`);
335
+ expect(tc.asyncStart.name).toBe(`tracing:${name}:asyncStart`);
336
+ expect(tc.asyncEnd.name).toBe(`tracing:${name}:asyncEnd`);
337
+ expect(tc.error.name).toBe(`tracing:${name}:error`);
338
+ });
339
+
340
+ await it('should accept channel objects instead of a name', async () => {
341
+ const name = uid('obj');
342
+ const tc = tracingChannel({
343
+ start: channel(`tracing:${name}:start`),
344
+ end: channel(`tracing:${name}:end`),
345
+ asyncStart: channel(`tracing:${name}:asyncStart`),
346
+ asyncEnd: channel(`tracing:${name}:asyncEnd`),
347
+ error: channel(`tracing:${name}:error`),
348
+ });
349
+ expect(tc.start.name).toBe(`tracing:${name}:start`);
350
+ });
351
+ });
352
+
353
+ // ---------------------------------------------------------------
354
+ // TracingChannel: hasSubscribers
355
+ // ---------------------------------------------------------------
356
+ await describe('TracingChannel hasSubscribers', async () => {
357
+ await it('should be false initially', async () => {
358
+ const tc = tracingChannel(uid('tc-hassub'));
359
+ expect(tc.hasSubscribers).toBe(false);
360
+ });
361
+
362
+ await it('should be true when subscribe is called with start handler', async () => {
363
+ const tc = tracingChannel(uid('tc-hassub-start'));
364
+ const handlers = { start: () => {} };
365
+ tc.subscribe(handlers);
366
+ expect(tc.hasSubscribers).toBe(true);
367
+ tc.unsubscribe(handlers);
368
+ expect(tc.hasSubscribers).toBe(false);
369
+ });
370
+
371
+ await it('should be true when any sub-channel has a subscriber', async () => {
372
+ const tc = tracingChannel(uid('tc-hassub-any'));
373
+ const handler = () => {};
374
+ tc.asyncEnd.subscribe(handler);
375
+ expect(tc.hasSubscribers).toBe(true);
376
+ tc.asyncEnd.unsubscribe(handler);
377
+ expect(tc.hasSubscribers).toBe(false);
378
+ });
379
+
380
+ await it('should track subscribe/unsubscribe with handlers object', async () => {
381
+ const tc = tracingChannel(uid('tc-hassub-handlers'));
382
+ const handlers = { asyncEnd: () => {} };
383
+ tc.subscribe(handlers);
384
+ expect(tc.hasSubscribers).toBe(true);
385
+ tc.unsubscribe(handlers);
386
+ expect(tc.hasSubscribers).toBe(false);
387
+ });
388
+ });
389
+
390
+ // ---------------------------------------------------------------
391
+ // TracingChannel: subscribe/unsubscribe
392
+ // ---------------------------------------------------------------
393
+ await describe('TracingChannel subscribe/unsubscribe', async () => {
394
+ await it('should subscribe handlers to respective channels', async () => {
395
+ const tc = tracingChannel(uid('tc-sub'));
396
+ const events: string[] = [];
397
+ const handlers = {
398
+ start: () => events.push('start'),
399
+ end: () => events.push('end'),
400
+ asyncStart: () => events.push('asyncStart'),
401
+ asyncEnd: () => events.push('asyncEnd'),
402
+ error: () => events.push('error'),
403
+ };
404
+ tc.subscribe(handlers);
405
+
406
+ tc.start.publish({});
407
+ tc.end.publish({});
408
+ tc.asyncStart.publish({});
409
+ tc.asyncEnd.publish({});
410
+ tc.error.publish({});
411
+
412
+ expect(events.length).toBe(5);
413
+ expect(events[0]).toBe('start');
414
+ expect(events[1]).toBe('end');
415
+ expect(events[2]).toBe('asyncStart');
416
+ expect(events[3]).toBe('asyncEnd');
417
+ expect(events[4]).toBe('error');
418
+ });
419
+
420
+ await it('should unsubscribe handlers and return true', async () => {
421
+ const tc = tracingChannel(uid('tc-unsub'));
422
+ const handlers = {
423
+ start: () => {},
424
+ end: () => {},
425
+ };
426
+ tc.subscribe(handlers);
427
+ const result = tc.unsubscribe(handlers);
428
+ expect(result).toBe(true);
429
+ expect(tc.hasSubscribers).toBe(false);
430
+ });
431
+
432
+ await it('should only subscribe provided handlers (partial)', async () => {
433
+ const tc = tracingChannel(uid('tc-partial'));
434
+ const events: string[] = [];
435
+ tc.subscribe({ start: () => events.push('start') });
436
+ tc.start.publish({});
437
+ tc.end.publish({});
438
+ expect(events.length).toBe(1);
439
+ });
440
+ });
441
+
442
+ // ---------------------------------------------------------------
443
+ // TracingChannel: traceSync
444
+ // ---------------------------------------------------------------
445
+ await describe('TracingChannel traceSync', async () => {
446
+ await it('should publish start and end on success', async () => {
447
+ const tc = tracingChannel(uid('ts-ok'));
448
+ const events: string[] = [];
449
+ tc.subscribe({
450
+ start: () => events.push('start'),
451
+ end: () => events.push('end'),
452
+ });
453
+ tc.traceSync(() => 'result', {});
454
+ expect(events.length).toBe(2);
455
+ expect(events[0]).toBe('start');
456
+ expect(events[1]).toBe('end');
457
+ });
458
+
459
+ await it('should return the function result', async () => {
460
+ const tc = tracingChannel(uid('ts-ret'));
461
+ const expected = { foo: 'bar' };
462
+ tc.subscribe({ start: () => {}, end: () => {} });
463
+ const result = tc.traceSync(() => expected, {});
464
+ expect(result).toBe(expected);
465
+ });
466
+
467
+ await it('should set result on context', async () => {
468
+ const tc = tracingChannel(uid('ts-ctx-res'));
469
+ const expected = { foo: 'bar' };
470
+ const ctx: Record<string, unknown> = {};
471
+ let endCtx: any = null;
472
+ tc.subscribe({
473
+ start: () => {},
474
+ end: (msg: unknown) => { endCtx = msg; },
475
+ });
476
+ tc.traceSync(() => expected, ctx);
477
+ expect(endCtx.result).toBe(expected);
478
+ });
479
+
480
+ await it('should pass context to start and end subscribers', async () => {
481
+ const tc = tracingChannel(uid('ts-ctx'));
482
+ const input = { foo: 'bar' };
483
+ let startMsg: any = null;
484
+ let endMsg: any = null;
485
+ tc.subscribe({
486
+ start: (msg: unknown) => { startMsg = msg; },
487
+ end: (msg: unknown) => { endMsg = msg; },
488
+ });
489
+ tc.traceSync(() => 'ok', input);
490
+ expect(startMsg).toBe(input);
491
+ expect(endMsg).toBe(input);
492
+ });
493
+
494
+ await it('should publish error and end on throw', async () => {
495
+ const tc = tracingChannel(uid('ts-err'));
496
+ const events: string[] = [];
497
+ let errorCtx: any = null;
498
+ tc.subscribe({
499
+ start: () => events.push('start'),
500
+ end: () => events.push('end'),
501
+ error: (msg: unknown) => {
502
+ events.push('error');
503
+ errorCtx = msg;
504
+ },
505
+ });
506
+ const expectedErr = new Error('test');
507
+ try {
508
+ tc.traceSync(() => { throw expectedErr; }, {});
509
+ } catch {
510
+ // expected
511
+ }
512
+ expect(events.length).toBe(3);
513
+ expect(events[0]).toBe('start');
514
+ expect(events[1]).toBe('error');
515
+ expect(events[2]).toBe('end');
516
+ expect(errorCtx.error).toBe(expectedErr);
517
+ });
518
+
519
+ await it('should re-throw the error', async () => {
520
+ const tc = tracingChannel(uid('ts-rethrow'));
521
+ tc.subscribe({ start: () => {}, end: () => {}, error: () => {} });
522
+ const expectedErr = new Error('rethrow-test');
523
+ let caught: unknown = null;
524
+ try {
525
+ tc.traceSync(() => { throw expectedErr; }, {});
526
+ } catch (e) {
527
+ caught = e;
528
+ }
529
+ expect(caught).toBe(expectedErr);
530
+ });
531
+
532
+ await it('should not publish asyncStart/asyncEnd on sync', async () => {
533
+ const tc = tracingChannel(uid('ts-noasync'));
534
+ let asyncStartCalled = false;
535
+ let asyncEndCalled = false;
536
+ tc.subscribe({
537
+ start: () => {},
538
+ end: () => {},
539
+ asyncStart: () => { asyncStartCalled = true; },
540
+ asyncEnd: () => { asyncEndCalled = true; },
541
+ });
542
+ tc.traceSync(() => 'ok', {});
543
+ expect(asyncStartCalled).toBe(false);
544
+ expect(asyncEndCalled).toBe(false);
545
+ });
546
+
547
+ await it('should pass thisArg and args to the function', async () => {
548
+ const tc = tracingChannel(uid('ts-thisarg'));
549
+ tc.subscribe({ start: () => {}, end: () => {} });
550
+ const thisArg = { baz: 'buz' };
551
+ const arg = { val: 123 };
552
+ let receivedThis: unknown = null;
553
+ let receivedArg: unknown = null;
554
+ tc.traceSync(function (this: unknown, a: unknown) {
555
+ receivedThis = this;
556
+ receivedArg = a;
557
+ return 'ok';
558
+ }, {}, thisArg, arg);
559
+ expect(receivedThis).toBe(thisArg);
560
+ expect(receivedArg).toBe(arg);
561
+ });
562
+
563
+ await it('should skip publish when no subscribers (fast path)', async () => {
564
+ const tc = tracingChannel(uid('ts-nosub'));
565
+ const thisArg = { x: 1 };
566
+ const arg = 'hello';
567
+ let receivedThis: unknown = null;
568
+ let receivedArg: unknown = null;
569
+ const result = tc.traceSync(function (this: unknown, a: unknown) {
570
+ receivedThis = this;
571
+ receivedArg = a;
572
+ return 42;
573
+ }, {}, thisArg, arg);
574
+ expect(result).toBe(42);
575
+ expect(receivedThis).toBe(thisArg);
576
+ expect(receivedArg).toBe(arg);
577
+ });
578
+ });
579
+
580
+ // ---------------------------------------------------------------
581
+ // TracingChannel: traceSync early exit (subscribe during trace)
582
+ // ---------------------------------------------------------------
583
+ await describe('TracingChannel traceSync early exit', async () => {
584
+ await it('should not fire end/error if subscribed during traceSync with no prior subscribers', async () => {
585
+ // When traceSync is called with no subscribers, it takes the fast path
586
+ // and directly calls fn — no events should fire even if subscribe
587
+ // happens inside fn
588
+ const tc = tracingChannel(uid('ts-early'));
589
+ let endCalled = false;
590
+ let errorCalled = false;
591
+ const handlers = {
592
+ start: () => {},
593
+ end: () => { endCalled = true; },
594
+ asyncStart: () => {},
595
+ asyncEnd: () => {},
596
+ error: () => { errorCalled = true; },
597
+ };
598
+
599
+ tc.traceSync(() => {
600
+ tc.subscribe(handlers);
601
+ }, {});
602
+
603
+ expect(endCalled).toBe(false);
604
+ expect(errorCalled).toBe(false);
605
+ });
606
+ });
607
+
608
+ // ---------------------------------------------------------------
609
+ // TracingChannel: tracePromise (success)
610
+ // ---------------------------------------------------------------
611
+ await describe('TracingChannel tracePromise', async () => {
612
+ await it('should publish start, end, asyncStart, asyncEnd on success', async () => {
613
+ const tc = tracingChannel(uid('tp-ok'));
614
+ const events: string[] = [];
615
+ tc.subscribe({
616
+ start: () => events.push('start'),
617
+ end: () => events.push('end'),
618
+ asyncStart: () => events.push('asyncStart'),
619
+ asyncEnd: () => events.push('asyncEnd'),
620
+ });
621
+ const result = await tc.tracePromise(() => Promise.resolve('done'), {});
622
+ expect(result).toBe('done');
623
+ expect(events[0]).toBe('start');
624
+ expect(events[1]).toBe('end');
625
+ expect(events[2]).toBe('asyncStart');
626
+ expect(events[3]).toBe('asyncEnd');
627
+ expect(events.length).toBe(4);
628
+ });
629
+
630
+ await it('should set result on context for async success', async () => {
631
+ const tc = tracingChannel(uid('tp-ctx-res'));
632
+ const ctx: Record<string, unknown> = { foo: 'bar' };
633
+ let asyncStartCtx: any = null;
634
+ tc.subscribe({
635
+ start: () => {},
636
+ end: () => {},
637
+ asyncStart: (msg: unknown) => { asyncStartCtx = msg; },
638
+ asyncEnd: () => {},
639
+ });
640
+ await tc.tracePromise(() => Promise.resolve('value'), ctx);
641
+ expect(asyncStartCtx.result).toBe('value');
642
+ expect(asyncStartCtx.error).toBeUndefined();
643
+ });
644
+
645
+ await it('should pass thisArg and args to the function', async () => {
646
+ const tc = tracingChannel(uid('tp-thisarg'));
647
+ tc.subscribe({ start: () => {}, end: () => {}, asyncStart: () => {}, asyncEnd: () => {} });
648
+ const thisArg = { baz: 'buz' };
649
+ const expected = { foo: 'bar' };
650
+ let receivedThis: unknown = null;
651
+ let receivedArg: unknown = null;
652
+ await tc.tracePromise(function (this: unknown, val: unknown) {
653
+ receivedThis = this;
654
+ receivedArg = val;
655
+ return Promise.resolve(val);
656
+ }, {}, thisArg, expected);
657
+ expect(receivedThis).toBe(thisArg);
658
+ expect(receivedArg).toBe(expected);
659
+ });
660
+ });
661
+
662
+ // ---------------------------------------------------------------
663
+ // TracingChannel: tracePromise (error)
664
+ // ---------------------------------------------------------------
665
+ await describe('TracingChannel tracePromise error', async () => {
666
+ await it('should publish error, asyncStart, asyncEnd on rejection', async () => {
667
+ const tc = tracingChannel(uid('tp-err'));
668
+ const events: string[] = [];
669
+ let errorCtx: any = null;
670
+ tc.subscribe({
671
+ start: () => events.push('start'),
672
+ end: () => events.push('end'),
673
+ asyncStart: () => events.push('asyncStart'),
674
+ asyncEnd: () => events.push('asyncEnd'),
675
+ error: (msg: unknown) => {
676
+ events.push('error');
677
+ errorCtx = msg;
678
+ },
679
+ });
680
+ const expectedErr = new Error('async-fail');
681
+ try {
682
+ await tc.tracePromise(() => Promise.reject(expectedErr), {});
683
+ } catch {
684
+ // expected
685
+ }
686
+ expect(events).toContain('start');
687
+ expect(events).toContain('end');
688
+ expect(events).toContain('error');
689
+ expect(events).toContain('asyncStart');
690
+ expect(events).toContain('asyncEnd');
691
+ expect(errorCtx.error).toBe(expectedErr);
692
+ });
693
+
694
+ await it('should re-throw the rejection error', async () => {
695
+ const tc = tracingChannel(uid('tp-rethrow'));
696
+ tc.subscribe({ start: () => {}, end: () => {}, error: () => {}, asyncStart: () => {}, asyncEnd: () => {} });
697
+ const expectedErr = new Error('reject-test');
698
+ let caught: unknown = null;
699
+ try {
700
+ await tc.tracePromise(() => Promise.reject(expectedErr), {});
701
+ } catch (e) {
702
+ caught = e;
703
+ }
704
+ expect(caught).toBe(expectedErr);
705
+ });
706
+ });
707
+
708
+ // ---------------------------------------------------------------
709
+ // TracingChannel: traceCallback
710
+ // ---------------------------------------------------------------
711
+ await describe('TracingChannel traceCallback', async () => {
712
+ await it('should publish start, end, asyncStart, asyncEnd on success callback', async () => {
713
+ const tc = tracingChannel(uid('tc-cb-ok'));
714
+ const events: string[] = [];
715
+ tc.subscribe({
716
+ start: () => events.push('start'),
717
+ end: () => events.push('end'),
718
+ asyncStart: () => events.push('asyncStart'),
719
+ asyncEnd: () => events.push('asyncEnd'),
720
+ error: () => events.push('error'),
721
+ });
722
+
723
+ await new Promise<void>((resolve) => {
724
+ tc.traceCallback(
725
+ (cb: unknown) => {
726
+ // Simulate async: call the callback synchronously for simplicity
727
+ (cb as (err: null, res: string) => void)(null, 'result');
728
+ },
729
+ 0,
730
+ {},
731
+ undefined,
732
+ (err: unknown, res: unknown) => {
733
+ expect(err).toBeNull();
734
+ expect(res).toBe('result');
735
+ resolve();
736
+ },
737
+ );
738
+ });
739
+
740
+ expect(events).toContain('start');
741
+ expect(events).toContain('end');
742
+ expect(events).toContain('asyncStart');
743
+ expect(events).toContain('asyncEnd');
744
+ expect(events).not.toContain('error');
745
+ });
746
+
747
+ await it('should publish error when callback receives an error', async () => {
748
+ const tc = tracingChannel(uid('tc-cb-err'));
749
+ let errorPublished = false;
750
+ tc.subscribe({
751
+ start: () => {},
752
+ end: () => {},
753
+ asyncStart: () => {},
754
+ asyncEnd: () => {},
755
+ error: () => { errorPublished = true; },
756
+ });
757
+
758
+ const expectedErr = new Error('cb-error');
759
+ await new Promise<void>((resolve) => {
760
+ tc.traceCallback(
761
+ (cb: unknown) => {
762
+ (cb as (err: Error) => void)(expectedErr);
763
+ },
764
+ 0,
765
+ {},
766
+ undefined,
767
+ (err: unknown) => {
768
+ expect(err).toBe(expectedErr);
769
+ resolve();
770
+ },
771
+ );
772
+ });
773
+
774
+ expect(errorPublished).toBe(true);
775
+ });
776
+
777
+ await it('should throw if callback argument is not a function', async () => {
778
+ const tc = tracingChannel(uid('tc-cb-type'));
779
+ tc.subscribe({ start: () => {} });
780
+ let threw = false;
781
+ try {
782
+ tc.traceCallback(() => {}, 0, {}, undefined, 'not-a-function' as any);
783
+ } catch (e: any) {
784
+ threw = true;
785
+ expect(e.message).toContain('callback');
786
+ }
787
+ expect(threw).toBe(true);
788
+ });
789
+ });
790
+
791
+ // ---------------------------------------------------------------
792
+ // Edge cases
793
+ // ---------------------------------------------------------------
794
+ await describe('edge cases', async () => {
795
+ await it('should handle empty string channel name', async () => {
796
+ const ch = channel('');
797
+ expect(ch.name).toBe('');
798
+ expect(ch.hasSubscribers).toBe(false);
799
+ });
800
+
801
+ await it('should handle publishing various data types', async () => {
802
+ const ch = channel(uid('types'));
803
+ const received: unknown[] = [];
804
+ ch.subscribe((msg: unknown) => received.push(msg));
805
+ ch.publish(null);
806
+ ch.publish(undefined);
807
+ ch.publish(0);
808
+ ch.publish('');
809
+ ch.publish(false);
810
+ ch.publish([1, 2, 3]);
811
+ expect(received.length).toBe(6);
812
+ expect(received[0]).toBeNull();
813
+ expect(received[1]).toBeUndefined();
814
+ expect(received[2]).toBe(0);
815
+ expect(received[3]).toBe('');
816
+ expect(received[4]).toBe(false);
817
+ });
818
+
819
+ await it('should handle duplicate subscribe (same function twice)', async () => {
820
+ // Node.js uses array-based subscribers so duplicates are allowed.
821
+ // Our Set-based impl de-duplicates. Either behavior is acceptable
822
+ // as long as it does not crash and the subscriber runs at least once.
823
+ const ch = channel(uid('dup'));
824
+ let count = 0;
825
+ const handler = () => { count++; };
826
+ ch.subscribe(handler);
827
+ ch.subscribe(handler);
828
+ ch.publish('test');
829
+ // Should be called at least once
830
+ expect(count).toBeGreaterThan(0);
831
+ });
832
+
833
+ await it('channel publish with no subscribers should be a no-op', async () => {
834
+ const ch = channel(uid('noop-pub'));
835
+ // Should not throw
836
+ ch.publish({ some: 'data' });
837
+ });
838
+
839
+ await it('TracingChannel traceSync with no subscribers should just call fn', async () => {
840
+ const tc = tracingChannel(uid('ts-noop'));
841
+ let called = false;
842
+ const result = tc.traceSync(() => {
843
+ called = true;
844
+ return 'value';
845
+ }, {});
846
+ expect(called).toBe(true);
847
+ expect(result).toBe('value');
848
+ });
849
+
850
+ await it('TracingChannel tracePromise with no subscribers should just call fn', async () => {
851
+ const tc = tracingChannel(uid('tp-noop'));
852
+ let called = false;
853
+ const result = await tc.tracePromise(() => {
854
+ called = true;
855
+ return Promise.resolve('value');
856
+ }, {});
857
+ expect(called).toBe(true);
858
+ expect(result).toBe('value');
859
+ });
860
+
861
+ await it('TracingChannel traceSync end fires even on error (finally semantics)', async () => {
862
+ const tc = tracingChannel(uid('ts-finally'));
863
+ let endFired = false;
864
+ tc.subscribe({
865
+ start: () => {},
866
+ end: () => { endFired = true; },
867
+ error: () => {},
868
+ });
869
+ try {
870
+ tc.traceSync(() => { throw new Error('boom'); }, {});
871
+ } catch {
872
+ // expected
873
+ }
874
+ expect(endFired).toBe(true);
875
+ });
876
+
877
+ await it('TracingChannel created from channel objects should work', async () => {
878
+ const name = uid('from-obj');
879
+ const tc = tracingChannel({
880
+ start: channel(`tracing:${name}:start`),
881
+ end: channel(`tracing:${name}:end`),
882
+ asyncStart: channel(`tracing:${name}:asyncStart`),
883
+ asyncEnd: channel(`tracing:${name}:asyncEnd`),
884
+ error: channel(`tracing:${name}:error`),
885
+ });
886
+ const events: string[] = [];
887
+ tc.subscribe({
888
+ start: () => events.push('start'),
889
+ end: () => events.push('end'),
890
+ });
891
+ tc.traceSync(() => 'ok', {});
892
+ expect(events.length).toBe(2);
893
+ });
894
+ });
895
+
896
+ // ---------------------------------------------------------------
897
+ // Interaction between module-level and Channel-level APIs
898
+ // ---------------------------------------------------------------
899
+ await describe('module-level and Channel-level API interaction', async () => {
900
+ await it('subscribe via module function, publish via Channel instance', async () => {
901
+ const name = uid('cross');
902
+ const messages: unknown[] = [];
903
+ subscribe(name, (msg: unknown) => messages.push(msg));
904
+ channel(name).publish('hello');
905
+ expect(messages.length).toBe(1);
906
+ expect(messages[0]).toBe('hello');
907
+ });
908
+
909
+ await it('subscribe via Channel instance, check via module hasSubscribers', async () => {
910
+ const name = uid('cross2');
911
+ const ch = channel(name);
912
+ expect(hasSubscribers(name)).toBe(false);
913
+ ch.subscribe(() => {});
914
+ expect(hasSubscribers(name)).toBe(true);
915
+ });
916
+
917
+ await it('unsubscribe via module function affects Channel.hasSubscribers', async () => {
918
+ const name = uid('cross3');
919
+ const handler = () => {};
920
+ subscribe(name, handler);
921
+ expect(channel(name).hasSubscribers).toBe(true);
922
+ unsubscribe(name, handler);
923
+ expect(channel(name).hasSubscribers).toBe(false);
924
+ });
925
+ });
926
+ });
927
+ };