Skip to content

diagnostics_channel: add BoundedChannel/boundedChannel, withStoreScope; make TracingChannel props non-enumerable - #34604

Open
robobun wants to merge 4 commits into
mainfrom
farm/9a593e04/dc-bounded-channel-withstorescope
Open

diagnostics_channel: add BoundedChannel/boundedChannel, withStoreScope; make TracingChannel props non-enumerable#34604
robobun wants to merge 4 commits into
mainfrom
farm/9a593e04/dc-bounded-channel-withstorescope

Conversation

@robobun

@robobun robobun commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Closes four surface gaps in node:diagnostics_channel against Node.js v26:

API Before After (matches Node v26)
dc.boundedChannel undefined function
dc.BoundedChannel undefined function
Channel.prototype.withStoreScope undefined function
Object.keys(tracingChannel("x")) ["start","end","asyncStart","asyncEnd","error"] []

Repro

import dc from "node:diagnostics_channel";
let bad = 0;
const chk = (k, v, want) => { const ok = v === want; if (!ok) bad++; console.log(`${k} = ${v}  ${ok ? "ok" : `BUG (node: ${want})`}`); };

chk("typeof dc.boundedChannel", typeof dc.boundedChannel, "function");
chk("typeof dc.BoundedChannel", typeof dc.BoundedChannel, "function");
chk("typeof Channel.prototype.withStoreScope", typeof dc.channel("sf").withStoreScope, "function");
const tc = dc.tracingChannel("sf2");
chk("Object.keys(tracingChannel)", JSON.stringify(Object.keys(tc)), "[]");

const names = ["start","end","asyncStart","asyncEnd","error"].map((k) => String(tc[k]?.name)).join(",");
if (!names.startsWith("tracing:sf2:start")) process.exit(2);
process.exit(bad ? 1 : 0);

Before: exit 1 (all four checks fail). After: exit 0.

How

  • BoundedChannel / boundedChannel: new class bundling start/end Channels with subscribe, unsubscribe, hasSubscribers, withScope, and run. start/end are installed as non-enumerable own properties via Object.defineProperty, matching Node.
  • withStoreScope(data): added to both Channel.prototype (returns a no-op disposable) and ActiveChannel.prototype (returns a RunStoresScope that enters every bound AsyncLocalStorage via withScope, publishes data, and restores the stores on [Symbol.dispose]).
  • TracingChannel shape: the five event channels are now backed by two private BoundedChannel instances. start/end/asyncStart/asyncEnd are exposed via prototype getters and error is defined as a non-enumerable own property, so Object.keys/spread/for-in over a TracingChannel return nothing, as on Node. A hasSubscribers getter is added that delegates to the underlying channels.

traceSync/tracePromise/traceCallback continue to read the channels via the new accessors and call the existing runStores path, so their behaviour is unchanged and all existing test/js/node/test/parallel/test-diagnostics-channel-* tests continue to pass.

Tests

  • test/js/node/diagnostics_channel/diagnostics_channel.test.ts: added describe blocks covering withStoreScope, BoundedChannel, and the TracingChannel shape/accessors (13 new tests, all failing on the released binary).
  • test/js/node/test/parallel/test-diagnostics-channel-bounded-channel.js: imported unmodified from Node v26.3.0.

[review] gate passed · iteration 0 · 3 files touched

fails on main (without fix)
ASAN without fix: 18 failed, 3 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/node/diagnostics_channel/diagnostics_channel.test.ts
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (7da374254)

