@zeix/cause-effect 0.12.3 → 0.13.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.
@@ -1,168 +1,172 @@
1
1
  import { describe, test, expect } from 'bun:test'
2
- import { state, computed, UNSET, isComputed } from '../'
2
+ import { state, computed, UNSET, isComputed, isState } from '../index.ts'
3
3
 
4
4
  /* === Utility Functions === */
5
5
 
6
6
  const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
7
- const increment = (n: number) => Number.isFinite(n) ? n + 1 : UNSET;
7
+ const increment = (n: number) => Number.isFinite(n) ? n + 1 : UNSET
8
8
 
9
9
  /* === Tests === */
10
10
 
11
11
  describe('Computed', function () {
12
12
 
13
+ test('should identify computed signals with isComputed()', () => {
14
+ const count = state(42)
15
+ const doubled = count.map(v => v * 2)
16
+ expect(isComputed(doubled)).toBe(true)
17
+ expect(isState(doubled)).toBe(false)
18
+ })
19
+
13
20
  test('should compute a function', function() {
14
- const derived = computed(() => 1 + 2);
15
- expect(derived.get()).toBe(3);
16
- });
21
+ const derived = computed(() => 1 + 2)
22
+ expect(derived.get()).toBe(3)
23
+ })
17
24
 
18
25
  test('should compute function dependent on a signal', function() {
19
- const derived = state(42).map(v => ++v);
20
- expect(derived.get()).toBe(43);
21
- });
26
+ const derived = state(42).map(v => ++v)
27
+ expect(derived.get()).toBe(43)
28
+ })
22
29
 
23
30
  test('should compute function dependent on an updated signal', function() {
24
- const cause = state(42);
25
- const derived = cause.map(v => ++v);
26
- cause.set(24);
27
- expect(derived.get()).toBe(25);
28
- });
31
+ const cause = state(42)
32
+ const derived = cause.map(v => ++v)
33
+ cause.set(24)
34
+ expect(derived.get()).toBe(25)
35
+ })
29
36
 
30
37
  test('should compute function dependent on an async signal', async function() {
31
- const status = state('pending');
38
+ const status = state('pending')
32
39
  const promised = computed(async () => {
33
- await wait(100);
34
- status.set('success');
35
- return 42;
36
- });
37
- const derived = promised.map(increment);
38
- expect(derived.get()).toBe(UNSET);
39
- expect(status.get()).toBe('pending');
40
- await wait(110);
41
- expect(derived.get()).toBe(43);
42
- expect(status.get()).toBe('success');
43
- });
40
+ await wait(100)
41
+ status.set('success')
42
+ return 42
43
+ })
44
+ const derived = promised.map(increment)
45
+ expect(derived.get()).toBe(UNSET)
46
+ expect(status.get()).toBe('pending')
47
+ await wait(110)
48
+ expect(derived.get()).toBe(43)
49
+ expect(status.get()).toBe('success')
50
+ })
44
51
 
45
52
  test('should handle errors from an async signal gracefully', async function() {
46
- const status = state('pending');
47
- const error = state('');
53
+ const status = state('pending')
54
+ const error = state('')
48
55
  const promised = computed(async () => {
49
- await wait(100);
50
- status.set('error');
51
- error.set('error occurred');
56
+ await wait(100)
57
+ status.set('error')
58
+ error.set('error occurred')
52
59
  return 0
53
- });
54
- const derived = promised.map(increment);
55
- expect(derived.get()).toBe(UNSET);
56
- expect(status.get()).toBe('pending');
57
- await wait(110);
58
- expect(error.get()).toBe('error occurred');
59
- expect(status.get()).toBe('error');
60
- });
60
+ })
61
+ const derived = promised.map(increment)
62
+ expect(derived.get()).toBe(UNSET)
63
+ expect(status.get()).toBe('pending')
64
+ await wait(110)
65
+ expect(error.get()).toBe('error occurred')
66
+ expect(status.get()).toBe('error')
67
+ })
61
68
 
62
69
  test('should compute async signals in parallel without waterfalls', async function() {
63
70
  const a = computed(async () => {
64
- await wait(100);
65
- return 10;
66
- });
71
+ await wait(100)
72
+ return 10
73
+ })
67
74
  const b = computed(async () => {
68
- await wait(100);
69
- return 20;
70
- });
71
- const c = computed(() => {
72
- const aValue = a.get();
73
- const bValue = b.get();
74
- return (aValue === UNSET || bValue === UNSET)
75
- ? UNSET
76
- : aValue + bValue;
77
- });
78
- expect(c.get()).toBe(UNSET);
79
- await wait(110);
80
- expect(c.get()).toBe(30);
81
- });
75
+ await wait(100)
76
+ return 20
77
+ })
78
+ const c = computed({
79
+ signals: [a, b],
80
+ ok: (aValue, bValue) => aValue + bValue
81
+ })
82
+ expect(c.get()).toBe(UNSET)
83
+ await wait(110)
84
+ expect(c.get()).toBe(30)
85
+ })
82
86
 
83
87
  test('should compute function dependent on a chain of computed states dependent on a signal', function() {
84
88
  const derived = state(42)
85
89
  .map(v => ++v)
86
90
  .map(v => v * 2)
87
- .map(v => ++v);
88
- expect(derived.get()).toBe(87);
89
- });
91
+ .map(v => ++v)
92
+ expect(derived.get()).toBe(87)
93
+ })
90
94
 
91
95
  test('should compute function dependent on a chain of computed states dependent on an updated signal', function() {
92
- const cause = state(42);
96
+ const cause = state(42)
93
97
  const derived = cause
94
98
  .map(v => ++v)
95
99
  .map(v => v * 2)
96
- .map(v => ++v);
97
- cause.set(24);
98
- expect(derived.get()).toBe(51);
99
- });
100
+ .map(v => ++v)
101
+ cause.set(24)
102
+ expect(derived.get()).toBe(51)
103
+ })
100
104
 
101
105
  test('should drop X->B->X updates', function () {
102
- let count = 0;
103
- const x = state(2);
104
- const a = x.map(v => --v);
105
- const b = computed(() => x.get() + a.get());
106
- const c = computed(() => {
107
- count++;
108
- return 'c: ' + b.get();
109
- });
110
- expect(c.get()).toBe('c: 3');
111
- expect(count).toBe(1);
112
- x.set(4);
113
- expect(c.get()).toBe('c: 7');
114
- expect(count).toBe(2);
115
- });
106
+ let count = 0
107
+ const x = state(2)
108
+ const a = x.map(v => --v)
109
+ const b = computed(() => x.get() + a.get())
110
+ const c = b.map(v => {
111
+ count++
112
+ return 'c: ' + v
113
+ })
114
+ expect(c.get()).toBe('c: 3')
115
+ expect(count).toBe(1)
116
+ x.set(4)
117
+ expect(c.get()).toBe('c: 7')
118
+ expect(count).toBe(2)
119
+ })
116
120
 
117
121
  test('should only update every signal once (diamond graph)', function() {
118
- let count = 0;
119
- const x = state('a');
120
- const a = x.map(v => v);
121
- const b = x.map(v => v);
122
+ let count = 0
123
+ const x = state('a')
124
+ const a = x.map(v => v)
125
+ const b = x.map(v => v)
122
126
  const c = computed(() => {
123
- count++;
124
- return a.get() + ' ' + b.get();
125
- });
126
- expect(c.get()).toBe('a a');
127
- expect(count).toBe(1);
128
- x.set('aa');
129
- // flush();
130
- expect(c.get()).toBe('aa aa');
131
- expect(count).toBe(2);
132
- });
127
+ count++
128
+ return a.get() + ' ' + b.get()
129
+ })
130
+ expect(c.get()).toBe('a a')
131
+ expect(count).toBe(1)
132
+ x.set('aa')
133
+ // flush()
134
+ expect(c.get()).toBe('aa aa')
135
+ expect(count).toBe(2)
136
+ })
133
137
 
134
138
  test('should only update every signal once (diamond graph + tail)', function() {
135
- let count = 0;
136
- const x = state('a');
137
- const a = x.map(v => v);
138
- const b = x.map(v => v);
139
- const c = computed(() => a.get() + ' ' + b.get());
140
- const d = computed(() => {
141
- count++;
142
- return c.get();
143
- });
144
- expect(d.get()).toBe('a a');
145
- expect(count).toBe(1);
146
- x.set('aa');
147
- expect(d.get()).toBe('aa aa');
148
- expect(count).toBe(2);
149
- });
139
+ let count = 0
140
+ const x = state('a')
141
+ const a = x.map(v => v)
142
+ const b = x.map(v => v)
143
+ const c = computed(() => a.get() + ' ' + b.get())
144
+ const d = c.map(v => {
145
+ count++
146
+ return v
147
+ })
148
+ expect(d.get()).toBe('a a')
149
+ expect(count).toBe(1)
150
+ x.set('aa')
151
+ expect(d.get()).toBe('aa aa')
152
+ expect(count).toBe(2)
153
+ })
150
154
 
151
155
  test('should update multiple times after multiple state changes', function() {
152
- const a = state(3);
153
- const b = state(4);
154
- let count = 0;
156
+ const a = state(3)
157
+ const b = state(4)
158
+ let count = 0
155
159
  const sum = computed(() => {
156
- count++;
160
+ count++
157
161
  return a.get() + b.get()
158
- });
159
- expect(sum.get()).toBe(7);
160
- a.set(6);
161
- expect(sum.get()).toBe(10);
162
- b.set(8);
163
- expect(sum.get()).toBe(14);
164
- expect(count).toBe(3);
165
- });
162
+ })
163
+ expect(sum.get()).toBe(7)
164
+ a.set(6)
165
+ expect(sum.get()).toBe(10)
166
+ b.set(8)
167
+ expect(sum.get()).toBe(14)
168
+ expect(count).toBe(3)
169
+ })
166
170
 
167
171
  /*
168
172
  * Note for the next two tests:
@@ -173,166 +177,261 @@ describe('Computed', function () {
173
177
  * error handling in most cases.
174
178
  */
175
179
  test('should bail out if result is the same', function() {
176
- let count = 0;
177
- const x = state('a');
178
- const a = computed(() => {
179
- x.get();
180
- return 'foo';
181
- });
182
- const b = computed(() => {
183
- count++;
184
- return a.get();
185
- });
186
- expect(b.get()).toBe('foo');
187
- expect(count).toBe(1);
188
- x.set('aa');
189
- x.set('aaa');
190
- x.set('aaaa');
191
- expect(b.get()).toBe('foo');
192
- expect(count).toBe(2);
193
- });
180
+ let count = 0
181
+ const x = state('a')
182
+ const b = x.map(() => 'foo').map(v => {
183
+ count++
184
+ return v
185
+ })
186
+ expect(b.get()).toBe('foo')
187
+ expect(count).toBe(1)
188
+ x.set('aa')
189
+ x.set('aaa')
190
+ x.set('aaaa')
191
+ expect(b.get()).toBe('foo')
192
+ expect(count).toBe(2)
193
+ })
194
194
 
195
195
  test('should block if result remains unchanged', function() {
196
- let count = 0;
197
- const x = state(42);
198
- const a = x.map(v => v % 2);
199
- const b = computed(() => a.get() ? 'odd' : 'even');
200
- const c = computed(() => {
201
- count++;
202
- return `c: ${b.get()}`;
203
- });
204
- expect(c.get()).toBe('c: even');
205
- expect(count).toBe(1);
206
- x.set(44);
207
- x.set(46);
208
- x.set(48);
209
- expect(c.get()).toBe('c: even');
210
- expect(count).toBe(2);
211
- });
196
+ let count = 0
197
+ const x = state(42)
198
+ const c = x.map(v => v % 2)
199
+ .map(v => v ? 'odd' : 'even')
200
+ .map(v => {
201
+ count++
202
+ return `c: ${v}`
203
+ })
204
+ expect(c.get()).toBe('c: even')
205
+ expect(count).toBe(1)
206
+ x.set(44)
207
+ x.set(46)
208
+ x.set(48)
209
+ expect(c.get()).toBe('c: even')
210
+ expect(count).toBe(2)
211
+ })
212
212
 
213
213
  test('should detect and throw error for circular dependencies', function() {
214
- const a = state(1);
215
- const b = computed(() => c.get() + 1);
216
- const c = computed(() => b.get() + a.get());
214
+ const a = state(1)
215
+ const b = computed(() => c.get() + 1)
216
+ const c = computed(() => b.get() + a.get())
217
217
  expect(() => {
218
- b.get(); // This should trigger the circular dependency
219
- }).toThrow('Circular dependency in computed detected');
220
- expect(a.get()).toBe(1);
221
- });
218
+ b.get() // This should trigger the circular dependency
219
+ }).toThrow('Circular dependency in computed detected')
220
+ expect(a.get()).toBe(1)
221
+ })
222
222
 
223
223
  test('should propagate error if an error occurred', function() {
224
- let okCount = 0;
225
- let errCount = 0;
226
- const x = state(0);
224
+ let okCount = 0
225
+ let errCount = 0
226
+ const x = state(0)
227
227
  const a = x.map(v => {
228
- if (v === 1) throw new Error('Calculation error');
229
- return 1;
230
- });
231
- const b = a.map({
228
+ if (v === 1) throw new Error('Calculation error')
229
+ return 1
230
+ })
231
+ const c = computed({
232
+ signals: [a],
232
233
  ok: v => v ? 'success' : 'failure',
233
- err: _e => {
234
- errCount++;
235
- // console.error(e);
236
- return `recovered`;
237
- }
238
- });
239
- const c = b.map(v => {
240
- okCount++;
241
- return `c: ${v}`;
242
- });
243
- expect(a.get()).toBe(1);
244
- expect(c.get()).toBe('c: success');
245
- expect(okCount).toBe(1);
234
+ err: () => {
235
+ errCount++
236
+ return 'recovered'
237
+ },
238
+ }).map(v => {
239
+ okCount++
240
+ return `c: ${v}`
241
+ })
242
+ expect(a.get()).toBe(1)
243
+ expect(c.get()).toBe('c: success')
244
+ expect(okCount).toBe(1)
246
245
  try {
247
246
  x.set(1)
248
- expect(a.get()).toBe(1);
247
+ expect(a.get()).toBe(1)
249
248
  expect(true).toBe(false); // This line should not be reached
250
249
  } catch (error) {
251
- expect(error.message).toBe('Calculation error');
250
+ expect(error.message).toBe('Calculation error')
252
251
  } finally {
253
- expect(c.get()).toBe('c: recovered');
254
- expect(okCount).toBe(2);
255
- expect(errCount).toBe(1);
252
+ expect(c.get()).toBe('c: recovered')
253
+ expect(okCount).toBe(2)
254
+ expect(errCount).toBe(1)
256
255
  }
257
- });
256
+ })
258
257
 
259
258
  test('should return a computed signal with .map()', function() {
260
- const cause = state(42);
261
- const derived = cause.map(v => ++v);
262
- const double = derived.map(v => v * 2);
263
- expect(isComputed(double)).toBe(true);
264
- expect(double.get()).toBe(86);
265
- });
266
-
267
- test('should create an effect that reacts on signal changes with .match()', async function() {
268
- const cause = state(42);
259
+ const cause = state(42)
260
+ const derived = cause.map(v => ++v)
261
+ const double = derived.map(v => v * 2)
262
+ expect(isComputed(double)).toBe(true)
263
+ expect(double.get()).toBe(86)
264
+ })
265
+
266
+ test('should create an effect that reacts on async computed changes with .tap()', async function() {
267
+ const cause = state(42)
269
268
  const derived = computed(async () => {
270
- await wait(100);
271
- return cause.get() + 1;
272
- });
273
- let okCount = 0;
274
- let nilCount = 0;
275
- let result: number = 0;
276
- derived.match({
269
+ await wait(100)
270
+ return cause.get() + 1
271
+ })
272
+ let okCount = 0
273
+ let nilCount = 0
274
+ let result: number = 0
275
+ derived.tap({
277
276
  ok: v => {
278
- result = v;
277
+ result = v
279
278
  okCount++
280
279
  },
281
280
  nil: () => {
282
281
  nilCount++
283
282
  }
284
283
  })
285
- cause.set(43);
286
- expect(okCount).toBe(0);
287
- expect(nilCount).toBe(1);
288
- expect(result).toBe(0);
284
+ cause.set(43)
285
+ expect(okCount).toBe(0)
286
+ expect(nilCount).toBe(1)
287
+ expect(result).toBe(0)
289
288
 
290
- await wait(110);
291
- expect(okCount).toBe(1); // not +1 because initial state never made it here
292
- expect(nilCount).toBe(1);
293
- expect(result).toBe(44);
294
- });
289
+ await wait(110)
290
+ expect(okCount).toBe(1) // not +1 because initial state never made it here
291
+ expect(nilCount).toBe(1)
292
+ expect(result).toBe(44)
293
+ })
295
294
 
296
295
  test('should handle complex computed signal with error and async dependencies', async function() {
297
- const toggleState = state(true);
296
+ const toggleState = state(true)
298
297
  const errorProne = toggleState.map(v => {
299
- if (v) throw new Error('Intentional error');
300
- return 42;
301
- });
298
+ if (v) throw new Error('Intentional error')
299
+ return 42
300
+ })
302
301
  const asyncValue = computed(async () => {
303
- await wait(50);
304
- return 10;
305
- });
306
- let okCount = 0;
307
- let nilCount = 0;
308
- let errCount = 0;
309
- let result: number = 0;
302
+ await wait(50)
303
+ return 10
304
+ })
305
+ let okCount = 0
306
+ let nilCount = 0
307
+ let errCount = 0
308
+ let result: number = 0
310
309
  const complexComputed = computed({
311
- ok: (x, y) => { // happy path
312
- okCount++;
313
- return x + y
310
+ signals: [errorProne, asyncValue],
311
+ ok: v => {
312
+ okCount++
313
+ return v
314
314
  },
315
- nil: () => { // not ready yet
316
- nilCount++;
315
+ nil: () => {
316
+ nilCount++
317
317
  return 0
318
318
  },
319
- err: (_e) => { // error path
320
- // console.error('Error:', _e);
321
- errCount++;
319
+ err: () => {
320
+ errCount++
322
321
  return -1
323
- },
324
- }, errorProne, asyncValue);
322
+ }
323
+ })
324
+
325
+ /* computed(() => {
326
+ try {
327
+ const x = errorProne.get()
328
+ const y = asyncValue.get()
329
+ if (y === UNSET) { // not ready yet
330
+ nilCount++
331
+ return 0
332
+ } else { // happy path
333
+ okCount++
334
+ return x + y
335
+ }
336
+ } catch (error) { // error path
337
+ errCount++
338
+ return -1
339
+ }
340
+ }) */
325
341
 
