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
|
||||
------------------------------------------------------------------------------------------
|
||||
|
||||
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)
|
||||
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,18 +9,8 @@ local create = graph.create
|
|||
local get = graph.get
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ if not game then script = (require :: any) "test/wrap-require" end
|
|||
local create = require(script.create)
|
||||
local source = require(script.source)
|
||||
local watch = require(script.watch)
|
||||
local cleanup, clean_garbage = require(script.cleanup)()
|
||||
local derive = require(script.derive)
|
||||
local map = require(script.map)
|
||||
|
||||
|
|
@ -25,9 +26,10 @@ local vide = {
|
|||
-- core
|
||||
create = create,
|
||||
source = source,
|
||||
watch = watch,
|
||||
cleanup = cleanup,
|
||||
derive = derive,
|
||||
map = map,
|
||||
watch = watch,
|
||||
|
||||
-- animations
|
||||
spring = spring,
|
||||
|
|
@ -45,6 +47,7 @@ local vide = {
|
|||
-- test
|
||||
step = function(dt: number)
|
||||
update_springs(dt)
|
||||
clean_garbage()
|
||||
end
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -93,14 +93,14 @@ local lerpable: { [string]: Lerp<any> } = {
|
|||
local springs: { [SpringData<any>]: Node<any> } = {}
|
||||
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 node = create(initial_position)
|
||||
|
||||
local data: SpringData<T> = {
|
||||
alpha = 0,
|
||||
duration = 0,
|
||||
period = period,
|
||||
period = period or 1,
|
||||
damping_ratio = damping_ratio or 1,
|
||||
velocity = 0,
|
||||
initial_velocity = 0,
|
||||
|
|
|
|||
|
|
@ -9,25 +9,18 @@ local set_effect = graph.set_effect
|
|||
local capture = graph.capture
|
||||
|
||||
local function watch(effect: () -> ()): () -> ()
|
||||
-- todo: call cleanup on initial debug call when strict
|
||||
local nodes, cleanup = capture(effect :: () -> (() -> ()?), nil)
|
||||
local nodes = capture(effect :: () -> nil)
|
||||
|
||||
nodes = table.clone(nodes)
|
||||
|
||||
local function fn()
|
||||
if cleanup then cleanup(); cleanup = nil end
|
||||
cleanup = effect()
|
||||
end
|
||||
|
||||
for _, node in next, nodes do
|
||||
set_effect(node, fn, true)
|
||||
set_effect(node, effect, true)
|
||||
end
|
||||
|
||||
local function unwatch()
|
||||
for _, node in next, nodes do
|
||||
set_effect(node, fn, nil)
|
||||
set_effect(node, effect, nil)
|
||||
end
|
||||
if cleanup then cleanup(); cleanup = nil end
|
||||
end
|
||||
|
||||
return unwatch
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
--!nocheck
|
||||
-- wrapped task library for use in pure luau
|
||||
local task = { spawn = function(thread, ...)
|
||||
local ok, err = coroutine.resume(thread, ...)
|
||||
if not ok then error(err, 3) end
|
||||
end }
|
||||
|
||||
export type Type = RBXScriptSignal & { Fire: (Type, ...any)-> () }
|
||||
|
||||
----------------------------------------------------------------------------------------------------
|
||||
-- Batched Yield-Safe Signal Implementation --
|
||||
-- This is a Signal class which has effectively identical behavior to a --
|
||||
|
|
@ -106,10 +109,12 @@ setmetatable(Connection, {
|
|||
local Signal = {}
|
||||
Signal.__index = Signal
|
||||
|
||||
function Signal.new()
|
||||
Signal.__type = "RBXScriptSignal"
|
||||
|
||||
function Signal.new(): Type
|
||||
return setmetatable({
|
||||
_handlerListHead = false,
|
||||
}, Signal)
|
||||
}, Signal) :: any
|
||||
end
|
||||
|
||||
function Signal:Connect(fn)
|
||||
|
|
@ -174,14 +179,4 @@ function Signal:Once(fn)
|
|||
return cn
|
||||
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
|
||||
|
|
|
|||
162
test/mock.luau
162
test/mock.luau
|
|
@ -1,101 +1,111 @@
|
|||
local Instance = {} do
|
||||
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,
|
||||
__newindex: any
|
||||
}
|
||||
|
||||
type Data = {
|
||||
Name: string,
|
||||
Parent: Data?,
|
||||
Children: { Data },
|
||||
Changed: { [string]: RBXScriptSignal & { Fire: any } },
|
||||
Table: { [string]: unknown },
|
||||
Destroying: RBXScriptSignal,
|
||||
ClassName: string,
|
||||
Type: "Instance"
|
||||
name: string,
|
||||
parent: Data?,
|
||||
children: { Data },
|
||||
changed: { [string]: Signal },
|
||||
properties: { [string]: unknown },
|
||||
destroying: Signal,
|
||||
class: string,
|
||||
type: "Instance"
|
||||
}
|
||||
|
||||
local function deepclone<T>(template: T & {}): T
|
||||
local function deep_clone<T>(template: T & {}): T
|
||||
local t = table.clone(template :: {}) :: {}
|
||||
|
||||
for i, v in next, t do
|
||||
if type(v) == "table" then
|
||||
t[i] = deepclone(v)
|
||||
t[i] = deep_clone(v)
|
||||
end
|
||||
end
|
||||
|
||||
return t :: T & {}
|
||||
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? }
|
||||
setmetatable(proxies :: any, { __mode = "v" })
|
||||
|
||||
local function getdata(userdata: userdata): Data
|
||||
local function getproxy(userdata: userdata): Proxy
|
||||
local function get_data(userdata: userdata): Data
|
||||
local function f(userdata: userdata): ProxyMT
|
||||
return getmetatable(userdata :: any)
|
||||
end
|
||||
|
||||
return getproxy(userdata)._Data
|
||||
return f(userdata).data
|
||||
end
|
||||
|
||||
local function isInstance(value: unknown): boolean
|
||||
local function is_instance(value: unknown): boolean
|
||||
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
|
||||
|
||||
local methods = {}
|
||||
|
||||
local function __index(userdata: userdata, property: string): ()
|
||||
local data = getdata(userdata)
|
||||
local data = get_data(userdata)
|
||||
return if methods[property] then methods[property]
|
||||
elseif property == "Name" then data.Name
|
||||
elseif property == "Parent" then data.Parent
|
||||
elseif property == "Destroying" then data.Destroying
|
||||
else data.Table[property]
|
||||
elseif property == "Name" then data.name
|
||||
elseif property == "Parent" then data.parent
|
||||
elseif property == "Destroying" then data.destroying
|
||||
else data.properties[property]
|
||||
end
|
||||
|
||||
local function __newindex(userdata: userdata, property: string, value: unknown)
|
||||
local data = getdata(userdata)
|
||||
local data = get_data(userdata)
|
||||
if property == "Name" then
|
||||
data.Name = value :: string
|
||||
data.name = value :: string
|
||||
elseif property == "Parent" then
|
||||
assert(value == nil or isInstance(value), "attempt to set non-instance as parent")
|
||||
local parent = data.Parent
|
||||
assert(value == nil or is_instance(value), "attempt to set non-instance as parent")
|
||||
local parent = data.parent
|
||||
if parent then
|
||||
data.Parent = nil
|
||||
table.remove(parent.Children, table.find(parent.Children, data))
|
||||
data.parent = nil
|
||||
table.remove(parent.children, table.find(parent.children, data))
|
||||
end
|
||||
if value then
|
||||
data.Parent = getdata(value :: userdata)
|
||||
table.insert(getdata(value :: userdata).Children, data)
|
||||
data.parent = get_data(value :: userdata)
|
||||
table.insert(get_data(value :: userdata).children, data)
|
||||
end
|
||||
else
|
||||
data.Table[property] = value
|
||||
data.properties[property] = value
|
||||
end
|
||||
|
||||
if data.Changed[property] then
|
||||
data.Changed[property]:Fire()
|
||||
if data.changed[property] then
|
||||
data.changed[property]:Fire()
|
||||
end
|
||||
end
|
||||
|
||||
local function getuserdata(data: Data): userdata
|
||||
local function get_proxy(data: Data): userdata
|
||||
return proxies[data] or (function()
|
||||
local userdata = newproxy(true)
|
||||
local proxy = getmetatable(userdata)
|
||||
proxy._Userdata = userdata
|
||||
proxy._Data = data
|
||||
proxy.proxy = userdata
|
||||
proxy.data = data
|
||||
proxy.__index = __index
|
||||
proxy.__newindex = __newindex
|
||||
proxies[data] = userdata
|
||||
|
|
@ -105,29 +115,29 @@ local Instance = {} do
|
|||
|
||||
function Instance.new(class: string): Instance
|
||||
local data = {
|
||||
Name = "UNNAMED",
|
||||
Parent = nil,
|
||||
Children = {},
|
||||
Changed = {},
|
||||
Table = {},
|
||||
ClassName = class,
|
||||
Destroying = Signal.new() :: any,
|
||||
Type = "Instance" :: "Instance"
|
||||
name = "UNNAMED",
|
||||
parent = nil,
|
||||
children = {},
|
||||
changed = {},
|
||||
properties = {},
|
||||
class = class,
|
||||
destroying = Signal.new() :: any,
|
||||
type = "Instance" :: "Instance"
|
||||
}
|
||||
|
||||
return getuserdata(data) :: any
|
||||
return get_proxy(data) :: any
|
||||
end
|
||||
|
||||
function Instance.isInstance(value: unknown): boolean
|
||||
return isInstance(value)
|
||||
function Instance.is_instance(value: unknown): boolean
|
||||
return is_instance(value)
|
||||
end
|
||||
|
||||
function methods.Clone(userdata: userdata): userdata
|
||||
local data = getdata(userdata)
|
||||
local data = get_data(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
|
||||
end
|
||||
|
||||
|
|
@ -135,40 +145,40 @@ local Instance = {} do
|
|||
end
|
||||
|
||||
function methods.FindFirstChild(userdata: userdata, target: string): userdata?
|
||||
local data = getdata(userdata)
|
||||
for _, child in data.Children do
|
||||
if child.Name == target then
|
||||
return getuserdata(child)
|
||||
local data = get_data(userdata)
|
||||
for _, child in data.children do
|
||||
if child.name == target then
|
||||
return get_proxy(child)
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function methods.GetChildren(userdata: userdata): { userdata }
|
||||
local children = getdata(userdata).Children
|
||||
local children = get_data(userdata).children
|
||||
local userdatas = table.create(#children)
|
||||
|
||||
for i, child in next, children do
|
||||
userdatas[i] = getuserdata(child)
|
||||
userdatas[i] = get_proxy(child)
|
||||
end
|
||||
|
||||
return userdatas
|
||||
end
|
||||
|
||||
function methods.GetPropertyChangedSignal(userdata: userdata, property: string): RBXScriptSignal
|
||||
local data = getdata(userdata)
|
||||
if not data.Changed[property] then
|
||||
data.Changed[property] = Signal.new() :: any
|
||||
local data = get_data(userdata)
|
||||
if not data.changed[property] then
|
||||
data.changed[property] = Signal.new() :: any
|
||||
end
|
||||
return data.Changed[property]
|
||||
return data.changed[property]
|
||||
end
|
||||
|
||||
function methods.Destroy(userdata: userdata)
|
||||
local data = getdata(userdata);
|
||||
(data.Destroying :: any):Fire()
|
||||
data.Parent = nil
|
||||
if data.Changed["Parent"] then
|
||||
data.Changed["Parent"]:Fire()
|
||||
local data = get_data(userdata);
|
||||
data.destroying:Fire()
|
||||
data.parent = nil
|
||||
if data.changed["Parent"] then
|
||||
data.changed["Parent"]:Fire()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -227,8 +237,10 @@ local Enum = {} :: any do
|
|||
end})
|
||||
end
|
||||
|
||||
local function typeof(v)
|
||||
return Instance.isInstance(v) and "Instance" or type(v)
|
||||
local function typeof(v): string
|
||||
return if Instance.is_instance(v) then "Instance"
|
||||
elseif getmetatable(v) and getmetatable(v).__type then getmetatable(v).__type
|
||||
else type(v)
|
||||
end
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,72 +1,43 @@
|
|||
local vide = require "src/init"
|
||||
local wrap = vide.wrap
|
||||
local source = vide.source
|
||||
local derive = vide.derive
|
||||
local map = vide.map
|
||||
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 New = nil :: any
|
||||
local function processor(priority: number, fn: (Instance) -> ()): Action<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 Frame(props)
|
||||
return create("Frame") {
|
||||
Position = props.Position
|
||||
}
|
||||
local function Changed(property: string, callback: () -> ())
|
||||
return processor(1, function(instance)
|
||||
instance:GetPropertyChangedSignal(property):Connect(callback)
|
||||
end) :: Action<"Changed">
|
||||
end
|
||||
|
||||
Frame { Position = UDim2.fromScale(0.5, 0.5) }
|
||||
local function Cleanup(t: {})
|
||||
|
||||
Frame { Position = positionState }
|
||||
end
|
||||
|
||||
function TextInput(p: {
|
||||
OnInput: Action<"Changed">
|
||||
})
|
||||
create "TextBox" {
|
||||
Text = "test",
|
||||
|
||||
Changed("Text", function()
|
||||
|
||||
end),
|
||||
|
||||
Cleanup {
|
||||
|
||||
},
|
||||
|
||||
create("Frame") {}
|
||||
}
|
||||
end
|
||||
|
|
|
|||
219
test/tests.luau
219
test/tests.luau
|
|
@ -363,6 +363,7 @@ end)
|
|||
TEST("watch()", function()
|
||||
local source = vide.source
|
||||
local watch = vide.watch
|
||||
local cleanup = vide.cleanup
|
||||
|
||||
do CASE "Capture states"
|
||||
local a = source(1)
|
||||
|
|
@ -405,7 +406,7 @@ TEST("watch()", function()
|
|||
local unwatch = watch(function()
|
||||
state()
|
||||
effect_runcount += 1
|
||||
return function() cleanup_runcount += 1 end
|
||||
cleanup(function() cleanup_runcount += 1 end)
|
||||
end)
|
||||
|
||||
CHECK(effect_runcount == 1)
|
||||
|
|
@ -413,7 +414,12 @@ TEST("watch()", function()
|
|||
state(2)
|
||||
CHECK(effect_runcount == 2)
|
||||
CHECK(cleanup_runcount == 1)
|
||||
|
||||
unwatch()
|
||||
unwatch = nil :: any
|
||||
gc()
|
||||
vide.step(0)
|
||||
|
||||
CHECK(effect_runcount == 2)
|
||||
CHECK(cleanup_runcount == 2)
|
||||
end
|
||||
|
|
@ -478,6 +484,117 @@ TEST("watch()", function()
|
|||
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()
|
||||
local create = vide.create
|
||||
local source = vide.source
|
||||
|
|
@ -894,6 +1011,7 @@ TEST("map()", function()
|
|||
end)
|
||||
|
||||
TEST("spring()", function()
|
||||
local create = vide.create
|
||||
local source = vide.source
|
||||
local spring = vide.spring
|
||||
|
||||
|
|
@ -909,43 +1027,43 @@ TEST("spring()", function()
|
|||
end
|
||||
|
||||
do CASE "Garbage collection"
|
||||
do -- `spring` should not allow gc of `state`
|
||||
local state = source(10)
|
||||
local _springed = spring(state, 1, 1)
|
||||
do -- `output` should not allow gc of `input`
|
||||
local input = source(10)
|
||||
local output = spring(input)
|
||||
|
||||
wref.state, state = state, nil :: any
|
||||
local wref = { input }
|
||||
input = nil :: any
|
||||
|
||||
gc()
|
||||
CHECK(wref.state)
|
||||
|
||||
CHECK(wref[1])
|
||||
end
|
||||
|
||||
do -- `value` should allow gc of `spring`
|
||||
local value = source(10)
|
||||
local springed = spring(value, 1, 1) :: State?
|
||||
do -- `input` should allow gc of `output`
|
||||
local input = source(10)
|
||||
local output = spring(input)
|
||||
|
||||
wref.springed, springed = springed, nil
|
||||
local wref = { output }
|
||||
output = nil :: any
|
||||
|
||||
gc()
|
||||
CHECK(not wref.springed)
|
||||
|
||||
CHECK(not wref[1])
|
||||
end
|
||||
end
|
||||
|
||||
local create = vide.create
|
||||
|
||||
do CASE "Garbage collection (binded)"
|
||||
local number = source(10)
|
||||
local springed = spring(number, 1, 1) :: State?
|
||||
local input = source(10)
|
||||
local output = spring(input, 1, 1)
|
||||
|
||||
local _label = create "TextLabel" {
|
||||
Text = springed
|
||||
Text = output
|
||||
}
|
||||
|
||||
wref.springed, springed = springed, nil
|
||||
local wref = { output }
|
||||
output = nil :: any
|
||||
|
||||
gc()
|
||||
CHECK(wref.springed) -- `springed` should not gc
|
||||
CHECK(wref[1]) -- `output` should not gc
|
||||
end
|
||||
end)
|
||||
|
||||
|
|
@ -954,9 +1072,11 @@ TEST("Events", function()
|
|||
|
||||
local function Thing(props)
|
||||
local instance = Instance.new("Thing") :: any
|
||||
instance.Signal = (Signal.new() :: any) :: RBXScriptSignal & { Fire: any }
|
||||
testkit.print2(getmetatable(instance))
|
||||
return create(instance)(props)
|
||||
instance.Signal = Signal.new()
|
||||
|
||||
local clone = create(instance)(props)
|
||||
|
||||
return clone
|
||||
end
|
||||
|
||||
do CASE "Connect event"
|
||||
|
|
@ -969,7 +1089,7 @@ TEST("Events", function()
|
|||
end
|
||||
}
|
||||
|
||||
testkit.print2(getmetatable(val))
|
||||
-- testkit.print2(getmetatable(val))
|
||||
|
||||
CHECK(not connected)
|
||||
val.Value = 1; val.Signal:Fire(val.Value)
|
||||
|
|
@ -977,6 +1097,7 @@ TEST("Events", function()
|
|||
end
|
||||
end)
|
||||
|
||||
--[[
|
||||
TEST("Changed", function()
|
||||
local create = vide.create
|
||||
local Changed = vide.Changed
|
||||
|
|
@ -1023,6 +1144,7 @@ TEST("Changed", function()
|
|||
CHECK(Changed.Test == Changed.Test)
|
||||
end
|
||||
end)
|
||||
]]
|
||||
|
||||
--[[
|
||||
TEST("Created", function()
|
||||
|
|
@ -1050,7 +1172,6 @@ TEST("strict", function()
|
|||
vide.strict = true
|
||||
|
||||
local source = vide.source
|
||||
local unsource = vide.unsource
|
||||
local derive = vide.derive
|
||||
local watch = vide.watch
|
||||
|
||||
|
|
@ -1058,9 +1179,9 @@ TEST("strict", function()
|
|||
local state = source(1)
|
||||
|
||||
local ok = pcall(function()
|
||||
local _derived = derive(function(from)
|
||||
local _derived = derive(function()
|
||||
coroutine.yield()
|
||||
return from(state)
|
||||
return state()
|
||||
end)
|
||||
end)
|
||||
|
||||
|
|
@ -1071,9 +1192,9 @@ TEST("strict", function()
|
|||
local state = source(1)
|
||||
|
||||
local ok = pcall(function()
|
||||
local _derived = watch(function(from)
|
||||
local _derived = watch(function()
|
||||
coroutine.yield()
|
||||
local _ = from(state)
|
||||
state()
|
||||
end)
|
||||
end)
|
||||
|
||||
|
|
@ -1081,59 +1202,33 @@ TEST("strict", function()
|
|||
end
|
||||
|
||||
do CASE "Run derived callback twice"
|
||||
local state, set = source(1)
|
||||
local state = source(1)
|
||||
local runcount = 0
|
||||
|
||||
local derived = derive(function(from)
|
||||
local _ = derive(function()
|
||||
runcount += 1
|
||||
return from(state)
|
||||
return state()
|
||||
end)
|
||||
|
||||
CHECK(runcount == 2)
|
||||
set(2)
|
||||
local _ = derived()
|
||||
state(2)
|
||||
CHECK(runcount == 4)
|
||||
end
|
||||
|
||||
do CASE "Run watcher callback twice"
|
||||
local state, set = source(1)
|
||||
local state = source(1)
|
||||
local runcount = 0
|
||||
|
||||
watch(function(from)
|
||||
watch(function()
|
||||
runcount += 1
|
||||
local _ = from(state)
|
||||
state()
|
||||
end)
|
||||
|
||||
CHECK(runcount == 2)
|
||||
set(2)
|
||||
vide.step(1/60)
|
||||
state(2)
|
||||
CHECK(runcount == 4)
|
||||
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
|
||||
end)
|
||||
|
||||
|
|
|
|||
26
todo.md
26
todo.md
|
|
@ -39,6 +39,32 @@ async/loading/suspense
|
|||
|
||||
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
|
||||
|
||||
```lua
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue