mirror of
https://github.com/littensy/charm.git
synced 2026-10-20 17:51:37 +00:00
refactor: sync 2.0.4
This commit is contained in:
parent
dcb1de2844
commit
571d2c0d91
15 changed files with 152 additions and 85 deletions
|
|
@ -4,8 +4,7 @@ local bench = require("./bench")
|
||||||
local atom = Charm.atom
|
local atom = Charm.atom
|
||||||
local computed = Charm.computed
|
local computed = Charm.computed
|
||||||
local mapped = Charm.mapped
|
local mapped = Charm.mapped
|
||||||
local pauseTracking = Charm.signals.pauseTracking
|
local setCurrentSub = Charm.signals.setCurrentSub
|
||||||
local resumeTracking = Charm.signals.resumeTracking
|
|
||||||
|
|
||||||
type Entry = { number }
|
type Entry = { number }
|
||||||
|
|
||||||
|
|
@ -26,9 +25,9 @@ local function simpleMapped(source: () -> { Entry }, mapper: (Entry, number) ->
|
||||||
local nextState = {}
|
local nextState = {}
|
||||||
|
|
||||||
for index, value in state do
|
for index, value in state do
|
||||||
pauseTracking()
|
local prevSub = setCurrentSub(nil)
|
||||||
local success, valueOut, keyOut = pcall(mapper, value, index)
|
local success, valueOut, keyOut = pcall(mapper, value, index)
|
||||||
resumeTracking()
|
setCurrentSub(prevSub)
|
||||||
|
|
||||||
if success then
|
if success then
|
||||||
nextState[keyOut] = valueOut
|
nextState[keyOut] = valueOut
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,12 @@ local function spec(name: string)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
spec("client")
|
describe("charm-sync", function()
|
||||||
spec("server")
|
spec("client")
|
||||||
spec("flatten")
|
spec("server")
|
||||||
spec("patch")
|
spec("flatten")
|
||||||
spec("validate")
|
spec("patch")
|
||||||
|
spec("validate")
|
||||||
|
end)
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
local signals = require("./signals")
|
local signals = require("./signals")
|
||||||
|
|
||||||
local effect = signals.effect
|
local effect = signals.effect
|
||||||
local pauseTracking = signals.pauseTracking
|
local setCurrentSub = signals.setCurrentSub
|
||||||
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
|
||||||
|
|
@ -17,9 +16,9 @@ local function listen<T>(selector: () -> T, listener: (state: T, previous: T?, d
|
||||||
current = selector()
|
current = selector()
|
||||||
|
|
||||||
if current ~= previous then
|
if current ~= previous then
|
||||||
pauseTracking()
|
local prevSub = setCurrentSub(nil)
|
||||||
local success, result: unknown = pcall(listener, current, previous, dispose)
|
local success, result: unknown = pcall(listener, current, previous, dispose)
|
||||||
resumeTracking()
|
setCurrentSub(prevSub)
|
||||||
|
|
||||||
if not success then
|
if not success then
|
||||||
error(`listen() encountered an error: {result}`)
|
error(`listen() encountered an error: {result}`)
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
--!optimize 2
|
|
||||||
|
|
||||||
local signals = require("./signals")
|
local signals = require("./signals")
|
||||||
|
|
||||||
local computed = signals.computed
|
local computed = signals.computed
|
||||||
local pauseTracking = signals.pauseTracking
|
local setCurrentSub = signals.setCurrentSub
|
||||||
local resumeTracking = signals.resumeTracking
|
|
||||||
|
|
||||||
type Mapped =
|
type Mapped =
|
||||||
& (<KI, VI, KO, VO>(selector: () -> { [KI]: VI }, mapper: (item: VI, key: KI) -> (VO, KO)) -> () -> { [KO]: VO })
|
& (<KI, VI, KO, VO>(selector: () -> { [KI]: VI }, mapper: (item: VI, key: KI) -> (VO, KO)) -> () -> { [KO]: VO })
|
||||||
|
|
@ -19,9 +16,9 @@ local function mapped<KI, VI, KO, VO>(selector: () -> { [KI]: VI }, mapper: (ite
|
||||||
local keyCache: { [KI]: KO } = {}
|
local keyCache: { [KI]: KO } = {}
|
||||||
|
|
||||||
local function map(value: VI, key: KI): (VO, KO)
|
local function map(value: VI, key: KI): (VO, KO)
|
||||||
pauseTracking()
|
local prevSub = setCurrentSub(nil)
|
||||||
local success, valueOut, keyOut = pcall(mapper, value, key)
|
local success, valueOut, keyOut = pcall(mapper, value, key)
|
||||||
resumeTracking()
|
setCurrentSub(prevSub)
|
||||||
|
|
||||||
if not success then
|
if not success then
|
||||||
error(`mapped() transform function failed: {valueOut}`)
|
error(`mapped() transform function failed: {valueOut}`)
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,9 @@ local signals = require("./signals")
|
||||||
local effect = signals.effect
|
local effect = signals.effect
|
||||||
local effectScope = signals.effectScope
|
local effectScope = signals.effectScope
|
||||||
local getCurrentSub = signals.getCurrentSub
|
local getCurrentSub = signals.getCurrentSub
|
||||||
|
local setCurrentSub = signals.setCurrentSub
|
||||||
local onEffectCleanup = signals.onEffectCleanup
|
local onEffectCleanup = signals.onEffectCleanup
|
||||||
local onScopeDispose = signals.onScopeDispose
|
local onScopeDispose = signals.onScopeDispose
|
||||||
local pauseTracking = signals.pauseTracking
|
|
||||||
local resumeTracking = signals.resumeTracking
|
|
||||||
|
|
||||||
type Cleanup = () -> ()
|
type Cleanup = () -> ()
|
||||||
|
|
||||||
|
|
@ -47,9 +46,9 @@ local function observe<K, V>(selector: () -> { [K]: V }, observer: (item: V, key
|
||||||
currentItems = selector()
|
currentItems = selector()
|
||||||
|
|
||||||
if currentItems ~= previousItems then
|
if currentItems ~= previousItems then
|
||||||
pauseTracking()
|
local prevSub = setCurrentSub(nil)
|
||||||
local success, result: unknown = pcall(updateScopes, currentItems, previousItems)
|
local success, result: unknown = pcall(updateScopes, currentItems, previousItems)
|
||||||
resumeTracking()
|
setCurrentSub(prevSub)
|
||||||
|
|
||||||
if not success then
|
if not success then
|
||||||
error(`observe() encountered an error: {result}`, 2)
|
error(`observe() encountered an error: {result}`, 2)
|
||||||
|
|
|
||||||
|
|
@ -62,8 +62,7 @@ type Signal<T = any> = ReactiveNode & {
|
||||||
}
|
}
|
||||||
|
|
||||||
local pauseStack: { ReactiveNode? } = {}
|
local pauseStack: { ReactiveNode? } = {}
|
||||||
local pauseStackCount = 0
|
local queuedEffects: { Effect | EffectScope? } = {}
|
||||||
local queuedEffects: { Effect | EffectScope } = {}
|
|
||||||
|
|
||||||
local link: (dep: ReactiveNode, sub: ReactiveNode) -> ()
|
local link: (dep: ReactiveNode, sub: ReactiveNode) -> ()
|
||||||
local unlink: (link: Link, sub: ReactiveNode?) -> Link?
|
local unlink: (link: Link, sub: ReactiveNode?) -> Link?
|
||||||
|
|
@ -193,16 +192,16 @@ local function endBatch()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- @deprecated Will be removed in the next major version. Use
|
||||||
|
--- `const pausedSub = setCurrentSub(undefined)` instead for better performance.
|
||||||
local function pauseTracking()
|
local function pauseTracking()
|
||||||
pauseStackCount += 1
|
table.insert(pauseStack, setCurrentSub(nil))
|
||||||
pauseStack[pauseStackCount] = activeSub
|
|
||||||
activeSub = nil
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- @deprecated Will be removed in the next major version. Use
|
||||||
|
--- `setCurrentSub(pausedSub)` instead for better performance.
|
||||||
local function resumeTracking()
|
local function resumeTracking()
|
||||||
activeSub = pauseStack[pauseStackCount]
|
setCurrentSub(table.remove(pauseStack))
|
||||||
pauseStack[pauseStackCount] = nil
|
|
||||||
pauseStackCount -= 1
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function updateComputed(c: Computed): boolean
|
local function updateComputed(c: Computed): boolean
|
||||||
|
|
@ -311,9 +310,8 @@ local function effectStop(this: Effect | EffectScope): () -> ()
|
||||||
dep = unlink(dep, this)
|
dep = unlink(dep, this)
|
||||||
end
|
end
|
||||||
local sub = this.subs
|
local sub = this.subs
|
||||||
while sub do
|
if sub then
|
||||||
unlink(sub)
|
unlink(sub)
|
||||||
sub = this.subs
|
|
||||||
end
|
end
|
||||||
(this :: Effect).flags = NONE
|
(this :: Effect).flags = NONE
|
||||||
cleanupEffect(this)
|
cleanupEffect(this)
|
||||||
|
|
@ -361,9 +359,11 @@ local function effectScope(fn: () -> (), detached: boolean?): () -> ()
|
||||||
if activeScope and not detached then
|
if activeScope and not detached then
|
||||||
link(e, activeScope)
|
link(e, activeScope)
|
||||||
end
|
end
|
||||||
local prev = setCurrentScope(e)
|
local prevSub = setCurrentSub(nil)
|
||||||
|
local prevScope = setCurrentScope(e)
|
||||||
local success, result: unknown = pcall(fn)
|
local success, result: unknown = pcall(fn)
|
||||||
setCurrentScope(prev)
|
setCurrentScope(prevScope)
|
||||||
|
setCurrentSub(prevSub)
|
||||||
if not success then
|
if not success then
|
||||||
error(result)
|
error(result)
|
||||||
end
|
end
|
||||||
|
|
@ -392,15 +392,18 @@ local function notify(e: Effect | EffectScope)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function unwatched(signal: Signal | Effect | Computed)
|
local function unwatched(node: Signal | Computed | Effect | EffectScope)
|
||||||
local toRemove = signal.deps
|
if (node :: Computed).getter then
|
||||||
if toRemove then
|
local toRemove = node.deps
|
||||||
repeat
|
if toRemove then
|
||||||
toRemove = unlink(toRemove, signal)
|
(node :: Computed).flags = bit32.bor(MUTABLE, DIRTY)
|
||||||
until toRemove == nil
|
repeat
|
||||||
(signal :: Signal).flags = bit32.bor(signal.flags, DIRTY)
|
toRemove = unlink(toRemove, node)
|
||||||
|
until not toRemove
|
||||||
|
end
|
||||||
|
elseif (node :: Signal).previousValue == nil then
|
||||||
|
effectStop(node :: Effect)()
|
||||||
end
|
end
|
||||||
cleanupEffect(signal :: Effect)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local reactiveSystem = createReactiveSystem({
|
local reactiveSystem = createReactiveSystem({
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
local signals = require("./signals")
|
local signals = require("./signals")
|
||||||
|
|
||||||
local effect = signals.effect
|
local effect = signals.effect
|
||||||
local pauseTracking = signals.pauseTracking
|
local setCurrentSub = signals.setCurrentSub
|
||||||
local resumeTracking = signals.resumeTracking
|
|
||||||
|
|
||||||
local function subscribe<T>(selector: () -> T, listener: (state: T, lastState: T) -> ()): () -> ()
|
local function subscribe<T>(selector: () -> T, listener: (state: T, lastState: T) -> ()): () -> ()
|
||||||
local current: T
|
local current: T
|
||||||
|
|
@ -15,9 +14,9 @@ local function subscribe<T>(selector: () -> T, listener: (state: T, lastState: T
|
||||||
if mounted then
|
if mounted then
|
||||||
mounted = false
|
mounted = false
|
||||||
elseif current ~= previous then
|
elseif current ~= previous then
|
||||||
pauseTracking()
|
local prevSub = setCurrentSub(nil)
|
||||||
local success, result: unknown = pcall(listener, current, previous)
|
local success, result: unknown = pcall(listener, current, previous)
|
||||||
resumeTracking()
|
setCurrentSub(prevSub)
|
||||||
|
|
||||||
if not success then
|
if not success then
|
||||||
error(`subscribe() encountered an error: {result}`)
|
error(`subscribe() encountered an error: {result}`)
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
local signals = require("./signals")
|
local signals = require("./signals")
|
||||||
|
|
||||||
local pauseTracking = signals.pauseTracking
|
local setCurrentSub = signals.setCurrentSub
|
||||||
local resumeTracking = signals.resumeTracking
|
|
||||||
|
|
||||||
local function untracked<T, Args...>(callback: ((Args...) -> T) | (Args...) -> (), ...: Args...): T
|
local function untracked<T, Args...>(callback: ((Args...) -> T) | (Args...) -> (), ...: Args...): T
|
||||||
pauseTracking()
|
local prevSub = setCurrentSub(nil)
|
||||||
local success, result = pcall(callback, ...)
|
local success, result = pcall(callback, ...)
|
||||||
resumeTracking()
|
setCurrentSub(prevSub)
|
||||||
|
|
||||||
if not success then
|
if not success then
|
||||||
error(result)
|
error(result)
|
||||||
|
|
|
||||||
|
|
@ -303,9 +303,14 @@ end)
|
||||||
test("should run inner effect cleanup", function()
|
test("should run inner effect cleanup", function()
|
||||||
local cleanups = 0
|
local cleanups = 0
|
||||||
|
|
||||||
local a, setA = signal(false)
|
local src, setSrc = signal(false)
|
||||||
local stopEffect = effect(function()
|
local stopEffect = effect(function()
|
||||||
a()
|
src()
|
||||||
|
effect(function()
|
||||||
|
onEffectCleanup(function()
|
||||||
|
cleanups += 1
|
||||||
|
end)
|
||||||
|
end)
|
||||||
effect(function()
|
effect(function()
|
||||||
onEffectCleanup(function()
|
onEffectCleanup(function()
|
||||||
cleanups += 1
|
cleanups += 1
|
||||||
|
|
@ -314,12 +319,12 @@ test("should run inner effect cleanup", function()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert(cleanups == 0, "should not cleanup immediately")
|
assert(cleanups == 0, "should not cleanup immediately")
|
||||||
setA(true)
|
setSrc(true)
|
||||||
assert(cleanups == 1, "should cleanup after outer update")
|
assert(cleanups == 2, "should cleanup after outer update")
|
||||||
stopEffect()
|
stopEffect()
|
||||||
assert(cleanups == 2, "should cleanup after outer dispose")
|
assert(cleanups == 4, "should cleanup after outer dispose")
|
||||||
setA(false)
|
setSrc(false)
|
||||||
assert(cleanups == 2, "should not cleanup after update")
|
assert(cleanups == 4, "should not cleanup after update")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
|
|
||||||
72
pkgs/charm/tests/signals/gc.spec.luau
Normal file
72
pkgs/charm/tests/signals/gc.spec.luau
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
local Charm = require("@pkgs/charm/src/lib")
|
||||||
|
local suite = require("@tests/suite")
|
||||||
|
|
||||||
|
local atom = Charm.atom
|
||||||
|
local computed = Charm.computed
|
||||||
|
local effect = Charm.effect
|
||||||
|
local effectScope = Charm.effectScope
|
||||||
|
local test = suite.test
|
||||||
|
|
||||||
|
--- Creates a weak reference to a value.
|
||||||
|
--- @return Function to dereference the value.
|
||||||
|
local function weakRef<T>(value: T): () -> T?
|
||||||
|
local ref = setmetatable({ value = value }, { __mode = "v" })
|
||||||
|
return function()
|
||||||
|
return ref.value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Triggers garbage collection by allocating excessive memory.
|
||||||
|
local function gc()
|
||||||
|
for _ = 1, 1000 do
|
||||||
|
local _ = table.create(100, 0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- In alien-signals, a computed value unlinks from dependencies when its last
|
||||||
|
--- parent scope is disposed. Calling a computed value outside of a scope links
|
||||||
|
--- it to dependencies, but the dependencies hold strong references to the
|
||||||
|
--- computed value, so it won't GC until called in a scope and disposed.
|
||||||
|
---
|
||||||
|
--- This function releases a computed value by linking it to a scope and then
|
||||||
|
--- immediately closing it, causing alien-signals to unlink the computed value
|
||||||
|
--- and allow it to be garbage collected.
|
||||||
|
local function release(getter: () -> ())
|
||||||
|
effectScope(function()
|
||||||
|
getter()
|
||||||
|
end)()
|
||||||
|
end
|
||||||
|
|
||||||
|
test("should release computed cache", function()
|
||||||
|
local srcRef = weakRef({})
|
||||||
|
local src = atom(srcRef())
|
||||||
|
|
||||||
|
local c = computed(function()
|
||||||
|
return src()
|
||||||
|
end)
|
||||||
|
|
||||||
|
c() -- cache src value
|
||||||
|
src(nil) -- release value
|
||||||
|
c = release(c) -- release computed
|
||||||
|
|
||||||
|
gc()
|
||||||
|
assert(srcRef() == nil, "should release computed cache")
|
||||||
|
end)
|
||||||
|
|
||||||
|
test("should not release effect for atom", function()
|
||||||
|
local effectCalls = 0
|
||||||
|
local src = atom(0)
|
||||||
|
|
||||||
|
effect(function()
|
||||||
|
effectCalls += 1
|
||||||
|
src()
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert(effectCalls == 1, "should call effect once")
|
||||||
|
|
||||||
|
gc()
|
||||||
|
src(src() + 1)
|
||||||
|
assert(effectCalls == 2, "should call effect again")
|
||||||
|
end)
|
||||||
|
|
||||||
|
return {}
|
||||||
|
|
@ -8,11 +8,14 @@ local function spec(name: string)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
spec("computed")
|
describe("signals", function()
|
||||||
spec("effect")
|
spec("computed")
|
||||||
spec("effectScope")
|
spec("effect")
|
||||||
spec("issue_48")
|
spec("effectScope")
|
||||||
spec("topology")
|
spec("issue_48")
|
||||||
spec("untrack")
|
spec("gc")
|
||||||
|
spec("topology")
|
||||||
|
spec("untrack")
|
||||||
|
end)
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
|
|
||||||
|
|
@ -2,19 +2,14 @@ local suite = require("@tests/suite")
|
||||||
|
|
||||||
local describe = suite.describe
|
local describe = suite.describe
|
||||||
|
|
||||||
local function collect(name: string)
|
|
||||||
describe(name, function()
|
|
||||||
return require(`./{name}/spec`) :: any
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function spec(name: string)
|
local function spec(name: string)
|
||||||
describe(name, function()
|
describe(name, function()
|
||||||
return require(`./{name}.spec`) :: any
|
return require(`./{name}.spec`) :: any
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
collect("signals")
|
require("./signals/spec")
|
||||||
|
|
||||||
spec("gc")
|
spec("gc")
|
||||||
spec("listen")
|
spec("listen")
|
||||||
spec("mapped")
|
spec("mapped")
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ local function spec(name: string)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
spec("useAtom")
|
describe("react-charm", function()
|
||||||
|
spec("useAtom")
|
||||||
|
end)
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ local function spec(name: string)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
spec("useAtom")
|
describe("vide-charm", function()
|
||||||
|
spec("useAtom")
|
||||||
|
end)
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
local suite = require("./suite")
|
local suite = require("./suite")
|
||||||
|
|
||||||
local describe = suite.describe
|
|
||||||
local finish = suite.finish
|
local finish = suite.finish
|
||||||
|
|
||||||
if not script then
|
if not script then
|
||||||
|
|
@ -9,16 +8,8 @@ end
|
||||||
|
|
||||||
_G.__DEV__ = true
|
_G.__DEV__ = true
|
||||||
|
|
||||||
describe("charm-sync", function()
|
require(script.Parent.CharmSync.spec)
|
||||||
require(script.Parent.CharmSync.spec)
|
require(script.Parent.ReactCharm.spec)
|
||||||
end)
|
require(script.Parent.VideCharm.spec)
|
||||||
|
|
||||||
describe("react-charm", function()
|
|
||||||
require(script.Parent.ReactCharm.spec)
|
|
||||||
end)
|
|
||||||
|
|
||||||
describe("vide-charm", function()
|
|
||||||
require(script.Parent.VideCharm.spec)
|
|
||||||
end)
|
|
||||||
|
|
||||||
finish()
|
finish()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue