Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
1import { describe, it, beforeEach, expect } from 'vitest';
2import * as InMemoryData from './data';
3import { keyOfField } from './keys';
4
5let data: InMemoryData.InMemoryData;
6
7beforeEach(() => {
8 data = InMemoryData.make('Query');
9 InMemoryData.initDataState('write', data, null);
10});
11
12describe('garbage collection', () => {
13 it('erases orphaned entities', () => {
14 InMemoryData.writeRecord('Todo:1', '__typename', 'Todo');
15 InMemoryData.writeRecord('Todo:1', 'id', '1');
16 InMemoryData.writeRecord('Todo:2', '__typename', 'Todo');
17 InMemoryData.writeRecord('Query', '__typename', 'Query');
18 InMemoryData.writeLink('Query', 'todo', 'Todo:1');
19 InMemoryData.writeType('Todo', 'Todo:1');
20
21 InMemoryData.gc();
22
23 expect(InMemoryData.readLink('Query', 'todo')).toBe('Todo:1');
24 expect(InMemoryData.getEntitiesForType('Todo')).toEqual(
25 new Set(['Todo:1'])
26 );
27
28 InMemoryData.writeLink('Query', 'todo', undefined);
29 InMemoryData.gc();
30
31 expect(InMemoryData.readLink('Query', 'todo')).toBe(undefined);
32 expect(InMemoryData.readRecord('Todo:1', 'id')).toBe(undefined);
33 expect(InMemoryData.getEntitiesForType('Todo')).toEqual(new Set());
34
35 expect(InMemoryData.getCurrentDependencies()).toEqual(
36 new Set(['Todo:1', 'Todo:2', 'Query.todo'])
37 );
38 });
39
40 it('keeps readopted entities', () => {
41 InMemoryData.writeRecord('Todo:1', '__typename', 'Todo');
42 InMemoryData.writeRecord('Todo:1', 'id', '1');
43 InMemoryData.writeRecord('Query', '__typename', 'Query');
44 InMemoryData.writeLink('Query', 'todo', 'Todo:1');
45 InMemoryData.writeLink('Query', 'todo', undefined);
46 InMemoryData.writeLink('Query', 'newTodo', 'Todo:1');
47 InMemoryData.writeType('Todo', 'Todo:1');
48
49 InMemoryData.gc();
50
51 expect(InMemoryData.readLink('Query', 'newTodo')).toBe('Todo:1');
52 expect(InMemoryData.readLink('Query', 'todo')).toBe(undefined);
53 expect(InMemoryData.readRecord('Todo:1', 'id')).toBe('1');
54 expect(InMemoryData.getEntitiesForType('Todo')).toEqual(
55 new Set(['Todo:1'])
56 );
57
58 expect(InMemoryData.getCurrentDependencies()).toEqual(
59 new Set(['Todo:1', 'Query.todo', 'Query.newTodo'])
60 );
61 });
62
63 it('keeps entities with multiple owners', () => {
64 InMemoryData.writeRecord('Todo:1', '__typename', 'Todo');
65 InMemoryData.writeRecord('Todo:1', 'id', '1');
66 InMemoryData.writeRecord('Query', '__typename', 'Query');
67 InMemoryData.writeLink('Query', 'todoA', 'Todo:1');
68 InMemoryData.writeLink('Query', 'todoB', 'Todo:1');
69 InMemoryData.writeLink('Query', 'todoA', undefined);
70
71 InMemoryData.gc();
72
73 expect(InMemoryData.readLink('Query', 'todoA')).toBe(undefined);
74 expect(InMemoryData.readLink('Query', 'todoB')).toBe('Todo:1');
75 expect(InMemoryData.readRecord('Todo:1', 'id')).toBe('1');
76
77 expect(InMemoryData.getCurrentDependencies()).toEqual(
78 new Set(['Todo:1', 'Query.todoA', 'Query.todoB'])
79 );
80 });
81
82 it('skips entities with optimistic updates', () => {
83 InMemoryData.writeRecord('Todo:1', '__typename', 'Todo');
84 InMemoryData.writeRecord('Todo:1', 'id', '1');
85 InMemoryData.writeLink('Query', 'todo', 'Todo:1');
86
87 InMemoryData.initDataState('write', data, 1, true);
88 InMemoryData.writeLink('Query', 'temp', 'Todo:1');
89
90 InMemoryData.initDataState('write', data, 0, true);
91 InMemoryData.writeLink('Query', 'todo', undefined);
92
93 InMemoryData.gc();
94
95 expect(InMemoryData.readRecord('Todo:1', 'id')).toBe('1');
96
97 InMemoryData.reserveLayer(data, 1);
98 InMemoryData.gc();
99
100 expect(InMemoryData.readRecord('Todo:1', 'id')).toBe('1');
101 // TODO: is it a problem that this fails, we are reading from Todo
102 // but we are not updating anything
103 expect(InMemoryData.getCurrentDependencies()).toEqual(
104 new Set(['Query.todo'])
105 );
106 });
107
108 it('erases child entities that are orphaned', () => {
109 InMemoryData.writeRecord('Author:1', '__typename', 'Author');
110 InMemoryData.writeRecord('Author:1', 'id', '1');
111 InMemoryData.writeLink('Todo:1', 'author', 'Author:1');
112 InMemoryData.writeRecord('Todo:1', '__typename', 'Todo');
113 InMemoryData.writeRecord('Todo:1', 'id', '1');
114 InMemoryData.writeLink('Query', 'todo', 'Todo:1');
115 InMemoryData.writeType('Todo', 'Todo:1');
116 InMemoryData.writeType('Author', 'Author:1');
117
118 InMemoryData.writeLink('Query', 'todo', undefined);
119 expect(InMemoryData.getEntitiesForType('Todo')).toEqual(
120 new Set(['Todo:1'])
121 );
122 expect(InMemoryData.getEntitiesForType('Author')).toEqual(
123 new Set(['Author:1'])
124 );
125 InMemoryData.gc();
126
127 expect(InMemoryData.readRecord('Todo:1', 'id')).toBe(undefined);
128 expect(InMemoryData.readRecord('Author:1', 'id')).toBe(undefined);
129 expect(InMemoryData.getEntitiesForType('Todo')).toEqual(new Set());
130 expect(InMemoryData.getEntitiesForType('Author')).toEqual(new Set());
131
132 expect(InMemoryData.getCurrentDependencies()).toEqual(
133 new Set(['Author:1', 'Todo:1', 'Query.todo'])
134 );
135 });
136});
137
138describe('inspectFields', () => {
139 it('returns field infos for all links and records', () => {
140 InMemoryData.writeRecord('Query', '__typename', 'Query');
141 InMemoryData.writeLink('Query', keyOfField('todo', { id: '1' }), 'Todo:1');
142 InMemoryData.writeRecord('Query', keyOfField('hasTodo', { id: '1' }), true);
143
144 InMemoryData.writeLink('Query', 'randomTodo', 'Todo:1');
145
146 expect(InMemoryData.inspectFields('Query')).toMatchInlineSnapshot(`
147 [
148 {
149 "arguments": {
150 "id": "1",
151 },
152 "fieldKey": "todo({"id":"1"})",
153 "fieldName": "todo",
154 },
155 {
156 "arguments": null,
157 "fieldKey": "randomTodo",
158 "fieldName": "randomTodo",
159 },
160 {
161 "arguments": null,
162 "fieldKey": "__typename",
163 "fieldName": "__typename",
164 },
165 {
166 "arguments": {
167 "id": "1",
168 },
169 "fieldKey": "hasTodo({"id":"1"})",
170 "fieldName": "hasTodo",
171 },
172 ]
173 `);
174
175 expect(InMemoryData.getCurrentDependencies()).toEqual(
176 new Set([
177 'Query.todo({"id":"1"})',
178 'Query.hasTodo({"id":"1"})',
179 'Query.randomTodo',
180 ])
181 );
182 });
183
184 it('returns an empty array when an entity is unknown', () => {
185 expect(InMemoryData.inspectFields('Random')).toEqual([]);
186
187 expect(InMemoryData.getCurrentDependencies()).toEqual(new Set(['Random']));
188 });
189
190 it('returns field infos for all optimistic updates', () => {
191 InMemoryData.initDataState('write', data, 1, true);
192 InMemoryData.writeLink('Query', 'todo', 'Todo:1');
193
194 expect(InMemoryData.inspectFields('Random')).toMatchInlineSnapshot('[]');
195 });
196
197 it('avoids duplicate field infos', () => {
198 InMemoryData.writeLink('Query', 'todo', 'Todo:1');
199
200 InMemoryData.initDataState('write', data, 1, true);
201 InMemoryData.writeLink('Query', 'todo', 'Todo:2');
202
203 expect(InMemoryData.inspectFields('Query')).toMatchInlineSnapshot(`
204 [
205 {
206 "arguments": null,
207 "fieldKey": "todo",
208 "fieldName": "todo",
209 },
210 ]
211 `);
212 });
213});
214
215describe('commutative changes', () => {
216 it('always applies out-of-order updates in-order', () => {
217 InMemoryData.reserveLayer(data, 1);
218 InMemoryData.reserveLayer(data, 2);
219
220 InMemoryData.initDataState('write', data, 2);
221 InMemoryData.writeRecord('Query', 'index', 2);
222 expect(InMemoryData.readRecord('Query', 'index')).toBe(2);
223 InMemoryData.clearDataState();
224
225 InMemoryData.initDataState('read', data, null);
226 expect(InMemoryData.readRecord('Query', 'index')).toBe(2);
227
228 InMemoryData.initDataState('write', data, 1);
229 InMemoryData.writeRecord('Query', 'index', 1);
230 expect(InMemoryData.readRecord('Query', 'index')).toBe(2);
231 InMemoryData.clearDataState();
232
233 InMemoryData.initDataState('read', data, null);
234 expect(InMemoryData.readRecord('Query', 'index')).toBe(2);
235
236 expect(data.optimisticOrder).toEqual([]);
237 });
238
239 it('creates optimistic layers that may be removed later', () => {
240 InMemoryData.reserveLayer(data, 1);
241
242 InMemoryData.initDataState('write', data, 2, true);
243 InMemoryData.writeRecord('Query', 'index', 2);
244 InMemoryData.clearDataState();
245
246 InMemoryData.initDataState('read', data, null);
247 expect(InMemoryData.readRecord('Query', 'index')).toBe(2);
248
249 // Actively clearing out layer 2
250 InMemoryData.noopDataState(data, 2);
251
252 InMemoryData.initDataState('read', data, null);
253 expect(InMemoryData.readRecord('Query', 'index')).toBe(undefined);
254
255 InMemoryData.initDataState('write', data, 1);
256 InMemoryData.writeRecord('Query', 'index', 1);
257 InMemoryData.clearDataState();
258
259 InMemoryData.initDataState('write', data, null);
260 expect(InMemoryData.readRecord('Query', 'index')).toBe(1);
261 InMemoryData.clearDataState();
262
263 expect(data.optimisticOrder).toEqual([]);
264 });
265
266 it('discards optimistic order when concrete data is written', () => {
267 InMemoryData.reserveLayer(data, 1);
268 InMemoryData.reserveLayer(data, 2);
269 InMemoryData.reserveLayer(data, 3);
270
271 InMemoryData.initDataState('write', data, 2, true);
272 InMemoryData.writeRecord('Query', 'index', 2);
273 InMemoryData.writeRecord('Query', 'optimistic', true);
274 InMemoryData.clearDataState();
275
276 InMemoryData.initDataState('write', data, 3);
277 InMemoryData.writeRecord('Query', 'index', 3);
278 InMemoryData.clearDataState();
279
280 // Expect Layer 3
281 expect(data.optimisticOrder).toEqual([3, 2, 1]);
282 InMemoryData.initDataState('read', data, null);
283 expect(InMemoryData.readRecord('Query', 'index')).toBe(3);
284 expect(InMemoryData.readRecord('Query', 'optimistic')).toBe(true);
285
286 // Write 2 again
287 InMemoryData.initDataState('write', data, 2);
288 InMemoryData.writeRecord('Query', 'index', 2);
289 InMemoryData.clearDataState();
290
291 // 2 has moved in front of 3
292 expect(data.optimisticOrder).toEqual([2, 3, 1]);
293 InMemoryData.initDataState('read', data, null);
294 expect(InMemoryData.readRecord('Query', 'index')).toBe(2);
295 expect(InMemoryData.readRecord('Query', 'optimistic')).toBe(undefined);
296 });
297
298 it('overrides data using optimistic layers', () => {
299 InMemoryData.reserveLayer(data, 1);
300 InMemoryData.reserveLayer(data, 2);
301 InMemoryData.reserveLayer(data, 3);
302
303 InMemoryData.initDataState('write', data, 2);
304 InMemoryData.writeRecord('Query', 'index', 2);
305 InMemoryData.clearDataState();
306
307 InMemoryData.initDataState('write', data, 3);
308 InMemoryData.writeRecord('Query', 'index', 3);
309 InMemoryData.clearDataState();
310
311 // Regular write that isn't optimistic
312 InMemoryData.initDataState('write', data, null);
313 InMemoryData.writeRecord('Query', 'index', 1);
314 InMemoryData.clearDataState();
315
316 InMemoryData.initDataState('read', data, null);
317 expect(InMemoryData.readRecord('Query', 'index')).toBe(3);
318
319 expect(data.optimisticOrder).toEqual([3, 2, 1]);
320 });
321
322 it('avoids optimistic layers when only one layer is pending', () => {
323 InMemoryData.reserveLayer(data, 1);
324
325 InMemoryData.initDataState('write', data, 1);
326 InMemoryData.writeRecord('Query', 'index', 2);
327 InMemoryData.clearDataState();
328
329 // This will be applied and visible since the above write isn't optimistic
330 InMemoryData.initDataState('write', data, null);
331 InMemoryData.writeRecord('Query', 'index', 1);
332 InMemoryData.clearDataState();
333
334 InMemoryData.initDataState('read', data, null);
335 expect(InMemoryData.readRecord('Query', 'index')).toBe(1);
336
337 expect(data.optimisticOrder).toEqual([]);
338 });
339
340 it('continues applying optimistic layers even if the first one completes', () => {
341 InMemoryData.reserveLayer(data, 1);
342 InMemoryData.reserveLayer(data, 2);
343 InMemoryData.reserveLayer(data, 3);
344 InMemoryData.reserveLayer(data, 4);
345
346 InMemoryData.initDataState('write', data, 1);
347 InMemoryData.writeRecord('Query', 'index', 1);
348 InMemoryData.clearDataState();
349
350 InMemoryData.initDataState('read', data, null);
351 expect(InMemoryData.readRecord('Query', 'index')).toBe(1);
352
353 InMemoryData.initDataState('write', data, 3);
354 InMemoryData.writeRecord('Query', 'index', 3);
355 InMemoryData.clearDataState();
356
357 InMemoryData.initDataState('read', data, null);
358 expect(InMemoryData.readRecord('Query', 'index')).toBe(3);
359
360 InMemoryData.initDataState('write', data, 4);
361 InMemoryData.writeRecord('Query', 'index', 4);
362 InMemoryData.clearDataState();
363
364 InMemoryData.initDataState('read', data, null);
365 expect(InMemoryData.readRecord('Query', 'index')).toBe(4);
366
367 InMemoryData.initDataState('write', data, 2);
368 InMemoryData.writeRecord('Query', 'index', 2);
369 InMemoryData.clearDataState();
370
371 InMemoryData.initDataState('read', data, null);
372 expect(InMemoryData.readRecord('Query', 'index')).toBe(4);
373
374 expect(data.optimisticOrder).toEqual([]);
375 });
376
377 it('allows noopDataState to clear layers only if necessary', () => {
378 InMemoryData.reserveLayer(data, 1);
379 InMemoryData.reserveLayer(data, 2);
380
381 InMemoryData.noopDataState(data, 2);
382 expect(data.optimisticOrder).toEqual([2, 1]);
383
384 InMemoryData.noopDataState(data, 1);
385 expect(data.optimisticOrder).toEqual([]);
386 });
387
388 it('respects non-reserved optimistic layers', () => {
389 InMemoryData.reserveLayer(data, 1);
390
391 InMemoryData.initDataState('write', data, 2, true);
392 InMemoryData.writeRecord('Query', 'index', 2);
393 InMemoryData.clearDataState();
394
395 InMemoryData.reserveLayer(data, 3);
396
397 expect(data.optimisticOrder).toEqual([3, 2, 1]);
398 expect([...data.commutativeKeys]).toEqual([1, 3]);
399
400 InMemoryData.initDataState('write', data, 1);
401 InMemoryData.writeRecord('Query', 'index', 1);
402 InMemoryData.clearDataState();
403 expect(data.optimisticOrder).toEqual([3, 2]);
404
405 InMemoryData.initDataState('read', data, null);
406 expect(InMemoryData.readRecord('Query', 'index')).toBe(2);
407
408 InMemoryData.initDataState('write', data, 3);
409 InMemoryData.writeRecord('Query', 'index', 3);
410 InMemoryData.clearDataState();
411 expect(data.optimisticOrder).toEqual([3, 2]);
412
413 InMemoryData.initDataState('read', data, null);
414 expect(InMemoryData.readRecord('Query', 'index')).toBe(3);
415 });
416
417 it('squashes when optimistic layers are completed', () => {
418 InMemoryData.reserveLayer(data, 1);
419
420 InMemoryData.initDataState('write', data, 2, true);
421 InMemoryData.writeRecord('Query', 'index', 2);
422 InMemoryData.clearDataState();
423 expect(data.optimisticOrder).toEqual([2, 1]);
424
425 InMemoryData.initDataState('write', data, 1);
426 InMemoryData.writeRecord('Query', 'index', 1);
427 InMemoryData.clearDataState();
428 expect(data.optimisticOrder).toEqual([2]);
429
430 // Delete optimistic layer
431 InMemoryData.noopDataState(data, 2);
432 expect(data.optimisticOrder).toEqual([]);
433
434 InMemoryData.initDataState('read', data, null);
435 expect(InMemoryData.readRecord('Query', 'index')).toBe(1);
436 });
437
438 it('squashes when optimistic layers are replaced with actual data', () => {
439 InMemoryData.reserveLayer(data, 1);
440
441 InMemoryData.initDataState('write', data, 2, true);
442 InMemoryData.writeRecord('Query', 'index', 2);
443 InMemoryData.clearDataState();
444 expect(data.optimisticOrder).toEqual([2, 1]);
445
446 InMemoryData.initDataState('write', data, 1);
447 InMemoryData.writeRecord('Query', 'index', 1);
448 InMemoryData.clearDataState();
449 expect(data.optimisticOrder).toEqual([2]);
450
451 // Convert optimistic layer to commutative layer
452 InMemoryData.initDataState('write', data, 2);
453 InMemoryData.writeRecord('Query', 'index', 2);
454 InMemoryData.clearDataState();
455 expect(data.optimisticOrder).toEqual([]);
456
457 InMemoryData.initDataState('read', data, null);
458 expect(InMemoryData.readRecord('Query', 'index')).toBe(2);
459 });
460
461 it('prevents inspectFields from failing for uninitialised layers', () => {
462 InMemoryData.initDataState('write', data, null);
463 InMemoryData.writeRecord('Query', 'test', true);
464 InMemoryData.clearDataState();
465
466 InMemoryData.reserveLayer(data, 1);
467
468 InMemoryData.initDataState('read', data, null);
469 expect(InMemoryData.inspectFields('Query')).toEqual([
470 {
471 arguments: null,
472 fieldKey: 'test',
473 fieldName: 'test',
474 },
475 ]);
476 });
477
478 it('allows reserveLayer to be called repeatedly', () => {
479 InMemoryData.reserveLayer(data, 1);
480 InMemoryData.reserveLayer(data, 1);
481 expect(data.optimisticOrder).toEqual([1]);
482 expect([...data.commutativeKeys]).toEqual([1]);
483 });
484
485 it('allows reserveLayer to be called after registering an optimistc layer', () => {
486 InMemoryData.noopDataState(data, 1, true);
487 expect(data.optimisticOrder).toEqual([1]);
488 expect(data.commutativeKeys.size).toBe(0);
489
490 InMemoryData.reserveLayer(data, 1);
491 expect(data.optimisticOrder).toEqual([1]);
492 expect([...data.commutativeKeys]).toEqual([1]);
493 });
494});
495
496describe('deferred changes', () => {
497 it('keeps a deferred layer around until completion', () => {
498 // initially it's unknown whether a layer is deferred
499 InMemoryData.reserveLayer(data, 1, true);
500 InMemoryData.reserveLayer(data, 2);
501
502 InMemoryData.reserveLayer(data, 2);
503 InMemoryData.initDataState('write', data, 2);
504 InMemoryData.writeRecord('Query', 'index', 2);
505 InMemoryData.clearDataState();
506
507 InMemoryData.initDataState('read', data, null);
508 expect(InMemoryData.readRecord('Query', 'index')).toBe(2);
509
510 // The layers must not be squashed
511 expect(data.optimisticOrder).toEqual([2, 1]);
512
513 // A future response may then clear the layer
514 InMemoryData.reserveLayer(data, 1, false);
515 InMemoryData.initDataState('write', data, 1);
516 InMemoryData.writeRecord('Query', 'index', 1);
517 InMemoryData.clearDataState();
518
519 // The layers must then be squashed
520 expect(data.optimisticOrder).toEqual([]);
521 });
522
523 it('does not erase data from a prior deferred layer when updating it', () => {
524 // initially it's unknown whether a layer is deferred
525 InMemoryData.reserveLayer(data, 1, true);
526 InMemoryData.reserveLayer(data, 2, true);
527
528 InMemoryData.initDataState('write', data, 2);
529 InMemoryData.writeRecord('Query', 'index', 2);
530 InMemoryData.clearDataState();
531
532 InMemoryData.initDataState('read', data, null);
533 expect(InMemoryData.readRecord('Query', 'index')).toBe(2);
534
535 // A subsequent reserve layer call should not erase the layer
536 InMemoryData.reserveLayer(data, 2, true);
537 InMemoryData.initDataState('read', data, null);
538 expect(InMemoryData.readRecord('Query', 'index')).toBe(2);
539
540 // The layers must not be squashed
541 expect(data.optimisticOrder).toEqual([2, 1]);
542 });
543
544 it('keeps a deferred layer around even if it is the lowest', () => {
545 // initially it's unknown whether a layer is deferred
546 InMemoryData.reserveLayer(data, 1);
547 InMemoryData.reserveLayer(data, 2);
548 InMemoryData.reserveLayer(data, 3);
549
550 InMemoryData.initDataState('write', data, 2);
551 InMemoryData.writeRecord('Query', 'index', 2);
552 InMemoryData.clearDataState();
553
554 // Mark layer 3 as deferred
555 InMemoryData.reserveLayer(data, 3, true);
556
557 // The value is unchanged
558 InMemoryData.initDataState('read', data, null);
559 expect(InMemoryData.readRecord('Query', 'index')).toBe(2);
560
561 // The layers must not be squashed
562 expect(data.optimisticOrder).toEqual([3, 2, 1]);
563
564 // A future response may not clear the layer
565 InMemoryData.initDataState('write', data, 1);
566 InMemoryData.writeRecord('Query', 'index', 1);
567 InMemoryData.clearDataState();
568 expect(data.optimisticOrder).toEqual([3]);
569
570 // The layers must then be squashed
571 InMemoryData.noopDataState(data, 3, false);
572 expect(data.optimisticOrder).toEqual([]);
573 });
574
575 it('unmarks deferred layers when they receive a noop write', () => {
576 // initially it's unknown whether a layer is deferred
577 InMemoryData.reserveLayer(data, 1);
578 InMemoryData.reserveLayer(data, 2);
579
580 InMemoryData.reserveLayer(data, 2);
581 InMemoryData.initDataState('write', data, 2);
582 InMemoryData.writeRecord('Query', 'index', 2);
583 InMemoryData.clearDataState();
584
585 // The layer is marked as deferred via re-reserving it
586 InMemoryData.reserveLayer(data, 1, true);
587 InMemoryData.initDataState('write', data, 1);
588 InMemoryData.clearDataState();
589
590 // The layer is then receiving a noop write
591 InMemoryData.noopDataState(data, 1, false);
592 expect(data.optimisticOrder).toEqual([]);
593 });
594});