Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 41 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,47 @@ pub fn asyncOp(val: Number) !Promise(Number) {
}
```

`Promise(T)` in this DSL path is synchronous-only: resolve or reject it before the exported function returns. For truly asynchronous completion, keep the `Deferred` handle in lower-level N-API code and bridge back with `napi.AsyncWork` or `napi.ThreadSafeFunction`.
`Promise(T)` in this DSL path is synchronous-only: resolve or reject it before the exported function returns. For work that must run off the JS thread, use `js.spawn` below.

### Async Tasks

`js.spawn` runs a task on the libuv worker pool and returns a JS Promise that settles when it finishes. A task is any struct with `compute`, `resolve`, and `deinit`:

```zig
const ScaleTask = struct {
data: []u32,
factor: u32,

// Worker thread — must not touch napi or DSL values.
pub fn compute(self: *ScaleTask) !void {
for (self.data) |*value| value.* *= self.factor;
}

// JS thread — the DSL env context is established, so DSL types work here.
pub fn resolve(self: *ScaleTask, _: napi.Env) !js.OwnedUint32Array {
const owned: js.OwnedUint32Array = .fromOwnedSlice(js.allocator(), self.data);
self.data = &.{}; // ownership handed to JS, no copy
return owned;
}

// Safe to call after resolve transferred ownership.
pub fn deinit(self: *ScaleTask) void {
js.allocator().free(self.data);
}
};

