⚡ perf: memoize per-class plans; add depth guard and per-index each reporting
Profiling the validator showed roughly half of all validation time re-deriving
answers that cannot change — collectConstraints 22%, getOwnMetadata 12%,
getMetadataChain 9%, getProperties 4%, getMetadata 3%, plus 8% GC from the
allocation churn. The constraint predicates themselves were under 1%.
Decorator metadata is fixed once classes are declared, so the derived structures
are now memoized per prototype: the validation plan, the serialization plan, the
deserialization plan, and serializer/deserializer instances, which were being
constructed fresh for every property of every object. MetadataStorage carries a
version counter that invalidates the caches when metadata is written, so
registerDecorator after first use still works — covered by a test.
Measured against JSON.parse + JSON.stringify as a fixed reference:
validate (50 orders) 221.6 us -> 49.6 us 4.5x
validate (10 orders) 47.8 us -> 12.9 us 3.7x
toInstance (50 orders) 255.1 us -> 74.0 us 3.4x
toInstance (10 orders) 64.6 us -> 19.0 us 3.4x
toPlain (50 orders) 294.4 us -> 95.3 us 3.1x
Reliability, in the same pass:
- maxDepth option (default 64) on every mapping function, on validate(), and on
configure(). All three engines recurse, so a payload nested thousands of levels
deep could exhaust the call stack. Cycles were already handled; legitimate deep
nesting was not bounded.
- each: true failures now name the element that failed ("failed at index 3"). A
bad entry in a 200-item array previously produced a message that could not
locate it. A message function now receives the failing element as args.value
rather than the whole array; caller-supplied strings stay verbatim.
150 tests (up from 136), all green on the existing suite unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAcqrz3FcadkYr3xG32CjK
This commit is contained in:
+11
-1
@@ -27,15 +27,24 @@ export interface TransformOptions {
|
||||
|
||||
/** What to do with incoming keys that match no declared property. Deserialization only. */
|
||||
unknownKeys?: UnknownKeyPolicy;
|
||||
|
||||
/**
|
||||
* Maximum nesting depth before a {@link JsonMappingError} is raised. Defaults to 64.
|
||||
*
|
||||
* All three engines recurse, so a hostile payload nested thousands of levels deep would
|
||||
* otherwise exhaust the call stack. Raise it if you legitimately model deep trees.
|
||||
*/
|
||||
maxDepth?: number;
|
||||
}
|
||||
|
||||
/** Options that can be set once for the whole application via {@link configure}. */
|
||||
export type GlobalOptions = Pick<TransformOptions, 'namingStrategy' | 'unknownKeys' | 'validate'>;
|
||||
export type GlobalOptions = Pick<TransformOptions, 'namingStrategy' | 'unknownKeys' | 'validate' | 'maxDepth'>;
|
||||
|
||||
const DEFAULTS: Required<GlobalOptions> = {
|
||||
namingStrategy: 'identity',
|
||||
unknownKeys: 'allow',
|
||||
validate: true,
|
||||
maxDepth: 64,
|
||||
};
|
||||
|
||||
let globalOptions: Required<GlobalOptions> = { ...DEFAULTS };
|
||||
@@ -71,5 +80,6 @@ export function resolveOptions(options?: TransformOptions): Required<GlobalOptio
|
||||
namingStrategy: options.namingStrategy ?? globalOptions.namingStrategy,
|
||||
unknownKeys: options.unknownKeys ?? globalOptions.unknownKeys,
|
||||
validate: options.validate ?? globalOptions.validate,
|
||||
maxDepth: options.maxDepth ?? globalOptions.maxDepth,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user