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(core): Deprecate the dedupExchange and absorb hasNext checks into Client (#3058)

authored by

Phil Pluckthun and committed by
GitHub
7222ee10 40911d2f

+50 -180
+10
.changeset/wise-cherries-juggle.md
··· 1 + --- 2 + '@urql/core': minor 3 + --- 4 + 5 + Deprecate the `dedupExchange`. The functionality of deduplicating queries and subscriptions has now been moved into and absorbed by the `Client`. 6 + 7 + Previously, the `Client` already started doing some work to share results between 8 + queries, and to avoid dispatching operations as needed. It now only dispatches operations 9 + strictly when the `dedupExchange` would allow so as well, moving its logic into the 10 + `Client`.
+1 -1
packages/core/src/client.test.ts
··· 909 909 return merge([ 910 910 pipe( 911 911 ops$, 912 - map(op => ({ data: 1, operation: op })), 912 + map(op => ({ hasNext: true, data: 1, operation: op })), 913 913 take(1) 914 914 ), 915 915 never,
+36 -22
packages/core/src/client.ts
··· 12 12 Source, 13 13 take, 14 14 takeUntil, 15 + takeWhile, 15 16 publish, 16 17 subscribe, 17 18 switchMap, ··· 566 567 567 568 // This subject forms the input of operations; executeOperation may be 568 569 // called to dispatch a new operation on the subject 569 - const { source: operations$, next: nextOperation } = makeSubject<Operation>(); 570 + const operations = makeSubject<Operation>(); 571 + 572 + function nextOperation(operation: Operation) { 573 + const prevReplay = replays.get(operation.key); 574 + if (operation.kind === 'mutation' || !prevReplay || !prevReplay.hasNext) 575 + operations.next(operation); 576 + } 570 577 571 578 // We define a queued dispatcher on the subject, which empties the queue when it's 572 579 // activated to allow `reexecuteOperation` to be trampoline-scheduled 573 580 let isOperationBatchActive = false; 574 581 function dispatchOperation(operation?: Operation | void) { 575 582 if (operation) nextOperation(operation); 583 + 576 584 if (!isOperationBatchActive) { 577 585 isOperationBatchActive = true; 578 586 while (isOperationBatchActive && (operation = queue.shift())) ··· 602 610 ); 603 611 } 604 612 613 + if (operation.kind !== 'query') { 614 + result$ = pipe( 615 + result$, 616 + onStart(() => { 617 + nextOperation(operation); 618 + }) 619 + ); 620 + } 621 + 605 622 // A mutation is always limited to just a single result and is never shared 606 623 if (operation.kind === 'mutation') { 607 - return pipe( 624 + return pipe(result$, take(1)); 625 + } 626 + 627 + if (operation.kind === 'subscription') { 628 + result$ = pipe( 608 629 result$, 609 - onStart(() => nextOperation(operation)), 610 - take(1) 630 + takeWhile(result => !!result.hasNext) 611 631 ); 612 632 } 613 633 614 - const source = pipe( 634 + return pipe( 615 635 result$, 616 636 // End the results stream when an active teardown event is sent 617 637 takeUntil( 618 638 pipe( 619 - operations$, 639 + operations.source, 620 640 filter(op => op.kind === 'teardown' && op.key === operation.key) 621 641 ) 622 642 ), ··· 629 649 fromValue(result), 630 650 // Mark a result as stale when a new operation is sent for it 631 651 pipe( 632 - operations$, 652 + operations.source, 633 653 filter( 634 654 op => 635 655 op.kind === 'query' && ··· 656 676 }), 657 677 share 658 678 ); 659 - 660 - return source; 661 679 }; 662 680 663 681 const instance: Client = 664 682 this instanceof Client ? this : Object.create(Client.prototype); 665 683 const client: Client = Object.assign(instance, { 666 684 suspense: !!opts.suspense, 667 - operations$, 685 + operations$: operations.source, 668 686 669 687 reexecuteOperation(operation: Operation) { 670 688 // Reexecute operation only if any subscribers are still subscribed to the ··· 708 726 709 727 return make<OperationResult>(observer => { 710 728 let source = active.get(operation.key); 711 - 712 729 if (!source) { 713 730 active.set(operation.key, (source = makeResultSource(operation))); 714 731 } 715 732 716 - const isNetworkOperation = 717 - operation.context.requestPolicy === 'cache-and-network' || 718 - operation.context.requestPolicy === 'network-only'; 719 - 720 733 return pipe( 721 734 source, 722 735 onStart(() => { 723 736 const prevReplay = replays.get(operation.key); 724 - 725 - if (operation.kind === 'subscription') { 726 - return dispatchOperation(operation); 737 + const isNetworkOperation = 738 + operation.context.requestPolicy === 'cache-and-network' || 739 + operation.context.requestPolicy === 'network-only'; 740 + if (operation.kind !== 'query') { 741 + return; 727 742 } else if (isNetworkOperation) { 728 743 dispatchOperation(operation); 744 + if (prevReplay && !prevReplay.hasNext) prevReplay.stale = true; 729 745 } 730 746 731 747 if ( 732 748 prevReplay != null && 733 749 prevReplay === replays.get(operation.key) 734 750 ) { 735 - observer.next( 736 - isNetworkOperation ? { ...prevReplay, stale: true } : prevReplay 737 - ); 751 + observer.next(prevReplay); 738 752 } else if (!isNetworkOperation) { 739 753 dispatchOperation(operation); 740 754 } ··· 824 838 client, 825 839 dispatchDebug, 826 840 forward: fallbackExchange({ dispatchDebug }), 827 - })(operations$) 841 + })(operations.source) 828 842 ); 829 843 830 844 // Prevent the `results$` exchange pipeline from being closed by active
-104
packages/core/src/exchanges/dedup.test.ts
··· 1 - import { 2 - filter, 3 - makeSubject, 4 - map, 5 - pipe, 6 - publish, 7 - Source, 8 - Subject, 9 - } from 'wonka'; 10 - import { vi, expect, it, beforeEach } from 'vitest'; 11 - 12 - import { 13 - mutationOperation, 14 - queryOperation, 15 - queryResponse, 16 - } from '../test-utils'; 17 - import { Operation } from '../types'; 18 - import { dedupExchange } from './dedup'; 19 - import { makeOperation } from '../utils'; 20 - 21 - const dispatchDebug = vi.fn(); 22 - let shouldRespond = false; 23 - let exchangeArgs; 24 - let forwardedOperations: Operation[]; 25 - let input: Subject<Operation>; 26 - 27 - beforeEach(() => { 28 - shouldRespond = false; 29 - forwardedOperations = []; 30 - input = makeSubject<Operation>(); 31 - 32 - // Collect all forwarded operations 33 - const forward = (s: Source<Operation>) => { 34 - return pipe( 35 - s, 36 - map(op => { 37 - forwardedOperations.push(op); 38 - return queryResponse; 39 - }), 40 - filter(() => !!shouldRespond) 41 - ); 42 - }; 43 - 44 - exchangeArgs = { forward, client: {}, dispatchDebug }; 45 - }); 46 - 47 - it('forwards query operations correctly', async () => { 48 - const { source: ops$, next, complete } = input; 49 - const exchange = dedupExchange(exchangeArgs)(ops$); 50 - 51 - publish(exchange); 52 - next(queryOperation); 53 - complete(); 54 - expect(forwardedOperations.length).toBe(1); 55 - }); 56 - 57 - it('forwards only non-pending query operations', async () => { 58 - shouldRespond = false; // We filter out our mock responses 59 - const { source: ops$, next, complete } = input; 60 - const exchange = dedupExchange(exchangeArgs)(ops$); 61 - 62 - publish(exchange); 63 - next(queryOperation); 64 - next(queryOperation); 65 - complete(); 66 - expect(forwardedOperations.length).toBe(1); 67 - }); 68 - 69 - it('forwards duplicate query operations as usual after they respond', async () => { 70 - shouldRespond = true; // Response will immediately resolve 71 - const { source: ops$, next, complete } = input; 72 - const exchange = dedupExchange(exchangeArgs)(ops$); 73 - 74 - publish(exchange); 75 - next(queryOperation); 76 - next(queryOperation); 77 - complete(); 78 - expect(forwardedOperations.length).toBe(2); 79 - }); 80 - 81 - it('forwards duplicate query operations after one was torn down', async () => { 82 - shouldRespond = false; // We filter out our mock responses 83 - const { source: ops$, next, complete } = input; 84 - const exchange = dedupExchange(exchangeArgs)(ops$); 85 - 86 - publish(exchange); 87 - next(queryOperation); 88 - next(makeOperation('teardown', queryOperation, queryOperation.context)); 89 - next(queryOperation); 90 - complete(); 91 - expect(forwardedOperations.length).toBe(3); 92 - }); 93 - 94 - it('always forwards mutation operations without deduplicating them', async () => { 95 - shouldRespond = false; // We filter out our mock responses 96 - const { source: ops$, next, complete } = input; 97 - const exchange = dedupExchange(exchangeArgs)(ops$); 98 - 99 - publish(exchange); 100 - next(mutationOperation); 101 - next(mutationOperation); 102 - complete(); 103 - expect(forwardedOperations.length).toBe(2); 104 - });
+3 -53
packages/core/src/exchanges/dedup.ts
··· 1 - import { filter, pipe, tap } from 'wonka'; 2 1 import { Exchange } from '../types'; 3 2 4 3 /** Default deduplication exchange. 5 - * 6 - * @remarks 7 - * The `dedupExchange` deduplicates queries and subscriptions that are 8 - * started with identical documents and variables by deduplicating by 9 - * their {@link Operation.key}. 10 - * This can prevent duplicate requests from being sent to your GraphQL API. 11 - * 12 - * Because this is a very safe exchange to add to any GraphQL setup, it’s 13 - * not only the default, but we also recommend you to always keep this 14 - * exchange added and included in your setup. 15 - * 16 - * Hint: In React and Vue, some common usage patterns can trigger duplicate 17 - * operations. For instance, in React a single render will actually 18 - * trigger two phases that execute an {@link Operation}. 4 + * @deprecated 5 + * This exchange's functionality is now built into the {@link Client}. 19 6 */ 20 - export const dedupExchange: Exchange = ({ forward, dispatchDebug }) => { 21 - const inFlightKeys = new Set<number>(); 22 - return ops$ => 23 - pipe( 24 - forward( 25 - pipe( 26 - ops$, 27 - filter(operation => { 28 - if ( 29 - operation.kind === 'teardown' || 30 - operation.kind === 'mutation' 31 - ) { 32 - inFlightKeys.delete(operation.key); 33 - return true; 34 - } 35 - 36 - const isInFlight = inFlightKeys.has(operation.key); 37 - inFlightKeys.add(operation.key); 38 - 39 - if (isInFlight) { 40 - dispatchDebug({ 41 - type: 'dedup', 42 - message: 'An operation has been deduped.', 43 - operation, 44 - }); 45 - } 46 - 47 - return !isInFlight; 48 - }) 49 - ) 50 - ), 51 - tap(result => { 52 - if (!result.hasNext) { 53 - inFlightKeys.delete(result.operation.key); 54 - } 55 - }) 56 - ); 57 - }; 7 + export const dedupExchange: Exchange = ({ forward }) => ops$ => forward(ops$);