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
@ -55,77 +65,110 @@ local function traceback(skips: number) -- ensures trace begins outside of any v
return debug.traceback(nil, s)
end
function bind(instance: Instance, property: string, setter: (Instance) -> ())
if flags.strict then
-- wrap setter in function with stack inspection for better error msgs
local fn = setter
local bind_trace = traceback(0)
setter = function(instance)
local ok, err: string? = xpcall(fn, function(err: string)
return err .. "\nsource updated at: " .. traceback(2)
end, instance)
if not ok then warn(`error occured updating {property}: {err}bound at: {bind_trace}`) end
end
end
function create_binding<T>(updater: (T) -> (), binding_data: T)
-- if flags.strict then
-- -- wrap setter in function with stack inspection for better error msgs
-- local fn = setter
-- local bind_trace = traceback(0)
-- setter = function(instance)
-- local ok, err: string? = xpcall(fn, function(err: string)
-- return err .. "\nsource updated at: " .. traceback(2)
-- end, instance)
-- if not ok then warn(`error occured updating {property}: {err}bound at: {bind_trace}`) end
-- end
-- end
local binding = create_node(instance)
binding.effect = setter
local binding = create_node(binding_data)
binding.effect = updater
local owner = get_scope()
assert(owner)
add_child(owner, binding)
open_scope(binding)
track(owner)
setter(instance)
updater(binding_data)
close_scope()
end
local function bind_property(instance: Instance, property: string, fn: () -> unknown)
bind(instance, property, function(instance_weak: any)
instance_weak[property] = fn()
end)
type PropertyBinding = {
instance: Instance,
property: string,
source: () -> unknown
}
local function update_property(p: PropertyBinding)
(p.instance :: any)[p.property] = p.source()
end
local function bind_parent(instance: Instance, fn: () -> Instance?)
bind(instance, "Parent", function(instance)
instance.Parent = fn()
end)
type ParentBinding = {
instance: Instance,
parent: () -> Instance
}
local function update_parent(p: ParentBinding)
p.instance.Parent = p.parent()
end
local function bind_children(parent: Instance, fn: () -> { Instance })
local current_child_set: { [Instance]: true } = {} -- cache of all children parented before update
local new_child_set: { [Instance]: true } = {} -- cache of all children parented after update
type ChildrenBinding = {
instance: Instance,
cur_children_set: { [Instance]: true },
new_children_set: { [Instance]: true },
children: () -> { Instance }
}
bind(parent, "Children", function(parent_weak)
local new_childs = fn() -- all (and only) children that should be parented after this update
if new_childs and type(new_childs) ~= "table" then
throw(`Cannot parent instance of type { type(new_childs) } `)
local function update_children(p: ChildrenBinding)
local cur_children_set: { [Instance]: true } = p.cur_children_set -- cache of all children parented before update
local new_child_set: { [Instance]: true } = p.new_children_set -- cache of all children parented after update
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
if new_childs then
for _, child in next, new_childs do
if new_children then
for _, child in next, new_children do
new_child_set[child] = true -- record child set from this update
if not current_child_set[child] then
child.Parent = parent_weak -- if child wasn't already parented then parent it
if not cur_children_set[child] then
child.Parent = p.instance -- if child wasn't already parented then parent it
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
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
end
table.clear(current_child_set) -- clear cache, preserve capacity
current_child_set, new_child_set = new_child_set, current_child_set
end)
table.clear(cur_children_set) -- clear cache, preserve capacity
p.cur_children_set, p.new_children_set = new_child_set, cur_children_set
end
return {
property = bind_property,
parent = bind_parent,
children = bind_children,
property = function(instance, property, source)
return create_binding(update_property, {
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
local update_queue = {} :: { Node<any> }
-- 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
for child in next, node.children do
table.insert(update_queue, child)
rec(child, update_queue)
rec(child)
end
table.clear(node.children)
end
local function update<T>(node: StartNode<T>)
local update_queue = {} :: { Node<any> }
--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
open_scope(n) -- todo
run_cleanups(n)
@ -138,11 +142,11 @@ local function update<T>(node: StartNode<T>)
table.clear(update_queue)
end
end
local function track<T>(node: StartNode<T>)
local scope = get_scope()
assert(scope)
if scope.effect then -- todo
if scope and scope.effect then -- todo
add_child(node, scope)
end
end

View file

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