diff --git a/src/validation/ajv-provider.ts b/src/validation/ajv-provider.ts index 3c2967c3a1..3e9a6a14d7 100644 --- a/src/validation/ajv-provider.ts +++ b/src/validation/ajv-provider.ts @@ -35,6 +35,7 @@ function createDefaultAjvInstance(): Ajv { */ export class AjvJsonSchemaValidator implements jsonSchemaValidator { private _ajv: Ajv; + private _compiledCache: Map> /** * Create an AJV validator @@ -58,6 +59,7 @@ export class AjvJsonSchemaValidator implements jsonSchemaValidator { */ constructor(ajv?: Ajv) { this._ajv = ajv ?? createDefaultAjvInstance(); + this._compiledCache = new Map(); } /** @@ -70,11 +72,7 @@ export class AjvJsonSchemaValidator implements jsonSchemaValidator { * @returns A validator function that validates input data */ getValidator(schema: JsonSchemaType): JsonSchemaValidator { - // Check if schema has $id and is already compiled/cached - const ajvValidator = - '$id' in schema && typeof schema.$id === 'string' - ? (this._ajv.getSchema(schema.$id) ?? this._ajv.compile(schema)) - : this._ajv.compile(schema); + const ajvValidator = this._getCompiled(schema); return (input: unknown): JsonSchemaValidatorResult => { const valid = ajvValidator(input); @@ -94,4 +92,22 @@ export class AjvJsonSchemaValidator implements jsonSchemaValidator { } }; } + + private _getCompiled(schema: JsonSchemaType) { + if ('$id' in schema && typeof schema.$id === 'string') { + return this._ajv.getSchema(schema.$id) ?? this._ajv.compile(schema); + } + let key: string; + try { + key = JSON.stringify(schema); + } catch { + return this._ajv.compile(schema); + } + let cached = this._compiledCache.get(key); + if (cached === undefined) { + cached = this._ajv.compile(JSON.parse(key)); + this._compiledCache.set(key, cached); + } + return cached; + } } diff --git a/test/validation/validation.test.ts b/test/validation/validation.test.ts index b9bba258a5..33f8ef762a 100644 --- a/test/validation/validation.test.ts +++ b/test/validation/validation.test.ts @@ -531,6 +531,62 @@ describe('JSON Schema Validators', () => { }); }); +describe('AJV caching', () => { + it('caches compiled validators for identical schemas without $id', () => { + const validator = new AjvJsonSchemaValidator(); + const schema: JsonSchemaType = { + type: 'object', + properties: { name: { type: 'string' } }, + required: ['name'] + }; + + // Get validator multiple times for the same schema + const v1 = validator.getValidator(schema); + const v2 = validator.getValidator(schema); + + // Both should produce the same results + expect(v1({ name: 'test' })).toEqual(v2({ name: 'test' })); + expect(v1({})).toEqual(v2({})); + }); + + it('caches validators for schemas with same content but different references', () => { + const validator = new AjvJsonSchemaValidator(); + const schema1: JsonSchemaType = { type: 'string' }; + const schema2: JsonSchemaType = { type: 'string' }; + + const v1 = validator.getValidator(schema1); + const v2 = validator.getValidator(schema2); + + expect(v1('test')).toEqual(v2('test')); + expect(v1(42)).toEqual(v2(42)); + }); + + it('returns different validators for different schemas', () => { + const validator = new AjvJsonSchemaValidator(); + const schema1: JsonSchemaType = { type: 'string' }; + const schema2: JsonSchemaType = { type: 'number' }; + + const v1 = validator.getValidator(schema1); + const v2 = validator.getValidator(schema2); + + expect(v1('test').valid).toBe(true); + expect(v2('test').valid).toBe(false); + }); + + it('compiles each unique schema only once', () => { + const validator = new AjvJsonSchemaValidator(); + const schema: JsonSchemaType = { type: 'string' }; + + // Call getValidator 5 times with same schema + for (let i = 0; i < 5; i++) { + validator.getValidator(schema)(i === 0 ? 'test' : 'other'); + } + + // The cache should have exactly 1 entry + expect((validator as any)._compiledCache.size).toBe(1); + }); +}); + describe('Missing dependencies', () => { describe('AJV not installed but CfWorker is', () => { beforeEach(() => {