@zeix/cause-effect 0.14.1 → 0.15.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 (45) hide show
  1. package/README.md +256 -27
  2. package/biome.json +35 -0
  3. package/index.d.ts +32 -7
  4. package/index.dev.js +629 -0
  5. package/index.js +1 -1
  6. package/index.ts +41 -21
  7. package/package.json +6 -7
  8. package/src/computed.ts +30 -21
  9. package/src/diff.ts +136 -0
  10. package/src/effect.ts +59 -49
  11. package/src/match.ts +57 -0
  12. package/src/resolve.ts +58 -0
  13. package/src/scheduler.ts +3 -3
  14. package/src/signal.ts +48 -15
  15. package/src/state.ts +4 -3
  16. package/src/store.ts +325 -0
  17. package/src/util.ts +57 -5
  18. package/test/batch.test.ts +29 -25
  19. package/test/benchmark.test.ts +81 -45
  20. package/test/computed.test.ts +43 -39
  21. package/test/diff.test.ts +638 -0
  22. package/test/effect.test.ts +657 -49
  23. package/test/match.test.ts +378 -0
  24. package/test/resolve.test.ts +156 -0
  25. package/test/state.test.ts +33 -33
  26. package/test/store.test.ts +719 -0
  27. package/test/util/framework-types.ts +2 -2
  28. package/test/util/perf-tests.ts +2 -2
  29. package/test/util/reactive-framework.ts +1 -1
  30. package/tsconfig.json +9 -10
  31. package/types/index.d.ts +15 -0
  32. package/{src → types/src}/computed.d.ts +2 -2
  33. package/types/src/diff.d.ts +27 -0
  34. package/types/src/effect.d.ts +16 -0
  35. package/types/src/match.d.ts +21 -0
  36. package/types/src/resolve.d.ts +29 -0
  37. package/{src → types/src}/scheduler.d.ts +2 -2
  38. package/types/src/signal.d.ts +40 -0
  39. package/{src → types/src}/state.d.ts +1 -1
  40. package/types/src/store.d.ts +57 -0
  41. package/types/src/util.d.ts +15 -0
  42. package/types/test-new-effect.d.ts +1 -0
  43. package/src/effect.d.ts +0 -17
  44. package/src/signal.d.ts +0 -26
  45. package/src/util.d.ts +0 -7
@@ -1,10 +1,7 @@
1
- import { describe, test, expect, mock } from 'bun:test'
2
- import { state, computed, effect, batch } from '../'
3
- import { makeGraph, runGraph, Counter } from './util/dependency-graph'
4
- import {
5
- type ReactiveFramework,
6
- type Computed,
7
- } from './util/reactive-framework'
1
+ import { describe, expect, mock, test } from 'bun:test'
2
+ import { batch, computed, effect, state } from '../'
3
+ import { Counter, makeGraph, runGraph } from './util/dependency-graph'
4
+ import type { Computed, ReactiveFramework } from './util/reactive-framework'
8
5
 
9
6
  /* === Utility Functions === */
10
7
 
@@ -30,8 +27,8 @@ const framework = {
30
27
  read: () => c.get(),
31
28
  }
32
29
  },
33
- effect: (fn: () => void) => effect(fn),
34
- withBatch: (fn: () => void) => batch(fn),
30
+ effect: (fn: () => undefined) => effect(fn),
31
+ withBatch: (fn: () => undefined) => batch(fn),
35
32
  withBuild: <T>(fn: () => T) => fn(),
36
33
  }
37
34
  const testPullCounts = true
