From 7a88c784faeb98e7a371c2d4f9c45db14e668396 Mon Sep 17 00:00:00 2001 From: littensy <56808540+littensy@users.noreply.github.com> Date: Fri, 31 Oct 2025 23:06:43 -0700 Subject: [PATCH] feat: update alien-signals --- .../src/__tests__/core/computed.spec.luau | 34 ++-- .../charm/src/__tests__/core/effect.spec.luau | 152 +++++++++--------- .../src/__tests__/core/effectScope.spec.luau | 16 +- .../src/__tests__/core/topology.spec.luau | 70 ++++---- packages/charm/src/__tests__/listen.spec.luau | 44 ++--- packages/charm/src/__tests__/mapped.spec.luau | 28 ++-- .../charm/src/__tests__/observe.spec.luau | 30 ++-- packages/charm/src/__tests__/peek.spec.luau | 58 +++---- .../charm/src/__tests__/subscribe.spec.luau | 48 +++--- .../charm/src/__tests__/untracked.spec.luau | 56 +++---- packages/charm/src/alien.luau | 43 ++++- packages/charm/src/core.luau | 141 +++++++++------- packages/charm/src/utils.luau | 12 +- rokit.toml | 8 +- 14 files changed, 397 insertions(+), 343 deletions(-) diff --git a/packages/charm/src/__tests__/core/computed.spec.luau b/packages/charm/src/__tests__/core/computed.spec.luau index 43c5fad..939d72a 100644 --- a/packages/charm/src/__tests__/core/computed.spec.luau +++ b/packages/charm/src/__tests__/core/computed.spec.luau @@ -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) diff --git a/packages/charm/src/__tests__/core/effect.spec.luau b/packages/charm/src/__tests__/core/effect.spec.luau index 89e4240..0d729ff 100644 --- a/packages/charm/src/__tests__/core/effect.spec.luau +++ b/packages/charm/src/__tests__/core/effect.spec.luau @@ -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) diff --git a/packages/charm/src/__tests__/core/effectScope.spec.luau b/packages/charm/src/__tests__/core/effectScope.spec.luau index c5dcbe8..b2dcdc7 100644 --- a/packages/charm/src/__tests__/core/effectScope.spec.luau +++ b/packages/charm/src/__tests__/core/effectScope.spec.luau @@ -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) diff --git a/packages/charm/src/__tests__/core/topology.spec.luau b/packages/charm/src/__tests__/core/topology.spec.luau index d3cdbf2..f40837d 100644 --- a/packages/charm/src/__tests__/core/topology.spec.luau +++ b/packages/charm/src/__tests__/core/topology.spec.luau @@ -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) diff --git a/packages/charm/src/__tests__/listen.spec.luau b/packages/charm/src/__tests__/listen.spec.luau index b529cde..d8c14c9 100644 --- a/packages/charm/src/__tests__/listen.spec.luau +++ b/packages/charm/src/__tests__/listen.spec.luau @@ -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) diff --git a/packages/charm/src/__tests__/mapped.spec.luau b/packages/charm/src/__tests__/mapped.spec.luau index 2d1e857..b812aea 100644 --- a/packages/charm/src/__tests__/mapped.spec.luau +++ b/packages/charm/src/__tests__/mapped.spec.luau @@ -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") diff --git a/packages/charm/src/__tests__/observe.spec.luau b/packages/charm/src/__tests__/observe.spec.luau index ed2bdda..1119b4e 100644 --- a/packages/charm/src/__tests__/observe.spec.luau +++ b/packages/charm/src/__tests__/observe.spec.luau @@ -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}`) diff --git a/packages/charm/src/__tests__/peek.spec.luau b/packages/charm/src/__tests__/peek.spec.luau index 0c9cc5b..b786135 100644 --- a/packages/charm/src/__tests__/peek.spec.luau +++ b/packages/charm/src/__tests__/peek.spec.luau @@ -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) diff --git a/packages/charm/src/__tests__/subscribe.spec.luau b/packages/charm/src/__tests__/subscribe.spec.luau index 3ebb003..938931b 100644 --- a/packages/charm/src/__tests__/subscribe.spec.luau +++ b/packages/charm/src/__tests__/subscribe.spec.luau @@ -1,9 +1,7 @@ local Charm = require("../") local suite = require("../../../../test/suite") -type Atom = Charm.Atom - -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) diff --git a/packages/charm/src/__tests__/untracked.spec.luau b/packages/charm/src/__tests__/untracked.spec.luau index f5c5f07..d4bfc4c 100644 --- a/packages/charm/src/__tests__/untracked.spec.luau +++ b/packages/charm/src/__tests__/untracked.spec.luau @@ -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) diff --git a/packages/charm/src/alien.luau b/packages/charm/src/alien.luau index b7604f3..5812be0 100644 --- a/packages/charm/src/alien.luau +++ b/packages/charm/src/alien.luau @@ -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 = { 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, diff --git a/packages/charm/src/core.luau b/packages/charm/src/core.luau index 4c5ee5e..f60d2e0 100644 --- a/packages/charm/src/core.luau +++ b/packages/charm/src/core.luau @@ -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(this: Computed): () -> T return function(): T local flags = this.flags @@ -196,6 +209,16 @@ local function computedOper(this: Computed): () -> 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(getter: (previousValue: T?) -> T): () -> T return computedOper({ - flags = bit32.bor(MUTABLE, DIRTY), + flags = NONE, getter = getter, }) end @@ -536,30 +559,40 @@ local function listen(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 diff --git a/packages/charm/src/utils.luau b/packages/charm/src/utils.luau index 1f19410..d5b19cf 100644 --- a/packages/charm/src/utils.luau +++ b/packages/charm/src/utils.luau @@ -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 diff --git a/rokit.toml b/rokit.toml index 1b1c5d7..30840b6 100644 --- a/rokit.toml +++ b/rokit.toml @@ -5,9 +5,9 @@ [tools] wally = "UpliftGames/wally@0.3.2" -rojo = "rojo-rbx/rojo@7.5.1" -stylua = "johnnymorganz/stylua@2.2.0" +rojo = "rojo-rbx/rojo@7.6.0" +stylua = "johnnymorganz/stylua@2.3.0" selene = "Kampfkarren/selene@0.29.0" -luau-lsp = "johnnymorganz/luau-lsp@1.53.5" -lune = "lune-org/lune@0.10.2" +luau-lsp = "johnnymorganz/luau-lsp@1.55.0" +lune = "lune-org/lune@0.10.4" lute = "luau-lang/lute@0.1.0-nightly.20250923"