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 computed = Charm.computed
|
||||
local mapped = Charm.mapped
|
||||
local pauseTracking = Charm.signals.pauseTracking
|
||||
local resumeTracking = Charm.signals.resumeTracking
|
||||
local setCurrentSub = Charm.signals.setCurrentSub
|
||||
|
||||
type Entry = { number }
|
||||
|
||||
|
|
@ -26,9 +25,9 @@ local function simpleMapped(source: () -> { Entry }, mapper: (Entry, number) ->
|
|||
local nextState = {}
|
||||
|
||||
for index, value in state do
|
||||
pauseTracking()
|
||||
local prevSub = setCurrentSub(nil)
|
||||
local success, valueOut, keyOut = pcall(mapper, value, index)
|
||||
resumeTracking()
|
||||
setCurrentSub(prevSub)
|
||||
|
||||
if success then
|
||||
nextState[keyOut] = valueOut
|
||||
|
|
|
|||
|
|
@ -10,10 +10,12 @@ local function spec(name: string)
|
|||
end)
|
||||
end
|
||||
|
||||
spec("client")
|
||||
spec("server")
|
||||
spec("flatten")
|
||||
spec("patch")
|
||||
spec("validate")
|
||||
describe("charm-sync", function()
|
||||
spec("client")
|
||||
spec("server")
|
||||
spec("flatten")
|
||||
spec("patch")
|
||||
spec("validate")
|
||||
end)
|
||||
|
||||
return {}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
local signals = require("./signals")
|
||||
|
||||
local effect = signals.effect
|
||||
local pauseTracking = signals.pauseTracking
|
||||
local resumeTracking = signals.resumeTracking
|
||||
local setCurrentSub = signals.setCurrentSub
|
||||
|
||||
local function listen<T>(selector: () -> T, listener: (state: T, previous: T?, dispose: () -> ()) -> ()): () -> ()
|
||||
local disposeImmediately = false
|
||||
|
|
@ -17,9 +16,9 @@ local function listen<T>(selector: () -> T, listener: (state: T, previous: T?, d
|
|||
current = selector()
|
||||
|
||||
if current ~= previous then
|
||||
pauseTracking()
|
||||
local prevSub = setCurrentSub(nil)
|
||||
local success, result: unknown = pcall(listener, current, previous, dispose)
|
||||
resumeTracking()
|
||||
setCurrentSub(prevSub)
|
||||
|
||||
if not success then
|
||||
error(`listen() encountered an error: {result}`)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
--!optimize 2
|
||||
|
||||
local signals = require("./signals")
|
||||
|
||||
local computed = signals.computed
|
||||
local pauseTracking = signals.pauseTracking
|
||||
local resumeTracking = signals.resumeTracking
|
||||
local setCurrentSub = signals.setCurrentSub
|
||||
|
||||
type Mapped =
|
||||
& (<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 function map(value: VI, key: KI): (VO, KO)
|
||||
pauseTracking()
|
||||
local prevSub = setCurrentSub(nil)
|
||||
local success, valueOut, keyOut = pcall(mapper, value, key)
|
||||
resumeTracking()
|
||||
setCurrentSub(prevSub)
|
||||
|
||||
if not success then
|
||||
error(`mapped() transform function failed: {valueOut}`)
|
||||
|
|
|
|||
|
|
@ -3,10 +3,9 @@ local signals = require("./signals")
|
|||
local effect = signals.effect
|
||||
local effectScope = signals.effectScope
|
||||
local getCurrentSub = signals.getCurrentSub
|
||||
local setCurrentSub = signals.setCurrentSub
|
||||
local onEffectCleanup = signals.onEffectCleanup
|
||||
local onScopeDispose = signals.onScopeDispose
|
||||
local pauseTracking = signals.pauseTracking
|
||||
local resumeTracking = signals.resumeTracking
|
||||
|
||||
type Cleanup = () -> ()
|
||||
|
||||
|
|
@ -47,9 +46,9 @@ local function observe<K, V>(selector: () -> { [K]: V }, observer: (item: V, key
|
|||
currentItems = selector()
|
||||
|
||||
if currentItems ~= previousItems then
|
||||
pauseTracking()
|
||||
local prevSub = setCurrentSub(nil)
|
||||
local success, result: unknown = pcall(updateScopes, currentItems, previousItems)
|
||||
resumeTracking()
|
||||
setCurrentSub(prevSub)
|
||||
|
||||
if not success then
|
||||
error(`observe() encountered an error: {result}`, 2)
|
||||
|
|
|
|||
|
|
@ -62,8 +62,7 @@ type Signal<T = any> = ReactiveNode & {
|
|||
}
|
||||
|
||||
local pauseStack: { ReactiveNode? } = {}
|
||||
local pauseStackCount = 0
|
||||
local queuedEffects: { Effect | EffectScope } = {}
|
||||
local queuedEffects: { Effect | EffectScope? } = {}
|
||||
|
||||
local link: (dep: ReactiveNode, sub: ReactiveNode) -> ()
|
||||
local unlink: (link: Link, sub: ReactiveNode?) -> Link?
|
||||
|
|
@ -193,16 +192,16 @@ local function endBatch()
|
|||
end
|
||||
end
|
||||
|
||||
--- @deprecated Will be removed in the next major version. Use
|
||||
--- `const pausedSub = setCurrentSub(undefined)` instead for better performance.
|
||||
local function pauseTracking()
|
||||
pauseStackCount += 1
|
||||
pauseStack[pauseStackCount] = activeSub
|
||||
activeSub = nil
|
||||
table.insert(pauseStack, setCurrentSub(nil))
|
||||
end
|
||||
|
||||
--- @deprecated Will be removed in the next major version. Use
|
||||
--- `setCurrentSub(pausedSub)` instead for better performance.
|
||||
local function resumeTracking()
|
||||
activeSub = pauseStack[pauseStackCount]
|
||||
pauseStack[pauseStackCount] = nil
|
||||
pauseStackCount -= 1
|
||||
setCurrentSub(table.remove(pauseStack))
|
||||
end
|
||||
|
||||
local function updateComputed(c: Computed): boolean
|
||||
|
|
@ -311,9 +310,8 @@ local function effectStop(this: Effect | EffectScope): () -> ()
|
|||
dep = unlink(dep, this)
|
||||
end
|
||||
local sub = this.subs
|
||||
while sub do
|
||||
if sub then
|
||||
unlink(sub)
|
||||
sub = this.subs
|
||||
end
|
||||
(this :: Effect).flags = NONE
|
||||
cleanupEffect(this)
|
||||
|
|
@ -361,9 +359,11 @@ local function effectScope(fn: () -> (), detached: boolean?): () -> ()
|
|||
if activeScope and not detached then
|
||||
link(e, activeScope)
|
||||
end
|
||||
local prev = setCurrentScope(e)
|
||||
local prevSub = setCurrentSub(nil)
|
||||
local prevScope = setCurrentScope(e)
|
||||
local success, result: unknown = pcall(fn)
|
||||
setCurrentScope(prev)
|
||||
setCurrentScope(prevScope)
|
||||
setCurrentSub(prevSub)
|
||||
if not success then
|
||||
error(result)
|
||||
end
|
||||
|
|
@ -392,15 +392,18 @@ local function notify(e: Effect | EffectScope)
|
|||
end
|
||||
end
|
||||
|
||||
local function unwatched(signal: Signal | Effect | Computed)
|
||||
local toRemove = signal.deps
|
||||
if toRemove then
|
||||
repeat
|
||||
toRemove = unlink(toRemove, signal)
|
||||
until toRemove == nil
|
||||
(signal :: Signal).flags = bit32.bor(signal.flags, DIRTY)
|
||||
local function unwatched(node: Signal | 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 :: Signal).previousValue == nil then
|
||||
effectStop(node :: Effect)()
|
||||
end
|
||||
cleanupEffect(signal :: Effect)
|
||||
end
|
||||
|
||||
local reactiveSystem = createReactiveSystem({
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
local signals = require("./signals")
|
||||
|
||||
local effect = signals.effect
|
||||
local pauseTracking = signals.pauseTracking
|
||||
local resumeTracking = signals.resumeTracking
|
||||
local setCurrentSub = signals.setCurrentSub
|
||||
|
||||
local function subscribe<T>(selector: () -> T, listener: (state: T, lastState: T) -> ()): () -> ()
|
||||
local current: T
|
||||
|
|
@ -15,9 +14,9 @@ local function subscribe<T>(selector: () -> T, listener: (state: T, lastState: T
|
|||
if mounted then
|
||||
mounted = false
|
||||
elseif current ~= previous then
|
||||
pauseTracking()
|
||||
local prevSub = setCurrentSub(nil)
|
||||
local success, result: unknown = pcall(listener, current, previous)
|
||||
resumeTracking()
|
||||
setCurrentSub(prevSub)
|
||||
|
||||
if not success then
|
||||
error(`subscribe() encountered an error: {result}`)
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
local signals = require("./signals")
|
||||
|
||||
local pauseTracking = signals.pauseTracking
|
||||
local resumeTracking = signals.resumeTracking
|
||||
local setCurrentSub = signals.setCurrentSub
|
||||
|
||||
local function untracked<T, Args...>(callback: ((Args...) -> T) | (Args...) -> (), ...: Args...): T
|
||||
pauseTracking()
|
||||
local prevSub = setCurrentSub(nil)
|
||||
local success, result = pcall(callback, ...)
|
||||
resumeTracking()
|
||||
setCurrentSub(prevSub)
|
||||
|
||||
if not success then
|
||||
error(result)
|
||||
|
|
|
|||
|
|
@ -303,9 +303,14 @@ end)
|
|||
test("should run inner effect cleanup", function()
|
||||
local cleanups = 0
|
||||
|
||||
local a, setA = signal(false)
|
||||
local src, setSrc = signal(false)
|
||||
local stopEffect = effect(function()
|
||||
a()
|
||||
src()
|
||||
effect(function()
|
||||
onEffectCleanup(function()
|
||||
cleanups += 1
|
||||
end)
|
||||
end)
|
||||
effect(function()
|
||||
onEffectCleanup(function()
|
||||
cleanups += 1
|
||||
|
|
@ -314,12 +319,12 @@ test("should run inner effect cleanup", function()
|
|||
end)
|
||||
|
||||
assert(cleanups == 0, "should not cleanup immediately")
|
||||
setA(true)
|
||||
assert(cleanups == 1, "should cleanup after outer update")
|
||||
setSrc(true)
|
||||
assert(cleanups == 2, "should cleanup after outer update")
|
||||
stopEffect()
|
||||
assert(cleanups == 2, "should cleanup after outer dispose")
|
||||
setA(false)
|
||||
assert(cleanups == 2, "should not cleanup after update")
|
||||
assert(cleanups == 4, "should cleanup after outer dispose")
|
||||
setSrc(false)
|
||||
assert(cleanups == 4, "should not cleanup after update")
|
||||
end)
|
||||
|
||||
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
|
||||
|
||||
spec("computed")
|
||||
spec("effect")
|
||||
spec("effectScope")
|
||||
spec("issue_48")
|
||||
spec("topology")
|
||||
spec("untrack")
|
||||
describe("signals", function()
|
||||
spec("computed")
|
||||
spec("effect")
|
||||
spec("effectScope")
|
||||
spec("issue_48")
|
||||
spec("gc")
|
||||
spec("topology")
|
||||
spec("untrack")
|
||||
end)
|
||||
|
||||
return {}
|
||||
|
|
|
|||
|
|
@ -2,19 +2,14 @@ local suite = require("@tests/suite")
|
|||
|
||||
local describe = suite.describe
|
||||
|
||||
local function collect(name: string)
|
||||
describe(name, function()
|
||||
return require(`./{name}/spec`) :: any
|
||||
end)
|
||||
end
|
||||
|
||||
local function spec(name: string)
|
||||
describe(name, function()
|
||||
return require(`./{name}.spec`) :: any
|
||||
end)
|
||||
end
|
||||
|
||||
collect("signals")
|
||||
require("./signals/spec")
|
||||
|
||||
spec("gc")
|
||||
spec("listen")
|
||||
spec("mapped")
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ local function spec(name: string)
|
|||
end)
|
||||
end
|
||||
|
||||
spec("useAtom")
|
||||
describe("react-charm", function()
|
||||
spec("useAtom")
|
||||
end)
|
||||
|
||||
return {}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ local function spec(name: string)
|
|||
end)
|
||||
end
|
||||
|
||||
spec("useAtom")
|
||||
describe("vide-charm", function()
|
||||
spec("useAtom")
|
||||
end)
|
||||
|
||||
return {}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
local suite = require("./suite")
|
||||
|
||||
local describe = suite.describe
|
||||
local finish = suite.finish
|
||||
|
||||
if not script then
|
||||
|
|
@ -9,16 +8,8 @@ end
|
|||
|
||||
_G.__DEV__ = true
|
||||
|
||||
describe("charm-sync", function()
|
||||
require(script.Parent.CharmSync.spec)
|
||||
end)
|
||||
|
||||
describe("react-charm", function()
|
||||
require(script.Parent.ReactCharm.spec)
|
||||
end)
|
||||
|
||||
describe("vide-charm", function()
|
||||
require(script.Parent.VideCharm.spec)
|
||||
end)
|
||||
require(script.Parent.CharmSync.spec)
|
||||
require(script.Parent.ReactCharm.spec)
|
||||
require(script.Parent.VideCharm.spec)
|
||||
|
||||
finish()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue