diff --git a/packages/cacheable/README.md b/packages/cacheable/README.md index 6b373028..de523063 100644 --- a/packages/cacheable/README.md +++ b/packages/cacheable/README.md @@ -796,6 +796,7 @@ const cache = new Cacheable({ tags: true }); await cache.set('page:/products', html, { ttl: '10m', tags: ['entity:42', 'collection:products'] }); await cache.set('page:/products/42', detailHtml, { ttl: '10m', tags: ['entity:42'] }); +await cache.getOrSet('summary:products', loadSummary, { ttl: '10m', tags: ['collection:products'] }); // entity 42 changed - purge everything that referenced it await cache.tags.invalidateTag('entity:42'); @@ -1069,13 +1070,14 @@ The `getOrSet` method that comes from [@cacheable/utils](https://cacheable.org/ ```typescript export type GetOrSetFunctionOptions = { ttl?: number | string | { primary?: number | string; secondary?: number | string }; + tags?: string[]; cacheErrors?: boolean; throwErrors?: boolean; nonBlocking?: boolean; }; ``` -The `ttl` also accepts a [per-store object](#per-store-ttl-per-operation) such as `{ primary: '10s', secondary: '5m' }` to give the primary and secondary stores different expirations for this operation. +The `ttl` also accepts a [per-store object](#per-store-ttl-per-operation) such as `{ primary: '10s', secondary: '5m' }` to give the primary and secondary stores different expirations for this operation. The `tags` option associates a newly computed value with tags for [tag-based invalidation](#tag-based-invalidation). The `nonBlocking` option allows you to override the instance-level `nonBlocking` setting for the `get` call within `getOrSet`. When set to `false`, the `get` will block and wait for a response from the secondary store before deciding whether to call the provided function. When set to `true`, the primary store returns immediately and syncs from secondary in the background. diff --git a/packages/cacheable/src/index.ts b/packages/cacheable/src/index.ts index 98682177..d7e853d4 100644 --- a/packages/cacheable/src/index.ts +++ b/packages/cacheable/src/index.ts @@ -1203,7 +1203,7 @@ export class Cacheable extends Hookified { * @param {GetOrSetKey} key - The key to retrieve or set in the cache. This can also be a function that returns a string key. * If a function is provided, it will be called with the cache options to generate the key. * @param {() => Promise} function_ - The asynchronous function that computes the value to be cached if the key does not exist. - * @param {GetOrSetFunctionOptions} [options] - Optional settings for caching, such as the time to live (TTL) or whether to cache errors. + * @param {GetOrSetFunctionOptions} [options] - Optional settings for caching, such as the time to live (TTL), tags, or whether to cache errors. * @return {Promise} - A promise that resolves to the cached or newly computed value, or undefined if an error occurs and caching is not configured for errors. */ public async getOrSet( @@ -1225,7 +1225,7 @@ export class Cacheable extends Hookified { value: unknown, ttl?: number | string | PerStoreTtl, ) => { - await this.set(key, value, { ttl }); + await this.set(key, value, { ttl, tags: options?.tags }); }, /* v8 ignore next -- @preserve */ on: (event: string, listener: (...args: unknown[]) => void) => { diff --git a/packages/cacheable/src/types.ts b/packages/cacheable/src/types.ts index 806fc05c..67fba581 100644 --- a/packages/cacheable/src/types.ts +++ b/packages/cacheable/src/types.ts @@ -20,6 +20,12 @@ export type GetOrSetFunctionOptions = Omit< "ttl" > & { ttl?: number | string | PerStoreTtl; + /** + * Tags to associate with a newly computed entry for tag-based invalidation. Tags are only + * applied when `getOrSet` stores a value after a cache miss. + * @type {string[]} + */ + tags?: string[]; }; /** diff --git a/packages/cacheable/test/tags.test.ts b/packages/cacheable/test/tags.test.ts index bf9e19f3..8c2d5605 100644 --- a/packages/cacheable/test/tags.test.ts +++ b/packages/cacheable/test/tags.test.ts @@ -69,6 +69,46 @@ describe("cacheable tags", () => { expect(await cacheable.get(key)).toBeUndefined(); }); + test("getOrSet associates tags with a newly computed entry", async () => { + const cacheable = new Cacheable({ tags: true }); + let calls = 0; + const options = { tags: ["entity:42"] }; + const getValue = async () => { + calls++; + return `value-${calls}`; + }; + + expect(await cacheable.getOrSet("k", getValue, options)).toEqual("value-1"); + expect(await cacheable.tags.getTags("k")).toEqual(["entity:42"]); + expect(await cacheable.getOrSet("k", getValue, options)).toEqual("value-1"); + expect(calls).toBe(1); + + await cacheable.tags.invalidateTag("entity:42"); + expect(await cacheable.getOrSet("k", getValue, options)).toEqual("value-2"); + expect(calls).toBe(2); + expect(await cacheable.tags.getTags("k")).toEqual(["entity:42"]); + }); + + test("getOrSet leaves a recomputed entry untagged when tags are omitted", async () => { + const cacheable = new Cacheable({ tags: true }); + let calls = 0; + const getValue = async () => { + calls++; + return `value-${calls}`; + }; + + await cacheable.getOrSet("k", getValue, { tags: ["entity:42"] }); + await cacheable.tags.invalidateTag("entity:42"); + + expect(await cacheable.getOrSet("k", getValue)).toEqual("value-2"); + expect(calls).toBe(2); + expect(await cacheable.tags.getTags("k")).toBeUndefined(); + + await cacheable.tags.invalidateTag("entity:42"); + expect(await cacheable.getOrSet("k", getValue)).toEqual("value-2"); + expect(calls).toBe(2); + }); + test("set still supports ttl as the third argument", async () => { const cacheable = new Cacheable(); const key = faker.string.uuid();