Skip to content

Commit

Permalink
feat(multicache): allow caching multiple keys at once
Browse files Browse the repository at this point in the history
  • Loading branch information
simllll committed Oct 21, 2020
1 parent b04edda commit 68315b3
Show file tree
Hide file tree
Showing 8 changed files with 274 additions and 9 deletions.
2 changes: 1 addition & 1 deletion storages/lru-redis/src/LRUWithRedisStorage.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AsynchronousCacheType } from "@hokify/node-ts-cache";
import {AsynchronousCacheType} from "@hokify/node-ts-cache";

import * as LRU from "lru-cache";
import * as Redis from "ioredis";
Expand Down
18 changes: 16 additions & 2 deletions storages/lru/src/LRUStorage.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,28 @@
import { SynchronousCacheType } from "@hokify/node-ts-cache";
import {
MultiSynchronousCacheType,
SynchronousCacheType,
} from "@hokify/node-ts-cache";

import * as LRU from "lru-cache";

export class LRUStorage implements SynchronousCacheType {
export class LRUStorage
implements SynchronousCacheType, MultiSynchronousCacheType {
myCache: LRU<string, any>;

constructor(private options: LRU.Options<string, any>) {
this.myCache = new LRU(options);
}

getItems<T>(keys: string[]): { [key: string]: T | undefined } {
return Object.fromEntries(keys.map((key) => [key, this.myCache.get(key)]));
}

setItems(values: { key: string; content: any }[]): void {
values.forEach((val) => {
this.myCache.set(val.key, val.content);
});
}

public getItem<T>(key: string): T | undefined {
return this.myCache.get(key) || undefined;
}
Expand Down
12 changes: 10 additions & 2 deletions storages/node-cache/src/node-cache.storage.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
import { SynchronousCacheType } from "@hokify/node-ts-cache";
import { SynchronousCacheType, MultiSynchronousCacheType } from "@hokify/node-ts-cache";

import * as NodeCache from "node-cache";

export class NodeCacheStorage implements SynchronousCacheType {
export class NodeCacheStorage implements SynchronousCacheType, MultiSynchronousCacheType {
myCache: NodeCache;

constructor(options: NodeCache.Options) {
this.myCache = new NodeCache(options);
}

getItems<T>(keys: string[]): { [key: string]: T | undefined } {
return this.myCache.mget(keys);
}

setItems(values: { key: string; content: any }[]): void {
this.myCache.mset(values.map(v => ({key: v.key, val: v.content})));
}

public getItem<T>(key: string): T | undefined {
return this.myCache.get(key) || undefined;
}
Expand Down
29 changes: 26 additions & 3 deletions storages/redisio/src/redisio.storage.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,28 @@
import { AsynchronousCacheType } from "@hokify/node-ts-cache";
import {
AsynchronousCacheType,
MultiAsynchronousCacheType,
} from "@hokify/node-ts-cache";
import * as Redis from "ioredis";

export class RedisIOStorage implements AsynchronousCacheType {
export class RedisIOStorage
implements AsynchronousCacheType, MultiAsynchronousCacheType {
constructor(private redis: () => Redis.Redis) {}

async getItems<T>(keys: string[]): Promise<{ [key: string]: T | undefined }> {
return Object.fromEntries(
(await this.redis().mget(keys)).map((result, i) => [
keys[i],
((result && JSON.parse(result)) ?? undefined) as T | undefined,
])
);
}

async setItems(
values: { key: string; content: any }[],
): Promise<void> {
await this.redis().mset(values)
}

public async getItem<T>(key: string): Promise<T | undefined> {
const entry: any = await this.redis().get(key);
let finalItem = entry;
Expand All @@ -13,7 +32,11 @@ export class RedisIOStorage implements AsynchronousCacheType {
return finalItem || undefined;
}

public async setItem(key: string, content: any, options?: { ttl?: number }): Promise<void> {
public async setItem(
key: string,
content: any,
options?: { ttl?: number }
): Promise<void> {
if (typeof content === "object") {
content = JSON.stringify(content);
} else if (content === undefined) {
Expand Down
153 changes: 153 additions & 0 deletions ts-cache/src/decorator/multicache.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { MultiAsynchronousCacheType, MultiSynchronousCacheType } from "..";

const defaultKeyStrategy = {
getKey(
className: string,
methodName: string,
parameter: any,
args: any
): string {
return `${className}:${methodName}:${JSON.stringify(
parameter
)}:${JSON.stringify(args)}`;
},
};

export function MultiCache(
cachingStrategies: (MultiAsynchronousCacheType | MultiSynchronousCacheType)[],
parameterIndex = 0,
keyStrategy = defaultKeyStrategy
): Function {
return function (
target: Object /* & {
__cache_decarator_pending_results: {
[key: string]: Promise<any> | undefined;
};
}*/,
methodName: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
const className = target.constructor.name;

descriptor.value = async function (...args: any[]) {
const runMethod = async (newSet: any[]) => {
const newArgs = [...args];
newArgs[parameterIndex] = newSet;

const methodCall = originalMethod.apply(this, newArgs);

let methodResult;

const isAsync =
methodCall?.constructor?.name === "AsyncFunction" ||
methodCall?.constructor?.name === "Promise";
if (isAsync) {
methodResult = await methodCall;
} else {
methodResult = methodCall;
}
return methodResult;
};

const parameters = args[parameterIndex];
const cacheKeys: (string| undefined)[] = parameters.map((parameter: any) => {
return keyStrategy.getKey(className, methodName, parameter, args);
});

let result: any[] = [];
if (!process.env.DISABLE_CACHE_DECORATOR) {
let currentCachingStrategy = 0;
do {
// console.log('cacheKeys', cacheKeys, currentCachingStrategy)
const foundEntries = ((await cachingStrategies[
currentCachingStrategy
]) as any).getItems(cacheKeys.filter(key => key !== undefined));

// console.log('foundEntries', foundEntries);

// remove all foudn entries from cacheKeys
Object.keys(foundEntries).forEach((entry) => {
if (foundEntries[entry] === undefined) return;
// remove entry from cacheKey
cacheKeys[cacheKeys.indexOf(entry)] = undefined;
});
// save back to strategies before this strategy
if (currentCachingStrategy > 0) {
const setCache =
Object.keys(foundEntries).map((key) => ({
key,
content: foundEntries[key],
})).filter(f => f.content !== undefined);

if (setCache.length > 0) {
let saveCurrentCachingStrategy = currentCachingStrategy - 1;
do {
await cachingStrategies[saveCurrentCachingStrategy].setItems(setCache);

saveCurrentCachingStrategy--;
} while (saveCurrentCachingStrategy >= 0);
}
}

// save to final result

result = [...result, ...Object.values(foundEntries).filter(f => f !== undefined)];
// console.log('result', result);

currentCachingStrategy++;
} while (
cacheKeys.filter(key => key !== undefined).length > 0 &&
currentCachingStrategy < cachingStrategies.length
);
}

if (cacheKeys.filter(key => key !== undefined).length > 0) {
// use original method to resolve them
const missingKeys = cacheKeys.map((key, i) => {
if (key !== undefined) {
return parameters[i]
}
return undefined;
}).filter(k => k !== undefined);

const originalMethodResult: any[] = await runMethod(missingKeys);
if (originalMethodResult.length !== missingKeys.length) {
throw new Error(
"input and output has different size! input: " +
cacheKeys.length +
", returned " +
originalMethodResult.length
);
}

// console.log('originalMethodResult', originalMethodResult);
if (!process.env.DISABLE_CACHE_DECORATOR) {
// save back to all caching strategies
const saveToCache =
originalMethodResult.map((content, i) => {
return {
key: keyStrategy.getKey(className, methodName, missingKeys[i], args),
content,
}
});

// console.log('saveToCache', saveToCache);

let saveCurrentCachingStrategy = cachingStrategies.length - 1;
do {
await cachingStrategies[saveCurrentCachingStrategy].setItems(saveToCache);

saveCurrentCachingStrategy--;
} while (saveCurrentCachingStrategy >= 0);
}

result = [...result, ...originalMethodResult];
}

return result;
};

return descriptor;
};
}
4 changes: 3 additions & 1 deletion ts-cache/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export {
SynchronousCacheType,
AsynchronousCacheType
AsynchronousCacheType,
MultiAsynchronousCacheType,
MultiSynchronousCacheType,
} from "./types/cache.types";
export { ExpirationStrategy } from "./strategy/caching/expiration.strategy";
export { ISyncKeyStrategy } from "./types/key.strategy.types";
Expand Down
22 changes: 22 additions & 0 deletions ts-cache/src/types/cache.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,28 @@ interface ICacheEntry {
meta: any;
}

export interface MultiAsynchronousCacheType<C = ICacheEntry> {
getItems<T>(keys: string[]): Promise<{ [key: string]: T | undefined }>;

setItems(
values: { key: string; content: C | undefined }[],
options?: any
): Promise<void>;

clear(): Promise<void>;
}

export interface MultiSynchronousCacheType<C = ICacheEntry> {
getItems<T>(keys: string[]): { [key: string]: T | undefined };

setItems(
values: { key: string; content: C | undefined }[],
options?: any
): void;

clear(): void;
}

export interface AsynchronousCacheType<C = ICacheEntry> {
getItem<T>(key: string): Promise<T | undefined>;

Expand Down
43 changes: 43 additions & 0 deletions ts-cache/test/multicache.decorator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//import * as Assert from "assert";
import {MultiCache} from "../src/decorator/multicache.decorator";
import {LRUStorage} from "../../storages/lru/src/LRUStorage";
import {NodeCacheStorage} from "../../storages/node-cache/src/node-cache.storage";

const storage = new LRUStorage({});
const storage2 = new NodeCacheStorage({});

// const data = ["user", "max", "test"];

storage2.setItem('TestClassOne:cachedCall:"elem3":[["elem1","elem2","elem3"]]', 'STORAGE2');
storage.setItem('TestClassOne:cachedCall:"elem2":[["elem1","elem2","elem3"]]', 'STORAGE1');

class TestClassOne {
callCount = 0;

@MultiCache([storage, storage2], 0/*, {
getKey(className: string, methodName: string, parameter: any, args: any): string {
return 'canonical:' + parameter +
}
}*/)
public cachedCall(param0: string[]): string[] {
console.log('called with', param0);
return param0.map(p => p + 'RETURN VALUE');
}
}

describe("MultiCacheDecorator", () => {
beforeEach(async () => {
// await storage.clear();
// await storage2.clear();
});

it("Should multi cache", async () => {
const myClass = new TestClassOne();
// call 1
const call1= await myClass.cachedCall(['elem1', 'elem2', 'elem3']);
console.log('CALL1', call1);

const call2= await myClass.cachedCall(['elem1', 'elem2', 'elem3']);
console.log('CALL2', call2);
});
});

0 comments on commit 68315b3

Please sign in to comment.