Skip to content

Instantly share code, notes, and snippets.

@Jordan-Hall
Last active December 28, 2021 22:46
Show Gist options
  • Save Jordan-Hall/7ae5d0e02764c50e6a7fc7350b370c6b to your computer and use it in GitHub Desktop.
Save Jordan-Hall/7ae5d0e02764c50e6a7fc7350b370c6b to your computer and use it in GitHub Desktop.
Memoize Decorator
interface MemoizeDecoratorOptions {
type: Storage;
ttl: number;
}
/// <reference path="../@types/main.d.ts" />
export abstract class MemoizeProvider implements IMemoizeProvider {
constructor(protected options: CacheOptions) {}
abstract getCached(key: string);
abstract setCached(key: string, value: any);
abstract isExpired(key: string): boolean;
abstract hasCache(key: string): boolean;
}
/// <reference path="./@types/main.d.ts" />
import { MemoizeProvider } from './provider/memoize-provider';
export class MemoizeService {
constructor(private provider: MemoizeProvider) { }
forMethod(method: Function, args: any[]) {
const argsString = JSON.stringify(args);
if (!this.provider.hasCache(argsString) || this.provider.isExpired(argsString)) {
const result = method.call(this, ...args);
this.provider.setCached(argsString, result);
return result;
} else {
return this.provider.getCached(argsString);
}
}
getValue(key: string) {
if(this.provider.isExpired(key)) {
return;
}
return this.provider.getCached(key);
}
setValue(key: string, value: any) {
this.provider.setCached(key, value);
}
}
/// <reference path="../@types/main.d.ts" />
import { StorageProvider } from '../provider/storage-provider';
import { MemoizeService } from '../memoize-service';
export function Memoize(_options: MemoizeDecoratorOptions = { type: window.sessionStorage, ttl: 0 }): MethodDecorator {
const provider: StorageProvider = new StorageProvider({
ttl: _options.ttl,
type: _options.type
});
const memoize: MemoizeService = new MemoizeService(provider);
return (target: object, method: string, descriptor: PropertyDescriptor): PropertyDescriptor => {
if (descriptor.hasOwnProperty('get') && descriptor.get) {
descriptor.get = function (this: Function, ...args: any[]): any {
return memoize.getValue(JSON.stringify(args));
}
const originalSet: Function = descriptor.set.bind(target);
descriptor.set = function (this: Function, ...args: any[]): void {
originalSet.call(this, ...args)
return memoize.setValue(JSON.stringify([]), args[0]);
}
} else if (!descriptor.hasOwnProperty('set') && descriptor.value) {
const originalMethod = descriptor.value.bind(target)
descriptor.value = function (this: Function, ...args: any[]): any {
return memoize.forMethod(originalMethod, (args))
}
} else {
throw new Error('Can\'t set cache decorator on a setter');
}
return descriptor;
}
}
/// <reference path="../@types/main.d.ts" />
import * as moment from 'moment';
import { MemoizeProvider } from './memoize-provider';
enum CacheReturnType {
STATIC = 1,
PROMISE,
MOMENT,
DATE,
}
const __STORAGE_KEY__ = "MEMORIZE";
export class StorageProvider extends MemoizeProvider {
static InstanceNumber = 0;
private storageKey: string;
private storage: Storage;
private memory: StorageObject;
constructor(options: CacheOptions) {
super(options);
StorageProvider.InstanceNumber++
this.storage = options.type;
this.storageKey = `${__STORAGE_KEY__}-inst-${StorageProvider.InstanceNumber}`;
this.memory = this.parseSafe(this.storageKey);
}
private parseSafe(t: any): any {
const memory = {
items: {},
ttl: {}
}
try {
return JSON.parse(t) || memory;
// tslint:disable-next-line:no-unused
} catch (err) {
return memory
}
}
private getCacheType(value): CacheReturnType {
if (Object.prototype.toString.call(value) === "[object Promise]") {
return CacheReturnType.PROMISE;
} else if (value instanceof moment) {
return CacheReturnType.MOMENT;
} else if (typeof value.getMonth === 'function' || value instanceof Date) {
return CacheReturnType.DATE;
}
return CacheReturnType.STATIC;
}
private convertToType(value: any) {
switch (this.memory.returnType) {
case CacheReturnType.PROMISE:
return Promise.resolve(value);
case CacheReturnType.MOMENT:
return moment(value);
case CacheReturnType.DATE:
return new Date(value);
case CacheReturnType.STATIC:
default:
return value;
}
}
public getCached(key: string): any {
let item = null;
if (this.memory.items.hasOwnProperty(key)) {
item = this.memory.items[key];
}
return this.convertToType(item);
}
public async setCached(key: string, value: any): Promise<void> {
this.memory.returnType = this.getCacheType(value);
if (this.memory.returnType === CacheReturnType.PROMISE) {
this.memory.items = { ...this.memory.items, [key]: await Promise.resolve(value) };
} else {
this.memory.items = { ...this.memory.items, [key]: value };
}
if (this.options.ttl) {
this.memory.ttl = { ...this.memory.ttl, [key]: Date.now() + this.options.ttl };
}
this.storage.setItem(this.storageKey, JSON.stringify(this.memory));
}
public hasCache(key: string): boolean {
return (this.memory.items.hasOwnProperty(key));
}
public isExpired(key: string): boolean {
if (!this.memory.ttl.hasOwnProperty(key)) {
return false;
}
return this.memory.ttl[key] < Date.now();
}
}
type StorageObject = {
items: { [args: string]: any };
ttl: { [args: string]: number };
returnType?: CacheReturnType;
}
type CacheOptions = {
type: Storage;
ttl: number;
}
interface IMemoizeProvider {
getCached(key: string): any;
setCached(key: string, value: any): Promise<void> | void;
isExpired(key: string): boolean;
hasCache(key: string): boolean;
}
import { Memoize } from '../decorator/memoized-decorator';
class TestMethodBaseClass {
add(numOne: number, numTwo: number) {
return numOne + numTwo;
}
}
class TestMethodClass extends TestMethodBaseClass {
add(numOne: number, numTwo: number) {
return super.add(numOne, numTwo)
}
}
class TestMethodMemoizeClass extends TestMethodBaseClass {
@Memoize()
add(numOne: number, numTwo: number) {
return super.add(numOne, numTwo)
}
}
describe('Memoized class method', () => {
let testMethodMemoizeClass: TestMethodMemoizeClass;
let testMethodClass: TestMethodClass;
beforeEach(() => {
sessionStorage.clear();
testMethodMemoizeClass = new TestMethodMemoizeClass();
testMethodClass = new TestMethodClass();
});
afterEach(() => {
window.sessionStorage.clear();
})
it('Ensure classes is working as expected', () => {
expect(testMethodClass.add(2,3)).toBe(5);
expect(testMethodMemoizeClass.add(2,3)).toBe(5);
});
it('ensure none memoized class dont use session storage', () => {
const spyStorageGetItem = spyOn(window.sessionStorage, 'getItem');
const spyStorageSetItem = spyOn(window.sessionStorage, 'setItem');
spyStorageGetItem.and.callFake((key) => {});
spyStorageSetItem.and.callFake((key, value) => {});
expect(testMethodClass.add(4,1)).toBe(5);
expect(spyStorageGetItem).not.toHaveBeenCalled();
expect(spyStorageSetItem).not.toHaveBeenCalled();
});
it('ensure memoized class have used session storage', () => {
const spyStorageSetItem = spyOn(window.sessionStorage, 'setItem');
spyStorageSetItem.and.callFake((key, value) => {});
expect(testMethodMemoizeClass.add(2,6)).toBe(8);
expect(spyStorageSetItem).toHaveBeenCalled();
});
it('ensure memoized class have used session storage twice with different params', () => {
const spyStorageSetItem = spyOn(window.sessionStorage, 'setItem');
spyStorageSetItem.and.callFake((key, value) => {});
expect(testMethodMemoizeClass.add(3,2)).toBe(5);
expect(testMethodMemoizeClass.add(3,7)).toBe(10);
expect(spyStorageSetItem).toHaveBeenCalledTimes(2);
});
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment