This commit is contained in:
Aaron Smith 2023-09-11 12:44:53 +01:00
parent b1b860d773
commit 4f9db6ff41
3 changed files with 205 additions and 158 deletions

View file

@ -30,6 +30,16 @@ todo: investigate behavior in case B is parented to A, and A has no parent or re
]] ]]
type Binding = {
instance: Instance,
property: string,
source: () -> unknown
}
local function binder(b: Binding)
(b.instance :: any)[b.property] = b.source()
end
-- todo: replace with throw's method -- todo: replace with throw's method
@ -55,77 +65,110 @@ local function traceback(skips: number) -- ensures trace begins outside of any v
return debug.traceback(nil, s) return debug.traceback(nil, s)
end end
function bind(instance: Instance, property: string, setter: (Instance) -> ()) function create_binding<T>(updater: (T) -> (), binding_data: T)
if flags.strict then -- if flags.strict then
-- wrap setter in function with stack inspection for better error msgs -- -- wrap setter in function with stack inspection for better error msgs
local fn = setter -- local fn = setter
local bind_trace = traceback(0) -- local bind_trace = traceback(0)
setter = function(instance) -- setter = function(instance)
local ok, err: string? = xpcall(fn, function(err: string) -- local ok, err: string? = xpcall(fn, function(err: string)
return err .. "\nsource updated at: " .. traceback(2) -- return err .. "\nsource updated at: " .. traceback(2)
end, instance) -- end, instance)
if not ok then warn(`error occured updating {property}: {err}bound at: {bind_trace}`) end -- if not ok then warn(`error occured updating {property}: {err}bound at: {bind_trace}`) end
end -- end
end -- end
local binding = create_node(instance) local binding = create_node(binding_data)
binding.effect = setter binding.effect = updater
local owner = get_scope() local owner = get_scope()
assert(owner) assert(owner)
add_child(owner, binding)
open_scope(binding) open_scope(binding)
track(owner)
setter(instance) updater(binding_data)
close_scope() close_scope()
end end
local function bind_property(instance: Instance, property: string, fn: () -> unknown) type PropertyBinding = {
bind(instance, property, function(instance_weak: any) instance: Instance,
instance_weak[property] = fn() property: string,
end) source: () -> unknown
}
local function update_property(p: PropertyBinding)
(p.instance :: any)[p.property] = p.source()
end end
local function bind_parent(instance: Instance, fn: () -> Instance?) type ParentBinding = {
bind(instance, "Parent", function(instance) instance: Instance,
instance.Parent = fn() parent: () -> Instance
end) }
local function update_parent(p: ParentBinding)
p.instance.Parent = p.parent()
end end
local function bind_children(parent: Instance, fn: () -> { Instance }) type ChildrenBinding = {
local current_child_set: { [Instance]: true } = {} -- cache of all children parented before update instance: Instance,
local new_child_set: { [Instance]: true } = {} -- cache of all children parented after update cur_children_set: { [Instance]: true },
new_children_set: { [Instance]: true },
children: () -> { Instance }
}
bind(parent, "Children", function(parent_weak) local function update_children(p: ChildrenBinding)
local new_childs = fn() -- all (and only) children that should be parented after this update local cur_children_set: { [Instance]: true } = p.cur_children_set -- cache of all children parented before update
if new_childs and type(new_childs) ~= "table" then local new_child_set: { [Instance]: true } = p.new_children_set -- cache of all children parented after update
throw(`Cannot parent instance of type { type(new_childs) } `)
end
if new_childs then local new_children = p.children() -- all (and only) children that should be parented after this update
for _, child in next, new_childs do
new_child_set[child] = true -- record child set from this update if type(new_children) ~= "table" then
if not current_child_set[child] then new_children = { new_children }
child.Parent = parent_weak -- if child wasn't already parented then parent it end
else
current_child_set[child] = nil -- remove child from cache if it was already in cache if new_children then
end for _, child in next, new_children do
new_child_set[child] = true -- record child set from this update
if not cur_children_set[child] then
child.Parent = p.instance -- if child wasn't already parented then parent it
else
cur_children_set[child] = nil -- remove child from cache if it was already in cache
end end
end end
end
for child in next, current_child_set do for child in next, cur_children_set do
child.Parent = nil -- unparent all children that weren't in the new children set child.Parent = nil -- unparent all children that weren't in the new children set
end end
table.clear(current_child_set) -- clear cache, preserve capacity table.clear(cur_children_set) -- clear cache, preserve capacity
current_child_set, new_child_set = new_child_set, current_child_set p.cur_children_set, p.new_children_set = new_child_set, cur_children_set
end)
end end
return { return {
property = bind_property, property = function(instance, property, source)
parent = bind_parent, return create_binding(update_property, {
children = bind_children, instance = instance,
property = property,
source = source
})
end,
parent = function(instance, parent)
return create_binding(update_parent, {
instance = instance,
parent = parent
})
end,
children = function(instance, children)
return create_binding(update_children, {
instance = instance,
cur_children_set = {},
new_children_set = {},
children = children
})
end
} }

View file

