Skip to content

Commit

Permalink
Merge pull request #363 from ruby/katei/async-call
Browse files Browse the repository at this point in the history
Add `RbValue#callAsync` JS API
  • Loading branch information
kateinoigakukun authored Jan 4, 2024
2 parents ba3a380 + a9c9c6a commit 84f85a3
Show file tree
Hide file tree
Showing 4 changed files with 173 additions and 32 deletions.
20 changes: 12 additions & 8 deletions packages/gems/js/lib/js.rb
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def await(promise)
)
if @loop == current
raise (
"JS::Object#await can be called only from RubyVM#evalAsync JS API\n" +
"JS::Object#await can be called only from RubyVM#evalAsync or RbValue#callAsync JS API\n" +
"If you are using browser.script.iife.js, please ensure that you specify `data-eval=\"async\"` in your script tag\n" +
"e.g. <script type=\"text/ruby\" data-eval=\"async\">puts :hello</script>\n" +
"Or <script type=\"text/ruby\" data-eval=\"async\" src=\"path/to/script.rb\"></script>"
Expand All @@ -105,15 +105,19 @@ def self.promise_scheduler
private

def self.__eval_async_rb(rb_code, future)
self.__async(future) do
JS::Object.wrap(Kernel.eval(rb_code.to_s, TOPLEVEL_BINDING, "eval_async"))
end
end

def self.__call_async_method(recv, method_name, future, *args)
self.__async(future) { recv.send(method_name.to_s, *args) }
end

def self.__async(future, &block)
Fiber
.new do
future.resolve JS::Object.wrap(
Kernel.eval(
rb_code.to_s,
TOPLEVEL_BINDING,
"eval_async"
)
)
future.resolve block.call
rescue => e
future.reject JS::Object.wrap(e)
end
Expand Down
68 changes: 67 additions & 1 deletion packages/npm-packages/ruby-wasm-wasi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,41 @@ See [Cheat Sheet](https://github.com/ruby/ruby.wasm/blob/main/docs/cheat_sheet.m

<!-- Generated by documentation.js. Update this documentation by updating the source code. -->

### consolePrinter

Create a console printer that can be used as an overlay of WASI imports.
See the example below for how to use it.

```javascript
const imports = {
wasi_snapshot_preview1: wasi.wasiImport,
};
const printer = consolePrinter();
printer.addToImports(imports);

const instance = await WebAssembly.instantiate(module, imports);
printer.setMemory(instance.exports.memory);
```

Note that the `stdout` and `stderr` functions are called with text, not
bytes. This means that bytes written to stdout/stderr will be decoded as
UTF-8 and then passed to the `stdout`/`stderr` functions every time a write
occurs without buffering.

#### Parameters

- `$0` **[Object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object)** (optional, default `{stdout:console.log,stderr:console.warn}`)

- `$0.stdout` &#x20;
- `$0.stderr` &#x20;

- `stdout` A function that will be called when stdout is written to.
Defaults to `console.log`.
- `stderr` A function that will be called when stderr is written to.
Defaults to `console.warn`.

Returns **any** An object that can be used as an overlay of WASI imports.

### RubyVM

A Ruby VM instance
Expand Down Expand Up @@ -46,7 +81,7 @@ Initialize the Ruby VM with the given command line arguments
##### Parameters

- `args` The command line arguments to pass to Ruby. Must be
an array of strings starting with the Ruby program name. (optional, default `["ruby.wasm","--disable-gems","-EUTF-8","-e_=0"]`)
an array of strings starting with the Ruby program name. (optional, default `["ruby.wasm","-EUTF-8","-e_=0"]`)

#### setInstance

Expand Down Expand Up @@ -151,6 +186,37 @@ ary.call("push", 4);
console.log(ary.call("sample").toString());
```

Returns **any** The result of the method call as a new RbValue.

#### callAsync

Call a given method that may call `JS::Object#await` with given arguments

##### Parameters

- `callee` name of the Ruby method to call
- `args` **...any** arguments to pass to the method. Must be an array of RbValue

##### Examples

```javascript
const client = vm.eval(`
require 'js'
class HttpClient
def get(url)
JS.global.fetch(url).await
end
end
HttpClient.new
`);
const response = await client.callAsync(
"get",
vm.eval(`"https://example.com"`),
);
```

Returns **any** A Promise that resolves to the result of the method call as a new RbValue.

#### toPrimitive

- **See**: <https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toPrimitive>
Expand Down
74 changes: 54 additions & 20 deletions packages/npm-packages/ruby-wasm-wasi/src/vm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -337,25 +337,8 @@ export class RubyVM {
*/
evalAsync(code: string): Promise<RbValue> {
const JS = this.eval("require 'js'; JS");
return new Promise((resolve, reject) => {
JS.call(
"__eval_async_rb",
this.wrap(code),
this.wrap({
resolve,
reject: (error: RbValue) => {
reject(
new RbError(
this.exceptionFormatter.format(
error,
this,
this.privateObject(),
),
),
);
},
}),
);
return newRbPromise(this, this.privateObject(), (future) => {
JS.call("__eval_async_rb", this.wrap(code), future);
});
}

Expand Down Expand Up @@ -454,12 +437,12 @@ export class RbValue {
*
* @param callee name of the Ruby method to call
* @param args arguments to pass to the method. Must be an array of RbValue
* @returns The result of the method call as a new RbValue.
*
* @example
* const ary = vm.eval("[1, 2, 3]");
* ary.call("push", 4);
* console.log(ary.call("sample").toString());
*
*/
call(callee: string, ...args: RbValue[]): RbValue {
const innerArgs = args.map((arg) => arg.inner);
Expand All @@ -470,6 +453,38 @@ export class RbValue {
);
}

/**
* Call a given method that may call `JS::Object#await` with given arguments
*
* @param callee name of the Ruby method to call
* @param args arguments to pass to the method. Must be an array of RbValue
* @returns A Promise that resolves to the result of the method call as a new RbValue.
*
* @example
* const client = vm.eval(`
* require 'js'
* class HttpClient
* def get(url)
* JS.global.fetch(url).await
* end
* end
* HttpClient.new
* `);
* const response = await client.callAsync("get", vm.eval(`"https://example.com"`));
*/
callAsync(callee: string, ...args: RbValue[]): Promise<RbValue> {
const JS = this.vm.eval("require 'js'; JS");
return newRbPromise(this.vm, this.privateObject, (future) => {
JS.call(
"__call_async_method",
this,
this.vm.wrap(callee),
future,
...args,
);
});
}

/**
* @see {@link https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toPrimitive}
* @param hint Preferred type of the result primitive value. `"number"`, `"string"`, or `"default"`.
Expand Down Expand Up @@ -702,6 +717,25 @@ const evalRbCode = (vm: RubyVM, privateObject: RubyVMPrivate, code: string) => {
});
};

function newRbPromise(
vm: RubyVM,
privateObject: RubyVMPrivate,
body: (future: RbValue) => void,
): Promise<RbValue> {
return new Promise((resolve, reject) => {
const future = vm.wrap({
resolve,
reject: (error: RbValue) => {
const rbError = new RbError(
privateObject.exceptionFormatter.format(error, vm, privateObject),
);
reject(rbError);
},
});
body(future);
});
}

/**
* Error class thrown by Ruby execution
*/
Expand Down
43 changes: 40 additions & 3 deletions packages/npm-packages/ruby-wasm-wasi/test/eval_async.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,49 @@ describe("Async Ruby code evaluation", () => {
expect(ret1.toString()).toBe("43");
});

test("await outside of evalAsync", async () => {
test("call async Ruby method from JS", async () => {
const vm = await initRubyVM();
const x = vm.eval(`
class X
def async_method
JS.global[:Promise].resolve(42).await
end
def async_method_with_args(a, b)
JS.global[:Promise].resolve(a + b).await
end
end
X.new
`);

const ret1 = await x.callAsync("async_method");
expect(ret1.toString()).toBe("42");

const ret2 = await x.callAsync(
"async_method_with_args",
vm.eval("1"),
vm.eval("2"),
);
expect(ret2.toString()).toBe("3");
});

test("await outside of evalAsync or callAsync", async () => {
const vm = await initRubyVM();
expect(() => {
vm.eval(`require "js"; JS.global[:Promise].resolve(42).await`);
}).toThrowError(
"JS::Object#await can be called only from RubyVM#evalAsync JS API",
}).toThrow(
"JS::Object#await can be called only from RubyVM#evalAsync or RbValue#callAsync JS API",
);

const x = vm.eval(`
class X
def async_method
JS.global[:Promise].resolve(42).await
end
end
X.new
`);
expect(() => x.call("async_method")).toThrow(
"JS::Object#await can be called only from RubyVM#evalAsync or RbValue#callAsync JS API",
);
});
});

0 comments on commit 84f85a3

Please sign in to comment.