mirror of
https://github.com/littensy/charm.git
synced 2026-10-24 06:51:37 +00:00
refactor: embrace scope cleanup, restructure code
This commit is contained in:
parent
3eba56e298
commit
8d385356cd
81 changed files with 687 additions and 626 deletions
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"name": "Charm",
|
||||
"tree": {
|
||||
"$path": "src"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
local signals = require("./signals")
|
||||
local types = require("./types")
|
||||
|
||||
type Atom<T> = types.Atom<T>
|
||||
type AtomOptions<T> = types.AtomOptions<T>
|
||||
|
||||
local signal = signals.signal
|
||||
local pauseTracking = signals.pauseTracking
|
||||
local resumeTracking = signals.resumeTracking
|
||||
|
||||
local function atomOper<T>(source: (...T) -> T, equals: ((T, T) -> boolean)?, ...: ((T) -> T) | T): T
|
||||
if select("#", ...) == 0 then
|
||||
return source()
|
||||
end
|
||||
|
||||
local transform = (...)
|
||||
local currentState = (source :: any).value
|
||||
local nextState: T
|
||||
|
||||
-- If the transform is a function, call it with the current state and pause
|
||||
-- tracking to avoid adding signals called by the transform.
|
||||
if type(transform) == "function" then
|
||||
pauseTracking()
|
||||
local success, result = pcall(transform, currentState)
|
||||
resumeTracking()
|
||||
|
||||
if not success then
|
||||
error(result, 2)
|
||||
end
|
||||
|
||||
nextState = result
|
||||
else
|
||||
nextState = transform
|
||||
end
|
||||
|
||||
if equals and equals(currentState, nextState) then
|
||||
return nextState
|
||||
end
|
||||
|
||||
source(nextState)
|
||||
|
||||
return nextState
|
||||
end
|
||||
|
||||
local function atom<T>(state: T, options: AtomOptions<T>?): Atom<T>
|
||||
local source = signal(state)
|
||||
local equals = options and options.equals
|
||||
|
||||
return function(...)
|
||||
return atomOper(source, equals, ...)
|
||||
end
|
||||
end
|
||||
|
||||
return atom
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
local signals = require("./signals")
|
||||
|
||||
local startBatch = signals.startBatch
|
||||
local endBatch = signals.endBatch
|
||||
|
||||
local function batched(callback: () -> ())
|
||||
startBatch()
|
||||
local success, result: any = pcall(callback)
|
||||
endBatch()
|
||||
|
||||
if not success then
|
||||
error(result)
|
||||
end
|
||||
end
|
||||
|
||||
return batched
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
local signals = require("./signals")
|
||||
|
||||
local effectImpl = signals.effect
|
||||
|
||||
local function effect(callback: (dispose: () -> ()) -> ...() -> ()?): () -> ()
|
||||
local disposed = false
|
||||
local dispose: () -> ()
|
||||
|
||||
local function disposeSafe()
|
||||
disposed = true
|
||||
if dispose then
|
||||
dispose()
|
||||
end
|
||||
end
|
||||
|
||||
dispose = effectImpl(function()
|
||||
return callback(disposeSafe)
|
||||
end)
|
||||
|
||||
if disposed then
|
||||
dispose()
|
||||
end
|
||||
|
||||
return disposeSafe
|
||||
end
|
||||
|
||||
return effect
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
local lib = require(script.lib)
|
||||
|
||||
export type Atom<T = any> = lib.Atom<T>
|
||||
export type AtomOptions<T = any> = lib.AtomOptions<T>
|
||||
export type Selector<T = any> = lib.Selector<T>
|
||||
|
||||
return lib
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
local atom = require("./atom")
|
||||
local batched = require("./batched")
|
||||
local effect = require("./effect")
|
||||
local listen = require("./listen")
|
||||
local mapped = require("./mapped")
|
||||
local observe = require("./observe")
|
||||
local signals = require("./signals")
|
||||
local subscribe = require("./subscribe")
|
||||
local types = require("./types")
|
||||
local untracked = require("./untracked")
|
||||
|
||||
export type Atom<T = any> = types.Atom<T>
|
||||
export type AtomOptions<T = any> = types.AtomOptions<T>
|
||||
export type Selector<T = any> = types.Selector<T>
|
||||
|
||||
local computed = signals.computed
|
||||
local effectScope = signals.effectScope
|
||||
|
||||
return {
|
||||
atom = atom,
|
||||
batched = batched,
|
||||
computed = computed,
|
||||
effect = effect,
|
||||
effectScope = effectScope,
|
||||
listen = listen,
|
||||
mapped = mapped,
|
||||
observe = observe,
|
||||
subscribe = subscribe,
|
||||
untracked = untracked,
|
||||
signals = signals,
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
local signals = require("./signals")
|
||||
local spawn = require("./spawn")
|
||||
|
||||
local effect = signals.effect
|
||||
local pauseTracking = signals.pauseTracking
|
||||
local resumeTracking = signals.resumeTracking
|
||||
|
||||
local function listen<T>(selector: () -> T, listener: (state: T, previous: T?, dispose: () -> ()) -> ()): () -> ()
|
||||
local disposed = false
|
||||
local mounted = true
|
||||
local current: T?
|
||||
local dispose: () -> ()
|
||||
|
||||
local function disposeSafe()
|
||||
disposed = true
|
||||
if dispose then
|
||||
dispose()
|
||||
end
|
||||
end
|
||||
|
||||
dispose = effect(function()
|
||||
local value = selector()
|
||||
|
||||
if mounted then
|
||||
mounted = false
|
||||
elseif value == current then
|
||||
return
|
||||
end
|
||||
|
||||
local previous = current
|
||||
current = value
|
||||
|
||||
pauseTracking()
|
||||
spawn(listener, value, previous, disposeSafe)
|
||||
resumeTracking()
|
||||
end)
|
||||
|
||||
if disposed then
|
||||
dispose()
|
||||
end
|
||||
|
||||
return disposeSafe
|
||||
end
|
||||
|
||||
return listen
|
||||
|
|
@ -1,127 +0,0 @@
|
|||
local signals = require("./signals")
|
||||
|
||||
local computed = signals.computed
|
||||
local pauseTracking = signals.pauseTracking
|
||||
local resumeTracking = signals.resumeTracking
|
||||
|
||||
type MappedKey<VI, KO, VO> = {
|
||||
key: KO,
|
||||
value: VO,
|
||||
}
|
||||
|
||||
local function mapped<KI, VI, KO, VO>(selector: () -> { [KI]: VI }, transform: (item: VI, key: KI) -> (VO, KO?)): () -> { [KO]: VO }
|
||||
local mappedKeys: { [KI]: MappedKey<VI, KO, VO> } = {}
|
||||
local previousItems: { [KI]: VI }?
|
||||
|
||||
local function getTransform(value: VI, key: KI): (VO, KO?)
|
||||
pauseTracking()
|
||||
local success, valueOut, keyOut = pcall(transform, value, key)
|
||||
resumeTracking()
|
||||
|
||||
if not success then
|
||||
error(`observe() transform function failed: {valueOut}`)
|
||||
end
|
||||
|
||||
return valueOut, keyOut
|
||||
end
|
||||
|
||||
return computed(function(previousMappedItems)
|
||||
local currentItems = selector()
|
||||
|
||||
-- If this is the initial run, create a map of the items and store the
|
||||
-- updated keys and values.
|
||||
if not previousItems or not previousMappedItems then
|
||||
local mappedItems: { [KO]: VO } = {}
|
||||
previousItems = currentItems
|
||||
|
||||
for key, value in currentItems do
|
||||
local valueOut, keyOut = getTransform(value, key)
|
||||
local keyOut = if keyOut ~= nil then keyOut else key :: never
|
||||
|
||||
if valueOut ~= nil then
|
||||
mappedKeys[key] = { key = keyOut, value = valueOut }
|
||||
mappedItems[keyOut] = valueOut
|
||||
end
|
||||
end
|
||||
|
||||
return mappedItems
|
||||
end
|
||||
|
||||
local mappedItems = table.clone(previousMappedItems)
|
||||
|
||||
-- Update and remove mapped keys if the source values changed
|
||||
for key, oldValue in previousItems do
|
||||
local currentValue = currentItems[key]
|
||||
local mappedKey = mappedKeys[key]
|
||||
|
||||
-- Remove the key if the source value changed from non-nil to nil
|
||||
if currentValue == nil then
|
||||
mappedKeys[key] = nil
|
||||
if mappedKey then
|
||||
mappedItems[mappedKey.key] = nil
|
||||
end
|
||||
continue
|
||||
end
|
||||
|
||||
-- Do not call transform if the value has not changed
|
||||
if currentValue == oldValue then
|
||||
continue
|
||||
end
|
||||
|
||||
local valueOut, keyOut = getTransform(currentValue, key)
|
||||
local keyOut = if keyOut ~= nil then keyOut else key :: never
|
||||
|
||||
-- If there is no previous mappedKey, then this item was filtered
|
||||
-- out to optimize memory. We need to check if the value is nil to
|
||||
-- determine if we should add it back to the list.
|
||||
if not mappedKey then
|
||||
if valueOut ~= nil then
|
||||
mappedKeys[key] = { key = keyOut, value = valueOut }
|
||||
mappedItems[keyOut] = valueOut
|
||||
end
|
||||
continue
|
||||
end
|
||||
|
||||
-- If the mapped value is nil, then delete mappedKey to save memory.
|
||||
-- This is treated the same as setting mappedKey.value to nil.
|
||||
if valueOut == nil then
|
||||
mappedKeys[key] = nil
|
||||
mappedItems[mappedKey.key] = nil
|
||||
continue
|
||||
end
|
||||
|
||||
-- If the key changed since the last transform call, remove
|
||||
-- the old key and update mappedKey.
|
||||
if keyOut ~= mappedKey.key then
|
||||
mappedItems[mappedKey.key] = nil
|
||||
mappedKey.key = keyOut
|
||||
end
|
||||
|
||||
mappedKey.value = valueOut
|
||||
mappedItems[keyOut] = valueOut
|
||||
end
|
||||
|
||||
-- Add new keys if the value was not present in the previous state
|
||||
for key, currentValue in currentItems do
|
||||
local oldValue = previousItems[key]
|
||||
|
||||
if oldValue ~= nil then
|
||||
continue
|
||||
end
|
||||
|
||||
local valueOut, keyOut = getTransform(currentValue, key)
|
||||
local keyOut = if keyOut ~= nil then keyOut else key :: never
|
||||
|
||||
if valueOut ~= nil then
|
||||
mappedKeys[key] = { key = keyOut, value = valueOut }
|
||||
mappedItems[keyOut] = valueOut
|
||||
end
|
||||
end
|
||||
|
||||
previousItems = currentItems
|
||||
|
||||
return mappedItems
|
||||
end)
|
||||
end
|
||||
|
||||
return mapped
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
local listen = require("./listen")
|
||||
|
||||
local function observe<K, V>(selector: () -> { [K]: V }, observer: (item: V, key: K) -> ...() -> ()?): () -> ()
|
||||
local cleanups: { [K]: () -> ()? } = {}
|
||||
|
||||
local unsub = listen(selector, function(currentItems, previousItems)
|
||||
if not previousItems then
|
||||
for key, value in currentItems do
|
||||
cleanups[key] = observer(value, key)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
for key in previousItems do
|
||||
local cleanup = cleanups[key]
|
||||
if currentItems[key] == nil and cleanup then
|
||||
cleanups[key] = nil
|
||||
cleanup()
|
||||
end
|
||||
end
|
||||
|
||||
for key, value in currentItems do
|
||||
if previousItems[key] == nil then
|
||||
cleanups[key] = observer(value, key)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
return function()
|
||||
unsub()
|
||||
for _, cleanup in cleanups do
|
||||
(cleanup :: () -> ())()
|
||||
end
|
||||
table.clear(cleanups)
|
||||
end
|
||||
end
|
||||
|
||||
return observe
|
||||
|
|
@ -1,400 +0,0 @@
|
|||
--[=[
|
||||
alien-signals 2.0.1 implementation with effect cleanups, adopted from
|
||||
https://github.com/Nicell/alien-signals-luau
|
||||
https://github.com/stackblitz/alien-signals
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024-present Johnson Chu, Nicholas Winans
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
]=]
|
||||
|
||||
local system = require("./system")
|
||||
|
||||
export type ReactiveFlags = system.ReactiveFlags
|
||||
export type ReactiveNode = system.ReactiveNode
|
||||
export type Link = system.Link
|
||||
|
||||
local ReactiveFlags = system.ReactiveFlags
|
||||
local createReactiveSystem = system.createReactiveSystem
|
||||
|
||||
local NONE = 0b0000000
|
||||
local MUTABLE = 0b0000001
|
||||
local WATCHING = 0b0000010
|
||||
local DIRTY = 0b0010000
|
||||
local PENDING = 0b0100000
|
||||
local QUEUED = 0b1000000
|
||||
|
||||
type EffectScope = ReactiveNode & {
|
||||
cleanup: (() -> ())?,
|
||||
}
|
||||
|
||||
type Effect = ReactiveNode & {
|
||||
fn: () -> (() -> ())?,
|
||||
cleanup: (() -> ())?,
|
||||
}
|
||||
|
||||
type Computed<T = any> = ReactiveNode & {
|
||||
value: T?,
|
||||
getter: (previousValue: T?) -> T,
|
||||
}
|
||||
|
||||
type Signal<T = any> = ReactiveNode & {
|
||||
previousValue: T,
|
||||
value: T,
|
||||
}
|
||||
|
||||
local pauseStack: { ReactiveNode? } = {}
|
||||
local pauseStackCount = 0
|
||||
local queuedEffects: { Effect | EffectScope } = {}
|
||||
|
||||
local link: (dep: ReactiveNode, sub: ReactiveNode) -> ()
|
||||
local unlink: (link: Link, sub: ReactiveNode?) -> Link?
|
||||
local propagate: (link: Link) -> ()
|
||||
local checkDirty: (link: Link, sub: ReactiveNode) -> boolean
|
||||
local endTracking: (sub: ReactiveNode) -> ()
|
||||
local startTracking: (sub: ReactiveNode) -> ()
|
||||
local shallowPropagate: (link: Link) -> ()
|
||||
|
||||
local batchDepth = 0
|
||||
|
||||
local notifyIndex = 0
|
||||
local queuedEffectsLength = 0
|
||||
local activeSub: ReactiveNode?
|
||||
local activeScope: EffectScope?
|
||||
|
||||
local function getCurrentSub(): ReactiveNode?
|
||||
return activeSub
|
||||
end
|
||||
|
||||
local function setCurrentSub(sub: ReactiveNode?)
|
||||
local prevSub = activeSub
|
||||
activeSub = sub
|
||||
return prevSub
|
||||
end
|
||||
|
||||
local function getCurrentScope(): EffectScope?
|
||||
return activeScope
|
||||
end
|
||||
|
||||
local function setCurrentScope(scope: EffectScope?)
|
||||
local prevScope = activeScope
|
||||
activeScope = scope
|
||||
return prevScope
|
||||
end
|
||||
|
||||
local function pauseTracking()
|
||||
pauseStackCount += 1
|
||||
pauseStack[pauseStackCount] = activeSub
|
||||
activeSub = nil
|
||||
end
|
||||
|
||||
local function resumeTracking()
|
||||
activeSub = pauseStack[pauseStackCount]
|
||||
pauseStack[pauseStackCount] = nil
|
||||
pauseStackCount -= 1
|
||||
end
|
||||
|
||||
local function cleanup(e: ReactiveNode)
|
||||
local e = e :: EffectScope
|
||||
local cleanup = e.cleanup
|
||||
if cleanup then
|
||||
e.cleanup = nil
|
||||
local prev = setCurrentSub(nil)
|
||||
local success, result: unknown = pcall(cleanup)
|
||||
setCurrentSub(prev)
|
||||
if not success then
|
||||
error(result)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function run(e: EffectScope, flags: ReactiveFlags): ()
|
||||
if bit32.band(flags, DIRTY) ~= 0 or (bit32.band(flags, PENDING) ~= 0 and checkDirty(e.deps :: Link, e)) then
|
||||
cleanup(e)
|
||||
local prev = setCurrentSub(e)
|
||||
startTracking(e)
|
||||
local success, result = pcall((e :: Effect).fn)
|
||||
setCurrentSub(prev)
|
||||
endTracking(e)
|
||||
if not success then
|
||||
error(result)
|
||||
end
|
||||
e.cleanup = result
|
||||
return
|
||||
elseif bit32.band(flags, PENDING) ~= 0 then
|
||||
e.flags = bit32.band(flags, bit32.bnot(PENDING))
|
||||
end
|
||||
local link = e.deps
|
||||
while link do
|
||||
local dep = link.dep
|
||||
local depFlags = dep.flags
|
||||
if bit32.band(depFlags, QUEUED) ~= 0 then
|
||||
depFlags = bit32.band(depFlags, bit32.bnot(QUEUED))
|
||||
dep.flags = depFlags
|
||||
run(dep :: EffectScope, depFlags)
|
||||
end
|
||||
link = link.nextDep
|
||||
end
|
||||
end
|
||||
|
||||
local function flush(): ()
|
||||
while notifyIndex < queuedEffectsLength do
|
||||
notifyIndex += 1
|
||||
local effect = queuedEffects[notifyIndex] :: EffectScope
|
||||
queuedEffects[notifyIndex] = nil
|
||||
effect.flags = bit32.band(effect.flags, bit32.bnot(QUEUED))
|
||||
run(effect, effect.flags)
|
||||
end
|
||||
notifyIndex = 0
|
||||
queuedEffectsLength = 0
|
||||
end
|
||||
|
||||
local function getBatchDepth(): number
|
||||
return batchDepth
|
||||
end
|
||||
|
||||
local function startBatch()
|
||||
batchDepth += 1
|
||||
end
|
||||
|
||||
local function endBatch()
|
||||
batchDepth -= 1
|
||||
if batchDepth == 0 then
|
||||
flush()
|
||||
end
|
||||
end
|
||||
|
||||
local function updateComputed(c: Computed): boolean
|
||||
local prevSub = setCurrentSub(c)
|
||||
startTracking(c)
|
||||
local oldValue = c.value
|
||||
local success, result = pcall(c.getter, oldValue)
|
||||
setCurrentSub(prevSub)
|
||||
endTracking(c)
|
||||
if not success then
|
||||
error(result)
|
||||
end
|
||||
if oldValue ~= result then
|
||||
c.value = result
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
local function updateSignal(s: Signal, value: any): boolean
|
||||
s.flags = MUTABLE
|
||||
if s.previousValue ~= value then
|
||||
s.previousValue = value
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
local function signalOper<T>(this: Signal<T>, ...: T): T?
|
||||
if select("#", ...) > 0 then
|
||||
local newValue = (...)
|
||||
if this.value ~= newValue then
|
||||
this.value = newValue
|
||||
this.flags = bit32.bor(MUTABLE, DIRTY)
|
||||
local subs = this.subs
|
||||
if subs then
|
||||
propagate(subs)
|
||||
if batchDepth == 0 then
|
||||
flush()
|
||||
end
|
||||
end
|
||||
end
|
||||
return
|
||||
else
|
||||
local value = this.value
|
||||
if bit32.band(this.flags, DIRTY) ~= 0 then
|
||||
if updateSignal(this, value) then
|
||||
local subs = this.subs
|
||||
if subs then
|
||||
shallowPropagate(subs)
|
||||
end
|
||||
end
|
||||
end
|
||||
if activeSub then
|
||||
link(this, activeSub)
|
||||
end
|
||||
return value
|
||||
end
|
||||
end
|
||||
|
||||
local function computedOper<T>(this: Computed<T>): T
|
||||
local flags = this.flags
|
||||
if bit32.band(flags, DIRTY) ~= 0 or (bit32.band(flags, PENDING) ~= 0 and checkDirty(this.deps :: Link, this)) then
|
||||
if updateComputed(this) then
|
||||
local subs = this.subs
|
||||
if subs then
|
||||
shallowPropagate(subs)
|
||||
end
|
||||
end
|
||||
elseif bit32.band(flags, PENDING) ~= 0 then
|
||||
this.flags = bit32.band(flags, bit32.bnot(PENDING))
|
||||
end
|
||||
if activeSub then
|
||||
link(this, activeSub)
|
||||
elseif activeScope then
|
||||
link(this, activeScope)
|
||||
end
|
||||
return this.value :: T
|
||||
end
|
||||
|
||||
local function effectOper(this: EffectScope): ()
|
||||
local dep = this.deps
|
||||
while dep do
|
||||
dep = unlink(dep, this)
|
||||
end
|
||||
local sub = this.subs
|
||||
while sub do
|
||||
unlink(sub)
|
||||
sub = this.subs
|
||||
end
|
||||
this.flags = NONE
|
||||
cleanup(this)
|
||||
end
|
||||
|
||||
local signalMetatable = { __call = signalOper }
|
||||
local computedMetatable = { __call = computedOper }
|
||||
local effectMetatable = { __call = effectOper }
|
||||
|
||||
local function signal<T>(initialValue: T): ((T) -> ()) & (() -> T)
|
||||
local this: Signal<T> = {
|
||||
previousValue = initialValue,
|
||||
value = initialValue,
|
||||
flags = MUTABLE,
|
||||
}
|
||||
return setmetatable(this, signalMetatable) :: any
|
||||
end
|
||||
|
||||
local function computed<T>(getter: (previousValue: T?) -> T): () -> T
|
||||
local this: Computed<T> = {
|
||||
flags = bit32.bor(MUTABLE, DIRTY),
|
||||
getter = getter,
|
||||
}
|
||||
return setmetatable(this, computedMetatable) :: any
|
||||
end
|
||||
|
||||
local function effect(fn: () -> ...() -> ()?): () -> ()
|
||||
local e: Effect = {
|
||||
fn = fn,
|
||||
flags = WATCHING,
|
||||
}
|
||||
if activeSub then
|
||||
link(e, activeSub)
|
||||
elseif activeScope then
|
||||
link(e, activeScope)
|
||||
end
|
||||
local prev = setCurrentSub(e)
|
||||
local success, result = pcall(fn)
|
||||
setCurrentSub(prev)
|
||||
if not success then
|
||||
error(result)
|
||||
end
|
||||
e.cleanup = result
|
||||
return setmetatable(e, effectMetatable) :: any
|
||||
end
|
||||
|
||||
local function effectScope(fn: () -> ...() -> ()?): () -> ()
|
||||
local e: EffectScope = {
|
||||
flags = NONE,
|
||||
}
|
||||
if activeScope then
|
||||
link(e, activeScope)
|
||||
end
|
||||
local prev = setCurrentScope(e)
|
||||
local success, result = pcall(fn)
|
||||
setCurrentScope(prev)
|
||||
if not success then
|
||||
error(result)
|
||||
end
|
||||
e.cleanup = result
|
||||
return setmetatable(e, effectMetatable) :: any
|
||||
end
|
||||
|
||||
local function notify(e: ReactiveNode)
|
||||
local flags = e.flags
|
||||
if bit32.band(flags, QUEUED) == 0 then
|
||||
e.flags = bit32.bor(flags, QUEUED)
|
||||
local subs = e.subs
|
||||
if subs then
|
||||
notify(subs.sub :: Effect | EffectScope)
|
||||
else
|
||||
queuedEffectsLength += 1
|
||||
queuedEffects[queuedEffectsLength] = e :: Effect | EffectScope
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function update(signal: ReactiveNode): boolean
|
||||
if (signal :: Computed).getter then
|
||||
return updateComputed(signal :: Computed)
|
||||
else
|
||||
return updateSignal(signal :: Signal, (signal :: Signal).value)
|
||||
end
|
||||
end
|
||||
|
||||
local function unwatched(signal: ReactiveNode)
|
||||
local toRemove = signal.deps
|
||||
if toRemove then
|
||||
repeat
|
||||
toRemove = unlink(toRemove, signal)
|
||||
until not toRemove
|
||||
signal.flags = bit32.bor(signal.flags, DIRTY)
|
||||
end
|
||||
cleanup(signal)
|
||||
end
|
||||
|
||||
local reactiveSystem = createReactiveSystem({
|
||||
update = update,
|
||||
notify = notify,
|
||||
unwatched = unwatched,
|
||||
})
|
||||
|
||||
link = reactiveSystem.link
|
||||
unlink = reactiveSystem.unlink
|
||||
propagate = reactiveSystem.propagate
|
||||
checkDirty = reactiveSystem.checkDirty
|
||||
endTracking = reactiveSystem.endTracking
|
||||
startTracking = reactiveSystem.startTracking
|
||||
shallowPropagate = reactiveSystem.shallowPropagate
|
||||
|
||||
return {
|
||||
ReactiveFlags = ReactiveFlags,
|
||||
createReactiveSystem = createReactiveSystem,
|
||||
getCurrentSub = getCurrentSub,
|
||||
setCurrentSub = setCurrentSub,
|
||||
getCurrentScope = getCurrentScope,
|
||||
setCurrentScope = setCurrentScope,
|
||||
getBatchDepth = getBatchDepth,
|
||||
startBatch = startBatch,
|
||||
endBatch = endBatch,
|
||||
pauseTracking = pauseTracking,
|
||||
resumeTracking = resumeTracking,
|
||||
signal = signal,
|
||||
computed = computed,
|
||||
effect = effect,
|
||||
effectScope = effectScope,
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
local freeThread: thread?
|
||||
|
||||
local function run<T...>(thread: thread, fn: (T...) -> (), ...: T...): boolean
|
||||
fn(...)
|
||||
if freeThread then
|
||||
return true
|
||||
else
|
||||
freeThread = thread
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
local function runner()
|
||||
local quit = false
|
||||
repeat
|
||||
quit = run(coroutine.yield())
|
||||
until quit
|
||||
end
|
||||
|
||||
local function spawn<T...>(callback: (T...) -> (), ...: T...)
|
||||
local thread
|
||||
|
||||
if freeThread then
|
||||
thread = freeThread
|
||||
freeThread = nil
|
||||
elseif task then
|
||||
thread = task.spawn(runner)
|
||||
else
|
||||
thread = coroutine.create(runner)
|
||||
coroutine.resume(thread)
|
||||
end
|
||||
|
||||
coroutine.resume(thread, thread, callback, ...)
|
||||
end
|
||||
|
||||
return spawn
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
local signals = require("./signals")
|
||||
local spawn = require("./spawn")
|
||||
|
||||
local effect = signals.effect
|
||||
local pauseTracking = signals.pauseTracking
|
||||
local resumeTracking = signals.resumeTracking
|
||||
|
||||
local function subscribe<T>(selector: () -> T, listener: (T, T) -> ()): () -> ()
|
||||
local current: T
|
||||
local mounted = true
|
||||
local dispose: () -> ()
|
||||
|
||||
dispose = effect(function()
|
||||
local value = selector()
|
||||
|
||||
if mounted then
|
||||
current = value
|
||||
mounted = false
|
||||
elseif value ~= current then
|
||||
local previous = current
|
||||
current = value
|
||||
|
||||
pauseTracking()
|
||||
spawn(listener, value, previous)
|
||||
resumeTracking()
|
||||
end
|
||||
end)
|
||||
|
||||
return function()
|
||||
dispose()
|
||||
end
|
||||
end
|
||||
|
||||
return subscribe
|
||||
|
|
@ -1,357 +0,0 @@
|
|||
--[=[
|
||||
alien-signals 2.0.1 implementation adopted from
|
||||
https://github.com/Nicell/alien-signals-luau
|
||||
https://github.com/stackblitz/alien-signals
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024-present Johnson Chu, Nicholas Winans
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
]=]
|
||||
|
||||
export type ReactiveNode = {
|
||||
deps: Link?,
|
||||
depsTail: Link?,
|
||||
subs: Link?,
|
||||
subsTail: Link?,
|
||||
flags: ReactiveFlags,
|
||||
}
|
||||
|
||||
export type Link = {
|
||||
dep: ReactiveNode,
|
||||
sub: ReactiveNode,
|
||||
prevSub: Link?,
|
||||
nextSub: Link?,
|
||||
prevDep: Link?,
|
||||
nextDep: Link?,
|
||||
}
|
||||
|
||||
type Stack<T> = {
|
||||
value: T,
|
||||
prev: Stack<T>?,
|
||||
}
|
||||
|
||||
export type ReactiveFlags = number
|
||||
|
||||
local NONE = 0b0000000
|
||||
local MUTABLE = 0b0000001
|
||||
local WATCHING = 0b0000010
|
||||
local RECURSED_CHECK = 0b0000100
|
||||
local RECURSED = 0b0001000
|
||||
local DIRTY = 0b0010000
|
||||
local PENDING = 0b0100000
|
||||
|
||||
local ReactiveFlags = {
|
||||
None = NONE,
|
||||
Mutable = MUTABLE,
|
||||
Watching = WATCHING,
|
||||
RecursedCheck = RECURSED_CHECK,
|
||||
Recursed = RECURSED,
|
||||
Dirty = DIRTY,
|
||||
Pending = PENDING,
|
||||
}
|
||||
|
||||
local function createReactiveSystem(props: {
|
||||
update: (sub: ReactiveNode) -> boolean,
|
||||
notify: (sub: ReactiveNode) -> (),
|
||||
unwatched: (sub: ReactiveNode) -> (),
|
||||
})
|
||||
local update = props.update
|
||||
local notify = props.notify
|
||||
local unwatched = props.unwatched
|
||||
|
||||
local function isValidLink(checkLink: Link, sub: ReactiveNode): boolean
|
||||
local depsTail = sub.depsTail
|
||||
if depsTail then
|
||||
local link = sub.deps :: Link
|
||||
repeat
|
||||
if link == checkLink then
|
||||
return true
|
||||
end
|
||||
if link == depsTail then
|
||||
break
|
||||
end
|
||||
link = link.nextDep :: Link
|
||||
until not link
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function link(dep: ReactiveNode, sub: ReactiveNode): ()
|
||||
local prevDep = sub.depsTail
|
||||
if prevDep and prevDep.dep == dep then
|
||||
return
|
||||
end
|
||||
local nextDep: Link?
|
||||
local recursedCheck = bit32.band(sub.flags, RECURSED_CHECK) ~= 0
|
||||
if recursedCheck then
|
||||
nextDep = if prevDep then prevDep.nextDep else sub.deps
|
||||
if nextDep and nextDep.dep == dep then
|
||||
sub.depsTail = nextDep
|
||||
return
|
||||
end
|
||||
end
|
||||
local prevSub = dep.subsTail
|
||||
if prevSub and prevSub.sub == sub and (not recursedCheck or isValidLink(prevSub, sub)) then
|
||||
return
|
||||
end
|
||||
local newLink: Link = {
|
||||
dep = dep,
|
||||
sub = sub,
|
||||
prevDep = prevDep,
|
||||
nextDep = nextDep,
|
||||
prevSub = prevSub,
|
||||
}
|
||||
sub.depsTail = newLink
|
||||
dep.subsTail = newLink
|
||||
if nextDep then
|
||||
nextDep.prevDep = newLink
|
||||
end
|
||||
if prevDep then
|
||||
prevDep.nextDep = newLink
|
||||
else
|
||||
sub.deps = newLink
|
||||
end
|
||||
if prevSub then
|
||||
prevSub.nextSub = newLink
|
||||
else
|
||||
dep.subs = newLink
|
||||
end
|
||||
end
|
||||
|
||||
local function unlink(link: Link, sub: ReactiveNode?): Link?
|
||||
local sub = sub or link.sub
|
||||
local dep = link.dep
|
||||
local prevDep = link.prevDep
|
||||
local nextDep = link.nextDep
|
||||
local nextSub = link.nextSub
|
||||
local prevSub = link.prevSub
|
||||
if nextDep then
|
||||
nextDep.prevDep = prevDep
|
||||
else
|
||||
sub.depsTail = prevDep
|
||||
end
|
||||
if prevDep then
|
||||
prevDep.nextDep = nextDep
|
||||
else
|
||||
sub.deps = nextDep
|
||||
end
|
||||
if nextSub then
|
||||
nextSub.prevSub = prevSub
|
||||
else
|
||||
dep.subsTail = prevSub
|
||||
end
|
||||
if prevSub then
|
||||
prevSub.nextSub = nextSub
|
||||
else
|
||||
dep.subs = nextSub
|
||||
if not nextSub then
|
||||
unwatched(dep)
|
||||
end
|
||||
end
|
||||
return nextDep
|
||||
end
|
||||
|
||||
local function propagate(link: Link): ()
|
||||
local next = link.nextSub
|
||||
local stack: Stack<Link?>?
|
||||
|
||||
repeat
|
||||
local sub = link.sub
|
||||
|
||||
local flags = sub.flags
|
||||
|
||||
if bit32.band(flags, bit32.bor(MUTABLE, WATCHING)) ~= 0 then
|
||||
if bit32.band(flags, bit32.bor(RECURSED_CHECK, RECURSED, DIRTY, PENDING)) == 0 then
|
||||
sub.flags = bit32.bor(flags, PENDING)
|
||||
elseif bit32.band(flags, bit32.bor(RECURSED_CHECK, RECURSED)) == 0 then
|
||||
flags = NONE
|
||||
elseif bit32.band(flags, RECURSED_CHECK) == 0 then
|
||||
sub.flags = bit32.bor(bit32.band(flags, bit32.bnot(RECURSED)), PENDING)
|
||||
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
|
||||
-- selene: allow(if_same_then_else)
|
||||
flags = NONE
|
||||
end
|
||||
|
||||
if bit32.band(flags, WATCHING) ~= 0 then
|
||||
notify(sub)
|
||||
end
|
||||
|
||||
if bit32.band(flags, MUTABLE) ~= 0 then
|
||||
local subSubs = sub.subs
|
||||
if subSubs then
|
||||
link = subSubs
|
||||
if subSubs.nextSub then
|
||||
stack = { value = next, prev = stack }
|
||||
next = link.nextSub
|
||||
end
|
||||
continue
|
||||
end
|
||||
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
|
||||
end
|
||||
|
||||
local function startTracking(sub: ReactiveNode): ()
|
||||
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): ()
|
||||
repeat
|
||||
local sub = link.sub
|
||||
local subFlags = sub.flags
|
||||
if bit32.band(subFlags, bit32.bor(PENDING, DIRTY)) == PENDING then
|
||||
sub.flags = bit32.bor(subFlags, DIRTY)
|
||||
if bit32.band(subFlags, WATCHING) ~= 0 then
|
||||
notify(sub)
|
||||
end
|
||||
end
|
||||
link = link.nextSub :: Link
|
||||
until not link
|
||||
end
|
||||
|
||||
local function checkDirty(link: Link, sub: ReactiveNode): boolean
|
||||
local stack: Stack<Link>?
|
||||
local checkDepth = 0
|
||||
|
||||
repeat
|
||||
local dep = link.dep
|
||||
local depFlags = dep.flags
|
||||
|
||||
local dirty = false
|
||||
|
||||
if bit32.band(sub.flags, DIRTY) ~= 0 then
|
||||
dirty = 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
|
||||
end
|
||||
elseif bit32.band(depFlags, 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 and link.nextDep then
|
||||
link = link.nextDep
|
||||
continue
|
||||
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)
|
||||
end
|
||||
sub = link.sub
|
||||
continue
|
||||
end
|
||||
else
|
||||
sub.flags = bit32.band(sub.flags, bit32.bnot(PENDING))
|
||||
end
|
||||
sub = link.sub
|
||||
if link.nextDep then
|
||||
link = link.nextDep
|
||||
continueTop = true
|
||||
break
|
||||
end
|
||||
dirty = false
|
||||
end
|
||||
|
||||
if continueTop then
|
||||
continue
|
||||
end
|
||||
|
||||
return dirty
|
||||
until false
|
||||
end
|
||||
|
||||
return {
|
||||
link = link,
|
||||
unlink = unlink,
|
||||
propagate = propagate,
|
||||
checkDirty = checkDirty,
|
||||
endTracking = endTracking,
|
||||
startTracking = startTracking,
|
||||
shallowPropagate = shallowPropagate,
|
||||
}
|
||||
end
|
||||
|
||||
return {
|
||||
ReactiveFlags = ReactiveFlags,
|
||||
createReactiveSystem = createReactiveSystem,
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
export type Selector<T = any> = () -> T
|
||||
|
||||
export type Atom<T = any> = ((((T) -> T) | T) -> T) & (() -> T)
|
||||
|
||||
export type AtomOptions<T = any> = {
|
||||
equals: ((prev: T, next: T) -> boolean)?,
|
||||
}
|
||||
|
||||
return {}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
local signals = require("./signals")
|
||||
|
||||
local pauseTracking = signals.pauseTracking
|
||||
local resumeTracking = signals.resumeTracking
|
||||
|
||||
local function untracked<T, Args...>(callback: ((Args...) -> T) | (Args...) -> (), ...: Args...): T
|
||||
pauseTracking()
|
||||
local success, result = pcall(callback, ...)
|
||||
resumeTracking()
|
||||
|
||||
if not success then
|
||||
error(result)
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
return untracked
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
[package]
|
||||
name = "littensy/charm"
|
||||
version = "0.10.0"
|
||||
registry = "https://github.com/UpliftGames/wally-index"
|
||||
realm = "shared"
|
||||
|
||||
[dependencies]
|
||||
Loading…
Add table
Add a link
Reference in a new issue