This commit is contained in:
aaron 2023-09-06 23:01:53 +01:00
parent fe3af737be
commit dafdc0739b
10 changed files with 249 additions and 550 deletions

View file

@ -6,9 +6,9 @@ local flags = require(script.Parent.flags)
local graph = require(script.Parent.graph)
type Node<T> = graph.Node<T>
local create = graph.create
local create_and_open_scope = graph.create_and_open_scope
local init_scope = graph.init_scope
local close_scope = graph.close_scope
local set_child = graph.set_child
local add_child = graph.add_child
local capture = graph.capture
--[[
@ -29,14 +29,7 @@ todo: investigate behavior in case B is parented to A, and A has no parent or re
]]
-- holds parented instance proxies in memory
local hold: { Instance? } = {}
-- weakly references instances with properties bound
local weak: { Instance? } = setmetatable({}, { __mode = "v" }) :: any
-- unique binding id
local bind_count = 0
-- todo: replace with throw's method
local root do
@ -74,45 +67,21 @@ function bind(instance: Instance, property: string, setter: (Instance) -> ())
end
end
local node = create(false)
local binding = create(instance)
create_and_open_scope(node)
init_scope(binding)
-- run setter to capture any nodes being depended on
local nodes = (capture(setter :: () -> unknown, instance))
close_scope()
-- get binding id
bind_count += 1
local bind_id = bind_count
node.effect = function()
local instance = weak[bind_id]
if instance == nil then return end
setter(weak[bind_id] :: Instance)
end
binding.effect = setter
-- register the setter as a side-effect of each node
for _, n in next, nodes do
set_child(n, node)
for _, node in next, nodes do
add_child(node, binding)
end
-- store reference of instance proxy without preventing gc
weak[bind_id] = instance
local function ref()
local _ = node -- prevent gc of node being depended on
local instance = weak[bind_id] :: Instance
-- keep proxy in memory if instance is still parented
hold[bind_id] = instance.Parent and instance or nil
end
ref()
instance:GetPropertyChangedSignal("Parent"):Connect(ref)
end
local function bind_property(instance: Instance, property: string, fn: () -> unknown)

View file

