local typeof = game and typeof or require "../test/mock".typeof :: never local flags = require "./flags" local implicit_effect = require "./implicit_effect" local _, is_action = require "./action"() local graph = require "./graph" type Node = graph.Node type Array = { V } type ArrayOrV = {ArrayOrV} | V type Map = { [K]: V } type Cache = { -- event listeners to connect after properties are set events: Array< | string -- 1. event name | () -> () -- 2. listener >, -- actions to run after events are connected actions: Map< number, -- priority Array<(Instance) -> ()> -- action callbacks >, -- what to parent the instance to after running actions parent: unknown, -- cache to detect duplicate property setting at same nesting depth nested_debug: Map< number, -- depth Map -- set of property names >, -- 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 free_cache: Cache? local function borrow_cache(): Cache if free_cache then local cache = free_cache free_cache = nil return cache else return { events = {}, actions = setmetatable({} :: any, { -- lazy init __index = function(self, i) self[i] = {}; return self[i] end }), parent = nil, nested_debug = setmetatable({} :: any, { __index = function(self, i: number) self[i] = {}; return self[i] end }), nested_stack = {} } end end local function return_cache(cache: Cache ) free_cache = cache end local function process_properties(properties: Map, instance: Instance, cache: Cache, depth: number) for property, value in properties do if type(property) == "string" then if flags.strict then -- check for duplicate property assignment at nesting depth if cache.nested_debug[depth][property] then error(`duplicate property {property} at depth {depth}`, 0) end cache.nested_debug[depth][property] = true end if property == "Parent" then cache.parent = value continue end if type(value) == "function" then if typeof((instance :: any)[property]) == "RBXScriptSignal" then table.insert(cache.events, property) -- add event name to buffer table.insert(cache.events, value :: () -> ()) -- add event listener to buffer else implicit_effect.property(instance, property, value :: () -> ()) -- create implicit effect for property end else (instance :: any)[property] = value -- set property end elseif type(property) == "number" then if type(value) == "function" then implicit_effect.children(instance, value :: () -> ArrayOrV) -- bind children elseif type(value) == "table" then if is_action(value) then 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 process_properties(value :: Map, instance, cache, depth + 1) end elseif type(value) == "userdata" then (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(instance: T & Instance, properties: { [unknown]: unknown }): T if not properties then error "attempt to call a constructor returned by create() with no properties" end 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 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 actions do for _, callback in queued do callback(instance) end end local parent = caches.parent if parent then if type(parent) == "function" then implicit_effect.parent(instance, parent :: () -> Instance) else instance.Parent = parent :: Instance end end table.clear(events) for _, queued in actions do table.clear(queued) end caches.parent = nil if flags.strict then table.clear(nested_debug) end table.clear(nested_stack) return_cache(caches) return instance end return apply