fix: avoid caching non-positive TTL entries
This commit is contained in:
@@ -48,7 +48,7 @@ interface Cache {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The cache key is the full request URL. `ttlMs` is in milliseconds and comes from `cacheTtlMs` or per-request `revalidateSeconds`.
|
The cache key is the full request URL. `ttlMs` is in milliseconds and comes from `cacheTtlMs` or per-request `revalidateSeconds`. A non-positive `ttlMs` means "do not cache"; built-in caches delete/skip the entry rather than storing it forever.
|
||||||
|
|
||||||
## Redis-style cache example
|
## Redis-style cache example
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
export interface Cache {
|
export interface Cache {
|
||||||
/** Return a cached value, or `undefined` on miss/expiry. */
|
/** Return a cached value, or `undefined` on miss/expiry. */
|
||||||
get(key: string): Promise<unknown | undefined>;
|
get(key: string): Promise<unknown | undefined>;
|
||||||
/** Store a value for the given TTL in milliseconds. */
|
/** Store a value for the given TTL in milliseconds. Non-positive TTL means "do not cache". */
|
||||||
set(key: string, value: unknown, ttlMs: number): Promise<void>;
|
set(key: string, value: unknown, ttlMs: number): Promise<void>;
|
||||||
/** Delete one cache entry. */
|
/** Delete one cache entry. */
|
||||||
delete(key: string): Promise<void>;
|
delete(key: string): Promise<void>;
|
||||||
@@ -31,6 +31,10 @@ export class MemoryCache implements Cache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async set(key: string, value: unknown, ttlMs: number): Promise<void> {
|
async set(key: string, value: unknown, ttlMs: number): Promise<void> {
|
||||||
|
if (ttlMs <= 0) {
|
||||||
|
this.map.delete(key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.map.set(key, { at: Date.now(), value, ttlMs });
|
this.map.set(key, { at: Date.now(), value, ttlMs });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -66,6 +66,19 @@ describe('SocialhoseClient', () => {
|
|||||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not cache a GET request when per-request revalidateSeconds is zero', async () => {
|
||||||
|
const fetchMock = vi
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValueOnce(ok({ count: 1, next: null, previous: null, results: [] }))
|
||||||
|
.mockResolvedValueOnce(ok({ count: 2, next: null, previous: null, results: [] }));
|
||||||
|
const client = new SocialhoseClient({ apiKey: 'test-key', fetch: fetchMock, cacheTtlMs: 60_000 });
|
||||||
|
|
||||||
|
await expect(client.getMentions({ page: 1 }, { revalidateSeconds: 0 })).resolves.toMatchObject({ count: 1 });
|
||||||
|
await expect(client.getMentions({ page: 1 }, { revalidateSeconds: 0 })).resolves.toMatchObject({ count: 2 });
|
||||||
|
|
||||||
|
expect(fetchMock).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
it('retries rate limits and transient server failures', async () => {
|
it('retries rate limits and transient server failures', async () => {
|
||||||
const fetchMock = vi
|
const fetchMock = vi
|
||||||
.fn()
|
.fn()
|
||||||
@@ -134,6 +147,16 @@ describe('MemoryCache', () => {
|
|||||||
expect(await cache.get('k')).toBeUndefined();
|
expect(await cache.get('k')).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not store values when TTL is zero or negative', async () => {
|
||||||
|
const cache = new MemoryCache();
|
||||||
|
|
||||||
|
await cache.set('zero', 'v', 0);
|
||||||
|
await cache.set('negative', 'v', -1);
|
||||||
|
|
||||||
|
expect(await cache.get('zero')).toBeUndefined();
|
||||||
|
expect(await cache.get('negative')).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
it('deletes a stored entry', async () => {
|
it('deletes a stored entry', async () => {
|
||||||
const cache = new MemoryCache();
|
const cache = new MemoryCache();
|
||||||
await cache.set('k', 'v', 60_000);
|
await cache.set('k', 'v', 60_000);
|
||||||
|
|||||||
Reference in New Issue
Block a user