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.

fix(core): Strictly deduplicate operations for cache-and-network and network-only (#3157)

authored by

Phil Pluckthun and committed by
GitHub
1fd479c4 fe4e08a6

+58 -61
+5
.changeset/good-coins-know.md
··· 1 + --- 2 + '@urql/core': patch 3 + --- 4 + 5 + Strictly deduplicate `cache-and-network` and `network-only` operations, while a non-stale response is being waited for.
+50 -58
packages/core/src/client.test.ts
··· 497 497 498 498 unsubscribe(); 499 499 }); 500 - 501 - it('does not reemit previous results as stale if it was marked as stale already', async () => { 502 - const output: OperationResult[] = []; 503 - const exchange: Exchange = () => ops$ => { 504 - return pipe( 505 - ops$, 506 - filter(op => op.kind !== 'teardown'), 507 - map(op => ({ 508 - data: 1, 509 - operation: op, 510 - stale: true, 511 - hasNext: false, 512 - })), 513 - delay(1) 514 - ); 515 - }; 516 - 517 - const client = createClient({ 518 - url: 'test', 519 - exchanges: [exchange], 520 - }); 521 - 522 - const { unsubscribe } = pipe( 523 - client.executeRequestOperation(queryOperation), 524 - subscribe(result => { 525 - output.push(result); 526 - }) 527 - ); 528 - 529 - vi.advanceTimersByTime(1); 530 - 531 - expect(output.length).toBe(1); 532 - expect(output[0]).toHaveProperty('operation.key', queryOperation.key); 533 - expect(output[0]).toHaveProperty( 534 - 'operation.context.requestPolicy', 535 - 'cache-first' 536 - ); 537 - 538 - client.reexecuteOperation( 539 - makeOperation(queryOperation.kind, queryOperation, { 540 - ...queryOperation.context, 541 - requestPolicy: 'network-only', 542 - }) 543 - ); 544 - 545 - await Promise.resolve(); 546 - vi.advanceTimersByTime(1); 547 - 548 - expect(output.length).toBe(2); 549 - expect(output[1]).toHaveProperty('stale', true); 550 - expect(output[1]).toHaveProperty('operation.key', queryOperation.key); 551 - expect(output[1]).toHaveProperty( 552 - 'operation.context.requestPolicy', 553 - 'network-only' 554 - ); 555 - 556 - unsubscribe(); 557 - }); 558 500 }); 559 501 560 502 describe('deduplication behavior', () => { ··· 655 597 expect(resultOne).toHaveBeenCalledTimes(1); 656 598 expect(resultTwo).toHaveBeenCalledTimes(1); 657 599 expect(onOperation).toHaveBeenCalledTimes(1); 600 + }); 601 + 602 + it('deduplicates otherwise if operation has already been sent', () => { 603 + const onOperation = vi.fn(); 604 + const onResult = vi.fn(); 605 + 606 + let hasSent = false; 607 + const exchange: Exchange = () => ops$ => 608 + pipe( 609 + ops$, 610 + onPush(onOperation), 611 + map(op => ({ 612 + hasNext: false, 613 + stale: false, 614 + data: 'test', 615 + operation: op, 616 + })), 617 + filter(() => { 618 + return hasSent ? false : (hasSent = true); 619 + }), 620 + delay(1) 621 + ); 622 + 623 + const client = createClient({ 624 + url: 'test', 625 + exchanges: [exchange], 626 + }); 627 + 628 + const operationOne = makeOperation('query', queryOperation, { 629 + ...queryOperation.context, 630 + requestPolicy: 'cache-first', 631 + }); 632 + 633 + const operationTwo = makeOperation('query', queryOperation, { 634 + ...queryOperation.context, 635 + requestPolicy: 'network-only', 636 + }); 637 + 638 + const operationThree = makeOperation('query', queryOperation, { 639 + ...queryOperation.context, 640 + requestPolicy: 'network-only', 641 + }); 642 + 643 + pipe(client.executeRequestOperation(operationOne), subscribe(onResult)); 644 + pipe(client.executeRequestOperation(operationTwo), subscribe(onResult)); 645 + pipe(client.executeRequestOperation(operationThree), subscribe(onResult)); 646 + vi.advanceTimersByTime(1); 647 + 648 + expect(onOperation).toHaveBeenCalledTimes(1); 649 + expect(onResult).toHaveBeenCalledTimes(3); 658 650 }); 659 651 }); 660 652
+3 -3
packages/core/src/client.ts
··· 579 579 const operations = makeSubject<Operation>(); 580 580 581 581 function nextOperation(operation: Operation) { 582 - const prevReplay = replays.get(operation.key); 583 582 if ( 584 583 operation.kind === 'mutation' || 585 584 operation.kind === 'teardown' || 586 - (prevReplay ? !prevReplay.hasNext : !dispatched.has(operation.key)) 585 + !dispatched.has(operation.key) 587 586 ) { 588 587 if (operation.kind === 'teardown') { 589 588 dispatched.delete(operation.key); ··· 664 663 result$, 665 664 // Store replay result 666 665 onPush(result => { 667 - dispatched.delete(operation.key); 666 + if (!result.hasNext && !result.stale) 667 + dispatched.delete(operation.key); 668 668 replays.set(operation.key, result); 669 669 }), 670 670 // Cleanup active states on end of source