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 ArrayOrV<V> = {ArrayOrV<V>} | V
type Map<K, V> = { [K]: V } type Map<K, V> = { [K]: V }
local free_caches: { type Cache = {
-- event listeners to connect after properties are set -- event listeners to connect after properties are set
events: Map< events: Array<
string, -- event name | string -- 1. event name
() -> () -- listener | () -> () -- 2. listener
>, >,
-- actions to run after events are connected -- actions to run after events are connected
@ -32,18 +32,18 @@ local free_caches: {
Map<string, true> -- set of property names 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 -- each nested layer occupies two indexes: 1. table ref 2. nested depth
-- e.g. { t1 = { t3 = {} }, t2 = {} } -> { t1, 1, t2, 1, t3, 2 } -- e.g. { t1 = { t3 = {} }, t2 = {} } -> { t1, 1, t2, 1, t3, 2 }
nested_stack: { {} | number } nested_stack: { {} | number }
}? }
local function borrow_caches(): typeof(assert(free_caches)) local free_cache: Cache?
if free_caches then
local caches = free_caches :: typeof(assert(free_caches)) local function borrow_cache(): Cache
free_caches = nil if free_cache then
return caches local cache = free_cache
free_cache = nil
return cache
else else
return { return {
events = {}, events = {},
@ -58,46 +58,28 @@ local function borrow_caches(): typeof(assert(free_caches))
end end
end end
local function return_caches(caches: typeof(free_caches) ) local function return_cache(cache: Cache )
free_caches = caches free_cache = cache
end end
-- applies table of nested properties to an instance using full vide semantics local function process_properties(properties: Map<unknown, unknown>, instance: Instance, cache: Cache, depth: number)
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
for property, value in properties do for property, value in properties do
if property == "Parent" then continue end if property == "Parent" then continue end
if type(property) == "string" then if type(property) == "string" then
if strict then -- check for duplicate prop assignment at nesting depth if flags.strict then -- check for duplicate property assignment at nesting depth
if nested_debug[depth][property] then if cache.nested_debug[depth][property] then
throw(`duplicate property {property} at depth {depth}`) throw(`duplicate property {property} at depth {depth}`)
end end
nested_debug[depth][property] = true cache.nested_debug[depth][property] = true
end end
if type(value) == "function" then if type(value) == "function" then
if typeof((instance :: any)[property]) == "RBXScriptSignal" 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 else
bind.property(instance, property, value :: () -> ()) -- bind property bind.property(instance, property, value :: () -> ()) -- create implicit effect for property
end end
else else
(instance :: any)[property] = value -- set property (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 bind.children(instance, value :: () -> ArrayOrV<Instance>) -- bind children
elseif type(value) == "table" then elseif type(value) == "table" then
if is_action(value) 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 else
table.insert(nested_stack, value :: {}) process_properties(value :: Map<unknown, unknown>, instance, cache, depth + 1)
table.insert(nested_stack, depth + 1) -- push table to stack for later processing
end end
else else
(value :: Instance).Parent = instance -- parent child (value :: Instance).Parent = instance -- parent child
end end
end 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 depth = table.remove(nested_stack) :: number
properties = table.remove(nested_stack) :: {} properties = table.remove(nested_stack) :: {}
until not properties until not properties
for event, listener in next, events do for i = 1, #events, 2 do
(instance :: any)[event]:Connect(listener) local event_name = events[i]
local event_listener = events[i + 1]
;(instance :: any)[event_name]:Connect(event_listener)
end end
for _, queued in next, actions do for _, queued in next, actions do
@ -145,10 +150,10 @@ local function apply<T>(instance: T & Instance, properties: { [unknown]: unknown
-- clear caches -- clear caches
table.clear(events) table.clear(events)
for _, queued in next, actions do table.clear(queued) end 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) table.clear(nested_stack)
return_caches(caches) return_cache(caches)
return instance return instance
end end

View file

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

View file

@ -759,18 +759,35 @@ TEST("create()", wrap_root(function()
CHECK(text.Text == "test") CHECK(text.Text == "test")
end end
do CASE "nested precedence" do CASE "nested deferred"
local text = create "TextLabel" { local text = create "TextLabel" {
{ {
{ Text = "2" },
Text = "1", Text = "1",
{ Text = "2" }
} }
} }
CHECK(text.Text == "2") CHECK(text.Text == "2")
end 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" do CASE "independent"
local frame = create "Frame" local frame = create "Frame"
CHECK(frame {} ~= frame {}) CHECK(frame {} ~= frame {})