test/js/node/diagnostics_channel/diagnostics_channel.test.ts:
(pass) Channel > can have subscribers [44.89ms]
(pass) Channel > can have symbol as name [29.31ms]
(pass) Channel > does not throw when unsubscribed [20.57ms]
(pass) Channel > can publish and subscribe [28.53ms]
(pass) Channel > can publish and subscribe using object [22.32ms]
(todo) Channel > can handle subscriber errors
(todo) Channel > can use bind store
(pass) Channel > references are not leaked [1126.02ms]
340 | });
341 | 
342 | describe("Channel.prototype.withStoreScope", () => {
343 |   test("is a function on both inactive and active channels", () => {
344 |     const ch = channel("withStoreScope-shape");
345 |     expect(type
... (truncated)

release without fix: 3 skipped
bun test v1.4.0-canary.1 (2319742bf)

test/js/node/diagnostics_channel/diagnostics_channel.test.ts:
(pass) Channel > can have subscribers [3.08ms]
(pass) Channel > can have symbol as name [1.31ms]
(pass) Channel > does not throw when unsubscribed [0.28ms]
(pass) Channel > can publish and subscribe [0.29ms]
(pass) Channel > can publish and subscribe using object [0.18ms]
(todo) Channel > can handle subscriber errors
(todo) Channel > can use bind store
(pass) Channel > references are not leaked [6.41ms]
(pass) Channel.prototype.withStoreScope > is a function on both inactive and active channels [0.31ms]
(pass) Channel.prototype.withStoreScope > enters bound stores for the duration of the scope [1.79ms]
(pass) BoundedChannel > boundedChannel and BoundedChannel are exported [0.26ms]
(pass) BoundedChannel > creates start/end channels from a name [0.11ms]
(pass) BoundedChannel > start/end are non-enumerable own properties [0.78ms]
(pass) BoundedChannel > accepts explicit channel objects [0.14ms]
(pass) BoundedChannel > subscribe/unsubscribe wires start and end handlers [0.93ms]
(pass) BoundedChannel > run publishes start, invokes fn, then publishes end [0.47ms]
(pass) Bou
... (truncated)
passes on PR (with fix)
ASAN with fix: 3 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/node/diagnostics_channel/diagnostics_channel.test.ts
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (7da374254)

test/js/node/diagnostics_channel/diagnostics_channel.test.ts:
(pass) Channel > can have subscribers [48.89ms]
(pass) Channel > can have symbol as name [28.45ms]
(pass) Channel > does not throw when unsubscribed [20.24ms]
(pass) Channel > can publish and subscribe [27.53ms]
(pass) Channel > can publish and subscribe using object [22.02ms]
(todo) Channel > can handle subscriber errors
(todo) Channel > can use bind store
(pass) Channel > references are not leaked [571.54ms]
(pass) Channel.prototype.withStoreScope > is a function on both inactive and active channels [13.12ms]
(pass) Channel.prototype.withStoreScope > enters bound stores for the duration of the scope [918.00ms]
(pass) BoundedChannel
... (truncated)

release with fix: 3 skipped
$ bun scripts/build.ts --profile=release
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
[configured] bun-profile → bun (stripped) in 1843ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/20] gen JS modules (bundle-modules)
Preprocess modules (17208ms)
Bundle modules (202ms)
Postprocesss modules (1092ms)
Bundle Functions (3432ms)
Generate Code (215ms)

[22.18s] Bundled "src/js" for production
  2040 kb
  165 internal modules
  13 native modules
  90 internal functions across 19 files
[1/6] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: component rust-std is up to date

  nightly-2026-05-06-x86_64-unknown-linux-gnu unchanged - rustc 1.97.0-nightly (e95e73209 2026-05-05)

info: checking for self-update (current 
... (truncated)
diff hotspot
src/js/node/diagnostics_channel.ts                 | 252 ++++++++++++++++++---
 .../diagnostics_channel.test.ts                    | 222 +++++++++++++++++-
 .../test-diagnostics-channel-bounded-channel.js    | 105 +++++++++
 3 files changed, 548 insertions(+), 31 deletions(-)

gate history · 1 passed · 0 rejected · iteration 0

evidence per changed file
file                                                      reads  edits  tests
src/js/node/diagnostics_channel.ts                            3      8      0
…js/node/diagnostics_channel/diagnostics_channel.test.ts      2      4      0
…st/parallel/test-diagnostics-channel-bounded-channel.js      0      0      0

…Channel props non-enumerable

Close four gaps against Node.js v26:

- Export boundedChannel() and the BoundedChannel class, which bundles
  start/end Channels with subscribe/unsubscribe/withScope/run helpers.
- Add Channel.prototype.withStoreScope(data) on both the inactive and
  active prototypes. The active variant returns a disposable that
  enters every bound store via AsyncLocalStorage#withScope, publishes
  the data, and restores the stores on [Symbol.dispose].
- Back TracingChannel with two private BoundedChannel instances and
  expose start/end/asyncStart/asyncEnd via prototype accessors, with
  error defined as a non-enumerable own property. Object.keys(tc) now
  yields [] as it does on Node.js, so spread/for-in/serialisation of a
  TracingChannel agree across runtimes.
- Add TracingChannel.prototype.hasSubscribers delegating to the
  underlying windows and error channel.

traceSync/tracePromise/traceCallback continue to use the existing
runStores path via the new accessors, so the existing node parallel
tests are unchanged.

Imports Node's test-diagnostics-channel-bounded-channel.js unmodified.
@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

Diagnostics channel enhancements

Layer / File(s) Summary
Disposable store scopes
src/js/node/diagnostics_channel.ts, test/js/node/diagnostics_channel/diagnostics_channel.test.ts
Channels now provide disposable store scopes that activate bound stores during publication and restore them afterward.
Bounded channel primitives
src/js/node/diagnostics_channel.ts, test/js/node/diagnostics_channel/diagnostics_channel.test.ts, test/js/node/test/parallel/test-diagnostics-channel-bounded-channel.js
Adds BoundedChannel and boundedChannel with paired start/end channels, subscription management, scoped disposal, and run() sequencing.
Tracing channel integration
src/js/node/diagnostics_channel.ts, test/js/node/diagnostics_channel/diagnostics_channel.test.ts
Refactors TracingChannel around bounded channels and exports the new bounded-channel APIs.

Possibly related PRs

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely summarizes the main diagnostics_channel API and shape changes in the PR.
Description check ✅ Passed It includes the required change summary and verification details, with both template sections substantially covered.

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 5:05 AM PT - Jul 18th, 2026

@robobun, your commit 7da3742 has 2 failures in Build #75307 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 34604

That installs a local version of the PR into your bun-34604 executable, so you can run:

bun-34604 --bun

@github-actions

Copy link
Copy Markdown
Contributor

Found 1 issue this PR may fix:

  1. tracingChannel().hasSubscribers is undefined #27805 - PR adds a hasSubscribers getter to TracingChannel, directly fixing the reported tracingChannel().hasSubscribers is undefined

If this is helpful, copy the block below into the PR description to auto-close this issue on merge.

Fixes #27805

🤖 Generated with Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. node:diagnostics_channel: sync with Node 26 + subsystem channels #32628 - Implements the same BoundedChannel/boundedChannel exports, Channel.prototype.withStoreScope, TracingChannel non-enumerable properties refactoring, and TracingChannel.hasSubscribers getter as part of a broader Node 26 diagnostics_channel sync
  2. diagnostics_channel: add hasSubscribers getter to TracingChannel #33449 - Adds the same TracingChannel.hasSubscribers getter
  3. fix: add hasSubscribers property to TracingChannel #27881 - Adds the same TracingChannel.hasSubscribers getter

🤖 Generated with Claude Code

@robobun

robobun commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator Author

This overlaps with #32628, which already contains the BoundedChannel/boundedChannel exports, Channel.prototype.withStoreScope, and the TracingChannel prototype-accessor restructuring (along with a much broader Node 26 sync).

This PR is intentionally the narrow subset covering just those four surface gaps, in case a smaller change is easier to land independently. All existing test-diagnostics-channel-* parallel tests continue to pass and the imported test-diagnostics-channel-bounded-channel.js is unmodified from Node v26.3.0. Happy to close in favour of #32628 if that is preferred.

CI: build 75307 finished 284/286 green. The two red lanes are unrelated to this diff: test-worker-message-port-transfer-terminate.js (pre-existing JSC assertion on x64-asan) and node-net.test.ts (mimalloc page-count threshold on ubuntu x64-baseline; net does not load diagnostics_channel). Both are tracked separately. All diagnostics_channel tests passed on every lane.

Comment thread src/js/node/diagnostics_channel.ts Outdated
Comment thread src/js/node/diagnostics_channel.ts Outdated
Comment thread src/js/node/diagnostics_channel.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/js/node/diagnostics_channel.ts`:
- Around line 407-488: Update TracingChannel.traceSync, tracePromise, and
traceCallback to execute their traced work through BoundedChannel.withScope(),
using `#callWindow` for synchronous calls and `#continuationWindow` for promise
continuations and callbacks. Ensure asyncStart/asyncEnd execution is scoped
through the continuation window rather than published directly. Add an
AsyncLocalStorage regression test confirming stores bound to the continuation
window are active during promise continuation tracing.

In `@test/js/node/diagnostics_channel/diagnostics_channel.test.ts`:
- Around line 510-519: Update the test “hasSubscribers reflects any of the five
channels” to exercise each tracingChannel subscriber property: start, end,
asyncStart, asyncEnd, and error. Parameterize or iterate the subscription,
assertion, and unsubscribe steps so every channel independently changes
hasSubscribers to true and back to false, preserving the existing cleanup
behavior.
- Around line 445-475: Extend the diagnostics channel tests around
boundedChannel.run to cover a throwing callback with subscribers: bind or
establish a store value, make the callback throw, assert the same original error
propagates, verify the end event still publishes, and confirm the bound store is
restored afterward. Keep the existing successful run and subscriber-free
withScope tests unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1722352d-9a48-497f-a24b-7da383457659

📥 Commits

Reviewing files that changed from the base of the PR and between 511caaa and eeca4be.

📒 Files selected for processing (3)
  • src/js/node/diagnostics_channel.ts
  • test/js/node/diagnostics_channel/diagnostics_channel.test.ts
  • test/js/node/test/parallel/test-diagnostics-channel-bounded-channel.js

Comment thread src/js/node/diagnostics_channel.ts
Comment thread test/js/node/diagnostics_channel/diagnostics_channel.test.ts
Comment thread test/js/node/diagnostics_channel/diagnostics_channel.test.ts Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No bugs found in this pass — the earlier three inline findings were addressed in eeca4be and the two CodeRabbit test-coverage asks in 7da3742. Deferring to a maintainer because this adds new public API surface (BoundedChannel/boundedChannel/withStoreScope) and restructures TracingChannel's internals, and because it overlaps with #32628 / #33449 / #27881 — someone needs to pick which of those lands.

What was reviewed:

  • RunStoresScope construction/dispose ordering — the taken flag + stack.move() correctly cleans up on a mid-construction throw, and BoundedChannelScope[Symbol.dispose] guards the no-subscriber path.
  • TracingChannel refactor — traceSync/tracePromise/traceCallback still read start/end/etc. via the new getters and hit the same runStores/publish path as before, so existing behavior is preserved.
  • subscribe/unsubscribe rewrite — the {start: asyncStart, end: asyncEnd} remapping into #continuationWindow round-trips correctly; unsubscribe still returns false on any miss.
Extended reasoning...

Overview

This PR closes four Node.js v26 surface gaps in node:diagnostics_channel: it adds the BoundedChannel class and boundedChannel() factory, adds Channel.prototype.withStoreScope / ActiveChannel.prototype.withStoreScope (returning a disposable that enters bound ALS stores and publishes), and restructures TracingChannel so its five event channels are exposed via non-enumerable accessors backed by two private BoundedChannel instances plus a non-enumerable own error property. It also adds a hasSubscribers getter on TracingChannel. ~200 net new lines in src/js/node/diagnostics_channel.ts, 13 new tests in the module's test file, and one Node parallel test imported unmodified.

Security risks

None identified. This is a pure-JS observability/tracing module with no I/O, network, or filesystem surface. The tamper-resistance concern (capturing DisposableStack at load) was addressed in eeca4be.

Level of scrutiny

Medium-high — this is new user-facing API surface in a node: builtin, and the repo's landing-PRs guidance flags API design changes as needing maintainer sign-off. The TracingChannel restructuring is not purely additive: subscribe/unsubscribe are rewritten to route through the new BoundedChannel instances, and the five channel properties change from own data properties to prototype getters. The PR description asserts existing test-diagnostics-channel-* parallel tests still pass, which mitigates regression risk, but I cannot verify CI status from here (build #75307 was still running per the last robobun update).

Other factors

  • Duplicate PRs: github-actions flagged overlap with #32628 (a broader Node 26 sync containing all of this), #33449, and #27881 (both add TracingChannel.hasSubscribers). The author acknowledged this is an intentional narrow subset. A maintainer needs to decide whether to land this independently or fold it into #32628.
  • Prior review cycle: three findings from an earlier pass (frozen kEmptyObject default, uncaptured DisposableStack global, shared mutable noopDisposable singleton) were all fixed in eeca4be. Two CodeRabbit test-coverage requests were addressed in 7da3742. One CodeRabbit finding (routing tracePromise continuations through withScope) was declined as out-of-scope pre-existing behavior, which is a reasonable scoping call but is another thing a maintainer should ratify.
  • Test coverage: solid — new tests cover shape, subscribe/unsubscribe, run on both success and throw paths, ALS store enter/exit via withStoreScope, and all five hasSubscribers branches via test.each.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant