Last active
December 28, 2021 22:46
-
-
Save Jordan-Hall/7ae5d0e02764c50e6a7fc7350b370c6b to your computer and use it in GitHub Desktop.
Memoize Decorator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
interface MemoizeDecoratorOptions { | |
type: Storage; | |
ttl: number; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// <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; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// <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); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// <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; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// <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(); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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