mirror of
https://github.com/littensy/charm.git
synced 2026-10-20 17:51:37 +00:00
feat: add recursive()
This commit is contained in:
parent
96ab2317f2
commit
d890f4fc2c
8 changed files with 120 additions and 38 deletions
|
|
@ -6,6 +6,7 @@ local effect = core.effect
|
|||
local effectScope = core.effectScope
|
||||
local endBatch = core.endBatch
|
||||
local onCleanup = core.onCleanup
|
||||
local recursive = core.recursive
|
||||
local setActiveSub = core.setActiveSub
|
||||
local signal = core.signal
|
||||
local startBatch = core.startBatch
|
||||
|
|
@ -290,6 +291,18 @@ test("should handle effect recursion for the first execution", function()
|
|||
assert(triggers2 == 1, `bad triggers2: {triggers2}`)
|
||||
end)
|
||||
|
||||
test("should opt in to recursion", function()
|
||||
local src, setSrc = signal(0)
|
||||
local order = {}
|
||||
|
||||
effect(function()
|
||||
recursive()
|
||||
table.insert(order, src())
|
||||
setSrc(math.min(src() + 1, 5))
|
||||
end)
|
||||
assert(table.concat(order) == "012345", `bad order: {table.concat(order)}`)
|
||||
end)
|
||||
|
||||
test("should run cleanups in sequence", function()
|
||||
local order = {}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
local globals = require("./globals")
|
||||
--[[
|
||||
Reactive system adapted from alien-signals at
|
||||
https://github.com/stackblitz/alien-signals/blob/master/src/system.ts
|
||||
|
|
@ -168,17 +167,10 @@ local function createReactiveSystem(props: {
|
|||
end
|
||||
|
||||
local function propagate(link: Link): ()
|
||||
local allowRecursion = globals.allowRecursion
|
||||
|
||||
repeat
|
||||
local sub = link.sub
|
||||
local flags = sub.flags
|
||||
|
||||
if allowRecursion then
|
||||
flags = bit32.band(flags, bit32.bnot(bit32.bor(RECURSED, RECURSED_CHECK)))
|
||||
sub.flags = flags
|
||||
end
|
||||
|
||||
if bit32.band(flags, bit32.bor(RECURSED_CHECK, RECURSED, DIRTY, PENDING)) == 0 then
|
||||
sub.flags = bit32.bor(flags, PENDING)
|
||||
elseif bit32.band(flags, bit32.bor(RECURSED_CHECK, RECURSED)) == 0 then
|
||||
|
|
|
|||
|
|
@ -555,6 +555,24 @@ local function subscribe<T>(getter: () -> T, callback: (state: T, lastState: T)
|
|||
end)
|
||||
end
|
||||
|
||||
--- Disables recursive checks for the current effect or computed signal. Useful
|
||||
--- for effects that intentionally update signals they depend on.
|
||||
---
|
||||
--- @example
|
||||
--- ```luau
|
||||
--- effect(function()
|
||||
--- recursive()
|
||||
--- setCount(getCount() + 1)
|
||||
--- end)
|
||||
--- ```
|
||||
local function recursive()
|
||||
if activeSub then
|
||||
activeSub.flags = bit32.band(activeSub.flags, bit32.bnot(RECURSED_CHECK))
|
||||
else
|
||||
warn(debug.traceback("recursive() can only be called inside an effect or computed signal.", 2))
|
||||
end
|
||||
end
|
||||
|
||||
local reactiveSystem = createReactiveSystem({
|
||||
update = function(node)
|
||||
if node.depsTail then
|
||||
|
|
@ -626,4 +644,5 @@ return {
|
|||
batch = batch,
|
||||
subscribe = subscribe,
|
||||
listen = listen,
|
||||
recursive = recursive,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,8 +17,4 @@ return {
|
|||
--- when the parent effect re-runs. This is usually desirable, but can be
|
||||
--- disabled to revert to the old behavior. Defaults to `true`.
|
||||
trackInnerEffects = true,
|
||||
--- Whether to allow recursive updates in effects and computed signals.
|
||||
--- Disabling this can help catch certain classes of bugs, but may also
|
||||
--- cause valid code to error. Defaults to `false`.
|
||||
allowRecursion = false,
|
||||
}
|
||||
|
|
|
|||
63
packages/charm/src/index.d.ts
vendored
63
packages/charm/src/index.d.ts
vendored
|
|
@ -36,12 +36,6 @@ export const globals: {
|
|||
* disabled to revert to the old behavior. Defaults to `true`.
|
||||
*/
|
||||
trackInnerEffects: boolean;
|
||||
/**
|
||||
* Whether to allow recursive updates in effects and computed signals.
|
||||
* Disabling this can help catch certain classes of bugs, but may also
|
||||
* cause valid code to error. Defaults to `false`.
|
||||
*/
|
||||
allowRecursion: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -238,3 +232,60 @@ export function observe<V, K extends Key>(
|
|||
getter: () => { readonly [K in Key]: V },
|
||||
callback: (value: V, key: K) => Cleanup | void,
|
||||
): Cleanup;
|
||||
|
||||
export namespace core {
|
||||
export { signal, effect, effectScope, atom, computed, onCleanup, untracked, peek, batch, listen };
|
||||
|
||||
export enum ReactiveFlags {
|
||||
None = 0b0000000,
|
||||
Mutable = 0b0000001,
|
||||
Watching = 0b0000010,
|
||||
RecursedCheck = 0b0000100,
|
||||
Recursed = 0b0001000,
|
||||
Dirty = 0b0010000,
|
||||
Pending = 0b0100000,
|
||||
Peeking = 0b1000000,
|
||||
}
|
||||
|
||||
export interface ReactiveNode {
|
||||
deps?: Link;
|
||||
depsTail?: Link;
|
||||
subs?: Link;
|
||||
subsTail?: Link;
|
||||
flags: ReactiveFlags;
|
||||
}
|
||||
|
||||
export interface Link {
|
||||
version: number;
|
||||
dep: ReactiveNode;
|
||||
sub: ReactiveNode;
|
||||
prevSub?: Link;
|
||||
nextSub?: Link;
|
||||
prevDep?: Link;
|
||||
nextDep?: Link;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables recursive checks for the current effect or computed signal.
|
||||
* Useful for effects that intentionally update signals they depend on.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* effect(() => {
|
||||
* recursive();
|
||||
* setCount(getCount() + 1);
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function recursive(): void;
|
||||
|
||||
export function getActiveSub(): ReactiveNode | undefined;
|
||||
|
||||
export function setActiveSub(sub?: ReactiveNode): ReactiveNode | undefined;
|
||||
|
||||
export function getBatchDepth(): number;
|
||||
|
||||
export function startBatch(): void;
|
||||
|
||||
export function endBatch(): void;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ return {
|
|||
listen = core.listen,
|
||||
onCleanup = core.onCleanup,
|
||||
peek = core.peek,
|
||||
recursive = core.recursive,
|
||||
signal = core.signal,
|
||||
subscribe = core.subscribe,
|
||||
untracked = core.untracked,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
local core = require("./core")
|
||||
|
||||
type ReactiveNode = core.ReactiveNode
|
||||
|
||||
local batch = core.batch
|
||||
local effect = core.effect
|
||||
local effectScope = core.effectScope
|
||||
local recursive = core.recursive
|
||||
|
||||
--- Calls the callback function for each unique key in the table returned by the
|
||||
--- getter. The callback is called once for each key, and again when a new key
|
||||
|
|
@ -42,19 +41,13 @@ local function observe<K, V>(getter: () -> { [K]: V }, callback: (item: V, key:
|
|||
|
||||
return effectScope(function()
|
||||
effect(function()
|
||||
local data = getter()
|
||||
|
||||
-- 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.
|
||||
-- This ensures that a scope creating or removing items doesn't
|
||||
-- interfere with the current update cycle.
|
||||
batch(updateScopes, data)
|
||||
|
||||
-- Effects can't trigger themselves, so if a scope creates or
|
||||
-- removes items, we need to check for updates again.
|
||||
local newData = getter()
|
||||
if newData ~= data then
|
||||
batch(updateScopes, newData)
|
||||
end
|
||||
batch(updateScopes, getter())
|
||||
end)
|
||||
|
||||
return function()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue