Skip to content

Commit c764f06

Browse files
committed
diagnostics_channel: add WindowChannel and scopes
This adds WindowChannel, adds using scope support to runStores, and modifies the internals to use these to avoid closures in several places.
1 parent 09cb6ad commit c764f06

12 files changed

+1865
-99
lines changed

doc/api/diagnostics_channel.md

Lines changed: 399 additions & 0 deletions
Large diffs are not rendered by default.

lib/diagnostics_channel.js

Lines changed: 273 additions & 99 deletions
Large diffs are not rendered by default.
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('node:assert');
4+
const dc = require('node:diagnostics_channel');
5+
const { AsyncLocalStorage } = require('node:async_hooks');
6+
7+
// Test RunStoresScope with transform error
8+
// Transform errors are scheduled via process.nextTick(triggerUncaughtException)
9+
10+
const channel = dc.channel('test-transform-error');
11+
const store = new AsyncLocalStorage();
12+
const events = [];
13+
14+
const transformError = new Error('transform failed');
15+
16+
// Set up uncaughtException handler to catch the transform error
17+
process.on('uncaughtException', common.mustCall((err) => {
18+
assert.strictEqual(err, transformError);
19+
events.push('uncaughtException');
20+
}));
21+
22+
channel.subscribe((message) => {
23+
events.push({ type: 'message', data: message });
24+
});
25+
26+
// Bind store with a transform that throws
27+
channel.bindStore(store, () => {
28+
throw transformError;
29+
});
30+
31+
// Store should remain undefined since transform failed
32+
assert.strictEqual(store.getStore(), undefined);
33+
34+
{
35+
// eslint-disable-next-line no-unused-vars
36+
using scope = channel.withStoreScope({ value: 'test' });
37+
38+
// Store should still be undefined because transform threw
39+
assert.strictEqual(store.getStore(), undefined);
40+
41+
events.push('inside-scope');
42+
}
43+
44+
// Store should still be undefined after scope exit
45+
assert.strictEqual(store.getStore(), undefined);
46+
47+
// Message should still be published despite transform error
48+
assert.strictEqual(events.length, 2);
49+
assert.strictEqual(events[0].type, 'message');
50+
assert.strictEqual(events[0].data.value, 'test');
51+
assert.strictEqual(events[1], 'inside-scope');
52+
53+
// Validate uncaughtException was triggered via nextTick
54+
process.on('beforeExit', common.mustCall(() => {
55+
assert.strictEqual(events.length, 3);
56+
assert.strictEqual(events[2], 'uncaughtException');
57+
}));
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
/* eslint-disable no-unused-vars */
2+
'use strict';
3+
require('../common');
4+
const assert = require('node:assert');
5+
const dc = require('node:diagnostics_channel');
6+
const { AsyncLocalStorage } = require('node:async_hooks');
7+
8+
// Test basic RunStoresScope with active channel
9+
{
10+
const channel = dc.channel('test-run-stores-scope-basic');
11+
const store = new AsyncLocalStorage();
12+
const events = [];
13+
14+
channel.subscribe((message) => {
15+
events.push({ type: 'message', data: message, store: store.getStore() });
16+
});
17+
18+
channel.bindStore(store, (data) => {
19+
return { transformed: data.value };
20+
});
21+
22+
assert.strictEqual(store.getStore(), undefined);
23+
24+
{
25+
using scope = channel.withStoreScope({ value: 'test' });
26+
27+
// Store should be set
28+
assert.deepStrictEqual(store.getStore(), { transformed: 'test' });
29+
30+
events.push({ type: 'inside', store: store.getStore() });
31+
}
32+
33+
// Store should be restored
34+
assert.strictEqual(store.getStore(), undefined);
35+
36+
assert.strictEqual(events.length, 2);
37+
assert.strictEqual(events[0].type, 'message');
38+
assert.strictEqual(events[0].data.value, 'test');
39+
assert.deepStrictEqual(events[0].store, { transformed: 'test' });
40+
41+
assert.strictEqual(events[1].type, 'inside');
42+
assert.deepStrictEqual(events[1].store, { transformed: 'test' });
43+
}
44+
45+
// Test RunStoresScope with inactive channel (no-op)
46+
{
47+
const channel = dc.channel('test-run-stores-scope-inactive');
48+
49+
// No subscribers, channel is inactive
50+
{
51+
using scope = channel.withStoreScope({ value: 'test' });
52+
assert.ok(scope);
53+
}
54+
55+
// Should not throw
56+
}
57+
58+
// Test RunStoresScope restores previous store value
59+
{
60+
const channel = dc.channel('test-run-stores-scope-restore');
61+
const store = new AsyncLocalStorage();
62+
63+
channel.subscribe(() => {});
64+
channel.bindStore(store, (data) => data);
65+
66+
store.enterWith('initial');
67+
assert.strictEqual(store.getStore(), 'initial');
68+
69+
{
70+
using scope = channel.withStoreScope('scoped');
71+
assert.strictEqual(store.getStore(), 'scoped');
72+
}
73+
74+
// Should restore to previous value
75+
assert.strictEqual(store.getStore(), 'initial');
76+
}
77+
78+
// Test RunStoresScope with multiple stores
79+
{
80+
const channel = dc.channel('test-run-stores-scope-multi');
81+
const store1 = new AsyncLocalStorage();
82+
const store2 = new AsyncLocalStorage();
83+
const store3 = new AsyncLocalStorage();
84+
85+
channel.subscribe(() => {});
86+
channel.bindStore(store1, (data) => `${data}-1`);
87+
channel.bindStore(store2, (data) => `${data}-2`);
88+
channel.bindStore(store3, (data) => `${data}-3`);
89+
90+
{
91+
using scope = channel.withStoreScope('test');
92+
93+
assert.strictEqual(store1.getStore(), 'test-1');
94+
assert.strictEqual(store2.getStore(), 'test-2');
95+
assert.strictEqual(store3.getStore(), 'test-3');
96+
}
97+
98+
assert.strictEqual(store1.getStore(), undefined);
99+
assert.strictEqual(store2.getStore(), undefined);
100+
assert.strictEqual(store3.getStore(), undefined);
101+
}
102+
103+
// Test manual dispose via Symbol.dispose
104+
{
105+
const channel = dc.channel('test-run-stores-scope-manual');
106+
const store = new AsyncLocalStorage();
107+
const events = [];
108+
109+
channel.subscribe((message) => {
110+
events.push(message);
111+
});
112+
113+
channel.bindStore(store, (data) => data);
114+
115+
const scope = channel.withStoreScope('test');
116+
117+
assert.strictEqual(events.length, 1);
118+
assert.strictEqual(store.getStore(), 'test');
119+
120+
scope[Symbol.dispose]();
121+
122+
// Store should be restored
123+
assert.strictEqual(store.getStore(), undefined);
124+
125+
// Double dispose should be idempotent
126+
scope[Symbol.dispose]();
127+
assert.strictEqual(store.getStore(), undefined);
128+
}
129+
130+
// Test nested RunStoresScope
131+
{
132+
const channel = dc.channel('test-run-stores-scope-nested');
133+
const store = new AsyncLocalStorage();
134+
const storeValues = [];
135+
136+
channel.subscribe(() => {});
137+
channel.bindStore(store, (data) => data);
138+
139+
{
140+
using outer = channel.withStoreScope('outer');
141+
storeValues.push(store.getStore());
142+
143+
{
144+
using inner = channel.withStoreScope('inner');
145+
storeValues.push(store.getStore());
146+
}
147+
148+
// Should restore to outer
149+
storeValues.push(store.getStore());
150+
}
151+
152+
// Should restore to undefined
153+
storeValues.push(store.getStore());
154+
155+
assert.deepStrictEqual(storeValues, ['outer', 'inner', 'outer', undefined]);
156+
}
157+
158+
// Test RunStoresScope with error during usage
159+
{
160+
const channel = dc.channel('test-run-stores-scope-error');
161+
const store = new AsyncLocalStorage();
162+
163+
channel.subscribe(() => {});
164+
channel.bindStore(store, (data) => data);
165+
166+
store.enterWith('before');
167+
168+
const testError = new Error('test');
169+
170+
assert.throws(() => {
171+
using scope = channel.withStoreScope('during');
172+
assert.strictEqual(store.getStore(), 'during');
173+
throw testError;
174+
}, testError);
175+
176+
// Store should be restored even after error
177+
assert.strictEqual(store.getStore(), 'before');
178+
}
179+
180+
// Test RunStoresScope with inactive channel (no stores or subscribers)
181+
{
182+
const channel = dc.channel('test-run-stores-scope-inactive');
183+
184+
// Channel is inactive (no subscribers or bound stores)
185+
{
186+
using scope = channel.withStoreScope('test');
187+
// No-op disposable, nothing happens
188+
assert.ok(scope);
189+
}
190+
}
191+
192+
// Test RunStoresScope with Symbol.dispose
193+
{
194+
const channel = dc.channel('test-run-stores-scope-symbol');
195+
const store = new AsyncLocalStorage();
196+
197+
channel.subscribe(() => {});
198+
channel.bindStore(store, (data) => data);
199+
200+
const scope = channel.withStoreScope('test');
201+
assert.strictEqual(store.getStore(), 'test');
202+
203+
// Call Symbol.dispose directly
204+
scope[Symbol.dispose]();
205+
assert.strictEqual(store.getStore(), undefined);
206+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('node:assert');
4+
const dc = require('node:diagnostics_channel');
5+
const { AsyncLocalStorage } = require('node:async_hooks');
6+
7+
// Test WindowChannel.run() with store transform error
8+
// Transform errors are scheduled via process.nextTick(triggerUncaughtException)
9+
10+
const windowChannel = dc.windowChannel('test-run-transform-error');
11+
const store = new AsyncLocalStorage();
12+
const events = [];
13+
14+
const transformError = new Error('transform failed');
15+
16+
// Set up uncaughtException handler to catch the transform error
17+
process.on('uncaughtException', common.mustCall((err) => {
18+
assert.strictEqual(err, transformError);
19+
events.push('uncaughtException');
20+
}));
21+
22+
windowChannel.subscribe({
23+
start(message) {
24+
events.push({ type: 'start', data: message });
25+
},
26+
end(message) {
27+
events.push({ type: 'end', data: message });
28+
},
29+
});
30+
31+
// Bind store with a transform that throws
32+
windowChannel.start.bindStore(store, () => {
33+
throw transformError;
34+
});
35+
36+
// Store should remain undefined since transform will fail
37+
assert.strictEqual(store.getStore(), undefined);
38+
39+
const result = windowChannel.run({ operationId: '123' }, common.mustCall(() => {
40+
// Store should still be undefined because transform threw
41+
assert.strictEqual(store.getStore(), undefined);
42+
43+
events.push('inside-run');
44+
45+
return 42;
46+
}));
47+
48+
// Should still return the result despite transform error
49+
assert.strictEqual(result, 42);
50+
51+
// Store should still be undefined after run
52+
assert.strictEqual(store.getStore(), undefined);
53+
54+
// Start and end events should still be published despite transform error
55+
assert.strictEqual(events.length, 3);
56+
assert.strictEqual(events[0].type, 'start');
57+
assert.strictEqual(events[0].data.operationId, '123');
58+
assert.strictEqual(events[1], 'inside-run');
59+
assert.strictEqual(events[2].type, 'end');
60+
assert.strictEqual(events[2].data.operationId, '123');
61+
62+
// Validate uncaughtException was triggered via nextTick
63+
process.on('beforeExit', common.mustCall(() => {
64+
assert.strictEqual(events.length, 4);
65+
assert.strictEqual(events[3], 'uncaughtException');
66+
}));

0 commit comments

Comments
 (0)