feat: add stale flag in request return object (#843)

* feature: added stale flag in request return object

* feature: added stale flag in request return object

* feat: added stale flag in request return object

* feat: added stale flag in request return object

* chore: fixed lint issues
This commit is contained in:
Kevin Foniciello 2024-08-03 18:52:56 -05:00 committed by GitHub
parent e073501222
commit 75aa9cfefd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 80 additions and 0 deletions

View File

@ -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. [`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.

7
src/cache/axios.ts vendored
View File

@ -44,6 +44,13 @@ export interface CacheAxiosResponse<R = any, D = any> extends AxiosResponse<R, D
* @see https://axios-cache-interceptor.js.org/config/response-object#cached * @see https://axios-cache-interceptor.js.org/config/response-object#cached
*/ */
cached: boolean; cached: boolean;
/**
* A simple boolean indicating if the request returned data is from valid or stale cache.
*
* @see https://axios-cache-interceptor.js.org/config/response-object#stale
*/
stale?: boolean;
} }
/** /**

View File

@ -239,6 +239,7 @@ export function defaultRequestInterceptor(axios: AxiosCacheInstance) {
status: cachedResponse.status, status: cachedResponse.status,
statusText: cachedResponse.statusText, statusText: cachedResponse.statusText,
cached: true, cached: true,
stale: (cache as LoadingStorageValue).previous === 'stale',
id: config.id! id: config.id!
}); });

View File

@ -348,6 +348,7 @@ export function defaultResponseInterceptor(axios: AxiosCacheInstance): ResponseI
return { return {
cached: true, cached: true,
stale: true,
config, config,
id, id,
data: cache.data.data, data: cache.data.data,

View File

@ -72,6 +72,7 @@ describe('Header Interpreter', () => {
const result = await axios.get('http://test.com'); const result = await axios.get('http://test.com');
assert.ok(result.cached); assert.ok(result.cached);
assert.equal(result.stale, false);
}); });
it('Header Interpreter Stale with StaleWhileRevalidate and MaxStale', () => { it('Header Interpreter Stale with StaleWhileRevalidate and MaxStale', () => {

View File

@ -14,6 +14,7 @@ describe('ETag handling', () => {
const response = await axios.get('http://test.com', config); const response = await axios.get('http://test.com', config);
assert.ok(response.cached); assert.ok(response.cached);
assert.equal(response.stale, false);
assert.ok(response.data); assert.ok(response.data);
// Sleep entire max age time. // Sleep entire max age time.
@ -22,6 +23,7 @@ describe('ETag handling', () => {
const response2 = await axios.get('http://test.com', config); const response2 = await axios.get('http://test.com', config);
// from revalidation // from revalidation
assert.ok(response2.cached); assert.ok(response2.cached);
assert.equal(response.stale, false);
// ensure value from stale cache is kept // ensure value from stale cache is kept
assert.ok(response2.data); assert.ok(response2.data);
}); });
@ -37,6 +39,7 @@ describe('ETag handling', () => {
const response = await axios.get('http://test.com'); const response = await axios.get('http://test.com');
assert.ok(response.cached); assert.ok(response.cached);
assert.equal(response.stale, false);
assert.ok(response.data); assert.ok(response.data);
// Sleep entire max age time. // Sleep entire max age time.
@ -45,6 +48,7 @@ describe('ETag handling', () => {
const response2 = await axios.get('http://test.com'); const response2 = await axios.get('http://test.com');
// from revalidation // from revalidation
assert.ok(response2.cached); assert.ok(response2.cached);
assert.equal(response.stale, false);
// ensure value from stale cache is kept // ensure value from stale cache is kept
assert.ok(response2.data); assert.ok(response2.data);
}); });
@ -61,6 +65,7 @@ describe('ETag handling', () => {
const response = await axios.get('http://test.com', config); const response = await axios.get('http://test.com', config);
// from etag revalidation // from etag revalidation
assert.ok(response.cached); assert.ok(response.cached);
assert.equal(response.stale, false);
assert.ok(response.data); assert.ok(response.data);
}); });
@ -70,12 +75,14 @@ describe('ETag handling', () => {
const response = await axios.get('http://test.com', config); const response = await axios.get('http://test.com', config);
assert.equal(response.cached, false); assert.equal(response.cached, false);
assert.equal(response.stale, undefined);
assert.ok(response.data); assert.ok(response.data);
assert.equal(response.config.headers?.[Header.IfModifiedSince], undefined); assert.equal(response.config.headers?.[Header.IfModifiedSince], undefined);
assert.equal(response.headers?.[Header.LastModified], undefined); assert.equal(response.headers?.[Header.LastModified], undefined);
const response2 = await axios.get('http://test.com', config); const response2 = await axios.get('http://test.com', config);
assert.ok(response2.cached); assert.ok(response2.cached);
assert.equal(!!response.stale, false);
assert.ok(response2.data); assert.ok(response2.data);
assert.equal(response2.config.headers?.[Header.IfNoneMatch], 'fake-etag'); assert.equal(response2.config.headers?.[Header.IfNoneMatch], 'fake-etag');
assert.equal(response2.headers?.[Header.ETag], 'fake-etag-2'); assert.equal(response2.headers?.[Header.ETag], 'fake-etag-2');

View File

@ -40,6 +40,7 @@ describe('Hydrate handling', () => {
assert.equal(m.mock.callCount(), 0); assert.equal(m.mock.callCount(), 0);
assert.ok(res2.cached); assert.ok(res2.cached);
assert.equal(res2.stale, false);
}); });
it('Hydrates when etag is set', async () => { it('Hydrates when etag is set', async () => {
@ -69,6 +70,7 @@ describe('Hydrate handling', () => {
assert.equal(m.mock.callCount(), 1); assert.equal(m.mock.callCount(), 1);
assert.ok(res2.cached); assert.ok(res2.cached);
assert.equal(!!res2.stale, false);
assert.deepEqual(m.mock.calls[0]?.arguments, [cache]); assert.deepEqual(m.mock.calls[0]?.arguments, [cache]);
}); });
@ -92,6 +94,7 @@ describe('Hydrate handling', () => {
assert.equal(m.mock.callCount(), 0); assert.equal(m.mock.callCount(), 0);
assert.equal(res2.cached, false); assert.equal(res2.cached, false);
assert.equal(res2.stale, undefined);
}); });
it('Only hydrates when stale while revalidate is not expired', async () => { 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(m.mock.callCount(), 0);
assert.equal(res2.cached, false); assert.equal(res2.cached, false);
assert.equal(res2.stale, undefined);
}); });
it('Hydrates when force stale', async () => { it('Hydrates when force stale', async () => {
@ -141,5 +145,6 @@ describe('Hydrate handling', () => {
assert.equal(m.mock.callCount(), 1); assert.equal(m.mock.callCount(), 1);
assert.deepEqual(m.mock.calls[0]?.arguments, [cache]); assert.deepEqual(m.mock.calls[0]?.arguments, [cache]);
assert.equal(res2.cached, false); assert.equal(res2.cached, false);
assert.equal(res2.stale, undefined);
}); });
}); });

View File

@ -24,6 +24,7 @@ describe('LastModified handling', () => {
const response = await axios.get('url', config); const response = await axios.get('url', config);
assert.ok(response.cached); assert.ok(response.cached);
assert.equal(response.stale, false);
assert.ok(response.data); assert.ok(response.data);
// Sleep entire max age time (using await to function as setImmediate) // 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); const response2 = await axios.get('url', config);
// from revalidation // from revalidation
assert.ok(response2.cached); assert.ok(response2.cached);
assert.equal(response.stale, false);
assert.equal(response2.status, 200); assert.equal(response2.status, 200);
}); });
@ -48,6 +50,7 @@ describe('LastModified handling', () => {
const response = await axios.get('url'); const response = await axios.get('url');
assert.ok(response.cached); assert.ok(response.cached);
assert.equal(response.stale, false);
assert.ok(response.data); assert.ok(response.data);
// Sleep entire max age time (using await to function as setImmediate) // Sleep entire max age time (using await to function as setImmediate)
@ -56,6 +59,7 @@ describe('LastModified handling', () => {
const response2 = await axios.get('url'); const response2 = await axios.get('url');
// from revalidation // from revalidation
assert.ok(response2.cached); assert.ok(response2.cached);
assert.equal(response.stale, false);
assert.equal(response2.status, 200); assert.equal(response2.status, 200);
}); });
@ -69,12 +73,14 @@ describe('LastModified handling', () => {
const response = await axios.get('url', config); const response = await axios.get('url', config);
assert.equal(response.cached, false); assert.equal(response.cached, false);
assert.equal(response.stale, undefined);
assert.ok(response.data); assert.ok(response.data);
assert.equal(response.config.headers?.[Header.IfModifiedSince], undefined); assert.equal(response.config.headers?.[Header.IfModifiedSince], undefined);
assert.ok(response.headers?.[Header.XAxiosCacheLastModified]); assert.ok(response.headers?.[Header.XAxiosCacheLastModified]);
const response2 = await axios.get('url', config); const response2 = await axios.get('url', config);
assert.ok(response2.cached); assert.ok(response2.cached);
assert.equal(!!response.stale, false);
assert.ok(response2.data); assert.ok(response2.data);
assert.ok(response2.config.headers?.[Header.IfModifiedSince]); assert.ok(response2.config.headers?.[Header.IfModifiedSince]);
assert.ok(response2.headers?.[Header.XAxiosCacheLastModified]); assert.ok(response2.headers?.[Header.XAxiosCacheLastModified]);

View File

@ -44,7 +44,9 @@ describe('Request Interceptor', () => {
]); ]);
assert.equal(resp1.cached, false); assert.equal(resp1.cached, false);
assert.equal(resp1.stale, undefined);
assert.ok(resp2.cached); assert.ok(resp2.cached);
assert.equal(resp2.stale, false);
}); });
it('Concurrent requests with `cache: false`', async () => { it('Concurrent requests with `cache: false`', async () => {
@ -57,6 +59,7 @@ describe('Request Interceptor', () => {
]); ]);
for (const result of results) { for (const result of results) {
assert.equal(result.cached, false); 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.cached, false);
assert.equal(resp2.stale, undefined);
}); });
it('`response.cached` is present', async () => { it('`response.cached` is present', async () => {
@ -85,19 +89,23 @@ describe('Request Interceptor', () => {
const response = await axios.get('http://test.com'); const response = await axios.get('http://test.com');
assert.equal(response.cached, false); assert.equal(response.cached, false);
assert.equal(response.stale, undefined);
const response2 = await axios.get('http://test.com'); const response2 = await axios.get('http://test.com');
assert.ok(response2.cached); assert.ok(response2.cached);
assert.equal(response2.stale, false);
const response3 = await axios.get('http://test.com', { const response3 = await axios.get('http://test.com', {
id: 'random-id' id: 'random-id'
}); });
assert.equal(response3.cached, false); assert.equal(response3.cached, false);
assert.equal(response3.stale, undefined);
const response4 = await axios.get('http://test.com', { const response4 = await axios.get('http://test.com', {
id: 'random-id' id: 'random-id'
}); });
assert.ok(response4.cached); assert.ok(response4.cached);
assert.equal(response4.stale, false);
}); });
it('Cache expiration', async () => { it('Cache expiration', async () => {
@ -109,12 +117,14 @@ describe('Request Interceptor', () => {
const resultCache = await axios.get('http://test.com'); const resultCache = await axios.get('http://test.com');
assert.ok(resultCache.cached); assert.ok(resultCache.cached);
assert.equal(resultCache.stale, false);
// Sleep entire max age time (using await to function as setImmediate) // Sleep entire max age time (using await to function as setImmediate)
await mockDateNow(1000); await mockDateNow(1000);
const response2 = await axios.get('http://test.com'); const response2 = await axios.get('http://test.com');
assert.equal(response2.cached, false); assert.equal(response2.cached, false);
assert.equal(response2.stale, undefined);
}); });
it('`must revalidate` does not allows stale', async () => { it('`must revalidate` does not allows stale', async () => {
@ -140,11 +150,14 @@ describe('Request Interceptor', () => {
const res3 = await axios.get('url', config); const res3 = await axios.get('url', config);
assert.equal(res1.cached, false); assert.equal(res1.cached, false);
assert.equal(res1.stale, undefined);
const headers1 = res1.headers as Record<string, string>; const headers1 = res1.headers as Record<string, string>;
const headers2 = res2.headers as Record<string, string>; const headers2 = res2.headers as Record<string, string>;
assert.equal(headers1['x-mock-random'], headers2['x-mock-random']); assert.equal(headers1['x-mock-random'], headers2['x-mock-random']);
assert.ok(res2.cached); assert.ok(res2.cached);
assert.equal(res2.stale, false);
assert.ok(res3.cached); assert.ok(res3.cached);
assert.equal(res3.stale, false);
// waits one second (using await to function as setImmediate) // waits one second (using await to function as setImmediate)
await mockDateNow(1000); await mockDateNow(1000);
@ -162,10 +175,12 @@ describe('Request Interceptor', () => {
const result = await axios.get('url', { data: { a: 1 } }); const result = await axios.get('url', { data: { a: 1 } });
assert.equal(result.cached, false); assert.equal(result.cached, false);
assert.equal(result.stale, undefined);
const result2 = await axios.get('url', { data: { a: 2 } }); const result2 = await axios.get('url', { data: { a: 2 } });
assert.equal(result2.cached, false); assert.equal(result2.cached, false);
assert.equal(result2.stale, undefined);
}); });
it('Tests a request with really long keys', async () => { it('Tests a request with really long keys', async () => {
@ -230,6 +245,7 @@ describe('Request Interceptor', () => {
const { id, ...initialResponse } = await axios.get('url'); const { id, ...initialResponse } = await axios.get('url');
assert.equal(initialResponse.cached, false); assert.equal(initialResponse.cached, false);
assert.equal(initialResponse.stale, undefined);
// Ensure cache was populated // Ensure cache was populated
const c1 = await axios.storage.get(id); const c1 = await axios.storage.get(id);
@ -276,6 +292,7 @@ describe('Request Interceptor', () => {
const c3 = await axios.storage.get(id); const c3 = await axios.storage.get(id);
assert.equal(newResponse.cached, false); assert.equal(newResponse.cached, false);
assert.equal(newResponse.stale, undefined);
assert.equal(c3.state, 'cached'); assert.equal(c3.state, 'cached');
assert.notEqual(c3.data, c1.data); // `'overridden response'`, not `true` assert.notEqual(c3.data, c1.data); // `'overridden response'`, not `true`
assert.notEqual(c3.createdAt, c1.createdAt); assert.notEqual(c3.createdAt, c1.createdAt);
@ -327,6 +344,7 @@ describe('Request Interceptor', () => {
const c3 = await axios.storage.get(id); const c3 = await axios.storage.get(id);
assert.equal(newResponse.cached, false); assert.equal(newResponse.cached, false);
assert.equal(newResponse.stale, undefined);
assert.equal(c3.state, 'cached'); assert.equal(c3.state, 'cached');
assert.ok(c3.data); assert.ok(c3.data);
assert.notEqual(c3.createdAt, c1.createdAt); 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')]); const [req0, req1] = await Promise.all([axios.get('url'), axios.get('url')]);
assert.equal(req0.cached, false); assert.equal(req0.cached, false);
assert.equal(req0.stale, undefined);
assert.equal(req1.cached, false); assert.equal(req1.cached, false);
assert.equal(req1.stale, undefined);
const [req2, req3] = await Promise.all([axios.get('some-other'), axios.get('some-other')]); const [req2, req3] = await Promise.all([axios.get('some-other'), axios.get('some-other')]);
assert.equal(req2.cached, false); assert.equal(req2.cached, false);
assert.equal(req2.stale, undefined);
assert.ok(req3.cached); assert.ok(req3.cached);
assert.equal(req3.stale, false);
}); });
it('ensures request with urls in exclude.paths are not cached (regex)', async () => { 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')]); const [req0, req1] = await Promise.all([axios.get('my/url'), axios.get('my/url')]);
assert.equal(req0.cached, false); assert.equal(req0.cached, false);
assert.equal(req0.stale, undefined);
assert.equal(req1.cached, false); assert.equal(req1.cached, false);
assert.equal(req1.stale, undefined);
const [req2, req3] = await Promise.all([axios.get('some-other'), axios.get('some-other')]); const [req2, req3] = await Promise.all([axios.get('some-other'), axios.get('some-other')]);
assert.equal(req2.cached, false); assert.equal(req2.cached, false);
assert.equal(req2.stale, undefined);
assert.ok(req3.cached); assert.ok(req3.cached);
assert.equal(req3.stale, false);
const [req4, req5] = await Promise.all([axios.get('other/url'), axios.get('other/url')]); const [req4, req5] = await Promise.all([axios.get('other/url'), axios.get('other/url')]);
assert.equal(req4.cached, false); assert.equal(req4.cached, false);
assert.equal(req4.stale, undefined);
assert.equal(req5.cached, false); assert.equal(req5.cached, false);
assert.equal(req5.stale, undefined);
}); });
}); });

View File

@ -81,6 +81,7 @@ describe('Response Interceptor', () => {
const result = await fetch(); const result = await fetch();
assert.equal(result.cached, false); assert.equal(result.cached, false);
assert.equal(result.stale, undefined);
}); });
it('HeaderInterpreter integration', async () => { it('HeaderInterpreter integration', async () => {
@ -91,6 +92,7 @@ describe('Response Interceptor', () => {
const resultNoCache = await axiosNoCache.get('http://test.com'); const resultNoCache = await axiosNoCache.get('http://test.com');
assert.equal(resultNoCache.cached, false); assert.equal(resultNoCache.cached, false);
assert.equal(resultNoCache.stale, undefined);
const axiosCache = mockAxios({}, { [Header.CacheControl]: `max-age=${60 * 60 * 24 * 365}` }); 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'); const resultCache = await axiosCache.get('http://test.com');
assert.ok(resultCache.cached); assert.ok(resultCache.cached);
assert.equal(resultCache.stale, false);
}); });
it('Update cache integration', async () => { it('Update cache integration', async () => {
@ -150,6 +153,7 @@ describe('Response Interceptor', () => {
cache: { cache: {
ttl: (resp) => { ttl: (resp) => {
assert.equal(resp.cached, false); assert.equal(resp.cached, false);
assert.equal(resp.stale, undefined);
assert.ok(resp.config); assert.ok(resp.config);
assert.notEqual(resp.headers[XMockRandom], NaN); assert.notEqual(resp.headers[XMockRandom], NaN);
assert.equal(resp.status, 200); assert.equal(resp.status, 200);
@ -325,6 +329,7 @@ describe('Response Interceptor', () => {
// p2 should succeed as it was not aborted // p2 should succeed as it was not aborted
await assert.ok(response.data); await assert.ok(response.data);
await assert.equal(response.cached, false); await assert.equal(response.cached, false);
assert.equal(response.stale, undefined);
const storage = await axios.storage.get(id); const storage = await axios.storage.get(id);
@ -348,6 +353,7 @@ describe('Response Interceptor', () => {
const response = await axios.get('url', { id }); const response = await axios.get('url', { id });
assert.equal(response.cached, false); assert.equal(response.cached, false);
assert.equal(response.stale, undefined);
assert.ok(response.data); assert.ok(response.data);
const storage = await axios.storage.get(id); const storage = await axios.storage.get(id);

View File

@ -125,6 +125,7 @@ describe('StaleIfError handling', () => {
assert.equal(response.statusText, cache.statusText); assert.equal(response.statusText, cache.statusText);
assert.strictEqual(response.headers, cache.headers); assert.strictEqual(response.headers, cache.headers);
assert.ok(response.cached); assert.ok(response.cached);
assert.ok(response.stale);
}); });
it('StaleIfError needs to be `true`', async () => { it('StaleIfError needs to be `true`', async () => {
@ -242,6 +243,7 @@ describe('StaleIfError handling', () => {
assert.equal(response.statusText, cache.statusText); assert.equal(response.statusText, cache.statusText);
assert.deepEqual(response.headers, cache.headers); assert.deepEqual(response.headers, cache.headers);
assert.ok(response.cached); assert.ok(response.cached);
assert.ok(response.stale);
}); });
it('StaleIfError with real 50X status code', async () => { it('StaleIfError with real 50X status code', async () => {
@ -283,6 +285,7 @@ describe('StaleIfError handling', () => {
assert.equal(response.statusText, cache.statusText); assert.equal(response.statusText, cache.statusText);
assert.deepEqual(response.headers, cache.headers); assert.deepEqual(response.headers, cache.headers);
assert.ok(response.cached); assert.ok(response.cached);
assert.ok(response.stale);
const newResponse = await axios.get('url', { const newResponse = await axios.get('url', {
id, id,
@ -350,6 +353,8 @@ describe('StaleIfError handling', () => {
assert.ok(res1.cached); assert.ok(res1.cached);
assert.ok(res2.cached); assert.ok(res2.cached);
assert.ok(res1.stale);
assert.ok(res2.stale);
const cache = await axios.storage.get(id); const cache = await axios.storage.get(id);
@ -387,6 +392,7 @@ describe('StaleIfError handling', () => {
assert.ok(response); assert.ok(response);
assert.equal(response.id, id); assert.equal(response.id, id);
assert.ok(response.cached); assert.ok(response.cached);
assert.ok(response.stale);
assert.ok(response.data); assert.ok(response.data);
// Advances on time // Advances on time
@ -434,6 +440,7 @@ describe('StaleIfError handling', () => {
const data = await axios.get('url', { id }); const data = await axios.get('url', { id });
assert.equal(data.cached, false); assert.equal(data.cached, false);
assert.equal(data.stale, undefined);
try { try {
await axios.get('url', { id, params: { fail: true } }); await axios.get('url', { id, params: { fail: true } });

View File

@ -71,9 +71,11 @@ describe('General storage functions', () => {
assert.equal(res1.status, 200); assert.equal(res1.status, 200);
assert.equal(res1.cached, false); assert.equal(res1.cached, false);
assert.equal(res1.stale, undefined);
assert.equal(res2.status, 200); assert.equal(res2.status, 200);
assert.ok(res2.cached); assert.ok(res2.cached);
assert.equal(res2.stale, false);
assert.equal(res1.id, res2.id); assert.equal(res1.id, res2.id);

View File

@ -253,11 +253,14 @@ describe('CachePredicate', () => {
assert.ok(req1.id); assert.ok(req1.id);
assert.equal(req1.cached, false); assert.equal(req1.cached, false);
assert.equal(req1.stale, undefined);
assert.ok(req2.id); assert.ok(req2.id);
assert.equal(req2.cached, true); assert.equal(req2.cached, true);
assert.equal(req2.stale, false);
assert.ok(req3.id); assert.ok(req3.id);
assert.equal(req3.cached, false); assert.equal(req3.cached, false);
assert.equal(req3.stale, undefined);
}); });
}); });