diff --git a/README.md b/README.md index dec25b6..4945d65 100644 --- a/README.md +++ b/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. diff --git a/packages/charm/src/index.d.ts b/packages/charm/src/index.d.ts index ebbacc9..ae37672 100644 --- a/packages/charm/src/index.d.ts +++ b/packages/charm/src/index.d.ts @@ -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(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(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. diff --git a/packages/charm/src/init.luau b/packages/charm/src/init.luau index 1342c99..c2c9275 100644 --- a/packages/charm/src/init.luau +++ b/packages/charm/src/init.luau @@ -44,13 +44,12 @@ type SignalNode = 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(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(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, diff --git a/packages/charm/src/system.luau b/packages/charm/src/system.luau index d67efb5..10ad273 100644 --- a/packages/charm/src/system.luau +++ b/packages/charm/src/system.luau @@ -28,14 +28,13 @@ type Stack = { 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 diff --git a/packages/charm/test/init.luau b/packages/charm/test/init.luau index bd58a2f..e13fab7 100644 --- a/packages/charm/test/init.luau +++ b/packages/charm/test/init.luau @@ -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 {} diff --git a/packages/charm/test/peek.test.luau b/packages/charm/test/peek.test.luau deleted file mode 100644 index 3fb1823..0000000 --- a/packages/charm/test/peek.test.luau +++ /dev/null @@ -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 {}