mirror of
https://github.com/centau/vide.git
synced 2026-10-20 17:34:08 +00:00
Merge a74452f46e into a4151bd41e
This commit is contained in:
commit
a50777c390
2 changed files with 134 additions and 42 deletions
102
src/graph.luau
102
src/graph.luau
|
|
@ -98,15 +98,20 @@ local function push_cleanup<T>(node: Node<T>, cleanup: () -> ())
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function flush_cleanups<T>(node: Node<T>)
|
-- runs every cleanup even if one errors, returns the first error
|
||||||
|
local function flush_cleanups<T>(node: Node<T>): string?
|
||||||
|
local first_err: string?
|
||||||
|
|
||||||
if node.cleanups then
|
if node.cleanups then
|
||||||
for _, fn in node.cleanups do
|
for _, fn in node.cleanups do
|
||||||
local ok, err: string? = xpcall(fn, debug.traceback)
|
local ok, err: string? = xpcall(fn, debug.traceback)
|
||||||
if not ok then error(`cleanup error: {err}`, 0) end
|
if not ok and not first_err then first_err = `cleanup error: {err}` end
|
||||||
end
|
end
|
||||||
|
|
||||||
table.clear(node.cleanups)
|
table.clear(node.cleanups)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
return first_err
|
||||||
end
|
end
|
||||||
|
|
||||||
local function find_and_swap_pop<T>(t: { T }, v: T)
|
local function find_and_swap_pop<T>(t: { T }, v: T)
|
||||||
|
|
@ -125,12 +130,15 @@ local function unparent<T>(node: Node<T>)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function destroy<T>(node: Node<T>)
|
local destroy_owned: <T>(node: Node<T>) -> string?
|
||||||
|
|
||||||
|
-- always fully destroys the node, returns the first cleanup error
|
||||||
|
local function destroy_node<T>(node: Node<T>): string?
|
||||||
if flags.strict and table.find(scopes, node) then
|
if flags.strict and table.find(scopes, node) then
|
||||||
error("attempt to destroy an active scope", 0)
|
error("attempt to destroy an active scope", 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
flush_cleanups(node)
|
local err = flush_cleanups(node)
|
||||||
unparent(node)
|
unparent(node)
|
||||||
|
|
||||||
if node.owner then
|
if node.owner then
|
||||||
|
|
@ -138,70 +146,85 @@ local function destroy<T>(node: Node<T>)
|
||||||
node.owner = false
|
node.owner = false
|
||||||
end
|
end
|
||||||
|
|
||||||
if node.owned then
|
local owned_err = destroy_owned(node)
|
||||||
local owned = node.owned
|
return err or owned_err
|
||||||
while owned[1] do destroy(owned[1]) end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function destroy_owned<T>(node: Node<T>)
|
function destroy_owned<T>(node: Node<T>): string?
|
||||||
|
local err: string?
|
||||||
|
|
||||||
if node.owned then
|
if node.owned then
|
||||||
local owned = node.owned
|
local owned = node.owned
|
||||||
while owned[1] do destroy(owned[1]) end
|
while owned[1] do
|
||||||
|
local owned_err = destroy_node(owned[1])
|
||||||
|
err = err or owned_err
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return err
|
||||||
|
end
|
||||||
|
|
||||||
|
local function destroy<T>(node: Node<T>)
|
||||||
|
local err = destroy_node(node)
|
||||||
|
if err then error(err, 0) end
|
||||||
end
|
end
|
||||||
|
|
||||||
local update_queue = { n = 0 } :: { n: number, [number]: Node<any> }
|
local update_queue = { n = 0 } :: { n: number, [number]: Node<any> }
|
||||||
|
|
||||||
local function evaluate_node<T>(node: Node<T>)
|
-- returns whether the cached value changed, and an error instead of throwing
|
||||||
|
local function try_evaluate_node<T>(node: Node<T>): (boolean, string?)
|
||||||
if flags.strict then
|
if flags.strict then
|
||||||
if table.find(scopes, node) then
|
if table.find(scopes, node) then
|
||||||
error("a scope, that should rerun due to the update of a source, is already active", 0)
|
return false, "a scope, that should rerun due to the update of a source, is already active"
|
||||||
end
|
end
|
||||||
|
|
||||||
local initial_value = node.cache
|
local initial_value = node.cache
|
||||||
|
local err: string?
|
||||||
|
|
||||||
for i = 1, 2 do
|
for i = 1, 2 do
|
||||||
local cur_value = node.cache
|
local cur_value = node.cache
|
||||||
|
|
||||||
flush_cleanups(node)
|
local cleanup_err = flush_cleanups(node)
|
||||||
destroy_owned(node)
|
local owned_err = destroy_owned(node)
|
||||||
|
err = err or cleanup_err or owned_err
|
||||||
|
|
||||||
push_scope(node)
|
push_scope(node)
|
||||||
local ok, new_value = ycall(node.effect :: (T) -> T, cur_value)
|
local ok, new_value = ycall(node.effect :: (T) -> T, cur_value)
|
||||||
pop_scope()
|
pop_scope()
|
||||||
|
|
||||||
if not ok then
|
if not ok then
|
||||||
table.clear(update_queue)
|
return false, `effect error stacktrace\n{new_value :: string}`
|
||||||
update_queue.n = 0
|
|
||||||
error(`effect error stacktrace\n{new_value :: string}`, 0)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
node.cache = new_value :: T
|
node.cache = new_value :: T
|
||||||
end
|
end
|
||||||
|
|
||||||
return initial_value ~= node.cache
|
return initial_value ~= node.cache, err
|
||||||
else
|
else
|
||||||
local cur_value = node.cache
|
local cur_value = node.cache
|
||||||
|
|
||||||
flush_cleanups(node)
|
local cleanup_err = flush_cleanups(node)
|
||||||
destroy_owned(node)
|
local owned_err = destroy_owned(node)
|
||||||
|
|
||||||
push_scope(node)
|
push_scope(node)
|
||||||
local ok, new_value = xpcall(node.effect :: (T) -> T, effect_traceback, node.cache)
|
local ok, new_value = xpcall(node.effect :: (T) -> T, effect_traceback, node.cache)
|
||||||
pop_scope()
|
pop_scope()
|
||||||
|
|
||||||
if not ok then
|
if not ok then
|
||||||
table.clear(update_queue)
|
return false, new_value
|
||||||
update_queue.n = 0
|
|
||||||
error(new_value, 0)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
node.cache = new_value
|
node.cache = new_value
|
||||||
return cur_value ~= new_value
|
return cur_value ~= new_value, cleanup_err or owned_err
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function evaluate_node<T>(node: Node<T>): boolean
|
||||||
|
local changed, err = try_evaluate_node(node)
|
||||||
|
if err then error(err, 0) end
|
||||||
|
return changed
|
||||||
|
end
|
||||||
|
|
||||||
local function queue_children_for_update<T>(node: SourceNode<T>)
|
local function queue_children_for_update<T>(node: SourceNode<T>)
|
||||||
local i = update_queue.n
|
local i = update_queue.n
|
||||||
while node[1] do
|
while node[1] do
|
||||||
|
|
@ -216,21 +239,30 @@ local function get_update_queue_length()
|
||||||
return update_queue.n
|
return update_queue.n
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- queued nodes are already unparented, so every one must be evaluated even if
|
||||||
|
-- another errors; the first error is thrown after the queue is flushed
|
||||||
local function flush_update_queue(from: number)
|
local function flush_update_queue(from: number)
|
||||||
|
local first_err: string?
|
||||||
|
|
||||||
local i = from + 1
|
local i = from + 1
|
||||||
while i <= update_queue.n do
|
while i <= update_queue.n do
|
||||||
local node = update_queue[i]
|
local node = update_queue[i]
|
||||||
--assert(node.effect)
|
--assert(node.effect)
|
||||||
|
|
||||||
if node.owner and evaluate_node(node) then
|
-- check if node is still owned in case destroyed after queued
|
||||||
queue_children_for_update(node)
|
if node.owner then
|
||||||
|
local changed, err = try_evaluate_node(node)
|
||||||
|
if changed then queue_children_for_update(node) end
|
||||||
|
first_err = first_err or err
|
||||||
end
|
end
|
||||||
|
|
||||||
update_queue[i] = false :: any
|
update_queue[i] = false :: any -- false instead of nil to avoid sparse
|
||||||
i += 1
|
i += 1
|
||||||
end
|
end
|
||||||
|
|
||||||
update_queue.n = from
|
update_queue.n = from
|
||||||
|
|
||||||
|
if first_err then error(first_err, 0) end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function update_descendants<T>(root: SourceNode<T>)
|
local function update_descendants<T>(root: SourceNode<T>)
|
||||||
|
|
@ -239,21 +271,7 @@ local function update_descendants<T>(root: SourceNode<T>)
|
||||||
|
|
||||||
if flags.batch then return end
|
if flags.batch then return end
|
||||||
|
|
||||||
local i = n0 + 1
|
flush_update_queue(n0)
|
||||||
while i <= update_queue.n do
|
|
||||||
local node = update_queue[i]
|
|
||||||
--assert(node.effect)
|
|
||||||
|
|
||||||
-- check if node is still owned in case destroyed after queued
|
|
||||||
if node.owner and evaluate_node(node) then
|
|
||||||
queue_children_for_update(node)
|
|
||||||
end
|
|
||||||
|
|
||||||
update_queue[i] = false :: any -- false instead of nil to avoid sparse
|
|
||||||
i += 1
|
|
||||||
end
|
|
||||||
|
|
||||||
update_queue.n = n0
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function push_scope_as_child_of<T>(node: SourceNode<T>)
|
local function push_scope_as_child_of<T>(node: SourceNode<T>)
|
||||||
|
|
|
||||||
|
|
@ -633,6 +633,25 @@ TEST("effect()", wrap_root(function()
|
||||||
|
|
||||||
CHECK(count == 2)
|
CHECK(count == 2)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
do CASE "error does not stop other effects"
|
||||||
|
local src = source(0)
|
||||||
|
|
||||||
|
local count = 0
|
||||||
|
effect(function()
|
||||||
|
if src() == 1 then error("effect error") end
|
||||||
|
end)
|
||||||
|
effect(function()
|
||||||
|
src()
|
||||||
|
count += 1
|
||||||
|
end)
|
||||||
|
|
||||||
|
CHECK(count == 1)
|
||||||
|
CHECK(not pcall(src, 1))
|
||||||
|
CHECK(count == 2)
|
||||||
|
src(2)
|
||||||
|
CHECK(count == 3)
|
||||||
|
end
|
||||||
end))
|
end))
|
||||||
|
|
||||||
TEST("cleanup()", wrap_root(function()
|
TEST("cleanup()", wrap_root(function()
|
||||||
|
|
@ -709,6 +728,46 @@ TEST("cleanup()", wrap_root(function()
|
||||||
CHECK(ran.destroy)
|
CHECK(ran.destroy)
|
||||||
CHECK(ran.Destroy)
|
CHECK(ran.Destroy)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
do CASE "error does not stop destruction"
|
||||||
|
local src = source(0)
|
||||||
|
|
||||||
|
local cleaned = false
|
||||||
|
local count = 0
|
||||||
|
|
||||||
|
local destroy = root(function()
|
||||||
|
cleanup(function() error("cleanup error") end)
|
||||||
|
cleanup(function() cleaned = true end)
|
||||||
|
effect(function()
|
||||||
|
src()
|
||||||
|
count += 1
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
CHECK(not pcall(destroy))
|
||||||
|
CHECK(cleaned)
|
||||||
|
src(1)
|
||||||
|
CHECK(count == 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
do CASE "error on rerun does not stop updates"
|
||||||
|
local src = source(0)
|
||||||
|
|
||||||
|
local count = 0
|
||||||
|
effect(function()
|
||||||
|
src()
|
||||||
|
cleanup(function() if src() == 1 then error("cleanup error") end end)
|
||||||
|
end)
|
||||||
|
effect(function()
|
||||||
|
src()
|
||||||
|
count += 1
|
||||||
|
end)
|
||||||
|
|
||||||
|
CHECK(not pcall(src, 1))
|
||||||
|
CHECK(count == 2)
|
||||||
|
src(2)
|
||||||
|
CHECK(count == 3)
|
||||||
|
end
|
||||||
end))
|
end))
|
||||||
|
|
||||||
TEST("create()", wrap_root(function()
|
TEST("create()", wrap_root(function()
|
||||||
|
|
@ -3112,6 +3171,21 @@ TEST("strict", wrap_root(function()
|
||||||
CHECK(count == 4)
|
CHECK(count == 4)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
do CASE "effect updating its own source"
|
||||||
|
local src = source(0)
|
||||||
|
local count = 0
|
||||||
|
|
||||||
|
effect(function()
|
||||||
|
count += 1
|
||||||
|
if count > 100 then return end
|
||||||
|
local v = src()
|
||||||
|
if v > 0 then src(v + 1) end
|
||||||
|
end)
|
||||||
|
|
||||||
|
CHECK(not pcall(src, 1))
|
||||||
|
CHECK(count < 100)
|
||||||
|
end
|
||||||
|
|
||||||
do CASE "destruction of active scope"
|
do CASE "destruction of active scope"
|
||||||
local src = source(false)
|
local src = source(false)
|
||||||
local count = 0
|
local count = 0
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue