mirror of
https://github.com/littensy/charm.git
synced 2026-10-20 17:51:37 +00:00
refactor!: remove peek
This commit is contained in:
parent
5c27052965
commit
fd6b3efbb5
6 changed files with 70 additions and 262 deletions
17
README.md
17
README.md
|
|
@ -50,7 +50,6 @@ Charm is a state management library based on [fine-grained reactivity](https://d
|
|||
- [`observe(getter, callback)`](#observegetter-callback)
|
||||
- [`subscribe(getter, callback)`](#subscribegetter-callback)
|
||||
- [`untracked(callback)`](#untrackedcallback)
|
||||
- [`peek(callback)`](#peekcallback)
|
||||
- [`batch(callback)`](#batchcallback)
|
||||
- [`mapped(getter, mapper)`](#mappedgetter-mapper)
|
||||
- [`onCleanup(callback, failSilently?)`](#oncleanupcallback-failsilently)
|
||||
|
|
@ -451,22 +450,6 @@ stopEffect() -- Cleaned up untracked effect
|
|||
|
||||
---
|
||||
|
||||
### `peek(callback)`
|
||||
|
||||
Similar to `untracked()`, but does not prevent the parent effect from tracking nested effects created in the callback. If your callback reads a signal and creates effects, only the signals get untracked.
|
||||
|
||||
```luau
|
||||
local getCounter, setCounter = signal(0)
|
||||
|
||||
local disposeOuter = effect(function()
|
||||
print(`Count is {peek(getCounter)}`)
|
||||
end) -- Count is 0
|
||||
|
||||
setCounter(1) -- No output; count was accessed in peek()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `batch(callback)`
|
||||
|
||||
Combines multiple signal updates made by the callback into a single commit that triggers effects once the callback completes.
|
||||
|
|
|
|||
33
packages/charm/src/index.d.ts
vendored
33
packages/charm/src/index.d.ts
vendored
|
|
@ -3,14 +3,13 @@ type Key = string | number | symbol;
|
|||
type Cleanup = () => void;
|
||||
|
||||
export enum ReactiveFlags {
|
||||
None = 0b0000000,
|
||||
Mutable = 0b0000001,
|
||||
Watching = 0b0000010,
|
||||
RecursedCheck = 0b0000100,
|
||||
Recursed = 0b0001000,
|
||||
Dirty = 0b0010000,
|
||||
Pending = 0b0100000,
|
||||
Peeking = 0b1000000,
|
||||
None = 0b000000,
|
||||
Mutable = 0b000001,
|
||||
Watching = 0b000010,
|
||||
RecursedCheck = 0b000100,
|
||||
Recursed = 0b001000,
|
||||
Dirty = 0b010000,
|
||||
Pending = 0b100000,
|
||||
}
|
||||
|
||||
export interface ReactiveNode {
|
||||
|
|
@ -154,7 +153,8 @@ export function onCleanup(fn: Cleanup, failSilently?: boolean): void;
|
|||
* current effect or scope from tracking inner effects created within the
|
||||
* function.
|
||||
*
|
||||
* For an alternative that allows nested effect cleanup, use `peek` instead.
|
||||
* To avoid tracking signals while still allowing inner effects to be tracked
|
||||
* by parent effects/scopes, use `effectScope()` instead.
|
||||
*
|
||||
* @param fn A function that may read signals or create effects.
|
||||
* @param args Arguments to pass to the function.
|
||||
|
|
@ -163,21 +163,6 @@ export function onCleanup(fn: Cleanup, failSilently?: boolean): void;
|
|||
*/
|
||||
export function untracked<Args extends any[], Result>(fn: (...args: Args) => Result, ...args: Args): Result;
|
||||
|
||||
/**
|
||||
* Runs the function without subscribing to signal updates, but still allows
|
||||
* 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.
|
||||
*
|
||||
* @param fn A function that may read signals.
|
||||
* @param args Arguments to pass to the function.
|
||||
* @returns The return value of the function.
|
||||
* @see https://github.com/littensy/charm?tab=readme-ov-file#peekcallback
|
||||
*/
|
||||
export function peek<Args extends any[], Result>(fn: (...args: Args) => Result, ...args: Args): Result;
|
||||
|
||||
/**
|
||||
* Combines multiple updates into a single "commit" that runs effects and
|
||||
* computed signals after the provided function finishes running.
|
||||
|
|
|
|||
|
|
@ -44,13 +44,12 @@ type SignalNode<T = any> = ReactiveNode & {
|
|||
|
||||
-- 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 NONE = 0b000000 -- Default state
|
||||
local MUTABLE = 0b000001 -- Can be changed (signal or computed)
|
||||
local WATCHING = 0b000010 -- Watching for changes (effects)
|
||||
local RECURSED_CHECK = 0b000100 -- Being checked for circular dependencies
|
||||
local DIRTY = 0b010000 -- Value has changed and needs updating
|
||||
local PENDING = 0b100000 -- Might be dirty, pending verification
|
||||
|
||||
-- Global version counter for dependency link deduplication. Incremented at the
|
||||
-- start of each effect/computed run to ensure links created in the same cycle
|
||||
|
|
@ -703,7 +702,8 @@ end
|
|||
current effect or scope from tracking inner effects created within the
|
||||
function.
|
||||
|
||||
For an alternative that allows nested effect cleanup, use `peek` instead.
|
||||
To avoid tracking signals while still allowing inner effects to be tracked
|
||||
by parent effects/scopes, use `effectScope()` instead.
|
||||
|
||||
@param fn A signal or function that accesses signals
|
||||
@param ...args Arguments to pass to the function
|
||||
|
|
@ -724,39 +724,6 @@ local function untracked<Args..., Results...>(fn: (Args...) -> Results..., ...:
|
|||
end
|
||||
end
|
||||
|
||||
--[[
|
||||
Runs the function without subscribing to signal updates, but still allows
|
||||
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.
|
||||
|
||||
@param fn A signal or function that accesses signals
|
||||
@param ...args Arguments to pass to the function
|
||||
@return Values returned by the function
|
||||
@see https://github.com/littensy/charm?tab=readme-ov-file#peekcallback
|
||||
]]
|
||||
local function peek<Args..., Results...>(fn: (Args...) -> Results..., ...: Args...): Results...
|
||||
if
|
||||
activeSub
|
||||
and bit32.band(activeSub.flags, PEEKING) == 0
|
||||
and bit32.btest(activeSub.flags, bit32.bor(WATCHING, MUTABLE))
|
||||
then
|
||||
activeSub.flags = bit32.bor(activeSub.flags, PEEKING)
|
||||
local results: { any } = { pcall(noYield(fn), ...) }
|
||||
activeSub.flags = bit32.band(activeSub.flags, bit32.bnot(PEEKING))
|
||||
if not results[1] then
|
||||
error(results[2])
|
||||
end
|
||||
return unpack(results, 2)
|
||||
else
|
||||
return fn(...)
|
||||
end
|
||||
end
|
||||
|
||||
--[[
|
||||
Combines multiple updates into a single "commit" that runs effects and
|
||||
computed signals after the provided callback finishes running.
|
||||
|
|
@ -978,7 +945,6 @@ return {
|
|||
effectScope = effectScope,
|
||||
trigger = trigger,
|
||||
untracked = untracked,
|
||||
peek = peek,
|
||||
batch = batch,
|
||||
subscribe = subscribe,
|
||||
listen = listen,
|
||||
|
|
|
|||
|
|
@ -28,14 +28,13 @@ type Stack<T> = {
|
|||
|
||||
export type ReactiveFlags = number
|
||||
|
||||
local NONE = 0b0000000
|
||||
local MUTABLE = 0b0000001
|
||||
local WATCHING = 0b0000010
|
||||
local RECURSED_CHECK = 0b0000100
|
||||
local RECURSED = 0b0001000
|
||||
local DIRTY = 0b0010000
|
||||
local PENDING = 0b0100000
|
||||
local PEEKING = 0b1000000
|
||||
local NONE = 0b000000
|
||||
local MUTABLE = 0b000001
|
||||
local WATCHING = 0b000010
|
||||
local RECURSED_CHECK = 0b000100
|
||||
local RECURSED = 0b001000
|
||||
local DIRTY = 0b010000
|
||||
local PENDING = 0b100000
|
||||
|
||||
local ReactiveFlags = table.freeze({
|
||||
None = NONE,
|
||||
|
|
@ -45,7 +44,6 @@ local ReactiveFlags = table.freeze({
|
|||
Recursed = RECURSED,
|
||||
Dirty = DIRTY,
|
||||
Pending = PENDING,
|
||||
Peeking = PEEKING,
|
||||
})
|
||||
|
||||
local update: (sub: ReactiveNode) -> boolean
|
||||
|
|
@ -64,10 +62,6 @@ local function isValidLink(checkLink: Link, sub: ReactiveNode): boolean
|
|||
end
|
||||
|
||||
local function link(dep: ReactiveNode, sub: ReactiveNode, version: number)
|
||||
-- DEVIATION: Do not link signals to peeking subscribers
|
||||
if bit32.btest(sub.flags, PEEKING) and bit32.btest(dep.flags, MUTABLE) then
|
||||
return
|
||||
end
|
||||
local prevDep = sub.depsTail
|
||||
if prevDep and prevDep.dep == dep then
|
||||
return
|
||||
|
|
|
|||
|
|
@ -1,25 +1,51 @@
|
|||
local test = require("../../tests/test")
|
||||
|
||||
local tests = {
|
||||
"computed",
|
||||
"effect",
|
||||
"effectScope",
|
||||
"gc",
|
||||
"listen",
|
||||
"mapped",
|
||||
"observe",
|
||||
"peek",
|
||||
"signal",
|
||||
"subscribe",
|
||||
"topology",
|
||||
"trigger",
|
||||
"untracked",
|
||||
}
|
||||
test.describe("computed", function()
|
||||
require("@self/computed.test")
|
||||
end)
|
||||
|
||||
for _, name in tests do
|
||||
test.describe(name, function()
|
||||
(require)(`@self/{name}.test`)
|
||||
end)
|
||||
end
|
||||
test.describe("effect", function()
|
||||
require("@self/effect.test")
|
||||
end)
|
||||
|
||||
test.describe("effectScope", function()
|
||||
require("@self/effectScope.test")
|
||||
end)
|
||||
|
||||
test.describe("gc", function()
|
||||
require("@self/gc.test")
|
||||
end)
|
||||
|
||||
test.describe("listen", function()
|
||||
require("@self/listen.test")
|
||||
end)
|
||||
|
||||
test.describe("mapped", function()
|
||||
require("@self/mapped.test")
|
||||
end)
|
||||
|
||||
test.describe("observe", function()
|
||||
require("@self/observe.test")
|
||||
end)
|
||||
|
||||
test.describe("signal", function()
|
||||
require("@self/signal.test")
|
||||
end)
|
||||
|
||||
test.describe("subscribe", function()
|
||||
require("@self/subscribe.test")
|
||||
end)
|
||||
|
||||
test.describe("topology", function()
|
||||
require("@self/topology.test")
|
||||
end)
|
||||
|
||||
test.describe("trigger", function()
|
||||
require("@self/trigger.test")
|
||||
end)
|
||||
|
||||
test.describe("untracked", function()
|
||||
require("@self/untracked.test")
|
||||
end)
|
||||
|
||||
return {}
|
||||
|
|
|
|||
|
|
@ -1,146 +0,0 @@
|
|||
local Charm = require("../src")
|
||||
local test = require("../../../tests/test")
|
||||
|
||||
local signal = Charm.signal
|
||||
local computed = Charm.computed
|
||||
local effect = Charm.effect
|
||||
local peek = Charm.peek
|
||||
|
||||
test("should not run detached", function()
|
||||
local a, setA = signal(1)
|
||||
local spyCalls = 0
|
||||
|
||||
effect(function()
|
||||
a()
|
||||
peek(function()
|
||||
effect(function()
|
||||
return function()
|
||||
spyCalls += 1
|
||||
end
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
assert(spyCalls == 0, "should not be called yet")
|
||||
setA(2)
|
||||
assert(spyCalls == 1, "should be called once")
|
||||
setA(3)
|
||||
assert(spyCalls == 2, "should be called twice")
|
||||
end)
|
||||
|
||||
test("should block tracking inside effects", function()
|
||||
local a, setA = signal(1)
|
||||
local b, setB = signal(2)
|
||||
local spyCalls = 0
|
||||
|
||||
effect(function()
|
||||
peek(function()
|
||||
spyCalls += 1
|
||||
return a() + b()
|
||||
end)
|
||||
end)
|
||||
assert(spyCalls == 1, "should be called once")
|
||||
|
||||
setA(10)
|
||||
setB(20)
|
||||
assert(spyCalls == 1, "should be called once")
|
||||
end)
|
||||
|
||||
test("should block tracking even when run inside effect run inside peek", function()
|
||||
local s, setS = signal(1)
|
||||
local spyCalls = 0
|
||||
|
||||
peek(function()
|
||||
effect(function()
|
||||
peek(function()
|
||||
spyCalls += 1
|
||||
return s()
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
assert(spyCalls == 1, "should be called once")
|
||||
|
||||
setS(2)
|
||||
assert(spyCalls == 1, "should be called once")
|
||||
end)
|
||||
|
||||
test("should not cause signal assignments to throw", function()
|
||||
local a, setA = signal(1)
|
||||
local aChangedTime, setAChangedTime = signal(0)
|
||||
|
||||
local dispose = effect(function()
|
||||
a()
|
||||
peek(function()
|
||||
setAChangedTime(aChangedTime() + 1)
|
||||
end)
|
||||
end)
|
||||
|
||||
setA(2)
|
||||
assert(aChangedTime() == 2, "aChangedTime should be 2")
|
||||
setA(3)
|
||||
assert(aChangedTime() == 3, "aChangedTime should be 3")
|
||||
|
||||
dispose()
|
||||
end)
|
||||
|
||||
test("should block tracking inside computed signals", function()
|
||||
local a, setA = signal(1)
|
||||
local b, setB = signal(2)
|
||||
local spyCalls = 0
|
||||
local c = computed(function()
|
||||
return peek(function()
|
||||
spyCalls += 1
|
||||
return a() + b()
|
||||
end)
|
||||
end)
|
||||
|
||||
assert(spyCalls == 0, "should not be called yet")
|
||||
assert(c() == 3, "c should be 3")
|
||||
setA(10)
|
||||
c()
|
||||
setB(20)
|
||||
c()
|
||||
assert(spyCalls == 1, "should be called once")
|
||||
assert(c() == 3, "c should be 3")
|
||||
end)
|
||||
|
||||
test("should track outside peek", function()
|
||||
local a, setA = signal(1)
|
||||
local b, setB = signal(2)
|
||||
local c, setC = signal(3)
|
||||
local spyCalls = 0
|
||||
|
||||
effect(function()
|
||||
a()
|
||||
peek(c)
|
||||
b()
|
||||
spyCalls += 1
|
||||
end)
|
||||
|
||||
assert(spyCalls == 1, "should be called once")
|
||||
setA(10)
|
||||
assert(spyCalls == 2, "should be called twice")
|
||||
setB(20)
|
||||
assert(spyCalls == 3, "should be called three times")
|
||||
setC(30)
|
||||
assert(spyCalls == 3, "should not run on c change")
|
||||
end)
|
||||
|
||||
test("should stack peek", function()
|
||||
local a, setA = signal(1)
|
||||
local spyCalls = 0
|
||||
|
||||
effect(function()
|
||||
peek(function()
|
||||
peek(a)
|
||||
return a()
|
||||
end)
|
||||
spyCalls += 1
|
||||
end)
|
||||
|
||||
assert(spyCalls == 1, "should be called once")
|
||||
setA(2)
|
||||
assert(spyCalls == 1, "should not be called twice")
|
||||
end)
|
||||
|
||||
return {}
|
||||
Loading…
Add table
Add a link
Reference in a new issue