@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
JavaScript
;
/**
* @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;
},
};
};
}