Skip to content

Worker: implement static Worker.data (Bun's workerData alias) - #35572

Open
robobun wants to merge 4 commits into
mainfrom
claude/farm/741bc245/worker-data-static
Open

Worker: implement static Worker.data (Bun's workerData alias)#35572
robobun wants to merge 4 commits into
mainfrom
claude/farm/741bc245/worker-data-static

Conversation

@robobun

@robobun robobun commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator

Fixes #9330.

Repro

// main.js
new Worker(new URL("worker.js", import.meta.url).href, { data: "hello" });
// worker.js
console.log(Worker.data); // prints: undefined  (expected: "hello")

Cause

The TypeScript declaration for Worker.data ("the cloned value of the data property passed to new Worker()", Bun's equivalent of workerData) has existed since #4052 but the runtime never exposed a data property on the Worker constructor. The data option was already being read in JSWorkerDOMConstructor::construct and routed to require("node:worker_threads").workerData; only the Worker.data surface was missing.

Fix

  • ZigGlobalObject gains a m_nodeWorkerData write barrier alongside m_nodeWorkerEnvironmentData.
  • createNodeWorkerThreadsBinding now caches the deserialized workerData on the global and reads it back on re-entry, so both access orders (Worker.data first vs require("node:worker_threads") first) resolve to the same JS object. A setWorkerData host function (binding index 11) lets src/js/node/worker_threads.ts push the unwrapped/unpackJSTransferables-processed value back into the cache after it strips the internal stdio/messaging transport wrapper, so Worker.data never exposes those ports.
  • JSWorkerDOMConstructor::initializeProperties installs a DontEnum CustomAccessor getter for data that triggers the one-time deserialization if node:worker_threads has not been loaded yet and then returns the cached value.
  • Bun.WorkerOptions in packages/bun-types/bun.d.ts now declares data, workerData, and transferList (all three are already read by the native option parser), and docs/runtime/workers.mdx gains a short Worker.data section.

Why this is correct

Worker.data is a Bun-specific alias for workerData, so it must be the same value (including object identity) that node:worker_threads sees. Routing through createNodeWorkerThreadsBinding and caching the result on the global guarantees that without deserializing twice or changing workerData's behaviour. The write-back from worker_threads.ts keeps the cache aligned with the JS-side unwrapping that node-kind workers perform. On the main thread workerData is null, and inside a worker with no data option it is undefined; Worker.data mirrors both, matching Node's workerData semantics.

Verification

USE_SYSTEM_BUN=1 bun test test/js/web/workers/worker.test.ts -t "Worker.data"   # 4 fail
bun bd test           test/js/web/workers/worker.test.ts -t "Worker.data"       # 4 pass
bun bd test           test/js/web/workers/worker.test.ts                        # 29 pass
bun bd test           test/js/node/worker_threads/worker_threads.test.ts        # 91 pass

The TypeScript declaration for Worker.data ('the cloned value of the
data property passed to new Worker()') has existed since #4052 but
the runtime never exposed it; reading Worker.data returned undefined.

The data option itself was already being read and routed to
node:worker_threads' workerData. This adds the missing surface: a
DontEnum custom-value getter on the Worker constructor that returns
the same deserialized value workerData sees, triggering the
deserialization once if node:worker_threads hasn't been loaded yet.
createNodeWorkerThreadsBinding now caches workerData on the global
and reads it back on re-entry so both access orders resolve to the
same JS object.

Also declares the data option on Bun.WorkerOptions and documents it
in runtime/workers.mdx.

Fixes #9330
@robobun
robobun requested a review from alii as a code owner July 25, 2026 07:15
@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b059f852-4d2a-4107-86a9-e4e32e37d1b5

📥 Commits

Reviewing files that changed from the base of the PR and between df6c7ee and e5343d7.

📒 Files selected for processing (8)
  • docs/runtime/workers.mdx
  • packages/bun-types/bun.d.ts
  • src/js/node/worker_threads.ts
  • src/jsc/bindings/ZigGlobalObject.cpp
  • src/jsc/bindings/ZigGlobalObject.h
  • src/jsc/bindings/webcore/JSWorker.cpp
  • src/jsc/bindings/webcore/Worker.cpp
  • test/js/web/workers/worker.test.ts

Walkthrough

Adds Worker data options and documentation, stores worker data in the global object, synchronizes Node worker data during bootstrap, exposes a read-only Worker.data getter, and adds cross-worker behavior tests.

Changes

Worker.data

Layer / File(s) Summary
Worker.data contract
packages/bun-types/bun.d.ts, docs/runtime/workers.mdx
Adds data, workerData, and transferList options and documents cloning and transfer behavior.
Native worker-data cache and binding
src/jsc/bindings/ZigGlobalObject.*, src/jsc/bindings/webcore/Worker.cpp, src/js/node/worker_threads.ts
Stores worker data in a GC-tracked global cache, exposes setWorkerData, and synchronizes unwrapped worker data during bootstrap.
Worker.data exposure and validation
src/jsc/bindings/webcore/JSWorker.cpp, test/js/web/workers/worker.test.ts
Adds the read-only getter and tests cloning, import order, unwrapping, and missing-data behavior.

Possibly related PRs

  • oven-sh/bun#34140: Both changes update Node worker-threads binding setup in createNodeWorkerThreadsBinding.

Suggested reviewers: alii, cirospaciari

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding static Worker.data as Bun's workerData alias.
Description check ✅ Passed The description covers the bug, root cause, fix, and verification, even if it doesn't follow the template headings exactly.
Linked Issues check ✅ Passed The changes implement Worker.data, type declarations, caching, and tests that match #9330's expected behavior.
Out of Scope Changes check ✅ Passed The docs, typings, runtime wiring, and tests all support the Worker.data fix and appear in scope.

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

@robobun

robobun commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 9:27 AM PT - Jul 25th, 2026

@robobun, your commit e5343d7 has 1 failures in Build #80917 (All Failures):

  • ❌ CPU instruction violation on Windows x64 — 1 check(s) failed
  • The baseline build contains instructions not available on Nehalem (SSE4.2, no AVX/AVX2/AVX512).

    • Static instruction scan

    Static scan violations

    strcspn  [RTM]  (1 insns)
    

    If these are runtime-dispatched behind a CPUID gate: add each symbol to scripts/verify-baseline-static/allowlist-x64-windows.txt with a comment pointing at the gate.

    If there's no gate: this is a real bug — a -march leaked into a subbuild.


🧪   To try this PR locally:

bunx bun-pr 35572

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

bun-35572 --bun

Comment thread src/jsc/bindings/webcore/Worker.cpp
Comment thread test/js/web/workers/worker.test.ts
Inside a node:worker_threads Worker, workerData is wrapped on the
parent (stdio/messaging ports ride along) and unwrapped JS-side in
worker_threads.ts. createNodeWorkerThreadsBinding was caching the
wrapper, so Worker.data leaked the internal MessagePorts instead of
the user's value.

Expose a _setWorkerData binding and call it from worker_threads.ts
after unpacking/unwrapping so m_nodeWorkerData stays identical to the
exported workerData. The getter is now CustomAccessor (not CustomValue)
so that write-back is observable.

Also declare workerData/transferList on Bun.WorkerOptions since the
native option parser already reads them and the new data JSDoc
references transferList. Tests now assert object identity and cover
the node:worker_threads Worker path.
Comment thread src/js/node/worker_threads.ts Outdated
Comment thread src/jsc/bindings/ZigGlobalObject.h Outdated
Comment thread src/jsc/bindings/webcore/JSWorker.cpp Outdated
Comment thread src/jsc/bindings/webcore/Worker.cpp Outdated
Comment thread src/jsc/bindings/webcore/Worker.cpp 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.

5f64571 addresses both earlier findings — _setWorkerData writes the unwrapped/unpackJSTransferables-processed value back into m_nodeWorkerData, and the new "is the unwrapped workerData inside a node:worker_threads Worker" test covers the wt.Worker path with an object-identity + Object.keys assertion (no @@bun… leak). Tests 1 and 2 now assert same: true on object payloads. Deferring to a human for the final sign-off given this adds new user-facing API surface on the Worker constructor and a new WriteBarrier on ZigGlobalObject; the comment-cop flags are also still open.

Extended reasoning...

Overview

Follow-up commit 5f64571d on top of f32c5825 implements static Worker.data (Bun's alias for workerData). Touches: ZigGlobalObject.{h,cpp} (new m_nodeWorkerData WriteBarrier<Unknown> in the FOR_EACH_GLOBALOBJECT_GC_MEMBER list + accessor), Worker.cpp (createNodeWorkerThreadsBinding now caches/re-reads the deserialized value and exports a setWorkerData host function at index 11), JSWorker.cpp (a DontEnum|ReadOnly|CustomAccessor data getter on the constructor that lazy-runs the binding), src/js/node/worker_threads.ts (destructures index 11 and calls _setWorkerData(workerData) when the module-local value diverges from the raw deserialized one), plus docs, bun.d.ts, and four new spawned tests.

Prior findings — resolved

My earlier 🔴 (wt.Worker leaks the internal @@bunWorkerThreadsMessaging/@@bunWorkerThreadsStdio transport wrapper via Worker.data) is fixed by option (b) from that comment: jsFunctionSetWorkerData lets the JS module push the final unwrapped value back into the native cache after workerData = workerData.data. Traced the guard if (workerData !== _workerData): unpackJSTransferables mutates plain objects in place (returns the same reference), so for a global-Worker payload with no JSTransferable markers the native cache already holds the right object and no write-back is needed; for wt.Worker the .data unwrap reassigns and triggers the write-back; if the top-level value is itself a marker, unpackJSTransferables returns a new instance and the write-back also fires. The 🟡 test-strength nit is fixed: test 1 now asserts same: before === workerData && Worker.data === workerData on an object, test 2 uses { v: 1 }, and the new third test asserts same: true and keys: ["greeting"] inside a require('node:worker_threads').Worker.

Security risks

None identified. _setWorkerData is only reachable via the $cpp(...) builtin intrinsic (not user-exposed). Worker.data reads per-VM state on ZigGlobalObject, so no cross-worker leakage.

Level of scrutiny

Moderate–high: new user-facing API on a global constructor, a new GC-visited field on ZigGlobalObject, and a re-entrancy contract between the Worker.data getter and the node:worker_threads binding factory. The WriteBarrier<Unknown> is declared inside FOR_EACH_GLOBALOBJECT_GC_MEMBER, so it is visited automatically. On re-entry, nodeWorkerData() returns jsNull()/jsUndefined() (both non-empty JSValues), so the if (JSValue cached = ...) guard correctly preserves them.

Other factors

The comment-cop action has five open flags on multi-line comments added in this diff. Those need to be addressed or dismissed before merge. Given the API-surface addition and the open bot comments, a human should sign off.

@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.

Both earlier findings are addressed — the _setWorkerData binding now syncs the unwrapped/unpackJSTransferables-processed value back into m_nodeWorkerData, the new wt.Worker test covers that path, and tests 1/2 now assert object identity with object payloads. I didn't find anything further, but this adds user-facing API surface (Worker.data, WorkerOptions.data/workerData/transferList) and a new re-entrant init path through createNodeWorkerThreadsBinding, so it's worth a maintainer sign-off.

What was reviewed:

  • Re-entry ordering (Worker.data-first vs require-first) and the moved-from workerDataAndEnvironmentData / dataMessagePorts on the second call — both guarded.
  • GC rooting: m_nodeWorkerData is a WriteBarrier<Unknown> inside FOR_EACH_GLOBALOBJECT_GC_MEMBER, so it's visited.
  • if (JSValue cached = ...) with a cached jsUndefined()JSValue::operator bool is non-empty-check, so the no-data-worker case reuses the cache correctly.
  • unpackJSTransferables mutates containers in place, so workerData === _workerData holds for the global-Worker path and _setWorkerData isn't spuriously called.
Extended reasoning...

Overview

Implements the long-declared-but-never-wired Worker.data static getter (Bun's alias for workerData). Adds m_nodeWorkerData (WriteBarrier<Unknown>) on ZigGlobalObject, a CustomAccessor getter on the Worker constructor that forces createNodeWorkerThreadsBinding if needed, a re-entry cache inside that binding, and a setWorkerData host function (index 11 of the binding array) so src/js/node/worker_threads.ts can push the unwrapped/unpackJSTransferables-processed value back into the native cache. Also adds data/workerData/transferList to Bun.WorkerOptions, a docs section, and four spawned-subprocess tests.

Findings addressed since the last review

  • 🔴 (wt.Worker leaked the transport wrapper via Worker.data): fixed by _setWorkerData + if (workerData !== _workerData) _setWorkerData(workerData) after the unwrap block, plus a dedicated test ("is the unwrapped workerData inside a node:worker_threads Worker") that asserts same: true and keys: ['greeting'].
  • 🟡 (identity not asserted on object payloads): tests 1 and 2 now both use object payloads and assert same: Worker.data === workerData (test 1 also checks before === workerData across the re-entry boundary).
  • comment-cop's multi-line-comment flags were trimmed in dc5bbf3.

Security risks

None identified. The wrapper-leak concern (internal control/stdio MessagePorts surfacing via Worker.data) from the first review is closed by the _setWorkerData sync — the new wt.Worker test asserts Object.keys(Worker.data) contains only the user's key.

Level of scrutiny

Medium-high. This is a small feature but it threads through JSC bindings (custom accessor on a DOM constructor, a new WriteBarrier on the global object), makes createNodeWorkerThreadsBinding re-entrant, and adds public API/type surface. REVIEW.md calls out API-design changes and "no new fields on ZigGlobalObject" as things maintainers care about — this one is a WriteBarrier alongside the existing m_nodeWorkerEnvironmentData, which seems in-pattern, but that's a maintainer call.

Other factors

  • Four new test.concurrent subprocess tests cover both access orderings, the wt.Worker wrapper path, and the null/undefined defaults; all wire onerror/error to a rejecting exit and drain stdout/stderr concurrently.
  • The entanglePorts(WTF::move(options.dataMessagePorts)) on the second binding call operates on an already-moved (empty) vector — harmless no-op.
  • if (JSValue cached = globalObject->nodeWorkerData()) correctly treats a cached jsUndefined() as present (JSValue's bool is emptiness, not JS-truthiness), so the no-data case doesn't fall through to jsNull().

Deferring to a maintainer for the API-surface/ZigGlobalObject-field sign-off rather than approving.

@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.

The two issues from my earlier review are addressed: _setWorkerData (binding index 11) now writes the unwrapped/unpackJSTransferables-processed value back into m_nodeWorkerData, and a new test covers the node:worker_threads Worker path (asserting Worker.data === wt.workerData and keys: ["greeting"] — no transport-wrapper leak). Test 1 now asserts object identity via same: before === workerData && Worker.data === workerData, and test 2 uses an object payload.

I didn't find further bugs, but this adds user-facing API surface (a DontEnum custom getter on the Worker constructor), a new WriteBarrier<Unknown> on ZigGlobalObject, and a JS→native write-back path — worth a maintainer glance at the shape before landing.

What was reviewed

  • Re-entry ordering: Worker.data-first calls createNodeWorkerThreadsBinding directly, then the module's $cpp call re-enters — the nodeWorkerData()/nodeWorkerEnvironmentData() cache reads make the second call idempotent (the moved-from serialized is guarded).
  • if (workerData !== _workerData) gate: fires exactly when the wt.Worker unwrap ran or _workerData was a top-level JSTransferable marker; nested markers mutate in place so the cached container reference stays coherent without a write-back.
  • GC: m_nodeWorkerData is in FOR_EACH_GLOBALOBJECT_GC_MEMBER alongside m_nodeWorkerEnvironmentData, so it's visited; setNodeWorkerData uses .set(vm(), this, data).
  • Empty/undefined/null: an unset WriteBarrier<Unknown> decodes to an empty JSValue (falsy in the if (JSValue cached = …) check), while jsNull()/jsUndefined() are truthy JSValues, so the cache-hit test distinguishes "never set" from "set to null/undefined".
Extended reasoning...

Overview

Implements the long-declared-but-never-implemented Worker.data static getter (Bun's alias for require('node:worker_threads').workerData, per #4052 / #9330). Touches: ZigGlobalObject.{h,cpp} (new m_nodeWorkerData WriteBarrier + accessor pair), Worker.cpp (createNodeWorkerThreadsBinding now caches/reuses workerData on the global and exports a setWorkerData host function at binding index 11), JSWorker.cpp (jsWorkerConstructor_data custom getter installed on the constructor in initializeProperties), src/js/node/worker_threads.ts (writes the unwrapped/unpackJSTransferables-processed value back via _setWorkerData), plus type declarations (data/workerData/transferList on Bun.WorkerOptions), docs, and four spawned-subprocess tests in worker.test.ts.

Follow-up on prior review

My earlier 🔴 finding (wt.Worker leaks the internal { '@@bunWorkerThreadsMessaging', '@@bunWorkerThreadsStdio', data } transport wrapper via Worker.data) is fixed by the _setWorkerData write-back after the JS-side unwrap, and covered by the new third test which asserts identity with wt.workerData and Object.keys(Worker.data) == ['greeting']. My 🟡 finding (identity not asserted on an object payload) is fixed: test 1 now sends same: before === workerData && Worker.data === workerData and asserts same: true, and test 2's payload is now { v: 1 }. The comment-cop bot's multi-line-comment complaints were trimmed in dc5bbf3. All prior inline threads are marked resolved.

Security risks

None. The value cached in m_nodeWorkerData is the same object already exposed as workerData; the write-back setWorkerData host function is only reachable through the internal $cpp binding array (not on any user-visible object). The wrapper leak from the earlier revision — which exposed the internal stdio/messaging MessagePorts — is closed.

Level of scrutiny

Medium. The API surface itself was already committed to in the type declarations, so there's no new design to bikeshed, but the implementation adds a WriteBarrier field to ZigGlobalObject, a custom getter on a global constructor, and re-entrancy handling in createNodeWorkerThreadsBinding. I traced the four ordering combinations (main/worker × Worker.data-first / require-first) plus the wt.Worker preload path and they all resolve to the same object; the empty-vs-null/undefined WriteBarrier semantics check out; the field is visited via FOR_EACH_GLOBALOBJECT_GC_MEMBER.

Other factors

Test coverage is good: four test.concurrent spawned subprocesses cover both access orders, the wt.Worker unwrap path, and the no-data null/undefined defaults, all asserting reference identity inside the worker body before structured-clone erases it. The PR description reports the full worker.test.ts (29 pass) and worker_threads.test.ts (91 pass) suites green. Deferring rather than approving because a new constructor-level custom accessor + ZigGlobalObject field is the kind of thing a maintainer should sign off on even when the mechanics look right.

@robobun

robobun commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator Author

Diff is green on every lane that ran the worker tests (no worker.test.ts / worker_threads.test.ts failures across 161 passing jobs). Remaining CI red is unrelated:

  • verify-baseline on Windows x64 flags an RTM Xbegin in CRT strcspn, which also fails on unrelated branch build 80916; reported for main-break triage.
  • test-fastutf8stream-reopen.js, napi.test.ts, bun-upgrade.test.ts, no-orphans.test.ts, require-cache.test.ts, 20144.test.ts are all marked flaky and passed on retry.
  • 10 build-cpp jobs expired waiting for agents, cascading 22 dependent jobs.

All review threads resolved. Ready for a maintainer to look at the API surface (new Worker.data getter + m_nodeWorkerData WriteBarrier).

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.

Passing data to Worker does not work

2 participants