Skip to content

Commit

Permalink
Merge bd10778 into 032e75a
Browse files Browse the repository at this point in the history
  • Loading branch information
Alorel committed Dec 8, 2023
2 parents 032e75a + bd10778 commit 98b1a50
Show file tree
Hide file tree
Showing 6 changed files with 773 additions and 375 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@ module.exports = {
'max-classes-per-file': 'off',
'no-duplicate-imports': 'off',
'consistent-return': 'off',
'prefer-rest-params': 'off',
}
};
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ An ES7 decorator for memoising (caching) a method's response

# Compatibility

The library's only goal is to be compatible with Typescript 5 decorators which, at the time of writing, use the [2022-03 stage 3 decorators proposal](https://2ality.com/2022/10/javascript-decorators.html).
The library's only goal is to be compatible with Typescript 5 decorators which, at the time of writing, use the [2022-03 stage 3 decorators proposal](https://2ality.com/2022/10/javascript-decorators.html). The bulk of your inputs' validation is offloaded to Typescript as well.

# Usage
## Basic Usage
Expand Down
78 changes: 78 additions & 0 deletions src/cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import type {SerialiserFn} from './core';

type Fn<T, A extends any[], R> = (this: T, ...args: A) => R;

/** @see {MEMOISE_CACHE} */
export interface Cache<K = any> {

/** Clear the cache */
clear(): void;

/**
* Delete a specific cache entry.
* @param key The result of passing the method call args through the associated {@link SerialiserFn serialiser fn}
*/
delete(key: K): boolean;

/**
* Check if a specific cache entry exists.
* @param key See {@link Cache#delete delete()}
*/
has(key: K): boolean;
}

/** @internal */
export class ArglessCtx<T, R> implements Cache {
public _f = true;

public _r!: R;

public constructor(private readonly _o: Fn<T, [], R>) { // eslint-disable-line no-empty-function
}

public autoGet(ctx: T): R {
if (this._f) {
this._r = this._o.call(ctx);
this._f = false;
}

return this._r;
}

public clear(): void {
this._f = true;
}

public delete(): boolean {
const ret = this.has();
this.clear();

return ret;
}

public has(): boolean {
return !this._f;
}
}

/** @internal */
export class ArgedCtx<T, A extends any[], R, K> extends Map<K, R> implements Cache<K> {
public constructor(
private readonly _o: Fn<T, A, R>,
private readonly _s: SerialiserFn<T, A, K>
) {
super();
}

public autoGet(ctx: T, args: A | IArguments): R {
const key = this._s.apply(ctx, args as A);
if (this.has(key)) {
return this.get(key)!;
}

const value = this._o.apply(ctx, args as A);
this.set(key, value);

return value;
}
}
196 changes: 88 additions & 108 deletions src/core.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,5 @@
/** @internal */
function namedFn<F extends Function>(name: string, fn: F): F {
Object.defineProperty(fn, 'name', {
configurable: true,
enumerable: true,
value: name,
writable: true
});

return fn;
}

/** @internal */
function applyRename<F extends Function>(origFn: F, label: string, newFn: F): F {
return namedFn(`${label}(${origFn.name})`, newFn);
}
import {ArgedCtx, ArglessCtx} from './cache';
import type {Cache} from './cache';

/**
* Cache associated with this function/method if it's been processed with one of:
Expand All @@ -30,36 +16,50 @@ export const MEMOISE_CACHE: unique symbol = Symbol('Memoise cache');
* uniquely identifiable inside {@link Map}s. Called with the class instance (or the class itself for static methods)
* as the thisArg and forwards the method call arguments.
*/
export type SerialiserFn<T, A extends any[]> = (this: T, ...args: A) => any;
export type SerialiserFn<T, A extends any[], K = any> = (this: T, ...args: A) => K;

type Fn<T, A extends any[], R> = (this: T, ...args: A) => R;
export type Decorator<T, A extends any[], R> = (
target: any,
ctx: ClassMethodDecoratorContext<T, Fn<T, A, R>>
) => undefined | Fn<T, A, R>;

/** @see {MEMOISE_CACHE} */
export interface Cache {
/** The default cache key {@link SerialiserFn serialiser}. */
export function defaultSerialiser(...args: any[]): string {
return JSON.stringify(args);
}

/** @internal */
export function identitySerialiser<T>(value: T): T {
return value;
}

interface Memoised<T, A extends any[], R> extends Fn<T, A, R> {
[MEMOISE_CACHE]: Cache;
}

/** Clear the cache */
clear(): void;
/** @internal */
function namedFn<F extends Function>(name: string, fn: F): F {
Object.defineProperty(fn, 'name', {
configurable: true,
enumerable: true,
value: name,
writable: true
});

/**
* Delete a specific cache entry.
* @param key The result of passing the method call args through the associated {@link SerialiserFn serialiser fn}
*/
delete(key: any): boolean;
return fn;
}

/**
* Check if a specific cache entry exists.
* @param key See {@link Cache#delete delete()}
*/
has(key: any): boolean;
/** @internal */
function applyRename<F extends Function>(origFn: F, label: string, newFn: F): F {
return namedFn(`${label}(${origFn.name})`, newFn);
}

/** The default cache key {@link SerialiserFn serialiser}. */
export function defaultSerialiser(...args: any[]): string {
return JSON.stringify(args);
function setCache(fn: Function, cache: Cache): void {
Object.defineProperty(fn, MEMOISE_CACHE, {
configurable: true,
value: cache
});
}

/**
Expand All @@ -70,106 +70,86 @@ export function defaultSerialiser(...args: any[]): string {
export function memoiseFunction<T, A extends [any, ...any[]], R>(
fn: Fn<T, A, R>,
serialiser: SerialiserFn<T, A> = defaultSerialiser
): Fn<T, A, R> {
const cache = new Map<any, R>();
): Memoised<T, A, R> {
const ctx = new ArgedCtx(fn, serialiser);

const memoisedFunction = applyRename(fn, 'Memoised', function (this: T, ...args: A): R {
const cacheKey = serialiser.apply(this, args);
if (cache.has(cacheKey)) {
return cache.get(cacheKey)!;
}

const returnValue = fn.apply(this, args);
cache.set(cacheKey, returnValue);

return returnValue;
const memoisedFunction: Fn<T, A, R> = applyRename(fn, 'Memoised', function (this: T): R {
return ctx.autoGet(this, arguments);
});

Object.defineProperty(memoisedFunction, MEMOISE_CACHE, {value: cache});
setCache(memoisedFunction, ctx);

return memoisedFunction;
return memoisedFunction as Memoised<T, A, R>;
}

/**
* Memoise the function's return value disregarding call arguments,
* effectively turning it into a lazily-evaluated value
* @param fn The function to memoise
*/
export function memoiseArglessFunction<T, R>(fn: Fn<T, [], R>): Fn<T, [], R> {
let firstCall = true;
let returnValue: R;

const memoisedFunction = applyRename(fn, 'MemoisedArgless', function (this: T): R {
if (firstCall) {
returnValue = fn.call(this);
firstCall = false;
}
export function memoiseArglessFunction<T, R>(fn: Fn<T, [], R>): Memoised<T, [], R> {
const ctx = new ArglessCtx(fn);

return returnValue;
const memoisedFunction: Fn<T, [], R> = applyRename(fn, 'MemoisedArgless', function (this: T): R {
return ctx.autoGet(this);
});

function clear(): void {
firstCall = true;
}

Object.defineProperty(memoisedFunction, MEMOISE_CACHE, {
value: {
clear,
delete() {
const wasFirstCall = firstCall;
clear();

return wasFirstCall !== firstCall;
},
has: () => !firstCall
} satisfies Cache
});
setCache(memoisedFunction, ctx);

return memoisedFunction as Fn<T, [], R>;
return memoisedFunction as Fn<T, [], R> as Memoised<T, [], R>;
}

const MARKER: unique symbol = Symbol('Memoised');

/** @internal */
function applyDecorator<T, A extends any[], R>(
decoratorFactory: (fn: Fn<T, A, R>, serialiser: SerialiserFn<T, A>) => Fn<T, A, R>,
serialiser?: SerialiserFn<T, A>
): Decorator<T, A, R>;
export function applyDecorator<T, R>(hasArgs: false): Decorator<T, [], R>;

/** @internal */
function applyDecorator<T, R>(decoratorFactory: (fn: Fn<T, [], R>) => Fn<T, [], R>): Decorator<T, [], R>;
export function applyDecorator<T, A extends [any, ...any[]], R, K = any>(
hasArgs: true,
serialiser: SerialiserFn<T, A, K>
): Decorator<T, A, R>;

/** @internal */
function applyDecorator<T, A extends any[], R>(
decoratorFactory: (fn: Fn<T, A, R>, ...args: any[]) => Fn<T, A, R>,
serialiser?: SerialiserFn<T, A>
export function applyDecorator<T, A extends any[], R, K = any>(
hasArgs: boolean,
serialiser?: SerialiserFn<T, A, K>
): Decorator<T, A, R> {
return function decorate(origFn, {name, static: isStatic, private: isPrivate, addInitializer}) {
if (isStatic) {
return decoratorFactory(origFn, serialiser);
} else if (isPrivate) {
throw new Error('Can\'t memoise private instance methods');
}

Object.defineProperty(origFn, MARKER, {configurable: true, value: true});
return function decorateWithMemoise(origFn, {
addInitializer,
name,
static: isStatic,
access: {get}
}) {
type CtxArged = ArgedCtx<T, A, R, K>;
type CtxArgless = ArglessCtx<T, R>;
type Ctx = CtxArged | CtxArgless;

const sName = String(name);
const ctxFactory: () => Ctx = hasArgs
? (() => new ArgedCtx<T, A, R, K>(origFn, serialiser!))
: (() => new ArglessCtx<T, R>(origFn));

if (isStatic) { // private/public static
const ctx = ctxFactory();

const outFn = applyRename(origFn, 'Memoised', function (this: T): R {
return ctx.autoGet(this, arguments);
});
setCache(outFn, ctx);

addInitializer(namedFn(`MemoiseInit(${String(name)})`, function () {
if (this[name as keyof T] !== origFn && !(this[name as keyof T] as any)?.[MARKER]) {
throw new Error(`The \`${String(name)}\` method is decorated with @Memoise or @MemoiseAll in the superclass; decorated instance methods cannot use inheritance unless the subclass method is decorated with @Memoise or @MemoiseAll as well.`);
}
return outFn;
}
const marker: unique symbol = Symbol(`Memoised value (${sName})`);
type Contextual = T & { [marker]: Ctx };

const value = decoratorFactory(origFn, serialiser);
Object.defineProperty(value, MARKER, {configurable: true, value: true});
addInitializer(applyRename(origFn, `MemoiseInit(${sName})`, function (this: T) {
const ctx = ctxFactory();
Object.defineProperty(this, marker, {value: ctx});
setCache(get(this), ctx);
}));

Object.defineProperty(this, name, {
configurable: true,
enumerable: true,
value,
writable: true
return applyRename(origFn, 'Memoised', function (this: T): R {
return (this as Contextual)[marker].autoGet(this, arguments);
});
}));

};
}

/** @internal */
export {applyDecorator};

0 comments on commit 98b1a50

Please sign in to comment.