@e280/strata 0.0.0-5 → 0.0.0-6

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 +119 -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 +4 -4
  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 +3 -3
  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,100 @@
1
1
 
2
2
  ![](https://i.imgur.com/h7FohWa.jpeg)
3
3
 
4
+ <br/>
5
+
4
6
  # ⛏️ strata
7
+ 📦 `npm install @e280/strata`
8
+ 🫩 probably my tenth state management library, lol
5
9
 
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
10
+ <br/>
11
+
12
+ ### get in loser, we're managing state
13
+ 🚦 **signals** ephemeral view-level state
14
+ 🌳 **tree**persistent app-level state
15
+ 🪄 **tracker** reactivity integration hub
16
+
17
+ <br/>
18
+
19
+ > [!TIP]
20
+ > incredibly, signals and trees are interoperable.
21
+ > that means, effects and computeds are responsive to changes in tree state.
13
22
 
14
23
  <br/>
15
24
 
16
- ## get in loser, we're managing state
25
+ ## 🚦 signals *ephemeral view-level state*
26
+ - `@e280/strata/signals`
27
+ ```ts
28
+ import {signal, effect, computed} from "@e280/strata"
29
+ ```
30
+ - **signals are little bundles of joy**
31
+ ```ts
32
+ const count = signal(0)
33
+
34
+ console.log(count.get())
35
+ // 0
17
36
 
18
- ### `Strata` is your app's state tree root
37
+ count.set(1)
38
+
39
+ console.log(count.value)
40
+ // 1
41
+ ```
42
+ - components/views will auto rerender when relevant signals change
43
+ (if your component/view library is cool an integrates with `tracker`)
44
+ - three ways to get it
45
+ ```ts
46
+ count() // 1
47
+ count.get() // 1
48
+ count.value // 1
49
+ ```
50
+ - three ways to set it
51
+ ```ts
52
+ await count(2)
53
+ await count.set(2)
54
+ count.value = 2
55
+ ```
56
+ - using `await` here allows you to wait for downstream effects to finish
57
+ - **effects are run when the relevant signals change**
58
+ ```ts
59
+ effect(() => console.log(count()))
60
+ // 2
61
+
62
+ count.value++
63
+ // 3
64
+ ```
65
+ - **computed signals are super lazy**
66
+ ```ts
67
+ const tenly = computed(() => {
68
+ console.log("recomputed!")
69
+ return count() * 10
70
+ })
71
+
72
+ console.log(tenly())
73
+ // "recomputed!"
74
+ // 30
75
+
76
+ await count(4)
77
+
78
+ console.log(tenly.value)
79
+ // "recomputed!"
80
+ // 40
81
+ ```
82
+
83
+ <br/>
84
+
85
+ ## 🌳 tree — *persistent app-level state*
86
+ - `@e280/strata/tree`
87
+ - single-source-of-truth state tree
88
+ - immutable except for `mutate(fn)` calls
89
+ - undo/redo history, cross-tab sync, localStorage persistence
90
+ - no spooky-dookie proxy magic — just god's honest javascript
91
+
92
+ #### `Trunk` is your app's state tree root
19
93
  - better stick to json-friendly serializable data
20
94
  ```ts
21
- import {Strata} from "@e280/strata"
95
+ import {Trunk} from "@e280/strata"
22
96
 
23
- const strata = new Strata({
97
+ const trunk = new Trunk({
24
98
  count: 0,
25
99
  snacks: {
26
100
  peanuts: 8,
@@ -28,26 +102,26 @@
28
102
  },
29
103
  })
30
104
 
31
- strata.state.count // 0
32
- strata.state.snacks.peanuts // 8
105
+ trunk.state.count // 0
106
+ trunk.state.snacks.peanuts // 8
33
107
  ```
34
108
 
35
- ### formal mutations to change state
109
+ #### formal mutations to change state
36
110
  - ⛔ informal mutations are denied
37
111
  ```ts
38
- strata.state.count++ // error is thrown
112
+ trunk.state.count++ // error is thrown
39
113
  ```
40
114
  - ✅ formal mutations are allowed
41
115
  ```ts
42
- await strata.mutate(s => s.count++)
116
+ await trunk.mutate(s => s.count++)
43
117
  ```
44
118
 
45
- ### `Substrata` is a view into a subtree
119
+ #### `Branch` is a view into a subtree
46
120
  - it's a lens, make lots of them, pass 'em around your app
47
121
  ```ts
48
- const snacks = strata.substrata(s => s.snacks)
122
+ const snacks = trunk.branch(s => s.snacks)
49
123
  ```
50
- - run substrata mutations
124
+ - run branch mutations
51
125
  ```ts
52
126
  await snacks.mutate(s => s.peanuts++)
53
127
  ```
@@ -55,43 +129,42 @@
55
129
  ```ts
56
130
  await snacks.mutate(s => s.bag.push("salt"))
57
131
  ```
58
- - you can make a substrata of another substrata
132
+ - you can branch a branch
59
133
 
60
- ### watch mutations
61
- - you can listen to global mutations on the strata
134
+ #### watch for mutations
135
+ - on the trunk, we can listen deeply for mutations within the whole tree
62
136
  ```ts
63
- strata.watch(s => console.log(s.count))
137
+ trunk.watch(s => console.log(s.count))
64
138
  ```
65
- - substrata listeners don't care about outside changes
139
+ - whereas branch listeners don't care about changes outside their scope
66
140
  ```ts
67
141
  snacks.watch(s => console.log(s.peanuts))
68
142
  ```
69
143
  - watch returns a fn to stop listening
70
144
  ```ts
71
- const stop = strata.watch(s => console.log(s.count))
145
+ const stop = trunk.watch(s => console.log(s.count))
72
146
  stop() // stop listening
73
147
  ```
74
148
 
75
- <br/>
149
+ ### only discerning high-class aristocrats are permitted beyond this point
76
150
 
77
- ## only high-class discerning aristocrats permitted beyond this point
78
-
79
- ### `Strata.setup` for localStorage persistence etc
151
+ #### `Trunk.setup` for localStorage persistence etc
80
152
  - simple setup
81
153
  ```ts
82
- const {strata} = await Strata.setup({
154
+ const {trunk} = await Trunk.setup({
83
155
  version: 1, // 👈 bump whenever your change state schema!
84
156
  initialState: {count: 0},
85
157
  })
86
158
  ```
159
+ - uses localStorage by default
87
160
  - it's compatible with [`@e280/kv`](https://github.com/e280/kv)
88
161
  ```ts
89
162
  import {Kv, StorageDriver} from "@e280/kv"
90
163
 
91
164
  const kv = new Kv(new StorageDriver())
92
- const store = kv.store<any>("strata")
165
+ const store = kv.store<any>("appState")
93
166
 
94
- const {strata} = await Strata.setup({
167
+ const {trunk} = await Trunk.setup({
95
168
  version: 1,
96
169
  initialState: {count: 0},
97
170
  persistence: {
@@ -101,21 +174,21 @@
101
174
  })
102
175
  ```
103
176
 
104
- ### `Chronstrata` for undo/redo history
177
+ #### `Chronobranch` for undo/redo history
105
178
  - first, put a `Chronicle` into your state tree
106
179
  ```ts
107
- const strata = new Strata({
180
+ const trunk = new Trunk({
108
181
  count: 0,
109
- snacks: Strata.chronicle({
182
+ snacks: Trunk.chronicle({
110
183
  peanuts: 8,
111
184
  bag: ["popcorn", "butter"],
112
185
  }),
113
186
  })
114
187
  ```
115
188
  - *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
189
+ - second, make a `Chronobranch` which is like a branch
117
190
  ```ts
118
- const snacks = strata.chronstrata(64, s => s.snacks)
191
+ const snacks = trunk.chronobranch(64, s => s.snacks)
119
192
  // \
120
193
  // how many past snapshots to store
121
194
  ```
@@ -134,8 +207,15 @@
134
207
  snacks.undoable // 2
135
208
  snacks.redoable // 1
136
209
  ```
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
210
+ - chronobranch can have its own branch — all their mutations advance history
211
+ - plz pinky-swear right now, that you won't create a chronobranch under a branch under another chronobranch 💀
212
+
213
+ <br/>
214
+
215
+ ## 🪄 tracker — integrations
216
+ - `@e280/strata/tracker`
217
+ - all reactivity is orchestrated by the `tracker`
218
+ - 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
219
 
140
220
  <br/>
141
221
 
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-6",
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
+