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
58 changes: 52 additions & 6 deletions packages/utils/src/memoize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,18 @@ export type CacheSyncInstance = {

export type GetOrSetKey = string | ((options?: GetOrSetOptions) => string);

type GetOrSetThrowErrorsContext = "function" | "store";

export type GetOrSetFunctionOptions = {
ttl?: number | string;
cacheErrors?: boolean;
throwErrors?: boolean;
/** Whether or not to throw errors:
* - `false` (default) - do not throw any errors
* - `true` - throw any error
* - `"function"` - only throw errors that occur in the provided function / setter
* - `"store"` - only throw errors that occur when getting/setting the cache
*/
throwErrors?: boolean | GetOrSetThrowErrorsContext;
};

export type GetOrSetOptions = GetOrSetFunctionOptions & {
Expand Down Expand Up @@ -111,26 +119,57 @@ export async function getOrSet<T>(
): Promise<T | undefined> {
const keyString = typeof key === "function" ? key(options) : key;

let value = (await options.cache.get(keyString)) as T | undefined;
let value: T | undefined;

try {
value = await options.cache.get(keyString);
} catch (error) {
options.cache.emit("error", error);
if (options.throwErrors === true || options.throwErrors === "store") {
throw error;
}
}

if (value === undefined) {
const cacheId = options.cacheId ?? "default";
const coalesceKey = `${cacheId}::${keyString}`;
value = await coalesceAsync(coalesceKey, async () => {
let result: T | undefined;
try {
const result = (await function_()) as T;
await options.cache.set(keyString, result, options.ttl);
// try to do the logic passed in as the setter
try {
result = await function_();
} catch (error) {
throw new ErrorEnvelope<GetOrSetThrowErrorsContext>(
error,
"function",
);
}
// try to write the result to the cache
try {
await options.cache.set(keyString, result, options.ttl);
} catch (error) {
throw new ErrorEnvelope<GetOrSetThrowErrorsContext>(error, "store");
}
return result;
} catch (error) {
} catch (caught) {
const errorType =
caught instanceof ErrorEnvelope
? (caught as ErrorEnvelope<GetOrSetThrowErrorsContext>).context
: /* c8 ignore next 1 */
undefined;
const error = caught instanceof ErrorEnvelope ? caught.error : caught;

options.cache.emit("error", error);
if (options.cacheErrors) {
await options.cache.set(keyString, error, options.ttl);
}

if (options.throwErrors) {
if (options.throwErrors === true || options.throwErrors === errorType) {
throw error;
}
}
return result;
});
}

Expand Down Expand Up @@ -181,3 +220,10 @@ export function createWrapKey(

return `${keyPrefix}::${function_.name}::${hashSync(arguments_, { serialize })}`;
}

class ErrorEnvelope<T> {
constructor(
public error: unknown,
public context: T,
) {}
}
80 changes: 79 additions & 1 deletion packages/utils/test/get-or-set.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ describe("cacheable get or set", () => {
expect(function_).toHaveBeenCalledTimes(2);
});

test("should throw on getOrSet error", async () => {
test("should throw on getOrSet error (`true` option)", async () => {
const cacheable = new MockCacheable();
const function_ = vi.fn(async () => {
throw new Error("Test error");
Expand All @@ -38,6 +38,84 @@ describe("cacheable get or set", () => {
expect(function_).toHaveBeenCalledTimes(1);
});

test("should throw on getOrSet error (`function` option)", async () => {
const cacheable = new MockCacheable();
const function_ = vi.fn(async () => {
throw new Error("Test error");
});

await expect(
getOrSet("key", function_, { cache: cacheable, throwErrors: "function" }),
).rejects.toThrow("Test error");
expect(function_).toHaveBeenCalledTimes(1);
});

test("should not throw on getOrSet cache error (`function` option)", async () => {
const cacheable = new MockCacheable();

cacheable.set = () => {
throw new Error("Cache error");
};

const function_ = vi.fn(async () => 1 + 2);

expect(
await getOrSet("key", function_, {
cache: cacheable,
throwErrors: "function",
}),
).toBe(3);
expect(function_).toHaveBeenCalledTimes(1);
});

test("should throw on getOrSet cache error on get (`store` option)", async () => {
const cacheable = new MockCacheable();

cacheable.get = () => {
throw new Error("Cache error");
};

const function_ = vi.fn(async () => 1 + 2);

await expect(
getOrSet("key", function_, { cache: cacheable, throwErrors: "store" }),
).rejects.toThrow("Cache error");
expect(function_).toHaveBeenCalledTimes(0);
});

test("should not throw on getOrSet cache error on get (`function` option)", async () => {
const cacheable = new MockCacheable();

cacheable.get = () => {
throw new Error("Cache error");
};

const function_ = vi.fn(async () => 1 + 2);

expect(
await getOrSet("key", function_, {
cache: cacheable,
throwErrors: "function",
}),
).toBe(3);
expect(function_).toHaveBeenCalledTimes(1);
});

test("should throw on getOrSet cache error (`store` option)", async () => {
const cacheable = new MockCacheable();

cacheable.set = () => {
throw new Error("Cache error");
};

const function_ = vi.fn(async () => 1 + 2);

await expect(
getOrSet("key", function_, { cache: cacheable, throwErrors: "store" }),
).rejects.toThrow("Cache error");
expect(function_).toHaveBeenCalledTimes(1);
});

test("should throw on getOrSet error with cache errors true", async () => {
const cacheable = new MockCacheable();
const function_ = vi.fn(async () => {
Expand Down