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)
|
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)
|
||||||
|
|
|
||||||
127
src/cleanup.luau
127
src/cleanup.luau
|
|
@ -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
|
|
||||||
]]
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
105
src/graph.luau
105
src/graph.luau
|
|
@ -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)),
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
408
test/tests.luau
408
test/tests.luau
|
|
@ -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
|
||||||
|
|
||||||
|
update(parent)
|
||||||
|
CHECK(ran)
|
||||||
end
|
end
|
||||||
|
|
||||||
do CASE "capture and link nodes"
|
-- todo: further tests
|
||||||
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
|
|
||||||
|
|
||||||
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 a = source(1)
|
||||||
|
|
||||||
local transform = function()
|
local b = function()
|
||||||
return tostring(input())
|
return tostring(a())
|
||||||
end
|
end
|
||||||
|
|
||||||
local output = derive(function()
|
local c = derive(function()
|
||||||
return tonumber(transform())
|
return tonumber(b())
|
||||||
end)
|
end)
|
||||||
|
|
||||||
CHECK(output() == 1)
|
CHECK(c() == 1)
|
||||||
input(2)
|
a(2)
|
||||||
CHECK(output() == 2)
|
CHECK(c() == 2)
|
||||||
end
|
end
|
||||||
|
|
||||||
do CASE "garbage collection"
|
do CASE "garbage collection"
|
||||||
do -- check that `b` does not allow gc of `a`
|
-- check that `b` does not allow gc of `a`
|
||||||
local wref, b
|
local a = source(1)
|
||||||
|
|
||||||
do
|
|
||||||
local a = source(1)
|
|
||||||
|
|
||||||
b = derive(function()
|
|
||||||
return a()
|
|
||||||
end)
|
|
||||||
|
|
||||||
wref = weak { a }
|
local b = derive(function()
|
||||||
end
|
return a()
|
||||||
|
end)
|
||||||
|
|
||||||
gc()
|
b = nil :: any
|
||||||
CHECK(wref[1])
|
|
||||||
b()
|
|
||||||
end
|
|
||||||
|
|
||||||
do -- check that `a` allows gc of `b`
|
local wref = weak { a }
|
||||||
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
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
-- todo: guarantee call order when gc? (currently not)
|
|
||||||
--testkit.print2(queue)
|
|
||||||
--CHECK(testkit.seq(queue, { 1, 2, 1, 2, 1, 2 }))
|
|
||||||
end
|
end
|
||||||
]]
|
|
||||||
|
do CASE "no scope"
|
||||||
|
local ok = pcall(function()
|
||||||
|
cleanup(function() end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
CHECK(not ok)
|
||||||
|
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
|
||||||
|
|
||||||
|
|
|
||||||
1
todo.md
1
todo.md
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue