napi-async-runtime
A shared, tokio-free async runtime for napi-rs addons: a pluggable
async/CPU/blocking scheduler plus an optional adapter that registers it as the
addon's AsyncRuntime backend (napi's async-runtime SPI).
Layers
- Scheduler (always compiled, napi-free). Exposed at the crate root:
spawn,try_spawn,spawn_detached,spawn_blocking,block_on,sleep_until,configure/configure_partial/configured_options,start/shutdown, metrics (metrics/reset_metrics), and the host driver SPIs (CurrentThreadTaskDriver,TimerDriver). - napi adapter (cargo feature
napi, on by default).install(options)configures the scheduler and callsregister_async_runtime; the JavaScript-facing host protocol is exported under stable names (reserveCurrentThreadHostRegistration,registerCurrentThreadTaskHost,registerTimerHost,unregisterCurrentThreadTaskHost,unregisterTimerHost,isCurrentThreadHostRegistrationActive,getCurrentThreadTaskHostContractVersion,configureAsyncRuntime,getAsyncRuntimeConfig,getAsyncRuntimeMetrics,resetAsyncRuntimeMetrics).
Usage
use ;
This crate deliberately ships no module_init of its own: the host stays
in charge of configuration resolution order. @napi-rs/async-runtime provides
the matching JavaScript host installers (task host + timer host) for
CurrentThread builds.
Implementation notes
- Flavors.
MultiThread(native only) runs futures on a Rayon-backed worker pool;CurrentThread(the only flavor on WebAssembly) never creates threads and instead publishes host turns through registeredCurrentThreadTaskDrivers — on Node that driver is a native threadsafe function installed byregisterCurrentThreadTaskHost(contract version 4: a registration capability is reserved and validated before host installation performs side effects). - Timers. MultiThread owns a timer heap serviced by a timekeeper thread.
CurrentThread delegates each timer to the host event loop through the JS
relay installed by
registerTimerHost((relayId, ms) => Promise<void>paired with(relayId) => voidcancellation). Schedule and cancel share ONE per-relay health record (a single strike); three consecutive live-host failures evict the host; eviction is decided only byStatus::Closingor the liveness probe, never by message text; relay ids are never reused while referable. - Blocking admission. The blocking cap is
worker_threads - 1: one execution lane always stays available for runnable futures and timer service. There is no hidden reserve worker, and napi never creates fallback threads whenspawn_blockingdeclines. - Lifecycle.
shutdowncloses admission, waits for the scheduler generation to quiesce, JOINS native workers (TLS destructors retire inside the barrier), and releases active resources before returning; generation identities fail closed before u64 reuse. Detached-task semantics match tokio: dropping aJoinHandledetaches, and shutdown may cancel accepted work by dropping futures. - Threadless wasm (
wasm32-wasip1,wasm32-unknown-unknown): no threads, noAtomics.wait. Ablock_onpark that provably can never be woken fails loudly with the typedBlockOnDeadlockpanic instead of hanging the JS event loop. Thewasm32-wasip1-threadstarget is discriminated by this crate's build.rs (napi_runtime_wasi_threadscfg) because rustc exposes identical cfg sets for both WASI targets.
The scheduler and adapter were extracted from rolldown's shared async runtime (rolldown#9977/#9978) and generalized; the wire behavior of the host protocol is byte-compatible with the rolldown hosts at contract version 4.