Commit 10aca63e authored by nanahira's avatar nanahira

add memorize

parent 7b6cae65
...@@ -8,3 +8,4 @@ export * from './src/middleware-dispatcher'; ...@@ -8,3 +8,4 @@ export * from './src/middleware-dispatcher';
export * from './src/i18n'; export * from './src/i18n';
export * from './src/patch-string-in-object'; export * from './src/patch-string-in-object';
export * from './src/observe-diff'; export * from './src/observe-diff';
export * from './src/memorize';
export const Memorize = () => {
const cache = new WeakMap<object, any>();
const isPromiseLike = (v: any): v is Promise<unknown> =>
v != null && typeof v.then === 'function' && typeof v.catch === 'function';
const getOrSet = (instance: object, compute: () => any) => {
if (cache.has(instance)) return cache.get(instance);
const result = compute();
if (isPromiseLike(result)) {
const wrapped = result.catch((err) => {
cache.delete(instance); // 下次还能重新算
throw err;
});
cache.set(instance, wrapped);
return wrapped;
}
cache.set(instance, result);
return result;
};
return function (
_target: any,
_propertyKey: string,
descriptor: PropertyDescriptor,
) {
// getter
if (typeof descriptor.get === 'function') {
const originalGetter = descriptor.get;
descriptor.get = function () {
return getOrSet(this, () => originalGetter.call(this));
};
return;
}
// method
if (typeof descriptor.value === 'function') {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
return getOrSet(this, () => originalMethod.apply(this, args));
};
return;
}
};
};
// memorize.spec.ts
import { Memorize } from '../src/memorize';
class TestMemorize {
getterCalls = 0;
methodCalls = 0;
asyncMethodCalls = 0;
asyncFailCalls = 0;
@Memorize()
get computed() {
this.getterCalls += 1;
return { value: Math.random() };
}
@Memorize()
method(): { value: number } {
this.methodCalls += 1;
return { value: Math.random() };
}
@Memorize()
async asyncMethod(): Promise<number> {
this.asyncMethodCalls += 1;
// 模拟一下真正的异步
await new Promise((resolve) => setTimeout(resolve, 10));
return Math.random();
}
@Memorize()
async asyncMethodFail(): Promise<never> {
this.asyncFailCalls += 1;
await new Promise((resolve) => setTimeout(resolve, 10));
throw new Error('boom');
}
}
describe('@Memorize()', () => {
it('should memoize getter per instance', () => {
const a = new TestMemorize();
const b = new TestMemorize();
const v1 = a.computed;
const v2 = a.computed;
const v3 = b.computed;
const v4 = b.computed;
// 同一个实例:只调用一次 getter
expect(a.getterCalls).toBe(1);
expect(v1).toBe(v2);
// 不同实例:各自一份缓存
expect(b.getterCalls).toBe(1);
expect(v3).toBe(v4);
// a / b 的值对象不一定相同引用(本来也不是要求)
expect(v1).not.toBe(v3);
});
it('should memoize sync method per instance', () => {
const a = new TestMemorize();
const b = new TestMemorize();
const r1 = a.method();
const r2 = a.method();
expect(a.methodCalls).toBe(1);
expect(r1).toBe(r2);
const r3 = b.method();
const r4 = b.method();
expect(b.methodCalls).toBe(1);
expect(r3).toBe(r4);
expect(r1).not.toBe(r3);
});
it('should memoize async method and cache the same Promise', async () => {
const a = new TestMemorize();
const p1 = a.asyncMethod();
const p2 = a.asyncMethod();
// 立即检查:底层只执行了一次
expect(a.asyncMethodCalls).toBe(1);
expect(p1).toBe(p2); // 同一条 Promise
const v1 = await p1;
const v2 = await p2;
expect(v1).toBe(v2); // 缓存的是同一个结果
expect(a.asyncMethodCalls).toBe(1); // 之后也不会再多跑
});
it('should clear cache when async method fails and re-run next call', async () => {
const a = new TestMemorize();
// 第一次调用:失败
await expect(a.asyncMethodFail()).rejects.toThrow('boom');
expect(a.asyncFailCalls).toBe(1);
// 第二次调用:因为上次失败清了缓存,会重新执行
await expect(a.asyncMethodFail()).rejects.toThrow('boom');
expect(a.asyncFailCalls).toBe(2);
});
it('should reuse cached async failure only during the same rejection (sanity check)', async () => {
const a = new TestMemorize();
// 并发调用,两次拿到同一条 Promise,底层只执行一次
const p1 = a.asyncMethodFail();
const p2 = a.asyncMethodFail();
expect(a.asyncFailCalls).toBe(1);
expect(p1).toBe(p2);
await expect(p1).rejects.toThrow('boom');
await expect(p2).rejects.toThrow('boom');
// 这时缓存因失败被清空,下一次再调会重新执行
await expect(a.asyncMethodFail()).rejects.toThrow('boom');
expect(a.asyncFailCalls).toBe(2);
});
});
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