Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(graphcache): only add dependency if we are changing the value (#3564)

authored by

Jovi De Croock and committed by
GitHub
b73de94f 05c1a0f4

+210 -12
+5
.changeset/nine-walls-behave.md
··· 1 + --- 2 + '@urql/exchange-graphcache': patch 3 + --- 4 + 5 + Only record dependencies that are changing data, this will reduce the amount of operations we re-invoke due to network-only/cache-and-network queries and mutations
+165 -2
exchanges/graphcache/src/cacheExchange.test.ts
··· 480 480 ); 481 481 }); 482 482 483 + it('does not notify related queries when a mutation update does not change the data', () => { 484 + vi.useFakeTimers(); 485 + 486 + const balanceFragment = gql` 487 + fragment BalanceFragment on Author { 488 + id 489 + balance { 490 + amount 491 + } 492 + } 493 + `; 494 + 495 + const queryById = gql` 496 + query ($id: ID!) { 497 + author(id: $id) { 498 + id 499 + name 500 + ...BalanceFragment 501 + } 502 + } 503 + 504 + ${balanceFragment} 505 + `; 506 + 507 + const queryByIdDataA = { 508 + __typename: 'Query', 509 + author: { 510 + __typename: 'Author', 511 + id: '1', 512 + name: 'Author 1', 513 + balance: { 514 + __typename: 'Balance', 515 + amount: 100, 516 + }, 517 + }, 518 + }; 519 + 520 + const queryByIdDataB = { 521 + __typename: 'Query', 522 + author: { 523 + __typename: 'Author', 524 + id: '2', 525 + name: 'Author 2', 526 + balance: { 527 + __typename: 'Balance', 528 + amount: 200, 529 + }, 530 + }, 531 + }; 532 + 533 + const mutation = gql` 534 + mutation ($userId: ID!, $amount: Int!) { 535 + updateBalance(userId: $userId, amount: $amount) { 536 + userId 537 + balance { 538 + amount 539 + } 540 + } 541 + } 542 + `; 543 + 544 + const mutationData = { 545 + __typename: 'Mutation', 546 + updateBalance: { 547 + __typename: 'UpdateBalanceResult', 548 + userId: '1', 549 + balance: { 550 + __typename: 'Balance', 551 + amount: 100, 552 + }, 553 + }, 554 + }; 555 + 556 + const client = createClient({ 557 + url: 'http://0.0.0.0', 558 + exchanges: [], 559 + }); 560 + const { source: ops$, next } = makeSubject<Operation>(); 561 + 562 + const reexec = vi 563 + .spyOn(client, 'reexecuteOperation') 564 + .mockImplementation(next); 565 + 566 + const opOne = client.createRequestOperation('query', { 567 + key: 1, 568 + query: queryById, 569 + variables: { id: 1 }, 570 + }); 571 + 572 + const opTwo = client.createRequestOperation('query', { 573 + key: 2, 574 + query: queryById, 575 + variables: { id: 2 }, 576 + }); 577 + 578 + const opMutation = client.createRequestOperation('mutation', { 579 + key: 3, 580 + query: mutation, 581 + variables: { userId: '1', amount: 1000 }, 582 + }); 583 + 584 + const response = vi.fn((forwardOp: Operation): OperationResult => { 585 + if (forwardOp.key === 1) { 586 + return { ...queryResponse, operation: opOne, data: queryByIdDataA }; 587 + } else if (forwardOp.key === 2) { 588 + return { ...queryResponse, operation: opTwo, data: queryByIdDataB }; 589 + } else if (forwardOp.key === 3) { 590 + return { 591 + ...queryResponse, 592 + operation: opMutation, 593 + data: mutationData, 594 + }; 595 + } 596 + 597 + return undefined as any; 598 + }); 599 + 600 + const result = vi.fn(); 601 + const forward: ExchangeIO = ops$ => 602 + pipe(ops$, delay(1), map(response), share); 603 + 604 + const updates = { 605 + Mutation: { 606 + updateBalance: vi.fn((result, _args, cache) => { 607 + const { 608 + updateBalance: { userId, balance }, 609 + } = result; 610 + cache.writeFragment(balanceFragment, { id: userId, balance }); 611 + }), 612 + }, 613 + }; 614 + 615 + const keys = { 616 + Balance: () => null, 617 + }; 618 + 619 + pipe( 620 + cacheExchange({ updates, keys })({ forward, client, dispatchDebug })( 621 + ops$ 622 + ), 623 + tap(result), 624 + publish 625 + ); 626 + 627 + next(opTwo); 628 + vi.runAllTimers(); 629 + expect(response).toHaveBeenCalledTimes(1); 630 + 631 + next(opOne); 632 + vi.runAllTimers(); 633 + expect(response).toHaveBeenCalledTimes(2); 634 + 635 + next(opMutation); 636 + vi.runAllTimers(); 637 + 638 + expect(response).toHaveBeenCalledTimes(3); 639 + expect(updates.Mutation.updateBalance).toHaveBeenCalledTimes(1); 640 + 641 + expect(reexec).toHaveBeenCalledTimes(0); 642 + }); 643 + 483 644 it('does nothing when no related queries have changed', () => { 484 645 const queryUnrelated = gql` 485 646 { ··· 1448 1609 1449 1610 expect(response).toHaveBeenCalledTimes(1); 1450 1611 expect(optimistic.concealAuthor).toHaveBeenCalledTimes(2); 1451 - expect(reexec).toHaveBeenCalledTimes(2); 1612 + expect(reexec).toHaveBeenCalledTimes(1); 1452 1613 expect(result).toHaveBeenCalledTimes(0); 1453 1614 1454 1615 vi.advanceTimersByTime(2); 1455 1616 expect(response).toHaveBeenCalledTimes(2); 1456 - expect(result).toHaveBeenCalledTimes(0); 1617 + expect(reexec).toHaveBeenCalledTimes(2); 1618 + expect(result).toHaveBeenCalledTimes(1); 1457 1619 1458 1620 vi.runAllTimers(); 1459 1621 expect(response).toHaveBeenCalledTimes(3); 1622 + expect(reexec).toHaveBeenCalledTimes(2); 1460 1623 expect(result).toHaveBeenCalledTimes(2); 1461 1624 }); 1462 1625
+1
exchanges/graphcache/src/operations/write.ts
··· 343 343 ? InMemoryData.readLink(entityKey || typename, fieldKey) 344 344 : undefined 345 345 ); 346 + 346 347 InMemoryData.writeLink(entityKey || typename, fieldKey, link); 347 348 } else { 348 349 writeField(ctx, getSelectionSet(node), ensureData(fieldValue));
+3 -1
exchanges/graphcache/src/store/data.test.ts
··· 98 98 InMemoryData.gc(); 99 99 100 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 101 103 expect(InMemoryData.getCurrentDependencies()).toEqual( 102 - new Set(['Query.todo', 'Todo:1']) 104 + new Set(['Query.todo']) 103 105 ); 104 106 }); 105 107
+36 -9
exchanges/graphcache/src/store/data.ts
··· 458 458 entityKey: string, 459 459 fieldKey: string 460 460 ): EntityField => { 461 - updateDependencies(entityKey, fieldKey); 461 + if (currentOperation === 'read') { 462 + updateDependencies(entityKey, fieldKey); 463 + } 462 464 return getNode(currentData!.records, entityKey, fieldKey); 463 465 }; 464 466 ··· 467 469 entityKey: string, 468 470 fieldKey: string 469 471 ): Link | undefined => { 470 - updateDependencies(entityKey, fieldKey); 472 + if (currentOperation === 'read') { 473 + updateDependencies(entityKey, fieldKey); 474 + } 471 475 return getNode(currentData!.links, entityKey, fieldKey); 472 476 }; 473 477 ··· 508 512 fieldKey: string, 509 513 value?: EntityField 510 514 ) => { 511 - updateDependencies(entityKey, fieldKey); 512 - updatePersist(entityKey, fieldKey); 515 + const existing = getNode(currentData!.records, entityKey, fieldKey); 516 + if (!isEqualLinkOrScalar(existing, value)) { 517 + updateDependencies(entityKey, fieldKey); 518 + updatePersist(entityKey, fieldKey); 519 + } 520 + 513 521 setNode(currentData!.records, entityKey, fieldKey, value); 514 522 }; 515 523 ··· 533 541 updateRCForLink(entityLinks && entityLinks[fieldKey], -1); 534 542 updateRCForLink(link, 1); 535 543 } 536 - // Update persistence batch and dependencies 537 - updateDependencies(entityKey, fieldKey); 538 - updatePersist(entityKey, fieldKey); 544 + const existing = getNode(currentData!.links, entityKey, fieldKey); 545 + if (!isEqualLinkOrScalar(existing, link)) { 546 + updateDependencies(entityKey, fieldKey); 547 + updatePersist(entityKey, fieldKey); 548 + } 549 + 539 550 // Update the link 540 551 setNode(currentData!.links, entityKey, fieldKey, link); 541 552 }; ··· 629 640 for (const entry of links.entries()) { 630 641 const entityKey = entry[0]; 631 642 const keyMap = entry[1]; 632 - for (const fieldKey in keyMap) 643 + for (const fieldKey in keyMap) { 633 644 writeLink(entityKey, fieldKey, keyMap[fieldKey]); 645 + } 634 646 } 635 647 } 636 648 ··· 639 651 for (const entry of records.entries()) { 640 652 const entityKey = entry[0]; 641 653 const keyMap = entry[1]; 642 - for (const fieldKey in keyMap) 654 + for (const fieldKey in keyMap) { 643 655 writeRecord(entityKey, fieldKey, keyMap[fieldKey]); 656 + } 644 657 } 645 658 } 646 659 ··· 710 723 data.hydrating = false; 711 724 clearDataState(); 712 725 }; 726 + 727 + function isEqualLinkOrScalar( 728 + a: Link | EntityField | undefined, 729 + b: Link | EntityField | undefined 730 + ) { 731 + if (typeof a !== typeof b) return false; 732 + if (a !== b) return false; 733 + if (Array.isArray(a) && Array.isArray(b)) { 734 + if (a.length !== b.length) return false; 735 + return !a.some((el, index) => el !== b[index]); 736 + } 737 + 738 + return true; 739 + }