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) } `)
local new_children = p.children() -- all (and only) children that should be parented after this update
if type(new_children) ~= "table" then
new_children = { new_children }
end end
if new_childs then if new_children then
for _, child in next, new_childs do for _, child in next, new_children do
new_child_set[child] = true -- record child set from this update new_child_set[child] = true -- record child set from this update
if not current_child_set[child] then if not cur_children_set[child] then
child.Parent = parent_weak -- if child wasn't already parented then parent it child.Parent = p.instance -- if child wasn't already parented then parent it
else else
current_child_set[child] = nil -- remove child from cache if it was already in cache cur_children_set[child] = nil -- remove child from cache if it was already in cache
end 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,24 +111,28 @@ 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
rec(node)
if first then
for _, n in next, update_queue do for _, n in next, update_queue do
open_scope(n) -- todo open_scope(n) -- todo
run_cleanups(n) run_cleanups(n)
@ -138,11 +142,11 @@ local function update<T>(node: StartNode<T>)
table.clear(update_queue) table.clear(update_queue)
end 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 ok = pcall(function()
-- -- local _derived = derive(function()
-- -- coroutine.yield()
-- -- return state()
-- -- end)
-- -- end)
-- -- CHECK(not ok)
-- -- end
-- -- do CASE "error on watcher callback yield"
-- -- local state = source(1)
-- -- local ok = pcall(function()
-- -- local _derived = watch(function()
-- -- coroutine.yield()
-- -- state()
-- -- end)
-- -- end)
-- -- CHECK(not ok)
-- -- end
-- do CASE "run derived callback twice"
-- local state = source(1) -- local state = source(1)
-- local runcount = 0
-- local ok = pcall(function() -- local _ = derive(function()
-- local _derived = derive(function() -- runcount += 1
-- coroutine.yield()
-- return state() -- return state()
-- end) -- end)
-- end)
-- CHECK(not ok) -- CHECK(runcount == 2)
-- state(2)
-- CHECK(runcount == 4)
-- end -- end
-- do CASE "error on watcher callback yield" -- do CASE "run watcher callback twice"
-- local state = source(1) -- local state = source(1)
-- local runcount = 0
-- local ok = pcall(function() -- watch(function()
-- local _derived = watch(function() -- runcount += 1
-- coroutine.yield()
-- state() -- state()
-- end) -- end)
-- CHECK(runcount == 2)
-- state(2)
-- CHECK(runcount == 4)
-- end
-- do CASE "indexes() error if primitive"
-- local state = source { 1 }
-- local ok = pcall(function()
-- indexes(state, function() return 1 end)
-- end) -- end)
-- CHECK(not ok) -- CHECK(not ok)
-- end -- end
do CASE "run derived callback twice" -- do CASE "values() error if duplicate"
local state = source(1) -- local state = source { 1, 2, 1 }
local runcount = 0
local _ = derive(function() -- local ok = pcall(function()
runcount += 1 -- values(state, function() return {} end)
return state() -- end)
end)
CHECK(runcount == 2) -- CHECK(not ok)
state(2) -- end
CHECK(runcount == 4)
end
do CASE "run watcher callback twice" -- do CASE "duplicate properties"
local state = source(1) -- local ok = pcall(function()
local runcount = 0 -- create "TextLabel" {
-- {
-- Name = "foo"
-- },
-- {
-- Name = "bar"
-- }
-- }
-- end)
watch(function() -- CHECK(not ok)
runcount += 1
state()
end)
CHECK(runcount == 2) -- ok = pcall(function()
state(2) -- create "TextLabel" {
CHECK(runcount == 4) -- {
end -- Name = "foo",
-- {
-- Name = "bar"
-- }
-- }
-- }
-- end)
do CASE "indexes() error if primitive" -- CHECK(ok)
local state = source { 1 } -- end
local ok = pcall(function() -- do CASE "multiple cleanup per scope"
indexes(state, function() return 1 end) -- local ok = pcall(function()
end) -- cleanup(function() end)
-- cleanup(function() end)
-- end)
CHECK(not ok) -- CHECK(not ok)
end -- end
-- end)
do CASE "values() error if duplicate"
local state = source { 1, 2, 1 }
local ok = pcall(function()
values(state, function() return {} end)
end)
CHECK(not ok)
end
do CASE "duplicate properties"
local ok = pcall(function()
create "TextLabel" {
{
Name = "foo"
},
{
Name = "bar"
}
}
end)
CHECK(not ok)
ok = pcall(function()
create "TextLabel" {
{
Name = "foo",
{
Name = "bar"
}
}
}
end)
CHECK(ok)
end
do CASE "multiple cleanup per scope"
local ok = pcall(function()
cleanup(function() end)
cleanup(function() end)
end)
CHECK(not ok)
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