@ -13,130 +13,3 @@ local function cleanup(fn: () -> ())
end
return cleanup
--[[
Cleanups associate a callback with an arbitrary value with an unknown lifetime.
Anytime a new callback is registered with a value that already has one registered,
the registered callback is ran and then replaced with the new one.
When the value is eventually garbage collected, Vide checks for callbacks
without an associated value, which it will then run and clear, recycling its
cleanup id.
By default the arbitrary value is the function object that calls `cleanup()`.
There are exceptions such as with `indexes()` and `values()` where the
arbitrary value is manually set to be the new source created instead of the
caller, as the same caller can be used to create multiple new objects.
todo: remove need for ref to id maps?
]]
--[[
-- maps a ref to cleanup id
local ref_to_id = {} :: { [string]: number }
-- maps a cleanup id to a ref
local id_to_ref = {} :: { [number]: string }
-- array of all cleanup callbacks
local cleanup_callbacks = {} :: { [number]: () -> () } -- always dense
-- weak array of all cleanup lifetimes
local cleanup_lifetime = {} :: { [number]: unknown } -- can be sparse
setmetatable(cleanup_lifetime :: any, { __mode = "v" })
-- detects in strict mode when multiple cleanups are registered in the same scope
local debug_caller_to_line = {} :: { [() -> ()]: number }
setmetatable(debug_caller_to_line, { __mode = "k" })
-- when active, cleanup callbacks are not automatically registered but are
-- added to an array for manual registering internally
local manual_mode = {
caller = false :: false | () -> (),
callbacks = {} :: { () -> () }
}
-- todo: rare case where mem address is reused by another function
-- does this case handle itself?
-- registers a callback with the given lifetime using the given ref
local function cleanup_ref(ref: string, lifetime: unknown, callback: () -> ())
local id = ref_to_id[ref]
if id then -- invoke previously registered callback then register new one
cleanup_callbacks[id]()
cleanup_lifetime[id] = lifetime -- rare case where ref is reused while lifetime is nil
else -- no previously registered callback, add and register new one
id = #cleanup_callbacks + 1
ref_to_id[ref] = id
id_to_ref[id :: any] = ref -- todo
cleanup_lifetime[id :: any] = lifetime -- todo
end
cleanup_callbacks[id] = callback
end
-- registers a callback with its caller as the lifetime, and caller address as the ref
local function cleanup(callback: () -> ())
local lifetime = debug.info(2, "f") -- `caller of cleanup() is lifetime of cleanup`
if flags.strict then
local line = debug.info(2, "l")
local cur_line = debug_caller_to_line[lifetime]
if cur_line and cur_line ~= line then
throw "only one cleanup call is allowed per function scope"
end
debug_caller_to_line[lifetime] = line
end
if manual_mode.caller == lifetime then
table.insert(manual_mode.callbacks, callback)
else
local ref = tostring(lifetime)
cleanup_ref(ref, lifetime, callback)
end
end
local function clean_garbage()
for id = #cleanup_callbacks, 1, -1 do
if cleanup_lifetime[id] == nil then -- lifetime was garbage collected
local callback = cleanup_callbacks[id]
do -- swap and pop
local max_id = #cleanup_callbacks
cleanup_callbacks[id] = cleanup_callbacks[max_id]
cleanup_callbacks[max_id] = nil
cleanup_lifetime[id] = cleanup_lifetime[max_id]
cleanup_lifetime[max_id] = nil
local ref = id_to_ref[id]
local max_ref = id_to_ref[max_id]
id_to_ref[id] = max_ref
id_to_ref[max_id] = nil
ref_to_id[max_ref] = id
ref_to_id[ref] = nil
end
local ok, err: string? = pcall(callback)
if not ok then warn(`error occured during cleanup: {err}`) end
end
end
end
local manual_cleanup_mode = function(caller: () -> ()?)
if caller == nil then
local clone = table.clone(manual_mode.callbacks)
manual_mode.caller = false
table.clear(manual_mode.callbacks)
return clone
else
manual_mode.caller = caller
end
return manual_mode.callbacks
end :: ( (caller: (...any) -> ()) -> () ) & ( (nil) -> { () -> () } )
return function() return cleanup, clean_garbage, manual_cleanup_mode, cleanup_ref end
]]

View file

@ -2,20 +2,38 @@ if not game then script = require "test/relative-string" end
local graph = require(script.Parent.graph)
local create = graph.create
local capture_and_link = graph.capture_and_link
local create_and_open_scope = graph.create_and_open_scope
local capture = graph.capture
local add_child = graph.add_child
local set_effect = graph.set_effect
local update = graph.update
local track = graph.track
local init_scope = graph.init_scope
local close_scope = graph.close_scope
local function derive<T>(fn: () -> T): () -> T
local node, read_node_value = create((false :: any) :: T)
local node = create((false :: any) :: T)
create_and_open_scope(node)
init_scope(node)
node.cache = capture_and_link(node, fn)
local nodes, value = capture(fn)
close_scope()
return read_node_value
for _, parent in next, nodes do
add_child(parent, node)
end
set_effect(node, function()
node.cache = fn()
update(node)
end)
node.cache = value
return function()
track(node)
return node.cache
end
end
return derive

View file

@ -6,7 +6,8 @@ local on_gc = require(script.Parent.on_gc)()
export type Node<T> = {
cache: T,
effect: () -> (),
effect: (unknown) -> (),
parents: { Node<T> } | false,
children: { Node<T> } | false, -- weak values
cleanups: { () -> () } | false
}
@ -98,68 +99,80 @@ local function set_effect<T>(node: Node<unknown>, fn: () -> ())
end
local function run_effect(node: Node<unknown>)
node.effect()
end
-- retrieves a node's cached value
-- add self to refs if ref capture flag is enabled
local function get<T>(node: Node<T>): T
if reff then table.insert(refs, node) end
return node.cache
node.effect(node.cache)
end
-- links two nodes as parent-child
local function set_child(parent: Node<unknown>, child: Node<unknown>)
local function add_child(parent: Node<unknown>, child: Node<unknown>)
if parent.children then
table.insert(parent.children, child)
else
parent.children = { child }
setmetatable(parent.children :: any, WEAK_VALUES)
setmetatable(parent.children :: any, {})
end
end
local function create_and_open_scope(node: Node<unknown>)
local parent = scopes[scopes.n]
if parent then
set_child(parent, node)
node.effect = function()
return parent
local function add_children(parent: Node<unknown>, children: { Node<unknown> })
if parent.children then
for _, child in next, children do
table.insert(parent.children, child)
end
else
node.cleanups = {}
local cleanups = node.cleanups :: { () -> () }
on_gc(node, function()
run_cleanups({ cleanups = cleanups })
end)
parent.children = table.clone(children)
end
end
local function add_parent(child: Node<unknown>, parent: Node<unknown>)
child.parents = { parent }
end
local function rec(node: { effect: any, children: { Node<unknown> }, cleanups: { () -> () }})
run_cleanups(node)
node.effect = function() assert(false) end
if node.children then
for _, child in node.children do
rec(child)
end
end
end
local function destroy(node: Node<unknown>)
if node.parents then
for _, parent in node.parents do
parent.children[table.find(parent.children, node)] = nil -- todo: can iter invalidation occur here?
end
end
rec(node)
end
local function init_scope(node: Node<unknown>)
local parent = scopes[scopes.n]
if parent then
add_child(parent, node)
add_parent(node, parent)
end
open_scope(node)
end
-- runs node effects, recalculates descendants and runs descendant effects
local function update(node: Node<unknown>)
open_scope(node)
run_cleanups(node)
run_effect(node)
close_scope()
if node.children then
for _, child in node.children do
open_scope(child)
run_cleanups(child)
run_effect(child)
update(child)
close_scope()
end
end
end
-- sets a node's cached value and updates all descendants
local function set<T>(node: Node<T>, value: T)
node.cache = value
update(node)
end
-- links two nodes as parent-child with a function to compute a new value for child
local function link<T>(parent: Node<unknown>, child: Node<T>, derive: () -> T)
child.effect = function()
child.cache = derive()
end
set_child(parent, child)
add_child(parent, child)
end
-- detect what nodes were referenced in the given callback and returns them in an array
@ -192,39 +205,43 @@ local function capture_and_link<T>(child: Node<T>, derive: () -> T): T
child.cache = derive()
end
for _, parent: Node<unknown> in next, nodes do
set_child(parent, child)
add_child(parent, child)
end
return value :: T
end
local function create<T>(value: T): (Node<T>, () -> T)
local function track(node: Node<unknown>)
if reff then table.insert(refs, node) end
end
local function create<T>(value: T): Node<T>
local node = {
cache = value,
effect = function() end,
parents = false :: false,
children = false :: false,
cleanups = false :: false
}
local function read_node_value()
return get(node)
end
return node, read_node_value
return node
end
return table.freeze {
create_and_open_scope = create_and_open_scope,
init_scope = init_scope,
open_scope = open_scope,
close_scope = close_scope,
get_scope = get_scope,
add_cleanup = add_cleanup,
destroy = destroy,
run_cleanups = run_cleanups,
set_effect = set_effect,
get = get,
set = set,
track = track,
update = update,
link = link,
set_child = set_child,
add_parent = add_parent,
add_child = add_child,
add_children = add_children,
capture = capture,
capture_and_link = capture_and_link,
create = create :: (<T>(value: T) -> (Node<T>, () -> T)) & (<T>() -> (Node<T>, () -> T)),

View file

@ -11,11 +11,12 @@ local set = graph.set
local capture = graph.capture
local run_cleanups = graph.run_cleanups
local set_child = graph.set_child
local create_and_open_scope = graph.create_and_open_scope
local open_new_scope = graph.open_new_scope
local get_scope = graph.get_scope
local open_scope = graph.open_scope
local close_scope = graph.close_scope
local link = graph.link
local destroy_tree = graph.destroy_tree
type Map<K, V> = { [K]: V }
@ -48,7 +49,7 @@ local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI,
-- remove queued values
for _, i in next, remove_queue do
run_cleanups(scopes[i])
destroy_tree(scopes[i])
input_cache[i] = nil
@ -67,7 +68,7 @@ local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI,
if cv == nil then
local scope = create(false)
create_and_open_scope(scope)
open_new_scope(scope)
local node, get_value = create(v)
input_nodes[i] = node
@ -94,7 +95,10 @@ local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI,
local output, read_output_value = create(nil :: any)
local scope = create(false)
local function derive()
local _ = scope
return recompute(input())
end
@ -106,13 +110,11 @@ local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI,
output.cache = recompute(value)
cleanup_ref(tostring(output), output, function()
for _, callbacks in next, cleanups do
for _, callback in next, callbacks do
callback() -- todo: pcall
end
end
end)
local scope_parent = get_scope()
set_child(scope_parent, scope)
return read_output_value
end

View file

@ -2,22 +2,35 @@ if not game then script = require "test/relative-string" end
local flags = require(script.Parent.flags)
local throw = require(script.Parent.throw)
local on_gc = require(script.Parent.on_gc)()
local graph = require(script.Parent.graph)
type Node<T> = graph.Node<T>
local create = graph.create
local create_and_open_scope = graph.create_and_open_scope
local init_scope = graph.init_scope
local close_scope = graph.close_scope
local get_scope = graph.get_scope
local add_cleanup = graph.add_cleanup
local destroy = graph.destroy
local refs = {} :: { [Node<unknown>]: unknown }
setmetatable(refs :: any, { __mode = "v" })
local function root<T>(fn: () -> T): T
assert(not get_scope())
local node = create(nil) -- todo: lifetime with return vaue from fn
create_and_open_scope(node)
init_scope(node)
local v = fn()
close_scope()
refs[node] = v
on_gc(v, function()
destroy(node)
end)
return v
end

View file

@ -3,22 +3,29 @@ if not game then script = require "test/relative-string" end
local graph = require(script.Parent.graph)
type Node<T> = graph.Node<T>
local create = graph.create
local set = graph.set
local track = graph.track
local update = graph.update
export type Source<T> = (() -> T) & ((T) -> T)
local function source<T>(value: T): Source<T>
local node, read_node_value = create(value :: T)
local function source<T>(initial_value: T): Source<T>
local node = create(initial_value)
return function(...): T
if select("#", ...) == 0 then return read_node_value() end -- check if any args were given
if select("#", ...) == 0 then -- no args were given
track(node)
return node.cache
end
local v = ... :: T
if node.cache == v and (type(v) ~= "table" or table.isfrozen(v)) then return v end
if node.cache == v and (type(v) ~= "table" or table.isfrozen(v)) then
return v
end
set(node, v)
node.cache = v
update(node)
return v
end
end
return source :: (<T>(value: T) -> Source<T>) & (<T>() -> Source<T>)
return source :: (<T>(initial_value: T) -> Source<T>) & (<T>() -> Source<T>)

View file

@ -2,17 +2,17 @@ if not game then script = require "test/relative-string" end
local graph = require(script.Parent.graph)
local create = graph.create
local set_child = graph.set_child
local add_parent = graph.add_parent
local add_child = graph.add_child
local capture = graph.capture
local create_and_open_scope = graph.create_and_open_scope
local init_scope = graph.init_scope
local close_scope = graph.close_scope
local ref = {}
local destroy = graph.destroy
local function watch(effect: () -> ()): () -> ()
local node = create(nil)
local node = create(false)
create_and_open_scope(node)
init_scope(node)
local nodes = capture(effect :: () -> nil)
@ -20,17 +20,14 @@ local function watch(effect: () -> ()): () -> ()
node.effect = effect
-- register effect with permanent lifetime
for _, parent in next, nodes do
set_child(parent, node)
add_parent(node, parent)
add_child(parent, node)
end
ref[node] = true -- prevent gc of node
local function unwatch()
-- unregister effect from all nodes
node.effect = function() end
ref[node] = nil
destroy(node)
end
return unwatch

View file

@ -21,132 +21,50 @@ end
TEST("graph", function()
local graph = require "src/graph"
local create = graph.create
local get = graph.get
local set = graph.set
local track = graph.track
local capture = graph.capture
local capture_and_link = graph.capture_and_link
local link = graph.link
local set_effect = graph.set_effect
local update = graph.update
local add_child = graph.add_child
do CASE "node creation"
local node = create(1)
CHECK(get(node) == 1)
end
do CASE "node value"
local node = create(0)
set(node, 1)
CHECK(get(node) == 1)
set(node, 2)
CHECK(get(node) == 2)
CHECK(node.cache == 1)
end
do CASE "capture nodes"
local node1 = create(nil)
local node2 = create(nil)
local nodes = capture(function()
return get(node1), get(node2)
local captured = capture(function()
track(node1)
track(node2)
return nil
end)
CHECK(nodes[1] == node1)
CHECK(nodes[2] == node2)
CHECK(captured[1] == node1)
CHECK(captured[2] == node2)
end
do CASE "linking nodes"
local parent = create(1)
local child = create(0)
link(parent, child, function()
return get(parent)
end)
add_child(parent, child)
set(parent, get(parent) + 1)
CHECK(get(child) == 2) -- child should automatically update
local ran = false
child.effect = function()
ran = true
end
update(parent)
CHECK(ran)
end
do CASE "capture and link nodes"
local parent = create(1)
local child = create()
child.cache = capture_and_link(child, function()
return tostring(get(parent))
end)
set(parent, 2)
CHECK(get(child) == "2")
end
-- todo: further tests
do CASE "nodes garbage collection"
local wref = weak { create(1) }
gc()
CHECK(not wref[1])
end
do CASE "node effect garbage collection"
do
local wref
do
local function factory(p) -- factory function to prevent closure caching
return function()
return get(p)
end
end
local node = create(1)
do
local effect1 = factory(node)
local effect2 = factory(node)
wref = weak { e1 = effect1, e2 = effect2, n = node}
set_effect(node, effect1, {})
set_effect(node, effect2, true)
end
gc()
CHECK(not wref.e1) -- effect1 should gc since nothing is referencing table `t`
CHECK(wref.e2) -- effect2 should not gc as `true` is not garbage collectable
end
gc()
CHECK(not wref.n and not wref.e2) -- node should now gc along with effect2
end
do
local wref
do -- same test but for multiple nodes referenced by watcher
local function factory(a, b)
return (function(c, d)
return function()
return get(c), get(d)
end
end)(a, b)
end
local node1 = create(1)
local node2 = create(1)
do
local effect = factory(node1, node2)
wref = weak { n1 = node1, n2 = node2, e = effect }
local t1 = {}
set_effect(node1, effect, t1)
set_effect(node2, effect, t1)
end
gc()
CHECK(not wref.e)
end
gc()
CHECK(not wref.n1)
CHECK(not wref.n2)
end
end
end)
TEST("source()", function()
@ -154,78 +72,65 @@ TEST("source()", function()
local watch = vide.watch
do CASE "create source"
local state = source(1)
CHECK(state() == 1)
local src = source(1)
CHECK(src() == 1)
end
do CASE "set and get source value"
local state = source(1)
state(2)
CHECK(state() == 2)
local src = source(1)
src(2)
CHECK(src() == 2)
end
do CASE "does not update if same value"
local state = source(1)
local src = source(1)
local updates = -1
local count = -1
watch(function()
state()
updates += 1
src()
count += 1
end)
CHECK(updates == 0)
state(1)
CHECK(updates == 0)
state(2)
CHECK(updates == 1)
CHECK(count == 0)
src(1)
CHECK(count == 0)
src(2)
CHECK(count == 1)
end
do CASE "does update if same value is table"
local state = source {}
do CASE "does update if same value is mutable table"
local src = source {}
local updates = -1
local count = -1
watch(function()
state()
updates += 1
src()
count += 1
end)
CHECK(updates == 0)
state(state())
CHECK(updates == 1)
CHECK(count == 0)
src(src())
CHECK(count == 1)
end
do CASE "does not update if same value is frozen table"
local a = table.freeze {}
local b = table.freeze {}
local state = source(a)
local src = source(a)
local updates = -1
local count = -1
watch(function()
state()
updates += 1
src()
count += 1
end)
CHECK(updates == 0)
state(a)
CHECK(updates == 0)
state(b)
CHECK(updates == 1)
state(b)
CHECK(updates == 1)
end
do CASE "garbage collection of node"
local capture = require "src/graph".capture
local src = source(0)
local wref do
local node = unpack(capture(src))
wref = weak { node }
end
gc()
CHECK(not wref[1])
CHECK(count == 0)
src(a)
CHECK(count == 0)
src(b)
CHECK(count == 1)
src(b)
CHECK(count == 1)
end
end)
@ -234,130 +139,48 @@ TEST("derive()", function()
local derive = vide.derive
do CASE "derive new value on source change"
local inputA = source(1)
local inputB = source(2)
local a = source(1)
local b = source(2)
local output = derive(function()
return tostring(inputA() + inputB())
local c = derive(function()
return tostring(a() + b())
end)
CHECK(output() == "3")
inputA(2)
CHECK(output() == "4")
CHECK(c() == "3")
a(2)
CHECK(c() == "4")
end
do CASE "derive wrapped source"
local input = source(1)
local a = source(1)
local transform = function()
return tostring(input())
local b = function()
return tostring(a())
end
local output = derive(function()
return tonumber(transform())
local c = derive(function()
return tonumber(b())
end)
CHECK(output() == 1)
input(2)
CHECK(output() == 2)
CHECK(c() == 1)
a(2)
CHECK(c() == 2)
end
do CASE "garbage collection"
do -- check that `b` does not allow gc of `a`
local wref, b
-- check that `b` does not allow gc of `a`
local a = source(1)
do
local a = source(1)
local b = derive(function()
return a()
end)
b = derive(function()
return a()
end)
b = nil :: any
wref = weak { a }
end
gc()
CHECK(wref[1])
b()
end
do -- check that `a` allows gc of `b`
local a = source(1)
local wref
do
local b = derive(function()
return a()
end)
wref = weak { b }
end
gc()
CHECK(not wref[1])
end
end
do CASE "garbage collection 2"
-- creats a chain `a -> b -> c` where `a` is the source
local function setup()
local a = source(0)
local b = derive(function()
return a()
end)
local c = derive(function()
return b()
end)
return weak { a, b, c }, a, b, c
end
do -- check that `b` and `c` can gc if `a` is referenced
local wref, _a = setup()
gc()
CHECK(not wref[2])
CHECK(not wref[3])
end
do -- check that `a` and `b` wont gc if `c` is referenced
local weak, _a, _b, _c = setup()
_a, _b = nil :: any, nil :: any
gc()
CHECK(weak[1])
CHECK(weak[2])
end
do -- check that `b` wont gc if `a` and `c` are referenced
local weak, a, _b, c = setup()
_b = nil :: any
gc()
CHECK(weak[2])
a(2)
CHECK(c() == 2)
end
end
do CASE "garbage collection of node"
local capture = require "src/graph".capture
local input = source(1)
local wref do
local output = derive(input)
local output_node = unpack(capture(output))
wref = weak { output_node }
end
local wref = weak { a }
gc()
CHECK(not wref[1])
CHECK(wref[1])
end
end)
@ -366,63 +189,63 @@ TEST("watch()", function()
local watch = vide.watch
local cleanup = vide.cleanup
do CASE "capture sourcess"
do CASE "capture sources"
local a = source(1)
local b = source(1)
local runcount = -1
local count = 0
watch(function()
a()
b()
runcount += 1
count += 1
end)
CHECK(runcount == 0)
CHECK(count == 1)
a(2)
CHECK(runcount == 1)
CHECK(count == 2)
b(2)
CHECK(runcount == 2)
CHECK(count == 3)
end
do CASE "stop watch"
local a = source(1)
local runcount = -1
local count = 0
local unwatch = watch(function()
a()
runcount += 1
count += 1
end)
unwatch()
a(2)
CHECK(runcount == 0)
CHECK(count == 1)
end
do CASE "side-effect cleanup"
local state = source(1)
local effect_runcount = 0
local cleanup_runcount = 0
local effect_count = 0
local cleanup_count = 0
local unwatch = watch(function()
state()
effect_runcount += 1
cleanup(function() cleanup_runcount += 1 end)
effect_count += 1
cleanup(function() cleanup_count += 1 end)
end)
CHECK(effect_runcount == 1)
CHECK(cleanup_runcount == 0)
CHECK(effect_count == 1)
CHECK(cleanup_count == 0)
state(2)
CHECK(effect_runcount == 2)
CHECK(cleanup_runcount == 1)
CHECK(effect_count == 2)
CHECK(cleanup_count == 1)
unwatch()
unwatch = nil :: any
gc()
vide.step(0)
CHECK(effect_runcount == 2)
CHECK(cleanup_runcount == 2)
CHECK(effect_count == 2)
CHECK(cleanup_count == 2)
end
do CASE "garbage collection"
@ -574,26 +397,6 @@ TEST("cleanup()", function()
CHECK(objB.cleaned == 2)
end
-- this is not allowed, test to verify behavior anyways
do CASE "multiple cleanup"
local state = source(1)
local queue = {}
watch(function()
state()
cleanup(function() table.insert(queue, 1) end)
cleanup(function() table.insert(queue, 2) end)
end)
CHECK(testkit.seq(queue, { 1 }))
state(2)
CHECK(testkit.seq(queue, { 1, 2, 1 }))
state(3)
CHECK(testkit.seq(queue, { 1, 2, 1, 2, 1 }))
end
--[[
do CASE "multiple cleanup"
local state = source(1)
@ -610,18 +413,15 @@ TEST("cleanup()", function()
CHECK(testkit.seq(queue, { 1, 2 }))
state(3)
CHECK(testkit.seq(queue, { 1, 2, 1, 2 }))
do
state = nil :: any
gc()
vide.step(0)
end
-- todo: guarantee call order when gc? (currently not)
--testkit.print2(queue)
--CHECK(testkit.seq(queue, { 1, 2, 1, 2, 1, 2 }))
end
]]
do CASE "no scope"
local ok = pcall(function()
cleanup(function() end)
end)
CHECK(not ok)
end
end)
TEST("create()", function()
@ -822,9 +622,11 @@ TEST("create()", function()
Text = state,
}
local binding = assert(node.children)[1]
wref = weak {
instance = instance,
binding = next(node.effects)
binding = binding
}
end

View file

@ -15,6 +15,7 @@
- look into SolidJS's reactive contexts
- solution to nested reactivity, see: SolidJS stores
- SolidJS control flow components
- equality checking of derived sources
- Show
- Switch
- Dynamic