Skip to content
Merged
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
31 changes: 20 additions & 11 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 31 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,15 @@ output.blockingWriteAndFlush(chunk);
`[static]` methods are exposed on the resource class and `[constructor]` makes
the class callable with `new`.

Owned imported resources support `[Symbol.dispose]()` for deterministic
cleanup. Passing one to a WIT `own<T>` parameter transfers ownership and
invalidates the original wrapper. Borrowed wrappers expire when their component
call completes and cannot be transferred or disposed as owners. Resources lent
to an in-flight import remain alive and cannot be disposed or transferred until
that import completes. Ownership is restored for unconsumed stream/future writes
and for imported calls cancelled before they start. Await imports using an
incoming borrowed resource before returning from its export.

### Async Exports

Async exports are declared with the `async` keyword in WIT and implemented
Expand Down Expand Up @@ -405,7 +414,7 @@ world async-value {
```

```js
async function compute() {
export async function compute() {
const { readable, writable } = wit.Future();

// Write the value (fire-and-forget; completes when reader reads)
Expand All @@ -418,6 +427,16 @@ async function compute() {
**Future type constants** follow the same pattern: `wit.Future.U32`,
`wit.Future.STRING`, etc.

Future handles are deliberately not thenable: use `await readable.read()` to
obtain the payload. This keeps `return readable` in an async export from
implicitly consuming the future, and preserves nested future handles.
Repeated `read()` calls share the same Promise; a cancelled read rejects that
Promise and allows a subsequent read to retry.

`wit.Future.from(value, type)` adapts a value or Promise and returns
`{ readable, completion }`. Return its `readable` from an async export rather
than returning the payload Promise directly, which JavaScript would await.

**FutureReadable methods:**

| Method | Returns | Description |
Expand All @@ -436,17 +455,25 @@ async function compute() {

### Resource Cleanup

Stream and future handles support
Owned imported resources, stream endpoints, and future endpoints support
[Explicit Resource Management](https://github.com/tc39/proposal-explicit-resource-management)
via `Symbol.dispose`. In environments that support `using`:

```js
{
using stream = wit.Stream();
// stream.writable and stream.readable are auto-dropped when leaving scope
const stream = wit.Stream();
using writable = stream.writable;
using readable = stream.readable;
// Each endpoint is disposed when leaving scope.
}
```

The factory's `{ readable, writable }` pair is not itself disposable.
Complete an endpoint's pending operation (or cancel it and await its
settlement) before leaving its `using` scope or calling `.drop()`.
Host-requested task cancellation retains pending ABI buffers until the host
acknowledges each operation's completion or cancellation.

Otherwise, call `.drop()` explicitly to release handles.

## Node.js API
Expand Down
14 changes: 14 additions & 0 deletions crates/core/src/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,20 @@ impl<'a> EmitContext<'a> {
return { readable, completion };
};"#,
);
} else {
self.multiline(
r#"wit.Future.from = function(value, type) {
const { readable, writable } = wit.Future(type);
const completion = (async () => {
try {
await writable.write(await value);
} finally {
writable.drop();
}
})();
return { readable, completion };
};"#,
);
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/runtime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ publish = false
crate-type = ["cdylib"]

[dependencies]
rquickjs = { version = "0.12", default-features = false, features = ["bindgen", "disable-assertions", "loader", "std", "macro"] }
rquickjs = { version = "0.13", default-features = false, features = ["bindgen", "disable-assertions", "loader", "std", "macro"] }
wit-bindgen = "0.61"
wit-dylib-ffi = { version = "0.1.0", git = "https://github.com/bytecodealliance/wasm-tools", tag = "v1.258.0", default-features = false, features = ["async-raw"] }
heck = "0.5"
Expand Down
30 changes: 26 additions & 4 deletions crates/runtime/src/abi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,10 +184,10 @@ impl CopyEnd {
/// Validate that the end is idle and ready for a new read/write.
/// Returns `(handle, type_index)` on success.
pub(crate) fn begin_op(&self) -> rquickjs::Result<(u32, u32)> {
if self.state.copying() {
if self.state != CopyState::Idle {
return Err(rquickjs::Error::new_from_js(
self.kind.label(),
"operation while copy in progress",
"operation requires an idle endpoint",
));
}

Expand All @@ -198,6 +198,30 @@ impl CopyEnd {
Ok((h, self.type_index))
}

pub(crate) fn begin_transfer(&mut self, type_index: u32) -> rquickjs::Result<u32> {
if self.type_index != type_index {
return Err(rquickjs::Error::new_from_js(
self.kind.label(),
"matching WIT type",
));
}
let (handle, _) = self.begin_op()?;
self.handle = None;
self.state = CopyState::Done;
Ok(handle)
}

pub(crate) fn begin_drop(&mut self) -> rquickjs::Result<Option<u32>> {
if self.state.copying() {
return Err(rquickjs::Error::new_from_js(
self.kind.label(),
"cancel and await the active operation before dropping",
));
}
self.state = CopyState::Done;
Ok(self.handle.take())
}

/// Validate that the end has an active async copy that can be cancelled.
/// Returns `(handle, type_index)` on success.
pub(crate) fn begin_cancel(&self) -> rquickjs::Result<(u32, u32)> {
Expand Down Expand Up @@ -308,7 +332,6 @@ mod async_builtins {
pub(crate) fn subtask_drop(task: u32);

#[link_name = "[subtask-cancel]"]
#[allow(dead_code)]
pub(crate) fn subtask_cancel(task: u32) -> u32;

#[link_name = "[context-get-0]"]
Expand All @@ -325,7 +348,6 @@ mod async_builtins {
#[link(wasm_import_module = "[export]$root")]
unsafe extern "C" {
#[link_name = "[task-cancel]"]
#[allow(dead_code)]
pub(crate) fn task_cancel();

#[link_name = "[backpressure-set]"]
Expand Down
Loading
Loading