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
81 changes: 81 additions & 0 deletions packages/cacheable/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ export class Cacheable extends Hookified {
private readonly _stats = new CacheableStats({ enabled: false });
private _namespace?: string | (() => string);
private _cacheId: string = Math.random().toString(36).slice(2);
// biome-ignore lint/suspicious/noExplicitAny: type format
private _serialize: (object: any) => string = JSON.stringify;
// biome-ignore lint/suspicious/noExplicitAny: type format
private _deserialize: (text: string) => any = JSON.parse;

/**
* Creates a new cacheable instance
Expand Down Expand Up @@ -67,6 +71,9 @@ export class Cacheable extends Hookified {
this._cacheId = options.cacheId;
}

this.setSerialize(options?.serialize ?? JSON.stringify);
this.setDeserialize(options?.deserialize ?? JSON.parse);

if (options?.namespace) {
this._namespace = options.namespace;
this._primary.namespace = this.getNameSpace();
Expand Down Expand Up @@ -223,6 +230,50 @@ export class Cacheable extends Hookified {
this._cacheId = cacheId;
}

/**
* Gets the serializer for the cacheable instance. It will also get the primary and secondary stores serializer.
* @returns {function} The serializer function
* @default JSON.stringify
*/
// biome-ignore lint/suspicious/noExplicitAny: type
public get serialize(): (object: any) => string {
return this._serialize;
}

/**
* Sets the serializer for the cacheable instance. It will also set the primary and secondary stores serializer.
* @param {function} serialize The serializer function
* @returns {void}
* @default JSON.stringify
*/
// biome-ignore lint/suspicious/noExplicitAny: type
public set serialize(serialize: (object: any) => string) {
this.setSerialize(serialize);
}

/**
* Sets the deserializer. This is used in many places such as Keyv. If you provide your own
* stringify function, you should also provide a parse function that is the inverse of the stringify function.
* @default JSON.parse
* @returns {function}
*/
// biome-ignore lint/suspicious/noExplicitAny: type
public get deserialize(): (text: string) => any {
return this._deserialize;
}

/**
* Sets the deserializer. This is used in many places such as Keyv. If you provide your own
* stringify function, you should also provide a parse function that is the inverse of the stringify function.
* @param {function} - the deserializer to use
* @default JSON.parse
* @returns {void}
*/
// biome-ignore lint/suspicious/noExplicitAny: type
public set deserialize(deserialize: (text: string) => any) {
this.setDeserialize(deserialize);
}

/**
* Sets the primary store for the cacheable instance
* @param {Keyv | KeyvStoreAdapter} primary The primary store for the cacheable instance
Expand Down Expand Up @@ -260,6 +311,35 @@ export class Cacheable extends Hookified {
this.emit(CacheableEvents.ERROR, error);
});
}
/**
* Sets the serializer for the cacheable instance. It will also set the primary and secondary stores serializer.
* @param {function} serialize The serializer function
* @returns {void}
* @default JSON.stringify
*/
// biome-ignore lint/suspicious/noExplicitAny: type format
public setSerialize(serialize: (object: any) => string): void {
this._serialize = serialize;
this._primary.serialize = serialize;
if (this._secondary) {
this._secondary.serialize = serialize;
}
}

/**
* Sets the deserializer for the cacheable instance. It will also set the primary and secondary stores deserializer.
* @param {function} deserialize The deserializer function
* @returns {void}
* @default JSON.parse
*/
// biome-ignore lint/suspicious/noExplicitAny: type format
public setDeserialize(deserialize: (text: string) => any): void {
this._deserialize = deserialize;
this._primary.deserialize = deserialize;
if (this._secondary) {
this._secondary.deserialize = deserialize;
}
}

// biome-ignore lint/suspicious/noExplicitAny: type format
public isKeyvInstance(keyv: any): boolean {
Expand Down Expand Up @@ -805,6 +885,7 @@ export class Cacheable extends Hookified {
cacheErrors: options?.cacheErrors,
cache: cacheAdapter,
cacheId: this._cacheId,
serialize: options?.serialize || this._serialize,
};

return wrap<T>(function_, wrapOptions);
Expand Down
20 changes: 20 additions & 0 deletions packages/cacheable/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,24 @@ export type CacheableOptions = {
* If it is not set then it will be a random string that is generated
*/
cacheId?: string;

/**
* Stringifies an object into a string representation. This is used in many places such as Keyv, hashing, and creating cache keys. If
* you are storing complex objects that JSON.stringify does not handle well, you may want to provide your own implementation.
* @param object The object to stringify.
* @default JSON.stringify
* @returns The string representation of the object.
*/
// biome-ignore lint/suspicious/noExplicitAny: type format
serialize?: (object: any) => string;

/**
* Parses a string back into an object. This is used in many places such as Keyv. If you provide your own
* stringify function, you should also provide a parse function that is the inverse of the stringify function.
* @param text The string to parse.
* @default JSON.parse
* @returns The parsed object.
*/
// biome-ignore lint/suspicious/noExplicitAny: type format
deserialize?: (text: string) => any;
};
2 changes: 1 addition & 1 deletion packages/cacheable/test/get-raw.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Cacheable, CacheableEvents, CacheableHooks } from "../src/index.js";
describe("cacheable getRaw method", async () => {
test("should get raw data object from primary store", async () => {
const cacheable = new Cacheable();
await cacheable.set("rawKey", "rawValue");
await cacheable.set("rawKey", "rawValue", 10);
const raw = await cacheable.getRaw("rawKey");
expect(raw).toHaveProperty("value", "rawValue");
expect(raw).toHaveProperty("expires");
Expand Down
155 changes: 36 additions & 119 deletions packages/cacheable/test/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createWrapKey, type GetOrSetOptions } from "@cacheable/memoize";
import type { GetOrSetOptions } from "@cacheable/memoize";
import { sleep } from "@cacheable/utils";
import { faker } from "@faker-js/faker";
import KeyvRedis from "@keyv/redis";
Expand Down Expand Up @@ -263,7 +263,7 @@ describe("cacheable get method", async () => {
});
test("should get a raw data object using getRaw", async () => {
const cacheable = new Cacheable();
await cacheable.set("rawKey", "rawValue");
await cacheable.set("rawKey", "rawValue", 10);
const raw = await cacheable.getRaw("rawKey");
expect(raw).toHaveProperty("value", "rawValue");
expect(raw).toHaveProperty("expires");
Expand Down Expand Up @@ -317,8 +317,8 @@ describe("cacheable get method", async () => {
});
test("should get raw data objects with getManyRaw", async () => {
const cacheable = new Cacheable();
await cacheable.set("rawKey1", "value1");
await cacheable.set("rawKey2", "value2");
await cacheable.set("rawKey1", "value1", 10);
await cacheable.set("rawKey2", "value2", 10);
const raws = await cacheable.getManyRaw(["rawKey1", "rawKey2"]);
expect(raws).toHaveLength(2);
expect(raws[0]).toHaveProperty("value", "value1");
Expand Down Expand Up @@ -679,78 +679,6 @@ describe("cacheable hash method", async () => {
});
});

describe("cacheable wrap", async () => {
test("should wrap method with key and ttl", async () => {
const cacheable = new Cacheable();
const asyncFunction = async (value: number) => Math.random() * value;
const options = {
keyPrefix: "keyPrefix",
ttl: 10,
};

const wrapped = cacheable.wrap(asyncFunction, options);
const result = await wrapped(1);
const result2 = await wrapped(1);
expect(result).toBe(result2);
const cacheKey = createWrapKey(asyncFunction, [1], options.keyPrefix);
const cacheResult1 = await cacheable.get(cacheKey);
expect(cacheResult1).toBe(result);
await sleep(20);
const cacheResult2 = await cacheable.get(cacheKey);
expect(cacheResult2).toBeUndefined();
});
test("wrap async function", async () => {
const cache = new Cacheable();
const options = {
keyPrefix: "wrapPrefix",
ttl: "5m",
};

const plus = async (a: number, b: number) => a + b;
const plusCached = cache.wrap(plus, options);

const multiply = async (a: number, b: number) => a * b;
const multiplyCached = cache.wrap(multiply, options);

const result1 = await plusCached(1, 2);
const result2 = await multiplyCached(1, 2);

expect(result1).toBe(3);
expect(result2).toBe(2);
});

test("should wrap to default ttl", async () => {
const cacheable = new Cacheable({ ttl: 10 });
const asyncFunction = async (value: number) => Math.random() * value;
const options = {
keyPrefix: "wrapPrefix",
};
const wrapped = cacheable.wrap(asyncFunction, options);
const result = await wrapped(1);
const result2 = await wrapped(1);
expect(result).toBe(result2); // Cached
await sleep(15);
const result3 = await wrapped(1);
expect(result3).not.toBe(result2);
});

test("Cacheable.wrap() passes createKey option through", async () => {
const cacheable = new Cacheable();
let createKeyCalled = false;
const asyncFunction = async (argument: string) => `Result for ${argument}`;
const options = {
createKey: () => {
createKeyCalled = true;
return "testKey";
},
};

const wrapped = cacheable.wrap(asyncFunction, options);
await wrapped("arg1");
expect(createKeyCalled).toBe(true);
});
});

describe("cacheable namespace", async () => {
test("should set the namespace via options", async () => {
const cacheable = new Cacheable({ namespace: "test" });
Expand Down Expand Up @@ -861,49 +789,6 @@ describe("cacheable get or set", () => {
});

describe("cacheable adapter coverage", () => {
test("should directly test wrap adapter on method", async () => {
const cacheable = new Cacheable();

// We need to directly call the adapter's on method to achieve 100% coverage
// Even though @cacheable/memoize doesn't call it, we need to test it works

// Access the wrap method's internals
const testFn = async () => "result";

// Create an adapter manually that mimics what wrap does
const adapter = {
get: async (key: string) => cacheable.get(key),
has: async (key: string) => cacheable.has(key),
// biome-ignore lint/suspicious/noExplicitAny: adapter interface
set: async (key: string, value: any, ttl?: number | string) => {
await cacheable.set(key, value, ttl);
},
// This is the method we need to cover (lines 838-839)
// biome-ignore lint/suspicious/noExplicitAny: adapter interface
on: (event: string, listener: (...args: any[]) => void) => {
cacheable.on(event, listener);
},
// biome-ignore lint/suspicious/noExplicitAny: adapter interface
emit: (event: string, ...args: any[]) => cacheable.emit(event, ...args),
};

// Test that the adapter's on method works
let listenerCalled = false;
adapter.on("test-wrap-event", () => {
listenerCalled = true;
});

// Trigger the event through the adapter
adapter.emit("test-wrap-event");

expect(listenerCalled).toBe(true);

// Also test the regular wrap functionality
const wrapped = cacheable.wrap(testFn, { keyPrefix: "test" });
await wrapped();
expect(await cacheable.has(createWrapKey(testFn, [], "test"))).toBe(true);
});

test("should directly test getOrSet adapter on method", async () => {
const cacheable = new Cacheable();

Expand Down Expand Up @@ -942,6 +827,38 @@ describe("cacheable adapter coverage", () => {
});
});

describe("cacheable serialize/deserialize properties", () => {
test("should get and set serialize property", () => {
const cacheable = new Cacheable();
// biome-ignore lint/suspicious/noExplicitAny: testing serializer interface
const customSerialize = (obj: any) => `custom:${JSON.stringify(obj)}`;

// Test getter
expect(typeof cacheable.serialize).toBe("function");

// Test setter
cacheable.serialize = customSerialize;
expect(cacheable.serialize).toBe(customSerialize);
});

test("should get and set deserialize property", () => {
const cacheable = new Cacheable();
const customDeserialize = (str: string) => {
if (str.startsWith("custom:")) {
return JSON.parse(str.substring(7));
}
return JSON.parse(str);
};

// Test getter
expect(typeof cacheable.deserialize).toBe("function");

// Test setter
cacheable.deserialize = customDeserialize;
expect(cacheable.deserialize).toBe(customDeserialize);
});
});

describe("cacheable hash method", () => {
test("should hash with sha512 algorithm", () => {
const cacheable = new Cacheable();
Expand Down
Loading