@zeix/cause-effect 0.17.2 → 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 (94) hide show
  1. package/.ai-context.md +163 -226
  2. package/.cursorrules +41 -35
  3. package/.github/copilot-instructions.md +166 -116
  4. package/.zed/settings.json +3 -0
  5. package/ARCHITECTURE.md +274 -0
  6. package/CLAUDE.md +197 -202
  7. package/COLLECTION_REFACTORING.md +161 -0
  8. package/GUIDE.md +298 -0
  9. package/README.md +241 -220
  10. package/REQUIREMENTS.md +100 -0
  11. package/bench/reactivity.bench.ts +577 -0
  12. package/index.dev.js +1326 -1174
  13. package/index.js +1 -1
  14. package/index.ts +58 -85
  15. package/package.json +9 -6
  16. package/src/errors.ts +118 -70
  17. package/src/graph.ts +601 -0
  18. package/src/nodes/collection.ts +474 -0
  19. package/src/nodes/effect.ts +149 -0
  20. package/src/nodes/list.ts +588 -0
  21. package/src/nodes/memo.ts +120 -0
  22. package/src/nodes/sensor.ts +139 -0
  23. package/src/nodes/state.ts +135 -0
  24. package/src/nodes/store.ts +383 -0
  25. package/src/nodes/task.ts +146 -0
  26. package/src/signal.ts +112 -64
  27. package/src/util.ts +26 -57
  28. package/test/batch.test.ts +96 -69
  29. package/test/benchmark.test.ts +473 -485
  30. package/test/collection.test.ts +455 -955
  31. package/test/effect.test.ts +293 -696
  32. package/test/list.test.ts +332 -857
  33. package/test/memo.test.ts +380 -0
  34. package/test/regression.test.ts +156 -0
  35. package/test/scope.test.ts +191 -0
  36. package/test/sensor.test.ts +454 -0
  37. package/test/signal.test.ts +220 -213
  38. package/test/state.test.ts +217 -271
  39. package/test/store.test.ts +346 -898
  40. package/test/task.test.ts +395 -0
  41. package/test/untrack.test.ts +167 -0
  42. package/test/util/dependency-graph.ts +2 -2
  43. package/tsconfig.build.json +11 -0
  44. package/tsconfig.json +5 -7
  45. package/types/index.d.ts +13 -15
  46. package/types/src/errors.d.ts +73 -19
  47. package/types/src/graph.d.ts +208 -0
  48. package/types/src/nodes/collection.d.ts +64 -0
  49. package/types/src/nodes/effect.d.ts +48 -0
  50. package/types/src/nodes/list.d.ts +65 -0
  51. package/types/src/nodes/memo.d.ts +57 -0
  52. package/types/src/nodes/sensor.d.ts +75 -0
  53. package/types/src/nodes/state.d.ts +78 -0
  54. package/types/src/nodes/store.d.ts +51 -0
  55. package/types/src/nodes/task.d.ts +73 -0
  56. package/types/src/signal.d.ts +43 -28
  57. package/types/src/util.d.ts +9 -16
  58. package/archive/benchmark.ts +0 -688
  59. package/archive/collection.ts +0 -310
  60. package/archive/computed.ts +0 -198
  61. package/archive/list.ts +0 -544
  62. package/archive/memo.ts +0 -140
  63. package/archive/state.ts +0 -90
  64. package/archive/store.ts +0 -357
  65. package/archive/task.ts +0 -191
  66. package/src/classes/collection.ts +0 -298
  67. package/src/classes/composite.ts +0 -171
  68. package/src/classes/computed.ts +0 -392
  69. package/src/classes/list.ts +0 -310
  70. package/src/classes/ref.ts +0 -96
  71. package/src/classes/state.ts +0 -131
  72. package/src/classes/store.ts +0 -227
  73. package/src/diff.ts +0 -138
  74. package/src/effect.ts +0 -96
  75. package/src/match.ts +0 -45
  76. package/src/resolve.ts +0 -49
  77. package/src/system.ts +0 -275
  78. package/test/computed.test.ts +0 -1126
  79. package/test/diff.test.ts +0 -955
  80. package/test/match.test.ts +0 -388
  81. package/test/ref.test.ts +0 -381
  82. package/test/resolve.test.ts +0 -154
  83. package/types/src/classes/collection.d.ts +0 -47
  84. package/types/src/classes/composite.d.ts +0 -15
  85. package/types/src/classes/computed.d.ts +0 -114
  86. package/types/src/classes/list.d.ts +0 -41
  87. package/types/src/classes/ref.d.ts +0 -48
  88. package/types/src/classes/state.d.ts +0 -61
  89. package/types/src/classes/store.d.ts +0 -51
  90. package/types/src/diff.d.ts +0 -28
  91. package/types/src/effect.d.ts +0 -15
  92. package/types/src/match.d.ts +0 -21
  93. package/types/src/resolve.d.ts +0 -29
  94. package/types/src/system.d.ts +0 -81
