mirror of
https://github.com/littensy/charm.git
synced 2026-10-22 21:01:38 +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")
|
assert(current == 2 and previous == 1, "should not run after unsub")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("should run in untracked", function()
|
test("should run in peek", function()
|
||||||
local runs = 0
|
local runs = 0
|
||||||
local cleanups = 0
|
local cleanups = 0
|
||||||
local a = atom(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()
|
suite.describe("untracked", function()
|
||||||
require("./untracked.spec")
|
require("./untracked.spec")
|
||||||
end)
|
end)
|
||||||
|
suite.describe("peek", function()
|
||||||
|
require("./peek.spec")
|
||||||
|
end)
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ test("should subscribe to selector", function()
|
||||||
assert(current == 2 and previous == 1, "should not run after unsub")
|
assert(current == 2 and previous == 1, "should not run after unsub")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("should run in untracked", function()
|
test("should run in peek", function()
|
||||||
local runs = 0
|
local runs = 0
|
||||||
local cleanups = 0
|
local cleanups = 0
|
||||||
local a = atom(0)
|
local a = atom(0)
|
||||||
|
|
|
||||||
|
|
@ -4,29 +4,59 @@ local suite = require("../../../../test/suite")
|
||||||
local atom = Charm.atom
|
local atom = Charm.atom
|
||||||
local computed = Charm.computed
|
local computed = Charm.computed
|
||||||
local effect = Charm.effect
|
local effect = Charm.effect
|
||||||
|
local effectScope = Charm.effectScope
|
||||||
local untracked = Charm.untracked
|
local untracked = Charm.untracked
|
||||||
local test = suite.test
|
local test = suite.test
|
||||||
|
|
||||||
test("should not run detached", function()
|
test("should run detached from effect", function()
|
||||||
local a = atom(1)
|
local a = atom(1)
|
||||||
local spyCalls = 0
|
local effects = 0
|
||||||
|
local innerEffects = {}
|
||||||
|
|
||||||
effect(function()
|
local stopEffect = effect(function()
|
||||||
a()
|
a()
|
||||||
untracked(function()
|
untracked(function()
|
||||||
effect(function()
|
local stopInnerEffect = effect(function()
|
||||||
|
effects += 1
|
||||||
return function()
|
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)
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert(spyCalls == 0, "should not be called yet")
|
assert(effects == 1, "should have one effect")
|
||||||
a(2)
|
stopScope()
|
||||||
assert(spyCalls == 1, "should be called once")
|
assert(effects == 1, "should still have one effect")
|
||||||
a(3)
|
stopEffect()
|
||||||
assert(spyCalls == 2, "should be called twice")
|
assert(effects == 0, "should have zero effects")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("should block tracking inside effects", function()
|
test("should block tracking inside effects", function()
|
||||||
|
|
|
||||||
|
|
@ -23,11 +23,11 @@ local PENDING = 0b0100000
|
||||||
local QUEUED = 0b1000000
|
local QUEUED = 0b1000000
|
||||||
local UNTRACKED = 0b10000000
|
local UNTRACKED = 0b10000000
|
||||||
|
|
||||||
type EffectScope = ReactiveNode & {
|
export type EffectScope = ReactiveNode & {
|
||||||
cleanups: { () -> () },
|
cleanups: { () -> () },
|
||||||
}
|
}
|
||||||
|
|
||||||
type Effect = ReactiveNode & {
|
export type Effect = ReactiveNode & {
|
||||||
fn: (dispose: () -> ()) -> ...(() -> ())?,
|
fn: (dispose: () -> ()) -> ...(() -> ())?,
|
||||||
cleanups: { () -> () },
|
cleanups: { () -> () },
|
||||||
cleanup: () -> (),
|
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;
|
type Cleanup = () => void;
|
||||||
|
|
||||||
export interface Atom<T> {
|
export interface Atom<T> {
|
||||||
(): T;
|
|
||||||
(value: T | ((value: T) => T)): T;
|
(value: T | ((value: T) => T)): T;
|
||||||
|
(): T;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Selector<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 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;
|
export function onCleanup(callback: Cleanup, failSilently?: boolean): void;
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ local flags = require("@self/flags")
|
||||||
local listen = require("@self/listen")
|
local listen = require("@self/listen")
|
||||||
local mapped = require("@self/mapped")
|
local mapped = require("@self/mapped")
|
||||||
local observe = require("@self/observe")
|
local observe = require("@self/observe")
|
||||||
|
local peek = require("@self/peek")
|
||||||
local subscribe = require("@self/subscribe")
|
local subscribe = require("@self/subscribe")
|
||||||
local types = require("@self/types")
|
local types = require("@self/types")
|
||||||
local untracked = require("@self/untracked")
|
local untracked = require("@self/untracked")
|
||||||
|
|
@ -24,6 +25,7 @@ return {
|
||||||
listen = listen,
|
listen = listen,
|
||||||
mapped = mapped,
|
mapped = mapped,
|
||||||
observe = observe,
|
observe = observe,
|
||||||
|
peek = peek,
|
||||||
subscribe = subscribe,
|
subscribe = subscribe,
|
||||||
untracked = untracked,
|
untracked = untracked,
|
||||||
deepFreeze = utils.deepFreeze,
|
deepFreeze = utils.deepFreeze,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
local core = require("./core")
|
local core = require("./core")
|
||||||
local untracked = require("./untracked")
|
local peek = require("./peek")
|
||||||
|
|
||||||
local computed = core.computed
|
local computed = core.computed
|
||||||
local effect = core.effect
|
local effect = core.effect
|
||||||
|
|
@ -19,7 +19,7 @@ local function listen<T>(getter: () -> T, listener: (state: T, previous: T?, dis
|
||||||
dispose = effect(function()
|
dispose = effect(function()
|
||||||
local previous = current
|
local previous = current
|
||||||
current = tracked()
|
current = tracked()
|
||||||
untracked(listener, current, previous, dispose)
|
peek(listener, current, previous, dispose)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
if disposed then
|
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 core = require("./core")
|
||||||
local untracked = require("./untracked")
|
local peek = require("./peek")
|
||||||
|
|
||||||
local computed = core.computed
|
local computed = core.computed
|
||||||
local effect = core.effect
|
local effect = core.effect
|
||||||
|
|
@ -18,7 +18,7 @@ local function subscribe<T>(getter: () -> T, listener: (state: T, lastState: T)
|
||||||
if mounted then
|
if mounted then
|
||||||
mounted = false
|
mounted = false
|
||||||
else
|
else
|
||||||
untracked(listener, current, previous)
|
peek(listener, current, previous)
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,20 @@
|
||||||
local core = require("./core")
|
local core = require("./core")
|
||||||
|
|
||||||
local UNTRACKED = core.ReactiveFlags.Untracked
|
local setCurrentSub = core.setCurrentSub
|
||||||
|
local setCurrentScope = core.setCurrentScope
|
||||||
local getCurrentSub = core.getCurrentSub
|
local getCurrentSub = core.getCurrentSub
|
||||||
|
local getCurrentScope = core.getCurrentScope
|
||||||
|
|
||||||
local function handleUntracked(sub: core.ReactiveNode?, ok: boolean, ...)
|
local function handleUntracked(
|
||||||
if sub then
|
prevSub: core.ReactiveNode?,
|
||||||
sub.flags = bit32.band(sub.flags, bit32.bnot(UNTRACKED))
|
prevScope: core.Effect | core.EffectScope?,
|
||||||
|
ok: boolean,
|
||||||
|
...
|
||||||
|
)
|
||||||
|
if prevSub then
|
||||||
|
setCurrentSub(prevSub)
|
||||||
|
elseif prevScope then
|
||||||
|
setCurrentScope(prevScope)
|
||||||
end
|
end
|
||||||
if ok then
|
if ok then
|
||||||
return ...
|
return ...
|
||||||
|
|
@ -15,13 +24,13 @@ local function handleUntracked(sub: core.ReactiveNode?, ok: boolean, ...)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function untracked<Args..., Results...>(callback: (Args...) -> Results..., ...: Args...): Results...
|
local function untracked<Args..., Results...>(callback: (Args...) -> Results..., ...: Args...): Results...
|
||||||
local sub = getCurrentSub()
|
if getCurrentSub() then
|
||||||
if not sub or bit32.btest(sub.flags, UNTRACKED) then
|
return handleUntracked(setCurrentSub(nil), nil, pcall(callback, ...))
|
||||||
return callback(...)
|
elseif getCurrentScope() then
|
||||||
|
return handleUntracked(nil, setCurrentScope(nil), pcall(callback, ...))
|
||||||
else
|
else
|
||||||
sub.flags = bit32.bor(sub.flags, UNTRACKED)
|
return callback(...)
|
||||||
end
|
end
|
||||||
return handleUntracked(sub, pcall(callback, ...))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return untracked
|
return untracked
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue