mirror of
https://github.com/littensy/charm.git
synced 2026-10-20 17:51:37 +00:00
feat: add peek, make untracked escape scope
This commit is contained in:
parent
f5709c3112
commit
1657e2cb74
12 changed files with 248 additions and 28 deletions
|
|
@ -28,7 +28,7 @@ test("should listen to selector", function()
|
|||
assert(current == 2 and previous == 1, "should not run after unsub")
|
||||
end)
|
||||
|
||||
test("should run in untracked", function()
|
||||
test("should run in peek", function()
|
||||
local runs = 0
|
||||
local cleanups = 0
|
||||
local a = atom(0)
|
||||
|
|
|
|||
147
packages/charm/src/__tests__/peek.spec.luau
Normal file
147
packages/charm/src/__tests__/peek.spec.luau
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
local Charm = require("../")
|
||||
local suite = require("../../../../test/suite")
|
||||
|
||||
local atom = Charm.atom
|
||||
local computed = Charm.computed
|
||||
local effect = Charm.effect
|
||||
local peek = Charm.peek
|
||||
local test = suite.test
|
||||
|
||||
test("should not run detached", function()
|
||||
local a = atom(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")
|
||||
a(2)
|
||||
assert(spyCalls == 1, "should be called once")
|
||||
a(3)
|
||||
assert(spyCalls == 2, "should be called twice")
|
||||
end)
|
||||
|
||||
test("should block tracking inside effects", function()
|
||||
local a = atom(1)
|
||||
local b = atom(2)
|
||||
local spyCalls = 0
|
||||
|
||||
effect(function()
|
||||
peek(function()
|
||||
spyCalls += 1
|
||||
return a() + b()
|
||||
end)
|
||||
end)
|
||||
assert(spyCalls == 1, "should be called once")
|
||||
|
||||
a(10)
|
||||
b(20)
|
||||
assert(spyCalls == 1, "should be called once")
|
||||
end)
|
||||
|
||||
test("should block tracking even when run inside effect run inside peek", function()
|
||||
local s = atom(1)
|
||||
local spyCalls = 0
|
||||
|
||||
peek(function()
|
||||
effect(function()
|
||||
peek(function()
|
||||
spyCalls += 1
|
||||
return s()
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
assert(spyCalls == 1, "should be called once")
|
||||
|
||||
s(2)
|
||||
assert(spyCalls == 1, "should be called once")
|
||||
end)
|
||||
|
||||
test("should not cause atom assignments throw", function()
|
||||
local a = atom(1)
|
||||
local aChangedTime = atom(0)
|
||||
|
||||
local dispose = effect(function()
|
||||
a()
|
||||
peek(function()
|
||||
aChangedTime(aChangedTime() + 1)
|
||||
end)
|
||||
end)
|
||||
|
||||
a(2)
|
||||
assert(aChangedTime() == 2, "aChangedTime should be 2")
|
||||
a(3)
|
||||
assert(aChangedTime() == 3, "aChangedTime should be 3")
|
||||
|
||||
dispose()
|
||||
end)
|
||||
|
||||
test("should block tracking inside computed atoms", function()
|
||||
local a = atom(1)
|
||||
local b = atom(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")
|
||||
a(10)
|
||||
c()
|
||||
b(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 = atom(1)
|
||||
local b = atom(2)
|
||||
local c = atom(3)
|
||||
local spyCalls = 0
|
||||
|
||||
effect(function()
|
||||
a()
|
||||
peek(c)
|
||||
b()
|
||||
spyCalls += 1
|
||||
end)
|
||||
|
||||
assert(spyCalls == 1, "should be called once")
|
||||
a(10)
|
||||
assert(spyCalls == 2, "should be called twice")
|
||||
b(20)
|
||||
assert(spyCalls == 3, "should be called three times")
|
||||
c(30)
|
||||
assert(spyCalls == 3, "should not run on c change")
|
||||
end)
|
||||
|
||||
test("should stack peek", function()
|
||||
local a = atom(1)
|
||||
local spyCalls = 0
|
||||
|
||||
effect(function()
|
||||
peek(function()
|
||||
peek(a)
|
||||
return a()
|
||||
end)
|
||||
spyCalls += 1
|
||||
end)
|
||||
|
||||
assert(spyCalls == 1, "should be called once")
|
||||
a(2)
|
||||
assert(spyCalls == 1, "should not be called twice")
|
||||
end)
|
||||
|
||||
return {}
|
||||
|
|
@ -36,5 +36,8 @@ end)
|
|||
suite.describe("untracked", function()
|
||||
require("./untracked.spec")
|
||||
end)
|
||||
suite.describe("peek", function()
|
||||
require("./peek.spec")
|
||||
end)
|
||||
|
||||
return {}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ test("should subscribe to selector", function()
|
|||
assert(current == 2 and previous == 1, "should not run after unsub")
|
||||
end)
|
||||
|
||||
test("should run in untracked", function()
|
||||
test("should run in peek", function()
|
||||
local runs = 0
|
||||
local cleanups = 0
|
||||
local a = atom(0)
|
||||
|
|
|
|||
|
|
@ -4,29 +4,59 @@ local suite = require("../../../../test/suite")
|
|||
local atom = Charm.atom
|
||||
local computed = Charm.computed
|
||||
local effect = Charm.effect
|
||||
local effectScope = Charm.effectScope
|
||||
local untracked = Charm.untracked
|
||||
local test = suite.test
|
||||
|
||||
test("should not run detached", function()
|
||||
test("should run detached from effect", function()
|
||||
local a = atom(1)
|
||||
local spyCalls = 0
|
||||
local effects = 0
|
||||
local innerEffects = {}
|
||||
|
||||
effect(function()
|
||||
local stopEffect = effect(function()
|
||||
a()
|
||||
untracked(function()
|
||||
effect(function()
|
||||
local stopInnerEffect = effect(function()
|
||||
effects += 1
|
||||
return function()
|
||||
spyCalls += 1
|
||||
effects -= 1
|
||||
end
|
||||
end)
|
||||
table.insert(innerEffects, stopInnerEffect)
|
||||
end)
|
||||
end)
|
||||
|
||||
assert(effects == 1, "should have one effect")
|
||||
a(2)
|
||||
assert(effects == 2, "should have two effects")
|
||||
stopEffect()
|
||||
assert(effects == 2, "should still have two effects")
|
||||
for _, stopInnerEffect in innerEffects do
|
||||
stopInnerEffect()
|
||||
end
|
||||
assert(effects == 0, "should have zero effects")
|
||||
end)
|
||||
|
||||
test("should run detached from effect scope", function()
|
||||
local effects = 0
|
||||
local stopEffect
|
||||
|
||||
local stopScope = effectScope(function()
|
||||
untracked(function()
|
||||
stopEffect = effect(function()
|
||||
effects += 1
|
||||
return function()
|
||||
effects -= 1
|
||||
end
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
assert(spyCalls == 0, "should not be called yet")
|
||||
a(2)
|
||||
assert(spyCalls == 1, "should be called once")
|
||||
a(3)
|
||||
assert(spyCalls == 2, "should be called twice")
|
||||
assert(effects == 1, "should have one effect")
|
||||
stopScope()
|
||||
assert(effects == 1, "should still have one effect")
|
||||
stopEffect()
|
||||
assert(effects == 0, "should have zero effects")
|
||||
end)
|
||||
|
||||
test("should block tracking inside effects", function()
|
||||
|
|
|
|||
|
|
@ -23,11 +23,11 @@ local PENDING = 0b0100000
|
|||
local QUEUED = 0b1000000
|
||||
local UNTRACKED = 0b10000000
|
||||
|
||||
type EffectScope = ReactiveNode & {
|
||||
export type EffectScope = ReactiveNode & {
|
||||
cleanups: { () -> () },
|
||||
}
|
||||
|
||||
type Effect = ReactiveNode & {
|
||||
export type Effect = ReactiveNode & {
|
||||
fn: (dispose: () -> ()) -> ...(() -> ())?,
|
||||
cleanups: { () -> () },
|
||||
cleanup: () -> (),
|
||||
|
|
|
|||
4
packages/charm/src/index.d.ts
vendored
4
packages/charm/src/index.d.ts
vendored
|
|
@ -2,8 +2,8 @@ type Key = string | number | symbol;
|
|||
type Cleanup = () => void;
|
||||
|
||||
export interface Atom<T> {
|
||||
(): T;
|
||||
(value: T | ((value: T) => T)): T;
|
||||
(): T;
|
||||
}
|
||||
|
||||
export type Selector<T> = () => T;
|
||||
|
|
@ -64,4 +64,6 @@ export function batched<Args extends any[], Result>(
|
|||
|
||||
export function untracked<Args extends any[], Result>(callback: (...args: Args) => Result, ...args: Args): Result;
|
||||
|
||||
export function peek<Args extends any[], Result>(callback: (...args: Args) => Result, ...args: Args): Result;
|
||||
|
||||
export function onCleanup(callback: Cleanup, failSilently?: boolean): void;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ local flags = require("@self/flags")
|
|||
local listen = require("@self/listen")
|
||||
local mapped = require("@self/mapped")
|
||||
local observe = require("@self/observe")
|
||||
local peek = require("@self/peek")
|
||||
local subscribe = require("@self/subscribe")
|
||||
local types = require("@self/types")
|
||||
local untracked = require("@self/untracked")
|
||||
|
|
@ -24,6 +25,7 @@ return {
|
|||
listen = listen,
|
||||
mapped = mapped,
|
||||
observe = observe,
|
||||
peek = peek,
|
||||
subscribe = subscribe,
|
||||
untracked = untracked,
|
||||
deepFreeze = utils.deepFreeze,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
local core = require("./core")
|
||||
local untracked = require("./untracked")
|
||||
local peek = require("./peek")
|
||||
|
||||
local computed = core.computed
|
||||
local effect = core.effect
|
||||
|
|
@ -19,7 +19,7 @@ local function listen<T>(getter: () -> T, listener: (state: T, previous: T?, dis
|
|||
dispose = effect(function()
|
||||
local previous = current
|
||||
current = tracked()
|
||||
untracked(listener, current, previous, dispose)
|
||||
peek(listener, current, previous, dispose)
|
||||
end)
|
||||
|
||||
if disposed then
|
||||
|
|
|
|||
27
packages/charm/src/peek.luau
Normal file
27
packages/charm/src/peek.luau
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
local core = require("./core")
|
||||
|
||||
local UNTRACKED = core.ReactiveFlags.Untracked
|
||||
local getCurrentSub = core.getCurrentSub
|
||||
|
||||
local function handlePeek(sub: core.ReactiveNode?, ok: boolean, ...)
|
||||
if sub then
|
||||
sub.flags = bit32.band(sub.flags, bit32.bnot(UNTRACKED))
|
||||
end
|
||||
if ok then
|
||||
return ...
|
||||
else
|
||||
error((...))
|
||||
end
|
||||
end
|
||||
|
||||
local function peek<Args..., Results...>(callback: (Args...) -> Results..., ...: Args...): Results...
|
||||
local sub = getCurrentSub()
|
||||
if not sub or bit32.btest(sub.flags, UNTRACKED) then
|
||||
return callback(...)
|
||||
else
|
||||
sub.flags = bit32.bor(sub.flags, UNTRACKED)
|
||||
end
|
||||
return handlePeek(sub, pcall(callback, ...))
|
||||
end
|
||||
|
||||
return peek
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
local core = require("./core")
|
||||
local untracked = require("./untracked")
|
||||
local peek = require("./peek")
|
||||
|
||||
local computed = core.computed
|
||||
local effect = core.effect
|
||||
|
|
@ -18,7 +18,7 @@ local function subscribe<T>(getter: () -> T, listener: (state: T, lastState: T)
|
|||
if mounted then
|
||||
mounted = false
|
||||
else
|
||||
untracked(listener, current, previous)
|
||||
peek(listener, current, previous)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,11 +1,20 @@
|
|||
local core = require("./core")
|
||||
|
||||
local UNTRACKED = core.ReactiveFlags.Untracked
|
||||
local setCurrentSub = core.setCurrentSub
|
||||
local setCurrentScope = core.setCurrentScope
|
||||
local getCurrentSub = core.getCurrentSub
|
||||
local getCurrentScope = core.getCurrentScope
|
||||
|
||||
local function handleUntracked(sub: core.ReactiveNode?, ok: boolean, ...)
|
||||
if sub then
|
||||
sub.flags = bit32.band(sub.flags, bit32.bnot(UNTRACKED))
|
||||
local function handleUntracked(
|
||||
prevSub: core.ReactiveNode?,
|
||||
prevScope: core.Effect | core.EffectScope?,
|
||||
ok: boolean,
|
||||
...
|
||||
)
|
||||
if prevSub then
|
||||
setCurrentSub(prevSub)
|
||||
elseif prevScope then
|
||||
setCurrentScope(prevScope)
|
||||
end
|
||||
if ok then
|
||||
return ...
|
||||
|
|
@ -15,13 +24,13 @@ local function handleUntracked(sub: core.ReactiveNode?, ok: boolean, ...)
|
|||
end
|
||||
|
||||
local function untracked<Args..., Results...>(callback: (Args...) -> Results..., ...: Args...): Results...
|
||||
local sub = getCurrentSub()
|
||||
if not sub or bit32.btest(sub.flags, UNTRACKED) then
|
||||
return callback(...)
|
||||
if getCurrentSub() then
|
||||
return handleUntracked(setCurrentSub(nil), nil, pcall(callback, ...))
|
||||
elseif getCurrentScope() then
|
||||
return handleUntracked(nil, setCurrentScope(nil), pcall(callback, ...))
|
||||
else
|
||||
sub.flags = bit32.bor(sub.flags, UNTRACKED)
|
||||
return callback(...)
|
||||
end
|
||||
return handleUntracked(sub, pcall(callback, ...))
|
||||
end
|
||||
|
||||
return untracked
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue