From dafdc0739b8ae3218161c039f655786fc78b65de Mon Sep 17 00:00:00 2001 From: aaron <83140718+centau@users.noreply.github.com> Date: Wed, 6 Sep 2023 23:01:53 +0100 Subject: [PATCH] --- src/bind.luau | 45 +----- src/cleanup.luau | 127 --------------- src/derive.luau | 30 +++- src/graph.luau | 105 +++++++----- src/maps.luau | 22 +-- src/root.luau | 19 ++- src/source.luau | 21 ++- src/watch.luau | 21 ++- test/tests.luau | 408 ++++++++++++----------------------------------- todo.md | 1 + 10 files changed, 249 insertions(+), 550 deletions(-) diff --git a/src/bind.luau b/src/bind.luau index 5792049..156bbfb 100644 --- a/src/bind.luau +++ b/src/bind.luau @@ -6,9 +6,9 @@ local flags = require(script.Parent.flags) local graph = require(script.Parent.graph) type Node = graph.Node 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) diff --git a/src/cleanup.luau b/src/cleanup.luau index 26be562..921a939 100644 --- a/src/cleanup.luau +++ b/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 -]] diff --git a/src/derive.luau b/src/derive.luau index 7515747..fd4805a 100644 --- a/src/derive.luau +++ b/src/derive.luau @@ -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(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 diff --git a/src/graph.luau b/src/graph.luau index ffa9e2f..9b7fc97 100644 --- a/src/graph.luau +++ b/src/graph.luau @@ -6,7 +6,8 @@ local on_gc = require(script.Parent.on_gc)() export type Node = { cache: T, - effect: () -> (), + effect: (unknown) -> (), + parents: { Node } | false, children: { Node } | false, -- weak values cleanups: { () -> () } | false } @@ -98,68 +99,80 @@ local function set_effect(node: Node, fn: () -> ()) end local function run_effect(node: Node) - node.effect() -end - --- retrieves a node's cached value --- add self to refs if ref capture flag is enabled -local function get(node: Node): 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, child: Node) +local function add_child(parent: Node, child: Node) 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) - local parent = scopes[scopes.n] - if parent then - set_child(parent, node) - node.effect = function() - return parent +local function add_children(parent: Node, children: { Node }) + 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, parent: Node) + child.parents = { parent } +end + +local function rec(node: { effect: any, children: { Node }, 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) + 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) + 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) - 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(node: Node, 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(parent: Node, child: Node, 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(child: Node, derive: () -> T): T child.cache = derive() end for _, parent: Node in next, nodes do - set_child(parent, child) + add_child(parent, child) end return value :: T end -local function create(value: T): (Node, () -> T) +local function track(node: Node) + if reff then table.insert(refs, node) end +end + +local function create(value: T): Node 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 :: ((value: T) -> (Node, () -> T)) & (() -> (Node, () -> T)), diff --git a/src/maps.luau b/src/maps.luau index 4527780..3405019 100644 --- a/src/maps.luau +++ b/src/maps.luau @@ -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 } @@ -48,7 +49,7 @@ local function indexes(input: () -> Map, 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(input: () -> Map, 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(input: () -> Map, 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(input: () -> Map, 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 diff --git a/src/root.luau b/src/root.luau index 51c42c1..b5f377a 100644 --- a/src/root.luau +++ b/src/root.luau @@ -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 = graph.Node 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 } +setmetatable(refs :: any, { __mode = "v" }) local function root(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 diff --git a/src/source.luau b/src/source.luau index d0da051..90971e3 100644 --- a/src/source.luau +++ b/src/source.luau @@ -3,22 +3,29 @@ if not game then script = require "test/relative-string" end local graph = require(script.Parent.graph) type Node = graph.Node local create = graph.create -local set = graph.set +local track = graph.track +local update = graph.update export type Source = (() -> T) & ((T) -> T) -local function source(value: T): Source - local node, read_node_value = create(value :: T) +local function source(initial_value: T): Source + 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 :: ((value: T) -> Source) & (() -> Source) +return source :: ((initial_value: T) -> Source) & (() -> Source) diff --git a/src/watch.luau b/src/watch.luau index edde546..c045529 100644 --- a/src/watch.luau +++ b/src/watch.luau @@ -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 diff --git a/test/tests.luau b/test/tests.luau index 10d6e52..83f9676 100644 --- a/test/tests.luau +++ b/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 - - do - local a = source(1) - - b = derive(function() - return a() - end) + -- check that `b` does not allow gc of `a` + local a = source(1) - wref = weak { a } - end + local b = derive(function() + return a() + end) - gc() - CHECK(wref[1]) - b() - end + b = nil :: any - 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 diff --git a/todo.md b/todo.md index 250bfb3..1586429 100644 --- a/todo.md +++ b/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