mirror of
https://github.com/centau/vide.git
synced 2026-08-20 14:41:37 +00:00
This commit is contained in:
parent
f7ce5f024d
commit
7cdcebf03c
11 changed files with 367 additions and 233 deletions
|
|
@ -2,7 +2,10 @@
|
||||||
-- vide/apply.lua
|
-- vide/apply.lua
|
||||||
------------------------------------------------------------------------------------------
|
------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
if not game then script = (require :: any) "test/wrap-require" end
|
if not game then
|
||||||
|
script = (require :: any) "test/wrap-require"
|
||||||
|
typeof = require "test/mock".typeof
|
||||||
|
end
|
||||||
|
|
||||||
local graph = require(script.Parent.graph)
|
local graph = require(script.Parent.graph)
|
||||||
type Node<T> = graph.Node<T>
|
type Node<T> = graph.Node<T>
|
||||||
|
|
|
||||||
46
src/cleanup.lua
Normal file
46
src/cleanup.lua
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
------------------------------------------------------------------------------------------
|
||||||
|
-- vide/cleanup.lua
|
||||||
|
------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if not game then script = require "test/wrap-require" end
|
||||||
|
|
||||||
|
-- todo: verify correct behavior in non-standard usage
|
||||||
|
local cleanup_callbacks = {} :: { [string]: () -> () }
|
||||||
|
local cleanup_callers = {} :: { [string]: () -> () }
|
||||||
|
|
||||||
|
setmetatable(cleanup_callers :: any, { __mode = "vs" })
|
||||||
|
|
||||||
|
-- todo: rare case where mem address is reused by another function on same line
|
||||||
|
|
||||||
|
local function cleanup(callback: () -> ())
|
||||||
|
local caller = debug.info(2, "f") :: () -> ()
|
||||||
|
local line = debug.info(2, "l") :: number
|
||||||
|
local ref = tostring(caller) .. "\0" .. line
|
||||||
|
|
||||||
|
local fn = cleanup_callbacks[ref]
|
||||||
|
if fn then
|
||||||
|
fn()
|
||||||
|
else
|
||||||
|
cleanup_callers[ref] = caller
|
||||||
|
end
|
||||||
|
cleanup_callbacks[ref] = callback
|
||||||
|
end
|
||||||
|
|
||||||
|
local buffer = {}
|
||||||
|
|
||||||
|
local function clean_garbage()
|
||||||
|
for ref, callback in next, cleanup_callbacks do
|
||||||
|
if cleanup_callers[ref] == nil then -- caller was garbage collected
|
||||||
|
callback()
|
||||||
|
table.insert(buffer, ref)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, ref in next, buffer do
|
||||||
|
cleanup_callbacks[ref] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
table.clear(buffer)
|
||||||
|
end
|
||||||
|
|
||||||
|
return function() return cleanup, clean_garbage end
|
||||||
|
|
@ -9,19 +9,9 @@ local create = graph.create
|
||||||
local get = graph.get
|
local get = graph.get
|
||||||
local capture_and_link = graph.capture_and_link
|
local capture_and_link = graph.capture_and_link
|
||||||
|
|
||||||
local function derive<T>(fn: () -> T, cleanup: (T) -> ()?): () -> T
|
local function derive<T>(fn: () -> T): () -> T
|
||||||
local node = create((nil :: any) :: T)
|
local node = create((nil :: any) :: T)
|
||||||
|
|
||||||
if cleanup then
|
|
||||||
local f = fn
|
|
||||||
local last: T? = nil
|
|
||||||
fn = function()
|
|
||||||
if last ~= nil then cleanup(last) end
|
|
||||||
last = f()
|
|
||||||
return last :: T
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
node.cache = capture_and_link(node, fn)
|
node.cache = capture_and_link(node, fn)
|
||||||
|
|
||||||
return function()
|
return function()
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ if not game then script = (require :: any) "test/wrap-require" end
|
||||||
local create = require(script.create)
|
local create = require(script.create)
|
||||||
local source = require(script.source)
|
local source = require(script.source)
|
||||||
local watch = require(script.watch)
|
local watch = require(script.watch)
|
||||||
|
local cleanup, clean_garbage = require(script.cleanup)()
|
||||||
local derive = require(script.derive)
|
local derive = require(script.derive)
|
||||||
local map = require(script.map)
|
local map = require(script.map)
|
||||||
|
|
||||||
|
|
@ -25,9 +26,10 @@ local vide = {
|
||||||
-- core
|
-- core
|
||||||
create = create,
|
create = create,
|
||||||
source = source,
|
source = source,
|
||||||
|
watch = watch,
|
||||||
|
cleanup = cleanup,
|
||||||
derive = derive,
|
derive = derive,
|
||||||
map = map,
|
map = map,
|
||||||
watch = watch,
|
|
||||||
|
|
||||||
-- animations
|
-- animations
|
||||||
spring = spring,
|
spring = spring,
|
||||||
|
|
@ -45,6 +47,7 @@ local vide = {
|
||||||
-- test
|
-- test
|
||||||
step = function(dt: number)
|
step = function(dt: number)
|
||||||
update_springs(dt)
|
update_springs(dt)
|
||||||
|
clean_garbage()
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -93,14 +93,14 @@ local lerpable: { [string]: Lerp<any> } = {
|
||||||
local springs: { [SpringData<any>]: Node<any> } = {}
|
local springs: { [SpringData<any>]: Node<any> } = {}
|
||||||
setmetatable(springs, { __mode = "vs" })
|
setmetatable(springs, { __mode = "vs" })
|
||||||
|
|
||||||
local function spring<T>(target: () -> T, period: number, damping_ratio: number?): () -> T
|
local function spring<T>(target: () -> T, period: number?, damping_ratio: number?): () -> T
|
||||||
local initial_position = target()
|
local initial_position = target()
|
||||||
local node = create(initial_position)
|
local node = create(initial_position)
|
||||||
|
|
||||||
local data: SpringData<T> = {
|
local data: SpringData<T> = {
|
||||||
alpha = 0,
|
alpha = 0,
|
||||||
duration = 0,
|
duration = 0,
|
||||||
period = period,
|
period = period or 1,
|
||||||
damping_ratio = damping_ratio or 1,
|
damping_ratio = damping_ratio or 1,
|
||||||
velocity = 0,
|
velocity = 0,
|
||||||
initial_velocity = 0,
|
initial_velocity = 0,
|
||||||
|
|
|
||||||
|
|
@ -9,25 +9,18 @@ local set_effect = graph.set_effect
|
||||||
local capture = graph.capture
|
local capture = graph.capture
|
||||||
|
|
||||||
local function watch(effect: () -> ()): () -> ()
|
local function watch(effect: () -> ()): () -> ()
|
||||||
-- todo: call cleanup on initial debug call when strict
|
local nodes = capture(effect :: () -> nil)
|
||||||
local nodes, cleanup = capture(effect :: () -> (() -> ()?), nil)
|
|
||||||
|
|
||||||
nodes = table.clone(nodes)
|
nodes = table.clone(nodes)
|
||||||
|
|
||||||
local function fn()
|
|
||||||
if cleanup then cleanup(); cleanup = nil end
|
|
||||||
cleanup = effect()
|
|
||||||
end
|
|
||||||
|
|
||||||
for _, node in next, nodes do
|
for _, node in next, nodes do
|
||||||
set_effect(node, fn, true)
|
set_effect(node, effect, true)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function unwatch()
|
local function unwatch()
|
||||||
for _, node in next, nodes do
|
for _, node in next, nodes do
|
||||||
set_effect(node, fn, nil)
|
set_effect(node, effect, nil)
|
||||||
end
|
end
|
||||||
if cleanup then cleanup(); cleanup = nil end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return unwatch
|
return unwatch
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
|
--!nocheck
|
||||||
-- wrapped task library for use in pure luau
|
-- wrapped task library for use in pure luau
|
||||||
local task = { spawn = function(thread, ...)
|
local task = { spawn = function(thread, ...)
|
||||||
local ok, err = coroutine.resume(thread, ...)
|
local ok, err = coroutine.resume(thread, ...)
|
||||||
if not ok then error(err, 3) end
|
if not ok then error(err, 3) end
|
||||||
end }
|
end }
|
||||||
|
|
||||||
|
export type Type = RBXScriptSignal & { Fire: (Type, ...any)-> () }
|
||||||
|
|
||||||
----------------------------------------------------------------------------------------------------
|
----------------------------------------------------------------------------------------------------
|
||||||
-- Batched Yield-Safe Signal Implementation --
|
-- Batched Yield-Safe Signal Implementation --
|
||||||
-- This is a Signal class which has effectively identical behavior to a --
|
-- This is a Signal class which has effectively identical behavior to a --
|
||||||
|
|
@ -106,10 +109,12 @@ setmetatable(Connection, {
|
||||||
local Signal = {}
|
local Signal = {}
|
||||||
Signal.__index = Signal
|
Signal.__index = Signal
|
||||||
|
|
||||||
function Signal.new()
|
Signal.__type = "RBXScriptSignal"
|
||||||
|
|
||||||
|
function Signal.new(): Type
|
||||||
return setmetatable({
|
return setmetatable({
|
||||||
_handlerListHead = false,
|
_handlerListHead = false,
|
||||||
}, Signal)
|
}, Signal) :: any
|
||||||
end
|
end
|
||||||
|
|
||||||
function Signal:Connect(fn)
|
function Signal:Connect(fn)
|
||||||
|
|
@ -174,14 +179,4 @@ function Signal:Once(fn)
|
||||||
return cn
|
return cn
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Make signal strict
|
|
||||||
setmetatable(Signal, {
|
|
||||||
__index = function(tb, key)
|
|
||||||
error(("Attempt to get Signal::%s (not a valid member)"):format(tostring(key)), 2)
|
|
||||||
end,
|
|
||||||
__newindex = function(tb, key, value)
|
|
||||||
error(("Attempt to set Signal::%s (not a valid member)"):format(tostring(key)), 2)
|
|
||||||
end
|
|
||||||
})
|
|
||||||
|
|
||||||
return Signal
|
return Signal
|
||||||
|
|
|
||||||
162
test/mock.luau
162
test/mock.luau
|
|
@ -1,101 +1,111 @@
|
||||||
local Instance = {} do
|
local Instance = {} do
|
||||||
local Signal = require "test/goodsignal"
|
local Signal = require "test/goodsignal"
|
||||||
|
type Signal = Signal.Type
|
||||||
|
|
||||||
type userdata = { __USERDATA__: true }
|
type userdata = { __USERDATA: true }
|
||||||
|
|
||||||
type Proxy = {
|
--[[
|
||||||
_Userdata: userdata,
|
|
||||||
_Data: Data,
|
attempt to mimic roblox engine's method of userdata proxy to actual instance data
|
||||||
|
proxy can gc independantly of actual instance data
|
||||||
|
proxy prevents gc of actual instance data
|
||||||
|
luau code never has direct access to actual instance data, only to proxy
|
||||||
|
|
||||||
|
proxy knows data
|
||||||
|
data does not know proxy
|
||||||
|
separate weak map kept data -> proxy
|
||||||
|
|
||||||
|
]]
|
||||||
|
|
||||||
|
type ProxyMT = {
|
||||||
|
proxy: userdata,
|
||||||
|
data: Data,
|
||||||
__index: any,
|
__index: any,
|
||||||
__newindex: any
|
__newindex: any
|
||||||
}
|
}
|
||||||
|
|
||||||
type Data = {
|
type Data = {
|
||||||
Name: string,
|
name: string,
|
||||||
Parent: Data?,
|
parent: Data?,
|
||||||
Children: { Data },
|
children: { Data },
|
||||||
Changed: { [string]: RBXScriptSignal & { Fire: any } },
|
changed: { [string]: Signal },
|
||||||
Table: { [string]: unknown },
|
properties: { [string]: unknown },
|
||||||
Destroying: RBXScriptSignal,
|
destroying: Signal,
|
||||||
ClassName: string,
|
class: string,
|
||||||
Type: "Instance"
|
type: "Instance"
|
||||||
}
|
}
|
||||||
|
|
||||||
local function deepclone<T>(template: T & {}): T
|
local function deep_clone<T>(template: T & {}): T
|
||||||
local t = table.clone(template :: {}) :: {}
|
local t = table.clone(template :: {}) :: {}
|
||||||
|
|
||||||
for i, v in next, t do
|
for i, v in next, t do
|
||||||
if type(v) == "table" then
|
if type(v) == "table" then
|
||||||
t[i] = deepclone(v)
|
t[i] = deep_clone(v)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return t :: T & {}
|
return t :: T & {}
|
||||||
end
|
end
|
||||||
|
|
||||||
--[[
|
|
||||||
attempt to mimic roblox engine's method of userdata proxy to actual instance data
|
|
||||||
proxy can gc independantly of actual instance data
|
|
||||||
proxy prevents gc of actual instance data
|
|
||||||
luau code never has direct access to actual instance data, only to proxy
|
|
||||||
]]
|
|
||||||
local proxies = {} :: { [Data]: userdata? }
|
local proxies = {} :: { [Data]: userdata? }
|
||||||
setmetatable(proxies :: any, { __mode = "v" })
|
setmetatable(proxies :: any, { __mode = "v" })
|
||||||
|
|
||||||
local function getdata(userdata: userdata): Data
|
local function get_data(userdata: userdata): Data
|
||||||
local function getproxy(userdata: userdata): Proxy
|
local function f(userdata: userdata): ProxyMT
|
||||||
return getmetatable(userdata :: any)
|
return getmetatable(userdata :: any)
|
||||||
end
|
end
|
||||||
|
|
||||||
return getproxy(userdata)._Data
|
return f(userdata).data
|
||||||
end
|
end
|
||||||
|
|
||||||
local function isInstance(value: unknown): boolean
|
local function is_instance(value: unknown): boolean
|
||||||
local mt = getmetatable(value :: any)
|
local mt = getmetatable(value :: any)
|
||||||
return mt and mt._Data and mt._Data.Type == "Instance"
|
return mt and mt.data and mt.data.type == "Instance"
|
||||||
end
|
end
|
||||||
|
|
||||||
local methods = {}
|
local methods = {}
|
||||||
|
|
||||||
local function __index(userdata: userdata, property: string): ()
|
local function __index(userdata: userdata, property: string): ()
|
||||||
local data = getdata(userdata)
|
local data = get_data(userdata)
|
||||||
return if methods[property] then methods[property]
|
return if methods[property] then methods[property]
|
||||||
elseif property == "Name" then data.Name
|
elseif property == "Name" then data.name
|
||||||
elseif property == "Parent" then data.Parent
|
elseif property == "Parent" then data.parent
|
||||||
elseif property == "Destroying" then data.Destroying
|
elseif property == "Destroying" then data.destroying
|
||||||
else data.Table[property]
|
else data.properties[property]
|
||||||
end
|
end
|
||||||
|
|
||||||
local function __newindex(userdata: userdata, property: string, value: unknown)
|
local function __newindex(userdata: userdata, property: string, value: unknown)
|
||||||
local data = getdata(userdata)
|
local data = get_data(userdata)
|
||||||
if property == "Name" then
|
if property == "Name" then
|
||||||
data.Name = value :: string
|
data.name = value :: string
|
||||||
elseif property == "Parent" then
|
elseif property == "Parent" then
|
||||||
assert(value == nil or isInstance(value), "attempt to set non-instance as parent")
|
assert(value == nil or is_instance(value), "attempt to set non-instance as parent")
|
||||||
local parent = data.Parent
|
local parent = data.parent
|
||||||
if parent then
|
if parent then
|
||||||
data.Parent = nil
|
data.parent = nil
|
||||||
table.remove(parent.Children, table.find(parent.Children, data))
|
table.remove(parent.children, table.find(parent.children, data))
|
||||||
end
|
end
|
||||||
if value then
|
if value then
|
||||||
data.Parent = getdata(value :: userdata)
|
data.parent = get_data(value :: userdata)
|
||||||
table.insert(getdata(value :: userdata).Children, data)
|
table.insert(get_data(value :: userdata).children, data)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
data.Table[property] = value
|
data.properties[property] = value
|
||||||
end
|
end
|
||||||
|
|
||||||
if data.Changed[property] then
|
if data.changed[property] then
|
||||||
data.Changed[property]:Fire()
|
data.changed[property]:Fire()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function getuserdata(data: Data): userdata
|
local function get_proxy(data: Data): userdata
|
||||||
return proxies[data] or (function()
|
return proxies[data] or (function()
|
||||||
local userdata = newproxy(true)
|
local userdata = newproxy(true)
|
||||||
local proxy = getmetatable(userdata)
|
local proxy = getmetatable(userdata)
|
||||||
proxy._Userdata = userdata
|
proxy.proxy = userdata
|
||||||
proxy._Data = data
|
proxy.data = data
|
||||||
proxy.__index = __index
|
proxy.__index = __index
|
||||||
proxy.__newindex = __newindex
|
proxy.__newindex = __newindex
|
||||||
proxies[data] = userdata
|
proxies[data] = userdata
|
||||||
|
|
@ -105,29 +115,29 @@ local Instance = {} do
|
||||||
|
|
||||||
function Instance.new(class: string): Instance
|
function Instance.new(class: string): Instance
|
||||||
local data = {
|
local data = {
|
||||||
Name = "UNNAMED",
|
name = "UNNAMED",
|
||||||
Parent = nil,
|
parent = nil,
|
||||||
Children = {},
|
children = {},
|
||||||
Changed = {},
|
changed = {},
|
||||||
Table = {},
|
properties = {},
|
||||||
ClassName = class,
|
class = class,
|
||||||
Destroying = Signal.new() :: any,
|
destroying = Signal.new() :: any,
|
||||||
Type = "Instance" :: "Instance"
|
type = "Instance" :: "Instance"
|
||||||
}
|
}
|
||||||
|
|
||||||
return getuserdata(data) :: any
|
return get_proxy(data) :: any
|
||||||
end
|
end
|
||||||
|
|
||||||
function Instance.isInstance(value: unknown): boolean
|
function Instance.is_instance(value: unknown): boolean
|
||||||
return isInstance(value)
|
return is_instance(value)
|
||||||
end
|
end
|
||||||
|
|
||||||
function methods.Clone(userdata: userdata): userdata
|
function methods.Clone(userdata: userdata): userdata
|
||||||
local data = getdata(userdata)
|
local data = get_data(userdata)
|
||||||
local clone_userdata = (Instance.new("") :: any) :: userdata
|
local clone_userdata = (Instance.new("") :: any) :: userdata
|
||||||
local clone_data = getdata(clone_userdata)
|
local clone_data = get_data(clone_userdata)
|
||||||
|
|
||||||
for i, v in next, deepclone(data) do
|
for i, v in next, deep_clone(data) do
|
||||||
clone_data[i] = v
|
clone_data[i] = v
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -135,40 +145,40 @@ local Instance = {} do
|
||||||
end
|
end
|
||||||
|
|
||||||
function methods.FindFirstChild(userdata: userdata, target: string): userdata?
|
function methods.FindFirstChild(userdata: userdata, target: string): userdata?
|
||||||
local data = getdata(userdata)
|
local data = get_data(userdata)
|
||||||
for _, child in data.Children do
|
for _, child in data.children do
|
||||||
if child.Name == target then
|
if child.name == target then
|
||||||
return getuserdata(child)
|
return get_proxy(child)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
function methods.GetChildren(userdata: userdata): { userdata }
|
function methods.GetChildren(userdata: userdata): { userdata }
|
||||||
local children = getdata(userdata).Children
|
local children = get_data(userdata).children
|
||||||
local userdatas = table.create(#children)
|
local userdatas = table.create(#children)
|
||||||
|
|
||||||
for i, child in next, children do
|
for i, child in next, children do
|
||||||
userdatas[i] = getuserdata(child)
|
userdatas[i] = get_proxy(child)
|
||||||
end
|
end
|
||||||
|
|
||||||
return userdatas
|
return userdatas
|
||||||
end
|
end
|
||||||
|
|
||||||
function methods.GetPropertyChangedSignal(userdata: userdata, property: string): RBXScriptSignal
|
function methods.GetPropertyChangedSignal(userdata: userdata, property: string): RBXScriptSignal
|
||||||
local data = getdata(userdata)
|
local data = get_data(userdata)
|
||||||
if not data.Changed[property] then
|
if not data.changed[property] then
|
||||||
data.Changed[property] = Signal.new() :: any
|
data.changed[property] = Signal.new() :: any
|
||||||
end
|
end
|
||||||
return data.Changed[property]
|
return data.changed[property]
|
||||||
end
|
end
|
||||||
|
|
||||||
function methods.Destroy(userdata: userdata)
|
function methods.Destroy(userdata: userdata)
|
||||||
local data = getdata(userdata);
|
local data = get_data(userdata);
|
||||||
(data.Destroying :: any):Fire()
|
data.destroying:Fire()
|
||||||
data.Parent = nil
|
data.parent = nil
|
||||||
if data.Changed["Parent"] then
|
if data.changed["Parent"] then
|
||||||
data.Changed["Parent"]:Fire()
|
data.changed["Parent"]:Fire()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -227,8 +237,10 @@ local Enum = {} :: any do
|
||||||
end})
|
end})
|
||||||
end
|
end
|
||||||
|
|
||||||
local function typeof(v)
|
local function typeof(v): string
|
||||||
return Instance.isInstance(v) and "Instance" or type(v)
|
return if Instance.is_instance(v) then "Instance"
|
||||||
|
elseif getmetatable(v) and getmetatable(v).__type then getmetatable(v).__type
|
||||||
|
else type(v)
|
||||||
end
|
end
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -1,72 +1,43 @@
|
||||||
local vide = require "src/init"
|
local vide = require "src/init"
|
||||||
local wrap = vide.wrap
|
local source = vide.source
|
||||||
local derive = vide.derive
|
local derive = vide.derive
|
||||||
local map = vide.map
|
local map = vide.map
|
||||||
local create = vide.create
|
local create = vide.create
|
||||||
local unwrap = vide.unwrap
|
|
||||||
local Event = vide.Event
|
|
||||||
local Layout = vide.Layout
|
|
||||||
|
|
||||||
do
|
|
||||||
local count = wrap(0)
|
|
||||||
local count2 = derive(function(from)
|
|
||||||
local c = from(count) * unwrap(count)
|
|
||||||
return c ^ 2
|
|
||||||
end)
|
|
||||||
local x = count2.value
|
|
||||||
|
|
||||||
local count2b = count + 1
|
|
||||||
local xb = unwrap(count2b)
|
|
||||||
end
|
|
||||||
|
|
||||||
local count = wrap(0)
|
|
||||||
local v = count.value
|
|
||||||
|
|
||||||
do
|
|
||||||
local t = map(1, function(i)
|
|
||||||
return ""
|
|
||||||
end)
|
|
||||||
|
|
||||||
local t = map({ true }, function(i, v)
|
|
||||||
return 1
|
|
||||||
end)
|
|
||||||
|
|
||||||
local data = wrap { "" }
|
|
||||||
|
|
||||||
local t = map(data, function(i, v)
|
|
||||||
return 1
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
create("Frame") {
|
|
||||||
|
|
||||||
|
type Action<T> = {
|
||||||
|
type: T,
|
||||||
|
priority: number,
|
||||||
|
callback: (Instance) -> ()
|
||||||
}
|
}
|
||||||
|
|
||||||
local Value, Computed, peek, OnEvent = nil :: any, nil :: any, nil :: any, nil :: any
|
local function processor(priority: number, fn: (Instance) -> ()): Action<any>
|
||||||
local New = nil :: any
|
|
||||||
|
|
||||||
local function Counter(props)
|
|
||||||
local count, set = wrap(0)
|
|
||||||
|
|
||||||
return create("TextLabel") {
|
|
||||||
Text = "Count: " .. count,
|
|
||||||
|
|
||||||
[Layout] = props[Layout],
|
|
||||||
|
|
||||||
[Event.Activated] = function()
|
|
||||||
set(count + 1)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function Changed(property: string, callback: () -> ())
|
||||||
|
return processor(1, function(instance)
|
||||||
|
instance:GetPropertyChangedSignal(property):Connect(callback)
|
||||||
|
end) :: Action<"Changed">
|
||||||
|
end
|
||||||
|
|
||||||
|
local function Cleanup(t: {})
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
function TextInput(p: {
|
||||||
|
OnInput: Action<"Changed">
|
||||||
|
})
|
||||||
|
create "TextBox" {
|
||||||
|
Text = "test",
|
||||||
|
|
||||||
|
Changed("Text", function()
|
||||||
|
|
||||||
|
end),
|
||||||
|
|
||||||
|
Cleanup {
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
create("Frame") {}
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
local function Frame(props)
|
|
||||||
return create("Frame") {
|
|
||||||
Position = props.Position
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
Frame { Position = UDim2.fromScale(0.5, 0.5) }
|
|
||||||
|
|
||||||
Frame { Position = positionState }
|
|
||||||
|
|
|
||||||
219
test/tests.luau
219
test/tests.luau
|
|
@ -363,6 +363,7 @@ end)
|
||||||
TEST("watch()", function()
|
TEST("watch()", function()
|
||||||
local source = vide.source
|
local source = vide.source
|
||||||
local watch = vide.watch
|
local watch = vide.watch
|
||||||
|
local cleanup = vide.cleanup
|
||||||
|
|
||||||
do CASE "Capture states"
|
do CASE "Capture states"
|
||||||
local a = source(1)
|
local a = source(1)
|
||||||
|
|
@ -405,7 +406,7 @@ TEST("watch()", function()
|
||||||
local unwatch = watch(function()
|
local unwatch = watch(function()
|
||||||
state()
|
state()
|
||||||
effect_runcount += 1
|
effect_runcount += 1
|
||||||
return function() cleanup_runcount += 1 end
|
cleanup(function() cleanup_runcount += 1 end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
CHECK(effect_runcount == 1)
|
CHECK(effect_runcount == 1)
|
||||||
|
|
@ -413,7 +414,12 @@ TEST("watch()", function()
|
||||||
state(2)
|
state(2)
|
||||||
CHECK(effect_runcount == 2)
|
CHECK(effect_runcount == 2)
|
||||||
CHECK(cleanup_runcount == 1)
|
CHECK(cleanup_runcount == 1)
|
||||||
|
|
||||||
unwatch()
|
unwatch()
|
||||||
|
unwatch = nil :: any
|
||||||
|
gc()
|
||||||
|
vide.step(0)
|
||||||
|
|
||||||
CHECK(effect_runcount == 2)
|
CHECK(effect_runcount == 2)
|
||||||
CHECK(cleanup_runcount == 2)
|
CHECK(cleanup_runcount == 2)
|
||||||
end
|
end
|
||||||
|
|
@ -478,6 +484,117 @@ TEST("watch()", function()
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
TEST("cleanup()", function()
|
||||||
|
local source = vide.source
|
||||||
|
local derive = vide.derive
|
||||||
|
local watch = vide.watch
|
||||||
|
local cleanup = vide.cleanup
|
||||||
|
|
||||||
|
do CASE "Cleanup runs for watcher"
|
||||||
|
local state = source(1)
|
||||||
|
|
||||||
|
local watched = 0
|
||||||
|
local cleaned = 0
|
||||||
|
|
||||||
|
local stop = watch(function()
|
||||||
|
state()
|
||||||
|
watched += 1
|
||||||
|
cleanup(function()
|
||||||
|
cleaned += 1
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
CHECK(watched == 1)
|
||||||
|
CHECK(cleaned == 0)
|
||||||
|
|
||||||
|
state(2)
|
||||||
|
|
||||||
|
CHECK(watched == 2)
|
||||||
|
CHECK(cleaned == 1)
|
||||||
|
|
||||||
|
stop()
|
||||||
|
|
||||||
|
do -- vide detects by iterating through and checking for gc'd refs
|
||||||
|
stop = nil :: any
|
||||||
|
gc()
|
||||||
|
vide.step(0)
|
||||||
|
end
|
||||||
|
|
||||||
|
CHECK(watched == 2)
|
||||||
|
CHECK(cleaned == 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
do CASE "Scoped"
|
||||||
|
local function setup()
|
||||||
|
local state = source(1)
|
||||||
|
local obj = { cleaned = 0 }
|
||||||
|
|
||||||
|
local _stop = watch(function()
|
||||||
|
state()
|
||||||
|
cleanup(function()
|
||||||
|
obj.cleaned += 1
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
return state, obj
|
||||||
|
end
|
||||||
|
|
||||||
|
local stateA, objA = setup()
|
||||||
|
local stateB, objB = setup()
|
||||||
|
|
||||||
|
CHECK(objA.cleaned == 0)
|
||||||
|
CHECK(objB.cleaned == 0)
|
||||||
|
|
||||||
|
stateA(2)
|
||||||
|
|
||||||
|
CHECK(objA.cleaned == 1)
|
||||||
|
CHECK(objB.cleaned == 0)
|
||||||
|
|
||||||
|
stateB(2)
|
||||||
|
|
||||||
|
CHECK(objA.cleaned == 1)
|
||||||
|
CHECK(objB.cleaned == 1)
|
||||||
|
|
||||||
|
do
|
||||||
|
stateA = nil :: any
|
||||||
|
stateB = nil :: any
|
||||||
|
gc()
|
||||||
|
vide.step(0)
|
||||||
|
end
|
||||||
|
|
||||||
|
CHECK(objA.cleaned == 2)
|
||||||
|
CHECK(objB.cleaned == 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
do CASE "Multiple cleanup"
|
||||||
|
local state = source(1)
|
||||||
|
|
||||||
|
local queue = {}
|
||||||
|
|
||||||
|
watch(function()
|
||||||
|
state()
|
||||||
|
cleanup(function() table.insert(queue, 1) end)
|
||||||
|
cleanup(function() table.insert(queue, 2) end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
CHECK(testkit.seq(queue, {}))
|
||||||
|
state(2)
|
||||||
|
CHECK(testkit.seq(queue, { 1, 2 }))
|
||||||
|
state(3)
|
||||||
|
CHECK(testkit.seq(queue, { 1, 2, 1, 2 }))
|
||||||
|
|
||||||
|
do
|
||||||
|
state = nil :: any
|
||||||
|
gc()
|
||||||
|
vide.step(0)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- todo: guarantee call order when gc? (currently not)
|
||||||
|
--testkit.print2(queue)
|
||||||
|
--CHECK(testkit.seq(queue, { 1, 2, 1, 2, 1, 2 }))
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
TEST("create()", function()
|
TEST("create()", function()
|
||||||
local create = vide.create
|
local create = vide.create
|
||||||
local source = vide.source
|
local source = vide.source
|
||||||
|
|
@ -894,6 +1011,7 @@ TEST("map()", function()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
TEST("spring()", function()
|
TEST("spring()", function()
|
||||||
|
local create = vide.create
|
||||||
local source = vide.source
|
local source = vide.source
|
||||||
local spring = vide.spring
|
local spring = vide.spring
|
||||||
|
|
||||||
|
|
@ -909,43 +1027,43 @@ TEST("spring()", function()
|
||||||
end
|
end
|
||||||
|
|
||||||
do CASE "Garbage collection"
|
do CASE "Garbage collection"
|
||||||
do -- `spring` should not allow gc of `state`
|
do -- `output` should not allow gc of `input`
|
||||||
local state = source(10)
|
local input = source(10)
|
||||||
local _springed = spring(state, 1, 1)
|
local output = spring(input)
|
||||||
|
|
||||||
wref.state, state = state, nil :: any
|
local wref = { input }
|
||||||
|
input = nil :: any
|
||||||
|
|
||||||
gc()
|
gc()
|
||||||
CHECK(wref.state)
|
CHECK(wref[1])
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
do -- `value` should allow gc of `spring`
|
do -- `input` should allow gc of `output`
|
||||||
local value = source(10)
|
local input = source(10)
|
||||||
local springed = spring(value, 1, 1) :: State?
|
local output = spring(input)
|
||||||
|
|
||||||
wref.springed, springed = springed, nil
|
local wref = { output }
|
||||||
|
output = nil :: any
|
||||||
|
|
||||||
gc()
|
gc()
|
||||||
CHECK(not wref.springed)
|
CHECK(not wref[1])
|
||||||
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local create = vide.create
|
|
||||||
|
|
||||||
do CASE "Garbage collection (binded)"
|
do CASE "Garbage collection (binded)"
|
||||||
local number = source(10)
|
local input = source(10)
|
||||||
local springed = spring(number, 1, 1) :: State?
|
local output = spring(input, 1, 1)
|
||||||
|
|
||||||
local _label = create "TextLabel" {
|
local _label = create "TextLabel" {
|
||||||
Text = springed
|
Text = output
|
||||||
}
|
}
|
||||||
|
|
||||||
wref.springed, springed = springed, nil
|
local wref = { output }
|
||||||
|
output = nil :: any
|
||||||
|
|
||||||
gc()
|
gc()
|
||||||
CHECK(wref.springed) -- `springed` should not gc
|
CHECK(wref[1]) -- `output` should not gc
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
@ -954,9 +1072,11 @@ TEST("Events", function()
|
||||||
|
|
||||||
local function Thing(props)
|
local function Thing(props)
|
||||||
local instance = Instance.new("Thing") :: any
|
local instance = Instance.new("Thing") :: any
|
||||||
instance.Signal = (Signal.new() :: any) :: RBXScriptSignal & { Fire: any }
|
instance.Signal = Signal.new()
|
||||||
testkit.print2(getmetatable(instance))
|
|
||||||
return create(instance)(props)
|
local clone = create(instance)(props)
|
||||||
|
|
||||||
|
return clone
|
||||||
end
|
end
|
||||||
|
|
||||||
do CASE "Connect event"
|
do CASE "Connect event"
|
||||||
|
|
@ -969,7 +1089,7 @@ TEST("Events", function()
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
|
|
||||||
testkit.print2(getmetatable(val))
|
-- testkit.print2(getmetatable(val))
|
||||||
|
|
||||||
CHECK(not connected)
|
CHECK(not connected)
|
||||||
val.Value = 1; val.Signal:Fire(val.Value)
|
val.Value = 1; val.Signal:Fire(val.Value)
|
||||||
|
|
@ -977,6 +1097,7 @@ TEST("Events", function()
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
--[[
|
||||||
TEST("Changed", function()
|
TEST("Changed", function()
|
||||||
local create = vide.create
|
local create = vide.create
|
||||||
local Changed = vide.Changed
|
local Changed = vide.Changed
|
||||||
|
|
@ -1023,6 +1144,7 @@ TEST("Changed", function()
|
||||||
CHECK(Changed.Test == Changed.Test)
|
CHECK(Changed.Test == Changed.Test)
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
]]
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
TEST("Created", function()
|
TEST("Created", function()
|
||||||
|
|
@ -1050,7 +1172,6 @@ TEST("strict", function()
|
||||||
vide.strict = true
|
vide.strict = true
|
||||||
|
|
||||||
local source = vide.source
|
local source = vide.source
|
||||||
local unsource = vide.unsource
|
|
||||||
local derive = vide.derive
|
local derive = vide.derive
|
||||||
local watch = vide.watch
|
local watch = vide.watch
|
||||||
|
|
||||||
|
|
@ -1058,9 +1179,9 @@ TEST("strict", function()
|
||||||
local state = source(1)
|
local state = source(1)
|
||||||
|
|
||||||
local ok = pcall(function()
|
local ok = pcall(function()
|
||||||
local _derived = derive(function(from)
|
local _derived = derive(function()
|
||||||
coroutine.yield()
|
coroutine.yield()
|
||||||
return from(state)
|
return state()
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
@ -1071,9 +1192,9 @@ TEST("strict", function()
|
||||||
local state = source(1)
|
local state = source(1)
|
||||||
|
|
||||||
local ok = pcall(function()
|
local ok = pcall(function()
|
||||||
local _derived = watch(function(from)
|
local _derived = watch(function()
|
||||||
coroutine.yield()
|
coroutine.yield()
|
||||||
local _ = from(state)
|
state()
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
@ -1081,59 +1202,33 @@ TEST("strict", function()
|
||||||
end
|
end
|
||||||
|
|
||||||
do CASE "Run derived callback twice"
|
do CASE "Run derived callback twice"
|
||||||
local state, set = source(1)
|
local state = source(1)
|
||||||
local runcount = 0
|
local runcount = 0
|
||||||
|
|
||||||
local derived = derive(function(from)
|
local _ = derive(function()
|
||||||
runcount += 1
|
runcount += 1
|
||||||
return from(state)
|
return state()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
CHECK(runcount == 2)
|
CHECK(runcount == 2)
|
||||||
set(2)
|
state(2)
|
||||||
local _ = derived()
|
|
||||||
CHECK(runcount == 4)
|
CHECK(runcount == 4)
|
||||||
end
|
end
|
||||||
|
|
||||||
do CASE "Run watcher callback twice"
|
do CASE "Run watcher callback twice"
|
||||||
local state, set = source(1)
|
local state = source(1)
|
||||||
local runcount = 0
|
local runcount = 0
|
||||||
|
|
||||||
watch(function(from)
|
watch(function()
|
||||||
runcount += 1
|
runcount += 1
|
||||||
local _ = from(state)
|
state()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
CHECK(runcount == 2)
|
CHECK(runcount == 2)
|
||||||
set(2)
|
state(2)
|
||||||
vide.step(1/60)
|
|
||||||
CHECK(runcount == 4)
|
CHECK(runcount == 4)
|
||||||
end
|
end
|
||||||
|
|
||||||
do CASE "Does not allow non-layout properties"
|
|
||||||
local ok = pcall(function()
|
|
||||||
vide.create "Frame" {
|
|
||||||
[vide.Layout] = {
|
|
||||||
AnchorPoint = Vector2.new(0, 0.5),
|
|
||||||
BackgroundColor3 = Color3.new(0, 0, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
end)
|
|
||||||
CHECK(not ok)
|
|
||||||
end
|
|
||||||
|
|
||||||
do CASE "Does not allow same table set"
|
|
||||||
local _, set = source()
|
|
||||||
|
|
||||||
local t = {}
|
|
||||||
|
|
||||||
set(t)
|
|
||||||
|
|
||||||
local ok = pcall(set, t)
|
|
||||||
|
|
||||||
CHECK(not ok)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- todo: add case for strict mode bindings
|
-- todo: add case for strict mode bindings
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
|
||||||
26
todo.md
26
todo.md
|
|
@ -39,6 +39,32 @@ async/loading/suspense
|
||||||
|
|
||||||
define order with nested properties
|
define order with nested properties
|
||||||
|
|
||||||
|
```lua
|
||||||
|
type Action<T> = {
|
||||||
|
type: T,
|
||||||
|
priority: number,
|
||||||
|
callback: (Instance) -> ()
|
||||||
|
}
|
||||||
|
|
||||||
|
local function action(priority: number, fn: (Instance) -> ()): Action
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
local function Changed(property: string, callback: () -> ())
|
||||||
|
return action(1, function(instance)
|
||||||
|
instance:GetPropertyChangedSignal(property):Connect(callback)
|
||||||
|
end) :: Action<"Changed">
|
||||||
|
end
|
||||||
|
|
||||||
|
create "TextBox" {
|
||||||
|
Text = "test",
|
||||||
|
|
||||||
|
Changed "Text" < function(self, data)
|
||||||
|
|
||||||
|
end
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## version 1
|
## version 1
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue