@zeix/cause-effect 0.17.3 → 0.18.0

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 (89) hide show
  1. package/.ai-context.md +163 -232
  2. package/.cursorrules +41 -35
  3. package/.github/copilot-instructions.md +166 -116
  4. package/ARCHITECTURE.md +274 -0
  5. package/CLAUDE.md +199 -143
  6. package/COLLECTION_REFACTORING.md +161 -0
  7. package/GUIDE.md +298 -0
  8. package/README.md +232 -197
  9. package/REQUIREMENTS.md +100 -0
  10. package/bench/reactivity.bench.ts +577 -0
  11. package/index.dev.js +1325 -997
  12. package/index.js +1 -1
  13. package/index.ts +58 -74
  14. package/package.json +4 -1
  15. package/src/errors.ts +118 -74
  16. package/src/graph.ts +601 -0
  17. package/src/nodes/collection.ts +474 -0
  18. package/src/nodes/effect.ts +149 -0
  19. package/src/nodes/list.ts +588 -0
  20. package/src/nodes/memo.ts +120 -0
  21. package/src/nodes/sensor.ts +139 -0
  22. package/src/nodes/state.ts +135 -0
  23. package/src/nodes/store.ts +383 -0
  24. package/src/nodes/task.ts +146 -0
  25. package/src/signal.ts +112 -66
  26. package/src/util.ts +26 -57
  27. package/test/batch.test.ts +96 -62
  28. package/test/benchmark.test.ts +473 -487
  29. package/test/collection.test.ts +466 -706
  30. package/test/effect.test.ts +293 -696
  31. package/test/list.test.ts +335 -592
  32. package/test/memo.test.ts +380 -0
  33. package/test/regression.test.ts +156 -0
  34. package/test/scope.test.ts +191 -0
  35. package/test/sensor.test.ts +454 -0
  36. package/test/signal.test.ts +220 -213
  37. package/test/state.test.ts +217 -265
  38. package/test/store.test.ts +346 -446
  39. package/test/task.test.ts +395 -0
  40. package/test/untrack.test.ts +167 -0
  41. package/types/index.d.ts +13 -15
  42. package/types/src/errors.d.ts +73 -17
  43. package/types/src/graph.d.ts +208 -0
  44. package/types/src/nodes/collection.d.ts +64 -0
  45. package/types/src/nodes/effect.d.ts +48 -0
  46. package/types/src/nodes/list.d.ts +65 -0
  47. package/types/src/nodes/memo.d.ts +57 -0
  48. package/types/src/nodes/sensor.d.ts +75 -0
  49. package/types/src/nodes/state.d.ts +78 -0
  50. package/types/src/nodes/store.d.ts +51 -0
  51. package/types/src/nodes/task.d.ts +73 -0
  52. package/types/src/signal.d.ts +43 -29
  53. package/types/src/util.d.ts +9 -16
  54. package/archive/benchmark.ts +0 -683
  55. package/archive/collection.ts +0 -253
  56. package/archive/composite.ts +0 -85
  57. package/archive/computed.ts +0 -195
  58. package/archive/list.ts +0 -483
  59. package/archive/memo.ts +0 -139
  60. package/archive/state.ts +0 -90
  61. package/archive/store.ts +0 -298
  62. package/archive/task.ts +0 -189
  63. package/src/classes/collection.ts +0 -245
  64. package/src/classes/computed.ts +0 -349
  65. package/src/classes/list.ts +0 -343
  66. package/src/classes/ref.ts +0 -70
  67. package/src/classes/state.ts +0 -102
  68. package/src/classes/store.ts +0 -262
  69. package/src/diff.ts +0 -138
  70. package/src/effect.ts +0 -93
  71. package/src/match.ts +0 -45
  72. package/src/resolve.ts +0 -49
  73. package/src/system.ts +0 -257
  74. package/test/computed.test.ts +0 -1108
  75. package/test/diff.test.ts +0 -955
  76. package/test/match.test.ts +0 -388
  77. package/test/ref.test.ts +0 -353
  78. package/test/resolve.test.ts +0 -154
  79. package/types/src/classes/collection.d.ts +0 -45
  80. package/types/src/classes/computed.d.ts +0 -94
  81. package/types/src/classes/list.d.ts +0 -43
  82. package/types/src/classes/ref.d.ts +0 -35
  83. package/types/src/classes/state.d.ts +0 -49
  84. package/types/src/classes/store.d.ts +0 -52
  85. package/types/src/diff.d.ts +0 -28
  86. package/types/src/effect.d.ts +0 -15
  87. package/types/src/match.d.ts +0 -21
  88. package/types/src/resolve.d.ts +0 -29
  89. package/types/src/system.d.ts +0 -78
@@ -1,5 +1,10 @@
1
1
  import { describe, expect, mock, test } from 'bun:test'
2
- import { batch, createEffect, Memo, State } from '../index.ts'
2
+ import {
3
+ batch as batchNext,
4
+ createEffect as createEffectNext,
5
+ createMemo,
6
+ createState,
7
+ } from '../index.ts'
3
8
  import { Counter, makeGraph, runGraph } from './util/dependency-graph'
4
9
  import type { Computed, ReactiveFramework } from './util/reactive-framework'
5
10
 
@@ -12,25 +17,27 @@ const busy = () => {
12
17
  }
13
18
  }
14
19
 
15
- const framework = {
16
- name: 'Cause & Effect',
20
+ /* === Framework Adapters === */
21
+
22
+ const v18: ReactiveFramework = {
23
+ name: 'v0.18.0 (graph)',
24
+ // @ts-expect-error ReactiveFramework doesn't have non-nullable signals
17
25
  signal: <T extends {}>(initialValue: T) => {
18
- const s = new State<T>(initialValue)
19
- return {
20
- write: (v: T) => s.set(v),
21
- read: () => s.get(),
22
- }
26
+ const s = createState(initialValue)
27
+ return { write: s.set, read: s.get }
23
28
  },
29
+ // @ts-expect-error ReactiveFramework doesn't have non-nullable signals
24
30
  computed: <T extends {}>(fn: () => T) => {
25
- const c = new Memo(fn)
26
- return {
27
- read: () => c.get(),
28
- }
31
+ const c = createMemo(fn)
32
+ return { read: c.get }
33
+ },
34
+ effect: (fn: () => undefined) => {
35
+ createEffectNext(() => fn())
29
36
  },
30
- effect: (fn: () => undefined) => createEffect(fn),
31
- withBatch: (fn: () => undefined) => batch(fn),
37
+ withBatch: fn => batchNext(fn),
32
38
  withBuild: <T>(fn: () => T) => fn(),
33
39
  }
40
+
34
41
  const testPullCounts = true
35
42
 
36
43
  function makeConfig() {
@@ -45,584 +52,563 @@ function makeConfig() {
45
52
  }
46
53
  }
47
54
 
48
- /* === Test functions === */
55
+ /* === Parameterized Test Suite === */
49
56
 
50
- /** some basic tests to validate the reactive framework
51
- * wrapper works and can run performance tests.
52
- */
53
- describe('Basic test', () => {
57
+ for (const framework of [v18]) {
54
58
  const name = framework.name
55
- test(`${name} | simple dependency executes`, () => {
56
- framework.withBuild(() => {
57
- const s = framework.signal(2)
58
- const c = framework.computed(() => s.read() * 2)
59
59
 
60
- expect(c.read()).toEqual(4)
60
+ describe(`Basic tests [${name}]`, () => {
61
+ test('simple dependency executes', () => {
62
+ framework.withBuild(() => {
63
+ const s = framework.signal(2)
64
+ const c = framework.computed(() => s.read() * 2)
65
+ expect(c.read()).toEqual(4)
66
+ })
61
67
  })
62
- })
63
68
 
64
- test(`${name} | simple write`, () => {
65
- framework.withBuild(() => {
66
- const s = framework.signal(2)
67
- const c = framework.computed(() => s.read() * 2)
68
- expect(s.read()).toEqual(2)
69
- expect(c.read()).toEqual(4)
69
+ test('simple write', () => {
70
+ framework.withBuild(() => {
71
+ const s = framework.signal(2)
72
+ const c = framework.computed(() => s.read() * 2)
73
+ expect(s.read()).toEqual(2)
74
+ expect(c.read()).toEqual(4)
70
75
 
71
- s.write(3)
72
- expect(s.read()).toEqual(3)
73
- expect(c.read()).toEqual(6)
76
+ s.write(3)
77
+ expect(s.read()).toEqual(3)
78
+ expect(c.read()).toEqual(6)
79
+ })
74
80
  })
75
- })
76
81
 
77
- test(`${name} | static graph`, () => {
78
- const config = makeConfig()
79
- const counter = new Counter()
80
- // @ts-expect-error - Framework object has incompatible type constraints with ReactiveFramework
81
- const graph = makeGraph(framework, config, counter)
82
- // @ts-expect-error - Framework object has incompatible type constraints with ReactiveFramework
83
- const sum = runGraph(graph, 2, 1, framework)
84
- expect(sum).toEqual(16)
85
- if (testPullCounts) {
86
- expect(counter.count).toEqual(11)
87
- } else {
88
- expect(counter.count).toBeGreaterThanOrEqual(11)
89
- }
90
- })
91
-
92
- test(`${name} | static graph, read 2/3 of leaves`, () => {
93
- framework.withBuild(() => {
82
+ test('static graph', () => {
94
83
  const config = makeConfig()
95
- config.readFraction = 2 / 3
96
- config.iterations = 10
97
84
  const counter = new Counter()
98
- // @ts-expect-error - Framework object has incompatible type constraints with ReactiveFramework
99
85
  const graph = makeGraph(framework, config, counter)
100
- // @ts-expect-error - Framework object has incompatible type constraints with ReactiveFramework
101
- const sum = runGraph(graph, 10, 2 / 3, framework)
102
-
103
- expect(sum).toEqual(71)
86
+ const sum = runGraph(graph, 2, 1, framework)
87
+ expect(sum).toEqual(16)
104
88
  if (testPullCounts) {
105
- expect(counter.count).toEqual(41)
89
+ expect(counter.count).toEqual(11)
106
90
  } else {
107
- expect(counter.count).toBeGreaterThanOrEqual(41)
91
+ expect(counter.count).toBeGreaterThanOrEqual(11)
108
92
  }
109
93
  })
110
- })
111
94
 
112
- test(`${name} | dynamic graph`, () => {
113
- framework.withBuild(() => {
114
- const config = makeConfig()
115
- config.staticFraction = 0.5
116
- config.width = 4
117
- config.totalLayers = 2
118
- const counter = new Counter()
119
- // @ts-expect-error - Framework object has incompatible type constraints with ReactiveFramework
120
- const graph = makeGraph(framework, config, counter)
121
- // @ts-expect-error - Framework object has incompatible type constraints with ReactiveFramework
122
- const sum = runGraph(graph, 10, 1, framework)
123
-
124
- expect(sum).toEqual(72)
125
- if (testPullCounts) {
126
- expect(counter.count).toEqual(22)
127
- } else {
128
- expect(counter.count).toBeGreaterThanOrEqual(22)
129
- }
95
+ test('static graph, read 2/3 of leaves', () => {
96
+ framework.withBuild(() => {
97
+ const config = makeConfig()
98
+ config.readFraction = 2 / 3
99
+ config.iterations = 10
100
+ const counter = new Counter()
101
+ const graph = makeGraph(framework, config, counter)
102
+ const sum = runGraph(graph, 10, 2 / 3, framework)
103
+
104
+ expect(sum).toEqual(71)
105
+ if (testPullCounts) {
106
+ expect(counter.count).toEqual(41)
107
+ } else {
108
+ expect(counter.count).toBeGreaterThanOrEqual(41)
109
+ }
110
+ })
130
111
  })
131
- })
132
-
133
- test(`${name} | withBuild`, () => {
134
- const r = framework.withBuild(() => {
135
- const s = framework.signal(2)
136
- const c = framework.computed(() => s.read() * 2)
137
112
 
138
- expect(c.read()).toEqual(4)
139
- return c.read()
113
+ test('dynamic graph', () => {
114
+ framework.withBuild(() => {
115
+ const config = makeConfig()
116
+ config.staticFraction = 0.5
117
+ config.width = 4
118
+ config.totalLayers = 2
119
+ const counter = new Counter()
120
+ const graph = makeGraph(framework, config, counter)
121
+ const sum = runGraph(graph, 10, 1, framework)
122
+
123
+ expect(sum).toEqual(72)
124
+ if (testPullCounts) {
125
+ expect(counter.count).toEqual(22)
126
+ } else {
127
+ expect(counter.count).toBeGreaterThanOrEqual(22)
128
+ }
129
+ })
140
130
  })
141
- expect(r).toEqual(4)
142
- })
143
-
144
- test(`${name} | effect`, () => {
145
- const spy = (_v: number) => {}
146
- const spyMock = mock(spy)
147
-
148
- const s = framework.signal(2)
149
- let c: { read: () => number } = { read: () => 0 }
150
131
 
151
- framework.withBuild(() => {
152
- c = framework.computed(() => s.read() * 2)
153
-
154
- framework.effect(() => {
155
- spyMock(c.read())
132
+ test('withBuild', () => {
133
+ const r = framework.withBuild(() => {
134
+ const s = framework.signal(2)
135
+ const c = framework.computed(() => s.read() * 2)
136
+ expect(c.read()).toEqual(4)
137
+ return c.read()
156
138
  })
139
+ expect(r).toEqual(4)
157
140
  })
158
- expect(spyMock.mock.calls.length).toBe(1)
159
141
 
160
- framework.withBatch(() => {
161
- s.write(3)
162
- })
163
- expect(s.read()).toEqual(3)
164
- expect(c.read()).toEqual(6)
165
- expect(spyMock.mock.calls.length).toBe(2)
166
- })
167
- })
142
+ test('effect', () => {
143
+ const spy = (_v: number) => {}
144
+ const spyMock = mock(spy)
168
145
 
169
- describe('Kairo tests', () => {
170
- const name = framework.name
146
+ const s = framework.signal(2)
147
+ let c: { read: () => number } = { read: () => 0 }
171
148
 
172
- test(`${name} | avoidable propagation`, async () => {
173
- const head = framework.signal(0)
174
- const computed1 = framework.computed(() => head.read())
175
- const computed2 = framework.computed(() => {
176
- computed1.read()
177
- return 0
178
- })
179
- const computed3 = framework.computed(() => {
180
- busy()
181
- return computed2.read() + 1
182
- }) // heavy computation
183
- const computed4 = framework.computed(() => computed3.read() + 2)
184
- const computed5 = framework.computed(() => computed4.read() + 3)
185
- framework.effect(() => {
186
- computed5.read()
187
- busy() // heavy side effect
188
- })
149
+ framework.withBuild(() => {
150
+ c = framework.computed(() => s.read() * 2)
151
+ framework.effect(() => {
152
+ spyMock(c.read())
153
+ })
154
+ })
155
+ expect(spyMock.mock.calls.length).toBe(1)
189
156
 
190
- return () => {
191
157
  framework.withBatch(() => {
192
- head.write(1)
158
+ s.write(3)
193
159
  })
194
- expect(computed5.read()).toBe(6)
195
- for (let i = 0; i < 10; i++) {
196
- framework.withBatch(() => {
197
- head.write(i)
198
- })
199
- expect(computed5.read()).toBe(6)
200
- }
201
- }
160
+ expect(s.read()).toEqual(3)
161
+ expect(c.read()).toEqual(6)
162
+ expect(spyMock.mock.calls.length).toBe(2)
163
+ })
202
164
  })
203
165
 
204
- test(`${name} | broad propagation`, async () => {
205
- const head = framework.signal(0)
206
- let last = head as { read: () => number }
207
- const callCounter = new Counter()
208
- for (let i = 0; i < 50; i++) {
209
- const current = framework.computed(() => {
210
- return head.read() + i
166
+ describe(`Kairo tests [${name}]`, () => {
167
+ test('avoidable propagation', async () => {
168
+ const head = framework.signal(0)
169
+ const computed1 = framework.computed(() => head.read())
170
+ const computed2 = framework.computed(() => {
171
+ computed1.read()
172
+ return 0
211
173
  })
212
- const current2 = framework.computed(() => {
213
- return current.read() + 1
174
+ const computed3 = framework.computed(() => {
175
+ busy()
176
+ return computed2.read() + 1
214
177
  })
178
+ const computed4 = framework.computed(() => computed3.read() + 2)
179
+ const computed5 = framework.computed(() => computed4.read() + 3)
215
180
  framework.effect(() => {
216
- current2.read()
217
- callCounter.count++
181
+ computed5.read()
182
+ busy()
218
183
  })
219
- last = current2
220
- }
221
184
 
222
- return () => {
223
- framework.withBatch(() => {
224
- head.write(1)
225
- })
226
- const atleast = 50 * 50
227
- callCounter.count = 0
228
- for (let i = 0; i < 50; i++) {
185
+ return () => {
229
186
  framework.withBatch(() => {
230
- head.write(i)
187
+ head.write(1)
231
188
  })
232
- expect(last.read()).toBe(i + 50)
189
+ expect(computed5.read()).toBe(6)
190
+ for (let i = 0; i < 10; i++) {
191
+ framework.withBatch(() => {
192
+ head.write(i)
193
+ })
194
+ expect(computed5.read()).toBe(6)
195
+ }
233
196
  }
234
- expect(callCounter.count).toBe(atleast)
235
- }
236
- })
237
-
238
- test(`${name} | deep propagation`, async () => {
239
- const len = 50
240
- const head = framework.signal(0)
241
- let current = head as { read: () => number }
242
- for (let i = 0; i < len; i++) {
243
- const c = current
244
- current = framework.computed(() => {
245
- return c.read() + 1
246
- })
247
- }
248
- const callCounter = new Counter()
249
- framework.effect(() => {
250
- current.read()
251
- callCounter.count++
252
197
  })
253
- const iter = 50
254
198
 
255
- return () => {
256
- framework.withBatch(() => {
257
- head.write(1)
258
- })
259
- const atleast = iter
260
- callCounter.count = 0
261
- for (let i = 0; i < iter; i++) {
262
- framework.withBatch(() => {
263
- head.write(i)
199
+ test('broad propagation', async () => {
200
+ const head = framework.signal(0)
201
+ let last = head as { read: () => number }
202
+ const callCounter = new Counter()
203
+ for (let i = 0; i < 50; i++) {
204
+ const current = framework.computed(() => head.read() + i)
205
+ const current2 = framework.computed(() => current.read() + 1)
206
+ framework.effect(() => {
207
+ current2.read()
208
+ callCounter.count++
264
209
  })
265
- expect(current.read()).toBe(len + i)
210
+ last = current2
266
211
  }
267
- expect(callCounter.count).toBe(atleast)
268
- }
269
- })
270
-
271
- test(`${name} | diamond`, async () => {
272
- const width = 5
273
- const head = framework.signal(0)
274
- const current: { read(): number }[] = []
275
- for (let i = 0; i < width; i++) {
276
- current.push(framework.computed(() => head.read() + 1))
277
- }
278
- const sum = framework.computed(() => {
279
- return current.map(x => x.read()).reduce((a, b) => a + b, 0)
280
- })
281
- const callCounter = new Counter()
282
- framework.effect(() => {
283
- sum.read()
284
- callCounter.count++
285
- })
286
212
 
287
- return () => {
288
- framework.withBatch(() => {
289
- head.write(1)
290
- })
291
- expect(sum.read()).toBe(2 * width)
292
- const atleast = 500
293
- callCounter.count = 0
294
- for (let i = 0; i < 500; i++) {
213
+ return () => {
295
214
  framework.withBatch(() => {
296
- head.write(i)
215
+ head.write(1)
297
216
  })
298
- expect(sum.read()).toBe((i + 1) * width)
217
+ const atleast = 50 * 50
218
+ callCounter.count = 0
219
+ for (let i = 0; i < 50; i++) {
220
+ framework.withBatch(() => {
221
+ head.write(i)
222
+ })
223
+ expect(last.read()).toBe(i + 50)
224
+ }
225
+ expect(callCounter.count).toBe(atleast)
299
226
  }
300
- expect(callCounter.count).toBe(atleast)
301
- }
302
- })
303
-
304
- test(`${name} | mux`, async () => {
305
- const heads = new Array(100).fill(null).map(_ => framework.signal(0))
306
- const mux = framework.computed(() => {
307
- return Object.fromEntries(heads.map(h => h.read()).entries())
308
227
  })
309
- const splited = heads
310
- .map((_, index) => framework.computed(() => mux.read()[index]))
311
- .map(x => framework.computed(() => x.read() + 1))
312
228
 
313
- splited.forEach(x => {
229
+ test('deep propagation', async () => {
230
+ const len = 50
231
+ const head = framework.signal(0)
232
+ let current = head as { read: () => number }
233
+ for (let i = 0; i < len; i++) {
234
+ const c = current
235
+ current = framework.computed(() => c.read() + 1)
236
+ }
237
+ const callCounter = new Counter()
314
238
  framework.effect(() => {
315
- x.read()
239
+ current.read()
240
+ callCounter.count++
316
241
  })
317
- })
242
+ const iter = 50
318
243
 
319
- return () => {
320
- for (let i = 0; i < 10; i++) {
321
- framework.withBatch(() => {
322
- heads[i].write(i)
323
- })
324
- expect(splited[i].read()).toBe(i + 1)
325
- }
326
- for (let i = 0; i < 10; i++) {
244
+ return () => {
327
245
  framework.withBatch(() => {
328
- heads[i].write(i * 2)
246
+ head.write(1)
329
247
  })
330
- expect(splited[i].read()).toBe(i * 2 + 1)
331
- }
332
- }
333
- })
334
-
335
- test(`${name} | repeated observers`, async () => {
336
- const size = 30
337
- const head = framework.signal(0)
338
- const current = framework.computed(() => {
339
- let result = 0
340
- for (let i = 0; i < size; i++) {
341
- result += head.read()
248
+ const atleast = iter
249
+ callCounter.count = 0
250
+ for (let i = 0; i < iter; i++) {
251
+ framework.withBatch(() => {
252
+ head.write(i)
253
+ })
254
+ expect(current.read()).toBe(len + i)
255
+ }
256
+ expect(callCounter.count).toBe(atleast)
342
257
  }
343
- return result
344
- })
345
- const callCounter = new Counter()
346
- framework.effect(() => {
347
- current.read()
348
- callCounter.count++
349
258
  })
350
259
 
351
- return () => {
352
- framework.withBatch(() => {
353
- head.write(1)
260
+ test('diamond', async () => {
261
+ const width = 5
262
+ const head = framework.signal(0)
263
+ const current: { read(): number }[] = []
264
+ for (let i = 0; i < width; i++) {
265
+ current.push(framework.computed(() => head.read() + 1))
266
+ }
267
+ const sum = framework.computed(() =>
268
+ current.map(x => x.read()).reduce((a, b) => a + b, 0),
269
+ )
270
+ const callCounter = new Counter()
271
+ framework.effect(() => {
272
+ sum.read()
273
+ callCounter.count++
354
274
  })
355
- expect(current.read()).toBe(size)
356
- const atleast = 100
357
- callCounter.count = 0
358
- for (let i = 0; i < 100; i++) {
275
+
276
+ return () => {
359
277
  framework.withBatch(() => {
360
- head.write(i)
278
+ head.write(1)
361
279
  })
362
- expect(current.read()).toBe(i * size)
280
+ expect(sum.read()).toBe(2 * width)
281
+ const atleast = 500
282
+ callCounter.count = 0
283
+ for (let i = 0; i < 500; i++) {
284
+ framework.withBatch(() => {
285
+ head.write(i)
286
+ })
287
+ expect(sum.read()).toBe((i + 1) * width)
288
+ }
289
+ expect(callCounter.count).toBe(atleast)
363
290
  }
364
- expect(callCounter.count).toBe(atleast)
365
- }
366
- })
367
-
368
- test(`${name} | triangle`, async () => {
369
- const width = 10
370
- const head = framework.signal(0)
371
- let current = head as { read: () => number }
372
- const list: { read: () => number }[] = []
373
- for (let i = 0; i < width; i++) {
374
- const c = current
375
- list.push(current)
376
- current = framework.computed(() => {
377
- return c.read() + 1
378
- })
379
- }
380
- const sum = framework.computed(() => {
381
- return list.map(x => x.read()).reduce((a, b) => a + b, 0)
382
- })
383
- const callCounter = new Counter()
384
- framework.effect(() => {
385
- sum.read()
386
- callCounter.count++
387
291
  })
388
292
 
389
- return () => {
390
- const count = (number: number) => {
391
- return new Array(number)
392
- .fill(0)
393
- .map((_, i) => i + 1)
394
- .reduce((x, y) => x + y, 0)
395
- }
396
- const constant = count(width)
397
- framework.withBatch(() => {
398
- head.write(1)
399
- })
400
- expect(sum.read()).toBe(constant)
401
- const atleast = 100
402
- callCounter.count = 0
403
- for (let i = 0; i < 100; i++) {
404
- framework.withBatch(() => {
405
- head.write(i)
293
+ test('mux', async () => {
294
+ const heads = new Array(100)
295
+ .fill(null)
296
+ .map(_ => framework.signal(0))
297
+ const mux = framework.computed(() =>
298
+ Object.fromEntries(heads.map(h => h.read()).entries()),
299
+ )
300
+ const splited = heads
301
+ .map((_, index) => framework.computed(() => mux.read()[index]))
302
+ .map(x => framework.computed(() => x.read() + 1))
303
+
304
+ for (const x of splited) {
305
+ framework.effect(() => {
306
+ x.read()
406
307
  })
407
- expect(sum.read()).toBe(constant - width + i * width)
408
308
  }
409
- expect(callCounter.count).toBe(atleast)
410
- }
411
- })
412
309
 
413
- test(`${name} | unstable`, async () => {
414
- const head = framework.signal(0)
415
- const double = framework.computed(() => head.read() * 2)
416
- const inverse = framework.computed(() => -head.read())
417
- const current = framework.computed(() => {
418
- let result = 0
419
- for (let i = 0; i < 20; i++) {
420
- result += head.read() % 2 ? double.read() : inverse.read()
310
+ return () => {
311
+ for (let i = 0; i < 10; i++) {
312
+ framework.withBatch(() => {
313
+ heads[i].write(i)
314
+ })
315
+ expect(splited[i].read()).toBe(i + 1)
316
+ }
317
+ for (let i = 0; i < 10; i++) {
318
+ framework.withBatch(() => {
319
+ heads[i].write(i * 2)
320
+ })
321
+ expect(splited[i].read()).toBe(i * 2 + 1)
322
+ }
421
323
  }
422
- return result
423
- })
424
- const callCounter = new Counter()
425
- framework.effect(() => {
426
- current.read()
427
- callCounter.count++
428
324
  })
429
325
 
430
- return () => {
431
- framework.withBatch(() => {
432
- head.write(1)
326
+ test('repeated observers', async () => {
327
+ const size = 30
328
+ const head = framework.signal(0)
329
+ const current = framework.computed(() => {
330
+ let result = 0
331
+ for (let i = 0; i < size; i++) {
332
+ result += head.read()
333
+ }
334
+ return result
433
335
  })
434
- expect(current.read()).toBe(40)
435
- const atleast = 100
436
- callCounter.count = 0
437
- for (let i = 0; i < 100; i++) {
336
+ const callCounter = new Counter()
337
+ framework.effect(() => {
338
+ current.read()
339
+ callCounter.count++
340
+ })
341
+
342
+ return () => {
438
343
  framework.withBatch(() => {
439
- head.write(i)
344
+ head.write(1)
440
345
  })
441
- expect(current.read()).toBe(i % 2 ? i * 2 * 10 : i * -10)
346
+ expect(current.read()).toBe(size)
347
+ const atleast = 100
348
+ callCounter.count = 0
349
+ for (let i = 0; i < 100; i++) {
350
+ framework.withBatch(() => {
351
+ head.write(i)
352
+ })
353
+ expect(current.read()).toBe(i * size)
354
+ }
355
+ expect(callCounter.count).toBe(atleast)
442
356
  }
443
- expect(callCounter.count).toBe(atleast)
444
- }
445
- })
446
- })
447
-
448
- describe('$mol_wire tests', () => {
449
- const name = framework.name
357
+ })
450
358
 
451
- test(`${name} | $mol_wire benchmark`, () => {
452
- // @ts-expect-error test
453
- const fib = (n: number) => {
454
- if (n < 2) return 1
455
- return fib(n - 1) + fib(n - 2)
456
- }
457
- const hard = (n: number, _log: string) => {
458
- return n + fib(16)
459
- }
460
- const numbers = Array.from({ length: 5 }, (_, i) => i)
461
- const res: (() => unknown)[] = []
462
- framework.withBuild(() => {
463
- const A = framework.signal(0)
464
- const B = framework.signal(0)
465
- const C = framework.computed(() => (A.read() % 2) + (B.read() % 2))
466
- const D = framework.computed(() =>
467
- numbers.map(i => ({ x: i + (A.read() % 2) - (B.read() % 2) })),
468
- )
469
- const E = framework.computed(() =>
470
- hard(C.read() + A.read() + D.read()[0].x, 'E'),
471
- )
472
- const F = framework.computed(() =>
473
- hard(D.read()[2].x || B.read(), 'F'),
474
- )
475
- const G = framework.computed(
476
- () =>
477
- C.read() +
478
- (C.read() || E.read() % 2) +
479
- D.read()[4].x +
480
- F.read(),
359
+ test('triangle', async () => {
360
+ const width = 10
361
+ const head = framework.signal(0)
362
+ let current = head as { read: () => number }
363
+ const list: { read: () => number }[] = []
364
+ for (let i = 0; i < width; i++) {
365
+ const c = current
366
+ list.push(current)
367
+ current = framework.computed(() => c.read() + 1)
368
+ }
369
+ const sum = framework.computed(() =>
370
+ list.map(x => x.read()).reduce((a, b) => a + b, 0),
481
371
  )
372
+ const callCounter = new Counter()
482
373
  framework.effect(() => {
483
- res.push(hard(G.read(), 'H'))
484
- })
485
- framework.effect(() => {
486
- res.push(G.read())
487
- }) // I
488
- framework.effect(() => {
489
- res.push(hard(F.read(), 'J'))
374
+ sum.read()
375
+ callCounter.count++
490
376
  })
491
- framework.effect(() => {
492
- res[0] = hard(G.read(), 'H')
377
+
378
+ return () => {
379
+ const count = (number: number) =>
380
+ new Array(number)
381
+ .fill(0)
382
+ .map((_, i) => i + 1)
383
+ .reduce((x, y) => x + y, 0)
384
+ const constant = count(width)
385
+ framework.withBatch(() => {
386
+ head.write(1)
387
+ })
388
+ expect(sum.read()).toBe(constant)
389
+ const atleast = 100
390
+ callCounter.count = 0
391
+ for (let i = 0; i < 100; i++) {
392
+ framework.withBatch(() => {
393
+ head.write(i)
394
+ })
395
+ expect(sum.read()).toBe(constant - width + i * width)
396
+ }
397
+ expect(callCounter.count).toBe(atleast)
398
+ }
399
+ })
400
+
401
+ test('unstable', async () => {
402
+ const head = framework.signal(0)
403
+ const double = framework.computed(() => head.read() * 2)
404
+ const inverse = framework.computed(() => -head.read())
405
+ const current = framework.computed(() => {
406
+ let result = 0
407
+ for (let i = 0; i < 20; i++) {
408
+ result += head.read() % 2 ? double.read() : inverse.read()
409
+ }
410
+ return result
493
411
  })
412
+ const callCounter = new Counter()
494
413
  framework.effect(() => {
495
- res[1] = G.read()
496
- }) // I
497
- framework.effect(() => {
498
- res[2] = hard(F.read(), 'J')
414
+ current.read()
415
+ callCounter.count++
499
416
  })
500
417
 
501
- return (i: number) => {
502
- res.length = 0
503
- framework.withBatch(() => {
504
- B.write(1)
505
- A.write(1 + i * 2)
506
- })
418
+ return () => {
507
419
  framework.withBatch(() => {
508
- A.write(2 + i * 2)
509
- B.write(2)
420
+ head.write(1)
510
421
  })
422
+ expect(current.read()).toBe(40)
423
+ const atleast = 100
424
+ callCounter.count = 0
425
+ for (let i = 0; i < 100; i++) {
426
+ framework.withBatch(() => {
427
+ head.write(i)
428
+ })
429
+ expect(current.read()).toBe(i % 2 ? i * 2 * 10 : i * -10)
430
+ }
431
+ expect(callCounter.count).toBe(atleast)
511
432
  }
512
433
  })
513
-
514
- expect(res.toString()).toBe([3201, 1604, 3196].toString())
515
434
  })
516
- })
517
435
 
518
- describe('CellX tests', () => {
519
- const name = framework.name
520
-
521
- test(`${name} | CellX benchmark`, () => {
522
- const expected = {
523
- 10: [
524
- [3, 6, 2, -2],
525
- [2, 4, -2, -3],
526
- ],
527
- 20: [
528
- [2, 4, -1, -6],
529
- [-2, 1, -4, -4],
530
- ],
531
- 50: [
532
- [-2, -4, 1, 6],
533
- [2, -1, 4, 4],
534
- ],
535
- }
536
-
537
- const cellx = (framework: ReactiveFramework, layers: number) => {
538
- const start = {
539
- prop1: framework.signal(1),
540
- prop2: framework.signal(2),
541
- prop3: framework.signal(3),
542
- prop4: framework.signal(4),
436
+ describe(`$mol_wire tests [${name}]`, () => {
437
+ test('$mol_wire benchmark', () => {
438
+ // @ts-expect-error test
439
+ const fib = (n: number) => {
440
+ if (n < 2) return 1
441
+ return fib(n - 1) + fib(n - 2)
543
442
  }
544
- let layer: Record<string, Computed<number>> = start
545
-
546
- for (let i = layers; i > 0; i--) {
547
- const m = layer
548
- const s = {
549
- prop1: framework.computed(() => m.prop2.read()),
550
- prop2: framework.computed(
551
- () => m.prop1.read() - m.prop3.read(),
552
- ),
553
- prop3: framework.computed(
554
- () => m.prop2.read() + m.prop4.read(),
555
- ),
556
- prop4: framework.computed(() => m.prop3.read()),
557
- }
558
-
443
+ const hard = (n: number, _log: string) => n + fib(16)
444
+ const numbers = Array.from({ length: 5 }, (_, i) => i)
445
+ const res: (() => unknown)[] = []
446
+ framework.withBuild(() => {
447
+ const A = framework.signal(0)
448
+ const B = framework.signal(0)
449
+ const C = framework.computed(
450
+ () => (A.read() % 2) + (B.read() % 2),
451
+ )
452
+ const D = framework.computed(() =>
453
+ numbers.map(i => ({
454
+ x: i + (A.read() % 2) - (B.read() % 2),
455
+ })),
456
+ )
457
+ const E = framework.computed(() =>
458
+ hard(C.read() + A.read() + D.read()[0].x, 'E'),
459
+ )
460
+ const F = framework.computed(() =>
461
+ hard(D.read()[2].x || B.read(), 'F'),
462
+ )
463
+ const G = framework.computed(
464
+ () =>
465
+ C.read() +
466
+ (C.read() || E.read() % 2) +
467
+ D.read()[4].x +
468
+ F.read(),
469
+ )
559
470
  framework.effect(() => {
560
- s.prop1.read()
471
+ res.push(hard(G.read(), 'H'))
561
472
  })
562
473
  framework.effect(() => {
563
- s.prop2.read()
474
+ res.push(G.read())
564
475
  })
565
476
  framework.effect(() => {
566
- s.prop3.read()
477
+ res.push(hard(F.read(), 'J'))
567
478
  })
568
479
  framework.effect(() => {
569
- s.prop4.read()
480
+ res[0] = hard(G.read(), 'H')
570
481
  })
571
-
572
482
  framework.effect(() => {
573
- s.prop1.read()
483
+ res[1] = G.read()
574
484
  })
575
485
  framework.effect(() => {
576
- s.prop2.read()
486
+ res[2] = hard(F.read(), 'J')
577
487
  })
578
- framework.effect(() => {
488
+
489
+ return (i: number) => {
490
+ res.length = 0
491
+ framework.withBatch(() => {
492
+ B.write(1)
493
+ A.write(1 + i * 2)
494
+ })
495
+ framework.withBatch(() => {
496
+ A.write(2 + i * 2)
497
+ B.write(2)
498
+ })
499
+ }
500
+ })
501
+
502
+ expect(res.toString()).toBe([3201, 1604, 3196].toString())
503
+ })
504
+ })
505
+
506
+ describe(`CellX tests [${name}]`, () => {
507
+ test('CellX benchmark', () => {
508
+ const expected = {
509
+ 10: [
510
+ [3, 6, 2, -2],
511
+ [2, 4, -2, -3],
512
+ ],
513
+ 20: [
514
+ [2, 4, -1, -6],
515
+ [-2, 1, -4, -4],
516
+ ],
517
+ 50: [
518
+ [-2, -4, 1, 6],
519
+ [2, -1, 4, 4],
520
+ ],
521
+ }
522
+
523
+ const cellx = (framework: ReactiveFramework, layers: number) => {
524
+ const start = {
525
+ prop1: framework.signal(1),
526
+ prop2: framework.signal(2),
527
+ prop3: framework.signal(3),
528
+ prop4: framework.signal(4),
529
+ }
530
+ let layer: Record<string, Computed<number>> = start
531
+
532
+ for (let i = layers; i > 0; i--) {
533
+ const m = layer
534
+ const s = {
535
+ prop1: framework.computed(() => m.prop2.read()),
536
+ prop2: framework.computed(
537
+ () => m.prop1.read() - m.prop3.read(),
538
+ ),
539
+ prop3: framework.computed(
540
+ () => m.prop2.read() + m.prop4.read(),
541
+ ),
542
+ prop4: framework.computed(() => m.prop3.read()),
543
+ }
544
+
545
+ framework.effect(() => {
546
+ s.prop1.read()
547
+ })
548
+ framework.effect(() => {
549
+ s.prop2.read()
550
+ })
551
+ framework.effect(() => {
552
+ s.prop3.read()
553
+ })
554
+ framework.effect(() => {
555
+ s.prop4.read()
556
+ })
557
+ framework.effect(() => {
558
+ s.prop1.read()
559
+ })
560
+ framework.effect(() => {
561
+ s.prop2.read()
562
+ })
563
+ framework.effect(() => {
564
+ s.prop3.read()
565
+ })
566
+ framework.effect(() => {
567
+ s.prop4.read()
568
+ })
569
+
570
+ s.prop1.read()
571
+ s.prop2.read()
579
572
  s.prop3.read()
580
- })
581
- framework.effect(() => {
582
573
  s.prop4.read()
583
- })
584
574
 
585
- s.prop1.read()
586
- s.prop2.read()
587
- s.prop3.read()
588
- s.prop4.read()
575
+ layer = s
576
+ }
589
577
 
590
- layer = s
591
- }
578
+ const end = layer
592
579
 
593
- const end = layer
580
+ const before = [
581
+ end.prop1.read(),
582
+ end.prop2.read(),
583
+ end.prop3.read(),
584
+ end.prop4.read(),
585
+ ]
594
586
 
595
- const before = [
596
- end.prop1.read(),
597
- end.prop2.read(),
598
- end.prop3.read(),
599
- end.prop4.read(),
600
- ]
587
+ framework.withBatch(() => {
588
+ start.prop1.write(4)
589
+ start.prop2.write(3)
590
+ start.prop3.write(2)
591
+ start.prop4.write(1)
592
+ })
601
593
 
602
- framework.withBatch(() => {
603
- start.prop1.write(4)
604
- start.prop2.write(3)
605
- start.prop3.write(2)
606
- start.prop4.write(1)
607
- })
594
+ const after = [
595
+ end.prop1.read(),
596
+ end.prop2.read(),
597
+ end.prop3.read(),
598
+ end.prop4.read(),
599
+ ]
608
600
 
609
- const after = [
610
- end.prop1.read(),
611
- end.prop2.read(),
612
- end.prop3.read(),
613
- end.prop4.read(),
614
- ]
615
-
616
- return [before, after]
617
- }
618
-
619
- for (const layers in expected) {
620
- // @ts-expect-error - Framework object has incompatible type constraints with ReactiveFramework
621
- const [before, after] = cellx(framework, layers)
622
- // @ts-expect-error - Framework object has incompatible type constraints with ReactiveFramework
623
- const [expectedBefore, expectedAfter] = expected[layers]
624
- expect(before.toString()).toBe(expectedBefore.toString())
625
- expect(after.toString()).toBe(expectedAfter.toString())
626
- }
601
+ return [before, after]
602
+ }
603
+
604
+ for (const layers in expected) {
605
+ // @ts-expect-error - Framework object has incompatible type constraints with ReactiveFramework
606
+ const [before, after] = cellx(framework, layers)
607
+ // @ts-expect-error - Framework object has incompatible type constraints with ReactiveFramework
608
+ const [expectedBefore, expectedAfter] = expected[layers]
609
+ expect(before.toString()).toBe(expectedBefore.toString())
610
+ expect(after.toString()).toBe(expectedAfter.toString())
611
+ }
612
+ })
627
613
  })
628
- })
614
+ }