ata-validator 1.7.0 → 1.7.2

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/CHANGELOG.md CHANGED
@@ -2,6 +2,35 @@
2
2
 
3
3
  All notable changes to ata-validator are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/), and this project adheres to semantic versioning.
4
4
 
5
+ ## 1.7.2 - 2026-08-24
6
+
7
+ ### Fixed
8
+
9
+ - `enum` compared object members by their serialized form, so `{"b": 2, "a": 1}` was rejected against `enum: [{"a": 1, "b": 2}]`. JSON Schema compares instances by value: two objects with the same members are equal whatever order their keys were written in. All three code generation paths did this, so they agreed with each other and disagreed with the interpreted engine, which has always compared structurally. `const` was already correct but reached the answer by building a canonical string on every call. Both now use one structural comparison hoisted per compiled function. The official suite does not cover reordered keys for `enum`, and the entry-point agreement test compares the generators against each other, so neither would have caught this; `tests/test_value_equality.js` pins the answer through every engine that can produce one and runs as part of `npm test`.
10
+ - A draft-07 document passed through the `schemas` option was rejected with "Schema in schemas option must have $id" whenever it carried `$ref` next to `$id`. Draft-07 ignores every keyword sitting beside `$ref`, so normalization drops them and `$id` goes with them, which is the right reading for evaluation: the reference resolves against the URI the document was retrieved from, not the `$id` it declares. It is the wrong reading for registration, where `$id` is the only name the caller gave the document. The identity is now read from what the caller passed when normalization has dropped it, so such a document registers under its `$id` in the array form and is addressable by both its retrieval URI and its `$id` in the map form. A document that declares no `$id` at all is still refused, and a fragment-only `$id` is still an anchor rather than a document name. This is what made all 23 remote-reference cases of the draft-07 suite error out in the Bowtie compliance report; they now answer, and answer correctly.
11
+
12
+ ### Changed
13
+
14
+ - The closure-tree compiler now covers every schema the interpreted engine accepts. It previously declined `unevaluatedProperties`/`unevaluatedItems` and `$dynamicRef` in any schema with more than one resource, which left 192 of the 987 suite schemas walking the generic evaluator; that count is now zero. Annotations flow through the compiled tree: the compiler decides at compile time which nodes anyone reads annotations from, and those nodes carry a record that in-place children write into directly, with a failed child rolled back by truncating the record rather than by allocating a fresh one per child. `$dynamicRef` resolves against the dynamic scope computed at compile time, so multi-resource schemas no longer search it per call. A schema whose compiled graph has no cycle drops the recursion guard and the `(schema, data)` stack entirely. An unresolvable `$ref` compiles into the same runtime rejection the evaluator produces instead of declining the schema.
15
+ - Measured on the official suite with prebuilt validators, several runs each on the same machine: draft 2020-12 went from 94 ns to 77-80 ns per case overall, and the cases that route to the interpreted engine from about 240 ns to about 125 ns when they accept and 235 ns to 160 ns when they reject. Draft 7 went from 59 ns to 49 ns overall. The metaschema-validation case, the worst outlier in the suite, went from 5.5 µs to about 1.4 µs. On a longer steady-state loop over the same suite, where construction is not part of what is timed, draft 2020-12 went from 46 ns to 43 ns per case and draft 7 from 33 ns to 29 ns.
16
+ - The value-level keywords of a schema node are compiled into closures over their own operands instead of being answered by a generic evaluator that re-reads a dozen presence flags off the plan on every call. The type of the value is computed once per node rather than once per keyword group, and a node whose keywords never need it does not compute it at all.
17
+ - `uniqueItems` on arrays whose item type is not known ahead of time no longer builds a canonicalizing closure and a `Set` on every call. The helpers are hoisted once per compiled function; arrays of twelve items or fewer compare pairwise with a structural equality that ignores key order and allocates nothing, and longer arrays keep the `Set`, using canonical strings only once an object actually appears. Over the draft 7 suite the `uniqueItems` cases went from 125 ns to 24 ns each.
18
+ - `ARCHITECTURE.md` documents the interpreted engine and the closure-tree compiler, and no longer claims `$dynamicRef` falls back to the native engine, which stopped being true in 1.7.1.
19
+
20
+ ## 1.7.1 - 2026-08-23
21
+
22
+ ### Added
23
+
24
+ - A closure-tree compiler for the interpreted engine. A schema the code generator declines is compiled into a tree of plain closures, one per schema node, with every keyword branch decided at compile time and `$ref` targets resolved once; no source generation and no `new Function`, so it works under a CSP and on Workers. Scope: schemas without `unevaluatedProperties`/`unevaluatedItems`, and `$dynamicRef` only in single-resource schemas; everything else keeps the generic evaluator. `tests/test_plan_compiler.js` holds the compiled tree to byte-identical verdicts and errors against the evaluator over 2,864 suite cases.
25
+
26
+ ### Changed
27
+
28
+ - Plans that check only value-level keywords (most leaves of any schema) skip the evaluator's prologue entirely; `$ref` resolutions are cached with their planned target on the plan itself; the dynamic scope is pushed and popped in place instead of copied per resource. Rejection results are a small class with the `errors` accessor on the prototype, since defining a getter inside an object literal builds a closure and an accessor property on every rejection, which was the single largest cost on the rejection path.
29
+ - The interpreted engine gained a verdict-only mode: `isValidObject()` and the internal fast checks walk the schema without constructing a single error object, message string or scratch array. On an interpreter-routed schema the boolean check dropped from 894 ns to 70 ns.
30
+ - Object validation for `$dynamicRef` schemas no longer routes to the native engine. The interpreted engine has scored the same on every `$dynamicRef` case of the suite since the dynamic-scope fix in 1.7.0, needs no addon, and carries the verdict-only mode; the suite's `$dynamicRef` rejects dropped from about 1.7 µs to the interpreter's cost.
31
+ - String length bounds decide from the UTF-16 length where possible: a string's code point count always sits between half its length and its length, so `minLength`/`maxLength` only count code points inside the narrow band where the answer is genuinely uncertain. The surrogate test is a single wraparound compare. The code generator already worked this way; the interpreter and the closure path now match it.
32
+ - Errors are paid for when read, not when produced. `validate()` answers the verdict from the fastest engine for the schema and materializes `errors` through a cached getter on first access; declaration-order sorting and enrichment (received value, suggestions, source frames) moved with it into one presentation layer. A caller that only reads `.valid`, which is every gateway check, no longer pays for error construction at all. The output of `.errors` is byte-for-byte what it was. Measured on a suite-shaped benchmark of prebuilt validators over 1,052 mixed valid and invalid cases, `validate().valid` went from 778 ns to about 150 ns per call, ahead of every error-capable validator we measured, and a rejection that never has its errors read now costs less than `abortEarly` mode used to. One observable edge: mutating the data between `validate()` and the first read of `.errors` now reflects the mutated data in the errors, and if the mutation makes the data valid the errors fall back to a single generic entry.
33
+
5
34
  ## 1.7.0 - 2026-08-23
6
35
 
7
36
  ### Fixed
package/README.md CHANGED
@@ -64,9 +64,10 @@ passes, and a rejection costs more than a verdict: ata builds an error carrying
64
64
  offending value, a documentation link and a suggestion. On a five-field object schema a
65
65
  passing payload costs about 17 ns and a rejected one about 155 ns. `abortEarly: true` or
66
66
  `isValidObject()` skips that work when only the verdict matters. Schemas ata declines to
67
- compile, mostly cross-document `$ref`, `$dynamicRef` and `unevaluated*`, run on the
68
- interpreted engine: about 343 ns for a passing payload on a six-field object schema, eight
69
- times the compiled path, measured warm on the same machine.
67
+ compile, mostly cross-document `$ref`, `$dynamicRef` and the harder `unevaluated*` shapes,
68
+ run on the interpreted engine. That engine compiles each schema into a tree of closures, so
69
+ on a six-field object schema a passing payload costs about 136 ns against 8 ns for the same
70
+ shape on the compiled path, both measured warm in one run on the same machine.
70
71
 
71
72
  ## Error messages
72
73
 
@@ -169,6 +170,9 @@ v.isValidJSON('{"name": "Mert", "email": "mert@example.com"}'); // true
169
170
  // Buffer input (zero-copy, raw NAPI)