@ -111,38 +111,42 @@ local function destroy<T>(node: Node<T>)
end end
end end
local update_queue = {} :: { Node<any> }
-- runs node effects, recalculates descendants and runs descendant effects -- runs node effects, recalculates descendants and runs descendant effects
local function rec<T>(node: StartNode<T>, update_queue: { Node<any> }) local function rec<T>(node: StartNode<T>)
if not node.children then return end if not node.children then return end
for child in next, node.children do for child in next, node.children do
table.insert(update_queue, child) table.insert(update_queue, child)
rec(child, update_queue) rec(child)
end end
table.clear(node.children) table.clear(node.children)
end end
local function update<T>(node: StartNode<T>) local function update<T>(node: StartNode<T>)
local update_queue = {} :: { Node<any> }
--assert(#update_queue == 0, "update already in progress") --assert(#update_queue == 0, "update already in progress")
rec(node, update_queue) -- check if recursive update
local first = update_queue[1] == nil
for _, n in next, update_queue do rec(node)
open_scope(n) -- todo
run_cleanups(n) if first then
run_effect(n) for _, n in next, update_queue do
close_scope() open_scope(n) -- todo
run_cleanups(n)
run_effect(n)
close_scope()
end
table.clear(update_queue)
end end
table.clear(update_queue)
end end
local function track<T>(node: StartNode<T>) local function track<T>(node: StartNode<T>)
local scope = get_scope() local scope = get_scope()
assert(scope) if scope and scope.effect then -- todo
if scope.effect then -- todo
add_child(node, scope) add_child(node, scope)
end end
end end

View file

@ -1218,127 +1218,127 @@ TEST("actions", function()
end end
end) end)
TEST("strict", function() -- TEST("strict", function()
vide.strict = true -- vide.strict = true
local create = vide.create -- local create = vide.create
local source = vide.source -- local source = vide.source
local derive = vide.derive -- local derive = vide.derive
local watch = vide.watch -- local watch = vide.watch
local indexes, values = vide.indexes, vide.values -- local indexes, values = vide.indexes, vide.values
local cleanup = vide.cleanup -- local cleanup = vide.cleanup
-- do CASE "error on derived callback yield" -- -- do CASE "error on derived callback yield"
-- local state = source(1) -- -- local state = source(1)
-- local ok = pcall(function() -- -- local ok = pcall(function()
-- local _derived = derive(function() -- -- local _derived = derive(function()
-- coroutine.yield() -- -- coroutine.yield()
-- return state() -- -- return state()
-- end) -- -- end)
-- end) -- -- end)
-- CHECK(not ok) -- -- CHECK(not ok)
-- end -- -- end
-- do CASE "error on watcher callback yield" -- -- do CASE "error on watcher callback yield"
-- local state = source(1) -- -- local state = source(1)
-- local ok = pcall(function() -- -- local ok = pcall(function()
-- local _derived = watch(function() -- -- local _derived = watch(function()
-- coroutine.yield() -- -- coroutine.yield()
-- state() -- -- state()
-- end) -- -- end)
-- end) -- -- end)
-- CHECK(not ok) -- -- CHECK(not ok)
-- end -- -- end
do CASE "run derived callback twice" -- do CASE "run derived callback twice"
local state = source(1) -- local state = source(1)
local runcount = 0 -- local runcount = 0
local _ = derive(function() -- local _ = derive(function()
runcount += 1 -- runcount += 1
return state() -- return state()
end) -- end)
CHECK(runcount == 2) -- CHECK(runcount == 2)
state(2) -- state(2)
CHECK(runcount == 4) -- CHECK(runcount == 4)
end -- end
do CASE "run watcher callback twice" -- do CASE "run watcher callback twice"
local state = source(1) -- local state = source(1)
local runcount = 0 -- local runcount = 0
watch(function() -- watch(function()
runcount += 1 -- runcount += 1
state() -- state()
end) -- end)
CHECK(runcount == 2) -- CHECK(runcount == 2)
state(2) -- state(2)
CHECK(runcount == 4) -- CHECK(runcount == 4)
end -- end
do CASE "indexes() error if primitive" -- do CASE "indexes() error if primitive"
local state = source { 1 } -- local state = source { 1 }
local ok = pcall(function() -- local ok = pcall(function()
indexes(state, function() return 1 end) -- indexes(state, function() return 1 end)
end) -- end)
CHECK(not ok) -- CHECK(not ok)
end -- end
do CASE "values() error if duplicate" -- do CASE "values() error if duplicate"
local state = source { 1, 2, 1 } -- local state = source { 1, 2, 1 }
local ok = pcall(function() -- local ok = pcall(function()
values(state, function() return {} end) -- values(state, function() return {} end)
end) -- end)
CHECK(not ok) -- CHECK(not ok)
end -- end
do CASE "duplicate properties" -- do CASE "duplicate properties"
local ok = pcall(function() -- local ok = pcall(function()
create "TextLabel" { -- create "TextLabel" {
{ -- {
Name = "foo" -- Name = "foo"
}, -- },
{ -- {
Name = "bar" -- Name = "bar"
} -- }
} -- }
end) -- end)
CHECK(not ok) -- CHECK(not ok)
ok = pcall(function() -- ok = pcall(function()
create "TextLabel" { -- create "TextLabel" {
{ -- {
Name = "foo", -- Name = "foo",
{ -- {
Name = "bar" -- Name = "bar"
} -- }
} -- }
} -- }
end) -- end)
CHECK(ok) -- CHECK(ok)
end -- end
do CASE "multiple cleanup per scope" -- do CASE "multiple cleanup per scope"
local ok = pcall(function() -- local ok = pcall(function()
cleanup(function() end) -- cleanup(function() end)
cleanup(function() end) -- cleanup(function() end)
end) -- end)
CHECK(not ok) -- CHECK(not ok)
end -- end
end) -- end)
local ok = FINISH() local ok = FINISH()
if not ok then error("Tests failed", 0) end if not ok then error("Tests failed", 0) end