mirror of
https://github.com/littensy/charm.git
synced 2026-10-21 19:54:09 +00:00
feat: add peek and roblox-ts types
This commit is contained in:
parent
d2979ebd4c
commit
708bc55de7
37 changed files with 363 additions and 1234 deletions
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"name": "charm",
|
||||
"tree": {
|
||||
"$path": "src"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
local core = require("./core")
|
||||
|
||||
local startBatch = core.startBatch
|
||||
local endBatch = core.endBatch
|
||||
|
||||
local function handleBatched(ok: boolean, ...)
|
||||
endBatch()
|
||||
if ok then
|
||||
return ...
|
||||
else
|
||||
error((...))
|
||||
end
|
||||
end
|
||||
|
||||
local function batched<Args..., Results...>(callback: (Args...) -> Results..., ...: Args...): Results...
|
||||
startBatch()
|
||||
return handleBatched(pcall(callback, ...))
|
||||
end
|
||||
|
||||
return batched
|
||||
|
|
@ -1,445 +0,0 @@
|
|||
--[[
|
||||
alien-signals 2.0.5 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.
|
||||
]]
|
||||
|
||||
local strict = require("./strict")
|
||||
local system = require("./system")
|
||||
local utils = require("./utils")
|
||||
|
||||
export type ReactiveFlags = system.ReactiveFlags
|
||||
export type ReactiveNode = system.ReactiveNode
|
||||
export type Link = system.Link
|
||||
|
||||
local ReactiveFlags = system.ReactiveFlags
|
||||
local createReactiveSystem = system.createReactiveSystem
|
||||
local noYield = utils.noYield
|
||||
|
||||
local NONE = 0b0000000
|
||||
local MUTABLE = 0b0000001
|
||||
local WATCHING = 0b0000010
|
||||
local DIRTY = 0b0010000
|
||||
local PENDING = 0b0100000
|
||||
local QUEUED = 0b1000000
|
||||
|
||||
type EffectScope = ReactiveNode & {
|
||||
cleanups: { () -> () },
|
||||
}
|
||||
|
||||
type Effect = ReactiveNode & {
|
||||
fn: () -> ...(() -> ())?,
|
||||
cleanups: { () -> () },
|
||||
}
|
||||
|
||||
type Computed<T = any> = ReactiveNode & {
|
||||
value: T?,
|
||||
getter: (previousValue: T?) -> T,
|
||||
equals: ((T, T) -> boolean)?,
|
||||
}
|
||||
|
||||
type Atom<T = any> = ReactiveNode & {
|
||||
previousValue: T,
|
||||
value: T,
|
||||
equals: ((T, T) -> boolean)?,
|
||||
}
|
||||
|
||||
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 queuedEffects: { Effect | EffectScope? } = {}
|
||||
local queuedEffectsLength = 0
|
||||
local notifyIndex = 0
|
||||
local activeSubTracking = true
|
||||
local activeSub: ReactiveNode?
|
||||
local activeScope: Effect | EffectScope?
|
||||
|
||||
local function getCurrentSub(): ReactiveNode?
|
||||
return activeSub
|
||||
end
|
||||
|
||||
local function setCurrentSub(sub: ReactiveNode?)
|
||||
local prevSub = activeSub
|
||||
activeSub = sub
|
||||
return prevSub
|
||||
end
|
||||
|
||||
local function getCurrentScope(): Effect | EffectScope?
|
||||
return activeScope
|
||||
end
|
||||
|
||||
local function setCurrentScope(scope: Effect | EffectScope?)
|
||||
local prevScope = activeScope
|
||||
activeScope = scope
|
||||
return prevScope
|
||||
end
|
||||
|
||||
local function setCurrentTracking(tracking: boolean): boolean
|
||||
local prevTracking = activeSubTracking
|
||||
activeSubTracking = tracking
|
||||
return prevTracking
|
||||
end
|
||||
|
||||
local function runCleanups(effect: EffectScope)
|
||||
for _, cleanup in effect.cleanups do
|
||||
local prev = setCurrentSub(nil)
|
||||
local success, result: unknown = (if strict.enabled then noYield else pcall)(cleanup)
|
||||
setCurrentSub(prev)
|
||||
if not success then
|
||||
error(result)
|
||||
end
|
||||
end
|
||||
table.clear(effect.cleanups)
|
||||
end
|
||||
|
||||
local function onCleanup(cleanup: () -> (), failSilently: boolean?)
|
||||
if activeSub then
|
||||
table.insert((activeSub :: Effect).cleanups, cleanup)
|
||||
elseif activeScope then
|
||||
table.insert(activeScope.cleanups, cleanup)
|
||||
elseif not failSilently then
|
||||
warn(debug.traceback("onCleanup() can only be called inside an effect or a scope.", 2))
|
||||
end
|
||||
end
|
||||
|
||||
local function run(effect: Effect | EffectScope, flags: ReactiveFlags): ()
|
||||
if bit32.btest(flags, DIRTY) or (bit32.btest(flags, PENDING) and checkDirty(effect.deps :: Link, effect)) then
|
||||
runCleanups(effect)
|
||||
local prev = setCurrentSub(effect)
|
||||
local prevTracking = setCurrentTracking(true)
|
||||
startTracking(effect)
|
||||
local success, result: (() -> ())? = (if strict.enabled then noYield else pcall)((effect :: Effect).fn)
|
||||
setCurrentSub(prev)
|
||||
endTracking(effect)
|
||||
setCurrentTracking(prevTracking)
|
||||
if not success then
|
||||
error(result)
|
||||
elseif result then
|
||||
table.insert(effect.cleanups, result)
|
||||
end
|
||||
return
|
||||
elseif bit32.btest(flags, PENDING) then
|
||||
(effect :: Effect).flags = bit32.band(flags, bit32.bnot(PENDING))
|
||||
end
|
||||
local link = effect.deps
|
||||
while link do
|
||||
local dep = link.dep
|
||||
local depFlags = dep.flags
|
||||
if bit32.btest(depFlags, QUEUED) then
|
||||
depFlags = bit32.band(depFlags, bit32.bnot(QUEUED))
|
||||
dep.flags = depFlags
|
||||
run(dep :: Effect, depFlags)
|
||||
end
|
||||
link = link.nextDep
|
||||
end
|
||||
end
|
||||
|
||||
local function flush(): ()
|
||||
while notifyIndex < queuedEffectsLength do
|
||||
notifyIndex += 1
|
||||
local effect = queuedEffects[notifyIndex] :: Effect
|
||||
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(computed: Computed): boolean
|
||||
local prevSub = setCurrentSub(computed)
|
||||
local prevTracking = setCurrentTracking(true)
|
||||
startTracking(computed)
|
||||
local oldValue = computed.value
|
||||
local success, result = (if strict.enabled then noYield else pcall)(computed.getter, oldValue)
|
||||
setCurrentSub(prevSub)
|
||||
endTracking(computed)
|
||||
setCurrentTracking(prevTracking)
|
||||
if not success then
|
||||
error(result)
|
||||
end
|
||||
local equals = computed.equals
|
||||
if if equals then not equals(oldValue, result) else 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 activeSubTracking 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 :: T) else oldValue ~= newValue then
|
||||
atom.value = newValue
|
||||
atom.flags = bit32.bor(MUTABLE, DIRTY)
|
||||
local subs = atom.subs
|
||||
if subs then
|
||||
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 strict.enabled 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
|
||||
|
||||
local function computedGetter<T>(computed: Computed<T>): () -> T
|
||||
return function()
|
||||
local flags = computed.flags
|
||||
if
|
||||
bit32.btest(flags, DIRTY) or (bit32.btest(flags, PENDING) and checkDirty(computed.deps :: Link, computed))
|
||||
then
|
||||
if updateComputed(computed) then
|
||||
local subs = computed.subs
|
||||
if subs then
|
||||
shallowPropagate(subs)
|
||||
end
|
||||
end
|
||||
elseif bit32.btest(flags, PENDING) then
|
||||
computed.flags = bit32.band(flags, bit32.bnot(PENDING))
|
||||
end
|
||||
if activeSub then
|
||||
if activeSubTracking then
|
||||
link(computed, activeSub)
|
||||
end
|
||||
elseif activeScope then
|
||||
link(computed, activeScope)
|
||||
end
|
||||
return computed.value :: T
|
||||
end
|
||||
end
|
||||
|
||||
local function effectStop(self: Effect | EffectScope): () -> ()
|
||||
return function()
|
||||
local dep = self.deps
|
||||
while dep do
|
||||
dep = unlink(dep, self)
|
||||
end
|
||||
local sub = self.subs
|
||||
if sub then
|
||||
unlink(sub)
|
||||
end
|
||||
(self :: Effect).flags = NONE
|
||||
runCleanups(self)
|
||||
end
|
||||
end
|
||||
|
||||
local function update(atom: Atom | Computed): boolean
|
||||
if (atom :: Computed).getter then
|
||||
return updateComputed(atom :: Computed)
|
||||
else
|
||||
return updateAtom(atom :: Atom, atom.value)
|
||||
end
|
||||
end
|
||||
|
||||
local function notify(effect: Effect | EffectScope)
|
||||
local flags = effect.flags
|
||||
if not bit32.btest(flags, QUEUED) then
|
||||
(effect :: Effect).flags = bit32.bor(flags, QUEUED)
|
||||
local subs = effect.subs
|
||||
if subs then
|
||||
notify(subs.sub :: Effect)
|
||||
else
|
||||
queuedEffectsLength += 1
|
||||
queuedEffects[queuedEffectsLength] = effect
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function unwatched(node: Atom | Computed | Effect | EffectScope)
|
||||
if (node :: Computed).getter then
|
||||
local toRemove = node.deps
|
||||
if toRemove then
|
||||
(node :: Computed).flags = bit32.bor(MUTABLE, DIRTY)
|
||||
repeat
|
||||
toRemove = unlink(toRemove, node)
|
||||
until not toRemove
|
||||
end
|
||||
elseif (node :: Effect).cleanups then
|
||||
effectStop(node :: Effect)()
|
||||
end
|
||||
end
|
||||
|
||||
local function atom<T>(initialValue: T, equals: ((T, T) -> boolean)?): (...((T) -> T) | T) -> T
|
||||
return atomGetterSetter({
|
||||
previousValue = initialValue,
|
||||
value = initialValue,
|
||||
flags = MUTABLE,
|
||||
equals = equals,
|
||||
})
|
||||
end
|
||||
|
||||
local function computed<T>(getter: (previousValue: T?) -> T, equals: ((T, T) -> boolean)?): () -> T
|
||||
return computedGetter({
|
||||
flags = bit32.bor(MUTABLE, DIRTY),
|
||||
getter = getter,
|
||||
equals = equals,
|
||||
})
|
||||
end
|
||||
|
||||
local function effect(fn: () -> ...(() -> ())?): () -> ()
|
||||
local self: Effect = {
|
||||
fn = fn,
|
||||
flags = WATCHING,
|
||||
cleanups = {},
|
||||
}
|
||||
if activeSub then
|
||||
link(self, activeSub)
|
||||
elseif activeScope then
|
||||
link(self, activeScope)
|
||||
end
|
||||
local prev = setCurrentSub(self)
|
||||
local prevTracking = setCurrentTracking(true)
|
||||
local success, result: (() -> ())? = (if strict.enabled then noYield else pcall)(fn)
|
||||
setCurrentSub(prev)
|
||||
setCurrentTracking(prevTracking)
|
||||
if not success then
|
||||
error(result)
|
||||
elseif result then
|
||||
table.insert(self.cleanups, result)
|
||||
end
|
||||
return effectStop(self)
|
||||
end
|
||||
|
||||
local function effectScope(fn: () -> ...(() -> ())?, detached: boolean?): () -> ()
|
||||
local self: EffectScope = {
|
||||
flags = NONE,
|
||||
cleanups = {},
|
||||
}
|
||||
if activeScope and not detached then
|
||||
link(self, activeScope)
|
||||
end
|
||||
local prevSub = setCurrentSub(nil)
|
||||
local prevScope = setCurrentScope(self)
|
||||
local success, result: (() -> ())? = (if strict.enabled then noYield else pcall)(fn)
|
||||
setCurrentScope(prevScope)
|
||||
setCurrentSub(prevSub)
|
||||
if not success then
|
||||
error(result)
|
||||
elseif result then
|
||||
table.insert(self.cleanups, result)
|
||||
end
|
||||
return effectStop(self)
|
||||
end
|
||||
|
||||
local reactiveSystem = createReactiveSystem({
|
||||
update = update :: (ReactiveNode) -> boolean,
|
||||
notify = notify :: (ReactiveNode) -> (),
|
||||
unwatched = unwatched :: (ReactiveNode) -> (),
|
||||
})
|
||||
|
||||
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,
|
||||
setCurrentTracking = setCurrentTracking,
|
||||
onCleanup = onCleanup,
|
||||
getBatchDepth = getBatchDepth,
|
||||
startBatch = startBatch,
|
||||
endBatch = endBatch,
|
||||
atom = atom,
|
||||
computed = computed,
|
||||
effect = effect,
|
||||
effectScope = effectScope,
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
local batched = require("@self/batched")
|
||||
local core = require("@self/core")
|
||||
local listen = require("@self/listen")
|
||||
local mapped = require("@self/mapped")
|
||||
local observe = require("@self/observe")
|
||||
local strict = require("@self/strict")
|
||||
local subscribe = require("@self/subscribe")
|
||||
local types = require("@self/types")
|
||||
local untracked = require("@self/untracked")
|
||||
|
||||
export type Atom<T = any> = types.Atom<T>
|
||||
export type Selector<T = any> = types.Selector<T>
|
||||
|
||||
local atom = core.atom
|
||||
local computed = core.computed
|
||||
local effect = core.effect
|
||||
local effectScope = core.effectScope
|
||||
local onCleanup = core.onCleanup
|
||||
|
||||
return {
|
||||
core = core,
|
||||
strict = strict,
|
||||
atom = atom,
|
||||
batched = batched,
|
||||
computed = computed,
|
||||
effect = effect,
|
||||
effectScope = effectScope,
|
||||
listen = listen,
|
||||
mapped = mapped,
|
||||
observe = observe,
|
||||
onCleanup = onCleanup,
|
||||
subscribe = subscribe,
|
||||
untracked = untracked,
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
local core = require("./core")
|
||||
local untracked = require("./untracked")
|
||||
|
||||
local computed = core.computed
|
||||
local effect = core.effect
|
||||
|
||||
local function listen<T>(selector: () -> T, listener: (state: T, previous: T?, dispose: () -> ()) -> ()): () -> ()
|
||||
local tracked = computed(function()
|
||||
return selector()
|
||||
end)
|
||||
|
||||
local current: T
|
||||
local disposed = false
|
||||
|
||||
local function dispose()
|
||||
disposed = true
|
||||
end
|
||||
|
||||
dispose = effect(function()
|
||||
local previous = current
|
||||
current = tracked()
|
||||
untracked(listener, current, previous, dispose)
|
||||
end)
|
||||
|
||||
if disposed then
|
||||
dispose()
|
||||
end
|
||||
|
||||
return dispose
|
||||
end
|
||||
|
||||
return listen
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
local core = require("./core")
|
||||
|
||||
local computed = core.computed
|
||||
local setCurrentSub = core.setCurrentSub
|
||||
|
||||
type Mapped =
|
||||
& (<KI, VI, KO, VO>(selector: () -> { [KI]: VI }, mapper: (item: VI, key: KI) -> (VO, KO)) -> () -> { [KO]: VO })
|
||||
& (<K, VI, VO>(selector: () -> { [K]: VI }, mapper: (item: VI, key: K) -> VO) -> () -> { [K]: VO })
|
||||
|
||||
local function mapped<KI, VI, KO, VO>(selector: () -> { [KI]: VI }, mapper: (item: VI, key: KI) -> (VO, KO)): () -> { [KO]: VO }
|
||||
local currentData: { [KI]: VI } = {}
|
||||
local keyCache: { [KI]: KO } = {}
|
||||
|
||||
local function map(value: VI, key: KI): (VO, KO)
|
||||
local currentSub = setCurrentSub(nil)
|
||||
local success, valueOut, keyOut = pcall(mapper, value, key)
|
||||
setCurrentSub(currentSub)
|
||||
|
||||
if not success then
|
||||
error(`mapped() transform function failed: {valueOut}`)
|
||||
end
|
||||
|
||||
return valueOut, if keyOut == nil then key else keyOut :: any
|
||||
end
|
||||
|
||||
return computed(function(previousOutput)
|
||||
local currentOutput: { [KO]: VO } = previousOutput or {}
|
||||
local previousData = currentData
|
||||
currentData = selector()
|
||||
|
||||
for key in previousData do
|
||||
if currentData[key] ~= nil then
|
||||
continue
|
||||
end
|
||||
local cachedKey = keyCache[key]
|
||||
if cachedKey then
|
||||
currentOutput = if currentOutput == previousOutput then table.clone(currentOutput) else currentOutput
|
||||
currentOutput[cachedKey] = nil
|
||||
keyCache[key] = nil
|
||||
end
|
||||
end
|
||||
|
||||
for key, value in currentData do
|
||||
if value == previousData[key] then
|
||||
continue
|
||||
end
|
||||
local valueOut, keyOut = map(value, key)
|
||||
if currentOutput[keyOut] ~= valueOut then
|
||||
currentOutput = if currentOutput == previousOutput then table.clone(currentOutput) else currentOutput
|
||||
currentOutput[keyOut] = valueOut
|
||||
keyCache[key] = keyOut
|
||||
end
|
||||
end
|
||||
|
||||
return currentOutput
|
||||
end)
|
||||
end
|
||||
|
||||
return mapped :: Mapped
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
local core = require("./core")
|
||||
|
||||
local effect = core.effect
|
||||
local effectScope = core.effectScope
|
||||
local onCleanup = core.onCleanup
|
||||
|
||||
local function observe<K, V>(selector: () -> { [K]: V }, observer: (item: V, key: K) -> ...(() -> ())?): () -> ()
|
||||
local scopes: { [K]: () -> () } = {}
|
||||
local currentItems: { [K]: V } = {}
|
||||
|
||||
local function updateScopes(currentItems: { [K]: V }, previousItems: { [K]: V })
|
||||
for key in previousItems do
|
||||
local dispose = scopes[key]
|
||||
if currentItems[key] == nil and dispose then
|
||||
dispose()
|
||||
scopes[key] = nil
|
||||
end
|
||||
end
|
||||
|
||||
for key, value in currentItems do
|
||||
if previousItems[key] == nil then
|
||||
scopes[key] = effectScope(function()
|
||||
return observer(value, key)
|
||||
end, true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function disposeScopes()
|
||||
for _, dispose in scopes do
|
||||
dispose()
|
||||
end
|
||||
table.clear(scopes)
|
||||
end
|
||||
|
||||
local disposeEffect = effect(function()
|
||||
local previousItems = currentItems
|
||||
currentItems = selector()
|
||||
if currentItems ~= previousItems then
|
||||
updateScopes(currentItems, previousItems)
|
||||
end
|
||||
end)
|
||||
|
||||
onCleanup(disposeScopes, true)
|
||||
|
||||
return function()
|
||||
disposeEffect()
|
||||
disposeScopes()
|
||||
end
|
||||
end
|
||||
|
||||
return observe
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
local function inlineTest(): string
|
||||
return debug.info(1, "n")
|
||||
end
|
||||
|
||||
return {
|
||||
-- Enable strict mode in O2
|
||||
enabled = inlineTest() == "inlineTest",
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
local core = require("./core")
|
||||
local untracked = require("./untracked")
|
||||
|
||||
local computed = core.computed
|
||||
local effect = core.effect
|
||||
|
||||
local function subscribe<T>(selector: () -> T, listener: (state: T, lastState: T) -> ()): () -> ()
|
||||
local tracked = computed(function()
|
||||
return selector()
|
||||
end)
|
||||
|
||||
local current: T
|
||||
local mounted = true
|
||||
|
||||
return effect(function()
|
||||
local previous = current
|
||||
current = tracked()
|
||||
if mounted then
|
||||
mounted = false
|
||||
else
|
||||
untracked(listener, current, previous)
|
||||
end
|
||||
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.btest(sub.flags, RECURSED_CHECK)
|
||||
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.btest(flags, bit32.bor(MUTABLE, WATCHING)) then
|
||||
if not bit32.btest(flags, bit32.bor(RECURSED_CHECK, RECURSED, DIRTY, PENDING)) then
|
||||
sub.flags = bit32.bor(flags, PENDING)
|
||||
elseif not bit32.btest(flags, bit32.bor(RECURSED_CHECK, RECURSED)) then
|
||||
flags = NONE
|
||||
elseif not bit32.btest(flags, RECURSED_CHECK) then
|
||||
sub.flags = bit32.bor(bit32.band(flags, bit32.bnot(RECURSED)), PENDING)
|
||||
elseif not bit32.btest(flags, bit32.bor(DIRTY, PENDING)) and isValidLink(link, sub) then
|
||||
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.btest(flags, WATCHING) then
|
||||
notify(sub)
|
||||
end
|
||||
|
||||
if bit32.btest(flags, MUTABLE) 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.btest(subFlags, WATCHING) 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.btest(sub.flags, DIRTY) 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,5 +0,0 @@
|
|||
type Update<T> = ((previous: T) -> T) | T
|
||||
export type Atom<T> = (...Update<T>) -> T
|
||||
export type Selector<T> = () -> T
|
||||
|
||||
return {}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
local core = require("./core")
|
||||
|
||||
local setCurrentTracking = core.setCurrentTracking
|
||||
|
||||
local function handleUntracked(prevTracking: boolean, ok: boolean, ...)
|
||||
setCurrentTracking(prevTracking)
|
||||
if ok then
|
||||
return ...
|
||||
else
|
||||
error((...))
|
||||
end
|
||||
end
|
||||
|
||||
local function untracked<Args..., Results...>(callback: (Args...) -> Results..., ...: Args...): Results...
|
||||
return handleUntracked(setCurrentTracking(false), pcall(callback, ...))
|
||||
end
|
||||
|
||||
return untracked
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
local function handleNoYield(thread: thread, ok: boolean, ...)
|
||||
if not ok then
|
||||
return false, debug.traceback(thread, tostring((...)))
|
||||
elseif coroutine.status(thread) ~= "dead" then
|
||||
return false, debug.traceback(thread, "Attempted to yield!")
|
||||
else
|
||||
return true, ...
|
||||
end
|
||||
end
|
||||
|
||||
local function noYield<Args..., Results...>(callback: (Args...) -> Results..., ...: Args...): (boolean, Results...)
|
||||
local thread = coroutine.create(callback)
|
||||
return handleNoYield(thread, coroutine.resume(thread, ...))
|
||||
end
|
||||
|
||||
return {
|
||||
noYield = noYield :: typeof(pcall),
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
[package]
|
||||
name = "littensy/charm"
|
||||
version = "0.10.0"
|
||||
registry = "https://github.com/UpliftGames/wally-index"
|
||||
realm = "shared"
|
||||
exclude = ["node_modules", "src/__tests__", "src/index.d.ts", "package.json"]
|
||||
|
||||
[dependencies]
|
||||
Loading…
Add table
Add a link
Reference in a new issue