ata-validator 1.44.0 → 1.45.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.
package/README.md CHANGED
@@ -244,21 +244,21 @@ is the most common way to get a misleading number out of this library.
244
244
 
245
245
  | | compiled with `ata build` | runtime `new Validator(schema)` |
246
246
  |---|---|---|
247
- | In a bundle, gzipped | **2.1 KB** | 110.7 KB |
248
- | Time to a served request | **3.5 ms** | 8.2 ms |
247
+ | In a bundle, gzipped | **2.1 KB** | 112.8 KB |
248
+ | Time to a served request | **3.5 ms** | 8.6 ms |
249
249
  | Schema known when | build time | any time |
250
250
 
251
251
  The bundle row is the ten-field user schema in
252
252
  `tests/fixtures/error-dx/user.schema.json`, every export of the compiled module against
253
- `new Validator(schema)`, built with `bun build --minify --target=browser` on ata 1.43.0.
253
+ `new Validator(schema)`, built with `bun build --minify --target=browser` on ata 1.44.0.
254
254
  The startup row is a Hono route on Bun 1.4, the median of three rounds of best-of-seven, from
255
- `benchmark/bundle`, against 3.4 ms for the same app doing no validation at all, so the
255
+ `benchmark/bundle`, against 3.5 ms for the same app doing no validation at all, so the
256
256
  compiled path costs nothing measurable to start. The runtime figure is what it is because
257
257
  a schema that arrives at run time can use any keyword, so the whole engine has to be
258
258
  there. The compiled module imports nothing and contains only the checks your schema asks
259
259
  for.
260
260
 
261
- **On a server, use whichever fits your schemas.** 111 KB of JavaScript on a Node or Bun
261
+ **On a server, use whichever fits your schemas.** 113 KB of JavaScript on a Node or Bun
262
262
  process is not a cost anyone notices, and the runtime API is the simpler thing to reach
263
263
  for. Speed is the same either way once warm.
264
264
 
@@ -491,7 +491,7 @@ const v = new Validator(schema, {
491
491
 
492
492
  ### Build-time compile (`ata compile`)
493
493
 
494
- The `ata` CLI turns a JSON Schema file into a self-contained JavaScript module. No runtime dependency on `ata-validator`, so only the generated validator ships to the browser. For the ten-field user schema in `tests/fixtures/error-dx/user.schema.json` the module is 2.1 KB gzipped, full error detail included, against 110.7 KB for the runtime bundled for the browser (ata 1.43.0).
494
+ The `ata` CLI turns a JSON Schema file into a self-contained JavaScript module. No runtime dependency on `ata-validator`, so only the generated validator ships to the browser. For the ten-field user schema in `tests/fixtures/error-dx/user.schema.json` the module is 2.1 KB gzipped, full error detail included, against 112.8 KB for the runtime bundled for the browser (ata 1.44.0).
495
495
 
496
496
  ```bash
497
497
  npx ata compile schemas/user.json -o src/generated/user.validator.mjs
@@ -533,11 +533,11 @@ npx ata build 'schemas/*.json' --out-dir build/validators --check
533
533
  Run with `--watch` during development for incremental rebuilds.
534
534
 
535
535
  Bundle sizes for the 10-field user schema in `tests/fixtures/error-dx/user.schema.json`,
536
- minified and gzipped, measured with `bun build --minify --target=browser` on ata 1.43.0:
536
+ minified and gzipped, measured with `bun build --minify --target=browser` on ata 1.44.0:
537
537
 
538
538
  | What the app imports | Size | Notes |
539
539
  |---|---|---|
540
- | `Validator` from `ata-validator` | 110.7 KB | The compiler ships with it, because a runtime schema can use any keyword |
540
+ | `Validator` from `ata-validator` | 112.8 KB | The compiler ships with it, because a runtime schema can use any keyword |
541
541
  | `isValid` from the compiled module | **1.3 KB** | Nothing else is reachable, so the error collector is dropped |
542
542
  | `validate` from the compiled module | **1.9 KB** | Adds the detailed error collector |
543
543
 
@@ -575,7 +575,7 @@ through a `setFormats()` export the module carries. `docs/API.md` has the
575
575
  details.
576
576
 
577
577
  **Fastify startup, 10 route schemas, from a cold process to the first validated request:
578
- ajv 19.9 ms, ata 2.9 ms, no build step required.** ata registers in 0.5 ms of that and
578
+ ajv 19.4 ms, ata 2.7 ms, no build step required.** ata registers in 0.2 ms of that and
579
579
  compiles on the first request, so counting only registration would overstate the gap.
580
580
  Reproduce with `node benchmark/bench_fastify_boot.mjs`.
581
581
 
package/lib/formats.js CHANGED
@@ -382,12 +382,18 @@ function uriChar (c) {
382
382
  function isHex (c) {
383
383
  return (c >= 48 && c <= 57) || (c >= 97 && c <= 102) || (c >= 65 && c <= 70);
384
384
  }
385
+ // The same set through the URI_CHAR and HEX_CHAR tables: one load per
386
+ // character, where uriChar() was a call and up to eleven comparisons, and on
387
+ // SchemaStore's SARIF samples, full of URIs, that loop was a sixth of what the
388
+ // interpreted engine spent. A "%" at the end reads past the string, NaN,
389
+ // which fails the bound as it failed isHex().
385
390
  function uriChars (s, from) {
386
391
  for (let i = from; i < s.length; i++) {
387
392
  const c = s.charCodeAt(i);
388
- if (!uriChar(c)) return false;
393
+ if (c > 127 || URI_CHAR[c] === 0) return false;
389
394
  if (c === 37) { // '%'
390
- if (!isHex(s.charCodeAt(i + 1)) || !isHex(s.charCodeAt(i + 2))) return false;
395
+ const h1 = s.charCodeAt(i + 1), h2 = s.charCodeAt(i + 2);
396
+ if (!(h1 <= 127) || !(h2 <= 127) || HEX_CHAR[h1] === 0 || HEX_CHAR[h2] === 0) return false;
391
397
  i += 2;
392
398
  }
393
399
  }
@@ -183,26 +183,22 @@ function resolveRef(ref, fromBase, state) {
183
183
 
184
184
  // ---------- value helpers ----------
185
185
 
186
- function deepEqual(a, b) {
187
- if (a === b) return true;
188
- if (typeof a === 'number' && typeof b === 'number') return a === b;
189
- if (a === null || b === null || typeof a !== 'object' || typeof b !== 'object') return false;
190
- const aArr = Array.isArray(a);
191
- if (aArr !== Array.isArray(b)) return false;
192
- if (aArr) {
193
- if (a.length !== b.length) return false;
194
- for (let i = 0; i < a.length; i++) if (!deepEqual(a[i], b[i])) return false;
195
- return true;
196
- }
197
- const ak = Object.keys(a);
198
- const bk = Object.keys(b);
199
- if (ak.length !== bk.length) return false;
200
- for (const k of ak) {
201
- if (!(k in b) || !deepEqual(a[k], b[k])) return false;
202
- }
203
- return true;
186
+ // uniqueItems: the functions the code generator emits as _uq and _uqp
187
+ // (lib/unique-items.js), so both engines run the same code.
188
+ const { allDistinct, duplicatePair, _deq } = require('./unique-items');
189
+
190
+ // Whether `data` deep-equals one of the node's enum values (see enumPrims).
191
+ function enumMember(P, data) {
192
+ if (data === null || typeof data !== 'object') return P.enumPrims.has(data);
193
+ const objs = P.enumObjs;
194
+ for (let i = 0; i < objs.length; i++) if (deepEqual(objs[i], data)) return true;
195
+ return false;
204
196
  }
205
197
 
198
+ // JSON equality: the same function the code generator emits as `_deq`, so the
199
+ // engines agree on what equal means (an object's own keys, in any order).
200
+ const deepEqual = _deq;
201
+
206
202
  function codePointLength(s) {
207
203
  let n = 0;
208
204
  for (let i = 0; i < s.length; i++) {
@@ -375,6 +371,21 @@ class Plan {
375
371
  }
376
372
  this.hasType = schema.type !== undefined;
377
373
  this.enum = schema.enum !== undefined ? schema.enum : null;
374
+ // Membership for enumMember(): the primitive values in a Set, so a long
375
+ // enum answers in one lookup, and the object and array values, which only
376
+ // deepEqual can compare, in a list. Scanning every value with deepEqual
377
+ // was a fifth to a third of what validating SchemaStore's samples cost.
378
+ // NaN stays out of the Set: deepEqual never matches it, and a Set would.
379
+ this.enumPrims = null;
380
+ this.enumObjs = null;
381
+ if (Array.isArray(this.enum)) {
382
+ this.enumPrims = new Set();
383
+ this.enumObjs = [];
384
+ for (const v of this.enum) {
385
+ if (v !== null && typeof v === 'object') this.enumObjs.push(v);
386
+ else if (!(typeof v === 'number' && v !== v)) this.enumPrims.add(v);
387
+ }
388
+ }
378
389
  this.hasConst = schema.const !== undefined;
379
390
  this.const = schema.const;
380
391
 
@@ -524,9 +535,7 @@ function evalLeafV(P, data) {
524
535
  const bits = dataBits(data);
525
536
  if (P.hasType && (bits & P.typeMask) === 0) return false;
526
537
  if (P.enum !== null) {
527
- let found = false;
528
- for (let i = 0; i < P.enum.length; i++) { if (deepEqual(P.enum[i], data)) { found = true; break; } }
529
- if (!found) return false;
538
+ if (!enumMember(P, data)) return false;
530
539
  }
531
540
  if (P.hasConst && !deepEqual(P.const, data)) return false;
532
541
  if (P.hasNumber && Number.isFinite(data)) {
@@ -545,13 +554,7 @@ function evalLeafV(P, data) {
545
554
  if (P.hasArray && bits === T_ARRAY) {
546
555
  if (P.minItems !== undefined && data.length < P.minItems) return false;
547
556
  if (P.maxItems !== undefined && data.length > P.maxItems) return false;
548
- if (P.uniqueItems) {
549
- for (let i = 0; i < data.length; i++) {
550
- for (let j = i + 1; j < data.length; j++) {
551
- if (deepEqual(data[i], data[j])) return false;
552
- }
553
- }
554
- }
557
+ if (P.uniqueItems && !allDistinct(data)) return false;
555
558
  }
556
559
  if (P.hasObject && bits === T_OBJECT) {
557
560
  if (P.required !== null) {
@@ -635,9 +638,7 @@ function evalLeaf(P, data, errors, instancePath, schemaPath) {
635
638
  valid = false;
636
639
  }
637
640
  if (P.enum !== null) {
638
- let found = false;
639
- for (let i = 0; i < P.enum.length; i++) { if (deepEqual(P.enum[i], data)) { found = true; break; } }
640
- if (!found) {
641
+ if (!enumMember(P, data)) {
641
642
  if (errors !== NOERRORS) errors.push(err('enum', 'enum', instancePath, schemaPath + '/enum', { allowedValues: P.enum }, 'must be equal to one of the allowed values'));
642
643
  valid = false;
643
644
  }
@@ -663,14 +664,11 @@ function evalLeaf(P, data, errors, instancePath, schemaPath) {
663
664
  if (P.minItems !== undefined && data.length < P.minItems) { errors === NOERRORS || errors.push(err('minItems', 'minItems', instancePath, schemaPath + '/minItems', { limit: P.minItems }, `must NOT have fewer than ${P.minItems} items`)); valid = false; }
664
665
  if (P.maxItems !== undefined && data.length > P.maxItems) { errors === NOERRORS || errors.push(err('maxItems', 'maxItems', instancePath, schemaPath + '/maxItems', { limit: P.maxItems }, `must NOT have more than ${P.maxItems} items`)); valid = false; }
665
666
  if (P.uniqueItems) {
666
- outer: for (let i = 0; i < data.length; i++) {
667
- for (let j = i + 1; j < data.length; j++) {
668
- if (deepEqual(data[i], data[j])) {
669
- if (errors !== NOERRORS) errors.push(err('uniqueItems', 'uniqueItems', instancePath, schemaPath + '/uniqueItems', { i, j }, `must NOT have duplicate items (items ## ${j} and ${i} are identical)`));
670
- valid = false;
671
- break outer;
672
- }
673
- }
667
+ const pair = duplicatePair(data);
668
+ if (pair !== null) {
669
+ const i = pair[0], j = pair[1];
670
+ if (errors !== NOERRORS) errors.push(err('uniqueItems', 'uniqueItems', instancePath, schemaPath + '/uniqueItems', { i, j }, `must NOT have duplicate items (items ## ${j} and ${i} are identical)`));
671
+ valid = false;
674
672
  }
675
673
  }
676
674
  }
@@ -924,9 +922,7 @@ class Interpreter {
924
922
  valid = false;
925
923
  }
926
924
  if (P.enum !== null) {
927
- let found = false;
928
- for (let i = 0; i < P.enum.length; i++) { if (deepEqual(P.enum[i], data)) { found = true; break; } }
929
- if (!found) {
925
+ if (!enumMember(P, data)) {
930
926
  if (errors !== NOERRORS) errors.push(err('enum', 'enum', instancePath, schemaPath + '/enum', { allowedValues: P.enum }, 'must be equal to one of the allowed values'));
931
927
  valid = false;
932
928
  }
@@ -958,14 +954,11 @@ class Interpreter {
958
954
  if (P.minItems !== undefined && data.length < P.minItems) { errors === NOERRORS || errors.push(err('minItems', 'minItems', instancePath, schemaPath + '/minItems', { limit: P.minItems }, `must NOT have fewer than ${P.minItems} items`)); valid = false; }
959
955
  if (P.maxItems !== undefined && data.length > P.maxItems) { errors === NOERRORS || errors.push(err('maxItems', 'maxItems', instancePath, schemaPath + '/maxItems', { limit: P.maxItems }, `must NOT have more than ${P.maxItems} items`)); valid = false; }
960
956
  if (P.uniqueItems) {
961
- outer: for (let i = 0; i < data.length; i++) {
962
- for (let j = i + 1; j < data.length; j++) {
963
- if (deepEqual(data[i], data[j])) {
964
- if (errors !== NOERRORS) errors.push(err('uniqueItems', 'uniqueItems', instancePath, schemaPath + '/uniqueItems', { i, j }, `must NOT have duplicate items (items ## ${j} and ${i} are identical)`));
965
- valid = false;
966
- break outer;
967
- }
968
- }
957
+ const pair = duplicatePair(data);
958
+ if (pair !== null) {
959
+ const i = pair[0], j = pair[1];
960
+ if (errors !== NOERRORS) errors.push(err('uniqueItems', 'uniqueItems', instancePath, schemaPath + '/uniqueItems', { i, j }, `must NOT have duplicate items (items ## ${j} and ${i} are identical)`));
961
+ valid = false;
969
962
  }
970
963
  }
971
964
  const prefix = P.prefixItems;
@@ -1246,7 +1239,7 @@ function err(code, keyword, instancePath, schemaPath, params, message) {
1246
1239
 
1247
1240
  const { compileInterpreter } = require('./plan-compiler').install({
1248
1241
  Plan, NOERRORS, err, evalLeaf, evalLeafV, runCustomV, dataBits, escapePointer,
1249
- deepEqual, multipleOfOk, cpAtLeast, cpAtMost,
1242
+ deepEqual, enumMember, allDistinct, multipleOfOk, cpAtLeast, cpAtMost,
1250
1243
  T_STRING, T_ARRAY, T_OBJECT, resolveRef, splitFragment, resolveUri,
1251
1244
  });
1252
1245
 
@@ -10,12 +10,20 @@
10
10
  // forms answers a different question and costs a string per call.
11
11
  const DEQ_HELPER = "function _deq(a,b){if(a===b)return true;if(a===null||b===null||typeof a!=='object'||typeof b!=='object')return false;var aa=Array.isArray(a);if(aa!==Array.isArray(b))return false;var i;if(aa){if(a.length!==b.length)return false;for(i=0;i<a.length;i++)if(!_deq(a[i],b[i]))return false;return true}var ka=Object.keys(a);if(ka.length!==Object.keys(b).length)return false;for(i=0;i<ka.length;i++){var k=ka[i];if(!Object.prototype.hasOwnProperty.call(b,k)||!_deq(a[k],b[k]))return false}return true}"
12
12
 
13
- // uniqueItems helpers for the boolean generator. `_uq` answers whether every
14
- // item is distinct: short arrays compare pairwise and allocate nothing,
15
- // longer ones use a Set of primitives and fall back to sorted-key canonical
16
- // strings only once an object actually appears.
17
- const UQ_HELPERS = "function _cn(x){if(x===null||typeof x!=='object')return typeof x+':'+x;if(Array.isArray(x))return'['+x.map(_cn).join(',')+']';return'{'+Object.keys(x).sort().map(function(k){return JSON.stringify(k)+':'+_cn(x[k])}).join(',')+'}'}" +
18
- "function _uq(a){var n=a.length,i,k;if(n<2)return true;if(n<=12){for(i=1;i<n;i++)for(k=0;k<i;k++)if(_deq(a[i],a[k]))return false;return true}var s=new Set();for(i=0;i<n;i++){var x=a[i];if(x!==null&&typeof x==='object')break;if(s.has(x))return false;s.add(x)}if(i===n)return true;s=new Set();for(i=0;i<n;i++){var c=_cn(a[i]);if(s.has(c))return false;s.add(c)}return true}"
13
+ // uniqueItems helpers, emitted from the functions the interpreted engine
14
+ // calls (lib/unique-items.js): `_uq` answers whether every item is distinct.
15
+ // Loaded at the first schema with uniqueItems, not on every cold start.
16
+ // Comment lines and indentation are dropped: the text lands in every
17
+ // generated function and ahead-of-time module that checks uniqueItems.
18
+ let _uqSources = null
19
+ function uqSources () {
20
+ if (_uqSources === null) {
21
+ const u = require('./unique-items')
22
+ const compact = (fn) => String(fn).replace(/\n\s*\/\/[^\n]*/g, '').replace(/\n\s*/g, '\n')
23
+ _uqSources = { uq: compact(u._uqh) + compact(u._uq), uqp: 'var _uqpI=0;' + compact(u._uqp) }
24
+ }
25
+ return _uqSources
26
+ }
19
27
 
20
28
  // Where a generator hoists a declaration so it runs once per compiled
21
29
  // function rather than once per call. The boolean generator has a preamble
@@ -97,7 +105,7 @@ function emitPtrEsc(ctx) {
97
105
  // `_uq` compares items with `_deq`, so it can never be hoisted alone.
98
106
  function emitUq(ctx) {
99
107
  emitDeq(ctx)
100
- hoistOnce(ctx, '_uqHoisted', UQ_HELPERS)
108
+ hoistOnce(ctx, '_uqHoisted', uqSources().uq)
101
109
  return '_uq'
102
110
  }
103
111
 
@@ -107,15 +115,11 @@ function emitUq(ctx) {
107
115
  // a later equal, paired with its first later equal. That is the pair the
108
116
  // interpreted engine reports, and the generators used to report a different
109
117
  // one (the earliest repeat, paired with what it repeats), so the same document
110
- // named a different pair depending on the engine that answered. Short arrays
111
- // compare pairwise and allocate nothing; longer ones key a Map by value, or by
112
- // canonical string once an object appears. Building a canonical string for
113
- // every item on every call, as the generators did, made reading a uniqueItems
114
- // error up to ten times the cost of the verdict.
115
- const UQP_HELPER = "var _uqpI=0;function _uqp(a){var n=a.length,i,k;if(n<2)return -1;if(n<=12){for(i=0;i<n-1;i++)for(k=i+1;k<n;k++)if(_deq(a[i],a[k])){_uqpI=i;return k}return -1}var prim=true;for(i=0;i<n;i++){var x=a[i];if(x!==null&&typeof x==='object'){prim=false;break}}var m=new Map(),bi=-1,bj=-1;for(i=0;i<n;i++){var c=prim?a[i]:_cn(a[i]);var p=m.get(c);if(p===undefined)m.set(c,i);else if(p>=0){if(bi<0||p<bi){bi=p;bj=i}m.set(c,-1)}}if(bi<0)return -1;_uqpI=bi;return bj}"
118
+ // named a different pair depending on the engine that answered. See
119
+ // lib/unique-items.js for how it finds the pair.
116
120
  function emitUqp(ctx) {
117
121
  emitUq(ctx)
118
- hoistOnce(ctx, '_uqpHoisted', UQP_HELPER)
122
+ hoistOnce(ctx, '_uqpHoisted', uqSources().uqp)
119
123
  return '_uqp'
120
124
  }
121
125
 
@@ -3481,6 +3485,10 @@ function genUniqueItemsNode(schema, v, lines, ctx, knownType, hoisted, isArr, ty
3481
3485
  // up. The previous form built the canonical closure and a Set on
3482
3486
  // every call, even for [1, 2, 3].
3483
3487
  inner = `if(!${emitUq(ctx)}(${v}))return false`
3488
+ } else if (ctx.helperCode) {
3489
+ // Inside the error or combined function, whose helper code is emitted
3490
+ // into its source, the same hoisted helper.
3491
+ inner = `if(!${emitUq(ctx)}(${v}))return false`
3484
3492
  } else {
3485
3493
  inner = `const _cn${si}=function(x){if(x===null||typeof x!=='object')return typeof x+':'+x;if(Array.isArray(x))return'['+x.map(_cn${si}).join(',')+']';return'{'+Object.keys(x).sort().map(function(k){return JSON.stringify(k)+':'+_cn${si}(x[k])}).join(',')+'}'};const _s${si}=new Set();for(let _i=0;_i<${v}.length;_i++){const _k=_cn${si}(${v}[_i]);if(_s${si}.has(_k))return false;_s${si}.add(_k)}`
3486
3494
  }
@@ -5203,7 +5211,7 @@ function emitBranchCollapse (branches, keyword, v, pathExpr, lines, ctx, schemaP
5203
5211
  // In the combined function `_e` is created on the first error and every
5204
5212
  // error is collected (`_all` is true there), so the report only pushes.
5205
5213
  const report = ctx.inCombined && !ctx.inBranchFn
5206
- ? `const ${collapsed}=__ataCollapse('${branchKw}',${resArr},_pp${fi},'${branchSp}',${ord},_bt${fi});if(${collapsed})(_e||(_e=[])).push(${collapsed})`
5214
+ ? `const ${collapsed}=__ataCollapse('${branchKw}',${resArr},_pp${fi},'${branchSp}',${ord},_bt${fi});if(${collapsed})(_e=_ap(_e,${collapsed}))`
5207
5215
  : `const ${collapsed}=__ataCollapse('${branchKw}',${resArr},_pp${fi},'${branchSp}',${ord},_bt${fi});if(${collapsed}){_e.push(${collapsed});if(!_all)return{valid:false,errors:_e}}`
5208
5216
  // The node's runtime unevaluatedProperties already counted how many of
5209
5217
  // these branches pass, through the annotation functions, which answer the
@@ -5219,7 +5227,7 @@ function emitBranchCollapse (branches, keyword, v, pathExpr, lines, ctx, schemaP
5219
5227
  // branch errors, so it is built from the bits the count left.
5220
5228
  if (guard !== '' && keyword === 'oneOf' && counted.mask && ctx.inCombined && !ctx.inBranchFn) {
5221
5229
  const ord0 = ctx.noOrdinal ? 'null' : (() => { const o = ordinalFor(ctx.rootSchema, unescapeSp(branchSp)); return o === null ? 'null' : o })()
5222
- guard = `if(${same}&&${counted.count}>1){const _pi${fi}=[];for(let _k=0;_k<${branches.length};_k++)if((${counted.mask}>>_k)&1)_pi${fi}.push(_k);(_e||(_e=[])).push(__ataMulti(_pi${fi},${branches.length},${pp},'${branchSp}',${ord0}))}else if(!(${same}&&${counted.count}===1))`
5230
+ guard = `if(${same}&&${counted.count}>1){const _pi${fi}=[];for(let _k=0;_k<${branches.length};_k++)if((${counted.mask}>>_k)&1)_pi${fi}.push(_k);(_e=_ap(_e,__ataMulti(_pi${fi},${branches.length},${pp},'${branchSp}',${ord0})))}else if(!(${same}&&${counted.count}===1))`
5223
5231
  }
5224
5232
  // Without such a count, each branch also gets a verdict function, and the
5225
5233
  // passing branches are counted with those: a oneOf with exactly one and an
@@ -5280,7 +5288,7 @@ function emitBranchCollapse (branches, keyword, v, pathExpr, lines, ctx, schemaP
5280
5288
  ? `const _pi${fi}=[];for(let _k=0;_k<${L};_k++)if((_m${fi}>>_k)&1)_pi${fi}.push(_k);`
5281
5289
  : ''
5282
5290
  const multi = `__ataMulti(_pi${fi},${L},_pp${fi},'${branchSp}',${ord})`
5283
- const pushMulti = top ? `(_e||(_e=[])).push(${multi})` : `_e.push(${multi});if(!_all)return{valid:false,errors:_e}`
5291
+ const pushMulti = top ? `(_e=_ap(_e,${multi}))` : `_e.push(${multi});if(!_all)return{valid:false,errors:_e}`
5284
5292
  const count = tests.map((t, i) => `if(${t}){_n${fi}++;${mk ? `_m${fi}|=${1 << i}` : `_pi${fi}.push(${i})`}}`).join('')
5285
5293
  lines.push(`{const _pp${fi}=${pp};let _n${fi}=0${mk ? `,_m${fi}=0` : `,_pi${fi}=[]`};${count}if(_n${fi}!==1){if(_all===0){_e.push(null);return{valid:false,errors:_e}}if(_n${fi}>1){${collect}${pushMulti}}else{const ${resArr}=[];for(let _bi=0;_bi<${L};_bi++)${resArr}.push(${call});${report}}}}`)
5286
5294
  }
@@ -6012,7 +6020,7 @@ function sharedErr (lit, rootSchema) {
6012
6020
  // the document. `paramsShared` names a frozen params object hoisted for a
6013
6021
  // static site, so a static error's params stay one shared object as before.
6014
6022
  function errPushC(ctx, keyword, sp, pathExpr, paramsCode, msgCode, valueExpr, lean, paramsShared) {
6015
- if (!ctx.rich) return `(_e||(_e=[])).push(${lean})`
6023
+ if (!ctx.rich) return `(_e=_ap(_e,${lean}))`
6016
6024
  if (!ctx.richMk) {
6017
6025
  ctx.richMk = true
6018
6026
  const { makeRich } = require('./enrich-site')
@@ -6028,7 +6036,7 @@ function errPushC(ctx, keyword, sp, pathExpr, paramsCode, msgCode, valueExpr, le
6028
6036
  // the definition and the reference's own comes in as `_sp`.
6029
6037
  const rel = sp.startsWith(SP_PLACEHOLDER)
6030
6038
  ctx.closureVals.push(errorSite(keyword, unescapeSp(rel ? sp.slice(SP_PLACEHOLDER.length) : sp), paramsVal, fmt))
6031
- return `(_e||(_e=[])).push(_mk(${siteVar},${pathExpr || '""'},${paramsShared || paramsCode},${msgCode},${valueExpr},d${rel ? ',_sp' : ''}))`
6039
+ return `(_e=_ap(_e,_mk(${siteVar},${pathExpr || '""'},${paramsShared || paramsCode},${msgCode},${valueExpr},d${rel ? ',_sp' : ''})))`
6032
6040
  }
6033
6041
 
6034
6042
  function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts) {
@@ -6110,7 +6118,11 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts)
6110
6118
  if (needsBaseTracking(schema, schemaMap, new Set())) return null
6111
6119
 
6112
6120
  // runtimeShape: errors without the ordinal, see compileToJSCodegenWithErrors.
6113
- const ctx = { noOrdinal: !!(opts && opts.runtimeShape), rich: !!(opts && opts.rich), inCombined: true, hoisted: [], rootC: schema, varCounter: 0, helperCode: [], shared: [], closureVars: ['_cpLen'], closureVals: [_cpLen],
6121
+ // `_ap` adds an error to a list that may not exist yet: the first one
6122
+ // creates it as a one-element literal. Pushing onto an empty `[]` grew its
6123
+ // backing store on every rejection, about 7% of reading errors across the
6124
+ // official suite.
6125
+ const ctx = { noOrdinal: !!(opts && opts.runtimeShape), rich: !!(opts && opts.rich), inCombined: true, hoisted: [], rootC: schema, varCounter: 0, helperCode: ['function _ap(e,x){if(e===undefined)return[x];e.push(x);return e}'], shared: [], closureVars: ['_cpLen'], closureVals: [_cpLen],
6114
6126
  rootDefs: cRootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors: cAnchors, rootSchema: inputSchema, userFormats: userFormats || null }
6115
6127
  const lines = []
6116
6128
  try {
@@ -6273,6 +6285,14 @@ function emitDefFnC(key, target, cyclic, v, pathExpr, lines, ctx, schemaPrefix)
6273
6285
  ctx.defFnsC.set(key, fnName)
6274
6286
  const outerHoisted = ctx.hoisted
6275
6287
  ctx.hoisted = []
6288
+ // The body is a function of its own: a reference the caller was in the
6289
+ // middle of expanding is not on the stack inside it, where reaching it
6290
+ // again is a call (this function, or another one) rather than a loop.
6291
+ // With the caller's stack, a definition expanded once on the way into
6292
+ // the root's function was taken for a cycle inside it, and the schema
6293
+ // (a tree whose nodes hold trees) lost its one-pass function.
6294
+ const outerRefs = ctx.refStack
6295
+ ctx.refStack = new Set()
6276
6296
  const bodyLines = []
6277
6297
  let hoisted
6278
6298
  try {
@@ -6280,6 +6300,7 @@ function emitDefFnC(key, target, cyclic, v, pathExpr, lines, ctx, schemaPrefix)
6280
6300
  } finally {
6281
6301
  hoisted = ctx.hoisted
6282
6302
  ctx.hoisted = outerHoisted
6303
+ ctx.refStack = outerRefs
6283
6304
  }
6284
6305
  let body = bodyLines.join('\n ')
6285
6306
  body = body.split(`'${SP_PLACEHOLDER}`).join(`_sp+'`).split(`"${SP_PLACEHOLDER}`).join(`_sp+"`)
@@ -6438,12 +6459,12 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
6438
6459
  const lit = {keyword, instancePath: pathVal, schemaPath: spVal, params: Object.freeze(paramsVal), message: msgVal}
6439
6460
  if (o !== null) lit._o = o
6440
6461
  ctx.closureVals.push(sharedErr(lit, ctx.rootSchema))
6441
- return `(_e||(_e=[])).push(${errVar})`
6462
+ return `(_e=_ap(_e,${errVar}))`
6442
6463
  }
6443
6464
  }
6444
6465
  }
6445
6466
  // Dynamic path (e.g., array index): inline as before
6446
- return `(_e||(_e=[])).push({keyword:'${keyword}',instancePath:${pathExpr||'""'},schemaPath:'${sp}',params:${paramsCode},message:${msgCode}${ordinalField(ctx, sp)}})`
6467
+ return `(_e=_ap(_e,{keyword:'${keyword}',instancePath:${pathExpr||'""'},schemaPath:'${sp}',params:${paramsCode},message:${msgCode}${ordinalField(ctx, sp)}}))`
6447
6468
  }
6448
6469
 
6449
6470
  // enum and const apply to a value of any type, so a value that fails
@@ -6533,7 +6554,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
6533
6554
  const pathVal = pathExpr ? literalValue(pathExpr) : ''
6534
6555
  ctx.closureVars.push(errVar)
6535
6556
  ctx.closureVals.push(sharedErr({keyword: 'required', instancePath: pathVal, schemaPath: unescapeSp(`${schemaPrefix}/required`), params: Object.freeze({missingProperty: key}), message: `must have required property '${key}'`}, ctx.rootSchema))
6536
- lines.push(`if(${check}){(_e||(_e=[])).push(${errVar})}`)
6557
+ lines.push(`if(${check}){(_e=_ap(_e,${errVar}))}`)
6537
6558
  } else {
6538
6559
  lines.push(`if(${check}){${errPushC(ctx, 'required', `${schemaPrefix}/required`, pathExpr, `{missingProperty:'${esc(key)}'}`, `"must have required property '${esc(key)}'"`, v, `{keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/required'${ordinalField(ctx, `${schemaPrefix}/required`)},params:{missingProperty:'${esc(key)}'},message:"must have required property '${esc(key)}'"}`)}}`)
6539
6560
  }
@@ -6547,7 +6568,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
6547
6568
  const pathVal = pathExpr ? literalValue(pathExpr) : ''
6548
6569
  ctx.closureVars.push(errVar)
6549
6570
  ctx.closureVals.push(sharedErr({keyword: 'required', instancePath: pathVal, schemaPath: unescapeSp(`${schemaPrefix}/required`), params: Object.freeze({missingProperty: key}), message: `must have required property '${key}'`}, ctx.rootSchema))
6550
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!${ownKeyExpr(ctx, v, key)}){(_e||(_e=[])).push(${errVar})}`)
6571
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!${ownKeyExpr(ctx, v, key)}){(_e=_ap(_e,${errVar}))}`)
6551
6572
  } else {
6552
6573
  lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!${ownKeyExpr(ctx, v, key)}){${errPushC(ctx, 'required', `${schemaPrefix}/required`, pathExpr, `{missingProperty:'${esc(key)}'}`, `"must have required property '${esc(key)}'"`, v, `{keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/required'${ordinalField(ctx, `${schemaPrefix}/required`)},params:{missingProperty:'${esc(key)}'},message:"must have required property '${esc(key)}'"}`)}}`)
6553
6574
  }
@@ -6699,7 +6720,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
6699
6720
  const pathVal = pathExpr ? literalValue(pathExpr) : ''
6700
6721
  ctx.closureVars.push(errVar)
6701
6722
  ctx.closureVals.push(sharedErr({keyword: 'required', instancePath: pathVal, schemaPath: unescapeSp(`${schemaPrefix}/dependentRequired`), params: Object.freeze({missingProperty: dep}), message: `must have required property '${dep}'`}, ctx.rootSchema))
6702
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${ownKeyExpr(ctx, v, key)}&&!${ownKeyExpr(ctx, v, dep)}){(_e||(_e=[])).push(${errVar})}`)
6723
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${ownKeyExpr(ctx, v, key)}&&!${ownKeyExpr(ctx, v, dep)}){(_e=_ap(_e,${errVar}))}`)
6703
6724
  } else {
6704
6725
  lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${ownKeyExpr(ctx, v, key)}&&!${ownKeyExpr(ctx, v, dep)}){${errPushC(ctx, 'required', `${schemaPrefix}/dependentRequired`, pathExpr, `{missingProperty:'${esc(dep)}'}`, `"must have required property '${esc(dep)}'"`, v, `{keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/dependentRequired'${ordinalField(ctx, `${schemaPrefix}/dependentRequired`)},params:{missingProperty:'${esc(dep)}'},message:"must have required property '${esc(dep)}'"}`)}}`)
6705
6726
  }
@@ -52,12 +52,16 @@ const branchTitle = (n) => (n && n.schema && typeof n.schema.title === 'string'
52
52
 
53
53
  function install(deps) {
54
54
  const { Plan, NOERRORS, err, evalLeaf, evalLeafV, runCustomV, dataBits, escapePointer,
55
- deepEqual, multipleOfOk, cpAtLeast, cpAtMost,
55
+ deepEqual, enumMember, allDistinct, multipleOfOk, cpAtLeast, cpAtMost,
56
56
  T_STRING, T_ARRAY, T_OBJECT, resolveRef, splitFragment, resolveUri } = deps;
57
57
 
58
58
  const TRUE_FN = () => true;
59
- const TRUE_PAIR = { v: TRUE_FN, c: TRUE_FN };
59
+ // `leaf`: the node compiled no subschema of its own (see compileNode). A
60
+ // collecting caller asks a leaf for its verdict first and builds the paths
61
+ // for its errors only when it fails.
62
+ const TRUE_PAIR = { v: TRUE_FN, c: TRUE_FN, leaf: true };
60
63
  const FALSE_PAIR = {
64
+ leaf: true,
61
65
  v: () => false,
62
66
  c: (data, errors, instancePath, schemaPath) => {
63
67
  if (errors !== NOERRORS) errors.push(err('false schema', 'not', instancePath, schemaPath, {}, 'boolean schema is false'));
@@ -164,12 +168,8 @@ function install(deps) {
164
168
  checks.push((data, bits) => (bits & mask) !== 0);
165
169
  }
166
170
  if (P.enum !== null) {
167
- const vals = P.enum;
168
- if (vals.length === 0) checks.push(() => false);
169
- else checks.push((data) => {
170
- for (let i = 0; i < vals.length; i++) if (deepEqual(vals[i], data)) return true;
171
- return false;
172
- });
171
+ if (P.enum.length === 0) checks.push(() => false);
172
+ else checks.push((data) => enumMember(P, data));
173
173
  }
174
174
  if (P.hasConst) {
175
175
  const c = P.const;
@@ -205,14 +205,7 @@ function install(deps) {
205
205
  const arrs = [];
206
206
  if (P.minItems !== undefined) { const m = P.minItems; arrs.push((d) => d.length >= m); }
207
207
  if (P.maxItems !== undefined) { const m = P.maxItems; arrs.push((d) => d.length <= m); }
208
- if (P.uniqueItems) {
209
- arrs.push((d) => {
210
- for (let i = 0; i < d.length; i++) {
211
- for (let j = i + 1; j < d.length; j++) if (deepEqual(d[i], d[j])) return false;
212
- }
213
- return true;
214
- });
215
- }
208
+ if (P.uniqueItems) arrs.push(allDistinct);
216
209
  const run = combineValue(arrs);
217
210
  if (run !== null) { needsBits = true; checks.push((data, bits) => bits !== T_ARRAY || run(data)); }
218
211
  }
@@ -300,6 +293,7 @@ function install(deps) {
300
293
  const box = { v: null, c: null };
301
294
  const pair = {
302
295
  open: true,
296
+ leaf: false,
303
297
  v: (d, st, rec) => box.v(d, st, rec),
304
298
  c: (d, e, ip, sp, st, rec) => box.c(d, e, ip, sp, st, rec),
305
299
  };
@@ -308,8 +302,11 @@ function install(deps) {
308
302
  // In-place children collect when this node does; child applicators
309
303
  // never do. `rec` is threaded as the trailing argument of every step
310
304
  // and is undefined on non-collecting nodes, where nothing reads it.
311
- const inPlace = (n, b) => compileNode(ctx, n, b, chain, collect);
312
- const child = (n) => compileNode(ctx, n, base, chain, false);
305
+ // Whether this node compiles any subschema: a node that does not is a
306
+ // leaf, whose verdict is a few comparisons.
307
+ let nested = false;
308
+ const inPlace = (n, b) => { nested = true; return compileNode(ctx, n, b, chain, collect); };
309
+ const child = (n) => { nested = true; return compileNode(ctx, n, base, chain, false); };
313
310
 
314
311
  const steps = []; // collect variant: (data, errors, ip, sp, stack, rec)
315
312
  const vsteps = []; // verdict variant: (data, stack, rec)
@@ -399,7 +396,7 @@ function install(deps) {
399
396
  let ok = true;
400
397
  const n = Math.min(fns.length, data.length);
401
398
  for (let i = 0; i < n; i++) {
402
- if (!fns[i].c(data[i], errors, instancePath + '/' + i, schemaPath + '/prefixItems/' + i, stack)) {
399
+ if (!((fns[i].leaf && fns[i].v(data[i], stack)) || fns[i].c(data[i], errors, instancePath + '/' + i, schemaPath + '/prefixItems/' + i, stack))) {
403
400
  ok = false;
404
401
  if (errors === NOERRORS) return false;
405
402
  }
@@ -422,7 +419,7 @@ function install(deps) {
422
419
  if (dataBits(data) !== T_ARRAY) return true;
423
420
  let ok = true;
424
421
  for (let i = start; i < data.length; i++) {
425
- if (!fn.c(data[i], errors, instancePath + '/' + i, schemaPath + '/items', stack)) {
422
+ if (!((fn.leaf && fn.v(data[i], stack)) || fn.c(data[i], errors, instancePath + '/' + i, schemaPath + '/items', stack))) {
426
423
  ok = false;
427
424
  if (errors === NOERRORS) return false;
428
425
  }
@@ -536,7 +533,7 @@ function install(deps) {
536
533
  let evaluated = false;
537
534
  const prop = lookup !== null ? lookup(key) : undefined;
538
535
  if (prop !== undefined) {
539
- if (!prop.fn.c(data[key], errors, instancePath + prop.seg, schemaPath + prop.schemaSeg, stack)) {
536
+ if (!((prop.fn.leaf && prop.fn.v(data[key], stack)) || prop.fn.c(data[key], errors, instancePath + prop.seg, schemaPath + prop.schemaSeg, stack))) {
540
537
  ok = false;
541
538
  if (errors === NOERRORS) return false;
542
539
  }
@@ -546,7 +543,7 @@ function install(deps) {
546
543
  for (let pi = 0; pi < patterns.length; pi++) {
547
544
  const pp = patterns[pi];
548
545
  if (pp.re.test(key)) {
549
- if (!pp.fn.c(data[key], errors, instancePath + '/' + escapePointer(key), schemaPath + '/patternProperties/' + escapePointer(pp.src), stack)) {
546
+ if (!((pp.fn.leaf && pp.fn.v(data[key], stack)) || pp.fn.c(data[key], errors, instancePath + '/' + escapePointer(key), schemaPath + '/patternProperties/' + escapePointer(pp.src), stack))) {
550
547
  ok = false;
551
548
  if (errors === NOERRORS) return false;
552
549
  }
@@ -559,7 +556,7 @@ function install(deps) {
559
556
  ok = false;
560
557
  if (errors === NOERRORS) return false;
561
558
  errors.push(err('additionalProperties', 'additionalProperties', instancePath, schemaPath + '/additionalProperties', { additionalProperty: key }, 'must NOT have additional properties'));
562
- } else if (!apFn.c(data[key], errors, instancePath + '/' + escapePointer(key), schemaPath + '/additionalProperties', stack)) {
559
+ } else if (!((apFn.leaf && apFn.v(data[key], stack)) || apFn.c(data[key], errors, instancePath + '/' + escapePointer(key), schemaPath + '/additionalProperties', stack))) {
563
560
  ok = false;
564
561
  if (errors === NOERRORS) return false;
565
562
  }
@@ -927,7 +924,7 @@ function install(deps) {
927
924
  for (let k = 0; k < keys.length; k++) {
928
925
  const key = keys[k];
929
926
  if (hasProp(rec, key)) continue;
930
- if (!fn.c(data[key], errors, instancePath + '/' + escapePointer(key), schemaPath + '/unevaluatedProperties', stack)) {
927
+ if (!((fn.leaf && fn.v(data[key], stack)) || fn.c(data[key], errors, instancePath + '/' + escapePointer(key), schemaPath + '/unevaluatedProperties', stack))) {
931
928
  ok = false;
932
929
  if (errors === NOERRORS) return false;
933
930
  }
@@ -983,7 +980,7 @@ function install(deps) {
983
980
  let ok = true;
984
981
  for (let i = 0; i < data.length; i++) {
985
982
  if (hasItem(rec, i)) continue;
986
- if (!fn.c(data[i], errors, instancePath + '/' + i, schemaPath + '/unevaluatedItems', stack)) {
983
+ if (!((fn.leaf && fn.v(data[i], stack)) || fn.c(data[i], errors, instancePath + '/' + i, schemaPath + '/unevaluatedItems', stack))) {
987
984
  ok = false;
988
985
  if (errors === NOERRORS) return false;
989
986
  }
@@ -1068,6 +1065,7 @@ function install(deps) {
1068
1065
  box.v = vfn;
1069
1066
  pair.c = cfn;
1070
1067
  pair.v = vfn;
1068
+ pair.leaf = !nested;
1071
1069
  pair.open = false;
1072
1070
  return pair;
1073
1071
  }
@@ -0,0 +1,139 @@
1
+ 'use strict';
2
+
3
+ // uniqueItems for every engine. These are real functions: the interpreted
4
+ // engine calls them, and the code generator emits their source text
5
+ // (`String(fn)`) into the function it builds, so both run the same code.
6
+ // They read only each other, `_deq` and `_uqpI`, which the generator emits
7
+ // beside them.
8
+ //
9
+ // Short arrays compare pairwise and allocate nothing. Longer arrays of
10
+ // primitives key a Set or Map by the value itself. Longer arrays that hold an
11
+ // object key a Map by a numeric structural hash, and only items whose hashes
12
+ // collide are compared with `_deq`. Equal values always hash alike: numbers
13
+ // by value (so 1 and 1.0, 0 and -0 agree), strings by their characters,
14
+ // arrays by position, and objects by the sum of their own key-value hashes,
15
+ // whatever the key order. The hash replaced a sorted-key canonical string
16
+ // built for every item on every call, which was over a quarter of the time a
17
+ // generated verdict spent on SchemaStore's sample documents.
18
+
19
+ // JSON equality, the same function the generator hoists as DEQ_HELPER.
20
+ function _deq(a, b) {
21
+ if (a === b) return true;
22
+ if (a === null || b === null || typeof a !== 'object' || typeof b !== 'object') return false;
23
+ var aa = Array.isArray(a);
24
+ if (aa !== Array.isArray(b)) return false;
25
+ var i;
26
+ if (aa) {
27
+ if (a.length !== b.length) return false;
28
+ for (i = 0; i < a.length; i++) if (!_deq(a[i], b[i])) return false;
29
+ return true;
30
+ }
31
+ var ka = Object.keys(a);
32
+ if (ka.length !== Object.keys(b).length) return false;
33
+ for (i = 0; i < ka.length; i++) {
34
+ var k = ka[i];
35
+ if (!Object.prototype.hasOwnProperty.call(b, k) || !_deq(a[k], b[k])) return false;
36
+ }
37
+ return true;
38
+ }
39
+
40
+ function _uqh(x) {
41
+ if (x === null) return 1;
42
+ var t = typeof x, h, i;
43
+ if (t === 'boolean') return x ? 2 : 3;
44
+ if (t === 'number') {
45
+ if (x === 0) return 4;
46
+ h = x | 0;
47
+ return Math.imul(h ^ (((x - h) * 1e9) | 0), 0x9e3779b1) ^ 5;
48
+ }
49
+ if (t === 'string') {
50
+ h = 0x811c9dc5 ^ x.length;
51
+ for (i = 0; i < x.length; i++) h = Math.imul(h ^ x.charCodeAt(i), 16777619);
52
+ return h;
53
+ }
54
+ if (t !== 'object') return 6;
55
+ if (Array.isArray(x)) {
56
+ h = 0x2545f491 ^ x.length;
57
+ for (i = 0; i < x.length; i++) h = Math.imul(h ^ _uqh(x[i]), 16777619);
58
+ return h;
59
+ }
60
+ var ks = Object.keys(x);
61
+ h = 0x6c8e9cf5 ^ ks.length;
62
+ for (i = 0; i < ks.length; i++) h = (h + Math.imul(_uqh(ks[i]) ^ Math.imul(_uqh(x[ks[i]]), 0x5bd1e995), 0x27d4eb2d)) | 0;
63
+ return h;
64
+ }
65
+
66
+ // Whether every item is distinct.
67
+ function _uq(a) {
68
+ var n = a.length, i, k;
69
+ if (n < 2) return true;
70
+ if (n <= 12) {
71
+ for (i = 1; i < n; i++) for (k = 0; k < i; k++) if (_deq(a[i], a[k])) return false;
72
+ return true;
73
+ }
74
+ var prim = true;
75
+ for (i = 0; i < n; i++) { var x = a[i]; if (x !== null && typeof x === 'object') { prim = false; break; } }
76
+ if (prim) {
77
+ var s = new Set();
78
+ for (i = 0; i < n; i++) { if (s.has(a[i])) return false; s.add(a[i]); }
79
+ return true;
80
+ }
81
+ var m = new Map();
82
+ for (i = 0; i < n; i++) {
83
+ var hh = _uqh(a[i]), b = m.get(hh);
84
+ if (b === undefined) m.set(hh, i);
85
+ else if (typeof b === 'number') { if (_deq(a[b], a[i])) return false; m.set(hh, [b, i]); }
86
+ else { for (k = 0; k < b.length; k++) if (_deq(a[b[k]], a[i])) return false; b.push(i); }
87
+ }
88
+ return true;
89
+ }
90
+
91
+ // The duplicate pair an error reports: the first item that has a later
92
+ // equal, and its first later equal. Returns that later index and leaves the
93
+ // first in _uqpI, or returns -1 when every item is distinct.
94
+ var _uqpI = 0;
95
+ function _uqp(a) {
96
+ var n = a.length, i, k;
97
+ if (n < 2) return -1;
98
+ if (n <= 12) {
99
+ for (i = 0; i < n - 1; i++) for (k = i + 1; k < n; k++) if (_deq(a[i], a[k])) { _uqpI = i; return k; }
100
+ return -1;
101
+ }
102
+ var prim = true;
103
+ for (i = 0; i < n; i++) { var x = a[i]; if (x !== null && typeof x === 'object') { prim = false; break; } }
104
+ var bi = -1, bj = -1, m = new Map();
105
+ if (prim) {
106
+ for (i = 0; i < n; i++) {
107
+ var p = m.get(a[i]);
108
+ if (p === undefined) m.set(a[i], i);
109
+ else if (p >= 0) { if (bi < 0 || p < bi) { bi = p; bj = i; } m.set(a[i], -1); }
110
+ }
111
+ } else {
112
+ // Each bucket holds the first occurrence of every distinct value that
113
+ // hashed there, in index order; a value is paired with its first later
114
+ // equal only once.
115
+ var paired = null;
116
+ for (i = 0; i < n; i++) {
117
+ var hh = _uqh(a[i]), b = m.get(hh), r = -1;
118
+ if (b === undefined) { m.set(hh, i); continue; }
119
+ if (typeof b === 'number') { if (_deq(a[b], a[i])) r = b; else m.set(hh, [b, i]); }
120
+ else { for (k = 0; k < b.length; k++) if (_deq(a[b[k]], a[i])) { r = b[k]; break; } if (r < 0) b.push(i); }
121
+ if (r >= 0) {
122
+ if (paired === null) paired = new Set();
123
+ if (!paired.has(r)) { paired.add(r); if (bi < 0 || r < bi) { bi = r; bj = i; } }
124
+ }
125
+ }
126
+ }
127
+ if (bi < 0) return -1;
128
+ _uqpI = bi;
129
+ return bj;
130
+ }
131
+
132
+ // For the interpreted engine.
133
+ function allDistinct(a) { return _uq(a); }
134
+ function duplicatePair(a) {
135
+ var j = _uqp(a);
136
+ return j < 0 ? null : [_uqpI, j];
137
+ }
138
+
139
+ module.exports = { allDistinct, duplicatePair, _deq, _uqh, _uq, _uqp };
package/lib/version.js CHANGED
@@ -7,4 +7,4 @@
7
7
  //
8
8
  // Kept in lockstep with package.json by `tests/test_version_sync.js`.
9
9
 
10
- module.exports = '1.44.0';
10
+ module.exports = '1.45.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ata-validator",
3
- "version": "1.44.0",
3
+ "version": "1.45.0",
4
4
  "description": "JSON Schema validator that compiles to plain JavaScript, still runs where code generation is blocked, and can be compiled away at build time. Passes the full official test suite.",
5
5
  "main": "index.js",
6
6
  "module": "index.mjs",
@@ -64,7 +64,7 @@
64
64
  "release:check": "node tests/test_pack_purity.js && node scripts/check-doc-coverage.js && node tests/test_error_codes_lock.js && node tests/test_safe_regex_embed.js && node tests/test_version_sync.js",
65
65
  "build": "cmake-js build --target ata",
66
66
  "rebuild": "cmake-js rebuild --target ata",
67
- "test": "node test.js && node tests/test_removed_aot_methods.js && node tests/test_no_native.js && node tests/test_no_eval.js && node tests/test_property_dependencies.js && node tests/test_v1_dialect.js && node tests/test_buffer_path_parity.js && node tests/test_number_text_parity.js && node tests/test_text_path_errors.js && node tests/test_compiled_parity.js && node tests/test_compiled_extension_parity.js && node tests/test_buffer_gate.js && node tests/test_scanner_differential.js && node tests/test_validate_json_scanner_cost.js && node tests/test_buffer_reject_cost.js && node tests/test_nan_verdict.js && node tests/test_remove_additional_nested.js && node tests/test_exclusive_bounds.js && node tests/test_engine_differential.js && node tests/test_error_shape_differential.js && node tests/test_error_list_differential.js && node tests/test_output_format.js && node tests/test_standard_json_schema.js && node tests/test_retry_message.js && node tests/test_describe_schema.js && node tests/test_aot_parse.js && node tests/test_draft7_semantics.js && node tests/test_metaschema_ref.js && node tests/test_pure_js_unsupported.js && node tests/test_native_load_order.js && node tests/test_pack_purity.js && node tests/test_make_native_package.js && node tests/test_browser.js && node tests/test_browser_nofs.js && node tests/test_browser_imports_guard.js && node tests/test_esm_exports.js && node tests/test_version_sync.js && node tests/test_native_loaded.js && node tests/test_native_lazy.js && node tests/test_buffer_apis_without_native.js && node tests/test_cold_start_modules.js && node tests/test_safe_regex_embed.js && node tests/test_t_builder.js && node tests/test_async_refine.js && node tests/test_safe_regex.js && node tests/test_safe_regex_integration.js && node tests/test_regex_ecma_whitespace.js && node tests/test_regex_native_linear.js && node tests/test_aot_build.js && node tests/test_aot_differential.js && node tests/test_aot_cli_build.js && node tests/test_aot_cli_smoke.js && node tests/test_bundle_standalone.js && node tests/test_standalone_anyof.js && node tests/test_standalone_formats.js && node tests/test_aot_format_mode.js && node tests/test_aot_additional_props_errors.js && node tests/test_id_anchor_refs.js && node tests/test_engine_routing.js && node tests/test_engine_option.js && node tests/test_compile_cache_order.js && node tests/test_engine_diagnostic.js && node tests/test_format_engine_parity.js && node tests/test_error_accumulator_collision.js && node tests/test_uri_helper_parity.js && node tests/test_uri_fast_path.js && node tests/test_extend_verdict.js && node tests/test_ipv4_regex.js && node tests/test_ipv6_fast_path.js && node tests/test_nested_recursion_decline.js && node tests/test_generator_error_content.js && node tests/test_inherited_key_presence.js && node tests/test_proto_accessor.js && node tests/test_hybrid_tier.js && node tests/test_first_rejection_builds_one.js && node tests/test_container_local.js && node tests/test_one_shot_validate.js && node tests/test_fixed_pattern_inline.js && node tests/test_plan_source.js && node tests/test_fused_remove_additional.js && node tests/test_runtime_parse.js && node tests/test_formats_single_pass.js && node tests/test_email_format_cost.js && node tests/test_format_single_error.js && node tests/test_defs_pointer_alias.js && node tests/test_encoded_refs.js && node tests/test_cross_doc_root_ref.js && node tests/test_codegen_entrypoint_agreement.js && node tests/test_schema_code_injection.js && node tests/test_aot_html_safe.js && node tests/test_ref_annotation_siblings.js && node tests/test_gate_wrapped_schemas.js && node tests/test_pattern_message_escape.js && node tests/test_nested_defaults_engines.js && node tests/test_nested_coercion_parity.js && node tests/test_preprocess_cache.js && node tests/test_large_schema_codegen.js && node tests/test_base_uri.js && node tests/test_hoisted_def_recursion.js && node tests/test_alias_definitions.js && node tests/test_property_names_ref.js && node tests/test_hybrid_agreement.js && node tests/test_codegen_edge_shapes.js && node tests/test_deferred_def_scope.js && node tests/test_node_scoped_ctx_flags.js && node tests/test_fast_slot_exhaustion.js && node tests/test_ajv_errors.js && node tests/test_custom_keywords.js && node tests/test_custom_keyword_codegen.js && node tests/test_strict_schema.js && node tests/test_aot_external_checks.js && node tests/test_ajv_parity.js && node tests/test_user_format_error_path.js && node tests/test_unevaluated_error_path.js && node tests/test_unevaluated_error_shape.js && node tests/test_unevaluated_differential.js && node tests/fuzz_differential.js && node tests/test_pattern_properties_errors.js && node tests/test_additional_properties_scaling.js && node tests/test_additional_props_combined_cost.js && node tests/test_no_input_mutation.js && node tests/test_typed_validator_runner.js && node tests/test_define_schema.js && node tests/test_error_codes_lock.js && node tests/test_error_code_lookup.js && node tests/test_error_order.js && node tests/test_error_order_ordinal.js && node tests/test_ref_error_order.js && node tests/test_rejection_shape.js && node tests/test_lazy_normalization.js && node tests/test_value_equality.js && node tests/test_vocabulary.js && node tests/test_schema_scan.js && node tests/test_lazy_errors.js && node tests/test_error_read_paths.js && node tests/test_unique_items_error_pair.js && node tests/test_rich_errors_generated.js && node tests/test_captured_verdict_stub.js && node tests/test_generated_returns_rewrite.js && node tests/test_deep_pointer_refs.js && node tests/test_uneval_props_dynamic.js && node tests/test_one_pass_no_fallback.js && node tests/test_scanner_size_cap.js && node tests/test_scanner_index_keys.js && node tests/test_both_defs_containers.js && node tests/test_uneval_branch_counts.js && node tests/test_branch_verdicts.js && node tests/test_property_names_subschema.js && node tests/test_property_named_ref.js && node tests/test_validate_json_engines_same_errors.js && node tests/test_prototype_property_names.js && node tests/test_proto_name_rename_invariance.js && node tests/test_shared_definitions.js && node tests/test_compile_cache_bound.js && node tests/test_uneval_items_dynamic.js && node tests/test_escaped_property_names.js && node tests/test_ref_siblings_combined.js && node tests/test_unicode_property_patterns.js && node tests/test_cold_twin.js && node tests/test_native_positions.js && node tests/test_validate_json_single_parse.js && node tests/test_single_pass_errors.js && node tests/test_lazy_json_errors.js && node tests/test_lazy_instance.js && node tests/test_cyclic_input.js && node tests/test_native_error_codes.js && node tests/test_verdict_preprocess.js && node tests/test_lite_parity.js && node tests/test_lite_bundle_size.js && node tests/test_defaults_own_keys.js && node tests/test_plan_compiler.js && node tests/test_nullable.js && node tests/test_validate_and_parse.js && node tests/test_validate_data.js && node tests/test_enrich_error.js && node tests/test_enrich_received.js && node tests/test_rich_errors_optout.js && node tests/test_error_messages.js && node tests/test_source_positions.js && node tests/fuzz_positions.js && node tests/test_malformed_json_termination.js && node tests/test_data_positions.js && node tests/test_targeted_positions.js && node tests/test_position_map_cost.js && node tests/test_targeted_positions_cost.js && node tests/test_aot_staleness.js && node tests/test_file_positions.js && node tests/test_aot_positions_cost.js && node tests/test_schema_hash.js && node tests/test_render_shared.js && node tests/test_renderers.js && node tests/test_render_without_process.js && node tests/test_additive_fields.js && node tests/test_diagnostic_source.js && node tests/test_diagnose.js && node tests/test_correlate.js && node tests/test_diagnostics_score.js && node tests/test_runtime_error_dx.js && node tests/test_aot_error_dx.js && node tests/test_aot_source_frames.js && node tests/test_abort_early.js && node tests/test_branch_collapse.js && node tests/test_suggestions.js && node tests/test_levenshtein_banded.js && node tests/test_cli_validate.js && node tests/test_cli_version.js && node tests/test_collect_evaluated.js && node tests/test_differential_tiers.js && node tests/test_draft7.js && node tests/test_dual_path.js && node tests/test_keywords_codegen.js && node tests/test_ref_cross.js && node tests/test_shape_classifier.js && node tests/test_tier0.js && node benchmark/bench_aot_size.mjs",
67
+ "test": "node tests/run.js",
68
68
  "bench:size": "node benchmark/bench_aot_size.mjs",
69
69
  "test:suite": "node tests/run_suite.js && node tests/run_suite.js draft7 && node tests/run_suite.js v1",
70
70
  "test:compat": "node tests/test_compat.js",
@@ -143,13 +143,13 @@
143
143
  "LICENSE"
144
144
  ],
145
145
  "optionalDependencies": {
146
- "@ata-validator/native-darwin-arm64": "1.44.0",
147
- "@ata-validator/native-darwin-x64": "1.44.0",
148
- "@ata-validator/native-linux-arm64-gnu": "1.44.0",
149
- "@ata-validator/native-linux-arm64-musl": "1.44.0",
150
- "@ata-validator/native-linux-x64-gnu": "1.44.0",
151
- "@ata-validator/native-linux-x64-musl": "1.44.0",
152
- "@ata-validator/native-win32-x64": "1.44.0"
146
+ "@ata-validator/native-darwin-arm64": "1.45.0",
147
+ "@ata-validator/native-darwin-x64": "1.45.0",
148
+ "@ata-validator/native-linux-arm64-gnu": "1.45.0",
149
+ "@ata-validator/native-linux-arm64-musl": "1.45.0",
150
+ "@ata-validator/native-linux-x64-gnu": "1.45.0",
151
+ "@ata-validator/native-linux-x64-musl": "1.45.0",
152
+ "@ata-validator/native-win32-x64": "1.45.0"
153
153
  },
154
154
  "peerDependencies": {
155
155
  "yaml": "^2.0.0"