diff --git a/package.json b/package.json index 798be0c8..a68668fc 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "format": "prettier --write .", "lint": "eslint . --ext .ts", "test": "NODE_V8_COVERAGE=coverage node -r ./dist/test/setup.js --enable-source-maps --trace-warnings --experimental-test-coverage --test dist/test/**/*.test.js", + "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", "version": "auto-changelog -p && cp CHANGELOG.md docs/src/others/changelog.md && git add CHANGELOG.md docs/src/others/changelog.md" }, "resolutions": { diff --git a/src/interceptors/build.ts b/src/interceptors/build.ts index 5ceb28b6..655bb0ce 100644 --- a/src/interceptors/build.ts +++ b/src/interceptors/build.ts @@ -2,7 +2,7 @@ import type { CacheAxiosResponse, InternalCacheRequestConfig } from '../cache/ax /** See {@link AxiosInterceptorManager} */ export interface AxiosInterceptor { - onFulfilled?(value: T): T | Promise; + onFulfilled(value: T): T | Promise; /** Returns a successful response or re-throws the error */ onRejected?(error: Record): T | Promise; diff --git a/src/interceptors/request.ts b/src/interceptors/request.ts index c234fb61..7efe9c2a 100644 --- a/src/interceptors/request.ts +++ b/src/interceptors/request.ts @@ -164,11 +164,10 @@ export function defaultRequestInterceptor(axios: AxiosCacheInstance) { if (cache.state === 'loading') { const deferred = axios.waiting[config.id]; - // Just in case, the deferred doesn't exists. - /* istanbul ignore if 'really hard to test' */ + // The deferred may not exists when the process is using a persistent + // storage and cancelled in the middle of a request, this would result in + // a pending loading state in the storage but no current promises to resolve if (!deferred) { - await axios.storage.remove(config.id, config); - // Hydrates any UI temporarily, if cache is available if (cache.data) { await config.cache.hydrate?.(cache); @@ -201,8 +200,9 @@ export function defaultRequestInterceptor(axios: AxiosCacheInstance) { await config.cache.hydrate?.(cache); } - // The deferred is rejected when the request that we are waiting rejected cache. - return config; + // The deferred is rejected when the request that we are waiting rejects its cache. + // In this case, we need to redo the request all over again. + return onFulfilled(config); } } else { cachedResponse = cache.data; @@ -210,8 +210,8 @@ export function defaultRequestInterceptor(axios: AxiosCacheInstance) { // Even though the response interceptor receives this one from here, // it has been configured to ignore cached responses = true - config.adapter = (): Promise => - Promise.resolve({ + config.adapter = function cachedAdapter(): Promise { + return Promise.resolve({ config, data: cachedResponse.data, headers: cachedResponse.headers, @@ -221,6 +221,7 @@ export function defaultRequestInterceptor(axios: AxiosCacheInstance) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion id: config.id! }); + }; if (__ACI_DEV__) { axios.debug?.({ diff --git a/test/interceptors/response.test.ts b/test/interceptors/response.test.ts index 25b1d50d..b4626e8c 100644 --- a/test/interceptors/response.test.ts +++ b/test/interceptors/response.test.ts @@ -296,4 +296,59 @@ describe('Response Interceptor', () => { await assert.rejects(promise, error); }); + + it('Cancelled deferred still should save cache after new response', async () => { + const axios = mockAxios(); + + const id = '1'; + const controller = new AbortController(); + + const cancelled = axios.get('url', { id, signal: controller.signal }); + const promise = axios.get('url', { id }); + + controller.abort(); + + // p1 should fail as it was aborted + try { + await cancelled; + assert.fail('should have thrown an error'); + } catch (error: any) { + assert.equal(error.code, 'ERR_CANCELED'); + } + + const response = await promise; + + // p2 should succeed as it was not aborted + await assert.ok(response.data); + await assert.equal(response.cached, false); + + const storage = await axios.storage.get(id); + + // P2 should have saved the cache + // even that his origin was from a cancelled deferred + assert.equal(storage.state, 'cached'); + assert.equal(storage.data?.data, true); + }); + + it('Response gets cached even if there is a pending request without deferred.', async () => { + const axios = mockAxios(); + + const id = '1'; + + // Simulates previous unresolved request + await axios.storage.set(id, { + state: 'loading', + previous: 'empty' + }); + + const response = await axios.get('url', { id }); + + assert.equal(response.cached, false); + assert.ok(response.data); + + const storage = await axios.storage.get(id); + + assert.equal(storage.state, 'cached'); + assert.equal(storage.data?.data, true); + }); });