feat(js): add js.spawn async task DSL - #76
Merged
Merged
Conversation
Worker-thread async previously meant hand-rolling napi.AsyncWork plus Deferred at every call site, and DSL values were unusable in the completion callback: js.env() panics there because only the sync wrappers establish the thread-local env context. js.spawn takes a comptime duck-typed task (compute/resolve/deinit, with optional errorMessage/reject) and returns a Promise that settles on the JS thread. Its completion callback sets the DSL env context, so resolve can return DSL types, and composes with OwnedTypedArray to hand results to JS without copying. compute deliberately does NOT get the env context — napi calls are illegal on the worker thread, so a panic there is the guard rail. Lifted from lodestar-z bindings/napi/async_task.zig, which was written to be upstreamed and is deleted once this ships.
nazarhussain
marked this pull request as draft
August 18, 2026 17:55
nazarhussain
marked this pull request as ready for review
August 20, 2026 10:26
GrapeBaBa
reviewed
Aug 21, 2026
Comment on lines
+726
to
+729
| // ============================================================================ | ||
| // Section 18: Async Tasks | ||
| // ============================================================================ | ||
|
|
Contributor
There was a problem hiding this comment.
Can we remove this task number comments by AI?
Contributor
Author
There was a problem hiding this comment.
It was not just added by AI, but rather followed existing pattern.
GrapeBaBa
reviewed
Aug 21, 2026
GrapeBaBa
reviewed
Sep 15, 2026
| context.allocator().destroy(ctx); | ||
| } | ||
|
|
||
| settle(env, status, ctx) catch { |
Contributor
There was a problem hiding this comment.
Follow the NAPI behaviour, seems we need check pendingException first and handle it first before calling rejectWithMessage again.
A failed N-API call in settle() can leave a pending JS exception. Every later N-API call then fails with napi_pending_exception, including the fallback rejectWithMessage, whose error was swallowed — so the promise never settled and callers hung forever. Check for a pending exception first and reject with it; the synthetic InternalError message is now only for the no-exception case.
Member
Is this one necessary? |
Two optional rejection hooks for one concern needed a precedence rule to arbitrate between them, and neither had a real consumer. Drop the narrower errorMessage; reject is strictly more general, expressing custom messages and non-Error rejection values alike. js.errorWithMessage keeps the common case a one-liner without exposing the createError code-argument idiom. It is env-explicit so it works in an async completion callback, and rejectWithMessage now delegates to it so both paths share one implementation.
Contributor
Author
|
@wemeetagain addressed in f8eba46 |
wemeetagain
approved these changes
Sep 15, 2026
wemeetagain
pushed a commit
that referenced
this pull request
Sep 25, 2026
🤖 I have created a release *beep* *boop* --- ## [4.1.0](zapi-v4.0.0...zapi-v4.1.0) (2026-09-25) ### Features * **js:** add exact u64 conversion ([#80](#80)) ([7626863](7626863)) * **js:** add js.spawn async task DSL ([#76](#76)) ([e60cca6](e60cca6)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
No DSL for worker-thread async:
js.Promiseis synchronous-only, so every call site hand-rollsnapi.AsyncWork+napi.Deferred(~100 lines each in lodestar-z). DSL types were unusable in the completion callback becausejs.env()panics there.Description
js.spawn(Task, task, name)runscomputeon the libuv pool and settles a Promise withresolve's value. A task is a comptime duck-typed struct:compute(*Task) !voidresolve(*Task, napi.Env) !TT= DSL type, owned typed array,napi.Value, orvoiddeinit(*Task) voidresolvetransferred ownershipreject(*Task, napi.Env, anyerror) !napi.ValueError(@errorName(err))If
spawnfails the task isn't consumed (caller frees); if it succeeds ownership transfers anddeinitruns after settling.All logic is in
src/js/async_task.zig— the other files are the export, doc fixes, tests, and README.Two decisions worth checking:
complete()sets the DSL env context, which is what makes DSL returns work.execute()deliberately doesn't: napi calls are illegal on the worker thread, so the panic is the guard rail.src/async_work.zig, so the rawnapilayer keeps no dependency onjs/context.zig.8 new vitest cases; deleting the
setEnvline reproduces the panic, confirming it's load-bearing. Full suite green.