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});