@mcabreradev/filter
Version:
A powerful, SQL-like array filtering library for TypeScript and JavaScript with advanced pattern matching, MongoDB-style operators, deep object comparison, and zero dependencies
196 lines (195 loc) • 7.05 kB
JavaScript
export class LRUCache {
cache = new Map();
maxSize;
maxAge;
constructor(maxSize = 1000, maxAge = 300000) {
this.maxSize = maxSize;
this.maxAge = maxAge;
}
get(key) {
const entry = this.cache.get(key);
if (!entry)
return undefined;
if (Date.now() - entry.timestamp > this.maxAge) {
this.cache.delete(key);
return undefined;
}
this.cache.delete(key);
this.cache.set(key, entry);
return entry.value;
}
set(key, value) {
if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value;
if (firstKey !== undefined) {
this.cache.delete(firstKey);
}
}
this.cache.set(key, { value, timestamp: Date.now() });
}
clear() {
this.cache.clear();
}
size() {
return this.cache.size;
}
}
export class MemoizationManager {
static instance;
predicateCache;
regexCache;
expressionHashCache;
constructor() {
this.predicateCache = new LRUCache(500, 300000);
this.regexCache = new LRUCache(500, 300000);
this.expressionHashCache = new WeakMap();
}
static getInstance() {
if (!MemoizationManager.instance) {
MemoizationManager.instance = new MemoizationManager();
}
return MemoizationManager.instance;
}
createExpressionHash(expression, config) {
if (typeof expression === 'function') {
return `fn:${expression.toString()}`;
}
if (typeof expression === 'string') {
return `str:${expression}:${config.caseSensitive}:${config.limit}`;
}
if (typeof expression === 'object' && expression !== null) {
const cached = this.expressionHashCache.get(expression);
const configHash = `:cs:${config.caseSensitive}:md:${config.maxDepth}:lim:${config.limit}`;
if (cached) {
return `${cached}${configHash}`;
}
const hash = this.hashObject(expression, config, true);
this.expressionHashCache.set(expression, hash);
return `${hash}${configHash}`;
}
return `primitive:${String(expression)}:${config.limit}`;
}
hashObject(obj, config, skipConfigSuffix = false) {
const parts = [];
const record = obj;
if ('$and' in record || '$or' in record || '$not' in record) {
if ('$and' in record) {
const andArray = Array.isArray(record.$and) ? record.$and : [record.$and];
parts.push(`$and:[${andArray.map((v) => this.hashValue(v)).join(',')}]`);
}
if ('$or' in record) {
const orArray = Array.isArray(record.$or) ? record.$or : [record.$or];
parts.push(`$or:[${orArray.map((v) => this.hashValue(v)).join(',')}]`);
}
if ('$not' in record) {
parts.push(`$not:${this.hashValue(record.$not)}`);
}
const otherKeys = Object.keys(record)
.filter((k) => !k.startsWith('$'))
.sort();
for (const key of otherKeys) {
const value = record[key];
parts.push(`${key}:${this.hashValue(value)}`);
}
}
else {
const sortedKeys = Object.keys(record).sort();
for (const key of sortedKeys) {
const value = record[key];
if (value === null) {
parts.push(`${key}:null`);
}
else if (value === undefined) {
parts.push(`${key}:undefined`);
}
else if (typeof value === 'object') {
if (value instanceof Date) {
parts.push(`${key}:date:${value.getTime()}`);
}
else if (value instanceof RegExp) {
parts.push(`${key}:regex:${value.source}:${value.flags}`);
}
else if (Array.isArray(value)) {
parts.push(`${key}:arr:[${value.map((v) => this.hashValue(v)).join(',')}]`);
}
else {
parts.push(`${key}:obj:{${this.hashObject(value, config)}}`);
}
}
else {
parts.push(`${key}:${typeof value}:${String(value)}`);
}
}
}
if (skipConfigSuffix) {
return parts.join('|');
}
const orderByHash = config.orderBy ? this.hashOrderBy(config.orderBy) : '';
return `${parts.join('|')}:cs:${config.caseSensitive}:md:${config.maxDepth}:lim:${config.limit}${orderByHash ? `:ob:${orderByHash}` : ''}`;
}
hashOrderBy(orderBy) {
if (typeof orderBy === 'string') {
return `str:${orderBy}`;
}
if (Array.isArray(orderBy)) {
return `arr:[${orderBy
.map((item) => {
if (typeof item === 'string') {
return `str:${item}`;
}
return `obj:{field:${item.field}:dir:${item.direction}}`;
})
.join(',')}]`;
}
if (typeof orderBy === 'object' && orderBy !== null) {
const obj = orderBy;
return `obj:{field:${obj.field}:dir:${obj.direction}}`;
}
return String(orderBy);
}
hashValue(value) {
if (value === null)
return 'null';
if (value === undefined)
return 'undefined';
if (value instanceof Date)
return `date:${value.getTime()}`;
if (value instanceof RegExp)
return `regex:${value.source}:${value.flags}`;
if (typeof value === 'object')
return `obj:${this.hashObject(value, { caseSensitive: false, maxDepth: 3, enableCache: false })}`;
return `${typeof value}:${String(value)}`;
}
getCachedPredicate(key) {
return this.predicateCache.get(key);
}
setCachedPredicate(key, predicate) {
this.predicateCache.set(key, predicate);
}
getCachedRegex(pattern, flags) {
const key = `${pattern}:${flags || ''}`;
return this.regexCache.get(key);
}
setCachedRegex(pattern, regex, flags) {
const key = `${pattern}:${flags || ''}`;
this.regexCache.set(key, regex);
}
clearPredicateCache() {
this.predicateCache.clear();
}
clearRegexCache() {
this.regexCache.clear();
}
clearAll() {
this.predicateCache.clear();
this.regexCache.clear();
this.expressionHashCache = new WeakMap();
}
getStats() {
return {
predicateCacheSize: this.predicateCache.size(),
regexCacheSize: this.regexCache.size(),
};
}
}
export const memoization = MemoizationManager.getInstance();