ata-validator 0.7.3 → 0.9.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/index.js CHANGED
@@ -264,6 +264,10 @@ function buildPreprocessCodegen(schema, options) {
264
264
  // Schema compilation cache: same schema string -> reuse compiled functions
265
265
  const _compileCache = new Map();
266
266
 
267
+ // Object identity cache: same schema object reference -> reuse entire compiled state
268
+ // Skips JSON.stringify, cache lookup, and all setup. Near-zero cost for repeated schemas.
269
+ const _identityCache = new WeakMap();
270
+
267
271
  const SIMDJSON_PADDING = 64;
268
272
  const VALID_RESULT = Object.freeze({ valid: true, errors: Object.freeze([]) });
269
273
 
@@ -309,17 +313,31 @@ function buildSchemaMap(schemas) {
309
313
  return map
310
314
  }
311
315
 
316
+ // Resolve a relative URI ref against a base URI
317
+ function resolveRelativeRef(ref, baseId) {
318
+ if (!baseId || ref.includes('://') || ref.startsWith('#')) return ref
319
+ const lastSlash = baseId.lastIndexOf('/')
320
+ if (lastSlash < 0) return ref
321
+ return baseId.substring(0, lastSlash + 1) + ref
322
+ }
323
+
312
324
  class Validator {
313
325
  constructor(schema, opts) {
314
326
  const options = opts || {};
315
327
  const schemaObj = typeof schema === "string" ? JSON.parse(schema) : schema;
316
328
 
329
+ // Ultra-fast path: same schema object reference -> return cached instance
330
+ // JS constructor returning an object makes `new` return that object
331
+ // Cost: one WeakMap lookup. No property copy, no setup, nothing.
332
+ if (!opts && typeof schema === "object" && schema !== null) {
333
+ const hit = _identityCache.get(schema);
334
+ if (hit) return hit;
335
+ }
336
+
317
337
  // Draft 7 normalization — convert keywords to 2020-12 equivalents in-place
318
338
  normalizeDraft7(schemaObj);
319
339
 
320
- const schemaStr = JSON.stringify(schemaObj);
321
-
322
- this._schemaStr = schemaStr;
340
+ this._schemaStr = null; // lazy: computed on first use
323
341
  this._schemaObj = schemaObj;
324
342
  this._options = options;
325
343
  this._initialized = false;
@@ -350,6 +368,11 @@ class Validator {
350
368
  this._ensureCompiled();
351
369
  return this.isValidJSON(jsonStr);
352
370
  };
371
+ this.validateAndParse = (jsonStr) => {
372
+ if (!native) throw new Error('Native addon required for validateAndParse()');
373
+ this._ensureCompiled();
374
+ return this.validateAndParse(jsonStr);
375
+ };
353
376
  this.isValid = (buf) => {
354
377
  if (!native) throw new Error('Native addon required for isValid() — use validate() or isValidObject() instead');
355
378
  this._ensureCompiled();
@@ -399,23 +422,29 @@ class Validator {
399
422
  const schemaObj = this._schemaObj;
400
423
  const options = this._options;
401
424
 
425
+ // Lazy stringify — only computed here, not in constructor
426
+ if (!this._schemaStr) this._schemaStr = JSON.stringify(schemaObj);
427
+
402
428
  // Check cache first -- reuse compiled functions for same schema
403
429
  const sm = this._schemaMap.size > 0 ? this._schemaMap : null;
404
430
  const mapKey = this._schemaMap.size > 0
405
431
  ? this._schemaStr + '\0' + [...this._schemaMap.keys()].sort().join('\0')
406
432
  : this._schemaStr;
407
433
  const cached = _compileCache.get(mapKey);
408
- let jsFn, jsCombinedFn, jsErrFn;
434
+ let jsFn, jsCombinedFn, jsErrFn, _isCodegen = false;
409
435
  var _forceNapi = typeof process !== 'undefined' && process.env && process.env.ATA_FORCE_NAPI;
410
436
  if (cached && !_forceNapi) {
411
437
  jsFn = cached.jsFn;
412
438
  jsCombinedFn = cached.combined;
413
439
  jsErrFn = cached.errFn;
440
+ _isCodegen = !!cached.isCodegen;
414
441
  } else if (!_forceNapi) {
415
- jsFn = compileToJSCodegen(schemaObj, sm) || compileToJS(schemaObj, null, sm);
442
+ const _cgFn = compileToJSCodegen(schemaObj, sm);
443
+ jsFn = _cgFn || compileToJS(schemaObj, null, sm);
416
444
  jsCombinedFn = compileToJSCombined(schemaObj, VALID_RESULT, sm);
417
445
  jsErrFn = compileToJSCodegenWithErrors(schemaObj, sm);
418
- _compileCache.set(mapKey, { jsFn, combined: jsCombinedFn, errFn: jsErrFn });
446
+ _isCodegen = !!_cgFn;
447
+ _compileCache.set(mapKey, { jsFn, combined: jsCombinedFn, errFn: jsErrFn, isCodegen: _isCodegen });
419
448
  } else {
420
449
  jsFn = null; jsCombinedFn = null; jsErrFn = null;
421
450
  }
@@ -464,15 +493,21 @@ class Validator {
464
493
  }
465
494
  // errFn: use JS codegen if safe, else lazy-native fallback
466
495
  // For unevaluated schemas without errFn, use jsFn as boolean-only fallback
467
- const hasUnevaluated = schemaObj && JSON.stringify(schemaObj).includes('unevaluatedProperties') || JSON.stringify(schemaObj).includes('unevaluatedItems')
496
+ const hasUnevaluated = schemaObj && (schemaObj.unevaluatedProperties !== undefined || schemaObj.unevaluatedItems !== undefined || this._schemaStr.includes('unevaluatedProperties') || this._schemaStr.includes('unevaluatedItems'))
497
+ const hasDynRef = this._schemaStr.includes('"$dynamicRef"') || this._schemaStr.includes('"$dynamicAnchor"')
468
498
  const errFn =
469
499
  safeErrFn ||
470
500
  (hasUnevaluated
471
501
  ? (d) => ({ valid: jsFn(d), errors: jsFn(d) ? [] : [{ code: 'unevaluated', path: '', message: 'unevaluated property or item' }] })
472
- : (d) => {
473
- this._ensureNative();
474
- return this._compiled.validate(d);
475
- });
502
+ : hasDynRef
503
+ ? (d) => {
504
+ this._ensureNative();
505
+ return this._compiled.validateJSON(JSON.stringify(d));
506
+ }
507
+ : (d) => {
508
+ this._ensureNative();
509
+ return this._compiled.validate(d);
510
+ });
476
511
 
477
512
  // Best path: combined validator (single pass, validates + collects errors)
478
513
  // Valid data: returns VALID_RESULT, no allocation
@@ -501,23 +536,30 @@ class Validator {
501
536
  } catch {}
502
537
  }
503
538
 
504
- if (safeCombinedFn && jsFn) {
505
- // Hybrid: jsFn boolean guard for valid (fast, no allocation), combined for invalid
539
+ if (hasDynRef && _isCodegen && jsFn) {
540
+ // $dynamicRef with JS codegen: direct path, no wrapper layers
541
+ const _fn = jsFn, _efn = safeErrFn || errFn, _R = VALID_RESULT;
506
542
  this.validate = preprocess
507
- ? (data) => {
508
- preprocess(data);
509
- return jsFn(data) ? VALID_RESULT : safeCombinedFn(data);
510
- }
511
- : (data) => jsFn(data) ? VALID_RESULT : safeCombinedFn(data);
543
+ ? (data) => { preprocess(data); return _fn(data) ? _R : _efn(data); }
544
+ : (data) => _fn(data) ? _R : _efn(data);
545
+ } else if (hasDynRef) {
546
+ // $dynamicRef without codegen: delegate to native C++ (interpretive path unreliable)
547
+ this.validate = preprocess
548
+ ? (data) => { preprocess(data); return errFn(data); }
549
+ : errFn;
550
+ } else if (jsFn && jsFn._hybridFactory) {
551
+ // Zero-wrapper: hybridFactory bakes VALID_RESULT + errFn into a single function
552
+ // No arrow function wrapper, no ternary, one function call
553
+ const hybridFn = jsFn._hybridFactory(VALID_RESULT, safeCombinedFn || errFn);
554
+ this.validate = preprocess
555
+ ? (data) => { preprocess(data); return hybridFn(data); }
556
+ : hybridFn;
512
557
  } else if (safeCombinedFn) {
513
558
  this.validate = preprocess
514
- ? (data) => {
515
- preprocess(data);
516
- return safeCombinedFn(data);
517
- }
559
+ ? (data) => { preprocess(data); return safeCombinedFn(data); }
518
560
  : safeCombinedFn;
519
561
  } else {
520
- const hybridFn = jsFn._hybridFactory
562
+ const hybridFn = jsFn && jsFn._hybridFactory
521
563
  ? jsFn._hybridFactory(VALID_RESULT, errFn)
522
564
  : null;
523
565
  this.validate = hybridFn
@@ -592,6 +634,17 @@ class Validator {
592
634
  return false;
593
635
  }
594
636
  };
637
+ // validateAndParse: requires native addon for simdjson parsing
638
+ if (native) {
639
+ const self = this;
640
+ this.validateAndParse = (jsonStr) => {
641
+ self._ensureNative();
642
+ self.validateAndParse = (s) => self._compiled.validateAndParse(s);
643
+ return self.validateAndParse(jsonStr);
644
+ };
645
+ } else {
646
+ this.validateAndParse = () => { throw new Error('Native addon required for validateAndParse()'); };
647
+ }
595
648
  // Buffer APIs: lazy native init — only compile native schema on first buffer call.
596
649
  // This keeps cold start fast (JS codegen only) for users who only use validate().
597
650
  if (native) {
@@ -616,17 +669,24 @@ class Validator {
616
669
  };
617
670
  }
618
671
  } else if (native) {
619
- // ATA_FORCE_NAPI path: no JS codegen, use native for everything
672
+ // Native-only path: no JS codegen, use native for everything
620
673
  this._ensureNative();
674
+ const _hasDynamic = this._schemaStr.includes('"$dynamicRef"') || this._schemaStr.includes('"$dynamicAnchor"') || this._schemaStr.includes('"$anchor"')
675
+ // For schemas with dynamic refs/anchors, use validateJSON (C++ path with full support)
676
+ // instead of validate (NAPI direct V8 path without anchor maps)
677
+ const _validate = _hasDynamic
678
+ ? (data) => this._compiled.validateJSON(JSON.stringify(data))
679
+ : (data) => this._compiled.validate(data);
621
680
  this.validate = preprocess
622
681
  ? (data) => {
623
682
  preprocess(data);
624
- return this._compiled.validate(data);
683
+ return _validate(data);
625
684
  }
626
- : (data) => this._compiled.validate(data);
627
- this.isValidObject = (data) => this._compiled.validate(data).valid;
685
+ : _validate;
686
+ this.isValidObject = (data) => _validate(data).valid;
628
687
  this.validateJSON = (jsonStr) => this._compiled.validateJSON(jsonStr);
629
688
  this.isValidJSON = (jsonStr) => this._compiled.isValidJSON(jsonStr);
689
+ this.validateAndParse = (jsonStr) => this._compiled.validateAndParse(jsonStr);
630
690
  {
631
691
  const slot = this._fastSlot;
632
692
  this.isValid = (buf) => {
@@ -655,6 +715,11 @@ class Validator {
655
715
  };
656
716
  }
657
717
  }
718
+
719
+ // Save to identity cache for ultra-fast reuse with same schema object
720
+ if (this._schemaObj && typeof this._schemaObj === 'object') {
721
+ _identityCache.set(this._schemaObj, this);
722
+ }
658
723
  }
659
724
 
660
725
  _ensureNative() {
@@ -831,6 +896,14 @@ module.exports = { boolFn, hybridFactory, errFn };
831
896
  }
832
897
  };
833
898
 
899
+ v.validateAndParse = native
900
+ ? (jsonStr) => {
901
+ v._ensureNative();
902
+ v.validateAndParse = (s) => v._compiled.validateAndParse(s);
903
+ return v.validateAndParse(jsonStr);
904
+ }
905
+ : () => { throw new Error('Native addon required for validateAndParse()'); };
906
+
834
907
  // Standard Schema V1
835
908
  Object.defineProperty(v, "~standard", {
836
909
  value: Object.freeze({
@@ -1030,10 +1103,31 @@ Validator.loadBundle = function (mods, schemas, opts) {
1030
1103
  });
1031
1104
  };
1032
1105
 
1106
+ const parseJSON = native ? native.parseJSON : JSON.parse;
1107
+
1108
+ // Ultra-fast compile: returns validate function directly, no Validator wrapper
1109
+ // WeakMap cached — second call with same schema object is ~3ns
1110
+ const _compileFnCache = new WeakMap();
1111
+ function compile(schema, opts) {
1112
+ if (!opts && typeof schema === 'object' && schema !== null) {
1113
+ const hit = _compileFnCache.get(schema);
1114
+ if (hit) return hit;
1115
+ }
1116
+ const v = new Validator(schema, opts);
1117
+ v._ensureCompiled();
1118
+ const fn = v.validate;
1119
+ if (!opts && typeof schema === 'object' && schema !== null) {
1120
+ _compileFnCache.set(schema, fn);
1121
+ }
1122
+ return fn;
1123
+ }
1124
+
1033
1125
  module.exports = {
1034
1126
  Validator,
1127
+ compile,
1035
1128
  validate,
1036
1129
  version,
1037
1130
  createPaddedBuffer,
1038
1131
  SIMDJSON_PADDING,
1132
+ parseJSON,
1039
1133
  };