Skip to content

Commit

Permalink
Merge pull request #17 from SaurusXI/feat/add-batched-ops
Browse files Browse the repository at this point in the history
feat: migrate to typescript 5 decorators
  • Loading branch information
SaurusXI authored Dec 8, 2023
2 parents 3bfca0b + 8112469 commit c0dee83
Show file tree
Hide file tree
Showing 9 changed files with 200 additions and 147 deletions.
3 changes: 2 additions & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
tests
dist
dist
types
197 changes: 120 additions & 77 deletions lib/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,34 +10,32 @@ import { dummyLogger, Logger } from './types/logging';
import {
MemoizeParams,
CreateCacheOptions,
InvalidateMemoizedParams,
VariablesByKeys,
TTL,
CachewiseTTL,
KeysObject,
UpdateMemoizedParams,
} from './types';

type KeysObject<KeyName extends string> = {
[_Property in KeyName]: string
};
import { DecoratedMethod } from './types/internals';

export default class SugarCache<
KeyName extends string,
const KeyNames extends readonly string[],
KeyName extends string = KeyNames[number],
Keys extends KeysObject<KeyName> = KeysObject<KeyName>,
> {
private namespace: string;

private cache: MultilevelCache;

private readonly logger: Logger;

private hashtags: Set<KeyName>;

private keyNames: KeyNames;

constructor(
redis: Redis | Cluster,
options: CreateCacheOptions<KeyName>,
logger: Logger = dummyLogger,
options: CreateCacheOptions<KeyNames>,
readonly logger: Logger = dummyLogger,
) {
this.logger = logger;
this.keyNames = options.keys;
this.cache = new MultilevelCache(options, redis, logger);
this.namespace = this.cache.namespace;

Expand All @@ -51,13 +49,13 @@ export default class SugarCache<
}
}

private validateKeys = (targetFn: any, cacheKeys: string[]) => {
private validateKeys = (targetFn: any, fnName: string) => {
const params = readFunctionParams(targetFn);
const invalidKeys = cacheKeys.filter((k) => !params.includes(k));
const missingKeys = this.keyNames.filter((k) => !(params.includes(k)));

if (invalidKeys.length) {
this.logger.debug(`[SugarCache:${this.namespace}] Function params - ${JSON.stringify(params)}, cacheKeys - ${JSON.stringify(cacheKeys)}, invalid keys - ${JSON.stringify(invalidKeys)}`);
throw new Error('[SugarCache] Keys passed to decorator do not match function params');
if (missingKeys.length) {
this.logger.debug(`[SugarCache:${this.namespace}] Function params - ${JSON.stringify(params)}, cacheKeys - ${JSON.stringify(this.keyNames)}, missing keys - ${JSON.stringify(missingKeys)}`);
throw new Error(`[SugarCache:${this.namespace}] Keys passed to decorator for function "${fnName}" do not match function params. Args passed- ${JSON.stringify(params)}. Required keys not found- ${JSON.stringify(missingKeys)}`);
}
};

Expand Down Expand Up @@ -184,58 +182,62 @@ export default class SugarCache<

// ----------- Decorator Methods -----------

// eslint-disable-next-line class-methods-use-this
private reduceVarsByKeysToKeys(keyVariables: VariablesByKeys<Keys>, namedArgs: any) {
private extractKeysFromFunc(namedArgs: any) {
const out = {} as Keys;

if (Object.keys(keyVariables).length > Object.keys(namedArgs).length) {
throw new Error('Invalid arguments passed to function');
}

Object.keys(keyVariables).forEach((keyName) => {
const variableName = keyVariables[keyName] as string;
const variableValue = namedArgs[variableName];
this.keyNames.forEach((keyName) => {
const variableValue = namedArgs[keyName];
if (variableValue === undefined) {
throw new Error('Invalid arguments passed to function');
throw new Error(`Invalid arguments passed to function- variable ${keyName} is required`);
}

out[keyName] = variableName;
out[keyName] = variableValue;
});

return out;
}

private getKeysFromFunc(
getKeysFromFunc(
args: IArguments,
originalFn: any,
variablesByKeys: VariablesByKeys<Keys>,
) {
const namedArguments = SugarCache.transformIntoNamedArgs(args, originalFn);
return this.reduceVarsByKeysToKeys(
variablesByKeys,
return this.extractKeysFromFunc(
namedArguments,
);
}

private static ORIGINAL_FN_PROPKEY = 'sugarcache-originalFn';

/**
* Decorator to read a value from cache if it exists
* If it doesn't the target function is called and the return value is set on cache
*/
public memoize(
params: MemoizeParams<Keys>,
) {
memoize<TThis, TArgs extends any[], TReturn>(
params: MemoizeParams,
): DecoratedMethod<TThis, TArgs, TReturn> {
const cacheInstance = this;
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor): any {
const originalFn = descriptor.value.originalFn || descriptor.value;
const currentFn = descriptor.value;

const { argnamesByKeys: keyVariables, ttl } = params;

cacheInstance.validateKeys(originalFn, Object.values(keyVariables));

// eslint-disable-next-line no-param-reassign
descriptor.value = async function () {
const keys = cacheInstance.getKeysFromFunc(arguments, originalFn, keyVariables);
return (
target: (_this: TThis, ..._args: TArgs) => TReturn,
context: ClassMethodDecoratorContext<TThis, (_this: TThis, ..._args: TArgs) => any>,
) => {
const originalFn = Object.getOwnPropertyDescriptor(
target,
SugarCache.ORIGINAL_FN_PROPKEY,
)?.value || target;
const currentFn = target;

const { ttl } = params;

// NOTE(Shantanu)
// Currently it is not possible to make the type system aware of
// function parameter names, so we can't throw type errors if keyNames are missing
// from function params. Current implementation only verifies at compile time
// Once https://github.com/microsoft/TypeScript/issues/44939 is resolved this can be implemented
cacheInstance.validateKeys(originalFn, context.name as string);

const out = async function (): Promise<TReturn> {
const keys = cacheInstance.getKeysFromFunc(arguments, originalFn);

const cachedResult = await cacheInstance.get(keys);
if (cachedResult !== null) {
Expand All @@ -248,28 +250,42 @@ export default class SugarCache<

return result;
};
descriptor.value.originalFn = originalFn;

// Hack to make decorator composable
Object.defineProperty(out, SugarCache.ORIGINAL_FN_PROPKEY, {
value: originalFn,
});

return out;
};
}

/**
* Decorator to remove memoized result at a key (computed from function args at runtime)
* from cache.
*/
public invalidateMemoized(
params: InvalidateMemoizedParams<Keys>,
) {
public invalidateMemoized<TThis, TArgs extends any[], TReturn>()
: DecoratedMethod<TThis, TArgs, TReturn> {
const cacheInstance = this;
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor): any {
const originalFn = descriptor.value.originalFn || descriptor.value;
const currentFn = descriptor.value;

const { argnamesByKeys: keyVariables } = params;

cacheInstance.validateKeys(originalFn, Object.values(keyVariables));

descriptor.value = async function () {
const keys = cacheInstance.getKeysFromFunc(arguments, originalFn, keyVariables);
return function (
target: ((_this: TThis, ..._args: TArgs) => TReturn) & { metadata?: any },
context: ClassMethodDecoratorContext<TThis, (_this: TThis, ..._args: TArgs) => any>,
) {
const originalFn = Object.getOwnPropertyDescriptor(
target,
SugarCache.ORIGINAL_FN_PROPKEY,
)?.value || target;
const currentFn = target;

// NOTE(Shantanu)
// Currently it is not possible to make the type system aware of
// function parameter names, so we can't throw type errors if keyNames are missing
// from function params. Current implementation only verifies at compile time
// Once https://github.com/microsoft/TypeScript/issues/44939 is resolved this can be implemented
cacheInstance.validateKeys(originalFn, context.name as string);

const out = async function (): Promise<TReturn> {
const keys = cacheInstance.getKeysFromFunc(arguments, originalFn);

await cacheInstance.del(keys)
.catch((err) => { throw new Error(`[SugarCache:${cacheInstance.namespace}] Unable to delete value from cache - ${err}`); });
Expand All @@ -278,7 +294,13 @@ export default class SugarCache<

return result;
};
descriptor.value.originalFn = originalFn;

// Hack to make decorator composable
Object.defineProperty(out, SugarCache.ORIGINAL_FN_PROPKEY, {
value: originalFn,
});

return out;
};
}

Expand All @@ -289,29 +311,50 @@ export default class SugarCache<
* this always executes the decorated function, whereas the latter will not
* execute if a memoized value is found.
*/
public updateMemoized(
params: MemoizeParams<Keys>,
) {
public updateMemoized<TThis, TArgs extends any[], TReturn>(
params: UpdateMemoizedParams,
): DecoratedMethod<TThis, TArgs, TReturn> {
const cacheInstance = this;
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor): any {
const originalFn = descriptor.value.originalFn || descriptor.value;
const currentFn = descriptor.value;

const { argnamesByKeys: keyVariables, ttl } = params;

cacheInstance.validateKeys(originalFn, Object.values(keyVariables));

descriptor.value = async function () {
const keys = cacheInstance.getKeysFromFunc(arguments, originalFn, keyVariables);

return function (
target: ((_this: TThis, ..._args: TArgs) => any) & { metadata?: any },
context: ClassMethodDecoratorContext<TThis, (_this: TThis, ..._args: TArgs) => any>,
) {
const originalFn = Object.getOwnPropertyDescriptor(
target,
SugarCache.ORIGINAL_FN_PROPKEY,
)?.value || target;
const currentFn = target;

const { ttl } = params;
// NOTE(Shantanu)
// Currently it is not possible to make the type system aware of
// function parameter names, so we can't throw type errors if keyNames are missing
// from function params. Current implementation only verifies at compile time
// Once https://github.com/microsoft/TypeScript/issues/44939 is resolved this can be implemented
cacheInstance.validateKeys(originalFn, context.name as string);

const out = async function (): Promise<TReturn> {
const keys = cacheInstance.getKeysFromFunc(arguments, originalFn);
const result = await currentFn.apply(this, arguments);

await cacheInstance.set(keys, result, ttl)
const value = result;
// if (accumulator) {
// const memoizedValue = await cacheInstance.get(
// keys) ?? accumulator.initialValue;
// value = accumulator.fn(memoizedValue, result);
// }
await cacheInstance.set(keys, value, ttl)
.catch((err) => { throw new Error(`[SugarCache:${cacheInstance.namespace}] Unable to set value to cache - ${err}`); });

return result;
};
descriptor.value.originalFn = originalFn;

// Hack to make decorator composable
Object.defineProperty(out, SugarCache.ORIGINAL_FN_PROPKEY, {
value: originalFn,
});

return out;
};
}
}
26 changes: 16 additions & 10 deletions lib/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import SugarCache from '../main';
import client from 'prom-client';

export type PrometheusClient = typeof client;

/**
* @param namespace Namespace of cache. All caches without this value set share a default namespace
*/
export type CreateCacheOptions<KeyName extends string> = {
export type CreateCacheOptions<
KeyNames,
KeyName extends string = KeyNames extends readonly string[] ? KeyNames[number] : never
> = {
keys: KeyNames,
namespace?: string;
inMemoryCache?: {
enable?: boolean,
Expand Down Expand Up @@ -41,21 +46,22 @@ export type CachewiseTTL = {
memory: TTL,
};

export type VariablesByKeys<T> = {
[_Property in keyof T]: string
export type KeysObject<KeyName extends string> = {
[_Property in KeyName]: string
};

export type MemoizeParams<T> = {

export type MemoizeParams = {
/**
* Object mapping cache keys to function args
*/
argnamesByKeys: VariablesByKeys<T>;
ttl: TTL | CachewiseTTL;
}

export type InvalidateMemoizedParams<T> = {
/**
* Object mapping function arguments to cache keys
*/
argnamesByKeys: VariablesByKeys<T>;
export type UpdateMemoizedParams = {
ttl: TTL | CachewiseTTL;
}

export type VariablesByKeys<T> = {
[_Property in keyof T]: string
};
7 changes: 7 additions & 0 deletions lib/types/internals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export type DecoratedMethod<TThis, TArgs extends any[], TReturn> = (
_originalMethod: Function,
_context: ClassMethodDecoratorContext<
TThis,
(_this: TThis, ..._args: TArgs) => TReturn
>
) => void;
5 changes: 4 additions & 1 deletion tests/batched.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ describe('Batched operations', () => {
host: '127.0.0.1',
});

const cache = new SugarCache<'mockKey'>(redis, { namespace: 'batched-ops' })
const cache = new SugarCache(redis, {
namespace: 'batched-ops',
keys: ['mockKey']
})

const mockCacheVals = [...Array(2).keys()].map((x) => ({ key: `foo-${x}`, val: `bar-${x}` }));

Expand Down
Loading

0 comments on commit c0dee83

Please sign in to comment.