pub fn asyncScale(data: js.Uint32Array, factor: Number) !Value {
const copy = try js.allocator().dupe(u32, try data.toSlice());
errdefer js.allocator().free(copy);
return js.spawn(ScaleTask, .{ .data = copy, .factor = @intCast(factor.assertI32()) }, "asyncScale");
}
```

`resolve` may return a DSL type (`js.Number`), an owned typed array (transferred without copying), `napi.Value`, or `void`.

If `compute` returns an error the promise rejects with an `Error`. Add an optional `errorMessage(err: anyerror) [:0]const u8` to control the message, or an optional `reject(self: *Task, env: napi.Env, err: anyerror) !napi.Value` to build the rejection value yourself; otherwise the message is `@errorName(err)`.

Ownership: if `spawn` fails the task is not consumed, so the caller's `errdefer`s must free it (as above). Once `spawn` succeeds the helper owns the task and calls `deinit` after the promise settles.

### Callbacks

Expand Down
42 changes: 42 additions & 0 deletions examples/js_dsl/mod.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -840,3 +840,45 @@ describe("enum export", () => {
expect(Object.isFrozen(mod.BlsPublicKey.Encoding)).toBe(true);
});
});

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

describe("async tasks", () => {
it("resolves with a DSL value built in the complete callback", async () => {
await expect(mod.asyncDouble(21)).resolves.toEqual(42);
});

it("returns a real Promise", () => {
const promise = mod.asyncDouble(1);
expect(promise).toBeInstanceOf(Promise);
return promise;
});

it("runs concurrent tasks independently", async () => {
const results = await Promise.all([1, 2, 3, 4, 5].map((n) => mod.asyncDouble(n)));
expect(results).toEqual([2, 4, 6, 8, 10]);
});

it("transfers an owned typed array without copying", async () => {
const result = await mod.asyncScale(new Uint32Array([1, 2, 3]), 3);
expect(result).toBeInstanceOf(Uint32Array);
expect(Array.from(result)).toEqual([3, 6, 9]);
});

it("handles empty typed array transfer", async () => {
const result = await mod.asyncScale(new Uint32Array(0), 2);
expect(result).toBeInstanceOf(Uint32Array);
expect(result.length).toEqual(0);
});

it("rejects with the task-supplied error message", async () => {
await expect(mod.asyncFail()).rejects.toThrow("worker could not finish the job");
});

it("rejects with the error name when errorMessage is absent", async () => {
await expect(mod.asyncFailBare()).rejects.toThrow("Unlucky");
});

it("rejects with an Error instance", async () => {
await expect(mod.asyncFail()).rejects.toBeInstanceOf(Error);
});
});
103 changes: 103 additions & 0 deletions examples/js_dsl/mod.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const std = @import("std");
const js = @import("zapi").js;
const napi = @import("zapi").napi;
const Number = js.Number;
const String = js.String;
const Boolean = js.Boolean;
Expand Down Expand Up @@ -722,6 +723,108 @@ pub var mutable_counter: u32 = 5;
pub const IDENTITY_MATRIX = [_]u32{ 1, 0, 0, 1 };
pub const VERSION_INFO = .{ .major = 3, .minor = 1 };

// ============================================================================
// Section 18: Async Tasks
// ============================================================================

Comment on lines +726 to +729

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?

/// Doubles a number on the libuv worker pool.
///
/// `resolve` returns a DSL `js.Number` rather than a raw `napi.Value`, which
/// works because the async complete callback establishes the DSL env context.
const DoubleTask = struct {
value: i32,

pub fn compute(self: *DoubleTask) !void {
self.value *= 2;
}

pub fn resolve(self: *DoubleTask, _: napi.Env) !Number {
return Number.from(self.value);
}

pub fn deinit(_: *DoubleTask) void {}
};

/// JS: asyncDouble(n): Promise<number>
pub fn asyncDouble(n: Number) !Value {
return js.spawn(DoubleTask, .{ .value = n.assertI32() }, "asyncDouble");
}

/// Scales a Uint32Array on the worker pool and hands the result back without
/// copying, composing `js.AsyncTask` with `js.OwnedUint32Array`.
const ScaleTask = struct {
data: []u32,
factor: u32,

pub fn compute(self: *ScaleTask) !void {
for (self.data) |*value| value.* *= self.factor;
}

pub fn resolve(self: *ScaleTask, _: napi.Env) !js.OwnedUint32Array {
const owned: js.OwnedUint32Array = .fromOwnedSlice(js.allocator(), self.data);
self.data = &.{};
return owned;
}

pub fn deinit(self: *ScaleTask) void {
// Empty (a no-op free) once resolve transferred ownership to JS.
js.allocator().free(self.data);
}
};

/// JS: asyncScale(data, factor): Promise<Uint32Array>
pub fn asyncScale(data: js.Uint32Array, factor: Number) !Value {
const copy = try js.allocator().dupe(u32, try data.toSlice());
errdefer js.allocator().free(copy);
return js.spawn(ScaleTask, .{
.data = copy,
.factor = @intCast(factor.assertI32()),
}, "asyncScale");
}

/// Rejects with a task-supplied message via the optional `errorMessage` decl.
const FailTask = struct {
pub fn compute(_: *FailTask) !void {
return error.ComputeFailed;
}

pub fn resolve(_: *FailTask, _: napi.Env) !Number {
return Number.from(0);
}

pub fn errorMessage(err: anyerror) [:0]const u8 {
return switch (err) {
error.ComputeFailed => "worker could not finish the job",
else => @errorName(err),
};
}

pub fn deinit(_: *FailTask) void {}
};

/// JS: asyncFail(): Promise<never>
pub fn asyncFail() !Value {
return js.spawn(FailTask, .{}, "asyncFail");
}

/// Without `errorMessage`, the rejection message defaults to `@errorName`.
const BareFailTask = struct {
pub fn compute(_: *BareFailTask) !void {
return error.Unlucky;
}

pub fn resolve(_: *BareFailTask, _: napi.Env) !Number {
return Number.from(0);
}

pub fn deinit(_: *BareFailTask) void {}
};

/// JS: asyncFailBare(): Promise<never>
pub fn asyncFailBare() !Value {
return js.spawn(BareFailTask, .{}, "asyncFailBare");
}

comptime {
js.exportModule(@This(), .{
.identity = @import("zapi_addon_identity"),
Expand Down
1 change: 1 addition & 0 deletions src/js.zig
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ pub const OwnedBigInt64Array = typed_arrays.OwnedBigInt64Array;
pub const OwnedBigUint64Array = typed_arrays.OwnedBigUint64Array;

pub const Promise = @import("js/promise.zig").Promise;
pub const spawn = @import("js/async_task.zig").spawn;
pub const createPromise = @import("js/promise.zig").createPromise;

pub const NoAddonIdentity = @import("js/class_runtime.zig").NoAddonIdentity;
Expand Down
Loading
Loading