mirror of
https://github.com/littensy/charm.git
synced 2026-10-20 17:51:37 +00:00
perf: optimize subscribe and mapped
This commit is contained in:
parent
939a812b43
commit
6bfd2f3d21
14 changed files with 204 additions and 213 deletions
74
pkgs/charm/benchmarks/propagate.luau
Normal file
74
pkgs/charm/benchmarks/propagate.luau
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
local Charm = require("@pkgs/charm/src/lib")
|
||||||
|
|
||||||
|
local atom = Charm.atom
|
||||||
|
local computed = Charm.computed
|
||||||
|
local effect = Charm.effect
|
||||||
|
local listen = Charm.listen
|
||||||
|
|
||||||
|
local signal = Charm.signals.signal
|
||||||
|
local computedSignal = Charm.signals.computed
|
||||||
|
local effectSignal = Charm.signals.effect
|
||||||
|
|
||||||
|
local w = 10
|
||||||
|
local h = 10
|
||||||
|
|
||||||
|
local function bench(name: string, test: () -> ())
|
||||||
|
local start = os.clock()
|
||||||
|
local iter = 0
|
||||||
|
while os.clock() - start < 2 do
|
||||||
|
iter += 1
|
||||||
|
test()
|
||||||
|
end
|
||||||
|
local endTime = os.clock()
|
||||||
|
local elapsed = endTime - start
|
||||||
|
print(`{name}: {elapsed / iter * 1000 * 1000} us/iter`)
|
||||||
|
end
|
||||||
|
|
||||||
|
bench(`listen {w}*{h}`, function()
|
||||||
|
local src = atom(1)
|
||||||
|
for _ = 1, w do
|
||||||
|
local last = src
|
||||||
|
for _ = 1, h do
|
||||||
|
local prev = last
|
||||||
|
last = computed(function()
|
||||||
|
return prev() + 1
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
listen(last, function() end)
|
||||||
|
end
|
||||||
|
src(src() + 1)
|
||||||
|
end)
|
||||||
|
|
||||||
|
bench(`effect {w}*{h}`, function()
|
||||||
|
local src = atom(1)
|
||||||
|
for _ = 1, w do
|
||||||
|
local last = src
|
||||||
|
for _ = 1, h do
|
||||||
|
local prev = last
|
||||||
|
last = computed(function()
|
||||||
|
return prev() + 1
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
effect(function()
|
||||||
|
last()
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
src(src() + 1)
|
||||||
|
end)
|
||||||
|
|
||||||
|
bench(`signals {w}*{h}`, function()
|
||||||
|
local src, setSrc = signal(1)
|
||||||
|
for _ = 1, w do
|
||||||
|
local last = src
|
||||||
|
for _ = 1, h do
|
||||||
|
local prev = last
|
||||||
|
last = computedSignal(function()
|
||||||
|
return prev() + 1
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
effectSignal(function()
|
||||||
|
last()
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
setSrc(src() + 1)
|
||||||
|
end)
|
||||||
|
|
@ -2,7 +2,6 @@ local signals = require("./signals")
|
||||||
local types = require("./types")
|
local types = require("./types")
|
||||||
|
|
||||||
type Atom<T> = types.Atom<T>
|
type Atom<T> = types.Atom<T>
|
||||||
type AtomOptions<T> = types.AtomOptions<T>
|
|
||||||
|
|
||||||
local signal = signals.signal
|
local signal = signals.signal
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,17 @@
|
||||||
local signals = require("./signals")
|
local signals = require("./signals")
|
||||||
|
|
||||||
local _effect = signals.effect
|
|
||||||
local onEffectCleanup = signals.onEffectCleanup
|
local onEffectCleanup = signals.onEffectCleanup
|
||||||
|
|
||||||
local function effect(callback: (dispose: () -> ()) -> ...() -> ()?): () -> ()
|
type Cleanup = () -> ()
|
||||||
|
|
||||||
|
local function effect(callback: (dispose: () -> ()) -> ...Cleanup?): () -> ()
|
||||||
local disposeImmediately = false
|
local disposeImmediately = false
|
||||||
|
|
||||||
local function dispose()
|
local function dispose()
|
||||||
disposeImmediately = true
|
disposeImmediately = true
|
||||||
end
|
end
|
||||||
|
|
||||||
dispose = _effect(function()
|
dispose = signals.effect(function()
|
||||||
local cleanup = callback(dispose)
|
local cleanup = callback(dispose)
|
||||||
if cleanup then
|
if cleanup then
|
||||||
onEffectCleanup(cleanup)
|
onEffectCleanup(cleanup)
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
local signals = require("./signals")
|
local signals = require("./signals")
|
||||||
|
|
||||||
local _effectScope = signals.effectScope
|
|
||||||
local onScopeDispose = signals.onScopeDispose
|
local onScopeDispose = signals.onScopeDispose
|
||||||
|
|
||||||
local function effectScope(callback: () -> ...() -> ()?, detached: boolean?): () -> ()
|
type Cleanup = () -> ()
|
||||||
return _effectScope(function()
|
|
||||||
|
local function effectScope(callback: () -> ...Cleanup?, detached: boolean?): () -> ()
|
||||||
|
return signals.effectScope(function()
|
||||||
local cleanup = callback()
|
local cleanup = callback()
|
||||||
if cleanup then
|
if cleanup then
|
||||||
onScopeDispose(cleanup)
|
onScopeDispose(cleanup)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
local lib = require(script.lib)
|
local lib = require(script.lib)
|
||||||
|
|
||||||
export type Atom<T = any> = lib.Atom<T>
|
export type Atom<T = any> = lib.Atom<T>
|
||||||
export type AtomOptions<T = any> = lib.AtomOptions<T>
|
|
||||||
export type Selector<T = any> = lib.Selector<T>
|
export type Selector<T = any> = lib.Selector<T>
|
||||||
|
|
||||||
return lib
|
return lib
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ local types = require("./types")
|
||||||
local untracked = require("./untracked")
|
local untracked = require("./untracked")
|
||||||
|
|
||||||
export type Atom<T = any> = types.Atom<T>
|
export type Atom<T = any> = types.Atom<T>
|
||||||
export type AtomOptions<T = any> = types.AtomOptions<T>
|
|
||||||
export type Selector<T = any> = types.Selector<T>
|
export type Selector<T = any> = types.Selector<T>
|
||||||
|
|
||||||
local computed = signals.computed
|
local computed = signals.computed
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
local signals = require("./signals")
|
local signals = require("./signals")
|
||||||
local spawn = require("./spawn")
|
|
||||||
|
|
||||||
local effect = signals.effect
|
local effect = signals.effect
|
||||||
local pauseTracking = signals.pauseTracking
|
local pauseTracking = signals.pauseTracking
|
||||||
|
|
@ -7,22 +6,24 @@ local resumeTracking = signals.resumeTracking
|
||||||
|
|
||||||
local function listen<T>(selector: () -> T, listener: (state: T, previous: T?, dispose: () -> ()) -> ()): () -> ()
|
local function listen<T>(selector: () -> T, listener: (state: T, previous: T?, dispose: () -> ()) -> ()): () -> ()
|
||||||
local disposeImmediately = false
|
local disposeImmediately = false
|
||||||
local current: T?
|
local current: T
|
||||||
|
|
||||||
local function dispose()
|
local function dispose()
|
||||||
disposeImmediately = true
|
disposeImmediately = true
|
||||||
end
|
end
|
||||||
|
|
||||||
dispose = effect(function()
|
dispose = effect(function()
|
||||||
local value = selector()
|
local previous: T? = current
|
||||||
|
current = selector()
|
||||||
if value ~= current then
|
|
||||||
local previous = current
|
|
||||||
current = value
|
|
||||||
|
|
||||||
|
if current ~= previous then
|
||||||
pauseTracking()
|
pauseTracking()
|
||||||
spawn(listener, value, previous, dispose)
|
local success, result: unknown = pcall(listener, current, previous, dispose)
|
||||||
resumeTracking()
|
resumeTracking()
|
||||||
|
|
||||||
|
if not success then
|
||||||
|
error(`listen() encountered an error: {result}`)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,128 +4,86 @@ local computed = signals.computed
|
||||||
local pauseTracking = signals.pauseTracking
|
local pauseTracking = signals.pauseTracking
|
||||||
local resumeTracking = signals.resumeTracking
|
local resumeTracking = signals.resumeTracking
|
||||||
|
|
||||||
type MappedKey<VI, KO, VO> = {
|
type Mapped =
|
||||||
key: KO,
|
& (<KI, VI, KO, VO>(selector: () -> { [KI]: VI }, mapper: (item: VI, key: KI) -> (VO, KO)) -> () -> { [KO]: VO })
|
||||||
value: 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 }, transform: (item: VI, key: KI) -> (VO, KO?)): () -> { [KO]: VO }
|
local function ensureCanMutateOutput<KO, VO>(output: { [KO]: VO }, previous: { [KO]: VO }?): { [KO]: VO }
|
||||||
local mappedKeys: { [KI]: MappedKey<VI, KO, VO> } = {}
|
return if output == previous then table.clone(output) else output
|
||||||
local previousItems: { [KI]: VI }?
|
end
|
||||||
|
|
||||||
local function getTransform(value: VI, key: KI): (VO, KO?)
|
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)
|
||||||
pauseTracking()
|
pauseTracking()
|
||||||
local success, valueOut, keyOut = pcall(transform, value, key)
|
local success, valueOut, keyOut = pcall(mapper, value, key)
|
||||||
resumeTracking()
|
resumeTracking()
|
||||||
|
|
||||||
if not success then
|
if not success then
|
||||||
error(`mapped() transform function failed: {valueOut}`)
|
error(`mapped() transform function failed: {valueOut}`)
|
||||||
end
|
end
|
||||||
|
|
||||||
return valueOut, keyOut
|
return valueOut, if keyOut ~= nil then keyOut else key :: any
|
||||||
end
|
end
|
||||||
|
|
||||||
return computed(function(previousMappedItems)
|
return computed(function(previousOutput)
|
||||||
local currentItems = selector()
|
local output: { [KO]: VO } = previousOutput or {}
|
||||||
|
local previousData = currentData
|
||||||
|
currentData = selector()
|
||||||
|
|
||||||
-- If this is the initial run, create a map of the items and store the
|
-- Check equality since the selector might not be a computed value
|
||||||
-- updated keys and values.
|
if currentData == previousData then
|
||||||
if not previousItems or not previousMappedItems then
|
return output
|
||||||
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
|
end
|
||||||
|
|
||||||
if previousItems == currentItems then
|
-- Clear removed values from the output and key cache
|
||||||
return previousMappedItems
|
for key in previousData do
|
||||||
|
if currentData[key] == nil then
|
||||||
|
local keyOut = keyCache[key]
|
||||||
|
|
||||||
|
if keyOut then
|
||||||
|
output = ensureCanMutateOutput(output, previousOutput)
|
||||||
|
output[keyOut] = nil
|
||||||
|
keyCache[key] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local mappedItems = table.clone(previousMappedItems)
|
-- Process new or changed values
|
||||||
|
for key, value in currentData do
|
||||||
-- Update and remove mapped keys if the source values changed
|
if value == previousData[key] then
|
||||||
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
|
continue
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Do not call transform if the value has not changed
|
local valueOut, keyOut = map(value, key)
|
||||||
if currentValue == oldValue then
|
local cachedKeyOut = keyCache[key]
|
||||||
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
|
if valueOut ~= nil then
|
||||||
mappedKeys[key] = { key = keyOut, value = valueOut }
|
-- Skip update if the key and value are the same
|
||||||
mappedItems[keyOut] = valueOut
|
if keyOut == cachedKeyOut and valueOut == output[cachedKeyOut] then
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
|
||||||
|
output = ensureCanMutateOutput(output, previousOutput)
|
||||||
|
output[keyOut] = valueOut
|
||||||
|
keyCache[key] = keyOut
|
||||||
|
|
||||||
|
-- Remove the old key if a new key is assigned
|
||||||
|
if cachedKeyOut ~= nil and keyOut ~= cachedKeyOut then
|
||||||
|
output[cachedKeyOut] = nil
|
||||||
|
end
|
||||||
|
elseif cachedKeyOut ~= nil then
|
||||||
|
-- Remove the cached key if the new value is nil
|
||||||
|
output = ensureCanMutateOutput(output, previousOutput)
|
||||||
|
output[cachedKeyOut] = nil
|
||||||
|
keyCache[key] = nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
previousItems = currentItems
|
return output
|
||||||
|
|
||||||
return mappedItems
|
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
return mapped
|
return mapped :: Mapped
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,13 @@ local onScopeDispose = signals.onScopeDispose
|
||||||
local pauseTracking = signals.pauseTracking
|
local pauseTracking = signals.pauseTracking
|
||||||
local resumeTracking = signals.resumeTracking
|
local resumeTracking = signals.resumeTracking
|
||||||
|
|
||||||
local function observe<K, V>(selector: () -> { [K]: V }, observer: (item: V, key: K) -> ...() -> ()?): () -> ()
|
type Cleanup = () -> ()
|
||||||
|
|
||||||
|
local function observe<K, V>(selector: () -> { [K]: V }, observer: (item: V, key: K) -> ...Cleanup?): () -> ()
|
||||||
local scopes: { [K]: () -> () } = {}
|
local scopes: { [K]: () -> () } = {}
|
||||||
local currentItems: { [K]: V } = {}
|
local currentItems: { [K]: V } = {}
|
||||||
|
|
||||||
local function update(currentItems: { [K]: V }, previousItems: { [K]: V })
|
local function updateScopes(currentItems: { [K]: V }, previousItems: { [K]: V })
|
||||||
for key in previousItems do
|
for key in previousItems do
|
||||||
if currentItems[key] == nil then
|
if currentItems[key] == nil then
|
||||||
local dispose = scopes[key]
|
local dispose = scopes[key]
|
||||||
|
|
@ -41,14 +43,12 @@ local function observe<K, V>(selector: () -> { [K]: V }, observer: (item: V, key
|
||||||
end
|
end
|
||||||
|
|
||||||
local disposeEffect = effect(function()
|
local disposeEffect = effect(function()
|
||||||
local nextItems = selector()
|
local previousItems = currentItems
|
||||||
|
currentItems = selector()
|
||||||
if nextItems ~= currentItems then
|
|
||||||
local previousItems = currentItems
|
|
||||||
currentItems = nextItems
|
|
||||||
|
|
||||||
|
if currentItems ~= previousItems then
|
||||||
pauseTracking()
|
pauseTracking()
|
||||||
local success, result: unknown = pcall(update, currentItems, previousItems)
|
local success, result: unknown = pcall(updateScopes, currentItems, previousItems)
|
||||||
resumeTracking()
|
resumeTracking()
|
||||||
|
|
||||||
if not success then
|
if not success then
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ local batchDepth = 0
|
||||||
local notifyIndex = 0
|
local notifyIndex = 0
|
||||||
local queuedEffectsLength = 0
|
local queuedEffectsLength = 0
|
||||||
local activeSub: ReactiveNode?
|
local activeSub: ReactiveNode?
|
||||||
local activeScope: EffectScope?
|
local activeScope: Effect | EffectScope?
|
||||||
|
|
||||||
local function getCurrentSub(): ReactiveNode?
|
local function getCurrentSub(): ReactiveNode?
|
||||||
return activeSub
|
return activeSub
|
||||||
|
|
@ -90,26 +90,28 @@ local function setCurrentSub(sub: ReactiveNode?)
|
||||||
return prevSub
|
return prevSub
|
||||||
end
|
end
|
||||||
|
|
||||||
local function getCurrentScope(): EffectScope?
|
local function getCurrentScope(): Effect | EffectScope?
|
||||||
return activeScope
|
return activeScope
|
||||||
end
|
end
|
||||||
|
|
||||||
local function setCurrentScope(scope: EffectScope?)
|
local function setCurrentScope(scope: Effect | EffectScope?)
|
||||||
local prevScope = activeScope
|
local prevScope = activeScope
|
||||||
activeScope = scope
|
activeScope = scope
|
||||||
return prevScope
|
return prevScope
|
||||||
end
|
end
|
||||||
|
|
||||||
local function addCleanup(e: EffectScope, cleanup: () -> ())
|
local function addCleanup(e: Effect | EffectScope, cleanup: () -> ())
|
||||||
local prevCleanup = e.cleanup
|
local prevCleanup: (() -> ())? = e.cleanup
|
||||||
|
local nextCleanup = cleanup
|
||||||
|
|
||||||
if prevCleanup then
|
if prevCleanup then
|
||||||
e.cleanup = function()
|
function cleanup()
|
||||||
prevCleanup()
|
prevCleanup()
|
||||||
cleanup()
|
nextCleanup()
|
||||||
end
|
end
|
||||||
else
|
|
||||||
e.cleanup = cleanup
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
(e :: Effect).cleanup = cleanup
|
||||||
end
|
end
|
||||||
|
|
||||||
local function onEffectCleanup(cleanup: () -> ())
|
local function onEffectCleanup(cleanup: () -> ())
|
||||||
|
|
@ -124,19 +126,19 @@ local function onScopeDispose(cleanup: () -> ())
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function cleanupEffect(e: EffectScope)
|
local function cleanupEffect(e: Effect | EffectScope)
|
||||||
if e.cleanup then
|
if e.cleanup then
|
||||||
local prev = setCurrentSub(nil)
|
local prev = setCurrentSub(nil)
|
||||||
local success, result: unknown = pcall(e.cleanup)
|
local success, result: unknown = pcall(e.cleanup)
|
||||||
setCurrentSub(prev)
|
setCurrentSub(prev);
|
||||||
e.cleanup = nil
|
(e :: Effect).cleanup = nil
|
||||||
if not success then
|
if not success then
|
||||||
error(result)
|
error(result)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function run(e: EffectScope, flags: ReactiveFlags): ()
|
local function run(e: Effect | EffectScope, flags: ReactiveFlags): ()
|
||||||
if bit32.band(flags, DIRTY) ~= 0 or (bit32.band(flags, PENDING) ~= 0 and checkDirty(e.deps :: Link, e)) then
|
if bit32.band(flags, DIRTY) ~= 0 or (bit32.band(flags, PENDING) ~= 0 and checkDirty(e.deps :: Link, e)) then
|
||||||
cleanupEffect(e)
|
cleanupEffect(e)
|
||||||
local prev = setCurrentSub(e)
|
local prev = setCurrentSub(e)
|
||||||
|
|
@ -149,7 +151,7 @@ local function run(e: EffectScope, flags: ReactiveFlags): ()
|
||||||
end
|
end
|
||||||
return
|
return
|
||||||
elseif bit32.band(flags, PENDING) ~= 0 then
|
elseif bit32.band(flags, PENDING) ~= 0 then
|
||||||
e.flags = bit32.band(flags, bit32.bnot(PENDING))
|
(e :: Effect).flags = bit32.band(flags, bit32.bnot(PENDING))
|
||||||
end
|
end
|
||||||
local link = e.deps
|
local link = e.deps
|
||||||
while link do
|
while link do
|
||||||
|
|
@ -158,7 +160,7 @@ local function run(e: EffectScope, flags: ReactiveFlags): ()
|
||||||
if bit32.band(depFlags, QUEUED) ~= 0 then
|
if bit32.band(depFlags, QUEUED) ~= 0 then
|
||||||
depFlags = bit32.band(depFlags, bit32.bnot(QUEUED))
|
depFlags = bit32.band(depFlags, bit32.bnot(QUEUED))
|
||||||
dep.flags = depFlags
|
dep.flags = depFlags
|
||||||
run(dep :: EffectScope, depFlags)
|
run(dep :: Effect, depFlags)
|
||||||
end
|
end
|
||||||
link = link.nextDep
|
link = link.nextDep
|
||||||
end
|
end
|
||||||
|
|
@ -167,7 +169,7 @@ end
|
||||||
local function flush(): ()
|
local function flush(): ()
|
||||||
while notifyIndex < queuedEffectsLength do
|
while notifyIndex < queuedEffectsLength do
|
||||||
notifyIndex += 1
|
notifyIndex += 1
|
||||||
local effect = queuedEffects[notifyIndex] :: EffectScope
|
local effect = queuedEffects[notifyIndex] :: Effect
|
||||||
queuedEffects[notifyIndex] = nil
|
queuedEffects[notifyIndex] = nil
|
||||||
effect.flags = bit32.band(effect.flags, bit32.bnot(QUEUED))
|
effect.flags = bit32.band(effect.flags, bit32.bnot(QUEUED))
|
||||||
run(effect, effect.flags)
|
run(effect, effect.flags)
|
||||||
|
|
@ -302,7 +304,7 @@ local function computedGetter<T>(this: Computed<T>): () -> T
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function effectStop(this: EffectScope): () -> ()
|
local function effectStop(this: Effect | EffectScope): () -> ()
|
||||||
return function()
|
return function()
|
||||||
local dep = this.deps
|
local dep = this.deps
|
||||||
while dep do
|
while dep do
|
||||||
|
|
@ -313,7 +315,7 @@ local function effectStop(this: EffectScope): () -> ()
|
||||||
unlink(sub)
|
unlink(sub)
|
||||||
sub = this.subs
|
sub = this.subs
|
||||||
end
|
end
|
||||||
this.flags = NONE
|
(this :: Effect).flags = NONE
|
||||||
cleanupEffect(this)
|
cleanupEffect(this)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -368,43 +370,43 @@ local function effectScope(fn: () -> (), detached: boolean?): () -> ()
|
||||||
return effectStop(e)
|
return effectStop(e)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function notify(e: ReactiveNode)
|
local function update(signal: Signal | Computed): boolean
|
||||||
|
if (signal :: Computed).getter then
|
||||||
|
return updateComputed(signal :: Computed)
|
||||||
|
else
|
||||||
|
return updateSignal(signal :: Signal, signal.value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function notify(e: Effect | EffectScope)
|
||||||
local flags = e.flags
|
local flags = e.flags
|
||||||
if bit32.band(flags, QUEUED) == 0 then
|
if bit32.band(flags, QUEUED) == 0 then
|
||||||
e.flags = bit32.bor(flags, QUEUED)
|
(e :: Effect).flags = bit32.bor(flags, QUEUED)
|
||||||
local subs = e.subs
|
local subs = e.subs
|
||||||
if subs then
|
if subs then
|
||||||
notify(subs.sub :: Effect | EffectScope)
|
notify(subs.sub :: Effect)
|
||||||
else
|
else
|
||||||
queuedEffectsLength += 1
|
queuedEffectsLength += 1
|
||||||
queuedEffects[queuedEffectsLength] = e :: Effect
|
queuedEffects[queuedEffectsLength] = e
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function update(signal: ReactiveNode): boolean
|
local function unwatched(signal: Signal | Effect | Computed)
|
||||||
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
|
local toRemove = signal.deps
|
||||||
if toRemove then
|
if toRemove then
|
||||||
repeat
|
repeat
|
||||||
toRemove = unlink(toRemove, signal)
|
toRemove = unlink(toRemove, signal)
|
||||||
until not toRemove
|
until toRemove == nil
|
||||||
signal.flags = bit32.bor(signal.flags, DIRTY)
|
(signal :: Signal).flags = bit32.bor(signal.flags, DIRTY)
|
||||||
end
|
end
|
||||||
cleanupEffect(signal :: EffectScope)
|
cleanupEffect(signal :: Effect)
|
||||||
end
|
end
|
||||||
|
|
||||||
local reactiveSystem = createReactiveSystem({
|
local reactiveSystem = createReactiveSystem({
|
||||||
update = update,
|
update = update :: (ReactiveNode) -> boolean,
|
||||||
notify = notify,
|
notify = notify :: (ReactiveNode) -> (),
|
||||||
unwatched = unwatched,
|
unwatched = unwatched :: (ReactiveNode) -> (),
|
||||||
})
|
})
|
||||||
|
|
||||||
link = reactiveSystem.link
|
link = reactiveSystem.link
|
||||||
|
|
|
||||||
|
|
@ -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,27 +1,27 @@
|
||||||
local signals = require("./signals")
|
local signals = require("./signals")
|
||||||
local spawn = require("./spawn")
|
|
||||||
|
|
||||||
local effect = signals.effect
|
local effect = signals.effect
|
||||||
local pauseTracking = signals.pauseTracking
|
local pauseTracking = signals.pauseTracking
|
||||||
local resumeTracking = signals.resumeTracking
|
local resumeTracking = signals.resumeTracking
|
||||||
|
|
||||||
local function subscribe<T>(selector: () -> T, listener: (T, T) -> ()): () -> ()
|
local function subscribe<T>(selector: () -> T, listener: (state: T, lastState: T) -> ()): () -> ()
|
||||||
local current: T
|
local current: T
|
||||||
local mounted = true
|
local mounted = true
|
||||||
|
|
||||||
return effect(function()
|
return effect(function()
|
||||||
local value = selector()
|
local previous = current
|
||||||
|
current = selector()
|
||||||
|
|
||||||
if mounted then
|
if mounted then
|
||||||
current = value
|
|
||||||
mounted = false
|
mounted = false
|
||||||
elseif value ~= current then
|
elseif current ~= previous then
|
||||||
local previous = current
|
|
||||||
current = value
|
|
||||||
|
|
||||||
pauseTracking()
|
pauseTracking()
|
||||||
spawn(listener, value, previous)
|
local success, result: unknown = pcall(listener, current, previous)
|
||||||
resumeTracking()
|
resumeTracking()
|
||||||
|
|
||||||
|
if not success then
|
||||||
|
error(`subscribe() encountered an error: {result}`)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,4 @@ export type Selector<T = any> = () -> T
|
||||||
|
|
||||||
export type Atom<T = any> = ((((T) -> T) | T) -> T) & (() -> T)
|
export type Atom<T = any> = ((((T) -> T) | T) -> T) & (() -> T)
|
||||||
|
|
||||||
export type AtomOptions<T = any> = {
|
|
||||||
equals: ((prev: T, next: T) -> boolean)?,
|
|
||||||
}
|
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,11 @@
|
||||||
local suite = require("@tests/suite")
|
local suite = require("@tests/suite")
|
||||||
|
|
||||||
local describe = suite.describe
|
|
||||||
local finish = suite.finish
|
local finish = suite.finish
|
||||||
|
|
||||||
if not table.isfrozen(_G) then
|
if not table.isfrozen(_G) then
|
||||||
_G.__DEV__ = true
|
_G.__DEV__ = true
|
||||||
end
|
end
|
||||||
|
|
||||||
describe("charm", function()
|
require("@pkgs/charm/tests/spec")
|
||||||
require("@pkgs/charm/tests/spec")
|
|
||||||
end)
|
|
||||||
|
|
||||||
finish()
|
finish()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue