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) local graph = require(script.Parent.graph)
type Node<T> = graph.Node<T> type Node<T> = graph.Node<T>
local create = graph.create 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 close_scope = graph.close_scope
local set_child = graph.set_child local add_child = graph.add_child
local capture = graph.capture 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 -- todo: replace with throw's method
local root do local root do
@ -74,45 +67,21 @@ function bind(instance: Instance, property: string, setter: (Instance) -> ())
end end
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 -- run setter to capture any nodes being depended on
local nodes = (capture(setter :: () -> unknown, instance)) local nodes = (capture(setter :: () -> unknown, instance))
close_scope() close_scope()
-- get binding id binding.effect = setter
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
-- register the setter as a side-effect of each node -- register the setter as a side-effect of each node
for _, n in next, nodes do for _, node in next, nodes do
set_child(n, node) add_child(node, binding)
end 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 end
local function bind_property(instance: Instance, property: string, fn: () -> unknown) local function bind_property(instance: Instance, property: string, fn: () -> unknown)

View file

@ -13,130 +13,3 @@ local function cleanup(fn: () -> ())
end end
return cleanup 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 graph = require(script.Parent.graph)
local create = graph.create local create = graph.create
local capture_and_link = graph.capture_and_link local capture = graph.capture
local create_and_open_scope = graph.create_and_open_scope 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 close_scope = graph.close_scope
local function derive<T>(fn: () -> T): () -> T 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() 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 end
return derive return derive

View file

