refactor: reduce type casting, housekeeping

This commit is contained in:
littensy 2025-07-20 20:52:17 -07:00
parent 77c98ca94a
commit a8051257e5
5 changed files with 57 additions and 52 deletions

View file

@ -52,12 +52,14 @@ local startTracking: (sub: ReactiveNode) -> ()
local shallowPropagate: (link: Link) -> () local shallowPropagate: (link: Link) -> ()
local batchDepth = 0 local batchDepth = 0
local queuedEffects: { Effect | EffectScope? } = {}
local queuedEffects: { Effect } = {}
local queuedEffectsLength = 0 local queuedEffectsLength = 0
local notifyIndex = 0 local notifyIndex = 0
local activeSubTracking = true
local activeSub: ReactiveNode?
local activeScope: Effect | EffectScope? local activeScope: Effect | EffectScope?
local activeSub: ReactiveNode?
local activeSubWatching = true
local function getCurrentSub(): ReactiveNode? local function getCurrentSub(): ReactiveNode?
return activeSub return activeSub
@ -79,16 +81,16 @@ local function setCurrentScope(scope: Effect | EffectScope?)
return prevScope return prevScope
end end
local function setCurrentTracking(tracking: boolean): boolean local function setCurrentWatching(watching: boolean): boolean
local prevSubTracking = activeSubTracking local prevSubWatching = activeSubWatching
activeSubTracking = tracking activeSubWatching = watching
return prevSubTracking return prevSubWatching
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 prev = setCurrentSub(nil)
local success, result: unknown = (if config.strict then noYield else pcall)(cleanup) local success, result: any = (config.strict and noYield or pcall)(cleanup)
setCurrentSub(prev) setCurrentSub(prev)
if not success then if not success then
error(result) error(result)
@ -98,7 +100,7 @@ local function runCleanups(effect: EffectScope)
end end
local function onCleanup(cleanup: () -> (), failSilently: boolean?) local function onCleanup(cleanup: () -> (), failSilently: boolean?)
if activeSub then if activeSub and (activeSub :: Effect).cleanups then
table.insert((activeSub :: Effect).cleanups, cleanup) table.insert((activeSub :: Effect).cleanups, cleanup)
elseif activeScope then elseif activeScope then
table.insert(activeScope.cleanups, cleanup) table.insert(activeScope.cleanups, cleanup)
@ -107,16 +109,16 @@ local function onCleanup(cleanup: () -> (), failSilently: boolean?)
end end
end end
local function run(effect: Effect | EffectScope, flags: ReactiveFlags): () local function run(effect: Effect, flags: ReactiveFlags): ()
if bit32.btest(flags, DIRTY) or (bit32.btest(flags, PENDING) and checkDirty(effect.deps :: Link, effect)) then if bit32.btest(flags, DIRTY) or (bit32.btest(flags, PENDING) and checkDirty(effect.deps :: Link, effect)) then
runCleanups(effect) runCleanups(effect)
local prev = setCurrentSub(effect) local prev = setCurrentSub(effect)
local prevTracking = setCurrentTracking(true) local prevTracking = setCurrentWatching(true)
startTracking(effect) startTracking(effect)
local success, result: (() -> ())? = (if config.strict then noYield else pcall)((effect :: Effect).fn) local success, result: (() -> ())? = (config.strict and noYield or pcall)(effect.fn)
setCurrentSub(prev) setCurrentSub(prev)
endTracking(effect) endTracking(effect)
setCurrentTracking(prevTracking) setCurrentWatching(prevTracking)
if not success then if not success then
error(result) error(result)
elseif result then elseif result then
@ -124,7 +126,7 @@ local function run(effect: Effect | EffectScope, flags: ReactiveFlags): ()
end end
return return
elseif bit32.btest(flags, PENDING) then elseif bit32.btest(flags, PENDING) then
(effect :: Effect).flags = bit32.band(flags, bit32.bnot(PENDING)) effect.flags = bit32.band(flags, bit32.bnot(PENDING))
end end
local link = effect.deps local link = effect.deps
while link do while link do
@ -142,7 +144,7 @@ end
local function flush(): () local function flush(): ()
while notifyIndex < queuedEffectsLength do while notifyIndex < queuedEffectsLength do
notifyIndex += 1 notifyIndex += 1
local effect = queuedEffects[notifyIndex] :: Effect local effect = queuedEffects[notifyIndex]
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)
@ -168,13 +170,13 @@ end
local function updateComputed(computed: Computed): boolean local function updateComputed(computed: Computed): boolean
local prevSub = setCurrentSub(computed) local prevSub = setCurrentSub(computed)
local prevTracking = setCurrentTracking(true) local prevWatching = setCurrentWatching(true)
startTracking(computed) startTracking(computed)
local oldValue = computed.value local oldValue = computed.value
local success, result = (if config.strict then noYield else pcall)(computed.getter, oldValue) local success, result = (config.strict and noYield or pcall)(computed.getter, oldValue)
setCurrentSub(prevSub) setCurrentSub(prevSub)
endTracking(computed) endTracking(computed)
setCurrentTracking(prevTracking) setCurrentWatching(prevWatching)
if not success then if not success then
error(result) error(result)
end end
@ -207,7 +209,7 @@ local function readAtom<T>(atom: Atom<T>): T
end end
end end
end end
if activeSub and activeSubTracking then if activeSub and activeSubWatching then
link(atom, activeSub) link(atom, activeSub)
end end
return value return value
@ -215,7 +217,7 @@ end
local function writeAtom<T>(atom: Atom<T>, oldValue: T, newValue: T): T local function writeAtom<T>(atom: Atom<T>, oldValue: T, newValue: T): T
local equals = atom.equals local equals = atom.equals
if if equals then not equals(oldValue, newValue :: T) else oldValue ~= newValue then if if equals then not equals(oldValue, newValue) else oldValue ~= newValue then
if config.frozen then if config.frozen then
deepFreeze(newValue) deepFreeze(newValue)
end end
@ -242,7 +244,7 @@ local function atomGetterSetter<T>(atom: Atom<T>): (...((T) -> T) | T) -> T
local newValue = ... local newValue = ...
if type(newValue) == "function" then if type(newValue) == "function" then
local prev = setCurrentSub(nil) local prev = setCurrentSub(nil)
local success, result = (if config.strict then noYield else pcall)(newValue, oldValue) local success, result = (config.strict and noYield or pcall)(newValue, oldValue)
setCurrentSub(prev) setCurrentSub(prev)
if not success then if not success then
error(result) error(result)
@ -268,10 +270,8 @@ local function computedGetter<T>(computed: Computed<T>): () -> T
elseif bit32.btest(flags, PENDING) then elseif bit32.btest(flags, PENDING) then
computed.flags = bit32.band(flags, bit32.bnot(PENDING)) computed.flags = bit32.band(flags, bit32.bnot(PENDING))
end end
if activeSub then if activeSub and activeSubWatching then
if activeSubTracking then
link(computed, activeSub) link(computed, activeSub)
end
elseif activeScope then elseif activeScope then
link(computed, activeScope) link(computed, activeScope)
end end
@ -279,7 +279,7 @@ local function computedGetter<T>(computed: Computed<T>): () -> T
end end
end end
local function effectStop(self: Effect | EffectScope): () -> () local function effectStop(self: EffectScope): () -> ()
return function() return function()
local dep = self.deps local dep = self.deps
while dep do while dep do
@ -289,38 +289,38 @@ local function effectStop(self: Effect | EffectScope): () -> ()
if sub then if sub then
unlink(sub) unlink(sub)
end end
(self :: Effect).flags = NONE self.flags = NONE
runCleanups(self) runCleanups(self)
end end
end end
local function update(atom: Atom | Computed): boolean local function update(atom: ReactiveNode): boolean
if (atom :: Computed).getter then if (atom :: Computed).getter then
return updateComputed(atom :: Computed) return updateComputed(atom :: Computed)
else else
return updateAtom(atom :: Atom, atom.value) return updateAtom(atom :: Atom, (atom :: Atom).value)
end end
end end
local function notify(effect: Effect | EffectScope) local function notify(effect: ReactiveNode)
local flags = effect.flags local flags = effect.flags
if not bit32.btest(flags, QUEUED) then if not bit32.btest(flags, QUEUED) then
(effect :: Effect).flags = bit32.bor(flags, QUEUED) effect.flags = bit32.bor(flags, QUEUED)
local subs = effect.subs local subs = effect.subs
if subs then if subs then
notify(subs.sub :: Effect) notify(subs.sub :: Effect)
else else
queuedEffectsLength += 1 queuedEffectsLength += 1
queuedEffects[queuedEffectsLength] = effect queuedEffects[queuedEffectsLength] = effect :: Effect
end end
end end
end end
local function unwatched(node: Atom | Computed | Effect | EffectScope) local function unwatched(node: ReactiveNode)
if (node :: Computed).getter then if (node :: Computed).getter then
local toRemove = node.deps local toRemove = node.deps
if toRemove then if toRemove then
(node :: Computed).flags = bit32.bor(MUTABLE, DIRTY) node.flags = bit32.bor(MUTABLE, DIRTY)
repeat repeat
toRemove = unlink(toRemove, node) toRemove = unlink(toRemove, node)
until not toRemove until not toRemove
@ -362,10 +362,10 @@ local function effect(fn: () -> ...(() -> ())?): () -> ()
link(self, activeScope) link(self, activeScope)
end end
local prev = setCurrentSub(self) local prev = setCurrentSub(self)
local prevTracking = setCurrentTracking(true) local prevWatching = setCurrentWatching(true)
local success, result: (() -> ())? = (if config.strict then noYield else pcall)(fn) local success, result: (() -> ())? = (config.strict and noYield or pcall)(fn)
setCurrentSub(prev) setCurrentSub(prev)
setCurrentTracking(prevTracking) setCurrentWatching(prevWatching)
if not success then if not success then
error(result) error(result)
elseif result then elseif result then
@ -384,7 +384,7 @@ local function effectScope(fn: () -> ...(() -> ())?, detached: boolean?): () ->
end end
local prevSub = setCurrentSub(nil) local prevSub = setCurrentSub(nil)
local prevScope = setCurrentScope(self) local prevScope = setCurrentScope(self)
local success, result: (() -> ())? = (if config.strict then noYield else pcall)(fn) local success, result: (() -> ())? = (config.strict and noYield or pcall)(fn)
setCurrentScope(prevScope) setCurrentScope(prevScope)
setCurrentSub(prevSub) setCurrentSub(prevSub)
if not success then if not success then
@ -396,9 +396,9 @@ local function effectScope(fn: () -> ...(() -> ())?, detached: boolean?): () ->
end end
local reactiveSystem = createReactiveSystem({ local reactiveSystem = createReactiveSystem({
update = update :: (ReactiveNode) -> boolean, update = update,
notify = notify :: (ReactiveNode) -> (), notify = notify,
unwatched = unwatched :: (ReactiveNode) -> (), unwatched = unwatched,
}) })
link = reactiveSystem.link link = reactiveSystem.link
@ -416,7 +416,7 @@ return {
setCurrentSub = setCurrentSub, setCurrentSub = setCurrentSub,
getCurrentScope = getCurrentScope, getCurrentScope = getCurrentScope,
setCurrentScope = setCurrentScope, setCurrentScope = setCurrentScope,
setCurrentTracking = setCurrentTracking, setCurrentWatching = setCurrentWatching,
onCleanup = onCleanup, onCleanup = onCleanup,
getBatchDepth = getBatchDepth, getBatchDepth = getBatchDepth,
startBatch = startBatch, startBatch = startBatch,

View file

@ -6,9 +6,10 @@ local isO2 = inlineTest() ~= "inlineTest"
return { return {
--- Enforces synchronous, non-yielding behavior in atoms and effects. --- Enforces synchronous, non-yielding behavior in atoms and effects.
--- Enabled if the optimize level is below 2, which is the case in studio. --- Also enables state validation in Charm Sync to catch sync errors early.
--- Enabled if the optimization level is below 2, which is true in studio.
strict = not isO2, strict = not isO2,
--- Enforces immutability of atoms by deep-freezing table values. Disabled --- Enforces immutability of atoms by deep-freezing table values. Enabled
--- by default, but can be enabled to detect accidental mutations. --- if the optimization level is below 2, which is true in studio.
frozen = false, frozen = not isO2,
} }

View file

@ -57,8 +57,6 @@ export function observe<V, K extends Key>(
observer: (value: V, key: K) => Cleanup | void, observer: (value: V, key: K) => Cleanup | void,
): Cleanup; ): Cleanup;
export function onCleanup(callback: Cleanup, failSilently?: boolean): void;
export function batched<Args extends any[], Result>( export function batched<Args extends any[], Result>(
callback: (...args: Args) => Result, callback: (...args: Args) => Result,
...args: Args ...args: Args
@ -66,6 +64,8 @@ export function batched<Args extends any[], Result>(
export function untracked<Args extends any[], Result>(callback: (...args: Args) => Result, ...args: Args): Result; export function untracked<Args extends any[], Result>(callback: (...args: Args) => Result, ...args: Args): Result;
export function onCleanup(callback: Cleanup, failSilently?: boolean): void;
/** /**
* @deprecated Use `batched()` instead. * @deprecated Use `batched()` instead.
*/ */

View file

@ -32,7 +32,9 @@ local function mapped<KI, VI, KO, VO>(selector: () -> { [KI]: VI }, mapper: (ite
if currentData[key] ~= nil then if currentData[key] ~= nil then
continue continue
end end
local cachedKey = keyCache[key] local cachedKey = keyCache[key]
if cachedKey then if cachedKey then
currentOutput = if currentOutput == previousOutput then table.clone(currentOutput) else currentOutput currentOutput = if currentOutput == previousOutput then table.clone(currentOutput) else currentOutput
currentOutput[cachedKey] = nil currentOutput[cachedKey] = nil
@ -44,7 +46,9 @@ local function mapped<KI, VI, KO, VO>(selector: () -> { [KI]: VI }, mapper: (ite
if value == previousData[key] then if value == previousData[key] then
continue continue
end end
local valueOut, keyOut = map(value, key) local valueOut, keyOut = map(value, key)
if currentOutput[keyOut] ~= valueOut then if currentOutput[keyOut] ~= valueOut then
currentOutput = if currentOutput == previousOutput then table.clone(currentOutput) else currentOutput currentOutput = if currentOutput == previousOutput then table.clone(currentOutput) else currentOutput
currentOutput[keyOut] = valueOut currentOutput[keyOut] = valueOut

View file

@ -1,9 +1,9 @@
local core = require("./core") local core = require("./core")
local setCurrentTracking = core.setCurrentTracking local setCurrentWatching = core.setCurrentWatching
local function handleUntracked(prevTracking: boolean, ok: boolean, ...) local function handleUntracked(prevWatching: boolean, ok: boolean, ...)
setCurrentTracking(prevTracking) setCurrentWatching(prevWatching)
if ok then if ok then
return ... return ...
else else
@ -12,7 +12,7 @@ local function handleUntracked(prevTracking: boolean, ok: boolean, ...)
end end
local function untracked<Args..., Results...>(callback: (Args...) -> Results..., ...: Args...): Results... local function untracked<Args..., Results...>(callback: (Args...) -> Results..., ...: Args...): Results...
return handleUntracked(setCurrentTracking(false), pcall(callback, ...)) return handleUntracked(setCurrentWatching(false), pcall(callback, ...))
end end
return untracked return untracked