Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
397 changes: 397 additions & 0 deletions doc/api/diagnostics_channel.md

Large diffs are not rendered by default.

391 changes: 285 additions & 106 deletions lib/diagnostics_channel.js

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
'use strict';
const common = require('../common');
const assert = require('node:assert');
const dc = require('node:diagnostics_channel');
const { AsyncLocalStorage } = require('node:async_hooks');

// Test BoundedChannel.run() with store transform error
// Transform errors are scheduled via process.nextTick(triggerUncaughtException)

const boundedChannel = dc.boundedChannel('test-run-transform-error');
const store = new AsyncLocalStorage();
const events = [];

const transformError = new Error('transform failed');

// Set up uncaughtException handler to catch the transform error
process.on('uncaughtException', common.mustCall((err) => {
assert.strictEqual(err, transformError);
events.push('uncaughtException');
}));

boundedChannel.subscribe({
start(message) {
events.push({ type: 'start', data: message });
},
end(message) {
events.push({ type: 'end', data: message });
},
});

// Bind store with a transform that throws
boundedChannel.start.bindStore(store, () => {
throw transformError;
});

// Store should remain undefined since transform will fail
assert.strictEqual(store.getStore(), undefined);

const result = boundedChannel.run({ operationId: '123' }, common.mustCall(() => {
// Store should still be undefined because transform threw
assert.strictEqual(store.getStore(), undefined);

events.push('inside-run');

return 42;
}));

// Should still return the result despite transform error
assert.strictEqual(result, 42);

// Store should still be undefined after run
assert.strictEqual(store.getStore(), undefined);

// Start and end events should still be published despite transform error
assert.strictEqual(events.length, 3);
assert.strictEqual(events[0].type, 'start');
assert.strictEqual(events[0].data.operationId, '123');
assert.strictEqual(events[1], 'inside-run');
assert.strictEqual(events[2].type, 'end');
assert.strictEqual(events[2].data.operationId, '123');

// Validate uncaughtException was triggered via nextTick
process.on('beforeExit', common.mustCall(() => {
assert.strictEqual(events.length, 4);
assert.strictEqual(events[3], 'uncaughtException');
}));
125 changes: 125 additions & 0 deletions test/parallel/test-diagnostics-channel-bounded-channel-run.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
'use strict';
require('../common');
const assert = require('node:assert');
const dc = require('node:diagnostics_channel');
const { AsyncLocalStorage } = require('node:async_hooks');

// Test basic run functionality
{
const boundedChannel = dc.boundedChannel('test-run-basic');
const events = [];

boundedChannel.subscribe({
start(message) {
events.push({ type: 'start', data: message });
},
end(message) {
events.push({ type: 'end', data: message });
},
});

const context = { id: 123 };
const result = boundedChannel.run(context, () => {
return 'success';
});

assert.strictEqual(result, 'success');
assert.strictEqual(events.length, 2);
assert.deepStrictEqual(events, [
{ type: 'start', data: { id: 123 } },
{ type: 'end', data: { id: 123 } },
]);
}

// Test run with error
{
const boundedChannel = dc.boundedChannel('test-run-error');
const events = [];

boundedChannel.subscribe({
start(message) {
events.push({ type: 'start', data: message });
},
end(message) {
events.push({ type: 'end', data: message });
},
});

const context = { id: 456 };
const testError = new Error('test error');

assert.throws(() => {
boundedChannel.run(context, () => {
throw testError;
});
}, testError);

// BoundedChannel does not handle errors - they just propagate
// Only start and end events are published
assert.strictEqual(events.length, 2);
assert.deepStrictEqual(events, [
{ type: 'start', data: { id: 456 } },
{ type: 'end', data: { id: 456 } },
]);
}

// Test run with thisArg and args
{
const boundedChannel = dc.boundedChannel('test-run-args');

const obj = { value: 10 };
const result = boundedChannel.run({}, function(a, b) {
return this.value + a + b;
}, obj, 5, 15);

assert.strictEqual(result, 30);
}

// Test run with AsyncLocalStorage
{
const boundedChannel = dc.boundedChannel('test-run-store');
const store = new AsyncLocalStorage();
const events = [];

boundedChannel.start.bindStore(store, (context) => {
return { traceId: context.traceId };
});

boundedChannel.subscribe({
start(message) {
events.push({ type: 'start', store: store.getStore() });
},
end(message) {
events.push({ type: 'end', store: store.getStore() });
},
});

const result = boundedChannel.run({ traceId: 'abc123' }, () => {
events.push({ type: 'inside', store: store.getStore() });
return 'result';
});

assert.strictEqual(result, 'result');
assert.strictEqual(events.length, 3);

// Innert events should have store set
assert.deepStrictEqual(events, [
{ type: 'start', store: { traceId: 'abc123' } },
{ type: 'inside', store: { traceId: 'abc123' } },
{ type: 'end', store: { traceId: 'abc123' } },
]);

// Store should be undefined outside
assert.strictEqual(store.getStore(), undefined);
}

// Test run without subscribers
{
const boundedChannel = dc.boundedChannel('test-run-no-subs');

const result = boundedChannel.run({}, () => {
return 'fast path';
});

assert.strictEqual(result, 'fast path');
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/* eslint-disable no-unused-vars */
'use strict';
require('../common');
const assert = require('node:assert');
const dc = require('node:diagnostics_channel');
const { AsyncLocalStorage } = require('node:async_hooks');

// Test scope with thrown error
{
const boundedChannel = dc.boundedChannel('test-scope-throw');
const events = [];

boundedChannel.subscribe({
start(message) {
events.push({ type: 'start', data: message });
},
end(message) {
events.push({ type: 'end', data: message });
},
});

const context = { id: 1 };
const testError = new Error('thrown error');

assert.throws(() => {
using scope = boundedChannel.withScope(context);
context.result = 'partial';
throw testError;
}, testError);

// End event should still be published
assert.strictEqual(events.length, 2);
assert.strictEqual(events[0].type, 'start');
assert.strictEqual(events[1].type, 'end');

// Context should have partial result but no error from throw
assert.strictEqual(context.result, 'partial');
assert.strictEqual(context.error, undefined);
}

// Test store restoration on error
{
const boundedChannel = dc.boundedChannel('test-scope-store-error');
const store = new AsyncLocalStorage();

boundedChannel.start.bindStore(store, (context) => context.value);

boundedChannel.subscribe({
start() {},
end() {},
});

store.enterWith('before');
assert.strictEqual(store.getStore(), 'before');

const testError = new Error('test');

assert.throws(() => {
using scope = boundedChannel.withScope({ value: 'during' });
assert.strictEqual(store.getStore(), 'during');
throw testError;
}, testError);

// Store should be restored even after error
assert.strictEqual(store.getStore(), 'before');
}

// Test dispose during exception handling
{
const boundedChannel = dc.boundedChannel('test-scope-dispose-exception');
const events = [];

boundedChannel.subscribe({
start() {
events.push('start');
},
end() {
events.push('end');
},
});

// Dispose should complete even when exception is thrown
assert.throws(() => {
using scope = boundedChannel.withScope({});
throw new Error('original error');
}, /original error/);

// End event should have been called
assert.deepStrictEqual(events, ['start', 'end']);
}
Loading
Loading