Commit a942ac11 authored by nanahira's avatar nanahira

wrapper

parent 9479f899
...@@ -11,15 +11,16 @@ ...@@ -11,15 +11,16 @@
"dependencies": { "dependencies": {
"better-lock": "^2.0.3", "better-lock": "^2.0.3",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"cosmokit": "^1.2.1",
"encoded-buffer": "^0.2.6", "encoded-buffer": "^0.2.6",
"ioredis": "^5.2.2", "ioredis": "^5.2.2",
"lodash": "^4.17.21",
"lru-cache": "^7.13.1", "lru-cache": "^7.13.1",
"redlock": "^5.0.0-beta.2", "redlock": "^5.0.0-beta.2",
"typed-reflector": "^1.0.11" "typed-reflector": "^1.0.11"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^28.1.6", "@types/jest": "^28.1.6",
"@types/lodash": "^4.14.182",
"@types/node": "^18.6.0", "@types/node": "^18.6.0",
"@typescript-eslint/eslint-plugin": "^4.33.0", "@typescript-eslint/eslint-plugin": "^4.33.0",
"@typescript-eslint/parser": "^4.33.0", "@typescript-eslint/parser": "^4.33.0",
...@@ -1209,6 +1210,12 @@ ...@@ -1209,6 +1210,12 @@
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
"dev": true "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": { "node_modules/@types/node": {
"version": "18.6.0", "version": "18.6.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.6.0.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.6.0.tgz",
...@@ -1859,11 +1866,6 @@ ...@@ -1859,11 +1866,6 @@
"safe-buffer": "~5.1.1" "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": { "node_modules/cross-spawn": {
"version": "7.0.3", "version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
...@@ -5836,6 +5838,12 @@ ...@@ -5836,6 +5838,12 @@
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
"dev": true "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": { "@types/node": {
"version": "18.6.0", "version": "18.6.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.6.0.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.6.0.tgz",
...@@ -6291,11 +6299,6 @@ ...@@ -6291,11 +6299,6 @@
"safe-buffer": "~5.1.1" "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": { "cross-spawn": {
"version": "7.0.3", "version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
......
import { BaseDriver } from './base-driver'; import { BaseDriver } from './base-driver';
import { AnyClass, AragamiOptions, ClassType } from './def'; import { AnyClass, AragamiOptions, Awaitable, ClassType } from './def';
import { RedisDriver } from './drivers/redis'; import { RedisDriver } from './drivers/redis';
import { MemoryDriver } from './drivers/memory'; import { MemoryDriver } from './drivers/memory';
import { reflector } from './metadata'; import { reflector } from './metadata';
import { instanceToPlain, plainToInstance } from 'class-transformer'; import { instanceToPlain, plainToInstance } from 'class-transformer';
import { encode, decode } from 'encoded-buffer'; import { encode, decode } from 'encoded-buffer';
import { Awaitable } from 'cosmokit';
import { makeArray, MayBeArray } from './utility/utility'; import { makeArray, MayBeArray } from './utility/utility';
import _ from 'lodash';
export class Aragami { export class Aragami {
readonly driver: BaseDriver; readonly driver: BaseDriver;
...@@ -42,9 +43,7 @@ export class Aragami { ...@@ -42,9 +43,7 @@ export class Aragami {
} }
private getTTL(o: any) { private getTTL(o: any) {
return ( return reflector.get('AragamiCacheTTL', o) ?? this.options.defaultTTL ?? 0;
reflector.get('AragamiCacheTTL', o) || this.options.defaultTTL || 1000
);
} }
private encode(o: any) { private encode(o: any) {
...@@ -75,7 +74,7 @@ export class Aragami { ...@@ -75,7 +74,7 @@ export class Aragami {
this.getBaseKey(options.prototype || o), this.getBaseKey(options.prototype || o),
options.key || (await this.getKey(o)), options.key || (await this.getKey(o)),
buf, buf,
options.ttl || this.getTTL(options.prototype || o), options.ttl ?? this.getTTL(options.prototype || o),
); );
return o; return o;
} }
...@@ -118,12 +117,15 @@ export class Aragami { ...@@ -118,12 +117,15 @@ export class Aragami {
wrap<T, A extends any[]>( wrap<T, A extends any[]>(
cl: ClassType<T>, cl: ClassType<T>,
cb: (...args: A) => T | Promise<T>, cb: (...args: A) => Awaitable<T>,
keySource: (...args: A) => Awaitable<string | T>, keySource: (...args: A) => Awaitable<string | T>,
) { ) {
return async (...args: A): Promise<T> => { return async (...args: A): Promise<T> => {
const keyMeta = await keySource(...args); const keyMeta = await keySource(...args);
const key = await this.getKey(keyMeta); const key = await this.getKey(keyMeta);
if (!key) {
return cb(...args);
}
const cachedValue = await this.get(cl, key); const cachedValue = await this.get(cl, key);
if (cachedValue) { if (cachedValue) {
return cachedValue; return cachedValue;
...@@ -142,9 +144,10 @@ export class Aragami { ...@@ -142,9 +144,10 @@ export class Aragami {
} }
const baseKey = this.getBaseKey(o); const baseKey = this.getBaseKey(o);
const keyTransformers = reflector.getArray('AragamiLockKeys', o); const keyTransformers = reflector.getArray('AragamiLockKeys', o);
return Promise.all( const actualKeys = await Promise.all(keyTransformers.map((fn) => fn(o)));
keyTransformers.map( return _.compact(
async (keyTransformer) => `${baseKey}:${await keyTransformer(o)}`, actualKeys.flatMap((mayBeKeyArray) =>
makeArray(mayBeKeyArray).map((key) => `${baseKey}:${key}`),
), ),
); );
} }
......
import { Metadata } from './metadata'; 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); Metadata.set('AragamiCacheTTL', ttl);
export const CachePrefix = (prefix: string): ClassDecorator => export const CachePrefix = (prefix: string): ClassDecorator =>
Metadata.set('AragamiCachePrefix', prefix); 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 = const DrainKey =
(decoratorFactory: (cb: ObjectKeyFunction) => ClassDecorator) => <T>(
decoratorFactory: (cb: ObjectFunction<T>) => ClassDecorator,
defaultValue: ObjectAndKeyFunction<T>,
) =>
( (
fun: ObjectKeyFunction = (obj) => obj.toString(), fun: ObjectAndKeyFunction<T> = defaultValue,
): PropertyDecorator & TypedMethodDecorator<ObjectKeyFunction> => ): PropertyDecorator & TypedMethodDecorator<ObjectFunction<T>> =>
(obj, key, des?) => { (obj, key, des?) => {
let cb: ObjectKeyFunction; let cb: ObjectFunction<T>;
if (des) { if (des) {
// method decorator // 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 { } else {
// property decorator // property decorator
cb = (o) => fun(o[key]); cb = (o) => fun(o[key], key);
} }
return decoratorFactory(cb)(obj.constructor); return decoratorFactory(cb)(obj.constructor);
}; };
export const CacheKey = DrainKey((cb) => Metadata.set('AragamiCacheKey', cb)); export const CacheKey = DrainKey<string>(
export const LockKey = DrainKey((cb) => Metadata.append('AragamiLockKeys', cb)); (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>( ...@@ -21,3 +21,4 @@ export type TypedMethodDecorator<F extends Function> = <T extends F>(
export type AnyClass = new (...args: any[]) => any; export type AnyClass = new (...args: any[]) => any;
export type ClassType<T> = new (...args: any[]) => T; export type ClassType<T> = new (...args: any[]) => T;
export type Awaitable<T> = T | Promise<T>;
...@@ -11,7 +11,7 @@ export class MemoryDriver extends BaseDriver { ...@@ -11,7 +11,7 @@ export class MemoryDriver extends BaseDriver {
this.cacheMap.set( this.cacheMap.set(
baseKey, baseKey,
new LRUCache({ new LRUCache({
ttl: 1000, ttl: 1,
updateAgeOnGet: false, updateAgeOnGet: false,
updateAgeOnHas: false, updateAgeOnHas: false,
}), }),
......
...@@ -26,7 +26,12 @@ export class RedisDriver extends BaseDriver { ...@@ -26,7 +26,12 @@ export class RedisDriver extends BaseDriver {
value: Buffer, value: Buffer,
ttl: number, ttl: number,
): Promise<void> { ): 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> { override async del(baseKey: string, key: string): Promise<boolean> {
......
import { Awaitable } from 'cosmokit';
import { MetadataSetter, Reflector } from 'typed-reflector'; import { MetadataSetter, Reflector } from 'typed-reflector';
import { MayBeArray } from './utility/utility';
import { Awaitable } from './def';
export class MetadataMap { interface MetadataMap {
AragamiCacheTTL: number; AragamiCacheTTL: number;
AragamiCachePrefix: string; AragamiCachePrefix: string;
AragamiCacheKey: (obj: any) => Awaitable<string>; AragamiCacheKey: (obj: any) => Awaitable<string>;
} }
export class MetadataArrayMap { interface MetadataArrayMap {
AragamiLockKeys: (obj: any) => Awaitable<string>; 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>(); 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 { 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.', () => { describe('Aragami.', () => {
let aragami: Aragami; let aragami: Aragami;
...@@ -56,8 +57,9 @@ describe('Aragami.', () => { ...@@ -56,8 +57,9 @@ describe('Aragami.', () => {
}, },
(name) => name, (name) => name,
); );
await expect(wrapped('John', 30)).resolves.toBeInstanceOf(User); await expect(wrapped('Sarah', 40)).resolves.toBeInstanceOf(User);
await expect(wrapped('John', 30)).resolves.toBeInstanceOf(User); await expect(aragami.has(User, 'Sarah')).resolves.toBeTruthy();
await expect(wrapped('Sarah', 40)).resolves.toBeInstanceOf(User);
}); });
it('should expire', async () => { it('should expire', async () => {
...@@ -72,7 +74,9 @@ describe('Aragami.', () => { ...@@ -72,7 +74,9 @@ describe('Aragami.', () => {
dress.name = 'Yuzu'; dress.name = 'Yuzu';
await aragami.set(dress); await aragami.set(dress);
await expect(aragami.has(dress)).resolves.toBeTruthy(); 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(); await expect(aragami.has(dress)).resolves.toBeFalsy();
}); });
...@@ -86,4 +90,49 @@ describe('Aragami.', () => { ...@@ -86,4 +90,49 @@ describe('Aragami.', () => {
); );
await expect(fun('foo', 'bar')).resolves.toEqual('foo.bar'); 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