Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/cacheable/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion packages/cacheable/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -870,8 +870,12 @@ export class Cacheable extends Hookified {
options?: GetOrSetFunctionOptions,
): Promise<T | undefined> {
// 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) => {
Expand All @@ -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);
}
Expand Down
93 changes: 93 additions & 0 deletions packages/cacheable/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
3 changes: 3 additions & 0 deletions packages/utils/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions packages/utils/src/memoize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 & {
Expand Down