mirror of
https://github.com/littensy/charm.git
synced 2026-10-20 17:51:37 +00:00
feat: allow recursion in effects by default
This commit is contained in:
parent
10521d05c2
commit
612612b130
6 changed files with 104 additions and 126 deletions
24
README.md
24
README.md
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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 {}
|
||||||
|
|
|
||||||
42
packages/charm/src/index.d.ts
vendored
42
packages/charm/src/index.d.ts
vendored
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue