Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
at main 594 lines 20 kB view raw
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});