feat: allow recursion in effects by default

This commit is contained in:
littensy 2026-02-22 23:14:50 -08:00
parent 10521d05c2
commit 612612b130
6 changed files with 104 additions and 126 deletions

View file

@ -56,7 +56,6 @@ Charm is a state management library that brings the [fine-grained reactivity](ht
- [`onCleanup(callback, failSilently?)`](#oncleanupcallback-failsilently) - [`onCleanup(callback, failSilently?)`](#oncleanupcallback-failsilently)
- [`reactive(initialValue)`](#reactiveinitialvalue) - [`reactive(initialValue)`](#reactiveinitialvalue)
- [`atom(initialValue, equals?)`](#atominitialvalue-equals) - [`atom(initialValue, equals?)`](#atominitialvalue-equals)
- [`recursive()`](#recursive)
- [`trigger(callback)`](#triggercallback) - [`trigger(callback)`](#triggercallback)
- [`flags`](#flags) - [`flags`](#flags)
- [Client-Server Sync](#client-server-sync) - [Client-Server Sync](#client-server-sync)
@ -608,24 +607,6 @@ max(-1) -- 1
--- ---
### `recursive()`
By design, Charm's reactive system [does not allow recursion](https://github.com/stackblitz/alien-signals/issues/90#issuecomment-3489711800) in effects or computed signals. In case you need to opt out of recursion checks for an effect, you can call `recursive()` at the top of the effect callback:
```luau
local getCounter, setCounter = signal(0)
effect(function()
recursive()
print(getCounter())
setCounter(function(count)
return math.min(count + 1, 3)
end)
end) -- Output: 0, 1, 2, 3
```
---
### `trigger(callback)` ### `trigger(callback)`
The `trigger()` function allows you to manually trigger updates for downstream dependencies when you've directly mutated a signal's value without using the signal setter: The `trigger()` function allows you to manually trigger updates for downstream dependencies when you've directly mutated a signal's value without using the signal setter:
@ -990,10 +971,7 @@ Charm v0.11 introduces _a lot_ of breaking changes, so below are some tips that
> >
> This meant effects created as a side effect of a source update would implicitly get added as a child of the `useAtom` effect, and they could get disposed at the wrong time and desync UI. > This meant effects created as a side effect of a source update would implicitly get added as a child of the `useAtom` effect, and they could get disposed at the wrong time and desync UI.
5. Recursion is now disallowed in effects and computed signals by default. This change may introduce bugs in code relying on the old behavior. 5. Consider refactoring your code to use some new quality-of-life features. Many of these are made possible thanks to [alien-signals](https://github.com/stackblitz/alien-signals)!
- Recursive checks are opt-out. You can allow recursion for a specific effect by calling [`recursive()`](#recursive) at the top of the effect callback.
6. Consider refactoring your code to use some new quality-of-life features. Many of these are made possible thanks to [alien-signals](https://github.com/stackblitz/alien-signals)!
- [`reactive()`](#reactiveinitialvalue): make mutable tables deeply reactive - [`reactive()`](#reactiveinitialvalue): make mutable tables deeply reactive
- [`signal()`](#signalinitialvalue-equals): make reads and writes more explicit - [`signal()`](#signalinitialvalue-equals): make reads and writes more explicit
- [`listen()`](#listengetter-callback): create a subscription that runs once immediately - [`listen()`](#listengetter-callback): create a subscription that runs once immediately

View file

@ -145,17 +145,12 @@ end)
-- https://github.com/vuejs/core/blob/minor/packages/reactivity/__tests__/computed.spec.ts#L528 -- https://github.com/vuejs/core/blob/minor/packages/reactivity/__tests__/computed.spec.ts#L528
test("should not override queried MaybeDirty result", function() test("should not override queried MaybeDirty result", function()
local Item = {
new = function()
return { v = atom(0) }
end,
}
local v1 = atom(nil :: any) local v1 = atom(nil :: any)
local v2 = atom(false) local v2 = atom(false)
local c1 = computed(function() local c1 = computed(function()
local c = v1() local c = v1()
if not v1() then if not v1() then
c = Item.new() c = { v = atom(0) }
v1(c) v1(c)
end end
return c.v() return c.v()

View file

@ -6,7 +6,6 @@ local effect = Charm.effect
local effectScope = Charm.effectScope local effectScope = Charm.effectScope
local endBatch = Charm.endBatch local endBatch = Charm.endBatch
local onCleanup = Charm.onCleanup local onCleanup = Charm.onCleanup
local recursive = Charm.recursive
local setActiveSub = Charm.setActiveSub local setActiveSub = Charm.setActiveSub
local signal = Charm.signal local signal = Charm.signal
local startBatch = Charm.startBatch local startBatch = Charm.startBatch
@ -269,33 +268,11 @@ test("should handle flags are indirectly updated during checkDirty", function()
assert(triggers == 2, "did not update indirectly") assert(triggers == 2, "did not update indirectly")
end) end)
test("should handle effect recursion for the first execution", function() test("should allow recursion", function()
local src1, setSrc1 = signal(0)
local src2, setSrc2 = signal(0)
local triggers1 = 0
local triggers2 = 0
effect(function()
triggers1 += 1
setSrc1(math.min(src1() + 1, 5))
end)
effect(function()
triggers2 += 1
setSrc2(math.min(src2() + 1, 5))
src2()
end)
assert(triggers1 == 1, `bad triggers1: {triggers1}`)
assert(triggers2 == 1, `bad triggers2: {triggers2}`)
end)
test("should opt in to recursion", function()
local src, setSrc = signal(0) local src, setSrc = signal(0)
local order = {} local order = {}
effect(function() effect(function()
recursive()
table.insert(order, src()) table.insert(order, src())
setSrc(math.min(src() + 1, 5)) setSrc(math.min(src() + 1, 5))
end) end)
@ -387,15 +364,15 @@ test("should run cleanups outside any evaluation context", function()
assert(calls == 0, "should not run after b update") assert(calls == 0, "should not run after b update")
end) end)
test("should avoid infinite recursive loops with multiple effects", function() test("infinite recursion should eventually throw", function()
local counter, setCounter = Charm.signal(0) local counter, setCounter = signal(0)
effect(function()
setCounter(counter() + 1) local success = pcall(function()
effect(function()
setCounter(counter() + 1)
end)
end) end)
effect(function() assert(not success, "should have thrown")
counter()
end)
setCounter(counter() + 1)
end) end)
return {} return {}

View file

@ -153,10 +153,11 @@ export function trigger(fn: () => void): void;
export function onCleanup(fn: Cleanup, failSilently?: boolean): void; export function onCleanup(fn: Cleanup, failSilently?: boolean): void;
/** /**
* Runs the function without subscribing to signal updates or capturing inner * Runs the function without subscribing to signal updates and prevents the
* effects in the parent effect or scope. * current effect or scope from tracking inner effects created within the
* function.
* *
* For an alternative that still cleans up inner effects, use `peek` instead. * For an alternative that allows nested effect cleanup, use `peek` instead.
* *
* @param fn A function that may read signals or create effects. * @param fn A function that may read signals or create effects.
* @param args Arguments to pass to the function. * @param args Arguments to pass to the function.
@ -166,10 +167,12 @@ export function onCleanup(fn: Cleanup, failSilently?: boolean): void;
export function untracked<Args extends any[], Result>(fn: (...args: Args) => Result, ...args: Args): Result; export function untracked<Args extends any[], Result>(fn: (...args: Args) => Result, ...args: Args): Result;
/** /**
* Runs the function without subscribing to signal updates, but still captures * Runs the function without subscribing to signal updates, but still allows
* inner effects so that they dispose with the parent effect. * inner effects to be tracked by parent effects/scopes.
* *
* To avoid capturing inner effects, use `untracked` instead. * This is useful for cases where you want to access the current value of a
* signal without creating a dependency, but still want any effects created
* within the function to be cleaned up properly.
* *
* @param fn A function that may read signals. * @param fn A function that may read signals.
* @param args Arguments to pass to the function. * @param args Arguments to pass to the function.
@ -270,14 +273,15 @@ export function observe<V, K extends Key>(
): Cleanup; ): Cleanup;
/** /**
* Disables recursive checks for the current effect or computed signal. * Disables recursive checks for the current reactive node. Currently, only
* Useful for effects that intentionally update signals they depend on. * computed signals use recursion checks, as effects allow recursion.
* *
* @example * @example
* ```ts * ```ts
* effect(() => { * computed(() => {
* recursive(); * recursive();
* setCount(getCount() + 1); * setCount(getCount() + 1);
* return getCount();
* }) * })
* ``` * ```
*/ */
@ -331,10 +335,30 @@ export function isReactive(value: unknown): boolean;
*/ */
export function toRaw<T>(value: T): T; export function toRaw<T>(value: T): T;
/**
* Returns the currently active subscriber (effect or computed node).
*
* @return Current active subscriber or `nil`
*/
export function getActiveSub(): ReactiveNode | undefined; export function getActiveSub(): ReactiveNode | undefined;
/**
* Sets the currently active subscriber (effect or computed node) and returns
* the previous subscriber.
*
* @param sub New subscriber to set as active
* @return Previous active subscriber
*/
export function setActiveSub(sub?: ReactiveNode): ReactiveNode | undefined; export function setActiveSub(sub?: ReactiveNode): ReactiveNode | undefined;
/**
* Starts a batch operation, incrementing the batch depth counter. Effects
* will not run until the outermost batch ends.
*/
export function startBatch(): void; export function startBatch(): void;
/**
* Ends a batch operation, decrementing the batch depth counter. If this is
* the outermost batch, queued effects are flushed and run.
*/
export function endBatch(): void; export function endBatch(): void;

View file

@ -9,6 +9,8 @@ export type Link = system.Link
export type ReactiveFlags = system.ReactiveFlags export type ReactiveFlags = system.ReactiveFlags
export type ReactiveNode = system.ReactiveNode export type ReactiveNode = system.ReactiveNode
type Cleanup = () -> ()
export type Atom<T = any> = (...((T) -> T) | T) -> T export type Atom<T = any> = (...((T) -> T) | T) -> T
export type ReactiveProxy<T = any> = typeof(setmetatable( export type ReactiveProxy<T = any> = typeof(setmetatable(
@ -21,12 +23,12 @@ export type ReactiveProxy<T = any> = typeof(setmetatable(
)) ))
type EffectScopeNode = ReactiveNode & { type EffectScopeNode = ReactiveNode & {
cleanups: { () -> () }, cleanups: { Cleanup },
} }
type EffectNode = ReactiveNode & { type EffectNode = ReactiveNode & {
fn: () -> ...(() -> ())?, fn: () -> ...Cleanup?,
cleanups: { () -> () }, cleanups: { Cleanup },
} }
type ComputedNode<T = any> = ReactiveNode & { type ComputedNode<T = any> = ReactiveNode & {
@ -39,16 +41,6 @@ type SignalNode<T = any> = ReactiveNode & {
pendingValue: T, pendingValue: T,
} }
-- Bitmask flags for tracking the state of reactive nodes. Flags are combined
-- and checked using bitwise operations for efficiency.
local NONE = 0b0000000 -- Default state
local MUTABLE = 0b0000001 -- Can be changed (signal or computed)
local WATCHING = 0b0000010 -- Watching for changes (effects)
local RECURSED_CHECK = 0b0000100 -- Being checked for circular dependencies
local DIRTY = 0b0010000 -- Value has changed and needs updating
local PENDING = 0b0100000 -- Might be dirty, pending verification
local PEEKING = 0b1000000 -- Blocking dependency tracking
local function inlineTest(): string local function inlineTest(): string
return debug.info(1, "n") return debug.info(1, "n")
end end
@ -79,6 +71,16 @@ local ReactiveProxy = {
proxyMap = setmetatable({} :: { [any]: ReactiveProxy }, { __mode = "v" }), proxyMap = setmetatable({} :: { [any]: ReactiveProxy }, { __mode = "v" }),
} }
-- Bitmask flags for tracking the state of reactive nodes. Flags are combined
-- and checked using bitwise operations for efficiency.
local NONE = 0b0000000 -- Default state
local MUTABLE = 0b0000001 -- Can be changed (signal or computed)
local WATCHING = 0b0000010 -- Watching for changes (effects)
local RECURSED_CHECK = 0b0000100 -- Being checked for circular dependencies
local DIRTY = 0b0010000 -- Value has changed and needs updating
local PENDING = 0b0100000 -- Might be dirty, pending verification
local PEEKING = 0b1000000 -- Blocking dependency tracking
-- Alien Signals reactive system -- Alien Signals reactive system
local link: (dep: ReactiveNode, sub: ReactiveNode, version: number) -> () local link: (dep: ReactiveNode, sub: ReactiveNode, version: number) -> ()
local unlink: (link: Link, sub: ReactiveNode?) -> Link? local unlink: (link: Link, sub: ReactiveNode?) -> Link?
@ -187,7 +189,7 @@ end
@param cleanups List of cleanup functions @param cleanups List of cleanup functions
]] ]]
local function runCleanups(cleanups: { () -> () }) local function runCleanups(cleanups: { Cleanup })
for key, cleanup in cleanups do for key, cleanup in cleanups do
cleanups[key] = nil cleanups[key] = nil
local prevSub = setActiveSub(nil) local prevSub = setActiveSub(nil)
@ -212,13 +214,12 @@ local function run(effect: EffectNode)
cycle += 1 cycle += 1
effect.depsTail = nil effect.depsTail = nil
effect.flags = bit32.bor(WATCHING, RECURSED_CHECK) effect.flags = WATCHING
local prevSub = setActiveSub(effect) local prevSub = setActiveSub(effect)
local success, result = pcall(effect.fn) local success, result = pcall(effect.fn)
activeSub = prevSub activeSub = prevSub
effect.flags = bit32.band(effect.flags, bit32.bnot(RECURSED_CHECK))
purgeDeps(effect) purgeDeps(effect)
if not success then if not success then
@ -349,10 +350,13 @@ local function computedOper<T>(this: ComputedNode<T>): () -> T
elseif flags == NONE then elseif flags == NONE then
-- First access should initialize the computed value -- First access should initialize the computed value
this.flags = bit32.bor(MUTABLE, RECURSED_CHECK) this.flags = bit32.bor(MUTABLE, RECURSED_CHECK)
local prevSub = setActiveSub(this) local prevSub = setActiveSub(this)
local success, result = pcall(this.getter) local success, result = pcall(this.getter)
activeSub = prevSub activeSub = prevSub
this.flags = bit32.band(this.flags, bit32.bnot(RECURSED_CHECK)) this.flags = bit32.band(this.flags, bit32.bnot(RECURSED_CHECK))
if success then if success then
this.value = result this.value = result
else else
@ -393,6 +397,7 @@ local function signalOper<T>(
local prevSub = setActiveSub(nil) local prevSub = setActiveSub(nil)
local success, result = pcall(noYield(update), this.pendingValue) local success, result = pcall(noYield(update), this.pendingValue)
activeSub = prevSub activeSub = prevSub
if success then if success then
value = result value = result
else else
@ -407,8 +412,10 @@ local function signalOper<T>(
if flags.frozen then if flags.frozen then
deepFreeze(value) deepFreeze(value)
end end
this.pendingValue = value this.pendingValue = value
this.flags = bit32.bor(MUTABLE, DIRTY) this.flags = bit32.bor(MUTABLE, DIRTY)
local subs = this.subs local subs = this.subs
if subs then if subs then
propagate(subs) propagate(subs)
@ -416,6 +423,7 @@ local function signalOper<T>(
flush() flush()
end end
end end
return value return value
end end
@ -462,7 +470,7 @@ end
@param this Effect node @param this Effect node
@return Effect cleanup operator @return Effect cleanup operator
]] ]]
local function effectOper(this: EffectScopeNode): () -> () local function effectOper(this: EffectScopeNode): Cleanup
return function() return function()
this.depsTail = nil this.depsTail = nil
this.flags = NONE this.flags = NONE
@ -511,16 +519,15 @@ local function notify(effect: ReactiveNode)
until false until false
queuedLength = insertIndex queuedLength = insertIndex
insertIndex -= 1
-- Reverse the collected effects to maintain correct order -- Reverse the collected effects to ensure outer effects run first
-- Note: insertIndex should decrement before the next condition evaluation while true do
while firstInsertedIndex < insertIndex do
local left = queued[firstInsertedIndex]
queued[firstInsertedIndex] = queued[insertIndex]
queued[insertIndex] = left
firstInsertedIndex += 1
insertIndex -= 1 insertIndex -= 1
if firstInsertedIndex >= insertIndex then
break
end
queued[firstInsertedIndex], queued[insertIndex] = queued[insertIndex], queued[firstInsertedIndex]
firstInsertedIndex += 1
end end
end end
@ -615,10 +622,10 @@ end
@return A cleanup function that stops the effect @return A cleanup function that stops the effect
@see https://github.com/littensy/charm?tab=readme-ov-file#effectcallback @see https://github.com/littensy/charm?tab=readme-ov-file#effectcallback
]] ]]
local function effect(fn: () -> ...(() -> ())?): () -> () local function effect(fn: () -> ...Cleanup?): Cleanup
local this: EffectNode = { local this: EffectNode = {
fn = noYield(fn), fn = noYield(fn),
flags = bit32.bor(WATCHING, RECURSED_CHECK), flags = WATCHING,
cleanups = {}, cleanups = {},
} }
@ -630,8 +637,6 @@ local function effect(fn: () -> ...(() -> ())?): () -> ()
local success, result = pcall(this.fn) local success, result = pcall(this.fn)
activeSub = prevSub activeSub = prevSub
this.flags = bit32.band(this.flags, bit32.bnot(RECURSED_CHECK))
if not success then if not success then
error(result) error(result)
elseif result then elseif result then
@ -650,7 +655,7 @@ end
@return A cleanup function that stops all effects in the scope @return A cleanup function that stops all effects in the scope
@see https://github.com/littensy/charm?tab=readme-ov-file#effectscopecallback-detached @see https://github.com/littensy/charm?tab=readme-ov-file#effectscopecallback-detached
]] ]]
local function effectScope(fn: () -> ...(() -> ())?, detached: boolean?): () -> () local function effectScope(fn: () -> ...Cleanup?, detached: boolean?): Cleanup
local this: EffectScopeNode = { local this: EffectScopeNode = {
flags = NONE, flags = NONE,
cleanups = {}, cleanups = {},
@ -719,7 +724,7 @@ end
@param failSilently If true, silences the warning when called outside an effect/scope @param failSilently If true, silences the warning when called outside an effect/scope
@see https://github.com/littensy/charm?tab=readme-ov-file#oncleanupcallback-failsilently @see https://github.com/littensy/charm?tab=readme-ov-file#oncleanupcallback-failsilently
]] ]]
local function onCleanup(fn: () -> (), failSilently: boolean?) local function onCleanup(fn: Cleanup, failSilently: boolean?)
if activeSub and (activeSub :: EffectNode).cleanups then if activeSub and (activeSub :: EffectNode).cleanups then
table.insert((activeSub :: EffectNode).cleanups, noYield(fn)) table.insert((activeSub :: EffectNode).cleanups, noYield(fn))
elseif not failSilently then elseif not failSilently then
@ -728,10 +733,11 @@ local function onCleanup(fn: () -> (), failSilently: boolean?)
end end
--[[ --[[
Runs the function without subscribing to signal updates or capturing inner Runs the function without subscribing to signal updates and prevents the
effects in the parent effect or scope. current effect or scope from tracking inner effects created within the
function.
For an alternative that still cleans up inner effects, use `peek` instead. For an alternative that allows nested effect cleanup, use `peek` instead.
@param fn A signal or function that accesses signals @param fn A signal or function that accesses signals
@param ...args Arguments to pass to the function @param ...args Arguments to pass to the function
@ -753,8 +759,12 @@ local function untracked<Args..., Results...>(fn: (Args...) -> Results..., ...:
end end
--[[ --[[
Runs the function without subscribing to signal updates, but still captures Runs the function without subscribing to signal updates, but still allows
inner effects so that they dispose with the parent effect. inner effects to be tracked by parent effects/scopes.
This is useful for cases where you want to access the current value of a
signal without creating a dependency, but still want any effects created
within the function to be cleaned up properly.
To avoid capturing inner effects, use `untracked` instead. To avoid capturing inner effects, use `untracked` instead.
@ -815,8 +825,8 @@ end
--[[ --[[
Creates an effect that runs the callback one immediately, and then again Creates an effect that runs the callback one immediately, and then again
when the value returned by the getter changes. The callback receives the new when the value returned by the getter changes. The callback receives the
value and the previous value as arguments. new value and the previous value as arguments.
Effects created within the callback are cleaned up when the listener runs Effects created within the callback are cleaned up when the listener runs
or the listener is disposed. or the listener is disposed.
@ -826,7 +836,7 @@ end
@return A cleanup function that stops the effect @return A cleanup function that stops the effect
@see https://github.com/littensy/charm?tab=readme-ov-file#listengetter-callback @see https://github.com/littensy/charm?tab=readme-ov-file#listengetter-callback
]] ]]
local function listen<T>(getter: () -> T, fn: (state: T, previous: T?) -> ()): () -> () local function listen<T>(getter: () -> T, fn: (state: T, previous: T?) -> ()): Cleanup
local tracked = toComputed(getter) local tracked = toComputed(getter)
local current: T local current: T
@ -838,9 +848,9 @@ local function listen<T>(getter: () -> T, fn: (state: T, previous: T?) -> ()): (
end end
--[[ --[[
Creates an effect that only runs the callback when the value returned by the Creates an effect that only runs the callback when the value returned by
getter changes. The callback receives the new value and the previous value the getter changes. The callback receives the new value and the previous
as arguments. value as arguments.
Effects created within the callback are cleaned up when the listener runs Effects created within the callback are cleaned up when the listener runs
or the listener is disposed. or the listener is disposed.
@ -850,7 +860,7 @@ end
@return A cleanup function that stops the effect @return A cleanup function that stops the effect
@see https://github.com/littensy/charm?tab=readme-ov-file#subscribegetter-callback @see https://github.com/littensy/charm?tab=readme-ov-file#subscribegetter-callback
]] ]]
local function subscribe<T>(getter: () -> T, fn: (state: T, lastState: T) -> ()): () -> () local function subscribe<T>(getter: () -> T, fn: (state: T, lastState: T) -> ()): Cleanup
local tracked = toComputed(getter) local tracked = toComputed(getter)
local current: T local current: T
local firstRun = true local firstRun = true
@ -867,14 +877,15 @@ local function subscribe<T>(getter: () -> T, fn: (state: T, lastState: T) -> ())
end end
--[[ --[[
Disables recursive checks for the current effect or computed signal. Useful Disables recursive checks for the current reactive node. Currently, only
for effects that intentionally update signals they depend on. computed signals use recursion checks, as effects allow recursion.
@example @example
```luau ```luau
effect(function() computed(function()
recursive() recursive()
setCount(getCount() + 1) setCount(getCount() + 1)
return getCount()
end) end)
``` ```
]] ]]
@ -882,7 +893,7 @@ local function recursive()
if activeSub then if activeSub then
activeSub.flags = bit32.band(activeSub.flags, bit32.bnot(RECURSED_CHECK)) activeSub.flags = bit32.band(activeSub.flags, bit32.bnot(RECURSED_CHECK))
else else
warn(debug.traceback("recursive() can only be called inside an effect or computed signal.", 2)) warn(debug.traceback("recursive() can only be called inside a reactive node.", 2))
end end
end end
@ -900,8 +911,8 @@ end
@return A cleanup function that disposes scopes for all keys @return A cleanup function that disposes scopes for all keys
@see https://github.com/littensy/charm?tab=readme-ov-file#observegetter-callback @see https://github.com/littensy/charm?tab=readme-ov-file#observegetter-callback
]] ]]
local function observe<K, V>(getter: () -> { [K]: V }, fn: (item: V, key: K) -> ...(() -> ())?): () -> () local function observe<K, V>(getter: () -> { [K]: V }, fn: (item: V, key: K) -> ...Cleanup?): Cleanup
local innerScopes: { [K]: () -> () } = {} local innerScopes: { [K]: Cleanup } = {}
local disposed = false local disposed = false
local function updateScopes(data: { [K]: V }) local function updateScopes(data: { [K]: V })
@ -930,9 +941,6 @@ local function observe<K, V>(getter: () -> { [K]: V }, fn: (item: V, key: K) ->
return effectScope(function() return effectScope(function()
effect(function() effect(function()
-- Allow recursive updates in case the callback creates or removes
-- items in the observed table.
recursive()
-- Batch so that side effects run after all scopes are created. -- Batch so that side effects run after all scopes are created.
-- This ensures that a scope creating or removing items doesn't -- This ensures that a scope creating or removing items doesn't
-- interfere with the current update cycle. -- interfere with the current update cycle.
@ -940,9 +948,6 @@ local function observe<K, V>(getter: () -> { [K]: V }, fn: (item: V, key: K) ->
end) end)
return function() return function()
if disposed then
return
end
disposed = true disposed = true
for key, dispose in innerScopes do for key, dispose in innerScopes do
innerScopes[key] = nil innerScopes[key] = nil

View file

@ -8,7 +8,6 @@
"jsxFragmentFactory": "React.Fragment", "jsxFragmentFactory": "React.Fragment",
"module": "commonjs", "module": "commonjs",
"moduleResolution": "Node", "moduleResolution": "Node",
"ignoreDeprecations": "6.0",
"noLib": true, "noLib": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"experimentalDecorators": true, "experimentalDecorators": true,