@@ -1,5 +1,10 @@
1
1
  import { describe, expect, mock, test } from 'bun:test'
2
- import { batchSignalWrites, 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) => batchSignalWrites(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,582 +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
- const fib = (n: number) => {
453
- if (n < 2) return 1
454
- return fib(n - 1) + fib(n - 2)
455
- }
456
- const hard = (n: number, _log: string) => {
457
- return n + fib(16)
458
- }
459
- const numbers = Array.from({ length: 5 }, (_, i) => i)
460
- const res: (() => unknown)[] = []
461
- framework.withBuild(() => {
462
- const A = framework.signal(0)
463
- const B = framework.signal(0)
464
- const C = framework.computed(() => (A.read() % 2) + (B.read() % 2))
465
- const D = framework.computed(() =>
466
- numbers.map(i => ({ x: i + (A.read() % 2) - (B.read() % 2) })),
467
- )
468
- const E = framework.computed(() =>
469
- hard(C.read() + A.read() + D.read()[0].x, 'E'),
470
- )
471
- const F = framework.computed(() =>
472
- hard(D.read()[2].x || B.read(), 'F'),
473
- )
474
- const G = framework.computed(
475
- () =>
476
- C.read() +
477
- (C.read() || E.read() % 2) +
478
- D.read()[4].x +
479
- 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),
480
371
  )
372
+ const callCounter = new Counter()
481
373
  framework.effect(() => {
482
- res.push(hard(G.read(), 'H'))
483
- })
484
- framework.effect(() => {
485
- res.push(G.read())
486
- }) // I
487
- framework.effect(() => {
488
- res.push(hard(F.read(), 'J'))
374
+ sum.read()
375
+ callCounter.count++
489
376
  })
490
- framework.effect(() => {
491
- 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
492
411
  })
412
+ const callCounter = new Counter()
493
413
  framework.effect(() => {
494
- res[1] = G.read()
495
- }) // I
496
- framework.effect(() => {
497
- res[2] = hard(F.read(), 'J')
414
+ current.read()
415
+ callCounter.count++
498
416
  })
499
417
 
500
- return (i: number) => {
501
- res.length = 0
502
- framework.withBatch(() => {
503
- B.write(1)
504
- A.write(1 + i * 2)
505
- })
418
+ return () => {
506
419
  framework.withBatch(() => {
507
- A.write(2 + i * 2)
508
- B.write(2)
420
+ head.write(1)
509
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)
510
432
  }
511
433
  })
512
-
513
- expect(res.toString()).toBe([3201, 1604, 3196].toString())
514
434
  })
515
- })
516
435
 
