mirror of
https://github.com/centau/vide.git
synced 2026-08-20 14:41:37 +00:00
Allow creation of nested tracking scopes
It turns out that handling destruction of a nested tracking scope was not as difficult as I thought and didn't need much changes
This commit is contained in:
parent
38342b3805
commit
c68d0a6c18
7 changed files with 128 additions and 137 deletions
|
|
@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Reactive scopes created within reactive scopes are now destroyed on rerun.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## [0.1.0] - 2023-09-20
|
## [0.1.0] - 2023-09-20
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,14 @@ export type StartNode<T> = {
|
||||||
|
|
||||||
export type Node<T> = {
|
export type Node<T> = {
|
||||||
cache: T,
|
cache: T,
|
||||||
effect: ((T) -> T) | "owner" | "untracked",
|
effect: ((T) -> T) | false,
|
||||||
cleanups: { () -> () } | false,
|
cleanups: { () -> () } | false,
|
||||||
parents: { owner: StartNode<T>?, [number]: StartNode<T> },
|
|
||||||
[number]: Node<T>
|
owned: { Node<T> } | false,
|
||||||
|
owner: Node<T> | false,
|
||||||
|
|
||||||
|
parents: { StartNode<T> },
|
||||||
|
[number]: Node<T> -- children
|
||||||
}
|
}
|
||||||
|
|
||||||
-- reactive scope stack
|
-- reactive scope stack
|
||||||
|
|
@ -50,7 +54,7 @@ local function get_owning_scope(): Node<unknown>
|
||||||
if not scope then
|
if not scope then
|
||||||
local caller_name = debug.info(2, "n")
|
local caller_name = debug.info(2, "n")
|
||||||
return throw(`cannot use {caller_name}() in non-reactive scope, must be used within a root() or mount() callback`)
|
return throw(`cannot use {caller_name}() in non-reactive scope, must be used within a root() or mount() callback`)
|
||||||
elseif scope.effect ~= "owner" then
|
elseif scope.effect then
|
||||||
throw("reactive scope is not an owning scope; new effects cannot be created in side-effects")
|
throw("reactive scope is not an owning scope; new effects cannot be created in side-effects")
|
||||||
end
|
end
|
||||||
return scope
|
return scope
|
||||||
|
|
@ -62,8 +66,12 @@ local function add_child<T>(parent: StartNode<any>, child: Node<any>)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function set_owner(node: Node<any>, owner: Node<any>)
|
local function set_owner(node: Node<any>, owner: Node<any>)
|
||||||
node.parents.owner = owner
|
node.owner = owner
|
||||||
table.insert(owner, node)
|
if owner.owned then
|
||||||
|
table.insert(owner.owned, node)
|
||||||
|
else
|
||||||
|
owner.owned = { node }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function open_scope<T>(node: Node<T>)
|
local function open_scope<T>(node: Node<T>)
|
||||||
|
|
@ -96,18 +104,29 @@ local function run_cleanups<T>(node: Node<T>)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function find_and_swap_pop<T>(t: { T }, v: T)
|
||||||
|
local idx = table.find(t, v)
|
||||||
|
assert(idx, "value not found")
|
||||||
|
local n = #t
|
||||||
|
t[idx] = t[n]
|
||||||
|
t[n] = nil
|
||||||
|
end
|
||||||
|
|
||||||
local function remove_child<T>(parent: StartNode<T>, child: Node<T>)
|
local function remove_child<T>(parent: StartNode<T>, child: Node<T>)
|
||||||
local idx = table.find(parent, child)
|
find_and_swap_pop(parent, child)
|
||||||
assert(idx, "child not found")
|
end
|
||||||
local n = #parent
|
|
||||||
parent[idx] = parent[n]
|
local function remove_owner<T>(node: Node<T>)
|
||||||
parent[n] = nil
|
local owner = node.owner :: Node<T>
|
||||||
|
if node.owner and owner.owned then
|
||||||
|
find_and_swap_pop(owner.owned, node)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function unparent<T>(node: Node<T>)
|
local function unparent<T>(node: Node<T>)
|
||||||
local parents = node.parents
|
local parents = node.parents
|
||||||
|
|
||||||
for i, parent in ipairs(parents) do
|
for i, parent in next, parents do
|
||||||
remove_child(parent, node)
|
remove_child(parent, node)
|
||||||
parents[i] = nil
|
parents[i] = nil
|
||||||
end
|
end
|
||||||
|
|
@ -116,15 +135,21 @@ end
|
||||||
local function destroy<T>(node: Node<T>)
|
local function destroy<T>(node: Node<T>)
|
||||||
run_cleanups(node)
|
run_cleanups(node)
|
||||||
unparent(node)
|
unparent(node)
|
||||||
|
remove_owner(node)
|
||||||
|
|
||||||
if node.parents.owner then
|
if node.owned then
|
||||||
remove_child(node.parents.owner, node)
|
local owned = node.owned
|
||||||
node.parents.owner = nil
|
while owned[1] do destroy(owned[1]) end
|
||||||
end
|
end
|
||||||
|
|
||||||
while node[1] do destroy(node[1]) end
|
while node[1] do destroy(node[1]) end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function destroy_owned<T>(node: Node<T>)
|
||||||
|
if node.owned then
|
||||||
|
while node.owned[1] do destroy(node.owned[1]) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
local update_queue = { n = 0 } :: { n: number, [number]: Node<any> }
|
local update_queue = { n = 0 } :: { n: number, [number]: Node<any> }
|
||||||
|
|
||||||
local function evaluate_node<T>(node: Node<T>)
|
local function evaluate_node<T>(node: Node<T>)
|
||||||
|
|
@ -132,6 +157,8 @@ local function evaluate_node<T>(node: Node<T>)
|
||||||
|
|
||||||
if flags.strict then
|
if flags.strict then
|
||||||
run_cleanups(node)
|
run_cleanups(node)
|
||||||
|
destroy_owned(node)
|
||||||
|
|
||||||
open_scope(node)
|
open_scope(node)
|
||||||
|
|
||||||
local ok, err = check_for_yield(node.effect :: (T) -> T, cur_value)
|
local ok, err = check_for_yield(node.effect :: (T) -> T, cur_value)
|
||||||
|
|
@ -141,7 +168,9 @@ local function evaluate_node<T>(node: Node<T>)
|
||||||
if not ok then throw(err :: string) end
|
if not ok then throw(err :: string) end
|
||||||
end
|
end
|
||||||
|
|
||||||
run_cleanups(node) -- todo: move in scope?
|
run_cleanups(node)
|
||||||
|
destroy_owned(node)
|
||||||
|
|
||||||
open_scope(node)
|
open_scope(node)
|
||||||
|
|
||||||
local ok, new_value = pcall(node.effect :: (T) -> T, cur_value)
|
local ok, new_value = pcall(node.effect :: (T) -> T, cur_value)
|
||||||
|
|
@ -204,11 +233,15 @@ local function track<T>(node: StartNode<T>)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function create_node<T>(value: T, effect: "owner" | (T) -> T): Node<T>
|
local function create_node<T>(value: T, effect: false | (T) -> T): Node<T>
|
||||||
return {
|
return {
|
||||||
cache = value,
|
cache = value,
|
||||||
effect = effect,
|
effect = effect,
|
||||||
cleanups = false,
|
cleanups = false,
|
||||||
|
|
||||||
|
owner = false,
|
||||||
|
owned = false,
|
||||||
|
|
||||||
parents = {},
|
parents = {},
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ end
|
||||||
local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI, K) -> VO): () -> { VO }
|
local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI, K) -> VO): () -> { VO }
|
||||||
local owner = get_owning_scope()
|
local owner = get_owning_scope()
|
||||||
|
|
||||||
local subowner = create_node(false, "owner")
|
local subowner = create_node(false, false)
|
||||||
set_owner(subowner, owner)
|
set_owner(subowner, owner)
|
||||||
|
|
||||||
local input_cache = {} :: Map<K, VI>
|
local input_cache = {} :: Map<K, VI>
|
||||||
|
|
@ -67,7 +67,7 @@ local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI,
|
||||||
|
|
||||||
if cv ~= v then
|
if cv ~= v then
|
||||||
if cv == nil then -- create new scope and run transform
|
if cv == nil then -- create new scope and run transform
|
||||||
local scope = create_node(false, "owner")
|
local scope = create_node(false, false)
|
||||||
scopes[i] = scope :: Node<any>
|
scopes[i] = scope :: Node<any>
|
||||||
|
|
||||||
local node = create_start_node(v)
|
local node = create_start_node(v)
|
||||||
|
|
@ -125,7 +125,7 @@ end
|
||||||
local function values<K, VI, VO>(input: () -> Map<K, VI>, transform: (VI, () -> K) -> VO): () -> { VO }
|
local function values<K, VI, VO>(input: () -> Map<K, VI>, transform: (VI, () -> K) -> VO): () -> { VO }
|
||||||
local owner = get_owning_scope()
|
local owner = get_owning_scope()
|
||||||
|
|
||||||
local subowner = create_node(false, "owner")
|
local subowner = create_node(false, false)
|
||||||
set_owner(subowner, owner)
|
set_owner(subowner, owner)
|
||||||
|
|
||||||
local cur_input_cache_up = {} :: Map<VI, K>
|
local cur_input_cache_up = {} :: Map<VI, K>
|
||||||
|
|
@ -156,7 +156,7 @@ local function values<K, VI, VO>(input: () -> Map<K, VI>, transform: (VI, () ->
|
||||||
local cv = cur_input_cache[v]
|
local cv = cur_input_cache[v]
|
||||||
|
|
||||||
if cv == nil then -- create new scope and run transform
|
if cv == nil then -- create new scope and run transform
|
||||||
local scope = create_node(false, "owner")
|
local scope = create_node(false, false)
|
||||||
scopes[v] = scope :: Node<any>
|
scopes[v] = scope :: Node<any>
|
||||||
|
|
||||||
local node = create_start_node(i)
|
local node = create_start_node(i)
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ local destroy = graph.destroy
|
||||||
local refs = {}
|
local refs = {}
|
||||||
|
|
||||||
local function root<T...>(fn: (destroy: () -> ()) -> T...): T...
|
local function root<T...>(fn: (destroy: () -> ()) -> T...): T...
|
||||||
local node = create_node(false, "owner")
|
local node = create_node(false, false)
|
||||||
|
|
||||||
refs[node] = true -- prevent gc of root node
|
refs[node] = true -- prevent gc of root node
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ local function switch<T, U>(source: () -> T): (map: Map<T, ((() -> U)?)>) -> ()
|
||||||
throw("map must map a value to a function")
|
throw("map must map a value to a function")
|
||||||
end
|
end
|
||||||
|
|
||||||
local new_scope = create_node(false, "owner")
|
local new_scope = create_node(false, false)
|
||||||
last_scope = new_scope :: Node<any>
|
last_scope = new_scope :: Node<any>
|
||||||
|
|
||||||
set_owner(new_scope, owner)
|
set_owner(new_scope, owner)
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ local function untrack<T>(source: () -> T): T
|
||||||
|
|
||||||
-- sources are only tracked if the node in scope has an effect
|
-- sources are only tracked if the node in scope has an effect
|
||||||
local effect = scope.effect
|
local effect = scope.effect
|
||||||
scope.effect = "untracked"
|
scope.effect = false
|
||||||
|
|
||||||
local ok, result = pcall(source)
|
local ok, result = pcall(source)
|
||||||
|
|
||||||
|
|
|
||||||
178
test/tests.luau
178
test/tests.luau
|
|
@ -51,7 +51,7 @@ TEST("graph", function()
|
||||||
end
|
end
|
||||||
|
|
||||||
local function scope()
|
local function scope()
|
||||||
return create_node(false, "owner")
|
return create_node(false, false)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function cleanup(fn: () -> ())
|
local function cleanup(fn: () -> ())
|
||||||
|
|
@ -257,10 +257,10 @@ TEST("graph", function()
|
||||||
|
|
||||||
do
|
do
|
||||||
local c = get_children(root)
|
local c = get_children(root)
|
||||||
CHECK(#c == 3)
|
CHECK(#c == 0)
|
||||||
CHECK(table.find(c, items_updated))
|
-- CHECK(table.find(c, items_updated))
|
||||||
CHECK(table.find(c, scope1 :: Node<any>))
|
-- CHECK(table.find(c, scope1 :: Node<any>))
|
||||||
CHECK(table.find(c, scope2 :: Node<any>))
|
-- CHECK(table.find(c, scope2 :: Node<any>))
|
||||||
end
|
end
|
||||||
|
|
||||||
do
|
do
|
||||||
|
|
@ -272,19 +272,19 @@ TEST("graph", function()
|
||||||
|
|
||||||
do
|
do
|
||||||
local c = get_children(scope1)
|
local c = get_children(scope1)
|
||||||
CHECK(#c == 1)
|
CHECK(#c == 0)
|
||||||
CHECK(table.find(c, bind1))
|
--CHECK(table.find(c, bind1))
|
||||||
end
|
end
|
||||||
|
|
||||||
do
|
do
|
||||||
local c = get_children(scope2)
|
local c = get_children(scope2)
|
||||||
CHECK(#c == 1)
|
CHECK(#c == 0)
|
||||||
CHECK(table.find(c, bind2))
|
--CHECK(table.find(c, bind2))
|
||||||
end
|
end
|
||||||
|
|
||||||
-- destroy
|
-- destroy
|
||||||
|
|
||||||
CHECK(table.find(get_children(root), scope1 :: Node<any>))
|
--CHECK(table.find(get_children(root), scope1 :: Node<any>))
|
||||||
|
|
||||||
destroy(scope1)
|
destroy(scope1)
|
||||||
CHECK(cleaned.scope1)
|
CHECK(cleaned.scope1)
|
||||||
|
|
@ -293,7 +293,7 @@ TEST("graph", function()
|
||||||
bind1 = NIL
|
bind1 = NIL
|
||||||
bind2 = NIL
|
bind2 = NIL
|
||||||
gc()
|
gc()
|
||||||
CHECK(#get_children(root) == 2)
|
CHECK(#get_children(root) == 0)
|
||||||
CHECK(#get_children(selected) == 1)
|
CHECK(#get_children(selected) == 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -1519,7 +1519,10 @@ end))
|
||||||
TEST("untrack()", wrap_root(function()
|
TEST("untrack()", wrap_root(function()
|
||||||
local source = vide.source
|
local source = vide.source
|
||||||
local effect = vide.effect
|
local effect = vide.effect
|
||||||
|
local derive = vide.derive
|
||||||
local untrack = vide.untrack
|
local untrack = vide.untrack
|
||||||
|
local cleanup = vide.cleanup
|
||||||
|
local root = vide.root
|
||||||
|
|
||||||
do CASE "does not register dependency"
|
do CASE "does not register dependency"
|
||||||
local a = source(0)
|
local a = source(0)
|
||||||
|
|
@ -1567,69 +1570,56 @@ TEST("untrack()", wrap_root(function()
|
||||||
CHECK(count == 2)
|
CHECK(count == 2)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- do CASE "outer scope"
|
do CASE "outer scope"
|
||||||
-- local outer_count = 0
|
local outer_count = 0
|
||||||
-- local inner_count = 0
|
local inner_count = 0
|
||||||
-- local cleaned_count = 0
|
local cleaned_count = 0
|
||||||
|
|
||||||
-- local input = source(0)
|
local input = source(0)
|
||||||
|
|
||||||
-- local output, destroy = root(function(destroy)
|
local output, destroy = root(function(destroy)
|
||||||
-- local output = derive(function()
|
local output = derive(function()
|
||||||
-- outer_count += 1
|
outer_count += 1
|
||||||
|
|
||||||
-- return untrack(function()
|
return untrack(function()
|
||||||
-- return derive(function()
|
return derive(function()
|
||||||
-- inner_count += 1
|
inner_count += 1
|
||||||
|
|
||||||
-- cleanup(function()
|
cleanup(function()
|
||||||
-- cleaned_count += 1
|
cleaned_count += 1
|
||||||
-- end)
|
end)
|
||||||
|
|
||||||
-- return tostring(input())
|
return tostring(input())
|
||||||
-- end)
|
end)
|
||||||
-- end)
|
|
||||||
-- end)
|
|
||||||
|
|
||||||
-- return output, destroy
|
|
||||||
-- end)
|
|
||||||
|
|
||||||
-- CHECK(outer_count == 1)
|
|
||||||
-- CHECK(inner_count == 1)
|
|
||||||
-- CHECK(cleaned_count == 0)
|
|
||||||
|
|
||||||
-- local output2 = output()
|
|
||||||
|
|
||||||
-- CHECK(output2() == "0")
|
|
||||||
|
|
||||||
-- input(1)
|
|
||||||
|
|
||||||
-- CHECK(outer_count == 1)
|
|
||||||
-- CHECK(inner_count == 2)
|
|
||||||
-- CHECK(cleaned_count == 1)
|
|
||||||
|
|
||||||
-- local output3 = output()
|
|
||||||
-- CHECK(output2() == "1")
|
|
||||||
-- CHECK(output3() == "1")
|
|
||||||
|
|
||||||
-- CHECK(output2 == output3)
|
|
||||||
|
|
||||||
-- destroy()
|
|
||||||
|
|
||||||
-- CHECK(cleaned_count == 2)
|
|
||||||
-- end
|
|
||||||
|
|
||||||
do CASE "cannot create effect within untrack()"
|
|
||||||
local ok = pcall(function()
|
|
||||||
effect(function()
|
|
||||||
untrack(function()
|
|
||||||
effect(function() end)
|
|
||||||
return nil
|
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
return output, destroy
|
||||||
end)
|
end)
|
||||||
|
|
||||||
CHECK(not ok)
|
CHECK(outer_count == 1)
|
||||||
|
CHECK(inner_count == 1)
|
||||||
|
CHECK(cleaned_count == 0)
|
||||||
|
|
||||||
|
local output2 = output()
|
||||||
|
|
||||||
|
CHECK(output2() == "0")
|
||||||
|
|
||||||
|
input(1)
|
||||||
|
|
||||||
|
CHECK(outer_count == 1)
|
||||||
|
CHECK(inner_count == 2)
|
||||||
|
CHECK(cleaned_count == 1)
|
||||||
|
|
||||||
|
local output3 = output()
|
||||||
|
CHECK(output2() == "1")
|
||||||
|
CHECK(output3() == "1")
|
||||||
|
|
||||||
|
CHECK(output2 == output3)
|
||||||
|
|
||||||
|
destroy()
|
||||||
|
|
||||||
|
CHECK(cleaned_count == 2)
|
||||||
end
|
end
|
||||||
end))
|
end))
|
||||||
|
|
||||||
|
|
@ -1765,48 +1755,6 @@ TEST("read()", wrap_root(function()
|
||||||
end))
|
end))
|
||||||
|
|
||||||
TEST("nested effects cases", function()
|
TEST("nested effects cases", function()
|
||||||
-- local vide = require "src/init"
|
|
||||||
-- local source = vide.source
|
|
||||||
-- local effect = vide.effect
|
|
||||||
-- local untrack = vide.untrack
|
|
||||||
-- local cleanup = vide.cleanup
|
|
||||||
-- local root = vide.root
|
|
||||||
|
|
||||||
-- local ran = 0
|
|
||||||
-- local cleaned = 0
|
|
||||||
|
|
||||||
-- local function Count()
|
|
||||||
-- local count = source(0)
|
|
||||||
|
|
||||||
-- effect(function()
|
|
||||||
-- count()
|
|
||||||
-- ran += 1
|
|
||||||
-- cleanup(function() cleaned += 1 end)
|
|
||||||
-- end)
|
|
||||||
|
|
||||||
-- return nil
|
|
||||||
-- end
|
|
||||||
|
|
||||||
-- local function App(destroy)
|
|
||||||
-- local name = source "a"
|
|
||||||
|
|
||||||
-- effect(function()
|
|
||||||
-- name()
|
|
||||||
-- untrack(Count)
|
|
||||||
-- end)
|
|
||||||
|
|
||||||
-- CHECK(ran == 1)
|
|
||||||
-- CHECK(cleaned == 0)
|
|
||||||
|
|
||||||
-- name "b"
|
|
||||||
|
|
||||||
-- CHECK(ran == 2)
|
|
||||||
-- CHECK(cleaned == 1)
|
|
||||||
-- print(cleaned)
|
|
||||||
-- end
|
|
||||||
|
|
||||||
-- root(App)
|
|
||||||
|
|
||||||
local vide = require "src/init"
|
local vide = require "src/init"
|
||||||
local source = vide.source
|
local source = vide.source
|
||||||
local effect = vide.effect
|
local effect = vide.effect
|
||||||
|
|
@ -1839,13 +1787,19 @@ TEST("nested effects cases", function()
|
||||||
|
|
||||||
CHECK(ran == 1)
|
CHECK(ran == 1)
|
||||||
CHECK(cleaned == 0)
|
CHECK(cleaned == 0)
|
||||||
|
|
||||||
|
name "b"
|
||||||
|
|
||||||
|
CHECK(ran == 2)
|
||||||
|
CHECK(cleaned == 1)
|
||||||
|
|
||||||
|
destroy()
|
||||||
|
|
||||||
|
CHECK(ran == 2)
|
||||||
|
CHECK(cleaned == 2)
|
||||||
end
|
end
|
||||||
|
|
||||||
local ok = pcall(function()
|
root(App)
|
||||||
root(App)
|
|
||||||
end)
|
|
||||||
|
|
||||||
CHECK(not ok)
|
|
||||||
end)
|
end)
|
||||||
|
|
||||||
vide.strict = true
|
vide.strict = true
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue