@zeix/cause-effect 0.10.1 → 0.12.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,458 +0,0 @@
1
- import { describe, test, expect } from 'bun:test'
2
- import { state, computed, isComputed, effect, batch } from '../index'
3
-
4
- /* === Utility Functions === */
5
-
6
- const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
7
- const increment = (n: number | void) => (n ?? 0) + 1;
8
- const decrement = (n: number | void) => (n ?? 0) - 1;
9
-
10
- /* === Tests === */
11
-
12
- describe('State', function () {
13
-
14
- describe('Boolean cause', function () {
15
-
16
- test('should be boolean', function () {
17
- const cause = state(false);
18
- expect(typeof cause.get()).toBe('boolean');
19
- });
20
-
21
- test('should set initial value to false', function () {
22
- const cause = state(false);
23
- expect(cause.get()).toBe(false);
24
- });
25
-
26
- test('should set initial value to true', function () {
27
- const cause = state(true);
28
- expect(cause.get()).toBe(true);
29
- });
30
-
31
- test('should set new value with .set(true)', function () {
32
- const cause = state(false);
33
- cause.set(true);
34
- expect(cause.get()).toBe(true);
35
- });
36
-
37
- test('should toggle initial value with .set(v => !v)', function () {
38
- const cause = state(false);
39
- cause.update((v) => !v);
40
- expect(cause.get()).toBe(true);
41
- });
42
-
43
- });
44
-
45
- describe('Number cause', function () {
46
-
47
- test('should be number', function () {
48
- const cause = state(0);
49
- expect(typeof cause.get()).toBe('number');
50
- });
51
-
52
- test('should set initial value to 0', function () {
53
- const cause = state(0);
54
- expect(cause.get()).toBe(0);
55
- });
56
-
57
- test('should set new value with .set(42)', function () {
58
- const cause = state(0);
59
- cause.set(42);
60
- expect(cause.get()).toBe(42);
61
- });
62
-
63
- test('should increment value with .set(v => ++v)', function () {
64
- const cause = state(0);
65
- cause.update(v => ++v);
66
- expect(cause.get()).toBe(1);
67
- });
68
-
69
- });
70
-
71
- describe('String cause', function () {
72
-
73
- test('should be string', function () {
74
- const cause = state('foo');
75
- expect(typeof cause.get()).toBe('string');
76
- });
77
-
78
- test('should set initial value to "foo"', function () {
79
- const cause = state('foo');
80
- expect(cause.get()).toBe('foo');
81
- });
82
-
83
- test('should set new value with .set("bar")', function () {
84
- const cause = state('foo');
85
- cause.set('bar');
86
- expect(cause.get()).toBe('bar');
87
- });
88
-
89
- test('should upper case value with .set(v => v.toUpperCase())', function () {
90
- const cause = state('foo');
91
- cause.update(v => v ? v.toUpperCase() : '');
92
- expect(cause.get()).toBe("FOO");
93
- });
94
-
95
- });
96
-
97
- describe('Array cause', function () {
98
-
99
- test('should be array', function () {
100
- const cause = state([1, 2, 3]);
101
- expect(Array.isArray(cause.get())).toBe(true);
102
- });
103
-
104
- test('should set initial value to [1, 2, 3]', function () {
105
- const cause = state([1, 2, 3]);
106
- expect(cause.get()).toEqual([1, 2, 3]);
107
- });
108
-
109
- test('should set new value with .set([4, 5, 6])', function () {
110
- const cause = state([1, 2, 3]);
111
- cause.set([4, 5, 6]);
112
- expect(cause.get()).toEqual([4, 5, 6]);
113
- });
114
-
115
- test('should reflect current value of array after modification', function () {
116
- const array = [1, 2, 3];
117
- const cause = state(array);
118
- array.push(4); // don't do this! the result will be correct, but we can't trigger effects
119
- expect(cause.get()).toEqual([1, 2, 3, 4]);
120
- });
121
-
122
- test('should set new value with .set([...array, 4])', function () {
123
- const array = [1, 2, 3];
124
- const cause = state(array);
125
- cause.set([...array, 4]); // use destructuring instead!
126
- expect(cause.get()).toEqual([1, 2, 3, 4]);
127
- });
128
-
129
- });
130
-
131
- describe('Object cause', function () {
132
-
133
- test('should be object', function () {
134
- const cause = state({ a: 'a', b: 1 });
135
- expect(typeof cause.get()).toBe('object');
136
- });
137
-
138
- test('should set initial value to { a: "a", b: 1 }', function () {
139
- const cause = state({ a: 'a', b: 1 });
140
- expect(cause.get()).toEqual({ a: 'a', b: 1 });
141
- });
142
-
143
- test('should set new value with .set({ c: true })', function () {
144
- const cause = state<Record<string, any>>({ a: 'a', b: 1 });
145
- cause.set({ c: true });
146
- expect(cause.get()).toEqual({ c: true });
147
- });
148
-
149
- test('should reflect current value of object after modification', function () {
150
- const obj = { a: 'a', b: 1 };
151
- const cause = state<Record<string, any>>(obj);
152
- // @ts-expect-error
153
- obj.c = true; // don't do this! the result will be correct, but we can't trigger effects
154
- expect(cause.get()).toEqual({ a: 'a', b: 1, c: true });
155
- });
156
-
157
- test('should set new value with .set({...obj, c: true})', function () {
158
- const obj = { a: 'a', b: 1 };
159
- const cause = state<Record<string, any>>(obj);
160
- cause.set({...obj, c: true}); // use destructuring instead!
161
- expect(cause.get()).toEqual({ a: 'a', b: 1, c: true });
162
- });
163
-
164
- });
165
-
166
- describe('Map method', function () {
167
-
168
- test('should return a computed signal', function() {
169
- const cause = state(42);
170
- const double = cause.map(v => v * 2);
171
- expect(isComputed(double)).toBe(true);
172
- expect(double.get()).toBe(84);
173
- });
174
-
175
- });
176
-
177
- });
178
-
179
- describe('Computed', function () {
180
-
181
- test('should compute a function', function() {
182
- const derived = computed(() => 1 + 2);
183
- expect(derived.get()).toBe(3);
184
- });
185
-
186
- test('should compute function dependent on a signal', function() {
187
- const derived = state(42).map(v => ++v);
188
- expect(derived.get()).toBe(43);
189
- });
190
-
191
- test('should compute function dependent on an updated signal', function() {
192
- const cause = state(42);
193
- const derived = cause.map(v => ++v);
194
- cause.set(24);
195
- expect(derived.get()).toBe(25);
196
- });
197
-
198
- test('should compute function dependent on an async signal', async function() {
199
- const status = state('pending');
200
- const promised = computed<number>(async () => {
201
- await wait(100);
202
- status.set('success');
203
- return 42;
204
- });
205
- const derived = promised.map(increment);
206
- expect(derived.get()).toBe(1);
207
- expect(status.get()).toBe('pending');
208
- await wait(100);
209
- expect(derived.get()).toBe(43);
210
- expect(status.get()).toBe('success');
211
- });
212
-
213
- test('should handle errors from an async signal gracefully', async function() {
214
- const status = state('pending');
215
- const error = state('');
216
- const promised = computed(async () => {
217
- await wait(100);
218
- status.set('error');
219
- error.set('error occurred');
220
- return 0
221
- });
222
- const derived = promised.map(increment);
223
- expect(derived.get()).toBe(1);
224
- expect(status.get()).toBe('pending');
225
- await wait(100);
226
- expect(error.get()).toBe('error occurred');
227
- expect(status.get()).toBe('error');
228
- });
229
-
230
- test('should compute function dependent on a chain of computed states dependent on a signal', function() {
231
- const derived = state(42)
232
- .map(v => ++v)
233
- .map(v => v * 2)
234
- .map(v => ++v);
235
- expect(derived.get()).toBe(87);
236
- });
237
-
238
- test('should compute function dependent on a chain of computed states dependent on an updated signal', function() {
239
- const cause = state(42);
240
- const derived = cause.map(v => ++v)
241
- .map(v => v * 2)
242
- .map(v => ++v);
243
- cause.set(24);
244
- expect(derived.get()).toBe(51);
245
- });
246
-
247
- test('should drop X->B->X updates', function () {
248
- let count = 0;
249
- const x = state(2);
250
- const a = x.map(decrement);
251
- const b = computed(() => x.get() + (a.get() ?? 0));
252
- const c = computed(() => {
253
- count++;
254
- return 'c: ' + b.get();
255
- });
256
- expect(c.get()).toBe('c: 3');
257
- expect(count).toBe(1);
258
- x.set(4);
259
- expect(c.get()).toBe('c: 7');
260
- expect(count).toBe(2);
261
- });
262
-
263
- test('should only update every signal once (diamond graph)', function() {
264
- let count = 0;
265
- const x = state('a');
266
- const a = x.map(v => v);
267
- const b = x.map(v => v);
268
- const c = computed(() => {
269
- count++;
270
- return a.get() + ' ' + b.get();
271
- });
272
- expect(c.get()).toBe('a a');
273
- expect(count).toBe(1);
274
- x.set('aa');
275
- expect(c.get()).toBe('aa aa');
276
- expect(count).toBe(2);
277
- });
278
-
279
- test('should only update every signal once (diamond graph + tail)', function() {
280
- let count = 0;
281
- const x = state('a');
282
- const a = x.map(v => v);
283
- const b = x.map(v => v);
284
- const c = computed(() => a.get() + ' ' + b.get());
285
- const d = computed(() => {
286
- count++;
287
- return c.get();
288
- });
289
- expect(d.get()).toBe('a a');
290
- expect(count).toBe(1);
291
- x.set('aa');
292
- expect(d.get()).toBe('aa aa');
293
- expect(count).toBe(2);
294
- });
295
-
296
- test('should bail out if result is the same', function() {
297
- let count = 0;
298
- const x = state('a');
299
- const a = computed(() => {
300
- x.get();
301
- return 'foo';
302
- });
303
- const b = computed(() => {
304
- count++;
305
- return a.get();
306
- }, true); // turn memoization on
307
- expect(b.get()).toBe('foo');
308
- expect(count).toBe(1);
309
- x.set('aa');
310
- expect(b.get()).toBe('foo');
311
- expect(count).toBe(1);
312
- });
313
-
314
- test('should block if result remains unchanged', function() {
315
- let count = 0;
316
- const x = state(42);
317
- const a = x.map(v => v % 2);
318
- const b = computed(() => a.get() ? 'odd' : 'even', true);
319
- const c = computed(() => {
320
- count++;
321
- return `c: ${b.get()}`;
322
- }, true);
323
- expect(c.get()).toBe('c: even');
324
- expect(count).toBe(1);
325
- x.set(44);
326
- expect(c.get()).toBe('c: even');
327
- expect(count).toBe(1);
328
- });
329
-
330
- test('should block if an error occurred', function() {
331
- let count = 0;
332
- const x = state(0);
333
- const a = computed(() => {
334
- if (x.get() === 1) throw new Error('Calculation error');
335
- return 1;
336
- }, true);
337
- const b = a.map(v => v ? 'success' : 'pending');
338
- const c = computed(() => {
339
- count++;
340
- return `c: ${b.get()}`;
341
- }, true);
342
- expect(a.get()).toBe(1);
343
- expect(c.get()).toBe('c: success');
344
- expect(count).toBe(1);
345
- x.set(1);
346
- try {
347
- expect(a.get()).toBe(1);
348
- } catch (error) {
349
- expect(error.message).toBe('Calculation error');
350
- } finally {
351
- expect(c.get()).toBe('c: success');
352
- expect(count).toBe(1);
353
- }
354
- });
355
-
356
- });
357
-
358
- describe('Effect', function () {
359
-
360
- /* test('should be added to state.effects', function () {
361
- const cause = state();
362
- effect(() => state());
363
- expect(state.effects.size).toBe(1);
364
- effect(() => state());
365
- expect(state.effects.size).toBe(2);
366
- });
367
-
368
- test('should be added to computed.effects', function () {
369
- const cause = state();
370
- const derived = computed(() => 1 + state());
371
- effect(() => computed());
372
- expect(computed.effects.size).toBe(1);
373
- const derived2 = computed(() => 2 + state());
374
- effect(() => computed() + computed2());
375
- expect(computed.effects.size).toBe(2);
376
- expect(computed2.effects.size).toBe(1);
377
- }); */
378
-
379
- test('should be triggered after a state change', function() {
380
- const cause = state('foo');
381
- let effectDidRun = false;
382
- effect(() => {
383
- cause.get();
384
- effectDidRun = true;
385
- });
386
- cause.set('bar');
387
- expect(effectDidRun).toBe(true);
388
- });
389
-
390
- test('should be triggered repeatedly after repeated state change', async function() {
391
- const cause = state(0);
392
- let count = 0;
393
- effect(() => {
394
- cause.get();
395
- count++;
396
- });
397
- for (let i = 0; i < 10; i++) {
398
- cause.set(i);
399
- expect(count).toBe(i + 1); // + 1 for the initial state change
400
- }
401
- });
402
-
403
- test('should update multiple times after multiple state changes', function() {
404
- const a = state(3);
405
- const b = state(4);
406
- let count = 0;
407
- const sum = computed(() => {
408
- count++;
409
- return a.get() + b.get()
410
- });
411
- expect(sum.get()).toBe(7);
412
- a.set(6);
413
- expect(sum.get()).toBe(10);
414
- b.set(8);
415
- expect(sum.get()).toBe(14);
416
- expect(count).toBe(3);
417
- });
418
-
419
- });
420
-
421
- describe('Batch', function () {
422
-
423
- test('should be triggered only once after repeated state change', function() {
424
- const cause = state(0);
425
- let result = 0;
426
- let count = 0;
427
- batch(() => {
428
- for (let i = 1; i <= 10; i++) {
429
- cause.set(i);
430
- }
431
- });
432
- effect(() => {
433
- result = cause.get();
434
- count++;
435
- });
436
- expect(result).toBe(10);
437
- expect(count).toBe(1);
438
- });
439
-
440
- test('should be triggered only once when multiple signals are set', function() {
441
- const a = state(3);
442
- const b = state(4);
443
- const sum = computed(() => a.get() + b.get());
444
- let result = 0;
445
- let count = 0;
446
- batch(() => {
447
- a.set(6);
448
- b.set(8);
449
- });
450
- effect(() => {
451
- result = sum.get();
452
- count++;
453
- });
454
- expect(sum.get()).toBe(14);
455
- expect(count).toBe(1);
456
- });
457
-
458
- });
@@ -1,45 +0,0 @@
1
- export function pseudoRandom(seed = "seed") {
2
- const hash = xmur3a(seed);
3
- const rng = sfc32(hash(), hash(), hash(), hash());
4
- return rng;
5
- }
6
- /* these are adapted from https://github.com/bryc/code/blob/master/jshash/PRNGs.md
7
- * (License: Public domain) */
8
- /** random number generator originally in PractRand */
9
- function sfc32(a, b, c, d) {
10
- return function () {
11
- a >>>= 0;
12
- b >>>= 0;
13
- c >>>= 0;
14
- d >>>= 0;
15
- let t = (a + b) | 0;
16
- a = b ^ (b >>> 9);
17
- b = (c + (c << 3)) | 0;
18
- c = (c << 21) | (c >>> 11);
19
- d = (d + 1) | 0;
20
- t = (t + d) | 0;
21
- c = (c + t) | 0;
22
- return (t >>> 0) / 4294967296;
23
- };
24
- }
25
- /** MurmurHash3 */
26
- export function xmur3a(str) {
27
- let h = 2166136261 >>> 0;
28
- for (let k, i = 0; i < str.length; i++) {
29
- k = Math.imul(str.charCodeAt(i), 3432918353);
30
- k = (k << 15) | (k >>> 17);
31
- h ^= Math.imul(k, 461845907);
32
- h = (h << 13) | (h >>> 19);
33
- h = (Math.imul(h, 5) + 3864292196) | 0;
34
- }
35
- h ^= str.length;
36
- return function () {
37
- h ^= h >>> 16;
38
- h = Math.imul(h, 2246822507);
39
- h ^= h >>> 13;
40
- h = Math.imul(h, 3266489909);
41
- h ^= h >>> 16;
42
- return h >>> 0;
43
- };
44
- }
45
-