mirror of
https://github.com/littensy/charm.git
synced 2026-10-20 17:51:37 +00:00
feat: update alien-signals, add recursion flag
This commit is contained in:
parent
dffeb35933
commit
96ab2317f2
8 changed files with 100 additions and 30 deletions
|
|
@ -269,6 +269,27 @@ test("should handle flags are indirectly updated during checkDirty", function()
|
|||
assert(triggers == 2, "did not update indirectly")
|
||||
end)
|
||||
|
||||
test("should handle effect recursion for the first execution", 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 run cleanups in sequence", function()
|
||||
local order = {}
|
||||
|
||||
|
|
|
|||
|
|
@ -90,6 +90,29 @@ test("should queue addition and removal", function()
|
|||
assert(table.concat(edits) == "+1+2+3+4-2-3-4", `bad edits: {table.concat(edits)}`)
|
||||
end)
|
||||
|
||||
test("should handle recursive additions and removals", function()
|
||||
local items, setItems = signal({ 1 })
|
||||
local edits = {}
|
||||
|
||||
observe(items, function(item)
|
||||
if item < 5 then
|
||||
setItems(function(state)
|
||||
state = table.clone(state)
|
||||
table.insert(state, item + 1)
|
||||
return state
|
||||
end)
|
||||
else
|
||||
setItems({})
|
||||
end
|
||||
table.insert(edits, `+{item}`)
|
||||
return function()
|
||||
table.insert(edits, `-{item}`)
|
||||
end
|
||||
end)
|
||||
|
||||
assert(table.concat(edits) == "+1+2+3+4+5-1-2-3-4-5", `bad edits: {table.concat(edits)}`)
|
||||
end)
|
||||
|
||||
test("should handle self removal", function()
|
||||
local items, setItems = signal({ 1, 2, 3, 4 })
|
||||
local edits = {}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
local globals = require("./globals")
|
||||
--[[
|
||||
Reactive system adapted from alien-signals at
|
||||
https://github.com/stackblitz/alien-signals/blob/master/src/system.ts
|
||||
|
|
@ -167,10 +168,17 @@ 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
|
||||
|
|
@ -185,7 +193,7 @@ local function createReactiveSystem(props: {
|
|||
flags = NONE
|
||||
end
|
||||
|
||||
if bit32.btest(flags, WATCHING) then
|
||||
if bit32.band(flags, bit32.bor(WATCHING, RECURSED_CHECK)) == WATCHING then
|
||||
notify(sub)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ type EffectScope = ReactiveNode & {
|
|||
}
|
||||
|
||||
type Effect = ReactiveNode & {
|
||||
fn: () -> (() -> ())?,
|
||||
fn: () -> ...(() -> ())?,
|
||||
cleanups: { () -> () },
|
||||
}
|
||||
|
||||
|
|
@ -116,7 +116,7 @@ local function run(e: Effect): ()
|
|||
e.depsTail = nil
|
||||
e.flags = bit32.bor(WATCHING, RECURSED_CHECK)
|
||||
local prevSub = setActiveSub(e)
|
||||
local success, result: (() -> ())? = (if globals.strict then noYield else pcall)(e.fn)
|
||||
local success, result = (if globals.strict then noYield else pcall)(e.fn)
|
||||
activeSub = prevSub
|
||||
e.flags = bit32.band(e.flags, bit32.bnot(RECURSED_CHECK))
|
||||
purgeDeps(e)
|
||||
|
|
@ -162,7 +162,7 @@ local function updateComputed(c: Computed): boolean
|
|||
c.flags = bit32.bor(MUTABLE, RECURSED_CHECK)
|
||||
local prevSub = setActiveSub(c)
|
||||
local oldValue = c.value
|
||||
local success, result: unknown = (if globals.strict then noYield else pcall)(c.getter, oldValue)
|
||||
local success, result = (if globals.strict then noYield else pcall)(c.getter, oldValue)
|
||||
activeSub = prevSub
|
||||
c.flags = bit32.band(c.flags, bit32.bnot(RECURSED_CHECK))
|
||||
purgeDeps(c)
|
||||
|
|
@ -209,10 +209,11 @@ local function computedOper<T>(this: Computed<T>): () -> T
|
|||
end
|
||||
end
|
||||
elseif flags == NONE then
|
||||
this.flags = MUTABLE
|
||||
this.flags = bit32.bor(MUTABLE, RECURSED_CHECK)
|
||||
local prevSub = setActiveSub(this)
|
||||
local success, result = pcall(this.getter)
|
||||
activeSub = prevSub
|
||||
this.flags = bit32.band(this.flags, bit32.bnot(RECURSED_CHECK))
|
||||
if success then
|
||||
this.value = result
|
||||
else
|
||||
|
|
@ -235,7 +236,7 @@ local function signalOper<T>(
|
|||
local value: T
|
||||
if type(update) == "function" then
|
||||
local prevSub = setActiveSub(nil)
|
||||
local success, result: T = (if globals.strict then noYield else pcall)(update, this.pendingValue)
|
||||
local success, result = (if globals.strict then noYield else pcall)(update, this.pendingValue)
|
||||
activeSub = prevSub
|
||||
if not success then
|
||||
error(result)
|
||||
|
|
@ -365,18 +366,19 @@ end
|
|||
--- @param fn The function to run when dependencies change.
|
||||
--- @return A function for disposing the effect.
|
||||
--- @see https://github.com/littensy/charm?tab=readme-ov-file#effectcallback
|
||||
local function effect(fn: () -> ()): () -> ()
|
||||
local function effect(fn: () -> ...(() -> ())?): () -> ()
|
||||
local e: Effect = {
|
||||
fn = fn,
|
||||
flags = WATCHING,
|
||||
flags = bit32.bor(WATCHING, RECURSED_CHECK),
|
||||
cleanups = {},
|
||||
}
|
||||
local prevSub = setActiveSub(e)
|
||||
if prevSub and (globals.trackInnerEffects or prevSub.flags == NONE) then
|
||||
link(e, prevSub, 0)
|
||||
end
|
||||
local success, result: (() -> ())? = (if globals.strict then noYield else pcall)(fn)
|
||||
local success, result = (if globals.strict then noYield else pcall)(fn)
|
||||
activeSub = prevSub
|
||||
e.flags = bit32.band(e.flags, bit32.bnot(RECURSED_CHECK))
|
||||
if not success then
|
||||
error(result)
|
||||
elseif result then
|
||||
|
|
@ -392,7 +394,7 @@ end
|
|||
--- @param detached If true, the scope will not be automatically disposed of when the parent scope is disposed. Defaults to false.
|
||||
--- @return A function for disposing all effects created within the scope.
|
||||
--- @see https://github.com/littensy/charm?tab=readme-ov-file#effectscopecallback-detached
|
||||
local function effectScope(fn: () -> (), detached: boolean?): () -> ()
|
||||
local function effectScope(fn: () -> ...(() -> ())?, detached: boolean?): () -> ()
|
||||
local e: EffectScope = {
|
||||
flags = NONE,
|
||||
cleanups = {},
|
||||
|
|
@ -401,7 +403,7 @@ local function effectScope(fn: () -> (), detached: boolean?): () -> ()
|
|||
if prevSub and not detached then
|
||||
link(e, prevSub, 0)
|
||||
end
|
||||
local success, result: (() -> ())? = (if globals.strict then noYield else pcall)(fn)
|
||||
local success, result = (if globals.strict then noYield else pcall)(fn)
|
||||
activeSub = prevSub
|
||||
if not success then
|
||||
error(result)
|
||||
|
|
|
|||
|
|
@ -17,4 +17,8 @@ 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,
|
||||
}
|
||||
|
|
|
|||
6
packages/charm/src/index.d.ts
vendored
6
packages/charm/src/index.d.ts
vendored
|
|
@ -36,6 +36,12 @@ 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;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -42,16 +42,19 @@ local function observe<K, V>(getter: () -> { [K]: V }, callback: (item: V, key:
|
|||
|
||||
return effectScope(function()
|
||||
effect(function()
|
||||
-- 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, getter())
|
||||
local data = getter()
|
||||
|
||||
-- Effects can't trigger themselves more than once, so if a scope
|
||||
-- creates or removes items, we need to check for updates again. The
|
||||
-- way we do this abuses a weird property of signal accesses that
|
||||
-- causes the effect to re-run.
|
||||
getter()
|
||||
-- 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
|
||||
end)
|
||||
|
||||
return function()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue