Skip to content

Commit 130ef0d

Browse files
fix: infinite loading states when a restart occurs in the middle of a request. (#612)
1 parent b352f12 commit 130ef0d

File tree

4 files changed

+66
-9
lines changed

4 files changed

+66
-9
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"format": "prettier --write .",
4949
"lint": "eslint . --ext .ts",
5050
"test": "NODE_V8_COVERAGE=coverage node -r ./dist/test/setup.js --enable-source-maps --trace-warnings --experimental-test-coverage --test dist/test/**/*.test.js",
51+
"test:only": "tsc -p tsconfig.test.json && NODE_V8_COVERAGE=coverage node -r ./dist/test/setup.js --enable-source-maps --trace-warnings --experimental-test-coverage --test-only",
5152
"version": "auto-changelog -p && cp CHANGELOG.md docs/src/others/changelog.md && git add CHANGELOG.md docs/src/others/changelog.md"
5253
},
5354
"resolutions": {

src/interceptors/build.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { CacheAxiosResponse, InternalCacheRequestConfig } from '../cache/ax
22

33
/** See {@link AxiosInterceptorManager} */
44
export interface AxiosInterceptor<T> {
5-
onFulfilled?(value: T): T | Promise<T>;
5+
onFulfilled(value: T): T | Promise<T>;
66

77
/** Returns a successful response or re-throws the error */
88
onRejected?(error: Record<string, unknown>): T | Promise<T>;

src/interceptors/request.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -164,11 +164,10 @@ export function defaultRequestInterceptor(axios: AxiosCacheInstance) {
164164
if (cache.state === 'loading') {
165165
const deferred = axios.waiting[config.id];
166166

167-
// Just in case, the deferred doesn't exists.
168-
/* istanbul ignore if 'really hard to test' */
167+
// The deferred may not exists when the process is using a persistent
168+
// storage and cancelled in the middle of a request, this would result in
169+
// a pending loading state in the storage but no current promises to resolve
169170
if (!deferred) {
170-
await axios.storage.remove(config.id, config);
171-
172171
// Hydrates any UI temporarily, if cache is available
173172
if (cache.data) {
174173
await config.cache.hydrate?.(cache);
@@ -201,17 +200,18 @@ export function defaultRequestInterceptor(axios: AxiosCacheInstance) {
201200
await config.cache.hydrate?.(cache);
202201
}
203202

204-
// The deferred is rejected when the request that we are waiting rejected cache.
205-
return config;
203+
// The deferred is rejected when the request that we are waiting rejects its cache.
204+
// In this case, we need to redo the request all over again.
205+
return onFulfilled(config);
206206
}
207207
} else {
208208
cachedResponse = cache.data;
209209
}
210210

211211
// Even though the response interceptor receives this one from here,
212212
// it has been configured to ignore cached responses = true
213-
config.adapter = (): Promise<CacheAxiosResponse> =>
214-
Promise.resolve({
213+
config.adapter = function cachedAdapter(): Promise<CacheAxiosResponse> {
214+
return Promise.resolve({
215215
config,
216216
data: cachedResponse.data,
217217
headers: cachedResponse.headers,
@@ -221,6 +221,7 @@ export function defaultRequestInterceptor(axios: AxiosCacheInstance) {
221221
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
222222
id: config.id!
223223
});
224+
};
224225

225226
if (__ACI_DEV__) {
226227
axios.debug?.({

test/interceptors/response.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,4 +296,59 @@ describe('Response Interceptor', () => {
296296

297297
await assert.rejects(promise, error);
298298
});
299+
300+
it('Cancelled deferred still should save cache after new response', async () => {
301+
const axios = mockAxios();
302+
303+
const id = '1';
304+
const controller = new AbortController();
305+
306+
const cancelled = axios.get('url', { id, signal: controller.signal });
307+
const promise = axios.get('url', { id });
308+
309+
controller.abort();
310+
311+
// p1 should fail as it was aborted
312+
try {
313+
await cancelled;
314+
assert.fail('should have thrown an error');
315+
} catch (error: any) {
316+
assert.equal(error.code, 'ERR_CANCELED');
317+
}
318+
319+
const response = await promise;
320+
321+
// p2 should succeed as it was not aborted
322+
await assert.ok(response.data);
323+
await assert.equal(response.cached, false);
324+
325+
const storage = await axios.storage.get(id);
326+
327+
// P2 should have saved the cache
328+
// even that his origin was from a cancelled deferred
329+
assert.equal(storage.state, 'cached');
330+
assert.equal(storage.data?.data, true);
331+
});
332+
333+
it('Response gets cached even if there is a pending request without deferred.', async () => {
334+
const axios = mockAxios();
335+
336+
const id = '1';
337+
338+
// Simulates previous unresolved request
339+
await axios.storage.set(id, {
340+
state: 'loading',
341+
previous: 'empty'
342+
});
343+
344+
const response = await axios.get('url', { id });
345+
346+
assert.equal(response.cached, false);
347+
assert.ok(response.data);
348+
349+
const storage = await axios.storage.get(id);
350+
351+
assert.equal(storage.state, 'cached');
352+
assert.equal(storage.data?.data, true);
353+
});
299354
});

0 commit comments

Comments
 (0)