@esportsplus/reactivity 0.38.0 → 0.39.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.
- package/bench/ownership.bench.ts +212 -0
- package/build/compiler/array.js +3 -1
- package/build/reactive/array.d.ts +3 -2
- package/build/reactive/array.js +28 -10
- package/build/reactive/index.js +1 -1
- package/build/system.d.ts +2 -1
- package/build/system.js +93 -37
- package/build/types.d.ts +5 -0
- package/package.json +1 -1
- package/src/compiler/array.ts +5 -1
- package/src/reactive/array.ts +42 -13
- package/src/reactive/index.ts +1 -1
- package/src/system.ts +134 -43
- package/src/types.ts +5 -0
- package/test/compiler/compiler.test.ts +14 -0
- package/test/ownership.test.ts +569 -0
- package/test/reactive/array.test.ts +68 -0
- package/test/system.test.ts +2 -2
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { computed, dispose, effect, flush, hasOwner, onCleanup, read, root, signal, untrack, write } from '~/system';
|
|
3
|
+
import type { Computed } from '~/system';
|
|
4
|
+
import { captureUncaught } from './lib/uncaught';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
// User cleanups plus owned children still registered on a node; also asserts the owned list's
|
|
8
|
+
// back-pointers, the head's tail pointer, and each child's owner stay consistent.
|
|
9
|
+
function registrations(node: Computed<unknown>): number {
|
|
10
|
+
let n = node.cleanup === null ? 0 : typeof node.cleanup === 'function' ? 1 : node.cleanup.length,
|
|
11
|
+
prev: Computed<unknown> | null = null;
|
|
12
|
+
|
|
13
|
+
for (let child = node.owned; child; child = child.nextOwned) {
|
|
14
|
+
expect(child.owner).toBe(node);
|
|
15
|
+
|
|
16
|
+
if (prev !== null) {
|
|
17
|
+
expect(child.prevOwned).toBe(prev);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
prev = child;
|
|
21
|
+
n++;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (node.owned) {
|
|
25
|
+
expect(node.owned.prevOwned).toBe(prev);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return n;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// A computed is the only owner whose node the public API hands back, so tests own through one,
|
|
32
|
+
// and settles the children it defers (any child after the first is queued, not run inline).
|
|
33
|
+
function owner(fn: () => void): Computed<unknown> {
|
|
34
|
+
let node = computed(() => {
|
|
35
|
+
fn();
|
|
36
|
+
return 0;
|
|
37
|
+
}) as Computed<unknown>;
|
|
38
|
+
|
|
39
|
+
flush();
|
|
40
|
+
|
|
41
|
+
return node;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
describe('ownership unlink', () => {
|
|
46
|
+
it('child root disposed early unlinks from its owner', () => {
|
|
47
|
+
let disposers: VoidFunction[] = [],
|
|
48
|
+
log: string[] = [],
|
|
49
|
+
parent = owner(() => {
|
|
50
|
+
for (let i = 0; i < 3; i++) {
|
|
51
|
+
root((d) => {
|
|
52
|
+
onCleanup(() => { log.push(`r${i}`); });
|
|
53
|
+
disposers.push(d);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
expect(registrations(parent)).toBe(3);
|
|
59
|
+
|
|
60
|
+
disposers[1]();
|
|
61
|
+
expect(log).toEqual(['r1']);
|
|
62
|
+
expect(registrations(parent)).toBe(2);
|
|
63
|
+
|
|
64
|
+
disposers[2]();
|
|
65
|
+
expect(registrations(parent)).toBe(1);
|
|
66
|
+
|
|
67
|
+
disposers[0]();
|
|
68
|
+
expect(registrations(parent)).toBe(0);
|
|
69
|
+
expect(parent.owned).toBe(null);
|
|
70
|
+
|
|
71
|
+
dispose(parent);
|
|
72
|
+
expect(log).toEqual(['r1', 'r2', 'r0']);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('child effect disposed early unlinks from its owner', () => {
|
|
76
|
+
let runs = 0,
|
|
77
|
+
stops: VoidFunction[] = [],
|
|
78
|
+
parent = owner(() => {
|
|
79
|
+
for (let i = 0; i < 3; i++) {
|
|
80
|
+
stops.push(effect(() => {
|
|
81
|
+
onCleanup(() => { runs++; });
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
expect(registrations(parent)).toBe(3);
|
|
87
|
+
|
|
88
|
+
stops[0]();
|
|
89
|
+
expect(registrations(parent)).toBe(2);
|
|
90
|
+
expect(parent.owned!.owner).toBe(parent);
|
|
91
|
+
|
|
92
|
+
stops[2]();
|
|
93
|
+
stops[1]();
|
|
94
|
+
expect(registrations(parent)).toBe(0);
|
|
95
|
+
|
|
96
|
+
dispose(parent);
|
|
97
|
+
expect(runs).toBe(3);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('child computed disposed early unlinks from its owner', () => {
|
|
101
|
+
let child: Computed<unknown> | null = null,
|
|
102
|
+
parent = owner(() => {
|
|
103
|
+
child = computed(() => 1) as Computed<unknown>;
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
expect(child!.owner).toBe(parent);
|
|
107
|
+
expect(registrations(parent)).toBe(1);
|
|
108
|
+
|
|
109
|
+
dispose(child!);
|
|
110
|
+
expect(child!.owner).toBe(null);
|
|
111
|
+
expect(registrations(parent)).toBe(0);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('a root inside a root is owned by the outer scope', () => {
|
|
115
|
+
let inner = 0;
|
|
116
|
+
|
|
117
|
+
root((d) => {
|
|
118
|
+
root((_) => {
|
|
119
|
+
onCleanup(() => { inner++; });
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
d();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
expect(inner).toBe(1);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('user cleanups registered alongside children stay on the cleanup slot', () => {
|
|
129
|
+
let parent = owner(() => {
|
|
130
|
+
onCleanup(() => {});
|
|
131
|
+
effect(() => {});
|
|
132
|
+
onCleanup(() => {});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
expect(typeof parent.cleanup === 'function' ? 1 : parent.cleanup!.length).toBe(2);
|
|
136
|
+
expect(registrations(parent)).toBe(3);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
describe('ownership churn', () => {
|
|
142
|
+
it('10k child roots created + disposed leave no registrations behind', () => {
|
|
143
|
+
let parent = owner(() => {
|
|
144
|
+
for (let i = 0; i < 10000; i++) {
|
|
145
|
+
root((d) => {
|
|
146
|
+
onCleanup(() => {});
|
|
147
|
+
d();
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
expect(registrations(parent)).toBe(0);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('10k child effects created + disposed leave no registrations behind', () => {
|
|
156
|
+
let parent = owner(() => {
|
|
157
|
+
for (let i = 0; i < 10000; i++) {
|
|
158
|
+
effect(() => {
|
|
159
|
+
onCleanup(() => {});
|
|
160
|
+
})();
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
expect(registrations(parent)).toBe(0);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('registrations stay bounded by the live children under push/remove churn', () => {
|
|
168
|
+
let live: VoidFunction[] = [],
|
|
169
|
+
max = 0,
|
|
170
|
+
parent = owner(() => {
|
|
171
|
+
for (let i = 0; i < 10000; i++) {
|
|
172
|
+
root((d) => {
|
|
173
|
+
live.push(d);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
if (live.length > 8) {
|
|
177
|
+
live.splice(i % live.length, 1)[0]();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
max = Math.max(max, registrations(parent!));
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
expect(max).toBeLessThanOrEqual(9);
|
|
185
|
+
expect(registrations(parent)).toBe(live.length);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('re-runs do not accumulate registrations', () => {
|
|
189
|
+
let s = signal(0),
|
|
190
|
+
node: Computed<unknown> | null = null;
|
|
191
|
+
|
|
192
|
+
node = computed(() => {
|
|
193
|
+
read(s);
|
|
194
|
+
onCleanup(() => {});
|
|
195
|
+
effect(() => {});
|
|
196
|
+
root((_) => {});
|
|
197
|
+
return 0;
|
|
198
|
+
}) as Computed<unknown>;
|
|
199
|
+
|
|
200
|
+
for (let i = 1; i <= 100; i++) {
|
|
201
|
+
write(s, i);
|
|
202
|
+
flush();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
expect(registrations(node)).toBe(3);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
describe('ownership ordering', () => {
|
|
211
|
+
it('re-run interleaves user cleanups and child disposal in registration order', () => {
|
|
212
|
+
let log: string[] = [],
|
|
213
|
+
s = signal(0);
|
|
214
|
+
|
|
215
|
+
effect(() => {
|
|
216
|
+
if (read(s) === 0) {
|
|
217
|
+
onCleanup(() => { log.push('a'); });
|
|
218
|
+
effect(() => {
|
|
219
|
+
onCleanup(() => { log.push('effect'); });
|
|
220
|
+
});
|
|
221
|
+
onCleanup(() => { log.push('b'); });
|
|
222
|
+
root((_) => {
|
|
223
|
+
onCleanup(() => { log.push('root'); });
|
|
224
|
+
});
|
|
225
|
+
computed(() => {
|
|
226
|
+
onCleanup(() => { log.push('computed'); });
|
|
227
|
+
return 0;
|
|
228
|
+
});
|
|
229
|
+
onCleanup(() => { log.push('c'); });
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
flush();
|
|
234
|
+
write(s, 1);
|
|
235
|
+
flush();
|
|
236
|
+
|
|
237
|
+
expect(log).toEqual(['a', 'effect', 'b', 'root', 'computed', 'c']);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('dispose runs user cleanups FIFO, then children newest-first', () => {
|
|
241
|
+
let log: string[] = [];
|
|
242
|
+
|
|
243
|
+
root((d) => {
|
|
244
|
+
onCleanup(() => { log.push('a'); });
|
|
245
|
+
effect(() => {
|
|
246
|
+
onCleanup(() => { log.push('e1'); });
|
|
247
|
+
effect(() => {
|
|
248
|
+
onCleanup(() => { log.push('e1.1'); });
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
onCleanup(() => { log.push('b'); });
|
|
252
|
+
root((_) => {
|
|
253
|
+
onCleanup(() => { log.push('r1'); });
|
|
254
|
+
});
|
|
255
|
+
effect(() => {
|
|
256
|
+
onCleanup(() => { log.push('e2'); });
|
|
257
|
+
});
|
|
258
|
+
onCleanup(() => { log.push('c'); });
|
|
259
|
+
|
|
260
|
+
d();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
expect(log).toEqual(['a', 'b', 'c', 'e2', 'r1', 'e1', 'e1.1']);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('an early-disposed child leaves the remaining order intact', () => {
|
|
267
|
+
let log: string[] = [],
|
|
268
|
+
s = signal(0),
|
|
269
|
+
stop: VoidFunction | null = null;
|
|
270
|
+
|
|
271
|
+
effect(() => {
|
|
272
|
+
if (read(s) === 0) {
|
|
273
|
+
onCleanup(() => { log.push('a'); });
|
|
274
|
+
effect(() => {
|
|
275
|
+
onCleanup(() => { log.push('x'); });
|
|
276
|
+
});
|
|
277
|
+
stop = effect(() => {
|
|
278
|
+
onCleanup(() => { log.push('y'); });
|
|
279
|
+
});
|
|
280
|
+
onCleanup(() => { log.push('b'); });
|
|
281
|
+
effect(() => {
|
|
282
|
+
onCleanup(() => { log.push('z'); });
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
flush();
|
|
288
|
+
stop!();
|
|
289
|
+
expect(log).toEqual(['y']);
|
|
290
|
+
|
|
291
|
+
write(s, 1);
|
|
292
|
+
flush();
|
|
293
|
+
|
|
294
|
+
expect(log).toEqual(['y', 'a', 'x', 'b', 'z']);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('cleanups registered during a run fire before the next run', () => {
|
|
298
|
+
let log: string[] = [],
|
|
299
|
+
s = signal(0);
|
|
300
|
+
|
|
301
|
+
effect(() => {
|
|
302
|
+
let v = read(s);
|
|
303
|
+
|
|
304
|
+
log.push(`run${v}`);
|
|
305
|
+
onCleanup(() => { log.push(`cleanup${v}`); });
|
|
306
|
+
effect(() => {
|
|
307
|
+
onCleanup(() => { log.push(`child${v}`); });
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
flush();
|
|
312
|
+
write(s, 1);
|
|
313
|
+
flush();
|
|
314
|
+
write(s, 2);
|
|
315
|
+
flush();
|
|
316
|
+
|
|
317
|
+
expect(log).toEqual(['run0', 'cleanup0', 'child0', 'run1', 'cleanup1', 'child1', 'run2']);
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
describe('ownership double dispose', () => {
|
|
323
|
+
it('disposing a child twice is a no-op and leaves siblings registered', () => {
|
|
324
|
+
let runs = 0,
|
|
325
|
+
stop: VoidFunction | null = null,
|
|
326
|
+
parent = owner(() => {
|
|
327
|
+
effect(() => {});
|
|
328
|
+
stop = effect(() => {
|
|
329
|
+
onCleanup(() => { runs++; });
|
|
330
|
+
});
|
|
331
|
+
effect(() => {});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
stop!();
|
|
335
|
+
stop!();
|
|
336
|
+
|
|
337
|
+
expect(runs).toBe(1);
|
|
338
|
+
expect(registrations(parent)).toBe(2);
|
|
339
|
+
|
|
340
|
+
dispose(parent);
|
|
341
|
+
dispose(parent);
|
|
342
|
+
|
|
343
|
+
expect(runs).toBe(1);
|
|
344
|
+
expect(registrations(parent)).toBe(0);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('a root disposer called twice runs its cleanups once', () => {
|
|
348
|
+
let d: VoidFunction | null = null,
|
|
349
|
+
runs = 0;
|
|
350
|
+
|
|
351
|
+
root((dispose) => {
|
|
352
|
+
onCleanup(() => { runs++; });
|
|
353
|
+
d = dispose;
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
d!();
|
|
357
|
+
d!();
|
|
358
|
+
|
|
359
|
+
expect(runs).toBe(1);
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
describe('ownership dispose mid-drain', () => {
|
|
365
|
+
it('a child cleanup disposing its parent and a sibling runs every cleanup once', () => {
|
|
366
|
+
let counts = { a: 0, b: 0, c: 0, parent: 0 },
|
|
367
|
+
stopB: VoidFunction | null = null,
|
|
368
|
+
stopParent: VoidFunction | null = null;
|
|
369
|
+
|
|
370
|
+
root((d) => {
|
|
371
|
+
stopParent = d;
|
|
372
|
+
onCleanup(() => { counts.parent++; });
|
|
373
|
+
effect(() => {
|
|
374
|
+
onCleanup(() => {
|
|
375
|
+
counts.a++;
|
|
376
|
+
stopB!();
|
|
377
|
+
stopParent!();
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
stopB = effect(() => {
|
|
381
|
+
onCleanup(() => { counts.b++; });
|
|
382
|
+
});
|
|
383
|
+
effect(() => {
|
|
384
|
+
onCleanup(() => { counts.c++; });
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
stopParent!();
|
|
389
|
+
|
|
390
|
+
expect(counts).toEqual({ a: 1, b: 1, c: 1, parent: 1 });
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it('a child disposing its parent from its own cleanup unlinks first', () => {
|
|
394
|
+
let counts = { child: 0, parent: 0, sibling: 0 },
|
|
395
|
+
parent: Computed<unknown> | null = null,
|
|
396
|
+
stop: VoidFunction | null = null;
|
|
397
|
+
|
|
398
|
+
parent = owner(() => {
|
|
399
|
+
onCleanup(() => { counts.parent++; });
|
|
400
|
+
stop = effect(() => {
|
|
401
|
+
onCleanup(() => {
|
|
402
|
+
counts.child++;
|
|
403
|
+
dispose(parent!);
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
effect(() => {
|
|
407
|
+
onCleanup(() => { counts.sibling++; });
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
stop!();
|
|
412
|
+
|
|
413
|
+
expect(counts).toEqual({ child: 1, parent: 1, sibling: 1 });
|
|
414
|
+
expect(registrations(parent)).toBe(0);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it('a re-run where one child disposes a later sibling skips it', () => {
|
|
418
|
+
let log: string[] = [],
|
|
419
|
+
s = signal(0),
|
|
420
|
+
stopY: VoidFunction | null = null;
|
|
421
|
+
|
|
422
|
+
effect(() => {
|
|
423
|
+
if (read(s) === 0) {
|
|
424
|
+
effect(() => {
|
|
425
|
+
onCleanup(() => {
|
|
426
|
+
log.push('x');
|
|
427
|
+
stopY!();
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
stopY = effect(() => {
|
|
431
|
+
onCleanup(() => { log.push('y'); });
|
|
432
|
+
});
|
|
433
|
+
effect(() => {
|
|
434
|
+
onCleanup(() => { log.push('z'); });
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
flush();
|
|
440
|
+
write(s, 1);
|
|
441
|
+
flush();
|
|
442
|
+
|
|
443
|
+
expect(log).toEqual(['x', 'y', 'z']);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it('deep owner trees dispose in one drain', () => {
|
|
447
|
+
let count = 0;
|
|
448
|
+
|
|
449
|
+
root((d) => {
|
|
450
|
+
let nest = (n: number): void => {
|
|
451
|
+
if (n === 0) {
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
root((_) => {
|
|
456
|
+
onCleanup(() => { count++; });
|
|
457
|
+
nest(n - 1);
|
|
458
|
+
});
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
for (let i = 0; i < 10; i++) {
|
|
462
|
+
nest(1000);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
d();
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
expect(count).toBe(10000);
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
describe('ownership throwing cleanup', () => {
|
|
474
|
+
it('a throwing child cleanup rethrows to the dispose caller after every node is disposed', () => {
|
|
475
|
+
let counts = { a: 0, c: 0, parent: 0 },
|
|
476
|
+
d: VoidFunction | null = null,
|
|
477
|
+
error = new Error('child boom');
|
|
478
|
+
|
|
479
|
+
root((dispose) => {
|
|
480
|
+
d = dispose;
|
|
481
|
+
onCleanup(() => { counts.parent++; });
|
|
482
|
+
effect(() => {
|
|
483
|
+
onCleanup(() => { counts.a++; });
|
|
484
|
+
});
|
|
485
|
+
effect(() => {
|
|
486
|
+
onCleanup(() => { throw error; });
|
|
487
|
+
});
|
|
488
|
+
effect(() => {
|
|
489
|
+
onCleanup(() => { counts.c++; });
|
|
490
|
+
});
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
expect(() => d!()).toThrow(error);
|
|
494
|
+
expect(counts).toEqual({ a: 1, c: 1, parent: 1 });
|
|
495
|
+
|
|
496
|
+
d!();
|
|
497
|
+
expect(counts).toEqual({ a: 1, c: 1, parent: 1 });
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
it('errors from several nodes in one drain aggregate', () => {
|
|
501
|
+
let d: VoidFunction | null = null;
|
|
502
|
+
|
|
503
|
+
root((dispose) => {
|
|
504
|
+
d = dispose;
|
|
505
|
+
onCleanup(() => { throw new Error('parent'); });
|
|
506
|
+
effect(() => {
|
|
507
|
+
onCleanup(() => { throw new Error('child'); });
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
try {
|
|
512
|
+
d!();
|
|
513
|
+
expect.unreachable();
|
|
514
|
+
}
|
|
515
|
+
catch (e) {
|
|
516
|
+
expect(e).toBeInstanceOf(AggregateError);
|
|
517
|
+
expect((e as AggregateError).errors.map((x) => (x as Error).message)).toEqual(['parent', 'child']);
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it('a throwing child cleanup during a re-run still disposes later siblings', async () => {
|
|
522
|
+
let log: string[] = [],
|
|
523
|
+
node: Computed<unknown> | null = null,
|
|
524
|
+
s = signal(0);
|
|
525
|
+
|
|
526
|
+
node = computed(() => {
|
|
527
|
+
if (read(s) === 0) {
|
|
528
|
+
effect(() => {
|
|
529
|
+
onCleanup(() => { throw new Error('rerun boom'); });
|
|
530
|
+
});
|
|
531
|
+
effect(() => {
|
|
532
|
+
onCleanup(() => { log.push('sibling'); });
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return read(s);
|
|
537
|
+
}) as Computed<unknown>;
|
|
538
|
+
|
|
539
|
+
flush();
|
|
540
|
+
|
|
541
|
+
let captured = await captureUncaught(() => {
|
|
542
|
+
write(s, 1);
|
|
543
|
+
flush();
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
expect(log).toEqual(['sibling']);
|
|
547
|
+
expect(captured.map((e) => (e as Error).message)).toEqual(['rerun boom']);
|
|
548
|
+
expect(registrations(node)).toBe(0);
|
|
549
|
+
});
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
describe('hasOwner', () => {
|
|
554
|
+
it('is true inside a tracking root or a run, false outside and in a detached root', () => {
|
|
555
|
+
let seen: boolean[] = [],
|
|
556
|
+
stop = effect(() => {
|
|
557
|
+
seen.push(hasOwner());
|
|
558
|
+
seen.push(untrack(() => hasOwner()));
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
seen.push(hasOwner());
|
|
562
|
+
root((d) => { seen.push(hasOwner()); d(); });
|
|
563
|
+
root(() => { seen.push(hasOwner()); });
|
|
564
|
+
stop();
|
|
565
|
+
|
|
566
|
+
expect(seen).toEqual([true, false, false, true, false]);
|
|
567
|
+
});
|
|
568
|
+
});
|
|
569
|
+
|
|
@@ -767,6 +767,74 @@ describe('ReactiveArray', () => {
|
|
|
767
767
|
expect(calls).toBe(1);
|
|
768
768
|
});
|
|
769
769
|
|
|
770
|
+
it('on returns an unsubscribe that stops the listener', () => {
|
|
771
|
+
let arr = new ReactiveArray<number>(),
|
|
772
|
+
calls = 0,
|
|
773
|
+
off = arr.on('push', () => { calls++; });
|
|
774
|
+
|
|
775
|
+
arr.push(1);
|
|
776
|
+
off();
|
|
777
|
+
arr.push(2);
|
|
778
|
+
off();
|
|
779
|
+
|
|
780
|
+
expect(calls).toBe(1);
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
it('a listener unsubscribing during dispatch does not disturb the others', () => {
|
|
784
|
+
let arr = new ReactiveArray<number>(),
|
|
785
|
+
calls: string[] = [],
|
|
786
|
+
offB: VoidFunction = () => {},
|
|
787
|
+
offA = arr.on('push', () => {
|
|
788
|
+
calls.push('a');
|
|
789
|
+
offA();
|
|
790
|
+
offB();
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
offB = arr.on('push', () => { calls.push('b'); });
|
|
794
|
+
arr.on('push', () => { calls.push('c'); });
|
|
795
|
+
arr.push(1);
|
|
796
|
+
arr.push(2);
|
|
797
|
+
|
|
798
|
+
expect(calls).toEqual(['a', 'c', 'c']);
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
it('deletes the event once its last listener unsubscribes', () => {
|
|
802
|
+
let arr = new ReactiveArray<number>(),
|
|
803
|
+
a = arr.on('push', () => {}),
|
|
804
|
+
b = arr.on('push', () => {});
|
|
805
|
+
|
|
806
|
+
a();
|
|
807
|
+
|
|
808
|
+
expect(arr.listeners.push!.filter(Boolean)).toHaveLength(1);
|
|
809
|
+
|
|
810
|
+
b();
|
|
811
|
+
|
|
812
|
+
expect('push' in arr.listeners).toBe(false);
|
|
813
|
+
|
|
814
|
+
arr.once('push', () => {});
|
|
815
|
+
arr.push(1);
|
|
816
|
+
|
|
817
|
+
expect('push' in arr.listeners).toBe(false);
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
it('sorts without computing an order once no sort listener remains', () => {
|
|
821
|
+
let arr = new ReactiveArray<number>([3, 1, 2]),
|
|
822
|
+
calls = 0,
|
|
823
|
+
off = arr.on('sort', () => { calls++; });
|
|
824
|
+
|
|
825
|
+
arr.sort((a, b) => a - b);
|
|
826
|
+
off();
|
|
827
|
+
|
|
828
|
+
let dispatch = vi.spyOn(arr, 'dispatch');
|
|
829
|
+
|
|
830
|
+
arr.sort((a, b) => b - a);
|
|
831
|
+
|
|
832
|
+
expect(calls).toBe(1);
|
|
833
|
+
expect([...arr]).toEqual([3, 2, 1]);
|
|
834
|
+
expect('sort' in arr.listeners).toBe(false);
|
|
835
|
+
expect(dispatch).not.toHaveBeenCalled();
|
|
836
|
+
});
|
|
837
|
+
|
|
770
838
|
it('once fires only once', () => {
|
|
771
839
|
let arr = new ReactiveArray<number>(),
|
|
772
840
|
calls = 0;
|
package/test/system.test.ts
CHANGED
|
@@ -686,10 +686,10 @@ describe('computed object size', () => {
|
|
|
686
686
|
expect('type' in c).toBe(false);
|
|
687
687
|
});
|
|
688
688
|
|
|
689
|
-
it('has no more own properties than
|
|
689
|
+
it('has no more own properties than 22 (14 slimmed + gv + rv + pending + 5 ownership)', () => {
|
|
690
690
|
let c = computed(() => 42);
|
|
691
691
|
|
|
692
|
-
expect(Object.keys(c).length).toBeLessThanOrEqual(
|
|
692
|
+
expect(Object.keys(c).length).toBeLessThanOrEqual(22);
|
|
693
693
|
});
|
|
694
694
|
|
|
695
695
|
it('shares one shape between sync and async computeds', () => {
|