diff --git a/src/graph.luau b/src/graph.luau index b85b66e..6e2cb1d 100644 --- a/src/graph.luau +++ b/src/graph.luau @@ -43,6 +43,7 @@ end local function ycall(fn: (T) -> U, arg: T): (boolean, string|U) + local n = scopes.n local thread = coroutine.create(xpcall) --local function efn(err: string) return debug.traceback(err, 3) end local resume_ok, run_ok, result = coroutine.resume(thread, fn, efn, arg) @@ -50,7 +51,15 @@ local function ycall(fn: (T) -> U, arg: T): (boolean, string|U) assert(resume_ok) if coroutine.status(thread) ~= "dead" then - return false, debug.traceback(thread, "attempt to yield in reactive scope") + local trace = debug.traceback(thread, "attempt to yield in reactive scope") + + -- the thread can still be resumed (e.g. by task.wait), so stop it and + -- pop the scopes it pushed before yielding + if task then task.cancel(thread) else coroutine.close(thread) end + for i = scopes.n, n + 1, -1 do scopes[i] = nil end + scopes.n = n + + return false, trace end return run_ok, result diff --git a/test/tests.luau b/test/tests.luau index cd9cd1d..d565237 100644 --- a/test/tests.luau +++ b/test/tests.luau @@ -3009,6 +3009,27 @@ TEST("strict", wrap_root(function() CHECK(not ok) end + do CASE "yield in a component" + local input = source(false) + local thread + local resumed = false + + local destroy = root(function() + show(input, function() + thread = coroutine.running() + coroutine.yield() + resumed = true + return {} + end) + end) + + CHECK(not pcall(input, true)) + CHECK(not coroutine.resume(thread)) + CHECK(not resumed) + CHECK(pcall(input, false)) + CHECK(pcall(destroy)) + end + do CASE "run derived callback twice" local src = source(1) local count = 0