326
342
  for (let i = 0; i < 10; i++) {
327
- toggleState.set(!!(i % 2));
328
- await wait(10);
329
- result = complexComputed.get();
330
- // console.log(`i: ${i}, result: ${result}`);
343
+ toggleState.set(!!(i % 2))
344
+ await wait(10)
345
+ result = complexComputed.get()
346
+ // console.log(`i: ${i}, result: ${result}`)
331
347
  }
332
348
 
333
- expect(nilCount).toBeGreaterThanOrEqual(4);
334
- expect(okCount).toBeGreaterThanOrEqual(2);
335
- expect(errCount).toBeGreaterThanOrEqual(2);
336
- expect(okCount + errCount + nilCount).toBe(10);
337
- });
338
- });
349
+ expect(nilCount).toBeGreaterThanOrEqual(5)
350
+ expect(okCount).toBeGreaterThanOrEqual(2)
351
+ expect(errCount).toBeGreaterThanOrEqual(3)
352
+ expect(okCount + errCount + nilCount).toBe(10)
353
+ })
354
+
355
+ test('should handle signal changes during async computation', async function() {
356
+ const source = state(1)
357
+ let computationCount = 0
358
+ const derived = computed(async abort => {
359
+ computationCount++
360
+ expect(abort?.aborted).toBe(false)
361
+ await wait(100)
362
+ return source.get()
363
+ })
364
+
365
+ // Start first computation
366
+ expect(derived.get()).toBe(UNSET)
367
+ expect(computationCount).toBe(1)
368
+
369
+ // Change source before first computation completes
370
+ source.set(2)
371
+ await wait(210)
372
+ expect(derived.get()).toBe(2)
373
+ expect(computationCount).toBe(1)
374
+ })
375
+
376
+ test('should handle multiple rapid changes during async computation', async function() {
377
+ const source = state(1)
378
+ let computationCount = 0
379
+ const derived = computed(async abort => {
380
+ computationCount++
381
+ expect(abort?.aborted).toBe(false)
382
+ await wait(100)
383
+ return source.get()
384
+ })
385
+
386
+ // Start first computation
387
+ expect(derived.get()).toBe(UNSET)
388
+ expect(computationCount).toBe(1)
389
+
390
+ // Make multiple rapid changes
391
+ source.set(2)
392
+ source.set(3)
393
+ source.set(4)
394
+ await wait(210)
395
+
396
+ // Should have computed twice (initial + final change)
397
+ expect(derived.get()).toBe(4)
398
+ expect(computationCount).toBe(1)
399
+ })
400
+
401
+ test('should handle errors in aborted computations', async function() {
402
+ // const startTime = performance.now()
403
+ const source = state(1)
404
+ const derived = computed(async () => {
405
+ await wait(100)
406
+ const value = source.get()
407
+ if (value === 2) throw new Error('Intentional error')
408
+ return value
409
+ })
410
+
411
+ /* derived.tap({
412
+ ok: v => {
413
+ console.log(`ok: ${v}, time: ${performance.now() - startTime}ms`)
414
+ },
415
+ nil: () => {
416
+ console.warn(`nil, time: ${performance.now() - startTime}ms`)
417
+ },
418
+ err: e => {
419
+ console.error(`err: ${e.message}, time: ${performance.now() - startTime}ms`)
420
+ }
421
+ }) */
422
+
423
+ // Start first computation
424
+ expect(derived.get()).toBe(UNSET)
425
+
426
+ // Change to error state before first computation completes
427
+ source.set(2)
428
+ await wait(110)
429
+ expect(() => derived.get()).toThrow('Intentional error')
430
+
431
+ // Change to normal state before second computation completes
432
+ source.set(3)
433
+ await wait(100)
434
+ expect(derived.get()).toBe(3)
435
+
436
+ })
437
+ })