@e280/strata 0.0.0-5 → 0.0.0-7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +140 -39
  2. package/package.json +9 -2
  3. package/s/index.ts +3 -5
  4. package/s/signals/index.ts +5 -0
  5. package/s/signals/parts/computed.ts +53 -0
  6. package/s/signals/parts/effect.ts +23 -0
  7. package/s/signals/parts/signal.ts +66 -0
  8. package/s/signals/signals.test.ts +181 -0
  9. package/s/tests.test.ts +45 -286
  10. package/s/tracker/index.ts +3 -0
  11. package/s/tracker/tracker.test.ts +40 -0
  12. package/s/tracker/tracker.ts +73 -0
  13. package/s/tree/index.ts +7 -0
  14. package/s/{parts/substrata.ts → tree/parts/branch.ts} +11 -10
  15. package/s/{parts/chronstrata.ts → tree/parts/chronobranch.ts} +8 -8
  16. package/s/{parts/strata.ts → tree/parts/trunk.ts} +14 -13
  17. package/s/{parts → tree/parts}/types.ts +8 -8
  18. package/s/{parts → tree/parts}/utils/setup.ts +9 -9
  19. package/s/tree/tree.test.ts +307 -0
  20. package/x/index.d.ts +3 -5
  21. package/x/index.js +3 -5
  22. package/x/index.js.map +1 -1
  23. package/x/signals/index.d.ts +3 -0
  24. package/x/signals/index.js +4 -0
  25. package/x/signals/index.js.map +1 -0
  26. package/x/signals/parts/computed.d.ts +14 -0
  27. package/x/signals/parts/computed.js +41 -0
  28. package/x/signals/parts/computed.js.map +1 -0
  29. package/x/signals/parts/effect.d.ts +5 -0
  30. package/x/signals/parts/effect.js +17 -0
  31. package/x/signals/parts/effect.js.map +1 -0
  32. package/x/signals/parts/signal.d.ts +18 -0
  33. package/x/signals/parts/signal.js +47 -0
  34. package/x/signals/parts/signal.js.map +1 -0
  35. package/x/signals/signals.test.d.ts +18 -0
  36. package/x/signals/signals.test.js +144 -0
  37. package/x/signals/signals.test.js.map +1 -0
  38. package/x/tests.test.js +46 -265
  39. package/x/tests.test.js.map +1 -1
  40. package/x/tracker/index.d.ts +1 -0
  41. package/x/tracker/index.js +2 -0
  42. package/x/tracker/index.js.map +1 -0
  43. package/x/tracker/tracker.d.ts +29 -0
  44. package/x/tracker/tracker.js +62 -0
  45. package/x/tracker/tracker.js.map +1 -0
  46. package/x/tracker/tracker.test.d.ts +6 -0
  47. package/x/tracker/tracker.test.js +32 -0
  48. package/x/tracker/tracker.test.js.map +1 -0
  49. package/x/tree/index.d.ts +5 -0
  50. package/x/tree/index.js +6 -0
  51. package/x/tree/index.js.map +1 -0
  52. package/x/tree/parts/branch.d.ts +15 -0
  53. package/x/{parts/substrata.js → tree/parts/branch.js} +10 -9
  54. package/x/tree/parts/branch.js.map +1 -0
  55. package/x/{parts/chronstrata.d.ts → tree/parts/chronobranch.d.ts} +6 -6
  56. package/x/{parts/chronstrata.js → tree/parts/chronobranch.js} +6 -6
  57. package/x/tree/parts/chronobranch.js.map +1 -0
  58. package/x/tree/parts/persistence.js.map +1 -0
  59. package/x/tree/parts/trunk.d.ts +17 -0
  60. package/x/{parts/strata.js → tree/parts/trunk.js} +13 -12
  61. package/x/tree/parts/trunk.js.map +1 -0
  62. package/x/{parts → tree/parts}/types.d.ts +8 -8
  63. package/x/{parts → tree/parts}/types.js.map +1 -1
  64. package/x/tree/parts/utils/process-options.js.map +1 -0
  65. package/x/tree/parts/utils/setup.d.ts +8 -0
  66. package/x/{parts → tree/parts}/utils/setup.js +8 -8
  67. package/x/tree/parts/utils/setup.js.map +1 -0
  68. package/x/tree/tree.test.d.ts +37 -0
  69. package/x/tree/tree.test.js +271 -0
  70. package/x/tree/tree.test.js.map +1 -0
  71. package/x/parts/chronstrata.js.map +0 -1
  72. package/x/parts/persistence.js.map +0 -1
  73. package/x/parts/strata.d.ts +0 -17
  74. package/x/parts/strata.js.map +0 -1
  75. package/x/parts/substrata.d.ts +0 -15
  76. package/x/parts/substrata.js.map +0 -1
  77. package/x/parts/utils/process-options.js.map +0 -1
  78. package/x/parts/utils/setup.d.ts +0 -8
  79. package/x/parts/utils/setup.js.map +0 -1
  80. /package/s/{parts → tree/parts}/persistence.ts +0 -0
  81. /package/s/{parts → tree/parts}/utils/process-options.ts +0 -0
  82. /package/x/{parts → tree/parts}/persistence.d.ts +0 -0
  83. /package/x/{parts → tree/parts}/persistence.js +0 -0
  84. /package/x/{parts → tree/parts}/types.js +0 -0
  85. /package/x/{parts → tree/parts}/utils/process-options.d.ts +0 -0
  86. /package/x/{parts → tree/parts}/utils/process-options.js +0 -0