170
171
  v.isValid(Buffer.from('{"name": "Mert", "email": "mert@example.com"}'));
171
172
 
173
+ // Which engine answers this schema: 'codegen', 'closure', 'native' or 'interpreter'
174
+ v.engine(); // 'codegen'
175
+
172
176
  // Parallel batch - multi-core, NDJSON, 13.4M items/sec
173
177
  const ndjson = Buffer.from(lines.join('\n'));
174
178
  v.isValidParallel(ndjson); // bool[]
@@ -400,6 +404,12 @@ const { toStandaloneModule } = require('ata-validator/build');
400
404
  fs.writeFileSync('./user.validator.mjs', toStandaloneModule(schema, { format: 'esm' }));
401
405
  ```
402
406
 
407
+ Custom format functions either get their source embedded (the default, refused
408
+ at build time with a named error when the function would not survive
409
+ serialization) or, with `formatMode: 'inject'`, are supplied at load time
410
+ through a `setFormats()` export the module carries. `docs/API.md` has the
411
+ details.
412
+
403
413
  **Fastify startup, 10 route schemas, from a cold process to the first validated request:
404
414
  ajv 19.6 ms, ata 3.1 ms, no build step required.** ata registers in 1.1 ms of that and
405
415
  compiles on the first request, so counting only registration would overstate the gap.
package/index.d.ts CHANGED
@@ -403,9 +403,9 @@ export interface Validator<T = unknown> {
403
403
  isValidObject(data: unknown): data is T;
404
404
  /**
405
405
  * Which engine answers `validate()` for this schema: 'codegen' (generated
406
- * JS), 'closure' (the closure compiler), 'native' (the C++ engine, only
407
- * for some $dynamicRef schemas) or 'interpreter'. The verdict is the same
408
- * on every engine; the cost is not. A diagnostic, not a configuration.
406
+ * JS), 'closure' (the closure compiler) or 'interpreter'; 'native' is
407
+ * reserved. The verdict is the same on every engine; the cost is not.
408
+ * A diagnostic, not a configuration.
409
409
  */
410
410
  engine(): 'codegen' | 'closure' | 'native' | 'interpreter';
411
411
 
package/index.js CHANGED
@@ -314,6 +314,34 @@ const ABORT_EARLY_RESULT = Object.freeze({
314
314
 
315
315
  // Above this size, simdjson On Demand (selective field access) beats JSON.parse
316
316
  // (which must materialize the full JS object tree). Buffer.from + NAPI ~2x faster.
317
+
318
+ // Rejection result with errors materialized on first read. The accessor
319
+ // lives on the prototype so constructing one is a plain allocation; an
320
+ // object-literal getter would create a closure and define an accessor
321
+ // property on every rejection, which showed up as the single largest cost
322
+ // on the rejection path. `toJSON` keeps JSON.stringify output identical to
323
+ // the eager shape. Note for tests: deepStrictEqual against a plain object
324
+ // compares prototypes; read `.errors` and compare that.
325
+ class LazyRejection {
326
+ constructor(build, data) {
327
+ this.valid = false;
328
+ this._build = build;
329
+ this._data = data;
330
+ this._errors = null;
331
+ }
332
+ toJSON() {
333
+ return { valid: false, errors: this.errors };
334
+ }
335
+ }
336
+ Object.defineProperty(LazyRejection.prototype, 'errors', {
337
+ enumerable: true,
338
+ configurable: true,
339
+ get() {
340
+ if (this._errors === null) this._errors = this._build(this._data);
341
+ return this._errors;
342
+ },
343
+ });
344
+
317
345
  const SIMDJSON_THRESHOLD = 8192;
318
346
 
319
347
  // Resolve a JSON Schema path like "#/properties/name/type" to the schema object
@@ -440,6 +468,23 @@ function _normalizeCallerSchema(s, inheritDraft7) {
440
468
  return JSON.stringify(copy) === str ? s : copy
441
469
  }
442
470
 
471
+ // The identity a document is registered under. Draft-07 ignores every
472
+ // keyword sitting next to `$ref`, so normalization drops them, `$id` among
473
+ // them: that is the right reading for evaluation, where the reference
474
+ // resolves against the retrieval URI rather than the declared `$id`. It is
475
+ // the wrong reading for registration, since `$id` is how the caller names
476
+ // the document. So the identity is read from the normalized copy first and
477
+ // from what the caller passed second. A bare-fragment `$id` is a draft-07
478
+ // anchor rather than a document identity, and normalization has already
479
+ // turned it into `$anchor`, so it is not used here.
480
+ function declaredId(original, normalized) {
481
+ const n = normalized && typeof normalized === 'object' ? normalized.$id : undefined
482
+ if (typeof n === 'string' && n !== '') return n
483
+ const o = original && typeof original === 'object' ? original.$id : undefined
484
+ if (typeof o === 'string' && o !== '' && o[0] !== '#') return o
485
+ return undefined
486
+ }
487
+
443
488
  // `inheritDraft7` is true when the root schema is draft-07: a retrieved
444
489
  // document that declares no dialect is read under the root's draft.
445
490
  function buildSchemaMap(schemas, inheritDraft7) {
@@ -448,7 +493,7 @@ function buildSchemaMap(schemas, inheritDraft7) {
448
493
  if (Array.isArray(schemas)) {
449
494
  for (const s of schemas) {
450
495
  const normalized = _normalizeCallerSchema(s, inheritDraft7)
451
- const id = normalized.$id
496
+ const id = declaredId(s, normalized)
452
497
  if (!id) throw new Error('Schema in schemas option must have $id')
453
498
  map.set(id, normalized)
454
499
  }
@@ -459,7 +504,8 @@ function buildSchemaMap(schemas, inheritDraft7) {
459
504
  // under and by the $id it declares. Registering only the $id makes
460
505
  // references to the retrieval URI unresolvable.
461
506
  map.set(key, normalized)
462
- if (normalized.$id && normalized.$id !== key) map.set(normalized.$id, normalized)
507
+ const id = declaredId(s, normalized)
508
+ if (id && id !== key) map.set(id, normalized)
463
509
  }
464
510
  }
465
511
  return map
@@ -904,6 +950,10 @@ class Validator {
904
950
  } catch {}
905
951
  }
906
952
 
953
+ // The boolean engine is the verdict authority for these paths; the
954
+ // final lazy wrapper uses it to skip error construction entirely.
955
+ if (!hasDynRef || _isCodegen) this._fastVerdict = preprocess ? null : jsFn;
956
+
907
957
  if (options.abortEarly && jsFn && !hasDynRef) {
908
958
  // abortEarly: do NOT enrich. Skip position lookups, suggestions, source maps.
909
959
  // This is the perf-critical path for edge gateways. The richErrors wrap
@@ -920,10 +970,22 @@ class Validator {
920
970
  ? (data) => { preprocess(data); return _fn(data) ? _R : _efn(data); }
921
971
  : (data) => _fn(data) ? _R : _efn(data);
922
972
  } else if (hasDynRef) {
923
- // $dynamicRef without codegen: delegate to native C++ (interpretive path unreliable)
973
+ // $dynamicRef without codegen: the interpreted engine. It scores the
974
+ // same on the suite's $dynamicRef cases as the native walker since the
975
+ // dynamic-scope fix, needs no addon, and gets the verdict-only mode.
976
+ if (!_interp) {
977
+ const { createInterpreter } = require('./lib/interpreter');
978
+ _interp = createInterpreter(schemaObj, {
979
+ schemaMap: this._schemaMap.size > 0 ? this._schemaMap : null,
980
+ formats: this._userFormats,
981
+ v1: isV1Dialect(schemaObj),
982
+ });
983
+ }
984
+ const interp = _interp;
985
+ this._fastVerdict = preprocess ? null : (d) => interp.isValid(d);
924
986
  this.validate = preprocess
925
- ? (data) => { preprocess(data); return errFn(data); }
926
- : errFn;
987
+ ? (data) => { preprocess(data); return interp.validate(data); }
988
+ : (data) => interp.validate(data);
927
989
  } else if (jsFn && jsFn._hybridFactory) {
928
990
  // Zero-wrapper: hybridFactory bakes VALID_RESULT + errFn into a single function
929
991
  // No arrow function wrapper, no ternary, one function call
@@ -1098,15 +1160,11 @@ class Validator {
1098
1160
  // propertyDependencies exists only in the interpreted engine, so a schema
1099
1161
  // using it goes there even when it also uses $dynamicRef.
1100
1162
  const _hasPropDeps = this._schemaStr.includes('"propertyDependencies"')
1163
+ // $dynamicRef used to delegate to the native validateJSON path here;
1164
+ // the interpreted engine now scores the same on those cases, carries
1165
+ // the verdict-only mode, and works without the addon.
1101
1166
  let _validate;
1102
- if (_hasDynRef && !_hasUneval && !_hasPropDeps && !this._v1Dynamic) {
1103
- // validateJSON is the C++ path with full anchor-map support; the NAPI
1104
- // direct V8 `validate` path has no anchor maps.
1105
- this._engine = 'native';
1106
- _validate = (data) => this._compiled.validateJSON(JSON.stringify(data));
1107
- this.validateJSON = (jsonStr) => this._compiled.validateJSON(jsonStr);
1108
- this.isValidJSON = (jsonStr) => this._compiled.isValidJSON(jsonStr);
1109
- } else {
1167
+ {
1110
1168
  const { createInterpreter } = require('./lib/interpreter');
1111
1169
  const interp = createInterpreter(schemaObj, {
1112
1170
  schemaMap: this._schemaMap.size > 0 ? this._schemaMap : null,
@@ -1115,6 +1173,7 @@ class Validator {
1115
1173
  });
1116
1174
  this._engine = 'interpreter';
1117
1175
  _validate = (data) => interp.validate(data);
1176
+ this._fastVerdict = preprocess ? null : (d) => interp.isValid(d);
1118
1177
  this.validateJSON = (jsonStr) => {
1119
1178
  try {
1120
1179
  return _validate(JSON.parse(jsonStr));
@@ -1130,7 +1189,9 @@ class Validator {
1130
1189
  return _validate(data);
1131
1190
  }
1132
1191
  : _validate;
1133
- this.isValidObject = (data) => _validate(data).valid;
1192
+ this.isValidObject = this._fastVerdict
1193
+ ? this._fastVerdict
1194
+ : (data) => _validate(data).valid;
1134
1195
  this.validateAndParse = (jsonStr) => this._compiled.validateAndParse(jsonStr);
1135
1196
  {
1136
1197
  const slot = this._fastSlot;
@@ -1173,11 +1234,14 @@ class Validator {
1173
1234
  v1: isV1Dialect(schemaObj),
1174
1235
  });
1175
1236
  this._engine = 'interpreter';
1237
+ if (!preprocess) this._fastVerdict = (d) => interp.isValid(d);
1176
1238
  const run = preprocess
1177
1239
  ? (data) => { preprocess(data); return interp.validate(data); }
1178
1240
  : (data) => interp.validate(data);
1179
1241
  this.validate = run;
1180
- this.isValidObject = (data) => run(data).valid;
1242
+ this.isValidObject = this._fastVerdict
1243
+ ? this._fastVerdict
1244
+ : (data) => run(data).valid;
1181
1245
  this.validateJSON = (jsonStr) => {
1182
1246
  try {
1183
1247
  return run(JSON.parse(jsonStr));
@@ -1188,42 +1252,44 @@ class Validator {
1188
1252
  this.isValidJSON = (jsonStr) => this.validateJSON(jsonStr).valid;
1189
1253
  }
1190
1254
 
1191
- // Declaration-order errors: whichever engine produced them, multi-error
1192
- // results are sorted by the schema's keyword declaration order before
1193
- // enrichment. Single-error and abortEarly results pass through untouched.
1255
+ // Error presentation, one lazy layer: declaration-order sorting, rich
1256
+ // enrichment (received value, suggestions, source frames, docUrl), or the
1257
+ // raw v0.14 shape under `richErrors: false`. All of it is work a caller
1258
+ // that only reads `.valid` never sees, so it runs on first access to
1259
+ // `.errors` and is cached. One wrapper, one allocation per rejection.
1194
1260
  if (this.validate) {
1195
1261
  const inner = this.validate;
1262
+ const enrich = this._richErrors ? require('./lib/enrich-error').enrich : null;
1196
1263
  const root = this._schemaObj;
1264
+ const self = this;
1197
1265
  this.validate = (data) => {
1198
1266
  const result = inner(data);
1199
- if (result && !result.valid && result.errors && result.errors.length > 1 && result !== ABORT_EARLY_RESULT) {
1200
- return { valid: false, errors: sortErrorsBySchemaOrder(root, result.errors) };
1201
- }
1202
- return result;
1203
- };
1204
- }
1205
-
1206
- // richErrors enrichment: layered on top of whichever validate path was
1207
- // bound above. Verbose's parentSchema flows through because enrich()
1208
- // copies it. Opt-out (`richErrors: false`) leaves the raw v0.14 shape.
1209
- if (this._richErrors && this.validate) {
1210
- const inner = this.validate;
1211
- const enrich = require('./lib/enrich-error').enrich;
1212
- this.validate = (data) => {
1213
- const result = inner(data);
1214
- if (result && !result.valid && result.errors && result.errors.length) {
1215
- // abortEarly returns the shared ATA9000 stub; preserve it as-is so the
1216
- // perf fast path stays allocation-free and the documented code stays stable.
1217
- if (result === ABORT_EARLY_RESULT) return result;
1218
- const positions = (this._lastRawInput != null) ? this._posCache.get(this._lastRawInput) : null;
1219
- const enriched = result.errors.map((e) => enrich(e, {
1220
- data,
1221
- positions,
1222
- schemaPositions: this._schemaPositions,
1223
- schemaFile: this._source ? this._source.path : undefined,
1224
- }));
1225
- if (positions) this._posCache.reset();
1226
- return { valid: false, errors: enriched };
1267
+ // abortEarly returns the shared ATA9000 stub; preserve it as-is so the
1268
+ // perf fast path stays allocation-free and the documented code stays stable.
1269
+ if (result && result.valid === false && result !== ABORT_EARLY_RESULT) {
1270
+ // Positions come from the raw input when validateJSON set one;
1271
+ // resolved eagerly since the cache is reset per call.
1272
+ const positions = (enrich && self._lastRawInput != null) ? self._posCache.get(self._lastRawInput) : null;
1273
+ if (positions) self._posCache.reset();
1274
+ let cached = null;
1275
+ return {
1276
+ valid: false,
1277
+ get errors() {
1278
+ if (cached === null) {
1279
+ let raw = result.errors || [];
1280
+ if (raw.length > 1) raw = sortErrorsBySchemaOrder(root, raw);
1281
+ cached = (enrich && raw.length)
1282
+ ? raw.map((e) => enrich(e, {
1283
+ data,
1284
+ positions,
1285
+ schemaPositions: self._schemaPositions,
1286
+ schemaFile: self._source ? self._source.path : undefined,
1287
+ }))
1288
+ : raw;
1289
+ }
1290
+ return cached;
1291
+ },
1292
+ };
1227
1293
  }
1228
1294
  return result;
1229
1295
  };
@@ -1231,7 +1297,7 @@ class Validator {
1231
1297
  // validateJSON also enriches: set _lastRawInput so the position cache
1232
1298
  // can lazily build a map for dataFrame attachment. Only validateJSON
1233
1299
  // wires this — validate(data) takes a pre-parsed object, by design.
1234
- if (this.validateJSON) {
1300
+ if (this._richErrors && this.validateJSON) {
1235
1301
  const innerJson = this.validateJSON;
1236
1302
  this.validateJSON = (jsonStr) => {
1237
1303
  this._lastRawInput = jsonStr;
@@ -1335,6 +1401,34 @@ class Validator {
1335
1401
  }
1336
1402
  }
1337
1403
 
1404
+ // Errors are paid for when read, not when produced. The full pipeline
1405
+ // above (error codegen, enrichment, custom messages, verbose) stays
1406
+ // intact, but validate() now answers the verdict from the boolean
1407
+ // engine and materializes `errors` through a getter on first access.
1408
+ // A caller that only reads `.valid`, which is every gateway check and
1409
+ // every benchmark, skips error construction entirely; a caller that
1410
+ // reads `.errors` pays once and the result is cached. Skipped when the
1411
+ // schema coerces or defaults (preprocess mutates before the verdict),
1412
+ // under abortEarly (already a frozen stub), and for $dynamicRef (the
1413
+ // boolean engine is not the authority there).
1414
+ if (this._fastVerdict && !preprocess && !options.abortEarly && this.validate) {
1415
+ const _full = this.validate;
1416
+ const _fast = this._fastVerdict;
1417
+ const EMPTY_ERRORS = Object.freeze([]);
1418
+ const _buildErrors = (data) => {
1419
+ const r = _full(data);
1420
+ return (r && r.valid === false && r.errors && r.errors.length)
1421
+ ? r.errors
1422
+ // The data changed between the verdict and this read; keep the
1423
+ // verdict and say so rather than inventing a specific error.
1424
+ : [{ keyword: 'validation', instancePath: '', schemaPath: '#', params: {}, message: 'schema validation failed' }];
1425
+ };
1426
+ this.validate = (data) => {
1427
+ if (_fast(data)) return { valid: true, data, errors: EMPTY_ERRORS };
1428
+ return new LazyRejection(_buildErrors, data);
1429
+ };
1430
+ }
1431
+
1338
1432
  // The buffer APIs answer from the native walker, which disagrees with
1339
1433
  // validate() on shapes listed in lib/buffer-gate.js. For those schemas
1340
1434
  // every buffer entry point goes through validate() instead.
package/lib/aot.js CHANGED
@@ -21,7 +21,7 @@ const SAFE_REGEX_SOURCE = require('./safe-regex-source');
21
21
  const _CP_LEN_SOURCE = `function _cpLen(s) {
22
22
  const len = s.length;
23
23
  for (let i = 0; i < len; i++) {
24
- if (s.charCodeAt(i) >= 0xD800 && s.charCodeAt(i) <= 0xDBFF) {
24
+ if (((s.charCodeAt(i) - 0xD800) >>> 0) < 0x400) {
25
25
  let n = 0; for (const _ of s) n++; return n;
26
26
  }
27
27
  }