Add flag to disable deferral of nested properties

This commit is contained in:
aaron 2024-12-26 22:36:58 +00:00
parent ccaeb030f3
commit 4c639f8388
4 changed files with 95 additions and 68 deletions

View file

@ -13,11 +13,11 @@ type Array<V> = { V }
type ArrayOrV<V> = {ArrayOrV<V>} | V
type Map<K, V> = { [K]: V }
local free_caches: {
type Cache = {
-- event listeners to connect after properties are set
events: Map<
string, -- event name
() -> () -- listener
events: Array<
| string -- 1. event name
| () -> () -- 2. listener
>,
-- actions to run after events are connected
@ -32,18 +32,18 @@ local free_caches: {
Map<string, true> -- set of property names
>,
-- use stack instead of recursive function to process nesting layers one at time
-- deeper-nested properties take precedence over shallower-nested ones
-- each nested layer occupies two indexes: 1. table ref 2. nested depth
-- e.g. { t1 = { t3 = {} }, t2 = {} } -> { t1, 1, t2, 1, t3, 2 }
nested_stack: { {} | number }
}?
}
local function borrow_caches(): typeof(assert(free_caches))
if free_caches then
local caches = free_caches :: typeof(assert(free_caches))
free_caches = nil
return caches
local free_cache: Cache?
local function borrow_cache(): Cache
if free_cache then
local cache = free_cache
free_cache = nil
return cache
else
return {
events = {},
@ -58,46 +58,28 @@ local function borrow_caches(): typeof(assert(free_caches))
end
end
local function return_caches(caches: typeof(free_caches) )
free_caches = caches
local function return_cache(cache: Cache )
free_cache = cache
end
-- applies table of nested properties to an instance using full vide semantics
local function apply<T>(instance: T & Instance, properties: { [unknown]: unknown }): T
if not properties then
throw("attempt to call a constructor returned by create() with no properties")
end
local strict = flags.strict
-- queue parent assignment if any for last
local parent: unknown = properties.Parent
local caches = borrow_caches()
local events = caches.events
local actions = caches.actions
local nested_debug = caches.nested_debug
local nested_stack = caches.nested_stack
-- process all properties
local depth = 1
repeat
local function process_properties(properties: Map<unknown, unknown>, instance: Instance, cache: Cache, depth: number)
for property, value in properties do
if property == "Parent" then continue end
if type(property) == "string" then
if strict then -- check for duplicate prop assignment at nesting depth
if nested_debug[depth][property] then
if flags.strict then -- check for duplicate property assignment at nesting depth
if cache.nested_debug[depth][property] then
throw(`duplicate property {property} at depth {depth}`)
end
nested_debug[depth][property] = true
cache.nested_debug[depth][property] = true
end
if type(value) == "function" then
if typeof((instance :: any)[property]) == "RBXScriptSignal" then
events[property] = value :: () -> () -- add event to buffer
table.insert(cache.events, property) -- add event name to buffer
table.insert(cache.events, value :: () -> ()) -- add event listener to buffer
else
bind.property(instance, property, value :: () -> ()) -- bind property
bind.property(instance, property, value :: () -> ()) -- create implicit effect for property
end
else
(instance :: any)[property] = value -- set property
@ -107,24 +89,47 @@ local function apply<T>(instance: T & Instance, properties: { [unknown]: unknown
bind.children(instance, value :: () -> ArrayOrV<Instance>) -- bind children
elseif type(value) == "table" then
if is_action(value) then
table.insert(actions[(value :: any).priority], (value :: any).callback :: () -> ()) -- add action to buffer
table.insert(cache.actions[(value :: any).priority], (value :: any).callback :: () -> ()) -- add action to buffer
elseif flags.defer_nested_properties then
table.insert(cache.nested_stack, value :: {})
table.insert(cache.nested_stack, depth + 1) -- push table to stack for later processing
else
table.insert(nested_stack, value :: {})
table.insert(nested_stack, depth + 1) -- push table to stack for later processing
process_properties(value :: Map<unknown, unknown>, instance, cache, depth + 1)
end
else
(value :: Instance).Parent = instance -- parent child
end
end
end
end
-- applies table of nested properties to an instance using full vide semantics
local function apply<T>(instance: T & Instance, properties: { [unknown]: unknown }): T
if not properties then
throw("attempt to call a constructor returned by create() with no properties")
end
-- queue parent assignment if any for last
local parent: unknown = properties.Parent
local caches = borrow_cache()
local events = caches.events
local actions = caches.actions
local nested_debug = caches.nested_debug
local nested_stack = caches.nested_stack
-- process all properties
local depth = 1
repeat
process_properties(properties, instance, caches, depth)
depth = table.remove(nested_stack) :: number
properties = table.remove(nested_stack) :: {}
until not properties
for event, listener in next, events do
(instance :: any)[event]:Connect(listener)
for i = 1, #events, 2 do
local event_name = events[i]
local event_listener = events[i + 1]
;(instance :: any)[event_name]:Connect(event_listener)
end
for _, queued in next, actions do
@ -145,10 +150,10 @@ local function apply<T>(instance: T & Instance, properties: { [unknown]: unknown
-- clear caches
table.clear(events)
for _, queued in next, actions do table.clear(queued) end
if strict then table.clear(nested_debug) end
if flags.strict then table.clear(nested_debug) end
table.clear(nested_stack)
return_caches(caches)
return_cache(caches)
return instance
end

View file

@ -4,4 +4,8 @@ end
local is_O2 = inline_test() ~= "inline_test"
return { strict = not is_O2, batch = false }
return {
strict = not is_O2,
batch = false,
defer_nested_properties = true
}

View file

@ -75,6 +75,7 @@ local vide = {
-- flags
strict = (nil :: any) :: boolean,
defer_nested_properties = (nil :: any) :: boolean,
-- temporary
apply = function(instance: Instance)
@ -96,18 +97,18 @@ local vide = {
setmetatable(vide :: any, {
__index = function(_, index: unknown): ()
if index == "strict" then
return flags.strict
else
if flags[index] == nil then
throw(`{tostring(index)} is not a valid member of vide`)
else
return flags[index]
end
end,
__newindex = function(_, index: unknown, value: unknown)
if index == "strict" then
flags.strict = value :: boolean
else
if flags[index] == nil then
throw(`{tostring(index)} is not a valid member of vide`)
else
flags[index] = value
end
end
})

View file

@ -759,18 +759,35 @@ TEST("create()", wrap_root(function()
CHECK(text.Text == "test")
end
do CASE "nested precedence"
do CASE "nested deferred"
local text = create "TextLabel" {
{
{ Text = "2" },
Text = "1",
{ Text = "2" }
}
}
CHECK(text.Text == "2")
end
do CASE "nested not deferred"
vide.defer_nested_properties = false
local t = {}
create "TextLabel" {
{
{ function() table.insert(t, 1) end } :: any,
function() table.insert(t, 2) end,
}
}
CHECK(t[1] == 1)
CHECK(t[2] == 2)
vide.defer_nested_properties = true
end
do CASE "independent"
local frame = create "Frame"
CHECK(frame {} ~= frame {})