@ -6,7 +6,8 @@ local on_gc = require(script.Parent.on_gc)()
export type Node<T> = { export type Node<T> = {
cache: T, cache: T,
effect: () -> (), effect: (unknown) -> (),
parents: { Node<T> } | false,
children: { Node<T> } | false, -- weak values children: { Node<T> } | false, -- weak values
cleanups: { () -> () } | false cleanups: { () -> () } | false
} }
@ -98,68 +99,80 @@ local function set_effect<T>(node: Node<unknown>, fn: () -> ())
end end
local function run_effect(node: Node<unknown>) local function run_effect(node: Node<unknown>)
node.effect() node.effect(node.cache)
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
end end
-- links two nodes as parent-child -- 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 if parent.children then
table.insert(parent.children, child) table.insert(parent.children, child)
else else
parent.children = { child } parent.children = { child }
setmetatable(parent.children :: any, WEAK_VALUES) setmetatable(parent.children :: any, {})
end end
end end
local function create_and_open_scope(node: Node<unknown>) local function add_children(parent: Node<unknown>, children: { Node<unknown> })
local parent = scopes[scopes.n] if parent.children then
if parent then for _, child in next, children do
set_child(parent, node) table.insert(parent.children, child)
node.effect = function()
return parent
end end
else else
node.cleanups = {} parent.children = table.clone(children)
local cleanups = node.cleanups :: { () -> () } end
on_gc(node, function() end
run_cleanups({ cleanups = cleanups })
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 end
open_scope(node) open_scope(node)
end end
-- runs node effects, recalculates descendants and runs descendant effects -- runs node effects, recalculates descendants and runs descendant effects
local function update(node: Node<unknown>) local function update(node: Node<unknown>)
open_scope(node)
run_cleanups(node)
run_effect(node)
close_scope()
if node.children then if node.children then
for _, child in node.children do for _, child in node.children do
open_scope(child)
run_cleanups(child)
run_effect(child)
update(child) update(child)
close_scope()
end end
end 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 -- 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) local function link<T>(parent: Node<unknown>, child: Node<T>, derive: () -> T)
child.effect = function() child.effect = function()
child.cache = derive() child.cache = derive()
end end
set_child(parent, child) add_child(parent, child)
end end
-- detect what nodes were referenced in the given callback and returns them in an array -- 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() child.cache = derive()
end end
for _, parent: Node<unknown> in next, nodes do for _, parent: Node<unknown> in next, nodes do
set_child(parent, child) add_child(parent, child)
end end
return value :: T return value :: T
end 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 = { local node = {
cache = value, cache = value,
effect = function() end, effect = function() end,
parents = false :: false,
children = false :: false, children = false :: false,
cleanups = false :: false cleanups = false :: false
} }
local function read_node_value() return node
return get(node)
end
return node, read_node_value
end end
return table.freeze { return table.freeze {
create_and_open_scope = create_and_open_scope, init_scope = init_scope,
open_scope = open_scope, open_scope = open_scope,
close_scope = close_scope, close_scope = close_scope,
get_scope = get_scope, get_scope = get_scope,
add_cleanup = add_cleanup, add_cleanup = add_cleanup,
destroy = destroy,
run_cleanups = run_cleanups, run_cleanups = run_cleanups,
set_effect = set_effect, set_effect = set_effect,
get = get, track = track,
set = set, update = update,
link = link, link = link,
set_child = set_child, add_parent = add_parent,
add_child = add_child,
add_children = add_children,
capture = capture, capture = capture,
capture_and_link = capture_and_link, capture_and_link = capture_and_link,
create = create :: (<T>(value: T) -> (Node<T>, () -> T)) & (<T>() -> (Node<T>, () -> T)), 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 capture = graph.capture
local run_cleanups = graph.run_cleanups local run_cleanups = graph.run_cleanups
local set_child = graph.set_child 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 get_scope = graph.get_scope
local open_scope = graph.open_scope local open_scope = graph.open_scope
local close_scope = graph.close_scope local close_scope = graph.close_scope
local link = graph.link local link = graph.link
local destroy_tree = graph.destroy_tree
type Map<K, V> = { [K]: V } 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 -- remove queued values
for _, i in next, remove_queue do for _, i in next, remove_queue do
run_cleanups(scopes[i]) destroy_tree(scopes[i])
input_cache[i] = nil input_cache[i] = nil
@ -67,7 +68,7 @@ local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI,
if cv == nil then if cv == nil then
local scope = create(false) local scope = create(false)
create_and_open_scope(scope) open_new_scope(scope)
local node, get_value = create(v) local node, get_value = create(v)
input_nodes[i] = node 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 output, read_output_value = create(nil :: any)
local scope = create(false)
local function derive() local function derive()
local _ = scope
return recompute(input()) return recompute(input())
end end
@ -106,13 +110,11 @@ local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI,
output.cache = recompute(value) output.cache = recompute(value)
cleanup_ref(tostring(output), output, function()
for _, callbacks in next, cleanups do
for _, callback in next, callbacks do local scope_parent = get_scope()
callback() -- todo: pcall
end set_child(scope_parent, scope)
end
end)
return read_output_value return read_output_value
end end

View file

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

View file

@ -3,22 +3,29 @@ if not game then script = require "test/relative-string" end
local graph = require(script.Parent.graph) local graph = require(script.Parent.graph)
type Node<T> = graph.Node<T> type Node<T> = graph.Node<T>
local create = graph.create local create = graph.create
local set = graph.set local track = graph.track
local update = graph.update
export type Source<T> = (() -> T) & ((T) -> T) export type Source<T> = (() -> T) & ((T) -> T)
local function source<T>(value: T): Source<T> local function source<T>(initial_value: T): Source<T>
local node, read_node_value = create(value :: T) local node = create(initial_value)
return function(...): T 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 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 return v
end end
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 graph = require(script.Parent.graph)
local create = graph.create 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 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 close_scope = graph.close_scope
local destroy = graph.destroy
local ref = {}
local function watch(effect: () -> ()): () -> () 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) local nodes = capture(effect :: () -> nil)
@ -20,17 +20,14 @@ local function watch(effect: () -> ()): () -> ()
node.effect = effect node.effect = effect
-- register effect with permanent lifetime
for _, parent in next, nodes do for _, parent in next, nodes do
set_child(parent, node) add_parent(node, parent)
add_child(parent, node)
end end
ref[node] = true -- prevent gc of node
local function unwatch() local function unwatch()
-- unregister effect from all nodes destroy(node)
node.effect = function() end
ref[node] = nil
end end
return unwatch return unwatch

View file

@ -21,132 +21,50 @@ end
TEST("graph", function() TEST("graph", function()
local graph = require "src/graph" local graph = require "src/graph"
local create = graph.create local create = graph.create
local get = graph.get local track = graph.track
local set = graph.set
local capture = graph.capture local capture = graph.capture
local capture_and_link = graph.capture_and_link local update = graph.update
local link = graph.link local add_child = graph.add_child
local set_effect = graph.set_effect
do CASE "node creation" do CASE "node creation"
local node = create(1) local node = create(1)
CHECK(get(node) == 1) CHECK(node.cache == 1)
end
do CASE "node value"
local node = create(0)
set(node, 1)
CHECK(get(node) == 1)
set(node, 2)
CHECK(get(node) == 2)
end end
do CASE "capture nodes" do CASE "capture nodes"
local node1 = create(nil) local node1 = create(nil)
local node2 = create(nil) local node2 = create(nil)
local nodes = capture(function() local captured = capture(function()
return get(node1), get(node2) track(node1)
track(node2)
return nil
end) end)
CHECK(nodes[1] == node1) CHECK(captured[1] == node1)
CHECK(nodes[2] == node2) CHECK(captured[2] == node2)
end end
do CASE "linking nodes" do CASE "linking nodes"
local parent = create(1) local parent = create(1)
local child = create(0) local child = create(0)
link(parent, child, function() add_child(parent, child)
return get(parent)
end)
set(parent, get(parent) + 1) local ran = false
CHECK(get(child) == 2) -- child should automatically update child.effect = function()
ran = true
end end
do CASE "capture and link nodes" update(parent)
local parent = create(1) CHECK(ran)
local child = create()
child.cache = capture_and_link(child, function()
return tostring(get(parent))
end)
set(parent, 2)
CHECK(get(child) == "2")
end end
-- todo: further tests
do CASE "nodes garbage collection" do CASE "nodes garbage collection"
local wref = weak { create(1) } local wref = weak { create(1) }
gc() gc()
CHECK(not wref[1]) CHECK(not wref[1])
end 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) end)
TEST("source()", function() TEST("source()", function()
@ -154,78 +72,65 @@ TEST("source()", function()
local watch = vide.watch local watch = vide.watch
do CASE "create source" do CASE "create source"
local state = source(1) local src = source(1)
CHECK(state() == 1) CHECK(src() == 1)
end end
do CASE "set and get source value" do CASE "set and get source value"
local state = source(1) local src = source(1)
state(2) src(2)
CHECK(state() == 2) CHECK(src() == 2)
end end
do CASE "does not update if same value" do CASE "does not update if same value"
local state = source(1) local src = source(1)
local updates = -1 local count = -1
watch(function() watch(function()
state() src()
updates += 1 count += 1
end) end)
CHECK(updates == 0) CHECK(count == 0)
state(1) src(1)
CHECK(updates == 0) CHECK(count == 0)
state(2) src(2)
CHECK(updates == 1) CHECK(count == 1)
end end
do CASE "does update if same value is table" do CASE "does update if same value is mutable table"
local state = source {} local src = source {}
local updates = -1 local count = -1
watch(function() watch(function()
state() src()
updates += 1 count += 1
end) end)
CHECK(updates == 0) CHECK(count == 0)
state(state()) src(src())
CHECK(updates == 1) CHECK(count == 1)
end end
do CASE "does not update if same value is frozen table" do CASE "does not update if same value is frozen table"
local a = table.freeze {} local a = table.freeze {}
local b = table.freeze {} local b = table.freeze {}
local state = source(a) local src = source(a)
local updates = -1 local count = -1
watch(function() watch(function()
state() src()
updates += 1 count += 1
end) end)
CHECK(updates == 0) CHECK(count == 0)
state(a) src(a)
CHECK(updates == 0) CHECK(count == 0)
state(b) src(b)
CHECK(updates == 1) CHECK(count == 1)
state(b) src(b)
CHECK(updates == 1) CHECK(count == 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])
end end
end) end)
@ -234,130 +139,48 @@ TEST("derive()", function()
local derive = vide.derive local derive = vide.derive
do CASE "derive new value on source change" do CASE "derive new value on source change"
local inputA = source(1) local a = source(1)
local inputB = source(2) local b = source(2)
local output = derive(function() local c = derive(function()
return tostring(inputA() + inputB()) return tostring(a() + b())
end) end)
CHECK(output() == "3") CHECK(c() == "3")
inputA(2) a(2)
CHECK(output() == "4") CHECK(c() == "4")
end end
do CASE "derive wrapped source" do CASE "derive wrapped source"
local input = source(1)
local transform = function()
return tostring(input())
end
local output = derive(function()
return tonumber(transform())
end)
CHECK(output() == 1)
input(2)
CHECK(output() == 2)
end
do CASE "garbage collection"
do -- check that `b` does not allow gc of `a`
local wref, b
do
local a = source(1) local a = source(1)
b = derive(function() local b = function()
return a() return tostring(a())
end)
wref = weak { a }
end 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() local c = derive(function()
return b() return tonumber(b())
end) end)
return weak { a, b, c }, a, b, c CHECK(c() == 1)
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) a(2)
CHECK(c() == 2) CHECK(c() == 2)
end end
end
do CASE "garbage collection of node" do CASE "garbage collection"
local capture = require "src/graph".capture -- check that `b` does not allow gc of `a`
local input = source(1) local a = source(1)
local wref do local b = derive(function()
local output = derive(input) return a()
local output_node = unpack(capture(output)) end)
wref = weak { output_node }
end b = nil :: any
local wref = weak { a }
gc() gc()
CHECK(not wref[1]) CHECK(wref[1])
end end
end) end)
@ -366,63 +189,63 @@ TEST("watch()", function()
local watch = vide.watch local watch = vide.watch
local cleanup = vide.cleanup local cleanup = vide.cleanup
do CASE "capture sourcess" do CASE "capture sources"
local a = source(1) local a = source(1)
local b = source(1) local b = source(1)
local runcount = -1 local count = 0
watch(function() watch(function()
a() a()
b() b()
runcount += 1 count += 1
end) end)
CHECK(runcount == 0) CHECK(count == 1)
a(2) a(2)
CHECK(runcount == 1) CHECK(count == 2)
b(2) b(2)
CHECK(runcount == 2) CHECK(count == 3)
end end
do CASE "stop watch" do CASE "stop watch"
local a = source(1) local a = source(1)
local runcount = -1 local count = 0
local unwatch = watch(function() local unwatch = watch(function()
a() a()
runcount += 1 count += 1
end) end)
unwatch() unwatch()
a(2) a(2)
CHECK(runcount == 0) CHECK(count == 1)
end end
do CASE "side-effect cleanup" do CASE "side-effect cleanup"
local state = source(1) local state = source(1)
local effect_runcount = 0 local effect_count = 0
local cleanup_runcount = 0 local cleanup_count = 0
local unwatch = watch(function() local unwatch = watch(function()
state() state()
effect_runcount += 1 effect_count += 1
cleanup(function() cleanup_runcount += 1 end) cleanup(function() cleanup_count += 1 end)
end) end)
CHECK(effect_runcount == 1) CHECK(effect_count == 1)
CHECK(cleanup_runcount == 0) CHECK(cleanup_count == 0)
state(2) state(2)
CHECK(effect_runcount == 2) CHECK(effect_count == 2)
CHECK(cleanup_runcount == 1) CHECK(cleanup_count == 1)
unwatch() unwatch()
unwatch = nil :: any unwatch = nil :: any
gc() gc()
vide.step(0) vide.step(0)
CHECK(effect_runcount == 2) CHECK(effect_count == 2)
CHECK(cleanup_runcount == 2) CHECK(cleanup_count == 2)
end end
do CASE "garbage collection" do CASE "garbage collection"
@ -574,26 +397,6 @@ TEST("cleanup()", function()
CHECK(objB.cleaned == 2) CHECK(objB.cleaned == 2)
end 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" do CASE "multiple cleanup"
local state = source(1) local state = source(1)
@ -610,18 +413,15 @@ TEST("cleanup()", function()
CHECK(testkit.seq(queue, { 1, 2 })) CHECK(testkit.seq(queue, { 1, 2 }))
state(3) state(3)
CHECK(testkit.seq(queue, { 1, 2, 1, 2 })) CHECK(testkit.seq(queue, { 1, 2, 1, 2 }))
do
state = nil :: any
gc()
vide.step(0)
end end
-- todo: guarantee call order when gc? (currently not) do CASE "no scope"
--testkit.print2(queue) local ok = pcall(function()
--CHECK(testkit.seq(queue, { 1, 2, 1, 2, 1, 2 })) cleanup(function() end)
end)
CHECK(not ok)
end end
]]
end) end)
TEST("create()", function() TEST("create()", function()
@ -822,9 +622,11 @@ TEST("create()", function()
Text = state, Text = state,
} }
local binding = assert(node.children)[1]
wref = weak { wref = weak {
instance = instance, instance = instance,
binding = next(node.effects) binding = binding
} }
end end

View file

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