mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Create a CachedHandler that caches AsyncHandler results
This commit is contained in:
parent
df2f69f532
commit
be7af277bb
@ -434,6 +434,7 @@ export * from './util/errors/UnsupportedMediaTypeHttpError';
|
||||
export * from './util/handlers/ArrayUnionHandler';
|
||||
export * from './util/handlers/AsyncHandler';
|
||||
export * from './util/handlers/BooleanHandler';
|
||||
export * from './util/handlers/CachedHandler';
|
||||
export * from './util/handlers/ConditionalHandler';
|
||||
export * from './util/handlers/HandlerUtil';
|
||||
export * from './util/handlers/MethodFilterHandler';
|
||||
|
52
src/util/handlers/CachedHandler.ts
Normal file
52
src/util/handlers/CachedHandler.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { AsyncHandler } from './AsyncHandler';
|
||||
|
||||
/**
|
||||
* Caches output data from the source handler based on the input object.
|
||||
* The `field` parameter can be used to instead use a specific entry from the input object as cache key,
|
||||
* so has as actual required typing `keyof TIn`.
|
||||
*
|
||||
* A {@link WeakMap} is used internally so strict object equality determines cache hits,
|
||||
* and data will be removed once the key stops existing.
|
||||
* This also means that the cache key needs to be an object.
|
||||
* Errors will be thrown in case a primitve is used.
|
||||
*/
|
||||
export class CachedHandler<TIn, TOut = void> extends AsyncHandler<TIn, TOut> {
|
||||
private readonly source: AsyncHandler<TIn, TOut>;
|
||||
private readonly field?: string;
|
||||
|
||||
private readonly cache: WeakMap<any, TOut>;
|
||||
|
||||
public constructor(source: AsyncHandler<TIn, TOut>, field?: string) {
|
||||
super();
|
||||
this.source = source;
|
||||
this.field = field;
|
||||
this.cache = new WeakMap();
|
||||
}
|
||||
|
||||
public async canHandle(input: TIn): Promise<void> {
|
||||
const key = this.getKey(input);
|
||||
|
||||
if (this.cache.has(key)) {
|
||||
return;
|
||||
}
|
||||
return this.source.canHandle(input);
|
||||
}
|
||||
|
||||
public async handle(input: TIn): Promise<TOut> {
|
||||
const key = this.getKey(input);
|
||||
|
||||
let result = this.cache.get(key);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
result = await this.source.handle(input);
|
||||
this.cache.set(key, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected getKey(input: TIn): any {
|
||||
return this.field ? input[this.field as keyof TIn] : input;
|
||||
}
|
||||
}
|
62
test/unit/util/handlers/CachedHandler.test.ts
Normal file
62
test/unit/util/handlers/CachedHandler.test.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import type { AsyncHandler } from '../../../../src/util/handlers/AsyncHandler';
|
||||
import {
|
||||
CachedHandler,
|
||||
} from '../../../../src/util/handlers/CachedHandler';
|
||||
|
||||
describe('A CachedHandler', (): void => {
|
||||
const input = { entry: { key: 'value' }};
|
||||
const output = 'response';
|
||||
let source: jest.Mocked<AsyncHandler<{ entry: { key: string }}, string>>;
|
||||
let handler: CachedHandler<{ entry: { key: string }}, string>;
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
source = {
|
||||
canHandle: jest.fn(),
|
||||
handle: jest.fn().mockResolvedValue(output),
|
||||
} as any;
|
||||
handler = new CachedHandler(source);
|
||||
});
|
||||
|
||||
it('can handle input if its source can.', async(): Promise<void> => {
|
||||
await expect(handler.canHandle(input)).resolves.toBeUndefined();
|
||||
expect(source.canHandle).toHaveBeenCalledTimes(1);
|
||||
source.canHandle.mockRejectedValue(new Error('bad input'));
|
||||
await expect(handler.canHandle(input)).rejects.toThrow('bad input');
|
||||
expect(source.canHandle).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('returns the source result.', async(): Promise<void> => {
|
||||
await expect(handler.handle(input)).resolves.toBe(output);
|
||||
expect(source.handle).toHaveBeenCalledTimes(1);
|
||||
expect(source.handle).toHaveBeenLastCalledWith(input);
|
||||
});
|
||||
|
||||
it('caches the result.', async(): Promise<void> => {
|
||||
await expect(handler.handle(input)).resolves.toBe(output);
|
||||
await expect(handler.handle(input)).resolves.toBe(output);
|
||||
expect(source.handle).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('caches on the object itself.', async(): Promise<void> => {
|
||||
const copy = { ...input };
|
||||
await expect(handler.handle(input)).resolves.toBe(output);
|
||||
await expect(handler.handle(copy)).resolves.toBe(output);
|
||||
expect(source.handle).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('can handle the input if it has a cached result.', async(): Promise<void> => {
|
||||
await expect(handler.handle(input)).resolves.toBe(output);
|
||||
source.canHandle.mockRejectedValue(new Error('bad input'));
|
||||
await expect(handler.canHandle(input)).resolves.toBeUndefined();
|
||||
expect(source.canHandle).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('can use a specific field of the input as key.', async(): Promise<void> => {
|
||||
handler = new CachedHandler(source, 'entry');
|
||||
|
||||
const copy = { ...input };
|
||||
await expect(handler.handle(input)).resolves.toBe(output);
|
||||
await expect(handler.handle(copy)).resolves.toBe(output);
|
||||
expect(source.handle).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user