ata-validator 1.41.0 → 1.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -135,10 +135,15 @@ function install(deps) {
135
135
  // Rollback marks: the two array lengths and the contiguous count.
136
136
  function pLen(rec) { return rec.props === null ? 0 : rec.props.length; }
137
137
  function xLen(rec) { return rec.x === null ? 0 : rec.x.length; }
138
+ // Writing an array's length is a slow path in V8 even when it does not
139
+ // change it, and most undos change nothing: a branch that failed before
140
+ // marking anything. So the length is written only when it moves.
138
141
  function undo(rec, pl, n0, xl) {
139
- if (rec.props !== null) rec.props.length = pl;
142
+ const p = rec.props;
143
+ if (p !== null && p.length !== pl) p.length = pl;
140
144
  rec.n = n0;
141
- if (rec.x !== null) rec.x.length = xl;
145
+ const x = rec.x;
146
+ if (x !== null && x.length !== xl) x.length = xl;
142
147
  }
143
148
 
144
149
  // The value-level keywords of one node, compiled. evalLeafV() answers the
package/lib/rejections.js CHANGED
@@ -261,9 +261,26 @@ const COLLAPSE_CODES = new Set(['ATA4001', 'ATA4002', 'ATA4003']);
261
261
  function stripOrdinal(e) {
262
262
  if (e === null || typeof e !== 'object') return e;
263
263
  // Generated code keeps errors at a fixed path as frozen literals shared by
264
- // every call. The caller gets its own copy, as it always has, so an error it
265
- // edits (a translated message) is its own and editing it does not throw.
266
- if (e._o === undefined && e.docUrl === undefined && (e.code === undefined || COLLAPSE_CODES.has(e.code)) && !e.branchErrors) return Object.isFrozen(e) ? { ...e } : e;
264
+ // every call, marked with a non-enumerable `_s` (see sharedErr in
265
+ // lib/js-compiler.js). The caller gets its own copy, as it always has, so an
266
+ // error it edits (a translated message) is its own and editing it does not
267
+ // throw. Those literals hold the five keys of this shape and nothing else
268
+ // enumerable, so a literal of the same shape copies them: spreading one cost
269
+ // twice as much, and Object.isFrozen, which used to pick them out, more than
270
+ // the copy itself.
271
+ if (e._s === true && e.code === undefined && e.docUrl === undefined && e.branchErrors === undefined) {
272
+ return { keyword: e.keyword, instancePath: e.instancePath, schemaPath: e.schemaPath, params: e.params, message: e.message };
273
+ }
274
+ // A collapsed oneOf/anyOf error has one of the two shapes __ataCollapse in
275
+ // lib/branch-collapse.js builds, the only place these codes come from. Its
276
+ // fields are copied by name, in that order, without the ordinal: walking
277
+ // its keys with for-in was most of the cost of reading such an error.
278
+ if (e.path !== undefined && e.docUrl === undefined && COLLAPSE_CODES.has(e.code)) {
279
+ const out = { code: e.code, keyword: e.keyword, instancePath: e.instancePath, path: e.path, schemaPath: e.schemaPath, message: e.message, params: e.params };
280
+ if ('branchErrors' in e) out.branchErrors = Array.isArray(e.branchErrors) ? e.branchErrors.map(stripOrdinal) : e.branchErrors;
281
+ return out;
282
+ }
283
+ if (e._o === undefined && e.docUrl === undefined && (e.code === undefined || COLLAPSE_CODES.has(e.code)) && !e.branchErrors) return e._s === true ? { ...e } : e;
267
284
  const out = {};
268
285
  for (const k in e) {
269
286
  if (k === '_o' || k === 'docUrl') continue;
@@ -90,6 +90,22 @@ const BASE_CHANGING = new Set(['$id', '$anchor', '$dynamicAnchor', '$dynamicRef'
90
90
  // it. Sized for a node per declared property of a large generated schema,
91
91
  // not just for hand-written ones.
92
92
  const MAX_NODES = 8192;
93
+ // See the size checks in compileScanner. One function past 64 KB is what V8's
94
+ // Maglev tier aborted on: SchemaStore scanners of 0.5 to 1 MB, single
95
+ // functions before references became calls. The total bounds the memory a
96
+ // scanner takes.
97
+ const MAX_FUNCTION_SOURCE = 64 * 1024;
98
+ const MAX_SOURCE = 2 * 1024 * 1024;
99
+ // A reference target is expanded at each reference while its expansions
100
+ // together are at most this much source, and called otherwise.
101
+ const INLINE_REF_MAX = 4096;
102
+ // An object member whose scan is more than this much source is moved into a
103
+ // function of its own (emitMember).
104
+ const SPLIT_MIN = 4 * 1024;
105
+ // The member source an object keeps inline before the rest move out.
106
+ const SPLIT_OBJECT = 16 * 1024;
107
+ // Declared names at least this long on average find a key's end with indexOf.
108
+ const INDEX_KEY_MIN_AVG = 10;
93
109
 
94
110
  // Each position in a tuple emits its own branch inside the element loop, so a
95
111
  // long one is both a lot of source and a linear test per element.
@@ -155,9 +171,19 @@ function resolveLocalRef(root, ref) {
155
171
  if (ref === '#') return root;
156
172
  if (!ref.startsWith('#/')) decline('$ref:not-local');
157
173
  let node = root;
174
+ let first = true;
158
175
  for (const rawToken of ref.slice(2).split('/')) {
159
- const token = decodeURIComponent(rawToken).split('~1').join('/').split('~0').join('~');
176
+ let token = decodeURIComponent(rawToken).split('~1').join('/').split('~0').join('~');
160
177
  if (node === null || typeof node !== 'object') decline('$ref:unresolved');
178
+ // Draft 7 schemas reach the scanner with `definitions` renamed to `$defs`
179
+ // (lib/draft7.js) and their references as written; the other engines
180
+ // read either name, and so does this. Without it no draft 7 schema that
181
+ // used a definition got a scanner.
182
+ if (first && node === root && !Array.isArray(node) && !Object.prototype.hasOwnProperty.call(node, token)) {
183
+ if (token === 'definitions' && Object.prototype.hasOwnProperty.call(node, '$defs')) token = '$defs';
184
+ else if (token === '$defs' && Object.prototype.hasOwnProperty.call(node, 'definitions')) token = 'definitions';
185
+ }
186
+ first = false;
161
187
  if (Array.isArray(node)) {
162
188
  if (!/^\d+$/.test(token)) decline('$ref:unresolved');
163
189
  node = node[Number(token)];
@@ -387,11 +413,45 @@ function spanHash(name) {
387
413
  // is a jump table.
388
414
  const HASH_DISPATCH_MIN = 48;
389
415
 
416
+ // The scan is first emitted as one function with every reference expanded
417
+ // where it is used, which is the fastest form: a call per member measured
418
+ // about twice the cost of csslintrc's scan, a small enum referenced thirty
419
+ // times. Only when a function would pass MAX_FUNCTION_SOURCE is it emitted
420
+ // again with large reference targets and large object members moved into
421
+ // functions of their own, so the source grows with the schema rather than
422
+ // with the number of references to it.
390
423
  function compileScanner(schema, options) {
391
424
  const opts = options || {};
425
+ const whole = build(schema, opts, false);
426
+ if (whole !== null || declineReason !== 'size') return finish(whole, opts);
427
+ return finish(build(schema, opts, true), opts);
428
+ }
429
+
430
+ function finish(built, opts) {
431
+ if (built === null) {
432
+ if (opts.onDecline) opts.onDecline(declineReason);
433
+ return null;
434
+ }
435
+ const { ctx, body } = built;
436
+ let fn;
437
+ try {
438
+ // eslint-disable-next-line no-new-func
439
+ fn = new Function('_h', '_r', ctx.subSource.join('\n') + '\nreturn function scan(s){\n' + body + '\n}')(ctx.helpers, require('./scan-runtime'));
440
+ } catch {
441
+ return null; // a runtime without code generation has no scanner, by design
442
+ }
443
+ return { scan: fn, source: body, helpers: ctx.helpers.length, functions: [body, ...ctx.subSource] };
444
+ }
445
+
446
+ function build(schema, opts, split) {
392
447
  const ctx = {
393
448
  uid: 0, helpers: [], userFormats: opts.userFormats || null,
394
449
  root: schema, refStack: [], nodes: 0,
450
+ // With `split`, reference targets map to null when expanded in place,
451
+ // else to the name of the function in subSource that scans one (see the
452
+ // $ref case in emitValue), and large members move out too (emitMember).
453
+ split, subs: new Map(), subSource: [], refCounts: split ? countRefs(schema) : null,
454
+ inlineBytes: new Map(),
395
455
  };
396
456
  declineReason = null;
397
457
  let body;
@@ -404,21 +464,19 @@ function compileScanner(schema, options) {
404
464
  out.push('if(i<len)return 0;');
405
465
  out.push('return 1;');
406
466
  body = out.join('\n');
467
+ // A function this large is a cost of its own, and V8's Maglev tier
468
+ // aborted the process compiling scanners of 500 KB and more (traefik,
469
+ // detekt from SchemaStore), in plain JavaScript, on Node 24 and 25, after
470
+ // about a hundred calls. Unsplit, past the bound the scan is emitted
471
+ // split; split, past either bound the schema has no scanner and the text
472
+ // path parses as it does without one.
473
+ if (body.length > MAX_FUNCTION_SOURCE || ctx.subSource.some((x) => x.length > MAX_FUNCTION_SOURCE)) decline('size');
474
+ if (body.length + ctx.subSource.reduce((n, x) => n + x.length, 0) > MAX_SOURCE) decline('size');
407
475
  } catch (e) {
408
- if (e === DECLINE) {
409
- if (opts.onDecline) opts.onDecline(declineReason);
410
- return null;
411
- }
476
+ if (e === DECLINE) return null;
412
477
  throw e;
413
478
  }
414
- let fn;
415
- try {
416
- // eslint-disable-next-line no-new-func
417
- fn = new Function('_h', '_r', 'return function scan(s){\n' + body + '\n}')(ctx.helpers, require('./scan-runtime'));
418
- } catch {
419
- return null; // a runtime without code generation has no scanner, by design
420
- }
421
- return { scan: fn, source: body, helpers: ctx.helpers.length };
479
+ return { ctx, body };
422
480
  }
423
481
 
424
482
  function emitValue(schema, ctx, out, depth) {
@@ -443,9 +501,51 @@ function emitValue(schema, ctx, out, depth) {
443
501
  const ref = schema.$ref;
444
502
  if (ctx.refStack.includes(ref)) decline('$ref:recursive');
445
503
  const target = resolveLocalRef(ctx.root, ref);
504
+ if (!ctx.split) {
505
+ ctx.refStack.push(ref);
506
+ emitValue(target, ctx, out, depth);
507
+ ctx.refStack.pop();
508
+ return;
509
+ }
510
+ // Split (see compileScanner): a target whose expansions add up to more
511
+ // than a little source is emitted once, as a function of its own, and
512
+ // called wherever it is referenced. Expanded at every reference, the
513
+ // source grows with the number of references rather than with the
514
+ // schema: SchemaStore's traefik and detekt reach the same definitions
515
+ // from hundreds of places, and their scanners came to 0.5 to 1 MB, which
516
+ // V8's Maglev tier aborted the process compiling.
517
+ const sub = ctx.subs.get(target);
518
+ if (sub !== undefined) {
519
+ if (sub === null) {
520
+ ctx.refStack.push(ref);
521
+ emitValue(target, ctx, out, depth);
522
+ ctx.refStack.pop();
523
+ } else {
524
+ emitSubCall(sub, ctx, out);
525
+ }
526
+ return;
527
+ }
528
+ const body = [];
446
529
  ctx.refStack.push(ref);
447
- emitValue(target, ctx, out, depth);
530
+ emitValue(target, ctx, body, depth);
448
531
  ctx.refStack.pop();
532
+ const text = body.join('\n');
533
+ // Expanded in place only while all its expansions together stay small:
534
+ // a small definition referenced from hundreds of places is source in
535
+ // hundreds of places.
536
+ if (text.length * (ctx.refCounts.get(ref) || 1) <= INLINE_REF_MAX) {
537
+ ctx.subs.set(target, null);
538
+ for (const line of body) out.push(line);
539
+ return;
540
+ }
541
+ const name = '_s' + (ctx.subSource.length + ctx.subs.size + ctx.uid++);
542
+ ctx.subs.set(target, name);
543
+ // The emitted scan of a value starts with `i` at its first character and
544
+ // `c` holding it, and ends with `i` past it; it answers 0 (invalid) or -1
545
+ // (bail) by returning. Here it returns the new position instead, which is
546
+ // at least 1, so the three cannot be confused.
547
+ ctx.subSource.push('function ' + name + '(s,i){var len=s.length,c=s.charCodeAt(i);\n' + text + '\nreturn i;}');
548
+ emitSubCall(name, ctx, out);
449
549
  return;
450
550
  }
451
551
 
@@ -478,6 +578,12 @@ function emitValue(schema, ctx, out, depth) {
478
578
  // present, not that it lists anything.
479
579
  let compositeAllowed = true;
480
580
  let compositeDelegate = null;
581
+ // The kinds the listed values have. A value of any other kind equals none
582
+ // of them, so its branch is left out and the value falls to the final
583
+ // `return 0`: csslintrc's `enum: [2, true, false]` used to scan strings
584
+ // only to reject them, at thirty references, and the dead branches alone
585
+ // pushed its scan past the size bound into calls.
586
+ let listedKinds = null;
481
587
  if (has('const') || has('enum')) {
482
588
  compositeAllowed = false;
483
589
  const listed = [];
@@ -486,6 +592,7 @@ function emitValue(schema, ctx, out, depth) {
486
592
  if (!Array.isArray(schema.enum)) decline('enum:not-array');
487
593
  for (const v of schema.enum) listed.push(v);
488
594
  }
595
+ listedKinds = new Set(listed.map((v) => (v === null ? 'null' : Array.isArray(v) ? 'array' : typeof v)));
489
596
  // An object or an array can only be compared as a whole. The scan steps
490
597
  // over that one subtree, parses its own text, and hands it to the check
491
598
  // the validator itself runs, so the document around it is still never
@@ -508,7 +615,7 @@ function emitValue(schema, ctx, out, depth) {
508
615
  + 'if(!' + compositeDelegate + '(_cv' + k + '))return 0;'
509
616
  + 'i=_ce' + k + ';c=s.charCodeAt(i);'];
510
617
  };
511
- if (allows(types, 'object')) {
618
+ if (allows(types, 'object') && (listedKinds === null || listedKinds.has('object'))) {
512
619
  if (compositeDelegate) branches.push(['c===123', compositeBranch()]);
513
620
  else if (!compositeAllowed) branches.push(['c===123', ['return 0;']]);
514
621
  else {
@@ -517,7 +624,7 @@ function emitValue(schema, ctx, out, depth) {
517
624
  branches.push(['c===123', b]);
518
625
  }
519
626
  }
520
- if (allows(types, 'array')) {
627
+ if (allows(types, 'array') && (listedKinds === null || listedKinds.has('array'))) {
521
628
  if (compositeDelegate) branches.push(['c===91', compositeBranch()]);
522
629
  else if (!compositeAllowed) branches.push(['c===91', ['return 0;']]);
523
630
  else {
@@ -526,22 +633,22 @@ function emitValue(schema, ctx, out, depth) {
526
633
  branches.push(['c===91', b]);
527
634
  }
528
635
  }
529
- if (allows(types, 'string')) {
636
+ if (allows(types, 'string') && (listedKinds === null || listedKinds.has('string'))) {
530
637
  const b = [];
531
638
  emitString(schema, ctx, b);
532
639
  branches.push(['c===34', b]);
533
640
  }
534
- if (allows(types, 'number')) {
641
+ if (allows(types, 'number') && (listedKinds === null || listedKinds.has('number'))) {
535
642
  const b = [];
536
643
  emitNumber(schema, ctx, b, types);
537
644
  branches.push(['c===45||(c>=48&&c<=57)', b]);
538
645
  }
539
- if (allows(types, 'boolean')) {
646
+ if (allows(types, 'boolean') && (listedKinds === null || listedKinds.has('boolean'))) {
540
647
  const b = [];
541
648
  emitLiteral(schema, ctx, b, 'boolean');
542
649
  branches.push(['c===116||c===102', b]);
543
650
  }
544
- if (allows(types, 'null')) {
651
+ if (allows(types, 'null') && (listedKinds === null || listedKinds.has('null'))) {
545
652
  const b = [];
546
653
  emitLiteral(schema, ctx, b, 'null');
547
654
  branches.push(['c===110', b]);
@@ -558,6 +665,42 @@ function emitValue(schema, ctx, out, depth) {
558
665
  out.push(parts.join('\n'));
559
666
  }
560
667
 
668
+ // How many times each `$ref` string occurs in the schema, for the expand or
669
+ // call decision. Schema positions and other values alike: a string that is not
670
+ // a reference is never looked up.
671
+ function countRefs(root) {
672
+ const counts = new Map();
673
+ const seen = new Set();
674
+ const walk = (n) => {
675
+ if (n === null || typeof n !== 'object' || seen.has(n)) return;
676
+ seen.add(n);
677
+ if (Array.isArray(n)) { for (const x of n) walk(x); return; }
678
+ if (typeof n.$ref === 'string') counts.set(n.$ref, (counts.get(n.$ref) || 0) + 1);
679
+ for (const k of Object.keys(n)) walk(n[k]);
680
+ };
681
+ walk(root);
682
+ return counts;
683
+ }
684
+
685
+ // A name with no character a JSON string would have to escape, so its raw
686
+ // text in a document is the name itself.
687
+ function plainName(name) {
688
+ for (let k = 0; k < name.length; k++) {
689
+ const c = name.charCodeAt(k);
690
+ if (c < 32 || c === 34 || c === 92) return false;
691
+ }
692
+ return true;
693
+ }
694
+
695
+ // A call answers invalid with the literal `return 0;`, as inline code does:
696
+ // an object member rewrites that text into its own failure path, which goes on
697
+ // scanning (a later duplicate key can replace the value), and a different
698
+ // spelling would end the whole scan instead.
699
+ function emitSubCall(name, ctx, out) {
700
+ const r = '_rq' + (ctx.uid++);
701
+ out.push('{var ' + r + '=' + name + '(s,i);if(' + r + '===-1)return -1;if(' + r + '===0)return 0;i=' + r + ';c=s.charCodeAt(i);}');
702
+ }
703
+
561
704
  // Any JSON value, validated as JSON and stepped over.
562
705
  function emitSkip(out) {
563
706
  out.push('{var _j=_r.skipValue(s,i);if(_j===-2)return -1;if(_j<0)return 0;i=_j;c=s.charCodeAt(i);}');
@@ -749,8 +892,27 @@ function emitArray(schema, ctx, out, depth) {
749
892
  // the rule reaches nested objects: an inner object's own verdict is a value
750
893
  // failure at the level above it.
751
894
  function emitMember(sub, ctx, out, depth, objId) {
752
- const lines = [];
895
+ let lines = [];
753
896
  emitValue(sub, ctx, lines, depth + 1);
897
+ // A large member value becomes a function of its own, so no one function
898
+ // grows with the whole schema: detekt nests hundreds of rule objects inline,
899
+ // no references involved. The value's code keeps its own labels and
900
+ // variables, so it moves as it is; the call answers invalid with
901
+ // `return 0;`, which the rewrite below turns into this member's failure.
902
+ // An object's members are judged together as well as one by one: thirty
903
+ // members just under SPLIT_MIN each would stay inline and add up to a
904
+ // function past the bound, so once the members kept inline in this object
905
+ // pass SPLIT_OBJECT, the rest move out whatever their size.
906
+ const size = lines.join('\n').length;
907
+ const kept = ctx.split ? (ctx.inlineBytes.get(objId) || 0) : 0;
908
+ if (ctx.split && (size > SPLIT_MIN || kept + size > SPLIT_OBJECT)) {
909
+ const name = '_s' + (ctx.subSource.length + ctx.subs.size + ctx.uid++);
910
+ ctx.subSource.push('function ' + name + '(s,i){var len=s.length,c=s.charCodeAt(i);\n' + lines.join('\n') + '\nreturn i;}');
911
+ lines = [];
912
+ emitSubCall(name, ctx, lines);
913
+ } else if (ctx.split) {
914
+ ctx.inlineBytes.set(objId, kept + size);
915
+ }
754
916
  const body = lines.join('\n');
755
917
  if (!body.includes('return 0;')) { out.push(body); return; }
756
918
  const label = '_mb' + (ctx.uid++);
@@ -828,10 +990,32 @@ function emitObject(schema, ctx, out, depth) {
828
990
  hashById = '_h[' + (ctx.helpers.length - 1) + ']';
829
991
  }
830
992
  }
831
- emitStringSpan(ctx, out, kn, false, hashById !== null);
832
- // A key written with an escape decodes to a name the raw span does not show,
833
- // so the dispatch below would read it as a different property.
834
- out.push('if(_x' + kn + ')return -1;');
993
+ // A key's end is found with indexOf, which is native, where the character
994
+ // loop below costs three compares a character: on csslintrc's 35 keys of
995
+ // twenty-odd characters the loop was most of the scan and made it slower
996
+ // than parsing. Every declared name is plain text, so a key that compares
997
+ // equal to one, character by character, is that name as written, and a
998
+ // key that does not is checked for an escape or a control character before
999
+ // it is read as additional. Either one bails, and so does a span that ends
1000
+ // on a backslash, since that quote may be escaped: the parser decides what
1001
+ // the key really is.
1002
+ // indexOf costs about the same whatever the key's length, the loop about a
1003
+ // nanosecond a character: on short keys the loop wins (problem-object's
1004
+ // `type`, `title`, `status` measured 10 to 15 percent slower by indexOf),
1005
+ // and from ten characters indexOf wins by 40 percent and more, so the
1006
+ // choice follows the declared names' average length.
1007
+ const indexKey = !useHash && names.length > 0 && names.every(plainName)
1008
+ && names.reduce((n, x) => n + x.length, 0) >= INDEX_KEY_MIN_AVG * names.length;
1009
+ if (indexKey) {
1010
+ // A quote with a backslash before it may be escaped, and then the key
1011
+ // goes on past it: bail before reading what follows as the colon.
1012
+ out.push('var _p' + kn + '=++i,_e' + kn + '=s.indexOf(\'"\',i);if(_e' + kn + '<0)return 0;if(s.charCodeAt(_e' + kn + '-1)===92)return -1;i=_e' + kn + ';c=s.charCodeAt(++i);');
1013
+ } else {
1014
+ emitStringSpan(ctx, out, kn, false, hashById !== null);
1015
+ // A key written with an escape decodes to a name the raw span does not
1016
+ // show, so the dispatch below would read it as a different property.
1017
+ out.push('if(_x' + kn + ')return -1;');
1018
+ }
835
1019
  out.push(WS);
836
1020
  out.push('if(c!==58)return 0;');
837
1021
  out.push('c=s.charCodeAt(++i);');
@@ -958,6 +1142,7 @@ function emitObject(schema, ctx, out, depth) {
958
1142
  additional.push(lines.join('\n').split('return 0;').join('return -1;'));
959
1143
  }
960
1144
 
1145
+ if (indexKey) additional.unshift('if(!_r.plainKey(s,_p' + kn + ',_e' + kn + '))return -1;');
961
1146
  out.push('switch(_kl' + kn + '){');
962
1147
  for (const [len, group] of byLen) {
963
1148
  out.push('case ' + len + ':{');
@@ -155,4 +155,16 @@ function span(s, p, e, escaped) {
155
155
  return JSON.parse(s.slice(p - 1, e + 1));
156
156
  }
157
157
 
158
- module.exports = { hex4, skipValue, skipString, skipNumber, span, MALFORMED, TOO_DEEP, MAX_SKIP_DEPTH };
158
+ // Whether the raw text of a key between `p` and `e` is its name as written:
159
+ // no escape, no control character. A scanner that found the key's end with
160
+ // indexOf calls this for a name it did not recognise; one it recognised was
161
+ // compared character by character against a name that has neither.
162
+ function plainKey(s, p, e) {
163
+ for (let k = p; k < e; k++) {
164
+ const c = s.charCodeAt(k);
165
+ if (c < 32 || c === 92) return false;
166
+ }
167
+ return true;
168
+ }
169
+
170
+ module.exports = { hex4, skipValue, skipString, skipNumber, span, plainKey, MALFORMED, TOO_DEEP, MAX_SKIP_DEPTH };
@@ -215,8 +215,27 @@ function codegenAvailable() {
215
215
  return _codegenAvailable;
216
216
  }
217
217
 
218
- // Schema compilation cache: same schema string -> reuse compiled functions
218
+ // Schema compilation cache: same schema string -> reuse compiled functions.
219
+ // It and the preprocess cache below are bounded, oldest out first. Unbounded,
220
+ // they held every schema's text, compiled functions and their sources for the
221
+ // life of the process: after compiling each of SchemaStore's schemas once and
222
+ // dropping the validators, 1.2 GB stayed on the heap. A schema compiled again
223
+ // after its entry left is compiled again, the cost of a miss and no more.
224
+ const CACHE_ENTRIES = 128;
225
+ const CACHE_KEY_CHARS = 16 * 1024 * 1024;
219
226
  const _compileCache = new Map();
227
+ // Adds an entry and drops the oldest past either bound. The key characters
228
+ // are counted on the map itself.
229
+ function _boundedSet(map, key, entry) {
230
+ if (!map.has(key)) map._chars = (map._chars || 0) + key.length;
231
+ map.set(key, entry);
232
+ while (map.size > CACHE_ENTRIES || (map._chars > CACHE_KEY_CHARS && map.size > 1)) {
233
+ const oldest = map.keys().next().value;
234
+ map.delete(oldest);
235
+ map._chars -= oldest.length;
236
+ }
237
+ }
238
+ function _compileCacheSet(key, entry) { _boundedSet(_compileCache, key, entry); }
220
239
  // Generated preprocess passes, keyed like _compileCache plus the options that
221
240
  // shape the pass. Only passes that exist are kept: a schema with nothing to
222
241
  // rewrite answers `null` quickly, and keeping that answer held its key, the
@@ -232,9 +251,16 @@ const VALID_RESULT = Object.freeze({ valid: true, errors: Object.freeze([]) });
232
251
  // How many calls a validator answers through its verdict function before its
233
252
  // validate() and validateJSON() compile the single-function hybrid.
234
253
  const HYBRID_TIER_CALLS = 64;
254
+ // A frozen error object shared between calls carries a non-enumerable `_s`, so
255
+ // the reader that hands errors out copies it (lib/rejections.js stripOrdinal)
256
+ // without asking Object.isFrozen, which costs more than the copy.
257
+ function _sharedError(e) {
258
+ Object.defineProperty(e, '_s', { value: true });
259
+ return Object.freeze(e);
260
+ }
235
261
  const ABORT_EARLY_RESULT = Object.freeze({
236
262
  valid: false,
237
- errors: Object.freeze([Object.freeze({
263
+ errors: Object.freeze([_sharedError({
238
264
  code: 'ATA9000',
239
265
  message: 'validation failed',
240
266
  keyword: '__abort_early__',
@@ -292,7 +318,7 @@ function resolveSchemaByPath(rootSchema, schemaPath) {
292
318
  //
293
319
  // A failing route sees the same handful of schemaPaths over and over, which is
294
320
  // what makes the first one worth having.
295
- const { LazyRejection, RichRejection, LazyJsonRejection, _enrichLazy, presentErrors, needsOrdering, sortErrorsBySchemaOrder } = require('./rejections');
321
+ const { LazyRejection, RichRejection, LazyJsonRejection, _enrichLazy, presentErrors, needsOrdering, sortErrorsBySchemaOrder, stripOrdinal } = require('./rejections');
296
322
 
297
323
  // Text that does not parse, reported the same way on every path and platform:
298
324
  // ATA9001 with the parser's own message. The native addon used to say only
@@ -904,7 +930,7 @@ class Validator {
904
930
  // A function bound to this validator's keyword functions or formats is
905
931
  // not the schema's alone, so it is not shared through the cache.
906
932
  if (!uf && !this._keywords) {
907
- _compileCache.set(mapKey, { jsFn, combined: undefined, errFn: undefined, isCodegen: _isCodegen, full: false });
933
+ _compileCacheSet(mapKey, { jsFn, combined: undefined, errFn: undefined, isCodegen: _isCodegen, full: false });
908
934
  }
909
935
  }
910
936
  this._jsFn = jsFn;
@@ -931,7 +957,7 @@ class Validator {
931
957
  if (hit !== undefined) preprocess = hit;
932
958
  else {
933
959
  preprocess = _codegen.buildPreprocess(preprocessSchema, options) || null;
934
- if (ppKey !== null && preprocess !== null) _preprocessCache.set(ppKey, preprocess);
960
+ if (ppKey !== null && preprocess !== null) _boundedSet(_preprocessCache, ppKey, preprocess);
935
961
  }
936
962
  }
937
963
  if (!preprocess) {
@@ -1026,14 +1052,27 @@ class Validator {
1026
1052
  // locals and on this object rather than on the instance: every call reads
1027
1053
  // `validate` off the instance, and three more own properties made that
1028
1054
  // read slower across the suite's validators.
1029
- let _rejectBase = null, _rejectPresented = null, _rejectPlain = false;
1055
+ let _rejectBase = null, _rejectPresented = null, _rejectPlain = false, _oneShotRich = null, _richBuilder = null, _oneShotPlain = false, _isCleanShape = null, _onePassOf = null, _isCombined = null;
1030
1056
  if (jsFn) {
1031
1057
  const _cg = {
1032
1058
  jsFn, _isCodegen, preprocess, fusedRemove, options, schemaObj, useSimdjsonForLarge, _buildCombined, _buildErr,
1033
- combined: () => jsCombinedFn, err: () => jsErrFn, rejectBase: null,
1059
+ combined: () => jsCombinedFn, err: () => jsErrFn, rejectBase: null, richBuilder: null, oneShotPlain: false, isCleanShape: null, isCombined: null, onePassOf: null,
1060
+ // The combined function with errors built enriched where they happen,
1061
+ // for a validator whose errors are read with richErrors on and no
1062
+ // schema source to attach frames from (see makeRich).
1063
+ buildRich: (this._richErrors && !this._schemaPositions && !this._usesKeywords)
1064
+ ? () => compileToJSCombined(schemaObj, VALID_RESULT, sm, this._userFormats, { runtimeShape: true, rich: true }) || null
1065
+ : null,
1066
+ oneShotRich: null,
1034
1067
  };
1035
1068
  _codegen.installPaths.call(this, _cg);
1036
1069
  _rejectBase = _cg.rejectBase;
1070
+ _oneShotRich = _cg.oneShotRich;
1071
+ _richBuilder = _cg.richBuilder;
1072
+ _oneShotPlain = _cg.oneShotPlain;
1073
+ _isCleanShape = _cg.isCleanShape;
1074
+ _onePassOf = _cg.onePassOf;
1075
+ _isCombined = _cg.isCombined;
1037
1076
  } else {
1038
1077
  // No JS codegen: the interpreted engine, with or without the addon. The
1039
1078
  // buffer APIs come from _installBufferApis on first use, as they do on
@@ -1264,10 +1303,67 @@ class Validator {
1264
1303
  }
1265
1304
  return _withExtra(raw, data);
1266
1305
  };
1267
- this.validate = (data) => {
1306
+ // validate() answers from the verdict and builds the errors when they
1307
+ // are read. What builds them improves once they are being read: from
1308
+ // the third read the combined function is called directly, without the
1309
+ // rejection layers, and with richErrors on it builds the errors
1310
+ // enriched where they fail (see makeRich in lib/enrich-site.js) rather
1311
+ // than plain and enriched afterwards. Built on the second read, not the
1312
+ // first, so a first rejection still compiles one generator. The rejection
1313
+ // holds `_errorsOf`, which dispatches to whichever builder is current.
1314
+ let _errorsNow = _buildErrors;
1315
+ let _onePass = null;
1316
+ let _richLater = (_plain !== null && _oneShotRich !== null && _richBuilder !== null && !_extra) ? 1 : 0;
1317
+ const _errorsOf = (data) => {
1318
+ if (_richLater !== 0 && ++_richLater > 2) {
1319
+ _richLater = 0;
1320
+ const combRich = _oneShotRich();
1321
+ if (combRich) {
1322
+ const clean = !this._richErrors && _isCleanShape !== null && _isCleanShape(combRich);
1323
+ _errorsNow = _richBuilder(combRich, this._schemaObj, (raw, d) => presentErrors(raw, d, null, this, this._schemaObj, this._richErrors ? _enrichLazy : null), _verdictFallback, _oneShotPlain, clean);
1324
+ _errorsOf.final = !_oneShotPlain;
1325
+ // Plain errors from the combined function: from here validate()
1326
+ // is that function in its result shape, one pass that decides,
1327
+ // collects and returns, instead of a verdict now and a second
1328
+ // pass when the errors are read. A caller reading only `.valid`
1329
+ // pays the collecting pass (about twice the verdict on a document
1330
+ // that fails); with richErrors on the errors stay lazy, since
1331
+ // enriching costs far more.
1332
+ // Collapsed branches are presented inside the one-pass function
1333
+ // (see onePassOf), so any plain combined function qualifies.
1334
+ // Plain errors only. With richErrors on, enriching is most of what
1335
+ // a read costs and the lazy path answers an accepted document from
1336
+ // the verdict, so one pass measured no faster on rejections and
1337
+ // slower on acceptances.
1338
+ const onePass = !this._richErrors && _onePassOf !== null && _isCombined !== null && _isCombined(combRich) ? _onePassOf(EMPTY_ERRORS, _plainValidate) : null;
1339
+ if (onePass) { _onePass = onePass; self.validate = onePass; }
1340
+ }
1341
+ }
1342
+ return _errorsNow(data);
1343
+ };
1344
+ _errorsOf.final = false;
1345
+ const _plainValidate = (data) => {
1268
1346
  if (_fast(data)) return { valid: true, data, errors: EMPTY_ERRORS };
1269
- return new LazyRejection(_buildErrors, data, _buildRawErrors);
1347
+ return new LazyRejection(_errorsOf, data, _buildRawErrors);
1270
1348
  };
1349
+ // Past its first calls, validate() is the generated checks returning
1350
+ // these same results directly (jsFn._resultFactory): no wrapper between
1351
+ // the caller and the checks, and allocation sites of its own. Compiled
1352
+ // only once the validator is in use, as the hybrid is.
1353
+ const self = this;
1354
+ const _factory = (_vx === null && _fast === this._fastVerdict && typeof _fast._resultFactory === 'function') ? _fast._resultFactory : null;
1355
+ if (_factory !== null) {
1356
+ let calls = 0;
1357
+ this.validate = (data) => {
1358
+ if (++calls === HYBRID_TIER_CALLS && _onePass === null) {
1359
+ const gen = _factory(LazyRejection, _errorsOf, _buildRawErrors, EMPTY_ERRORS);
1360
+ if (gen) self.validate = gen;
1361
+ }
1362
+ return _plainValidate(data);
1363
+ };
1364
+ } else {
1365
+ this.validate = _plainValidate;
1366
+ }
1271
1367
  }
1272
1368
  if (_vx && !_vxApplied && this.validate) {
1273
1369
  const inner = this.validate;
@@ -1751,7 +1847,7 @@ Object.defineProperty(Validator.prototype, "~standard", {
1751
1847
  // reported as a generic failure rather than hidden.
1752
1848
  const _VERDICT_DISAGREES = Object.freeze({
1753
1849
  valid: false,
1754
- errors: Object.freeze([Object.freeze({ keyword: 'validation', instancePath: '', schemaPath: '#', params: Object.freeze({}), message: 'schema validation failed' })]),
1850
+ errors: Object.freeze([_sharedError({ keyword: 'validation', instancePath: '', schemaPath: '#', params: Object.freeze({}), message: 'schema validation failed' })]),
1755
1851
  });
1756
1852
  // The error builders of the common rejection path (see _ensureCompiled): run
1757
1853
  // the error function behind `reject` for a document the verdict rejected, and
@@ -1764,9 +1860,12 @@ function _plainBuilders(reject, enrich, root, self, fallback) {
1764
1860
  };
1765
1861
  return {
1766
1862
  errors: (data) => presentErrors(rawOf(data), data, null, self, root, enrich),
1863
+ // The raw list in the plain shape: a collapsed branch error carried its
1864
+ // ordering key out to `_ataRaw()` callers, and a shared literal was
1865
+ // handed out as it is.
1767
1866
  raw: (data) => {
1768
1867
  const raw = rawOf(data);
1769
- return needsOrdering(raw) ? sortErrorsBySchemaOrder(root, raw) : raw;
1868
+ return (needsOrdering(raw) ? sortErrorsBySchemaOrder(root, raw) : raw).map(stripOrdinal);
1770
1869
  },
1771
1870
  };
1772
1871
  }
@@ -1859,7 +1958,7 @@ function _jsonEntryWrappers({ check, errors }) {
1859
1958
  },
1860
1959
  };
1861
1960
  }
1862
- const _EXT_FALLBACK = Object.freeze({ keyword: 'validation', instancePath: '', schemaPath: '#', params: {}, message: 'schema validation failed' });
1961
+ const _EXT_FALLBACK = _sharedError({ keyword: 'validation', instancePath: '', schemaPath: '#', params: {}, message: 'schema validation failed' });
1863
1962
 
1864
1963
  // A verdict function that also runs `tail` on what it accepts. The generated
1865
1964
  // function can take the check in place of its final `return true`, one call
@@ -2089,7 +2188,15 @@ _defineLazyMethod('validate', (self) => (data) => {
2089
2188
  self._ensureCompiled();
2090
2189
  return self.validate(data);
2091
2190
  });
2092
- _defineLazyMethod('isValidObject', (self) => (data) => {
2191
+ _defineLazyMethod('isValidObject', (self) => {
2192
+ // A caller can hold this stub itself: `const check = v.isValidObject`, or a
2193
+ // bound copy, taken before the first call. Once the first call has bound the
2194
+ // real method, the stub hands straight to it; it used to classify the schema
2195
+ // again on every call, about 200 ns, ten times the check.
2196
+ let bound = false;
2197
+ return (data) => {
2198
+ if (bound) return self.isValidObject(data);
2199
+ bound = true;
2093
2200
  // A validator that rewrites its input goes through the full compile, which
2094
2201
  // binds a verdict method that runs the rewrite first. So does one whose
2095
2202
  // schema uses a custom keyword: neither the tier-0 plan nor the code
@@ -2124,6 +2231,7 @@ _defineLazyMethod('isValidObject', (self) => (data) => {
2124
2231
  if (!self._jsFn) self._ensureCompiled();
2125
2232
  }
2126
2233
  return self.isValidObject(data);
2234
+ };
2127
2235
  });
2128
2236
  _defineLazyMethod('validateJSON', (self) => (jsonStr) => {
2129
2237
  self._ensureCompiled();
@@ -2170,4 +2278,4 @@ module.exports = {
2170
2278
  // surface of `require('ata-validator')` is what it was.
2171
2279
  Object.defineProperty(module.exports, '_registerCodegen', { value: _registerCodegen, enumerable: false });
2172
2280
  // What the compiled paths in index.js need from this module.
2173
- Object.defineProperty(module.exports, '_internals', { value: { _jsonSyntaxRejection, ABORT_EARLY_RESULT, HYBRID_TIER_CALLS, SIMDJSON_THRESHOLD, VALID_RESULT, _bindEntry, _bindVerdict, _compileCache, _mustReject, _rememberInstance, _verboseWrap, compileCacheKey, getNative, isV1Dialect, resolveSchemaByPath }, enumerable: false });
2281
+ Object.defineProperty(module.exports, '_internals', { value: { _jsonSyntaxRejection, ABORT_EARLY_RESULT, HYBRID_TIER_CALLS, SIMDJSON_THRESHOLD, VALID_RESULT, _bindEntry, _bindVerdict, _compileCache, _compileCacheSet, _mustReject, _rememberInstance, _verboseWrap, compileCacheKey, getNative, isV1Dialect, resolveSchemaByPath }, enumerable: false });