diff --git a/packages/cacheable/README.md b/packages/cacheable/README.md index 55487217..c4197c78 100644 --- a/packages/cacheable/README.md +++ b/packages/cacheable/README.md @@ -793,9 +793,12 @@ export type GetOrSetFunctionOptions = { ttl?: number | string; cacheErrors?: boolean; throwErrors?: boolean; + nonBlocking?: boolean; }; ``` +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. + Here is an example of how to use the `getOrSet` method: ```javascript diff --git a/packages/cacheable/src/index.ts b/packages/cacheable/src/index.ts index 1db26980..5a06fb41 100644 --- a/packages/cacheable/src/index.ts +++ b/packages/cacheable/src/index.ts @@ -870,8 +870,12 @@ export class Cacheable extends Hookified { options?: GetOrSetFunctionOptions, ): Promise { // Create an adapter that converts Cacheable to CacheInstance + const getOptions = + options?.nonBlocking === undefined + ? undefined + : { nonBlocking: options.nonBlocking }; const cacheAdapter: CacheInstance = { - get: async (key: string) => this.get(key), + get: async (key: string) => this.get(key, getOptions), /* v8 ignore next -- @preserve */ has: async (key: string) => this.has(key), set: async (key: string, value: unknown, ttl?: number | string) => { @@ -891,6 +895,7 @@ export class Cacheable extends Hookified { ttl: options?.ttl ?? this._ttl, cacheErrors: options?.cacheErrors, throwErrors: options?.throwErrors, + nonBlocking: options?.nonBlocking, }; return getOrSet(key, function_, getOrSetOptions); } diff --git a/packages/cacheable/test/index.test.ts b/packages/cacheable/test/index.test.ts index 6a4bdce0..88deb941 100644 --- a/packages/cacheable/test/index.test.ts +++ b/packages/cacheable/test/index.test.ts @@ -1301,6 +1301,99 @@ describe("cacheable get or set", () => { // The adapter's on and emit methods exist and are used internally // This test covers their creation (lines 880-884) }); + + test("should pass nonBlocking option to get in getOrSet", async () => { + const secondary = new Keyv(); + const cacheable = new Cacheable({ secondary, nonBlocking: true }); + + // Set a value only in secondary store + await secondary.set("nb-getorset-key", "nb-value"); + + // Call getOrSet with nonBlocking: false to override instance setting + const function_ = vi.fn(async () => "fallback-value"); + const result = await cacheable.getOrSet("nb-getorset-key", function_, { + nonBlocking: false, + }); + + // Should get value from secondary (blocking mode) without calling the function + expect(result).toBe("nb-value"); + expect(function_).not.toHaveBeenCalled(); + + // Since nonBlocking was overridden to false, primary store should be populated immediately + const primaryResult = await cacheable.primary.get("nb-getorset-key"); + expect(primaryResult).toBe("nb-value"); + }); + + test("should use nonBlocking mode in getOrSet when option is true", async () => { + const secondary = new Keyv(); + const cacheable = new Cacheable({ secondary, nonBlocking: false }); + + // Set a value only in secondary store + await secondary.set("nb-true-key", "nb-true-value"); + + // Call getOrSet with nonBlocking: true to override instance setting + const function_ = vi.fn(async () => "fallback-value"); + const result = await cacheable.getOrSet("nb-true-key", function_, { + nonBlocking: true, + }); + + // Should get value from secondary in non-blocking mode + expect(result).toBe("nb-true-value"); + expect(function_).not.toHaveBeenCalled(); + + // Wait for background primary store population + await new Promise((resolve) => setTimeout(resolve, 50)); + + // Verify the value was populated to primary store in the background + const primaryResult = await cacheable.primary.get("nb-true-key"); + expect(primaryResult).toBe("nb-true-value"); + }); + + test("should forward nonBlocking to key generator function options", async () => { + const cacheable = new Cacheable(); + + // Key generator that embeds nonBlocking into the cache key + const generateKey = vi.fn( + (options?: GetOrSetOptions) => `key_nb_${options?.nonBlocking}`, + ); + const function_ = vi.fn(async () => "value"); + + await cacheable.getOrSet(generateKey, function_, { nonBlocking: true }); + + // The key generator should have received nonBlocking: true in options + expect(generateKey).toHaveBeenCalledWith( + expect.objectContaining({ nonBlocking: true }), + ); + + // Call again with nonBlocking: false - should produce a different key + await cacheable.getOrSet(generateKey, function_, { nonBlocking: false }); + + expect(generateKey).toHaveBeenCalledWith( + expect.objectContaining({ nonBlocking: false }), + ); + }); + + test("should use instance nonBlocking setting when getOrSet option is not provided", async () => { + const secondary = new Keyv(); + const cacheable = new Cacheable({ secondary, nonBlocking: true }); + + // Set a value only in secondary store + await secondary.set("nb-default-key", "nb-default-value"); + + // Call getOrSet without nonBlocking option - should use instance default (true) + const function_ = vi.fn(async () => "fallback-value"); + const result = await cacheable.getOrSet("nb-default-key", function_); + + // Should get value from secondary + expect(result).toBe("nb-default-value"); + expect(function_).not.toHaveBeenCalled(); + + // Wait for background primary store population + await new Promise((resolve) => setTimeout(resolve, 50)); + + const primaryResult = await cacheable.primary.get("nb-default-key"); + expect(primaryResult).toBe("nb-default-value"); + }); }); describe("cacheable adapter coverage", () => { diff --git a/packages/utils/README.md b/packages/utils/README.md index d16176ba..6ac1bdff 100644 --- a/packages/utils/README.md +++ b/packages/utils/README.md @@ -480,9 +480,12 @@ export type GetOrSetFunctionOptions = { ttl?: number | string; cacheErrors?: boolean; throwErrors?: boolean; + nonBlocking?: boolean; }; ``` +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. + Here is an example of how to use the `getOrSet` method: ```javascript diff --git a/packages/utils/src/memoize.ts b/packages/utils/src/memoize.ts index 93443c27..7237ea4f 100644 --- a/packages/utils/src/memoize.ts +++ b/packages/utils/src/memoize.ts @@ -39,6 +39,11 @@ export type GetOrSetFunctionOptions = { * - `"store"` - only throw errors that occur when getting/setting the cache */ throwErrors?: boolean | GetOrSetThrowErrorsContext; + /** + * If set, this will bypass the instances nonBlocking setting for the get call. + * @type {boolean} + */ + nonBlocking?: boolean; }; export type GetOrSetOptions = GetOrSetFunctionOptions & {