Skip to content

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

Open
nazarhussain wants to merge 1 commit into
mainfrom
nh/feat-async-task
Open

feat(js): add js.spawn async task DSL#76
nazarhussain wants to merge 1 commit 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
errorMessage(anyerror) [:0]const u8 JS optional; rejection message, defaults to @errorName
reject(*Task, napi.Env, anyerror) !napi.Value JS optional; builds the rejection value, wins over errorMessage

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?

});
});

// 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.

Same task number comment

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

2 participants