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.

(core) - Avoid dispatching an operation for already active cache-first/only operations (#1600)

authored by

Phil Pluckthun and committed by
GitHub
f02178f3 e51daa5d

+152 -22
+5
.changeset/large-frogs-tease.md
··· 1 + --- 2 + '@urql/core': minor 3 + --- 4 + 5 + With the "single-source behavior" the `Client` will now also avoid executing an operation if it's already active, has a previous result available, and is either run with the `cache-first` or `cache-only` request policies. This is similar to a "short circuiting" behavior, where unnecessary work is avoided by not issuing more operations into the exchange pipeline than expected.
+121 -13
packages/core/src/client.test.ts
··· 519 519 jest.useRealTimers(); 520 520 }); 521 521 522 - it('replays results from prior operation result as needed', async () => { 522 + it('replays results from prior operation result as needed (cache-first)', async () => { 523 523 const exchange: Exchange = () => ops$ => { 524 524 let i = 0; 525 525 return pipe( ··· 556 556 557 557 expect(resultTwo).toHaveBeenCalledWith({ 558 558 data: 1, 559 + operation: queryOperation, 560 + }); 561 + 562 + jest.advanceTimersByTime(1); 563 + 564 + // With cache-first we don't expect a new operation to be issued 565 + expect(resultTwo).toHaveBeenCalledTimes(1); 566 + }); 567 + 568 + it('replays results from prior operation result as needed (network-only)', async () => { 569 + const exchange: Exchange = () => ops$ => { 570 + let i = 0; 571 + return pipe( 572 + ops$, 573 + map(op => ({ 574 + data: ++i, 575 + operation: op, 576 + })), 577 + delay(1) 578 + ); 579 + }; 580 + 581 + const client = createClient({ 582 + url: 'test', 583 + exchanges: [exchange], 584 + }); 585 + 586 + const operation = makeOperation('query', queryOperation, { 587 + ...queryOperation.context, 588 + requestPolicy: 'network-only', 589 + }); 590 + 591 + const resultOne = jest.fn(); 592 + const resultTwo = jest.fn(); 593 + 594 + pipe(client.executeRequestOperation(operation), subscribe(resultOne)); 595 + 596 + expect(resultOne).toHaveBeenCalledTimes(0); 597 + 598 + jest.advanceTimersByTime(1); 599 + 600 + expect(resultOne).toHaveBeenCalledTimes(1); 601 + expect(resultOne).toHaveBeenCalledWith({ 602 + data: 1, 603 + operation, 604 + }); 605 + 606 + pipe(client.executeRequestOperation(operation), subscribe(resultTwo)); 607 + 608 + expect(resultTwo).toHaveBeenCalledWith({ 609 + data: 1, 610 + operation, 559 611 stale: true, 612 + }); 613 + 614 + jest.advanceTimersByTime(1); 615 + 616 + // With network-only we expect a new operation to be issued, hence a new result 617 + expect(resultTwo).toHaveBeenCalledTimes(2); 618 + 619 + expect(resultTwo).toHaveBeenCalledWith({ 620 + data: 2, 621 + operation, 622 + }); 623 + }); 624 + 625 + it('does not replay values from a past subscription', async () => { 626 + const exchange: Exchange = () => ops$ => { 627 + let i = 0; 628 + return pipe( 629 + ops$, 630 + filter(op => op.kind !== 'teardown'), 631 + map(op => ({ 632 + data: ++i, 633 + operation: op, 634 + })), 635 + delay(1) 636 + ); 637 + }; 638 + 639 + const client = createClient({ 640 + url: 'test', 641 + exchanges: [exchange], 642 + }); 643 + 644 + // We keep the source in-memory 645 + const source = client.executeRequestOperation(queryOperation); 646 + const resultOne = jest.fn(); 647 + let subscription; 648 + 649 + subscription = pipe(source, subscribe(resultOne)); 650 + 651 + expect(resultOne).toHaveBeenCalledTimes(0); 652 + jest.advanceTimersByTime(1); 653 + 654 + expect(resultOne).toHaveBeenCalledWith({ 655 + data: 1, 560 656 operation: queryOperation, 561 657 }); 562 658 659 + subscription.unsubscribe(); 660 + const resultTwo = jest.fn(); 661 + subscription = pipe(source, subscribe(resultTwo)); 662 + 663 + expect(resultTwo).toHaveBeenCalledTimes(0); 563 664 jest.advanceTimersByTime(1); 564 665 565 666 expect(resultTwo).toHaveBeenCalledWith({ ··· 586 687 exchanges: [exchange], 587 688 }); 588 689 690 + const operation = makeOperation('query', queryOperation, { 691 + ...queryOperation.context, 692 + requestPolicy: 'network-only', 693 + }); 694 + 589 695 const resultOne = jest.fn(); 590 696 const resultTwo = jest.fn(); 591 697 592 - pipe(client.executeRequestOperation(queryOperation), subscribe(resultOne)); 698 + pipe(client.executeRequestOperation(operation), subscribe(resultOne)); 699 + pipe(client.executeRequestOperation(operation), subscribe(resultTwo)); 593 700 594 - pipe(client.executeRequestOperation(queryOperation), subscribe(resultTwo)); 595 - 596 - expect(resultOne).toHaveBeenCalledTimes(1); 597 701 expect(resultTwo).toHaveBeenCalledTimes(1); 598 - 599 702 expect(resultTwo).toHaveBeenCalledWith({ 600 703 data: 1, 601 - operation: queryOperation, 704 + operation, 602 705 stale: true, 603 706 }); 604 707 }); ··· 628 731 expect(resultTwo).toHaveBeenCalledTimes(0); 629 732 }); 630 733 631 - it('skips replaying results when a result is emitted immediately', () => { 734 + it('skips replaying results when a result is emitted immediately (network-only)', () => { 632 735 const exchange: Exchange = () => ops$ => { 633 736 let i = 0; 634 737 return pipe( ··· 642 745 exchanges: [exchange], 643 746 }); 644 747 748 + const operation = makeOperation('query', queryOperation, { 749 + ...queryOperation.context, 750 + requestPolicy: 'network-only', 751 + }); 752 + 645 753 const resultOne = jest.fn(); 646 754 const resultTwo = jest.fn(); 647 755 648 - pipe(client.executeRequestOperation(queryOperation), subscribe(resultOne)); 756 + pipe(client.executeRequestOperation(operation), subscribe(resultOne)); 649 757 650 758 expect(resultOne).toHaveBeenCalledWith({ 651 759 data: 1, 652 - operation: queryOperation, 760 + operation, 653 761 }); 654 762 655 - pipe(client.executeRequestOperation(queryOperation), subscribe(resultTwo)); 763 + pipe(client.executeRequestOperation(operation), subscribe(resultTwo)); 656 764 657 765 expect(resultTwo).toHaveBeenCalledWith({ 658 766 data: 2, 659 - operation: queryOperation, 767 + operation, 660 768 }); 661 769 662 770 expect(resultOne).toHaveBeenCalledWith({ 663 771 data: 2, 664 - operation: queryOperation, 772 + operation, 665 773 }); 666 774 }); 667 775
+6 -1
packages/core/src/client.ts
··· 273 273 share 274 274 ); 275 275 } else { 276 + const mode = 277 + operation.context.requestPolicy === 'cache-and-network' || 278 + operation.context.requestPolicy === 'network-only' 279 + ? 'pre' 280 + : 'post'; 276 281 active = pipe( 277 282 result$, 278 - replayOnStart(() => { 283 + replayOnStart(mode, () => { 279 284 this.activeOperations.set(operation.key, active!); 280 285 this.dispatchOperation(operation); 281 286 })
+20 -8
packages/core/src/utils/streamUtils.ts
··· 20 20 return source$ as PromisifiedSource<T>; 21 21 } 22 22 23 + export type ReplayMode = 'pre' | 'post'; 24 + 23 25 export function replayOnStart<T extends OperationResult>( 24 - start?: () => void 26 + mode: ReplayMode, 27 + start: () => void 25 28 ): Operator<T, T> { 26 29 return source$ => { 27 30 let replay: T | void; 28 31 29 32 const shared$ = pipe( 30 33 source$, 34 + onEnd(() => { 35 + replay = undefined; 36 + }), 31 37 onPush(value => { 32 38 replay = value; 33 39 }), ··· 37 43 return make<T>(observer => { 38 44 const prevReplay = replay; 39 45 40 - const subscription = pipe( 46 + return pipe( 41 47 shared$, 42 48 onEnd(observer.complete), 43 49 onStart(() => { 44 - if (start) start(); 45 - if (prevReplay !== undefined && prevReplay === replay) 46 - observer.next({ ...prevReplay, stale: true }); 50 + if (mode === 'pre') { 51 + start(); 52 + } 53 + 54 + if (prevReplay !== undefined && prevReplay === replay) { 55 + observer.next( 56 + mode === 'pre' ? { ...prevReplay, stale: true } : prevReplay 57 + ); 58 + } else if (mode === 'post') { 59 + start(); 60 + } 47 61 }), 48 62 subscribe(observer.next) 49 - ); 50 - 51 - return subscription.unsubscribe; 63 + ).unsubscribe; 52 64 }); 53 65 }; 54 66 }