Skip to content

Commit

Permalink
Prohibit await in non-async context and remove unnecessary loop fiber
Browse files Browse the repository at this point in the history
Some part of this change are derived from
#205 by @tmtm

Co-authored-by: TOMITA Masahiro <tommy@tmtm.org>
  • Loading branch information
kateinoigakukun and tmtm committed May 18, 2023
1 parent ac38a84 commit 32cbe2f
Show file tree
Hide file tree
Showing 3 changed files with 21 additions and 32 deletions.
36 changes: 8 additions & 28 deletions ext/js/lib/js.rb
Expand Up @@ -39,42 +39,22 @@ module JS
Null = JS.eval("return null")

class PromiseScheduler
Task = Struct.new(:fiber, :status, :value)

def initialize(main_fiber)
@tasks = []
@is_spinning = false
@loop_fiber =
Fiber.new do
loop do
while task = @tasks.shift
task.fiber.transfer(task.value, task.status)
end
@is_spinning = false
main_fiber.transfer
end
end
def initialize(loop)
@loop = loop
end

def await(promise)
current = Fiber.current
promise.call(
:then,
->(value) { enqueue Task.new(current, :success, value) },
->(value) { enqueue Task.new(current, :failure, value) }
->(value) { current.transfer(value, :success) },
->(value) { current.transfer(value, :failure) }
)
value, status = @loop_fiber.transfer
raise "JS::Object#await can be called only from evalAsync" if @loop == current
value, status = @loop.transfer
raise JS::Error.new(value) if status == :failure
value
end

def enqueue(task)
@tasks << task
unless @is_spinning
@is_spinning = true
JS.global.queueMicrotask -> { @loop_fiber.transfer }
end
end
end

@promise_scheduler = PromiseScheduler.new Fiber.current
Expand Down Expand Up @@ -120,8 +100,8 @@ def respond_to_missing?(sym, include_private)
# This method looks like a synchronous method, but it actually runs asynchronously using fibers.
# In other words, the next line to the `await` call at Ruby source will be executed after the
# promise will be resolved. However, it does not block JavaScript event loop, so the next line
# to the `RubyVM.eval` or `RubyVM.evalAsync` (in the case when no `await` operator before the
# call expression) at JavaScript source will be executed without waiting for the promise.
# to the RubyVM.evalAsync` (in the case when no `await` operator before the call expression)
# at JavaScript source will be executed without waiting for the promise.
#
# The below example shows how the execution order goes. It goes in the order of "step N"
#
Expand Down
7 changes: 3 additions & 4 deletions packages/npm-packages/ruby-wasm-wasi/test/eval_async.test.ts
Expand Up @@ -32,9 +32,8 @@ describe("Async Ruby code evaluation", () => {

test("await outside of evalAsync", async () => {
const vm = await initRubyVM();
const result = vm.eval(
`require "js"; JS.global[:Promise].resolve(42).await`
);
expect(result.call("nil?").toString()).toBe("true");
expect(() => {
vm.eval(`require "js"; JS.global[:Promise].resolve(42).await`);
}).toThrowError("JS::Object#await can be called only from evalAsync");
});
});
10 changes: 10 additions & 0 deletions packages/npm-packages/ruby-wasm-wasi/test/unit/test_async.rb
Expand Up @@ -42,4 +42,14 @@ def test_concurrent_promises
assert_equal 43, pac1[:promise].await.to_i
assert_equal 42, pac0[:promise].await.to_i
end

def test_await_in_fiber
fiber_ended = false
Fiber.new do
promise = JS.eval("return Promise.resolve(42)")
assert_equal 42, promise.await.to_i
fiber_ended = true
end.resume
assert_equal true, fiber_ended
end
end

0 comments on commit 32cbe2f

Please sign in to comment.