jotai-state-tree 0.1.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/LICENSE +21 -0
- package/README.md +168 -0
- package/dist/chunk-XXZK62DD.mjs +931 -0
- package/dist/index.d.mts +1109 -0
- package/dist/index.d.ts +1109 -0
- package/dist/index.js +3579 -0
- package/dist/index.mjs +2625 -0
- package/dist/react.d.mts +144 -0
- package/dist/react.d.ts +144 -0
- package/dist/react.js +1259 -0
- package/dist/react.mjs +372 -0
- package/package.json +77 -0
- package/src/__tests__/index.test.ts +1371 -0
- package/src/__tests__/memory.test.ts +681 -0
- package/src/__tests__/performance.test.ts +667 -0
- package/src/__tests__/react.react.test.tsx +811 -0
- package/src/__tests__/registry.test.ts +589 -0
- package/src/array.ts +335 -0
- package/src/compat.ts +294 -0
- package/src/index.ts +647 -0
- package/src/lifecycle.ts +580 -0
- package/src/map.ts +276 -0
- package/src/model.ts +832 -0
- package/src/primitives.ts +400 -0
- package/src/react.ts +626 -0
- package/src/registry.ts +741 -0
- package/src/tree.ts +1275 -0
- package/src/types.ts +520 -0
- package/src/undo.ts +566 -0
- package/src/utilities.ts +616 -0
|
@@ -0,0 +1,667 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Performance and stress tests for jotai-state-tree
|
|
3
|
+
* These tests ensure the library performs well under load and doesn't have memory issues
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
7
|
+
import {
|
|
8
|
+
types,
|
|
9
|
+
destroy,
|
|
10
|
+
getSnapshot,
|
|
11
|
+
applySnapshot,
|
|
12
|
+
onSnapshot,
|
|
13
|
+
onPatch,
|
|
14
|
+
clone,
|
|
15
|
+
clearAllRegistries,
|
|
16
|
+
resetGlobalStore,
|
|
17
|
+
getRegistryStats,
|
|
18
|
+
} from "../index";
|
|
19
|
+
|
|
20
|
+
// ============================================================================
|
|
21
|
+
// Test Setup
|
|
22
|
+
// ============================================================================
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
clearAllRegistries();
|
|
26
|
+
resetGlobalStore();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
clearAllRegistries();
|
|
31
|
+
resetGlobalStore();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// ============================================================================
|
|
35
|
+
// Performance Benchmarks
|
|
36
|
+
// ============================================================================
|
|
37
|
+
|
|
38
|
+
describe("Performance", () => {
|
|
39
|
+
describe("Creation Performance", () => {
|
|
40
|
+
it("should create 10,000 simple models efficiently", () => {
|
|
41
|
+
const SimpleModel = types.model("Simple", {
|
|
42
|
+
id: types.identifier,
|
|
43
|
+
name: types.string,
|
|
44
|
+
value: types.number,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const start = performance.now();
|
|
48
|
+
|
|
49
|
+
const instances = Array.from({ length: 10000 }, (_, i) =>
|
|
50
|
+
SimpleModel.create({
|
|
51
|
+
id: `item-${i}`,
|
|
52
|
+
name: `Item ${i}`,
|
|
53
|
+
value: i,
|
|
54
|
+
})
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const elapsed = performance.now() - start;
|
|
58
|
+
|
|
59
|
+
expect(instances.length).toBe(10000);
|
|
60
|
+
// Should complete in reasonable time (less than 5 seconds on most machines)
|
|
61
|
+
expect(elapsed).toBeLessThan(5000);
|
|
62
|
+
|
|
63
|
+
// Cleanup
|
|
64
|
+
instances.forEach((i) => destroy(i));
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("should create deeply nested models efficiently", () => {
|
|
68
|
+
const Leaf = types.model("Leaf", { value: types.number });
|
|
69
|
+
const Branch = types.model("Branch", {
|
|
70
|
+
left: types.maybe(types.late(() => Branch)),
|
|
71
|
+
right: types.maybe(types.late(() => Branch)),
|
|
72
|
+
leaf: types.maybe(Leaf),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const createTree = (depth: number): any => {
|
|
76
|
+
if (depth === 0) return { leaf: { value: 1 } };
|
|
77
|
+
return {
|
|
78
|
+
left: createTree(depth - 1),
|
|
79
|
+
right: createTree(depth - 1),
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const start = performance.now();
|
|
84
|
+
const tree = Branch.create(createTree(10)); // 2^10 = 1024 leaf nodes
|
|
85
|
+
const elapsed = performance.now() - start;
|
|
86
|
+
|
|
87
|
+
expect(elapsed).toBeLessThan(5000);
|
|
88
|
+
|
|
89
|
+
destroy(tree);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("should create large arrays efficiently", () => {
|
|
93
|
+
const Item = types.model("Item", {
|
|
94
|
+
id: types.identifier,
|
|
95
|
+
data: types.string,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const List = types.model("List", {
|
|
99
|
+
items: types.array(Item),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const items = Array.from({ length: 10000 }, (_, i) => ({
|
|
103
|
+
id: `id-${i}`,
|
|
104
|
+
data: `data-${i}`,
|
|
105
|
+
}));
|
|
106
|
+
|
|
107
|
+
const start = performance.now();
|
|
108
|
+
const list = List.create({ items });
|
|
109
|
+
const elapsed = performance.now() - start;
|
|
110
|
+
|
|
111
|
+
expect(list.items.length).toBe(10000);
|
|
112
|
+
expect(elapsed).toBeLessThan(5000);
|
|
113
|
+
|
|
114
|
+
destroy(list);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("Update Performance", () => {
|
|
119
|
+
it("should handle rapid updates efficiently", () => {
|
|
120
|
+
const Counter = types
|
|
121
|
+
.model("Counter", {
|
|
122
|
+
value: types.number,
|
|
123
|
+
})
|
|
124
|
+
.actions((self) => ({
|
|
125
|
+
increment() {
|
|
126
|
+
self.value += 1;
|
|
127
|
+
},
|
|
128
|
+
}));
|
|
129
|
+
|
|
130
|
+
const counter = Counter.create({ value: 0 });
|
|
131
|
+
|
|
132
|
+
const start = performance.now();
|
|
133
|
+
|
|
134
|
+
for (let i = 0; i < 10000; i++) {
|
|
135
|
+
counter.increment();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const elapsed = performance.now() - start;
|
|
139
|
+
|
|
140
|
+
expect(counter.value).toBe(10000);
|
|
141
|
+
expect(elapsed).toBeLessThan(2000);
|
|
142
|
+
|
|
143
|
+
destroy(counter);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("should handle array mutations efficiently", () => {
|
|
147
|
+
const Item = types.model("Item", {
|
|
148
|
+
id: types.identifier,
|
|
149
|
+
value: types.number,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const List = types
|
|
153
|
+
.model("List", {
|
|
154
|
+
items: types.array(Item),
|
|
155
|
+
})
|
|
156
|
+
.actions((self) => ({
|
|
157
|
+
addItem(id: string, value: number) {
|
|
158
|
+
self.items.push({ id, value });
|
|
159
|
+
},
|
|
160
|
+
removeFirst() {
|
|
161
|
+
if (self.items.length > 0) {
|
|
162
|
+
self.items.splice(0, 1);
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
}));
|
|
166
|
+
|
|
167
|
+
const list = List.create({ items: [] });
|
|
168
|
+
|
|
169
|
+
const start = performance.now();
|
|
170
|
+
|
|
171
|
+
// Add 1000 items
|
|
172
|
+
for (let i = 0; i < 1000; i++) {
|
|
173
|
+
list.addItem(`id-${i}`, i);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Remove 500 items
|
|
177
|
+
for (let i = 0; i < 500; i++) {
|
|
178
|
+
list.removeFirst();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const elapsed = performance.now() - start;
|
|
182
|
+
|
|
183
|
+
expect(list.items.length).toBe(500);
|
|
184
|
+
expect(elapsed).toBeLessThan(5000);
|
|
185
|
+
|
|
186
|
+
destroy(list);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("should handle applySnapshot efficiently", () => {
|
|
190
|
+
const Model = types.model("Model", {
|
|
191
|
+
items: types.array(
|
|
192
|
+
types.model("Item", {
|
|
193
|
+
id: types.identifier,
|
|
194
|
+
value: types.number,
|
|
195
|
+
})
|
|
196
|
+
),
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const instance = Model.create({
|
|
200
|
+
items: Array.from({ length: 1000 }, (_, i) => ({
|
|
201
|
+
id: `id-${i}`,
|
|
202
|
+
value: i,
|
|
203
|
+
})),
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const newSnapshot = {
|
|
207
|
+
items: Array.from({ length: 1000 }, (_, i) => ({
|
|
208
|
+
id: `id-${i}`,
|
|
209
|
+
value: i * 2,
|
|
210
|
+
})),
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const start = performance.now();
|
|
214
|
+
|
|
215
|
+
for (let i = 0; i < 100; i++) {
|
|
216
|
+
applySnapshot(instance, newSnapshot);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const elapsed = performance.now() - start;
|
|
220
|
+
|
|
221
|
+
expect(elapsed).toBeLessThan(5000);
|
|
222
|
+
|
|
223
|
+
destroy(instance);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
describe("Snapshot Performance", () => {
|
|
228
|
+
it("should generate snapshots efficiently", () => {
|
|
229
|
+
const Item = types.model("Item", {
|
|
230
|
+
id: types.identifier,
|
|
231
|
+
name: types.string,
|
|
232
|
+
value: types.number,
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const Store = types.model("Store", {
|
|
236
|
+
items: types.array(Item),
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const store = Store.create({
|
|
240
|
+
items: Array.from({ length: 5000 }, (_, i) => ({
|
|
241
|
+
id: `id-${i}`,
|
|
242
|
+
name: `Item ${i}`,
|
|
243
|
+
value: i,
|
|
244
|
+
})),
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const start = performance.now();
|
|
248
|
+
|
|
249
|
+
for (let i = 0; i < 100; i++) {
|
|
250
|
+
getSnapshot(store);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const elapsed = performance.now() - start;
|
|
254
|
+
|
|
255
|
+
expect(elapsed).toBeLessThan(2000);
|
|
256
|
+
|
|
257
|
+
destroy(store);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
describe("Listener Performance", () => {
|
|
262
|
+
it("should handle many snapshot listeners efficiently", () => {
|
|
263
|
+
const Model = types
|
|
264
|
+
.model("Model", {
|
|
265
|
+
value: types.number,
|
|
266
|
+
})
|
|
267
|
+
.actions((self) => ({
|
|
268
|
+
setValue(v: number) {
|
|
269
|
+
self.value = v;
|
|
270
|
+
},
|
|
271
|
+
}));
|
|
272
|
+
|
|
273
|
+
const instance = Model.create({ value: 0 });
|
|
274
|
+
|
|
275
|
+
// Add many listeners
|
|
276
|
+
const disposers: (() => void)[] = [];
|
|
277
|
+
let callCount = 0;
|
|
278
|
+
|
|
279
|
+
for (let i = 0; i < 100; i++) {
|
|
280
|
+
disposers.push(
|
|
281
|
+
onSnapshot(instance, () => {
|
|
282
|
+
callCount++;
|
|
283
|
+
})
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const start = performance.now();
|
|
288
|
+
|
|
289
|
+
// Trigger many updates
|
|
290
|
+
for (let i = 0; i < 100; i++) {
|
|
291
|
+
instance.setValue(i);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const elapsed = performance.now() - start;
|
|
295
|
+
|
|
296
|
+
expect(callCount).toBe(10000); // 100 listeners * 100 updates
|
|
297
|
+
expect(elapsed).toBeLessThan(2000);
|
|
298
|
+
|
|
299
|
+
// Cleanup
|
|
300
|
+
disposers.forEach((d) => d());
|
|
301
|
+
destroy(instance);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("should handle many patch listeners efficiently", () => {
|
|
305
|
+
const Model = types
|
|
306
|
+
.model("Model", {
|
|
307
|
+
value: types.number,
|
|
308
|
+
})
|
|
309
|
+
.actions((self) => ({
|
|
310
|
+
setValue(v: number) {
|
|
311
|
+
self.value = v;
|
|
312
|
+
},
|
|
313
|
+
}));
|
|
314
|
+
|
|
315
|
+
const instance = Model.create({ value: 0 });
|
|
316
|
+
|
|
317
|
+
const disposers: (() => void)[] = [];
|
|
318
|
+
let patchCount = 0;
|
|
319
|
+
|
|
320
|
+
for (let i = 0; i < 100; i++) {
|
|
321
|
+
disposers.push(
|
|
322
|
+
onPatch(instance, () => {
|
|
323
|
+
patchCount++;
|
|
324
|
+
})
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const start = performance.now();
|
|
329
|
+
|
|
330
|
+
for (let i = 0; i < 100; i++) {
|
|
331
|
+
instance.setValue(i);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const elapsed = performance.now() - start;
|
|
335
|
+
|
|
336
|
+
expect(patchCount).toBe(10000);
|
|
337
|
+
expect(elapsed).toBeLessThan(2000);
|
|
338
|
+
|
|
339
|
+
disposers.forEach((d) => d());
|
|
340
|
+
destroy(instance);
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
describe("Clone Performance", () => {
|
|
345
|
+
it("should clone large structures efficiently", () => {
|
|
346
|
+
const Item = types.model("Item", {
|
|
347
|
+
id: types.identifier,
|
|
348
|
+
data: types.string,
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
const Store = types.model("Store", {
|
|
352
|
+
items: types.array(Item),
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
const original = Store.create({
|
|
356
|
+
items: Array.from({ length: 1000 }, (_, i) => ({
|
|
357
|
+
id: `id-${i}`,
|
|
358
|
+
data: `data-${i}`,
|
|
359
|
+
})),
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
const start = performance.now();
|
|
363
|
+
|
|
364
|
+
const clones = [];
|
|
365
|
+
for (let i = 0; i < 10; i++) {
|
|
366
|
+
clones.push(clone(original));
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const elapsed = performance.now() - start;
|
|
370
|
+
|
|
371
|
+
expect(clones.length).toBe(10);
|
|
372
|
+
expect(elapsed).toBeLessThan(3000);
|
|
373
|
+
|
|
374
|
+
// Cleanup
|
|
375
|
+
destroy(original);
|
|
376
|
+
clones.forEach((c) => destroy(c));
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// ============================================================================
|
|
382
|
+
// Stress Tests
|
|
383
|
+
// ============================================================================
|
|
384
|
+
|
|
385
|
+
describe("Stress Tests", () => {
|
|
386
|
+
describe("Memory Stress", () => {
|
|
387
|
+
it("should handle create/destroy cycles without memory growth", () => {
|
|
388
|
+
const Model = types.model("Model", {
|
|
389
|
+
id: types.identifier,
|
|
390
|
+
value: types.number,
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
const statsBefore = getRegistryStats();
|
|
394
|
+
|
|
395
|
+
// Create and destroy many times
|
|
396
|
+
for (let cycle = 0; cycle < 100; cycle++) {
|
|
397
|
+
const instances = Array.from({ length: 100 }, (_, i) =>
|
|
398
|
+
Model.create({ id: `cycle${cycle}-item${i}`, value: i })
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
instances.forEach((i) => destroy(i));
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const statsAfter = getRegistryStats();
|
|
405
|
+
|
|
406
|
+
// Registry should not have grown
|
|
407
|
+
expect(statsAfter.liveNodeCount).toBe(statsBefore.liveNodeCount);
|
|
408
|
+
expect(statsAfter.identifierRegistrySize).toBe(
|
|
409
|
+
statsBefore.identifierRegistrySize
|
|
410
|
+
);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it("should handle listener add/remove cycles", () => {
|
|
414
|
+
const Model = types
|
|
415
|
+
.model("Model", {
|
|
416
|
+
value: types.number,
|
|
417
|
+
})
|
|
418
|
+
.actions((self) => ({
|
|
419
|
+
setValue(v: number) {
|
|
420
|
+
self.value = v;
|
|
421
|
+
},
|
|
422
|
+
}));
|
|
423
|
+
|
|
424
|
+
const instance = Model.create({ value: 0 });
|
|
425
|
+
|
|
426
|
+
// Add and remove listeners many times
|
|
427
|
+
for (let cycle = 0; cycle < 100; cycle++) {
|
|
428
|
+
const disposers: (() => void)[] = [];
|
|
429
|
+
|
|
430
|
+
for (let i = 0; i < 50; i++) {
|
|
431
|
+
disposers.push(onSnapshot(instance, () => {}));
|
|
432
|
+
disposers.push(onPatch(instance, () => {}));
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Trigger some updates
|
|
436
|
+
for (let i = 0; i < 10; i++) {
|
|
437
|
+
instance.setValue(i);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Remove all listeners
|
|
441
|
+
disposers.forEach((d) => d());
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Should complete without issues
|
|
445
|
+
expect(instance.value).toBe(9);
|
|
446
|
+
|
|
447
|
+
destroy(instance);
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
describe("Concurrent Operations", () => {
|
|
452
|
+
it("should handle interleaved operations on multiple stores", () => {
|
|
453
|
+
const Counter = types
|
|
454
|
+
.model("Counter", {
|
|
455
|
+
id: types.identifier,
|
|
456
|
+
value: types.number,
|
|
457
|
+
})
|
|
458
|
+
.actions((self) => ({
|
|
459
|
+
increment() {
|
|
460
|
+
self.value += 1;
|
|
461
|
+
},
|
|
462
|
+
}));
|
|
463
|
+
|
|
464
|
+
const counters = Array.from({ length: 100 }, (_, i) =>
|
|
465
|
+
Counter.create({ id: `counter-${i}`, value: 0 })
|
|
466
|
+
);
|
|
467
|
+
|
|
468
|
+
// Interleaved operations
|
|
469
|
+
for (let round = 0; round < 100; round++) {
|
|
470
|
+
counters.forEach((c) => c.increment());
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Verify all counters have correct value
|
|
474
|
+
counters.forEach((c) => {
|
|
475
|
+
expect(c.value).toBe(100);
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
// Cleanup
|
|
479
|
+
counters.forEach((c) => destroy(c));
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
describe("Edge Cases Under Load", () => {
|
|
484
|
+
it("should handle rapid snapshot subscriptions during updates", () => {
|
|
485
|
+
const Model = types
|
|
486
|
+
.model("Model", {
|
|
487
|
+
value: types.number,
|
|
488
|
+
})
|
|
489
|
+
.actions((self) => ({
|
|
490
|
+
setValue(v: number) {
|
|
491
|
+
self.value = v;
|
|
492
|
+
},
|
|
493
|
+
}));
|
|
494
|
+
|
|
495
|
+
const instance = Model.create({ value: 0 });
|
|
496
|
+
const disposers: (() => void)[] = [];
|
|
497
|
+
let snapshotCount = 0;
|
|
498
|
+
|
|
499
|
+
// Add listeners while updating
|
|
500
|
+
for (let i = 0; i < 100; i++) {
|
|
501
|
+
instance.setValue(i);
|
|
502
|
+
|
|
503
|
+
if (i % 10 === 0) {
|
|
504
|
+
disposers.push(
|
|
505
|
+
onSnapshot(instance, () => {
|
|
506
|
+
snapshotCount++;
|
|
507
|
+
})
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// More updates after all listeners added
|
|
513
|
+
for (let i = 100; i < 200; i++) {
|
|
514
|
+
instance.setValue(i);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
expect(snapshotCount).toBeGreaterThan(0);
|
|
518
|
+
|
|
519
|
+
disposers.forEach((d) => d());
|
|
520
|
+
destroy(instance);
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
it("should handle destroy during iteration", () => {
|
|
524
|
+
const Item = types.model("Item", {
|
|
525
|
+
id: types.identifier,
|
|
526
|
+
value: types.number,
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
const List = types
|
|
530
|
+
.model("List", {
|
|
531
|
+
items: types.array(Item),
|
|
532
|
+
})
|
|
533
|
+
.actions((self) => ({
|
|
534
|
+
clearAll() {
|
|
535
|
+
self.items.length = 0;
|
|
536
|
+
},
|
|
537
|
+
}));
|
|
538
|
+
|
|
539
|
+
const list = List.create({
|
|
540
|
+
items: Array.from({ length: 100 }, (_, i) => ({
|
|
541
|
+
id: `id-${i}`,
|
|
542
|
+
value: i,
|
|
543
|
+
})),
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
// Get items for reference
|
|
547
|
+
const itemsRef = [...list.items];
|
|
548
|
+
|
|
549
|
+
// Clear the list
|
|
550
|
+
list.clearAll();
|
|
551
|
+
|
|
552
|
+
expect(list.items.length).toBe(0);
|
|
553
|
+
|
|
554
|
+
destroy(list);
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
describe("Identifier Registry Stress", () => {
|
|
559
|
+
it("should handle massive identifier churn", () => {
|
|
560
|
+
const Item = types.model("Item", {
|
|
561
|
+
id: types.identifier,
|
|
562
|
+
value: types.number,
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
const List = types
|
|
566
|
+
.model("List", {
|
|
567
|
+
items: types.array(Item),
|
|
568
|
+
})
|
|
569
|
+
.actions((self) => ({
|
|
570
|
+
addItem(id: string, value: number) {
|
|
571
|
+
self.items.push({ id, value });
|
|
572
|
+
},
|
|
573
|
+
removeFirst() {
|
|
574
|
+
self.items.splice(0, 1);
|
|
575
|
+
},
|
|
576
|
+
}));
|
|
577
|
+
|
|
578
|
+
const list = List.create({ items: [] });
|
|
579
|
+
|
|
580
|
+
// Add and remove many items
|
|
581
|
+
for (let i = 0; i < 5000; i++) {
|
|
582
|
+
list.addItem(`item-${i}`, i);
|
|
583
|
+
|
|
584
|
+
// Remove items periodically to test cleanup
|
|
585
|
+
if (i > 100 && i % 2 === 0) {
|
|
586
|
+
list.removeFirst();
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const stats = getRegistryStats();
|
|
591
|
+
|
|
592
|
+
// Should have proper cleanup
|
|
593
|
+
expect(stats.identifierRegistrySize).toBeLessThan(5000);
|
|
594
|
+
|
|
595
|
+
destroy(list);
|
|
596
|
+
|
|
597
|
+
const statsAfter = getRegistryStats();
|
|
598
|
+
expect(statsAfter.identifierRegistrySize).toBe(0);
|
|
599
|
+
});
|
|
600
|
+
});
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
// ============================================================================
|
|
604
|
+
// Regression Tests
|
|
605
|
+
// ============================================================================
|
|
606
|
+
|
|
607
|
+
describe("Regression Tests", () => {
|
|
608
|
+
it("should not leak nodes when using maybe types", () => {
|
|
609
|
+
const Child = types.model("Child", { value: types.number });
|
|
610
|
+
const Parent = types
|
|
611
|
+
.model("Parent", {
|
|
612
|
+
child: types.maybe(Child),
|
|
613
|
+
})
|
|
614
|
+
.actions((self) => ({
|
|
615
|
+
setChild(value: number | null) {
|
|
616
|
+
self.child = value !== null ? { value } : undefined;
|
|
617
|
+
},
|
|
618
|
+
}));
|
|
619
|
+
|
|
620
|
+
const parent = Parent.create({ child: { value: 1 } });
|
|
621
|
+
|
|
622
|
+
const statsBefore = getRegistryStats();
|
|
623
|
+
|
|
624
|
+
// Toggle child many times
|
|
625
|
+
for (let i = 0; i < 100; i++) {
|
|
626
|
+
parent.setChild(i % 2 === 0 ? i : null);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
destroy(parent);
|
|
630
|
+
|
|
631
|
+
const statsAfter = getRegistryStats();
|
|
632
|
+
expect(statsAfter.liveNodeCount).toBe(0);
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
it("should not leak nodes when using late types in arrays", () => {
|
|
636
|
+
const Node = types.model("Node", {
|
|
637
|
+
id: types.identifier,
|
|
638
|
+
children: types.array(types.late(() => Node)),
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
const root = Node.create({
|
|
642
|
+
id: "root",
|
|
643
|
+
children: [
|
|
644
|
+
{
|
|
645
|
+
id: "child1",
|
|
646
|
+
children: [
|
|
647
|
+
{ id: "grandchild1", children: [] },
|
|
648
|
+
{ id: "grandchild2", children: [] },
|
|
649
|
+
],
|
|
650
|
+
},
|
|
651
|
+
{
|
|
652
|
+
id: "child2",
|
|
653
|
+
children: [],
|
|
654
|
+
},
|
|
655
|
+
],
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
const statsBefore = getRegistryStats();
|
|
659
|
+
expect(statsBefore.liveNodeCount).toBeGreaterThan(0);
|
|
660
|
+
|
|
661
|
+
destroy(root);
|
|
662
|
+
|
|
663
|
+
const statsAfter = getRegistryStats();
|
|
664
|
+
expect(statsAfter.liveNodeCount).toBe(0);
|
|
665
|
+
expect(statsAfter.identifierRegistrySize).toBe(0);
|
|
666
|
+
});
|
|
667
|
+
});
|