Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
at main 33 kB view raw
1import { print } from '@0no-co/graphql.web'; 2import { vi, expect, it, beforeEach, describe, afterEach } from 'vitest'; 3 4/** NOTE: Testing in this file is designed to test both the client and its interaction with default Exchanges */ 5 6import { 7 Source, 8 delay, 9 map, 10 never, 11 pipe, 12 merge, 13 subscribe, 14 publish, 15 filter, 16 share, 17 toArray, 18 toPromise, 19 onPush, 20 tap, 21 take, 22 fromPromise, 23 fromValue, 24 mergeMap, 25} from 'wonka'; 26 27import { gql } from './gql'; 28import { Exchange, Operation, OperationResult } from './types'; 29import { makeOperation } from './utils'; 30import { Client, createClient } from './client'; 31import { 32 mutationOperation, 33 queryOperation, 34 subscriptionOperation, 35} from './test-utils'; 36 37const url = 'https://hostname.com'; 38 39describe('createClient / Client', () => { 40 it('creates an instance of Client', () => { 41 expect(createClient({ url, exchanges: [] }) instanceof Client).toBeTruthy(); 42 expect(new Client({ url, exchanges: [] }) instanceof Client).toBeTruthy(); 43 }); 44 45 it('passes snapshot', () => { 46 const client = createClient({ url, exchanges: [] }); 47 expect(client).toMatchSnapshot(); 48 }); 49}); 50 51const query = { 52 key: 1, 53 query: gql` 54 { 55 todos { 56 id 57 } 58 } 59 `, 60 variables: { example: 1234 }, 61}; 62 63const mutation = { 64 key: 1, 65 query: gql` 66 mutation { 67 todos { 68 id 69 } 70 } 71 `, 72 variables: { example: 1234 }, 73}; 74 75const subscription = { 76 key: 1, 77 query: gql` 78 subscription { 79 todos { 80 id 81 } 82 } 83 `, 84 variables: { example: 1234 }, 85}; 86 87let receivedOps: Operation[] = []; 88let client = createClient({ 89 url: '1234', 90 exchanges: [], 91}); 92const receiveMock = vi.fn((s: Source<Operation>) => 93 pipe( 94 s, 95 tap(op => (receivedOps = [...receivedOps, op])), 96 map(op => ({ operation: op })) 97 ) 98); 99const exchangeMock = vi.fn(() => receiveMock); 100 101beforeEach(() => { 102 receivedOps = []; 103 exchangeMock.mockClear(); 104 receiveMock.mockClear(); 105 client = createClient({ 106 url, 107 exchanges: [exchangeMock] as any[], 108 requestPolicy: 'cache-and-network', 109 }); 110}); 111 112describe('exchange args', () => { 113 it('receives forward function', () => { 114 // @ts-ignore 115 expect(typeof exchangeMock.mock.calls[0][0].forward).toBe('function'); 116 }); 117 118 it('receives client', () => { 119 // @ts-ignore 120 expect(exchangeMock.mock.calls[0][0]).toHaveProperty('client', client); 121 }); 122}); 123 124describe('promisified methods', () => { 125 it('query', () => { 126 const queryResult = client 127 .query( 128 gql` 129 { 130 todos { 131 id 132 } 133 } 134 `, 135 { example: 1234 }, 136 { requestPolicy: 'cache-only' } 137 ) 138 .toPromise(); 139 140 const received = receivedOps[0]; 141 expect(print(received.query)).toEqual(print(query.query)); 142 expect(received.key).toBeDefined(); 143 expect(received.variables).toEqual({ example: 1234 }); 144 expect(received.kind).toEqual('query'); 145 expect(received.context).toEqual({ 146 url: 'https://hostname.com', 147 requestPolicy: 'cache-only', 148 fetchOptions: undefined, 149 fetch: undefined, 150 suspense: false, 151 preferGetMethod: 'within-url-limit', 152 }); 153 expect(queryResult).toHaveProperty('then'); 154 }); 155 156 it('mutation', () => { 157 const mut = gql` 158 mutation { 159 todos { 160 id 161 } 162 } 163 `; 164 const mutationResult = client.mutation(mut, { example: 1234 }).toPromise(); 165 166 const received = receivedOps[0]; 167 expect(print(received.query)).toEqual(print(mut)); 168 expect(received.key).toBeDefined(); 169 expect(received.variables).toEqual({ example: 1234 }); 170 expect(received.kind).toEqual('mutation'); 171 expect(received.context).toMatchObject({ 172 url: 'https://hostname.com', 173 requestPolicy: 'cache-and-network', 174 fetchOptions: undefined, 175 fetch: undefined, 176 suspense: false, 177 preferGetMethod: 'within-url-limit', 178 }); 179 expect(mutationResult).toHaveProperty('then'); 180 }); 181}); 182 183describe('synchronous methods', () => { 184 it('readQuery', () => { 185 const result = client.readQuery( 186 gql` 187 { 188 todos { 189 id 190 } 191 } 192 `, 193 { example: 1234 } 194 ); 195 196 expect(receivedOps.length).toBe(2); 197 expect(receivedOps[0].kind).toBe('query'); 198 expect(receivedOps[1].kind).toBe('teardown'); 199 expect(result).toEqual({ 200 operation: { 201 ...query, 202 context: expect.anything(), 203 key: expect.any(Number), 204 kind: 'query', 205 }, 206 }); 207 }); 208}); 209 210describe('executeQuery', () => { 211 it('passes query string exchange', () => { 212 pipe( 213 client.executeQuery(query), 214 subscribe(x => x) 215 ); 216 217 const receivedQuery = receivedOps[0].query; 218 expect(print(receivedQuery)).toBe(print(query.query)); 219 }); 220 221 it('should throw when passing in a mutation', () => { 222 try { 223 client.executeQuery(mutation); 224 expect(true).toBeFalsy(); 225 } catch (e: any) { 226 expect(e.message).toMatchInlineSnapshot( 227 `"Expected operation of type "query" but found "mutation""` 228 ); 229 } 230 }); 231 232 it('passes variables type to exchange', () => { 233 pipe( 234 client.executeQuery(query), 235 subscribe(x => x) 236 ); 237 238 expect(receivedOps[0]).toHaveProperty('variables', query.variables); 239 }); 240 241 it('passes requestPolicy to exchange', () => { 242 pipe( 243 client.executeQuery(query), 244 subscribe(x => x) 245 ); 246 247 expect(receivedOps[0].context).toHaveProperty( 248 'requestPolicy', 249 'cache-and-network' 250 ); 251 }); 252 253 it('allows overriding the requestPolicy', () => { 254 pipe( 255 client.executeQuery(query, { requestPolicy: 'cache-first' }), 256 subscribe(x => x) 257 ); 258 259 expect(receivedOps[0].context).toHaveProperty( 260 'requestPolicy', 261 'cache-first' 262 ); 263 }); 264 265 it('passes kind type to exchange', () => { 266 pipe( 267 client.executeQuery(query), 268 subscribe(x => x) 269 ); 270 271 expect(receivedOps[0]).toHaveProperty('kind', 'query'); 272 }); 273 274 it('passes url (from context) to exchange', () => { 275 pipe( 276 client.executeQuery(query), 277 subscribe(x => x) 278 ); 279 280 expect(receivedOps[0]).toHaveProperty('context.url', url); 281 }); 282}); 283 284describe('executeMutation', () => { 285 it('passes query string exchange', async () => { 286 pipe( 287 client.executeMutation(mutation), 288 subscribe(x => x) 289 ); 290 291 const receivedQuery = receivedOps[0].query; 292 expect(print(receivedQuery)).toBe(print(mutation.query)); 293 }); 294 295 it('passes variables type to exchange', () => { 296 pipe( 297 client.executeMutation(mutation), 298 subscribe(x => x) 299 ); 300 301 expect(receivedOps[0]).toHaveProperty('variables', query.variables); 302 }); 303 304 it('passes kind type to exchange', () => { 305 pipe( 306 client.executeMutation(mutation), 307 subscribe(x => x) 308 ); 309 310 expect(receivedOps[0]).toHaveProperty('kind', 'mutation'); 311 }); 312 313 it('passes url (from context) to exchange', () => { 314 pipe( 315 client.executeMutation(mutation), 316 subscribe(x => x) 317 ); 318 319 expect(receivedOps[0]).toHaveProperty('context.url', url); 320 }); 321}); 322 323describe('executeSubscription', () => { 324 it('passes query string exchange', async () => { 325 pipe( 326 client.executeSubscription(subscription), 327 subscribe(x => x) 328 ); 329 330 const receivedQuery = receivedOps[0].query; 331 expect(print(receivedQuery)).toBe(print(subscription.query)); 332 }); 333 334 it('passes variables type to exchange', () => { 335 pipe( 336 client.executeSubscription(subscription), 337 subscribe(x => x) 338 ); 339 340 expect(receivedOps[0]).toHaveProperty('variables', subscription.variables); 341 }); 342 343 it('passes kind type to exchange', () => { 344 pipe( 345 client.executeSubscription(subscription), 346 subscribe(x => x) 347 ); 348 349 expect(receivedOps[0]).toHaveProperty('kind', 'subscription'); 350 }); 351}); 352 353describe('queuing behavior', () => { 354 beforeEach(() => { 355 vi.useFakeTimers(); 356 }); 357 358 afterEach(() => { 359 vi.useRealTimers(); 360 }); 361 362 it('queues reexecuteOperation, which dispatchOperation consumes', () => { 363 const output: Array<Operation | OperationResult> = []; 364 365 const exchange: Exchange = 366 ({ client }) => 367 ops$ => { 368 return pipe( 369 ops$, 370 filter(op => op.kind !== 'teardown'), 371 tap(op => { 372 output.push(op); 373 if ( 374 op.key === queryOperation.key && 375 op.context.requestPolicy !== 'network-only' 376 ) { 377 client.reexecuteOperation({ 378 ...op, 379 context: { 380 ...op.context, 381 requestPolicy: 'network-only', 382 }, 383 }); 384 } 385 }), 386 map(op => ({ 387 stale: false, 388 hasNext: false, 389 data: op.key, 390 operation: op, 391 })) 392 ); 393 }; 394 395 const client = createClient({ 396 url: 'test', 397 exchanges: [exchange], 398 }); 399 400 const shared = pipe( 401 client.executeRequestOperation(queryOperation), 402 onPush(result => output.push(result)), 403 share 404 ); 405 406 const results = pipe(shared, toArray); 407 pipe(shared, publish); 408 409 expect(output.length).toBe(8); 410 expect(results.length).toBe(2); 411 412 expect(output[0]).toHaveProperty('key', queryOperation.key); 413 expect(output[0]).toHaveProperty('context.requestPolicy', 'cache-first'); 414 415 expect(output[1]).toHaveProperty('operation.key', queryOperation.key); 416 expect(output[1]).toHaveProperty( 417 'operation.context.requestPolicy', 418 'cache-first' 419 ); 420 421 expect(output[2]).toHaveProperty('key', queryOperation.key); 422 expect(output[2]).toHaveProperty('context.requestPolicy', 'network-only'); 423 424 expect(output[3]).toHaveProperty('operation.key', queryOperation.key); 425 expect(output[3]).toHaveProperty( 426 'operation.context.requestPolicy', 427 'network-only' 428 ); 429 430 expect(output[1]).toBe(results[0]); 431 expect(output[3]).toBe(results[1]); 432 }); 433 434 it('reemits previous results as stale if the operation is reexecuted as network-only', async () => { 435 const output: OperationResult[] = []; 436 437 const exchange: Exchange = () => { 438 let countRes = 0; 439 return ops$ => { 440 return pipe( 441 ops$, 442 filter(op => op.kind !== 'teardown'), 443 map(op => ({ 444 hasNext: false, 445 stale: false, 446 data: ++countRes, 447 operation: op, 448 })), 449 delay(1) 450 ); 451 }; 452 }; 453 454 const client = createClient({ 455 url: 'test', 456 exchanges: [exchange], 457 }); 458 459 const { unsubscribe } = pipe( 460 client.executeRequestOperation(queryOperation), 461 subscribe(result => { 462 output.push(result); 463 }) 464 ); 465 466 vi.advanceTimersByTime(1); 467 468 expect(output.length).toBe(1); 469 expect(output[0]).toHaveProperty('data', 1); 470 expect(output[0]).toHaveProperty('operation.key', queryOperation.key); 471 expect(output[0]).toHaveProperty( 472 'operation.context.requestPolicy', 473 'cache-first' 474 ); 475 476 client.reexecuteOperation( 477 makeOperation(queryOperation.kind, queryOperation, { 478 ...queryOperation.context, 479 requestPolicy: 'network-only', 480 }) 481 ); 482 483 await Promise.resolve(); 484 485 expect(output.length).toBe(2); 486 expect(output[1]).toHaveProperty('data', 1); 487 expect(output[1]).toHaveProperty('stale', true); 488 expect(output[1]).toHaveProperty('operation.key', queryOperation.key); 489 expect(output[1]).toHaveProperty( 490 'operation.context.requestPolicy', 491 'cache-first' 492 ); 493 494 vi.advanceTimersByTime(1); 495 496 expect(output.length).toBe(3); 497 expect(output[2]).toHaveProperty('data', 2); 498 expect(output[2]).toHaveProperty('stale', false); 499 expect(output[2]).toHaveProperty('operation.key', queryOperation.key); 500 expect(output[2]).toHaveProperty( 501 'operation.context.requestPolicy', 502 'network-only' 503 ); 504 505 unsubscribe(); 506 }); 507}); 508 509describe('deduplication behavior', () => { 510 beforeEach(() => { 511 vi.useFakeTimers(); 512 }); 513 514 afterEach(() => { 515 vi.useRealTimers(); 516 }); 517 518 it('deduplicates operations when no result has been sent yet', () => { 519 const onOperation = vi.fn(); 520 521 const exchange: Exchange = () => ops$ => { 522 let i = 0; 523 return pipe( 524 ops$, 525 onPush(onOperation), 526 map(op => ({ 527 hasNext: false, 528 stale: false, 529 data: ++i, 530 operation: op, 531 })), 532 delay(1) 533 ); 534 }; 535 536 const client = createClient({ 537 url: 'test', 538 exchanges: [exchange], 539 }); 540 541 const resultOne = vi.fn(); 542 const resultTwo = vi.fn(); 543 const operationOne = makeOperation('query', queryOperation, { 544 ...queryOperation.context, 545 requestPolicy: 'cache-first', 546 }); 547 const operationTwo = makeOperation('query', queryOperation, { 548 ...queryOperation.context, 549 requestPolicy: 'network-only', 550 }); 551 552 pipe(client.executeRequestOperation(operationOne), subscribe(resultOne)); 553 pipe(client.executeRequestOperation(operationTwo), subscribe(resultTwo)); 554 expect(resultOne).toHaveBeenCalledTimes(0); 555 expect(resultTwo).toHaveBeenCalledTimes(0); 556 557 vi.advanceTimersByTime(1); 558 559 expect(resultOne).toHaveBeenCalledTimes(1); 560 expect(resultTwo).toHaveBeenCalledTimes(1); 561 expect(onOperation).toHaveBeenCalledTimes(1); 562 }); 563 564 it('deduplicates operations when hasNext: true is set', () => { 565 const onOperation = vi.fn(); 566 567 const exchange: Exchange = () => ops$ => { 568 let i = 0; 569 return pipe( 570 ops$, 571 onPush(onOperation), 572 map(op => ({ 573 hasNext: true, 574 stale: false, 575 data: ++i, 576 operation: op, 577 })) 578 ); 579 }; 580 581 const client = createClient({ 582 url: 'test', 583 exchanges: [exchange], 584 }); 585 586 const resultOne = vi.fn(); 587 const resultTwo = vi.fn(); 588 const operationOne = makeOperation('query', queryOperation, { 589 ...queryOperation.context, 590 requestPolicy: 'cache-first', 591 }); 592 const operationTwo = makeOperation('query', queryOperation, { 593 ...queryOperation.context, 594 requestPolicy: 'network-only', 595 }); 596 597 pipe(client.executeRequestOperation(operationOne), subscribe(resultOne)); 598 pipe(client.executeRequestOperation(operationTwo), subscribe(resultTwo)); 599 expect(resultOne).toHaveBeenCalledTimes(1); 600 expect(resultTwo).toHaveBeenCalledTimes(1); 601 602 vi.advanceTimersByTime(1); 603 604 expect(resultOne).toHaveBeenCalledTimes(1); 605 expect(resultTwo).toHaveBeenCalledTimes(1); 606 expect(onOperation).toHaveBeenCalledTimes(1); 607 }); 608 609 it('deduplicates otherwise if operation has already been sent', () => { 610 const onOperation = vi.fn(); 611 const onResult = vi.fn(); 612 613 let hasSent = false; 614 const exchange: Exchange = () => ops$ => 615 pipe( 616 ops$, 617 onPush(onOperation), 618 map(op => ({ 619 hasNext: false, 620 stale: false, 621 data: 'test', 622 operation: op, 623 })), 624 filter(() => { 625 return hasSent ? false : (hasSent = true); 626 }), 627 delay(1) 628 ); 629 630 const client = createClient({ 631 url: 'test', 632 exchanges: [exchange], 633 }); 634 635 const operationOne = makeOperation('query', queryOperation, { 636 ...queryOperation.context, 637 requestPolicy: 'cache-first', 638 }); 639 640 const operationTwo = makeOperation('query', queryOperation, { 641 ...queryOperation.context, 642 requestPolicy: 'network-only', 643 }); 644 645 const operationThree = makeOperation('query', queryOperation, { 646 ...queryOperation.context, 647 requestPolicy: 'network-only', 648 }); 649 650 pipe(client.executeRequestOperation(operationOne), subscribe(onResult)); 651 pipe(client.executeRequestOperation(operationTwo), subscribe(onResult)); 652 pipe(client.executeRequestOperation(operationThree), subscribe(onResult)); 653 vi.advanceTimersByTime(1); 654 655 expect(onOperation).toHaveBeenCalledTimes(1); 656 expect(onResult).toHaveBeenCalledTimes(3); 657 }); 658 659 it('does not deduplicate cache-and-network’s follow-up operations', () => { 660 const onOperation = vi.fn(); 661 const onResult = vi.fn(); 662 663 const operationOne = makeOperation('query', queryOperation, { 664 ...queryOperation.context, 665 requestPolicy: 'cache-and-network', 666 }); 667 668 const operationTwo = makeOperation('query', queryOperation, { 669 ...queryOperation.context, 670 requestPolicy: 'network-only', 671 }); 672 673 let shouldSend = true; 674 const exchange: Exchange = () => ops$ => 675 pipe( 676 ops$, 677 onPush(onOperation), 678 map(op => ({ 679 hasNext: false, 680 stale: true, 681 data: 'test', 682 operation: op, 683 })), 684 filter(() => { 685 if (shouldSend) { 686 shouldSend = false; 687 client.reexecuteOperation(operationTwo); 688 return true; 689 } else { 690 return false; 691 } 692 }) 693 ); 694 695 const client = createClient({ 696 url: 'test', 697 exchanges: [exchange], 698 }); 699 700 const operationThree = makeOperation('query', queryOperation, { 701 ...queryOperation.context, 702 requestPolicy: 'network-only', 703 }); 704 705 pipe(client.executeRequestOperation(operationOne), subscribe(onResult)); 706 pipe(client.executeRequestOperation(operationThree), subscribe(onResult)); 707 708 expect(onOperation).toHaveBeenCalledTimes(2); 709 }); 710 711 it('unblocks mutation operations on call to reexecuteOperation', async () => { 712 const onOperation = vi.fn(); 713 const onResult = vi.fn(); 714 715 let hasSent = false; 716 const exchange: Exchange = () => ops$ => 717 pipe( 718 ops$, 719 onPush(onOperation), 720 map(op => ({ 721 hasNext: false, 722 stale: false, 723 data: 'test', 724 operation: op, 725 })), 726 filter(() => hasSent || !(hasSent = true)) 727 ); 728 729 const client = createClient({ 730 url: 'test', 731 exchanges: [exchange], 732 }); 733 734 const operation = makeOperation('mutation', mutationOperation, { 735 ...mutationOperation.context, 736 requestPolicy: 'cache-first', 737 }); 738 739 pipe(client.executeRequestOperation(operation), subscribe(onResult)); 740 741 expect(onOperation).toHaveBeenCalledTimes(1); 742 expect(onResult).toHaveBeenCalledTimes(0); 743 744 client.reexecuteOperation(operation); 745 await Promise.resolve(); 746 747 expect(onOperation).toHaveBeenCalledTimes(2); 748 expect(onResult).toHaveBeenCalledTimes(1); 749 }); 750 751 // See https://github.com/urql-graphql/urql/issues/3254 752 it('unblocks stale operations', async () => { 753 const onOperation = vi.fn(); 754 const onResult = vi.fn(); 755 756 let sends = 0; 757 const exchange: Exchange = () => ops$ => 758 pipe( 759 ops$, 760 onPush(onOperation), 761 map(op => ({ 762 hasNext: false, 763 stale: sends++ ? false : true, 764 data: 'test', 765 operation: op, 766 })) 767 ); 768 769 const client = createClient({ 770 url: 'test', 771 exchanges: [exchange], 772 }); 773 774 const operation = makeOperation('query', queryOperation, { 775 ...queryOperation.context, 776 requestPolicy: 'cache-first', 777 }); 778 779 pipe(client.executeRequestOperation(operation), subscribe(onResult)); 780 781 expect(onOperation).toHaveBeenCalledTimes(1); 782 expect(onResult).toHaveBeenCalledTimes(1); 783 784 client.reexecuteOperation(operation); 785 await Promise.resolve(); 786 787 expect(onOperation).toHaveBeenCalledTimes(2); 788 expect(onResult).toHaveBeenCalledTimes(2); 789 }); 790 791 // See https://github.com/urql-graphql/urql/issues/3565 792 it('blocks reexecuting operations that are in-flight', async () => { 793 const onOperation = vi.fn(); 794 const onResult = vi.fn(); 795 796 let resolve; 797 const exchange: Exchange = 798 ({ client }) => 799 ops$ => 800 pipe( 801 ops$, 802 onPush(onOperation), 803 mergeMap(op => { 804 if (op.key === queryOperation.key) { 805 const promise = new Promise<OperationResult>(res => { 806 resolve = res; 807 }); 808 return fromPromise( 809 promise.then(() => { 810 return { 811 hasNext: false, 812 stale: false, 813 data: 'test', 814 operation: op, 815 }; 816 }) 817 ); 818 } else { 819 client.reexecuteOperation(queryOperation); 820 return fromValue({ 821 hasNext: false, 822 stale: false, 823 data: 'test', 824 operation: op, 825 }); 826 } 827 }) 828 ); 829 830 const client = createClient({ 831 url: 'test', 832 exchanges: [exchange], 833 }); 834 835 const operation = makeOperation('query', queryOperation, { 836 ...queryOperation.context, 837 requestPolicy: 'cache-first', 838 }); 839 840 const mutation = makeOperation('mutation', mutationOperation, { 841 ...mutationOperation.context, 842 requestPolicy: 'cache-first', 843 }); 844 845 pipe(client.executeRequestOperation(operation), subscribe(onResult)); 846 847 expect(onOperation).toHaveBeenCalledTimes(1); 848 expect(onResult).toHaveBeenCalledTimes(0); 849 850 pipe(client.executeRequestOperation(mutation), subscribe(onResult)); 851 await Promise.resolve(); 852 853 expect(onOperation).toHaveBeenCalledTimes(2); 854 expect(onResult).toHaveBeenCalledTimes(1); 855 856 resolve(); 857 await Promise.resolve(); 858 await Promise.resolve(); 859 await Promise.resolve(); 860 expect(onOperation).toHaveBeenCalledTimes(2); 861 expect(onResult).toHaveBeenCalledTimes(2); 862 }); 863}); 864 865describe('shared sources behavior', () => { 866 beforeEach(() => { 867 vi.useFakeTimers(); 868 }); 869 870 afterEach(() => { 871 vi.useRealTimers(); 872 }); 873 874 it('replays results from prior operation result as needed (cache-first)', async () => { 875 const exchange: Exchange = () => ops$ => { 876 let i = 0; 877 return pipe( 878 ops$, 879 map(op => ({ 880 hasNext: false, 881 stale: false, 882 data: ++i, 883 operation: op, 884 })), 885 delay(1) 886 ); 887 }; 888 889 const client = createClient({ 890 url: 'test', 891 exchanges: [exchange], 892 }); 893 894 const resultOne = vi.fn(); 895 const resultTwo = vi.fn(); 896 897 pipe(client.executeRequestOperation(queryOperation), subscribe(resultOne)); 898 899 expect(resultOne).toHaveBeenCalledTimes(0); 900 901 vi.advanceTimersByTime(1); 902 903 expect(resultOne).toHaveBeenCalledTimes(1); 904 expect(resultOne).toHaveBeenCalledWith({ 905 data: 1, 906 operation: queryOperation, 907 stale: false, 908 hasNext: false, 909 }); 910 911 pipe(client.executeRequestOperation(queryOperation), subscribe(resultTwo)); 912 913 expect(resultTwo).toHaveBeenCalledWith({ 914 data: 1, 915 operation: queryOperation, 916 stale: true, 917 hasNext: false, 918 }); 919 920 vi.advanceTimersByTime(1); 921 922 // With cache-first we don't expect a new operation to be issued 923 expect(resultTwo).toHaveBeenCalledTimes(2); 924 }); 925 926 it('dispatches the correct request policy on subsequent sources', async () => { 927 const exchange: Exchange = () => ops$ => { 928 let i = 0; 929 return pipe( 930 ops$, 931 map(op => ({ 932 hasNext: false, 933 stale: false, 934 data: ++i, 935 operation: op, 936 })), 937 delay(1) 938 ); 939 }; 940 941 const client = createClient({ 942 url: 'test', 943 exchanges: [exchange], 944 }); 945 946 const resultOne = vi.fn(); 947 const resultTwo = vi.fn(); 948 const operationOne = makeOperation('query', queryOperation, { 949 ...queryOperation.context, 950 requestPolicy: 'cache-first', 951 }); 952 const operationTwo = makeOperation('query', queryOperation, { 953 ...queryOperation.context, 954 requestPolicy: 'network-only', 955 }); 956 957 pipe(client.executeRequestOperation(operationOne), subscribe(resultOne)); 958 959 expect(resultOne).toHaveBeenCalledTimes(0); 960 961 vi.advanceTimersByTime(1); 962 963 expect(resultOne).toHaveBeenCalledTimes(1); 964 expect(resultOne).toHaveBeenCalledWith({ 965 data: 1, 966 operation: operationOne, 967 hasNext: false, 968 stale: false, 969 }); 970 971 pipe(client.executeRequestOperation(operationTwo), subscribe(resultTwo)); 972 973 expect(resultTwo).toHaveBeenCalledWith({ 974 data: 1, 975 operation: operationOne, 976 stale: true, 977 hasNext: false, 978 }); 979 980 vi.advanceTimersByTime(1); 981 982 expect(resultTwo).toHaveBeenCalledWith({ 983 data: 2, 984 operation: operationTwo, 985 stale: false, 986 hasNext: false, 987 }); 988 }); 989 990 it('replays results from prior operation result as needed (network-only)', async () => { 991 const exchange: Exchange = () => ops$ => { 992 let i = 0; 993 return pipe( 994 ops$, 995 map(op => ({ 996 hasNext: false, 997 stale: false, 998 data: ++i, 999 operation: op, 1000 })), 1001 delay(1) 1002 ); 1003 }; 1004 1005 const client = createClient({ 1006 url: 'test', 1007 exchanges: [exchange], 1008 }); 1009 1010 const operation = makeOperation('query', queryOperation, { 1011 ...queryOperation.context, 1012 requestPolicy: 'network-only', 1013 }); 1014 1015 const resultOne = vi.fn(); 1016 const resultTwo = vi.fn(); 1017 1018 pipe(client.executeRequestOperation(operation), subscribe(resultOne)); 1019 1020 expect(resultOne).toHaveBeenCalledTimes(0); 1021 1022 vi.advanceTimersByTime(1); 1023 1024 expect(resultOne).toHaveBeenCalledTimes(1); 1025 expect(resultOne).toHaveBeenCalledWith({ 1026 data: 1, 1027 operation, 1028 stale: false, 1029 hasNext: false, 1030 }); 1031 1032 pipe(client.executeRequestOperation(operation), subscribe(resultTwo)); 1033 1034 expect(resultTwo).toHaveBeenCalledWith({ 1035 data: 1, 1036 operation, 1037 stale: true, 1038 hasNext: false, 1039 }); 1040 1041 expect(resultOne).toHaveBeenCalledWith({ 1042 data: 1, 1043 operation, 1044 stale: true, 1045 hasNext: false, 1046 }); 1047 1048 expect(resultTwo).toHaveBeenCalledTimes(1); 1049 expect(resultOne).toHaveBeenCalledTimes(2); 1050 1051 vi.advanceTimersByTime(1); 1052 1053 // With network-only we expect a new operation to be issued, hence a new result 1054 expect(resultTwo).toHaveBeenCalledTimes(2); 1055 expect(resultOne).toHaveBeenCalledTimes(3); 1056 1057 expect(resultTwo).toHaveBeenCalledWith({ 1058 data: 2, 1059 operation, 1060 stale: false, 1061 hasNext: false, 1062 }); 1063 1064 expect(resultOne).toHaveBeenCalledWith({ 1065 data: 2, 1066 operation, 1067 stale: false, 1068 hasNext: false, 1069 }); 1070 }); 1071 1072 it('does not replay values from a past subscription', async () => { 1073 const exchange: Exchange = () => ops$ => { 1074 let i = 0; 1075 return pipe( 1076 ops$, 1077 filter(op => op.kind !== 'teardown'), 1078 map(op => ({ 1079 hasNext: false, 1080 stale: false, 1081 data: ++i, 1082 operation: op, 1083 })), 1084 delay(1) 1085 ); 1086 }; 1087 1088 const client = createClient({ 1089 url: 'test', 1090 exchanges: [exchange], 1091 }); 1092 1093 // We keep the source in-memory 1094 const source = client.executeRequestOperation(queryOperation); 1095 const resultOne = vi.fn(); 1096 let subscription; 1097 1098 subscription = pipe(source, subscribe(resultOne)); 1099 1100 expect(resultOne).toHaveBeenCalledTimes(0); 1101 vi.advanceTimersByTime(1); 1102 1103 expect(resultOne).toHaveBeenCalledWith({ 1104 data: 1, 1105 operation: queryOperation, 1106 hasNext: false, 1107 stale: false, 1108 }); 1109 1110 subscription.unsubscribe(); 1111 const resultTwo = vi.fn(); 1112 subscription = pipe(source, subscribe(resultTwo)); 1113 1114 expect(resultTwo).toHaveBeenCalledTimes(0); 1115 vi.advanceTimersByTime(1); 1116 1117 expect(resultTwo).toHaveBeenCalledWith({ 1118 data: 2, 1119 operation: queryOperation, 1120 stale: false, 1121 hasNext: false, 1122 }); 1123 }); 1124 1125 it('replayed results are not emitted on the shared source', () => { 1126 const exchange: Exchange = () => ops$ => { 1127 let i = 0; 1128 return pipe( 1129 ops$, 1130 map(op => ({ 1131 data: ++i, 1132 operation: op, 1133 hasNext: false, 1134 stale: false, 1135 })), 1136 take(1) 1137 ); 1138 }; 1139 1140 const client = createClient({ 1141 url: 'test', 1142 exchanges: [exchange], 1143 }); 1144 1145 const operation = makeOperation('query', queryOperation, { 1146 ...queryOperation.context, 1147 requestPolicy: 'network-only', 1148 }); 1149 1150 const resultOne = vi.fn(); 1151 const resultTwo = vi.fn(); 1152 1153 pipe(client.executeRequestOperation(operation), subscribe(resultOne)); 1154 pipe(client.executeRequestOperation(operation), subscribe(resultTwo)); 1155 1156 expect(resultTwo).toHaveBeenCalledTimes(1); 1157 expect(resultTwo).toHaveBeenCalledWith({ 1158 data: 1, 1159 operation, 1160 stale: true, 1161 hasNext: false, 1162 }); 1163 }); 1164 1165 it('does nothing when no operation result has been emitted yet', () => { 1166 const dispatched = vi.fn(); 1167 1168 const exchange: Exchange = () => ops$ => { 1169 return pipe( 1170 ops$, 1171 map(op => { 1172 dispatched(op); 1173 return { hasNext: false, stale: false, data: 1, operation: op }; 1174 }), 1175 filter(() => false) 1176 ); 1177 }; 1178 1179 const client = createClient({ 1180 url: 'test', 1181 exchanges: [exchange], 1182 }); 1183 1184 const resultOne = vi.fn(); 1185 const resultTwo = vi.fn(); 1186 1187 pipe(client.executeRequestOperation(queryOperation), subscribe(resultOne)); 1188 1189 pipe(client.executeRequestOperation(queryOperation), subscribe(resultTwo)); 1190 1191 expect(resultOne).toHaveBeenCalledTimes(0); 1192 expect(resultTwo).toHaveBeenCalledTimes(0); 1193 expect(dispatched).toHaveBeenCalledTimes(1); 1194 }); 1195 1196 it('skips replaying results when a result is emitted immediately (network-only)', () => { 1197 const exchange: Exchange = () => ops$ => { 1198 let i = 0; 1199 return pipe( 1200 ops$, 1201 map(op => ({ hasNext: false, stale: false, data: ++i, operation: op })) 1202 ); 1203 }; 1204 1205 const client = createClient({ 1206 url: 'test', 1207 exchanges: [exchange], 1208 }); 1209 1210 const operation = makeOperation('query', queryOperation, { 1211 ...queryOperation.context, 1212 requestPolicy: 'network-only', 1213 }); 1214 1215 const resultOne = vi.fn(); 1216 const resultTwo = vi.fn(); 1217 1218 pipe(client.executeRequestOperation(operation), subscribe(resultOne)); 1219 1220 expect(resultOne).toHaveBeenCalledWith({ 1221 data: 1, 1222 operation, 1223 hasNext: false, 1224 stale: false, 1225 }); 1226 1227 pipe(client.executeRequestOperation(operation), subscribe(resultTwo)); 1228 1229 expect(resultTwo).toHaveBeenCalledWith({ 1230 data: 2, 1231 operation, 1232 hasNext: false, 1233 stale: false, 1234 }); 1235 1236 expect(resultOne).toHaveBeenCalledWith({ 1237 data: 2, 1238 operation, 1239 hasNext: false, 1240 stale: false, 1241 }); 1242 }); 1243 1244 it('replays stale results as needed', () => { 1245 const exchange: Exchange = () => ops$ => { 1246 return pipe( 1247 ops$, 1248 map(op => ({ hasNext: false, stale: true, data: 1, operation: op })), 1249 take(1) 1250 ); 1251 }; 1252 1253 const client = createClient({ 1254 url: 'test', 1255 exchanges: [exchange], 1256 }); 1257 1258 const resultOne = vi.fn(); 1259 const resultTwo = vi.fn(); 1260 1261 pipe(client.executeRequestOperation(queryOperation), subscribe(resultOne)); 1262 1263 expect(resultOne).toHaveBeenCalledWith({ 1264 data: 1, 1265 operation: queryOperation, 1266 stale: true, 1267 hasNext: false, 1268 }); 1269 1270 pipe(client.executeRequestOperation(queryOperation), subscribe(resultTwo)); 1271 1272 expect(resultTwo).toHaveBeenCalledWith({ 1273 data: 1, 1274 operation: queryOperation, 1275 stale: true, 1276 hasNext: false, 1277 }); 1278 }); 1279 1280 it('does nothing when operation is a subscription has been emitted yet', () => { 1281 const exchange: Exchange = () => ops$ => { 1282 return merge([ 1283 pipe( 1284 ops$, 1285 map(op => ({ hasNext: true, data: 1, operation: op })), 1286 take(1) 1287 ), 1288 never, 1289 ]); 1290 }; 1291 1292 const client = createClient({ 1293 url: 'test', 1294 exchanges: [exchange], 1295 }); 1296 1297 const resultOne = vi.fn(); 1298 const resultTwo = vi.fn(); 1299 1300 pipe( 1301 client.executeRequestOperation(subscriptionOperation), 1302 subscribe(resultOne) 1303 ); 1304 expect(resultOne).toHaveBeenCalledTimes(1); 1305 1306 pipe( 1307 client.executeRequestOperation(subscriptionOperation), 1308 subscribe(resultTwo) 1309 ); 1310 expect(resultTwo).toHaveBeenCalledTimes(0); 1311 }); 1312 1313 it('supports promisified sources', async () => { 1314 const exchange: Exchange = () => ops$ => { 1315 return pipe( 1316 ops$, 1317 map(op => ({ hasNext: false, stale: true, data: 1, operation: op })) 1318 ); 1319 }; 1320 1321 const client = createClient({ 1322 url: 'test', 1323 exchanges: [exchange], 1324 }); 1325 1326 const resultOne = await pipe( 1327 client.executeRequestOperation(queryOperation), 1328 take(1), 1329 toPromise 1330 ); 1331 1332 expect(resultOne).toEqual({ 1333 data: 1, 1334 operation: queryOperation, 1335 stale: true, 1336 hasNext: false, 1337 }); 1338 }); 1339});