@zeix/cause-effect 0.11.0 → 0.12.1
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.
- package/README.md +72 -32
- package/index.d.ts +4 -3
- package/index.js +1 -1
- package/index.ts +9 -6
- package/lib/computed.d.ts +11 -8
- package/lib/computed.ts +101 -56
- package/lib/effect.d.ts +4 -10
- package/lib/effect.ts +12 -52
- package/lib/scheduler.d.ts +40 -0
- package/lib/scheduler.ts +127 -0
- package/lib/signal.d.ts +31 -27
- package/lib/signal.ts +83 -72
- package/lib/state.d.ts +11 -48
- package/lib/state.ts +88 -68
- package/lib/util.d.ts +2 -2
- package/lib/util.ts +4 -4
- package/package.json +3 -2
- package/test/batch.test.ts +99 -0
- package/test/benchmark.test.ts +124 -49
- package/test/computed.test.ts +329 -0
- package/test/effect.test.ts +157 -0
- package/test/state.test.ts +215 -0
- package/test/util/dependency-graph.ts +95 -37
- package/test/util/framework-types.ts +53 -0
- package/test/util/perf-tests.ts +42 -0
- package/test/util/reactive-framework.ts +22 -0
- package/test/cause-effect.test.ts +0 -597
- package/test/util/pseudo-random.ts +0 -45
|
@@ -1,597 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect } from 'bun:test'
|
|
2
|
-
import { state, computed, isComputed, effect, batch, UNSET } from '../index'
|
|
3
|
-
|
|
4
|
-
/* === Utility Functions === */
|
|
5
|
-
|
|
6
|
-
const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
|
|
7
|
-
const increment = (n: number) => Number.isFinite(n) ? n + 1 : UNSET;
|
|
8
|
-
const decrement = (n: number) => Number.isFinite(n) ? n - 1 : UNSET;
|
|
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(async () => {
|
|
201
|
-
await wait(100);
|
|
202
|
-
status.set('success');
|
|
203
|
-
return 42;
|
|
204
|
-
});
|
|
205
|
-
const derived = promised.map(increment);
|
|
206
|
-
expect(derived.get()).toBe(UNSET);
|
|
207
|
-
expect(status.get()).toBe('pending');
|
|
208
|
-
await wait(110);
|
|
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(UNSET);
|
|
224
|
-
expect(status.get()).toBe('pending');
|
|
225
|
-
await wait(110);
|
|
226
|
-
expect(error.get()).toBe('error occurred');
|
|
227
|
-
expect(status.get()).toBe('error');
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
test('should compute async signals in parallel without waterfalls', async function() {
|
|
231
|
-
const a = computed(async () => {
|
|
232
|
-
await wait(100);
|
|
233
|
-
return 10;
|
|
234
|
-
});
|
|
235
|
-
const b = computed(async () => {
|
|
236
|
-
await wait(100);
|
|
237
|
-
return 20;
|
|
238
|
-
});
|
|
239
|
-
const c = computed(() => {
|
|
240
|
-
const aValue = a.get();
|
|
241
|
-
const bValue = b.get();
|
|
242
|
-
return (aValue === UNSET || bValue === UNSET)
|
|
243
|
-
? UNSET
|
|
244
|
-
: aValue + bValue;
|
|
245
|
-
});
|
|
246
|
-
expect(c.get()).toBe(UNSET);
|
|
247
|
-
await wait(110);
|
|
248
|
-
expect(c.get()).toBe(30);
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
test('should compute function dependent on a chain of computed states dependent on a signal', function() {
|
|
252
|
-
const derived = state(42)
|
|
253
|
-
.map(v => ++v)
|
|
254
|
-
.map(v => v * 2)
|
|
255
|
-
.map(v => ++v);
|
|
256
|
-
expect(derived.get()).toBe(87);
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
test('should compute function dependent on a chain of computed states dependent on an updated signal', function() {
|
|
260
|
-
const cause = state(42);
|
|
261
|
-
const derived = cause
|
|
262
|
-
.map(v => ++v)
|
|
263
|
-
.map(v => v * 2)
|
|
264
|
-
.map(v => ++v);
|
|
265
|
-
cause.set(24);
|
|
266
|
-
expect(derived.get()).toBe(51);
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
test('should drop X->B->X updates', function () {
|
|
270
|
-
let count = 0;
|
|
271
|
-
const x = state(2);
|
|
272
|
-
const a = x.map(decrement);
|
|
273
|
-
const b = computed(() => x.get() + (a.get() ?? 0));
|
|
274
|
-
const c = computed(() => {
|
|
275
|
-
count++;
|
|
276
|
-
return 'c: ' + b.get();
|
|
277
|
-
});
|
|
278
|
-
expect(c.get()).toBe('c: 3');
|
|
279
|
-
expect(count).toBe(1);
|
|
280
|
-
x.set(4);
|
|
281
|
-
expect(c.get()).toBe('c: 7');
|
|
282
|
-
expect(count).toBe(2);
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
test('should only update every signal once (diamond graph)', function() {
|
|
286
|
-
let count = 0;
|
|
287
|
-
const x = state('a');
|
|
288
|
-
const a = x.map(v => v);
|
|
289
|
-
const b = x.map(v => v);
|
|
290
|
-
const c = computed(() => {
|
|
291
|
-
count++;
|
|
292
|
-
return a.get() + ' ' + b.get();
|
|
293
|
-
});
|
|
294
|
-
expect(c.get()).toBe('a a');
|
|
295
|
-
expect(count).toBe(1);
|
|
296
|
-
x.set('aa');
|
|
297
|
-
expect(c.get()).toBe('aa aa');
|
|
298
|
-
expect(count).toBe(2);
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
test('should only update every signal once (diamond graph + tail)', function() {
|
|
302
|
-
let count = 0;
|
|
303
|
-
const x = state('a');
|
|
304
|
-
const a = x.map(v => v);
|
|
305
|
-
const b = x.map(v => v);
|
|
306
|
-
const c = computed(() => a.get() + ' ' + b.get());
|
|
307
|
-
const d = computed(() => {
|
|
308
|
-
count++;
|
|
309
|
-
return c.get();
|
|
310
|
-
});
|
|
311
|
-
expect(d.get()).toBe('a a');
|
|
312
|
-
expect(count).toBe(1);
|
|
313
|
-
x.set('aa');
|
|
314
|
-
expect(d.get()).toBe('aa aa');
|
|
315
|
-
expect(count).toBe(2);
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
/*
|
|
319
|
-
* Note for the next two tests:
|
|
320
|
-
*
|
|
321
|
-
* Due to the lazy evaluation strategy, unchanged computed signals may propagate
|
|
322
|
-
* change notifications one additional time before stabilizing. This is a
|
|
323
|
-
* one-time performance cost that allows for efficient memoization and
|
|
324
|
-
* error handling in most cases.
|
|
325
|
-
*/
|
|
326
|
-
test('should bail out if result is the same', function() {
|
|
327
|
-
let count = 0;
|
|
328
|
-
const x = state('a');
|
|
329
|
-
const a = computed(() => {
|
|
330
|
-
x.get();
|
|
331
|
-
return 'foo';
|
|
332
|
-
});
|
|
333
|
-
const b = computed(() => {
|
|
334
|
-
count++;
|
|
335
|
-
return a.get();
|
|
336
|
-
});
|
|
337
|
-
expect(b.get()).toBe('foo');
|
|
338
|
-
expect(count).toBe(1);
|
|
339
|
-
x.set('aa');
|
|
340
|
-
x.set('aaa');
|
|
341
|
-
expect(b.get()).toBe('foo');
|
|
342
|
-
expect(count).toBe(2);
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
test('should block if result remains unchanged', function() {
|
|
346
|
-
let count = 0;
|
|
347
|
-
const x = state(42);
|
|
348
|
-
const a = x.map(v => v % 2);
|
|
349
|
-
const b = computed(() => a.get() ? 'odd' : 'even');
|
|
350
|
-
const c = computed(() => {
|
|
351
|
-
count++;
|
|
352
|
-
return `c: ${b.get()}`;
|
|
353
|
-
});
|
|
354
|
-
expect(c.get()).toBe('c: even');
|
|
355
|
-
expect(count).toBe(1);
|
|
356
|
-
x.set(44);
|
|
357
|
-
x.set(46);
|
|
358
|
-
expect(c.get()).toBe('c: even');
|
|
359
|
-
expect(count).toBe(2);
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
/* test('should propagate error if an error occurred', function() {
|
|
363
|
-
let count = 0;
|
|
364
|
-
const x = state(0);
|
|
365
|
-
const a = computed(() => {
|
|
366
|
-
if (x.get() === 1) throw new Error('Calculation error');
|
|
367
|
-
return 1;
|
|
368
|
-
});
|
|
369
|
-
const b = a.map(v => v ? 'success' : 'pending');
|
|
370
|
-
const c = computed(() => {
|
|
371
|
-
count++;
|
|
372
|
-
return `c: ${b.get()}`;
|
|
373
|
-
});
|
|
374
|
-
expect(a.get()).toBe(1);
|
|
375
|
-
expect(c.get()).toBe('c: success');
|
|
376
|
-
expect(count).toBe(1);
|
|
377
|
-
x.set(1)
|
|
378
|
-
try {
|
|
379
|
-
expect(a.get()).toBe(1);
|
|
380
|
-
expect(true).toBe(false); // This line should not be reached
|
|
381
|
-
} catch (error) {
|
|
382
|
-
expect(error.message).toBe('Calculation error');
|
|
383
|
-
} finally {
|
|
384
|
-
expect(c.get()).toBe('c: success');
|
|
385
|
-
expect(count).toBe(2);
|
|
386
|
-
}
|
|
387
|
-
}); */
|
|
388
|
-
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
describe('Effect', function () {
|
|
392
|
-
|
|
393
|
-
/* test('should be added to state.effects', function () {
|
|
394
|
-
const cause = state();
|
|
395
|
-
effect(() => state());
|
|
396
|
-
expect(state.effects.size).toBe(1);
|
|
397
|
-
effect(() => state());
|
|
398
|
-
expect(state.effects.size).toBe(2);
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
test('should be added to computed.effects', function () {
|
|
402
|
-
const cause = state();
|
|
403
|
-
const derived = computed(() => 1 + state());
|
|
404
|
-
effect(() => computed());
|
|
405
|
-
expect(computed.effects.size).toBe(1);
|
|
406
|
-
const derived2 = computed(() => 2 + state());
|
|
407
|
-
effect(() => computed() + computed2());
|
|
408
|
-
expect(computed.effects.size).toBe(2);
|
|
409
|
-
expect(computed2.effects.size).toBe(1);
|
|
410
|
-
}); */
|
|
411
|
-
|
|
412
|
-
test('should be triggered after a state change', function() {
|
|
413
|
-
const cause = state('foo');
|
|
414
|
-
let effectDidRun = false;
|
|
415
|
-
effect((_value) => {
|
|
416
|
-
effectDidRun = true;
|
|
417
|
-
}, cause);
|
|
418
|
-
cause.set('bar');
|
|
419
|
-
expect(effectDidRun).toBe(true);
|
|
420
|
-
});
|
|
421
|
-
|
|
422
|
-
test('should be triggered after computed async signals resolve without waterfalls', async function() {
|
|
423
|
-
const a = computed(async () => {
|
|
424
|
-
await wait(100);
|
|
425
|
-
return 10;
|
|
426
|
-
});
|
|
427
|
-
const b = computed(async () => {
|
|
428
|
-
await wait(100);
|
|
429
|
-
return 20;
|
|
430
|
-
});
|
|
431
|
-
let result = 0;
|
|
432
|
-
let count = 0;
|
|
433
|
-
effect((aValue, bValue) => {
|
|
434
|
-
result = aValue + bValue;
|
|
435
|
-
count++;
|
|
436
|
-
}, a, b);
|
|
437
|
-
expect(result).toBe(0);
|
|
438
|
-
expect(count).toBe(0);
|
|
439
|
-
await wait(110);
|
|
440
|
-
expect(result).toBe(30);
|
|
441
|
-
expect(count).toBe(1);
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
test('should be triggered repeatedly after repeated state change', async function() {
|
|
445
|
-
const cause = state(0);
|
|
446
|
-
let result = 0;
|
|
447
|
-
let count = 0;
|
|
448
|
-
effect((res) => {
|
|
449
|
-
result = res;
|
|
450
|
-
count++;
|
|
451
|
-
}, cause);
|
|
452
|
-
for (let i = 0; i < 10; i++) {
|
|
453
|
-
cause.set(i);
|
|
454
|
-
expect(result).toBe(i);
|
|
455
|
-
expect(count).toBe(i + 1); // + 1 for the initial state change
|
|
456
|
-
}
|
|
457
|
-
});
|
|
458
|
-
|
|
459
|
-
test('should update multiple times after multiple state changes', function() {
|
|
460
|
-
const a = state(3);
|
|
461
|
-
const b = state(4);
|
|
462
|
-
let count = 0;
|
|
463
|
-
const sum = computed(() => {
|
|
464
|
-
count++;
|
|
465
|
-
return a.get() + b.get()
|
|
466
|
-
});
|
|
467
|
-
expect(sum.get()).toBe(7);
|
|
468
|
-
a.set(6);
|
|
469
|
-
expect(sum.get()).toBe(10);
|
|
470
|
-
b.set(8);
|
|
471
|
-
expect(sum.get()).toBe(14);
|
|
472
|
-
expect(count).toBe(3);
|
|
473
|
-
});
|
|
474
|
-
|
|
475
|
-
test('should detect and throw error for circular dependencies', function() {
|
|
476
|
-
const a = state(1);
|
|
477
|
-
const b = computed(() => a.get() + 1);
|
|
478
|
-
|
|
479
|
-
effect(() => {
|
|
480
|
-
a.set(b.get());
|
|
481
|
-
});
|
|
482
|
-
|
|
483
|
-
a.set(2);
|
|
484
|
-
|
|
485
|
-
try {
|
|
486
|
-
expect(b.get()).toBe(3);
|
|
487
|
-
} catch (error) {
|
|
488
|
-
expect(error.message).toBe('Circular dependency detected: exceeded 1000 iterations');
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
expect(a.get()).toBeLessThan(1002);
|
|
492
|
-
});
|
|
493
|
-
|
|
494
|
-
test('should handle errors in effects', function() {
|
|
495
|
-
const a = state(1);
|
|
496
|
-
const b = computed(() => {
|
|
497
|
-
if (a.get() > 5) throw new Error('Value too high');
|
|
498
|
-
return a.get() * 2;
|
|
499
|
-
});
|
|
500
|
-
|
|
501
|
-
let normalCallCount = 0;
|
|
502
|
-
let errorCallCount = 0;
|
|
503
|
-
|
|
504
|
-
effect({
|
|
505
|
-
ok: (_bValue) => {
|
|
506
|
-
// console.log('Normal effect:', _bValue);
|
|
507
|
-
normalCallCount++;
|
|
508
|
-
},
|
|
509
|
-
err: (error) => {
|
|
510
|
-
// console.log('Error effect:', error);
|
|
511
|
-
errorCallCount++;
|
|
512
|
-
expect(error.message).toBe('Value too high');
|
|
513
|
-
}
|
|
514
|
-
}, b);
|
|
515
|
-
|
|
516
|
-
// Normal case
|
|
517
|
-
a.set(2);
|
|
518
|
-
expect(normalCallCount).toBe(2);
|
|
519
|
-
expect(errorCallCount).toBe(0);
|
|
520
|
-
|
|
521
|
-
// Error case
|
|
522
|
-
a.set(6);
|
|
523
|
-
expect(normalCallCount).toBe(2);
|
|
524
|
-
expect(errorCallCount).toBe(1);
|
|
525
|
-
|
|
526
|
-
// Back to normal
|
|
527
|
-
a.set(3);
|
|
528
|
-
expect(normalCallCount).toBe(3);
|
|
529
|
-
expect(errorCallCount).toBe(1);
|
|
530
|
-
});
|
|
531
|
-
|
|
532
|
-
test('should handle UNSET values in effects', async function() {
|
|
533
|
-
const a = computed(async () => {
|
|
534
|
-
await wait(100);
|
|
535
|
-
return 42;
|
|
536
|
-
});
|
|
537
|
-
|
|
538
|
-
let normalCallCount = 0;
|
|
539
|
-
let nilCount = 0;
|
|
540
|
-
|
|
541
|
-
effect({
|
|
542
|
-
ok: (aValue) => {
|
|
543
|
-
normalCallCount++;
|
|
544
|
-
expect(aValue).toBe(42);
|
|
545
|
-
},
|
|
546
|
-
nil: () => {
|
|
547
|
-
nilCount++
|
|
548
|
-
}
|
|
549
|
-
}, a);
|
|
550
|
-
|
|
551
|
-
expect(normalCallCount).toBe(0);
|
|
552
|
-
expect(nilCount).toBe(1);
|
|
553
|
-
expect(a.get()).toBe(UNSET);
|
|
554
|
-
await wait(110);
|
|
555
|
-
expect(normalCallCount).toBe(2); // + 1 for effect initialization
|
|
556
|
-
expect(a.get()).toBe(42);
|
|
557
|
-
});
|
|
558
|
-
});
|
|
559
|
-
|
|
560
|
-
describe('Batch', function () {
|
|
561
|
-
|
|
562
|
-
test('should be triggered only once after repeated state change', function() {
|
|
563
|
-
const cause = state(0);
|
|
564
|
-
let result = 0;
|
|
565
|
-
let count = 0;
|
|
566
|
-
batch(() => {
|
|
567
|
-
for (let i = 1; i <= 10; i++) {
|
|
568
|
-
cause.set(i);
|
|
569
|
-
}
|
|
570
|
-
});
|
|
571
|
-
effect((res) => {
|
|
572
|
-
result = res;
|
|
573
|
-
count++;
|
|
574
|
-
}, cause);
|
|
575
|
-
expect(result).toBe(10);
|
|
576
|
-
expect(count).toBe(1);
|
|
577
|
-
});
|
|
578
|
-
|
|
579
|
-
test('should be triggered only once when multiple signals are set', function() {
|
|
580
|
-
const a = state(3);
|
|
581
|
-
const b = state(4);
|
|
582
|
-
const sum = computed(() => a.get() + b.get());
|
|
583
|
-
let result = 0;
|
|
584
|
-
let count = 0;
|
|
585
|
-
batch(() => {
|
|
586
|
-
a.set(6);
|
|
587
|
-
b.set(8);
|
|
588
|
-
});
|
|
589
|
-
effect((res) => {
|
|
590
|
-
result = res;
|
|
591
|
-
count++;
|
|
592
|
-
}, sum);
|
|
593
|
-
expect(result).toBe(14);
|
|
594
|
-
expect(count).toBe(1);
|
|
595
|
-
});
|
|
596
|
-
|
|
597
|
-
});
|
|
@@ -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
|
-
|