Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
7f82a21
node:worker_threads: add 5 upstream Node worker tests that already pass
cirospaciari Jul 16, 2026
f69da29
node:worker_threads: add 3 more upstream Node worker tests that alrea…
cirospaciari Jul 17, 2026
356f936
node:worker_threads: drop test-worker-dns-terminate, exempt fshandles…
cirospaciari Jul 17, 2026
f57c53b
node:worker_threads: exempt both fshandles tests from LeakSanitizer
cirospaciari Jul 17, 2026
2872f8f
node:worker_threads: drop the two fshandles tests instead of exemptin…
cirospaciari Jul 17, 2026
c8a6e90
worker_threads: publish node's 'worker_threads' diagnostics channel
cirospaciari Jul 17, 2026
10fe341
worker_threads: async_hooks WORKER resource, error-clone stack, share…
cirospaciari Jul 17, 2026
c308af4
BroadcastChannel: ref() should return the channel
cirospaciari Jul 17, 2026
f1384cd
worker_threads: fire 'online' before the entry point runs, as node does
cirospaciari Jul 17, 2026
5056973
Merge origin/main into ciro/worker-threads-node-tests
cirospaciari Jul 17, 2026
d7e1659
worker_threads: address review feedback and drop test-worker-memory
robobun Jul 17, 2026
c7d3e81
cli: support node's -pe alias
cirospaciari Jul 17, 2026
80aae50
worker_threads: profile worker threads under --cpu-prof
cirospaciari Jul 17, 2026
e61bfa4
cli: scope the -pe alias to the bun/node entry points in execArgv
robobun Jul 17, 2026
4f599cf
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 17, 2026
fb8b3d3
cpu profiler: clamp the sampling interval to a usable range
cirospaciari Jul 17, 2026
dbe27a0
tls: make --use-system-ca a per-Environment option, add --no-use-syst…
cirospaciari Jul 17, 2026
0fe453a
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 17, 2026
e20129c
cpu profiler: do not inherit --cpu-prof-name into workers
robobun Jul 17, 2026
3a09c2e
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 17, 2026
fc01f2f
worker_threads: keep error.code when the thrown value cannot be cloned
cirospaciari Jul 17, 2026
57cf856
perf_hooks: implement eventLoopUtilization()
cirospaciari Jul 17, 2026
dfe31cd
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 17, 2026
aba81dd
cpu profiler: let workers inherit --cpu-prof-name, as node does
cirospaciari Jul 17, 2026
74c1ebf
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 17, 2026
f17365c
fix lint and clippy fallout from the eventLoopUtilization work
cirospaciari Jul 17, 2026
997ce14
address review: once() this-binding, init sites, nested cpu-prof, lint
robobun Jul 17, 2026
e19fa93
Merge branch 'main' into ciro/worker-threads-node-tests
cirospaciari Jul 17, 2026
1fbc0d1
eventLoopUtilization: fix two cross-thread races
robobun Jul 18, 2026
19b36ff
eventLoopUtilization: use seq_cst for the idle_ns/idle_entry_ns pair
robobun Jul 18, 2026
c7cfb4e
web_worker: fix two aliasing/race hazards in the cross-thread ELU/cpu…
robobun Jul 18, 2026
f0f5e8e
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 18, 2026
de70866
ci: retrigger
robobun Jul 18, 2026
31fb9fc
epoll_kqueue: hoist the post-park clock read before zeroing idle_entr…
robobun Jul 18, 2026
a757b03
Worker.cpp: guard errorCodeOf against a pending TerminationException
robobun Jul 18, 2026
46f9f32
Merge branch 'main' into ciro/worker-threads-node-tests
cirospaciari Jul 19, 2026
1efb86f
Merge remote-tracking branch 'origin/main' into ciro/worker-threads-n…
robobun Jul 22, 2026
89e5f6e
Merge remote-tracking branch 'origin/main' into ciro/worker-threads-n…
robobun Aug 3, 2026
747cbbb
trim comments to <=3 lines, cite spec/node source
robobun Aug 3, 2026
79b3862
Worker.cpp: guard WebWorker__dispatchError/dispatchErrorWithValue aga…
robobun Aug 3, 2026
388af0e
bun_get_loop_elu: use raw us_loop_idle_ns, drop the &self idle_ns wra…
robobun Aug 3, 2026
134c6b0
Merge remote-tracking branch 'origin/main' into ciro/worker-threads-n…
robobun Aug 4, 2026
5e6f3ac
Merge branch 'main' into ciro/worker-threads-node-tests
cirospaciari Aug 5, 2026
fb72372
web_worker: don't report a terminate()-rejected entry promise as unca…
robobun Aug 5, 2026
f99b7c0
Add missing SAFETY comment on us_loop_idle_ns call
cirospaciari Aug 5, 2026
20f5734
web_worker: close the Bun-side termination-exception holes in the ter…
robobun Aug 5, 2026
04dca3c
worker: keep flush_logs and teardown termination-safe on the remainin…
cirospaciari Aug 5, 2026
a4993b9
Merge remote-tracking branch 'origin/main' into ciro/worker-threads-n…
robobun Aug 7, 2026
5d77db6
Merge branch 'main' into ciro/worker-threads-node-tests
cirospaciari Aug 7, 2026
3b28311
execArgv: drop the unconditional -pe insertion the merge reintroduced
robobun Aug 7, 2026
49f1a4f
Trim comments to node-source/spec references
robobun Aug 7, 2026
7d70027
Skip worker_destruction under ASAN while the terminate-during-load as…
robobun Aug 7, 2026
c6045a8
worker_destruction: terminate when the worker reports its action started
robobun Aug 8, 2026
3288369
Merge remote-tracking branch 'origin/main' into ciro/worker-threads-n…
dylan-conway Aug 8, 2026
bc4eab3
worker_threads: read a Worker's hasRef / loop utilization through pri…
dylan-conway Aug 8, 2026
f3c28b8
Reattach release_parent_poll_ref doc comment to its function
robobun Aug 8, 2026
8c4567d
Drop unreachable keep-alive guard in WorkerMessagingProxy::hasRef
robobun Aug 8, 2026
94db9ae
worker_threads: honour --cpu-prof-dir, --cpu-prof-name and --cpu-prof…
dylan-conway Aug 8, 2026
fec0b60
perf_hooks: eventLoopUtilization needs no null-loop guard
dylan-conway Aug 8, 2026
47cceef
tls: a Worker's --use-system-ca / --no-use-system-ca governs the root…
dylan-conway Aug 9, 2026
e5e73b0
Merge remote-tracking branch 'origin/main' into ciro/worker-threads-n…
dylan-conway Aug 9, 2026
153e900
Fix two review findings in the per-worker use-system-ca plumbing
robobun Aug 9, 2026
e7f89fd
Only install OpenSSL default cert paths in the system-CA store variant
robobun Aug 9, 2026
ab623d0
Merge branch 'main' into ciro/worker-threads-node-tests
cirospaciari Aug 10, 2026
dc6b8bb
tls.getCACertificates('default') reports the decision connections use
robobun Aug 10, 2026
6222f4d
root_certs: cite node's NewRootCertStore for the default-paths gating
robobun Aug 10, 2026
6ab78d8
Restore per-thread NODE_USE_SYSTEM_CA fallback in getCACertificates
robobun Aug 10, 2026
28ea0a8
worker_threads: apply the thread's --use-system-ca to the default cli…
cirospaciari Aug 10, 2026
bb88250
worker_threads: resolve a flagless thread's CA option from its env, r…
cirospaciari Aug 10, 2026
6fc1c9d
tls/worker_threads: keep OpenSSL's default lookups in the flagless st…
cirospaciari Aug 10, 2026
efc5781
tls: back to node's root-store rules; make --use-openssl-ca exclusive
cirospaciari Aug 10, 2026
8eb5f3e
uws: safety comment on us_get_shared_default_ca_store matches its arg…
robobun Aug 10, 2026
9d5b413
tls.getCACertificates('default') leaves bundled and system roots out …
robobun Aug 10, 2026
ef0dc09
tls/worker_threads: system store is the OS store alone, openssl-ca re…
cirospaciari Aug 10, 2026
b6f0c62
Pin the web Worker 'open' event to thread start, ahead of the entry p…
robobun Aug 10, 2026
a24ad08
web_worker: borrow the ELU atomics directly (clippy deref_addrof)
cirospaciari Aug 11, 2026
3fcfde3
Point the use_system_ca_flag docs at InitOptions; arm the ELU test's …
robobun Aug 11, 2026
da612ae
Merge branch 'main' into ciro/worker-threads-node-tests
alii Aug 11, 2026
09f3e89
bake: init the production VM as the main thread like its siblings
robobun Aug 11, 2026
7e37ca0
bake: pass the CA flag explicitly instead of initializing as the main…
robobun Aug 11, 2026
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
12 changes: 8 additions & 4 deletions src/js/node/async_hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -493,16 +493,19 @@ function createHook(hook) {
throw $ERR_ASYNC_CALLBACK("hook.promiseResolve");

let enabledInit;
return {
const asyncHook = {
enable() {
if (init !== undefined && enabledInit === undefined) {
// init is delivered for TickObject resources (process.nextTick);
// other resource types are still unimplemented.
// init is delivered for TickObject (process.nextTick) and WORKER
// resources; other resource types are still unimplemented.
Comment thread
robobun marked this conversation as resolved.
Outdated
// Per-instance wrapper: two hooks registered with the same init
// function must stay independently removable (removal is by
// identity, and removing the other instance's entry would reorder
// its callback relative to unrelated hooks).
enabledInit = (asyncId, type, triggerAsyncId, resource) => init(asyncId, type, triggerAsyncId, resource);
// node invokes init as a method on the AsyncHook instance
// (lib/internal/async_hooks.js), so `this.disable()` inside init works.
enabledInit = (asyncId, type, triggerAsyncId, resource) =>
init.$call(asyncHook, asyncId, type, triggerAsyncId, resource);
require("internal/async_hooks_tick").tickInitHooks.push(enabledInit);
}
if (before !== undefined || after !== undefined || destroy !== undefined || promiseResolve !== undefined) {
Expand All @@ -529,6 +532,7 @@ function createHook(hook) {
return this;
},
};
return asyncHook;
}

const executionAsyncIdNotImpl = createWarning(
Expand Down
55 changes: 55 additions & 0 deletions src/js/node/worker_threads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,11 @@ type NodeWorkerOptions = import("node:worker_threads").WorkerOptions;
// Used to ensure that Blobs created to hold the source code for `eval: true` Workers get cleaned up
// after their Worker exits
let urlRevokeRegistry: FinalizationRegistry<string> | undefined = undefined;
// Resolved at module load, as node does (lib/internal/worker.js): resolving it
// lazily inside the constructor would build the channel out of whatever
// Map/Object user code had tampered with by then.
const workerThreadsChannel = require("node:diagnostics_channel").channel("worker_threads");
const { tickInitHooks, newAsyncId } = require("internal/async_hooks_tick");

function injectFakeEmitter(Class) {
// Per-instance registry mapping each event to (user listener -> wrapper), so
Expand Down Expand Up @@ -935,6 +940,9 @@ class Worker extends EventEmitter {
#stdin;
#stdout;
#stderr;
// Mirrors ref()/unref() for the async_hooks WORKER resource's hasRef();
// undefined once the thread has exited, as node's handle reads back.
#hasRef: boolean | undefined = true;
Comment thread
robobun marked this conversation as resolved.
Outdated

// this is used by terminate();
// either is the exit code if exited, a promise resolving to the exit code, or undefined if we haven't sent .terminate() yet
Expand Down Expand Up @@ -1092,6 +1100,45 @@ class Worker extends EventEmitter {
}
urlRevokeRegistry.register(this.#worker, this.#urlToRevoke);
}
// node publishes the newly-constructed Worker here, at the end of the
// constructor (lib/internal/worker.js).
if (workerThreadsChannel.hasSubscribers) {
workerThreadsChannel.publish({ worker: this });
}
this.#emitAsyncHooksInit();
Comment thread
robobun marked this conversation as resolved.
Outdated
}

// node's WORKER resource is the C++ handle, so hasRef() follows ref()/unref()
// and reads back undefined once the handle is gone. Bun delivered init for
// TickObject only; this extends the same array to the type worker consumers
// look for. Only hasRef() is exposed, not the full handle.
#emitAsyncHooksInit() {
const count = tickInitHooks.length;
if (count === 0) return;
const worker = this;
const resource = {
hasRef() {
return worker.#hasRef;
},
};
const asyncId = newAsyncId();
// Snapshot: enable()/disable() from inside a hook must not affect the
// in-flight dispatch (node stages such mutations in tmp_array).
const snapshot = $newArrayWithSize<Function>(count);
for (let i = 0; i < count; i++) snapshot[i] = tickInitHooks[i];
for (let i = 0; i < count; i++) {
try {
snapshot[i](asyncId, "WORKER", 0, resource);
} catch (err) {
// node: a throwing init hook is fatal (fatalError: print + exit 1),
// never surfaced to the `new Worker` caller — which here has already
// spawned the thread. console is user-mutable, so shield the print.
try {
console.error(typeof err?.stack === "string" ? err.stack : err);
} catch {}
process.exit(1);
}
}
}

get threadId() {
Expand All @@ -1106,10 +1153,14 @@ class Worker extends EventEmitter {
// stdio ports are not touched here (node's ref()/unref() only touch the
// handle and the public port); their ref state tracks in-flight I/O.
this.#worker.ref();
// node's ref()/unref() no-op once the handle is gone, leaving hasRef()
// undefined rather than resurrecting it.
if (!this.#exited) this.#hasRef = true;
}

unref() {
this.#worker.unref();
if (!this.#exited) this.#hasRef = false;
}

get stdin() {
Expand Down Expand Up @@ -1291,6 +1342,10 @@ class Worker extends EventEmitter {
this.#stdinPort?.close();
this.#onExitPromise = e.code;
this.emit("exit", e.code);
// node's WORKER handle is gone once the thread has exited, so its
// hasRef() reads back undefined. 'exit' listeners ran synchronously above
// and still saw the live value.
this.#hasRef = undefined;
}

#onError(event: ErrorEvent) {
Expand Down
14 changes: 12 additions & 2 deletions src/jsc/VirtualMachine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1988,6 +1988,15 @@ fn get_origin_timestamp() -> u64 {
(now - ORIGIN_RELATIVE_EPOCH).max(0) as u64
}

/// `performance.timeOrigin` is the PROCESS start time and every thread reports
/// the same one — a worker's timeOrigin equals the main thread's in node, and
/// `performance.now()` inside a worker is relative to that same origin. Capture
/// it on the first VM so worker VMs inherit it instead of restarting the clock.
fn process_origin() -> (std::time::Instant, u64) {
static ORIGIN: std::sync::OnceLock<(std::time::Instant, u64)> = std::sync::OnceLock::new();
*ORIGIN.get_or_init(|| (std::time::Instant::now(), get_origin_timestamp()))
}

impl VirtualMachine {
/// `VirtualMachine.init(opts)` — allocate + wire the per-thread VM.
///
Expand Down Expand Up @@ -2079,8 +2088,9 @@ impl VirtualMachine {
addr_of_mut!((*vm).pending_internal_promise_reported_at).write(u32::MAX);
addr_of_mut!((*vm).on_unhandled_rejection)
.write(VirtualMachine::default_on_unhandled_rejection);
addr_of_mut!((*vm).origin_timer).write(std::time::Instant::now());
addr_of_mut!((*vm).origin_timestamp).write(get_origin_timestamp());
let (origin_timer, origin_timestamp) = process_origin();
addr_of_mut!((*vm).origin_timer).write(origin_timer);
addr_of_mut!((*vm).origin_timestamp).write(origin_timestamp);
addr_of_mut!((*vm).smol).write(opts.smol);
// `Option<{CPU,Heap}ProfilerConfig>` are NOT zero-valid: each
// payload contains a `bool`, and rustc picks that field's invalid
Expand Down
4 changes: 3 additions & 1 deletion src/jsc/bindings/webcore/JSBroadcastChannel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,9 @@
UNUSED_PARAM(throwScope);
UNUSED_PARAM(callFrame);
auto& impl = castedThis->wrapped();
RELEASE_AND_RETURN(throwScope, JSValue::encode(toJS<IDLUndefined>(*lexicalGlobalObject, throwScope, [&]() -> decltype(auto) { return impl.jsRef(lexicalGlobalObject); })));
impl.jsRef(lexicalGlobalObject);
// node's ref() returns the channel so it chains (lib/internal/worker/io.js).
RELEASE_AND_RETURN(throwScope, JSValue::encode(castedThis));

Check warning on line 392 in src/jsc/bindings/webcore/JSBroadcastChannel.cpp

View check run for this annotation

Claude / Claude Code Review

BroadcastChannel ref() return-value change ships without a test

nit: `BroadcastChannel#ref()` now returns `this` (a behavioral change) but no test asserts on it — commit c308af4b touches only `JSBroadcastChannel.cpp`, and the commit message cites manual verification against node v26.3.0. Per REVIEW.md ("Every behavioral change ships an automated test in the same PR … 'Verified manually' doesn't count, even for one-liners"), a one-liner in `test/js/web/broadcastchannel/broadcast-channel.test.ts` would guard this against a bindings-regeneration silently revert
Comment thread
robobun marked this conversation as resolved.
}

JSC_DEFINE_HOST_FUNCTION(jsBroadcastChannelPrototypeFunction_ref, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame))
Expand Down
52 changes: 37 additions & 15 deletions src/jsc/bindings/webcore/Worker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
#include "Event.h"
#include "EventNames.h"
#include "StructuredSerializeOptions.h"
#include <JavaScriptCore/ErrorInstance.h>
#include <JavaScriptCore/IteratorOperations.h>
#include <JavaScriptCore/ScriptCallStack.h>
#include <wtf/TZoneMallocInlines.h>
Expand Down Expand Up @@ -446,31 +447,29 @@ void Worker::rejectAllCrossVMRequests(JSC::JSGlobalObject* globalObject)

// ---- Worker-thread entry points ---------------------------------------------

// Posted before the entry point runs, matching node's 'online', so a worker whose
// top-level never returns still reports online. Deliberately leaves m_state alone:
// Pending queues in postTaskToWorkerGlobalScope, it does not reject.
void Worker::dispatchOnlineEvent()
{
postTaskToParent([protectedThis = Ref { *this }](ScriptExecutionContext&) {
if (protectedThis->hasEventListeners(eventNames().openEvent)) {
auto event = Event::create(eventNames().openEvent, Event::CanBubble::No, Event::IsCancelable::No);
protectedThis->dispatchEvent(event);
}
});
}

void Worker::dispatchOnline(Zig::GlobalObject* workerGlobalObject)
{
// Pending→Running under the same lock postTaskToWorkerGlobalScope uses, so
// a message post racing this transition either queues (drained below by
// fireEarlyMessages) or posts directly — never both, never neither.
//
// This MUST happen BEFORE the open event is posted to the parent: the
// parent's `online` handler may immediately call getHeapSnapshot() (or
// anything else gated on isOnline() / postTaskToWorkerGlobalScope()). If
// the state flip happens after the post, a fast parent thread can run the
// open task while m_state is still Pending and observe
// ERR_WORKER_NOT_RUNNING — flaky `await once(worker, "online");
// worker.getHeapSnapshot()` in worker_threads.test.ts.
{
Locker lock(m_pendingTasksMutex);
m_state.store(State::Running);
}

postTaskToParent([protectedThis = Ref { *this }](ScriptExecutionContext&) {
if (protectedThis->hasEventListeners(eventNames().openEvent)) {
auto event = Event::create(eventNames().openEvent, Event::CanBubble::No, Event::IsCancelable::No);
protectedThis->dispatchEvent(event);
}
});

auto* thisContext = workerGlobalObject->scriptExecutionContext();
if (!thisContext) {
return;
Expand Down Expand Up @@ -539,6 +538,24 @@ bool Worker::dispatchErrorWithValue(Zig::GlobalObject* workerGlobalObject, JSVal

auto serialized = SerializedScriptValue::create(*workerGlobalObject, value, SerializationForStorage::No, SerializationErrorMode::NonThrowing);
CLEAR_IF_EXCEPTION(scope);
// Cloning an Error reads `stack`, so a throwing Error.prepareStackTrace takes
// the whole error down and the caller reports the pretty-printed text as the
// message instead. Node drops only the unreadable `stack`
// (lib/internal/error_serdes.js TryGetAllProperties); retry once with an own
// undefined `stack` so name/message/code still cross. Safe to mutate: the
// worker's own error event and the fallback message are both already
// materialized by the time we get here, and the thread is terminating.
if (!serialized && !scope.exception()) {
if (auto* errorInstance = dynamicDowncast<JSC::ErrorInstance>(value)) {
errorInstance->putDirect(vm, vm.propertyNames->stack, JSC::jsUndefined(), JSC::PropertyAttribute::DontEnum | 0);
// putDirect bypasses the ErrorInstance property overrides, so tell it
// not to re-materialize `stack` (and re-run prepareStackTrace) below.
errorInstance->setStackPropertyAlreadyMaterialized();
CLEAR_IF_EXCEPTION(scope);
serialized = SerializedScriptValue::create(*workerGlobalObject, value, SerializationForStorage::No, SerializationErrorMode::NonThrowing);
CLEAR_IF_EXCEPTION(scope);
}
}
if (!serialized)
return false;

Expand Down Expand Up @@ -713,6 +730,11 @@ extern "C" void WebWorker__entrySettled(Zig::GlobalObject* globalObject)
CLEAR_IF_EXCEPTION(scope);
}

extern "C" void WebWorker__dispatchOnlineEvent(Worker* worker)
{
worker->dispatchOnlineEvent();
}

extern "C" void WebWorker__dispatchOnline(Worker* worker, Zig::GlobalObject* globalObject)
{
WebWorker__entrySettled(globalObject);
Expand Down
1 change: 1 addition & 0 deletions src/jsc/bindings/webcore/Worker.h
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ class Worker final : public ThreadSafeRefCounted<Worker>, public EventTargetWith
WorkerOptions& options() { return m_options; }

// -- Worker-thread entry points (each posts to m_parentContextId) --------
void dispatchOnlineEvent();
void dispatchOnline(Zig::GlobalObject* workerGlobalObject);
void fireEarlyMessages(Zig::GlobalObject* workerGlobalObject);
void dispatchErrorWithMessage(WTF::String message);
Expand Down
18 changes: 10 additions & 8 deletions src/jsc/web_worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
//! Lifecycle of the worker thread (`threadMain`):
//! 1. `startVM()` — build a mimalloc arena, clone env, initialise a
//! `jsc.VirtualMachine`, publish `vm` under `vm_lock`.
//! 2. `spin()` — load the entry point, call `dispatchOnline` +
//! 2. `spin()` — post 'online', load the entry point, call `dispatchOnline` +
//! `fireEarlyMessages`, run the event loop until it drains or
//! `requested_terminate` is observed, run `beforeExit`.
//! 3. `shutdown()` — call `vm.onExit()`, tear down the JSC VM, post
Expand Down Expand Up @@ -221,6 +221,7 @@ unsafe extern "C" {
// Re-declared here (also private in VM.rs) so `thread_main` can take the
// API lock as a raw FFI call with NO RAII guard — see the note there.
safe fn JSC__VM__getAPILock(vm: &jsc::VM);
safe fn WebWorker__dispatchOnlineEvent(cpp_worker: *mut c_void);
safe fn WebWorker__dispatchOnline(cpp_worker: *mut c_void, global: &JSGlobalObject);
safe fn WebWorker__fireEarlyMessages(cpp_worker: *mut c_void, global: &JSGlobalObject);
safe fn WebWorker__entrySettled(global: &JSGlobalObject);
Expand Down Expand Up @@ -1094,6 +1095,11 @@ impl WebWorker {
return self.shutdown();
}

// node reports 'online' before user code runs, so a worker whose entry
// point never returns still goes online. Only the event goes out here:
// the Pending→Running flip stays put, so message routing is unchanged.
WebWorker__dispatchOnlineEvent(self.cpp_worker);

// `path` borrows the resolver's process-lifetime string store, the
// standalone module graph, or `self.unresolved_specifier` — all of
// which outlive the worker VM. `vm.main` stores it as a raw BACKREF
Expand Down Expand Up @@ -1146,13 +1152,9 @@ impl WebWorker {

self.flush_logs(vm);
log!("[{}] event loop start", self.execution_context_id);
// dispatchOnline fires the parent-side 'open' event and flips the C++
// state to Running (which routes postMessage directly instead of
// queuing). It is placed after the entry point has loaded so the parent
// observes 'online' only once the worker's top-level code has completed;
// moving it earlier would change that observable ordering.
// `cpp_worker` is the opaque C++-owned handle round-tripped via `safe fn`;
// `vm.global()` yields the live `&JSGlobalObject` published in start_vm.
// Flips the C++ state to Running, which routes postMessage directly
// instead of queuing; the 'online' event already went out before the
// entry point loaded.
WebWorker__dispatchOnline(self.cpp_worker, vm.global());
WebWorker__fireEarlyMessages(self.cpp_worker, vm.global());
self.set_status(Status::Running);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const { Worker } = require('worker_threads');
const dc = require('diagnostics_channel');

dc.subscribe('worker_threads', common.mustCall(({ worker }) => {
assert.strictEqual(worker instanceof Worker, true);
}));

new Worker('const a = 1;', { eval: true });
20 changes: 20 additions & 0 deletions test/js/node/test/parallel/test-perf-hooks-worker-timeorigin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
'use strict';

const common = require('../common');
const assert = require('assert');
const { Worker } = require('worker_threads');

const w = new Worker(`
require('worker_threads').parentPort.postMessage(performance.timeOrigin);
`, { eval: true });

w.on('message', common.mustCall((timeOrigin) => {
// PerformanceNodeTiming exposes process milestones so the
// `performance.timeOrigin` in the `worker_threads.Worker` must be the start
// time of the process.
assert.strictEqual(timeOrigin, performance.timeOrigin);
}));

w.on('exit', common.mustCall((code) => {
assert.strictEqual(code, 0);
}));
29 changes: 29 additions & 0 deletions test/js/node/test/parallel/test-worker-cleanup-handles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
'use strict';
const common = require('../common');

const assert = require('assert');
const fs = require('fs');
const { Server } = require('net');
const { Worker, isMainThread, parentPort } = require('worker_threads');

if (isMainThread) {
const w = new Worker(__filename);
let fd = null;
w.on('message', common.mustCall((fd_) => {
assert.strictEqual(typeof fd_, 'number');
fd = fd_;
}));
w.on('exit', common.mustCall(() => {
if (fd === -1) {
// This happens when server sockets don’t have file descriptors,
// i.e. on Windows.
return;
}
assert.throws(() => fs.fstatSync(fd), { code: 'EBADF' });
}));
} else {
const server = new Server();
server.listen(0);
parentPort.postMessage(server._handle.fd);
server.unref();
}
8 changes: 8 additions & 0 deletions test/js/node/test/parallel/test-worker-dispose.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import * as common from '../common/index.mjs';
import { Worker } from 'node:worker_threads';

{
// Verifies that the worker is async disposable
await using worker = new Worker('for(;;) {}', { eval: true });
worker.on('exit', common.mustCall());
}
Loading
Loading