mirror of
https://github.com/littensy/charm.git
synced 2026-10-21 11:44:08 +00:00
feat: use alien-signals
This commit is contained in:
parent
d7a1f62c3c
commit
861ee5acd7
76 changed files with 3112 additions and 1862 deletions
82
tests/signals/computed.spec.luau
Normal file
82
tests/signals/computed.spec.luau
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
local test = require("../suite").test
|
||||
local signals = require("../../packages/charm/src/signals")
|
||||
|
||||
local computed = signals.computed
|
||||
local signal = signals.signal
|
||||
|
||||
test("should correctly propagate changes through computed signals", function()
|
||||
local src = signal(0)
|
||||
local c1 = computed(function()
|
||||
return src() % 2
|
||||
end)
|
||||
local c2 = computed(function()
|
||||
return c1()
|
||||
end)
|
||||
local c3 = computed(function()
|
||||
return c2()
|
||||
end)
|
||||
|
||||
c3()
|
||||
src(1) -- c1 -> dirty, c2 -> toCheckDirty, c3 -> toCheckDirty
|
||||
c2() -- c1 -> none, c2 -> none
|
||||
src(3) -- c1 -> dirty, c2 -> toCheckDirty
|
||||
|
||||
assert(c3() == 1, "bad final value")
|
||||
end)
|
||||
|
||||
test("should propagate updated source value through chained computations", function()
|
||||
local src = signal(0)
|
||||
local a = computed(function()
|
||||
return src()
|
||||
end)
|
||||
local b = computed(function()
|
||||
return a() % 2
|
||||
end)
|
||||
local c = computed(function()
|
||||
return src()
|
||||
end)
|
||||
local d = computed(function()
|
||||
return b() + c()
|
||||
end)
|
||||
|
||||
assert(d() == 0, "bad initial value")
|
||||
src(2)
|
||||
assert(d() == 2, "bad updated value")
|
||||
end)
|
||||
|
||||
test("should handle flags are indirectly updated during checkDirty", function()
|
||||
local a = signal(false)
|
||||
local b = computed(function()
|
||||
return a()
|
||||
end)
|
||||
local c = computed(function()
|
||||
b()
|
||||
return 0
|
||||
end)
|
||||
local d = computed(function()
|
||||
c()
|
||||
return b()
|
||||
end)
|
||||
|
||||
assert(d() == false, "bad initial value")
|
||||
a(true)
|
||||
assert(d() == true, "bad updated value")
|
||||
end)
|
||||
|
||||
test("should not update if the signal value is reverted", function()
|
||||
local times = 0
|
||||
|
||||
local src = signal(0)
|
||||
local c1 = computed(function()
|
||||
times += 1
|
||||
return src()
|
||||
end)
|
||||
c1()
|
||||
assert(times == 1, "computed did not run")
|
||||
src(1)
|
||||
src(0)
|
||||
c1()
|
||||
assert(times == 1, "computed ran when it should not")
|
||||
end)
|
||||
|
||||
return {}
|
||||
337
tests/signals/effect.spec.luau
Normal file
337
tests/signals/effect.spec.luau
Normal file
|
|
@ -0,0 +1,337 @@
|
|||
local test = require("../suite").test
|
||||
local signals = require("../../packages/charm/src/signals")
|
||||
|
||||
local computed = signals.computed
|
||||
local effect = signals.effect
|
||||
local effectScope = signals.effectScope
|
||||
local endBatch = signals.endBatch
|
||||
local pauseTracking = signals.pauseTracking
|
||||
local resumeTracking = signals.resumeTracking
|
||||
local signal = signals.signal
|
||||
local startBatch = signals.startBatch
|
||||
|
||||
test("should clear subscriptions when untracked by all subscribers", function()
|
||||
local bRunTimes = 0
|
||||
|
||||
local a = signal(1)
|
||||
local b = computed(function()
|
||||
bRunTimes += 1
|
||||
return a() * 2
|
||||
end)
|
||||
local stopEffect = effect(function()
|
||||
b()
|
||||
end)
|
||||
|
||||
assert(bRunTimes == 1, "should run initially")
|
||||
a(2)
|
||||
assert(bRunTimes == 2, "should run after change")
|
||||
stopEffect()
|
||||
a(3)
|
||||
assert(bRunTimes == 2, "should not run after stop")
|
||||
end)
|
||||
|
||||
test("should not run untracked inner effect", function()
|
||||
local a = signal(3)
|
||||
local b = computed(function()
|
||||
return a() > 0
|
||||
end)
|
||||
|
||||
effect(function()
|
||||
if b() then
|
||||
effect(function()
|
||||
if a() == 0 then
|
||||
error("bad")
|
||||
end
|
||||
end)
|
||||
end
|
||||
end)
|
||||
|
||||
a(2)
|
||||
a(1)
|
||||
a(0)
|
||||
end)
|
||||
|
||||
test("should run outer effect first", function()
|
||||
local a = signal(1)
|
||||
local b = signal(1)
|
||||
|
||||
effect(function()
|
||||
if a() > 0 then
|
||||
effect(function()
|
||||
b()
|
||||
if a() == 0 then
|
||||
error("bad")
|
||||
end
|
||||
end)
|
||||
end
|
||||
end)
|
||||
|
||||
startBatch()
|
||||
b(0)
|
||||
a(0)
|
||||
endBatch()
|
||||
end)
|
||||
|
||||
test("should not trigger inner effect when resolve maybe dirty", function()
|
||||
local a = signal(0)
|
||||
local b = computed(function()
|
||||
return a() % 2
|
||||
end)
|
||||
|
||||
local innerTriggerTimes = 0
|
||||
|
||||
effect(function()
|
||||
effect(function()
|
||||
b()
|
||||
innerTriggerTimes += 1
|
||||
if innerTriggerTimes >= 2 then
|
||||
error("bad")
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
a(2)
|
||||
end)
|
||||
|
||||
test("should trigger inner effects in sequence", function()
|
||||
local a = signal(0)
|
||||
local b = signal(0)
|
||||
local c = computed(function()
|
||||
return a() - b()
|
||||
end)
|
||||
local order: { string } = {}
|
||||
|
||||
effect(function()
|
||||
c()
|
||||
|
||||
effect(function()
|
||||
table.insert(order, "first inner")
|
||||
a()
|
||||
end)
|
||||
|
||||
effect(function()
|
||||
table.insert(order, "last inner")
|
||||
a()
|
||||
b()
|
||||
end)
|
||||
end)
|
||||
|
||||
table.clear(order)
|
||||
|
||||
startBatch()
|
||||
b(1)
|
||||
a(1)
|
||||
endBatch()
|
||||
|
||||
assert(table.concat(order, ", ") == "first inner, last inner", `wrong order: {table.concat(order, ", ")}`)
|
||||
end)
|
||||
|
||||
test("should trigger inner effects in sequence in effect scope", function()
|
||||
local a = signal(0)
|
||||
local b = signal(0)
|
||||
local order: { string } = {}
|
||||
|
||||
effectScope(function()
|
||||
effect(function()
|
||||
table.insert(order, "first inner")
|
||||
a()
|
||||
end)
|
||||
|
||||
effect(function()
|
||||
table.insert(order, "last inner")
|
||||
a()
|
||||
b()
|
||||
end)
|
||||
end)
|
||||
|
||||
table.clear(order)
|
||||
|
||||
startBatch()
|
||||
b(1)
|
||||
a(1)
|
||||
endBatch()
|
||||
|
||||
assert(table.concat(order, ", ") == "first inner, last inner", `wrong order: {table.concat(order, ", ")}`)
|
||||
end)
|
||||
|
||||
test("should custom effect support batch", function()
|
||||
local function batchEffect(fn: () -> ())
|
||||
return effect(function()
|
||||
startBatch()
|
||||
local success, result: unknown = pcall(fn)
|
||||
endBatch()
|
||||
if not success then
|
||||
error(result)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local logs: { string } = {}
|
||||
local a = signal(0)
|
||||
local b = signal(0)
|
||||
|
||||
local aa = computed(function()
|
||||
table.insert(logs, "aa-0")
|
||||
if a() == 0 then
|
||||
b(1)
|
||||
end
|
||||
table.insert(logs, "aa-1")
|
||||
return nil
|
||||
end)
|
||||
|
||||
local bb = computed(function()
|
||||
table.insert(logs, "bb")
|
||||
return b()
|
||||
end)
|
||||
|
||||
batchEffect(function()
|
||||
bb()
|
||||
end)
|
||||
batchEffect(function()
|
||||
aa()
|
||||
end)
|
||||
|
||||
assert(table.concat(logs, ", ") == "bb, aa-0, aa-1, bb", `wrong order: {table.concat(logs, ", ")}`)
|
||||
end)
|
||||
|
||||
test("should duplicate subscribers do not affect the notify order", function()
|
||||
local src1 = signal(0)
|
||||
local src2 = signal(0)
|
||||
local order: { string } = {}
|
||||
|
||||
effect(function()
|
||||
table.insert(order, "a")
|
||||
pauseTracking()
|
||||
local isOne = src2() == 1
|
||||
resumeTracking()
|
||||
if isOne then
|
||||
src1()
|
||||
end
|
||||
src2()
|
||||
src1()
|
||||
end)
|
||||
effect(function()
|
||||
table.insert(order, "b")
|
||||
src1()
|
||||
end)
|
||||
src2(1) -- src1.subs: a -> b -> a
|
||||
|
||||
table.clear(order)
|
||||
src1(src1() + 1)
|
||||
|
||||
assert(table.concat(order) == "ab", `wrong order: {table.concat(order)}`)
|
||||
end)
|
||||
|
||||
test("should handle side effect with inner effects", function()
|
||||
local a = signal(0)
|
||||
local b = signal(0)
|
||||
local order: { string } = {}
|
||||
|
||||
effect(function()
|
||||
effect(function()
|
||||
a()
|
||||
table.insert(order, "a")
|
||||
end)
|
||||
effect(function()
|
||||
b()
|
||||
table.insert(order, "b")
|
||||
end)
|
||||
assert(table.concat(order) == "ab", `wrong order: {table.concat(order)}`)
|
||||
|
||||
table.clear(order)
|
||||
b(1)
|
||||
a(1)
|
||||
assert(table.concat(order) == "ba", `wrong order: {table.concat(order)}`)
|
||||
end)
|
||||
end)
|
||||
|
||||
test("should handle flags are indirectly updated during checkDirty", function()
|
||||
local a = signal(false)
|
||||
local b = computed(function()
|
||||
return a()
|
||||
end)
|
||||
local c = computed(function()
|
||||
b()
|
||||
return 0
|
||||
end)
|
||||
local d = computed(function()
|
||||
c()
|
||||
return b()
|
||||
end)
|
||||
|
||||
local triggers = 0
|
||||
|
||||
effect(function()
|
||||
d()
|
||||
triggers += 1
|
||||
end)
|
||||
assert(triggers == 1, "did not run initially")
|
||||
a(true)
|
||||
assert(triggers == 2, "did not update indirectly")
|
||||
end)
|
||||
|
||||
test("should run effect cleanup", function()
|
||||
local cleanups = 0
|
||||
|
||||
local a = signal(false)
|
||||
local cleanup = effect(function()
|
||||
a()
|
||||
return function()
|
||||
cleanups += 1
|
||||
end
|
||||
end)
|
||||
|
||||
assert(cleanups == 0, "should not cleanup immediately")
|
||||
a(true)
|
||||
assert(cleanups == 1, "should cleanup once")
|
||||
cleanup()
|
||||
assert(cleanups == 2, "should cleanup after effect stop")
|
||||
a(false)
|
||||
assert(cleanups == 2, "should not cleanup after update")
|
||||
end)
|
||||
|
||||
test("should run inner effect cleanup in scope", function()
|
||||
local cleanups = 0
|
||||
|
||||
local a = signal(false)
|
||||
local stopScope = effectScope(function()
|
||||
effect(function()
|
||||
a()
|
||||
return function()
|
||||
cleanups += 1
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
assert(cleanups == 0, "should not cleanup immediately")
|
||||
a(true)
|
||||
assert(cleanups == 1, "should cleanup once")
|
||||
stopScope()
|
||||
assert(cleanups == 2, "should cleanup after outer dispose")
|
||||
a(false)
|
||||
assert(cleanups == 2, "should not cleanup after update")
|
||||
end)
|
||||
|
||||
test("should run inner effect cleanup", function()
|
||||
local cleanups = 0
|
||||
|
||||
local a = signal(false)
|
||||
local stopScope = effect(function()
|
||||
a()
|
||||
effect(function()
|
||||
return function()
|
||||
cleanups += 1
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
assert(cleanups == 0, "should not cleanup immediately")
|
||||
a(true)
|
||||
assert(cleanups == 1, "should cleanup after outer update")
|
||||
stopScope()
|
||||
assert(cleanups == 2, "should cleanup after outer dispose")
|
||||
a(false)
|
||||
assert(cleanups == 2, "should not cleanup after update")
|
||||
end)
|
||||
|
||||
return {}
|
||||
66
tests/signals/effectScope.spec.luau
Normal file
66
tests/signals/effectScope.spec.luau
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
local test = require("../suite").test
|
||||
local signals = require("../../packages/charm/src/signals")
|
||||
|
||||
local effect = signals.effect
|
||||
local effectScope = signals.effectScope
|
||||
local signal = signals.signal
|
||||
|
||||
test("should not trigger after stop", function()
|
||||
local count = signal(1)
|
||||
|
||||
local triggers = 0
|
||||
|
||||
local stopScope = effectScope(function()
|
||||
effect(function()
|
||||
triggers += 1
|
||||
count()
|
||||
end)
|
||||
assert(triggers == 1, "did not run initially")
|
||||
|
||||
count(2)
|
||||
assert(triggers == 2, "did not run after update")
|
||||
end)
|
||||
|
||||
count(3)
|
||||
assert(triggers == 3, "did not run after update outside scope")
|
||||
stopScope()
|
||||
count(4)
|
||||
assert(triggers == 3, "ran after stop")
|
||||
end)
|
||||
|
||||
test("should run effect cleanup", function()
|
||||
local cleanups = 0
|
||||
|
||||
local cleanup = effectScope(function()
|
||||
return function()
|
||||
cleanups += 1
|
||||
end
|
||||
end)
|
||||
|
||||
assert(cleanups == 0, "should not cleanup immediately")
|
||||
cleanup()
|
||||
assert(cleanups == 1, "should cleanup after effect stop")
|
||||
cleanup()
|
||||
assert(cleanups == 1, "should skip redundant cleanup")
|
||||
end)
|
||||
|
||||
test("should not run indirect effect cleanup", function()
|
||||
local cleanups = 0
|
||||
|
||||
local cleanup
|
||||
local stopScope = effectScope(function()
|
||||
cleanup = effectScope(function()
|
||||
return function()
|
||||
cleanups += 1
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
assert(cleanups == 0, "should not cleanup immediately")
|
||||
stopScope()
|
||||
assert(cleanups == 0, "should be detached from parent dispose")
|
||||
cleanup()
|
||||
assert(cleanups == 1, "should cleanup after inner dispose")
|
||||
end)
|
||||
|
||||
return {}
|
||||
113
tests/signals/issue_48.spec.luau
Normal file
113
tests/signals/issue_48.spec.luau
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
local test = require("../suite").test
|
||||
local signals = require("../../packages/charm/src/signals")
|
||||
|
||||
local computed = signals.computed
|
||||
local effect = signals.effect
|
||||
local pauseTracking = signals.pauseTracking
|
||||
local resumeTracking = signals.resumeTracking
|
||||
local signal = signals.signal
|
||||
|
||||
type ReactionOptions<T = unknown> = {
|
||||
fireImmediately: boolean?,
|
||||
equals: ((a: T, b: T?) -> boolean)?,
|
||||
onError: (error: unknown) -> ()?,
|
||||
scheduler: (fn: () -> ()) -> ()?,
|
||||
once: boolean?,
|
||||
}
|
||||
|
||||
local function untracked<T>(callback: () -> T): T
|
||||
pauseTracking()
|
||||
local success, result = pcall(callback)
|
||||
resumeTracking()
|
||||
if not success then
|
||||
error(result)
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
local function reaction<T>(
|
||||
dataFn: () -> T,
|
||||
effectFn: (newValue: T, oldValue: T?) -> (),
|
||||
options: ReactionOptions<T>?
|
||||
): () -> ()
|
||||
local options: ReactionOptions<T> = options or {}
|
||||
local scheduler: (() -> ()) -> () = options.scheduler or function(fn)
|
||||
return fn()
|
||||
end
|
||||
local equals: (T, T?) -> boolean = options.equals or function(a, b)
|
||||
return a == b
|
||||
end
|
||||
local onError = options.onError
|
||||
local once = options.once or false
|
||||
local fireImmediately = options.fireImmediately or false
|
||||
|
||||
local prevValue: T?
|
||||
local version = 0
|
||||
|
||||
local tracked = computed(function()
|
||||
local success, result = pcall(dataFn)
|
||||
if not success then
|
||||
untracked(function()
|
||||
if onError then
|
||||
onError(result)
|
||||
end
|
||||
return nil
|
||||
end)
|
||||
return prevValue
|
||||
end
|
||||
return result
|
||||
end)
|
||||
|
||||
local dispose
|
||||
dispose = effect(function()
|
||||
local current = tracked()
|
||||
if not fireImmediately and version == 0 then
|
||||
prevValue = current
|
||||
end
|
||||
version += 1
|
||||
if equals(current :: T, prevValue) then
|
||||
return
|
||||
end
|
||||
local oldValue = prevValue
|
||||
prevValue = current
|
||||
untracked(function()
|
||||
scheduler(function()
|
||||
local success = pcall(effectFn, current :: T, oldValue)
|
||||
if not success and onError then
|
||||
onError(error)
|
||||
end
|
||||
if once then
|
||||
if (fireImmediately and version > 1) or (not fireImmediately and version > 0) then
|
||||
dispose()
|
||||
end
|
||||
end
|
||||
end)
|
||||
return nil
|
||||
end)
|
||||
end)
|
||||
|
||||
return dispose
|
||||
end
|
||||
|
||||
test("#48", function()
|
||||
local source = signal(0)
|
||||
local disposeInner: () -> ()
|
||||
|
||||
reaction(function()
|
||||
return source()
|
||||
end, function(val)
|
||||
if val == 1 then
|
||||
disposeInner = reaction(function()
|
||||
return source()
|
||||
end, function() end)
|
||||
elseif val == 2 then
|
||||
disposeInner()
|
||||
end
|
||||
end)
|
||||
|
||||
source(1)
|
||||
source(2)
|
||||
source(3)
|
||||
end)
|
||||
|
||||
return {}
|
||||
18
tests/signals/spec.luau
Normal file
18
tests/signals/spec.luau
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
local suite = require("../suite")
|
||||
|
||||
local describe = suite.describe
|
||||
|
||||
local function test(name: string)
|
||||
describe(name, function()
|
||||
return require(`./{name}.spec`) :: any
|
||||
end)
|
||||
end
|
||||
|
||||
test("computed")
|
||||
test("effect")
|
||||
test("effectScope")
|
||||
test("issue_48")
|
||||
test("topology")
|
||||
test("untrack")
|
||||
|
||||
return {}
|
||||
493
tests/signals/topology.spec.luau
Normal file
493
tests/signals/topology.spec.luau
Normal file
|
|
@ -0,0 +1,493 @@
|
|||
local signals = require("../../packages/charm/src/signals")
|
||||
local suite = require("../suite")
|
||||
|
||||
local test = suite.test
|
||||
local describe = suite.describe
|
||||
|
||||
local computed = signals.computed
|
||||
local effect = signals.effect
|
||||
local signal = signals.signal
|
||||
|
||||
--[[
|
||||
Tests adopted with thanks from preact-signals implementation at
|
||||
https://github.com/preactjs/signals/blob/main/packages/core/test/signal.test.tsx
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2022-present Preact Team
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE
|
||||
]]
|
||||
|
||||
describe("graph updates", function()
|
||||
test("should drop A->B->A updates", function()
|
||||
-- A
|
||||
-- / |
|
||||
-- B | <- Looks like a flag doesn't it? :D
|
||||
-- \ |
|
||||
-- C
|
||||
-- |
|
||||
-- D
|
||||
local a = signal(2)
|
||||
|
||||
local b = computed(function()
|
||||
return a() - 1
|
||||
end)
|
||||
local c = computed(function()
|
||||
return a() + b()
|
||||
end)
|
||||
|
||||
local computeCalls = 0
|
||||
local compute = function()
|
||||
computeCalls += 1
|
||||
return "d: " .. c()
|
||||
end
|
||||
local d = computed(compute)
|
||||
|
||||
-- Trigger read
|
||||
assert(d() == "d: 3", "d should be 3")
|
||||
assert(computeCalls == 1, "compute should be called once")
|
||||
computeCalls = 0
|
||||
|
||||
a(4)
|
||||
d()
|
||||
assert(computeCalls == 1, "compute should drop update")
|
||||
end)
|
||||
|
||||
test("should only update every signal once (diamond graph)", function()
|
||||
-- In this scenario "D" should only update once when "A" receives
|
||||
-- an update. This is sometimes referred to as the "diamond" scenario.
|
||||
-- A
|
||||
-- / \
|
||||
-- B C
|
||||
-- \ /
|
||||
-- D
|
||||
|
||||
local a = signal("a")
|
||||
local b = computed(function()
|
||||
return a()
|
||||
end)
|
||||
local c = computed(function()
|
||||
return a()
|
||||
end)
|
||||
|
||||
local spyCalls = 0
|
||||
local spy = function()
|
||||
spyCalls += 1
|
||||
return b() .. " " .. c()
|
||||
end
|
||||
local d = computed(spy)
|
||||
|
||||
assert(d() == "a a", "d should be 'a a'")
|
||||
assert(spyCalls == 1, "compute should be called once")
|
||||
|
||||
a("aa")
|
||||
assert(d() == "aa aa", "d should be 'aa aa'")
|
||||
assert(spyCalls == 2, "compute should be called only twice")
|
||||
end)
|
||||
|
||||
test("should only update every signal once (diamond graph + tail)", function()
|
||||
-- "E" will be likely updated twice if our mark+sweep logic is buggy.
|
||||
-- A
|
||||
-- / \
|
||||
-- B C
|
||||
-- \ /
|
||||
-- D
|
||||
-- |
|
||||
-- E
|
||||
|
||||
local a = signal("a")
|
||||
local b = computed(function()
|
||||
return a()
|
||||
end)
|
||||
local c = computed(function()
|
||||
return a()
|
||||
end)
|
||||
|
||||
local d = computed(function()
|
||||
return b() .. " " .. c()
|
||||
end)
|
||||
|
||||
local spyCalls = 0
|
||||
local spy = function()
|
||||
spyCalls += 1
|
||||
return d()
|
||||
end
|
||||
local e = computed(spy)
|
||||
|
||||
assert(e() == "a a", "e should be 'a a'")
|
||||
assert(spyCalls == 1, "compute should be called once")
|
||||
|
||||
a("aa")
|
||||
assert(e() == "aa aa", "e should be 'aa aa'")
|
||||
assert(spyCalls == 2, "compute should be called only twice")
|
||||
end)
|
||||
|
||||
test("should bail out if result is the same", function()
|
||||
-- Bail out if value of "B" never changes
|
||||
-- A->B->C
|
||||
local a = signal("a")
|
||||
local b = computed(function()
|
||||
a()
|
||||
return "foo"
|
||||
end)
|
||||
|
||||
local spyCalls = 0
|
||||
local spy = function()
|
||||
spyCalls += 1
|
||||
return b()
|
||||
end
|
||||
local c = computed(spy)
|
||||
|
||||
assert(c() == "foo", "c should be 'foo'")
|
||||
assert(spyCalls == 1, "compute should be called once")
|
||||
|
||||
a("aa")
|
||||
assert(c() == "foo", "c should be 'foo'")
|
||||
assert(spyCalls == 1, "compute should bail out")
|
||||
end)
|
||||
|
||||
test("should only update every signal once (jagged diamond graph + tails)", function()
|
||||
-- "F" and "G" will be likely updated twice if our mark+sweep logic is buggy.
|
||||
-- A
|
||||
-- / \
|
||||
-- B C
|
||||
-- | |
|
||||
-- | D
|
||||
-- \ /
|
||||
-- E
|
||||
-- / \
|
||||
-- F G
|
||||
local a = signal("a")
|
||||
|
||||
local b = computed(function()
|
||||
return a()
|
||||
end)
|
||||
local c = computed(function()
|
||||
return a()
|
||||
end)
|
||||
|
||||
local d = computed(function()
|
||||
return c()
|
||||
end)
|
||||
|
||||
local eSpyCalls = 0
|
||||
local eSpyTime = 0
|
||||
local eSpy = function()
|
||||
eSpyCalls += 1
|
||||
eSpyTime = os.clock()
|
||||
return b() .. " " .. d()
|
||||
end
|
||||
local e = computed(eSpy)
|
||||
|
||||
local fSpyCalls = 0
|
||||
local fSpyTime = 0
|
||||
local fSpy = function()
|
||||
fSpyCalls += 1
|
||||
fSpyTime = os.clock()
|
||||
return e()
|
||||
end
|
||||
local f = computed(fSpy)
|
||||
local gSpyCalls = 0
|
||||
local gSpyTime = 0
|
||||
local gSpy = function()
|
||||
gSpyCalls += 1
|
||||
gSpyTime = os.clock()
|
||||
return e()
|
||||
end
|
||||
local g = computed(gSpy)
|
||||
|
||||
assert(f() == "a a", "f should be 'a a'")
|
||||
assert(fSpyCalls == 1, "f should be called once")
|
||||
|
||||
assert(g() == "a a", "g should be 'a a'")
|
||||
assert(gSpyCalls == 1, "g should be called once")
|
||||
|
||||
eSpyCalls, eSpyTime = 0, 0
|
||||
fSpyCalls, fSpyTime = 0, 0
|
||||
gSpyCalls, gSpyTime = 0, 0
|
||||
|
||||
a("b")
|
||||
|
||||
assert(e() == "b b", "e should be 'b b'")
|
||||
assert(eSpyCalls == 1, "e should run once after update")
|
||||
|
||||
assert(f() == "b b", "f should be 'b b'")
|
||||
assert(fSpyCalls == 1, "f should run once after update")
|
||||
|
||||
assert(g() == "b b", "g should be 'b b'")
|
||||
assert(gSpyCalls == 1, "g should run once after update")
|
||||
|
||||
eSpyCalls, eSpyTime = 0, 0
|
||||
fSpyCalls, fSpyTime = 0, 0
|
||||
gSpyCalls, gSpyTime = 0, 0
|
||||
|
||||
a("c")
|
||||
|
||||
assert(e() == "c c", "e should be 'c c'")
|
||||
assert(eSpyCalls == 1, "e should run once after update")
|
||||
|
||||
assert(f() == "c c", "f should be 'c c'")
|
||||
assert(fSpyCalls == 1, "f should run once after update")
|
||||
|
||||
assert(g() == "c c", "g should be 'c c'")
|
||||
assert(gSpyCalls == 1, "g should run once after update")
|
||||
|
||||
-- top to bottom
|
||||
assert(eSpyTime < fSpyTime, "e should be called before f")
|
||||
-- left to right
|
||||
assert(eSpyTime < gSpyTime, "e should be called before g")
|
||||
end)
|
||||
|
||||
test("should only subscribe to signals listened to", function()
|
||||
-- *A
|
||||
-- / \
|
||||
-- *B C <- we don't listen to C
|
||||
local a = signal("a")
|
||||
|
||||
local b = computed(function()
|
||||
return a()
|
||||
end)
|
||||
local spyCalls = 0
|
||||
local spy = function()
|
||||
spyCalls += 1
|
||||
return a()
|
||||
end
|
||||
computed(spy)
|
||||
|
||||
assert(b() == "a", "b should be 'a'")
|
||||
assert(spyCalls == 0, "compute should not run")
|
||||
|
||||
a("aa")
|
||||
assert(b() == "aa", "b should be 'aa'")
|
||||
assert(spyCalls == 0, "compute should not run")
|
||||
end)
|
||||
|
||||
test("should only subscribe to signals listened to II", function()
|
||||
-- Here both "B" and "C" are active in the beginning, but
|
||||
-- "B" becomes inactive later. At that point it should
|
||||
-- not receive any updates anymore.
|
||||
-- *A
|
||||
-- / \
|
||||
-- *B D <- we don't listen to C
|
||||
-- |
|
||||
-- *C
|
||||
local a = signal("a")
|
||||
local spyBCalls = 0
|
||||
local spyB = function()
|
||||
spyBCalls += 1
|
||||
return a()
|
||||
end
|
||||
local b = computed(spyB)
|
||||
|
||||
local spyCCalls = 0
|
||||
local spyC = function()
|
||||
spyCCalls += 1
|
||||
return b()
|
||||
end
|
||||
local c = computed(spyC)
|
||||
|
||||
local d = computed(function()
|
||||
return a()
|
||||
end)
|
||||
|
||||
local result = ""
|
||||
local unsub = effect(function()
|
||||
result = c()
|
||||
end)
|
||||
|
||||
assert(result == "a", "result should be 'a'")
|
||||
assert(d() == "a", "d should be 'a'")
|
||||
|
||||
spyBCalls = 0
|
||||
spyCCalls = 0
|
||||
unsub()
|
||||
|
||||
a("aa")
|
||||
|
||||
assert(spyBCalls == 0, "b should not run")
|
||||
assert(spyCCalls == 0, "c should not run")
|
||||
assert(d() == "aa", "d should be 'aa'")
|
||||
end)
|
||||
|
||||
test("should ensure subs update even if one dep unmarks it", function()
|
||||
-- In this scenario "C" always returns the same value. When "A"
|
||||
-- changes, "B" will update, then "C" at which point its update
|
||||
-- to "D" will be unmarked. But "D" must still update because
|
||||
-- "B" marked it. If "D" isn't updated, then we have a bug.
|
||||
-- A
|
||||
-- / \
|
||||
-- B *C <- returns same value every time
|
||||
-- \ /
|
||||
-- D
|
||||
local a = signal("a")
|
||||
local b = computed(function()
|
||||
return a()
|
||||
end)
|
||||
local c = computed(function()
|
||||
a()
|
||||
return "c"
|
||||
end)
|
||||
local spyResult: string?
|
||||
local spy = function()
|
||||
spyResult = b() .. " " .. c()
|
||||
return spyResult
|
||||
end
|
||||
local d = computed(spy)
|
||||
|
||||
assert(d() == "a c", "d should be 'a c'")
|
||||
spyResult = nil
|
||||
|
||||
a("aa")
|
||||
d()
|
||||
assert(spyResult == "aa c", "d should have returned 'aa c'")
|
||||
end)
|
||||
|
||||
test("should ensure subs update even if two deps unmark it", function()
|
||||
-- In this scenario both "C" and "D" always return the same
|
||||
-- value. But "E" must still update because "A" marked it.
|
||||
-- If "E" isn't updated, then we have a bug.
|
||||
-- A
|
||||
-- / | \
|
||||
-- B *C *D
|
||||
-- \ | /
|
||||
-- E
|
||||
local a = signal("a")
|
||||
local b = computed(function()
|
||||
return a()
|
||||
end)
|
||||
local c = computed(function()
|
||||
a()
|
||||
return "c"
|
||||
end)
|
||||
local d = computed(function()
|
||||
a()
|
||||
return "d"
|
||||
end)
|
||||
local spyResult: string?
|
||||
local spy = function()
|
||||
spyResult = b() .. " " .. c() .. " " .. d()
|
||||
return spyResult
|
||||
end
|
||||
local e = computed(spy)
|
||||
|
||||
assert(e() == "a c d", "e should be 'a c d'")
|
||||
spyResult = nil
|
||||
|
||||
a("aa")
|
||||
e()
|
||||
assert(spyResult == "aa c d", "e should have returned 'aa c d'")
|
||||
end)
|
||||
|
||||
test("should support lazy branches", function()
|
||||
local a = signal(0)
|
||||
local b = computed(function()
|
||||
return a()
|
||||
end)
|
||||
local c = computed(function()
|
||||
return if a() > 0 then a() else b()
|
||||
end)
|
||||
|
||||
assert(c() == 0, "c should be 0")
|
||||
a(1)
|
||||
assert(c() == 1, "c should be 1")
|
||||
|
||||
a(0)
|
||||
assert(c() == 0, "c should be 0 again")
|
||||
end)
|
||||
|
||||
test("should not update a sub if all deps unmark it", function()
|
||||
-- In this scenario "B" and "C" always return the same value. When "A"
|
||||
-- changes, "D" should not update.
|
||||
-- A
|
||||
-- / \
|
||||
-- *B *C
|
||||
-- \ /
|
||||
-- D
|
||||
local a = signal("a")
|
||||
local b = computed(function()
|
||||
a()
|
||||
return "b"
|
||||
end)
|
||||
local c = computed(function()
|
||||
a()
|
||||
return "c"
|
||||
end)
|
||||
local spyCalls = 0
|
||||
local spy = function()
|
||||
spyCalls += 1
|
||||
return b() .. " " .. c()
|
||||
end
|
||||
local d = computed(spy)
|
||||
|
||||
assert(d() == "b c", "d should be 'b c'")
|
||||
spyCalls = 0
|
||||
|
||||
a("aa")
|
||||
assert(spyCalls == 0, "d should not run")
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("error handling", function()
|
||||
test("should keep graph consistent on errors during activation", function()
|
||||
local a = signal(0)
|
||||
local b = computed(function()
|
||||
error("fail")
|
||||
end)
|
||||
local c = computed(function()
|
||||
return a()
|
||||
end)
|
||||
|
||||
local success, result = pcall(function()
|
||||
b()
|
||||
end)
|
||||
assert(not success, "should throw")
|
||||
assert(string.match(result, "fail"), "should throw fail")
|
||||
|
||||
a(1)
|
||||
assert(c() == 1, "c should recover and return 1")
|
||||
end)
|
||||
|
||||
test("should keep graph consistent on errors in computeds", function()
|
||||
local a = signal(0)
|
||||
local b = computed(function()
|
||||
if a() == 1 then
|
||||
error("fail")
|
||||
end
|
||||
return a()
|
||||
end)
|
||||
local c = computed(function()
|
||||
return b()
|
||||
end)
|
||||
|
||||
assert(c() == 0, "c should be 0")
|
||||
|
||||
a(1)
|
||||
local success, result = pcall(function()
|
||||
b()
|
||||
end)
|
||||
assert(not success, "should throw")
|
||||
assert(string.match(result, "fail"), "should throw fail")
|
||||
|
||||
a(2)
|
||||
assert(c() == 2, "c should recover and return 2")
|
||||
end)
|
||||
end)
|
||||
|
||||
return {}
|
||||
23
tests/signals/untrack.spec.luau
Normal file
23
tests/signals/untrack.spec.luau
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
local test = require("../suite").test
|
||||
local signals = require("../../packages/charm/src/signals")
|
||||
|
||||
local computed = signals.computed
|
||||
local pauseTracking = signals.pauseTracking
|
||||
local resumeTracking = signals.resumeTracking
|
||||
local signal = signals.signal
|
||||
|
||||
test("should pause tracking", function()
|
||||
local src = signal(0)
|
||||
local c = computed(function()
|
||||
pauseTracking()
|
||||
local value = src()
|
||||
resumeTracking()
|
||||
return value
|
||||
end)
|
||||
assert(c() == 0, "should not track src")
|
||||
|
||||
src(1)
|
||||
assert(c() == 0, "should not track src")
|
||||
end)
|
||||
|
||||
return {}
|
||||
Loading…
Add table
Add a link
Reference in a new issue