Skip to content

Commit 84f85a3

Browse files
Merge pull request #363 from ruby/katei/async-call
Add `RbValue#callAsync` JS API
2 parents ba3a380 + a9c9c6a commit 84f85a3

File tree

4 files changed

+173
-32
lines changed

4 files changed

+173
-32
lines changed

packages/gems/js/lib/js.rb

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ def await(promise)
8484
)
8585
if @loop == current
8686
raise (
87-
"JS::Object#await can be called only from RubyVM#evalAsync JS API\n" +
87+
"JS::Object#await can be called only from RubyVM#evalAsync or RbValue#callAsync JS API\n" +
8888
"If you are using browser.script.iife.js, please ensure that you specify `data-eval=\"async\"` in your script tag\n" +
8989
"e.g. <script type=\"text/ruby\" data-eval=\"async\">puts :hello</script>\n" +
9090
"Or <script type=\"text/ruby\" data-eval=\"async\" src=\"path/to/script.rb\"></script>"
@@ -105,15 +105,19 @@ def self.promise_scheduler
105105
private
106106

107107
def self.__eval_async_rb(rb_code, future)
108+
self.__async(future) do
109+
JS::Object.wrap(Kernel.eval(rb_code.to_s, TOPLEVEL_BINDING, "eval_async"))
110+
end
111+
end
112+
113+
def self.__call_async_method(recv, method_name, future, *args)
114+
self.__async(future) { recv.send(method_name.to_s, *args) }
115+
end
116+
117+
def self.__async(future, &block)
108118
Fiber
109119
.new do
110-
future.resolve JS::Object.wrap(
111-
Kernel.eval(
112-
rb_code.to_s,
113-
TOPLEVEL_BINDING,
114-
"eval_async"
115-
)
116-
)
120+
future.resolve block.call
117121
rescue => e
118122
future.reject JS::Object.wrap(e)
119123
end

packages/npm-packages/ruby-wasm-wasi/README.md

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,41 @@ See [Cheat Sheet](https://github.com/ruby/ruby.wasm/blob/main/docs/cheat_sheet.m
1818

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

21+
### consolePrinter
22+
23+
Create a console printer that can be used as an overlay of WASI imports.
24+
See the example below for how to use it.
25+
26+
```javascript
27+
const imports = {
28+
wasi_snapshot_preview1: wasi.wasiImport,
29+
};
30+
const printer = consolePrinter();
31+
printer.addToImports(imports);
32+
33+
const instance = await WebAssembly.instantiate(module, imports);
34+
printer.setMemory(instance.exports.memory);
35+
```
36+
37+
Note that the `stdout` and `stderr` functions are called with text, not
38+
bytes. This means that bytes written to stdout/stderr will be decoded as
39+
UTF-8 and then passed to the `stdout`/`stderr` functions every time a write
40+
occurs without buffering.
41+
42+
#### Parameters
43+
44+
- `$0` **[Object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object)** (optional, default `{stdout:console.log,stderr:console.warn}`)
45+
46+
- `$0.stdout` &#x20;
47+
- `$0.stderr` &#x20;
48+
49+
- `stdout` A function that will be called when stdout is written to.
50+
Defaults to `console.log`.
51+
- `stderr` A function that will be called when stderr is written to.
52+
Defaults to `console.warn`.
53+
54+
Returns **any** An object that can be used as an overlay of WASI imports.
55+
2156
### RubyVM
2257

2358
A Ruby VM instance
@@ -46,7 +81,7 @@ Initialize the Ruby VM with the given command line arguments
4681
##### Parameters
4782

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

5186
#### setInstance
5287

@@ -151,6 +186,37 @@ ary.call("push", 4);
151186
console.log(ary.call("sample").toString());
152187
```
153188

189+
Returns **any** The result of the method call as a new RbValue.
190+
191+
#### callAsync
192+
193+
Call a given method that may call `JS::Object#await` with given arguments
194+
195+
##### Parameters
196+
197+
- `callee` name of the Ruby method to call
198+
- `args` **...any** arguments to pass to the method. Must be an array of RbValue
199+
200+
##### Examples
201+
202+
```javascript
203+
const client = vm.eval(`
204+
require 'js'
205+
class HttpClient
206+
def get(url)
207+
JS.global.fetch(url).await
208+
end
209+
end
210+
HttpClient.new
211+
`);
212+
const response = await client.callAsync(
213+
"get",
214+
vm.eval(`"https://example.com"`),
215+
);
216+
```
217+
218+
Returns **any** A Promise that resolves to the result of the method call as a new RbValue.
219+
154220
#### toPrimitive
155221

156222
- **See**: <https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toPrimitive>

packages/npm-packages/ruby-wasm-wasi/src/vm.ts

Lines changed: 54 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -337,25 +337,8 @@ export class RubyVM {
337337
*/
338338
evalAsync(code: string): Promise<RbValue> {
339339
const JS = this.eval("require 'js'; JS");
340-
return new Promise((resolve, reject) => {
341-
JS.call(
342-
"__eval_async_rb",
343-
this.wrap(code),
344-
this.wrap({
345-
resolve,
346-
reject: (error: RbValue) => {
347-
reject(
348-
new RbError(
349-
this.exceptionFormatter.format(
350-
error,
351-
this,
352-
this.privateObject(),
353-
),
354-
),
355-
);
356-
},
357-
}),
358-
);
340+
return newRbPromise(this, this.privateObject(), (future) => {
341+
JS.call("__eval_async_rb", this.wrap(code), future);
359342
});
360343
}
361344

@@ -454,12 +437,12 @@ export class RbValue {
454437
*
455438
* @param callee name of the Ruby method to call
456439
* @param args arguments to pass to the method. Must be an array of RbValue
440+
* @returns The result of the method call as a new RbValue.
457441
*
458442
* @example
459443
* const ary = vm.eval("[1, 2, 3]");
460444
* ary.call("push", 4);
461445
* console.log(ary.call("sample").toString());
462-
*
463446
*/
464447
call(callee: string, ...args: RbValue[]): RbValue {
465448
const innerArgs = args.map((arg) => arg.inner);
@@ -470,6 +453,38 @@ export class RbValue {
470453
);
471454
}
472455

456+
/**
457+
* Call a given method that may call `JS::Object#await` with given arguments
458+
*
459+
* @param callee name of the Ruby method to call
460+
* @param args arguments to pass to the method. Must be an array of RbValue
461+
* @returns A Promise that resolves to the result of the method call as a new RbValue.
462+
*
463+
* @example
464+
* const client = vm.eval(`
465+
* require 'js'
466+
* class HttpClient
467+
* def get(url)
468+
* JS.global.fetch(url).await
469+
* end
470+
* end
471+
* HttpClient.new
472+
* `);
473+
* const response = await client.callAsync("get", vm.eval(`"https://example.com"`));
474+
*/
475+
callAsync(callee: string, ...args: RbValue[]): Promise<RbValue> {
476+
const JS = this.vm.eval("require 'js'; JS");
477+
return newRbPromise(this.vm, this.privateObject, (future) => {
478+
JS.call(
479+
"__call_async_method",
480+
this,
481+
this.vm.wrap(callee),
482+
future,
483+
...args,
484+
);
485+
});
486+
}
487+
473488
/**
474489
* @see {@link https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toPrimitive}
475490
* @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) => {
702717
});
703718
};
704719

720+
function newRbPromise(
721+
vm: RubyVM,
722+
privateObject: RubyVMPrivate,
723+
body: (future: RbValue) => void,
724+
): Promise<RbValue> {
725+
return new Promise((resolve, reject) => {
726+
const future = vm.wrap({
727+
resolve,
728+
reject: (error: RbValue) => {
729+
const rbError = new RbError(
730+
privateObject.exceptionFormatter.format(error, vm, privateObject),
731+
);
732+
reject(rbError);
733+
},
734+
});
735+
body(future);
736+
});
737+
}
738+
705739
/**
706740
* Error class thrown by Ruby execution
707741
*/

packages/npm-packages/ruby-wasm-wasi/test/eval_async.test.js

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,49 @@ describe("Async Ruby code evaluation", () => {
3030
expect(ret1.toString()).toBe("43");
3131
});
3232

33-
test("await outside of evalAsync", async () => {
33+
test("call async Ruby method from JS", async () => {
34+
const vm = await initRubyVM();
35+
const x = vm.eval(`
36+
class X
37+
def async_method
38+
JS.global[:Promise].resolve(42).await
39+
end
40+
def async_method_with_args(a, b)
41+
JS.global[:Promise].resolve(a + b).await
42+
end
43+
end
44+
X.new
45+
`);
46+
47+
const ret1 = await x.callAsync("async_method");
48+
expect(ret1.toString()).toBe("42");
49+
50+
const ret2 = await x.callAsync(
51+
"async_method_with_args",
52+
vm.eval("1"),
53+
vm.eval("2"),
54+
);
55+
expect(ret2.toString()).toBe("3");
56+
});
57+
58+
test("await outside of evalAsync or callAsync", async () => {
3459
const vm = await initRubyVM();
3560
expect(() => {
3661
vm.eval(`require "js"; JS.global[:Promise].resolve(42).await`);
37-
}).toThrowError(
38-
"JS::Object#await can be called only from RubyVM#evalAsync JS API",
62+
}).toThrow(
63+
"JS::Object#await can be called only from RubyVM#evalAsync or RbValue#callAsync JS API",
64+
);
65+
66+
const x = vm.eval(`
67+
class X
68+
def async_method
69+
JS.global[:Promise].resolve(42).await
70+
end
71+
end
72+
X.new
73+
`);
74+
expect(() => x.call("async_method")).toThrow(
75+
"JS::Object#await can be called only from RubyVM#evalAsync or RbValue#callAsync JS API",
3976
);
4077
});
4178
});

0 commit comments

Comments
 (0)