mirror of
https://github.com/centau/vide.git
synced 2026-08-20 14:41:37 +00:00
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:
parent
8eb5f96c5b
commit
c288cb92c4
16 changed files with 169 additions and 130 deletions
|
|
@ -10,14 +10,14 @@ local function is_action(v: any)
|
|||
end
|
||||
|
||||
local function action(callback: (Instance) -> (), priority: number?): Action
|
||||
local t = {
|
||||
local a = {
|
||||
priority = priority or 1,
|
||||
callback = callback
|
||||
}
|
||||
|
||||
setmetatable(t :: any, ActionMT)
|
||||
setmetatable(a :: any, ActionMT)
|
||||
|
||||
return table.freeze(t)
|
||||
return table.freeze(a)
|
||||
end
|
||||
|
||||
return function()
|
||||
|
|
|
|||
146
src/apply.luau
146
src/apply.luau
|
|
@ -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]
|
||||
end
|
||||
})
|
||||
-- cache to detect duplicate property setting at same nesting depth
|
||||
nested_debug: Map<
|
||||
number, -- depth
|
||||
Map<string, true> -- set of property names
|
||||
>,
|
||||
|
||||
-- cache in strict mode to detect duplicate property set at same nesting level
|
||||
local nested_debug_cache = {} :: Map<number, Map<string, true>>
|
||||
|
||||
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
|
||||
-- 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 }
|
||||
local nested_stack = {} :: { {} | number }
|
||||
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
|
||||
|
||||
local function return_caches(caches: typeof(free_caches) )
|
||||
free_caches = caches
|
||||
end
|
||||
|
||||
-- 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
|
||||
|
||||
-- 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
|
||||
|
||||
-- processes a potentially nested table of values to assign to an instance
|
||||
local function process_props(instance: Instance, properties: Map<unknown, unknown>)
|
||||
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
|
||||
|
||||
for event, listener in next, events do
|
||||
(instance :: any)[event]:Connect(listener)
|
||||
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()?")
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ local flags = require(script.Parent.flags)
|
|||
local graph = require(script.Parent.graph)
|
||||
type Node<T> = graph.Node<T>
|
||||
local create_node = graph.create_node
|
||||
local get_owning_scope = graph.get_owning_scope
|
||||
local assert_owning_scope = graph.assert_owning_scope
|
||||
local evaluate_node = graph.evaluate_node
|
||||
local set_owner = graph.set_owner
|
||||
|
||||
|
|
@ -31,8 +31,7 @@ function create_binding<T>(updater: (T) -> T, binding: T)
|
|||
end
|
||||
end
|
||||
|
||||
|
||||
local owner = get_owning_scope()
|
||||
local owner = assert_owning_scope()
|
||||
|
||||
local node = create_node(binding, updater)
|
||||
|
||||
|
|
|
|||
|
|
@ -5,9 +5,11 @@ local Instance = game and Instance or require "test/mock".Instance :: never
|
|||
local throw = require(script.Parent.throw)
|
||||
local defaults = require(script.Parent.defaults)
|
||||
local apply = require(script.Parent.apply)
|
||||
local memoize = require(script.Parent.memoize)
|
||||
|
||||
local function create_instance(class: string)
|
||||
local ctor_cache = {} :: { [string]: () -> Instance }
|
||||
|
||||
setmetatable(ctor_cache :: any, {
|
||||
__index = function(self, class)
|
||||
local ok, instance: Instance = pcall(Instance.new, class :: any)
|
||||
if not ok then throw(`invalid class name, could not create instance of class { class }`) end
|
||||
|
||||
|
|
@ -18,29 +20,37 @@ local function create_instance(class: string)
|
|||
end
|
||||
end
|
||||
|
||||
return function(properties: { [any]: unknown }): Instance
|
||||
local function ctor(properties: Props): Instance
|
||||
return apply(instance:Clone(), properties)
|
||||
end
|
||||
end; create_instance = memoize(create_instance) -- always return same constructor for given class
|
||||
|
||||
self[class] = ctor
|
||||
return ctor
|
||||
end
|
||||
})
|
||||
|
||||
local function create_instance(class: string)
|
||||
return ctor_cache[class]
|
||||
end
|
||||
|
||||
local function clone_instance(instance: Instance)
|
||||
return function(properties: { [any]: unknown }): Instance
|
||||
return function(properties: Props): Instance
|
||||
local clone = instance:Clone()
|
||||
if not clone then error("Attempt to clone a non-archivable instance", 3) end
|
||||
if not clone then throw "attempt to clone a non-archivable instance" end
|
||||
return apply(clone, properties)
|
||||
end
|
||||
end
|
||||
|
||||
local function create(class_or_instance: string|Instance)
|
||||
local function create(class_or_instance: string|Instance): (Props) -> Instance
|
||||
if type(class_or_instance) == "string" then
|
||||
return create_instance(class_or_instance)
|
||||
elseif typeof(class_or_instance) == "Instance" then
|
||||
return clone_instance(class_or_instance)
|
||||
else
|
||||
throw("bad argument #1, expected string or instance, got " .. typeof(class_or_instance))
|
||||
end
|
||||
return nil :: never
|
||||
end
|
||||
end
|
||||
|
||||
type Props = { [any]: any }
|
||||
return (create :: any) ::
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ local graph = require(script.Parent.graph)
|
|||
local create_node = graph.create_node
|
||||
local set_owner = graph.set_owner
|
||||
local track = graph.track
|
||||
local get_owning_scope = graph.get_owning_scope
|
||||
local assert_owning_scope = graph.assert_owning_scope
|
||||
local evaluate_node = graph.evaluate_node
|
||||
|
||||
local function derive<T>(source: () -> T): () -> T
|
||||
local owner = get_owning_scope()
|
||||
local owner = assert_owning_scope()
|
||||
|
||||
local node = create_node(false :: any, source)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@ if not game then script = require "test/relative-string" end
|
|||
|
||||
local graph = require(script.Parent.graph)
|
||||
local create_node = graph.create_node
|
||||
local get_owning_scope = graph.get_owning_scope
|
||||
local assert_owning_scope = graph.assert_owning_scope
|
||||
local evaluate_node = graph.evaluate_node
|
||||
local set_owner = graph.set_owner
|
||||
|
||||
local function effect<T>(callback: (T) -> T, initial_value: T)
|
||||
local owner = get_owning_scope()
|
||||
local owner = assert_owning_scope()
|
||||
|
||||
local node = create_node(initial_value, callback)
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ local function get_scope(): Node<unknown>?
|
|||
return scopes[scopes.n]
|
||||
end
|
||||
|
||||
local function get_owning_scope(): Node<unknown>
|
||||
local function assert_owning_scope(): Node<unknown>
|
||||
local scope = get_scope()
|
||||
|
||||
if not scope then
|
||||
|
|
@ -187,7 +187,7 @@ end
|
|||
|
||||
local _flushing = false
|
||||
local function flush_update_queue()
|
||||
assert(not flushing, "recursive queue flush occured") -- todo
|
||||
assert(not _flushing, "recursive queue flush occured") -- todo
|
||||
_flushing = true
|
||||
|
||||
local n0 = 0
|
||||
|
|
@ -266,7 +266,7 @@ return table.freeze {
|
|||
close_scope = close_scope,
|
||||
evaluate_node = evaluate_node,
|
||||
get_scope = get_scope,
|
||||
get_owning_scope = get_owning_scope,
|
||||
assert_owning_scope = assert_owning_scope,
|
||||
add_cleanup = add_cleanup,
|
||||
set_owner = set_owner,
|
||||
destroy = destroy,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ local effect = require(script.effect)
|
|||
local derive = require(script.derive)
|
||||
local cleanup = require(script.cleanup)
|
||||
local untrack = require(script.untrack)
|
||||
local read = require(script.read)
|
||||
local batch = require(script.batch)
|
||||
local switch = require(script.switch)
|
||||
local show = require(script.show)
|
||||
|
|
@ -60,10 +61,8 @@ local vide = {
|
|||
-- util
|
||||
cleanup = cleanup,
|
||||
untrack = untrack,
|
||||
read = read,
|
||||
batch = batch,
|
||||
read = function<T>(value: T | () -> T): T
|
||||
return if type(value) == "function" then value() else value
|
||||
end,
|
||||
|
||||
-- animations
|
||||
spring = spring,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ local create_start_node = graph.create_start_node
|
|||
local set_owner = graph.set_owner
|
||||
local track = graph.track
|
||||
local update = graph.update
|
||||
local get_owning_scope = graph.get_owning_scope
|
||||
local assert_owning_scope = graph.assert_owning_scope
|
||||
local open_scope = graph.open_scope
|
||||
local close_scope = graph.close_scope
|
||||
local evaluate_node = graph.evaluate_node
|
||||
|
|
@ -28,7 +28,7 @@ local function check_primitives(t: {})
|
|||
end
|
||||
|
||||
local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI, K) -> VO): () -> { VO }
|
||||
local owner = get_owning_scope()
|
||||
local owner = assert_owning_scope()
|
||||
|
||||
local subowner = create_node(false, false)
|
||||
set_owner(subowner, owner)
|
||||
|
|
@ -123,7 +123,7 @@ local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI,
|
|||
end
|
||||
|
||||
local function values<K, VI, VO>(input: () -> Map<K, VI>, transform: (VI, () -> K) -> VO): () -> { VO }
|
||||
local owner = get_owning_scope()
|
||||
local owner = assert_owning_scope()
|
||||
|
||||
local subowner = create_node(false, false)
|
||||
set_owner(subowner, owner)
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
local function memoize<X, Y>(f: (X) -> Y): (X) -> Y
|
||||
local cache: { [X]: Y? } = {}
|
||||
|
||||
return function(x: X): Y
|
||||
local y = cache[x]
|
||||
|
||||
if not y then
|
||||
y = f(x)
|
||||
cache[x] = y
|
||||
end
|
||||
|
||||
return y :: Y
|
||||
end
|
||||
end
|
||||
|
||||
return memoize
|
||||
|
||||
7
src/read.luau
Normal file
7
src/read.luau
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
if not game then script = require "test/relative-string" end
|
||||
|
||||
local function read<T>(value: T | () -> T): T
|
||||
return if type(value) == "function" then value() else value
|
||||
end
|
||||
|
||||
return read
|
||||
|
|
@ -6,7 +6,7 @@ local create_start_node = graph.create_start_node
|
|||
local track = graph.track
|
||||
local update = graph.update
|
||||
|
||||
export type Source<T> = (() -> T) & ((T) -> T)
|
||||
export type Source<T> = (() -> T) & ((value: T) -> T)
|
||||
|
||||
local function source<T>(initial_value: T): Source<T>
|
||||
local node = create_start_node(initial_value)
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ type Node<T> = graph.Node<T>
|
|||
type StartNode<T> = graph.StartNode<T>
|
||||
local create_node = graph.create_node
|
||||
local create_start_node = graph.create_start_node
|
||||
local get_owning_scope = graph.get_owning_scope
|
||||
local assert_owning_scope = graph.assert_owning_scope
|
||||
local evaluate_node = graph.evaluate_node
|
||||
local update = graph.update
|
||||
local set_owner = graph.set_owner
|
||||
|
|
@ -150,7 +150,7 @@ local springs: { [SpringData<any>]: StartNode<any> } = {}
|
|||
setmetatable(springs, { __mode = "v" })
|
||||
|
||||
local function spring<T>(source: () -> T, period: number?, damping_ratio: number?): () -> T
|
||||
local owner = get_owning_scope()
|
||||
local owner = assert_owning_scope()
|
||||
|
||||
-- https://en.wikipedia.org/wiki/Damping
|
||||
|
||||
|
|
|
|||
|
|
@ -9,14 +9,14 @@ local evaluate_node = graph.evaluate_node
|
|||
local set_owner = graph.set_owner
|
||||
local track = graph.track
|
||||
local destroy = graph.destroy
|
||||
local get_owning_scope = graph.get_owning_scope
|
||||
local assert_owning_scope = graph.assert_owning_scope
|
||||
local open_scope = graph.open_scope
|
||||
local close_scope = graph.close_scope
|
||||
|
||||
type Map<K, V> = { [K]: V }
|
||||
|
||||
local function switch<T, U>(source: () -> T): (map: Map<T, ((() -> U)?)>) -> () -> U?
|
||||
local owner = get_owning_scope()
|
||||
local owner = assert_owning_scope()
|
||||
|
||||
return function(map)
|
||||
local last_scope: Node<false>?
|
||||
|
|
@ -35,7 +35,7 @@ local function switch<T, U>(source: () -> T): (map: Map<T, ((() -> U)?)>) -> ()
|
|||
if component == nil then return nil end
|
||||
|
||||
if type(component) ~= "function" then
|
||||
throw("map must map a value to a function")
|
||||
throw "map must map a value to a function"
|
||||
end
|
||||
|
||||
local new_scope = create_node(false, false)
|
||||
|
|
|
|||
|
|
@ -942,6 +942,33 @@ TEST("create()", wrap_root(function()
|
|||
CHECK(not wref[1])
|
||||
end
|
||||
|
||||
do CASE "recursive create"
|
||||
local set_test_to_true = vide.action(function(self) (self :: any).test = true end)
|
||||
|
||||
local f2
|
||||
|
||||
local to_apply = {
|
||||
{ a = 1 },
|
||||
set_test_to_true,
|
||||
b = function() f2 = create "Frame" { a = 2 } end,
|
||||
} :: { [number|string]: unknown }
|
||||
|
||||
-- do -- confirm iteration order
|
||||
-- local t = {}
|
||||
-- for i in to_apply do
|
||||
-- table.insert(t, i)
|
||||
-- end
|
||||
-- assert(t[1] == "a")
|
||||
-- end
|
||||
|
||||
local f = create "Frame" (to_apply)
|
||||
|
||||
CHECK((f :: any).a == 1)
|
||||
CHECK((f :: any).test == true )
|
||||
|
||||
CHECK((f2 :: any).a == 2)
|
||||
end
|
||||
|
||||
do CASE "garbage collection test"
|
||||
local wref
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue