Commit a942ac11 authored by nanahira's avatar nanahira

wrapper

parent 9479f899
......@@ -11,15 +11,16 @@
"dependencies": {
"better-lock": "^2.0.3",
"class-transformer": "^0.5.1",
"cosmokit": "^1.2.1",
"encoded-buffer": "^0.2.6",
"ioredis": "^5.2.2",
"lodash": "^4.17.21",
"lru-cache": "^7.13.1",
"redlock": "^5.0.0-beta.2",
"typed-reflector": "^1.0.11"
},
"devDependencies": {
"@types/jest": "^28.1.6",
"@types/lodash": "^4.14.182",
"@types/node": "^18.6.0",
"@typescript-eslint/eslint-plugin": "^4.33.0",
"@typescript-eslint/parser": "^4.33.0",
......@@ -1209,6 +1210,12 @@
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
"dev": true
},
"node_modules/@types/lodash": {
"version": "4.14.182",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.182.tgz",
"integrity": "sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q==",
"dev": true
},
"node_modules/@types/node": {
"version": "18.6.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.6.0.tgz",
......@@ -1859,11 +1866,6 @@
"safe-buffer": "~5.1.1"
}
},
"node_modules/cosmokit": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/cosmokit/-/cosmokit-1.2.1.tgz",
"integrity": "sha512-BTn7vRr31WUwX7Tq8Q/r+Qz+LPKTE3vA0d7xzVaYNes2NPvGPmIWiljYP0m/PIrdpqLLtdHpY1zGNr+OwDhA7A=="
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
......@@ -5836,6 +5838,12 @@
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
"dev": true
},
"@types/lodash": {
"version": "4.14.182",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.182.tgz",
"integrity": "sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q==",
"dev": true
},
"@types/node": {
"version": "18.6.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.6.0.tgz",
......@@ -6291,11 +6299,6 @@
"safe-buffer": "~5.1.1"
}
},
"cosmokit": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/cosmokit/-/cosmokit-1.2.1.tgz",
"integrity": "sha512-BTn7vRr31WUwX7Tq8Q/r+Qz+LPKTE3vA0d7xzVaYNes2NPvGPmIWiljYP0m/PIrdpqLLtdHpY1zGNr+OwDhA7A=="
},
"cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
......
import { BaseDriver } from './base-driver';
import { AnyClass, AragamiOptions, ClassType } from './def';
import { AnyClass, AragamiOptions, Awaitable, ClassType } from './def';
import { RedisDriver } from './drivers/redis';
import { MemoryDriver } from './drivers/memory';
import { reflector } from './metadata';
import { instanceToPlain, plainToInstance } from 'class-transformer';
import { encode, decode } from 'encoded-buffer';
import { Awaitable } from 'cosmokit';
import { makeArray, MayBeArray } from './utility/utility';
import _ from 'lodash';
export class Aragami {
readonly driver: BaseDriver;
......@@ -42,9 +43,7 @@ export class Aragami {
}
private getTTL(o: any) {
return (
reflector.get('AragamiCacheTTL', o) || this.options.defaultTTL || 1000
);
return reflector.get('AragamiCacheTTL', o) ?? this.options.defaultTTL ?? 0;
}
private encode(o: any) {
......@@ -75,7 +74,7 @@ export class Aragami {
this.getBaseKey(options.prototype || o),
options.key || (await this.getKey(o)),
buf,
options.ttl || this.getTTL(options.prototype || o),
options.ttl ?? this.getTTL(options.prototype || o),
);
return o;
}
......@@ -118,12 +117,15 @@ export class Aragami {
wrap<T, A extends any[]>(
cl: ClassType<T>,
cb: (...args: A) => T | Promise<T>,
cb: (...args: A) => Awaitable<T>,
keySource: (...args: A) => Awaitable<string | T>,
) {
return async (...args: A): Promise<T> => {
const keyMeta = await keySource(...args);
const key = await this.getKey(keyMeta);
if (!key) {
return cb(...args);
}
const cachedValue = await this.get(cl, key);
if (cachedValue) {
return cachedValue;
......@@ -142,9 +144,10 @@ export class Aragami {
}
const baseKey = this.getBaseKey(o);
const keyTransformers = reflector.getArray('AragamiLockKeys', o);
return Promise.all(
keyTransformers.map(
async (keyTransformer) => `${baseKey}:${await keyTransformer(o)}`,
const actualKeys = await Promise.all(keyTransformers.map((fn) => fn(o)));
return _.compact(
actualKeys.flatMap((mayBeKeyArray) =>
makeArray(mayBeKeyArray).map((key) => `${baseKey}:${key}`),
),
);
}
......
import { Metadata } from './metadata';
import { Awaitable } from 'cosmokit';
import { TypedMethodDecorator } from './def';
export const CacheTTL = (ttl: number): ClassDecorator =>
import { Awaitable, TypedMethodDecorator } from './def';
import { MayBeArray } from './utility/utility';
export const CacheTTL = (ttl: number): ClassDecorator & MethodDecorator =>
Metadata.set('AragamiCacheTTL', ttl);
export const CachePrefix = (prefix: string): ClassDecorator =>
Metadata.set('AragamiCachePrefix', prefix);
type ObjectKeyFunction = (o: any) => Awaitable<string>;
type ObjectFunction<T> = (o: any) => Awaitable<T>;
type ObjectAndKeyFunction<T> = (o: any, key: string) => Awaitable<T>;
const DrainKey =
(decoratorFactory: (cb: ObjectKeyFunction) => ClassDecorator) =>
<T>(
decoratorFactory: (cb: ObjectFunction<T>) => ClassDecorator,
defaultValue: ObjectAndKeyFunction<T>,
) =>
(
fun: ObjectKeyFunction = (obj) => obj.toString(),
): PropertyDecorator & TypedMethodDecorator<ObjectKeyFunction> =>
fun: ObjectAndKeyFunction<T> = defaultValue,
): PropertyDecorator & TypedMethodDecorator<ObjectFunction<T>> =>
(obj, key, des?) => {
let cb: ObjectKeyFunction;
let cb: ObjectFunction<T>;
if (des) {
// method decorator
cb = async (o) => fun(await o[key]());
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
cb = async (o) => fun(await o[key](), key);
} else {
// property decorator
cb = (o) => fun(o[key]);
cb = (o) => fun(o[key], key);
}
return decoratorFactory(cb)(obj.constructor);
};
export const CacheKey = DrainKey((cb) => Metadata.set('AragamiCacheKey', cb));
export const LockKey = DrainKey((cb) => Metadata.append('AragamiLockKeys', cb));
export const CacheKey = DrainKey<string>(
(cb) => Metadata.set('AragamiCacheKey', cb),
(o) => o.toString(),
);
export const LockKey = DrainKey<MayBeArray<string>>(
(cb) => Metadata.append('AragamiLockKeys', cb),
(o, key) => `${key}_${o}`,
);
......@@ -21,3 +21,4 @@ export type TypedMethodDecorator<F extends Function> = <T extends F>(
export type AnyClass = new (...args: any[]) => any;
export type ClassType<T> = new (...args: any[]) => T;
export type Awaitable<T> = T | Promise<T>;
......@@ -11,7 +11,7 @@ export class MemoryDriver extends BaseDriver {
this.cacheMap.set(
baseKey,
new LRUCache({
ttl: 1000,
ttl: 1,
updateAgeOnGet: false,
updateAgeOnHas: false,
}),
......
......@@ -26,7 +26,12 @@ export class RedisDriver extends BaseDriver {
value: Buffer,
ttl: number,
): Promise<void> {
await this.redis.set(this.usingKey(baseKey, key), value, 'PX', ttl);
const redisKey = this.usingKey(baseKey, key);
if (ttl) {
await this.redis.set(redisKey, value, 'PX', ttl);
} else {
await this.redis.set(redisKey, value);
}
}
override async del(baseKey: string, key: string): Promise<boolean> {
......
import { Awaitable } from 'cosmokit';
import { MetadataSetter, Reflector } from 'typed-reflector';
import { MayBeArray } from './utility/utility';
import { Awaitable } from './def';
export class MetadataMap {
interface MetadataMap {
AragamiCacheTTL: number;
AragamiCachePrefix: string;
AragamiCacheKey: (obj: any) => Awaitable<string>;
}
export class MetadataArrayMap {
AragamiLockKeys: (obj: any) => Awaitable<string>;
interface MetadataArrayMap {
AragamiLockKeys: (obj: any) => Awaitable<MayBeArray<string>>;
AragamiWithKey: (param: any, obj: any, key: string) => Awaitable<string>;
AragamiWithLockKey: (
param: any,
obj: any,
key: string,
) => Awaitable<MayBeArray<string>>;
}
export const Metadata = new MetadataSetter<MetadataMap, MetadataArrayMap>();
......
import { Metadata, reflector } from './metadata';
import { makeArray, MayBeArray } from './utility/utility';
import { Awaitable, ClassType, TypedMethodDecorator } from './def';
import { Aragami } from './aragami';
import _ from 'lodash';
export const WithKey = (
factory: (param: any, obj: any, key: string) => Awaitable<any> = (param) =>
param,
) => Metadata.param('AragamiWithKey', factory);
export const WithLockKey = (
factory: (param: any, obj: any, key: string) => Awaitable<MayBeArray<any>> = (
param,
obj,
key,
) => param,
) => Metadata.param('AragamiWithLockKey', factory);
export class WrapDecoratorBuilder {
constructor(private aragamiFactory: (obj: any) => Awaitable<Aragami>) {}
build() {
const { aragamiFactory } = this;
return {
UseLock:
(): TypedMethodDecorator<(...args: any[]) => Awaitable<any>> =>
(obj, key, des) => {
const oldFun = des.value;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
des.value = async function (...args) {
const aragami = await aragamiFactory(this);
const wrapped = aragami.lock(
() => oldFun.apply(this, args),
async () => {
const lockKeyParams = await reflector.getArray(
'AragamiWithLockKey',
this,
key,
);
return (
await Promise.all(
_.compact(
lockKeyParams.map(async (fun, i) => {
if (!fun) return;
const keyResult = (await fun(
args[i],
this,
key as string,
)) as MayBeArray<any>;
return makeArray(keyResult);
}),
),
)
).flat();
},
);
return wrapped();
};
},
UseCache:
<T>(
cl: ClassType<T>,
): TypedMethodDecorator<(...args: any[]) => Awaitable<T>> =>
(obj, key, des) => {
const oldFun = des.value;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
des.value = async function (...args) {
const aragami = await aragamiFactory(this);
const wrapped = aragami.wrap<T, []>(
cl,
() => oldFun.apply(this, args),
async () => {
const withKeyParameters = reflector.getArray(
'AragamiWithKey',
this,
key,
);
const firstIndex = withKeyParameters.findIndex((f) => f);
if (firstIndex === -1) {
return;
}
return withKeyParameters[firstIndex](
args[firstIndex],
this,
key as string,
);
},
);
return wrapped();
};
},
};
}
}
import { Aragami } from '../src/aragami';
import { CacheKey, CachePrefix, CacheTTL } from '../src/decorators';
import { CacheKey, CachePrefix, CacheTTL, LockKey } from '../src/decorators';
import { WithKey, WithLockKey, WrapDecoratorBuilder } from '../src/wrappers';
describe('Aragami.', () => {
let aragami: Aragami;
......@@ -56,8 +57,9 @@ describe('Aragami.', () => {
},
(name) => name,
);
await expect(wrapped('John', 30)).resolves.toBeInstanceOf(User);
await expect(wrapped('John', 30)).resolves.toBeInstanceOf(User);
await expect(wrapped('Sarah', 40)).resolves.toBeInstanceOf(User);
await expect(aragami.has(User, 'Sarah')).resolves.toBeTruthy();
await expect(wrapped('Sarah', 40)).resolves.toBeInstanceOf(User);
});
it('should expire', async () => {
......@@ -72,7 +74,9 @@ describe('Aragami.', () => {
dress.name = 'Yuzu';
await aragami.set(dress);
await expect(aragami.has(dress)).resolves.toBeTruthy();
await new Promise((resolve) => setTimeout(resolve, 101));
await new Promise((resolve) => setTimeout(resolve, 90));
await expect(aragami.has(dress)).resolves.toBeTruthy();
await new Promise((resolve) => setTimeout(resolve, 15));
await expect(aragami.has(dress)).resolves.toBeFalsy();
});
......@@ -86,4 +90,49 @@ describe('Aragami.', () => {
);
await expect(fun('foo', 'bar')).resolves.toEqual('foo.bar');
});
it('should wrap class', async () => {
const { UseCache, UseLock } = new WrapDecoratorBuilder(
() => aragami,
).build();
class Book {
@CacheKey()
@LockKey()
title: string;
@LockKey()
content: string;
}
class MyService {
@UseCache(Book)
getBook(@WithKey() title: string, content: string) {
const book = new Book();
book.title = title;
book.content = content;
console.log('got book', book);
return book;
}
@UseLock()
async saveBook(@WithLockKey() book: Book) {
console.log('saving book', book);
await new Promise((resolve) => setTimeout(resolve, 1000));
console.log('saved book', book);
return book;
}
}
await aragami.clear(Book);
const service = new MyService();
const book = await service.getBook('foo', 'bar');
const bookFromCache = await aragami.get(Book, 'foo');
expect(bookFromCache).toEqual(book);
await new Promise((resolve) => setTimeout(resolve, 20));
const book2 = await service.getBook('foo', 'baz');
expect(book2).toEqual({ title: 'foo', content: 'bar' });
const savedBook = await service.saveBook(book);
expect(savedBook).toEqual(book);
});
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment