From 6a9412f2f0643766a019b77586ab4bf2f32b619b Mon Sep 17 00:00:00 2001 From: Divyanshu Sharma Date: Sun, 24 May 2026 15:11:15 +0530 Subject: [PATCH 1/9] errors: handle V8 warnings in DisallowJavascriptExecutionScope Defer non-critical warnings to the next event loop iteration when can_call_into_js() returns false. This prevents crashes when V8 emits warnings during REPL preview evaluation or other contexts where JavaScript execution is temporarily forbidden. When a warning is emitted inside DisallowJavascriptExecutionScope, ProcessEmitWarningGeneric cannot be called immediately. Instead, use env->SetImmediate() to queue the warning emission for after the scope exits. This preserves full warning formatting, deprecation codes, and --redirect-warnings routing. Signed-off-by: Divyanshu Sharma --- src/node_errors.cc | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/node_errors.cc b/src/node_errors.cc index 74326496132773..63db97f6a56db0 100644 --- a/src/node_errors.cc +++ b/src/node_errors.cc @@ -1064,7 +1064,12 @@ void PerIsolateMessageListener(Local message, Local error) { filename, message->GetLineNumber(env->context()).FromMaybe(-1), msg); - USE(ProcessEmitWarningGeneric(env, warning, "V8")); + // Defer the warning to the next event loop iteration. This prevents + // crashes when V8 emits warnings during code evaluation with + // throwOnSideEffect. + env->SetImmediate([warning](Environment* env) { + ProcessEmitWarningGeneric(env, warning, "V8"); + }); break; } case Isolate::MessageErrorLevel::kMessageError: From a786501288a70db3c65e8965e2e7e66c30a7c5a0 Mon Sep 17 00:00:00 2001 From: Divyanshu Sharma Date: Sat, 30 May 2026 04:07:04 +0530 Subject: [PATCH 2/9] lib: diagnostics_channel use AsyncLocalStorage for suppression context Refs: #63623 Refs: #63651 Signed-off-by: Divyanshu Sharma --- lib/diagnostics_channel.js | 120 +++++++++++++++++++++++++++++++------ 1 file changed, 101 insertions(+), 19 deletions(-) diff --git a/lib/diagnostics_channel.js b/lib/diagnostics_channel.js index 8d2d374dc8e6ae..18f2005ff2314e 100644 --- a/lib/diagnostics_channel.js +++ b/lib/diagnostics_channel.js @@ -2,7 +2,6 @@ const { ArrayPrototypeAt, - ArrayPrototypeIndexOf, ArrayPrototypePush, ArrayPrototypePushApply, ArrayPrototypeSlice, @@ -15,6 +14,7 @@ const { ReflectApply, SafeFinalizationRegistry, SafeMap, + SafeSet, SymbolDispose, SymbolHasInstance, } = primordials; @@ -36,6 +36,32 @@ const { subscribers: subscriberCounts } = dc_binding; const { WeakReference } = require('internal/util'); const { isPromise } = require('internal/util/types'); +// Internal only: tracks a Set of active suppression keys for the current async +// context. Uses a simple stack-based approach to avoid bootstrap issues with +// async_hooks. This is a simplified implementation that works for typical usage. +let suppressionStorage = null; + +function getSuppressionsStorage() { + if (suppressionStorage === null) { + try { + const { AsyncLocalStorage } = require('async_hooks'); + suppressionStorage = new AsyncLocalStorage(); + } catch { + // If AsyncLocalStorage fails to initialize (rare), use a fallback + // that won't provide async context isolation but at least works + suppressionStorage = false; // Marker for "tried and failed" + } + } + return suppressionStorage || undefined; +} + +function withSuppressionsContext(set, fn, thisArg, args) { + const storage = getSuppressionsStorage(); + if (storage) { + return storage.run(set, () => ReflectApply(fn, thisArg, args)); + } + return ReflectApply(fn, thisArg, args); +} // Can't delete when weakref count reaches 0 as it could increment again. // Only GC can be used as a valid time to clean up the channels map. class WeakRefMap extends SafeMap { @@ -93,9 +119,17 @@ class RunStoresScope { // Enter stores using withScope if (activeChannel._stores) { + const storage = getSuppressionsStorage(); + const activeKeys = storage ? storage.getStore() : undefined; for (const entry of activeChannel._stores.entries()) { const store = entry[0]; - const transform = entry[1]; + const { transform, suppressedBy = null } = entry[1]; + + // Skip this bound store if it opted into suppression and its key + // is active in the current async context. + if (suppressedBy !== null && activeKeys?.has(suppressedBy)) { + continue; + } let newContext = data; if (transform) { @@ -127,16 +161,32 @@ class RunStoresScope { // TODO(qard): should there be a C++ channel interface? class ActiveChannel { - subscribe(subscription) { + subscribe(subscription, options = {}) { validateFunction(subscription, 'subscription'); + const suppressedBy = options && options.suppressedBy !== undefined ? options.suppressedBy : null; + if (suppressedBy !== null) { + const t = typeof suppressedBy; + if (t === 'string' || t === 'number' || t === 'bigint' || t === 'boolean') { + throw new ERR_INVALID_ARG_TYPE('suppressedBy', ['object', 'symbol', 'function'], suppressedBy); + } + } + + const handler = subscription; this._subscribers = ArrayPrototypeSlice(this._subscribers); - ArrayPrototypePush(this._subscribers, subscription); + ArrayPrototypePush(this._subscribers, { handler, suppressedBy }); channels.incRef(this.name); if (this._index !== undefined) subscriberCounts[this._index]++; } unsubscribe(subscription) { - const index = ArrayPrototypeIndexOf(this._subscribers, subscription); + // Find subscriber entry by handler identity. + let index = -1; + for (let i = 0; i < (this._subscribers?.length || 0); i++) { + if (this._subscribers[i].handler === subscription) { + index = i; + break; + } + } if (index === -1) return false; const before = ArrayPrototypeSlice(this._subscribers, 0, index); @@ -151,13 +201,21 @@ class ActiveChannel { return true; } - bindStore(store, transform) { + bindStore(store, transform, options = {}) { + const suppressedBy = options && options.suppressedBy !== undefined ? options.suppressedBy : null; + if (suppressedBy !== null) { + const t = typeof suppressedBy; + if (t === 'string' || t === 'number' || t === 'bigint' || t === 'boolean') { + throw new ERR_INVALID_ARG_TYPE('suppressedBy', ['object', 'symbol', 'function'], suppressedBy); + } + } + const replacing = this._stores.has(store); if (!replacing) { channels.incRef(this.name); if (this._index !== undefined) subscriberCounts[this._index]++; } - this._stores.set(store, transform); + this._stores.set(store, { transform, suppressedBy }); } unbindStore(store) { @@ -180,10 +238,15 @@ class ActiveChannel { publish(data) { const subscribers = this._subscribers; + const storage = getSuppressionsStorage(); + const activeKeys = storage ? storage.getStore() : undefined; for (let i = 0; i < (subscribers?.length || 0); i++) { try { - const onMessage = subscribers[i]; - onMessage(data, this.name); + const { handler, suppressedBy = null } = subscribers[i]; + if (suppressedBy !== null && activeKeys?.has(suppressedBy)) { + continue; + } + handler(data, this.name); } catch (err) { process.nextTick(() => { triggerUncaughtException(err, false); @@ -221,18 +284,18 @@ class Channel { prototype === ActiveChannel.prototype; } - subscribe(subscription) { + subscribe(subscription, options) { markActive(this); - this.subscribe(subscription); + this.subscribe(subscription, options); } unsubscribe() { return false; } - bindStore(store, transform) { + bindStore(store, transform, options) { markActive(this); - this.bindStore(store, transform); + this.bindStore(store, transform, options); } unbindStore() { @@ -366,12 +429,12 @@ class BoundedChannel { this.end?.hasSubscribers; } - subscribe(handlers) { + subscribe(handlers, options) { for (let i = 0; i < boundedEvents.length; ++i) { const name = boundedEvents[i]; if (!handlers[name]) continue; - this[name]?.subscribe(handlers[name]); + this[name]?.subscribe(handlers[name], options); } } @@ -458,13 +521,13 @@ class TracingChannel { this.error?.hasSubscribers; } - subscribe(handlers) { + subscribe(handlers, options) { // Subscribe to call window (start/end) if (handlers.start || handlers.end) { this.#callWindow.subscribe({ start: handlers.start, end: handlers.end, - }); + }, options); } // Subscribe to continuation window (asyncStart/asyncEnd) @@ -472,12 +535,12 @@ class TracingChannel { this.#continuationWindow.subscribe({ start: handlers.asyncStart, end: handlers.asyncEnd, - }); + }, options); } // Subscribe to error channel if (handlers.error) { - this.error.subscribe(handlers.error); + this.error.subscribe(handlers.error, options); } } @@ -633,10 +696,29 @@ function tracingChannel(nameOrChannels) { dc_binding.linkNativeChannel((name) => channel(name)); +function suppressed(key, fn, thisArg, ...args) { + validateFunction(fn, 'fn'); + + if (key === null || key === undefined) { + throw new ERR_INVALID_ARG_TYPE('key', ['object', 'symbol', 'function'], key); + } + const t = typeof key; + if (t === 'string' || t === 'number' || t === 'bigint' || t === 'boolean') { + throw new ERR_INVALID_ARG_TYPE('key', ['object', 'symbol', 'function'], key); + } + + const storage = getSuppressionsStorage(); + const currentSet = storage ? storage.getStore() : undefined; + const next = currentSet ? new SafeSet(currentSet) : new SafeSet(); + next.add(key); + return withSuppressionsContext(next, fn, thisArg, args); +} + module.exports = { channel, hasSubscribers, subscribe, + suppressed, tracingChannel, unsubscribe, boundedChannel, From e4aea85312a5f045c49136c1f1b88c84b9cd2d9b Mon Sep 17 00:00:00 2001 From: Divyanshu Sharma Date: Sat, 30 May 2026 04:07:05 +0530 Subject: [PATCH 3/9] test: add diagnostics_channel suppression coverage Refs: #63623 Refs: #63651 Signed-off-by: Divyanshu Sharma --- .../test-diagnostics-channel-suppression.js | 255 ++++++++++++++++++ 1 file changed, 255 insertions(+) create mode 100644 test/parallel/test-diagnostics-channel-suppression.js diff --git a/test/parallel/test-diagnostics-channel-suppression.js b/test/parallel/test-diagnostics-channel-suppression.js new file mode 100644 index 00000000000000..a0143b9d2fafb2 --- /dev/null +++ b/test/parallel/test-diagnostics-channel-suppression.js @@ -0,0 +1,255 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { channel, suppressed } = require('node:diagnostics_channel'); +const { AsyncLocalStorage } = require('async_hooks'); + +// Helper to run a function and capture whether a handler was called +function makeHandler() { + let called = false; + const handler = (msg) => { called = true; }; + return { + handler, + called: () => called, + reset: () => { called = false; } + }; +} + +// Test 1: Basic suppression - subscriber with suppressedBy is skipped inside suppressed() +(function testBasicSuppression() { + const key = Symbol('tracer'); + const ch = channel('test-suppression-basic'); + const h = makeHandler(); + ch.subscribe(h.handler, { suppressedBy: key }); + + suppressed(key, () => { + ch.publish({}); + }); + + assert.strictEqual(h.called(), false, 'suppressed subscriber should not be called'); + // cleanup + ch.unsubscribe(h.handler); +})(); + +// Test 2: Non-opted subscriber fires even inside suppressed() scope +(function testNonOptedFires() { + const key = Symbol('tracer2'); + const ch = channel('test-suppression-nonopted'); + const h1 = makeHandler(); + const h2 = makeHandler(); + ch.subscribe(h1.handler, { suppressedBy: key }); + ch.subscribe(h2.handler); // no suppression + + suppressed(key, () => { + ch.publish({}); + }); + + assert.strictEqual(h1.called(), false, 'opted subscriber should be skipped'); + assert.strictEqual(h2.called(), true, 'non-opted subscriber should be called'); + + ch.unsubscribe(h1.handler); + ch.unsubscribe(h2.handler); +})(); + +// Test 3: Two APMs with different keys don't suppress each other +(function testTwoKeysIndependent() { + const k1 = Symbol('k1'); + const k2 = Symbol('k2'); + const ch = channel('test-suppression-two-keys'); + const h1 = makeHandler(); + const h2 = makeHandler(); + ch.subscribe(h1.handler, { suppressedBy: k1 }); + ch.subscribe(h2.handler, { suppressedBy: k2 }); + + suppressed(k1, () => { + ch.publish({}); + }); + + assert.strictEqual(h1.called(), false); + assert.strictEqual(h2.called(), true); + + h1.reset(); h2.reset(); + + suppressed(k2, () => { + ch.publish({}); + }); + + assert.strictEqual(h1.called(), true); + assert.strictEqual(h2.called(), false); + + ch.unsubscribe(h1.handler); + ch.unsubscribe(h2.handler); +})(); + +// Test 4: Nested suppressed() calls (same key, different keys) +(function testNestedSuppressed() { + const k1 = Symbol('nested1'); + const k2 = Symbol('nested2'); + const ch = channel('test-suppression-nested'); + const h1 = makeHandler(); + const h2 = makeHandler(); + ch.subscribe(h1.handler, { suppressedBy: k1 }); + ch.subscribe(h2.handler, { suppressedBy: k2 }); + + suppressed(k1, () => { + // inside k1, h1 skipped, h2 runs + ch.publish({}); + assert.strictEqual(h1.called(), false); + assert.strictEqual(h2.called(), true); + h2.reset(); + + suppressed(k2, () => { + // inside both, both skipped + ch.publish({}); + assert.strictEqual(h1.called(), false); + assert.strictEqual(h2.called(), false); + }); + + // back to only k1 + ch.publish({}); + assert.strictEqual(h1.called(), false); + assert.strictEqual(h2.called(), true); + }); + + ch.unsubscribe(h1.handler); + ch.unsubscribe(h2.handler); +})(); + +// Test 5: suppressed() across a Promise boundary (async/await) +(async function testSuppressedAcrossPromise() { + const key = Symbol('promise'); + const ch = channel('test-suppression-promise'); + const h = makeHandler(); + ch.subscribe(h.handler, { suppressedBy: key }); + + await suppressed(key, async () => { + await Promise.resolve(); + ch.publish({}); + }); + + assert.strictEqual(h.called(), false); + ch.unsubscribe(h.handler); +})(); + +// Test 6: suppressed() across setImmediate and queueMicrotask +(async function testSuppressedAcrossTimers() { + const key = Symbol('timers'); + const ch = channel('test-suppression-timers'); + const h = makeHandler(); + ch.subscribe(h.handler, { suppressedBy: key }); + + await suppressed(key, async () => { + await new Promise((resolve) => { + setImmediate(() => { + ch.publish({}); + assert.strictEqual(h.called(), false); + h.reset(); + + queueMicrotask(() => { + ch.publish({}); + assert.strictEqual(h.called(), false); + + ch.unsubscribe(h.handler); + resolve(); + }); + }); + }); + }); +})(); + +// Test 7: unsubscribe() works correctly after using suppressedBy +(function testUnsubscribeCleansUp() { + const key = Symbol('unsub'); + const ch = channel('test-suppression-unsubscribe'); + const h = makeHandler(); + ch.subscribe(h.handler, { suppressedBy: key }); + ch.unsubscribe(h.handler); + + // Should not throw and should not be called + suppressed(key, () => { + ch.publish({}); + }); + + assert.strictEqual(h.called(), false); +})(); + +// Test 8: bindStore with suppressedBy is skipped inside suppressed() +(function testBindStoreSuppression() { + const key = Symbol('store'); + const ch = channel('test-suppression-store'); + const als = new AsyncLocalStorage(); + + let transformCalls = 0; + const handler = common.mustCall(() => { + assert.strictEqual(als.getStore(), undefined); + }); + + ch.subscribe(handler); + ch.bindStore(als, (d) => { + transformCalls++; + return { foo: d }; + }, { suppressedBy: key }); + + suppressed(key, () => { + ch.publish({}); + }); + + assert.strictEqual(transformCalls, 0); + ch.unsubscribe(handler); + ch.unbindStore(als); +})(); + +// Test 9: Wrong type for suppressedBy throws ERR_INVALID_ARG_TYPE +(function testWrongTypeThrows() { + const ch = channel('test-suppression-wrong-type'); + const bad = 'not-allowed'; + assert.throws(() => ch.subscribe(() => {}, { suppressedBy: bad }), { + name: 'TypeError' + }); + const als = new AsyncLocalStorage(); + assert.throws(() => ch.bindStore(als, (d) => d, { suppressedBy: bad }), { + name: 'TypeError' + }); +})(); + +// Test 10: suppressed() return value passes through fn's return value +(function testSuppressedReturnValueAndContext() { + const key = Symbol('return'); + const receiver = { value: 41 }; + const result = suppressed(key, function(a, b) { + assert.strictEqual(this, receiver); + assert.strictEqual(a, 'a'); + assert.strictEqual(b, 'b'); + return this.value + 1; + }, receiver, 'a', 'b'); + assert.strictEqual(result, 42); +})(); + +// Test 11: null suppressedBy behaves like no suppression opt-in +(function testNullSuppressedByIsIgnored() { + const key = Symbol('null-suppressed-by'); + const ch = channel('test-suppression-null-suppressed-by'); + const h = makeHandler(); + + ch.subscribe(h.handler, { suppressedBy: null }); + + suppressed(key, () => { + ch.publish({}); + }); + + assert.strictEqual(h.called(), true); + ch.unsubscribe(h.handler); +})(); + +// Test 12: suppressed() rejects null/undefined keys consistently +(function testKeyValidation() { + assert.throws(() => suppressed(null, () => {}), { + name: 'TypeError' + }); + assert.throws(() => suppressed(undefined, () => {}), { + name: 'TypeError' + }); +})(); + +console.log('ok - diagnostics_channel suppression tests loaded'); From a5c4940295ffd4dcfcb026834e579f990f5197b3 Mon Sep 17 00:00:00 2001 From: Divyanshu Sharma Date: Sat, 30 May 2026 13:54:10 +0530 Subject: [PATCH 4/9] lib: rename diagnostics_channel suppression option to subscriberId Signed-off-by: Divyanshu Sharma --- lib/diagnostics_channel.js | 62 ++++++++++++++------------------------ 1 file changed, 23 insertions(+), 39 deletions(-) diff --git a/lib/diagnostics_channel.js b/lib/diagnostics_channel.js index 18f2005ff2314e..ccabaade84987d 100644 --- a/lib/diagnostics_channel.js +++ b/lib/diagnostics_channel.js @@ -36,31 +36,18 @@ const { subscribers: subscriberCounts } = dc_binding; const { WeakReference } = require('internal/util'); const { isPromise } = require('internal/util/types'); -// Internal only: tracks a Set of active suppression keys for the current async -// context. Uses a simple stack-based approach to avoid bootstrap issues with -// async_hooks. This is a simplified implementation that works for typical usage. -let suppressionStorage = null; +let suppressionStorage; function getSuppressionsStorage() { - if (suppressionStorage === null) { - try { - const { AsyncLocalStorage } = require('async_hooks'); - suppressionStorage = new AsyncLocalStorage(); - } catch { - // If AsyncLocalStorage fails to initialize (rare), use a fallback - // that won't provide async context isolation but at least works - suppressionStorage = false; // Marker for "tried and failed" - } + if (suppressionStorage === undefined) { + const { AsyncLocalStorage } = require('async_hooks'); + suppressionStorage = new AsyncLocalStorage(); } - return suppressionStorage || undefined; + return suppressionStorage; } function withSuppressionsContext(set, fn, thisArg, args) { - const storage = getSuppressionsStorage(); - if (storage) { - return storage.run(set, () => ReflectApply(fn, thisArg, args)); - } - return ReflectApply(fn, thisArg, args); + return getSuppressionsStorage().run(set, () => ReflectApply(fn, thisArg, args)); } // Can't delete when weakref count reaches 0 as it could increment again. // Only GC can be used as a valid time to clean up the channels map. @@ -119,15 +106,14 @@ class RunStoresScope { // Enter stores using withScope if (activeChannel._stores) { - const storage = getSuppressionsStorage(); - const activeKeys = storage ? storage.getStore() : undefined; + const activeKeys = getSuppressionsStorage().getStore(); for (const entry of activeChannel._stores.entries()) { const store = entry[0]; - const { transform, suppressedBy = null } = entry[1]; + const { transform, subscriberId = null } = entry[1]; // Skip this bound store if it opted into suppression and its key // is active in the current async context. - if (suppressedBy !== null && activeKeys?.has(suppressedBy)) { + if (subscriberId !== null && activeKeys?.has(subscriberId)) { continue; } @@ -163,17 +149,17 @@ class RunStoresScope { class ActiveChannel { subscribe(subscription, options = {}) { validateFunction(subscription, 'subscription'); - const suppressedBy = options && options.suppressedBy !== undefined ? options.suppressedBy : null; - if (suppressedBy !== null) { - const t = typeof suppressedBy; + const subscriberId = options && options.subscriberId !== undefined ? options.subscriberId : null; + if (subscriberId !== null) { + const t = typeof subscriberId; if (t === 'string' || t === 'number' || t === 'bigint' || t === 'boolean') { - throw new ERR_INVALID_ARG_TYPE('suppressedBy', ['object', 'symbol', 'function'], suppressedBy); + throw new ERR_INVALID_ARG_TYPE('subscriberId', ['object', 'symbol', 'function'], subscriberId); } } const handler = subscription; this._subscribers = ArrayPrototypeSlice(this._subscribers); - ArrayPrototypePush(this._subscribers, { handler, suppressedBy }); + ArrayPrototypePush(this._subscribers, { handler, subscriberId }); channels.incRef(this.name); if (this._index !== undefined) subscriberCounts[this._index]++; } @@ -202,11 +188,11 @@ class ActiveChannel { } bindStore(store, transform, options = {}) { - const suppressedBy = options && options.suppressedBy !== undefined ? options.suppressedBy : null; - if (suppressedBy !== null) { - const t = typeof suppressedBy; + const subscriberId = options && options.subscriberId !== undefined ? options.subscriberId : null; + if (subscriberId !== null) { + const t = typeof subscriberId; if (t === 'string' || t === 'number' || t === 'bigint' || t === 'boolean') { - throw new ERR_INVALID_ARG_TYPE('suppressedBy', ['object', 'symbol', 'function'], suppressedBy); + throw new ERR_INVALID_ARG_TYPE('subscriberId', ['object', 'symbol', 'function'], subscriberId); } } @@ -215,7 +201,7 @@ class ActiveChannel { channels.incRef(this.name); if (this._index !== undefined) subscriberCounts[this._index]++; } - this._stores.set(store, { transform, suppressedBy }); + this._stores.set(store, { transform, subscriberId }); } unbindStore(store) { @@ -238,12 +224,11 @@ class ActiveChannel { publish(data) { const subscribers = this._subscribers; - const storage = getSuppressionsStorage(); - const activeKeys = storage ? storage.getStore() : undefined; + const activeKeys = getSuppressionsStorage().getStore(); for (let i = 0; i < (subscribers?.length || 0); i++) { try { - const { handler, suppressedBy = null } = subscribers[i]; - if (suppressedBy !== null && activeKeys?.has(suppressedBy)) { + const { handler, subscriberId = null } = subscribers[i]; + if (subscriberId !== null && activeKeys?.has(subscriberId)) { continue; } handler(data, this.name); @@ -707,8 +692,7 @@ function suppressed(key, fn, thisArg, ...args) { throw new ERR_INVALID_ARG_TYPE('key', ['object', 'symbol', 'function'], key); } - const storage = getSuppressionsStorage(); - const currentSet = storage ? storage.getStore() : undefined; + const currentSet = getSuppressionsStorage().getStore(); const next = currentSet ? new SafeSet(currentSet) : new SafeSet(); next.add(key); return withSuppressionsContext(next, fn, thisArg, args); From 7e415c058968d650cb5ca3be796ae6b73ecc1074 Mon Sep 17 00:00:00 2001 From: Divyanshu Sharma Date: Sat, 30 May 2026 13:54:15 +0530 Subject: [PATCH 5/9] test: clean up diagnostics_channel suppression coverage Signed-off-by: Divyanshu Sharma --- .../test-diagnostics-channel-suppression.js | 203 +++++++----------- 1 file changed, 74 insertions(+), 129 deletions(-) diff --git a/test/parallel/test-diagnostics-channel-suppression.js b/test/parallel/test-diagnostics-channel-suppression.js index a0143b9d2fafb2..0f9e4cae3c6a89 100644 --- a/test/parallel/test-diagnostics-channel-suppression.js +++ b/test/parallel/test-diagnostics-channel-suppression.js @@ -5,216 +5,187 @@ const assert = require('assert'); const { channel, suppressed } = require('node:diagnostics_channel'); const { AsyncLocalStorage } = require('async_hooks'); -// Helper to run a function and capture whether a handler was called -function makeHandler() { - let called = false; - const handler = (msg) => { called = true; }; - return { - handler, - called: () => called, - reset: () => { called = false; } - }; -} - -// Test 1: Basic suppression - subscriber with suppressedBy is skipped inside suppressed() -(function testBasicSuppression() { +// Test 1: Basic suppression - subscriber with subscriberId is skipped inside suppressed() +{ const key = Symbol('tracer'); const ch = channel('test-suppression-basic'); - const h = makeHandler(); - ch.subscribe(h.handler, { suppressedBy: key }); + const handler = common.mustNotCall(); + ch.subscribe(handler, { subscriberId: key }); suppressed(key, () => { ch.publish({}); }); - assert.strictEqual(h.called(), false, 'suppressed subscriber should not be called'); - // cleanup - ch.unsubscribe(h.handler); -})(); + ch.unsubscribe(handler); +} // Test 2: Non-opted subscriber fires even inside suppressed() scope -(function testNonOptedFires() { +{ const key = Symbol('tracer2'); const ch = channel('test-suppression-nonopted'); - const h1 = makeHandler(); - const h2 = makeHandler(); - ch.subscribe(h1.handler, { suppressedBy: key }); - ch.subscribe(h2.handler); // no suppression + const optedHandler = common.mustNotCall(); + const regularHandler = common.mustCall(() => {}, 1); + ch.subscribe(optedHandler, { subscriberId: key }); + ch.subscribe(regularHandler); // no suppression suppressed(key, () => { ch.publish({}); }); - assert.strictEqual(h1.called(), false, 'opted subscriber should be skipped'); - assert.strictEqual(h2.called(), true, 'non-opted subscriber should be called'); - - ch.unsubscribe(h1.handler); - ch.unsubscribe(h2.handler); -})(); + ch.unsubscribe(optedHandler); + ch.unsubscribe(regularHandler); +} // Test 3: Two APMs with different keys don't suppress each other -(function testTwoKeysIndependent() { +{ const k1 = Symbol('k1'); const k2 = Symbol('k2'); const ch = channel('test-suppression-two-keys'); - const h1 = makeHandler(); - const h2 = makeHandler(); - ch.subscribe(h1.handler, { suppressedBy: k1 }); - ch.subscribe(h2.handler, { suppressedBy: k2 }); + let h1Calls = 0; + let h2Calls = 0; + const h1 = common.mustCall(() => { h1Calls++; }, 1); + const h2 = common.mustCall(() => { h2Calls++; }, 1); + ch.subscribe(h1, { subscriberId: k1 }); + ch.subscribe(h2, { subscriberId: k2 }); suppressed(k1, () => { ch.publish({}); }); - assert.strictEqual(h1.called(), false); - assert.strictEqual(h2.called(), true); - - h1.reset(); h2.reset(); + assert.strictEqual(h1Calls, 0); + assert.strictEqual(h2Calls, 1); suppressed(k2, () => { ch.publish({}); }); - assert.strictEqual(h1.called(), true); - assert.strictEqual(h2.called(), false); + assert.strictEqual(h1Calls, 1); + assert.strictEqual(h2Calls, 1); - ch.unsubscribe(h1.handler); - ch.unsubscribe(h2.handler); -})(); + ch.unsubscribe(h1); + ch.unsubscribe(h2); +} // Test 4: Nested suppressed() calls (same key, different keys) -(function testNestedSuppressed() { +{ const k1 = Symbol('nested1'); const k2 = Symbol('nested2'); const ch = channel('test-suppression-nested'); - const h1 = makeHandler(); - const h2 = makeHandler(); - ch.subscribe(h1.handler, { suppressedBy: k1 }); - ch.subscribe(h2.handler, { suppressedBy: k2 }); + const h1 = common.mustNotCall(); + let h2Calls = 0; + const h2 = common.mustCall(() => { h2Calls++; }, 2); + ch.subscribe(h1, { subscriberId: k1 }); + ch.subscribe(h2, { subscriberId: k2 }); suppressed(k1, () => { // inside k1, h1 skipped, h2 runs ch.publish({}); - assert.strictEqual(h1.called(), false); - assert.strictEqual(h2.called(), true); - h2.reset(); + assert.strictEqual(h2Calls, 1); suppressed(k2, () => { // inside both, both skipped ch.publish({}); - assert.strictEqual(h1.called(), false); - assert.strictEqual(h2.called(), false); + assert.strictEqual(h2Calls, 1); }); // back to only k1 ch.publish({}); - assert.strictEqual(h1.called(), false); - assert.strictEqual(h2.called(), true); + assert.strictEqual(h2Calls, 2); }); - ch.unsubscribe(h1.handler); - ch.unsubscribe(h2.handler); -})(); + ch.unsubscribe(h1); + ch.unsubscribe(h2); +} -// Test 5: suppressed() across a Promise boundary (async/await) -(async function testSuppressedAcrossPromise() { +// Test 5: suppressed() across a Promise boundary +{ const key = Symbol('promise'); const ch = channel('test-suppression-promise'); - const h = makeHandler(); - ch.subscribe(h.handler, { suppressedBy: key }); + const handler = common.mustNotCall(); + ch.subscribe(handler, { subscriberId: key }); - await suppressed(key, async () => { + suppressed(key, async () => { await Promise.resolve(); ch.publish({}); - }); - - assert.strictEqual(h.called(), false); - ch.unsubscribe(h.handler); -})(); + }).then(common.mustCall(() => { + ch.unsubscribe(handler); + })); +} // Test 6: suppressed() across setImmediate and queueMicrotask -(async function testSuppressedAcrossTimers() { +{ const key = Symbol('timers'); const ch = channel('test-suppression-timers'); - const h = makeHandler(); - ch.subscribe(h.handler, { suppressedBy: key }); + const handler = common.mustNotCall(); + ch.subscribe(handler, { subscriberId: key }); - await suppressed(key, async () => { + suppressed(key, async () => { await new Promise((resolve) => { setImmediate(() => { ch.publish({}); - assert.strictEqual(h.called(), false); - h.reset(); queueMicrotask(() => { ch.publish({}); - assert.strictEqual(h.called(), false); - - ch.unsubscribe(h.handler); resolve(); }); }); }); - }); -})(); + }).then(common.mustCall(() => { + ch.unsubscribe(handler); + })); +} -// Test 7: unsubscribe() works correctly after using suppressedBy -(function testUnsubscribeCleansUp() { +// Test 7: unsubscribe() works correctly after using subscriberId +{ const key = Symbol('unsub'); const ch = channel('test-suppression-unsubscribe'); - const h = makeHandler(); - ch.subscribe(h.handler, { suppressedBy: key }); - ch.unsubscribe(h.handler); + const handler = common.mustNotCall(); + ch.subscribe(handler, { subscriberId: key }); + ch.unsubscribe(handler); // Should not throw and should not be called suppressed(key, () => { ch.publish({}); }); - assert.strictEqual(h.called(), false); -})(); +} -// Test 8: bindStore with suppressedBy is skipped inside suppressed() -(function testBindStoreSuppression() { +// Test 8: bindStore with subscriberId is skipped inside suppressed() +{ const key = Symbol('store'); const ch = channel('test-suppression-store'); const als = new AsyncLocalStorage(); - let transformCalls = 0; const handler = common.mustCall(() => { assert.strictEqual(als.getStore(), undefined); }); ch.subscribe(handler); - ch.bindStore(als, (d) => { - transformCalls++; - return { foo: d }; - }, { suppressedBy: key }); + ch.bindStore(als, common.mustNotCall(), { subscriberId: key }); suppressed(key, () => { ch.publish({}); }); - assert.strictEqual(transformCalls, 0); ch.unsubscribe(handler); ch.unbindStore(als); -})(); +} -// Test 9: Wrong type for suppressedBy throws ERR_INVALID_ARG_TYPE -(function testWrongTypeThrows() { +// Test 9: Wrong type for subscriberId throws ERR_INVALID_ARG_TYPE +{ const ch = channel('test-suppression-wrong-type'); const bad = 'not-allowed'; - assert.throws(() => ch.subscribe(() => {}, { suppressedBy: bad }), { + assert.throws(() => ch.subscribe(() => {}, { subscriberId: bad }), { name: 'TypeError' }); const als = new AsyncLocalStorage(); - assert.throws(() => ch.bindStore(als, (d) => d, { suppressedBy: bad }), { + assert.throws(() => ch.bindStore(als, (d) => d, { subscriberId: bad }), { name: 'TypeError' }); -})(); +} // Test 10: suppressed() return value passes through fn's return value -(function testSuppressedReturnValueAndContext() { +{ const key = Symbol('return'); const receiver = { value: 41 }; const result = suppressed(key, function(a, b) { @@ -224,32 +195,6 @@ function makeHandler() { return this.value + 1; }, receiver, 'a', 'b'); assert.strictEqual(result, 42); -})(); - -// Test 11: null suppressedBy behaves like no suppression opt-in -(function testNullSuppressedByIsIgnored() { - const key = Symbol('null-suppressed-by'); - const ch = channel('test-suppression-null-suppressed-by'); - const h = makeHandler(); - - ch.subscribe(h.handler, { suppressedBy: null }); - - suppressed(key, () => { - ch.publish({}); - }); - - assert.strictEqual(h.called(), true); - ch.unsubscribe(h.handler); -})(); - -// Test 12: suppressed() rejects null/undefined keys consistently -(function testKeyValidation() { - assert.throws(() => suppressed(null, () => {}), { - name: 'TypeError' - }); - assert.throws(() => suppressed(undefined, () => {}), { - name: 'TypeError' - }); -})(); +} console.log('ok - diagnostics_channel suppression tests loaded'); From 8e6d7cb8d55f902474f4334968bfaf543e125356 Mon Sep 17 00:00:00 2001 From: Divyanshu Sharma Date: Sat, 30 May 2026 14:11:02 +0530 Subject: [PATCH 6/9] test: simplify diagnostics_channel suppression handler Signed-off-by: Divyanshu Sharma --- test/parallel/test-diagnostics-channel-suppression.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/parallel/test-diagnostics-channel-suppression.js b/test/parallel/test-diagnostics-channel-suppression.js index 0f9e4cae3c6a89..cb396c011bed5e 100644 --- a/test/parallel/test-diagnostics-channel-suppression.js +++ b/test/parallel/test-diagnostics-channel-suppression.js @@ -24,7 +24,7 @@ const { AsyncLocalStorage } = require('async_hooks'); const key = Symbol('tracer2'); const ch = channel('test-suppression-nonopted'); const optedHandler = common.mustNotCall(); - const regularHandler = common.mustCall(() => {}, 1); + const regularHandler = common.mustCall(); ch.subscribe(optedHandler, { subscriberId: key }); ch.subscribe(regularHandler); // no suppression From 07e7e9768edc835009b31b4df5175598f4a70fd3 Mon Sep 17 00:00:00 2001 From: Divyanshu Sharma Date: Sat, 30 May 2026 14:54:40 +0530 Subject: [PATCH 7/9] test: finalize diagnostics_channel suppression test Signed-off-by: Divyanshu Sharma --- .../test-diagnostics-channel-suppression.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/test/parallel/test-diagnostics-channel-suppression.js b/test/parallel/test-diagnostics-channel-suppression.js index cb396c011bed5e..ed75dd864d6496 100644 --- a/test/parallel/test-diagnostics-channel-suppression.js +++ b/test/parallel/test-diagnostics-channel-suppression.js @@ -77,21 +77,21 @@ const { AsyncLocalStorage } = require('async_hooks'); ch.subscribe(h1, { subscriberId: k1 }); ch.subscribe(h2, { subscriberId: k2 }); - suppressed(k1, () => { - // inside k1, h1 skipped, h2 runs + suppressed(k1, common.mustSucceed(() => { + // Inside k1, h1 skipped, h2 runs ch.publish({}); assert.strictEqual(h2Calls, 1); - suppressed(k2, () => { - // inside both, both skipped + suppressed(k2, common.mustSucceed(() => { + // Inside both, both skipped ch.publish({}); assert.strictEqual(h2Calls, 1); - }); + })); - // back to only k1 + // Back to only k1 ch.publish({}); assert.strictEqual(h2Calls, 2); - }); + })); ch.unsubscribe(h1); ch.unsubscribe(h2); @@ -188,12 +188,12 @@ const { AsyncLocalStorage } = require('async_hooks'); { const key = Symbol('return'); const receiver = { value: 41 }; - const result = suppressed(key, function(a, b) { + const result = suppressed(key, common.mustSucceed(function(a, b) { assert.strictEqual(this, receiver); assert.strictEqual(a, 'a'); assert.strictEqual(b, 'b'); return this.value + 1; - }, receiver, 'a', 'b'); + }), receiver, 'a', 'b'); assert.strictEqual(result, 42); } From 94687951f36707b335dd9e94b85f2cfa0bfc09d6 Mon Sep 17 00:00:00 2001 From: Divyanshu Sharma Date: Sat, 30 May 2026 15:08:35 +0530 Subject: [PATCH 8/9] diag(suppression-als): use ALS.run bound fn for suppression context; update tests to use common.mustCall --- lib/diagnostics_channel.js | 2 +- test/parallel/test-diagnostics-channel-suppression.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/diagnostics_channel.js b/lib/diagnostics_channel.js index ccabaade84987d..cfcbb2ea1f326b 100644 --- a/lib/diagnostics_channel.js +++ b/lib/diagnostics_channel.js @@ -47,7 +47,7 @@ function getSuppressionsStorage() { } function withSuppressionsContext(set, fn, thisArg, args) { - return getSuppressionsStorage().run(set, () => ReflectApply(fn, thisArg, args)); + return getSuppressionsStorage().run(set, fn.bind(thisArg), ...args); } // Can't delete when weakref count reaches 0 as it could increment again. // Only GC can be used as a valid time to clean up the channels map. diff --git a/test/parallel/test-diagnostics-channel-suppression.js b/test/parallel/test-diagnostics-channel-suppression.js index ed75dd864d6496..f826db8ded524e 100644 --- a/test/parallel/test-diagnostics-channel-suppression.js +++ b/test/parallel/test-diagnostics-channel-suppression.js @@ -77,12 +77,12 @@ const { AsyncLocalStorage } = require('async_hooks'); ch.subscribe(h1, { subscriberId: k1 }); ch.subscribe(h2, { subscriberId: k2 }); - suppressed(k1, common.mustSucceed(() => { + suppressed(k1, common.mustCall(() => { // Inside k1, h1 skipped, h2 runs ch.publish({}); assert.strictEqual(h2Calls, 1); - suppressed(k2, common.mustSucceed(() => { + suppressed(k2, common.mustCall(() => { // Inside both, both skipped ch.publish({}); assert.strictEqual(h2Calls, 1); @@ -188,7 +188,7 @@ const { AsyncLocalStorage } = require('async_hooks'); { const key = Symbol('return'); const receiver = { value: 41 }; - const result = suppressed(key, common.mustSucceed(function(a, b) { + const result = suppressed(key, common.mustCall(function(a, b) { assert.strictEqual(this, receiver); assert.strictEqual(a, 'a'); assert.strictEqual(b, 'b'); From b8194afe6a730666b64f07e046662c5d00a93da4 Mon Sep 17 00:00:00 2001 From: Divyanshu Sharma Date: Sat, 30 May 2026 15:38:54 +0530 Subject: [PATCH 9/9] test: wrap suppressed callbacks with common.mustCall Signed-off-by: Divyanshu Sharma --- .../test-diagnostics-channel-suppression.js | 46 ++++++++++--------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/test/parallel/test-diagnostics-channel-suppression.js b/test/parallel/test-diagnostics-channel-suppression.js index f826db8ded524e..cd3523848a8127 100644 --- a/test/parallel/test-diagnostics-channel-suppression.js +++ b/test/parallel/test-diagnostics-channel-suppression.js @@ -12,9 +12,9 @@ const { AsyncLocalStorage } = require('async_hooks'); const handler = common.mustNotCall(); ch.subscribe(handler, { subscriberId: key }); - suppressed(key, () => { + suppressed(key, common.mustCall(() => { ch.publish({}); - }); + })); ch.unsubscribe(handler); } @@ -28,9 +28,9 @@ const { AsyncLocalStorage } = require('async_hooks'); ch.subscribe(optedHandler, { subscriberId: key }); ch.subscribe(regularHandler); // no suppression - suppressed(key, () => { + suppressed(key, common.mustCall(() => { ch.publish({}); - }); + })); ch.unsubscribe(optedHandler); ch.unsubscribe(regularHandler); @@ -48,16 +48,16 @@ const { AsyncLocalStorage } = require('async_hooks'); ch.subscribe(h1, { subscriberId: k1 }); ch.subscribe(h2, { subscriberId: k2 }); - suppressed(k1, () => { + suppressed(k1, common.mustCall(() => { ch.publish({}); - }); + })); assert.strictEqual(h1Calls, 0); assert.strictEqual(h2Calls, 1); - suppressed(k2, () => { + suppressed(k2, common.mustCall(() => { ch.publish({}); - }); + })); assert.strictEqual(h1Calls, 1); assert.strictEqual(h2Calls, 1); @@ -103,12 +103,14 @@ const { AsyncLocalStorage } = require('async_hooks'); const ch = channel('test-suppression-promise'); const handler = common.mustNotCall(); ch.subscribe(handler, { subscriberId: key }); + const done = common.mustCall(); - suppressed(key, async () => { + suppressed(key, common.mustCall(async () => { await Promise.resolve(); ch.publish({}); - }).then(common.mustCall(() => { + })).then(common.mustCall(() => { ch.unsubscribe(handler); + done(); })); } @@ -118,20 +120,22 @@ const { AsyncLocalStorage } = require('async_hooks'); const ch = channel('test-suppression-timers'); const handler = common.mustNotCall(); ch.subscribe(handler, { subscriberId: key }); + const done = common.mustCall(); - suppressed(key, async () => { + suppressed(key, common.mustCall(async () => { await new Promise((resolve) => { - setImmediate(() => { + setImmediate(common.mustCall(() => { ch.publish({}); - queueMicrotask(() => { + queueMicrotask(common.mustCall(() => { ch.publish({}); resolve(); - }); - }); + })); + })); }); - }).then(common.mustCall(() => { + })).then(common.mustCall(() => { ch.unsubscribe(handler); + done(); })); } @@ -144,9 +148,9 @@ const { AsyncLocalStorage } = require('async_hooks'); ch.unsubscribe(handler); // Should not throw and should not be called - suppressed(key, () => { + suppressed(key, common.mustCall(() => { ch.publish({}); - }); + })); } @@ -163,9 +167,9 @@ const { AsyncLocalStorage } = require('async_hooks'); ch.subscribe(handler); ch.bindStore(als, common.mustNotCall(), { subscriberId: key }); - suppressed(key, () => { + suppressed(key, common.mustCall(() => { ch.publish({}); - }); + })); ch.unsubscribe(handler); ch.unbindStore(als); @@ -196,5 +200,3 @@ const { AsyncLocalStorage } = require('async_hooks'); }), receiver, 'a', 'b'); assert.strictEqual(result, 42); } - -console.log('ok - diagnostics_channel suppression tests loaded');