package/README.md CHANGED
@@ -1,26 +1,119 @@
1
1
 
2
2
  ![](https://i.imgur.com/h7FohWa.jpeg)
3
3
 
4
+ <br/>
5
+
4
6
  # ⛏️ strata
5
7
 
6
- **strata isn't just state management. *it's pure sex.***
7
- - 📦 `npm install @e280/strata`
8
- - single-source-of-truth state tree
9
- - immutable except for `mutate(fn)` calls
10
- - no spooky-dookie proxy magic just god's honest javascript
11
- - undo/redo history, cross-tab sync, localStorage persistence
12
- - probably my 10th state management library, lol
8
+ ### get in loser, we're managing state
9
+ 📦 `npm install @e280/strata`
10
+ 🧙‍♂️ probably my tenth state management library, lol
11
+
12
+ 🚦 **signals**ephemeral view-level state
13
+ 🌳 **tree** persistent app-level state
14
+ 🪄 **tracker** reactivity integration hub
13
15
 
14
16
  <br/>
15
17
 
16
- ## get in loser, we're managing state
18
+ > [!TIP]
19
+ > incredibly, signals and trees are interoperable.
20
+ > that means, effects and computeds are responsive to changes in tree state.
21
+
22
+ <br/>
23
+
24
+ ## 🚦 signals — *ephemeral view-level state*
25
+ ```ts
26
+ import {signal, effect, computed} from "@e280/strata"
27
+ ```
28
+
29
+ ### each signal holds a value
30
+ - **create a signal**
31
+ ```ts
32
+ const count = signal(0)
33
+ ```
34
+ - **read a signal**
35
+ ```ts
36
+ count() // 0
37
+ ```
38
+ - **set a signal**
39
+ ```ts
40
+ count(1)
41
+ ```
42
+ - **set a signal, and await effect propagation**
43
+ ```ts
44
+ await count(2)
45
+ ```
46
+ - **signals are for auto rerendering your ui.**
47
+ components/views will auto rerender when relevant signals change
48
+ — well only if your ui lib is cool and integrates `tracker`.
17
49
 
18
- ### `Strata` is your app's state tree root
50
+ ### pick your poison
51
+ - **signals hipster fn syntax**
52
+ ```ts
53
+ count() // get
54
+ await count(2) // set
55
+ ```
56
+ - **signals get/set syntax**
57
+ ```ts
58
+ count.get() // get
59
+ await count.set(2) // set
60
+ ```
61
+ - **signals .value accessor syntax**
62
+ ```ts
63
+ count.value // get
64
+ count.value = 2 // set
65
+ ```
66
+ value pattern is nice for this vibe
67
+ ```ts
68
+ count.value++
69
+ count.value += 1
70
+ ```
71
+
72
+ ### effects
73
+ - **effects run when the relevant signals change**
74
+ ```ts
75
+ effect(() => console.log(count()))
76
+ // 1
77
+ // the system detects 'count' is relevant
78
+
79
+ count.value++
80
+ // 2
81
+ // when count is changed, the effect fn is run
82
+ ```
83
+ - **computed signals are super lazy**
84
+ they only run if and when you get the value
85
+ ```ts
86
+ const tenly = computed(() => {
87
+ console.log("recomputed!")
88
+ return count() * 10
89
+ })
90
+
91
+ console.log(tenly())
92
+ // "recomputed!"
93
+ // 20
94
+
95
+ await count(3)
96
+
97
+ console.log(tenly.value)
98
+ // "recomputed!"
99
+ // 30
100
+ ```
101
+
102
+ <br/>
103
+
104
+ ## 🌳 tree — *persistent app-level state*
105
+ - single-source-of-truth state tree
106
+ - immutable except for `mutate(fn)` calls
107
+ - undo/redo history, cross-tab sync, localStorage persistence
108
+ - no spooky-dookie proxy magic — just god's honest javascript
109
+ - separate but compatible with signals
110
+
111
+ #### `Trunk` is your app's state tree root
19
112
  - better stick to json-friendly serializable data
20
113
  ```ts
21
- import {Strata} from "@e280/strata"
114
+ import {Trunk} from "@e280/strata"
22
115
 
23
- const strata = new Strata({
116
+ const trunk = new Trunk({
24
117
  count: 0,
25
118
  snacks: {
26
119
  peanuts: 8,
@@ -28,26 +121,26 @@
28
121
  },
29
122
  })
30
123
 
31
- strata.state.count // 0
32
- strata.state.snacks.peanuts // 8
124
+ trunk.state.count // 0
125
+ trunk.state.snacks.peanuts // 8
33
126
  ```
34
127
 
35
- ### formal mutations to change state
128
+ #### formal mutations to change state
36
129
  - ⛔ informal mutations are denied
37
130
  ```ts
38
- strata.state.count++ // error is thrown
131
+ trunk.state.count++ // error is thrown
39
132
  ```
40
133
  - ✅ formal mutations are allowed
41
134
  ```ts
42
- await strata.mutate(s => s.count++)
135
+ await trunk.mutate(s => s.count++)
43
136
  ```
44
137
 
45
- ### `Substrata` is a view into a subtree
138
+ #### `Branch` is a view into a subtree
46
139
  - it's a lens, make lots of them, pass 'em around your app
47
140
  ```ts
48
- const snacks = strata.substrata(s => s.snacks)
141
+ const snacks = trunk.branch(s => s.snacks)
49
142
  ```
50
- - run substrata mutations
143
+ - run branch mutations
51
144
  ```ts
52
145
  await snacks.mutate(s => s.peanuts++)
53
146
  ```
@@ -55,43 +148,42 @@
55
148
  ```ts
56
149
  await snacks.mutate(s => s.bag.push("salt"))
57
150
  ```
58
- - you can make a substrata of another substrata
151
+ - you can branch a branch
59
152
 
60
- ### watch mutations
61
- - you can listen to global mutations on the strata
153
+ #### watch for mutations
154
+ - on the trunk, we can listen deeply for mutations within the whole tree
62
155
  ```ts
63
- strata.watch(s => console.log(s.count))
156
+ trunk.watch(s => console.log(s.count))
64
157
  ```
65
- - substrata listeners don't care about outside changes
158
+ - whereas branch listeners don't care about changes outside their scope
66
159
  ```ts
67
160
  snacks.watch(s => console.log(s.peanuts))
68
161
  ```
69
162
  - watch returns a fn to stop listening
70
163
  ```ts
71
- const stop = strata.watch(s => console.log(s.count))
164
+ const stop = trunk.watch(s => console.log(s.count))
72
165
  stop() // stop listening
73
166
  ```
74
167
 
75
- <br/>
76
-
77
- ## only high-class discerning aristocrats permitted beyond this point
168
+ ### only discerning high-class aristocrats are permitted beyond this point
78
169
 
79
- ### `Strata.setup` for localStorage persistence etc
170
+ #### `Trunk.setup` for localStorage persistence etc
80
171
  - simple setup
81
172
  ```ts
82
- const {strata} = await Strata.setup({
173
+ const {trunk} = await Trunk.setup({
83
174
  version: 1, // 👈 bump whenever your change state schema!
84
175
  initialState: {count: 0},
85
176
  })
86
177
  ```
178
+ - uses localStorage by default
87
179
  - it's compatible with [`@e280/kv`](https://github.com/e280/kv)
88
180
  ```ts
89
181
  import {Kv, StorageDriver} from "@e280/kv"
90
182
 
91
183
  const kv = new Kv(new StorageDriver())
92
- const store = kv.store<any>("strata")
184
+ const store = kv.store<any>("appState")
93
185
 
94
- const {strata} = await Strata.setup({
186
+ const {trunk} = await Trunk.setup({
95
187
  version: 1,
96
188
  initialState: {count: 0},
97
189
  persistence: {
@@ -101,21 +193,21 @@
101
193
  })
102
194
  ```
103
195
 
104
- ### `Chronstrata` for undo/redo history
196
+ #### `Chronobranch` for undo/redo history
105
197
  - first, put a `Chronicle` into your state tree
106
198
  ```ts
107
- const strata = new Strata({
199
+ const trunk = new Trunk({
108
200
  count: 0,
109
- snacks: Strata.chronicle({
201
+ snacks: Trunk.chronicle({
110
202
  peanuts: 8,
111
203
  bag: ["popcorn", "butter"],
112
204
  }),
113
205
  })
114
206
  ```
115
207
  - *big-brain moment:* the whole chronicle *itself* is stored in the state.. serializable.. think persistence — user can close their project, reopen, and their undo/redo history is still chillin' — *brat girl summer*
116
- - second, make a `Chronstrata` which is like a substrata
208
+ - second, make a `Chronobranch` which is like a branch, but is concerned with history
117
209
  ```ts
118
- const snacks = strata.chronstrata(64, s => s.snacks)
210
+ const snacks = trunk.chronobranch(64, s => s.snacks)
119
211
  // \
120
212
  // how many past snapshots to store
121
213
  ```
@@ -134,8 +226,17 @@
134
226
  snacks.undoable // 2
135
227
  snacks.redoable // 1
136
228
  ```
137
- - chronstrata can have its own substrata — all their mutations advance history
138
- - plz pinky-swear right now, that you won't create a chronstrata under a substrata under a chronstrata
229
+ - chronobranch can have its own branches — all their mutations advance history
230
+ - plz pinky-swear right now, that you won't create a chronobranch under a branch under another chronobranch 💀
231
+
232
+ <br/>
233
+
234
+ ## 🪄 tracker — integrations
235
+ - ```ts
236
+ import {tracker} from "@e280/strata/tracker"
237
+ ```
238
+ - all reactivity is orchestrated by the `tracker`
239
+ - if you are integrating a new state object, or a new view layer that needs to react to state changes, just read [tracker.ts](./s/tracker/tracker.ts)
139
240
 
140
241
  <br/>
141
242
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@e280/strata",
3
- "version": "0.0.0-5",
3
+ "version": "0.0.0-7",
4
4
  "description": "state management",
5
5
  "license": "MIT",
6
6
  "author": "Chase Moskal <chasemoskal@gmail.com>",
@@ -10,6 +10,13 @@
10
10
  "x",
11
11
  "s"
12
12
  ],
13
+ "exports": {
14
+ ".": "./x/index.js",
15
+ "./signals": "./x/signals/index.js",
16
+ "./tracker": "./x/tracker/index.js",
17
+ "./tree": "./x/tree/index.js",
18
+ "./*": "./*"
19
+ },
13
20
  "scripts": {
14
21
  "build": "run-s _clean _links _tsc",
15
22
  "test": "node x/tests.test.js",
@@ -24,7 +31,7 @@
24
31
  },
25
32
  "devDependencies": {
26
33
  "@e280/science": "^0.0.5",
27
- "@types/node": "^24.0.4",
34
+ "@types/node": "^24.0.7",
28
35
  "npm-run-all": "^4.1.5",
29
36
  "typescript": "^5.8.3"
30
37
  },
package/s/index.ts CHANGED
@@ -1,7 +1,5 @@
1
1
 
2
- export * from "./parts/chronstrata.js"
3
- export * from "./parts/persistence.js"
4
- export * from "./parts/strata.js"
5
- export * from "./parts/substrata.js"
6
- export * from "./parts/types.js"
2
+ export * from "./signals/index.js"
3
+ export * from "./tracker/index.js"
4
+ export * from "./tree/index.js"
7
5
 
@@ -0,0 +1,5 @@
1
+
2
+ export * from "./parts/computed.js"
3
+ export * from "./parts/effect.js"
4
+ export * from "./parts/signal.js"
5
+
@@ -0,0 +1,53 @@
1
+
2
+ import {initEffect} from "./effect.js"
3
+ import {SignalCore} from "./signal.js"
4
+
5
+ export type Computed<V> = {(): V} & ComputedCore<V>
6
+
7
+ export function computed<V>(fn: () => V) {
8
+ const core = new ComputedCore<V>(fn)
9
+
10
+ function f(): V {
11
+ return (f as any).value
12
+ }
13
+
14
+ Object.setPrototypeOf(f, ComputedCore.prototype)
15
+ Object.assign(f, core)
16
+
17
+ return f as Computed<V>
18
+ }
19
+
20
+ export class ComputedCore<V> extends SignalCore<V> {
21
+ _dirty = false
22
+ _formula: () => V
23
+ _effect: (() => void) | undefined
24
+
25
+ constructor(formula: () => V) {
26
+ super(undefined as any)
27
+ this._formula = formula
28
+ }
29
+
30
+ get() {
31
+ if (!this._effect) {
32
+ const {result, dispose} = initEffect(this._formula, () => this._dirty = true)
33
+ this._effect = dispose
34
+ this.sneak = result
35
+ }
36
+ if (this._dirty) {
37
+ this._dirty = false
38
+ super.set(this._formula())
39
+ }
40
+ return super.get()
41
+ }
42
+
43
+ async set() {
44
+ throw new Error("computed is readonly")
45
+ }
46
+
47
+ dispose() {
48
+ if (this._effect)
49
+ this._effect()
50
+ super.dispose()
51
+ }
52
+ }
53
+
@@ -0,0 +1,23 @@
1
+
2
+ import {debounce} from "@e280/stz"
3
+ import {tracker} from "../../tracker/tracker.js"
4
+
5
+ export function effect<C = void>(collector: () => C, responder: () => void = collector) {
6
+ return initEffect<C>(collector, responder).dispose
7
+ }
8
+
9
+ export function initEffect<C = void>(collector: () => C, responder: () => void = collector) {
10
+ const {seen, result} = tracker.seen(collector)
11
+ const fn = debounce(0, responder)
12
+
13
+ const disposers: (() => void)[] = []
14
+ const dispose = () => disposers.forEach(d => d())
15
+
16
+ for (const saw of seen) {
17
+ const dispose = tracker.changed(saw, fn)
18
+ disposers.push(dispose)
19
+ }
20
+
21
+ return {result, dispose}
22
+ }
23
+
@@ -0,0 +1,66 @@
1
+
2
+ import {sub} from "@e280/stz"
3
+ import {tracker} from "../../tracker/tracker.js"
4
+
5
+ export type Signal<V> = {
6
+ (): V
7
+ (v: V): Promise<void>
8
+ (v?: V): V | Promise<void>
9
+ } & SignalCore<V>
10
+
11
+ export function signal<V>(value: V) {
12
+ const core = new SignalCore(value)
13
+
14
+ function fn(): V
15
+ function fn(v: V): Promise<void>
16
+ function fn(v?: V): V | Promise<void> {
17
+ return v !== undefined
18
+ ? (fn as any).set(v)
19
+ : (fn as any).get()
20
+ }
21
+
22
+ Object.setPrototypeOf(fn, SignalCore.prototype)
23
+ Object.assign(fn, core)
24
+
25
+ return fn as Signal<V>
26
+ }
27
+
28
+ export class SignalCore<V> {
29
+ on = sub<[V]>()
30
+ published: Promise<V>
31
+
32
+ constructor(public sneak: V) {
33
+ this.published = Promise.resolve(sneak)
34
+ }
35
+
36
+ get() {
37
+ tracker.see(this)
38
+ return this.sneak
39
+ }
40
+
41
+ async set(v: V) {
42
+ if (v !== this.sneak)
43
+ await this.publish(v)
44
+ }
45
+
46
+ get value() {
47
+ return this.get()
48
+ }
49
+
50
+ set value(v: V) {
51
+ this.set(v)
52
+ }
53
+
54
+ async publish(v = this.get()) {
55
+ this.sneak = v
56
+ await Promise.all([
57
+ tracker.change(this),
58
+ this.published = this.on.pub(v).then(() => v),
59
+ ])
60
+ }
61
+
62
+ dispose() {
63
+ this.on.clear()
64
+ }
65
+ }
66
+
@@ -0,0 +1,181 @@
1
+
2
+ import {Science, test, expect} from "@e280/science"
3
+
4
+ import {effect} from "./parts/effect.js"
5
+ import {signal} from "./parts/signal.js"
6
+ import {computed} from "./parts/computed.js"
7
+
8
+ export default Science.suite({
9
+ "signal get/set value": test(async() => {
10
+ const count = signal(0)
11
+ expect(count.value).is(0)
12
+
13
+ count.value++
14
+ expect(count.value).is(1)
15
+
16
+ count.value = 5
17
+ expect(count.value).is(5)
18
+ }),
19
+
20
+ "signal fn syntax": test(async() => {
21
+ const count = signal(0)
22
+ expect(count()).is(0)
23
+
24
+ count(count() + 1)
25
+ expect(count()).is(1)
26
+
27
+ count(5)
28
+ expect(count()).is(5)
29
+ }),
30
+
31
+ "signal syntax interop": test(async() => {
32
+ const count = signal(0)
33
+
34
+ count.value = 1
35
+ expect(count()).is(1)
36
+ }),
37
+
38
+ "signal on is not debounced": test(async() => {
39
+ const count = signal(1)
40
+ let runs = 0
41
+ count.on(() => void runs++)
42
+ await count.set(2)
43
+ await count.set(3)
44
+ expect(runs).is(2)
45
+ }),
46
+
47
+ "signal on only fires on change": test(async() => {
48
+ const count = signal(1)
49
+ let runs = 0
50
+ count.on(() => void runs++)
51
+ await count.set(2)
52
+ await count.set(2)
53
+ expect(runs).is(1)
54
+ }),
55
+
56
+ "effect tracks signal changes": test(async() => {
57
+ const count = signal(1)
58
+ let doubled = 0
59
+
60
+ effect(() => doubled = count.value * 2)
61
+ expect(doubled).is(2)
62
+
63
+ await count.set(3)
64
+ expect(doubled).is(6)
65
+ }),
66
+
67
+ "effect is only called when signal actually changes": test(async() => {
68
+ const count = signal(1)
69
+ let runs = 0
70
+ effect(() => {
71
+ count.get()
72
+ runs++
73
+ })
74
+ expect(runs).is(1)
75
+ await count.set(999)
76
+ expect(runs).is(2)
77
+ await count.set(999)
78
+ expect(runs).is(2)
79
+ }),
80
+
81
+ "effects are debounced": test(async() => {
82
+ const count = signal(1)
83
+ let runs = 0
84
+ effect(() => {
85
+ count.get()
86
+ runs++
87
+ })
88
+ expect(runs).is(1)
89
+ count.value++
90
+ count.value++
91
+ await count.set(count.get() + 1)
92
+ expect(runs).is(2)
93
+ }),
94
+
95
+ "effects can be disposed": test(async() => {
96
+ const count = signal(1)
97
+ let doubled = 0
98
+
99
+ const dispose = effect(() => doubled = count.value * 2)
100
+ expect(doubled).is(2)
101
+
102
+ await count.set(3)
103
+ expect(doubled).is(6)
104
+
105
+ dispose()
106
+ await count.set(4)
107
+ expect(doubled).is(6) // old value
108
+ }),
109
+
110
+ "signal set promise waits for effects": test(async() => {
111
+ const count = signal(1)
112
+ let doubled = 0
113
+
114
+ effect(() => doubled = count.value * 2)
115
+ expect(doubled).is(2)
116
+
117
+ await count.set(3)
118
+ expect(doubled).is(6)
119
+ }),
120
+
121
+ "effect only runs on change": test(async() => {
122
+ const sig = signal("a")
123
+ let runs = 0
124
+
125
+ effect(() => {
126
+ sig.value
127
+ runs++
128
+ })
129
+ expect(runs).is(1)
130
+
131
+ await sig.set("a")
132
+ expect(runs).is(1)
133
+
134
+ await sig.set("b")
135
+ expect(runs).is(2)
136
+ }),
137
+
138
+ "computed values": test(async() => {
139
+ const a = signal(2)
140
+ const b = signal(3)
141
+ const sum = computed(() => a.value + b.value)
142
+ expect(sum.value).is(5)
143
+
144
+ await a.set(5)
145
+ expect(sum.value).is(8)
146
+
147
+ await b.set(7)
148
+ expect(sum.value).is(12)
149
+ }),
150
+
151
+ "computed is lazy": test(async() => {
152
+ const a = signal(1)
153
+ let runs = 0
154
+
155
+ const comp = computed(() => {
156
+ runs++
157
+ return a.value * 10
158
+ })
159
+
160
+ expect(runs).is(0)
161
+ expect(comp.value).is(10)
162
+ expect(runs).is(1)
163
+
164
+ await a.set(2)
165
+ expect(runs).is(1)
166
+ expect(comp.value).is(20)
167
+ expect(runs).is(2)
168
+ }),
169
+
170
+ "computed fn syntax": test(async() => {
171
+ const a = signal(2)
172
+ const b = signal(3)
173
+ const sum = computed(() => a.value + b.value)
174
+ expect(sum.value).is(5)
175
+
176
+ await a.set(5)
177
+ expect(sum.value).is(8)
178
+ expect(sum()).is(8)
179
+ }),
180
+ })
181
+