@@ -53,7 +50,7 @@ function makeConfig() {
53
50
  /** some basic tests to validate the reactive framework
54
51
  * wrapper works and can run performance tests.
55
52
  */
56
- describe('Basic test', function () {
53
+ describe('Basic test', () => {
57
54
  const name = framework.name
58
55
  test(`${name} | simple dependency executes`, () => {
59
56
  framework.withBuild(() => {
@@ -145,11 +142,11 @@ describe('Basic test', function () {
145
142
  })
146
143
 
147
144
  test(`${name} | effect`, () => {
148
- const spy = _v => {}
145
+ const spy = (_v: number) => {}
149
146
  const spyMock = mock(spy)
150
147
 
151
148
  const s = framework.signal(2)
152
- let c: any
149
+ let c: { read: () => number } = { read: () => 0 }
153
150
 
154
151
  framework.withBuild(() => {
155
152
  c = framework.computed(() => s.read() * 2)
@@ -169,18 +166,22 @@ describe('Basic test', function () {
169
166
  })
170
167
  })
171
168
 
172
- describe('Kairo tests', function () {
169
+ describe('Kairo tests', () => {
173
170
  const name = framework.name
174
171
 
175
- test(`${name} | avoidable propagation`, () => {
172
+ test(`${name} | avoidable propagation`, async () => {
176
173
  const head = framework.signal(0)
177
174
  const computed1 = framework.computed(() => head.read())
178
- const computed2 = framework.computed(() => (computed1.read(), 0))
179
- const computed3 = framework.computed(
180
- () => (busy(), computed2.read()! + 1),
181
- ) // heavy computation
182
- const computed4 = framework.computed(() => computed3.read()! + 2)
183
- const computed5 = framework.computed(() => computed4.read()! + 3)
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)
184
185
  framework.effect(() => {
185
186
  computed5.read()
186
187
  busy() // heavy side effect
@@ -200,16 +201,16 @@ describe('Kairo tests', function () {
200
201
  }
201
202
  })
202
203
 
203
- test(`${name} | broad propagation`, () => {
204
+ test(`${name} | broad propagation`, async () => {
204
205
  const head = framework.signal(0)
205
206
  let last = head as { read: () => number }
206
207
  const callCounter = new Counter()
207
208
  for (let i = 0; i < 50; i++) {
208
209
  const current = framework.computed(() => {
209
- return head.read()! + i
210
+ return head.read() + i
210
211
  })
211
212
  const current2 = framework.computed(() => {
212
- return current.read()! + 1
213
+ return current.read() + 1
213
214
  })
214
215
  framework.effect(() => {
215
216
  current2.read()
@@ -234,7 +235,7 @@ describe('Kairo tests', function () {
234
235
  }
235
236
  })
236
237
 
237
- test(`${name} | deep propagation`, () => {
238
+ test(`${name} | deep propagation`, async () => {
238
239
  const len = 50
239
240
  const head = framework.signal(0)
240
241
  let current = head as { read: () => number }
@@ -267,7 +268,7 @@ describe('Kairo tests', function () {
267
268
  }
268
269
  })
269
270
 
270
- test(`${name} | diamond`, function () {
271
+ test(`${name} | diamond`, async () => {
271
272
  const width = 5
272
273
  const head = framework.signal(0)
273
274
  const current: { read(): number }[] = []
@@ -300,7 +301,7 @@ describe('Kairo tests', function () {
300
301
  }
301
302
  })
302
303
 
303
- test(`${name} | mux`, function () {
304
+ test(`${name} | mux`, async () => {
304
305
  const heads = new Array(100).fill(null).map(_ => framework.signal(0))
305
306
  const mux = framework.computed(() => {
306
307
  return Object.fromEntries(heads.map(h => h.read()).entries())
@@ -310,7 +311,9 @@ describe('Kairo tests', function () {
310
311
  .map(x => framework.computed(() => x.read() + 1))
311
312
 
312
313
  splited.forEach(x => {
313
- framework.effect(() => x.read())
314
+ framework.effect(() => {
315
+ x.read()
316
+ })
314
317
  })
315
318
 
316
319
  return () => {
@@ -329,7 +332,7 @@ describe('Kairo tests', function () {
329
332
  }
330
333
  })
331
334
 
332
- test(`${name} | repeated observers`, function () {
335
+ test(`${name} | repeated observers`, async () => {
333
336
  const size = 30
334
337
  const head = framework.signal(0)
335
338
  const current = framework.computed(() => {
@@ -362,7 +365,7 @@ describe('Kairo tests', function () {
362
365
  }
363
366
  })
364
367
 
365
- test(`${name} | triangle`, function () {
368
+ test(`${name} | triangle`, async () => {
366
369
  const width = 10
367
370
  const head = framework.signal(0)
368
371
  let current = head as { read: () => number }
@@ -407,7 +410,7 @@ describe('Kairo tests', function () {
407
410
  }
408
411
  })
409
412
 
410
- test(`${name} | unstable`, function () {
413
+ test(`${name} | unstable`, async () => {
411
414
  const head = framework.signal(0)
412
415
  const double = framework.computed(() => head.read() * 2)
413
416
  const inverse = framework.computed(() => -head.read())
@@ -442,10 +445,10 @@ describe('Kairo tests', function () {
442
445
  })
443
446
  })
444
447
 
445
- describe('$mol_wire tests', function () {
448
+ describe('$mol_wire tests', () => {
446
449
  const name = framework.name
447
450
 
448
- test(`${name} | $mol_wire benchmark`, function () {
451
+ test(`${name} | $mol_wire benchmark`, () => {
449
452
  const fib = (n: number) => {
450
453
  if (n < 2) return 1
451
454
  return fib(n - 1) + fib(n - 2)
@@ -454,7 +457,7 @@ describe('$mol_wire tests', function () {
454
457
  return n + fib(16)
455
458
  }
456
459
  const numbers = Array.from({ length: 5 }, (_, i) => i)
457
- const res: (() => any)[] = []
460
+ const res: (() => unknown)[] = []
458
461
  framework.withBuild(() => {
459
462
  const A = framework.signal(0)
460
463
  const B = framework.signal(0)
@@ -475,12 +478,24 @@ describe('$mol_wire tests', function () {
475
478
  D.read()[4].x +
476
479
  F.read(),
477
480
  )
478
- framework.effect(() => res.push(hard(G.read(), 'H')))
479
- framework.effect(() => res.push(G.read())) // I
480
- framework.effect(() => res.push(hard(F.read(), 'J')))
481
- framework.effect(() => (res[0] = hard(G.read(), 'H')))
482
- framework.effect(() => (res[1] = G.read())) // I
483
- framework.effect(() => (res[2] = hard(F.read(), 'J')))
481
+ 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'))
489
+ })
490
+ framework.effect(() => {
491
+ res[0] = hard(G.read(), 'H')
492
+ })
493
+ framework.effect(() => {
494
+ res[1] = G.read()
495
+ }) // I
496
+ framework.effect(() => {
497
+ res[2] = hard(F.read(), 'J')
498
+ })
484
499
 
485
500
  return (i: number) => {
486
501
  res.length = 0
@@ -499,10 +514,10 @@ describe('$mol_wire tests', function () {
499
514
  })
500
515
  })
501
516
 
502
- describe('CellX tests', function () {
517
+ describe('CellX tests', () => {
503
518
  const name = framework.name
504
519
 
505
- test(`${name} | CellX benchmark`, function () {
520
+ test(`${name} | CellX benchmark`, () => {
506
521
  const expected = {
507
522
  10: [
508
523
  [3, 6, 2, -2],
@@ -540,10 +555,31 @@ describe('CellX tests', function () {
540
555
  prop4: framework.computed(() => m.prop3.read()),
541
556
  }
542
557
 
543
- framework.effect(() => s.prop1.read())
544
- framework.effect(() => s.prop2.read())
545
- framework.effect(() => s.prop3.read())
546
- framework.effect(() => s.prop4.read())
558
+ framework.effect(() => {
559
+ s.prop1.read()
560
+ })
561
+ framework.effect(() => {
562
+ s.prop2.read()
563
+ })
564
+ framework.effect(() => {
565
+ s.prop3.read()
566
+ })
567
+ framework.effect(() => {
568
+ s.prop4.read()
569
+ })
570
+
571
+ framework.effect(() => {
572
+ s.prop1.read()
573
+ })
574
+ framework.effect(() => {
575
+ s.prop2.read()
576
+ })
577
+ framework.effect(() => {
578
+ s.prop3.read()
579
+ })
580
+ framework.effect(() => {
581
+ s.prop4.read()
582
+ })
547
583
 
548
584
  s.prop1.read()
549
585
  s.prop2.read()
@@ -1,12 +1,14 @@
1
- import { describe, test, expect } from 'bun:test'
1
+ import { describe, expect, test } from 'bun:test'
2
2
  import {
3
- state,
4
3
  computed,
5
- UNSET,
4
+ effect,
6
5
  isComputed,
7
6
  isState,
8
- effect,
9
- } from '../index.ts'
7
+ match,
8
+ resolve,
9
+ state,
10
+ UNSET,
11
+ } from '../'
10
12
 
11
13
  /* === Utility Functions === */
12
14
 
@@ -15,7 +17,7 @@ const increment = (n: number) => (Number.isFinite(n) ? n + 1 : UNSET)
15
17
 
16
18
  /* === Tests === */
17
19
 
18
- describe('Computed', function () {
20
+ describe('Computed', () => {
19
21
  test('should identify computed signals with isComputed()', () => {
20
22
  const count = state(42)
21
23
  const doubled = computed(() => count.get() * 2)
@@ -23,25 +25,25 @@ describe('Computed', function () {
23
25
  expect(isState(doubled)).toBe(false)
24
26
  })
25
27
 
26
- test('should compute a function', function () {
28
+ test('should compute a function', () => {
27
29
  const derived = computed(() => 1 + 2)
28
30
  expect(derived.get()).toBe(3)
29
31
  })
30
32
 
31
- test('should compute function dependent on a signal', function () {
33
+ test('should compute function dependent on a signal', () => {
32
34
  const cause = state(42)
33
35
  const derived = computed(() => cause.get() + 1)
34
36
  expect(derived.get()).toBe(43)
35
37
  })
36
38
 
37
- test('should compute function dependent on an updated signal', function () {
39
+ test('should compute function dependent on an updated signal', () => {
38
40
  const cause = state(42)
39
41
  const derived = computed(() => cause.get() + 1)
40
42
  cause.set(24)
41
43
  expect(derived.get()).toBe(25)
42
44
  })
43
45
 
44
- test('should compute function dependent on an async signal', async function () {
46
+ test('should compute function dependent on an async signal', async () => {
45
47
  const status = state('pending')
46
48
  const promised = computed(async () => {
47
49
  await wait(100)
@@ -56,7 +58,7 @@ describe('Computed', function () {
56
58
  expect(status.get()).toBe('success')
57
59
  })
58
60
 
59
- test('should handle errors from an async signal gracefully', async function () {
61
+ test('should handle errors from an async signal gracefully', async () => {
60
62
  const status = state('pending')
61
63
  const error = state('')
62
64
  const promised = computed(async () => {
@@ -73,7 +75,7 @@ describe('Computed', function () {
73
75
  expect(status.get()).toBe('error')
74
76
  })
75
77
 
76
- test('should compute task signals in parallel without waterfalls', async function () {
78
+ test('should compute task signals in parallel without waterfalls', async () => {
77
79
  const a = computed(async () => {
78
80
  await wait(100)
79
81
  return 10
@@ -94,7 +96,7 @@ describe('Computed', function () {
94
96
  expect(c.get()).toBe(30)
95
97
  })
96
98
 
97
- test('should compute function dependent on a chain of computed states dependent on a signal', function () {
99
+ test('should compute function dependent on a chain of computed states dependent on a signal', () => {
98
100
  const x = state(42)
99
101
  const a = computed(() => x.get() + 1)
100
102
  const b = computed(() => a.get() * 2)
@@ -102,7 +104,7 @@ describe('Computed', function () {
102
104
  expect(c.get()).toBe(87)
103
105
  })
104
106
 
105
- test('should compute function dependent on a chain of computed states dependent on an updated signal', function () {
107
+ test('should compute function dependent on a chain of computed states dependent on an updated signal', () => {
106
108
  const x = state(42)
107
109
  const a = computed(() => x.get() + 1)
108
110
  const b = computed(() => a.get() * 2)
@@ -111,14 +113,14 @@ describe('Computed', function () {
111
113
  expect(c.get()).toBe(51)
112
114
  })
113
115
 
114
- test('should drop X->B->X updates', function () {
116
+ test('should drop X->B->X updates', () => {
115
117
  let count = 0
116
118
  const x = state(2)
117
119
  const a = computed(() => x.get() - 1)
118
120
  const b = computed(() => x.get() + a.get())
119
121
  const c = computed(() => {
120
122
  count++
121
- return 'c: ' + b.get()
123
+ return `c: ${b.get()}`
122
124
  })
123
125
  expect(c.get()).toBe('c: 3')
124
126
  expect(count).toBe(1)
@@ -127,14 +129,14 @@ describe('Computed', function () {
127
129
  expect(count).toBe(2)
128
130
  })
129
131
 
130
- test('should only update every signal once (diamond graph)', function () {
132
+ test('should only update every signal once (diamond graph)', () => {
131
133
  let count = 0
132
134
  const x = state('a')
133
135
  const a = computed(() => x.get())
134
136
  const b = computed(() => x.get())
135
137
  const c = computed(() => {
136
138
  count++
137
- return a.get() + ' ' + b.get()
139
+ return `${a.get()} ${b.get()}`
138
140
  })
139
141
  expect(c.get()).toBe('a a')
140
142
  expect(count).toBe(1)
@@ -144,12 +146,12 @@ describe('Computed', function () {
144
146
  expect(count).toBe(2)
145
147
  })
146
148
 
147
- test('should only update every signal once (diamond graph + tail)', function () {
149
+ test('should only update every signal once (diamond graph + tail)', () => {
148
150
  let count = 0
149
151
  const x = state('a')
150
152
  const a = computed(() => x.get())
151
153
  const b = computed(() => x.get())
152
- const c = computed(() => a.get() + ' ' + b.get())
154
+ const c = computed(() => `${a.get()} ${b.get()}`)
153
155
  const d = computed(() => {
154
156
  count++
155
157
  return c.get()
@@ -161,7 +163,7 @@ describe('Computed', function () {
161
163
  expect(count).toBe(2)
162
164
  })
163
165
 
164
- test('should update multiple times after multiple state changes', function () {
166
+ test('should update multiple times after multiple state changes', () => {
165
167
  const a = state(3)
166
168
  const b = state(4)
167
169
  let count = 0
@@ -185,7 +187,7 @@ describe('Computed', function () {
185
187
  * one-time performance cost that allows for efficient memoization and
186
188
  * error handling in most cases.
187
189
  */
188
- test('should bail out if result is the same', function () {
190
+ test('should bail out if result is the same', () => {
189
191
  let count = 0
190
192
  const x = state('a')
191
193
  const a = computed(() => {
@@ -205,7 +207,7 @@ describe('Computed', function () {
205
207
  expect(count).toBe(2)
206
208
  })
207
209
 
208
- test('should block if result remains unchanged', function () {
210
+ test('should block if result remains unchanged', () => {
209
211
  let count = 0
210
212
  const x = state(42)
211
213
  const a = computed(() => x.get() % 2)
@@ -223,7 +225,7 @@ describe('Computed', function () {
223
225
  expect(count).toBe(2)
224
226
  })
225
227
 
226
- test('should detect and throw error for circular dependencies', function () {
228
+ test('should detect and throw error for circular dependencies', () => {
227
229
  const a = state(1)
228
230
  const b = computed(() => c.get() + 1)
229
231
  const c = computed(() => b.get() + a.get())
@@ -233,7 +235,7 @@ describe('Computed', function () {
233
235
  expect(a.get()).toBe(1)
234
236
  })
235
237
 
236
- test('should propagate error if an error occurred', function () {
238
+ test('should propagate error if an error occurred', () => {
237
239
  let okCount = 0
238
240
  let errCount = 0
239
241
  const x = state(0)
@@ -273,7 +275,7 @@ describe('Computed', function () {
273
275
  }
274
276
  })
275
277
 
276
- test('should create an effect that reacts on async computed changes', async function () {
278
+ test('should create an effect that reacts on async computed changes', async () => {
277
279
  const cause = state(42)
278
280
  const derived = computed(async () => {
279
281
  await wait(100)
@@ -282,15 +284,17 @@ describe('Computed', function () {
282
284
  let okCount = 0
283
285
  let nilCount = 0
284
286
  let result: number = 0
285
- effect({
286
- signals: [derived],
287
- ok: v => {
288
- result = v
289
- okCount++
290
- },
291
- nil: () => {
292
- nilCount++
293
- },
287
+ effect(() => {
288
+ const resolved = resolve({ derived })
289
+ match(resolved, {
290
+ ok: ({ derived: v }) => {
291
+ result = v
292
+ okCount++
293
+ },
294
+ nil: () => {
295
+ nilCount++
296
+ },
297
+ })
294
298
  })
295
299
  cause.set(43)
296
300
  expect(okCount).toBe(0)
@@ -303,7 +307,7 @@ describe('Computed', function () {
303
307
  expect(result).toBe(44)
304
308
  })
305
309
 
306
- test('should handle complex computed signal with error and async dependencies', async function () {
310
+ test('should handle complex computed signal with error and async dependencies', async () => {
307
311
  const toggleState = state(true)
308
312
  const errorProne = computed(() => {
309
313
  if (toggleState.get()) throw new Error('Intentional error')
@@ -350,7 +354,7 @@ describe('Computed', function () {
350
354
  expect(errCount).toBeGreaterThan(0)
351
355
  })
352
356
 
353
- test('should handle signal changes during async computation', async function () {
357
+ test('should handle signal changes during async computation', async () => {
354
358
  const source = state(1)
355
359
  let computationCount = 0
356
360
  const derived = computed(async abort => {
@@ -371,7 +375,7 @@ describe('Computed', function () {
371
375
  expect(computationCount).toBe(1)
372
376
  })
373
377
 
374
- test('should handle multiple rapid changes during async computation', async function () {
378
+ test('should handle multiple rapid changes during async computation', async () => {
375
379
  const source = state(1)
376
380
  let computationCount = 0
377
381
  const derived = computed(async abort => {
@@ -396,7 +400,7 @@ describe('Computed', function () {
396
400
  expect(computationCount).toBe(1)
397
401
  })
398
402
 
399
- test('should handle errors in aborted computations', async function () {
403
+ test('should handle errors in aborted computations', async () => {
400
404
  const source = state(1)
401
405
  const derived = computed(async () => {
402
406
  await wait(100)