1import {
2 gql,
3 createClient,
4 ExchangeIO,
5 Operation,
6 OperationResult,
7 CombinedError,
8} from '@urql/core';
9
10import { print, stripIgnoredCharacters } from 'graphql';
11import { vi, expect, it, describe } from 'vitest';
12
13import {
14 Source,
15 pipe,
16 share,
17 map,
18 merge,
19 mergeMap,
20 filter,
21 fromValue,
22 makeSubject,
23 tap,
24 publish,
25 delay,
26} from 'wonka';
27
28import { minifyIntrospectionQuery } from '@urql/introspection';
29import { queryResponse } from '../../../packages/core/src/test-utils';
30import { cacheExchange } from './cacheExchange';
31
32const queryOne = gql`
33 {
34 author {
35 id
36 name
37 }
38 unrelated {
39 id
40 }
41 }
42`;
43
44const queryOneData = {
45 __typename: 'Query',
46 author: {
47 __typename: 'Author',
48 id: '123',
49 name: 'Author',
50 },
51 unrelated: {
52 __typename: 'Unrelated',
53 id: 'unrelated',
54 },
55};
56
57const dispatchDebug = vi.fn();
58
59describe('data dependencies', () => {
60 it('writes queries to the cache', () => {
61 const client = createClient({
62 url: 'http://0.0.0.0',
63 exchanges: [],
64 });
65 const op = client.createRequestOperation('query', {
66 key: 1,
67 query: queryOne,
68 variables: undefined,
69 });
70
71 const expected = {
72 __typename: 'Query',
73 author: {
74 id: '123',
75 name: 'Author',
76 __typename: 'Author',
77 },
78 unrelated: {
79 id: 'unrelated',
80 __typename: 'Unrelated',
81 },
82 };
83
84 const response = vi.fn((forwardOp: Operation): OperationResult => {
85 expect(forwardOp.key).toBe(op.key);
86 return { ...queryResponse, operation: forwardOp, data: expected };
87 });
88
89 const { source: ops$, next } = makeSubject<Operation>();
90 const result = vi.fn();
91 const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share);
92
93 pipe(
94 cacheExchange({})({ forward, client, dispatchDebug })(ops$),
95 tap(result),
96 publish
97 );
98
99 next(op);
100 next(op);
101 expect(response).toHaveBeenCalledTimes(1);
102 expect(result).toHaveBeenCalledTimes(2);
103
104 expect(expected).toMatchObject(result.mock.calls[0][0].data);
105 expect(result.mock.calls[1][0]).toHaveProperty(
106 'operation.context.meta.cacheOutcome',
107 'hit'
108 );
109 expect(expected).toMatchObject(result.mock.calls[1][0].data);
110 expect(result.mock.calls[1][0].data).toBe(result.mock.calls[0][0].data);
111 });
112
113 it('logs cache misses', () => {
114 const client = createClient({
115 url: 'http://0.0.0.0',
116 exchanges: [],
117 });
118 const op = client.createRequestOperation('query', {
119 key: 1,
120 query: queryOne,
121 variables: undefined,
122 });
123
124 const expected = {
125 __typename: 'Query',
126 author: {
127 id: '123',
128 name: 'Author',
129 __typename: 'Author',
130 },
131 unrelated: {
132 id: 'unrelated',
133 __typename: 'Unrelated',
134 },
135 };
136
137 const response = vi.fn((forwardOp: Operation): OperationResult => {
138 expect(forwardOp.key).toBe(op.key);
139 return { ...queryResponse, operation: forwardOp, data: expected };
140 });
141
142 const { source: ops$, next } = makeSubject<Operation>();
143 const result = vi.fn();
144 const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share);
145
146 const messages: string[] = [];
147 pipe(
148 cacheExchange({
149 logger(severity, message) {
150 if (severity === 'debug') {
151 messages.push(message);
152 }
153 },
154 })({ forward, client, dispatchDebug })(ops$),
155 tap(result),
156 publish
157 );
158
159 next(op);
160 next(op);
161 next({
162 ...op,
163 query: gql`
164 query ($id: ID!) {
165 author(id: $id) {
166 id
167 name
168 }
169 }
170 `,
171 variables: { id: '123' },
172 });
173 expect(response).toHaveBeenCalledTimes(1);
174 expect(result).toHaveBeenCalledTimes(2);
175
176 expect(expected).toMatchObject(result.mock.calls[0][0].data);
177 expect(result.mock.calls[1][0]).toHaveProperty(
178 'operation.context.meta.cacheOutcome',
179 'hit'
180 );
181 expect(expected).toMatchObject(result.mock.calls[1][0].data);
182 expect(result.mock.calls[1][0].data).toBe(result.mock.calls[0][0].data);
183 expect(messages).toEqual([
184 'No value for field "author" on entity "Query"',
185 'No value for field "author" with args {"id":"123"} on entity "Query"',
186 ]);
187 });
188
189 it('respects cache-only operations', () => {
190 const client = createClient({
191 url: 'http://0.0.0.0',
192 exchanges: [],
193 });
194 const op = client.createRequestOperation(
195 'query',
196 {
197 key: 1,
198 query: queryOne,
199 variables: undefined,
200 },
201 {
202 requestPolicy: 'cache-only',
203 }
204 );
205
206 const response = vi.fn((forwardOp: Operation): OperationResult => {
207 expect(forwardOp.key).toBe(op.key);
208 return { ...queryResponse, operation: forwardOp, data: queryOneData };
209 });
210
211 const { source: ops$, next } = makeSubject<Operation>();
212 const result = vi.fn();
213 const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share);
214
215 pipe(
216 cacheExchange({})({ forward, client, dispatchDebug })(ops$),
217 tap(result),
218 publish
219 );
220
221 next(op);
222 expect(response).toHaveBeenCalledTimes(0);
223 expect(result).toHaveBeenCalledTimes(1);
224
225 expect(result.mock.calls[0][0]).toHaveProperty(
226 'operation.context.meta.cacheOutcome',
227 'miss'
228 );
229
230 expect(result.mock.calls[0][0].data).toBe(null);
231 });
232
233 it('updates related queries when their data changes', () => {
234 const queryMultiple = gql`
235 {
236 authors {
237 id
238 name
239 }
240 }
241 `;
242
243 const queryMultipleData = {
244 __typename: 'Query',
245 authors: [
246 {
247 __typename: 'Author',
248 id: '123',
249 name: 'New Author Name',
250 },
251 ],
252 };
253
254 const client = createClient({
255 url: 'http://0.0.0.0',
256 exchanges: [],
257 });
258 const { source: ops$, next } = makeSubject<Operation>();
259
260 const reexec = vi
261 .spyOn(client, 'reexecuteOperation')
262 .mockImplementation(next);
263
264 const opOne = client.createRequestOperation('query', {
265 key: 1,
266 query: queryOne,
267 variables: undefined,
268 });
269
270 const opMultiple = client.createRequestOperation('query', {
271 key: 2,
272 query: queryMultiple,
273 variables: undefined,
274 });
275
276 const response = vi.fn((forwardOp: Operation): OperationResult => {
277 if (forwardOp.key === 1) {
278 return { ...queryResponse, operation: opOne, data: queryOneData };
279 } else if (forwardOp.key === 2) {
280 return {
281 ...queryResponse,
282 operation: opMultiple,
283 data: queryMultipleData,
284 };
285 }
286
287 return undefined as any;
288 });
289
290 const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share);
291 const result = vi.fn();
292
293 pipe(
294 cacheExchange({})({ forward, client, dispatchDebug })(ops$),
295 tap(result),
296 publish
297 );
298
299 next(opOne);
300 expect(response).toHaveBeenCalledTimes(1);
301 expect(result).toHaveBeenCalledTimes(1);
302
303 next(opMultiple);
304 expect(response).toHaveBeenCalledTimes(2);
305 expect(reexec.mock.calls[0][0]).toHaveProperty('key', opOne.key);
306 expect(result).toHaveBeenCalledTimes(3);
307
308 // test for reference reuse
309 const firstDataOne = result.mock.calls[0][0].data;
310 const firstDataTwo = result.mock.calls[1][0].data;
311 expect(firstDataOne).not.toBe(firstDataTwo);
312 expect(firstDataOne.author).not.toBe(firstDataTwo.author);
313 expect(firstDataOne.unrelated).toBe(firstDataTwo.unrelated);
314 });
315
316 it('updates related queries when a mutation update touches query data', () => {
317 vi.useFakeTimers();
318
319 const balanceFragment = gql`
320 fragment BalanceFragment on Author {
321 id
322 balance {
323 amount
324 }
325 }
326 `;
327
328 const queryById = gql`
329 query ($id: ID!) {
330 author(id: $id) {
331 id
332 name
333 ...BalanceFragment
334 }
335 }
336
337 ${balanceFragment}
338 `;
339
340 const queryByIdDataA = {
341 __typename: 'Query',
342 author: {
343 __typename: 'Author',
344 id: '1',
345 name: 'Author 1',
346 balance: {
347 __typename: 'Balance',
348 amount: 100,
349 },
350 },
351 };
352
353 const queryByIdDataB = {
354 __typename: 'Query',
355 author: {
356 __typename: 'Author',
357 id: '2',
358 name: 'Author 2',
359 balance: {
360 __typename: 'Balance',
361 amount: 200,
362 },
363 },
364 };
365
366 const mutation = gql`
367 mutation ($userId: ID!, $amount: Int!) {
368 updateBalance(userId: $userId, amount: $amount) {
369 userId
370 balance {
371 amount
372 }
373 }
374 }
375 `;
376
377 const mutationData = {
378 __typename: 'Mutation',
379 updateBalance: {
380 __typename: 'UpdateBalanceResult',
381 userId: '1',
382 balance: {
383 __typename: 'Balance',
384 amount: 1000,
385 },
386 },
387 };
388
389 const client = createClient({
390 url: 'http://0.0.0.0',
391 exchanges: [],
392 });
393 const { source: ops$, next } = makeSubject<Operation>();
394
395 const reexec = vi
396 .spyOn(client, 'reexecuteOperation')
397 .mockImplementation(next);
398
399 const opOne = client.createRequestOperation('query', {
400 key: 1,
401 query: queryById,
402 variables: { id: 1 },
403 });
404
405 const opTwo = client.createRequestOperation('query', {
406 key: 2,
407 query: queryById,
408 variables: { id: 2 },
409 });
410
411 const opMutation = client.createRequestOperation('mutation', {
412 key: 3,
413 query: mutation,
414 variables: { userId: '1', amount: 1000 },
415 });
416
417 const response = vi.fn((forwardOp: Operation): OperationResult => {
418 if (forwardOp.key === 1) {
419 return { ...queryResponse, operation: opOne, data: queryByIdDataA };
420 } else if (forwardOp.key === 2) {
421 return { ...queryResponse, operation: opTwo, data: queryByIdDataB };
422 } else if (forwardOp.key === 3) {
423 return {
424 ...queryResponse,
425 operation: opMutation,
426 data: mutationData,
427 };
428 }
429
430 return undefined as any;
431 });
432
433 const result = vi.fn();
434 const forward: ExchangeIO = ops$ =>
435 pipe(ops$, delay(1), map(response), share);
436
437 const updates = {
438 Mutation: {
439 updateBalance: vi.fn((result, _args, cache) => {
440 const {
441 updateBalance: { userId, balance },
442 } = result;
443 cache.writeFragment(balanceFragment, { id: userId, balance });
444 }),
445 },
446 };
447
448 const keys = {
449 Balance: () => null,
450 };
451
452 pipe(
453 cacheExchange({ updates, keys })({ forward, client, dispatchDebug })(
454 ops$
455 ),
456 tap(result),
457 publish
458 );
459
460 next(opTwo);
461 vi.runAllTimers();
462 expect(response).toHaveBeenCalledTimes(1);
463
464 next(opOne);
465 vi.runAllTimers();
466 expect(response).toHaveBeenCalledTimes(2);
467
468 next(opMutation);
469 vi.runAllTimers();
470
471 expect(response).toHaveBeenCalledTimes(3);
472 expect(updates.Mutation.updateBalance).toHaveBeenCalledTimes(1);
473
474 expect(reexec).toHaveBeenCalledTimes(1);
475 expect(reexec.mock.calls[0][0].key).toBe(1);
476
477 expect(result.mock.calls[2][0]).toHaveProperty(
478 'data.author.balance.amount',
479 1000
480 );
481 });
482
483 it('does not notify related queries when a mutation update does not change the data', () => {
484 vi.useFakeTimers();
485
486 const balanceFragment = gql`
487 fragment BalanceFragment on Author {
488 id
489 balance {
490 amount
491 }
492 }
493 `;
494
495 const queryById = gql`
496 query ($id: ID!) {
497 author(id: $id) {
498 id
499 name
500 ...BalanceFragment
501 }
502 }
503
504 ${balanceFragment}
505 `;
506
507 const queryByIdDataA = {
508 __typename: 'Query',
509 author: {
510 __typename: 'Author',
511 id: '1',
512 name: 'Author 1',
513 balance: {
514 __typename: 'Balance',
515 amount: 100,
516 },
517 },
518 };
519
520 const queryByIdDataB = {
521 __typename: 'Query',
522 author: {
523 __typename: 'Author',
524 id: '2',
525 name: 'Author 2',
526 balance: {
527 __typename: 'Balance',
528 amount: 200,
529 },
530 },
531 };
532
533 const mutation = gql`
534 mutation ($userId: ID!, $amount: Int!) {
535 updateBalance(userId: $userId, amount: $amount) {
536 userId
537 balance {
538 amount
539 }
540 }
541 }
542 `;
543
544 const mutationData = {
545 __typename: 'Mutation',
546 updateBalance: {
547 __typename: 'UpdateBalanceResult',
548 userId: '1',
549 balance: {
550 __typename: 'Balance',
551 amount: 100,
552 },
553 },
554 };
555
556 const client = createClient({
557 url: 'http://0.0.0.0',
558 exchanges: [],
559 });
560 const { source: ops$, next } = makeSubject<Operation>();
561
562 const reexec = vi
563 .spyOn(client, 'reexecuteOperation')
564 .mockImplementation(next);
565
566 const opOne = client.createRequestOperation('query', {
567 key: 1,
568 query: queryById,
569 variables: { id: 1 },
570 });
571
572 const opTwo = client.createRequestOperation('query', {
573 key: 2,
574 query: queryById,
575 variables: { id: 2 },
576 });
577
578 const opMutation = client.createRequestOperation('mutation', {
579 key: 3,
580 query: mutation,
581 variables: { userId: '1', amount: 1000 },
582 });
583
584 const response = vi.fn((forwardOp: Operation): OperationResult => {
585 if (forwardOp.key === 1) {
586 return { ...queryResponse, operation: opOne, data: queryByIdDataA };
587 } else if (forwardOp.key === 2) {
588 return { ...queryResponse, operation: opTwo, data: queryByIdDataB };
589 } else if (forwardOp.key === 3) {
590 return {
591 ...queryResponse,
592 operation: opMutation,
593 data: mutationData,
594 };
595 }
596
597 return undefined as any;
598 });
599
600 const result = vi.fn();
601 const forward: ExchangeIO = ops$ =>
602 pipe(ops$, delay(1), map(response), share);
603
604 const updates = {
605 Mutation: {
606 updateBalance: vi.fn((result, _args, cache) => {
607 const {
608 updateBalance: { userId, balance },
609 } = result;
610 cache.writeFragment(balanceFragment, { id: userId, balance });
611 }),
612 },
613 };
614
615 const keys = {
616 Balance: () => null,
617 };
618
619 pipe(
620 cacheExchange({ updates, keys })({ forward, client, dispatchDebug })(
621 ops$
622 ),
623 tap(result),
624 publish
625 );
626
627 next(opTwo);
628 vi.runAllTimers();
629 expect(response).toHaveBeenCalledTimes(1);
630
631 next(opOne);
632 vi.runAllTimers();
633 expect(response).toHaveBeenCalledTimes(2);
634
635 next(opMutation);
636 vi.runAllTimers();
637
638 expect(response).toHaveBeenCalledTimes(3);
639 expect(updates.Mutation.updateBalance).toHaveBeenCalledTimes(1);
640
641 expect(reexec).toHaveBeenCalledTimes(0);
642 });
643
644 it('does nothing when no related queries have changed', () => {
645 const queryUnrelated = gql`
646 {
647 user {
648 id
649 name
650 }
651 }
652 `;
653
654 const queryUnrelatedData = {
655 __typename: 'Query',
656 user: {
657 __typename: 'User',
658 id: 'me',
659 name: 'Me',
660 },
661 };
662
663 const client = createClient({
664 url: 'http://0.0.0.0',
665 exchanges: [],
666 });
667 const { source: ops$, next } = makeSubject<Operation>();
668 const reexec = vi
669 .spyOn(client, 'reexecuteOperation')
670 .mockImplementation(next);
671
672 const opOne = client.createRequestOperation('query', {
673 key: 1,
674 query: queryOne,
675 variables: undefined,
676 });
677 const opUnrelated = client.createRequestOperation('query', {
678 key: 2,
679 query: queryUnrelated,
680 variables: undefined,
681 });
682
683 const response = vi.fn((forwardOp: Operation): OperationResult => {
684 if (forwardOp.key === 1) {
685 return { ...queryResponse, operation: opOne, data: queryOneData };
686 } else if (forwardOp.key === 2) {
687 return {
688 ...queryResponse,
689 operation: opUnrelated,
690 data: queryUnrelatedData,
691 };
692 }
693
694 return undefined as any;
695 });
696
697 const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share);
698 const result = vi.fn();
699
700 pipe(
701 cacheExchange({})({ forward, client, dispatchDebug })(ops$),
702 tap(result),
703 publish
704 );
705
706 next(opOne);
707 expect(response).toHaveBeenCalledTimes(1);
708
709 next(opUnrelated);
710 expect(response).toHaveBeenCalledTimes(2);
711
712 expect(reexec).not.toHaveBeenCalled();
713 expect(result).toHaveBeenCalledTimes(2);
714 });
715
716 it('does not reach updater when mutation has no selectionset in optimistic phase', () => {
717 vi.useFakeTimers();
718
719 const mutation = gql`
720 mutation {
721 concealAuthor
722 }
723 `;
724
725 const mutationData = {
726 __typename: 'Mutation',
727 concealAuthor: true,
728 };
729
730 const client = createClient({
731 url: 'http://0.0.0.0',
732 exchanges: [],
733 });
734 const { source: ops$, next } = makeSubject<Operation>();
735
736 vi.spyOn(client, 'reexecuteOperation').mockImplementation(next);
737
738 const opMutation = client.createRequestOperation('mutation', {
739 key: 1,
740 query: mutation,
741 variables: undefined,
742 });
743
744 const response = vi.fn((forwardOp: Operation): OperationResult => {
745 if (forwardOp.key === 1) {
746 return {
747 ...queryResponse,
748 operation: opMutation,
749 data: mutationData,
750 };
751 }
752
753 return undefined as any;
754 });
755
756 const result = vi.fn();
757 const forward: ExchangeIO = ops$ =>
758 pipe(ops$, delay(1), map(response), share);
759
760 const updates = {
761 Mutation: {
762 concealAuthor: vi.fn(),
763 },
764 };
765
766 pipe(
767 cacheExchange({ updates })({ forward, client, dispatchDebug })(ops$),
768 tap(result),
769 publish
770 );
771
772 next(opMutation);
773 expect(updates.Mutation.concealAuthor).toHaveBeenCalledTimes(0);
774
775 vi.runAllTimers();
776 expect(updates.Mutation.concealAuthor).toHaveBeenCalledTimes(1);
777 });
778
779 it('does reach updater when mutation has no selectionset in optimistic phase with optimistic update', () => {
780 vi.useFakeTimers();
781
782 const mutation = gql`
783 mutation {
784 concealAuthor
785 }
786 `;
787
788 const mutationData = {
789 __typename: 'Mutation',
790 concealAuthor: true,
791 };
792
793 const client = createClient({
794 url: 'http://0.0.0.0',
795 exchanges: [],
796 });
797 const { source: ops$, next } = makeSubject<Operation>();
798
799 vi.spyOn(client, 'reexecuteOperation').mockImplementation(next);
800
801 const opMutation = client.createRequestOperation('mutation', {
802 key: 1,
803 query: mutation,
804 variables: undefined,
805 });
806
807 const response = vi.fn((forwardOp: Operation): OperationResult => {
808 if (forwardOp.key === 1) {
809 return {
810 ...queryResponse,
811 operation: opMutation,
812 data: mutationData,
813 };
814 }
815
816 return undefined as any;
817 });
818
819 const result = vi.fn();
820 const forward: ExchangeIO = ops$ =>
821 pipe(ops$, delay(1), map(response), share);
822
823 const updates = {
824 Mutation: {
825 concealAuthor: vi.fn(),
826 },
827 };
828
829 const optimistic = {
830 concealAuthor: vi.fn(() => true) as any,
831 };
832
833 pipe(
834 cacheExchange({ updates, optimistic })({
835 forward,
836 client,
837 dispatchDebug,
838 })(ops$),
839 tap(result),
840 publish
841 );
842
843 next(opMutation);
844 expect(optimistic.concealAuthor).toHaveBeenCalledTimes(1);
845 expect(updates.Mutation.concealAuthor).toHaveBeenCalledTimes(1);
846
847 vi.runAllTimers();
848 expect(updates.Mutation.concealAuthor).toHaveBeenCalledTimes(2);
849 });
850
851 it('marks errored null fields as uncached but delivers them as expected', () => {
852 const client = createClient({
853 url: 'http://0.0.0.0',
854 exchanges: [],
855 });
856 const { source: ops$, next } = makeSubject<Operation>();
857
858 const query = gql`
859 {
860 field
861 author {
862 id
863 }
864 }
865 `;
866
867 const operation = client.createRequestOperation('query', {
868 key: 1,
869 query,
870 variables: undefined,
871 });
872
873 const queryResult: OperationResult = {
874 ...queryResponse,
875 operation,
876 data: {
877 __typename: 'Query',
878 field: 'test',
879 author: null,
880 },
881 error: new CombinedError({
882 graphQLErrors: [
883 {
884 message: 'Test',
885 path: ['author'],
886 },
887 ],
888 }),
889 };
890
891 const reexecuteOperation = vi
892 .spyOn(client, 'reexecuteOperation')
893 .mockImplementation(next);
894
895 const response = vi.fn((forwardOp: Operation): OperationResult => {
896 if (forwardOp.key === 1) return queryResult;
897 return undefined as any;
898 });
899
900 const result = vi.fn();
901 const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share);
902
903 pipe(
904 cacheExchange({})({ forward, client, dispatchDebug })(ops$),
905 tap(result),
906 publish
907 );
908
909 next(operation);
910
911 expect(response).toHaveBeenCalledTimes(1);
912 expect(result).toHaveBeenCalledTimes(1);
913 expect(reexecuteOperation).toHaveBeenCalledTimes(0);
914 expect(result.mock.calls[0][0]).toHaveProperty('data.author', null);
915 });
916
917 it('mutation does not change number of reexecute request after a query', () => {
918 const client = createClient({
919 url: 'http://0.0.0.0',
920 exchanges: [],
921 });
922
923 const { source: ops$, next: nextOp } = makeSubject<Operation>();
924
925 const reexec = vi
926 .spyOn(client, 'reexecuteOperation')
927 .mockImplementation(nextOp);
928
929 const mutation = gql`
930 mutation {
931 updateNode {
932 __typename
933 id
934 }
935 }
936 `;
937
938 const normalQuery = gql`
939 {
940 __typename
941 item {
942 __typename
943 id
944 }
945 }
946 `;
947
948 const extendedQuery = gql`
949 {
950 __typename
951 item {
952 __typename
953 extended: id
954 extra @_optional
955 }
956 }
957 `;
958
959 const mutationOp = client.createRequestOperation('mutation', {
960 key: 0,
961 query: mutation,
962 variables: undefined,
963 });
964
965 const normalOp = client.createRequestOperation(
966 'query',
967 {
968 key: 1,
969 query: normalQuery,
970 variables: undefined,
971 },
972 {
973 requestPolicy: 'cache-and-network',
974 }
975 );
976
977 const extendedOp = client.createRequestOperation(
978 'query',
979 {
980 key: 2,
981 query: extendedQuery,
982 variables: undefined,
983 },
984 {
985 requestPolicy: 'cache-only',
986 }
987 );
988
989 const response = vi.fn((forwardOp: Operation): OperationResult => {
990 if (forwardOp.key === 0) {
991 return {
992 operation: mutationOp,
993 data: {
994 __typename: 'Mutation',
995 updateNode: {
996 __typename: 'Node',
997 id: 'id',
998 },
999 },
1000 stale: false,
1001 hasNext: false,
1002 };
1003 } else if (forwardOp.key === 1) {
1004 return {
1005 operation: normalOp,
1006 data: {
1007 __typename: 'Query',
1008 item: {
1009 __typename: 'Node',
1010 id: 'id',
1011 },
1012 },
1013 stale: false,
1014 hasNext: false,
1015 };
1016 } else if (forwardOp.key === 2) {
1017 return {
1018 operation: extendedOp,
1019 data: {
1020 __typename: 'Query',
1021 item: {
1022 __typename: 'Node',
1023 extended: 'id',
1024 extra: 'extra',
1025 },
1026 },
1027 stale: false,
1028 hasNext: false,
1029 };
1030 }
1031
1032 return undefined as any;
1033 });
1034
1035 const forward = (ops$: Source<Operation>): Source<OperationResult> =>
1036 pipe(ops$, map(response), share);
1037
1038 pipe(cacheExchange()({ forward, client, dispatchDebug })(ops$), publish);
1039
1040 nextOp(normalOp);
1041 expect(reexec).toHaveBeenCalledTimes(0);
1042
1043 nextOp(extendedOp);
1044 expect(reexec).toHaveBeenCalledTimes(0);
1045
1046 // re-execute first operation
1047 reexec.mockClear();
1048 nextOp(normalOp);
1049 expect(reexec).toHaveBeenCalledTimes(4);
1050
1051 nextOp(mutationOp);
1052
1053 // re-execute first operation after mutation
1054 reexec.mockClear();
1055 nextOp(normalOp);
1056 expect(reexec).toHaveBeenCalledTimes(4);
1057 });
1058});
1059
1060describe('directives', () => {
1061 it('returns optional fields as partial', () => {
1062 const client = createClient({
1063 url: 'http://0.0.0.0',
1064 exchanges: [],
1065 });
1066 const { source: ops$, next } = makeSubject<Operation>();
1067
1068 const query = gql`
1069 {
1070 todos {
1071 id
1072 text
1073 completed @_optional
1074 }
1075 }
1076 `;
1077
1078 const operation = client.createRequestOperation('query', {
1079 key: 1,
1080 query,
1081 variables: undefined,
1082 });
1083
1084 const queryResult: OperationResult = {
1085 ...queryResponse,
1086 operation,
1087 data: {
1088 __typename: 'Query',
1089 todos: [
1090 {
1091 id: '1',
1092 text: 'learn urql',
1093 __typename: 'Todo',
1094 },
1095 ],
1096 },
1097 };
1098
1099 const reexecuteOperation = vi
1100 .spyOn(client, 'reexecuteOperation')
1101 .mockImplementation(next);
1102
1103 const response = vi.fn((forwardOp: Operation): OperationResult => {
1104 if (forwardOp.key === 1) return queryResult;
1105 return undefined as any;
1106 });
1107
1108 const result = vi.fn();
1109 const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share);
1110
1111 pipe(
1112 cacheExchange({})({ forward, client, dispatchDebug })(ops$),
1113 tap(result),
1114 publish
1115 );
1116
1117 next(operation);
1118
1119 expect(response).toHaveBeenCalledTimes(1);
1120 expect(result).toHaveBeenCalledTimes(1);
1121 expect(reexecuteOperation).toHaveBeenCalledTimes(0);
1122 expect(result.mock.calls[0][0].data).toEqual({
1123 todos: [
1124 {
1125 completed: null,
1126 id: '1',
1127 text: 'learn urql',
1128 },
1129 ],
1130 });
1131 });
1132
1133 it('Does not return partial data for nested selections', () => {
1134 const client = createClient({
1135 url: 'http://0.0.0.0',
1136 exchanges: [],
1137 });
1138 const { source: ops$, next } = makeSubject<Operation>();
1139
1140 const query = gql`
1141 {
1142 todo {
1143 ... on Todo @_optional {
1144 id
1145 text
1146 author {
1147 id
1148 name
1149 }
1150 }
1151 }
1152 }
1153 `;
1154
1155 const operation = client.createRequestOperation('query', {
1156 key: 1,
1157 query,
1158 variables: undefined,
1159 });
1160
1161 const queryResult: OperationResult = {
1162 ...queryResponse,
1163 operation,
1164 data: {
1165 __typename: 'Query',
1166 todo: {
1167 id: '1',
1168 text: 'learn urql',
1169 __typename: 'Todo',
1170 author: {
1171 __typename: 'Author',
1172 },
1173 },
1174 },
1175 };
1176
1177 const reexecuteOperation = vi
1178 .spyOn(client, 'reexecuteOperation')
1179 .mockImplementation(next);
1180
1181 const response = vi.fn((forwardOp: Operation): OperationResult => {
1182 if (forwardOp.key === 1) return queryResult;
1183 return undefined as any;
1184 });
1185
1186 const result = vi.fn();
1187 const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share);
1188
1189 pipe(
1190 cacheExchange({})({ forward, client, dispatchDebug })(ops$),
1191 tap(result),
1192 publish
1193 );
1194
1195 next(operation);
1196
1197 expect(response).toHaveBeenCalledTimes(1);
1198 expect(result).toHaveBeenCalledTimes(1);
1199 expect(reexecuteOperation).toHaveBeenCalledTimes(0);
1200 expect(result.mock.calls[0][0].data).toEqual(null);
1201 });
1202
1203 it('returns partial results when an inline-fragment is marked as optional', () => {
1204 const client = createClient({
1205 url: 'http://0.0.0.0',
1206 exchanges: [],
1207 });
1208 const { source: ops$, next } = makeSubject<Operation>();
1209
1210 const query = gql`
1211 {
1212 todos {
1213 id
1214 text
1215 ... @_optional {
1216 ... on Todo {
1217 completed
1218 }
1219 }
1220 }
1221 }
1222 `;
1223
1224 const operation = client.createRequestOperation('query', {
1225 key: 1,
1226 query,
1227 variables: undefined,
1228 });
1229
1230 const queryResult: OperationResult = {
1231 ...queryResponse,
1232 operation,
1233 data: {
1234 __typename: 'Query',
1235 todos: [
1236 {
1237 id: '1',
1238 text: 'learn urql',
1239 __typename: 'Todo',
1240 },
1241 ],
1242 },
1243 };
1244
1245 const reexecuteOperation = vi
1246 .spyOn(client, 'reexecuteOperation')
1247 .mockImplementation(next);
1248
1249 const response = vi.fn((forwardOp: Operation): OperationResult => {
1250 if (forwardOp.key === 1) return queryResult;
1251 return undefined as any;
1252 });
1253
1254 const result = vi.fn();
1255 const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share);
1256
1257 pipe(
1258 cacheExchange({})({ forward, client, dispatchDebug })(ops$),
1259 tap(result),
1260 publish
1261 );
1262
1263 next(operation);
1264
1265 expect(response).toHaveBeenCalledTimes(1);
1266 expect(result).toHaveBeenCalledTimes(1);
1267 expect(reexecuteOperation).toHaveBeenCalledTimes(0);
1268 expect(result.mock.calls[0][0].data).toEqual({
1269 todos: [
1270 {
1271 completed: null,
1272 id: '1',
1273 text: 'learn urql',
1274 },
1275 ],
1276 });
1277 });
1278
1279 it('does not return partial results when an inline-fragment is marked as optional with a required child fragment', () => {
1280 const client = createClient({
1281 url: 'http://0.0.0.0',
1282 exchanges: [],
1283 });
1284 const { source: ops$, next } = makeSubject<Operation>();
1285
1286 const query = gql`
1287 {
1288 todos {
1289 id
1290 ... on Todo @_optional {
1291 text
1292 ... on Todo @_required {
1293 completed
1294 }
1295 }
1296 }
1297 }
1298 `;
1299
1300 const operation = client.createRequestOperation('query', {
1301 key: 1,
1302 query,
1303 variables: undefined,
1304 });
1305
1306 const queryResult: OperationResult = {
1307 ...queryResponse,
1308 operation,
1309 data: {
1310 __typename: 'Query',
1311 todos: [
1312 {
1313 id: '1',
1314 text: 'learn urql',
1315 __typename: 'Todo',
1316 },
1317 ],
1318 },
1319 };
1320
1321 const reexecuteOperation = vi
1322 .spyOn(client, 'reexecuteOperation')
1323 .mockImplementation(next);
1324
1325 const response = vi.fn((forwardOp: Operation): OperationResult => {
1326 if (forwardOp.key === 1) return queryResult;
1327 return undefined as any;
1328 });
1329
1330 const result = vi.fn();
1331 const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share);
1332
1333 pipe(
1334 cacheExchange({})({ forward, client, dispatchDebug })(ops$),
1335 tap(result),
1336 publish
1337 );
1338
1339 next(operation);
1340
1341 expect(response).toHaveBeenCalledTimes(1);
1342 expect(result).toHaveBeenCalledTimes(1);
1343 expect(reexecuteOperation).toHaveBeenCalledTimes(0);
1344 expect(result.mock.calls[0][0].data).toEqual(null);
1345 });
1346
1347 it('does not return partial results when an inline-fragment is marked as optional with a required field', () => {
1348 const client = createClient({
1349 url: 'http://0.0.0.0',
1350 exchanges: [],
1351 });
1352 const { source: ops$, next } = makeSubject<Operation>();
1353
1354 const query = gql`
1355 {
1356 todos {
1357 id
1358 ... on Todo @_optional {
1359 text
1360 completed @_required
1361 }
1362 }
1363 }
1364 `;
1365
1366 const operation = client.createRequestOperation('query', {
1367 key: 1,
1368 query,
1369 variables: undefined,
1370 });
1371
1372 const queryResult: OperationResult = {
1373 ...queryResponse,
1374 operation,
1375 data: {
1376 __typename: 'Query',
1377 todos: [
1378 {
1379 id: '1',
1380 text: 'learn urql',
1381 __typename: 'Todo',
1382 },
1383 ],
1384 },
1385 };
1386
1387 const reexecuteOperation = vi
1388 .spyOn(client, 'reexecuteOperation')
1389 .mockImplementation(next);
1390
1391 const response = vi.fn((forwardOp: Operation): OperationResult => {
1392 if (forwardOp.key === 1) return queryResult;
1393 return undefined as any;
1394 });
1395
1396 const result = vi.fn();
1397 const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share);
1398
1399 pipe(
1400 cacheExchange({})({ forward, client, dispatchDebug })(ops$),
1401 tap(result),
1402 publish
1403 );
1404
1405 next(operation);
1406
1407 expect(response).toHaveBeenCalledTimes(1);
1408 expect(result).toHaveBeenCalledTimes(1);
1409 expect(reexecuteOperation).toHaveBeenCalledTimes(0);
1410 expect(result.mock.calls[0][0].data).toEqual(null);
1411 });
1412
1413 it('returns partial results when a fragment-definition is marked as optional', () => {
1414 const client = createClient({
1415 url: 'http://0.0.0.0',
1416 exchanges: [],
1417 });
1418 const { source: ops$, next } = makeSubject<Operation>();
1419
1420 const query = gql`
1421 {
1422 todos {
1423 id
1424 text
1425 ...Fields
1426 }
1427 }
1428
1429 fragment Fields on Todo @_optional {
1430 completed
1431 }
1432 `;
1433
1434 const operation = client.createRequestOperation('query', {
1435 key: 1,
1436 query,
1437 variables: undefined,
1438 });
1439
1440 const queryResult: OperationResult = {
1441 ...queryResponse,
1442 operation,
1443 data: {
1444 __typename: 'Query',
1445 todos: [
1446 {
1447 id: '1',
1448 text: 'learn urql',
1449 __typename: 'Todo',
1450 },
1451 ],
1452 },
1453 };
1454
1455 const reexecuteOperation = vi
1456 .spyOn(client, 'reexecuteOperation')
1457 .mockImplementation(next);
1458
1459 const response = vi.fn((forwardOp: Operation): OperationResult => {
1460 if (forwardOp.key === 1) return queryResult;
1461 return undefined as any;
1462 });
1463
1464 const result = vi.fn();
1465 const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share);
1466
1467 pipe(
1468 cacheExchange({})({ forward, client, dispatchDebug })(ops$),
1469 tap(result),
1470 publish
1471 );
1472
1473 next(operation);
1474
1475 expect(response).toHaveBeenCalledTimes(1);
1476 expect(result).toHaveBeenCalledTimes(1);
1477 expect(reexecuteOperation).toHaveBeenCalledTimes(0);
1478 expect(result.mock.calls[0][0].data).toEqual(null);
1479 });
1480
1481 it('does not return missing required fields', () => {
1482 const client = createClient({
1483 url: 'http://0.0.0.0',
1484 exchanges: [],
1485 });
1486 const { source: ops$, next } = makeSubject<Operation>();
1487
1488 const query = gql`
1489 {
1490 todos {
1491 id
1492 text
1493 completed @_required
1494 }
1495 }
1496 `;
1497
1498 const operation = client.createRequestOperation('query', {
1499 key: 1,
1500 query,
1501 variables: undefined,
1502 });
1503
1504 const queryResult: OperationResult = {
1505 ...queryResponse,
1506 operation,
1507 data: {
1508 __typename: 'Query',
1509 todos: [
1510 {
1511 id: '1',
1512 text: 'learn urql',
1513 __typename: 'Todo',
1514 },
1515 ],
1516 },
1517 };
1518
1519 const reexecuteOperation = vi
1520 .spyOn(client, 'reexecuteOperation')
1521 .mockImplementation(next);
1522
1523 const response = vi.fn((forwardOp: Operation): OperationResult => {
1524 if (forwardOp.key === 1) return queryResult;
1525 return undefined as any;
1526 });
1527
1528 const result = vi.fn();
1529 const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share);
1530
1531 pipe(
1532 cacheExchange({})({ forward, client, dispatchDebug })(ops$),
1533 tap(result),
1534 publish
1535 );
1536
1537 next(operation);
1538
1539 expect(response).toHaveBeenCalledTimes(1);
1540 expect(result).toHaveBeenCalledTimes(1);
1541 expect(
1542 stripIgnoredCharacters(print(response.mock.calls[0][0].query))
1543 ).toEqual('{todos{id text completed __typename}}');
1544 expect(reexecuteOperation).toHaveBeenCalledTimes(0);
1545 expect(result.mock.calls[0][0].data).toEqual(null);
1546 });
1547
1548 it('does not return missing fields when nullable fields from a defined schema are marked as required in the query', () => {
1549 const client = createClient({
1550 url: 'http://0.0.0.0',
1551 exchanges: [],
1552 });
1553 const { source: ops$, next } = makeSubject<Operation>();
1554
1555 const initialQuery = gql`
1556 query {
1557 latestTodo {
1558 id
1559 }
1560 }
1561 `;
1562
1563 const query = gql`
1564 {
1565 latestTodo {
1566 id
1567 author @_required {
1568 id
1569 name
1570 }
1571 }
1572 }
1573 `;
1574
1575 const initialQueryOperation = client.createRequestOperation('query', {
1576 key: 1,
1577 query: initialQuery,
1578 variables: undefined,
1579 });
1580
1581 const queryOperation = client.createRequestOperation('query', {
1582 key: 2,
1583 query,
1584 variables: undefined,
1585 });
1586
1587 const initialQueryResult: OperationResult = {
1588 ...queryResponse,
1589 operation: initialQueryOperation,
1590 data: {
1591 __typename: 'Query',
1592 latestTodo: {
1593 __typename: 'Todo',
1594 id: '1',
1595 },
1596 },
1597 };
1598
1599 const queryResult: OperationResult = {
1600 ...queryResponse,
1601 operation: queryOperation,
1602 data: {
1603 __typename: 'Query',
1604 latestTodo: {
1605 __typename: 'Todo',
1606 id: '1',
1607 author: null,
1608 },
1609 },
1610 };
1611
1612 const response = vi.fn((forwardOp: Operation): OperationResult => {
1613 if (forwardOp.key === 1) {
1614 return initialQueryResult;
1615 } else if (forwardOp.key === 2) {
1616 return queryResult;
1617 }
1618 return undefined as any;
1619 });
1620
1621 const result = vi.fn();
1622 const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share);
1623
1624 pipe(
1625 cacheExchange({
1626 schema: minifyIntrospectionQuery(
1627 // eslint-disable-next-line
1628 require('./test-utils/simple_schema.json')
1629 ),
1630 })({ forward, client, dispatchDebug })(ops$),
1631 tap(result),
1632 publish
1633 );
1634
1635 next(initialQueryOperation);
1636 vi.runAllTimers();
1637 next(queryOperation);
1638 vi.runAllTimers();
1639
1640 expect(result.mock.calls[0][0].data).toEqual({
1641 latestTodo: {
1642 id: '1',
1643 },
1644 });
1645 expect(result.mock.calls[1][0].data).toEqual(null);
1646 });
1647});
1648
1649describe('optimistic updates', () => {
1650 it('writes optimistic mutations to the cache', () => {
1651 vi.useFakeTimers();
1652
1653 const mutation = gql`
1654 mutation {
1655 concealAuthor {
1656 id
1657 name
1658 }
1659 }
1660 `;
1661
1662 const optimisticMutationData = {
1663 __typename: 'Mutation',
1664 concealAuthor: {
1665 __typename: 'Author',
1666 id: '123',
1667 name() {
1668 return '[REDACTED OFFLINE]';
1669 },
1670 },
1671 };
1672
1673 const mutationData = {
1674 __typename: 'Mutation',
1675 concealAuthor: {
1676 __typename: 'Author',
1677 id: '123',
1678 name: '[REDACTED ONLINE]',
1679 },
1680 };
1681
1682 const client = createClient({
1683 url: 'http://0.0.0.0',
1684 exchanges: [],
1685 });
1686 const { source: ops$, next } = makeSubject<Operation>();
1687
1688 const reexec = vi
1689 .spyOn(client, 'reexecuteOperation')
1690 .mockImplementation(next);
1691
1692 const opOne = client.createRequestOperation('query', {
1693 key: 1,
1694 query: queryOne,
1695 variables: undefined,
1696 });
1697
1698 const opMutation = client.createRequestOperation('mutation', {
1699 key: 2,
1700 query: mutation,
1701 variables: undefined,
1702 });
1703
1704 const response = vi.fn((forwardOp: Operation): OperationResult => {
1705 if (forwardOp.key === 1) {
1706 return { ...queryResponse, operation: opOne, data: queryOneData };
1707 } else if (forwardOp.key === 2) {
1708 return {
1709 ...queryResponse,
1710 operation: opMutation,
1711 data: mutationData,
1712 };
1713 }
1714
1715 return undefined as any;
1716 });
1717
1718 const result = vi.fn();
1719 const forward: ExchangeIO = ops$ =>
1720 pipe(ops$, delay(1), map(response), share);
1721
1722 const optimistic = {
1723 concealAuthor: vi.fn(() => optimisticMutationData.concealAuthor) as any,
1724 };
1725
1726 pipe(
1727 cacheExchange({ optimistic })({ forward, client, dispatchDebug })(ops$),
1728 tap(result),
1729 publish
1730 );
1731
1732 next(opOne);
1733 vi.runAllTimers();
1734 expect(response).toHaveBeenCalledTimes(1);
1735
1736 next(opMutation);
1737 expect(response).toHaveBeenCalledTimes(1);
1738 expect(optimistic.concealAuthor).toHaveBeenCalledTimes(1);
1739 expect(reexec).toHaveBeenCalledTimes(1);
1740
1741 expect(result.mock.calls[1][0]?.data).toMatchObject({
1742 author: { name: '[REDACTED OFFLINE]' },
1743 });
1744
1745 vi.runAllTimers();
1746 expect(response).toHaveBeenCalledTimes(2);
1747 expect(result).toHaveBeenCalledTimes(4);
1748 });
1749
1750 it('batches optimistic mutation result application', () => {
1751 vi.useFakeTimers();
1752
1753 const mutation = gql`
1754 mutation {
1755 concealAuthor {
1756 id
1757 name
1758 }
1759 }
1760 `;
1761
1762 const optimisticMutationData = {
1763 __typename: 'Mutation',
1764 concealAuthor: {
1765 __typename: 'Author',
1766 id: '123',
1767 name: '[REDACTED OFFLINE]',
1768 },
1769 };
1770
1771 const mutationData = {
1772 __typename: 'Mutation',
1773 concealAuthor: {
1774 __typename: 'Author',
1775 id: '123',
1776 name: '[REDACTED ONLINE]',
1777 },
1778 };
1779
1780 const client = createClient({
1781 url: 'http://0.0.0.0',
1782 exchanges: [],
1783 });
1784 const { source: ops$, next } = makeSubject<Operation>();
1785
1786 const reexec = vi
1787 .spyOn(client, 'reexecuteOperation')
1788 .mockImplementation(next);
1789
1790 const opOne = client.createRequestOperation('query', {
1791 key: 1,
1792 query: queryOne,
1793 variables: undefined,
1794 });
1795
1796 const opMutationOne = client.createRequestOperation('mutation', {
1797 key: 2,
1798 query: mutation,
1799 variables: undefined,
1800 });
1801
1802 const opMutationTwo = client.createRequestOperation('mutation', {
1803 key: 3,
1804 query: mutation,
1805 variables: undefined,
1806 });
1807
1808 const response = vi.fn((forwardOp: Operation): OperationResult => {
1809 if (forwardOp.key === 1) {
1810 return { ...queryResponse, operation: opOne, data: queryOneData };
1811 } else if (forwardOp.key === 2) {
1812 return {
1813 ...queryResponse,
1814 operation: opMutationOne,
1815 data: mutationData,
1816 };
1817 } else if (forwardOp.key === 3) {
1818 return {
1819 ...queryResponse,
1820 operation: opMutationTwo,
1821 data: mutationData,
1822 };
1823 }
1824
1825 return undefined as any;
1826 });
1827
1828 const result = vi.fn();
1829 const forward: ExchangeIO = ops$ =>
1830 pipe(ops$, delay(3), map(response), share);
1831
1832 const optimistic = {
1833 concealAuthor: vi.fn(() => optimisticMutationData.concealAuthor) as any,
1834 };
1835
1836 pipe(
1837 cacheExchange({ optimistic })({ forward, client, dispatchDebug })(ops$),
1838 filter(x => x.operation.kind === 'mutation'),
1839 tap(result),
1840 publish
1841 );
1842
1843 next(opOne);
1844 vi.runAllTimers();
1845 expect(response).toHaveBeenCalledTimes(1);
1846 expect(result).toHaveBeenCalledTimes(0);
1847
1848 next(opMutationOne);
1849 vi.advanceTimersByTime(1);
1850 next(opMutationTwo);
1851
1852 expect(response).toHaveBeenCalledTimes(1);
1853 expect(optimistic.concealAuthor).toHaveBeenCalledTimes(2);
1854 expect(reexec).toHaveBeenCalledTimes(1);
1855 expect(result).toHaveBeenCalledTimes(0);
1856
1857 vi.advanceTimersByTime(2);
1858 expect(response).toHaveBeenCalledTimes(2);
1859 expect(reexec).toHaveBeenCalledTimes(2);
1860 expect(result).toHaveBeenCalledTimes(1);
1861
1862 vi.runAllTimers();
1863 expect(response).toHaveBeenCalledTimes(3);
1864 expect(reexec).toHaveBeenCalledTimes(2);
1865 expect(result).toHaveBeenCalledTimes(2);
1866 });
1867
1868 it('blocks refetches of overlapping queries', () => {
1869 vi.useFakeTimers();
1870
1871 const mutation = gql`
1872 mutation {
1873 concealAuthor {
1874 id
1875 name
1876 }
1877 }
1878 `;
1879
1880 const optimisticMutationData = {
1881 __typename: 'Mutation',
1882 concealAuthor: {
1883 __typename: 'Author',
1884 id: '123',
1885 name: '[REDACTED OFFLINE]',
1886 },
1887 };
1888
1889 const client = createClient({
1890 url: 'http://0.0.0.0',
1891 exchanges: [],
1892 });
1893 const { source: ops$, next } = makeSubject<Operation>();
1894
1895 const reexec = vi
1896 .spyOn(client, 'reexecuteOperation')
1897 .mockImplementation(next);
1898
1899 const opOne = client.createRequestOperation(
1900 'query',
1901 {
1902 key: 1,
1903 query: queryOne,
1904 variables: undefined,
1905 },
1906 {
1907 requestPolicy: 'cache-and-network',
1908 }
1909 );
1910
1911 const opMutation = client.createRequestOperation('mutation', {
1912 key: 2,
1913 query: mutation,
1914 variables: undefined,
1915 });
1916
1917 const response = vi.fn((forwardOp: Operation): OperationResult => {
1918 if (forwardOp.key === 1) {
1919 return { ...queryResponse, operation: opOne, data: queryOneData };
1920 }
1921
1922 return undefined as any;
1923 });
1924
1925 const result = vi.fn();
1926 const forward: ExchangeIO = ops$ =>
1927 pipe(
1928 ops$,
1929 delay(1),
1930 filter(x => x.kind !== 'mutation'),
1931 map(response),
1932 share
1933 );
1934
1935 const optimistic = {
1936 concealAuthor: vi.fn(() => optimisticMutationData.concealAuthor) as any,
1937 };
1938
1939 pipe(
1940 cacheExchange({ optimistic })({ forward, client, dispatchDebug })(ops$),
1941 tap(result),
1942 publish
1943 );
1944
1945 next(opOne);
1946 vi.runAllTimers();
1947 expect(response).toHaveBeenCalledTimes(1);
1948
1949 next(opMutation);
1950 expect(response).toHaveBeenCalledTimes(1);
1951 expect(optimistic.concealAuthor).toHaveBeenCalledTimes(1);
1952 expect(reexec).toHaveBeenCalledTimes(1);
1953
1954 expect(reexec.mock.calls[0][0]).toHaveProperty(
1955 'context.requestPolicy',
1956 'cache-first'
1957 );
1958
1959 vi.runAllTimers();
1960 expect(response).toHaveBeenCalledTimes(1);
1961
1962 next(opOne);
1963 expect(response).toHaveBeenCalledTimes(1);
1964 expect(reexec).toHaveBeenCalledTimes(1);
1965 });
1966
1967 it('correctly clears on error', () => {
1968 vi.useFakeTimers();
1969
1970 const authorsQuery = gql`
1971 query {
1972 authors {
1973 id
1974 name
1975 }
1976 }
1977 `;
1978
1979 const authorsQueryData = {
1980 __typename: 'Query',
1981 authors: [
1982 {
1983 __typename: 'Author',
1984 id: '1',
1985 name: 'Author',
1986 },
1987 ],
1988 };
1989
1990 const mutation = gql`
1991 mutation {
1992 addAuthor {
1993 id
1994 name
1995 }
1996 }
1997 `;
1998
1999 const optimisticMutationData = {
2000 __typename: 'Mutation',
2001 addAuthor: {
2002 __typename: 'Author',
2003 id: '123',
2004 name: '[REDACTED OFFLINE]',
2005 },
2006 };
2007
2008 const client = createClient({
2009 url: 'http://0.0.0.0',
2010 exchanges: [],
2011 });
2012 const { source: ops$, next } = makeSubject<Operation>();
2013
2014 const reexec = vi
2015 .spyOn(client, 'reexecuteOperation')
2016 .mockImplementation(next);
2017
2018 const opOne = client.createRequestOperation('query', {
2019 key: 1,
2020 query: authorsQuery,
2021 variables: undefined,
2022 });
2023
2024 const opMutation = client.createRequestOperation('mutation', {
2025 key: 2,
2026 query: mutation,
2027 variables: undefined,
2028 });
2029
2030 const response = vi.fn((forwardOp: Operation): OperationResult => {
2031 if (forwardOp.key === 1) {
2032 return { ...queryResponse, operation: opOne, data: authorsQueryData };
2033 } else if (forwardOp.key === 2) {
2034 return {
2035 ...queryResponse,
2036 operation: opMutation,
2037 error: 'error' as any,
2038 data: { __typename: 'Mutation', addAuthor: null },
2039 };
2040 }
2041
2042 return undefined as any;
2043 });
2044
2045 const result = vi.fn();
2046 const forward: ExchangeIO = ops$ =>
2047 pipe(ops$, delay(1), map(response), share);
2048
2049 const optimistic = {
2050 addAuthor: vi.fn(() => optimisticMutationData.addAuthor) as any,
2051 };
2052
2053 const updates = {
2054 Mutation: {
2055 addAuthor: vi.fn((data, _, cache) => {
2056 cache.updateQuery({ query: authorsQuery }, (prevData: any) => ({
2057 ...prevData,
2058 authors: [...prevData.authors, data.addAuthor],
2059 }));
2060 }),
2061 },
2062 };
2063
2064 pipe(
2065 cacheExchange({ optimistic, updates })({
2066 forward,
2067 client,
2068 dispatchDebug,
2069 })(ops$),
2070 tap(result),
2071 publish
2072 );
2073
2074 next(opOne);
2075 vi.runAllTimers();
2076 expect(response).toHaveBeenCalledTimes(1);
2077
2078 next(opMutation);
2079 expect(response).toHaveBeenCalledTimes(1);
2080 expect(optimistic.addAuthor).toHaveBeenCalledTimes(1);
2081 expect(updates.Mutation.addAuthor).toHaveBeenCalledTimes(1);
2082 expect(reexec).toHaveBeenCalledTimes(1);
2083
2084 vi.runAllTimers();
2085
2086 expect(updates.Mutation.addAuthor).toHaveBeenCalledTimes(2);
2087 expect(response).toHaveBeenCalledTimes(2);
2088 expect(result).toHaveBeenCalledTimes(4);
2089 expect(reexec).toHaveBeenCalledTimes(2);
2090
2091 next(opOne);
2092 vi.runAllTimers();
2093 expect(result).toHaveBeenCalledTimes(5);
2094 });
2095
2096 it('does not block subsequent query operations', () => {
2097 vi.useFakeTimers();
2098
2099 const authorsQuery = gql`
2100 query {
2101 authors {
2102 id
2103 name
2104 }
2105 }
2106 `;
2107
2108 const authorsQueryData = {
2109 __typename: 'Query',
2110 authors: [
2111 {
2112 __typename: 'Author',
2113 id: '123',
2114 name: 'Author',
2115 },
2116 ],
2117 };
2118
2119 const mutation = gql`
2120 mutation {
2121 deleteAuthor {
2122 id
2123 name
2124 }
2125 }
2126 `;
2127
2128 const optimisticMutationData = {
2129 __typename: 'Mutation',
2130 deleteAuthor: {
2131 __typename: 'Author',
2132 id: '123',
2133 name: '[REDACTED OFFLINE]',
2134 },
2135 };
2136
2137 const client = createClient({
2138 url: 'http://0.0.0.0',
2139 exchanges: [],
2140 });
2141 const { source: ops$, next } = makeSubject<Operation>();
2142
2143 const reexec = vi
2144 .spyOn(client, 'reexecuteOperation')
2145 .mockImplementation(next);
2146
2147 const opOne = client.createRequestOperation('query', {
2148 key: 1,
2149 query: authorsQuery,
2150 variables: undefined,
2151 });
2152
2153 const opMutation = client.createRequestOperation('mutation', {
2154 key: 2,
2155 query: mutation,
2156 variables: undefined,
2157 });
2158
2159 const response = vi.fn((forwardOp: Operation): OperationResult => {
2160 if (forwardOp.key === 1) {
2161 return { ...queryResponse, operation: opOne, data: authorsQueryData };
2162 } else if (forwardOp.key === 2) {
2163 return {
2164 ...queryResponse,
2165 operation: opMutation,
2166 data: {
2167 __typename: 'Mutation',
2168 deleteAuthor: optimisticMutationData.deleteAuthor,
2169 },
2170 };
2171 }
2172
2173 return undefined as any;
2174 });
2175
2176 const result = vi.fn();
2177 const forward: ExchangeIO = ops$ =>
2178 pipe(ops$, delay(1), map(response), share);
2179
2180 const optimistic = {
2181 deleteAuthor: vi.fn(() => optimisticMutationData.deleteAuthor) as any,
2182 };
2183
2184 const updates = {
2185 Mutation: {
2186 deleteAuthor: vi.fn((_data, _, cache) => {
2187 cache.invalidate({
2188 __typename: 'Author',
2189 id: optimisticMutationData.deleteAuthor.id,
2190 });
2191 }),
2192 },
2193 };
2194
2195 pipe(
2196 cacheExchange({ optimistic, updates })({
2197 forward,
2198 client,
2199 dispatchDebug,
2200 })(ops$),
2201 tap(result),
2202 publish
2203 );
2204
2205 next(opOne);
2206 vi.runAllTimers();
2207 expect(response).toHaveBeenCalledTimes(1);
2208 expect(result).toHaveBeenCalledTimes(1);
2209
2210 next(opMutation);
2211 expect(response).toHaveBeenCalledTimes(1);
2212 expect(optimistic.deleteAuthor).toHaveBeenCalledTimes(1);
2213 expect(updates.Mutation.deleteAuthor).toHaveBeenCalledTimes(1);
2214 expect(reexec).toHaveBeenCalledTimes(1);
2215 expect(result).toHaveBeenCalledTimes(1);
2216
2217 vi.runAllTimers();
2218
2219 expect(updates.Mutation.deleteAuthor).toHaveBeenCalledTimes(2);
2220 expect(response).toHaveBeenCalledTimes(2);
2221 expect(result).toHaveBeenCalledTimes(2);
2222 expect(reexec).toHaveBeenCalledTimes(2);
2223 expect(reexec.mock.calls[1][0]).toMatchObject(opOne);
2224
2225 next(opOne);
2226 vi.runAllTimers();
2227 expect(result).toHaveBeenCalledTimes(3);
2228 });
2229});
2230
2231describe('mutation updates', () => {
2232 it('invalidates the type when the entity is not present in the cache', () => {
2233 vi.useFakeTimers();
2234
2235 const authorsQuery = gql`
2236 query {
2237 authors {
2238 id
2239 name
2240 }
2241 }
2242 `;
2243
2244 const authorsQueryData = {
2245 __typename: 'Query',
2246 authors: [
2247 {
2248 __typename: 'Author',
2249 id: '1',
2250 name: 'Author',
2251 },
2252 ],
2253 };
2254
2255 const mutation = gql`
2256 mutation {
2257 addAuthor {
2258 id
2259 name
2260 }
2261 }
2262 `;
2263
2264 const client = createClient({
2265 url: 'http://0.0.0.0',
2266 exchanges: [],
2267 });
2268 const { source: ops$, next } = makeSubject<Operation>();
2269
2270 const reexec = vi
2271 .spyOn(client, 'reexecuteOperation')
2272 .mockImplementation(next);
2273
2274 const opOne = client.createRequestOperation('query', {
2275 key: 1,
2276 query: authorsQuery,
2277 variables: undefined,
2278 });
2279
2280 const opMutation = client.createRequestOperation('mutation', {
2281 key: 2,
2282 query: mutation,
2283 variables: undefined,
2284 });
2285
2286 const response = vi.fn((forwardOp: Operation): OperationResult => {
2287 if (forwardOp.key === 1) {
2288 return { ...queryResponse, operation: opOne, data: authorsQueryData };
2289 } else if (forwardOp.key === 2) {
2290 return {
2291 ...queryResponse,
2292 operation: opMutation,
2293 data: {
2294 __typename: 'Mutation',
2295 addAuthor: { id: '2', name: 'Author 2', __typename: 'Author' },
2296 },
2297 };
2298 }
2299
2300 return undefined as any;
2301 });
2302
2303 const result = vi.fn();
2304 const forward: ExchangeIO = ops$ =>
2305 pipe(ops$, delay(1), map(response), share);
2306
2307 pipe(
2308 cacheExchange()({
2309 forward,
2310 client,
2311 dispatchDebug,
2312 })(ops$),
2313 tap(result),
2314 publish
2315 );
2316
2317 next(opOne);
2318 vi.runAllTimers();
2319 expect(response).toHaveBeenCalledTimes(1);
2320
2321 next(opMutation);
2322 expect(response).toHaveBeenCalledTimes(1);
2323 expect(reexec).toHaveBeenCalledTimes(0);
2324
2325 vi.runAllTimers();
2326
2327 expect(response).toHaveBeenCalledTimes(2);
2328 expect(result).toHaveBeenCalledTimes(2);
2329 expect(reexec).toHaveBeenCalledTimes(1);
2330
2331 next(opOne);
2332 vi.runAllTimers();
2333 expect(response).toHaveBeenCalledTimes(3);
2334 expect(result).toHaveBeenCalledTimes(3);
2335 expect(result.mock.calls[1][0].data).toEqual({
2336 addAuthor: {
2337 id: '2',
2338 name: 'Author 2',
2339 },
2340 });
2341 });
2342});
2343
2344describe('extra variables', () => {
2345 it('allows extra variables to be applied to updates', () => {
2346 vi.useFakeTimers();
2347
2348 const mutation = gql`
2349 mutation TestMutation($test: Boolean) {
2350 test(test: $test) {
2351 id
2352 }
2353 }
2354 `;
2355
2356 const mutationData = {
2357 __typename: 'Mutation',
2358 test: {
2359 __typename: 'Author',
2360 id: '123',
2361 },
2362 };
2363
2364 const client = createClient({
2365 url: 'http://0.0.0.0',
2366 exchanges: [],
2367 });
2368
2369 const { source: ops$, next } = makeSubject<Operation>();
2370
2371 const opQuery = client.createRequestOperation('query', {
2372 key: 1,
2373 query: queryOne,
2374 variables: undefined,
2375 });
2376
2377 const opMutation = client.createRequestOperation('mutation', {
2378 key: 2,
2379 query: mutation,
2380 variables: {
2381 test: true,
2382 extra: 'extra',
2383 },
2384 });
2385
2386 const response = vi.fn((forwardOp: Operation): OperationResult => {
2387 if (forwardOp.key === 1) {
2388 return { ...queryResponse, operation: forwardOp, data: queryOneData };
2389 } else if (forwardOp.key === 2) {
2390 return {
2391 ...queryResponse,
2392 operation: forwardOp,
2393 data: mutationData,
2394 };
2395 }
2396
2397 return undefined as any;
2398 });
2399
2400 const result = vi.fn();
2401 const forward: ExchangeIO = ops$ =>
2402 pipe(ops$, delay(3), map(response), share);
2403
2404 const optimistic = {
2405 test: vi.fn() as any,
2406 };
2407
2408 const updates = {
2409 Mutation: {
2410 test: vi.fn() as any,
2411 },
2412 };
2413
2414 pipe(
2415 cacheExchange({ optimistic, updates })({
2416 forward,
2417 client,
2418 dispatchDebug,
2419 })(ops$),
2420 filter(x => x.operation.kind === 'mutation'),
2421 tap(result),
2422 publish
2423 );
2424
2425 next(opQuery);
2426 vi.runAllTimers();
2427 expect(response).toHaveBeenCalledTimes(1);
2428 expect(result).toHaveBeenCalledTimes(0);
2429
2430 next(opMutation);
2431 vi.advanceTimersByTime(1);
2432
2433 expect(response).toHaveBeenCalledTimes(1);
2434 expect(result).toHaveBeenCalledTimes(0);
2435 expect(optimistic.test).toHaveBeenCalledTimes(1);
2436
2437 expect(optimistic.test.mock.calls[0][2].variables).toEqual({
2438 test: true,
2439 extra: 'extra',
2440 });
2441
2442 vi.runAllTimers();
2443
2444 expect(response).toHaveBeenCalledTimes(2);
2445 expect(result).toHaveBeenCalledTimes(1);
2446 expect(updates.Mutation.test).toHaveBeenCalledTimes(2);
2447
2448 expect(updates.Mutation.test.mock.calls[1][3].variables).toEqual({
2449 test: true,
2450 extra: 'extra',
2451 });
2452 });
2453});
2454
2455describe('custom resolvers', () => {
2456 it('follows resolvers on initial write', () => {
2457 const client = createClient({
2458 url: 'http://0.0.0.0',
2459 exchanges: [],
2460 });
2461 const { source: ops$, next } = makeSubject<Operation>();
2462
2463 const opOne = client.createRequestOperation('query', {
2464 key: 1,
2465 query: queryOne,
2466 variables: undefined,
2467 });
2468
2469 const response = vi.fn((forwardOp: Operation): OperationResult => {
2470 if (forwardOp.key === 1) {
2471 return { ...queryResponse, operation: opOne, data: queryOneData };
2472 }
2473
2474 return undefined as any;
2475 });
2476
2477 const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share);
2478
2479 const result = vi.fn();
2480 const fakeResolver = vi.fn();
2481
2482 pipe(
2483 cacheExchange({
2484 resolvers: {
2485 Author: {
2486 name: () => {
2487 fakeResolver();
2488 return 'newName';
2489 },
2490 },
2491 },
2492 })({ forward, client, dispatchDebug })(ops$),
2493 tap(result),
2494 publish
2495 );
2496
2497 next(opOne);
2498 expect(response).toHaveBeenCalledTimes(1);
2499 expect(fakeResolver).toHaveBeenCalledTimes(1);
2500 expect(result).toHaveBeenCalledTimes(1);
2501 expect(result.mock.calls[0][0].data).toMatchObject({
2502 author: {
2503 id: '123',
2504 name: 'newName',
2505 },
2506 });
2507 });
2508
2509 it('follows resolvers for mutations', () => {
2510 vi.useFakeTimers();
2511
2512 const mutation = gql`
2513 mutation {
2514 concealAuthor {
2515 id
2516 name
2517 __typename
2518 }
2519 }
2520 `;
2521
2522 const mutationData = {
2523 __typename: 'Mutation',
2524 concealAuthor: {
2525 __typename: 'Author',
2526 id: '123',
2527 name: '[REDACTED ONLINE]',
2528 },
2529 };
2530
2531 const client = createClient({
2532 url: 'http://0.0.0.0',
2533 exchanges: [],
2534 });
2535 const { source: ops$, next } = makeSubject<Operation>();
2536
2537 const opOne = client.createRequestOperation('query', {
2538 key: 1,
2539 query: queryOne,
2540 variables: undefined,
2541 });
2542
2543 const opMutation = client.createRequestOperation('mutation', {
2544 key: 2,
2545 query: mutation,
2546 variables: undefined,
2547 });
2548
2549 const response = vi.fn((forwardOp: Operation): OperationResult => {
2550 if (forwardOp.key === 1) {
2551 return { ...queryResponse, operation: opOne, data: queryOneData };
2552 } else if (forwardOp.key === 2) {
2553 return {
2554 ...queryResponse,
2555 operation: opMutation,
2556 data: mutationData,
2557 };
2558 }
2559
2560 return undefined as any;
2561 });
2562
2563 const result = vi.fn();
2564 const forward: ExchangeIO = ops$ =>
2565 pipe(ops$, delay(1), map(response), share);
2566
2567 const fakeResolver = vi.fn();
2568
2569 pipe(
2570 cacheExchange({
2571 resolvers: {
2572 Author: {
2573 name: () => {
2574 fakeResolver();
2575 return 'newName';
2576 },
2577 },
2578 },
2579 })({ forward, client, dispatchDebug })(ops$),
2580 tap(result),
2581 publish
2582 );
2583
2584 next(opOne);
2585 vi.runAllTimers();
2586 expect(response).toHaveBeenCalledTimes(1);
2587
2588 next(opMutation);
2589 expect(response).toHaveBeenCalledTimes(1);
2590 expect(fakeResolver).toHaveBeenCalledTimes(1);
2591
2592 vi.runAllTimers();
2593 expect(result.mock.calls[1][0].data).toEqual({
2594 concealAuthor: {
2595 __typename: 'Author',
2596 id: '123',
2597 name: 'newName',
2598 },
2599 });
2600 });
2601
2602 it('follows nested resolvers for mutations', () => {
2603 vi.useFakeTimers();
2604
2605 const mutation = gql`
2606 mutation {
2607 concealAuthors {
2608 id
2609 name
2610 book {
2611 id
2612 title
2613 __typename
2614 }
2615 __typename
2616 }
2617 }
2618 `;
2619
2620 const client = createClient({
2621 url: 'http://0.0.0.0',
2622 exchanges: [],
2623 });
2624 const { source: ops$, next } = makeSubject<Operation>();
2625
2626 const query = gql`
2627 query {
2628 authors {
2629 id
2630 name
2631 book {
2632 id
2633 title
2634 __typename
2635 }
2636 __typename
2637 }
2638 }
2639 `;
2640
2641 const queryOperation = client.createRequestOperation('query', {
2642 key: 1,
2643 query,
2644 variables: undefined,
2645 });
2646
2647 const mutationOperation = client.createRequestOperation('mutation', {
2648 key: 2,
2649 query: mutation,
2650 variables: undefined,
2651 });
2652
2653 const mutationData = {
2654 __typename: 'Mutation',
2655 concealAuthors: [
2656 {
2657 __typename: 'Author',
2658 id: '123',
2659 book: null,
2660 name: '[REDACTED ONLINE]',
2661 },
2662 {
2663 __typename: 'Author',
2664 id: '456',
2665 name: 'Formidable',
2666 book: {
2667 id: '1',
2668 title: 'AwesomeGQL',
2669 __typename: 'Book',
2670 },
2671 },
2672 ],
2673 };
2674
2675 const queryData = {
2676 __typename: 'Query',
2677 authors: [
2678 {
2679 __typename: 'Author',
2680 id: '123',
2681 name: '[REDACTED ONLINE]',
2682 book: null,
2683 },
2684 {
2685 __typename: 'Author',
2686 id: '456',
2687 name: 'Formidable',
2688 book: {
2689 id: '1',
2690 title: 'AwesomeGQL',
2691 __typename: 'Book',
2692 },
2693 },
2694 ],
2695 };
2696
2697 const response = vi.fn((forwardOp: Operation): OperationResult => {
2698 if (forwardOp.key === 1) {
2699 return {
2700 ...queryResponse,
2701 operation: queryOperation,
2702 data: queryData,
2703 };
2704 } else if (forwardOp.key === 2) {
2705 return {
2706 ...queryResponse,
2707 operation: mutationOperation,
2708 data: mutationData,
2709 };
2710 }
2711
2712 return undefined as any;
2713 });
2714
2715 const result = vi.fn();
2716 const forward: ExchangeIO = ops$ =>
2717 pipe(ops$, delay(1), map(response), share);
2718
2719 const fakeResolver = vi.fn();
2720 const called: any[] = [];
2721
2722 pipe(
2723 cacheExchange({
2724 resolvers: {
2725 Query: {
2726 // TS-check
2727 author: (_parent, args) => ({ __typename: 'Author', id: args.id }),
2728 },
2729 Author: {
2730 name: parent => {
2731 called.push(parent.name);
2732 fakeResolver();
2733 return 'Secret Author';
2734 },
2735 },
2736 Book: {
2737 title: parent => {
2738 called.push(parent.title);
2739 fakeResolver();
2740 return 'Secret Book';
2741 },
2742 },
2743 },
2744 })({ forward, client, dispatchDebug })(ops$),
2745 tap(result),
2746 publish
2747 );
2748
2749 next(queryOperation);
2750 vi.runAllTimers();
2751 expect(response).toHaveBeenCalledTimes(1);
2752 expect(fakeResolver).toHaveBeenCalledTimes(3);
2753
2754 next(mutationOperation);
2755 vi.runAllTimers();
2756 expect(response).toHaveBeenCalledTimes(2);
2757 expect(fakeResolver).toHaveBeenCalledTimes(6);
2758 expect(result.mock.calls[1][0].data).toEqual({
2759 concealAuthors: [
2760 {
2761 __typename: 'Author',
2762 id: '123',
2763 book: null,
2764 name: 'Secret Author',
2765 },
2766 {
2767 __typename: 'Author',
2768 id: '456',
2769 name: 'Secret Author',
2770 book: {
2771 id: '1',
2772 title: 'Secret Book',
2773 __typename: 'Book',
2774 },
2775 },
2776 ],
2777 });
2778
2779 expect(called).toEqual([
2780 // Query
2781 '[REDACTED ONLINE]',
2782 'Formidable',
2783 'AwesomeGQL',
2784 // Mutation
2785 '[REDACTED ONLINE]',
2786 'Formidable',
2787 'AwesomeGQL',
2788 ]);
2789 });
2790});
2791
2792describe('schema awareness', () => {
2793 it('reexecutes query and returns data on partial result', () => {
2794 vi.useFakeTimers();
2795 const client = createClient({
2796 url: 'http://0.0.0.0',
2797 exchanges: [],
2798 });
2799 const { source: ops$, next } = makeSubject<Operation>();
2800 const reexec = vi
2801 .spyOn(client, 'reexecuteOperation')
2802 // Empty mock to avoid going in an endless loop, since we would again return
2803 // partial data.
2804 .mockImplementation(() => undefined);
2805
2806 const initialQuery = gql`
2807 query {
2808 todos {
2809 id
2810 text
2811 __typename
2812 }
2813 }
2814 `;
2815
2816 const query = gql`
2817 query {
2818 todos {
2819 id
2820 text
2821 complete
2822 author {
2823 id
2824 name
2825 __typename
2826 }
2827 __typename
2828 }
2829 }
2830 `;
2831
2832 const initialQueryOperation = client.createRequestOperation('query', {
2833 key: 1,
2834 query: initialQuery,
2835 variables: undefined,
2836 });
2837
2838 const queryOperation = client.createRequestOperation('query', {
2839 key: 2,
2840 query,
2841 variables: undefined,
2842 });
2843
2844 const queryData = {
2845 __typename: 'Query',
2846 todos: [
2847 {
2848 __typename: 'Todo',
2849 id: '123',
2850 text: 'Learn',
2851 },
2852 {
2853 __typename: 'Todo',
2854 id: '456',
2855 text: 'Teach',
2856 },
2857 ],
2858 };
2859
2860 const response = vi.fn((forwardOp: Operation): OperationResult => {
2861 if (forwardOp.key === 1) {
2862 return {
2863 ...queryResponse,
2864 operation: initialQueryOperation,
2865 data: queryData,
2866 };
2867 } else if (forwardOp.key === 2) {
2868 return {
2869 ...queryResponse,
2870 operation: queryOperation,
2871 data: queryData,
2872 };
2873 }
2874
2875 return undefined as any;
2876 });
2877
2878 const result = vi.fn();
2879 const forward: ExchangeIO = ops$ =>
2880 pipe(ops$, delay(1), map(response), share);
2881
2882 pipe(
2883 cacheExchange({
2884 schema: minifyIntrospectionQuery(
2885 // eslint-disable-next-line
2886 require('./test-utils/simple_schema.json')
2887 ),
2888 })({ forward, client, dispatchDebug })(ops$),
2889 tap(result),
2890 publish
2891 );
2892
2893 next(initialQueryOperation);
2894 vi.runAllTimers();
2895 expect(response).toHaveBeenCalledTimes(1);
2896 expect(reexec).toHaveBeenCalledTimes(0);
2897 expect(result.mock.calls[0][0].data).toMatchObject({
2898 todos: [
2899 {
2900 __typename: 'Todo',
2901 id: '123',
2902 text: 'Learn',
2903 },
2904 {
2905 __typename: 'Todo',
2906 id: '456',
2907 text: 'Teach',
2908 },
2909 ],
2910 });
2911
2912 next(queryOperation);
2913 vi.runAllTimers();
2914 expect(result).toHaveBeenCalledTimes(2);
2915 expect(reexec).toHaveBeenCalledTimes(1);
2916 expect(result.mock.calls[1][0].stale).toBe(true);
2917 expect(result.mock.calls[1][0].data).toEqual({
2918 todos: [
2919 {
2920 __typename: 'Todo',
2921 author: null,
2922 complete: null,
2923 id: '123',
2924 text: 'Learn',
2925 },
2926 {
2927 __typename: 'Todo',
2928 author: null,
2929 complete: null,
2930 id: '456',
2931 text: 'Teach',
2932 },
2933 ],
2934 });
2935
2936 expect(result.mock.calls[1][0]).toHaveProperty(
2937 'operation.context.meta.cacheOutcome',
2938 'partial'
2939 );
2940 });
2941
2942 it('reexecutes query and returns data on partial results for nullable lists', () => {
2943 vi.useFakeTimers();
2944 const client = createClient({
2945 url: 'http://0.0.0.0',
2946 exchanges: [],
2947 });
2948 const { source: ops$, next } = makeSubject<Operation>();
2949 const reexec = vi
2950 .spyOn(client, 'reexecuteOperation')
2951 // Empty mock to avoid going in an endless loop, since we would again return
2952 // partial data.
2953 .mockImplementation(() => undefined);
2954
2955 const initialQuery = gql`
2956 query {
2957 todos {
2958 id
2959 __typename
2960 }
2961 }
2962 `;
2963
2964 const query = gql`
2965 query {
2966 todos {
2967 id
2968 text
2969 __typename
2970 }
2971 }
2972 `;
2973
2974 const initialQueryOperation = client.createRequestOperation('query', {
2975 key: 1,
2976 query: initialQuery,
2977 variables: undefined,
2978 });
2979
2980 const queryOperation = client.createRequestOperation('query', {
2981 key: 2,
2982 query,
2983 variables: undefined,
2984 });
2985
2986 const queryData = {
2987 __typename: 'Query',
2988 todos: [
2989 {
2990 __typename: 'Todo',
2991 id: '123',
2992 },
2993 {
2994 __typename: 'Todo',
2995 id: '456',
2996 },
2997 ],
2998 };
2999
3000 const response = vi.fn((forwardOp: Operation): OperationResult => {
3001 if (forwardOp.key === 1) {
3002 return {
3003 ...queryResponse,
3004 operation: initialQueryOperation,
3005 data: queryData,
3006 };
3007 } else if (forwardOp.key === 2) {
3008 return {
3009 ...queryResponse,
3010 operation: queryOperation,
3011 data: queryData,
3012 };
3013 }
3014
3015 return undefined as any;
3016 });
3017
3018 const result = vi.fn();
3019 const forward: ExchangeIO = ops$ =>
3020 pipe(ops$, delay(1), map(response), share);
3021
3022 pipe(
3023 cacheExchange({
3024 schema: minifyIntrospectionQuery(
3025 // eslint-disable-next-line
3026 require('./test-utils/simple_schema.json')
3027 ),
3028 })({ forward, client, dispatchDebug })(ops$),
3029 tap(result),
3030 publish
3031 );
3032
3033 next(initialQueryOperation);
3034 vi.runAllTimers();
3035 expect(response).toHaveBeenCalledTimes(1);
3036 expect(reexec).toHaveBeenCalledTimes(0);
3037 expect(result.mock.calls[0][0].data).toMatchObject({
3038 todos: [
3039 {
3040 __typename: 'Todo',
3041 id: '123',
3042 },
3043 {
3044 __typename: 'Todo',
3045 id: '456',
3046 },
3047 ],
3048 });
3049
3050 next(queryOperation);
3051 vi.runAllTimers();
3052 expect(result).toHaveBeenCalledTimes(2);
3053 expect(reexec).toHaveBeenCalledTimes(1);
3054 expect(result.mock.calls[1][0].stale).toBe(true);
3055 expect(result.mock.calls[1][0].data).toEqual({
3056 todos: [null, null],
3057 });
3058
3059 expect(result.mock.calls[1][0]).toHaveProperty(
3060 'operation.context.meta.cacheOutcome',
3061 'partial'
3062 );
3063 });
3064});
3065
3066describe('looping protection', () => {
3067 it('applies stale to blocked looping queries', () => {
3068 let normalData: OperationResult | undefined;
3069 let extendedData: OperationResult | undefined;
3070
3071 const client = createClient({
3072 url: 'http://0.0.0.0',
3073 exchanges: [],
3074 });
3075
3076 const { source: ops$, next: nextOp } = makeSubject<Operation>();
3077 const { source: res$, next: nextRes } = makeSubject<OperationResult>();
3078
3079 vi.spyOn(client, 'reexecuteOperation').mockImplementation(nextOp);
3080
3081 const normalQuery = gql`
3082 {
3083 __typename
3084 item {
3085 __typename
3086 id
3087 }
3088 }
3089 `;
3090
3091 const extendedQuery = gql`
3092 {
3093 __typename
3094 item {
3095 __typename
3096 extended: id
3097 extra @_optional
3098 }
3099 }
3100 `;
3101
3102 const forward = (ops$: Source<Operation>): Source<OperationResult> =>
3103 share(
3104 merge([
3105 pipe(
3106 ops$,
3107 filter(() => false)
3108 ) as any,
3109 res$,
3110 ])
3111 );
3112
3113 pipe(
3114 cacheExchange()({ forward, client, dispatchDebug })(ops$),
3115 tap(result => {
3116 if (result.operation.kind === 'query') {
3117 if (result.operation.key === 1) {
3118 normalData = result;
3119 } else if (result.operation.key === 2) {
3120 extendedData = result;
3121 }
3122 }
3123 }),
3124 publish
3125 );
3126
3127 const normalOp = client.createRequestOperation(
3128 'query',
3129 {
3130 key: 1,
3131 query: normalQuery,
3132 variables: undefined,
3133 },
3134 {
3135 requestPolicy: 'cache-first',
3136 }
3137 );
3138
3139 const extendedOp = client.createRequestOperation(
3140 'query',
3141 {
3142 key: 2,
3143 query: extendedQuery,
3144 variables: undefined,
3145 },
3146 {
3147 requestPolicy: 'cache-first',
3148 }
3149 );
3150
3151 nextOp(normalOp);
3152
3153 nextRes({
3154 operation: normalOp,
3155 data: {
3156 __typename: 'Query',
3157 item: {
3158 __typename: 'Node',
3159 id: 'id',
3160 },
3161 },
3162 stale: false,
3163 hasNext: false,
3164 });
3165
3166 expect(normalData).toMatchObject({ stale: false });
3167 expect(client.reexecuteOperation).toHaveBeenCalledTimes(0);
3168
3169 nextOp(extendedOp);
3170
3171 expect(extendedData).toMatchObject({ stale: true });
3172 expect(client.reexecuteOperation).toHaveBeenCalledTimes(1);
3173
3174 // Out of band re-execute first operation
3175 nextOp(normalOp);
3176 nextRes({
3177 ...queryResponse,
3178 operation: normalOp,
3179 data: {
3180 __typename: 'Query',
3181 item: {
3182 __typename: 'Node',
3183 id: 'id',
3184 },
3185 },
3186 });
3187
3188 expect(normalData).toMatchObject({ stale: false });
3189 expect(extendedData).toMatchObject({ stale: true });
3190 expect(client.reexecuteOperation).toHaveBeenCalledTimes(3);
3191
3192 nextOp(extendedOp);
3193
3194 expect(normalData).toMatchObject({ stale: false });
3195 expect(extendedData).toMatchObject({ stale: true });
3196 expect(client.reexecuteOperation).toHaveBeenCalledTimes(3);
3197
3198 nextRes({
3199 ...queryResponse,
3200 operation: extendedOp,
3201 data: {
3202 __typename: 'Query',
3203 item: {
3204 __typename: 'Node',
3205 extended: 'id',
3206 extra: 'extra',
3207 },
3208 },
3209 });
3210
3211 expect(extendedData).toMatchObject({ stale: false });
3212 expect(client.reexecuteOperation).toHaveBeenCalledTimes(4);
3213 });
3214});
3215
3216describe('commutativity', () => {
3217 it('applies results that come in out-of-order commutatively and consistently', () => {
3218 vi.useFakeTimers();
3219
3220 let data: any;
3221
3222 const client = createClient({
3223 url: 'http://0.0.0.0',
3224 requestPolicy: 'cache-and-network',
3225 exchanges: [],
3226 });
3227 const { source: ops$, next: next } = makeSubject<Operation>();
3228 const query = gql`
3229 {
3230 index
3231 }
3232 `;
3233
3234 const result = (operation: Operation): Source<OperationResult> =>
3235 pipe(
3236 fromValue({
3237 ...queryResponse,
3238 operation,
3239 data: {
3240 __typename: 'Query',
3241 index: operation.key,
3242 },
3243 }),
3244 delay(operation.key === 2 ? 5 : operation.key * 10)
3245 );
3246
3247 const output = vi.fn(result => {
3248 data = result.data;
3249 });
3250
3251 const forward = (ops$: Source<Operation>): Source<OperationResult> =>
3252 pipe(
3253 ops$,
3254 filter(op => op.kind !== 'teardown'),
3255 mergeMap(result)
3256 );
3257
3258 pipe(
3259 cacheExchange()({ forward, client, dispatchDebug })(ops$),
3260 tap(output),
3261 publish
3262 );
3263
3264 next(
3265 client.createRequestOperation('query', {
3266 key: 1,
3267 query,
3268 variables: undefined,
3269 })
3270 );
3271
3272 next(
3273 client.createRequestOperation('query', {
3274 key: 2,
3275 query,
3276 variables: undefined,
3277 })
3278 );
3279
3280 // This shouldn't have any effect:
3281 next(
3282 client.createRequestOperation('teardown', {
3283 key: 2,
3284 query,
3285 variables: undefined,
3286 })
3287 );
3288
3289 next(
3290 client.createRequestOperation('query', {
3291 key: 3,
3292 query,
3293 variables: undefined,
3294 })
3295 );
3296
3297 vi.advanceTimersByTime(5);
3298 expect(output).toHaveBeenCalledTimes(1);
3299 expect(data.index).toBe(2);
3300
3301 vi.advanceTimersByTime(10);
3302 expect(output).toHaveBeenCalledTimes(2);
3303 expect(data.index).toBe(2);
3304
3305 vi.advanceTimersByTime(30);
3306 expect(output).toHaveBeenCalledTimes(3);
3307 expect(data.index).toBe(3);
3308 });
3309
3310 it('applies optimistic updates on top of commutative queries as query result comes in', () => {
3311 let data: any;
3312 const client = createClient({
3313 url: 'http://0.0.0.0',
3314 exchanges: [],
3315 });
3316 const { source: ops$, next: nextOp } = makeSubject<Operation>();
3317 const { source: res$, next: nextRes } = makeSubject<OperationResult>();
3318
3319 const reexec = vi
3320 .spyOn(client, 'reexecuteOperation')
3321 .mockImplementation(nextOp);
3322
3323 const query = gql`
3324 {
3325 node {
3326 id
3327 name
3328 }
3329 }
3330 `;
3331
3332 const mutation = gql`
3333 mutation {
3334 node {
3335 id
3336 name
3337 }
3338 }
3339 `;
3340
3341 const forward = (ops$: Source<Operation>): Source<OperationResult> =>
3342 share(
3343 merge([
3344 pipe(
3345 ops$,
3346 filter(() => false)
3347 ) as any,
3348 res$,
3349 ])
3350 );
3351
3352 const optimistic = {
3353 node: () => ({
3354 __typename: 'Node',
3355 id: 'node',
3356 name: 'optimistic',
3357 }),
3358 };
3359
3360 pipe(
3361 cacheExchange({ optimistic })({ forward, client, dispatchDebug })(ops$),
3362 tap(result => {
3363 if (result.operation.kind === 'query') {
3364 data = result.data;
3365 }
3366 }),
3367 publish
3368 );
3369
3370 const queryOpA = client.createRequestOperation('query', {
3371 key: 1,
3372 query,
3373 variables: undefined,
3374 });
3375
3376 const mutationOp = client.createRequestOperation('mutation', {
3377 key: 2,
3378 query: mutation,
3379 variables: undefined,
3380 });
3381
3382 expect(data).toBe(undefined);
3383
3384 nextOp(queryOpA);
3385
3386 nextRes({
3387 ...queryResponse,
3388 operation: queryOpA,
3389 data: {
3390 __typename: 'Query',
3391 node: {
3392 __typename: 'Node',
3393 id: 'node',
3394 name: 'query a',
3395 },
3396 },
3397 });
3398
3399 expect(data).toHaveProperty('node.name', 'query a');
3400
3401 nextOp(mutationOp);
3402 expect(reexec).toHaveBeenCalledTimes(1);
3403 expect(data).toHaveProperty('node.name', 'optimistic');
3404 });
3405
3406 it('applies mutation results on top of commutative queries', () => {
3407 let data: any;
3408 const client = createClient({
3409 url: 'http://0.0.0.0',
3410 exchanges: [],
3411 });
3412 const { source: ops$, next: nextOp } = makeSubject<Operation>();
3413 const { source: res$, next: nextRes } = makeSubject<OperationResult>();
3414
3415 const reexec = vi
3416 .spyOn(client, 'reexecuteOperation')
3417 .mockImplementation(nextOp);
3418
3419 const query = gql`
3420 {
3421 node {
3422 id
3423 name
3424 }
3425 }
3426 `;
3427
3428 const mutation = gql`
3429 mutation {
3430 node {
3431 id
3432 name
3433 }
3434 }
3435 `;
3436
3437 const forward = (ops$: Source<Operation>): Source<OperationResult> =>
3438 share(
3439 merge([
3440 pipe(
3441 ops$,
3442 filter(() => false)
3443 ) as any,
3444 res$,
3445 ])
3446 );
3447
3448 pipe(
3449 cacheExchange()({ forward, client, dispatchDebug })(ops$),
3450 tap(result => {
3451 if (result.operation.kind === 'query') {
3452 data = result.data;
3453 }
3454 }),
3455 publish
3456 );
3457
3458 const queryOpA = client.createRequestOperation('query', {
3459 key: 1,
3460 query,
3461 variables: undefined,
3462 });
3463
3464 const mutationOp = client.createRequestOperation('mutation', {
3465 key: 2,
3466 query: mutation,
3467 variables: undefined,
3468 });
3469
3470 const queryOpB = client.createRequestOperation('query', {
3471 key: 3,
3472 query,
3473 variables: undefined,
3474 });
3475
3476 expect(data).toBe(undefined);
3477
3478 nextOp(queryOpA);
3479 nextOp(mutationOp);
3480 nextOp(queryOpB);
3481
3482 nextRes({
3483 ...queryResponse,
3484 operation: queryOpA,
3485 data: {
3486 __typename: 'Query',
3487 node: {
3488 __typename: 'Node',
3489 id: 'node',
3490 name: 'query a',
3491 },
3492 },
3493 });
3494
3495 expect(data).toHaveProperty('node.name', 'query a');
3496
3497 nextRes({
3498 ...queryResponse,
3499 operation: mutationOp,
3500 data: {
3501 __typename: 'Mutation',
3502 node: {
3503 __typename: 'Node',
3504 id: 'node',
3505 name: 'mutation',
3506 },
3507 },
3508 });
3509
3510 expect(reexec).toHaveBeenCalledTimes(3);
3511 expect(data).toHaveProperty('node.name', 'mutation');
3512
3513 nextRes({
3514 ...queryResponse,
3515 operation: queryOpB,
3516 data: {
3517 __typename: 'Query',
3518 node: {
3519 __typename: 'Node',
3520 id: 'node',
3521 name: 'query b',
3522 },
3523 },
3524 });
3525
3526 expect(reexec).toHaveBeenCalledTimes(4);
3527 expect(data).toHaveProperty('node.name', 'mutation');
3528 });
3529
3530 it('applies optimistic updates on top of commutative queries until mutation resolves', () => {
3531 let data: any;
3532 const client = createClient({
3533 url: 'http://0.0.0.0',
3534 exchanges: [],
3535 });
3536 const { source: ops$, next: nextOp } = makeSubject<Operation>();
3537 const { source: res$, next: nextRes } = makeSubject<OperationResult>();
3538
3539 vi.spyOn(client, 'reexecuteOperation').mockImplementation(nextOp);
3540
3541 const query = gql`
3542 {
3543 node {
3544 id
3545 name
3546 }
3547 }
3548 `;
3549
3550 const mutation = gql`
3551 mutation {
3552 node {
3553 id
3554 name
3555 optimistic
3556 }
3557 }
3558 `;
3559
3560 const forward = (ops$: Source<Operation>): Source<OperationResult> =>
3561 share(
3562 merge([
3563 pipe(
3564 ops$,
3565 filter(() => false)
3566 ) as any,
3567 res$,
3568 ])
3569 );
3570
3571 const optimistic = {
3572 node: () => ({
3573 __typename: 'Node',
3574 id: 'node',
3575 name: 'optimistic',
3576 }),
3577 };
3578
3579 pipe(
3580 cacheExchange({ optimistic })({ forward, client, dispatchDebug })(ops$),
3581 tap(result => {
3582 if (result.operation.kind === 'query') {
3583 data = result.data;
3584 }
3585 }),
3586 publish
3587 );
3588
3589 const queryOp = client.createRequestOperation('query', {
3590 key: 1,
3591 query,
3592 variables: undefined,
3593 });
3594 const mutationOp = client.createRequestOperation('mutation', {
3595 key: 2,
3596 query: mutation,
3597 variables: undefined,
3598 });
3599
3600 expect(data).toBe(undefined);
3601
3602 nextOp(queryOp);
3603 nextOp(mutationOp);
3604
3605 nextRes({
3606 ...queryResponse,
3607 operation: queryOp,
3608 data: {
3609 __typename: 'Query',
3610 node: {
3611 __typename: 'Node',
3612 id: 'node',
3613 name: 'query a',
3614 },
3615 },
3616 });
3617
3618 expect(data).toHaveProperty('node.name', 'optimistic');
3619
3620 nextRes({
3621 ...queryResponse,
3622 operation: mutationOp,
3623 data: {
3624 __typename: 'Query',
3625 node: {
3626 __typename: 'Node',
3627 id: 'node',
3628 name: 'mutation',
3629 },
3630 },
3631 });
3632
3633 expect(data).toHaveProperty('node.name', 'mutation');
3634 });
3635
3636 it('allows subscription results to be commutative when necessary', () => {
3637 let data: any;
3638 const client = createClient({
3639 url: 'http://0.0.0.0',
3640 exchanges: [],
3641 });
3642 const { source: ops$, next: nextOp } = makeSubject<Operation>();
3643 const { source: res$, next: nextRes } = makeSubject<OperationResult>();
3644
3645 vi.spyOn(client, 'reexecuteOperation').mockImplementation(nextOp);
3646
3647 const query = gql`
3648 {
3649 node {
3650 id
3651 name
3652 }
3653 }
3654 `;
3655
3656 const subscription = gql`
3657 subscription {
3658 node {
3659 id
3660 name
3661 }
3662 }
3663 `;
3664
3665 const forward = (ops$: Source<Operation>): Source<OperationResult> =>
3666 share(
3667 merge([
3668 pipe(
3669 ops$,
3670 filter(() => false)
3671 ) as any,
3672 res$,
3673 ])
3674 );
3675
3676 pipe(
3677 cacheExchange()({ forward, client, dispatchDebug })(ops$),
3678 tap(result => {
3679 if (result.operation.kind === 'query') {
3680 data = result.data;
3681 }
3682 }),
3683 publish
3684 );
3685
3686 const queryOpA = client.createRequestOperation('query', {
3687 key: 1,
3688 query,
3689 variables: undefined,
3690 });
3691
3692 const subscriptionOp = client.createRequestOperation('subscription', {
3693 key: 3,
3694 query: subscription,
3695 variables: undefined,
3696 });
3697
3698 nextOp(queryOpA);
3699 // Force commutative layers to be created:
3700 nextOp(
3701 client.createRequestOperation('query', {
3702 key: 2,
3703 query,
3704 variables: undefined,
3705 })
3706 );
3707
3708 nextOp(subscriptionOp);
3709
3710 nextRes({
3711 ...queryResponse,
3712 operation: queryOpA,
3713 data: {
3714 __typename: 'Query',
3715 node: {
3716 __typename: 'Node',
3717 id: 'node',
3718 name: 'query a',
3719 },
3720 },
3721 });
3722
3723 nextRes({
3724 ...queryResponse,
3725 operation: subscriptionOp,
3726 data: {
3727 node: {
3728 __typename: 'Node',
3729 id: 'node',
3730 name: 'subscription',
3731 },
3732 },
3733 });
3734
3735 expect(data).toHaveProperty('node.name', 'subscription');
3736 });
3737
3738 it('allows subscription results to be commutative above mutations', () => {
3739 let data: any;
3740 const client = createClient({
3741 url: 'http://0.0.0.0',
3742 exchanges: [],
3743 });
3744 const { source: ops$, next: nextOp } = makeSubject<Operation>();
3745 const { source: res$, next: nextRes } = makeSubject<OperationResult>();
3746
3747 vi.spyOn(client, 'reexecuteOperation').mockImplementation(nextOp);
3748
3749 const query = gql`
3750 {
3751 node {
3752 id
3753 name
3754 }
3755 }
3756 `;
3757
3758 const subscription = gql`
3759 subscription {
3760 node {
3761 id
3762 name
3763 }
3764 }
3765 `;
3766
3767 const mutation = gql`
3768 mutation {
3769 node {
3770 id
3771 name
3772 }
3773 }
3774 `;
3775
3776 const forward = (ops$: Source<Operation>): Source<OperationResult> =>
3777 share(
3778 merge([
3779 pipe(
3780 ops$,
3781 filter(() => false)
3782 ) as any,
3783 res$,
3784 ])
3785 );
3786
3787 pipe(
3788 cacheExchange()({ forward, client, dispatchDebug })(ops$),
3789 tap(result => {
3790 if (result.operation.kind === 'query') {
3791 data = result.data;
3792 }
3793 }),
3794 publish
3795 );
3796
3797 const queryOpA = client.createRequestOperation('query', {
3798 key: 1,
3799 query,
3800 variables: undefined,
3801 });
3802
3803 const subscriptionOp = client.createRequestOperation('subscription', {
3804 key: 2,
3805 query: subscription,
3806 variables: undefined,
3807 });
3808
3809 const mutationOp = client.createRequestOperation('mutation', {
3810 key: 3,
3811 query: mutation,
3812 variables: undefined,
3813 });
3814
3815 nextOp(queryOpA);
3816 // Force commutative layers to be created:
3817 nextOp(
3818 client.createRequestOperation('query', {
3819 key: 2,
3820 query,
3821 variables: undefined,
3822 })
3823 );
3824
3825 nextOp(subscriptionOp);
3826
3827 nextRes({
3828 ...queryResponse,
3829 operation: queryOpA,
3830 data: {
3831 __typename: 'Query',
3832 node: {
3833 __typename: 'Node',
3834 id: 'node',
3835 name: 'query a',
3836 },
3837 },
3838 });
3839
3840 nextOp(mutationOp);
3841
3842 nextRes({
3843 ...queryResponse,
3844 operation: mutationOp,
3845 data: {
3846 node: {
3847 __typename: 'Node',
3848 id: 'node',
3849 name: 'mutation',
3850 },
3851 },
3852 });
3853
3854 nextRes({
3855 ...queryResponse,
3856 operation: subscriptionOp,
3857 data: {
3858 node: {
3859 __typename: 'Node',
3860 id: 'node',
3861 name: 'subscription a',
3862 },
3863 },
3864 });
3865
3866 nextRes({
3867 ...queryResponse,
3868 operation: subscriptionOp,
3869 data: {
3870 node: {
3871 __typename: 'Node',
3872 id: 'node',
3873 name: 'subscription b',
3874 },
3875 },
3876 });
3877
3878 expect(data).toHaveProperty('node.name', 'subscription b');
3879 });
3880
3881 it('applies deferred results to previous layers', () => {
3882 let normalData: OperationResult | undefined;
3883 let deferredData: OperationResult | undefined;
3884 let combinedData: OperationResult | undefined;
3885
3886 const client = createClient({
3887 url: 'http://0.0.0.0',
3888 exchanges: [],
3889 });
3890 const { source: ops$, next: nextOp } = makeSubject<Operation>();
3891 const { source: res$, next: nextRes } = makeSubject<OperationResult>();
3892 client.reexecuteOperation = nextOp;
3893
3894 const normalQuery = gql`
3895 {
3896 node {
3897 id
3898 name
3899 }
3900 }
3901 `;
3902
3903 const deferredQuery = gql`
3904 {
3905 ... @defer {
3906 deferred {
3907 id
3908 name
3909 }
3910 }
3911 }
3912 `;
3913
3914 const combinedQuery = gql`
3915 {
3916 node {
3917 id
3918 name
3919 }
3920 ... @defer {
3921 deferred {
3922 id
3923 name
3924 }
3925 }
3926 }
3927 `;
3928
3929 const forward = (operations$: Source<Operation>): Source<OperationResult> =>
3930 share(
3931 merge([
3932 pipe(
3933 operations$,
3934 filter(() => false)
3935 ) as any,
3936 res$,
3937 ])
3938 );
3939
3940 pipe(
3941 cacheExchange()({ forward, client, dispatchDebug })(ops$),
3942 tap(result => {
3943 if (result.operation.kind === 'query') {
3944 if (result.operation.key === 1) {
3945 deferredData = result;
3946 } else if (result.operation.key === 42) {
3947 combinedData = result;
3948 } else {
3949 normalData = result;
3950 }
3951 }
3952 }),
3953 publish
3954 );
3955
3956 const combinedOp = client.createRequestOperation('query', {
3957 key: 42,
3958 query: combinedQuery,
3959 variables: undefined,
3960 });
3961 const deferredOp = client.createRequestOperation('query', {
3962 key: 1,
3963 query: deferredQuery,
3964 variables: undefined,
3965 });
3966 const normalOp = client.createRequestOperation('query', {
3967 key: 2,
3968 query: normalQuery,
3969 variables: undefined,
3970 });
3971
3972 nextOp(combinedOp);
3973 nextOp(deferredOp);
3974 nextOp(normalOp);
3975
3976 nextRes({
3977 ...queryResponse,
3978 operation: deferredOp,
3979 data: {
3980 __typename: 'Query',
3981 },
3982 hasNext: true,
3983 });
3984
3985 expect(deferredData).not.toHaveProperty('deferred');
3986
3987 nextRes({
3988 ...queryResponse,
3989 operation: normalOp,
3990 data: {
3991 __typename: 'Query',
3992 node: {
3993 __typename: 'Node',
3994 id: 2,
3995 name: 'normal',
3996 },
3997 },
3998 });
3999
4000 expect(normalData).toHaveProperty('data.node.id', 2);
4001 expect(combinedData).not.toHaveProperty('data.deferred');
4002 expect(combinedData).toHaveProperty('data.node.id', 2);
4003
4004 nextRes({
4005 ...queryResponse,
4006 operation: deferredOp,
4007 data: {
4008 __typename: 'Query',
4009 deferred: {
4010 __typename: 'Node',
4011 id: 1,
4012 name: 'deferred',
4013 },
4014 },
4015 hasNext: true,
4016 });
4017
4018 expect(deferredData).toHaveProperty('hasNext', true);
4019 expect(deferredData).toHaveProperty('data.deferred.id', 1);
4020
4021 expect(combinedData).toHaveProperty('hasNext', false);
4022 expect(combinedData).toHaveProperty('data.deferred.id', 1);
4023 expect(combinedData).toHaveProperty('data.node.id', 2);
4024 });
4025
4026 it('applies deferred logic only to deferred operations', () => {
4027 let failingData: OperationResult | undefined;
4028
4029 const client = createClient({
4030 url: 'http://0.0.0.0',
4031 exchanges: [],
4032 });
4033
4034 const { source: ops$, next: nextOp } = makeSubject<Operation>();
4035 const { source: res$ } = makeSubject<OperationResult>();
4036
4037 const deferredQuery = gql`
4038 {
4039 ... @defer {
4040 deferred {
4041 id
4042 name
4043 }
4044 }
4045 }
4046 `;
4047
4048 const failingQuery = gql`
4049 {
4050 deferred {
4051 id
4052 name
4053 }
4054 }
4055 `;
4056
4057 const forward = (ops$: Source<Operation>): Source<OperationResult> =>
4058 share(
4059 merge([
4060 pipe(
4061 ops$,
4062 filter(() => false)
4063 ) as any,
4064 res$,
4065 ])
4066 );
4067
4068 pipe(
4069 cacheExchange()({ forward, client, dispatchDebug })(ops$),
4070 tap(result => {
4071 if (result.operation.kind === 'query') {
4072 if (result.operation.key === 1) {
4073 failingData = result;
4074 }
4075 }
4076 }),
4077 publish
4078 );
4079
4080 const failingOp = client.createRequestOperation('query', {
4081 key: 1,
4082 query: failingQuery,
4083 variables: undefined,
4084 });
4085 const deferredOp = client.createRequestOperation('query', {
4086 key: 2,
4087 query: deferredQuery,
4088 variables: undefined,
4089 });
4090
4091 nextOp(deferredOp);
4092 nextOp(failingOp);
4093
4094 expect(failingData).not.toMatchObject({ hasNext: true });
4095 });
4096});
4097
4098describe('abstract types', () => {
4099 it('works with two responses giving different concrete types for a union', () => {
4100 const query = gql`
4101 query ($id: ID!) {
4102 field(id: $id) {
4103 id
4104 union {
4105 ... on Type1 {
4106 id
4107 name
4108 __typename
4109 }
4110 ... on Type2 {
4111 id
4112 title
4113 __typename
4114 }
4115 }
4116 __typename
4117 }
4118 }
4119 `;
4120 const client = createClient({
4121 url: 'http://0.0.0.0',
4122 exchanges: [],
4123 });
4124 const { source: ops$, next } = makeSubject<Operation>();
4125 const operation1 = client.createRequestOperation('query', {
4126 key: 1,
4127 query,
4128 variables: { id: '1' },
4129 });
4130 const operation2 = client.createRequestOperation('query', {
4131 key: 2,
4132 query,
4133 variables: { id: '2' },
4134 });
4135 const queryResult1: OperationResult = {
4136 ...queryResponse,
4137 operation: operation1,
4138 data: {
4139 __typename: 'Query',
4140 field: {
4141 id: '1',
4142 __typename: 'Todo',
4143 union: {
4144 id: '1',
4145 name: 'test',
4146 __typename: 'Type1',
4147 },
4148 },
4149 },
4150 };
4151
4152 const queryResult2: OperationResult = {
4153 ...queryResponse,
4154 operation: operation2,
4155 data: {
4156 __typename: 'Query',
4157 field: {
4158 id: '2',
4159 __typename: 'Todo',
4160 union: {
4161 id: '2',
4162 title: 'test',
4163 __typename: 'Type2',
4164 },
4165 },
4166 },
4167 };
4168
4169 vi.spyOn(client, 'reexecuteOperation').mockImplementation(next);
4170 const response = vi.fn((forwardOp: Operation): OperationResult => {
4171 if (forwardOp.key === 1) return queryResult1;
4172 if (forwardOp.key === 2) return queryResult2;
4173 return undefined as any;
4174 });
4175
4176 const result = vi.fn();
4177 const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share);
4178
4179 pipe(
4180 cacheExchange({})({ forward, client, dispatchDebug })(ops$),
4181 tap(result),
4182 publish
4183 );
4184
4185 next(operation1);
4186 expect(response).toHaveBeenCalledTimes(1);
4187 expect(result).toHaveBeenCalledTimes(1);
4188 expect(result.mock.calls[0][0].data).toEqual({
4189 field: {
4190 __typename: 'Todo',
4191 id: '1',
4192 union: {
4193 __typename: 'Type1',
4194 id: '1',
4195 name: 'test',
4196 },
4197 },
4198 });
4199
4200 next(operation2);
4201 expect(response).toHaveBeenCalledTimes(2);
4202 expect(result).toHaveBeenCalledTimes(2);
4203 expect(result.mock.calls[1][0].data).toEqual({
4204 field: {
4205 __typename: 'Todo',
4206 id: '2',
4207 union: {
4208 __typename: 'Type2',
4209 id: '2',
4210 title: 'test',
4211 },
4212 },
4213 });
4214 });
4215});