Skip to content

feat(js): add js.spawn async task DSL - #76

Merged
nazarhussain merged 5 commits into
mainfrom
nh/feat-async-task
Sep 15, 2026
Merged

nazarhussain merged 5 commits into
mainfrom
nh/feat-async-task

Conversation

@nazarhussain

@nazarhussain nazarhussain commented Aug 18, 2026 •

Copy link
Copy Markdown
Contributor

Motivation

No DSL for worker-thread async: js.Promise is synchronous-only, so every call site hand-rolls napi.AsyncWork + napi.Deferred (~100 lines each in lodestar-z). DSL types were unusable in the completion callback because js.env() panics there.

Description

js.spawn(Task, task, name) runs compute on the libuv pool and settles a Promise with resolve's value. A task is a comptime duck-typed struct:

Decl Thread Notes
compute(*Task) !void worker required; must not call napi
resolve(*Task, napi.Env) !T JS required; T = DSL type, owned typed array, napi.Value, or void
deinit(*Task) void JS required; must be safe after resolve transferred ownership
reject(*Task, napi.Env, anyerror) !napi.Value JS optional; builds the rejection value, else Error(@errorName(err))

If spawn fails the task isn't consumed (caller frees); if it succeeds ownership transfers and deinit runs 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.
  • It's set there rather than in src/async_work.zig, so the raw napi layer keeps no dependency on js/context.zig.

8 new vitest cases; deleting the setEnv line reproduces the panic, confirming it's load-bearing. Full suite green.

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
nazarhussain marked this pull request as draft August 18, 2026 17:55
@nazarhussain
nazarhussain marked this pull request as ready for review August 20, 2026 10:26
Comment thread examples/js_dsl/mod.zig
Comment on lines +726 to +729
// ============================================================================
// Section 18: Async Tasks
// ============================================================================

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we remove this task number comments by AI?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was not just added by AI, but rather followed existing pattern.

Comment thread examples/js_dsl/mod.test.ts Outdated
@matthewkeil matthewkeil moved this from In Progress to Awaiting Author in Lodestar Team Coordination Sep 10, 2026
Comment thread src/js/async_task.zig
context.allocator().destroy(ctx);
}

settle(env, status, ctx) catch {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow the NAPI behaviour, seems we need check pendingException first and handle it first before calling rejectWithMessage again.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 4e12b52

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.
GrapeBaBa
GrapeBaBa previously approved these changes Sep 15, 2026

@GrapeBaBa GrapeBaBa left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good

@wemeetagain

Copy link
Copy Markdown
Member

errorMessage(anyerror) [:0]const u8

Is this one necessary?
Imo compute, deinit, resolve, reject all make sense, but errorMessage is questionable. I think its trying to be too convenient.

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.
@nazarhussain

Copy link
Copy Markdown
Contributor Author

@wemeetagain addressed in f8eba46

@nazarhussain
nazarhussain merged commit e60cca6 into main Sep 15, 2026
6 checks passed
@github-project-automation github-project-automation Bot moved this from Awaiting Author to Done in Lodestar Team Coordination Sep 15, 2026
@wemeetagain
wemeetagain deleted the nh/feat-async-task branch September 15, 2026 14:05
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants