diff --git a/packages/gems/js/lib/js.rb b/packages/gems/js/lib/js.rb
index eaf0d34cbb..cbf1074485 100644
--- a/packages/gems/js/lib/js.rb
+++ b/packages/gems/js/lib/js.rb
@@ -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. \n" +
"Or "
@@ -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
diff --git a/packages/npm-packages/ruby-wasm-wasi/README.md b/packages/npm-packages/ruby-wasm-wasi/README.md
index b4ce77839f..8df1d6c449 100644
--- a/packages/npm-packages/ruby-wasm-wasi/README.md
+++ b/packages/npm-packages/ruby-wasm-wasi/README.md
@@ -18,6 +18,41 @@ See [Cheat Sheet](https://github.com/ruby/ruby.wasm/blob/main/docs/cheat_sheet.m
+### 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`
+ - `$0.stderr`
+
+- `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
@@ -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
@@ -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**:
diff --git a/packages/npm-packages/ruby-wasm-wasi/src/vm.ts b/packages/npm-packages/ruby-wasm-wasi/src/vm.ts
index 2ea09597bb..6631164acd 100644
--- a/packages/npm-packages/ruby-wasm-wasi/src/vm.ts
+++ b/packages/npm-packages/ruby-wasm-wasi/src/vm.ts
@@ -337,25 +337,8 @@ export class RubyVM {
*/
evalAsync(code: string): Promise {
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);
});
}
@@ -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);
@@ -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 {
+ 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"`.
@@ -702,6 +717,25 @@ const evalRbCode = (vm: RubyVM, privateObject: RubyVMPrivate, code: string) => {
});
};
+function newRbPromise(
+ vm: RubyVM,
+ privateObject: RubyVMPrivate,
+ body: (future: RbValue) => void,
+): Promise {
+ 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
*/
diff --git a/packages/npm-packages/ruby-wasm-wasi/test/eval_async.test.js b/packages/npm-packages/ruby-wasm-wasi/test/eval_async.test.js
index eaaeef89e0..209c3cd078 100644
--- a/packages/npm-packages/ruby-wasm-wasi/test/eval_async.test.js
+++ b/packages/npm-packages/ruby-wasm-wasi/test/eval_async.test.js
@@ -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",
);
});
});