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
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,36 @@ export async function greet(name) {
}
```

Exported resource methods and static methods may also be async. Implement them
as `async` members on the exported JavaScript class; resource constructors
remain synchronous:

```wit
package example:counter;

interface counter-api {
resource counter {
constructor(initial: u32);
add: async func(value: u32) -> u32;
create: static async func(initial: u32) -> counter;
}
}

world counter {
export counter-api;
}
```

```js
class Counter {
constructor(initial) { this.value = initial; }
async add(value) { this.value += value; return this.value; }
static async create(initial) { return new this(initial); }
}

export const counterApi = { Counter };
```

### Streams

Streams transfer a sequence of values between components. Lifted WIT streams
Expand Down
68 changes: 55 additions & 13 deletions crates/runtime/src/bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use crate::futures::{make_future, register_future_classes};
use crate::result::ResultBoundary;
use crate::streams::{make_stream, register_stream_classes};
use crate::task::Pending;
use crate::trivia::iface_lookup;
use crate::trivia::{fn_lookup, iface_lookup};
use crate::wit_imports::{FuncKind, WitInterface, classify, find_resource};
use crate::{DetHashSet, DetIndexMap, QjsCallContext, coerce_fn};

Expand Down Expand Up @@ -243,8 +243,8 @@ fn call_import<'js>(

/// Build the `asyncExports` object for the `__cqjs` namespace.
///
/// Each wrapper calls the user's export function, then chains `.then()` to
/// signal `task_return` back to the host.
/// Each wrapper calls the user's export function or resource member, then
/// chains `.then()` to signal `task_return` back to the host.
fn build_async_exports<'js>(
ctx: &rquickjs::Ctx<'js>,
wit_def: Wit,
Expand All @@ -255,30 +255,72 @@ fn build_async_exports<'js>(
let mut iface_objs: DetIndexMap<String, rquickjs::Object<'_>> = DetIndexMap::default();

for (func_index, func) in wit_def.iter_export_funcs().enumerate() {
let func_name = func.name().to_lower_camel_case();
let wrapper_name = func.name().to_lower_camel_case();
let func_name = func.name();
let kind = classify(func_name);
let iface_name = func
.interface()
.map(|interface| iface_lookup(ctx, interface).to_string());

let fn_name = func_name.clone();
let iface = iface_name.clone();

let wrapper = Function::new(
ctx.clone(),
coerce_fn(move |ctx: Ctx<'_>, args: Rest<Value<'_>>| {
let exports = ctx.user_module().exports(&ctx)?;
let export_scope = || {
if let Some(ref iface) = iface {
exports.get(iface.as_str())
} else {
Ok(exports.clone())
}
};

let user_fn: Function = if let Some(ref iface) = iface {
let iface_obj: rquickjs::Object = exports.get(iface.as_str())?;
iface_obj.get(fn_name.as_str())?
} else {
exports.get(fn_name.as_str())?
let mut values = args.0;
let (user_fn, this): (Function<'_>, Option<Value<'_>>) = match kind {
FuncKind::Freestanding => {
let scope: Object = export_scope()?;
(scope.get(fn_lookup(&ctx, func_name))?, None)
}
FuncKind::Constructor { .. } => {
return Err(rquickjs::Exception::throw_type(
&ctx,
"resource constructors cannot be async",
));
}
FuncKind::Method { method, .. } => {
if values.is_empty() {
return Err(rquickjs::Exception::throw_type(
&ctx,
"async resource method receiver is missing",
));
}
let receiver = values.remove(0);
let receiver_obj = receiver.as_object().ok_or_else(|| {
rquickjs::Exception::throw_type(
&ctx,
"async resource method receiver is not an object",
)
})?;
let method: Function = receiver_obj.get(fn_lookup(&ctx, method))?;
(method, Some(receiver))
}
FuncKind::Static { resource, method } => {
let scope: Object = export_scope()?;
let class_name = resource.to_upper_camel_case();
let class: Object = scope.get(class_name.as_str())?;
let method: Function = class.get(fn_lookup(&ctx, method))?;
(method, Some(class.into_value()))
}
};

let mut js_args = function::Args::new(ctx.clone(), args.0.len());
for arg in args.0 {
let mut js_args = function::Args::new(ctx.clone(), values.len());
for arg in values {
js_args.push_arg(arg)?;
}
if let Some(this) = this {
js_args.this(this)?;
}
let result = user_fn.call_arg::<Value>(js_args)?;

let promise_obj = result
Expand Down Expand Up @@ -350,7 +392,7 @@ fn build_async_exports<'js>(
.or_insert_with(|| rquickjs::Object::new(ctx.clone()).unwrap()),
None => &exports,
};
target.set(func_name.as_str(), wrapper)?;
target.set(wrapper_name.as_str(), wrapper)?;
}

for (name, obj) in iface_objs {
Expand Down
3 changes: 2 additions & 1 deletion crates/runtime/src/interpreter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,8 @@ impl Interpreter for QjsInterpreter {
let js_func: Function = class.get(method_name).unwrap_or_else(|err| {
panic!("static method '{method_name}' not found: {err:?}")
});
let args = cx.stack_into_args(ctx);
let mut args = cx.stack_into_args(ctx);
args.this(class).expect("failed to set static this");
call_export(ctx, func, method_name, js_func, args, cx);
}
FuncKind::Freestanding => {
Expand Down
Loading
Loading