|
1 | 1 | 'use strict' |
2 | 2 |
|
3 | | -import { ESQuery, CachedQuery } from '../types' |
| 3 | +import { ESQuery } from '../types' |
4 | 4 | import { getType, validateType } from './core' |
5 | 5 | import { errors } from '@feathersjs/errors' |
6 | 6 | import { $or, $and, $all, $sqs, $nested, $childOr$parent, $existsOr$missing } from './query-handlers/special' |
7 | 7 | import { processCriteria, processTermQuery } from './query-handlers/criteria' |
| 8 | +import { createHash } from 'crypto' |
8 | 9 |
|
9 | | -// Query cache for performance |
10 | | -const queryCache = new WeakMap<Record<string, unknown>, CachedQuery>() |
| 10 | +// Content-based query cache for performance |
| 11 | +// Uses Map with hash keys for better hit rate vs WeakMap with object references |
| 12 | +const queryCache = new Map<string, { result: ESQuery | null; timestamp: number }>() |
| 13 | +const CACHE_MAX_SIZE = 1000 |
| 14 | +const CACHE_MAX_AGE = 5 * 60 * 1000 // 5 minutes |
| 15 | + |
| 16 | +/** |
| 17 | + * Generate a stable hash for a query object |
| 18 | + * @param query - Query object to hash |
| 19 | + * @param idProp - ID property name |
| 20 | + * @returns Hash string |
| 21 | + */ |
| 22 | +function hashQuery(query: Record<string, unknown>, idProp: string): string { |
| 23 | + // Create deterministic string representation |
| 24 | + const normalized = JSON.stringify(query, Object.keys(query).sort()) |
| 25 | + return createHash('sha256').update(`${normalized}:${idProp}`).digest('hex').slice(0, 16) |
| 26 | +} |
| 27 | + |
| 28 | +/** |
| 29 | + * Clean expired cache entries |
| 30 | + */ |
| 31 | +function cleanCache(): void { |
| 32 | + const now = Date.now() |
| 33 | + const toDelete: string[] = [] |
| 34 | + |
| 35 | + for (const [key, entry] of queryCache.entries()) { |
| 36 | + if (now - entry.timestamp > CACHE_MAX_AGE) { |
| 37 | + toDelete.push(key) |
| 38 | + } |
| 39 | + } |
| 40 | + |
| 41 | + toDelete.forEach((key) => queryCache.delete(key)) |
| 42 | + |
| 43 | + // If still over max size, remove oldest entries |
| 44 | + if (queryCache.size > CACHE_MAX_SIZE) { |
| 45 | + const entries = Array.from(queryCache.entries()).sort((a, b) => a[1].timestamp - b[1].timestamp) |
| 46 | + |
| 47 | + const toRemove = entries.slice(0, queryCache.size - CACHE_MAX_SIZE) |
| 48 | + toRemove.forEach(([key]) => queryCache.delete(key)) |
| 49 | + } |
| 50 | +} |
11 | 51 |
|
12 | 52 | type QueryHandler = ( |
13 | 53 | value: unknown, |
@@ -57,17 +97,27 @@ export function parseQuery( |
57 | 97 | return null |
58 | 98 | } |
59 | 99 |
|
60 | | - // Check cache first |
61 | | - const cached = queryCache.get(query) |
62 | | - if (cached && cached.query === query) { |
63 | | - return cached.result |
| 100 | + // Check content-based cache first (only for root level queries) |
| 101 | + if (currentDepth === 0) { |
| 102 | + const cacheKey = hashQuery(query, idProp) |
| 103 | + const cached = queryCache.get(cacheKey) |
| 104 | + |
| 105 | + if (cached) { |
| 106 | + // Return cached result (deep clone to prevent mutations) |
| 107 | + return cached.result ? JSON.parse(JSON.stringify(cached.result)) : null |
| 108 | + } |
64 | 109 | } |
65 | 110 |
|
66 | 111 | // Validate query depth to prevent stack overflow attacks |
67 | 112 | if (currentDepth > maxDepth) { |
68 | 113 | throw new errors.BadRequest(`Query nesting exceeds maximum depth of ${maxDepth}`) |
69 | 114 | } |
70 | 115 |
|
| 116 | + // Periodically clean cache (every ~100 queries) |
| 117 | + if (currentDepth === 0 && Math.random() < 0.01) { |
| 118 | + cleanCache() |
| 119 | + } |
| 120 | + |
71 | 121 | const bool = Object.entries(query).reduce((result: ESQuery, [key, value]) => { |
72 | 122 | const type = getType(value) |
73 | 123 |
|
@@ -95,8 +145,14 @@ export function parseQuery( |
95 | 145 |
|
96 | 146 | const queryResult = Object.keys(bool).length ? bool : null |
97 | 147 |
|
98 | | - // Cache the result |
99 | | - queryCache.set(query, { query: query as never, result: queryResult }) |
| 148 | + // Cache the result (only for root level queries) |
| 149 | + if (currentDepth === 0) { |
| 150 | + const cacheKey = hashQuery(query, idProp) |
| 151 | + queryCache.set(cacheKey, { |
| 152 | + result: queryResult, |
| 153 | + timestamp: Date.now() |
| 154 | + }) |
| 155 | + } |
100 | 156 |
|
101 | 157 | return queryResult |
102 | 158 | } |
0 commit comments