mirror of
https://github.com/littensy/charm.git
synced 2026-10-20 17:51:37 +00:00
fix: prevent early reactive gc, alien-signals 3.1.1
This commit is contained in:
parent
b826401bf3
commit
9d4af8e669
5 changed files with 94 additions and 49 deletions
|
|
@ -8,7 +8,15 @@ type SyncPayload = types.SyncPayload
|
||||||
type Setter<T = any> = (T | (T) -> any) -> ()
|
type Setter<T = any> = (T | (T) -> any) -> ()
|
||||||
|
|
||||||
local trackedSignals: { [string]: Setter } = {}
|
local trackedSignals: { [string]: Setter } = {}
|
||||||
local trackedReactives: { [string]: Setter } = {}
|
local trackedReactives: { [string]: ReactiveProxy } = {}
|
||||||
|
|
||||||
|
local function shallowMergeReactive(reactive: ReactiveProxy, source: { [any]: any })
|
||||||
|
local target = reactive.__target
|
||||||
|
for key, value in source do
|
||||||
|
target[key] = value
|
||||||
|
end
|
||||||
|
Charm.trigger(reactive.__track)
|
||||||
|
end
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
Registers a map of setters to the state with the corresponding keys on the
|
Registers a map of setters to the state with the corresponding keys on the
|
||||||
|
|
@ -26,7 +34,7 @@ local function addSignals(signals: { [string]: Setter | {} })
|
||||||
if type(signal) == "function" then
|
if type(signal) == "function" then
|
||||||
trackedSignals[key] = signal
|
trackedSignals[key] = signal
|
||||||
else
|
else
|
||||||
trackedReactives[key] = ((signal :: any) :: ReactiveProxy).__set
|
trackedReactives[key] = (signal :: any) :: ReactiveProxy
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -54,9 +62,10 @@ end
|
||||||
|
|
||||||
local function applyInitPayload(data: { [string]: any })
|
local function applyInitPayload(data: { [string]: any })
|
||||||
for key, value in data do
|
for key, value in data do
|
||||||
local trackedSignal: Setter? = trackedSignals[key] or trackedReactives[key]
|
if trackedSignals[key] then
|
||||||
if trackedSignal then
|
trackedSignals[key](value)
|
||||||
trackedSignal(value)
|
elseif trackedReactives[key] then
|
||||||
|
shallowMergeReactive(trackedReactives[key], value)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -71,9 +80,8 @@ local function applyPatchPayload(data: { [string]: any })
|
||||||
return patch.apply(value, diffs)
|
return patch.apply(value, diffs)
|
||||||
end)
|
end)
|
||||||
elseif trackedReactive then
|
elseif trackedReactive then
|
||||||
trackedReactive(function(value): ()
|
patch.applyMutable(trackedReactive.__target, diffs)
|
||||||
patch.applyMutable(value, diffs)
|
Charm.trigger(trackedReactive.__track)
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -66,33 +66,34 @@ test("should not release effect for atom", function()
|
||||||
assert(effectCalls == 2, "should call effect again")
|
assert(effectCalls == 2, "should call effect again")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("should release reactive tables", function()
|
test("should release inner reactive proxy", function()
|
||||||
local valueRef = weakRef({})
|
local valueRef = weakRef({})
|
||||||
local src = reactive({ value = valueRef.value })
|
local src: any = reactive({ value = valueRef.value })
|
||||||
|
|
||||||
effect(function()
|
local dispose = effect(function()
|
||||||
local _ = src.value -- create proxy
|
local _ = src.value -- create proxy
|
||||||
end)
|
end)
|
||||||
src.value = nil :: any -- release proxy
|
src.value = nil :: any -- release proxy
|
||||||
|
dispose()
|
||||||
|
|
||||||
gc()
|
gc()
|
||||||
assert(valueRef.value == nil, "should release reactive table")
|
assert(src.__signals.value == nil, "should release signals")
|
||||||
assert((src :: ReactiveProxy).__signals.value == nil, "should release reactive signals")
|
assert(valueRef.value == nil, "should release inner proxy")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("should release reactive property deps", function()
|
test("should release reactive property deps", function()
|
||||||
local src = reactive({ foo = 1 })
|
local src: any = reactive({ foo = 1 })
|
||||||
|
|
||||||
local c = computed(function()
|
local c = computed(function()
|
||||||
return src.foo
|
return src.foo
|
||||||
end)
|
end)
|
||||||
|
|
||||||
c()
|
c()
|
||||||
assert((src :: ReactiveProxy).__signals.foo ~= nil, "should have deps for foo")
|
assert(src.__signals.foo ~= nil, "should have deps for foo")
|
||||||
|
|
||||||
c = nil :: any
|
c = nil :: any
|
||||||
gc()
|
gc()
|
||||||
assert((src :: ReactiveProxy).__signals.foo == nil, "should release deps for foo")
|
assert(src.__signals.foo == nil, "should release deps for foo")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("should not release effect for reactive", function()
|
test("should not release effect for reactive", function()
|
||||||
|
|
@ -110,4 +111,17 @@ test("should not release effect for reactive", function()
|
||||||
assert(calls == 2, "should call effect again")
|
assert(calls == 2, "should call effect again")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
test("should not release tracked reactive", function()
|
||||||
|
local src = Charm.reactive({ users = {} :: { [string]: {}? } })
|
||||||
|
local user
|
||||||
|
|
||||||
|
effect(function()
|
||||||
|
user = src.users.user
|
||||||
|
end)
|
||||||
|
|
||||||
|
gc()
|
||||||
|
src.users.user = {}
|
||||||
|
assert(user ~= nil, "src.users.user should not be released")
|
||||||
|
end)
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
|
|
||||||
|
|
@ -111,14 +111,18 @@ test("should not track unread properties", function()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("should overwrite with table passed to setter", function()
|
test("should overwrite with table passed to setter", function()
|
||||||
local src, setSrc = reactive({ a = 1, b = 2, c = 3 } :: { [string]: number })
|
local src, setSrc = reactive({ a = 1, b = 2, c = 3 } :: { [string]: number? })
|
||||||
local calls = {}
|
local calls = {}
|
||||||
|
|
||||||
effect(function()
|
effect(function()
|
||||||
table.insert(calls, `{src.a}{src.b}{src.c}`)
|
table.insert(calls, `{src.a}{src.b}{src.c}`)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
setSrc({ a = 2, c = 4 })
|
setSrc(function(src)
|
||||||
|
src.a = 2
|
||||||
|
src.b = nil
|
||||||
|
src.c = 4
|
||||||
|
end)
|
||||||
assert(table.concat(calls, ",") == "123,2nil4", table.concat(calls, ","))
|
assert(table.concat(calls, ",") == "123,2nil4", table.concat(calls, ","))
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,22 @@ test("should trigger effect once", function()
|
||||||
assert(triggers == 2, "effect should run twice")
|
assert(triggers == 2, "effect should run twice")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
test("should not notify the trigger function sub", function()
|
||||||
|
local src1 = signal({})
|
||||||
|
local src2 = computed(function()
|
||||||
|
return src1()
|
||||||
|
end)
|
||||||
|
|
||||||
|
effect(function()
|
||||||
|
src1()
|
||||||
|
src2()
|
||||||
|
end)
|
||||||
|
trigger(function()
|
||||||
|
src1()
|
||||||
|
src2()
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
test("should trigger updates for subscribe", function()
|
test("should trigger updates for subscribe", function()
|
||||||
local src = signal({} :: { number })
|
local src = signal({} :: { number })
|
||||||
local triggers = 0
|
local triggers = 0
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
local system = require("@self/system")
|
local system = require("@self/system")
|
||||||
|
|
||||||
export type ReactiveFlags = system.ReactiveFlags
|
export type ReactiveFlags = system.ReactiveFlags
|
||||||
|
|
||||||
export type ReactiveNode = system.ReactiveNode
|
export type ReactiveNode = system.ReactiveNode
|
||||||
|
|
||||||
export type Link = system.Link
|
export type Link = system.Link
|
||||||
|
|
||||||
export type Atom<T = any> = (...((T) -> T) | T) -> T
|
export type Atom<T = any> = (...((T) -> T) | T) -> T
|
||||||
|
|
@ -9,11 +11,10 @@ export type Atom<T = any> = (...((T) -> T) | T) -> T
|
||||||
export type ReactiveProxy<T = any> = typeof(setmetatable(
|
export type ReactiveProxy<T = any> = typeof(setmetatable(
|
||||||
{} :: {
|
{} :: {
|
||||||
__track: () -> (),
|
__track: () -> (),
|
||||||
__set: (T | ((T) -> ())?) -> (),
|
|
||||||
__target: T,
|
__target: T,
|
||||||
__signals: { [unknown]: () -> unknown },
|
__signals: { [unknown]: () -> unknown },
|
||||||
},
|
},
|
||||||
{}
|
{} :: { __index: T }
|
||||||
))
|
))
|
||||||
|
|
||||||
type EffectScopeNode = ReactiveNode & {
|
type EffectScopeNode = ReactiveNode & {
|
||||||
|
|
@ -73,6 +74,10 @@ local propagate: (link: Link) -> ()
|
||||||
local checkDirty: (link: Link, sub: ReactiveNode) -> boolean
|
local checkDirty: (link: Link, sub: ReactiveNode) -> boolean
|
||||||
local shallowPropagate: (link: Link) -> ()
|
local shallowPropagate: (link: Link) -> ()
|
||||||
|
|
||||||
|
local ReactiveProxy = {
|
||||||
|
proxyMap = setmetatable({} :: { [any]: ReactiveProxy }, { __mode = "v" }),
|
||||||
|
}
|
||||||
|
|
||||||
local cycle = 0
|
local cycle = 0
|
||||||
local batchDepth = 0
|
local batchDepth = 0
|
||||||
local notifyIndex = 0
|
local notifyIndex = 0
|
||||||
|
|
@ -80,9 +85,6 @@ local queued: { EffectNode? } = {}
|
||||||
local queuedLength = 0
|
local queuedLength = 0
|
||||||
local activeSub: ReactiveNode?
|
local activeSub: ReactiveNode?
|
||||||
|
|
||||||
local ReactiveProxy = {}
|
|
||||||
local proxyMap = setmetatable({} :: { [any]: ReactiveProxy }, { __mode = "v" })
|
|
||||||
|
|
||||||
local function noYield<Args..., Results...>(fn: (Args...) -> Results...): (Args...) -> Results...
|
local function noYield<Args..., Results...>(fn: (Args...) -> Results...): (Args...) -> Results...
|
||||||
if not flags.strict then
|
if not flags.strict then
|
||||||
return fn
|
return fn
|
||||||
|
|
@ -504,13 +506,15 @@ local function trigger(fn: () -> ())
|
||||||
local prevSub = setActiveSub(sub)
|
local prevSub = setActiveSub(sub)
|
||||||
local success, result: any = pcall(noYield(fn))
|
local success, result: any = pcall(noYield(fn))
|
||||||
activeSub = prevSub
|
activeSub = prevSub
|
||||||
while sub.deps do
|
local link = sub.deps
|
||||||
local link = sub.deps :: Link
|
while link do
|
||||||
local dep = link.dep
|
local dep = link.dep
|
||||||
unlink(link, sub)
|
link = unlink(link, sub)
|
||||||
if dep.subs then
|
local subs = dep.subs
|
||||||
propagate(dep.subs)
|
if subs then
|
||||||
shallowPropagate(dep.subs)
|
sub.flags = NONE
|
||||||
|
propagate(subs)
|
||||||
|
shallowPropagate(subs)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if batchDepth == 0 then
|
if batchDepth == 0 then
|
||||||
|
|
@ -848,7 +852,7 @@ end
|
||||||
]]
|
]]
|
||||||
local function toRaw<T>(value: T): T
|
local function toRaw<T>(value: T): T
|
||||||
if isReactive(value) then
|
if isReactive(value) then
|
||||||
local value: ReactiveProxy<T> = value :: any
|
local value = value :: ReactiveProxy<T> & T
|
||||||
value.__track()
|
value.__track()
|
||||||
return toRaw(value.__target)
|
return toRaw(value.__target)
|
||||||
end
|
end
|
||||||
|
|
@ -881,42 +885,41 @@ end
|
||||||
@return A function for updating the original table.
|
@return A function for updating the original table.
|
||||||
@see https://github.com/littensy/charm?tab=readme-ov-file#reactiveinitialvalue
|
@see https://github.com/littensy/charm?tab=readme-ov-file#reactiveinitialvalue
|
||||||
]]
|
]]
|
||||||
local function reactive<T>(initialValue: T & {}, trackParent: (() -> ())?): (T, (update: (T) -> () | T?) -> ())
|
local function reactive<T>(initialValue: T & {}, trackParent: (() -> ())?): (T, (update: ((T) -> ())?) -> ())
|
||||||
initialValue = toRaw(initialValue)
|
initialValue = toRaw(initialValue)
|
||||||
|
|
||||||
local proxy = proxyMap[initialValue]
|
local proxy = ReactiveProxy.proxyMap[initialValue]
|
||||||
|
|
||||||
if not proxy then
|
if not proxy then
|
||||||
local track = computed(function()
|
local track = computed(function(version: number?)
|
||||||
|
-- Reference the proxy to avoid GCing while an effect is actively
|
||||||
|
-- tracking this reactive object.
|
||||||
|
local _ref = proxy
|
||||||
|
-- Allow updates from the parent to propagate down when the
|
||||||
|
-- target is mutated directly through the setter.
|
||||||
if trackParent then
|
if trackParent then
|
||||||
trackParent()
|
trackParent()
|
||||||
end
|
end
|
||||||
return {}
|
return (version or 0) + 1
|
||||||
end)
|
end)
|
||||||
|
|
||||||
local function set(update: (T) -> () | T?)
|
|
||||||
if type(update) == "function" then
|
|
||||||
update(initialValue)
|
|
||||||
elseif type(update) == "table" then
|
|
||||||
table.clear(initialValue)
|
|
||||||
for key, value in update :: {} do
|
|
||||||
(initialValue :: {})[key] = value
|
|
||||||
end
|
|
||||||
end
|
|
||||||
trigger(track)
|
|
||||||
end
|
|
||||||
|
|
||||||
proxy = setmetatable({
|
proxy = setmetatable({
|
||||||
__target = initialValue,
|
__target = initialValue,
|
||||||
__signals = setmetatable({}, { __mode = "v" }) :: any,
|
__signals = setmetatable({}, { __mode = "v" }) :: any,
|
||||||
__track = track,
|
__track = track,
|
||||||
__set = set,
|
|
||||||
}, ReactiveProxy)
|
}, ReactiveProxy)
|
||||||
|
|
||||||
proxyMap[initialValue] = proxy
|
ReactiveProxy.proxyMap[initialValue] = proxy
|
||||||
end
|
end
|
||||||
|
|
||||||
return proxy :: any, proxy.__set
|
local function setValue(update: ((T) -> ())?)
|
||||||
|
if update then
|
||||||
|
update(proxy.__target)
|
||||||
|
end
|
||||||
|
trigger(proxy.__track)
|
||||||
|
end
|
||||||
|
|
||||||
|
return proxy :: any, setValue
|
||||||
end
|
end
|
||||||
|
|
||||||
function ReactiveProxy.__index(self: ReactiveProxy, key: any): any
|
function ReactiveProxy.__index(self: ReactiveProxy, key: any): any
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue