mirror of
https://github.com/littensy/charm.git
synced 2026-10-23 05:46:27 +00:00
chore: housekeeping
This commit is contained in:
parent
ed6dd74512
commit
1740a440d2
2 changed files with 65 additions and 68 deletions
10
README.md
10
README.md
|
|
@ -18,15 +18,15 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
Charm is a state management library based on [fine-grained reactivity](https://dev.to/ryansolid/a-hands-on-introduction-to-fine-grained-reactivity-3ndf). Connect behavior to data with reactive signals, ensuring games stay up-to-date with their underlying data while eliminating the need for manual updates.
|
Charm is a reactive state management library designed for storing data in discrete containers called _signals_. Connect behavior to data with signals, ensuring systems stay up-to-date with their underlying data while reactivity eliminates the need for manual updates.
|
||||||
|
|
||||||
**Build game state from simple building blocks:**
|
**Build game state from simple building blocks:**
|
||||||
|
|
||||||
- Store state in [signals](#signalinitialvalue-equals): state containers that hold a value
|
- Store data in [signals](#signalinitialvalue-equals): reactive state containers that hold a value
|
||||||
- React to state changes with [effects](#effectcallback): re-run code when a dependency updates
|
- React to state changes with [effects](#effectcallback): re-run code when signals update
|
||||||
- Derive values from state with [computed signals](#computedgetter): memoized functions with dependency tracking
|
- Derive new values from data with [computed signals](#computedgetter): memoize functions that access signals
|
||||||
|
|
||||||
**Want to learn more about signals?**
|
**Want to learn more about reactivity?**
|
||||||
|
|
||||||
- https://dev.to/ryansolid/a-hands-on-introduction-to-fine-grained-reactivity-3ndf
|
- https://dev.to/ryansolid/a-hands-on-introduction-to-fine-grained-reactivity-3ndf
|
||||||
- https://preactjs.com/blog/introducing-signals
|
- https://preactjs.com/blog/introducing-signals
|
||||||
|
|
|
||||||
|
|
@ -45,11 +45,11 @@ type SignalNode<T = any> = ReactiveNode & {
|
||||||
-- Bitmask flags for tracking the state of reactive nodes. Flags are combined
|
-- Bitmask flags for tracking the state of reactive nodes. Flags are combined
|
||||||
-- and checked using bitwise operations for efficiency.
|
-- and checked using bitwise operations for efficiency.
|
||||||
local NONE = 0b000000 -- Default state
|
local NONE = 0b000000 -- Default state
|
||||||
local MUTABLE = 0b000001 -- Can be changed (signal or computed)
|
local MUTABLE = 0b000001 -- Can be written to (signal or computed)
|
||||||
local WATCHING = 0b000010 -- Watching for changes (effects)
|
local WATCHING = 0b000010 -- Notified for changes in dependencies (effects)
|
||||||
local RECURSED_CHECK = 0b000100 -- Being checked for circular dependencies
|
local RECURSED_CHECK = 0b000100 -- Being checked for circular dependencies
|
||||||
local RECURSED = 0b001000 -- Has been visited in a recursion check
|
local RECURSED = 0b001000 -- Has been visited in a recursion check
|
||||||
local DIRTY = 0b010000 -- Value has changed and needs updating
|
local DIRTY = 0b010000 -- Value is outdated and needs recomputation
|
||||||
local PENDING = 0b100000 -- Might be dirty, pending verification
|
local PENDING = 0b100000 -- Might be dirty, pending verification
|
||||||
|
|
||||||
-- Global version counter for dependency link deduplication. Incremented at the
|
-- Global version counter for dependency link deduplication. Incremented at the
|
||||||
|
|
@ -104,21 +104,20 @@ local function noYield<Args..., Results...>(callback: (Args...) -> Results...):
|
||||||
return callback
|
return callback
|
||||||
end
|
end
|
||||||
|
|
||||||
local function packResults<Args...>(callback: (Args...) -> ...any, ...: Args...): { any }
|
return function(...: Args...): Results...
|
||||||
return { callback(...) }
|
|
||||||
end
|
|
||||||
|
|
||||||
local function noYieldCallback(...): Results...
|
|
||||||
if coroutine.isyieldable() then
|
if coroutine.isyieldable() then
|
||||||
for results in packResults, callback, ... do
|
for results in
|
||||||
|
function(...)
|
||||||
|
return { callback(...) }
|
||||||
|
end,
|
||||||
|
...
|
||||||
|
do
|
||||||
return unpack(results)
|
return unpack(results)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
return callback(...)
|
return callback(...)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return noYieldCallback
|
|
||||||
end
|
end
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
|
|
@ -264,6 +263,7 @@ local function flush()
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
-- Skip over any remaining queued effects that were not run due to an error
|
||||||
while notifyIndex < queuedLength do
|
while notifyIndex < queuedLength do
|
||||||
local effect = queued[notifyIndex] :: EffectNode
|
local effect = queued[notifyIndex] :: EffectNode
|
||||||
queued[notifyIndex] = nil
|
queued[notifyIndex] = nil
|
||||||
|
|
@ -354,27 +354,24 @@ local function clearPending(sub: ReactiveNode, subFlags: ReactiveFlags)
|
||||||
end
|
end
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
Performs a thorough cleanup of the given effect or effect scope:
|
Clears all dependencies and subscribers from the effect node. After calling
|
||||||
|
this, the node will no longer be subscribed to any dependencies and will be
|
||||||
|
inactive, with all cleanup functions run and resources released.
|
||||||
|
|
||||||
1. Removes all dependency links
|
@param node Effect node
|
||||||
2. Unlinks from parent effects/scopes
|
|
||||||
3. Clears state flags to mark the effect as inactive
|
|
||||||
4. Runs all registered cleanup functions
|
|
||||||
|
|
||||||
@param this Effect node
|
|
||||||
]]
|
]]
|
||||||
local function stopEffect(this: EffectScopeNode)
|
local function stopEffect(node: EffectScopeNode)
|
||||||
this.depsTail = nil
|
node.depsTail = nil
|
||||||
this.flags = NONE
|
node.flags = NONE
|
||||||
|
|
||||||
purgeDeps(this)
|
purgeDeps(node)
|
||||||
|
|
||||||
local sub = this.subs
|
local sub = node.subs
|
||||||
if sub then
|
if sub then
|
||||||
unlink(sub, this)
|
unlink(sub, node)
|
||||||
end
|
end
|
||||||
|
|
||||||
runCleanups(this.cleanups)
|
runCleanups(node.cleanups)
|
||||||
end
|
end
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
|
|
@ -458,7 +455,7 @@ end
|
||||||
@see https://github.com/littensy/charm?tab=readme-ov-file#signalinitialValue-equals
|
@see https://github.com/littensy/charm?tab=readme-ov-file#signalinitialValue-equals
|
||||||
]]
|
]]
|
||||||
local function signal<T>(initialValue: T, equals: Equals<T>?): (Getter<T>, Setter<T>)
|
local function signal<T>(initialValue: T, equals: Equals<T>?): (Getter<T>, Setter<T>)
|
||||||
local this: SignalNode = {
|
local node: SignalNode = {
|
||||||
currentValue = initialValue,
|
currentValue = initialValue,
|
||||||
pendingValue = initialValue,
|
pendingValue = initialValue,
|
||||||
flags = MUTABLE,
|
flags = MUTABLE,
|
||||||
|
|
@ -475,7 +472,7 @@ local function signal<T>(initialValue: T, equals: Equals<T>?): (Getter<T>, Sette
|
||||||
-- a direct value
|
-- a direct value
|
||||||
if type(newValue) == "function" then
|
if type(newValue) == "function" then
|
||||||
local prevSub = setActiveSub(nil)
|
local prevSub = setActiveSub(nil)
|
||||||
local success, result = xpcall(noYield(newValue), debug.traceback, this.pendingValue)
|
local success, result = xpcall(noYield(newValue), debug.traceback, node.pendingValue)
|
||||||
activeSub = prevSub
|
activeSub = prevSub
|
||||||
|
|
||||||
if success then
|
if success then
|
||||||
|
|
@ -488,15 +485,15 @@ local function signal<T>(initialValue: T, equals: Equals<T>?): (Getter<T>, Sette
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Check if the new value is different from the pending value
|
-- Check if the new value is different from the pending value
|
||||||
if if equals then not equals(this.pendingValue, value) else this.pendingValue ~= value then
|
if if equals then not equals(node.pendingValue, value) else node.pendingValue ~= value then
|
||||||
if flags.frozen then
|
if flags.frozen then
|
||||||
deepFreeze(value)
|
deepFreeze(value)
|
||||||
end
|
end
|
||||||
|
|
||||||
this.pendingValue = value
|
node.pendingValue = value
|
||||||
this.flags = bit32.bor(MUTABLE, DIRTY)
|
node.flags = bit32.bor(MUTABLE, DIRTY)
|
||||||
|
|
||||||
local subs = this.subs
|
local subs = node.subs
|
||||||
if subs then
|
if subs then
|
||||||
propagate(subs)
|
propagate(subs)
|
||||||
flush()
|
flush()
|
||||||
|
|
@ -505,14 +502,14 @@ local function signal<T>(initialValue: T, equals: Equals<T>?): (Getter<T>, Sette
|
||||||
return value
|
return value
|
||||||
end
|
end
|
||||||
|
|
||||||
return this.currentValue
|
return node.currentValue
|
||||||
end
|
end
|
||||||
|
|
||||||
local function readSignal(): T
|
local function readSignal(): T
|
||||||
-- Check if the signal has a pending update that needs to be applied
|
-- Check if the signal has a pending update that needs to be applied
|
||||||
if bit32.btest(this.flags, DIRTY) then
|
if bit32.btest(node.flags, DIRTY) then
|
||||||
if updateSignal(this) then
|
if updateSignal(node) then
|
||||||
local subs = this.subs
|
local subs = node.subs
|
||||||
if subs then
|
if subs then
|
||||||
shallowPropagate(subs)
|
shallowPropagate(subs)
|
||||||
end
|
end
|
||||||
|
|
@ -524,14 +521,14 @@ local function signal<T>(initialValue: T, equals: Equals<T>?): (Getter<T>, Sette
|
||||||
local sub = activeSub
|
local sub = activeSub
|
||||||
while sub do
|
while sub do
|
||||||
if bit32.btest(sub.flags, bit32.bor(MUTABLE, WATCHING)) then
|
if bit32.btest(sub.flags, bit32.bor(MUTABLE, WATCHING)) then
|
||||||
link(this, sub, cycle)
|
link(node, sub, cycle)
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
local subs = sub.subs
|
local subs = sub.subs
|
||||||
sub = if subs then subs.sub else nil
|
sub = if subs then subs.sub else nil
|
||||||
end
|
end
|
||||||
|
|
||||||
return this.currentValue
|
return node.currentValue
|
||||||
end
|
end
|
||||||
|
|
||||||
return readSignal, writeSignal
|
return readSignal, writeSignal
|
||||||
|
|
@ -564,50 +561,50 @@ end
|
||||||
@see https://github.com/littensy/charm?tab=readme-ov-file#computedgetter
|
@see https://github.com/littensy/charm?tab=readme-ov-file#computedgetter
|
||||||
]]
|
]]
|
||||||
local function computed<T>(getter: (previousValue: T?) -> T): Getter<T>
|
local function computed<T>(getter: (previousValue: T?) -> T): Getter<T>
|
||||||
local this: ComputedNode = {
|
local node: ComputedNode = {
|
||||||
flags = NONE,
|
flags = NONE,
|
||||||
getter = noYield(getter),
|
getter = noYield(getter),
|
||||||
}
|
}
|
||||||
|
|
||||||
local function computedOper(): T
|
local function computedOper(): T
|
||||||
local computedFlags = this.flags
|
local nodeFlags = node.flags
|
||||||
|
|
||||||
if
|
if
|
||||||
bit32.btest(computedFlags, DIRTY)
|
bit32.btest(nodeFlags, DIRTY)
|
||||||
or (
|
or (
|
||||||
bit32.btest(computedFlags, PENDING)
|
bit32.btest(nodeFlags, PENDING)
|
||||||
and (checkDirty(this.deps :: Link, this) or clearPending(this, computedFlags))
|
and (checkDirty(node.deps :: Link, node) or clearPending(node, nodeFlags))
|
||||||
)
|
)
|
||||||
then
|
then
|
||||||
if updateComputed(this) then
|
if updateComputed(node) then
|
||||||
-- Notify subscribers if the value changed
|
-- Notify subscribers if the value changed
|
||||||
local subs = this.subs
|
local subs = node.subs
|
||||||
if subs then
|
if subs then
|
||||||
shallowPropagate(subs)
|
shallowPropagate(subs)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
elseif computedFlags == NONE then
|
elseif nodeFlags == NONE then
|
||||||
-- First access should initialize the computed value
|
-- First access should initialize the computed value
|
||||||
this.flags = bit32.bor(MUTABLE, RECURSED_CHECK)
|
node.flags = bit32.bor(MUTABLE, RECURSED_CHECK)
|
||||||
|
|
||||||
local prevSub = setActiveSub(this)
|
local prevSub = setActiveSub(node)
|
||||||
local success, result = xpcall(this.getter, debug.traceback)
|
local success, result = xpcall(node.getter, debug.traceback)
|
||||||
activeSub = prevSub
|
activeSub = prevSub
|
||||||
|
|
||||||
this.flags = bit32.band(this.flags, bit32.bnot(RECURSED_CHECK))
|
node.flags = bit32.band(node.flags, bit32.bnot(RECURSED_CHECK))
|
||||||
|
|
||||||
if success then
|
if success then
|
||||||
this.value = result
|
node.value = result
|
||||||
else
|
else
|
||||||
error(result, 2)
|
error(result, 2)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if activeSub then
|
if activeSub then
|
||||||
link(this, activeSub, cycle)
|
link(node, activeSub, cycle)
|
||||||
end
|
end
|
||||||
|
|
||||||
return this.value :: T
|
return node.value :: T
|
||||||
end
|
end
|
||||||
|
|
||||||
return computedOper
|
return computedOper
|
||||||
|
|
@ -629,35 +626,35 @@ end
|
||||||
@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: () -> ...Cleanup?): Cleanup
|
local function effect(fn: () -> ...Cleanup?): Cleanup
|
||||||
local this: EffectNode = {
|
local node: EffectNode = {
|
||||||
fn = noYield(fn),
|
fn = noYield(fn),
|
||||||
flags = WATCHING,
|
flags = WATCHING,
|
||||||
cleanups = {},
|
cleanups = {},
|
||||||
}
|
}
|
||||||
|
|
||||||
if activeSub and (flags.trackInnerEffects or activeSub.flags == NONE) then
|
if activeSub and (flags.trackInnerEffects or activeSub.flags == NONE) then
|
||||||
link(this, activeSub, 0)
|
link(node, activeSub, 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Start a batch so that any signal updates triggered during the effect
|
-- Start a batch so that any signal updates triggered during the effect
|
||||||
-- execution are queued after the effect registers a cleanup function
|
-- execution are queued after the effect registers a cleanup function
|
||||||
startBatch()
|
startBatch()
|
||||||
|
|
||||||
local prevSub = setActiveSub(this)
|
local prevSub = setActiveSub(node)
|
||||||
local success, result = xpcall(this.fn, debug.traceback)
|
local success, result = xpcall(node.fn, debug.traceback)
|
||||||
activeSub = prevSub
|
activeSub = prevSub
|
||||||
|
|
||||||
if not success then
|
if not success then
|
||||||
endBatch()
|
endBatch()
|
||||||
error(result, 2)
|
error(result, 2)
|
||||||
elseif result then
|
elseif result then
|
||||||
table.insert(this.cleanups, noYield(result))
|
table.insert(node.cleanups, noYield(result))
|
||||||
end
|
end
|
||||||
|
|
||||||
endBatch()
|
endBatch()
|
||||||
|
|
||||||
return function()
|
return function()
|
||||||
stopEffect(this)
|
stopEffect(node)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -671,27 +668,27 @@ end
|
||||||
@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: () -> ...Cleanup?, detached: boolean?): Cleanup
|
local function effectScope(fn: () -> ...Cleanup?, detached: boolean?): Cleanup
|
||||||
local this: EffectScopeNode = {
|
local node: EffectScopeNode = {
|
||||||
flags = NONE,
|
flags = NONE,
|
||||||
cleanups = {},
|
cleanups = {},
|
||||||
}
|
}
|
||||||
|
|
||||||
if activeSub and not detached then
|
if activeSub and not detached then
|
||||||
link(this, activeSub, 0)
|
link(node, activeSub, 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
local prevSub = setActiveSub(this)
|
local prevSub = setActiveSub(node)
|
||||||
local success, result = xpcall(noYield(fn), debug.traceback)
|
local success, result = xpcall(noYield(fn), debug.traceback)
|
||||||
activeSub = prevSub
|
activeSub = prevSub
|
||||||
|
|
||||||
if not success then
|
if not success then
|
||||||
error(result, 2)
|
error(result, 2)
|
||||||
elseif result then
|
elseif result then
|
||||||
table.insert(this.cleanups, noYield(result))
|
table.insert(node.cleanups, noYield(result))
|
||||||
end
|
end
|
||||||
|
|
||||||
return function()
|
return function()
|
||||||
stopEffect(this)
|
stopEffect(node)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue