1import {
2 makeSubject,
3 map,
4 pipe,
5 publish,
6 Source,
7 Subject,
8 forEach,
9 scan,
10 toPromise,
11} from 'wonka';
12import { vi, expect, it, beforeEach, describe } from 'vitest';
13
14import { Client } from '../client';
15import {
16 mutationOperation,
17 mutationResponse,
18 queryOperation,
19 queryResponse,
20 subscriptionOperation,
21 subscriptionResult,
22 undefinedQueryResponse,
23} from '../test-utils';
24import { Operation, OperationResult, ExchangeInput } from '../types';
25import { cacheExchange } from './cache';
26
27const reexecuteOperation = vi.fn();
28const dispatchDebug = vi.fn();
29
30let response;
31let exchangeArgs: ExchangeInput;
32let forwardedOperations: Operation[];
33let input: Subject<Operation>;
34
35beforeEach(() => {
36 response = queryResponse;
37 forwardedOperations = [];
38 input = makeSubject<Operation>();
39
40 // Collect all forwarded operations
41 const forward = (s: Source<Operation>) => {
42 return pipe(
43 s,
44 map(op => {
45 forwardedOperations.push(op);
46 return response;
47 })
48 );
49 };
50
51 const client = {
52 reexecuteOperation: reexecuteOperation as any,
53 } as Client;
54
55 exchangeArgs = { forward, client, dispatchDebug };
56});
57
58describe('on query', () => {
59 it('forwards to next exchange when no cache hit', () => {
60 const { source: ops$, next, complete } = input;
61 const exchange = cacheExchange(exchangeArgs)(ops$);
62
63 publish(exchange);
64 next(queryOperation);
65 complete();
66 expect(forwardedOperations.length).toBe(1);
67 expect(reexecuteOperation).not.toBeCalled();
68 });
69
70 it('caches results', () => {
71 const { source: ops$, next, complete } = input;
72 const exchange = cacheExchange(exchangeArgs)(ops$);
73
74 publish(exchange);
75 next(queryOperation);
76 next(queryOperation);
77 complete();
78 expect(forwardedOperations.length).toBe(1);
79 expect(reexecuteOperation).not.toBeCalled();
80 });
81
82 it('respects cache-and-network', () => {
83 const { source: ops$, next, complete } = input;
84 const result = vi.fn();
85 const exchange = cacheExchange(exchangeArgs)(ops$);
86
87 pipe(exchange, forEach(result));
88 next(queryOperation);
89
90 next({
91 ...queryOperation,
92 context: {
93 ...queryOperation.context,
94 requestPolicy: 'cache-and-network',
95 },
96 });
97
98 complete();
99 expect(forwardedOperations.length).toBe(1);
100 expect(reexecuteOperation).toHaveBeenCalledTimes(1);
101 expect(result).toHaveBeenCalledTimes(2);
102 expect(result.mock.calls[1][0].stale).toBe(true);
103
104 expect(reexecuteOperation.mock.calls[0][0]).toEqual({
105 ...queryOperation,
106 context: { ...queryOperation.context, requestPolicy: 'network-only' },
107 });
108 });
109
110 it('respects cache-only', () => {
111 const { source: ops$, next, complete } = input;
112 const exchange = cacheExchange(exchangeArgs)(ops$);
113
114 publish(exchange);
115 next({
116 ...queryOperation,
117 context: {
118 ...queryOperation.context,
119 requestPolicy: 'cache-only',
120 },
121 });
122 complete();
123 expect(forwardedOperations.length).toBe(0);
124 expect(reexecuteOperation).not.toBeCalled();
125 });
126
127 describe('cache hit', () => {
128 it('is miss when operation is forwarded', () => {
129 const { source: ops$, next, complete } = input;
130 const exchange = cacheExchange(exchangeArgs)(ops$);
131
132 publish(exchange);
133 next(queryOperation);
134 complete();
135
136 expect(forwardedOperations[0].context).toHaveProperty(
137 'meta.cacheOutcome',
138 'miss'
139 );
140 });
141
142 it('is true when cached response is returned', async () => {
143 const { source: ops$, next, complete } = input;
144 const exchange = cacheExchange(exchangeArgs)(ops$);
145
146 const results$ = pipe(
147 exchange,
148 scan((acc, x) => [...acc, x], [] as OperationResult[]),
149 toPromise
150 );
151
152 publish(exchange);
153 next(queryOperation);
154 next(queryOperation);
155 complete();
156
157 const results = await results$;
158 expect(results[1].operation.context).toHaveProperty(
159 'meta.cacheOutcome',
160 'hit'
161 );
162 });
163 });
164});
165
166describe('on mutation', () => {
167 it('does not cache', () => {
168 response = mutationResponse;
169 const { source: ops$, next, complete } = input;
170 const exchange = cacheExchange(exchangeArgs)(ops$);
171
172 publish(exchange);
173 next(mutationOperation);
174 next(mutationOperation);
175 complete();
176 expect(forwardedOperations.length).toBe(2);
177 expect(reexecuteOperation).not.toBeCalled();
178 });
179});
180
181describe('on subscription', () => {
182 it('forwards subscriptions', () => {
183 response = subscriptionResult;
184 const { source: ops$, next, complete } = input;
185 const exchange = cacheExchange(exchangeArgs)(ops$);
186
187 publish(exchange);
188 next(subscriptionOperation);
189 next(subscriptionOperation);
190 complete();
191 expect(forwardedOperations.length).toBe(2);
192 expect(reexecuteOperation).not.toBeCalled();
193 });
194});
195
196// Empty query response implies the data propertys is undefined
197describe('on empty query response', () => {
198 beforeEach(() => {
199 response = undefinedQueryResponse;
200 forwardedOperations = [];
201 input = makeSubject<Operation>();
202
203 // Collect all forwarded operations
204 const forward = (s: Source<Operation>) => {
205 return pipe(
206 s,
207 map(op => {
208 forwardedOperations.push(op);
209 return response;
210 })
211 );
212 };
213
214 const client = {
215 reexecuteOperation: reexecuteOperation as any,
216 } as Client;
217
218 exchangeArgs = { forward, client, dispatchDebug };
219 });
220
221 it('does not cache response', () => {
222 const { source: ops$, next, complete } = input;
223 const exchange = cacheExchange(exchangeArgs)(ops$);
224
225 publish(exchange);
226 next(queryOperation);
227 next(queryOperation);
228 complete();
229 // 2 indicates it's not cached.
230 expect(forwardedOperations.length).toBe(2);
231 expect(reexecuteOperation).not.toBeCalled();
232 });
233});