mirror of
https://github.com/littensy/charm.git
synced 2026-10-20 17:51:37 +00:00
refactor: align with alien-signals 3.0
This commit is contained in:
parent
7a63993293
commit
bf92413a97
5 changed files with 254 additions and 279 deletions
|
|
@ -5,7 +5,7 @@ local computed = core.computed
|
||||||
local effect = core.effect
|
local effect = core.effect
|
||||||
local effectScope = core.effectScope
|
local effectScope = core.effectScope
|
||||||
local endBatch = core.endBatch
|
local endBatch = core.endBatch
|
||||||
local setCurrentSub = core.setCurrentSub
|
local setActiveSub = core.setActiveSub
|
||||||
local onCleanup = core.onCleanup
|
local onCleanup = core.onCleanup
|
||||||
local atom = core.atom
|
local atom = core.atom
|
||||||
local startBatch = core.startBatch
|
local startBatch = core.startBatch
|
||||||
|
|
@ -202,9 +202,9 @@ test("should duplicate subscribers do not affect the notify order", function()
|
||||||
|
|
||||||
effect(function()
|
effect(function()
|
||||||
table.insert(order, "a")
|
table.insert(order, "a")
|
||||||
local currentSub = setCurrentSub(nil)
|
local currentSub = setActiveSub(nil)
|
||||||
local isOne = src2() == 1
|
local isOne = src2() == 1
|
||||||
setCurrentSub(currentSub)
|
setActiveSub(currentSub)
|
||||||
if isOne then
|
if isOne then
|
||||||
src1()
|
src1()
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,3 @@
|
||||||
-- Adapted from Alien Signals
|
|
||||||
-- https://github.com/stackblitz/alien-signals
|
|
||||||
-- https://github.com/Nicell/alien-signals-luau
|
|
||||||
|
|
||||||
export type ReactiveNode = {
|
export type ReactiveNode = {
|
||||||
deps: Link?,
|
deps: Link?,
|
||||||
depsTail: Link?,
|
depsTail: Link?,
|
||||||
|
|
@ -34,7 +30,7 @@ local RECURSED_CHECK = 0b00000100
|
||||||
local RECURSED = 0b00001000
|
local RECURSED = 0b00001000
|
||||||
local DIRTY = 0b00010000
|
local DIRTY = 0b00010000
|
||||||
local PENDING = 0b00100000
|
local PENDING = 0b00100000
|
||||||
local UNTRACKED = 0b10000000
|
local PEEKING = 0b10000000
|
||||||
|
|
||||||
local ReactiveFlags = {
|
local ReactiveFlags = {
|
||||||
None = NONE,
|
None = NONE,
|
||||||
|
|
@ -44,7 +40,6 @@ local ReactiveFlags = {
|
||||||
Recursed = RECURSED,
|
Recursed = RECURSED,
|
||||||
Dirty = DIRTY,
|
Dirty = DIRTY,
|
||||||
Pending = PENDING,
|
Pending = PENDING,
|
||||||
Untracked = UNTRACKED,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
local function createReactiveSystem(props: {
|
local function createReactiveSystem(props: {
|
||||||
|
|
@ -55,7 +50,6 @@ local function createReactiveSystem(props: {
|
||||||
local update = props.update
|
local update = props.update
|
||||||
local notify = props.notify
|
local notify = props.notify
|
||||||
local unwatched = props.unwatched
|
local unwatched = props.unwatched
|
||||||
local currentVersion = 0
|
|
||||||
|
|
||||||
local function isValidLink(checkLink: Link, sub: ReactiveNode): boolean
|
local function isValidLink(checkLink: Link, sub: ReactiveNode): boolean
|
||||||
local link = sub.depsTail
|
local link = sub.depsTail
|
||||||
|
|
@ -68,23 +62,27 @@ local function createReactiveSystem(props: {
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
local function link(dep: ReactiveNode, sub: ReactiveNode): ()
|
local function link(dep: ReactiveNode, sub: ReactiveNode, version: number): ()
|
||||||
|
-- DEVIATION: Do not link signals to sub while peeking
|
||||||
|
if bit32.btest(sub.flags, PEEKING) and bit32.btest(dep.flags, MUTABLE) then
|
||||||
|
return
|
||||||
|
end
|
||||||
local prevDep = sub.depsTail
|
local prevDep = sub.depsTail
|
||||||
if prevDep and prevDep.dep == dep then
|
if prevDep and prevDep.dep == dep then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
local nextDep = if prevDep then prevDep.nextDep else sub.deps
|
local nextDep = if prevDep then prevDep.nextDep else sub.deps
|
||||||
if nextDep and nextDep.dep == dep then
|
if nextDep and nextDep.dep == dep then
|
||||||
nextDep.version = currentVersion
|
nextDep.version = version
|
||||||
sub.depsTail = nextDep
|
sub.depsTail = nextDep
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
local prevSub = dep.subsTail
|
local prevSub = dep.subsTail
|
||||||
if prevSub and prevSub.version == currentVersion and prevSub.sub == sub then
|
if prevSub and prevSub.version == version and prevSub.sub == sub then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
local newLink: Link = {
|
local newLink: Link = {
|
||||||
version = currentVersion,
|
version = version,
|
||||||
dep = dep,
|
dep = dep,
|
||||||
sub = sub,
|
sub = sub,
|
||||||
prevDep = prevDep,
|
prevDep = prevDep,
|
||||||
|
|
@ -208,21 +206,6 @@ local function createReactiveSystem(props: {
|
||||||
until false
|
until false
|
||||||
end
|
end
|
||||||
|
|
||||||
local function startTracking(sub: ReactiveNode): ()
|
|
||||||
currentVersion += 1
|
|
||||||
sub.depsTail = nil
|
|
||||||
sub.flags = bit32.bor(bit32.band(sub.flags, bit32.bnot(bit32.bor(RECURSED, DIRTY, PENDING))), RECURSED_CHECK)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function endTracking(sub: ReactiveNode): ()
|
|
||||||
local depsTail = sub.depsTail
|
|
||||||
local toRemove = if depsTail then depsTail.nextDep else sub.deps
|
|
||||||
while toRemove do
|
|
||||||
toRemove = unlink(toRemove, sub)
|
|
||||||
end
|
|
||||||
sub.flags = bit32.band(sub.flags, bit32.bnot(RECURSED_CHECK))
|
|
||||||
end
|
|
||||||
|
|
||||||
local function shallowPropagate(link: Link): ()
|
local function shallowPropagate(link: Link): ()
|
||||||
repeat
|
repeat
|
||||||
local sub = link.sub
|
local sub = link.sub
|
||||||
|
|
@ -320,8 +303,6 @@ local function createReactiveSystem(props: {
|
||||||
unlink = unlink,
|
unlink = unlink,
|
||||||
propagate = propagate,
|
propagate = propagate,
|
||||||
checkDirty = checkDirty,
|
checkDirty = checkDirty,
|
||||||
endTracking = endTracking,
|
|
||||||
startTracking = startTracking,
|
|
||||||
shallowPropagate = shallowPropagate,
|
shallowPropagate = shallowPropagate,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,3 @@
|
||||||
-- Adapted from Alien Signals
|
|
||||||
-- https://github.com/stackblitz/alien-signals
|
|
||||||
-- https://github.com/Nicell/alien-signals-luau
|
|
||||||
|
|
||||||
local alien = require("./alien")
|
local alien = require("./alien")
|
||||||
local config = require("./flags")
|
local config = require("./flags")
|
||||||
local utils = require("./utils")
|
local utils = require("./utils")
|
||||||
|
|
@ -15,22 +11,23 @@ local createReactiveSystem = alien.createReactiveSystem
|
||||||
local deepFreeze = utils.deepFreeze
|
local deepFreeze = utils.deepFreeze
|
||||||
local noYield = utils.noYield
|
local noYield = utils.noYield
|
||||||
|
|
||||||
local NONE = 0b0000000
|
local NONE = 0b00000000
|
||||||
local MUTABLE = 0b0000001
|
local MUTABLE = 0b00000001
|
||||||
local WATCHING = 0b0000010
|
local WATCHING = 0b00000010
|
||||||
local DIRTY = 0b0010000
|
local RECURSED_CHECK = 0b00000100
|
||||||
local PENDING = 0b0100000
|
local DIRTY = 0b00010000
|
||||||
local QUEUED = 0b1000000
|
local PENDING = 0b00100000
|
||||||
local UNTRACKED = 0b10000000
|
local QUEUED = 0b01000000
|
||||||
|
local PEEKING = 0b10000000
|
||||||
|
|
||||||
type EffectScope = ReactiveNode & {
|
type EffectScope = ReactiveNode & {
|
||||||
cleanups: { () -> () },
|
cleanups: { () -> () },
|
||||||
}
|
}
|
||||||
|
|
||||||
type Effect = ReactiveNode & {
|
type Effect = ReactiveNode & {
|
||||||
fn: (dispose: () -> ()) -> ...(() -> ())?,
|
fn: (stop: () -> ()) -> (() -> ())?,
|
||||||
cleanups: { () -> () },
|
cleanups: { () -> () },
|
||||||
cleanup: () -> (),
|
stop: () -> (),
|
||||||
}
|
}
|
||||||
|
|
||||||
type Computed<T = any> = ReactiveNode & {
|
type Computed<T = any> = ReactiveNode & {
|
||||||
|
|
@ -38,54 +35,47 @@ type Computed<T = any> = ReactiveNode & {
|
||||||
getter: (previousValue: T?) -> T,
|
getter: (previousValue: T?) -> T,
|
||||||
}
|
}
|
||||||
|
|
||||||
type Atom<T = any> = ReactiveNode & {
|
type Signal<T = any> = ReactiveNode & {
|
||||||
previousValue: T,
|
currentValue: T,
|
||||||
value: T,
|
pendingValue: T,
|
||||||
equals: ((T, T) -> boolean)?,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
local link: (dep: ReactiveNode, sub: ReactiveNode) -> ()
|
local link: (dep: ReactiveNode, sub: ReactiveNode, version: number) -> ()
|
||||||
local unlink: (link: Link, sub: ReactiveNode?) -> Link?
|
local unlink: (link: Link, sub: ReactiveNode?) -> Link?
|
||||||
local propagate: (link: Link) -> ()
|
local propagate: (link: Link) -> ()
|
||||||
local checkDirty: (link: Link, sub: ReactiveNode) -> boolean
|
local checkDirty: (link: Link, sub: ReactiveNode) -> boolean
|
||||||
local endTracking: (sub: ReactiveNode) -> ()
|
|
||||||
local startTracking: (sub: ReactiveNode) -> ()
|
|
||||||
local shallowPropagate: (link: Link) -> ()
|
local shallowPropagate: (link: Link) -> ()
|
||||||
|
|
||||||
|
local cycle = 0
|
||||||
local batchDepth = 0
|
local batchDepth = 0
|
||||||
|
|
||||||
local queuedEffects: { Effect } = {}
|
|
||||||
local queuedEffectsLength = 0
|
|
||||||
local notifyIndex = 0
|
local notifyIndex = 0
|
||||||
|
local queuedEffects: { Effect | EffectScope? } = {}
|
||||||
local activeScope: Effect | EffectScope?
|
local queuedEffectsLength = 0
|
||||||
local activeSub: ReactiveNode?
|
local activeSub: ReactiveNode?
|
||||||
|
|
||||||
local function getCurrentSub(): ReactiveNode?
|
local function getActiveSub(): ReactiveNode?
|
||||||
return activeSub
|
return activeSub
|
||||||
end
|
end
|
||||||
|
|
||||||
local function setCurrentSub(sub: ReactiveNode?)
|
local function setActiveSub(sub: ReactiveNode?)
|
||||||
local prevSub = activeSub
|
local prevSub = activeSub
|
||||||
activeSub = sub
|
activeSub = sub
|
||||||
return prevSub
|
return prevSub
|
||||||
end
|
end
|
||||||
|
|
||||||
local function getCurrentScope(): Effect | EffectScope?
|
local function purgeDeps(sub: ReactiveNode)
|
||||||
return activeScope
|
local depsTail = sub.depsTail
|
||||||
end
|
local dep = if depsTail then depsTail.nextDep else sub.deps
|
||||||
|
while dep do
|
||||||
local function setCurrentScope(scope: Effect | EffectScope?)
|
dep = unlink(dep, sub)
|
||||||
local prevScope = activeScope
|
end
|
||||||
activeScope = scope
|
|
||||||
return prevScope
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function runCleanups(effect: EffectScope)
|
local function runCleanups(effect: EffectScope)
|
||||||
for _, cleanup in effect.cleanups do
|
for _, cleanup in effect.cleanups do
|
||||||
local prev = setCurrentSub(nil)
|
local prevSub = setActiveSub(nil)
|
||||||
local success, result: any = (if config.strict then noYield else pcall)(cleanup)
|
local success, result: any = (if config.strict then noYield else pcall)(cleanup)
|
||||||
setCurrentSub(prev)
|
activeSub = prevSub
|
||||||
if not success then
|
if not success then
|
||||||
error(result)
|
error(result)
|
||||||
end
|
end
|
||||||
|
|
@ -93,40 +83,49 @@ local function runCleanups(effect: EffectScope)
|
||||||
table.clear(effect.cleanups)
|
table.clear(effect.cleanups)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function run(effect: Effect, flags: ReactiveFlags): ()
|
local function setNotPending(sub: ReactiveNode, flags: ReactiveFlags)
|
||||||
if bit32.btest(flags, DIRTY) or (bit32.btest(flags, PENDING) and checkDirty(effect.deps :: Link, effect)) then
|
sub.flags = bit32.band(flags, bit32.bnot(PENDING))
|
||||||
runCleanups(effect)
|
end
|
||||||
local prev = setCurrentSub(effect)
|
|
||||||
startTracking(effect)
|
local function run(e: Effect | EffectScope, flags: ReactiveFlags): ()
|
||||||
local success, result: (() -> ())? = (if config.strict then noYield else pcall)(effect.fn, effect.cleanup)
|
if
|
||||||
setCurrentSub(prev)
|
bit32.btest(flags, DIRTY)
|
||||||
endTracking(effect)
|
or (bit32.btest(flags, PENDING) and (checkDirty(e.deps :: Link, e) or setNotPending(e, flags)))
|
||||||
|
then
|
||||||
|
runCleanups(e)
|
||||||
|
local e = e :: Effect
|
||||||
|
cycle += 1
|
||||||
|
e.depsTail = nil
|
||||||
|
e.flags = bit32.bor(WATCHING, RECURSED_CHECK)
|
||||||
|
local prevSub = setActiveSub(e)
|
||||||
|
local success, result: (() -> ())? = (if config.strict then noYield else pcall)(e.fn, e.stop)
|
||||||
|
activeSub = prevSub
|
||||||
|
e.flags = bit32.band(e.flags, bit32.bnot(RECURSED_CHECK))
|
||||||
|
purgeDeps(e)
|
||||||
if not success then
|
if not success then
|
||||||
error(result)
|
error(result)
|
||||||
elseif result then
|
elseif result then
|
||||||
table.insert(effect.cleanups, result)
|
table.insert(e.cleanups, result)
|
||||||
end
|
end
|
||||||
return
|
else
|
||||||
elseif bit32.btest(flags, PENDING) then
|
local link = e.deps
|
||||||
effect.flags = bit32.band(flags, bit32.bnot(PENDING))
|
while link do
|
||||||
end
|
local dep = link.dep
|
||||||
local link = effect.deps
|
local depFlags = dep.flags
|
||||||
while link do
|
if bit32.btest(depFlags, QUEUED) then
|
||||||
local dep = link.dep
|
depFlags = bit32.band(depFlags, bit32.bnot(QUEUED))
|
||||||
local depFlags = dep.flags
|
dep.flags = depFlags
|
||||||
if bit32.btest(depFlags, QUEUED) then
|
run(dep :: Effect, depFlags)
|
||||||
depFlags = bit32.band(depFlags, bit32.bnot(QUEUED))
|
end
|
||||||
dep.flags = depFlags
|
link = link.nextDep
|
||||||
run(dep :: Effect, depFlags)
|
|
||||||
end
|
end
|
||||||
link = link.nextDep
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function flush(): ()
|
local function flush(): ()
|
||||||
while notifyIndex < queuedEffectsLength do
|
while notifyIndex < queuedEffectsLength do
|
||||||
notifyIndex += 1
|
notifyIndex += 1
|
||||||
local effect = queuedEffects[notifyIndex]
|
local effect = queuedEffects[notifyIndex] :: Effect
|
||||||
queuedEffects[notifyIndex] = nil
|
queuedEffects[notifyIndex] = nil
|
||||||
effect.flags = bit32.band(effect.flags, bit32.bnot(QUEUED))
|
effect.flags = bit32.band(effect.flags, bit32.bnot(QUEUED))
|
||||||
run(effect, effect.flags)
|
run(effect, effect.flags)
|
||||||
|
|
@ -150,165 +149,136 @@ local function endBatch()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function updateComputed(computed: Computed): boolean
|
local function updateComputed(c: Computed): boolean
|
||||||
local prevSub = setCurrentSub(computed)
|
cycle += 1
|
||||||
startTracking(computed)
|
c.depsTail = nil
|
||||||
local oldValue = computed.value
|
c.flags = bit32.bor(MUTABLE, RECURSED_CHECK)
|
||||||
local success, result = (if config.strict then noYield else pcall)(computed.getter, oldValue)
|
local prevSub = setActiveSub(c)
|
||||||
setCurrentSub(prevSub)
|
local oldValue = c.value
|
||||||
endTracking(computed)
|
local success, result: unknown = (if config.strict then noYield else pcall)(c.getter, oldValue)
|
||||||
|
activeSub = prevSub
|
||||||
|
c.flags = bit32.band(c.flags, bit32.bnot(RECURSED_CHECK))
|
||||||
|
purgeDeps(c)
|
||||||
if not success then
|
if not success then
|
||||||
error(result)
|
error(result)
|
||||||
end
|
end
|
||||||
if oldValue ~= result then
|
if oldValue ~= result then
|
||||||
computed.value = result
|
|
||||||
return true
|
|
||||||
else
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function updateAtom(atom: Atom, value: any): boolean
|
|
||||||
atom.flags = MUTABLE
|
|
||||||
if atom.previousValue ~= value then
|
|
||||||
atom.previousValue = value
|
|
||||||
return true
|
|
||||||
else
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function readAtom<T>(atom: Atom<T>): T
|
|
||||||
local value = atom.value
|
|
||||||
if bit32.btest(atom.flags, DIRTY) then
|
|
||||||
if updateAtom(atom, value) then
|
|
||||||
local subs = atom.subs
|
|
||||||
if subs then
|
|
||||||
shallowPropagate(subs)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if activeSub and not bit32.btest(activeSub.flags, UNTRACKED) then
|
|
||||||
link(atom, activeSub)
|
|
||||||
end
|
|
||||||
return value
|
|
||||||
end
|
|
||||||
|
|
||||||
local function writeAtom<T>(atom: Atom<T>, oldValue: T, newValue: T): T
|
|
||||||
local equals = atom.equals
|
|
||||||
if if equals then not equals(oldValue, newValue) else oldValue ~= newValue then
|
|
||||||
if config.frozen then
|
if config.frozen then
|
||||||
deepFreeze(newValue)
|
deepFreeze(result)
|
||||||
end
|
end
|
||||||
atom.value = newValue
|
c.value = result
|
||||||
atom.flags = bit32.bor(MUTABLE, DIRTY)
|
return true
|
||||||
local subs = atom.subs
|
else
|
||||||
if subs then
|
return false
|
||||||
propagate(subs)
|
|
||||||
if batchDepth == 0 then
|
|
||||||
flush()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return newValue
|
|
||||||
end
|
|
||||||
return oldValue
|
|
||||||
end
|
|
||||||
|
|
||||||
local function atomGetterSetter<T>(atom: Atom<T>): (...((T) -> T) | T) -> T
|
|
||||||
return function(...)
|
|
||||||
if select("#", ...) == 0 then
|
|
||||||
return readAtom(atom)
|
|
||||||
end
|
|
||||||
local oldValue = atom.value
|
|
||||||
local newValue = ...
|
|
||||||
if type(newValue) == "function" then
|
|
||||||
local prev = setCurrentSub(nil)
|
|
||||||
local success, result = (if config.strict then noYield else pcall)(newValue, oldValue)
|
|
||||||
setCurrentSub(prev)
|
|
||||||
if not success then
|
|
||||||
error(result)
|
|
||||||
end
|
|
||||||
newValue = result
|
|
||||||
end
|
|
||||||
return writeAtom(atom, oldValue, newValue :: T)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function computedGetter<T>(computed: Computed<T>): () -> T
|
local function updateSignal(s: Signal): boolean
|
||||||
return function()
|
s.flags = MUTABLE
|
||||||
local flags = computed.flags
|
local pendingValue = s.pendingValue
|
||||||
|
if s.currentValue ~= pendingValue then
|
||||||
|
s.currentValue = pendingValue
|
||||||
|
return true
|
||||||
|
else
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function computedOper<T>(this: Computed<T>): () -> T
|
||||||
|
return function(): T
|
||||||
|
local flags = this.flags
|
||||||
if
|
if
|
||||||
bit32.btest(flags, DIRTY) or (bit32.btest(flags, PENDING) and checkDirty(computed.deps :: Link, computed))
|
bit32.btest(flags, DIRTY)
|
||||||
|
or (bit32.btest(flags, PENDING) and (checkDirty(this.deps :: Link, this) or setNotPending(this, flags)))
|
||||||
then
|
then
|
||||||
if updateComputed(computed) then
|
if updateComputed(this) then
|
||||||
local subs = computed.subs
|
local subs = this.subs
|
||||||
if subs then
|
if subs then
|
||||||
shallowPropagate(subs)
|
shallowPropagate(subs)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
elseif bit32.btest(flags, PENDING) then
|
|
||||||
computed.flags = bit32.band(flags, bit32.bnot(PENDING))
|
|
||||||
end
|
end
|
||||||
if activeSub and not bit32.btest(activeSub.flags, UNTRACKED) then
|
if activeSub then
|
||||||
link(computed, activeSub)
|
link(this, activeSub, cycle)
|
||||||
elseif activeScope then
|
|
||||||
link(computed, activeScope)
|
|
||||||
end
|
end
|
||||||
return computed.value :: T
|
return this.value :: T
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function effectStop(self: EffectScope): () -> ()
|
local function signalOper<T>(this: Signal<T>, equals: ((current: T, incoming: T) -> boolean)?): (...((T) -> T) | T) -> T
|
||||||
return function()
|
return function(...: ((T) -> T) | T): T
|
||||||
local dep = self.deps
|
if select("#", ...) ~= 0 then
|
||||||
while dep do
|
local update = ...
|
||||||
dep = unlink(dep, self)
|
local value: T
|
||||||
end
|
if type(update) == "function" then
|
||||||
local sub = self.subs
|
local prevSub = setActiveSub(nil)
|
||||||
if sub then
|
local success, result: T = (if config.strict then noYield else pcall)(update, this.currentValue)
|
||||||
unlink(sub)
|
activeSub = prevSub
|
||||||
end
|
if not success then
|
||||||
self.flags = NONE
|
error(result)
|
||||||
runCleanups(self)
|
end
|
||||||
end
|
value = result
|
||||||
end
|
else
|
||||||
|
value = update
|
||||||
local function update(atom: ReactiveNode): boolean
|
end
|
||||||
if (atom :: Computed).getter then
|
if if equals then not equals(this.pendingValue, value) else this.pendingValue ~= value then
|
||||||
return updateComputed(atom :: Computed)
|
if config.frozen then
|
||||||
else
|
deepFreeze(value)
|
||||||
return updateAtom(atom :: Atom, (atom :: Atom).value)
|
end
|
||||||
end
|
this.pendingValue = value
|
||||||
end
|
this.flags = bit32.bor(MUTABLE, DIRTY)
|
||||||
|
local subs = this.subs
|
||||||
local function notify(effect: ReactiveNode)
|
if subs then
|
||||||
local flags = effect.flags
|
propagate(subs)
|
||||||
if not bit32.btest(flags, QUEUED) then
|
if batchDepth == 0 then
|
||||||
effect.flags = bit32.bor(flags, QUEUED)
|
flush()
|
||||||
local subs = effect.subs
|
end
|
||||||
if subs then
|
end
|
||||||
notify(subs.sub :: Effect)
|
return value
|
||||||
|
end
|
||||||
|
return this.currentValue
|
||||||
else
|
else
|
||||||
queuedEffectsLength += 1
|
if bit32.btest(this.flags, DIRTY) then
|
||||||
queuedEffects[queuedEffectsLength] = effect :: Effect
|
if updateSignal(this) then
|
||||||
|
local subs = this.subs
|
||||||
|
if subs then
|
||||||
|
shallowPropagate(subs)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local sub = activeSub
|
||||||
|
while sub do
|
||||||
|
if bit32.btest(sub.flags, bit32.bor(MUTABLE, WATCHING)) then
|
||||||
|
link(this, sub, cycle)
|
||||||
|
break
|
||||||
|
end
|
||||||
|
local subs = sub.subs
|
||||||
|
if subs then
|
||||||
|
sub = subs.sub
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return this.currentValue
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function unwatched(node: ReactiveNode)
|
local function effectScopeOper(this: EffectScope): () -> ()
|
||||||
if (node :: Computed).getter then
|
return function()
|
||||||
local toRemove = node.deps
|
this.depsTail = nil
|
||||||
if toRemove then
|
this.flags = NONE
|
||||||
node.flags = bit32.bor(MUTABLE, DIRTY)
|
purgeDeps(this)
|
||||||
repeat
|
local sub = this.subs
|
||||||
toRemove = unlink(toRemove, node)
|
if sub then
|
||||||
until not toRemove
|
unlink(sub, this)
|
||||||
end
|
end
|
||||||
elseif (node :: Effect).cleanups then
|
runCleanups(this)
|
||||||
effectStop(node :: Effect)()
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function effectOper(this: Effect): () -> ()
|
||||||
|
return effectScopeOper(this)
|
||||||
|
end
|
||||||
|
|
||||||
--- Creates a reactive atom that acts as both a getter and setter. Calling the
|
--- Creates a reactive atom that acts as both a getter and setter. Calling the
|
||||||
--- atom with an argument sets its value, while calling it with no arguments
|
--- atom with an argument sets its value, while calling it with no arguments
|
||||||
--- returns its current value.
|
--- returns its current value.
|
||||||
|
|
@ -316,16 +286,15 @@ end
|
||||||
--- @param value The initial value of the atom. If empty, the atom's value will be `undefined`.
|
--- @param value The initial value of the atom. If empty, the atom's value will be `undefined`.
|
||||||
--- @param equals A comparator function to determine if the atom's value has changed.
|
--- @param equals A comparator function to determine if the atom's value has changed.
|
||||||
--- @see https://github.com/littensy/charm?tab=readme-ov-file#atomstate-options
|
--- @see https://github.com/littensy/charm?tab=readme-ov-file#atomstate-options
|
||||||
local function atom<T>(value: T, equals: ((T, T) -> boolean)?): (...((T) -> T) | T) -> T
|
local function atom<T>(initialValue: T, equals: ((current: T, incoming: T) -> boolean)?): (...((T) -> T) | T) -> T
|
||||||
if config.frozen then
|
if config.frozen then
|
||||||
deepFreeze(value)
|
deepFreeze(initialValue)
|
||||||
end
|
end
|
||||||
return atomGetterSetter({
|
return signalOper({
|
||||||
previousValue = value,
|
currentValue = initialValue,
|
||||||
value = value,
|
pendingValue = initialValue,
|
||||||
flags = MUTABLE,
|
flags = MUTABLE,
|
||||||
equals = equals,
|
}, equals)
|
||||||
})
|
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Creates a new atom that is computed based on the values of other atoms.
|
--- Creates a new atom that is computed based on the values of other atoms.
|
||||||
|
|
@ -345,11 +314,10 @@ end
|
||||||
--- @param getter A function that produces the next value.
|
--- @param getter A function that produces the next value.
|
||||||
--- @param value An optional initial value for the selector.
|
--- @param value An optional initial value for the selector.
|
||||||
--- @see https://github.com/littensy/charm?tab=readme-ov-file#computedcallback-options
|
--- @see https://github.com/littensy/charm?tab=readme-ov-file#computedcallback-options
|
||||||
local function computed<T>(getter: (previousValue: T?) -> T, value: T?): () -> T
|
local function computed<T>(getter: (previousValue: T?) -> T): () -> T
|
||||||
return computedGetter({
|
return computedOper({
|
||||||
flags = bit32.bor(MUTABLE, DIRTY),
|
flags = bit32.bor(MUTABLE, DIRTY),
|
||||||
getter = getter,
|
getter = getter,
|
||||||
value = value,
|
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -366,28 +334,32 @@ end
|
||||||
--- @param fn The function to run when dependencies change.
|
--- @param fn The function to run when dependencies change.
|
||||||
--- @return A function for disposing the effect.
|
--- @return A function for disposing the effect.
|
||||||
--- @see https://github.com/littensy/charm?tab=readme-ov-file#effectcallback
|
--- @see https://github.com/littensy/charm?tab=readme-ov-file#effectcallback
|
||||||
local function effect(fn: (dispose: () -> ()) -> ...(() -> ())?): () -> ()
|
local function effect(fn: (stop: () -> ()) -> ()): () -> ()
|
||||||
local self: Effect = {
|
local e: Effect = {
|
||||||
fn = fn,
|
fn = fn,
|
||||||
flags = WATCHING,
|
flags = WATCHING,
|
||||||
cleanups = {},
|
cleanups = {},
|
||||||
cleanup = nil :: any,
|
stop = nil :: any,
|
||||||
}
|
}
|
||||||
self.cleanup = effectStop(self)
|
e.stop = effectOper(e)
|
||||||
if activeSub and config.trackInnerEffects then
|
local prevSub = setActiveSub(e)
|
||||||
link(self, activeSub)
|
if prevSub and (config.trackInnerEffects or prevSub.flags == NONE) then
|
||||||
elseif activeScope then
|
link(e, prevSub, 0)
|
||||||
link(self, activeScope)
|
|
||||||
end
|
end
|
||||||
local prev = setCurrentSub(self)
|
local stopped = false
|
||||||
local success, result: (() -> ())? = (if config.strict then noYield else pcall)(fn, self.cleanup)
|
local success, result: (() -> ())? = (if config.strict then noYield else pcall)(fn, function()
|
||||||
setCurrentSub(prev)
|
stopped = true
|
||||||
|
end)
|
||||||
|
activeSub = prevSub
|
||||||
if not success then
|
if not success then
|
||||||
error(result)
|
error(result)
|
||||||
elseif result then
|
elseif result then
|
||||||
table.insert(self.cleanups, result)
|
table.insert(e.cleanups, result)
|
||||||
end
|
end
|
||||||
return self.cleanup
|
if stopped then
|
||||||
|
e.stop()
|
||||||
|
end
|
||||||
|
return e.stop
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Creates an effect scope that can capture multiple reactive effects (i.e.
|
--- Creates an effect scope that can capture multiple reactive effects (i.e.
|
||||||
|
|
@ -398,25 +370,23 @@ end
|
||||||
--- @param detached If true, the scope will not be automatically disposed of when the parent scope is disposed. Defaults to false.
|
--- @param detached If true, the scope will not be automatically disposed of when the parent scope is disposed. Defaults to false.
|
||||||
--- @return A function for disposing all effects created within the scope.
|
--- @return A function for disposing all effects created within the scope.
|
||||||
--- @see https://github.com/littensy/charm?tab=readme-ov-file#effectscopecallback-detached
|
--- @see https://github.com/littensy/charm?tab=readme-ov-file#effectscopecallback-detached
|
||||||
local function effectScope(fn: () -> ...(() -> ())?, detached: boolean?): () -> ()
|
local function effectScope(fn: () -> (), detached: boolean?): () -> ()
|
||||||
local self: EffectScope = {
|
local e: EffectScope = {
|
||||||
flags = NONE,
|
flags = NONE,
|
||||||
cleanups = {},
|
cleanups = {},
|
||||||
}
|
}
|
||||||
if activeScope and not detached then
|
local prevSub = setActiveSub(e)
|
||||||
link(self, activeScope)
|
if prevSub and not detached then
|
||||||
|
link(e, prevSub, 0)
|
||||||
end
|
end
|
||||||
local prevSub = setCurrentSub(nil)
|
|
||||||
local prevScope = setCurrentScope(self)
|
|
||||||
local success, result: (() -> ())? = (if config.strict then noYield else pcall)(fn)
|
local success, result: (() -> ())? = (if config.strict then noYield else pcall)(fn)
|
||||||
setCurrentScope(prevScope)
|
activeSub = prevSub
|
||||||
setCurrentSub(prevSub)
|
|
||||||
if not success then
|
if not success then
|
||||||
error(result)
|
error(result)
|
||||||
elseif result then
|
elseif result then
|
||||||
table.insert(self.cleanups, result)
|
table.insert(e.cleanups, result)
|
||||||
end
|
end
|
||||||
return effectStop(self)
|
return effectScopeOper(e)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Registers a cleanup function to the current active effect or effect scope.
|
--- Registers a cleanup function to the current active effect or effect scope.
|
||||||
|
|
@ -429,8 +399,6 @@ end
|
||||||
local function onCleanup(cleanup: () -> (), failSilently: boolean?)
|
local function onCleanup(cleanup: () -> (), failSilently: boolean?)
|
||||||
if activeSub and (activeSub :: Effect).cleanups then
|
if activeSub and (activeSub :: Effect).cleanups then
|
||||||
table.insert((activeSub :: Effect).cleanups, cleanup)
|
table.insert((activeSub :: Effect).cleanups, cleanup)
|
||||||
elseif activeScope then
|
|
||||||
table.insert(activeScope.cleanups, cleanup)
|
|
||||||
elseif not failSilently then
|
elseif not failSilently then
|
||||||
warn(debug.traceback("onCleanup() can only be called inside an effect or a scope.", 2))
|
warn(debug.traceback("onCleanup() can only be called inside an effect or a scope.", 2))
|
||||||
end
|
end
|
||||||
|
|
@ -445,11 +413,9 @@ end
|
||||||
--- @param args Arguments to pass to the callback.
|
--- @param args Arguments to pass to the callback.
|
||||||
--- @return The return value of the callback.
|
--- @return The return value of the callback.
|
||||||
local function untracked<Args..., Results...>(callback: (Args...) -> Results..., ...: Args...): Results...
|
local function untracked<Args..., Results...>(callback: (Args...) -> Results..., ...: Args...): Results...
|
||||||
local prevSub = setCurrentSub(nil)
|
local prevSub = setActiveSub(nil)
|
||||||
local prevScope = setCurrentScope(nil)
|
|
||||||
local results: { any } = { pcall(callback, ...) }
|
local results: { any } = { pcall(callback, ...) }
|
||||||
setCurrentSub(prevSub)
|
activeSub = prevSub
|
||||||
setCurrentScope(prevScope)
|
|
||||||
if not results[1] then
|
if not results[1] then
|
||||||
error(results[2])
|
error(results[2])
|
||||||
end
|
end
|
||||||
|
|
@ -465,16 +431,20 @@ end
|
||||||
--- @param args Arguments to pass to the callback.
|
--- @param args Arguments to pass to the callback.
|
||||||
--- @return The return value of the callback.
|
--- @return The return value of the callback.
|
||||||
local function peek<Args..., Results...>(callback: (Args...) -> Results..., ...: Args...): Results...
|
local function peek<Args..., Results...>(callback: (Args...) -> Results..., ...: Args...): Results...
|
||||||
if not activeSub or bit32.btest(activeSub.flags, UNTRACKED) then
|
if
|
||||||
return callback(...)
|
activeSub
|
||||||
else
|
and bit32.band(activeSub.flags, PEEKING) == 0
|
||||||
activeSub.flags = bit32.bor(activeSub.flags, UNTRACKED)
|
and bit32.btest(activeSub.flags, bit32.bor(WATCHING, MUTABLE))
|
||||||
|
then
|
||||||
|
activeSub.flags = bit32.bor(activeSub.flags, PEEKING)
|
||||||
local results: { any } = { pcall(callback, ...) }
|
local results: { any } = { pcall(callback, ...) }
|
||||||
activeSub.flags = bit32.band(activeSub.flags, bit32.bnot(UNTRACKED))
|
activeSub.flags = bit32.band(activeSub.flags, bit32.bnot(PEEKING))
|
||||||
if not results[1] then
|
if not results[1] then
|
||||||
error(results[2])
|
error(results[2])
|
||||||
end
|
end
|
||||||
return unpack(results, 2)
|
return unpack(results, 2)
|
||||||
|
else
|
||||||
|
return callback(...)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -494,28 +464,51 @@ local function batched<Args..., Results...>(callback: (Args...) -> Results..., .
|
||||||
return unpack(results, 2)
|
return unpack(results, 2)
|
||||||
end
|
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({
|
local reactiveSystem = createReactiveSystem({
|
||||||
update = update,
|
update = function(node: ReactiveNode): boolean
|
||||||
|
if node.depsTail then
|
||||||
|
return updateComputed(node :: Computed)
|
||||||
|
else
|
||||||
|
return updateSignal(node :: Signal)
|
||||||
|
end
|
||||||
|
end,
|
||||||
notify = notify,
|
notify = notify,
|
||||||
unwatched = unwatched,
|
unwatched = function(node: ReactiveNode)
|
||||||
|
if bit32.band(node.flags, MUTABLE) == 0 then
|
||||||
|
effectScopeOper(node :: EffectScope)()
|
||||||
|
elseif node.depsTail then
|
||||||
|
node.depsTail = nil
|
||||||
|
node.flags = bit32.bor(MUTABLE, DIRTY)
|
||||||
|
purgeDeps(node)
|
||||||
|
end
|
||||||
|
end,
|
||||||
})
|
})
|
||||||
|
|
||||||
link = reactiveSystem.link
|
link = reactiveSystem.link
|
||||||
unlink = reactiveSystem.unlink
|
unlink = reactiveSystem.unlink
|
||||||
propagate = reactiveSystem.propagate
|
propagate = reactiveSystem.propagate
|
||||||
checkDirty = reactiveSystem.checkDirty
|
checkDirty = reactiveSystem.checkDirty
|
||||||
endTracking = reactiveSystem.endTracking
|
|
||||||
startTracking = reactiveSystem.startTracking
|
|
||||||
shallowPropagate = reactiveSystem.shallowPropagate
|
shallowPropagate = reactiveSystem.shallowPropagate
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ReactiveFlags = ReactiveFlags,
|
ReactiveFlags = ReactiveFlags,
|
||||||
createReactiveSystem = createReactiveSystem,
|
createReactiveSystem = createReactiveSystem,
|
||||||
getCurrentSub = getCurrentSub,
|
getActiveSub = getActiveSub,
|
||||||
setCurrentSub = setCurrentSub,
|
setActiveSub = setActiveSub,
|
||||||
getCurrentScope = getCurrentScope,
|
|
||||||
setCurrentScope = setCurrentScope,
|
|
||||||
onCleanup = onCleanup,
|
|
||||||
getBatchDepth = getBatchDepth,
|
getBatchDepth = getBatchDepth,
|
||||||
startBatch = startBatch,
|
startBatch = startBatch,
|
||||||
endBatch = endBatch,
|
endBatch = endBatch,
|
||||||
|
|
@ -523,6 +516,7 @@ return {
|
||||||
computed = computed,
|
computed = computed,
|
||||||
effect = effect,
|
effect = effect,
|
||||||
effectScope = effectScope,
|
effectScope = effectScope,
|
||||||
|
onCleanup = onCleanup,
|
||||||
untracked = untracked,
|
untracked = untracked,
|
||||||
peek = peek,
|
peek = peek,
|
||||||
batched = batched,
|
batched = batched,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
local core = require("./core")
|
local core = require("./core")
|
||||||
|
|
||||||
local computed = core.computed
|
local computed = core.computed
|
||||||
local setCurrentSub = core.setCurrentSub
|
local setActiveSub = core.setActiveSub
|
||||||
|
|
||||||
type Mapped =
|
type Mapped =
|
||||||
& (<KI, VI, KO, VO>(getter: () -> { [KI]: VI }, mapper: (item: VI, key: KI) -> (VO, KO)) -> () -> { [KO]: VO })
|
& (<KI, VI, KO, VO>(getter: () -> { [KI]: VI }, mapper: (item: VI, key: KI) -> (VO, KO)) -> () -> { [KO]: VO })
|
||||||
|
|
@ -12,9 +12,9 @@ local function mapped<KI, VI, KO, VO>(getter: () -> { [KI]: VI }, mapper: (item:
|
||||||
local keyCache: { [KI]: KO } = {}
|
local keyCache: { [KI]: KO } = {}
|
||||||
|
|
||||||
local function map(value: VI, key: KI): (VO, KO)
|
local function map(value: VI, key: KI): (VO, KO)
|
||||||
local currentSub = setCurrentSub(nil)
|
local prevSub = setActiveSub(nil)
|
||||||
local success, valueOut, keyOut = pcall(mapper, value, key)
|
local success, valueOut, keyOut = pcall(mapper, value, key)
|
||||||
setCurrentSub(currentSub)
|
setActiveSub(prevSub)
|
||||||
|
|
||||||
if not success then
|
if not success then
|
||||||
error(`mapped() transform function failed: {valueOut}`)
|
error(`mapped() transform function failed: {valueOut}`)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
local function handleNoYield(thread: thread, ok: boolean, ...)
|
local function noYieldHandler(thread: thread, ok: boolean, ...)
|
||||||
if not ok then
|
if not ok then
|
||||||
return false, debug.traceback(thread, tostring((...)))
|
return false, debug.traceback(thread, tostring((...)))
|
||||||
elseif coroutine.status(thread) ~= "dead" then
|
elseif coroutine.status(thread) ~= "dead" then
|
||||||
|
|
@ -10,7 +10,7 @@ end
|
||||||
|
|
||||||
local function noYield<Args..., Results...>(callback: (Args...) -> Results..., ...: Args...): (boolean, Results...)
|
local function noYield<Args..., Results...>(callback: (Args...) -> Results..., ...: Args...): (boolean, Results...)
|
||||||
local thread = coroutine.create(callback)
|
local thread = coroutine.create(callback)
|
||||||
return handleNoYield(thread, coroutine.resume(thread, ...))
|
return noYieldHandler(thread, coroutine.resume(thread, ...))
|
||||||
end
|
end
|
||||||
|
|
||||||
local function deepFreeze(object: any)
|
local function deepFreeze(object: any)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue