Repository navigation
v0.15.0 — promise continuations that don't crash or leak
Promise continuations that do not crash or leak. Host-mode code gets a
framework primitive for continuing after a JavaScript promise,
JsPromise.on_settled, replacing a hand-rolled pattern the docs recommended
that crashed the process on rejection, swallowed its own errors, and leaked
for every promise that rejected or never settled. The reason host mode cannot
simply wait for a promise is also corrected: it was never that Mojo lacks
await.
No existing Mojo signature changed — on_settled, Settlement and
JsObject.add_finalizer are additions — so downstream addons compile against
0.15.0 unmodified. The demo addon's thenDouble and deferredRequire exports
do change shape; see Fixed.
Added
JsPromise.on_settledattaches a Mojo continuation to a promise — or to
any value, likeawait— and returns the promise.then()made. The
continuation reads the outcome withSettlement.read(env, info):ok,
value, the JS values passed ascaptures, and native state via
user_data(). Nothing is freed when the continuation fires, because it may
never fire: captures are bound arguments the GC traces, anddatais
adopted by a GC finalizer on every path, including failure.JsObject.add_finalizer—napi_add_finalizeron any object, the way
to tie native memory to a JS object's lifetime. Previously raw-only.thenScaled(value, factor, counter, onResult)demo export: a continuation
with native state whose finalizer is observable from JavaScript.
Fixed
-
The host-mode continuation examples crashed, hid errors and leaked.
thenDoubleanddeferredRequirewere the pattern the docs told users to
copy, and three things were wrong with it, each now tested and each
mutation-checked against the suite:- it attached no rejection handler, so a rejected promise crashed the
process with an unhandled rejection andonResultnever ran; - its
except: passswallowed Mojo-side failures, so a promise resolving to
the wrong type madeonResultsilently never run; - it freed its payload only when the continuation fired and held
onResult
in a strongnapi_ref, so every promise that rejected or never settled
leaked both — 209.7 MB for 200 closures over 1 MB, where the same
shape in plain JavaScript retains nothing.
Both are rebuilt on
on_settled. Their JS signatures changed: they now
return the promise.then()made, and callonResultnode-style —
onResult(null, value)on success,onResult(reason)on rejection. - it attached no rejection handler, so a rejected promise crashed the
-
napi-mojo release --scaffoldpinned a stalesetup-pixi. The release
workflow it writes lives in a template string Dependabot cannot see, so it
keptv0.10.1after this repo moved tov0.10.2. It now matches, and
tests/template_action_pins.test.jsfails whenever an action the template
shares with this repo's workflows drifts.
Docs
- "Mojo has no
await" was wrong, and is gone from README, CLAUDE.md,
js_host.mojoand the plan docs. Mojo hasawait; it suspends Mojo
coroutines. What host mode cannot do is wait for a JS promise, because
its code runs on the JS thread and a promise settles only after that code
returns.spike/await_probe.mojorecords the measurement behind "do not add
a blocking await helper": re-enteringuv_runfrom a callback runs other JS
on the nested stack and still never runs the promise's reactions. docs/plan-promise-bridge.mdrecords how
napi-rs lets Rust await a JS promise (worker thread, threadsafe function,
one-shot channel — read from the 3.12.1 source), what napi-mojo would still
need, and the gate for building it.