mirror of
https://github.com/centau/vide.git
synced 2026-08-20 14:41:37 +00:00
This commit is contained in:
parent
fe3af737be
commit
dafdc0739b
10 changed files with 249 additions and 550 deletions
|
|
@ -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)
|
||||
|
|
|
|||
127
src/cleanup.luau
127
src/cleanup.luau
|
|
@ -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
|
||||
]]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
105
src/graph.luau
105
src/graph.luau
|
|
@ -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)),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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>)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
408
test/tests.luau
408
test/tests.luau
|
|
@ -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
|
||||
|
||||
|
|
|
|||
1
todo.md
1
todo.md
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue