/** * This module allows to deduplicate function calls with the same params and * cache result of function call. */ export class CachingProxy { cacheEnabled: boolean; ttl: number; cache: any; promises: any; constructor(cacheOptions) { this.cacheEnabled = cacheOptions.enabled; this.ttl = cacheOptions.ttl || 600000; // 10 minutes by default // Internal objects for data storing this.cache = {}; this.promises = {}; } /** * Check that result is present in the cache and is up to date or send request otherwise. */ cacheRequest(func, funcName, funcScope) { return cacheRequest(func, funcName, funcScope, this); } /** * Wrap request to prevent multiple calls with same params when request is waiting for response. */ proxyfy(func, funcName, funcScope) { if (!this.promises[funcName]) { this.promises[funcName] = {}; } const promiseKeeper = this.promises[funcName]; return callOnce(func, promiseKeeper, funcScope); } proxyfyWithCache(func, funcName, funcScope) { const proxyfied = this.proxyfy(func, funcName, funcScope); return this.cacheRequest(proxyfied, funcName, funcScope); } _isExpired(cacheObject) { if (cacheObject) { const object_age = Date.now() - cacheObject.timestamp; return !(cacheObject.timestamp && object_age < this.ttl); } else { return true; } } } /** * Wrap request to prevent multiple calls * with same params when waiting for result. */ function callOnce(func, promiseKeeper, funcScope) { // tslint:disable-next-line: only-arrow-functions return function() { const hash = getRequestHash(arguments); if (!promiseKeeper[hash]) { promiseKeeper[hash] = Promise.resolve( func.apply(funcScope, arguments) .then(result => { promiseKeeper[hash] = null; return result; }) ); } return promiseKeeper[hash]; }; } function cacheRequest(func, funcName, funcScope, self) { // tslint:disable-next-line: only-arrow-functions return function() { if (!self.cache[funcName]) { self.cache[funcName] = {}; } const cacheObject = self.cache[funcName]; const hash = getRequestHash(arguments); if (self.cacheEnabled && !self._isExpired(cacheObject[hash])) { return Promise.resolve(cacheObject[hash].value); } else { return func.apply(funcScope, arguments) .then(result => { if (result !== undefined) { cacheObject[hash] = { value: result, timestamp: Date.now() }; } return result; }); } }; } function getRequestHash(args) { const argsJson = JSON.stringify(args); return getHash(argsJson); } function getHash(str: string): number { let hash = 0, i, chr, len; if (str.length !== 0) { for (i = 0, len = str.length; i < len; i++) { chr = str.charCodeAt(i); hash = ((hash << 5) - hash) + chr; hash |= 0; // Convert to 32bit integer } } return hash; }