mirror of
https://github.com/littensy/charm.git
synced 2026-10-20 17:51:37 +00:00
feat: update alien-signals
This commit is contained in:
parent
32788b19e1
commit
7a88c784fa
14 changed files with 397 additions and 343 deletions
|
|
@ -1,13 +1,13 @@
|
|||
local core = require("../../core")
|
||||
local suite = require("../../../../../test/suite")
|
||||
|
||||
local atom = core.atom
|
||||
local computed = core.computed
|
||||
local effect = core.effect
|
||||
local signal = core.signal
|
||||
local test = suite.test
|
||||
|
||||
test("should correctly propagate changes through computed core", function()
|
||||
local src = atom(0)
|
||||
local src, setSrc = signal(0)
|
||||
local c1 = computed(function()
|
||||
return src() % 2
|
||||
end)
|
||||
|
|
@ -19,15 +19,15 @@ test("should correctly propagate changes through computed core", function()
|
|||
end)
|
||||
|
||||
c3()
|
||||
src(1) -- c1 -> dirty, c2 -> toCheckDirty, c3 -> toCheckDirty
|
||||
setSrc(1) -- c1 -> dirty, c2 -> toCheckDirty, c3 -> toCheckDirty
|
||||
c2() -- c1 -> none, c2 -> none
|
||||
src(3) -- c1 -> dirty, c2 -> toCheckDirty
|
||||
setSrc(3) -- c1 -> dirty, c2 -> toCheckDirty
|
||||
|
||||
assert(c3() == 1, "bad final value")
|
||||
end)
|
||||
|
||||
test("should propagate updated source value through chained computations", function()
|
||||
local src = atom(0)
|
||||
local src, setSrc = signal(0)
|
||||
local a = computed(function()
|
||||
return src()
|
||||
end)
|
||||
|
|
@ -42,12 +42,12 @@ test("should propagate updated source value through chained computations", funct
|
|||
end)
|
||||
|
||||
assert(d() == 0, "bad initial value")
|
||||
src(2)
|
||||
setSrc(2)
|
||||
assert(d() == 2, "bad updated value")
|
||||
end)
|
||||
|
||||
test("should handle flags are indirectly updated during checkDirty", function()
|
||||
local a = atom(false)
|
||||
local a, setA = signal(false)
|
||||
local b = computed(function()
|
||||
return a()
|
||||
end)
|
||||
|
|
@ -61,29 +61,29 @@ test("should handle flags are indirectly updated during checkDirty", function()
|
|||
end)
|
||||
|
||||
assert(d() == false, "bad initial value")
|
||||
a(true)
|
||||
setA(true)
|
||||
assert(d() == true, "bad updated value")
|
||||
end)
|
||||
|
||||
test("should not update if the atom value is reverted", function()
|
||||
test("should not update if the signal value is reverted", function()
|
||||
local times = 0
|
||||
|
||||
local src = atom(0)
|
||||
local src, setSrc = signal(0)
|
||||
local c1 = computed(function()
|
||||
times += 1
|
||||
return src()
|
||||
end)
|
||||
c1()
|
||||
assert(times == 1, "computed did not run")
|
||||
src(1)
|
||||
src(0)
|
||||
setSrc(1)
|
||||
setSrc(0)
|
||||
c1()
|
||||
assert(times == 1, "computed ran when it should not")
|
||||
end)
|
||||
|
||||
test("should receive previous value", function()
|
||||
local calls = {}
|
||||
local src = atom(0)
|
||||
local src, setSrc = signal(0)
|
||||
local c1 = computed(function(current): number
|
||||
local incoming = src()
|
||||
return if current == incoming or incoming % 2 == 1 then current or incoming else incoming
|
||||
|
|
@ -93,12 +93,12 @@ test("should receive previous value", function()
|
|||
table.insert(calls, c1())
|
||||
end)
|
||||
|
||||
src(2)
|
||||
src(3)
|
||||
setSrc(2)
|
||||
setSrc(3)
|
||||
assert(c1() == 2, "should skip odd value")
|
||||
|
||||
src(4)
|
||||
src(5)
|
||||
setSrc(4)
|
||||
setSrc(5)
|
||||
assert(c1() == 4, "should skip odd value")
|
||||
assert(table.concat(calls) == "024", `should only call on even updates, got {table.concat(calls)}`)
|
||||
end)
|
||||
|
|
|
|||
|
|
@ -5,16 +5,16 @@ local computed = core.computed
|
|||
local effect = core.effect
|
||||
local effectScope = core.effectScope
|
||||
local endBatch = core.endBatch
|
||||
local setActiveSub = core.setActiveSub
|
||||
local onCleanup = core.onCleanup
|
||||
local atom = core.atom
|
||||
local setActiveSub = core.setActiveSub
|
||||
local signal = core.signal
|
||||
local startBatch = core.startBatch
|
||||
local test = suite.test
|
||||
|
||||
test("should clear subscriptions when untracked by all subscribers", function()
|
||||
local bRunTimes = 0
|
||||
|
||||
local a = atom(1)
|
||||
local a, setA = signal(1)
|
||||
local b = computed(function()
|
||||
bRunTimes += 1
|
||||
return a() * 2
|
||||
|
|
@ -24,15 +24,15 @@ test("should clear subscriptions when untracked by all subscribers", function()
|
|||
end)
|
||||
|
||||
assert(bRunTimes == 1, "should run initially")
|
||||
a(2)
|
||||
setA(2)
|
||||
assert(bRunTimes == 2, "should run after change")
|
||||
stopEffect()
|
||||
a(3)
|
||||
setA(3)
|
||||
assert(bRunTimes == 2, "should not run after stop")
|
||||
end)
|
||||
|
||||
test("should not run untracked inner effect", function()
|
||||
local a = atom(3)
|
||||
local a, setA = signal(3)
|
||||
local b = computed(function()
|
||||
return a() > 0
|
||||
end)
|
||||
|
|
@ -47,14 +47,14 @@ test("should not run untracked inner effect", function()
|
|||
end
|
||||
end)
|
||||
|
||||
a(2)
|
||||
a(1)
|
||||
a(0)
|
||||
setA(2)
|
||||
setA(1)
|
||||
setA(0)
|
||||
end)
|
||||
|
||||
test("should run outer effect first", function()
|
||||
local a = atom(1)
|
||||
local b = atom(1)
|
||||
local a, setA = signal(1)
|
||||
local b, setB = signal(1)
|
||||
|
||||
effect(function()
|
||||
if a() > 0 then
|
||||
|
|
@ -68,13 +68,13 @@ test("should run outer effect first", function()
|
|||
end)
|
||||
|
||||
startBatch()
|
||||
b(0)
|
||||
a(0)
|
||||
setB(0)
|
||||
setA(0)
|
||||
endBatch()
|
||||
end)
|
||||
|
||||
test("should not trigger inner effect when resolve maybe dirty", function()
|
||||
local a = atom(0)
|
||||
local a, setA = signal(0)
|
||||
local b = computed(function()
|
||||
return a() % 2
|
||||
end)
|
||||
|
|
@ -91,68 +91,66 @@ test("should not trigger inner effect when resolve maybe dirty", function()
|
|||
end)
|
||||
end)
|
||||
|
||||
a(2)
|
||||
setA(2)
|
||||
end)
|
||||
|
||||
test("should trigger inner effects in sequence", function()
|
||||
local a = atom(0)
|
||||
local b = atom(0)
|
||||
test("should notify inner effects in the same order as non-inner effects", function()
|
||||
local getA, setA = signal(0)
|
||||
local getB, setB = signal(0)
|
||||
local c = computed(function()
|
||||
return a() - b()
|
||||
return getA() - getB()
|
||||
end)
|
||||
local order1 = {}
|
||||
local order2 = {}
|
||||
local order3 = {}
|
||||
|
||||
effect(function()
|
||||
table.insert(order1, "effect1")
|
||||
getA()
|
||||
end)
|
||||
effect(function()
|
||||
table.insert(order1, "effect2")
|
||||
getA()
|
||||
getB()
|
||||
end)
|
||||
local order: { string } = {}
|
||||
|
||||
effect(function()
|
||||
c()
|
||||
|
||||
effect(function()
|
||||
table.insert(order, "first inner")
|
||||
a()
|
||||
table.insert(order2, "effect1")
|
||||
getA()
|
||||
end)
|
||||
|
||||
effect(function()
|
||||
table.insert(order, "last inner")
|
||||
a()
|
||||
b()
|
||||
table.insert(order2, "effect2")
|
||||
getA()
|
||||
getB()
|
||||
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 = atom(0)
|
||||
local b = atom(0)
|
||||
local order: { string } = {}
|
||||
|
||||
effectScope(function()
|
||||
effect(function()
|
||||
table.insert(order, "first inner")
|
||||
a()
|
||||
table.insert(order3, "effect1")
|
||||
getA()
|
||||
end)
|
||||
|
||||
effect(function()
|
||||
table.insert(order, "last inner")
|
||||
a()
|
||||
b()
|
||||
table.insert(order3, "effect2")
|
||||
getA()
|
||||
getB()
|
||||
end)
|
||||
end)
|
||||
|
||||
table.clear(order)
|
||||
table.clear(order1)
|
||||
table.clear(order2)
|
||||
table.clear(order3)
|
||||
|
||||
startBatch()
|
||||
b(1)
|
||||
a(1)
|
||||
setB(1)
|
||||
setA(1)
|
||||
endBatch()
|
||||
|
||||
assert(table.concat(order, ", ") == "first inner, last inner", `wrong order: {table.concat(order, ", ")}`)
|
||||
assert(table.concat(order1, ", ") == "effect2, effect1", "wrong order 1")
|
||||
assert(table.concat(order2, ", ") == "effect2, effect1", "wrong order 2")
|
||||
assert(table.concat(order3, ", ") == "effect2, effect1", "wrong order 3")
|
||||
end)
|
||||
|
||||
test("should custom effect support batch", function()
|
||||
|
|
@ -168,13 +166,13 @@ test("should custom effect support batch", function()
|
|||
end
|
||||
|
||||
local logs: { string } = {}
|
||||
local a = atom(0)
|
||||
local b = atom(0)
|
||||
local a = signal(0)
|
||||
local b, setB = signal(0)
|
||||
|
||||
local aa = computed(function()
|
||||
table.insert(logs, "aa-0")
|
||||
if a() == 0 then
|
||||
b(1)
|
||||
setB(1)
|
||||
end
|
||||
table.insert(logs, "aa-1")
|
||||
return nil
|
||||
|
|
@ -196,8 +194,8 @@ test("should custom effect support batch", function()
|
|||
end)
|
||||
|
||||
test("should duplicate subscribers do not affect the notify order", function()
|
||||
local src1 = atom(0)
|
||||
local src2 = atom(0)
|
||||
local src1, setSrc1 = signal(0)
|
||||
local src2, setSrc2 = signal(0)
|
||||
local order: { string } = {}
|
||||
|
||||
effect(function()
|
||||
|
|
@ -215,17 +213,17 @@ test("should duplicate subscribers do not affect the notify order", function()
|
|||
table.insert(order, "b")
|
||||
src1()
|
||||
end)
|
||||
src2(1) -- src1.subs: a -> b -> a
|
||||
setSrc2(1) -- src1.subs: a -> b -> a
|
||||
|
||||
table.clear(order)
|
||||
src1(src1() + 1)
|
||||
setSrc1(src1() + 1)
|
||||
|
||||
assert(table.concat(order) == "ab", `wrong order: {table.concat(order)}`)
|
||||
end)
|
||||
|
||||
test("should handle side effect with inner effects", function()
|
||||
local a = atom(0)
|
||||
local b = atom(0)
|
||||
local a, setA = signal(0)
|
||||
local b, setB = signal(0)
|
||||
local order: { string } = {}
|
||||
|
||||
effect(function()
|
||||
|
|
@ -240,14 +238,14 @@ test("should handle side effect with inner effects", function()
|
|||
assert(table.concat(order) == "ab", `wrong order: {table.concat(order)}`)
|
||||
|
||||
table.clear(order)
|
||||
b(1)
|
||||
a(1)
|
||||
setB(1)
|
||||
setA(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 = atom(false)
|
||||
local a, setA = signal(false)
|
||||
local b = computed(function()
|
||||
return a()
|
||||
end)
|
||||
|
|
@ -267,14 +265,14 @@ test("should handle flags are indirectly updated during checkDirty", function()
|
|||
triggers += 1
|
||||
end)
|
||||
assert(triggers == 1, "did not run initially")
|
||||
a(true)
|
||||
setA(true)
|
||||
assert(triggers == 2, "did not update indirectly")
|
||||
end)
|
||||
|
||||
test("should run cleanups in sequence", function()
|
||||
local order = {}
|
||||
|
||||
local a = atom(false)
|
||||
local a, setA = signal(false)
|
||||
local stopEffect = effect(function()
|
||||
a()
|
||||
onCleanup(function()
|
||||
|
|
@ -287,7 +285,7 @@ test("should run cleanups in sequence", function()
|
|||
|
||||
assert(table.concat(order) == "", "should not cleanup immediately")
|
||||
|
||||
a(true)
|
||||
setA(true)
|
||||
assert(table.concat(order) == "ab", `bad update: {table.concat(order)}`)
|
||||
|
||||
table.clear(order)
|
||||
|
|
@ -295,14 +293,14 @@ test("should run cleanups in sequence", function()
|
|||
assert(table.concat(order) == "ab", `bad dispose: {table.concat(order)}`)
|
||||
|
||||
table.clear(order)
|
||||
a(false)
|
||||
setA(false)
|
||||
assert(table.concat(order) == "", "should not cleanup after update")
|
||||
end)
|
||||
|
||||
test("should run inner effect cleanup", function()
|
||||
local cleanups = 0
|
||||
|
||||
local src = atom(false)
|
||||
local src, setSrc = signal(false)
|
||||
local stopEffect = effect(function()
|
||||
src()
|
||||
effect(function()
|
||||
|
|
@ -318,18 +316,18 @@ test("should run inner effect cleanup", function()
|
|||
end)
|
||||
|
||||
assert(cleanups == 0, "should not cleanup immediately")
|
||||
src(true)
|
||||
setSrc(true)
|
||||
assert(cleanups == 2, "should cleanup after outer update")
|
||||
stopEffect()
|
||||
assert(cleanups == 4, "should cleanup after outer dispose")
|
||||
src(false)
|
||||
setSrc(false)
|
||||
assert(cleanups == 4, "should not cleanup after update")
|
||||
end)
|
||||
|
||||
test("should run cleanups outside any evaluation context", function()
|
||||
local calls = 0
|
||||
local a = atom(0)
|
||||
local b = atom(0)
|
||||
local a, setA = signal(0)
|
||||
local b, setB = signal(0)
|
||||
local c = computed(function()
|
||||
if a() == 0 then
|
||||
effect(function()
|
||||
|
|
@ -348,18 +346,18 @@ test("should run cleanups outside any evaluation context", function()
|
|||
assert(calls == 1, "should run once")
|
||||
|
||||
calls = 0
|
||||
a(1)
|
||||
setA(1)
|
||||
assert(calls == 1, "should run once after a update")
|
||||
|
||||
calls = 0
|
||||
b(1)
|
||||
setB(1)
|
||||
assert(calls == 0, "should not run after b update")
|
||||
end)
|
||||
|
||||
test("should allow self cleanup", function()
|
||||
local calls = 0
|
||||
local cleanups = 0
|
||||
local a = atom(0)
|
||||
local a, setA = signal(0)
|
||||
|
||||
effect(function(stopEffect)
|
||||
onCleanup(function()
|
||||
|
|
@ -372,7 +370,7 @@ test("should allow self cleanup", function()
|
|||
|
||||
assert(calls == 1, "should call effect once")
|
||||
assert(cleanups == 1, "should call cleanup once")
|
||||
a(1)
|
||||
setA(1)
|
||||
assert(calls == 1, "should not call effect after a update")
|
||||
assert(cleanups == 1, "should not call cleanup after a update")
|
||||
end)
|
||||
|
|
|
|||
|
|
@ -3,12 +3,12 @@ local suite = require("../../../../../test/suite")
|
|||
|
||||
local effect = core.effect
|
||||
local effectScope = core.effectScope
|
||||
local atom = core.atom
|
||||
local signal = core.signal
|
||||
local onCleanup = core.onCleanup
|
||||
local test = suite.test
|
||||
|
||||
test("should not trigger after stop", function()
|
||||
local count = atom(1)
|
||||
local count, setCount = signal(1)
|
||||
|
||||
local triggers = 0
|
||||
|
||||
|
|
@ -19,14 +19,14 @@ test("should not trigger after stop", function()
|
|||
end)
|
||||
assert(triggers == 1, "did not run initially")
|
||||
|
||||
count(2)
|
||||
setCount(2)
|
||||
assert(triggers == 2, "did not run after update")
|
||||
end)
|
||||
|
||||
count(3)
|
||||
setCount(3)
|
||||
assert(triggers == 3, "did not run after update outside scope")
|
||||
stopScope()
|
||||
count(4)
|
||||
setCount(4)
|
||||
assert(triggers == 3, "ran after stop")
|
||||
end)
|
||||
|
||||
|
|
@ -52,7 +52,7 @@ end)
|
|||
test("should run indirect effect cleanup", function()
|
||||
local cleanups = 0
|
||||
|
||||
local a = atom(false)
|
||||
local a, setA = signal(false)
|
||||
local stopScope = effectScope(function()
|
||||
effect(function()
|
||||
a()
|
||||
|
|
@ -63,11 +63,11 @@ test("should run indirect effect cleanup", function()
|
|||
end)
|
||||
|
||||
assert(cleanups == 0, "should not cleanup immediately")
|
||||
a(true)
|
||||
setA(true)
|
||||
assert(cleanups == 1, "should cleanup once")
|
||||
stopScope()
|
||||
assert(cleanups == 2, "should cleanup after outer dispose")
|
||||
a(false)
|
||||
setA(false)
|
||||
assert(cleanups == 2, "should not cleanup after update")
|
||||
end)
|
||||
|
||||
|
|
|
|||
|
|
@ -3,13 +3,13 @@ local suite = require("../../../../../test/suite")
|
|||
|
||||
local computed = core.computed
|
||||
local effect = core.effect
|
||||
local atom = core.atom
|
||||
local signal = core.signal
|
||||
local describe = suite.describe
|
||||
local test = suite.test
|
||||
|
||||
--[[
|
||||
Tests adopted with thanks from preact-core implementation at
|
||||
https://github.com/preactjs/core/blob/main/packages/core/test/atom.test.tsx
|
||||
https://github.com/preactjs/signals/blob/main/packages/core/test/signal.test.tsx
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
|
|
@ -43,7 +43,7 @@ describe("graph updates", function()
|
|||
-- C
|
||||
-- |
|
||||
-- D
|
||||
local a = atom(2)
|
||||
local a, setA = signal(2)
|
||||
|
||||
local b = computed(function()
|
||||
return a() - 1
|
||||
|
|
@ -64,12 +64,12 @@ describe("graph updates", function()
|
|||
assert(computeCalls == 1, "compute should be called once")
|
||||
computeCalls = 0
|
||||
|
||||
a(4)
|
||||
setA(4)
|
||||
d()
|
||||
assert(computeCalls == 1, "compute should drop update")
|
||||
end)
|
||||
|
||||
test("should only update every atom once (diamond graph)", function()
|
||||
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
|
||||
|
|
@ -77,8 +77,7 @@ describe("graph updates", function()
|
|||
-- B C
|
||||
-- \ /
|
||||
-- D
|
||||
|
||||
local a = atom("a")
|
||||
local a, setA = signal("a")
|
||||
local b = computed(function()
|
||||
return a()
|
||||
end)
|
||||
|
|
@ -96,12 +95,12 @@ describe("graph updates", function()
|
|||
assert(d() == "a a", "d should be 'a a'")
|
||||
assert(spyCalls == 1, "compute should be called once")
|
||||
|
||||
a("aa")
|
||||
setA("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 atom once (diamond graph + tail)", function()
|
||||
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
|
||||
-- / \
|
||||
|
|
@ -110,8 +109,7 @@ describe("graph updates", function()
|
|||
-- D
|
||||
-- |
|
||||
-- E
|
||||
|
||||
local a = atom("a")
|
||||
local a, setA = signal("a")
|
||||
local b = computed(function()
|
||||
return a()
|
||||
end)
|
||||
|
|
@ -133,7 +131,7 @@ describe("graph updates", function()
|
|||
assert(e() == "a a", "e should be 'a a'")
|
||||
assert(spyCalls == 1, "compute should be called once")
|
||||
|
||||
a("aa")
|
||||
setA("aa")
|
||||
assert(e() == "aa aa", "e should be 'aa aa'")
|
||||
assert(spyCalls == 2, "compute should be called only twice")
|
||||
end)
|
||||
|
|
@ -141,7 +139,7 @@ describe("graph updates", function()
|
|||
test("should bail out if result is the same", function()
|
||||
-- Bail out if value of "B" never changes
|
||||
-- A->B->C
|
||||
local a = atom("a")
|
||||
local a, setA = signal("a")
|
||||
local b = computed(function()
|
||||
a()
|
||||
return "foo"
|
||||
|
|
@ -157,12 +155,12 @@ describe("graph updates", function()
|
|||
assert(c() == "foo", "c should be 'foo'")
|
||||
assert(spyCalls == 1, "compute should be called once")
|
||||
|
||||
a("aa")
|
||||
setA("aa")
|
||||
assert(c() == "foo", "c should be 'foo'")
|
||||
assert(spyCalls == 1, "compute should bail out")
|
||||
end)
|
||||
|
||||
test("should only update every atom once (jagged diamond graph + tails)", function()
|
||||
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
|
||||
-- / \
|
||||
|
|
@ -173,7 +171,7 @@ describe("graph updates", function()
|
|||
-- E
|
||||
-- / \
|
||||
-- F G
|
||||
local a = atom("a")
|
||||
local a, setA = signal("a")
|
||||
|
||||
local b = computed(function()
|
||||
return a()
|
||||
|
|
@ -222,7 +220,7 @@ describe("graph updates", function()
|
|||
fSpyCalls, fSpyTime = 0, 0
|
||||
gSpyCalls, gSpyTime = 0, 0
|
||||
|
||||
a("b")
|
||||
setA("b")
|
||||
|
||||
assert(e() == "b b", "e should be 'b b'")
|
||||
assert(eSpyCalls == 1, "e should run once after update")
|
||||
|
|
@ -237,7 +235,7 @@ describe("graph updates", function()
|
|||
fSpyCalls, fSpyTime = 0, 0
|
||||
gSpyCalls, gSpyTime = 0, 0
|
||||
|
||||
a("c")
|
||||
setA("c")
|
||||
|
||||
assert(e() == "c c", "e should be 'c c'")
|
||||
assert(eSpyCalls == 1, "e should run once after update")
|
||||
|
|
@ -258,7 +256,7 @@ describe("graph updates", function()
|
|||
-- *A
|
||||
-- / \
|
||||
-- *B C <- we don't listen to C
|
||||
local a = atom("a")
|
||||
local a, setA = signal("a")
|
||||
|
||||
local b = computed(function()
|
||||
return a()
|
||||
|
|
@ -273,7 +271,7 @@ describe("graph updates", function()
|
|||
assert(b() == "a", "b should be 'a'")
|
||||
assert(spyCalls == 0, "compute should not run")
|
||||
|
||||
a("aa")
|
||||
setA("aa")
|
||||
assert(b() == "aa", "b should be 'aa'")
|
||||
assert(spyCalls == 0, "compute should not run")
|
||||
end)
|
||||
|
|
@ -287,7 +285,7 @@ describe("graph updates", function()
|
|||
-- *B D <- we don't listen to C
|
||||
-- |
|
||||
-- *C
|
||||
local a = atom("a")
|
||||
local a, setA = signal("a")
|
||||
local spyBCalls = 0
|
||||
local spyB = function()
|
||||
spyBCalls += 1
|
||||
|
|
@ -318,7 +316,7 @@ describe("graph updates", function()
|
|||
spyCCalls = 0
|
||||
unsub()
|
||||
|
||||
a("aa")
|
||||
setA("aa")
|
||||
|
||||
assert(spyBCalls == 0, "b should not run")
|
||||
assert(spyCCalls == 0, "c should not run")
|
||||
|
|
@ -335,7 +333,7 @@ describe("graph updates", function()
|
|||
-- B *C <- returns same value every time
|
||||
-- \ /
|
||||
-- D
|
||||
local a = atom("a")
|
||||
local a, setA = signal("a")
|
||||
local b = computed(function()
|
||||
return a()
|
||||
end)
|
||||
|
|
@ -353,7 +351,7 @@ describe("graph updates", function()
|
|||
assert(d() == "a c", "d should be 'a c'")
|
||||
spyResult = nil
|
||||
|
||||
a("aa")
|
||||
setA("aa")
|
||||
d()
|
||||
assert(spyResult == "aa c", "d should have returned 'aa c'")
|
||||
end)
|
||||
|
|
@ -367,7 +365,7 @@ describe("graph updates", function()
|
|||
-- B *C *D
|
||||
-- \ | /
|
||||
-- E
|
||||
local a = atom("a")
|
||||
local a, setA = signal("a")
|
||||
local b = computed(function()
|
||||
return a()
|
||||
end)
|
||||
|
|
@ -389,13 +387,13 @@ describe("graph updates", function()
|
|||
assert(e() == "a c d", "e should be 'a c d'")
|
||||
spyResult = nil
|
||||
|
||||
a("aa")
|
||||
setA("aa")
|
||||
e()
|
||||
assert(spyResult == "aa c d", "e should have returned 'aa c d'")
|
||||
end)
|
||||
|
||||
test("should support lazy branches", function()
|
||||
local a = atom(0)
|
||||
local a, setA = signal(0)
|
||||
local b = computed(function()
|
||||
return a()
|
||||
end)
|
||||
|
|
@ -404,10 +402,10 @@ describe("graph updates", function()
|
|||
end)
|
||||
|
||||
assert(c() == 0, "c should be 0")
|
||||
a(1)
|
||||
setA(1)
|
||||
assert(c() == 1, "c should be 1")
|
||||
|
||||
a(0)
|
||||
setA(0)
|
||||
assert(c() == 0, "c should be 0 again")
|
||||
end)
|
||||
|
||||
|
|
@ -419,7 +417,7 @@ describe("graph updates", function()
|
|||
-- *B *C
|
||||
-- \ /
|
||||
-- D
|
||||
local a = atom("a")
|
||||
local a, setA = signal("a")
|
||||
local b = computed(function()
|
||||
a()
|
||||
return "b"
|
||||
|
|
@ -438,14 +436,14 @@ describe("graph updates", function()
|
|||
assert(d() == "b c", "d should be 'b c'")
|
||||
spyCalls = 0
|
||||
|
||||
a("aa")
|
||||
setA("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 = atom(0)
|
||||
local a, setA = signal(0)
|
||||
local b = computed(function()
|
||||
error("fail")
|
||||
end)
|
||||
|
|
@ -459,12 +457,12 @@ describe("error handling", function()
|
|||
assert(not success, "should throw")
|
||||
assert(string.match(result, "fail"), "should throw fail")
|
||||
|
||||
a(1)
|
||||
setA(1)
|
||||
assert(c() == 1, "c should recover and return 1")
|
||||
end)
|
||||
|
||||
test("should keep graph consistent on errors in computeds", function()
|
||||
local a = atom(0)
|
||||
local a, setA = signal(0)
|
||||
local b = computed(function()
|
||||
if a() == 1 then
|
||||
error("fail")
|
||||
|
|
@ -477,14 +475,14 @@ describe("error handling", function()
|
|||
|
||||
assert(c() == 0, "c should be 0")
|
||||
|
||||
a(1)
|
||||
setA(1)
|
||||
local success, result = pcall(function()
|
||||
b()
|
||||
end)
|
||||
assert(not success, "should throw")
|
||||
assert(string.match(result, "fail"), "should throw fail")
|
||||
|
||||
a(2)
|
||||
setA(2)
|
||||
assert(c() == 2, "c should recover and return 2")
|
||||
end)
|
||||
end)
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
local Charm = require("../")
|
||||
local suite = require("../../../../test/suite")
|
||||
|
||||
local atom = Charm.atom
|
||||
local signal = Charm.signal
|
||||
local listen = Charm.listen
|
||||
local effect = Charm.effect
|
||||
local onCleanup = Charm.onCleanup
|
||||
local test = suite.test
|
||||
|
||||
test("should listen to selector", function()
|
||||
local source = atom(0)
|
||||
local source, setSource = signal(0)
|
||||
local current, previous
|
||||
|
||||
local unsub = listen(function()
|
||||
|
|
@ -19,20 +19,20 @@ test("should listen to selector", function()
|
|||
end)
|
||||
|
||||
assert(current == 1 and previous == nil, "should run initially")
|
||||
source(0)
|
||||
setSource(0)
|
||||
assert(current == 1 and previous == nil, "should skip redundant update")
|
||||
source(1)
|
||||
setSource(1)
|
||||
assert(current == 2 and previous == 1, "should run with new value")
|
||||
unsub()
|
||||
source(2)
|
||||
setSource(2)
|
||||
assert(current == 2 and previous == 1, "should not run after unsub")
|
||||
end)
|
||||
|
||||
test("should run in peek", function()
|
||||
local runs = 0
|
||||
local cleanups = 0
|
||||
local a = atom(0)
|
||||
local b = atom(0)
|
||||
local a, setA = signal(0)
|
||||
local b, setB = signal(0)
|
||||
|
||||
listen(a, function()
|
||||
runs += 1
|
||||
|
|
@ -47,31 +47,31 @@ test("should run in peek", function()
|
|||
assert(runs == 1, "should run once on init")
|
||||
assert(cleanups == 0, "should not cleanup yet")
|
||||
|
||||
a(1)
|
||||
setA(1)
|
||||
assert(runs == 2, "should run on a change")
|
||||
assert(cleanups == 1, "should cleanup first call")
|
||||
|
||||
b(1)
|
||||
setB(1)
|
||||
assert(runs == 2, "should not run on b change")
|
||||
assert(cleanups == 1, "should not cleanup second call")
|
||||
end)
|
||||
|
||||
test("should run if first value is nil", function()
|
||||
local runs = 0
|
||||
local source = atom(nil :: number?)
|
||||
local source, setSource = signal(nil :: number?)
|
||||
|
||||
listen(source, function()
|
||||
runs += 1
|
||||
end)
|
||||
|
||||
assert(runs == 1, "should run once on init")
|
||||
source(1)
|
||||
setSource(1)
|
||||
assert(runs == 2, "should run once on change")
|
||||
end)
|
||||
|
||||
test("should track inner subs", function()
|
||||
local order = {}
|
||||
local a = atom(1)
|
||||
local a, setA = signal(1)
|
||||
|
||||
local unsub = listen(a, function()
|
||||
table.insert(order, 1)
|
||||
|
|
@ -86,22 +86,22 @@ test("should track inner subs", function()
|
|||
assert(table.concat(order) == "123", `bad mount order: {table.concat(order)}`)
|
||||
|
||||
table.clear(order)
|
||||
a(a() + 1)
|
||||
setA(a() + 1)
|
||||
assert(table.concat(order) == "123", `bad order: {table.concat(order)}`)
|
||||
|
||||
table.clear(order)
|
||||
a(a() + 1)
|
||||
setA(a() + 1)
|
||||
assert(table.concat(order) == "123", `bad order: {table.concat(order)}`)
|
||||
|
||||
table.clear(order)
|
||||
unsub()
|
||||
a(a() + 1)
|
||||
setA(a() + 1)
|
||||
assert(table.concat(order) == "", `bad order: {table.concat(order)}`)
|
||||
end)
|
||||
|
||||
test("should skip redundant updates", function()
|
||||
local order = {}
|
||||
local a = atom(0)
|
||||
local a, setA = signal(0)
|
||||
|
||||
listen(function(): "even" | "odd"
|
||||
return if a() % 2 == 0 then "even" else "odd"
|
||||
|
|
@ -109,17 +109,17 @@ test("should skip redundant updates", function()
|
|||
table.insert(order, value)
|
||||
end)
|
||||
|
||||
a(1)
|
||||
a(3)
|
||||
a(4)
|
||||
a(6)
|
||||
setA(1)
|
||||
setA(3)
|
||||
setA(4)
|
||||
setA(6)
|
||||
assert(table.concat(order) == "evenoddeven", `bad order: {table.concat(order)}`)
|
||||
end)
|
||||
|
||||
test("should allow self cleanup", function()
|
||||
local calls = 0
|
||||
local cleanups = 0
|
||||
local a = atom(0)
|
||||
local a, setA = signal(0)
|
||||
|
||||
listen(a, function(_, _, stopEffect)
|
||||
onCleanup(function()
|
||||
|
|
@ -131,7 +131,7 @@ test("should allow self cleanup", function()
|
|||
|
||||
assert(calls == 1, "should call effect once")
|
||||
assert(cleanups == 1, "should call cleanup once")
|
||||
a(1)
|
||||
setA(1)
|
||||
assert(calls == 1, "should not call effect after a update")
|
||||
assert(cleanups == 1, "should not call cleanup after a update")
|
||||
end)
|
||||
|
|
|
|||
|
|
@ -1,25 +1,25 @@
|
|||
local Charm = require("../")
|
||||
local suite = require("../../../../test/suite")
|
||||
|
||||
local atom = Charm.atom
|
||||
local signal = Charm.signal
|
||||
local mapped = Charm.mapped
|
||||
local test = suite.test
|
||||
|
||||
test("should map atom", function()
|
||||
local src = atom({ 1, 2, 3 })
|
||||
test("should map state", function()
|
||||
local src, setSrc = signal({ 1, 2, 3 })
|
||||
local map = mapped(src, function(value: number)
|
||||
return value + 1
|
||||
end)
|
||||
|
||||
assert(table.concat(map()) == "234", table.concat(map()))
|
||||
src({ 4, 5, 6 })
|
||||
setSrc({ 4, 5, 6 })
|
||||
assert(table.concat(map()) == "567", table.concat(map()))
|
||||
end)
|
||||
|
||||
test("should not map unchanged values", function()
|
||||
local calls = 0
|
||||
|
||||
local src = atom({ 1, 2, 3 })
|
||||
local src, setSrc = signal({ 1, 2, 3 })
|
||||
local map = mapped(src, function(value: number)
|
||||
calls += 1
|
||||
return value + 1
|
||||
|
|
@ -28,11 +28,11 @@ test("should not map unchanged values", function()
|
|||
assert(table.concat(map()) == "234", table.concat(map()))
|
||||
assert(calls == 3, "should map 3 values")
|
||||
|
||||
src({ 1, 3, 3 })
|
||||
setSrc({ 1, 3, 3 })
|
||||
assert(table.concat(map()) == "244", table.concat(map()))
|
||||
assert(calls == 4, "should map 1 change")
|
||||
|
||||
src({})
|
||||
setSrc({})
|
||||
assert(#map() == 0, "should be empty")
|
||||
assert(calls == 4, "should not map for removed values")
|
||||
end)
|
||||
|
|
@ -40,7 +40,7 @@ end)
|
|||
test("should not update if no values change", function()
|
||||
local calls = 0
|
||||
|
||||
local src = atom({ 1, 2, 3 })
|
||||
local src, setSrc = signal({ 1, 2, 3 })
|
||||
local map = mapped(src, function(value: number)
|
||||
calls += 1
|
||||
return value + 1
|
||||
|
|
@ -50,8 +50,8 @@ test("should not update if no values change", function()
|
|||
assert(calls == 3, "should map 3 values")
|
||||
assert(table.concat(map()) == "234", table.concat(map()))
|
||||
|
||||
src({ 4, 5, 6, 7 })
|
||||
src({ 1, 2, 3 })
|
||||
setSrc({ 4, 5, 6, 7 })
|
||||
setSrc({ 1, 2, 3 })
|
||||
map()
|
||||
assert(calls == 3, "should not map unchanged values")
|
||||
assert(table.concat(map()) == "234", table.concat(map()))
|
||||
|
|
@ -61,7 +61,7 @@ end)
|
|||
test("should filter values", function()
|
||||
local calls = 0
|
||||
|
||||
local src = atom({ a = 1, b = 2, c = 3 })
|
||||
local src, setSrc = signal({ a = 1, b = 2, c = 3 })
|
||||
local map = mapped(src, function(value: number)
|
||||
calls += 1
|
||||
return if value < 0 then nil else value
|
||||
|
|
@ -72,7 +72,7 @@ test("should filter values", function()
|
|||
assert(state.a and state.b and state.c, "should map all values")
|
||||
|
||||
calls = 0
|
||||
src({ a = -1, b = 2, c = 3, d = 4 })
|
||||
setSrc({ a = -1, b = 2, c = 3, d = 4 })
|
||||
state = map()
|
||||
assert(calls == 2, "should map 2 values")
|
||||
assert(state.a == nil, "should remove a")
|
||||
|
|
@ -83,7 +83,7 @@ end)
|
|||
test("should map keys", function()
|
||||
local calls = 0
|
||||
|
||||
local src = atom({ a = 1, b = 2, c = 3 })
|
||||
local src, setSrc = signal({ a = 1, b = 2, c = 3 })
|
||||
local map = mapped(src, function(value: number, key: string)
|
||||
calls += 1
|
||||
return if value < 0 then nil else value, string.upper(key)
|
||||
|
|
@ -97,7 +97,7 @@ test("should map keys", function()
|
|||
assert(state.C == 3, "should map c")
|
||||
|
||||
calls = 0
|
||||
src({ a = -1, b = 2, c = 3, d = 4 })
|
||||
setSrc({ a = -1, b = 2, c = 3, d = 4 })
|
||||
state = map()
|
||||
assert(calls == 2, "should map 2 values")
|
||||
assert(state.A == nil and state.a == nil, "should remove a")
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
local Charm = require("../")
|
||||
local suite = require("../../../../test/suite")
|
||||
|
||||
local atom = Charm.atom
|
||||
local signal = Charm.signal
|
||||
local computed = Charm.computed
|
||||
local effect = Charm.effect
|
||||
local observe = Charm.observe
|
||||
local test = suite.test
|
||||
|
||||
test("should observe items", function()
|
||||
local items = atom({ 0, 0, 0 })
|
||||
local items, setItems = signal({ 0, 0, 0 })
|
||||
local updates = {}
|
||||
local cleanups = {}
|
||||
|
||||
|
|
@ -29,18 +29,18 @@ test("should observe items", function()
|
|||
assert(table.concat(updates) == "000", `bad mount: {table.concat(updates)}`)
|
||||
|
||||
table.clear(updates)
|
||||
items({ 1, 2, 3 })
|
||||
setItems({ 1, 2, 3 })
|
||||
assert(table.concat(updates) == "123", `bad update: {table.concat(updates)}`)
|
||||
|
||||
table.clear(updates)
|
||||
items({ 1, 2, nil :: any, 4 })
|
||||
setItems({ 1, 2, nil :: any, 4 })
|
||||
assert(table.concat(updates) == "4", `should add 4: {table.concat(updates)}`)
|
||||
assert(table.concat(cleanups) == "3", `bad cleanup: {table.concat(cleanups)}`)
|
||||
end)
|
||||
|
||||
test("should dispose scopes", function()
|
||||
local items = atom({ 1, 2, 3 })
|
||||
local a = atom(0)
|
||||
local items, setItems = signal({ 1, 2, 3 })
|
||||
local a, setA = signal(0)
|
||||
local runs = {}
|
||||
|
||||
local dispose = observe(items, function(item)
|
||||
|
|
@ -51,23 +51,23 @@ test("should dispose scopes", function()
|
|||
end)
|
||||
|
||||
table.clear(runs)
|
||||
items({ 1, 2 })
|
||||
a(1)
|
||||
setItems({ 1, 2 })
|
||||
setA(1)
|
||||
assert(table.concat(runs) == "12", `bad reruns: {table.concat(runs)}`)
|
||||
|
||||
table.clear(runs)
|
||||
dispose()
|
||||
a(2)
|
||||
setA(2)
|
||||
assert(table.concat(runs) == "", `should not rerun: {table.concat(runs)}`)
|
||||
end)
|
||||
|
||||
test("should queue addition", function()
|
||||
local items = atom({ 1 })
|
||||
local items, setItems = signal({ 1 })
|
||||
local edits = {}
|
||||
|
||||
observe(items, function(item)
|
||||
items({ 1, 2 })
|
||||
items({ 1, 2, 3, 4 })
|
||||
setItems({ 1, 2 })
|
||||
setItems({ 1, 2, 3, 4 })
|
||||
table.insert(edits, `+{item}`)
|
||||
return function()
|
||||
table.insert(edits, `-{item}`)
|
||||
|
|
@ -78,12 +78,12 @@ test("should queue addition", function()
|
|||
end)
|
||||
|
||||
test("should queue removal", function()
|
||||
local items = atom({ 1, 2, 3, 4 })
|
||||
local items, setItems = signal({ 1, 2, 3, 4 })
|
||||
local edits = {}
|
||||
|
||||
observe(items, function(item)
|
||||
items({ 1, 2, 3 })
|
||||
items({ 1 })
|
||||
setItems({ 1, 2, 3 })
|
||||
setItems({ 1 })
|
||||
table.insert(edits, `+{item}`)
|
||||
return function()
|
||||
table.insert(edits, `-{item}`)
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
local Charm = require("../")
|
||||
local suite = require("../../../../test/suite")
|
||||
|
||||
local atom = Charm.atom
|
||||
local signal = Charm.signal
|
||||
local computed = Charm.computed
|
||||
local effect = Charm.effect
|
||||
local peek = Charm.peek
|
||||
local test = suite.test
|
||||
|
||||
test("should not run detached", function()
|
||||
local a = atom(1)
|
||||
local a, setA = signal(1)
|
||||
local spyCalls = 0
|
||||
|
||||
effect(function()
|
||||
|
|
@ -23,15 +23,15 @@ test("should not run detached", function()
|
|||
end)
|
||||
|
||||
assert(spyCalls == 0, "should not be called yet")
|
||||
a(2)
|
||||
setA(2)
|
||||
assert(spyCalls == 1, "should be called once")
|
||||
a(3)
|
||||
setA(3)
|
||||
assert(spyCalls == 2, "should be called twice")
|
||||
end)
|
||||
|
||||
test("should block tracking inside effects", function()
|
||||
local a = atom(1)
|
||||
local b = atom(2)
|
||||
local a, setA = signal(1)
|
||||
local b, setB = signal(2)
|
||||
local spyCalls = 0
|
||||
|
||||
effect(function()
|
||||
|
|
@ -42,13 +42,13 @@ test("should block tracking inside effects", function()
|
|||
end)
|
||||
assert(spyCalls == 1, "should be called once")
|
||||
|
||||
a(10)
|
||||
b(20)
|
||||
setA(10)
|
||||
setB(20)
|
||||
assert(spyCalls == 1, "should be called once")
|
||||
end)
|
||||
|
||||
test("should block tracking even when run inside effect run inside peek", function()
|
||||
local s = atom(1)
|
||||
local s, setS = signal(1)
|
||||
local spyCalls = 0
|
||||
|
||||
peek(function()
|
||||
|
|
@ -61,32 +61,32 @@ test("should block tracking even when run inside effect run inside peek", functi
|
|||
end)
|
||||
assert(spyCalls == 1, "should be called once")
|
||||
|
||||
s(2)
|
||||
setS(2)
|
||||
assert(spyCalls == 1, "should be called once")
|
||||
end)
|
||||
|
||||
test("should not cause atom assignments throw", function()
|
||||
local a = atom(1)
|
||||
local aChangedTime = atom(0)
|
||||
test("should not cause signal assignments to throw", function()
|
||||
local a, setA = signal(1)
|
||||
local aChangedTime, setAChangedTime = signal(0)
|
||||
|
||||
local dispose = effect(function()
|
||||
a()
|
||||
peek(function()
|
||||
aChangedTime(aChangedTime() + 1)
|
||||
setAChangedTime(aChangedTime() + 1)
|
||||
end)
|
||||
end)
|
||||
|
||||
a(2)
|
||||
setA(2)
|
||||
assert(aChangedTime() == 2, "aChangedTime should be 2")
|
||||
a(3)
|
||||
setA(3)
|
||||
assert(aChangedTime() == 3, "aChangedTime should be 3")
|
||||
|
||||
dispose()
|
||||
end)
|
||||
|
||||
test("should block tracking inside computed atoms", function()
|
||||
local a = atom(1)
|
||||
local b = atom(2)
|
||||
test("should block tracking inside computed signals", function()
|
||||
local a, setA = signal(1)
|
||||
local b, setB = signal(2)
|
||||
local spyCalls = 0
|
||||
local c = computed(function()
|
||||
return peek(function()
|
||||
|
|
@ -97,18 +97,18 @@ test("should block tracking inside computed atoms", function()
|
|||
|
||||
assert(spyCalls == 0, "should not be called yet")
|
||||
assert(c() == 3, "c should be 3")
|
||||
a(10)
|
||||
setA(10)
|
||||
c()
|
||||
b(20)
|
||||
setB(20)
|
||||
c()
|
||||
assert(spyCalls == 1, "should be called once")
|
||||
assert(c() == 3, "c should be 3")
|
||||
end)
|
||||
|
||||
test("should track outside peek", function()
|
||||
local a = atom(1)
|
||||
local b = atom(2)
|
||||
local c = atom(3)
|
||||
local a, setA = signal(1)
|
||||
local b, setB = signal(2)
|
||||
local c, setC = signal(3)
|
||||
local spyCalls = 0
|
||||
|
||||
effect(function()
|
||||
|
|
@ -119,16 +119,16 @@ test("should track outside peek", function()
|
|||
end)
|
||||
|
||||
assert(spyCalls == 1, "should be called once")
|
||||
a(10)
|
||||
setA(10)
|
||||
assert(spyCalls == 2, "should be called twice")
|
||||
b(20)
|
||||
setB(20)
|
||||
assert(spyCalls == 3, "should be called three times")
|
||||
c(30)
|
||||
setC(30)
|
||||
assert(spyCalls == 3, "should not run on c change")
|
||||
end)
|
||||
|
||||
test("should stack peek", function()
|
||||
local a = atom(1)
|
||||
local a, setA = signal(1)
|
||||
local spyCalls = 0
|
||||
|
||||
effect(function()
|
||||
|
|
@ -140,7 +140,7 @@ test("should stack peek", function()
|
|||
end)
|
||||
|
||||
assert(spyCalls == 1, "should be called once")
|
||||
a(2)
|
||||
setA(2)
|
||||
assert(spyCalls == 1, "should not be called twice")
|
||||
end)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
local Charm = require("../")
|
||||
local suite = require("../../../../test/suite")
|
||||
|
||||
type Atom<T> = Charm.Atom<T>
|
||||
|
||||
local atom = Charm.atom
|
||||
local signal = Charm.signal
|
||||
local computed = Charm.computed
|
||||
local subscribe = Charm.subscribe
|
||||
local listen = Charm.listen
|
||||
|
|
@ -11,7 +9,7 @@ local effect = Charm.effect
|
|||
local test = suite.test
|
||||
|
||||
test("should subscribe to selector", function()
|
||||
local source = atom(0)
|
||||
local source, setSource = signal(0)
|
||||
local current, previous
|
||||
|
||||
local unsub = subscribe(function()
|
||||
|
|
@ -22,20 +20,20 @@ test("should subscribe to selector", function()
|
|||
end)
|
||||
|
||||
assert(not current and not previous, "should not run initially")
|
||||
source(0)
|
||||
setSource(0)
|
||||
assert(not current and not previous, "should skip redundant update")
|
||||
source(1)
|
||||
setSource(1)
|
||||
assert(current == 2 and previous == 1, "should run with new value")
|
||||
unsub()
|
||||
source(2)
|
||||
setSource(2)
|
||||
assert(current == 2 and previous == 1, "should not run after unsub")
|
||||
end)
|
||||
|
||||
test("should run in peek", function()
|
||||
local runs = 0
|
||||
local cleanups = 0
|
||||
local a = atom(0)
|
||||
local b = atom(0)
|
||||
local a, setA = signal(0)
|
||||
local b, setB = signal(0)
|
||||
|
||||
subscribe(a, function()
|
||||
runs += 1
|
||||
|
|
@ -50,30 +48,30 @@ test("should run in peek", function()
|
|||
assert(runs == 0, "should not run once on init")
|
||||
assert(cleanups == 0, "should not cleanup yet")
|
||||
|
||||
a(1)
|
||||
setA(1)
|
||||
assert(runs == 1, "should run once on change")
|
||||
assert(cleanups == 0, "should not cleanup yet")
|
||||
|
||||
b(1)
|
||||
setB(1)
|
||||
assert(runs == 1, "should not run on b change")
|
||||
assert(cleanups == 0, "should not cleanup first call")
|
||||
|
||||
a(2)
|
||||
setA(2)
|
||||
assert(runs == 2, "should run on a change")
|
||||
assert(cleanups == 1, "should cleanup first call")
|
||||
end)
|
||||
|
||||
test("should ensure correct execution order", function()
|
||||
local order = {}
|
||||
local n1 = atom(0)
|
||||
local n2 = atom(0)
|
||||
local n1, setN1 = signal(0)
|
||||
local n2, setN2 = signal(0)
|
||||
local sum = computed(function()
|
||||
return n1() + n2()
|
||||
end)
|
||||
|
||||
subscribe(n1, function()
|
||||
table.insert(order, 1)
|
||||
n2(n2() + 1)
|
||||
setN2(n2() + 1)
|
||||
end)
|
||||
|
||||
subscribe(sum, function()
|
||||
|
|
@ -84,14 +82,14 @@ test("should ensure correct execution order", function()
|
|||
table.insert(order, 3)
|
||||
end)
|
||||
|
||||
n1(n1() + 1)
|
||||
setN1(n1() + 1)
|
||||
|
||||
assert(table.concat(order) == "123", `bad order: {table.concat(order)}`)
|
||||
end)
|
||||
|
||||
test("should track inner subs", function()
|
||||
local order = {}
|
||||
local a = atom(1)
|
||||
local a, setA = signal(1)
|
||||
|
||||
local unsub = subscribe(a, function()
|
||||
table.insert(order, 1)
|
||||
|
|
@ -103,22 +101,22 @@ test("should track inner subs", function()
|
|||
end)
|
||||
end)
|
||||
|
||||
a(a() + 1)
|
||||
setA(a() + 1)
|
||||
assert(table.concat(order) == "123", `bad order: {table.concat(order)}`)
|
||||
|
||||
table.clear(order)
|
||||
a(a() + 1)
|
||||
setA(a() + 1)
|
||||
assert(table.concat(order) == "123", `bad order: {table.concat(order)}`)
|
||||
|
||||
table.clear(order)
|
||||
unsub()
|
||||
a(a() + 1)
|
||||
setA(a() + 1)
|
||||
assert(table.concat(order) == "", `bad order: {table.concat(order)}`)
|
||||
end)
|
||||
|
||||
test("should skip redundant updates", function()
|
||||
local order = {}
|
||||
local a = atom(0)
|
||||
local a, setA = signal(0)
|
||||
|
||||
subscribe(function(): "even" | "odd"
|
||||
return if a() % 2 == 0 then "even" else "odd"
|
||||
|
|
@ -126,10 +124,10 @@ test("should skip redundant updates", function()
|
|||
table.insert(order, value)
|
||||
end)
|
||||
|
||||
a(1)
|
||||
a(3)
|
||||
a(4)
|
||||
a(6)
|
||||
setA(1)
|
||||
setA(3)
|
||||
setA(4)
|
||||
setA(6)
|
||||
assert(table.concat(order) == "oddeven", `bad order: {table.concat(order)}`)
|
||||
end)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
local Charm = require("../")
|
||||
local suite = require("../../../../test/suite")
|
||||
|
||||
local atom = Charm.atom
|
||||
local signal = Charm.signal
|
||||
local computed = Charm.computed
|
||||
local effect = Charm.effect
|
||||
local effectScope = Charm.effectScope
|
||||
|
|
@ -9,7 +9,7 @@ local untracked = Charm.untracked
|
|||
local test = suite.test
|
||||
|
||||
test("should run detached from effect", function()
|
||||
local a = atom(1)
|
||||
local a, setA = signal(1)
|
||||
local effects = 0
|
||||
local innerEffects = {}
|
||||
|
||||
|
|
@ -27,7 +27,7 @@ test("should run detached from effect", function()
|
|||
end)
|
||||
|
||||
assert(effects == 1, "should have one effect")
|
||||
a(2)
|
||||
setA(2)
|
||||
assert(effects == 2, "should have two effects")
|
||||
stopEffect()
|
||||
assert(effects == 2, "should still have two effects")
|
||||
|
|
@ -60,8 +60,8 @@ test("should run detached from effect scope", function()
|
|||
end)
|
||||
|
||||
test("should block tracking inside effects", function()
|
||||
local a = atom(1)
|
||||
local b = atom(2)
|
||||
local a, setA = signal(1)
|
||||
local b, setB = signal(2)
|
||||
local spyCalls = 0
|
||||
|
||||
effect(function()
|
||||
|
|
@ -72,13 +72,13 @@ test("should block tracking inside effects", function()
|
|||
end)
|
||||
assert(spyCalls == 1, "should be called once")
|
||||
|
||||
a(10)
|
||||
b(20)
|
||||
setA(10)
|
||||
setB(20)
|
||||
assert(spyCalls == 1, "should be called once")
|
||||
end)
|
||||
|
||||
test("should block tracking even when run inside effect run inside untracked", function()
|
||||
local s = atom(1)
|
||||
local s, setS = signal(1)
|
||||
local spyCalls = 0
|
||||
|
||||
untracked(function()
|
||||
|
|
@ -91,32 +91,32 @@ test("should block tracking even when run inside effect run inside untracked", f
|
|||
end)
|
||||
assert(spyCalls == 1, "should be called once")
|
||||
|
||||
s(2)
|
||||
setS(2)
|
||||
assert(spyCalls == 1, "should be called once")
|
||||
end)
|
||||
|
||||
test("should not cause atom assignments throw", function()
|
||||
local a = atom(1)
|
||||
local aChangedTime = atom(0)
|
||||
test("should not cause signal assignments to throw", function()
|
||||
local a, setA = signal(1)
|
||||
local aChangedTime, setAChangedTime = signal(0)
|
||||
|
||||
local dispose = effect(function()
|
||||
a()
|
||||
untracked(function()
|
||||
aChangedTime(aChangedTime() + 1)
|
||||
setAChangedTime(aChangedTime() + 1)
|
||||
end)
|
||||
end)
|
||||
|
||||
a(2)
|
||||
setA(2)
|
||||
assert(aChangedTime() == 2, "aChangedTime should be 2")
|
||||
a(3)
|
||||
setA(3)
|
||||
assert(aChangedTime() == 3, "aChangedTime should be 3")
|
||||
|
||||
dispose()
|
||||
end)
|
||||
|
||||
test("should block tracking inside computed atoms", function()
|
||||
local a = atom(1)
|
||||
local b = atom(2)
|
||||
test("should block tracking inside computed signals", function()
|
||||
local a, setA = signal(1)
|
||||
local b, setB = signal(2)
|
||||
local spyCalls = 0
|
||||
local c = computed(function()
|
||||
return untracked(function()
|
||||
|
|
@ -127,18 +127,18 @@ test("should block tracking inside computed atoms", function()
|
|||
|
||||
assert(spyCalls == 0, "should not be called yet")
|
||||
assert(c() == 3, "c should be 3")
|
||||
a(10)
|
||||
setA(10)
|
||||
c()
|
||||
b(20)
|
||||
setB(20)
|
||||
c()
|
||||
assert(spyCalls == 1, "should be called once")
|
||||
assert(c() == 3, "c should be 3")
|
||||
end)
|
||||
|
||||
test("should track outside untracked", function()
|
||||
local a = atom(1)
|
||||
local b = atom(2)
|
||||
local c = atom(3)
|
||||
local a, setA = signal(1)
|
||||
local b, setB = signal(2)
|
||||
local c, setC = signal(3)
|
||||
local spyCalls = 0
|
||||
|
||||
effect(function()
|
||||
|
|
@ -149,16 +149,16 @@ test("should track outside untracked", function()
|
|||
end)
|
||||
|
||||
assert(spyCalls == 1, "should be called once")
|
||||
a(10)
|
||||
setA(10)
|
||||
assert(spyCalls == 2, "should be called twice")
|
||||
b(20)
|
||||
setB(20)
|
||||
assert(spyCalls == 3, "should be called three times")
|
||||
c(30)
|
||||
setC(30)
|
||||
assert(spyCalls == 3, "should not run on c change")
|
||||
end)
|
||||
|
||||
test("should stack untracked", function()
|
||||
local a = atom(1)
|
||||
local a, setA = signal(1)
|
||||
local spyCalls = 0
|
||||
|
||||
effect(function()
|
||||
|
|
@ -170,7 +170,7 @@ test("should stack untracked", function()
|
|||
end)
|
||||
|
||||
assert(spyCalls == 1, "should be called once")
|
||||
a(2)
|
||||
setA(2)
|
||||
assert(spyCalls == 1, "should not be called twice")
|
||||
end)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,30 @@
|
|||
--[[
|
||||
Reactive system adapted from alien-signals at
|
||||
https://github.com/stackblitz/alien-signals/blob/master/src/system.ts
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024-present Johnson Chu
|
||||
|
||||
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.
|
||||
]]
|
||||
|
||||
export type ReactiveNode = {
|
||||
deps: Link?,
|
||||
depsTail: Link?,
|
||||
|
|
@ -23,14 +50,14 @@ type Stack<T> = {
|
|||
|
||||
export type ReactiveFlags = number
|
||||
|
||||
local NONE = 0b00000000
|
||||
local MUTABLE = 0b00000001
|
||||
local WATCHING = 0b00000010
|
||||
local RECURSED_CHECK = 0b00000100
|
||||
local RECURSED = 0b00001000
|
||||
local DIRTY = 0b00010000
|
||||
local PENDING = 0b00100000
|
||||
local PEEKING = 0b10000000
|
||||
local NONE = 0b0000000
|
||||
local MUTABLE = 0b0000001
|
||||
local WATCHING = 0b0000010
|
||||
local RECURSED_CHECK = 0b0000100
|
||||
local RECURSED = 0b0001000
|
||||
local DIRTY = 0b0010000
|
||||
local PENDING = 0b0100000
|
||||
local PEEKING = 0b1000000
|
||||
|
||||
local ReactiveFlags = {
|
||||
None = NONE,
|
||||
|
|
|
|||
|
|
@ -11,14 +11,40 @@ local createReactiveSystem = alien.createReactiveSystem
|
|||
local deepFreeze = utils.deepFreeze
|
||||
local noYield = utils.noYield
|
||||
|
||||
local NONE = 0b00000000
|
||||
local MUTABLE = 0b00000001
|
||||
local WATCHING = 0b00000010
|
||||
local RECURSED_CHECK = 0b00000100
|
||||
local DIRTY = 0b00010000
|
||||
local PENDING = 0b00100000
|
||||
local QUEUED = 0b01000000
|
||||
local PEEKING = 0b10000000
|
||||
--[[
|
||||
Reactive system adapted from alien-signals at
|
||||
https://github.com/stackblitz/alien-signals/blob/master/src/index.ts
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024-present Johnson Chu
|
||||
|
||||
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.
|
||||
]]
|
||||
|
||||
local NONE = 0b0000000
|
||||
local MUTABLE = 0b0000001
|
||||
local WATCHING = 0b0000010
|
||||
local RECURSED_CHECK = 0b0000100
|
||||
local DIRTY = 0b0010000
|
||||
local PENDING = 0b0100000
|
||||
local PEEKING = 0b1000000
|
||||
|
||||
type EffectScope = ReactiveNode & {
|
||||
cleanups: { () -> () },
|
||||
|
|
@ -49,8 +75,8 @@ local shallowPropagate: (link: Link) -> ()
|
|||
local cycle = 0
|
||||
local batchDepth = 0
|
||||
local notifyIndex = 0
|
||||
local queuedEffects: { Effect | EffectScope? } = {}
|
||||
local queuedEffectsLength = 0
|
||||
local queued: { Effect? } = {}
|
||||
local queuedLength = 0
|
||||
local activeSub: ReactiveNode?
|
||||
|
||||
local function getActiveSub(): ReactiveNode?
|
||||
|
|
@ -83,15 +109,9 @@ local function runCleanups(effect: EffectScope)
|
|||
table.clear(effect.cleanups)
|
||||
end
|
||||
|
||||
local function setNotPending(sub: ReactiveNode, flags: ReactiveFlags)
|
||||
sub.flags = bit32.band(flags, bit32.bnot(PENDING))
|
||||
end
|
||||
|
||||
local function run(e: Effect, flags: ReactiveFlags): ()
|
||||
if
|
||||
bit32.btest(flags, DIRTY)
|
||||
or (bit32.btest(flags, PENDING) and (checkDirty(e.deps :: Link, e) or setNotPending(e, flags)))
|
||||
then
|
||||
local function run(e: Effect): ()
|
||||
local flags = e.flags
|
||||
if bit32.btest(flags, DIRTY) or (bit32.btest(flags, PENDING) and checkDirty(e.deps :: Link, e)) then
|
||||
runCleanups(e)
|
||||
cycle += 1
|
||||
e.depsTail = nil
|
||||
|
|
@ -107,30 +127,19 @@ local function run(e: Effect, flags: ReactiveFlags): ()
|
|||
table.insert(e.cleanups, result)
|
||||
end
|
||||
else
|
||||
local link = e.deps
|
||||
while link do
|
||||
local dep = link.dep
|
||||
local depFlags = dep.flags
|
||||
if bit32.btest(depFlags, QUEUED) then
|
||||
depFlags = bit32.band(depFlags, bit32.bnot(QUEUED))
|
||||
dep.flags = depFlags
|
||||
run(dep :: Effect, depFlags)
|
||||
end
|
||||
link = link.nextDep
|
||||
end
|
||||
e.flags = WATCHING
|
||||
end
|
||||
end
|
||||
|
||||
local function flush(): ()
|
||||
while notifyIndex < queuedEffectsLength do
|
||||
while notifyIndex < queuedLength do
|
||||
local effect = queued[notifyIndex] :: Effect
|
||||
queued[notifyIndex] = nil
|
||||
notifyIndex += 1
|
||||
local effect = queuedEffects[notifyIndex] :: Effect
|
||||
queuedEffects[notifyIndex] = nil
|
||||
effect.flags = bit32.band(effect.flags, bit32.bnot(QUEUED))
|
||||
run(effect, effect.flags)
|
||||
run(effect)
|
||||
end
|
||||
notifyIndex = 0
|
||||
queuedEffectsLength = 0
|
||||
queuedLength = 0
|
||||
end
|
||||
|
||||
local function getBatchDepth(): number
|
||||
|
|
@ -183,6 +192,10 @@ local function updateSignal(s: Signal): boolean
|
|||
end
|
||||
end
|
||||
|
||||
local function setNotPending(sub: ReactiveNode, flags: ReactiveFlags)
|
||||
sub.flags = bit32.band(flags, bit32.bnot(PENDING))
|
||||
end
|
||||
|
||||
local function computedOper<T>(this: Computed<T>): () -> T
|
||||
return function(): T
|
||||
local flags = this.flags
|
||||
|
|
@ -196,6 +209,16 @@ local function computedOper<T>(this: Computed<T>): () -> T
|
|||
shallowPropagate(subs)
|
||||
end
|
||||
end
|
||||
elseif flags == NONE then
|
||||
this.flags = MUTABLE
|
||||
local prevSub = setActiveSub(this)
|
||||
local success, result = pcall(this.getter)
|
||||
activeSub = prevSub
|
||||
if success then
|
||||
this.value = result
|
||||
else
|
||||
error(result)
|
||||
end
|
||||
end
|
||||
if activeSub then
|
||||
link(this, activeSub, cycle)
|
||||
|
|
@ -332,7 +355,7 @@ end
|
|||
--- @see https://github.com/littensy/charm?tab=readme-ov-file#computedgetter
|
||||
local function computed<T>(getter: (previousValue: T?) -> T): () -> T
|
||||
return computedOper({
|
||||
flags = bit32.bor(MUTABLE, DIRTY),
|
||||
flags = NONE,
|
||||
getter = getter,
|
||||
})
|
||||
end
|
||||
|
|
@ -536,30 +559,40 @@ local function listen<T>(getter: () -> T, callback: (state: T, previous: T?, dis
|
|||
return dispose
|
||||
end
|
||||
|
||||
local function notify(e: ReactiveNode)
|
||||
local flags = e.flags
|
||||
if not bit32.btest(flags, QUEUED) then
|
||||
e.flags = bit32.bor(flags, QUEUED)
|
||||
local subs = e.subs
|
||||
if subs then
|
||||
notify(subs.sub)
|
||||
else
|
||||
queuedEffectsLength += 1
|
||||
queuedEffects[queuedEffectsLength] = e :: Effect
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local reactiveSystem = createReactiveSystem({
|
||||
update = function(node: ReactiveNode): boolean
|
||||
update = function(node)
|
||||
if node.depsTail then
|
||||
return updateComputed(node :: Computed)
|
||||
else
|
||||
return updateSignal(node :: Signal)
|
||||
end
|
||||
end,
|
||||
notify = notify,
|
||||
unwatched = function(node: ReactiveNode)
|
||||
notify = function(effect)
|
||||
local insertIndex = queuedLength
|
||||
local firstInsertedIndex = insertIndex
|
||||
|
||||
repeat
|
||||
effect.flags = bit32.band(effect.flags, bit32.bnot(WATCHING))
|
||||
queued[insertIndex] = effect :: Effect
|
||||
insertIndex += 1
|
||||
effect = (effect.subs and effect.subs.sub) :: Effect
|
||||
if not effect or bit32.band(effect.flags, WATCHING) == 0 then
|
||||
break
|
||||
end
|
||||
until false
|
||||
|
||||
queuedLength = insertIndex
|
||||
|
||||
insertIndex -= 1
|
||||
while firstInsertedIndex < insertIndex do
|
||||
local left = queued[firstInsertedIndex]
|
||||
queued[firstInsertedIndex] = queued[insertIndex]
|
||||
queued[insertIndex] = left
|
||||
firstInsertedIndex += 1
|
||||
insertIndex -= 1
|
||||
end
|
||||
end,
|
||||
unwatched = function(node)
|
||||
if bit32.band(node.flags, MUTABLE) == 0 then
|
||||
effectScopeOper(node :: EffectScope)()
|
||||
elseif node.depsTail then
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
local function noYieldHandler(thread: thread, ok: boolean, ...)
|
||||
if not ok then
|
||||
return false, debug.traceback(thread, tostring((...)))
|
||||
elseif coroutine.status(thread) ~= "dead" then
|
||||
return false, debug.traceback(thread, "Attempted to yield!")
|
||||
else
|
||||
local function noYieldHandler(thread: thread, ok: boolean, ...): (boolean, ...any)
|
||||
if ok and coroutine.status(thread) == "dead" then
|
||||
return true, ...
|
||||
elseif not ok then
|
||||
return false, debug.traceback(thread, tostring((...)))
|
||||
else
|
||||
return false, debug.traceback(thread, "Attempted to yield!")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue