Minor refactors

Also fixed potential bug with `create()` being called recursively if a
property binding passed as a property to `create()` also calls
`create()`
This commit is contained in:
Aaron Smith 2023-11-20 16:53:30 +00:00
parent 8eb5f96c5b
commit c288cb92c4
16 changed files with 169 additions and 130 deletions

View file

@ -13,41 +13,58 @@ type Node<T> = graph.Node<T>
type Array<V> = { V }
type Map<K, V> = { [K]: V }
-- buffer of event -> callback to connect after properties are set
local event_buffer = {} :: Map<string, () -> ()>
local free_caches: {
-- event listeners to connect after properties are set
events: Map<
string, -- event name
() -> () -- listener
>,
-- buffer of priority -> callback to run after events are connected
local action_buffers = {} :: Map<number, Array<(Instance) -> ()>>
-- actions to run after events are connected
actions: Map<
number, -- priority
Array<(Instance) -> ()> -- action callbacks
>,
-- lazily create buffers on nil index
setmetatable(action_buffers :: any, {
__index = function(_, i: number)
action_buffers[i] = {}
return action_buffers[i]
-- cache to detect duplicate property setting at same nesting depth
nested_debug: Map<
number, -- depth
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
else
return {
events = {},
actions = setmetatable({} :: any, { -- lazy init
__index = function(self, i) self[i] = {}; return self[i] end
}),
nested_debug = setmetatable({} :: any, {
__index = function(self, i: number) self[i] = {}; return self[i] end
}),
nested_stack = {}
}
end
})
end
-- cache in strict mode to detect duplicate property set at same nesting level
local nested_debug_cache = {} :: Map<number, Map<string, true>>
local function return_caches(caches: typeof(free_caches) )
free_caches = caches
end
setmetatable(nested_debug_cache :: any, {
__index = function(_, i: number)
nested_debug_cache[i] = {}
return nested_debug_cache[i]
end
})
-- use stack instead of recursive function to process nested 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 }
local nested_stack = {} :: { {} | number }
-- todo: solution without manual updating of this table
-- map of datatype names to class default constructor for aggregate init
local aggregates = {}
for i, v in next, {
for name, class in {
CFrame = CFrame,
Color3 = Color3,
UDim = UDim,
@ -55,27 +72,39 @@ for i, v in next, {
Vector2 = Vector2,
Vector3 = Vector3,
Rect = Rect
} do
aggregates[i] = v.new
} :: Map<string, { [string]: any }> do
aggregates[name] = class.new
end
-- processes a potentially nested table of values to assign to an instance
local function process_props(instance: Instance, properties: Map<unknown, unknown>)
-- 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
table.clear(nested_stack)
if strict then table.clear(nested_debug_cache) end
-- 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
if property == "Parent" then continue end
if type(property) == "string" then
if strict then -- check for duplicate prop assignment at nesting layer
if nested_debug_cache[depth][property] then
if strict then -- check for duplicate prop assignment at nesting depth
if nested_debug[depth][property] then
throw(`duplicate property {property} at depth {depth}`)
end
nested_debug_cache[depth][property] = true
nested_debug[depth][property] = true
end
if type(value) == "table" then -- attempt aggregate init
@ -86,7 +115,7 @@ local function process_props(instance: Instance, properties: Map<unknown, unknow
(instance :: any)[property] = ctor(unpack(value :: {}))
elseif type(value) == "function" then
if typeof((instance :: any)[property]) == "RBXScriptSignal" then
event_buffer[property] = value :: () -> () -- add event to buffer
events[property] = value :: () -> () -- add event to buffer
else
bind.property(instance, property, value :: () -> ()) -- bind property
end
@ -98,7 +127,7 @@ local function process_props(instance: Instance, properties: Map<unknown, unknow
bind.children(instance, value :: () -> Instance | Array<Instance>) -- bind children
elseif type(value) == "table" then
if is_action(value) then
table.insert(action_buffers[(value :: any).priority], (value :: any).callback :: () -> ()) -- add action to buffer
table.insert(actions[(value :: any).priority], (value :: any).callback :: () -> ()) -- add action to buffer
else
table.insert(nested_stack, value :: {})
table.insert(nested_stack, depth + 1) -- push table to stack for later processing
@ -109,40 +138,17 @@ local function process_props(instance: Instance, properties: Map<unknown, unknow
end
end
-- pop next nested table off stack
depth = table.remove(nested_stack) :: number
properties = table.remove(nested_stack) :: {}
until not properties
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("no properties given, did you forget to call the constructor returned by create()?")
for event, listener in next, events do
(instance :: any)[event]:Connect(listener)
end
-- queue parent assignment if any for last
local parent: unknown = properties.Parent
if parent then properties.Parent = nil end
-- reset buffers
table.clear(event_buffer)
for _, buffer in next, action_buffers do
table.clear(buffer)
end
-- process all properties for immediate setting or buffering
process_props(instance, properties)
-- connect buffered events
for event, fn in next, event_buffer do
(instance :: any)[event]:Connect(fn)
end
-- run buffered actions
for _, buffer in next, action_buffers do
for _, callback in next, buffer do
for _, queued in next, actions do
for _, callback in next, queued do
callback(instance)
end
end
@ -156,6 +162,14 @@ local function apply<T>(instance: T & Instance, properties: { [unknown]: unknown
end
end
-- clear caches
table.clear(events)
for _, queued in next, actions do table.clear(queued) end
if strict then table.clear(nested_debug) end
table.clear(nested_stack)
return_caches(caches)
return instance
end