517
- describe('CellX tests', () => {
518
- const name = framework.name
519
-
520
- test(`${name} | CellX benchmark`, () => {
521
- const expected = {
522
- 10: [
523
- [3, 6, 2, -2],
524
- [2, 4, -2, -3],
525
- ],
526
- 20: [
527
- [2, 4, -1, -6],
528
- [-2, 1, -4, -4],
529
- ],
530
- 50: [
531
- [-2, -4, 1, 6],
532
- [2, -1, 4, 4],
533
- ],
534
- }
535
-
536
- const cellx = (framework: ReactiveFramework, layers: number) => {
537
- const start = {
538
- prop1: framework.signal(1),
539
- prop2: framework.signal(2),
540
- prop3: framework.signal(3),
541
- 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)
542
442
  }
543
- let layer: Record<string, Computed<number>> = start
544
-
545
- for (let i = layers; i > 0; i--) {
546
- const m = layer
547
- const s = {
548
- prop1: framework.computed(() => m.prop2.read()),
549
- prop2: framework.computed(
550
- () => m.prop1.read() - m.prop3.read(),
551
- ),
552
- prop3: framework.computed(
553
- () => m.prop2.read() + m.prop4.read(),
554
- ),
555
- prop4: framework.computed(() => m.prop3.read()),
556
- }
557
-
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
+ )
558
470
  framework.effect(() => {
559
- s.prop1.read()
471
+ res.push(hard(G.read(), 'H'))
560
472
  })
561
473
  framework.effect(() => {
562
- s.prop2.read()
474
+ res.push(G.read())
563
475
  })
564
476
  framework.effect(() => {
565
- s.prop3.read()
477
+ res.push(hard(F.read(), 'J'))
566
478
  })
567
479
  framework.effect(() => {
568
- s.prop4.read()
480
+ res[0] = hard(G.read(), 'H')
569
481
  })
570
-
571
482
  framework.effect(() => {
572
- s.prop1.read()
483
+ res[1] = G.read()
573
484
  })
574
485
  framework.effect(() => {
575
- s.prop2.read()
486
+ res[2] = hard(F.read(), 'J')
576
487
  })
577
- 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()
578
572
  s.prop3.read()
579
- })
580
- framework.effect(() => {
581
573
  s.prop4.read()
582
- })
583
574
 
584
- s.prop1.read()
585
- s.prop2.read()
586
- s.prop3.read()
587
- s.prop4.read()
575
+ layer = s
576
+ }
588
577
 
589
- layer = s
590
- }
578
+ const end = layer
591
579
 
592
- const end = layer
580
+ const before = [
581
+ end.prop1.read(),
582
+ end.prop2.read(),
583
+ end.prop3.read(),
584
+ end.prop4.read(),
585
+ ]
593
586
 
594
- const before = [
595
- end.prop1.read(),
596
- end.prop2.read(),
597
- end.prop3.read(),
598
- end.prop4.read(),
599
- ]
587
+ framework.withBatch(() => {
588
+ start.prop1.write(4)
589
+ start.prop2.write(3)
590
+ start.prop3.write(2)
591
+ start.prop4.write(1)
592
+ })
600
593
 
601
- framework.withBatch(() => {
602
- start.prop1.write(4)
603
- start.prop2.write(3)
604
- start.prop3.write(2)
605
- start.prop4.write(1)
606
- })
594
+ const after = [
595
+ end.prop1.read(),
596
+ end.prop2.read(),
597
+ end.prop3.read(),
598
+ end.prop4.read(),
599
+ ]
607
600
 
608
- const after = [
609
- end.prop1.read(),
610
- end.prop2.read(),
611
- end.prop3.read(),
612
- end.prop4.read(),
613
- ]
614
-
615
- return [before, after]
616
- }
617
-
618
- for (const layers in expected) {
619
- // @ts-expect-error - Framework object has incompatible type constraints with ReactiveFramework
620
- const [before, after] = cellx(framework, layers)
621
- const [expectedBefore, expectedAfter] = expected[layers]
622
- expect(before.toString()).toBe(expectedBefore.toString())
623
- expect(after.toString()).toBe(expectedAfter.toString())
624
- }
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
+ })
625
613
  })
626
- })
614
+ }