UNPKG

@eurusik/memoized

Version:

🧠 Smart memoization decorator for TypeScript - A lightweight and flexible TypeScript decorator that memoizes method or getter results using deep argument comparison

297 lines (296 loc) • 10.3 kB
"use strict"; /** * @memoized - Smart memoization decorator for TypeScript * A lightweight and flexible TypeScript decorator that memoizes method or getter results * using deep argument comparison. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.deepEqual = deepEqual; exports.createMemoizedMethod = createMemoizedMethod; exports.clearAllMemoized = clearAllMemoized; exports.memoized = memoized; exports.memoizedTTL = memoizedTTL; /** * Deep equality comparison function * Compares two values recursively for equality * * @param a - First value to compare * @param b - Second value to compare * @param depth - Maximum recursion depth (default: 100) * @returns boolean indicating if values are deeply equal */ function deepEqual(a, b, depth = 100) { // Prevent stack overflow with circular references or very deep objects if (depth <= 0) return false; // Fast path for strict equality (handles primitives efficiently) if (a === b) return true; // Handle null/undefined cases if (a == null || b == null) return false; // Fast path for different types const typeA = typeof a; const typeB = typeof b; if (typeA !== typeB) return false; // Fast path for primitives - already checked with === above // This avoids unnecessary deep comparison for primitives if (typeA !== 'object' && typeA !== 'function') return false; // Handle special types if (a instanceof Date && b instanceof Date) { return a.getTime() === b.getTime(); } if (a instanceof RegExp && b instanceof RegExp) { return a.toString() === b.toString(); } // At this point, we know both a and b are objects (or null, which was handled above) if (a && b) { // Handle arrays if (Array.isArray(a) && Array.isArray(b)) { if (a.length !== b.length) return false; // Fast path for primitive arrays - use direct comparison first const allPrimitives = a.every(item => item === null || item === undefined || (typeof item !== 'object' && typeof item !== 'function')); if (allPrimitives) { // For primitive arrays, we can do a faster comparison return a.every((val, i) => val === b[i]); } // For arrays with objects, do deep comparison return a.every((val, i) => deepEqual(val, b[i], depth - 1)); } // Handle Maps if (a instanceof Map && b instanceof Map) { if (a.size !== b.size) return false; for (const [key, val] of a.entries()) { if (!b.has(key) || !deepEqual(val, b.get(key), depth - 1)) { return false; } } return true; } // Handle Sets if (a instanceof Set && b instanceof Set) { if (a.size !== b.size) return false; for (const item of a) { // For sets, we need to find an equal item if (![...b].some(bItem => deepEqual(item, bItem, depth - 1))) { return false; } } return true; } // Handle plain objects if (!Array.isArray(a) && !Array.isArray(b) && Object.getPrototypeOf(a) === Object.prototype && Object.getPrototypeOf(b) === Object.prototype) { const keysA = Object.keys(a); const keysB = Object.keys(b); if (keysA.length !== keysB.length) return false; return keysA.every(key => deepEqual(a[key], b[key], depth - 1)); } // Handle custom objects with equals method if (typeof a.equals === 'function') { return a.equals(b); } return false; } return false; } /** * Creates a memoized version of a method * * @param originalMethod - The original method to memoize * @param key - The property key of the method * @param options - Optional memoization options * @param instance - Optional instance to attach clearMemo function to * @returns A memoized version of the original method with a clearMemo function */ function createMemoizedMethod(originalMethod, key, options = {}, instance) { let previousArgs = []; let calledOnce = false; let cachedResult; let cachedTimestamp = null; const memoizedFn = function (...args) { const now = Date.now(); const ttlExpired = options.ttl && cachedTimestamp && (now - cachedTimestamp > options.ttl); // Check if cache is valid (arguments match and not expired) const isSame = calledOnce && !ttlExpired && previousArgs.length === args.length && args.every((arg, i) => deepEqual(arg, previousArgs[i])); if (isSame) { return cachedResult; } previousArgs = [...args]; // Create a copy to prevent mutation cachedResult = originalMethod.apply(this, args); cachedTimestamp = Date.now(); calledOnce = true; return cachedResult; }; memoizedFn.clearMemo = () => { previousArgs = []; cachedResult = undefined; cachedTimestamp = null; calledOnce = false; }; if (instance) { if (!instance.__memoizedClearFns) { instance.__memoizedClearFns = {}; } instance.__memoizedClearFns[key] = memoizedFn.clearMemo; } return memoizedFn; } /** * Clears all memoized values on an instance * * @param instance - Instance with memoized methods */ function clearAllMemoized(instance) { if (instance.__memoizedClearFns) { Object.values(instance.__memoizedClearFns).forEach(clearFn => clearFn()); } } function memoized(target, contextOrKey, descriptor) { if (typeof target === 'function') { const context = contextOrKey; if (context.kind === 'getter') { return function memoizedGetter() { const value = target.call(this); Object.defineProperty(this, context.name, { enumerable: true, configurable: true, value, }); return value; }; } if (context.kind === 'method') { return createMemoizedMethod(target, context.name.toString(), {}); } throw new Error('memoized can only be used on methods or getters'); } const propertyKey = contextOrKey; const { get, value } = descriptor; if (get) { return { configurable: true, enumerable: true, get: function memoizedGetter() { const result = get.call(this); Object.defineProperty(this, propertyKey, { configurable: true, enumerable: true, value: result, }); return result; }, }; } if (typeof value !== 'function') { throw new Error('memoized can only be used on methods or getters'); } return { configurable: true, enumerable: true, get() { const fn = createMemoizedMethod(value, propertyKey, {}, this); Object.defineProperty(this, propertyKey, { configurable: true, value: fn, }); return fn; }, }; } /** * Time-based memoization decorator for class methods and getters * Automatically expires cached values after the specified time-to-live (TTL) * Supports both legacy and stage 3 decorators * * @param ttl - Time-to-live in milliseconds for cached values * * @example * // Legacy decorator * class Example { * @memoizedTTL(5000) * expensiveMethod(arg1: string, arg2: number) { * // expensive calculation * return result; * } * } * * @example * // Stage 3 decorator * class Example { * @memoizedTTL(5000) * expensiveMethod(arg1: string, arg2: number) { * // expensive calculation * return result; * } * } */ function memoizedTTL(ttl) { return function (target, contextOrKey, descriptorOrUndefined) { // Stage 3 decorator format if (contextOrKey && typeof contextOrKey === 'object' && 'kind' in contextOrKey) { const context = contextOrKey; if (context.kind === 'getter') { return function memoizedGetter() { const value = target.call(this); Object.defineProperty(this, context.name, { enumerable: true, configurable: true, value, }); return value; }; } if (context.kind === 'method') { return createMemoizedMethod(target, context.name.toString(), { ttl }); } throw new Error('memoizedTTL can only be used on methods or getters'); } // Legacy decorator format const propertyKey = contextOrKey; const descriptor = descriptorOrUndefined; const { value, get } = descriptor; if (get) { return { configurable: true, enumerable: true, get: function memoizedGetter() { const result = get.call(this); Object.defineProperty(this, propertyKey, { configurable: true, enumerable: true, value: result, }); return result; }, }; } if (typeof value !== 'function') { throw new Error('memoizedTTL can only be used on methods or getters'); } return { configurable: true, enumerable: true, get() { const fn = createMemoizedMethod(value, propertyKey, { ttl }, this); Object.defineProperty(this, propertyKey, { configurable: true, value: fn, }); return fn; }, }; }; }