mirror of
https://github.com/littensy/charm.git
synced 2026-10-20 17:51:37 +00:00
refactor: rename batch, misc fixes & perf updates
This commit is contained in:
parent
2aa7bd3e15
commit
7bb6edb403
16 changed files with 214 additions and 258 deletions
21
README.md
21
README.md
|
|
@ -44,7 +44,7 @@
|
||||||
- [`effect(callback)`](#effectcallback)
|
- [`effect(callback)`](#effectcallback)
|
||||||
- [`untracked(callback)`](#untrackedcallback)
|
- [`untracked(callback)`](#untrackedcallback)
|
||||||
- [`peek(callback)`](#peekcallback)
|
- [`peek(callback)`](#peekcallback)
|
||||||
- [`batched(callback)`](#batchedcallback)
|
- [`batch(callback)`](#batchcallback)
|
||||||
- [`effectScope(callback)`](#effectscopecallback)
|
- [`effectScope(callback)`](#effectscopecallback)
|
||||||
- [`listen(getter, callback)`](#listengetter-callback)
|
- [`listen(getter, callback)`](#listengetter-callback)
|
||||||
- [`subscribe(getter, callback)`](#subscribegetter-callback)
|
- [`subscribe(getter, callback)`](#subscribegetter-callback)
|
||||||
|
|
@ -334,7 +334,7 @@ disposeOuter() --> Cleaned up inner effect
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### `batched(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.
|
||||||
|
|
||||||
|
|
@ -348,7 +348,7 @@ end)
|
||||||
|
|
||||||
-- Combines both writes into a single update.
|
-- Combines both writes into a single update.
|
||||||
-- Once the callback completes, outputs "Full name: Foo Bar"
|
-- Once the callback completes, outputs "Full name: Foo Bar"
|
||||||
batched(function()
|
batch(function()
|
||||||
setName("Foo")
|
setName("Foo")
|
||||||
setSurname("Bar")
|
setSurname("Bar")
|
||||||
end)
|
end)
|
||||||
|
|
@ -542,13 +542,15 @@ max(-1) --> 1
|
||||||
|
|
||||||
### `globals`
|
### `globals`
|
||||||
|
|
||||||
Global flags that customize the behavior of Charm. The `strict` and `frozen` flags are automatically enabled when the Luau optimization level is less than `2`, which is true in Roblox Studio.
|
Global flags that customize the behavior of Charm.
|
||||||
|
|
||||||
| Flag | Default | Description |
|
The `strict` and `frozen` flags are automatically enabled when the Luau optimization level is less than `2`, which is true in Roblox Studio.
|
||||||
| ----------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
||||||
| strict | `true` in Studio | Enforces synchronous, non-yielding behavior in signals, effects, and critical code. |
|
| Flag | Default | Description |
|
||||||
| frozen | `true` in Studio | Enforces immutability by deep-freezing tables passed to signals. |
|
| ----------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| trackInnerEffects | `true` | Whether nested effects should be tracked and cleaned up when the parent effect re-runs. Should only be disabled to debug issues during migration. |
|
| strict | `true`/`false` | Enforces synchronous, non-yielding behavior in signals, effects, and other critical code. |
|
||||||
|
| frozen | `true`/`false` | Enforces data immutability by deep-freezing tables passed to signals. |
|
||||||
|
| trackInnerEffects | `true` | Whether nested effects should be tracked and cleaned up when the parent effect re-runs. Should only be disabled to debug issues during migration. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -812,7 +814,6 @@ Charm v0.11 introduces _a lot_ of breaking changes, so below are some tips that
|
||||||
**What to look out for:**
|
**What to look out for:**
|
||||||
|
|
||||||
1. Address all of the type errors introduced in your project after updating Charm. Most of them are caused by changes like:
|
1. Address all of the type errors introduced in your project after updating Charm. Most of them are caused by changes like:
|
||||||
- `batch()` was renamed to `batched()`
|
|
||||||
- The second arguments of `atom()` changed from an `options` table to an equality function
|
- The second arguments of `atom()` changed from an `options` table to an equality function
|
||||||
- Removed the second argument of `computed()` (you can do your own equality checks now)
|
- Removed the second argument of `computed()` (you can do your own equality checks now)
|
||||||
- Removed the cleanup argument in effect callbacks (`effect(function(cleanup) end)`)
|
- Removed the cleanup argument in effect callbacks (`effect(function(cleanup) end)`)
|
||||||
|
|
|
||||||
|
|
@ -65,9 +65,9 @@ end
|
||||||
local function hydrate(payloads: { SyncPayload })
|
local function hydrate(payloads: { SyncPayload })
|
||||||
for _, payload in payloads do
|
for _, payload in payloads do
|
||||||
if payload.type == "init" then
|
if payload.type == "init" then
|
||||||
Charm.batched(applyInitPayload, payload.data)
|
Charm.batch(applyInitPayload, payload.data)
|
||||||
elseif payload.type == "patch" then
|
elseif payload.type == "patch" then
|
||||||
Charm.batched(applyPatchPayload, payload.data)
|
Charm.batch(applyPatchPayload, payload.data)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
local core = require("../../core")
|
local core = require("../core")
|
||||||
local suite = require("../../../../../test/suite")
|
local suite = require("../../../../test/suite")
|
||||||
|
|
||||||
local computed = core.computed
|
local computed = core.computed
|
||||||
local effect = core.effect
|
local effect = core.effect
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
local core = require("../../core")
|
local core = require("../core")
|
||||||
local suite = require("../../../../../test/suite")
|
local suite = require("../../../../test/suite")
|
||||||
|
|
||||||
local computed = core.computed
|
local computed = core.computed
|
||||||
local effect = core.effect
|
local effect = core.effect
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
local core = require("../../core")
|
local core = require("../core")
|
||||||
local suite = require("../../../../../test/suite")
|
local suite = require("../../../../test/suite")
|
||||||
|
|
||||||
local effect = core.effect
|
local effect = core.effect
|
||||||
local effectScope = core.effectScope
|
local effectScope = core.effectScope
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
local core = require("../../core")
|
local core = require("../core")
|
||||||
local suite = require("../../../../../test/suite")
|
local suite = require("../../../../test/suite")
|
||||||
|
|
||||||
local atom = core.atom
|
local atom = core.atom
|
||||||
local computed = core.computed
|
local computed = core.computed
|
||||||
|
|
@ -4,6 +4,7 @@ local suite = require("../../../../test/suite")
|
||||||
local signal = Charm.signal
|
local signal = Charm.signal
|
||||||
local computed = Charm.computed
|
local computed = Charm.computed
|
||||||
local effect = Charm.effect
|
local effect = Charm.effect
|
||||||
|
local effectScope = Charm.effectScope
|
||||||
local observe = Charm.observe
|
local observe = Charm.observe
|
||||||
local test = suite.test
|
local test = suite.test
|
||||||
|
|
||||||
|
|
@ -42,48 +43,44 @@ test("should dispose scopes", function()
|
||||||
local items, setItems = signal({ 1, 2, 3 })
|
local items, setItems = signal({ 1, 2, 3 })
|
||||||
local a, setA = signal(0)
|
local a, setA = signal(0)
|
||||||
local runs = {}
|
local runs = {}
|
||||||
|
local disposes = {}
|
||||||
|
|
||||||
local dispose = observe(items, function(item)
|
local dispose = observe(items, function(item)
|
||||||
effect(function()
|
effect(function()
|
||||||
table.insert(runs, item)
|
table.insert(runs, item)
|
||||||
a()
|
a()
|
||||||
end)
|
end)
|
||||||
|
effectScope(function()
|
||||||
|
return function()
|
||||||
|
table.insert(disposes, item)
|
||||||
|
end
|
||||||
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
table.clear(runs)
|
table.clear(runs)
|
||||||
setItems({ 1, 2 })
|
setItems({ 1, 2 })
|
||||||
setA(1)
|
setA(1)
|
||||||
assert(table.concat(runs) == "12", `bad reruns: {table.concat(runs)}`)
|
assert(table.concat(runs) == "12", `bad reruns: {table.concat(runs)}`)
|
||||||
|
assert(table.concat(disposes) == "3", `should dispose 3: {table.concat(disposes)}`)
|
||||||
|
|
||||||
table.clear(runs)
|
table.clear(runs)
|
||||||
|
table.clear(disposes)
|
||||||
dispose()
|
dispose()
|
||||||
setA(2)
|
setA(2)
|
||||||
assert(table.concat(runs) == "", `should not rerun: {table.concat(runs)}`)
|
assert(table.concat(runs) == "", `should not rerun: {table.concat(runs)}`)
|
||||||
|
assert(table.concat(disposes) == "12", `should dispose 1 and 2: {table.concat(disposes)}`)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("should queue addition", function()
|
test("should queue addition and removal", function()
|
||||||
local items, setItems = signal({ 1 })
|
local items, setItems = signal({ 1 })
|
||||||
local edits = {}
|
local edits = {}
|
||||||
|
|
||||||
observe(items, function(item)
|
observe(items, function(item)
|
||||||
setItems({ 1, 2 })
|
if item == 1 then
|
||||||
setItems({ 1, 2, 3, 4 })
|
setItems({ 1, 2, 3, 4 })
|
||||||
table.insert(edits, `+{item}`)
|
elseif item == 4 then
|
||||||
return function()
|
setItems({ 1 })
|
||||||
table.insert(edits, `-{item}`)
|
|
||||||
end
|
end
|
||||||
end)
|
|
||||||
|
|
||||||
assert(table.concat(edits) == "+1+2+3+4", `bad edits: {table.concat(edits)}`)
|
|
||||||
end)
|
|
||||||
|
|
||||||
test("should queue removal", function()
|
|
||||||
local items, setItems = signal({ 1, 2, 3, 4 })
|
|
||||||
local edits = {}
|
|
||||||
|
|
||||||
observe(items, function(item)
|
|
||||||
setItems({ 1, 2, 3 })
|
|
||||||
setItems({ 1 })
|
|
||||||
table.insert(edits, `+{item}`)
|
table.insert(edits, `+{item}`)
|
||||||
return function()
|
return function()
|
||||||
table.insert(edits, `-{item}`)
|
table.insert(edits, `-{item}`)
|
||||||
|
|
@ -93,4 +90,23 @@ test("should queue removal", function()
|
||||||
assert(table.concat(edits) == "+1+2+3+4-2-3-4", `bad edits: {table.concat(edits)}`)
|
assert(table.concat(edits) == "+1+2+3+4-2-3-4", `bad edits: {table.concat(edits)}`)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
test("should handle self removal", function()
|
||||||
|
local items, setItems = signal({ 1, 2, 3, 4 })
|
||||||
|
local edits = {}
|
||||||
|
|
||||||
|
observe(items, function(item, index)
|
||||||
|
setItems(function(state)
|
||||||
|
state = table.clone(state)
|
||||||
|
state[index] = nil
|
||||||
|
return state
|
||||||
|
end)
|
||||||
|
table.insert(edits, `+{item}`)
|
||||||
|
return function()
|
||||||
|
table.insert(edits, `-{item}`)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert(table.concat(edits) == "+1+2+3+4-1-2-3-4", `bad edits: {table.concat(edits)}`)
|
||||||
|
end)
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
local core = require("../../core")
|
local core = require("../core")
|
||||||
local suite = require("../../../../../test/suite")
|
local suite = require("../../../../test/suite")
|
||||||
|
|
||||||
local signal = core.signal
|
local signal = core.signal
|
||||||
local effect = core.effect
|
local effect = core.effect
|
||||||
|
|
@ -37,4 +37,14 @@ test("should enforce immutability", function()
|
||||||
end), "should not allow mutation of new value")
|
end), "should not allow mutation of new value")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
test("should not freeze values with metatables", function()
|
||||||
|
local src = signal({ object = setmetatable({}, {}) })
|
||||||
|
assert(
|
||||||
|
pcall(function()
|
||||||
|
src().object.a = 2
|
||||||
|
end),
|
||||||
|
"should allow mutation of values with metatables"
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
|
@ -1,26 +1,17 @@
|
||||||
local suite = require("../../../../test/suite")
|
local suite = require("../../../../test/suite")
|
||||||
|
|
||||||
suite.describe("core", function()
|
suite.describe("computed", function()
|
||||||
suite.describe("computed", function()
|
require("./computed.spec")
|
||||||
require("./core/computed.spec")
|
end)
|
||||||
end)
|
suite.describe("effect", function()
|
||||||
suite.describe("effect", function()
|
require("./effect.spec")
|
||||||
require("./core/effect.spec")
|
end)
|
||||||
end)
|
suite.describe("effectScope", function()
|
||||||
suite.describe("effectScope", function()
|
require("./effectScope.spec")
|
||||||
require("./core/effectScope.spec")
|
end)
|
||||||
end)
|
suite.describe("gc", function()
|
||||||
suite.describe("gc", function()
|
require("./gc.spec")
|
||||||
require("./core/gc.spec")
|
|
||||||
end)
|
|
||||||
suite.describe("signal", function()
|
|
||||||
require("./core/signal.spec")
|
|
||||||
end)
|
|
||||||
suite.describe("topology", function()
|
|
||||||
require("./core/topology.spec")
|
|
||||||
end)
|
|
||||||
end)
|
end)
|
||||||
|
|
||||||
suite.describe("listen", function()
|
suite.describe("listen", function()
|
||||||
require("./listen.spec")
|
require("./listen.spec")
|
||||||
end)
|
end)
|
||||||
|
|
@ -30,14 +21,20 @@ end)
|
||||||
suite.describe("observe", function()
|
suite.describe("observe", function()
|
||||||
require("./observe.spec")
|
require("./observe.spec")
|
||||||
end)
|
end)
|
||||||
|
suite.describe("peek", function()
|
||||||
|
require("./peek.spec")
|
||||||
|
end)
|
||||||
|
suite.describe("signal", function()
|
||||||
|
require("./signal.spec")
|
||||||
|
end)
|
||||||
suite.describe("subscribe", function()
|
suite.describe("subscribe", function()
|
||||||
require("./subscribe.spec")
|
require("./subscribe.spec")
|
||||||
end)
|
end)
|
||||||
|
suite.describe("topology", function()
|
||||||
|
require("./topology.spec")
|
||||||
|
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 {}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
local core = require("../../core")
|
local core = require("../core")
|
||||||
local suite = require("../../../../../test/suite")
|
local suite = require("../../../../test/suite")
|
||||||
|
|
||||||
|
local atom = core.atom
|
||||||
local computed = core.computed
|
local computed = core.computed
|
||||||
local effect = core.effect
|
local effect = core.effect
|
||||||
local signal = core.signal
|
|
||||||
local describe = suite.describe
|
local describe = suite.describe
|
||||||
local test = suite.test
|
local test = suite.test
|
||||||
|
|
||||||
|
|
@ -43,7 +43,7 @@ describe("graph updates", function()
|
||||||
-- C
|
-- C
|
||||||
-- |
|
-- |
|
||||||
-- D
|
-- D
|
||||||
local a, setA = signal(2)
|
local a = atom(2)
|
||||||
|
|
||||||
local b = computed(function()
|
local b = computed(function()
|
||||||
return a() - 1
|
return a() - 1
|
||||||
|
|
@ -64,7 +64,7 @@ describe("graph updates", function()
|
||||||
assert(computeCalls == 1, "compute should be called once")
|
assert(computeCalls == 1, "compute should be called once")
|
||||||
computeCalls = 0
|
computeCalls = 0
|
||||||
|
|
||||||
setA(4)
|
a(4)
|
||||||
d()
|
d()
|
||||||
assert(computeCalls == 1, "compute should drop update")
|
assert(computeCalls == 1, "compute should drop update")
|
||||||
end)
|
end)
|
||||||
|
|
@ -77,7 +77,7 @@ describe("graph updates", function()
|
||||||
-- B C
|
-- B C
|
||||||
-- \ /
|
-- \ /
|
||||||
-- D
|
-- D
|
||||||
local a, setA = signal("a")
|
local a = atom("a")
|
||||||
local b = computed(function()
|
local b = computed(function()
|
||||||
return a()
|
return a()
|
||||||
end)
|
end)
|
||||||
|
|
@ -95,7 +95,7 @@ describe("graph updates", function()
|
||||||
assert(d() == "a a", "d should be 'a a'")
|
assert(d() == "a a", "d should be 'a a'")
|
||||||
assert(spyCalls == 1, "compute should be called once")
|
assert(spyCalls == 1, "compute should be called once")
|
||||||
|
|
||||||
setA("aa")
|
a("aa")
|
||||||
assert(d() == "aa aa", "d should be 'aa aa'")
|
assert(d() == "aa aa", "d should be 'aa aa'")
|
||||||
assert(spyCalls == 2, "compute should be called only twice")
|
assert(spyCalls == 2, "compute should be called only twice")
|
||||||
end)
|
end)
|
||||||
|
|
@ -109,7 +109,7 @@ describe("graph updates", function()
|
||||||
-- D
|
-- D
|
||||||
-- |
|
-- |
|
||||||
-- E
|
-- E
|
||||||
local a, setA = signal("a")
|
local a = atom("a")
|
||||||
local b = computed(function()
|
local b = computed(function()
|
||||||
return a()
|
return a()
|
||||||
end)
|
end)
|
||||||
|
|
@ -131,7 +131,7 @@ describe("graph updates", function()
|
||||||
assert(e() == "a a", "e should be 'a a'")
|
assert(e() == "a a", "e should be 'a a'")
|
||||||
assert(spyCalls == 1, "compute should be called once")
|
assert(spyCalls == 1, "compute should be called once")
|
||||||
|
|
||||||
setA("aa")
|
a("aa")
|
||||||
assert(e() == "aa aa", "e should be 'aa aa'")
|
assert(e() == "aa aa", "e should be 'aa aa'")
|
||||||
assert(spyCalls == 2, "compute should be called only twice")
|
assert(spyCalls == 2, "compute should be called only twice")
|
||||||
end)
|
end)
|
||||||
|
|
@ -139,7 +139,7 @@ describe("graph updates", function()
|
||||||
test("should bail out if result is the same", function()
|
test("should bail out if result is the same", function()
|
||||||
-- Bail out if value of "B" never changes
|
-- Bail out if value of "B" never changes
|
||||||
-- A->B->C
|
-- A->B->C
|
||||||
local a, setA = signal("a")
|
local a = atom("a")
|
||||||
local b = computed(function()
|
local b = computed(function()
|
||||||
a()
|
a()
|
||||||
return "foo"
|
return "foo"
|
||||||
|
|
@ -155,7 +155,7 @@ describe("graph updates", function()
|
||||||
assert(c() == "foo", "c should be 'foo'")
|
assert(c() == "foo", "c should be 'foo'")
|
||||||
assert(spyCalls == 1, "compute should be called once")
|
assert(spyCalls == 1, "compute should be called once")
|
||||||
|
|
||||||
setA("aa")
|
a("aa")
|
||||||
assert(c() == "foo", "c should be 'foo'")
|
assert(c() == "foo", "c should be 'foo'")
|
||||||
assert(spyCalls == 1, "compute should bail out")
|
assert(spyCalls == 1, "compute should bail out")
|
||||||
end)
|
end)
|
||||||
|
|
@ -171,7 +171,7 @@ describe("graph updates", function()
|
||||||
-- E
|
-- E
|
||||||
-- / \
|
-- / \
|
||||||
-- F G
|
-- F G
|
||||||
local a, setA = signal("a")
|
local a = atom("a")
|
||||||
|
|
||||||
local b = computed(function()
|
local b = computed(function()
|
||||||
return a()
|
return a()
|
||||||
|
|
@ -220,7 +220,7 @@ describe("graph updates", function()
|
||||||
fSpyCalls, fSpyTime = 0, 0
|
fSpyCalls, fSpyTime = 0, 0
|
||||||
gSpyCalls, gSpyTime = 0, 0
|
gSpyCalls, gSpyTime = 0, 0
|
||||||
|
|
||||||
setA("b")
|
a("b")
|
||||||
|
|
||||||
assert(e() == "b b", "e should be 'b b'")
|
assert(e() == "b b", "e should be 'b b'")
|
||||||
assert(eSpyCalls == 1, "e should run once after update")
|
assert(eSpyCalls == 1, "e should run once after update")
|
||||||
|
|
@ -235,7 +235,7 @@ describe("graph updates", function()
|
||||||
fSpyCalls, fSpyTime = 0, 0
|
fSpyCalls, fSpyTime = 0, 0
|
||||||
gSpyCalls, gSpyTime = 0, 0
|
gSpyCalls, gSpyTime = 0, 0
|
||||||
|
|
||||||
setA("c")
|
a("c")
|
||||||
|
|
||||||
assert(e() == "c c", "e should be 'c c'")
|
assert(e() == "c c", "e should be 'c c'")
|
||||||
assert(eSpyCalls == 1, "e should run once after update")
|
assert(eSpyCalls == 1, "e should run once after update")
|
||||||
|
|
@ -256,7 +256,7 @@ describe("graph updates", function()
|
||||||
-- *A
|
-- *A
|
||||||
-- / \
|
-- / \
|
||||||
-- *B C <- we don't listen to C
|
-- *B C <- we don't listen to C
|
||||||
local a, setA = signal("a")
|
local a = atom("a")
|
||||||
|
|
||||||
local b = computed(function()
|
local b = computed(function()
|
||||||
return a()
|
return a()
|
||||||
|
|
@ -271,7 +271,7 @@ describe("graph updates", function()
|
||||||
assert(b() == "a", "b should be 'a'")
|
assert(b() == "a", "b should be 'a'")
|
||||||
assert(spyCalls == 0, "compute should not run")
|
assert(spyCalls == 0, "compute should not run")
|
||||||
|
|
||||||
setA("aa")
|
a("aa")
|
||||||
assert(b() == "aa", "b should be 'aa'")
|
assert(b() == "aa", "b should be 'aa'")
|
||||||
assert(spyCalls == 0, "compute should not run")
|
assert(spyCalls == 0, "compute should not run")
|
||||||
end)
|
end)
|
||||||
|
|
@ -285,7 +285,7 @@ describe("graph updates", function()
|
||||||
-- *B D <- we don't listen to C
|
-- *B D <- we don't listen to C
|
||||||
-- |
|
-- |
|
||||||
-- *C
|
-- *C
|
||||||
local a, setA = signal("a")
|
local a = atom("a")
|
||||||
local spyBCalls = 0
|
local spyBCalls = 0
|
||||||
local spyB = function()
|
local spyB = function()
|
||||||
spyBCalls += 1
|
spyBCalls += 1
|
||||||
|
|
@ -316,7 +316,7 @@ describe("graph updates", function()
|
||||||
spyCCalls = 0
|
spyCCalls = 0
|
||||||
unsub()
|
unsub()
|
||||||
|
|
||||||
setA("aa")
|
a("aa")
|
||||||
|
|
||||||
assert(spyBCalls == 0, "b should not run")
|
assert(spyBCalls == 0, "b should not run")
|
||||||
assert(spyCCalls == 0, "c should not run")
|
assert(spyCCalls == 0, "c should not run")
|
||||||
|
|
@ -333,7 +333,7 @@ describe("graph updates", function()
|
||||||
-- B *C <- returns same value every time
|
-- B *C <- returns same value every time
|
||||||
-- \ /
|
-- \ /
|
||||||
-- D
|
-- D
|
||||||
local a, setA = signal("a")
|
local a = atom("a")
|
||||||
local b = computed(function()
|
local b = computed(function()
|
||||||
return a()
|
return a()
|
||||||
end)
|
end)
|
||||||
|
|
@ -351,7 +351,7 @@ describe("graph updates", function()
|
||||||
assert(d() == "a c", "d should be 'a c'")
|
assert(d() == "a c", "d should be 'a c'")
|
||||||
spyResult = nil
|
spyResult = nil
|
||||||
|
|
||||||
setA("aa")
|
a("aa")
|
||||||
d()
|
d()
|
||||||
assert(spyResult == "aa c", "d should have returned 'aa c'")
|
assert(spyResult == "aa c", "d should have returned 'aa c'")
|
||||||
end)
|
end)
|
||||||
|
|
@ -365,7 +365,7 @@ describe("graph updates", function()
|
||||||
-- B *C *D
|
-- B *C *D
|
||||||
-- \ | /
|
-- \ | /
|
||||||
-- E
|
-- E
|
||||||
local a, setA = signal("a")
|
local a = atom("a")
|
||||||
local b = computed(function()
|
local b = computed(function()
|
||||||
return a()
|
return a()
|
||||||
end)
|
end)
|
||||||
|
|
@ -387,13 +387,13 @@ describe("graph updates", function()
|
||||||
assert(e() == "a c d", "e should be 'a c d'")
|
assert(e() == "a c d", "e should be 'a c d'")
|
||||||
spyResult = nil
|
spyResult = nil
|
||||||
|
|
||||||
setA("aa")
|
a("aa")
|
||||||
e()
|
e()
|
||||||
assert(spyResult == "aa c d", "e should have returned 'aa c d'")
|
assert(spyResult == "aa c d", "e should have returned 'aa c d'")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("should support lazy branches", function()
|
test("should support lazy branches", function()
|
||||||
local a, setA = signal(0)
|
local a = atom(0)
|
||||||
local b = computed(function()
|
local b = computed(function()
|
||||||
return a()
|
return a()
|
||||||
end)
|
end)
|
||||||
|
|
@ -402,10 +402,10 @@ describe("graph updates", function()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert(c() == 0, "c should be 0")
|
assert(c() == 0, "c should be 0")
|
||||||
setA(1)
|
a(1)
|
||||||
assert(c() == 1, "c should be 1")
|
assert(c() == 1, "c should be 1")
|
||||||
|
|
||||||
setA(0)
|
a(0)
|
||||||
assert(c() == 0, "c should be 0 again")
|
assert(c() == 0, "c should be 0 again")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
@ -417,7 +417,7 @@ describe("graph updates", function()
|
||||||
-- *B *C
|
-- *B *C
|
||||||
-- \ /
|
-- \ /
|
||||||
-- D
|
-- D
|
||||||
local a, setA = signal("a")
|
local a = atom("a")
|
||||||
local b = computed(function()
|
local b = computed(function()
|
||||||
a()
|
a()
|
||||||
return "b"
|
return "b"
|
||||||
|
|
@ -436,14 +436,14 @@ describe("graph updates", function()
|
||||||
assert(d() == "b c", "d should be 'b c'")
|
assert(d() == "b c", "d should be 'b c'")
|
||||||
spyCalls = 0
|
spyCalls = 0
|
||||||
|
|
||||||
setA("aa")
|
a("aa")
|
||||||
assert(spyCalls == 0, "d should not run")
|
assert(spyCalls == 0, "d should not run")
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
describe("error handling", function()
|
describe("error handling", function()
|
||||||
test("should keep graph consistent on errors during activation", function()
|
test("should keep graph consistent on errors during activation", function()
|
||||||
local a, setA = signal(0)
|
local a = atom(0)
|
||||||
local b = computed(function()
|
local b = computed(function()
|
||||||
error("fail")
|
error("fail")
|
||||||
end)
|
end)
|
||||||
|
|
@ -457,12 +457,12 @@ describe("error handling", function()
|
||||||
assert(not success, "should throw")
|
assert(not success, "should throw")
|
||||||
assert(string.match(result, "fail"), "should throw fail")
|
assert(string.match(result, "fail"), "should throw fail")
|
||||||
|
|
||||||
setA(1)
|
a(1)
|
||||||
assert(c() == 1, "c should recover and return 1")
|
assert(c() == 1, "c should recover and return 1")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("should keep graph consistent on errors in computeds", function()
|
test("should keep graph consistent on errors in computeds", function()
|
||||||
local a, setA = signal(0)
|
local a = atom(0)
|
||||||
local b = computed(function()
|
local b = computed(function()
|
||||||
if a() == 1 then
|
if a() == 1 then
|
||||||
error("fail")
|
error("fail")
|
||||||
|
|
@ -475,14 +475,14 @@ describe("error handling", function()
|
||||||
|
|
||||||
assert(c() == 0, "c should be 0")
|
assert(c() == 0, "c should be 0")
|
||||||
|
|
||||||
setA(1)
|
a(1)
|
||||||
local success, result = pcall(function()
|
local success, result = pcall(function()
|
||||||
b()
|
b()
|
||||||
end)
|
end)
|
||||||
assert(not success, "should throw")
|
assert(not success, "should throw")
|
||||||
assert(string.match(result, "fail"), "should throw fail")
|
assert(string.match(result, "fail"), "should throw fail")
|
||||||
|
|
||||||
setA(2)
|
a(2)
|
||||||
assert(c() == 2, "c should recover and return 2")
|
assert(c() == 2, "c should recover and return 2")
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
@ -167,21 +167,17 @@ local function createReactiveSystem(props: {
|
||||||
end
|
end
|
||||||
|
|
||||||
local function propagate(link: Link): ()
|
local function propagate(link: Link): ()
|
||||||
local next = link.nextSub
|
|
||||||
local stack: Stack<Link?>?
|
|
||||||
|
|
||||||
repeat
|
repeat
|
||||||
local sub = link.sub
|
local sub = link.sub
|
||||||
|
|
||||||
local flags = sub.flags
|
local flags = sub.flags
|
||||||
|
|
||||||
if not bit32.btest(flags, bit32.bor(RECURSED_CHECK, RECURSED, DIRTY, PENDING)) then
|
if bit32.band(flags, bit32.bor(RECURSED_CHECK, RECURSED, DIRTY, PENDING)) == 0 then
|
||||||
sub.flags = bit32.bor(flags, PENDING)
|
sub.flags = bit32.bor(flags, PENDING)
|
||||||
elseif not bit32.btest(flags, bit32.bor(RECURSED_CHECK, RECURSED)) then
|
elseif bit32.band(flags, bit32.bor(RECURSED_CHECK, RECURSED)) == 0 then
|
||||||
flags = NONE
|
flags = NONE
|
||||||
elseif not bit32.btest(flags, RECURSED_CHECK) then
|
elseif bit32.band(flags, RECURSED_CHECK) == 0 then
|
||||||
sub.flags = bit32.bor(bit32.band(flags, bit32.bnot(RECURSED)), PENDING)
|
sub.flags = bit32.bor(bit32.band(flags, bit32.bnot(RECURSED)), PENDING)
|
||||||
elseif not bit32.btest(flags, bit32.bor(DIRTY, PENDING)) and isValidLink(link, sub) then
|
elseif bit32.band(flags, bit32.bor(DIRTY, PENDING)) == 0 and isValidLink(link, sub) then
|
||||||
sub.flags = bit32.bor(flags, bit32.bor(RECURSED, PENDING))
|
sub.flags = bit32.bor(flags, bit32.bor(RECURSED, PENDING))
|
||||||
flags = bit32.band(flags, MUTABLE)
|
flags = bit32.band(flags, MUTABLE)
|
||||||
else
|
else
|
||||||
|
|
@ -196,41 +192,12 @@ local function createReactiveSystem(props: {
|
||||||
if bit32.btest(flags, MUTABLE) then
|
if bit32.btest(flags, MUTABLE) then
|
||||||
local subSubs = sub.subs
|
local subSubs = sub.subs
|
||||||
if subSubs then
|
if subSubs then
|
||||||
link = subSubs
|
propagate(subSubs)
|
||||||
local nextSub = link.nextSub
|
|
||||||
if nextSub then
|
|
||||||
stack = { value = next, prev = stack }
|
|
||||||
next = nextSub
|
|
||||||
end
|
|
||||||
continue
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
link = next :: Link
|
link = link.nextSub :: Link
|
||||||
|
until not link
|
||||||
if link then
|
|
||||||
next = link.nextSub
|
|
||||||
continue
|
|
||||||
end
|
|
||||||
|
|
||||||
local continueTop = false
|
|
||||||
|
|
||||||
while stack do
|
|
||||||
link = stack.value :: Link
|
|
||||||
stack = stack.prev
|
|
||||||
if link then
|
|
||||||
next = link.nextSub
|
|
||||||
continueTop = true
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if continueTop then
|
|
||||||
continue
|
|
||||||
end
|
|
||||||
|
|
||||||
break
|
|
||||||
until false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function shallowPropagate(link: Link): ()
|
local function shallowPropagate(link: Link): ()
|
||||||
|
|
@ -248,81 +215,38 @@ local function createReactiveSystem(props: {
|
||||||
end
|
end
|
||||||
|
|
||||||
local function checkDirty(link: Link, sub: ReactiveNode): boolean
|
local function checkDirty(link: Link, sub: ReactiveNode): boolean
|
||||||
local stack: Stack<Link>?
|
|
||||||
local checkDepth = 0
|
|
||||||
local dirty = false
|
|
||||||
|
|
||||||
repeat
|
repeat
|
||||||
local dep = link.dep
|
local dep = link.dep
|
||||||
local flags = dep.flags
|
local depFlags = dep.flags
|
||||||
|
|
||||||
if bit32.btest(sub.flags, DIRTY) then
|
if bit32.btest(sub.flags, DIRTY) then
|
||||||
dirty = true
|
return true
|
||||||
elseif bit32.band(flags, bit32.bor(MUTABLE, DIRTY)) == bit32.bor(MUTABLE, DIRTY) then
|
elseif bit32.band(depFlags, bit32.bor(MUTABLE, DIRTY)) == bit32.bor(MUTABLE, DIRTY) then
|
||||||
if update(dep) then
|
if update(dep) then
|
||||||
local subs = dep.subs :: Link
|
local subs = dep.subs :: Link
|
||||||
if subs.nextSub then
|
if subs.nextSub then
|
||||||
shallowPropagate(subs)
|
shallowPropagate(subs)
|
||||||
end
|
end
|
||||||
dirty = true
|
return true
|
||||||
end
|
end
|
||||||
elseif bit32.band(flags, bit32.bor(MUTABLE, PENDING)) == bit32.bor(MUTABLE, PENDING) then
|
elseif bit32.band(depFlags, bit32.bor(MUTABLE, PENDING)) == bit32.bor(MUTABLE, PENDING) then
|
||||||
if link.nextSub or link.prevSub then
|
if checkDirty(dep.deps :: Link, dep) then
|
||||||
stack = { value = link, prev = stack }
|
if update(dep) then
|
||||||
end
|
local subs = dep.subs :: Link
|
||||||
link = dep.deps :: Link
|
if subs.nextSub then
|
||||||
sub = dep
|
shallowPropagate(subs)
|
||||||
checkDepth += 1
|
|
||||||
continue
|
|
||||||
end
|
|
||||||
|
|
||||||
if not dirty then
|
|
||||||
local nextDep = link.nextDep
|
|
||||||
if nextDep then
|
|
||||||
link = nextDep
|
|
||||||
continue
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local continueTop = false
|
|
||||||
|
|
||||||
while checkDepth ~= 0 do
|
|
||||||
checkDepth -= 1
|
|
||||||
local firstSub = sub.subs :: Link
|
|
||||||
local hasMultipleSubs = firstSub.nextSub ~= nil
|
|
||||||
if hasMultipleSubs then
|
|
||||||
link = (stack :: Stack<Link>).value
|
|
||||||
stack = (stack :: Stack<Link>).prev
|
|
||||||
else
|
|
||||||
link = firstSub
|
|
||||||
end
|
|
||||||
if dirty then
|
|
||||||
if update(sub) then
|
|
||||||
if hasMultipleSubs then
|
|
||||||
shallowPropagate(firstSub)
|
|
||||||
end
|
end
|
||||||
sub = link.sub
|
return true
|
||||||
continue
|
|
||||||
end
|
end
|
||||||
dirty = false
|
|
||||||
else
|
else
|
||||||
sub.flags = bit32.band(sub.flags, bit32.bnot(PENDING))
|
dep.flags = bit32.band(depFlags, bit32.bnot(PENDING))
|
||||||
end
|
|
||||||
sub = link.sub
|
|
||||||
local nextDep = link.nextDep
|
|
||||||
if nextDep then
|
|
||||||
link = nextDep
|
|
||||||
continueTop = true
|
|
||||||
break
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if continueTop then
|
link = link.nextDep :: Link
|
||||||
continue
|
until not link
|
||||||
end
|
|
||||||
|
|
||||||
return dirty
|
return false
|
||||||
until false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -235,7 +235,7 @@ local function signalOper<T>(
|
||||||
local value: T
|
local value: T
|
||||||
if type(update) == "function" then
|
if type(update) == "function" then
|
||||||
local prevSub = setActiveSub(nil)
|
local prevSub = setActiveSub(nil)
|
||||||
local success, result: T = (if globals.strict then noYield else pcall)(update, this.currentValue)
|
local success, result: T = (if globals.strict then noYield else pcall)(update, this.pendingValue)
|
||||||
activeSub = prevSub
|
activeSub = prevSub
|
||||||
if not success then
|
if not success then
|
||||||
error(result)
|
error(result)
|
||||||
|
|
@ -278,9 +278,7 @@ local function signalOper<T>(
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
local subs = sub.subs
|
local subs = sub.subs
|
||||||
if subs then
|
sub = if subs then subs.sub else nil
|
||||||
sub = subs.sub
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
return this.currentValue
|
return this.currentValue
|
||||||
end
|
end
|
||||||
|
|
@ -434,17 +432,21 @@ end
|
||||||
--- For an alternative that still cleans up inner effects, use `peek` instead.
|
--- For an alternative that still cleans up inner effects, use `peek` instead.
|
||||||
---
|
---
|
||||||
--- @param callback The callback function.
|
--- @param callback The callback function.
|
||||||
--- @param args Arguments to pass to the callback.
|
--- @param ...args Arguments to pass to the callback.
|
||||||
--- @return The return value of the callback.
|
--- @return The return value of the callback.
|
||||||
--- @see https://github.com/littensy/charm?tab=readme-ov-file#untrackedcallback
|
--- @see https://github.com/littensy/charm?tab=readme-ov-file#untrackedcallback
|
||||||
local function untracked<Args..., Results...>(callback: (Args...) -> Results..., ...: Args...): Results...
|
local function untracked<Args..., Results...>(callback: (Args...) -> Results..., ...: Args...): Results...
|
||||||
local prevSub = setActiveSub(nil)
|
if activeSub then
|
||||||
local results: { any } = { pcall(callback, ...) }
|
local prevSub = setActiveSub(nil)
|
||||||
activeSub = prevSub
|
local results: { any } = { pcall(callback, ...) }
|
||||||
if not results[1] then
|
activeSub = prevSub
|
||||||
error(results[2])
|
if not results[1] then
|
||||||
|
error(results[2])
|
||||||
|
end
|
||||||
|
return unpack(results, 2)
|
||||||
|
else
|
||||||
|
return callback(...)
|
||||||
end
|
end
|
||||||
return unpack(results, 2)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Runs the callback without subscribing to signal updates, but still captures
|
--- Runs the callback without subscribing to signal updates, but still captures
|
||||||
|
|
@ -453,7 +455,7 @@ end
|
||||||
--- To avoid capturing inner effects, use `untracked` instead.
|
--- To avoid capturing inner effects, use `untracked` instead.
|
||||||
---
|
---
|
||||||
--- @param callback The callback function.
|
--- @param callback The callback function.
|
||||||
--- @param args Arguments to pass to the callback.
|
--- @param ...args Arguments to pass to the callback.
|
||||||
--- @return The return value of the callback.
|
--- @return The return value of the callback.
|
||||||
--- @see https://github.com/littensy/charm?tab=readme-ov-file#peekcallback
|
--- @see https://github.com/littensy/charm?tab=readme-ov-file#peekcallback
|
||||||
local function peek<Args..., Results...>(callback: (Args...) -> Results..., ...: Args...): Results...
|
local function peek<Args..., Results...>(callback: (Args...) -> Results..., ...: Args...): Results...
|
||||||
|
|
@ -478,10 +480,10 @@ end
|
||||||
--- computed signals after the provided callback finishes running.
|
--- computed signals after the provided callback finishes running.
|
||||||
---
|
---
|
||||||
--- @param callback A function that performs multiple updates.
|
--- @param callback A function that performs multiple updates.
|
||||||
--- @param args Arguments to pass to the callback.
|
--- @param ...args Arguments to pass to the callback.
|
||||||
--- @return The return value of the callback.
|
--- @return The return value of the callback.
|
||||||
--- @see https://github.com/littensy/charm?tab=readme-ov-file#batchedcallback
|
--- @see https://github.com/littensy/charm?tab=readme-ov-file#batchcallback
|
||||||
local function batched<Args..., Results...>(callback: (Args...) -> Results..., ...: Args...): Results...
|
local function batch<Args..., Results...>(callback: (Args...) -> Results..., ...: Args...): Results...
|
||||||
startBatch()
|
startBatch()
|
||||||
local results: { any } = { (if globals.strict then noYield else pcall)(callback, ...) }
|
local results: { any } = { (if globals.strict then noYield else pcall)(callback, ...) }
|
||||||
endBatch()
|
endBatch()
|
||||||
|
|
@ -497,7 +499,7 @@ end
|
||||||
---
|
---
|
||||||
--- @param getter A function that returns the value to track.
|
--- @param getter A function that returns the value to track.
|
||||||
--- @return A computed signal that tracks the value returned by the getter.
|
--- @return A computed signal that tracks the value returned by the getter.
|
||||||
local function toComputed<T>(getter: () -> T): () -> T
|
local function toSignal<T>(getter: () -> T): () -> T
|
||||||
local name = debug.info(getter, "n")
|
local name = debug.info(getter, "n")
|
||||||
if name == "signalGetter" or name == "computedGetter" or name == "atomGetterSetter" then
|
if name == "signalGetter" or name == "computedGetter" or name == "atomGetterSetter" then
|
||||||
return getter
|
return getter
|
||||||
|
|
@ -519,7 +521,7 @@ end
|
||||||
--- @return A function for disposing the effect.
|
--- @return A function for disposing the effect.
|
||||||
--- @see https://github.com/littensy/charm?tab=readme-ov-file#listengetter-callback
|
--- @see https://github.com/littensy/charm?tab=readme-ov-file#listengetter-callback
|
||||||
local function listen<T>(getter: () -> T, callback: (state: T, previous: T?) -> ()): () -> ()
|
local function listen<T>(getter: () -> T, callback: (state: T, previous: T?) -> ()): () -> ()
|
||||||
local tracked = toComputed(getter)
|
local tracked = toSignal(getter)
|
||||||
local current: T
|
local current: T
|
||||||
return effect(function()
|
return effect(function()
|
||||||
local previous = current
|
local previous = current
|
||||||
|
|
@ -540,15 +542,12 @@ end
|
||||||
--- @return A function for disposing the effect.
|
--- @return A function for disposing the effect.
|
||||||
--- @see https://github.com/littensy/charm?tab=readme-ov-file#subscribegetter-callback
|
--- @see https://github.com/littensy/charm?tab=readme-ov-file#subscribegetter-callback
|
||||||
local function subscribe<T>(getter: () -> T, callback: (state: T, lastState: T) -> ()): () -> ()
|
local function subscribe<T>(getter: () -> T, callback: (state: T, lastState: T) -> ()): () -> ()
|
||||||
local tracked = toComputed(getter)
|
local tracked = toSignal(getter)
|
||||||
local current: T
|
local current = tracked()
|
||||||
local mounted = true
|
|
||||||
return effect(function()
|
return effect(function()
|
||||||
local previous = current
|
local previous = current
|
||||||
current = tracked()
|
current = tracked()
|
||||||
if mounted then
|
if current ~= previous then
|
||||||
mounted = false
|
|
||||||
else
|
|
||||||
peek(callback, current, previous)
|
peek(callback, current, previous)
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
@ -578,12 +577,14 @@ local reactiveSystem = createReactiveSystem({
|
||||||
|
|
||||||
queuedLength = insertIndex
|
queuedLength = insertIndex
|
||||||
|
|
||||||
|
-- while (firstInsertedIndex < --insertIndex)
|
||||||
insertIndex -= 1
|
insertIndex -= 1
|
||||||
while firstInsertedIndex < insertIndex do
|
while firstInsertedIndex < insertIndex do
|
||||||
local left = queued[firstInsertedIndex]
|
local left = queued[firstInsertedIndex]
|
||||||
queued[firstInsertedIndex] = queued[insertIndex]
|
queued[firstInsertedIndex] = queued[insertIndex]
|
||||||
queued[insertIndex] = left
|
queued[insertIndex] = left
|
||||||
firstInsertedIndex += 1
|
firstInsertedIndex += 1
|
||||||
|
-- Decrement before next iteration to simulate --insertIndex
|
||||||
insertIndex -= 1
|
insertIndex -= 1
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
|
|
@ -620,7 +621,7 @@ return {
|
||||||
onCleanup = onCleanup,
|
onCleanup = onCleanup,
|
||||||
untracked = untracked,
|
untracked = untracked,
|
||||||
peek = peek,
|
peek = peek,
|
||||||
batched = batched,
|
batch = batch,
|
||||||
subscribe = subscribe,
|
subscribe = subscribe,
|
||||||
listen = listen,
|
listen = listen,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
4
packages/charm/src/index.d.ts
vendored
4
packages/charm/src/index.d.ts
vendored
|
|
@ -149,9 +149,9 @@ export function peek<Args extends any[], Result>(callback: (...args: Args) => Re
|
||||||
* @param callback A function that performs multiple updates.
|
* @param callback A function that performs multiple updates.
|
||||||
* @param args Arguments to pass to the callback.
|
* @param args Arguments to pass to the callback.
|
||||||
* @returns The return value of the callback.
|
* @returns The return value of the callback.
|
||||||
* @see https://github.com/littensy/charm?tab=readme-ov-file#batchedcallback
|
* @see https://github.com/littensy/charm?tab=readme-ov-file#batchcallback
|
||||||
*/
|
*/
|
||||||
export function batched<Args extends any[], Result>(callback: (...args: Args) => Result, ...args: Args): Result;
|
export function batch<Args extends any[], Result>(callback: (...args: Args) => Result, ...args: Args): Result;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an effect that runs the callback one immediately, and then again
|
* Creates an effect that runs the callback one immediately, and then again
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,22 @@ type Update<T> = ((previous: T) -> T) | T
|
||||||
--- its current value.
|
--- its current value.
|
||||||
export type Atom<T = any> = (...Update<T>) -> T
|
export type Atom<T = any> = (...Update<T>) -> T
|
||||||
|
|
||||||
|
--- A getter-only reactive signal. Calling the signal with no arguments returns
|
||||||
|
--- its current value. It cannot be set directly.
|
||||||
|
export type Getter<T = any> = () -> T
|
||||||
|
|
||||||
|
--- A setter-only reactive signal. Calling the signal with an argument sets its
|
||||||
|
--- value. It cannot be read directly.
|
||||||
|
export type Setter<T = any> = (...Update<T>) -> ()
|
||||||
|
|
||||||
|
export type ReactiveNode = core.ReactiveNode
|
||||||
|
export type Link = core.Link
|
||||||
|
|
||||||
return {
|
return {
|
||||||
core = core,
|
core = core,
|
||||||
globals = globals,
|
globals = globals,
|
||||||
atom = core.atom,
|
atom = core.atom,
|
||||||
batched = core.batched,
|
batch = core.batch,
|
||||||
computed = core.computed,
|
computed = core.computed,
|
||||||
effect = core.effect,
|
effect = core.effect,
|
||||||
effectScope = core.effectScope,
|
effectScope = core.effectScope,
|
||||||
|
|
|
||||||
|
|
@ -1,70 +1,66 @@
|
||||||
local core = require("./core")
|
local core = require("./core")
|
||||||
local globals = require("./globals")
|
|
||||||
|
|
||||||
local batched = core.batched
|
type ReactiveNode = core.ReactiveNode
|
||||||
|
|
||||||
|
local batch = core.batch
|
||||||
local effect = core.effect
|
local effect = core.effect
|
||||||
local effectScope = core.effectScope
|
local effectScope = core.effectScope
|
||||||
local onCleanup = core.onCleanup
|
|
||||||
|
|
||||||
--- Calls the callback function for each unique key in the table returned by the
|
--- Calls the callback function for each unique key in the table returned by the
|
||||||
--- getter. The callback is called once for each key, and again when a new key
|
--- getter. The callback is called once for each key, and again when a new key
|
||||||
--- is added.
|
--- is added.
|
||||||
---
|
---
|
||||||
--- The callback may return a cleanup function, which will be called when the
|
--- The callback may return a cleanup function, which will be called when the
|
||||||
--- key is removed or when the entire callback is disposed.
|
--- key is removed or the `observe()` effect is disposed. Effects created
|
||||||
---
|
--- during the callback will also be bound to the lifetime of the key.
|
||||||
--- Effects created within the callback are cleaned up when the item is removed
|
|
||||||
--- from the table or when the callback is disposed.
|
|
||||||
---
|
---
|
||||||
--- @param getter A function that returns the table to observe.
|
--- @param getter A function that returns the table to observe.
|
||||||
--- @param callback A function that is called for each key in the table.
|
--- @param callback A function that is called for each key in the table.
|
||||||
--- @return A function for disposing the observer.
|
--- @return A function for disposing the observer.
|
||||||
--- @see https://github.com/littensy/charm?tab=readme-ov-file#observegetter-callback
|
--- @see https://github.com/littensy/charm?tab=readme-ov-file#observegetter-callback
|
||||||
local function observe<K, V>(getter: () -> { [K]: V }, callback: (item: V, key: K) -> ...(() -> ())?): () -> ()
|
local function observe<K, V>(getter: () -> { [K]: V }, callback: (item: V, key: K) -> ...(() -> ())?): () -> ()
|
||||||
local scopes: { [K]: () -> () } = {}
|
local innerScopes: { [K]: () -> () } = {}
|
||||||
local currentItems: { [K]: V } = {}
|
|
||||||
|
|
||||||
local function updateScopes(currentItems: { [K]: V }, previousItems: { [K]: V })
|
local function updateScopes(data: { [K]: V })
|
||||||
for key in previousItems do
|
for key, dispose in innerScopes do
|
||||||
local dispose = scopes[key]
|
if data[key] == nil then
|
||||||
if currentItems[key] == nil and dispose then
|
innerScopes[key] = nil
|
||||||
dispose()
|
dispose()
|
||||||
scopes[key] = nil
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
for key, value in currentItems do
|
for key, value in data do
|
||||||
if previousItems[key] == nil and scopes[key] == nil then
|
if innerScopes[key] == nil then
|
||||||
scopes[key] = effectScope(function()
|
-- Detach the inner scope from the parent effect to prevent
|
||||||
|
-- disposing when the effect re-runs.
|
||||||
|
innerScopes[key] = effectScope(function()
|
||||||
return callback(value, key)
|
return callback(value, key)
|
||||||
end, true)
|
end, true)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function disposeScopes()
|
return effectScope(function()
|
||||||
for _, dispose in scopes do
|
effect(function()
|
||||||
dispose()
|
-- Batch so that side effects run after all scopes are created. This
|
||||||
end
|
-- ensures that a scope creating or removing items doesn't interfere
|
||||||
table.clear(scopes)
|
-- with the current update cycle.
|
||||||
end
|
batch(updateScopes, getter())
|
||||||
|
|
||||||
local disposeEffect = effect(function()
|
-- Effects can't trigger themselves more than once, so if a scope
|
||||||
local previousItems = currentItems
|
-- creates or removes items, we need to check for updates again. The
|
||||||
currentItems = getter()
|
-- way we do this abuses a weird property of signal accesses that
|
||||||
if currentItems ~= previousItems then
|
-- causes the effect to re-run.
|
||||||
batched(updateScopes, currentItems, previousItems)
|
getter()
|
||||||
|
end)
|
||||||
|
|
||||||
|
return function()
|
||||||
|
for _, dispose in innerScopes do
|
||||||
|
dispose()
|
||||||
|
end
|
||||||
|
table.clear(innerScopes)
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
if globals.trackInnerEffects then
|
|
||||||
onCleanup(disposeScopes, true)
|
|
||||||
end
|
|
||||||
|
|
||||||
return function()
|
|
||||||
disposeEffect()
|
|
||||||
disposeScopes()
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return observe
|
return observe
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ local function noYield<Args..., Results...>(callback: (Args...) -> Results..., .
|
||||||
end
|
end
|
||||||
|
|
||||||
local function deepFreeze(object: any)
|
local function deepFreeze(object: any)
|
||||||
if type(object) == "table" and not table.isfrozen(object) then
|
if type(object) == "table" and not table.isfrozen(object) and getmetatable(object) == nil then
|
||||||
table.freeze(object)
|
table.freeze(object)
|
||||||
for _, value in object do
|
for _, value in object do
|
||||||
deepFreeze(value)
|
deepFreeze(value)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue