Architecture
Deep dive into the architecture of @mcabreradev/filter.
Overview
@mcabreradev/filter is built with a modular architecture that separates concerns and enables tree-shaking for optimal bundle sizes. The library has evolved through multiple versions, with v5.8.2 featuring MongoDB-style operators, framework integrations (React, Vue, Angular, SolidJS, Preact), lazy evaluation, memoization, geospatial operators, datetime operators, and visual debugging.
Data Flow
filter(array, expression, options)
↓
Performance monitoring starts
↓
validateExpression(expression)
↓
mergeConfig(options) ← defaults: caseSensitive=false, maxDepth=3, enableCache=false
↓
debug=true? → filterDebug path (tree visualization + stats)
↓
enableCache=true? → cache lookup by expression hash
↓ (cache miss)
createPredicateFn(expression, config) ← predicate factory dispatches by expression type
↓
array.filter(predicate)
↓
post-process: orderBy, limit
↓
cache result (if enableCache=true)
↓
return T[]Core Architecture
src/
├── core/
│ ├── filter/filter.ts # Main filter function with caching & debug
│ └── lazy/filter-lazy.ts # Lazy evaluation with generators
├── operators/ # Operator implementations (each in a subdirectory)
│ ├── comparison/ # $gt, $gte, $lt, $lte, $eq, $ne
│ ├── logical/ # $and, $or, $not
│ ├── string/ # $startsWith, $endsWith, $contains, $regex, $match
│ ├── array/ # $in, $nin, $contains, $size
│ ├── geospatial/ # $near, $geoBox, $geoPolygon (v5.6.0+)
│ ├── datetime/ # $recent, $upcoming, $dayOfWeek, $timeOfDay, $age (v5.6.0+)
│ └── operator-processor.ts # Orchestrates operator evaluation
├── comparison/ # Deep comparison (each in a subdirectory)
│ ├── deep/ # Recursive deep equality for nested objects
│ ├── object/ # Object comparison with maxDepth support
│ └── property/ # Property-level comparison with wildcards
├── predicate/
│ └── factory/predicate-factory.ts # Dispatches to string/object/function predicate builders
├── debug/ # Debug & visualization (v5.5.0+)
│ ├── debug-filter.ts # Debug-enabled filter with statistics
│ ├── debug-tree-builder.ts # Expression tree builder
│ ├── debug-formatter.ts # ANSI color output
│ └── debug-evaluator.ts # Tracks condition evaluations
├── types/ # TypeScript definitions
│ ├── expression.types.ts
│ ├── operators.types.ts
│ ├── config.types.ts
│ └── lazy.types.ts
├── utils/ # Utilities (each in a subdirectory)
│ ├── cache/ # Result caching with WeakMap + LRU
│ ├── memoization/ # Multi-layer LRU memoization (v5.2.0+)
│ ├── lazy-iterators/ # Generator utilities (v5.1.0+)
│ ├── pattern-matching/ # SQL wildcards (%, _)
│ ├── operator-detection/ # Detects $-prefixed operator expressions
│ ├── type-guards/ # TypeScript type guards
│ ├── sort/ # OrderBy sorting utilities
│ ├── date-time/ # Datetime helpers (v5.6.0+)
│ ├── geo-distance/ # Geospatial distance calculation (v5.6.0+)
│ ├── performance-monitor/ # Optional performance metric tracking
│ └── typed-filter/ # Typed filter utilities
├── errors/ # Custom error types and helpers
├── constants/ # Shared constants (filter.constants.ts)
├── validation/ # Runtime validation with Zod
├── config/
│ ├── default-config.ts
│ └── config-builder.ts
└── integrations/ # Framework integrations (v5.3.0+)
├── react/ # useFilter, useFilteredState, useDebouncedFilter, usePaginatedFilter
├── vue/ # Same 4 composables with Composition API
├── angular/ # Angular integration (v5.7.0+)
├── preact/ # Preact hooks (v5.7.0+)
├── solidjs/ # SolidJS integration (v5.7.0+)
└── shared/ # Debounce, pagination helpersCore Components
Filter Engine
The filter engine is the heart of the library, processing expressions with multi-layer caching and debug support.
export function filter<T>(
array: T[],
expression: Expression<T>,
options?: FilterOptions
): T[] {
if (!Array.isArray(array)) {
throw new Error(`Expected array but received: ${typeof array}`);
}
const config = mergeConfig(options);
const validatedExpression = validateExpression<T>(expression);
// Debug mode (v5.5.0+)
if (config.debug) {
const result = filterDebug(array, validatedExpression, options);
result.print();
return result.items;
}
// Multi-layer caching (v5.2.0+)
if (config.enableCache) {
const cacheKey = memoization.createExpressionHash(validatedExpression, config);
const cached = globalFilterCache.get(array, cacheKey);
if (cached !== undefined) {
return cached as T[];
}
const predicate = createPredicateFn<T>(validatedExpression, config);
const result = array.filter(predicate);
globalFilterCache.set(array, cacheKey, result);
return result;
}
// Standard filtering
const predicate = createPredicateFn<T>(validatedExpression, config);
return array.filter(predicate);
}Expression Parser
Converts user expressions into executable predicates with support for operators, wildcards, and functions.
export function createPredicateFn<T>(
expression: Expression<T>,
config: FilterConfig,
): (item: T) => boolean {
// Handle predicate functions
if (typeof expression === 'function') {
return expression as PredicateFunction<T>;
}
// Handle primitive expressions (string, number, boolean)
if (isPrimitive(expression)) {
return createStringPredicate<T>(expression, config);
}
// Handle object expressions with operators
if (isObjectExpression(expression)) {
return createObjectPredicate<T>(expression, config);
}
throw new Error(`Invalid expression type: ${typeof expression}`);
}Operator Processor
Orchestrates operator evaluation with support for 30+ operators across multiple categories.
export function processOperators<T>(
value: unknown,
operators: Record<string, unknown>,
config: FilterConfig,
): boolean {
for (const [op, operand] of Object.entries(operators)) {
// Comparison operators
if (isComparisonOperator(op)) {
if (!evaluateComparison(value, op, operand)) return false;
}
// Array operators
else if (isArrayOperator(op)) {
if (!evaluateArray(value, op, operand)) return false;
}
// String operators
else if (isStringOperator(op)) {
if (!evaluateString(value, op, operand, config)) return false;
}
// Logical operators (v5.2.0+)
else if (isLogicalOperator(op)) {
if (!evaluateLogical(value, op, operand, config)) return false;
}
// Geospatial operators (v5.6.0+)
else if (isGeospatialOperator(op)) {
if (!evaluateGeospatial(value, op, operand)) return false;
}
// Datetime operators (v5.6.0+)
else if (isDateTimeOperator(op)) {
if (!evaluateDateTime(value, op, operand)) return false;
}
else {
throw new Error(`Unknown operator: ${op}`);
}
}
return true;
}Operator Categories
Comparison Operators (v5.0.0+):
export function evaluateComparison(
value: unknown,
operator: ComparisonOperator,
operand: unknown,
): boolean {
switch (operator) {
case '$eq': return value === operand;
case '$ne': return value !== operand;
case '$gt': return (value as number) > (operand as number);
case '$gte': return (value as number) >= (operand as number);
case '$lt': return (value as number) < (operand as number);
case '$lte': return (value as number) <= (operand as number);
default: return false;
}
}Array Operators (v5.0.0+):
export function evaluateArray(
value: unknown,
operator: ArrayOperator,
operand: unknown,
): boolean {
switch (operator) {
case '$in':
return Array.isArray(operand) && operand.includes(value);
case '$nin':
return Array.isArray(operand) && !operand.includes(value);
case '$contains':
return Array.isArray(value) && value.includes(operand);
case '$size':
return Array.isArray(value) && value.length === operand;
default:
return false;
}
}String Operators (v5.0.0+):
export function evaluateString(
value: unknown,
operator: StringOperator,
operand: unknown,
config: FilterConfig,
): boolean {
const str = String(value);
const pattern = String(operand);
switch (operator) {
case '$startsWith':
return config.caseSensitive
? str.startsWith(pattern)
: str.toLowerCase().startsWith(pattern.toLowerCase());
case '$endsWith':
return config.caseSensitive
? str.endsWith(pattern)
: str.toLowerCase().endsWith(pattern.toLowerCase());
case '$contains':
return config.caseSensitive
? str.includes(pattern)
: str.toLowerCase().includes(pattern.toLowerCase());
case '$regex':
case '$match':
const regex = operand instanceof RegExp ? operand : new RegExp(pattern);
return regex.test(str);
default:
return false;
}
}Logical Operators (v5.2.0+):
export function evaluateLogical<T>(
item: T,
operator: LogicalOperator,
operand: unknown,
config: FilterConfig,
): boolean {
switch (operator) {
case '$and':
if (!Array.isArray(operand)) return false;
return operand.every(expr => {
const predicate = createPredicateFn(expr, config);
return predicate(item);
});
case '$or':
if (!Array.isArray(operand)) return false;
return operand.some(expr => {
const predicate = createPredicateFn(expr, config);
return predicate(item);
});
case '$not':
const predicate = createPredicateFn(operand as Expression<T>, config);
return !predicate(item);
default:
return false;
}
}Geospatial Operators (v5.6.0+):
export function evaluateNear(point: GeoPoint, query: NearQuery): boolean {
const distance = calculateDistance(point, query.center);
return distance <= query.maxDistanceMeters;
}
export function evaluateGeoBox(point: GeoPoint, box: BoundingBox): boolean {
return (
point.lat >= box.southwest.lat &&
point.lat <= box.northeast.lat &&
point.lng >= box.southwest.lng &&
point.lng <= box.northeast.lng
);
}
export function evaluateGeoPolygon(point: GeoPoint, query: PolygonQuery): boolean {
// Ray casting algorithm for point-in-polygon
let inside = false;
const { points } = query;
for (let i = 0, j = points.length - 1; i < points.length; j = i++) {
const xi = points[i].lng, yi = points[i].lat;
const xj = points[j].lng, yj = points[j].lat;
const intersect = ((yi > point.lat) !== (yj > point.lat)) &&
(point.lng < (xj - xi) * (point.lat - yi) / (yj - yi) + xi);
if (intersect) inside = !inside;
}
return inside;
}
// Spherical law of cosines for distance calculation
export function calculateDistance(p1: GeoPoint, p2: GeoPoint): number {
const R = 6371000; // Earth's radius in meters
const φ1 = p1.lat * Math.PI / 180;
const φ2 = p2.lat * Math.PI / 180;
const Δλ = (p2.lng - p1.lng) * Math.PI / 180;
const d = Math.acos(
Math.sin(φ1) * Math.sin(φ2) +
Math.cos(φ1) * Math.cos(φ2) * Math.cos(Δλ)
) * R;
return d;
}Datetime Operators (v5.6.0+):
export function evaluateRecent(date: Date, query: RelativeTimeQuery): boolean {
const now = new Date();
const diff = now.getTime() - date.getTime();
const threshold = calculateTimeDifference(query);
return diff <= threshold;
}
export function evaluateUpcoming(date: Date, query: RelativeTimeQuery): boolean {
const now = new Date();
const diff = date.getTime() - now.getTime();
const threshold = calculateTimeDifference(query);
return diff >= 0 && diff <= threshold;
}
export function evaluateDayOfWeek(date: Date, days: number[]): boolean {
return days.includes(date.getDay());
}
export function evaluateTimeOfDay(date: Date, query: TimeOfDayQuery): boolean {
const hour = date.getHours();
return hour >= query.start && hour <= query.end;
}
export function evaluateAge(birthDate: Date, query: AgeQuery): boolean {
const age = calculateAge(birthDate, query.unit);
if (query.min !== undefined && age < query.min) return false;
if (query.max !== undefined && age > query.max) return false;
return true;
}
export function calculateAge(birthDate: Date, unit: 'years' | 'months' | 'days' = 'years'): number {
const now = new Date();
const diff = now.getTime() - birthDate.getTime();
switch (unit) {
case 'years':
return Math.floor(diff / (1000 * 60 * 60 * 24 * 365.25));
case 'months':
return Math.floor(diff / (1000 * 60 * 60 * 24 * 30.44));
case 'days':
return Math.floor(diff / (1000 * 60 * 60 * 24));
}
}Memoization System (v5.2.0+)
Multi-Layer Caching Strategy
The library implements a three-layer LRU caching system:
| Layer | Storage | Max Size | TTL | Key |
|---|---|---|---|---|
| Result cache | WeakMap → LRU per array | 100 entries/array | — | expression hash |
| Predicate cache | LRU | 500 entries | 300s | expression + config |
| Regex cache | LRU | 500 entries | 300s | pattern + flags |
The LRU eviction moves recently accessed items to the end of the cache and evicts from the front when max size is reached. The WeakMap keyed on the array reference ensures result caches are garbage-collected when the array goes out of scope.
Cache Key Generation
Cache keys include: expression structure + caseSensitive, maxDepth, limit, and orderBy config options. Logical operators ($and, $or, $not) are handled specially during hashing. A secondary WeakMap prevents rehashing the same object reference across calls.
// Simplified hash structure
// For strings: "str:expression:caseSensitive:limit"
// For objects: hashed structure + logical operator sub-expressions
// Includes config: caseSensitive, maxDepth, limit, orderBy
createExpressionHash(expression: unknown, config: FilterConfig): stringResult Cache Implementation
export class FilterCache<T> {
// WeakMap per array reference → LRU (max 100 entries)
private cache = new WeakMap<T[], LRUCache<string, T[]>>();
get(array: T[], key: string): T[] | undefined {
return this.cache.get(array)?.get(key);
}
set(array: T[], key: string, value: T[]): void {
let lru = this.cache.get(array);
if (!lru) {
lru = new LRUCache({ max: 100 });
this.cache.set(array, lru);
}
lru.set(key, value);
}
}Cache Statistics and Control
// Get current cache sizes
getFilterCacheStats(); // → { predicateCacheSize, regexCacheSize }
// Clear all caches
clearFilterCache();Performance Gains
| Operation | Without Cache | With Cache | Speedup |
|---|---|---|---|
| Simple query | 5.3ms | 0.01ms | 530x |
| Regex pattern | 12.1ms | 0.02ms | 605x |
| Complex nested | 15.2ms | 0.01ms | 1520x |
Lazy Evaluation (v5.1.0+)
Generator-Based Filtering
Efficiently process large datasets with lazy evaluation and early exit optimization.
export function* filterLazy<T>(
array: T[],
expression: Expression<T>,
options?: FilterOptions,
): IterableIterator<T> {
const config = mergeConfig(options);
const validatedExpression = validateExpression<T>(expression);
const predicate = createPredicateFn<T>(validatedExpression, config);
for (const item of array) {
if (predicate(item)) {
yield item;
}
}
}
// Helper functions for lazy operations
export function filterFirst<T>(
array: T[],
expression: Expression<T>,
count: number,
options?: FilterOptions,
): T[] {
const iterator = filterLazy(array, expression, options);
const result: T[] = [];
for (const item of iterator) {
result.push(item);
if (result.length >= count) break; // Early exit
}
return result;
}
export function filterExists<T>(
array: T[],
expression: Expression<T>,
options?: FilterOptions,
): boolean {
const iterator = filterLazy(array, expression, options);
const { value, done } = iterator.next();
return !done && value !== undefined;
}
export function filterCount<T>(
array: T[],
expression: Expression<T>,
options?: FilterOptions,
): number {
const iterator = filterLazy(array, expression, options);
let count = 0;
for (const _ of iterator) {
count++;
}
return count;
}Chunked and Async Variants
// Async generator for async iterables
export async function* filterLazyAsync<T>(
iterable: AsyncIterable<T>,
expression: Expression<T>,
options?: FilterOptions,
): AsyncGenerator<T>
// Group results into chunks for batch processing
export function filterChunked<T>(
array: T[],
expression: Expression<T>,
chunkSize: number,
options?: FilterOptions,
): T[][]
// Lazy chunked generator - yields chunks on demand
export function* filterLazyChunked<T>(
array: T[],
expression: Expression<T>,
chunkSize: number,
options?: FilterOptions,
): Generator<T[]>Lazy Iterator Utilities
// Take first N items
export function* take<T>(iterator: Iterable<T>, count: number): IterableIterator<T> {
let taken = 0;
for (const item of iterator) {
if (taken >= count) break;
yield item;
taken++;
}
}
// Skip first N items
export function* skip<T>(iterator: Iterable<T>, count: number): IterableIterator<T> {
let skipped = 0;
for (const item of iterator) {
if (skipped < count) {
skipped++;
continue;
}
yield item;
}
}
// Map transformation
export function* map<T, U>(
iterator: Iterable<T>,
fn: (item: T) => U,
): IterableIterator<U> {
for (const item of iterator) {
yield fn(item);
}
}
// Convert to array
export function toArray<T>(iterator: Iterable<T>): T[] {
return Array.from(iterator);
}Debug System (v5.5.0+)
Debug Filter Implementation
export const filterDebug = <T>(
array: T[],
expression: Expression<T>,
options?: FilterOptions,
): DebugResult<T> => {
const startTime = performance.now();
const config = mergeConfig(options);
const validatedExpression = validateExpression<T>(expression);
// Build expression tree
const tree = buildDebugTree(validatedExpression, config);
// Evaluate with tracking
const { items, tree: populatedTree } = evaluateWithDebug(
array,
tree,
validatedExpression,
config,
);
const executionTime = performance.now() - startTime;
const conditionsEvaluated = countConditions(populatedTree);
return {
items,
stats: {
matched: items.length,
total: array.length,
percentage: (items.length / array.length) * 100,
executionTime,
conditionsEvaluated,
},
debug: {
tree: populatedTree,
expression: validatedExpression,
},
print: () => {
const formatted = formatDebugTree(populatedTree, config);
console.log(formatted);
},
};
};Debug Tree Builder
export const buildDebugTree = (
expression: Expression<unknown>,
config: FilterConfig,
): DebugNode => {
// Handle logical operators
if (hasLogicalOperator(expression)) {
return buildLogicalNode(expression, config);
}
// Handle object expressions
if (isObjectExpression(expression)) {
return buildObjectNode(expression, config);
}
// Handle primitive expressions
return buildPrimitiveNode(expression, config);
};
const buildLogicalNode = (
expression: Record<string, unknown>,
config: FilterConfig,
): DebugNode => {
const operator = getLogicalOperator(expression);
const operands = expression[operator] as unknown[];
return {
type: 'logical',
operator,
children: operands.map(op => buildDebugTree(op, config)),
};
};Debug Tree Formatter
export const formatDebugTree = (
tree: DebugNode,
config: FilterConfig,
): string => {
const lines: string[] = [];
const colorize = config.colorize ?? false;
const formatNode = (node: DebugNode, prefix = '', isLast = true): void => {
const connector = isLast ? '└─' : '├─';
const matchInfo = node.matched !== undefined
? ` (${node.matched}/${node.total} matched, ${((node.matched / node.total) * 100).toFixed(1)}%)`
: '';
const status = node.matched === node.total ? '✓' : '✗';
const line = `${prefix}${connector} ${status} ${node.label}${matchInfo}`;
lines.push(colorize ? colorizeOutput(line, node.matched === node.total) : line);
if (node.children) {
const childPrefix = prefix + (isLast ? ' ' : '│ ');
node.children.forEach((child, i) => {
formatNode(child, childPrefix, i === node.children!.length - 1);
});
}
};
formatNode(tree);
return lines.join('\n');
};
const colorizeOutput = (text: string, success: boolean): string => {
const colors = {
green: '\x1b[32m',
red: '\x1b[31m',
reset: '\x1b[0m',
};
return success
? `${colors.green}${text}${colors.reset}`
: `${colors.red}${text}${colors.reset}`;
};Framework Integration Architecture (v5.3.0+)
React Integration
Uses React hooks for state management and reactivity with memoization.
export function useFilter<T>(
data: T[],
expression: Expression<T>,
options?: FilterOptions,
): UseFilterResult<T> {
const filtered = useMemo(() => {
if (!data || data.length === 0) {
return [];
}
try {
return filter(data, expression, options);
} catch {
return [];
}
}, [data, expression, options]);
const isFiltering = useMemo(() => {
return filtered.length !== data.length;
}, [filtered.length, data.length]);
return {
filtered,
isFiltering,
};
}
export function useDebouncedFilter<T>(
data: T[],
expression: Expression<T>,
options?: UseDebouncedFilterOptions,
): UseDebouncedFilterResult<T> {
const { delay = 300, ...filterOptions } = options || {};
const [debouncedExpression, setDebouncedExpression] = useState(expression);
const [isPending, setIsPending] = useState(false);
useEffect(() => {
setIsPending(true);
const timer = setTimeout(() => {
setDebouncedExpression(expression);
setIsPending(false);
}, delay);
return () => {
clearTimeout(timer);
setIsPending(false);
};
}, [expression, delay]);
const filtered = useMemo(() => {
if (!data || data.length === 0) {
return [];
}
try {
return filter(data, debouncedExpression, filterOptions);
} catch {
return [];
}
}, [data, debouncedExpression, filterOptions]);
const isFiltering = useMemo(() => {
return filtered.length !== data.length;
}, [filtered.length, data.length]);
return {
filtered,
isFiltering,
isPending,
};
}Vue Integration
Uses Vue's reactive system with computed properties for automatic updates.
export function useFilter<T>(
data: MaybeRef<T[]>,
expression: MaybeRef<Expression<T>>,
options?: MaybeRef<FilterOptions>,
): UseFilterResult<T> {
const filtered = computed(() => {
const dataValue = unref(data);
const expressionValue = unref(expression);
const optionsValue = unref(options);
if (!dataValue || dataValue.length === 0) {
return [];
}
try {
return filter(dataValue, expressionValue, optionsValue);
} catch {
return [];
}
});
const isFiltering = computed(() => {
const dataValue = unref(data);
return filtered.value.length !== dataValue.length;
});
return {
filtered,
isFiltering,
};
}Type System
Generic Type Constraints with Operator Autocomplete
The type system provides intelligent autocomplete based on property types:
// Core expression type
type Expression<T> =
| PrimitiveExpression
| PredicateFunction<T>
| ObjectExpression<T>
| LogicalExpression<T>;
// Object expression with typed operators
type ObjectExpression<T> = {
[K in keyof T]?:
| T[K]
| OperatorExpression<T[K]>
| T[K][]; // Array OR syntax (v5.5.0+)
} & {
$and?: Expression<T>[];
$or?: Expression<T>[];
$not?: Expression<T>;
};
// Type-aware operator expressions
type OperatorExpression<T> =
& ComparisonOperators<T>
& ArrayOperators<T>
& StringOperators<T>
& GeospatialOperators<T>
& DateTimeOperators<T>;
// Comparison operators (available for all types)
interface ComparisonOperators<T> {
$eq?: T;
$ne?: T;
$gt?: T extends number | Date ? T : never;
$gte?: T extends number | Date ? T : never;
$lt?: T extends number | Date ? T : never;
$lte?: T extends number | Date ? T : never;
}
// Array operators (available for array types)
interface ArrayOperators<T> {
$in?: T[];
$nin?: T[];
$contains?: T extends Array<infer U> ? U : never;
$size?: T extends unknown[] ? number : never;
}
// String operators (available for string types)
interface StringOperators<T> {
$startsWith?: T extends string ? string : never;
$endsWith?: T extends string ? string : never;
$contains?: T extends string ? string : never;
$regex?: T extends string ? RegExp | string : never;
$match?: T extends string ? RegExp | string : never;
}
// Geospatial operators (v5.6.0+)
interface GeospatialOperators<T> {
$near?: T extends GeoPoint ? NearQuery : never;
$geoBox?: T extends GeoPoint ? BoundingBox : never;
$geoPolygon?: T extends GeoPoint ? PolygonQuery : never;
}
// Datetime operators (v5.6.0+)
interface DateTimeOperators<T> {
$recent?: T extends Date ? RelativeTimeQuery : never;
$upcoming?: T extends Date ? RelativeTimeQuery : never;
$dayOfWeek?: T extends Date ? number[] : never;
$timeOfDay?: T extends Date ? TimeOfDayQuery : never;
$age?: T extends Date ? AgeQuery : never;
$isWeekday?: T extends Date ? boolean : never;
$isWeekend?: T extends Date ? boolean : never;
$isBefore?: T extends Date ? Date : never;
$isAfter?: T extends Date ? Date : never;
}
// Geospatial types
export interface GeoPoint {
lat: number;
lng: number;
}
export interface NearQuery {
center: GeoPoint;
maxDistanceMeters: number;
}
export interface BoundingBox {
southwest: GeoPoint;
northeast: GeoPoint;
}
export interface PolygonQuery {
points: GeoPoint[];
}
// Datetime types
export interface RelativeTimeQuery {
days?: number;
hours?: number;
minutes?: number;
}
export interface TimeOfDayQuery {
start: number; // 0-23
end: number; // 0-23
}
export interface AgeQuery {
min?: number;
max?: number;
unit?: 'years' | 'months' | 'days';
}Type Inference Example
interface User {
id: number;
name: string;
email: string;
age: number;
tags: string[];
location: GeoPoint;
birthDate: Date;
active: boolean;
}
// TypeScript suggests only valid operators for each property type
const expression: Expression<User> = {
// number property - suggests: $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin
age: { $gte: 18, $lte: 65 },
// string property - suggests: $eq, $ne, $startsWith, $endsWith, $contains, $regex, $match, $in, $nin
name: { $startsWith: 'A' },
email: { $endsWith: '@example.com' },
// array property - suggests: $contains, $size, $in, $nin
tags: { $contains: 'premium' },
// GeoPoint property - suggests: $near, $geoBox, $geoPolygon
location: {
$near: {
center: { lat: 52.52, lng: 13.405 },
maxDistanceMeters: 5000
}
},
// Date property - suggests: $recent, $upcoming, $dayOfWeek, $timeOfDay, $age, $isWeekday, $isWeekend, $isBefore, $isAfter
birthDate: {
$age: { min: 18, max: 65, unit: 'years' }
},
// boolean property - suggests: $eq, $ne
active: { $eq: true }
};
// Filtered result is properly typed as User[]
const filtered = filter(users, expression);Performance Optimizations
1. Early Exit Strategy
function createObjectPredicate<T>(
expression: ObjectExpression<T>,
config: FilterConfig,
): (item: T) => boolean {
return (item: T): boolean => {
// Early exit on first failed condition
for (const [key, condition] of Object.entries(expression)) {
if (!evaluateCondition(item[key as keyof T], condition, config)) {
return false; // Early exit
}
}
return true;
};
}2. Operator Specialization
// Fast lookup maps for operators
const comparisonOps = new Map([
['$eq', (a: any, b: any) => a === b],
['$ne', (a: any, b: any) => a !== b],
['$gt', (a: any, b: any) => a > b],
['$gte', (a: any, b: any) => a >= b],
['$lt', (a: any, b: any) => a < b],
['$lte', (a: any, b: any) => a <= b],
]);
export function evaluateComparison(
value: unknown,
operator: string,
operand: unknown,
): boolean {
const fn = comparisonOps.get(operator);
return fn ? fn(value, operand) : false;
}3. Pattern Matching Optimization
// Compile wildcard patterns once
export function compileWildcardPattern(pattern: string): RegExp {
// Memoized via regex cache
const escaped = pattern
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
.replace(/%/g, '.*')
.replace(/_/g, '.');
return memoization.memoizeRegex(`^${escaped}$`, 'i');
}4. Deep Comparison Caching
const comparisonCache = new WeakMap<object, Map<object, boolean>>();
export function deepCompare(a: unknown, b: unknown): boolean {
// Use cache for object comparisons
if (typeof a === 'object' && typeof b === 'object' && a !== null && b !== null) {
let cache = comparisonCache.get(a);
if (!cache) {
cache = new Map();
comparisonCache.set(a, cache);
}
if (cache.has(b)) {
return cache.get(b)!;
}
const result = deepCompareImpl(a, b);
cache.set(b, result);
return result;
}
return a === b;
}Configuration System
Configuration Builder
export interface FilterConfig {
caseSensitive: boolean;
maxDepth: number;
enableCache: boolean;
debug: boolean;
verbose: boolean;
showTimings: boolean;
colorize: boolean;
customComparator?: Comparator;
}
export const defaultConfig: FilterConfig = {
caseSensitive: false,
maxDepth: 3,
enableCache: false,
debug: false,
verbose: false,
showTimings: false,
colorize: false,
};
export function mergeConfig(options?: FilterOptions): FilterConfig {
return {
...defaultConfig,
...options,
};
}
export function createFilterConfig(options?: FilterOptions): FilterConfig {
const config = mergeConfig(options);
// Validate configuration
if (config.maxDepth < 1 || config.maxDepth > 10) {
throw new Error('maxDepth must be between 1 and 10');
}
return config;
}Validation System
Zod Schema Validation
import { z } from 'zod';
const operatorSchema = z.object({
$eq: z.any().optional(),
$ne: z.any().optional(),
$gt: z.union([z.number(), z.date()]).optional(),
$gte: z.union([z.number(), z.date()]).optional(),
$lt: z.union([z.number(), z.date()]).optional(),
$lte: z.union([z.number(), z.date()]).optional(),
$in: z.array(z.any()).optional(),
$nin: z.array(z.any()).optional(),
$contains: z.any().optional(),
$size: z.number().optional(),
$startsWith: z.string().optional(),
$endsWith: z.string().optional(),
$regex: z.union([z.instanceof(RegExp), z.string()]).optional(),
$match: z.union([z.instanceof(RegExp), z.string()]).optional(),
$near: z.object({
center: z.object({ lat: z.number(), lng: z.number() }),
maxDistanceMeters: z.number().positive(),
}).optional(),
$recent: z.object({
days: z.number().optional(),
hours: z.number().optional(),
minutes: z.number().optional(),
}).optional(),
// ... other operators
});
export function validateExpression<T>(expression: unknown): Expression<T> {
// Validate expression structure
if (typeof expression === 'function') {
return expression as PredicateFunction<T>;
}
if (isPrimitive(expression)) {
return expression as PrimitiveExpression;
}
if (typeof expression === 'object' && expression !== null) {
// Validate operators
for (const [key, value] of Object.entries(expression)) {
if (key.startsWith('$')) {
operatorSchema.parse({ [key]: value });
}
}
return expression as ObjectExpression<T>;
}
throw new Error(`Invalid expression: ${typeof expression}`);
}Extension Points
Custom Operators
const customOperators = new Map<string, OperatorFunction>();
export function registerOperator(
name: string,
fn: OperatorFunction,
): void {
if (!name.startsWith('$')) {
throw new Error('Operator name must start with $');
}
customOperators.set(name, fn);
}
export function getOperator(name: string): OperatorFunction | undefined {
return customOperators.get(name) || builtInOperators.get(name);
}
// Example: Register custom operator
registerOperator('$divisibleBy', (value: number, divisor: number) => {
return value % divisor === 0;
});
// Usage
filter(numbers, { value: { $divisibleBy: 3 } });Configuration Hooks
export interface FilterConfig {
// ... existing config
onBeforeFilter?: (data: any[], expression: any) => void;
onAfterFilter?: (result: any[], executionTime: number) => void;
onError?: (error: Error) => void;
}
export function filter<T>(
array: T[],
expression: Expression<T>,
options?: FilterOptions,
): T[] {
const config = mergeConfig(options);
// Before hook
config.onBeforeFilter?.(array, expression);
const startTime = performance.now();
try {
const result = filterImpl(array, expression, config);
const executionTime = performance.now() - startTime;
// After hook
config.onAfterFilter?.(result, executionTime);
return result;
} catch (error) {
config.onError?.(error as Error);
throw error;
}
}Bundle Optimization
Tree-Shaking Support
// Separate exports for optimal tree-shaking
export { filter } from './core/filter/filter';
export { filterLazy, filterFirst, filterExists, filterCount } from './core/lazy/filter-lazy';
export { filterDebug } from './debug/debug-filter';
// Framework integrations in separate entry points
export { useFilter, useDebouncedFilter } from './integrations/react';
export { useFilter as useFilterVue } from './integrations/vue';Code Splitting Configuration
{
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
},
"./react": {
"import": "./dist/integrations/react/index.js",
"types": "./dist/integrations/react/index.d.ts"
},
"./vue": {
"import": "./dist/integrations/vue/index.js",
"types": "./dist/integrations/vue/index.d.ts"
},
}
}Testing Architecture
Unit Tests
Each component has isolated unit tests with 100% coverage:
describe('filter', () => {
it('should filter by equality operator', () => {
const data = [{ age: 25 }, { age: 30 }];
const result = filter(data, { age: { $eq: 25 } });
expect(result).toEqual([{ age: 25 }]);
});
it('should support geospatial operators', () => {
const data = [
{ location: { lat: 52.52, lng: 13.405 } },
{ location: { lat: 51.5074, lng: -0.1278 } },
];
const result = filter(data, {
location: {
$near: {
center: { lat: 52.52, lng: 13.405 },
maxDistanceMeters: 100,
},
},
});
expect(result).toHaveLength(1);
});
it('should support Datetime operators', () => {
const data = [
{ birthDate: new Date('1990-01-01') },
{ birthDate: new Date('2010-01-01') },
];
const result = filter(data, {
birthDate: { $age: { min: 18, unit: 'years' } },
});
expect(result).toHaveLength(1);
});
});Integration Tests
Test framework integrations across all frameworks:
describe('useFilter (React)', () => {
it('should update when data changes', () => {
const { result, rerender } = renderHook(
({ data }) => useFilter(data, { age: { $gte: 18 } }),
{ initialProps: { data: [{ age: 20 }] } }
);
expect(result.current.filtered).toHaveLength(1);
rerender({ data: [{ age: 15 }] });
expect(result.current.filtered).toHaveLength(0);
});
});
describe('useFilter (Vue)', () => {
it('should be reactive to data changes', () => {
const data = ref([{ age: 20 }]);
const { filtered } = useFilter(data, { age: { $gte: 18 } });
expect(filtered.value).toHaveLength(1);
data.value = [{ age: 15 }];
expect(filtered.value).toHaveLength(0);
});
});Performance Tests
describe('performance', () => {
it('should handle large datasets efficiently', () => {
const data = Array.from({ length: 100000 }, (_, i) => ({ id: i }));
const start = performance.now();
filter(data, { id: { $gte: 50000 } });
const end = performance.now();
expect(end - start).toBeLessThan(100); // < 100ms
});
it('should benefit from caching', () => {
const data = Array.from({ length: 10000 }, (_, i) => ({ id: i }));
const expression = { id: { $gte: 5000 } };
// First call
const start1 = performance.now();
filter(data, expression, { enableCache: true });
const time1 = performance.now() - start1;
// Second call (cached)
const start2 = performance.now();
filter(data, expression, { enableCache: true });
const time2 = performance.now() - start2;
expect(time2).toBeLessThan(time1 * 0.1); // 10x faster
});
});Design Decisions & Trade-offs
Strengths
- Zero runtime dependencies in core — framework integrations use optional peer dependencies; validation uses optional Zod
- Type-safe operators — conditional types restrict operators by field type (e.g.
$startsWithonly available onstringproperties,$nearonly onGeoPoint) - Three-tier LRU caching — WeakMap prevents memory leaks on the result cache; separate TTL-based LRU caches for predicates and regex patterns prevent unbounded growth
- Generator-based lazy evaluation — enables early exit and constant-memory processing of large datasets
- Modular subpath exports — consumers import only what they need (
@mcabreradev/filter/react,/operators/comparison, etc.), enabling full tree-shaking - Framework-agnostic core — all 6 framework integrations are thin adapters over the same core
Known Trade-offs
| Area | Detail |
|---|---|
| Cache TTL | Predicate and regex caches expire after 300s (hardcoded, not configurable per consumer) |
maxDepth default | Defaults to 3, silently limiting deeply nested object matching |
| Logical operator re-evaluation | $and/$or/$not create new sub-predicates on each call — no sub-predicate caching |
| Silent type mismatch | Mismatched operator types (e.g. $gt on a string) return false rather than throwing |
| DateTime timezone | DateTime operators are synchronous and have no timezone support |
| WeakMap dependency | Result cache requires the array reference to stay alive; a new array reference always misses cache |
Evolution Timeline
- v5.0.0: MongoDB-style operators, configuration API, validation
- v5.1.0: Lazy evaluation with generators
- v5.2.0: Multi-layer memoization, logical operators
- v5.3.0: Initial framework integrations
- v5.4.0: Full React, Vue support
- v5.5.0: Array OR syntax, visual debugging, playground
- v5.6.0: Geospatial operators, datetime operators
- v5.7.0: Angular, SolidJS, Preact integrations
- v5.8.0: OrderBy and limit configuration options
- v5.8.2: Current stable version with comprehensive documentation