Repository navigation
v0.12.0
Minor Changes
-
#265
523e75cThanks @JounQin! - feat: surface worker failures and exits instead of hanging forever inAtomics.wait()A worker that never answers no longer leaves its caller blocked: a module graph that fails to load (a missing top-level import, a syntax error, a failing global shim), a throw after a top-level
await, a runtime failure while a call is in flight, and a worker that exits — through a handler that callsprocess.exit(), or on its own — are all reported to the caller instead.The guard now reaches a worker through its own
NODE_OPTIONSrather than itsexecArgv, so it is in place before a preload the worker inherits fromNODE_OPTIONS, which run first: a failure there reaches the caller like any other instead of being left to the call's deadline, or to no deadline at all. The worker's environment is passed explicitly to do that, and itsNODE_OPTIONSkeeps whatever it inherited, after the preload. A runtime that refuses that environment — Node 18 with--openssl-legacy-provider, Node 24 with--title, the set varies by version — makes the worker fall back to the inherited environment with the guard in itsexecArgvinstead, so the guard loads after preloads inherited throughNODE_OPTIONSand a failure in one of those is reported by nothing. synckit warns once per worker that takes that path, under theSYNCKIT_GUARD_ORDERINGcode, so the caller can bound the wait themselves withSYNCKIT_TIMEOUTor a timeout passed per call.This changes the failure semantics, which is why it is a minor. A call that used to hang now throws; a worker that swallows the failure in its own handler no longer lets the call in flight return normally; and a worker that fails fatally no longer takes the process down with it — the caller gets the failure instead, and later calls keep throwing it.
A worker which reached
runAsWorkerand still has a handler for the event keeps serving later calls: the guard reports the failure and re-arms rather than deciding for that handler, so the worker stays usable — it does not mean the handler ran. For a rejection, the guard's ownunhandledRejectionlistener is what keeps the runtime from promoting it under the defaultthrowmode, so a handler listening only onuncaughtExceptionnever sees it; understrictthe runtime promotes it anyway, and that handler does see it. Otherwise the failure is fatal for that synchronous function, and later calls keep throwing it rather than waiting on a worker which never registered, or which has nothing left to keep it alive.A recoverable failure only tells the caller that the worker can still serve; it does not stop the request that was in flight. A worker whose own handler absorbs the failure may still be running that request, so a retry can overlap it — a worker module holding state across calls has to expect that.
The timeout is now a single deadline for the whole call, and each wait receives only the time left of it. Previously every wait measured its own slice, so the time spent between waits was not counted and outdated responses could push the total wait past the timeout.
The module specifiers
encodeImportModulegenerates are escaped withJSON.stringifynow — double quotes and full escaping — where a single-quoted literal escaped only backslashes and quotes, so a path carrying a newline or another control character is emitted correctly instead of breaking the generated source.Breaking: the
INT32_BYTESexport is gone. It was reachable from the public entry point —src/index.tsre-exports the whole constants module — so an import of it that compiles onmainno longer compiles here. Nothing in the package used it, and the release stays aminorunder the 0.x convention rather than amajor.The
hasFlagutility now reads the flags the runtime applies —execArgvandNODE_OPTIONS, in either the--flag=valueor--flag valueform — and no longer counts a flag that is only a script argument, which Node does not apply.NODE_OPTIONSis now read the way the runtime reads it: split on spaces, with double quotes grouping and removed and a backslash escaping inside them, so a quoted value keeps its spaces and an unset variable is[]where a naive split gave[''].DataMessage<T>is now a discriminated union whose failure arm requireserror, so a reason that is present butundefinedis still a failure rather than a result, andpropertieson that arm is typedobjectinstead ofunknown.A reported failure carries the worker's reason exactly as it was thrown, even a falsy one; when its properties cannot cross the thread boundary the error is sent bare, and only a reason that cannot be cloned at all is replaced by a synthetic error that still names it.
Patch Changes
- #269
0086192Thanks @JounQin! - Resolve global shims from the worker for both CommonJS and ESM: the CommonJS eval entry rebindsrequiretocreateRequire(workerPath), and the ESM entry resolves each shim against the worker's path and export conditions —@dual-bundle/import-meta-resolveundernode_modules,pnpapiunder Yarn PnP — and imports it as an absolute URL, instead of loading a generated file under the package's ownnode_modules. Availability is judged with that same resolver, and the conditions follow the worker'sexecArgv(node-addons,module-sync,--conditions) as well as this thread's.