Skip to content

Commit c05c326

Browse files
giovaborgognoclaude
andcommitted
fix(events): phase 13 — LOW audit fixes (cache bounds)
- 13.1: Add LRU eviction to validatorCache, filterCache, patternCache (max 1000) - 13.2: Zod schema tightening done in phase 10 commit Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 6d4434e commit c05c326

File tree

3 files changed

+29
-1
lines changed

3 files changed

+29
-1
lines changed

apps/webapp/app/v3/services/events/schemaRegistry.server.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import { BaseService, ServiceValidationError } from "../baseService.server";
66

77
const ajv = new Ajv({ allErrors: true, strict: false });
88

9-
/** Cached compiled validators keyed by EventDefinition.id */
9+
/** Cached compiled validators keyed by EventDefinition.id (bounded to prevent memory leaks) */
1010
const validatorCache = new Map<string, ValidateFunction>();
11+
const VALIDATOR_CACHE_MAX = 1000;
1112

1213
export type SchemaValidationResult =
1314
| { success: true }
@@ -176,6 +177,15 @@ export class SchemaRegistryService extends BaseService {
176177

177178
if (!validate) {
178179
validate = ajv.compile(schema as object);
180+
if (validatorCache.size >= VALIDATOR_CACHE_MAX) {
181+
// Evict oldest entries (Map iterates in insertion order)
182+
const toDelete = Math.floor(VALIDATOR_CACHE_MAX / 2);
183+
let i = 0;
184+
for (const key of validatorCache.keys()) {
185+
if (i++ >= toDelete) break;
186+
validatorCache.delete(key);
187+
}
188+
}
179189
validatorCache.set(eventDefinitionId, validate);
180190
}
181191

packages/core/src/v3/events/filterEvaluator.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { eventFilterMatches } from "../../eventFilterMatches.js";
44
type CompiledFilter = (payload: unknown) => boolean;
55

66
const filterCache = new Map<string, CompiledFilter>();
7+
const FILTER_CACHE_MAX = 1000;
78

89
/**
910
* Compile an EventFilter into a reusable predicate function.
@@ -20,6 +21,14 @@ export function compileFilter(filter: EventFilter, cacheKey?: string): CompiledF
2021
const fn: CompiledFilter = (payload: unknown) => eventFilterMatches(payload, filter);
2122

2223
if (cacheKey) {
24+
if (filterCache.size >= FILTER_CACHE_MAX) {
25+
const toDelete = Math.floor(FILTER_CACHE_MAX / 2);
26+
let i = 0;
27+
for (const key of filterCache.keys()) {
28+
if (i++ >= toDelete) break;
29+
filterCache.delete(key);
30+
}
31+
}
2332
filterCache.set(cacheKey, fn);
2433
}
2534

packages/core/src/v3/events/patternMatcher.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
type PatternPredicate = (eventSlug: string) => boolean;
1616

1717
const patternCache = new Map<string, PatternPredicate>();
18+
const PATTERN_CACHE_MAX = 1000;
1819

1920
/**
2021
* Compile a wildcard pattern into a reusable predicate.
@@ -25,6 +26,14 @@ export function compilePattern(pattern: string): PatternPredicate {
2526
if (cached) return cached;
2627

2728
const fn = buildPatternFn(pattern);
29+
if (patternCache.size >= PATTERN_CACHE_MAX) {
30+
const toDelete = Math.floor(PATTERN_CACHE_MAX / 2);
31+
let i = 0;
32+
for (const key of patternCache.keys()) {
33+
if (i++ >= toDelete) break;
34+
patternCache.delete(key);
35+
}
36+
}
2837
patternCache.set(pattern, fn);
2938
return fn;
3039
}

0 commit comments

Comments
 (0)