diff --git a/docs/src/config/response-object.md b/docs/src/config/response-object.md index 84475cb..472aac8 100644 --- a/docs/src/config/response-object.md +++ b/docs/src/config/response-object.md @@ -33,3 +33,9 @@ This does not indicated if the request was capable of being cached or not, as op [`cache.override`](./request-specifics.md#cache-override) may have been enabled. ::: + +## stale + +- Type: `boolean` + +A simple boolean indicating if the request returned data is from valid or stale cache. diff --git a/src/cache/axios.ts b/src/cache/axios.ts index ec73b61..84a889c 100644 --- a/src/cache/axios.ts +++ b/src/cache/axios.ts @@ -44,6 +44,13 @@ export interface CacheAxiosResponse extends AxiosResponse { const result = await axios.get('http://test.com'); assert.ok(result.cached); + assert.equal(result.stale, false); }); it('Header Interpreter Stale with StaleWhileRevalidate and MaxStale', () => { diff --git a/test/interceptors/etag.test.ts b/test/interceptors/etag.test.ts index ec67049..d9f0eb7 100644 --- a/test/interceptors/etag.test.ts +++ b/test/interceptors/etag.test.ts @@ -14,6 +14,7 @@ describe('ETag handling', () => { const response = await axios.get('http://test.com', config); assert.ok(response.cached); + assert.equal(response.stale, false); assert.ok(response.data); // Sleep entire max age time. @@ -22,6 +23,7 @@ describe('ETag handling', () => { const response2 = await axios.get('http://test.com', config); // from revalidation assert.ok(response2.cached); + assert.equal(response.stale, false); // ensure value from stale cache is kept assert.ok(response2.data); }); @@ -37,6 +39,7 @@ describe('ETag handling', () => { const response = await axios.get('http://test.com'); assert.ok(response.cached); + assert.equal(response.stale, false); assert.ok(response.data); // Sleep entire max age time. @@ -45,6 +48,7 @@ describe('ETag handling', () => { const response2 = await axios.get('http://test.com'); // from revalidation assert.ok(response2.cached); + assert.equal(response.stale, false); // ensure value from stale cache is kept assert.ok(response2.data); }); @@ -61,6 +65,7 @@ describe('ETag handling', () => { const response = await axios.get('http://test.com', config); // from etag revalidation assert.ok(response.cached); + assert.equal(response.stale, false); assert.ok(response.data); }); @@ -70,12 +75,14 @@ describe('ETag handling', () => { const response = await axios.get('http://test.com', config); assert.equal(response.cached, false); + assert.equal(response.stale, undefined); assert.ok(response.data); assert.equal(response.config.headers?.[Header.IfModifiedSince], undefined); assert.equal(response.headers?.[Header.LastModified], undefined); const response2 = await axios.get('http://test.com', config); assert.ok(response2.cached); + assert.equal(!!response.stale, false); assert.ok(response2.data); assert.equal(response2.config.headers?.[Header.IfNoneMatch], 'fake-etag'); assert.equal(response2.headers?.[Header.ETag], 'fake-etag-2'); diff --git a/test/interceptors/hydrate.test.ts b/test/interceptors/hydrate.test.ts index af7809e..361741e 100644 --- a/test/interceptors/hydrate.test.ts +++ b/test/interceptors/hydrate.test.ts @@ -40,6 +40,7 @@ describe('Hydrate handling', () => { assert.equal(m.mock.callCount(), 0); assert.ok(res2.cached); + assert.equal(res2.stale, false); }); it('Hydrates when etag is set', async () => { @@ -69,6 +70,7 @@ describe('Hydrate handling', () => { assert.equal(m.mock.callCount(), 1); assert.ok(res2.cached); + assert.equal(!!res2.stale, false); assert.deepEqual(m.mock.calls[0]?.arguments, [cache]); }); @@ -92,6 +94,7 @@ describe('Hydrate handling', () => { assert.equal(m.mock.callCount(), 0); assert.equal(res2.cached, false); + assert.equal(res2.stale, undefined); }); it('Only hydrates when stale while revalidate is not expired', async () => { @@ -117,6 +120,7 @@ describe('Hydrate handling', () => { assert.equal(m.mock.callCount(), 0); assert.equal(res2.cached, false); + assert.equal(res2.stale, undefined); }); it('Hydrates when force stale', async () => { @@ -141,5 +145,6 @@ describe('Hydrate handling', () => { assert.equal(m.mock.callCount(), 1); assert.deepEqual(m.mock.calls[0]?.arguments, [cache]); assert.equal(res2.cached, false); + assert.equal(res2.stale, undefined); }); }); diff --git a/test/interceptors/last-modified.test.ts b/test/interceptors/last-modified.test.ts index ce85343..f50a6e5 100644 --- a/test/interceptors/last-modified.test.ts +++ b/test/interceptors/last-modified.test.ts @@ -24,6 +24,7 @@ describe('LastModified handling', () => { const response = await axios.get('url', config); assert.ok(response.cached); + assert.equal(response.stale, false); assert.ok(response.data); // Sleep entire max age time (using await to function as setImmediate) @@ -32,6 +33,7 @@ describe('LastModified handling', () => { const response2 = await axios.get('url', config); // from revalidation assert.ok(response2.cached); + assert.equal(response.stale, false); assert.equal(response2.status, 200); }); @@ -48,6 +50,7 @@ describe('LastModified handling', () => { const response = await axios.get('url'); assert.ok(response.cached); + assert.equal(response.stale, false); assert.ok(response.data); // Sleep entire max age time (using await to function as setImmediate) @@ -56,6 +59,7 @@ describe('LastModified handling', () => { const response2 = await axios.get('url'); // from revalidation assert.ok(response2.cached); + assert.equal(response.stale, false); assert.equal(response2.status, 200); }); @@ -69,12 +73,14 @@ describe('LastModified handling', () => { const response = await axios.get('url', config); assert.equal(response.cached, false); + assert.equal(response.stale, undefined); assert.ok(response.data); assert.equal(response.config.headers?.[Header.IfModifiedSince], undefined); assert.ok(response.headers?.[Header.XAxiosCacheLastModified]); const response2 = await axios.get('url', config); assert.ok(response2.cached); + assert.equal(!!response.stale, false); assert.ok(response2.data); assert.ok(response2.config.headers?.[Header.IfModifiedSince]); assert.ok(response2.headers?.[Header.XAxiosCacheLastModified]); diff --git a/test/interceptors/request.test.ts b/test/interceptors/request.test.ts index 54a1cbb..dfac2c9 100644 --- a/test/interceptors/request.test.ts +++ b/test/interceptors/request.test.ts @@ -44,7 +44,9 @@ describe('Request Interceptor', () => { ]); assert.equal(resp1.cached, false); + assert.equal(resp1.stale, undefined); assert.ok(resp2.cached); + assert.equal(resp2.stale, false); }); it('Concurrent requests with `cache: false`', async () => { @@ -57,6 +59,7 @@ describe('Request Interceptor', () => { ]); for (const result of results) { assert.equal(result.cached, false); + assert.equal(result.stale, undefined); } }); @@ -78,6 +81,7 @@ describe('Request Interceptor', () => { ]); assert.equal(resp2.cached, false); + assert.equal(resp2.stale, undefined); }); it('`response.cached` is present', async () => { @@ -85,19 +89,23 @@ describe('Request Interceptor', () => { const response = await axios.get('http://test.com'); assert.equal(response.cached, false); + assert.equal(response.stale, undefined); const response2 = await axios.get('http://test.com'); assert.ok(response2.cached); + assert.equal(response2.stale, false); const response3 = await axios.get('http://test.com', { id: 'random-id' }); assert.equal(response3.cached, false); + assert.equal(response3.stale, undefined); const response4 = await axios.get('http://test.com', { id: 'random-id' }); assert.ok(response4.cached); + assert.equal(response4.stale, false); }); it('Cache expiration', async () => { @@ -109,12 +117,14 @@ describe('Request Interceptor', () => { const resultCache = await axios.get('http://test.com'); assert.ok(resultCache.cached); + assert.equal(resultCache.stale, false); // Sleep entire max age time (using await to function as setImmediate) await mockDateNow(1000); const response2 = await axios.get('http://test.com'); assert.equal(response2.cached, false); + assert.equal(response2.stale, undefined); }); it('`must revalidate` does not allows stale', async () => { @@ -140,11 +150,14 @@ describe('Request Interceptor', () => { const res3 = await axios.get('url', config); assert.equal(res1.cached, false); + assert.equal(res1.stale, undefined); const headers1 = res1.headers as Record; const headers2 = res2.headers as Record; assert.equal(headers1['x-mock-random'], headers2['x-mock-random']); assert.ok(res2.cached); + assert.equal(res2.stale, false); assert.ok(res3.cached); + assert.equal(res3.stale, false); // waits one second (using await to function as setImmediate) await mockDateNow(1000); @@ -162,10 +175,12 @@ describe('Request Interceptor', () => { const result = await axios.get('url', { data: { a: 1 } }); assert.equal(result.cached, false); + assert.equal(result.stale, undefined); const result2 = await axios.get('url', { data: { a: 2 } }); assert.equal(result2.cached, false); + assert.equal(result2.stale, undefined); }); it('Tests a request with really long keys', async () => { @@ -230,6 +245,7 @@ describe('Request Interceptor', () => { const { id, ...initialResponse } = await axios.get('url'); assert.equal(initialResponse.cached, false); + assert.equal(initialResponse.stale, undefined); // Ensure cache was populated const c1 = await axios.storage.get(id); @@ -276,6 +292,7 @@ describe('Request Interceptor', () => { const c3 = await axios.storage.get(id); assert.equal(newResponse.cached, false); + assert.equal(newResponse.stale, undefined); assert.equal(c3.state, 'cached'); assert.notEqual(c3.data, c1.data); // `'overridden response'`, not `true` assert.notEqual(c3.createdAt, c1.createdAt); @@ -327,6 +344,7 @@ describe('Request Interceptor', () => { const c3 = await axios.storage.get(id); assert.equal(newResponse.cached, false); + assert.equal(newResponse.stale, undefined); assert.equal(c3.state, 'cached'); assert.ok(c3.data); assert.notEqual(c3.createdAt, c1.createdAt); @@ -393,12 +411,16 @@ describe('Request Interceptor', () => { const [req0, req1] = await Promise.all([axios.get('url'), axios.get('url')]); assert.equal(req0.cached, false); + assert.equal(req0.stale, undefined); assert.equal(req1.cached, false); + assert.equal(req1.stale, undefined); const [req2, req3] = await Promise.all([axios.get('some-other'), axios.get('some-other')]); assert.equal(req2.cached, false); + assert.equal(req2.stale, undefined); assert.ok(req3.cached); + assert.equal(req3.stale, false); }); it('ensures request with urls in exclude.paths are not cached (regex)', async () => { @@ -411,16 +433,22 @@ describe('Request Interceptor', () => { const [req0, req1] = await Promise.all([axios.get('my/url'), axios.get('my/url')]); assert.equal(req0.cached, false); + assert.equal(req0.stale, undefined); assert.equal(req1.cached, false); + assert.equal(req1.stale, undefined); const [req2, req3] = await Promise.all([axios.get('some-other'), axios.get('some-other')]); assert.equal(req2.cached, false); + assert.equal(req2.stale, undefined); assert.ok(req3.cached); + assert.equal(req3.stale, false); const [req4, req5] = await Promise.all([axios.get('other/url'), axios.get('other/url')]); assert.equal(req4.cached, false); + assert.equal(req4.stale, undefined); assert.equal(req5.cached, false); + assert.equal(req5.stale, undefined); }); }); diff --git a/test/interceptors/response.test.ts b/test/interceptors/response.test.ts index d5b6eb1..3b8db90 100644 --- a/test/interceptors/response.test.ts +++ b/test/interceptors/response.test.ts @@ -81,6 +81,7 @@ describe('Response Interceptor', () => { const result = await fetch(); assert.equal(result.cached, false); + assert.equal(result.stale, undefined); }); it('HeaderInterpreter integration', async () => { @@ -91,6 +92,7 @@ describe('Response Interceptor', () => { const resultNoCache = await axiosNoCache.get('http://test.com'); assert.equal(resultNoCache.cached, false); + assert.equal(resultNoCache.stale, undefined); const axiosCache = mockAxios({}, { [Header.CacheControl]: `max-age=${60 * 60 * 24 * 365}` }); @@ -99,6 +101,7 @@ describe('Response Interceptor', () => { const resultCache = await axiosCache.get('http://test.com'); assert.ok(resultCache.cached); + assert.equal(resultCache.stale, false); }); it('Update cache integration', async () => { @@ -150,6 +153,7 @@ describe('Response Interceptor', () => { cache: { ttl: (resp) => { assert.equal(resp.cached, false); + assert.equal(resp.stale, undefined); assert.ok(resp.config); assert.notEqual(resp.headers[XMockRandom], NaN); assert.equal(resp.status, 200); @@ -325,6 +329,7 @@ describe('Response Interceptor', () => { // p2 should succeed as it was not aborted await assert.ok(response.data); await assert.equal(response.cached, false); + assert.equal(response.stale, undefined); const storage = await axios.storage.get(id); @@ -348,6 +353,7 @@ describe('Response Interceptor', () => { const response = await axios.get('url', { id }); assert.equal(response.cached, false); + assert.equal(response.stale, undefined); assert.ok(response.data); const storage = await axios.storage.get(id); diff --git a/test/interceptors/stale-if-error.test.ts b/test/interceptors/stale-if-error.test.ts index 4146a78..589262a 100644 --- a/test/interceptors/stale-if-error.test.ts +++ b/test/interceptors/stale-if-error.test.ts @@ -125,6 +125,7 @@ describe('StaleIfError handling', () => { assert.equal(response.statusText, cache.statusText); assert.strictEqual(response.headers, cache.headers); assert.ok(response.cached); + assert.ok(response.stale); }); it('StaleIfError needs to be `true`', async () => { @@ -242,6 +243,7 @@ describe('StaleIfError handling', () => { assert.equal(response.statusText, cache.statusText); assert.deepEqual(response.headers, cache.headers); assert.ok(response.cached); + assert.ok(response.stale); }); it('StaleIfError with real 50X status code', async () => { @@ -283,6 +285,7 @@ describe('StaleIfError handling', () => { assert.equal(response.statusText, cache.statusText); assert.deepEqual(response.headers, cache.headers); assert.ok(response.cached); + assert.ok(response.stale); const newResponse = await axios.get('url', { id, @@ -350,6 +353,8 @@ describe('StaleIfError handling', () => { assert.ok(res1.cached); assert.ok(res2.cached); + assert.ok(res1.stale); + assert.ok(res2.stale); const cache = await axios.storage.get(id); @@ -387,6 +392,7 @@ describe('StaleIfError handling', () => { assert.ok(response); assert.equal(response.id, id); assert.ok(response.cached); + assert.ok(response.stale); assert.ok(response.data); // Advances on time @@ -434,6 +440,7 @@ describe('StaleIfError handling', () => { const data = await axios.get('url', { id }); assert.equal(data.cached, false); + assert.equal(data.stale, undefined); try { await axios.get('url', { id, params: { fail: true } }); diff --git a/test/storage/storages.test.ts b/test/storage/storages.test.ts index 9de0470..7f17c0a 100644 --- a/test/storage/storages.test.ts +++ b/test/storage/storages.test.ts @@ -71,9 +71,11 @@ describe('General storage functions', () => { assert.equal(res1.status, 200); assert.equal(res1.cached, false); + assert.equal(res1.stale, undefined); assert.equal(res2.status, 200); assert.ok(res2.cached); + assert.equal(res2.stale, false); assert.equal(res1.id, res2.id); diff --git a/test/util/cache-predicate.test.ts b/test/util/cache-predicate.test.ts index 75e2196..8aa8132 100644 --- a/test/util/cache-predicate.test.ts +++ b/test/util/cache-predicate.test.ts @@ -253,11 +253,14 @@ describe('CachePredicate', () => { assert.ok(req1.id); assert.equal(req1.cached, false); + assert.equal(req1.stale, undefined); assert.ok(req2.id); assert.equal(req2.cached, true); + assert.equal(req2.stale, false); assert.ok(req3.id); assert.equal(req3.cached, false); + assert.equal(req3.stale, undefined); }); });