mirror of
https://github.com/littensy/charm.git
synced 2026-10-20 17:51:37 +00:00
refactor: rename batch, misc fixes & perf updates
This commit is contained in:
parent
2aa7bd3e15
commit
7bb6edb403
16 changed files with 214 additions and 258 deletions
|
|
@ -65,9 +65,9 @@ end
|
|||
local function hydrate(payloads: { SyncPayload })
|
||||
for _, payload in payloads do
|
||||
if payload.type == "init" then
|
||||
Charm.batched(applyInitPayload, payload.data)
|
||||
Charm.batch(applyInitPayload, payload.data)
|
||||
elseif payload.type == "patch" then
|
||||
Charm.batched(applyPatchPayload, payload.data)
|
||||
Charm.batch(applyPatchPayload, payload.data)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
local core = require("../../core")
|
||||
local suite = require("../../../../../test/suite")
|
||||
local core = require("../core")
|
||||
local suite = require("../../../../test/suite")
|
||||
|
||||
local computed = core.computed
|
||||
local effect = core.effect
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
local core = require("../../core")
|
||||
local suite = require("../../../../../test/suite")
|
||||
local core = require("../core")
|
||||
local suite = require("../../../../test/suite")
|
||||
|
||||
local computed = core.computed
|
||||
local effect = core.effect
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
local core = require("../../core")
|
||||
local suite = require("../../../../../test/suite")
|
||||
local core = require("../core")
|
||||
local suite = require("../../../../test/suite")
|
||||
|
||||
local effect = core.effect
|
||||
local effectScope = core.effectScope
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
local core = require("../../core")
|
||||
local suite = require("../../../../../test/suite")
|
||||
local core = require("../core")
|
||||
local suite = require("../../../../test/suite")
|
||||
|
||||
local atom = core.atom
|
||||
local computed = core.computed
|
||||
|
|
@ -4,6 +4,7 @@ local suite = require("../../../../test/suite")
|
|||
local signal = Charm.signal
|
||||
local computed = Charm.computed
|
||||
local effect = Charm.effect
|
||||
local effectScope = Charm.effectScope
|
||||
local observe = Charm.observe
|
||||
local test = suite.test
|
||||
|
||||
|
|
@ -42,48 +43,44 @@ test("should dispose scopes", function()
|
|||
local items, setItems = signal({ 1, 2, 3 })
|
||||
local a, setA = signal(0)
|
||||
local runs = {}
|
||||
local disposes = {}
|
||||
|
||||
local dispose = observe(items, function(item)
|
||||
effect(function()
|
||||
table.insert(runs, item)
|
||||
a()
|
||||
end)
|
||||
effectScope(function()
|
||||
return function()
|
||||
table.insert(disposes, item)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
table.clear(runs)
|
||||
setItems({ 1, 2 })
|
||||
setA(1)
|
||||
assert(table.concat(runs) == "12", `bad reruns: {table.concat(runs)}`)
|
||||
assert(table.concat(disposes) == "3", `should dispose 3: {table.concat(disposes)}`)
|
||||
|
||||
table.clear(runs)
|
||||
table.clear(disposes)
|
||||
dispose()
|
||||
setA(2)
|
||||
assert(table.concat(runs) == "", `should not rerun: {table.concat(runs)}`)
|
||||
assert(table.concat(disposes) == "12", `should dispose 1 and 2: {table.concat(disposes)}`)
|
||||
end)
|
||||
|
||||
test("should queue addition", function()
|
||||
test("should queue addition and removal", function()
|
||||
local items, setItems = signal({ 1 })
|
||||
local edits = {}
|
||||
|
||||
observe(items, function(item)
|
||||
setItems({ 1, 2 })
|
||||
setItems({ 1, 2, 3, 4 })
|
||||
table.insert(edits, `+{item}`)
|
||||
return function()
|
||||
table.insert(edits, `-{item}`)
|
||||
if item == 1 then
|
||||
setItems({ 1, 2, 3, 4 })
|
||||
elseif item == 4 then
|
||||
setItems({ 1 })
|
||||
end
|
||||
end)
|
||||
|
||||
assert(table.concat(edits) == "+1+2+3+4", `bad edits: {table.concat(edits)}`)
|
||||
end)
|
||||
|
||||
test("should queue removal", function()
|
||||
local items, setItems = signal({ 1, 2, 3, 4 })
|
||||
local edits = {}
|
||||
|
||||
observe(items, function(item)
|
||||
setItems({ 1, 2, 3 })
|
||||
setItems({ 1 })
|
||||
table.insert(edits, `+{item}`)
|
||||
return function()
|
||||
table.insert(edits, `-{item}`)
|
||||
|
|
@ -93,4 +90,23 @@ test("should queue removal", function()
|
|||
assert(table.concat(edits) == "+1+2+3+4-2-3-4", `bad edits: {table.concat(edits)}`)
|
||||
end)
|
||||
|
||||
test("should handle self removal", function()
|
||||
local items, setItems = signal({ 1, 2, 3, 4 })
|
||||
local edits = {}
|
||||
|
||||
observe(items, function(item, index)
|
||||
setItems(function(state)
|
||||
state = table.clone(state)
|
||||
state[index] = nil
|
||||
return state
|
||||
end)
|
||||
table.insert(edits, `+{item}`)
|
||||
return function()
|
||||
table.insert(edits, `-{item}`)
|
||||
end
|
||||
end)
|
||||
|
||||
assert(table.concat(edits) == "+1+2+3+4-1-2-3-4", `bad edits: {table.concat(edits)}`)
|
||||
end)
|
||||
|
||||
return {}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
local core = require("../../core")
|
||||
local suite = require("../../../../../test/suite")
|
||||
local core = require("../core")
|
||||
local suite = require("../../../../test/suite")
|
||||
|
||||
local signal = core.signal
|
||||
local effect = core.effect
|
||||
|
|
@ -37,4 +37,14 @@ test("should enforce immutability", function()
|
|||
end), "should not allow mutation of new value")
|
||||
end)
|
||||
|
||||
test("should not freeze values with metatables", function()
|
||||
local src = signal({ object = setmetatable({}, {}) })
|
||||
assert(
|
||||
pcall(function()
|
||||
src().object.a = 2
|
||||
end),
|
||||
"should allow mutation of values with metatables"
|
||||
)
|
||||
end)
|
||||
|
||||
return {}
|
||||
|
|
@ -1,26 +1,17 @@
|
|||
local suite = require("../../../../test/suite")
|
||||
|
||||
suite.describe("core", function()
|
||||
suite.describe("computed", function()
|
||||
require("./core/computed.spec")
|
||||
end)
|
||||
suite.describe("effect", function()
|
||||
require("./core/effect.spec")
|
||||
end)
|
||||
suite.describe("effectScope", function()
|
||||
require("./core/effectScope.spec")
|
||||
end)
|
||||
suite.describe("gc", function()
|
||||
require("./core/gc.spec")
|
||||
end)
|
||||
suite.describe("signal", function()
|
||||
require("./core/signal.spec")
|
||||
end)
|
||||
suite.describe("topology", function()
|
||||
require("./core/topology.spec")
|
||||
end)
|
||||
suite.describe("computed", function()
|
||||
require("./computed.spec")
|
||||
end)
|
||||
suite.describe("effect", function()
|
||||
require("./effect.spec")
|
||||
end)
|
||||
suite.describe("effectScope", function()
|
||||
require("./effectScope.spec")
|
||||
end)
|
||||
suite.describe("gc", function()
|
||||
require("./gc.spec")
|
||||
end)
|
||||
|
||||
suite.describe("listen", function()
|
||||
require("./listen.spec")
|
||||
end)
|
||||
|
|
@ -30,14 +21,20 @@ end)
|
|||
suite.describe("observe", function()
|
||||
require("./observe.spec")
|
||||
end)
|
||||
suite.describe("peek", function()
|
||||
require("./peek.spec")
|
||||
end)
|
||||
suite.describe("signal", function()
|
||||
require("./signal.spec")
|
||||
end)
|
||||
suite.describe("subscribe", function()
|
||||
require("./subscribe.spec")
|
||||
end)
|
||||
suite.describe("topology", function()
|
||||
require("./topology.spec")
|
||||
end)
|
||||
suite.describe("untracked", function()
|
||||
require("./untracked.spec")
|
||||
end)
|
||||
suite.describe("peek", function()
|
||||
require("./peek.spec")
|
||||
end)
|
||||
|
||||
return {}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
local core = require("../../core")
|
||||
local suite = require("../../../../../test/suite")
|
||||
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 describe = suite.describe
|
||||
local test = suite.test
|
||||
|
||||
|
|
@ -43,7 +43,7 @@ describe("graph updates", function()
|
|||
-- C
|
||||
-- |
|
||||
-- D
|
||||
local a, setA = signal(2)
|
||||
local a = atom(2)
|
||||
|
||||
local b = computed(function()
|
||||
return a() - 1
|
||||
|
|
@ -64,7 +64,7 @@ describe("graph updates", function()
|
|||
assert(computeCalls == 1, "compute should be called once")
|
||||
computeCalls = 0
|
||||
|
||||
setA(4)
|
||||
a(4)
|
||||
d()
|
||||
assert(computeCalls == 1, "compute should drop update")
|
||||
end)
|
||||
|
|
@ -77,7 +77,7 @@ describe("graph updates", function()
|
|||
-- B C
|
||||
-- \ /
|
||||
-- D
|
||||
local a, setA = signal("a")
|
||||
local a = atom("a")
|
||||
local b = computed(function()
|
||||
return a()
|
||||
end)
|
||||
|
|
@ -95,7 +95,7 @@ describe("graph updates", function()
|
|||
assert(d() == "a a", "d should be 'a a'")
|
||||
assert(spyCalls == 1, "compute should be called once")
|
||||
|
||||
setA("aa")
|
||||
a("aa")
|
||||
assert(d() == "aa aa", "d should be 'aa aa'")
|
||||
assert(spyCalls == 2, "compute should be called only twice")
|
||||
end)
|
||||
|
|
@ -109,7 +109,7 @@ describe("graph updates", function()
|
|||
-- D
|
||||
-- |
|
||||
-- E
|
||||
local a, setA = signal("a")
|
||||
local a = atom("a")
|
||||
local b = computed(function()
|
||||
return a()
|
||||
end)
|
||||
|
|
@ -131,7 +131,7 @@ describe("graph updates", function()
|
|||
assert(e() == "a a", "e should be 'a a'")
|
||||
assert(spyCalls == 1, "compute should be called once")
|
||||
|
||||
setA("aa")
|
||||
a("aa")
|
||||
assert(e() == "aa aa", "e should be 'aa aa'")
|
||||
assert(spyCalls == 2, "compute should be called only twice")
|
||||
end)
|
||||
|
|
@ -139,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, setA = signal("a")
|
||||
local a = atom("a")
|
||||
local b = computed(function()
|
||||
a()
|
||||
return "foo"
|
||||
|
|
@ -155,7 +155,7 @@ describe("graph updates", function()
|
|||
assert(c() == "foo", "c should be 'foo'")
|
||||
assert(spyCalls == 1, "compute should be called once")
|
||||
|
||||
setA("aa")
|
||||
a("aa")
|
||||
assert(c() == "foo", "c should be 'foo'")
|
||||
assert(spyCalls == 1, "compute should bail out")
|
||||
end)
|
||||
|
|
@ -171,7 +171,7 @@ describe("graph updates", function()
|
|||
-- E
|
||||
-- / \
|
||||
-- F G
|
||||
local a, setA = signal("a")
|
||||
local a = atom("a")
|
||||
|
||||
local b = computed(function()
|
||||
return a()
|
||||
|
|
@ -220,7 +220,7 @@ describe("graph updates", function()
|
|||
fSpyCalls, fSpyTime = 0, 0
|
||||
gSpyCalls, gSpyTime = 0, 0
|
||||
|
||||
setA("b")
|
||||
a("b")
|
||||
|
||||
assert(e() == "b b", "e should be 'b b'")
|
||||
assert(eSpyCalls == 1, "e should run once after update")
|
||||
|
|
@ -235,7 +235,7 @@ describe("graph updates", function()
|
|||
fSpyCalls, fSpyTime = 0, 0
|
||||
gSpyCalls, gSpyTime = 0, 0
|
||||
|
||||
setA("c")
|
||||
a("c")
|
||||
|
||||
assert(e() == "c c", "e should be 'c c'")
|
||||
assert(eSpyCalls == 1, "e should run once after update")
|
||||
|
|
@ -256,7 +256,7 @@ describe("graph updates", function()
|
|||
-- *A
|
||||
-- / \
|
||||
-- *B C <- we don't listen to C
|
||||
local a, setA = signal("a")
|
||||
local a = atom("a")
|
||||
|
||||
local b = computed(function()
|
||||
return a()
|
||||
|
|
@ -271,7 +271,7 @@ describe("graph updates", function()
|
|||
assert(b() == "a", "b should be 'a'")
|
||||
assert(spyCalls == 0, "compute should not run")
|
||||
|
||||
setA("aa")
|
||||
a("aa")
|
||||
assert(b() == "aa", "b should be 'aa'")
|
||||
assert(spyCalls == 0, "compute should not run")
|
||||
end)
|
||||
|
|
@ -285,7 +285,7 @@ describe("graph updates", function()
|
|||
-- *B D <- we don't listen to C
|
||||
-- |
|
||||
-- *C
|
||||
local a, setA = signal("a")
|
||||
local a = atom("a")
|
||||
local spyBCalls = 0
|
||||
local spyB = function()
|
||||
spyBCalls += 1
|
||||
|
|
@ -316,7 +316,7 @@ describe("graph updates", function()
|
|||
spyCCalls = 0
|
||||
unsub()
|
||||
|
||||
setA("aa")
|
||||
a("aa")
|
||||
|
||||
assert(spyBCalls == 0, "b should not run")
|
||||
assert(spyCCalls == 0, "c should not run")
|
||||
|
|
@ -333,7 +333,7 @@ describe("graph updates", function()
|
|||
-- B *C <- returns same value every time
|
||||
-- \ /
|
||||
-- D
|
||||
local a, setA = signal("a")
|
||||
local a = atom("a")
|
||||
local b = computed(function()
|
||||
return a()
|
||||
end)
|
||||
|
|
@ -351,7 +351,7 @@ describe("graph updates", function()
|
|||
assert(d() == "a c", "d should be 'a c'")
|
||||
spyResult = nil
|
||||
|
||||
setA("aa")
|
||||
a("aa")
|
||||
d()
|
||||
assert(spyResult == "aa c", "d should have returned 'aa c'")
|
||||
end)
|
||||
|
|
@ -365,7 +365,7 @@ describe("graph updates", function()
|
|||
-- B *C *D
|
||||
-- \ | /
|
||||
-- E
|
||||
local a, setA = signal("a")
|
||||
local a = atom("a")
|
||||
local b = computed(function()
|
||||
return a()
|
||||
end)
|
||||
|
|
@ -387,13 +387,13 @@ describe("graph updates", function()
|
|||
assert(e() == "a c d", "e should be 'a c d'")
|
||||
spyResult = nil
|
||||
|
||||
setA("aa")
|
||||
a("aa")
|
||||
e()
|
||||
assert(spyResult == "aa c d", "e should have returned 'aa c d'")
|
||||
end)
|
||||
|
||||
test("should support lazy branches", function()
|
||||
local a, setA = signal(0)
|
||||
local a = atom(0)
|
||||
local b = computed(function()
|
||||
return a()
|
||||
end)
|
||||
|
|
@ -402,10 +402,10 @@ describe("graph updates", function()
|
|||
end)
|
||||
|
||||
assert(c() == 0, "c should be 0")
|
||||
setA(1)
|
||||
a(1)
|
||||
assert(c() == 1, "c should be 1")
|
||||
|
||||
setA(0)
|
||||
a(0)
|
||||
assert(c() == 0, "c should be 0 again")
|
||||
end)
|
||||
|
||||
|
|
@ -417,7 +417,7 @@ describe("graph updates", function()
|
|||
-- *B *C
|
||||
-- \ /
|
||||
-- D
|
||||
local a, setA = signal("a")
|
||||
local a = atom("a")
|
||||
local b = computed(function()
|
||||
a()
|
||||
return "b"
|
||||
|
|
@ -436,14 +436,14 @@ describe("graph updates", function()
|
|||
assert(d() == "b c", "d should be 'b c'")
|
||||
spyCalls = 0
|
||||
|
||||
setA("aa")
|
||||
a("aa")
|
||||
assert(spyCalls == 0, "d should not run")
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("error handling", function()
|
||||
test("should keep graph consistent on errors during activation", function()
|
||||
local a, setA = signal(0)
|
||||
local a = atom(0)
|
||||
local b = computed(function()
|
||||
error("fail")
|
||||
end)
|
||||
|
|
@ -457,12 +457,12 @@ describe("error handling", function()
|
|||
assert(not success, "should throw")
|
||||
assert(string.match(result, "fail"), "should throw fail")
|
||||
|
||||
setA(1)
|
||||
a(1)
|
||||
assert(c() == 1, "c should recover and return 1")
|
||||
end)
|
||||
|
||||
test("should keep graph consistent on errors in computeds", function()
|
||||
local a, setA = signal(0)
|
||||
local a = atom(0)
|
||||
local b = computed(function()
|
||||
if a() == 1 then
|
||||
error("fail")
|
||||
|
|
@ -475,14 +475,14 @@ describe("error handling", function()
|
|||
|
||||
assert(c() == 0, "c should be 0")
|
||||
|
||||
setA(1)
|
||||
a(1)
|
||||
local success, result = pcall(function()
|
||||
b()
|
||||
end)
|
||||
assert(not success, "should throw")
|
||||
assert(string.match(result, "fail"), "should throw fail")
|
||||
|
||||
setA(2)
|
||||
a(2)
|
||||
assert(c() == 2, "c should recover and return 2")
|
||||
end)
|
||||
end)
|
||||
|
|
@ -167,21 +167,17 @@ local function createReactiveSystem(props: {
|
|||
end
|
||||
|
||||
local function propagate(link: Link): ()
|
||||
local next = link.nextSub
|
||||
local stack: Stack<Link?>?
|
||||
|
||||
repeat
|
||||
local sub = link.sub
|
||||
|
||||
local flags = sub.flags
|
||||
|
||||
if not bit32.btest(flags, bit32.bor(RECURSED_CHECK, RECURSED, DIRTY, PENDING)) then
|
||||
if bit32.band(flags, bit32.bor(RECURSED_CHECK, RECURSED, DIRTY, PENDING)) == 0 then
|
||||
sub.flags = bit32.bor(flags, PENDING)
|
||||
elseif not bit32.btest(flags, bit32.bor(RECURSED_CHECK, RECURSED)) then
|
||||
elseif bit32.band(flags, bit32.bor(RECURSED_CHECK, RECURSED)) == 0 then
|
||||
flags = NONE
|
||||
elseif not bit32.btest(flags, RECURSED_CHECK) then
|
||||
elseif bit32.band(flags, RECURSED_CHECK) == 0 then
|
||||
sub.flags = bit32.bor(bit32.band(flags, bit32.bnot(RECURSED)), PENDING)
|
||||
elseif not bit32.btest(flags, bit32.bor(DIRTY, PENDING)) and isValidLink(link, sub) then
|
||||
elseif bit32.band(flags, bit32.bor(DIRTY, PENDING)) == 0 and isValidLink(link, sub) then
|
||||
sub.flags = bit32.bor(flags, bit32.bor(RECURSED, PENDING))
|
||||
flags = bit32.band(flags, MUTABLE)
|
||||
else
|
||||
|
|
@ -196,41 +192,12 @@ local function createReactiveSystem(props: {
|
|||
if bit32.btest(flags, MUTABLE) then
|
||||
local subSubs = sub.subs
|
||||
if subSubs then
|
||||
link = subSubs
|
||||
local nextSub = link.nextSub
|
||||
if nextSub then
|
||||
stack = { value = next, prev = stack }
|
||||
next = nextSub
|
||||
end
|
||||
continue
|
||||
propagate(subSubs)
|
||||
end
|
||||
end
|
||||
|
||||
link = next :: Link
|
||||
|
||||
if link then
|
||||
next = link.nextSub
|
||||
continue
|
||||
end
|
||||
|
||||
local continueTop = false
|
||||
|
||||
while stack do
|
||||
link = stack.value :: Link
|
||||
stack = stack.prev
|
||||
if link then
|
||||
next = link.nextSub
|
||||
continueTop = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if continueTop then
|
||||
continue
|
||||
end
|
||||
|
||||
break
|
||||
until false
|
||||
link = link.nextSub :: Link
|
||||
until not link
|
||||
end
|
||||
|
||||
local function shallowPropagate(link: Link): ()
|
||||
|
|
@ -248,81 +215,38 @@ local function createReactiveSystem(props: {
|
|||
end
|
||||
|
||||
local function checkDirty(link: Link, sub: ReactiveNode): boolean
|
||||
local stack: Stack<Link>?
|
||||
local checkDepth = 0
|
||||
local dirty = false
|
||||
|
||||
repeat
|
||||
local dep = link.dep
|
||||
local flags = dep.flags
|
||||
local depFlags = dep.flags
|
||||
|
||||
if bit32.btest(sub.flags, DIRTY) then
|
||||
dirty = true
|
||||
elseif bit32.band(flags, bit32.bor(MUTABLE, DIRTY)) == bit32.bor(MUTABLE, DIRTY) then
|
||||
return true
|
||||
elseif bit32.band(depFlags, bit32.bor(MUTABLE, DIRTY)) == bit32.bor(MUTABLE, DIRTY) then
|
||||
if update(dep) then
|
||||
local subs = dep.subs :: Link
|
||||
if subs.nextSub then
|
||||
shallowPropagate(subs)
|
||||
end
|
||||
dirty = true
|
||||
return true
|
||||
end
|
||||
elseif bit32.band(flags, bit32.bor(MUTABLE, PENDING)) == bit32.bor(MUTABLE, PENDING) then
|
||||
if link.nextSub or link.prevSub then
|
||||
stack = { value = link, prev = stack }
|
||||
end
|
||||
link = dep.deps :: Link
|
||||
sub = dep
|
||||
checkDepth += 1
|
||||
continue
|
||||
end
|
||||
|
||||
if not dirty then
|
||||
local nextDep = link.nextDep
|
||||
if nextDep then
|
||||
link = nextDep
|
||||
continue
|
||||
end
|
||||
end
|
||||
|
||||
local continueTop = false
|
||||
|
||||
while checkDepth ~= 0 do
|
||||
checkDepth -= 1
|
||||
local firstSub = sub.subs :: Link
|
||||
local hasMultipleSubs = firstSub.nextSub ~= nil
|
||||
if hasMultipleSubs then
|
||||
link = (stack :: Stack<Link>).value
|
||||
stack = (stack :: Stack<Link>).prev
|
||||
else
|
||||
link = firstSub
|
||||
end
|
||||
if dirty then
|
||||
if update(sub) then
|
||||
if hasMultipleSubs then
|
||||
shallowPropagate(firstSub)
|
||||
elseif bit32.band(depFlags, bit32.bor(MUTABLE, PENDING)) == bit32.bor(MUTABLE, PENDING) then
|
||||
if checkDirty(dep.deps :: Link, dep) then
|
||||
if update(dep) then
|
||||
local subs = dep.subs :: Link
|
||||
if subs.nextSub then
|
||||
shallowPropagate(subs)
|
||||
end
|
||||
sub = link.sub
|
||||
continue
|
||||
return true
|
||||
end
|
||||
dirty = false
|
||||
else
|
||||
sub.flags = bit32.band(sub.flags, bit32.bnot(PENDING))
|
||||
end
|
||||
sub = link.sub
|
||||
local nextDep = link.nextDep
|
||||
if nextDep then
|
||||
link = nextDep
|
||||
continueTop = true
|
||||
break
|
||||
dep.flags = bit32.band(depFlags, bit32.bnot(PENDING))
|
||||
end
|
||||
end
|
||||
|
||||
if continueTop then
|
||||
continue
|
||||
end
|
||||
link = link.nextDep :: Link
|
||||
until not link
|
||||
|
||||
return dirty
|
||||
until false
|
||||
return false
|
||||
end
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -235,7 +235,7 @@ local function signalOper<T>(
|
|||
local value: T
|
||||
if type(update) == "function" then
|
||||
local prevSub = setActiveSub(nil)
|
||||
local success, result: T = (if globals.strict then noYield else pcall)(update, this.currentValue)
|
||||
local success, result: T = (if globals.strict then noYield else pcall)(update, this.pendingValue)
|
||||
activeSub = prevSub
|
||||
if not success then
|
||||
error(result)
|
||||
|
|
@ -278,9 +278,7 @@ local function signalOper<T>(
|
|||
break
|
||||
end
|
||||
local subs = sub.subs
|
||||
if subs then
|
||||
sub = subs.sub
|
||||
end
|
||||
sub = if subs then subs.sub else nil
|
||||
end
|
||||
return this.currentValue
|
||||
end
|
||||
|
|
@ -434,17 +432,21 @@ end
|
|||
--- For an alternative that still cleans up inner effects, use `peek` instead.
|
||||
---
|
||||
--- @param callback The callback function.
|
||||
--- @param args Arguments to pass to the callback.
|
||||
--- @param ...args Arguments to pass to the callback.
|
||||
--- @return The return value of the callback.
|
||||
--- @see https://github.com/littensy/charm?tab=readme-ov-file#untrackedcallback
|
||||
local function untracked<Args..., Results...>(callback: (Args...) -> Results..., ...: Args...): Results...
|
||||
local prevSub = setActiveSub(nil)
|
||||
local results: { any } = { pcall(callback, ...) }
|
||||
activeSub = prevSub
|
||||
if not results[1] then
|
||||
error(results[2])
|
||||
if activeSub then
|
||||
local prevSub = setActiveSub(nil)
|
||||
local results: { any } = { pcall(callback, ...) }
|
||||
activeSub = prevSub
|
||||
if not results[1] then
|
||||
error(results[2])
|
||||
end
|
||||
return unpack(results, 2)
|
||||
else
|
||||
return callback(...)
|
||||
end
|
||||
return unpack(results, 2)
|
||||
end
|
||||
|
||||
--- Runs the callback without subscribing to signal updates, but still captures
|
||||
|
|
@ -453,7 +455,7 @@ end
|
|||
--- To avoid capturing inner effects, use `untracked` instead.
|
||||
---
|
||||
--- @param callback The callback function.
|
||||
--- @param args Arguments to pass to the callback.
|
||||
--- @param ...args Arguments to pass to the callback.
|
||||
--- @return The return value of the callback.
|
||||
--- @see https://github.com/littensy/charm?tab=readme-ov-file#peekcallback
|
||||
local function peek<Args..., Results...>(callback: (Args...) -> Results..., ...: Args...): Results...
|
||||
|
|
@ -478,10 +480,10 @@ end
|
|||
--- computed signals after the provided callback finishes running.
|
||||
---
|
||||
--- @param callback A function that performs multiple updates.
|
||||
--- @param args Arguments to pass to the callback.
|
||||
--- @param ...args Arguments to pass to the callback.
|
||||
--- @return The return value of the callback.
|
||||
--- @see https://github.com/littensy/charm?tab=readme-ov-file#batchedcallback
|
||||
local function batched<Args..., Results...>(callback: (Args...) -> Results..., ...: Args...): Results...
|
||||
--- @see https://github.com/littensy/charm?tab=readme-ov-file#batchcallback
|
||||
local function batch<Args..., Results...>(callback: (Args...) -> Results..., ...: Args...): Results...
|
||||
startBatch()
|
||||
local results: { any } = { (if globals.strict then noYield else pcall)(callback, ...) }
|
||||
endBatch()
|
||||
|
|
@ -497,7 +499,7 @@ end
|
|||
---
|
||||
--- @param getter A function that returns the value to track.
|
||||
--- @return A computed signal that tracks the value returned by the getter.
|
||||
local function toComputed<T>(getter: () -> T): () -> T
|
||||
local function toSignal<T>(getter: () -> T): () -> T
|
||||
local name = debug.info(getter, "n")
|
||||
if name == "signalGetter" or name == "computedGetter" or name == "atomGetterSetter" then
|
||||
return getter
|
||||
|
|
@ -519,7 +521,7 @@ end
|
|||
--- @return A function for disposing the effect.
|
||||
--- @see https://github.com/littensy/charm?tab=readme-ov-file#listengetter-callback
|
||||
local function listen<T>(getter: () -> T, callback: (state: T, previous: T?) -> ()): () -> ()
|
||||
local tracked = toComputed(getter)
|
||||
local tracked = toSignal(getter)
|
||||
local current: T
|
||||
return effect(function()
|
||||
local previous = current
|
||||
|
|
@ -540,15 +542,12 @@ end
|
|||
--- @return A function for disposing the effect.
|
||||
--- @see https://github.com/littensy/charm?tab=readme-ov-file#subscribegetter-callback
|
||||
local function subscribe<T>(getter: () -> T, callback: (state: T, lastState: T) -> ()): () -> ()
|
||||
local tracked = toComputed(getter)
|
||||
local current: T
|
||||
local mounted = true
|
||||
local tracked = toSignal(getter)
|
||||
local current = tracked()
|
||||
return effect(function()
|
||||
local previous = current
|
||||
current = tracked()
|
||||
if mounted then
|
||||
mounted = false
|
||||
else
|
||||
if current ~= previous then
|
||||
peek(callback, current, previous)
|
||||
end
|
||||
end)
|
||||
|
|
@ -578,12 +577,14 @@ local reactiveSystem = createReactiveSystem({
|
|||
|
||||
queuedLength = insertIndex
|
||||
|
||||
-- while (firstInsertedIndex < --insertIndex)
|
||||
insertIndex -= 1
|
||||
while firstInsertedIndex < insertIndex do
|
||||
local left = queued[firstInsertedIndex]
|
||||
queued[firstInsertedIndex] = queued[insertIndex]
|
||||
queued[insertIndex] = left
|
||||
firstInsertedIndex += 1
|
||||
-- Decrement before next iteration to simulate --insertIndex
|
||||
insertIndex -= 1
|
||||
end
|
||||
end,
|
||||
|
|
@ -620,7 +621,7 @@ return {
|
|||
onCleanup = onCleanup,
|
||||
untracked = untracked,
|
||||
peek = peek,
|
||||
batched = batched,
|
||||
batch = batch,
|
||||
subscribe = subscribe,
|
||||
listen = listen,
|
||||
}
|
||||
|
|
|
|||
4
packages/charm/src/index.d.ts
vendored
4
packages/charm/src/index.d.ts
vendored
|
|
@ -149,9 +149,9 @@ export function peek<Args extends any[], Result>(callback: (...args: Args) => Re
|
|||
* @param callback A function that performs multiple updates.
|
||||
* @param args Arguments to pass to the callback.
|
||||
* @returns The return value of the callback.
|
||||
* @see https://github.com/littensy/charm?tab=readme-ov-file#batchedcallback
|
||||
* @see https://github.com/littensy/charm?tab=readme-ov-file#batchcallback
|
||||
*/
|
||||
export function batched<Args extends any[], Result>(callback: (...args: Args) => Result, ...args: Args): Result;
|
||||
export function batch<Args extends any[], Result>(callback: (...args: Args) => Result, ...args: Args): Result;
|
||||
|
||||
/**
|
||||
* Creates an effect that runs the callback one immediately, and then again
|
||||
|
|
|
|||
|
|
@ -11,11 +11,22 @@ type Update<T> = ((previous: T) -> T) | T
|
|||
--- its current value.
|
||||
export type Atom<T = any> = (...Update<T>) -> T
|
||||
|
||||
--- A getter-only reactive signal. Calling the signal with no arguments returns
|
||||
--- its current value. It cannot be set directly.
|
||||
export type Getter<T = any> = () -> T
|
||||
|
||||
--- A setter-only reactive signal. Calling the signal with an argument sets its
|
||||
--- value. It cannot be read directly.
|
||||
export type Setter<T = any> = (...Update<T>) -> ()
|
||||
|
||||
export type ReactiveNode = core.ReactiveNode
|
||||
export type Link = core.Link
|
||||
|
||||
return {
|
||||
core = core,
|
||||
globals = globals,
|
||||
atom = core.atom,
|
||||
batched = core.batched,
|
||||
batch = core.batch,
|
||||
computed = core.computed,
|
||||
effect = core.effect,
|
||||
effectScope = core.effectScope,
|
||||
|
|
|
|||
|
|
@ -1,70 +1,66 @@
|
|||
local core = require("./core")
|
||||
local globals = require("./globals")
|
||||
|
||||
local batched = core.batched
|
||||
type ReactiveNode = core.ReactiveNode
|
||||
|
||||
local batch = core.batch
|
||||
local effect = core.effect
|
||||
local effectScope = core.effectScope
|
||||
local onCleanup = core.onCleanup
|
||||
|
||||
--- Calls the callback function for each unique key in the table returned by the
|
||||
--- getter. The callback is called once for each key, and again when a new key
|
||||
--- is added.
|
||||
---
|
||||
--- The callback may return a cleanup function, which will be called when the
|
||||
--- key is removed or when the entire callback is disposed.
|
||||
---
|
||||
--- Effects created within the callback are cleaned up when the item is removed
|
||||
--- from the table or when the callback is disposed.
|
||||
--- key is removed or the `observe()` effect is disposed. Effects created
|
||||
--- during the callback will also be bound to the lifetime of the key.
|
||||
---
|
||||
--- @param getter A function that returns the table to observe.
|
||||
--- @param callback A function that is called for each key in the table.
|
||||
--- @return A function for disposing the observer.
|
||||
--- @see https://github.com/littensy/charm?tab=readme-ov-file#observegetter-callback
|
||||
local function observe<K, V>(getter: () -> { [K]: V }, callback: (item: V, key: K) -> ...(() -> ())?): () -> ()
|
||||
local scopes: { [K]: () -> () } = {}
|
||||
local currentItems: { [K]: V } = {}
|
||||
local innerScopes: { [K]: () -> () } = {}
|
||||
|
||||
local function updateScopes(currentItems: { [K]: V }, previousItems: { [K]: V })
|
||||
for key in previousItems do
|
||||
local dispose = scopes[key]
|
||||
if currentItems[key] == nil and dispose then
|
||||
local function updateScopes(data: { [K]: V })
|
||||
for key, dispose in innerScopes do
|
||||
if data[key] == nil then
|
||||
innerScopes[key] = nil
|
||||
dispose()
|
||||
scopes[key] = nil
|
||||
end
|
||||
end
|
||||
|
||||
for key, value in currentItems do
|
||||
if previousItems[key] == nil and scopes[key] == nil then
|
||||
scopes[key] = effectScope(function()
|
||||
for key, value in data do
|
||||
if innerScopes[key] == nil then
|
||||
-- Detach the inner scope from the parent effect to prevent
|
||||
-- disposing when the effect re-runs.
|
||||
innerScopes[key] = effectScope(function()
|
||||
return callback(value, key)
|
||||
end, true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function disposeScopes()
|
||||
for _, dispose in scopes do
|
||||
dispose()
|
||||
end
|
||||
table.clear(scopes)
|
||||
end
|
||||
return effectScope(function()
|
||||
effect(function()
|
||||
-- Batch so that side effects run after all scopes are created. This
|
||||
-- ensures that a scope creating or removing items doesn't interfere
|
||||
-- with the current update cycle.
|
||||
batch(updateScopes, getter())
|
||||
|
||||
local disposeEffect = effect(function()
|
||||
local previousItems = currentItems
|
||||
currentItems = getter()
|
||||
if currentItems ~= previousItems then
|
||||
batched(updateScopes, currentItems, previousItems)
|
||||
-- Effects can't trigger themselves more than once, so if a scope
|
||||
-- creates or removes items, we need to check for updates again. The
|
||||
-- way we do this abuses a weird property of signal accesses that
|
||||
-- causes the effect to re-run.
|
||||
getter()
|
||||
end)
|
||||
|
||||
return function()
|
||||
for _, dispose in innerScopes do
|
||||
dispose()
|
||||
end
|
||||
table.clear(innerScopes)
|
||||
end
|
||||
end)
|
||||
|
||||
if globals.trackInnerEffects then
|
||||
onCleanup(disposeScopes, true)
|
||||
end
|
||||
|
||||
return function()
|
||||
disposeEffect()
|
||||
disposeScopes()
|
||||
end
|
||||
end
|
||||
|
||||
return observe
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ local function noYield<Args..., Results...>(callback: (Args...) -> Results..., .
|
|||
end
|
||||
|
||||
local function deepFreeze(object: any)
|
||||
if type(object) == "table" and not table.isfrozen(object) then
|
||||
if type(object) == "table" and not table.isfrozen(object) and getmetatable(object) == nil then
|
||||
table.freeze(object)
|
||||
for _, value in object do
|
||||
deepFreeze(value)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue