mirror of
https://github.com/centau/vide.git
synced 2026-08-20 14:41:37 +00:00
Merge ff020bacb8 into f3bfc65607
This commit is contained in:
commit
c3c1599148
3 changed files with 282 additions and 109 deletions
185
src/graph.luau
185
src/graph.luau
|
|
@ -7,7 +7,7 @@ export type SourceNode<T> = {
|
||||||
|
|
||||||
export type Node<T> = {
|
export type Node<T> = {
|
||||||
cache: T,
|
cache: T,
|
||||||
effect: ((T) -> T) | false,
|
effect: ((T) -> T) | false,
|
||||||
cleanups: { () -> () } | false,
|
cleanups: { () -> () } | false,
|
||||||
|
|
||||||
context: { [number]: unknown } | false,
|
context: { [number]: unknown } | false,
|
||||||
|
|
@ -25,13 +25,11 @@ local function efn(err: string)
|
||||||
local trace = debug.traceback(err, 2)
|
local trace = debug.traceback(err, 2)
|
||||||
|
|
||||||
if string.find(err, "^effect error stacktrace") then -- if effect error is nested
|
if string.find(err, "^effect error stacktrace") then -- if effect error is nested
|
||||||
trace = string.gsub(" " .. trace, "\n", function() -- indent entire error
|
trace = string.gsub(" " .. trace, "\n", "\n ") -- indent entire error
|
||||||
return "\n "
|
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
trace ..= "\nsource update stacktrace:"
|
trace ..= "\nsource update stacktrace:"
|
||||||
return trace
|
return trace
|
||||||
end
|
end
|
||||||
|
|
||||||
local function ycall<T, U>(fn: (T) -> U, arg: T): (boolean, string|U)
|
local function ycall<T, U>(fn: (T) -> U, arg: T): (boolean, string|U)
|
||||||
|
|
@ -41,7 +39,7 @@ local function ycall<T, U>(fn: (T) -> U, arg: T): (boolean, string|U)
|
||||||
local resume_ok, run_ok, result = coroutine.resume(thread, fn, efn, arg)
|
local resume_ok, run_ok, result = coroutine.resume(thread, fn, efn, arg)
|
||||||
|
|
||||||
assert(resume_ok)
|
assert(resume_ok)
|
||||||
|
|
||||||
if coroutine.status(thread) ~= "dead" then
|
if coroutine.status(thread) ~= "dead" then
|
||||||
return false, debug.traceback(thread, "attempt to yield in reactive scope")
|
return false, debug.traceback(thread, "attempt to yield in reactive scope")
|
||||||
end
|
end
|
||||||
|
|
@ -125,7 +123,7 @@ local function destroy<T>(node: Node<T>)
|
||||||
|
|
||||||
flush_cleanups(node)
|
flush_cleanups(node)
|
||||||
unparent(node)
|
unparent(node)
|
||||||
|
|
||||||
if node.owner then
|
if node.owner then
|
||||||
find_and_swap_pop(node.owner.owned :: { Node<T> }, node)
|
find_and_swap_pop(node.owner.owned :: { Node<T> }, node)
|
||||||
node.owner = false
|
node.owner = false
|
||||||
|
|
@ -144,9 +142,19 @@ local function destroy_owned<T>(node: Node<T>)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local update_queue = { n = 0 } :: { n: number, [number]: Node<any> }
|
local update_queue_n = 0
|
||||||
|
local in_queue = {}
|
||||||
|
local update_queue = {} :: { [number]: Node<any> }
|
||||||
|
local node_deps_left = {} -- any node that goes into the update queue needs to store how many dependencies it's waiting on
|
||||||
|
|
||||||
local function evaluate_node<T>(node: Node<T>)
|
local function reset_queue()
|
||||||
|
update_queue_n = 0
|
||||||
|
table.clear(in_queue)
|
||||||
|
table.clear(update_queue)
|
||||||
|
table.clear(node_deps_left)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function evaluate_node<T>(node: Node<T>, continue_on_error: boolean) -- if continue_on_error, the calling function wishes to continue even if the update fails (without using pcall)
|
||||||
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)
|
error("a scope, that should rerun due to the update of a source, is already active", 0)
|
||||||
|
|
@ -159,15 +167,19 @@ local function evaluate_node<T>(node: Node<T>)
|
||||||
|
|
||||||
flush_cleanups(node)
|
flush_cleanups(node)
|
||||||
destroy_owned(node)
|
destroy_owned(node)
|
||||||
|
|
||||||
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)
|
local msg = debug.traceback(`effect error: {new_value}`, 2)
|
||||||
update_queue.n = 0
|
if continue_on_error then
|
||||||
error(`effect error stacktrace\n{new_value :: string}`, 0)
|
print(msg)
|
||||||
|
return false
|
||||||
|
else
|
||||||
|
error(msg, 2)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
node.cache = new_value :: T
|
node.cache = new_value :: T
|
||||||
|
|
@ -181,72 +193,137 @@ local function evaluate_node<T>(node: Node<T>)
|
||||||
destroy_owned(node)
|
destroy_owned(node)
|
||||||
|
|
||||||
push_scope(node)
|
push_scope(node)
|
||||||
local ok, new_value = pcall(node.effect :: (T) -> T, node.cache)
|
local ok, new_value = pcall(node.effect :: (T) -> T, cur_value)
|
||||||
pop_scope()
|
pop_scope()
|
||||||
|
|
||||||
if not ok then
|
if not ok then
|
||||||
table.clear(update_queue)
|
local msg = debug.traceback(`effect error: {new_value}`, 2)
|
||||||
update_queue.n = 0
|
if continue_on_error then
|
||||||
error(`effect error:\n{new_value}\n`, 0)
|
print(msg)
|
||||||
|
return false
|
||||||
|
else
|
||||||
|
error(msg, 2)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
node.cache = new_value
|
node.cache = new_value
|
||||||
return cur_value ~= new_value
|
return cur_value ~= new_value
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function queue_children_for_update<T>(node: SourceNode<T>)
|
local function add_dependency_counts(node)
|
||||||
local i = update_queue.n
|
for i = 1, #node do
|
||||||
while node[1] do
|
local child = node[i]
|
||||||
i += 1
|
if not child.owner then continue end -- we won't be updating this anyway
|
||||||
update_queue[i] = node[1]
|
local deps_left = node_deps_left[child]
|
||||||
unparent(node[1])
|
if deps_left then
|
||||||
|
node_deps_left[child] = deps_left + 1
|
||||||
|
else
|
||||||
|
node_deps_left[child] = 1
|
||||||
|
add_dependency_counts(child) -- only recurse if we haven't seen this node before
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local function remove_dependency_counts(node) -- to be called when updating a node if its value remains unchanged
|
||||||
|
for i = 1, #node do
|
||||||
|
local child = node[i]
|
||||||
|
if not node.owner then continue end -- we won't be updating this anyway
|
||||||
|
local deps_left = node_deps_left[child]
|
||||||
|
if deps_left > 1 then
|
||||||
|
node_deps_left[child] = deps_left - 1
|
||||||
|
else
|
||||||
|
node_deps_left[child] = 0
|
||||||
|
-- only recurse if we're removing the last dependency *and* if we aren't planning on updating it
|
||||||
|
if not in_queue[child] then
|
||||||
|
remove_dependency_counts(child)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function queue_children_for_update<T>(node: SourceNode<T>)
|
||||||
|
for i = 1, #node do
|
||||||
|
local child = node[i]
|
||||||
|
local deps_left = node_deps_left[child]
|
||||||
|
if deps_left then
|
||||||
|
node_deps_left[child] = deps_left - 1
|
||||||
|
else
|
||||||
|
-- This can happen if child adds parent as a new dependency after the initial update
|
||||||
|
node_deps_left[child] = 0
|
||||||
|
end
|
||||||
|
if not in_queue[child] then
|
||||||
|
in_queue[child] = true
|
||||||
|
update_queue_n += 1
|
||||||
|
update_queue[update_queue_n] = child
|
||||||
|
end
|
||||||
end
|
end
|
||||||
update_queue.n = i
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function get_update_queue_length()
|
local function get_update_queue_length()
|
||||||
return update_queue.n
|
return update_queue_n
|
||||||
end
|
end
|
||||||
|
|
||||||
local function flush_update_queue(from: number)
|
local function flush_update_queue(from: number)
|
||||||
local i = from + 1
|
while true do
|
||||||
while i <= update_queue.n do
|
local i = from
|
||||||
local node = update_queue[i]
|
if i >= update_queue_n then return end -- nothing more to do
|
||||||
--assert(node.effect)
|
local changed = false
|
||||||
|
local moveTo1 = from -- 1 less than where to store an update if we can't process it yet
|
||||||
|
while i < update_queue_n do
|
||||||
|
i += 1
|
||||||
|
local node = update_queue[i]
|
||||||
|
-- assert(node.effect)
|
||||||
|
if node.owner then
|
||||||
|
local deps_left = node_deps_left[node]
|
||||||
|
if deps_left > 0 then -- waiting on more dependencies
|
||||||
|
moveTo1 += 1
|
||||||
|
if i ~= moveTo1 then
|
||||||
|
update_queue[moveTo1] = node
|
||||||
|
update_queue[i] = false :: any
|
||||||
|
end
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
changed = true
|
||||||
|
-- Reset in_queue and node_deps_left before evaluation in case the node's depedencies are changed during evaluation (which usually implies an infinite loop, but we have explicit errors for this case)
|
||||||
|
in_queue[node] = nil
|
||||||
|
node_deps_left[node] = nil
|
||||||
|
unparent(node)
|
||||||
|
if evaluate_node(node, true) then
|
||||||
|
queue_children_for_update(node)
|
||||||
|
else
|
||||||
|
remove_dependency_counts(node)
|
||||||
|
end
|
||||||
|
else -- otherwise node was cleaned up
|
||||||
|
in_queue[node] = nil
|
||||||
|
node_deps_left[node] = nil
|
||||||
|
end
|
||||||
|
|
||||||
if node.owner and evaluate_node(node) then
|
update_queue[i] = false :: any
|
||||||
queue_children_for_update(node)
|
end
|
||||||
|
if moveTo1 == 0 then -- no pending updates
|
||||||
|
reset_queue()
|
||||||
|
return
|
||||||
|
else
|
||||||
|
update_queue_n = moveTo1
|
||||||
|
if not changed then -- Can occur in recursive updates (where 'from' is > 0)
|
||||||
|
if from == 0 then
|
||||||
|
print("Some nodes failed to update", update_queue, node_deps_left, debug.traceback())
|
||||||
|
reset_queue()
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
update_queue[i] = false :: any
|
|
||||||
i += 1
|
|
||||||
end
|
end
|
||||||
|
|
||||||
update_queue.n = from
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function update_descendants<T>(root: SourceNode<T>)
|
local function update_descendants<T>(root: SourceNode<T>)
|
||||||
local n0 = update_queue.n
|
add_dependency_counts(root)
|
||||||
|
local n0 = update_queue_n
|
||||||
queue_children_for_update(root)
|
queue_children_for_update(root)
|
||||||
|
|
||||||
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>)
|
||||||
|
|
|
||||||
204
test/tests.luau
204
test/tests.luau
|
|
@ -116,7 +116,7 @@ TEST("graph", function()
|
||||||
effect(c.cache)
|
effect(c.cache)
|
||||||
|
|
||||||
pop_scope()
|
pop_scope()
|
||||||
|
|
||||||
CHECK(count == 1)
|
CHECK(count == 1)
|
||||||
update_descendants(a)
|
update_descendants(a)
|
||||||
CHECK(count == 2)
|
CHECK(count == 2)
|
||||||
|
|
@ -188,7 +188,7 @@ TEST("graph", function()
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
do push_scope(root)
|
do push_scope(root)
|
||||||
clean "root"
|
clean "root"
|
||||||
items_updated = node(root)
|
items_updated = node(root)
|
||||||
push_scope_as_child_of(items_updated) -- should not
|
push_scope_as_child_of(items_updated) -- should not
|
||||||
|
|
@ -216,9 +216,9 @@ TEST("graph", function()
|
||||||
pop_scope() end
|
pop_scope() end
|
||||||
pop_scope() end
|
pop_scope() end
|
||||||
pop_scope() end
|
pop_scope() end
|
||||||
pop_scope() end
|
pop_scope() end
|
||||||
pop_scope() end
|
pop_scope() end
|
||||||
|
|
||||||
|
|
||||||
-- verify graph
|
-- verify graph
|
||||||
|
|
||||||
|
|
@ -226,7 +226,7 @@ TEST("graph", function()
|
||||||
local c = get_children(items_updated)
|
local c = get_children(items_updated)
|
||||||
CHECK(#c == 0)
|
CHECK(#c == 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
do
|
do
|
||||||
local c = get_children(root)
|
local c = get_children(root)
|
||||||
CHECK(#c == 0)
|
CHECK(#c == 0)
|
||||||
|
|
@ -298,7 +298,7 @@ TEST("graph", function()
|
||||||
|
|
||||||
depth=1
|
depth=1
|
||||||
_, _ <- attempt to update_descendants nothing
|
_, _ <- attempt to update_descendants nothing
|
||||||
^
|
^
|
||||||
|
|
||||||
]]
|
]]
|
||||||
|
|
||||||
|
|
@ -319,6 +319,102 @@ TEST("graph", function()
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
TEST("duplicate update", function()
|
||||||
|
local cases = {
|
||||||
|
{
|
||||||
|
name = "b",
|
||||||
|
err = "b",
|
||||||
|
e1 = {1, 2, 2, 2, 2},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name = "c",
|
||||||
|
err = "c",
|
||||||
|
e1 = {2, 1, 1, 2, 2},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name = "c2",
|
||||||
|
err = "c2",
|
||||||
|
e1 = {2, 2, 1, 2, 2},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name = "d",
|
||||||
|
err = "d",
|
||||||
|
e1 = {2, 2, 2, 1, 2},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name = "e",
|
||||||
|
err = "e",
|
||||||
|
e1 = {2, 2, 2, 2, 1},
|
||||||
|
e2 = {3, 3, 3, 3, 3}, -- e has a dependency on just 'a' after the first time, so it'll get updated 2x (only way to prevent this extra update is to yield, which isn't currently supported)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name = "no error",
|
||||||
|
err = "",
|
||||||
|
e1 = {2, 2, 2, 2, 2},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, case in cases do
|
||||||
|
CASE(case.name)
|
||||||
|
local destroy = root(function()
|
||||||
|
local e1, e2, err = case.e1, case.e2, case.err
|
||||||
|
if not e2 then
|
||||||
|
e2 = {}
|
||||||
|
for i, v in e1 do
|
||||||
|
e2[i] = v + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local a = source(0)
|
||||||
|
local nb = 0
|
||||||
|
local b = derive(function()
|
||||||
|
if err == "b" and a() == 1 then error("user error") end
|
||||||
|
nb += 1
|
||||||
|
return a() + 1
|
||||||
|
end)
|
||||||
|
local nc = 0
|
||||||
|
local c = derive(function()
|
||||||
|
if err == "c" and a() == 1 then error("user error") end
|
||||||
|
nc += 1
|
||||||
|
return a() * 2
|
||||||
|
end)
|
||||||
|
local nc2 = 0
|
||||||
|
local c2 = derive(function()
|
||||||
|
if err == "c2" and c() == 2 then error("user error") end
|
||||||
|
nc2 += 1
|
||||||
|
return c()
|
||||||
|
end)
|
||||||
|
local nd = 0
|
||||||
|
local d = derive(function()
|
||||||
|
if err == "d" and b() == 2 then error("user error") end
|
||||||
|
nd += 1
|
||||||
|
return b() * 100 + c2()
|
||||||
|
end)
|
||||||
|
local ne = 0
|
||||||
|
effect(function()
|
||||||
|
if err == "e" and a() == 1 then error("user error") end
|
||||||
|
ne += 1
|
||||||
|
a(); b(); c(); c2(); d()
|
||||||
|
end)
|
||||||
|
local length = graph.get_update_queue_length()
|
||||||
|
|
||||||
|
a(1)
|
||||||
|
CHECK(nb == e1[1])
|
||||||
|
CHECK(nc == e1[2])
|
||||||
|
CHECK(nc2 == e1[3])
|
||||||
|
CHECK(nd == e1[4])
|
||||||
|
CHECK(ne == e1[5])
|
||||||
|
CHECK(graph.get_update_queue_length() == length)
|
||||||
|
|
||||||
|
a(2)
|
||||||
|
CHECK(nb == e2[1])
|
||||||
|
CHECK(nc == e2[2])
|
||||||
|
CHECK(nc2 == e2[3])
|
||||||
|
CHECK(nd == e2[4])
|
||||||
|
CHECK(ne == e2[5])
|
||||||
|
end)
|
||||||
|
destroy()
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
TEST("mount()", function()
|
TEST("mount()", function()
|
||||||
local screen = create "ScreenGui" {}
|
local screen = create "ScreenGui" {}
|
||||||
|
|
||||||
|
|
@ -466,7 +562,7 @@ TEST("derive()", wrap_root(function()
|
||||||
is_even()
|
is_even()
|
||||||
count += 1
|
count += 1
|
||||||
end)
|
end)
|
||||||
|
|
||||||
num(1) -- odd
|
num(1) -- odd
|
||||||
CHECK(count == 2)
|
CHECK(count == 2)
|
||||||
num(2) -- even
|
num(2) -- even
|
||||||
|
|
@ -559,7 +655,7 @@ TEST("derive()", wrap_root(function()
|
||||||
-- end)
|
-- end)
|
||||||
|
|
||||||
-- destroy()
|
-- destroy()
|
||||||
|
|
||||||
-- CHECK(ran[1] == 1)
|
-- CHECK(ran[1] == 1)
|
||||||
-- CHECK(ran[2] == 1)
|
-- CHECK(ran[2] == 1)
|
||||||
-- CHECK(cleaned[1] == 1)
|
-- CHECK(cleaned[1] == 1)
|
||||||
|
|
@ -567,7 +663,7 @@ TEST("derive()", wrap_root(function()
|
||||||
-- end
|
-- end
|
||||||
|
|
||||||
do CASE "garbage collection"
|
do CASE "garbage collection"
|
||||||
-- check that `b` does not allow gc of `a`
|
-- check that `b` does not allow gc of `a`
|
||||||
local a = source(1)
|
local a = source(1)
|
||||||
|
|
||||||
local _b = derive(function()
|
local _b = derive(function()
|
||||||
|
|
@ -606,7 +702,7 @@ TEST("effect()", wrap_root(function()
|
||||||
local num = source(0)
|
local num = source(0)
|
||||||
|
|
||||||
local text = derive(function() return tostring(num()) end)
|
local text = derive(function() return tostring(num()) end)
|
||||||
|
|
||||||
local count = 0
|
local count = 0
|
||||||
effect(function()
|
effect(function()
|
||||||
text()
|
text()
|
||||||
|
|
@ -660,7 +756,7 @@ TEST("cleanup()", wrap_root(function()
|
||||||
src()
|
src()
|
||||||
effected += 1
|
effected += 1
|
||||||
cleanup(function()
|
cleanup(function()
|
||||||
cleaned += 1
|
cleaned += 1
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
@ -677,7 +773,7 @@ TEST("cleanup()", wrap_root(function()
|
||||||
local src = source(1)
|
local src = source(1)
|
||||||
|
|
||||||
local queue = {}
|
local queue = {}
|
||||||
|
|
||||||
effect(function()
|
effect(function()
|
||||||
src()
|
src()
|
||||||
cleanup(function() table.insert(queue, 1) end)
|
cleanup(function() table.insert(queue, 1) end)
|
||||||
|
|
@ -772,7 +868,7 @@ TEST("create()", wrap_root(function()
|
||||||
vide.defer_nested_properties = false
|
vide.defer_nested_properties = false
|
||||||
|
|
||||||
local t = {}
|
local t = {}
|
||||||
|
|
||||||
create "TextLabel" {
|
create "TextLabel" {
|
||||||
{
|
{
|
||||||
{ function() table.insert(t, 1) end } :: any,
|
{ function() table.insert(t, 1) end } :: any,
|
||||||
|
|
@ -891,7 +987,7 @@ TEST("create()", wrap_root(function()
|
||||||
do CASE "bind children"
|
do CASE "bind children"
|
||||||
local children = source()
|
local children = source()
|
||||||
|
|
||||||
local a, b, c =
|
local a, b, c =
|
||||||
create "TextLabel" { Name = "A" },
|
create "TextLabel" { Name = "A" },
|
||||||
create "TextLabel" { Name = "B" },
|
create "TextLabel" { Name = "B" },
|
||||||
create "TextLabel" { Name = "C" }
|
create "TextLabel" { Name = "C" }
|
||||||
|
|
@ -899,14 +995,14 @@ TEST("create()", wrap_root(function()
|
||||||
local frame = create "Frame" {
|
local frame = create "Frame" {
|
||||||
children
|
children
|
||||||
}
|
}
|
||||||
|
|
||||||
children { a, b }
|
children { a, b }
|
||||||
|
|
||||||
CHECK(frame:FindFirstChild "A")
|
CHECK(frame:FindFirstChild "A")
|
||||||
CHECK(frame:FindFirstChild "B")
|
CHECK(frame:FindFirstChild "B")
|
||||||
|
|
||||||
-- check that b is removed and c is added while a remains untouched
|
-- check that b is removed and c is added while a remains untouched
|
||||||
|
|
||||||
children { a, c }
|
children { a, c }
|
||||||
|
|
||||||
CHECK(frame:FindFirstChild "A")
|
CHECK(frame:FindFirstChild "A")
|
||||||
|
|
@ -949,7 +1045,7 @@ TEST("create()", wrap_root(function()
|
||||||
local set_test_to_true = action(function(self) (self :: any).test = true end)
|
local set_test_to_true = action(function(self) (self :: any).test = true end)
|
||||||
|
|
||||||
local f2
|
local f2
|
||||||
|
|
||||||
local to_apply = {
|
local to_apply = {
|
||||||
{ a = 1 },
|
{ a = 1 },
|
||||||
set_test_to_true,
|
set_test_to_true,
|
||||||
|
|
@ -978,13 +1074,13 @@ TEST("create()", wrap_root(function()
|
||||||
local c = create "Frame" { Name = "c" }
|
local c = create "Frame" { Name = "c" }
|
||||||
local d = create "Frame" { Name = "d" }
|
local d = create "Frame" { Name = "d" }
|
||||||
local e = create "Frame" { Name = "e" }
|
local e = create "Frame" { Name = "e" }
|
||||||
|
|
||||||
local children = source {
|
local children = source {
|
||||||
a,
|
a,
|
||||||
{ b, c, { d } },
|
{ b, c, { d } },
|
||||||
{ { e } }
|
{ { e } }
|
||||||
}
|
}
|
||||||
|
|
||||||
local obj = create "Frame" {
|
local obj = create "Frame" {
|
||||||
children
|
children
|
||||||
}
|
}
|
||||||
|
|
@ -1125,7 +1221,7 @@ TEST("show()", wrap_root(function()
|
||||||
show(input :: () -> number?, function(value: () -> number)
|
show(input :: () -> number?, function(value: () -> number)
|
||||||
effect(function()
|
effect(function()
|
||||||
local v = value()
|
local v = value()
|
||||||
|
|
||||||
count += 1
|
count += 1
|
||||||
|
|
||||||
CHECK(v == count)
|
CHECK(v == count)
|
||||||
|
|
@ -1166,7 +1262,7 @@ TEST("show()", wrap_root(function()
|
||||||
|
|
||||||
show(weapon, function(weapon: () -> Weapon)
|
show(weapon, function(weapon: () -> Weapon)
|
||||||
local enchant = function() return weapon().enchant end
|
local enchant = function() return weapon().enchant end
|
||||||
|
|
||||||
show(enchant, function(enchant: () -> string)
|
show(enchant, function(enchant: () -> string)
|
||||||
effect(function()
|
effect(function()
|
||||||
local e = enchant()
|
local e = enchant()
|
||||||
|
|
@ -1295,7 +1391,7 @@ TEST("show()", wrap_root(function()
|
||||||
CHECK(value_upval() == true)
|
CHECK(value_upval() == true)
|
||||||
CHECK(present_upval() == false)
|
CHECK(present_upval() == false)
|
||||||
CHECK(not cleaned)
|
CHECK(not cleaned)
|
||||||
|
|
||||||
step(0.5)
|
step(0.5)
|
||||||
|
|
||||||
CHECK(output() == obj)
|
CHECK(output() == obj)
|
||||||
|
|
@ -1342,7 +1438,7 @@ TEST("show()", wrap_root(function()
|
||||||
CHECK(value_upval() == true)
|
CHECK(value_upval() == true)
|
||||||
CHECK(present_upval() == false)
|
CHECK(present_upval() == false)
|
||||||
CHECK(not cleaned)
|
CHECK(not cleaned)
|
||||||
|
|
||||||
step(0.5)
|
step(0.5)
|
||||||
|
|
||||||
CHECK(output() == obj)
|
CHECK(output() == obj)
|
||||||
|
|
@ -1432,15 +1528,15 @@ TEST("switch()", wrap_root(function()
|
||||||
|
|
||||||
CHECK(count == 1)
|
CHECK(count == 1)
|
||||||
CHECK(output() == 1)
|
CHECK(output() == 1)
|
||||||
|
|
||||||
input(false)
|
input(false)
|
||||||
CHECK(output() == 0)
|
CHECK(output() == 0)
|
||||||
CHECK(count == 2)
|
CHECK(count == 2)
|
||||||
|
|
||||||
input(false)
|
input(false)
|
||||||
CHECK(output() == 0)
|
CHECK(output() == 0)
|
||||||
CHECK(count == 2)
|
CHECK(count == 2)
|
||||||
|
|
||||||
input(NIL)
|
input(NIL)
|
||||||
CHECK(output() == nil)
|
CHECK(output() == nil)
|
||||||
end
|
end
|
||||||
|
|
@ -1477,12 +1573,12 @@ TEST("switch()", wrap_root(function()
|
||||||
cleanup(function() owner_count += 1 end)
|
cleanup(function() owner_count += 1 end)
|
||||||
|
|
||||||
local output = switch(input) {
|
local output = switch(input) {
|
||||||
[true] = function()
|
[true] = function()
|
||||||
cleanup(function() switch1_count += 1 end)
|
cleanup(function() switch1_count += 1 end)
|
||||||
return 1
|
return 1
|
||||||
end,
|
end,
|
||||||
|
|
||||||
[false] = function()
|
[false] = function()
|
||||||
cleanup(function() switch0_count += 1 end)
|
cleanup(function() switch0_count += 1 end)
|
||||||
return 0
|
return 0
|
||||||
end
|
end
|
||||||
|
|
@ -1546,7 +1642,7 @@ TEST("indexes()", wrap_root(function()
|
||||||
local input = source { 1, 2, 3 }
|
local input = source { 1, 2, 3 }
|
||||||
|
|
||||||
local output = indexes(input, function(v, k)
|
local output = indexes(input, function(v, k)
|
||||||
return tostring(v())
|
return tostring(v())
|
||||||
end)
|
end)
|
||||||
|
|
||||||
CHECK("" .. input()[1] == output()[1])
|
CHECK("" .. input()[1] == output()[1])
|
||||||
|
|
@ -1619,7 +1715,7 @@ TEST("indexes()", wrap_root(function()
|
||||||
input = NIL
|
input = NIL
|
||||||
|
|
||||||
gc()
|
gc()
|
||||||
CHECK(wref[1])
|
CHECK(wref[1])
|
||||||
end
|
end
|
||||||
|
|
||||||
do -- check that `input` allows gc of `output`
|
do -- check that `input` allows gc of `output`
|
||||||
|
|
@ -1645,7 +1741,7 @@ TEST("indexes()", wrap_root(function()
|
||||||
local input = source { 1, 2, 3 }
|
local input = source { 1, 2, 3 }
|
||||||
|
|
||||||
local count = table.create(3, 0)
|
local count = table.create(3, 0)
|
||||||
|
|
||||||
local output = indexes(input, function(v, i)
|
local output = indexes(input, function(v, i)
|
||||||
cleanup(function()
|
cleanup(function()
|
||||||
count[i] += 1
|
count[i] += 1
|
||||||
|
|
@ -1691,12 +1787,12 @@ TEST("indexes()", wrap_root(function()
|
||||||
indexes(items, function(item)
|
indexes(items, function(item)
|
||||||
effect(function()
|
effect(function()
|
||||||
item()
|
item()
|
||||||
updated[1] += 1
|
updated[1] += 1
|
||||||
end)
|
end)
|
||||||
|
|
||||||
effect(function()
|
effect(function()
|
||||||
item()
|
item()
|
||||||
updated[2] += 1
|
updated[2] += 1
|
||||||
end)
|
end)
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
|
@ -1739,7 +1835,7 @@ TEST("indexes()", wrap_root(function()
|
||||||
local input = source {}
|
local input = source {}
|
||||||
|
|
||||||
local cleaned_counts = {} :: Map<number, number>
|
local cleaned_counts = {} :: Map<number, number>
|
||||||
|
|
||||||
local output = indexes(input, function(v, i, present)
|
local output = indexes(input, function(v, i, present)
|
||||||
cleanup(function()
|
cleanup(function()
|
||||||
cleaned_counts[i] = (cleaned_counts[i] or 0) + 1
|
cleaned_counts[i] = (cleaned_counts[i] or 0) + 1
|
||||||
|
|
@ -1816,7 +1912,7 @@ TEST("values()", wrap_root(function()
|
||||||
local input = source { 1, 2, 3 }
|
local input = source { 1, 2, 3 }
|
||||||
|
|
||||||
local output = values(input, function(v, k)
|
local output = values(input, function(v, k)
|
||||||
return tostring(v)
|
return tostring(v)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
CHECK("" .. input()[1] == output()[1])
|
CHECK("" .. input()[1] == output()[1])
|
||||||
|
|
@ -1894,7 +1990,7 @@ TEST("values()", wrap_root(function()
|
||||||
local input = source { 1, 2, 3 }
|
local input = source { 1, 2, 3 }
|
||||||
|
|
||||||
local count = table.create(3, 0)
|
local count = table.create(3, 0)
|
||||||
|
|
||||||
local output = values(input, function(v, i)
|
local output = values(input, function(v, i)
|
||||||
cleanup(function()
|
cleanup(function()
|
||||||
count[i()] += 1
|
count[i()] += 1
|
||||||
|
|
@ -1935,7 +2031,7 @@ TEST("values()", wrap_root(function()
|
||||||
local input = source {}
|
local input = source {}
|
||||||
|
|
||||||
local cleaned_counts = {} :: Map<number, number>
|
local cleaned_counts = {} :: Map<number, number>
|
||||||
|
|
||||||
local output = values(input, function(v, i, present)
|
local output = values(input, function(v, i, present)
|
||||||
cleanup(function()
|
cleanup(function()
|
||||||
cleaned_counts[v] = (cleaned_counts[v] or 0) + 1
|
cleaned_counts[v] = (cleaned_counts[v] or 0) + 1
|
||||||
|
|
@ -2056,10 +2152,10 @@ TEST("spring()", wrap_root(function()
|
||||||
do -- `input` should allow gc of `output`
|
do -- `input` should allow gc of `output`
|
||||||
local input = source(10)
|
local input = source(10)
|
||||||
local output = spring(input)
|
local output = spring(input)
|
||||||
|
|
||||||
local wref = weak { output }
|
local wref = weak { output }
|
||||||
output = NIL
|
output = NIL
|
||||||
|
|
||||||
gc()
|
gc()
|
||||||
CHECK(not wref[1])
|
CHECK(not wref[1])
|
||||||
end
|
end
|
||||||
|
|
@ -2073,7 +2169,7 @@ TEST("spring()", wrap_root(function()
|
||||||
-- local output_node = unpack(capture(output))
|
-- local output_node = unpack(capture(output))
|
||||||
-- wref = weak { output_node, data }
|
-- wref = weak { output_node, data }
|
||||||
-- end
|
-- end
|
||||||
|
|
||||||
-- gc()
|
-- gc()
|
||||||
-- CHECK(not wref[1])
|
-- CHECK(not wref[1])
|
||||||
-- CHECK(not wref[2])
|
-- CHECK(not wref[2])
|
||||||
|
|
@ -2208,11 +2304,11 @@ TEST("untrack()", wrap_root(function()
|
||||||
return untrack(function()
|
return untrack(function()
|
||||||
return derive(function()
|
return derive(function()
|
||||||
inner_count += 1
|
inner_count += 1
|
||||||
|
|
||||||
cleanup(function()
|
cleanup(function()
|
||||||
cleaned_count += 1
|
cleaned_count += 1
|
||||||
end)
|
end)
|
||||||
|
|
||||||
return tostring(input())
|
return tostring(input())
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
@ -2435,7 +2531,7 @@ TEST("batch()", wrap_root(function()
|
||||||
b(a())
|
b(a())
|
||||||
c(a())
|
c(a())
|
||||||
end)
|
end)
|
||||||
|
|
||||||
batch(function()
|
batch(function()
|
||||||
a(1)
|
a(1)
|
||||||
end)
|
end)
|
||||||
|
|
@ -2491,10 +2587,10 @@ TEST("batch()", wrap_root(function()
|
||||||
do CASE "recursive queue flush diamond A,B,C,D,E"
|
do CASE "recursive queue flush diamond A,B,C,D,E"
|
||||||
--[[
|
--[[
|
||||||
where b and c batches d
|
where b and c batches d
|
||||||
|
|
||||||
a > b > e
|
a > b > e
|
||||||
> c > d >
|
> c > d >
|
||||||
|
|
||||||
]]
|
]]
|
||||||
|
|
||||||
local a = source(0)
|
local a = source(0)
|
||||||
|
|
@ -2551,7 +2647,7 @@ TEST("batch()", wrap_root(function()
|
||||||
--[[
|
--[[
|
||||||
|
|
||||||
a > b > d > E > G
|
a > b > d > E > G
|
||||||
> c ^ > F
|
> c ^ > F
|
||||||
|
|
||||||
]]
|
]]
|
||||||
|
|
||||||
|
|
@ -2560,7 +2656,7 @@ TEST("batch()", wrap_root(function()
|
||||||
local b = source(0)
|
local b = source(0)
|
||||||
local c = source(0)
|
local c = source(0)
|
||||||
local d = source(0)
|
local d = source(0)
|
||||||
|
|
||||||
local e = source(0)
|
local e = source(0)
|
||||||
local f = source(0)
|
local f = source(0)
|
||||||
local g = source(0)
|
local g = source(0)
|
||||||
|
|
@ -2668,7 +2764,7 @@ TEST("context()", function()
|
||||||
local ok = pcall(function()
|
local ok = pcall(function()
|
||||||
ctx(1, function() end)
|
ctx(1, function() end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
CHECK(not ok)
|
CHECK(not ok)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -2847,14 +2943,14 @@ TEST("graph edge cases", wrap_root(function()
|
||||||
CHECK(count.b == 2)
|
CHECK(count.b == 2)
|
||||||
CHECK(count.c == 2)
|
CHECK(count.c == 2)
|
||||||
CHECK(count.d == 2)
|
CHECK(count.d == 2)
|
||||||
CHECK(count.e == 3) -- todo: redundant re-eval
|
CHECK(count.e == 2)
|
||||||
CHECK(e() == 4)
|
CHECK(e() == 4)
|
||||||
|
|
||||||
a(3)
|
a(3)
|
||||||
CHECK(count.b == 2)
|
CHECK(count.b == 2)
|
||||||
CHECK(count.c == 3)
|
CHECK(count.c == 3)
|
||||||
CHECK(count.d == 3)
|
CHECK(count.d == 3)
|
||||||
CHECK(count.e == 4)
|
CHECK(count.e == 3)
|
||||||
CHECK(e() == 12)
|
CHECK(e() == 12)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -2871,7 +2967,7 @@ TEST("graph edge cases", wrap_root(function()
|
||||||
end
|
end
|
||||||
|
|
||||||
do CASE "do not destroy children"
|
do CASE "do not destroy children"
|
||||||
local parent = source(0)
|
local parent = source(0)
|
||||||
|
|
||||||
local _,
|
local _,
|
||||||
destroy,
|
destroy,
|
||||||
|
|
@ -2899,10 +2995,10 @@ TEST("graph edge cases", wrap_root(function()
|
||||||
CHECK(count == 3)
|
CHECK(count == 3)
|
||||||
|
|
||||||
destroy()
|
destroy()
|
||||||
|
|
||||||
update_parent_to_destroy(2)
|
update_parent_to_destroy(2)
|
||||||
CHECK(count == 3)
|
CHECK(count == 3)
|
||||||
|
|
||||||
parent(parent() + 1)
|
parent(parent() + 1)
|
||||||
CHECK(count == 4)
|
CHECK(count == 4)
|
||||||
end
|
end
|
||||||
|
|
@ -2931,7 +3027,7 @@ TEST("graph edge cases", wrap_root(function()
|
||||||
local _, destroy_child, _child_B = nil, function() end, nil
|
local _, destroy_child, _child_B = nil, function() end, nil
|
||||||
|
|
||||||
local count_A = 0
|
local count_A = 0
|
||||||
|
|
||||||
-- child_A
|
-- child_A
|
||||||
effect(function()
|
effect(function()
|
||||||
count_A += 1
|
count_A += 1
|
||||||
|
|
|
||||||
2
todo.md
2
todo.md
|
|
@ -1,4 +1,4 @@
|
||||||
# todo
|
# todo
|
||||||
|
|
||||||
- improve error traces
|
- improve error traces
|
||||||
- prevent redundant re-eval of nodes in a complex diamond graph
|
- prevent redundant re-eval of nodes in complex graphs after a node adds a dependency mid-update
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue