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)
|
- [`observe(getter, callback)`](#observegetter-callback)
|
||||||
- [`subscribe(getter, callback)`](#subscribegetter-callback)
|
- [`subscribe(getter, callback)`](#subscribegetter-callback)
|
||||||
- [`untracked(callback)`](#untrackedcallback)
|
- [`untracked(callback)`](#untrackedcallback)
|
||||||
- [`peek(callback)`](#peekcallback)
|
|
||||||
- [`batch(callback)`](#batchcallback)
|
- [`batch(callback)`](#batchcallback)
|
||||||
- [`mapped(getter, mapper)`](#mappedgetter-mapper)
|
- [`mapped(getter, mapper)`](#mappedgetter-mapper)
|
||||||
- [`onCleanup(callback, failSilently?)`](#oncleanupcallback-failsilently)
|
- [`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)`
|
### `batch(callback)`
|
||||||
|
|
||||||
Combines multiple signal updates made by the callback into a single commit that triggers effects once the callback completes.
|
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;
|
type Cleanup = () => void;
|
||||||
|
|
||||||
export enum ReactiveFlags {
|
export enum ReactiveFlags {
|
||||||
None = 0b0000000,
|
None = 0b000000,
|
||||||
Mutable = 0b0000001,
|
Mutable = 0b000001,
|
||||||
Watching = 0b0000010,
|
Watching = 0b000010,
|
||||||
RecursedCheck = 0b0000100,
|
RecursedCheck = 0b000100,
|
||||||
Recursed = 0b0001000,
|
Recursed = 0b001000,
|
||||||
Dirty = 0b0010000,
|
Dirty = 0b010000,
|
||||||
Pending = 0b0100000,
|
Pending = 0b100000,
|
||||||
Peeking = 0b1000000,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ReactiveNode {
|
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
|
* current effect or scope from tracking inner effects created within the
|
||||||
* function.
|
* 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 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.
|
||||||
|
|
@ -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;
|
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
|
* Combines multiple updates into a single "commit" that runs effects and
|
||||||
* computed signals after the provided function finishes running.
|
* 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
|
-- Bitmask flags for tracking the state of reactive nodes. Flags are combined
|
||||||
-- and checked using bitwise operations for efficiency.
|
-- and checked using bitwise operations for efficiency.
|
||||||
local NONE = 0b0000000 -- Default state
|
local NONE = 0b000000 -- Default state
|
||||||
local MUTABLE = 0b0000001 -- Can be changed (signal or computed)
|
local MUTABLE = 0b000001 -- Can be changed (signal or computed)
|
||||||
local WATCHING = 0b0000010 -- Watching for changes (effects)
|
local WATCHING = 0b000010 -- Watching for changes (effects)
|
||||||
local RECURSED_CHECK = 0b0000100 -- Being checked for circular dependencies
|
local RECURSED_CHECK = 0b000100 -- Being checked for circular dependencies
|
||||||
local DIRTY = 0b0010000 -- Value has changed and needs updating
|
local DIRTY = 0b010000 -- Value has changed and needs updating
|
||||||
local PENDING = 0b0100000 -- Might be dirty, pending verification
|
local PENDING = 0b100000 -- Might be dirty, pending verification
|
||||||
local PEEKING = 0b1000000 -- Blocking dependency tracking
|
|
||||||
|
|
||||||
-- Global version counter for dependency link deduplication. Incremented at the
|
-- Global version counter for dependency link deduplication. Incremented at the
|
||||||
-- start of each effect/computed run to ensure links created in the same cycle
|
-- 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
|
current effect or scope from tracking inner effects created within the
|
||||||
function.
|
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 fn A signal or function that accesses signals
|
||||||
@param ...args Arguments to pass to the function
|
@param ...args Arguments to pass to the function
|
||||||
|
|
@ -724,39 +724,6 @@ local function untracked<Args..., Results...>(fn: (Args...) -> Results..., ...:
|
||||||
end
|
end
|
||||||
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
|
Combines multiple updates into a single "commit" that runs effects and
|
||||||
computed signals after the provided callback finishes running.
|
computed signals after the provided callback finishes running.
|
||||||
|
|
@ -978,7 +945,6 @@ return {
|
||||||
effectScope = effectScope,
|
effectScope = effectScope,
|
||||||
trigger = trigger,
|
trigger = trigger,
|
||||||
untracked = untracked,
|
untracked = untracked,
|
||||||
peek = peek,
|
|
||||||
batch = batch,
|
batch = batch,
|
||||||
subscribe = subscribe,
|
subscribe = subscribe,
|
||||||
listen = listen,
|
listen = listen,
|
||||||
|
|
|
||||||
|
|
@ -28,14 +28,13 @@ type Stack<T> = {
|
||||||
|
|
||||||
export type ReactiveFlags = number
|
export type ReactiveFlags = number
|
||||||
|
|
||||||
local NONE = 0b0000000
|
local NONE = 0b000000
|
||||||
local MUTABLE = 0b0000001
|
local MUTABLE = 0b000001
|
||||||
local WATCHING = 0b0000010
|
local WATCHING = 0b000010
|
||||||
local RECURSED_CHECK = 0b0000100
|
local RECURSED_CHECK = 0b000100
|
||||||
local RECURSED = 0b0001000
|
local RECURSED = 0b001000
|
||||||
local DIRTY = 0b0010000
|
local DIRTY = 0b010000
|
||||||
local PENDING = 0b0100000
|
local PENDING = 0b100000
|
||||||
local PEEKING = 0b1000000
|
|
||||||
|
|
||||||
local ReactiveFlags = table.freeze({
|
local ReactiveFlags = table.freeze({
|
||||||
None = NONE,
|
None = NONE,
|
||||||
|
|
@ -45,7 +44,6 @@ local ReactiveFlags = table.freeze({
|
||||||
Recursed = RECURSED,
|
Recursed = RECURSED,
|
||||||
Dirty = DIRTY,
|
Dirty = DIRTY,
|
||||||
Pending = PENDING,
|
Pending = PENDING,
|
||||||
Peeking = PEEKING,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
local update: (sub: ReactiveNode) -> boolean
|
local update: (sub: ReactiveNode) -> boolean
|
||||||
|
|
@ -64,10 +62,6 @@ local function isValidLink(checkLink: Link, sub: ReactiveNode): boolean
|
||||||
end
|
end
|
||||||
|
|
||||||
local function link(dep: ReactiveNode, sub: ReactiveNode, version: number)
|
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
|
local prevDep = sub.depsTail
|
||||||
if prevDep and prevDep.dep == dep then
|
if prevDep and prevDep.dep == dep then
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,51 @@
|
||||||
local test = require("../../tests/test")
|
local test = require("../../tests/test")
|
||||||
|
|
||||||
local tests = {
|
test.describe("computed", function()
|
||||||
"computed",
|
require("@self/computed.test")
|
||||||
"effect",
|
end)
|
||||||
"effectScope",
|
|
||||||
"gc",
|
|
||||||
"listen",
|
|
||||||
"mapped",
|
|
||||||
"observe",
|
|
||||||
"peek",
|
|
||||||
"signal",
|
|
||||||
"subscribe",
|
|
||||||
"topology",
|
|
||||||
"trigger",
|
|
||||||
"untracked",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, name in tests do
|
test.describe("effect", function()
|
||||||
test.describe(name, function()
|
require("@self/effect.test")
|
||||||
(require)(`@self/{name}.test`)
|
end)
|
||||||
end)
|
|
||||||
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 {}
|
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