ata-validator 0.4.8 → 0.4.9
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/binding/ata_napi.cpp +1 -1
- package/binding.gyp +2 -2
- package/index.js +204 -0
- package/lib/js-compiler.js +7 -3
- package/package.json +1 -1
- package/src/ata.cpp +5 -0
package/binding/ata_napi.cpp
CHANGED
|
@@ -1048,7 +1048,7 @@ static ThreadPool& pool() {
|
|
|
1048
1048
|
|
|
1049
1049
|
// --- Fast Validation Registry ---
|
|
1050
1050
|
// Global schema slots for V8 Fast API (bypasses NAPI overhead)
|
|
1051
|
-
static constexpr size_t MAX_FAST_SLOTS =
|
|
1051
|
+
static constexpr size_t MAX_FAST_SLOTS = 4096;
|
|
1052
1052
|
static ata::schema_ref g_fast_schemas[MAX_FAST_SLOTS];
|
|
1053
1053
|
static std::string g_fast_schema_jsons[MAX_FAST_SLOTS];
|
|
1054
1054
|
static uint32_t g_fast_slot_count = 0;
|
package/binding.gyp
CHANGED
|
@@ -11,10 +11,10 @@
|
|
|
11
11
|
"<!@(node -p \"require('node-addon-api').include\")",
|
|
12
12
|
"include",
|
|
13
13
|
"deps/simdjson",
|
|
14
|
-
"<!@(node -e \"var p=process.platform,a=process.arch;if(p==='darwin'){console.log(a==='arm64'?'/opt/homebrew/opt/re2/include':'/usr/local/opt/re2/include');console.log(a==='arm64'?'/opt/homebrew/opt/abseil/include':'/usr/local/opt/abseil/include')}else{console.log('/usr/include')}\")"
|
|
14
|
+
"<!@(node -e \"var p=process.platform,a=process.arch;if(p==='darwin'){console.log(a==='arm64'?'/opt/homebrew/opt/re2/include':'/usr/local/opt/re2/include');console.log(a==='arm64'?'/opt/homebrew/opt/abseil/include':'/usr/local/opt/abseil/include');console.log(a==='arm64'?'/opt/homebrew/opt/mimalloc/include':'/usr/local/opt/mimalloc/include')}else{console.log('/usr/include')}\")"
|
|
15
15
|
],
|
|
16
16
|
"libraries": [
|
|
17
|
-
"<!@(node -e \"var p=process.platform,a=process.arch;if(p==='darwin'){var pre=a==='arm64'?'/opt/homebrew/opt/re2':'/usr/local/opt/re2';console.log('-L'+pre+'/lib -lre2')}else{console.log('-lre2')}\")"
|
|
17
|
+
"<!@(node -e \"var p=process.platform,a=process.arch;if(p==='darwin'){var pre=a==='arm64'?'/opt/homebrew/opt/re2':'/usr/local/opt/re2';var mi=a==='arm64'?'/opt/homebrew/opt/mimalloc':'/usr/local/opt/mimalloc';console.log('-L'+pre+'/lib -lre2 -L'+mi+'/lib -lmimalloc')}else{console.log('-lre2')}\")"
|
|
18
18
|
],
|
|
19
19
|
"dependencies": [
|
|
20
20
|
"<!(node -p \"require('node-addon-api').gyp\")"
|
package/index.js
CHANGED
|
@@ -211,6 +211,7 @@ class Validator {
|
|
|
211
211
|
// Pure JS fast path — no NAPI, runs in V8 JIT
|
|
212
212
|
// Set ATA_FORCE_NAPI=1 to disable JS codegen (for correctness testing)
|
|
213
213
|
const schemaObj = typeof schema === "string" ? JSON.parse(schema) : schema;
|
|
214
|
+
this._schemaObj = schemaObj;
|
|
214
215
|
const jsFn = process.env.ATA_FORCE_NAPI
|
|
215
216
|
? null
|
|
216
217
|
: (compileToJSCodegen(schemaObj) || compileToJS(schemaObj));
|
|
@@ -330,6 +331,107 @@ class Validator {
|
|
|
330
331
|
});
|
|
331
332
|
}
|
|
332
333
|
|
|
334
|
+
// --- Standalone pre-compilation ---
|
|
335
|
+
// Generate a JS module string that can be written to a file.
|
|
336
|
+
// On next startup, load with Validator.fromStandalone() — zero compile time.
|
|
337
|
+
toStandalone() {
|
|
338
|
+
const jsFn = this._jsFn;
|
|
339
|
+
if (!jsFn || !jsFn._source) return null;
|
|
340
|
+
const src = jsFn._source;
|
|
341
|
+
const hybridSrc = jsFn._hybridSource || '';
|
|
342
|
+
|
|
343
|
+
// Also capture error function source for zero-compile standalone load
|
|
344
|
+
const jsErrFn = compileToJSCodegenWithErrors(
|
|
345
|
+
typeof this._schemaObj === 'object' ? this._schemaObj : {}
|
|
346
|
+
);
|
|
347
|
+
const errSrc = jsErrFn && jsErrFn._errSource ? jsErrFn._errSource : '';
|
|
348
|
+
|
|
349
|
+
return `// Auto-generated by ata-validator — do not edit
|
|
350
|
+
'use strict';
|
|
351
|
+
const boolFn = function(d) {
|
|
352
|
+
${src}
|
|
353
|
+
};
|
|
354
|
+
const hybridFactory = function(R, E) {
|
|
355
|
+
return function(d) {
|
|
356
|
+
${hybridSrc}
|
|
357
|
+
};
|
|
358
|
+
};
|
|
359
|
+
${errSrc ? `const errFn = function(d, _all) {\n ${errSrc}\n};` : 'const errFn = null;'}
|
|
360
|
+
module.exports = { boolFn, hybridFactory, errFn };
|
|
361
|
+
`;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Load a pre-compiled standalone module. Zero schema compilation.
|
|
365
|
+
// No NAPI, no native compile — pure JS. Startup in microseconds.
|
|
366
|
+
// Usage: const v = Validator.fromStandalone(require('./compiled.js'), schema, opts)
|
|
367
|
+
static fromStandalone(mod, schema, opts) {
|
|
368
|
+
const options = opts || {};
|
|
369
|
+
const schemaObj = typeof schema === "string" ? JSON.parse(schema) : schema;
|
|
370
|
+
|
|
371
|
+
// Create a lightweight instance — skip NAPI compile entirely
|
|
372
|
+
const v = Object.create(Validator.prototype);
|
|
373
|
+
v._jsFn = mod.boolFn;
|
|
374
|
+
v._compiled = null;
|
|
375
|
+
v._fastSlot = -1;
|
|
376
|
+
|
|
377
|
+
// Mutators
|
|
378
|
+
const applyDefaults = buildDefaultsApplier(schemaObj);
|
|
379
|
+
const applyCoerce = options.coerceTypes ? buildCoercer(schemaObj) : null;
|
|
380
|
+
const applyRemove = options.removeAdditional ? buildRemover(schemaObj) : null;
|
|
381
|
+
const mutators = [applyRemove, applyCoerce, applyDefaults].filter(Boolean);
|
|
382
|
+
const preprocess = mutators.length === 0 ? null
|
|
383
|
+
: mutators.length === 1 ? mutators[0]
|
|
384
|
+
: (data) => { for (let i = 0; i < mutators.length; i++) mutators[i](data); };
|
|
385
|
+
v._preprocess = preprocess;
|
|
386
|
+
|
|
387
|
+
// Error function — use pre-compiled from standalone if available, else compile
|
|
388
|
+
let errFn = (d) => ({ valid: false, errors: [{ code: 'validation_failed', path: '', message: 'validation failed' }] });
|
|
389
|
+
if (mod.errFn) {
|
|
390
|
+
errFn = (d) => mod.errFn(d, true);
|
|
391
|
+
} else {
|
|
392
|
+
const jsErrFn = compileToJSCodegenWithErrors(schemaObj);
|
|
393
|
+
if (jsErrFn) {
|
|
394
|
+
try { jsErrFn({}, true); errFn = (d) => jsErrFn(d, true); } catch {}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Hybrid or speculative
|
|
399
|
+
const hybridFn = mod.hybridFactory
|
|
400
|
+
? mod.hybridFactory(VALID_RESULT, errFn)
|
|
401
|
+
: null;
|
|
402
|
+
|
|
403
|
+
v.validate = hybridFn
|
|
404
|
+
? (preprocess ? (data) => { preprocess(data); return hybridFn(data); } : hybridFn)
|
|
405
|
+
: (preprocess
|
|
406
|
+
? (data) => { preprocess(data); return mod.boolFn(data) ? VALID_RESULT : errFn(data); }
|
|
407
|
+
: (data) => mod.boolFn(data) ? VALID_RESULT : errFn(data));
|
|
408
|
+
v.isValidObject = mod.boolFn;
|
|
409
|
+
v.isValidJSON = (jsonStr) => {
|
|
410
|
+
try { return mod.boolFn(JSON.parse(jsonStr)); } catch { return false; }
|
|
411
|
+
};
|
|
412
|
+
v.validateJSON = (jsonStr) => {
|
|
413
|
+
try {
|
|
414
|
+
const obj = JSON.parse(jsonStr);
|
|
415
|
+
return hybridFn ? hybridFn(obj) : (mod.boolFn(obj) ? VALID_RESULT : errFn(obj));
|
|
416
|
+
} catch { return { valid: false, errors: [{ code: 'invalid_json', path: '', message: 'invalid JSON' }] }; }
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
// Standard Schema V1
|
|
420
|
+
Object.defineProperty(v, "~standard", {
|
|
421
|
+
value: Object.freeze({
|
|
422
|
+
version: 1, vendor: "ata-validator",
|
|
423
|
+
validate(value) {
|
|
424
|
+
const result = v.validate(value);
|
|
425
|
+
if (result.valid) return { value };
|
|
426
|
+
return { issues: result.errors.map(e => ({ message: e.message, path: parsePointerPath(e.path) })) };
|
|
427
|
+
},
|
|
428
|
+
}),
|
|
429
|
+
writable: false, enumerable: false, configurable: false,
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
return v;
|
|
433
|
+
}
|
|
434
|
+
|
|
333
435
|
// Fallback methods — only used when JS codegen is unavailable
|
|
334
436
|
validate(data) {
|
|
335
437
|
if (this._preprocess) this._preprocess(data);
|
|
@@ -384,4 +486,106 @@ function version() {
|
|
|
384
486
|
return native.version();
|
|
385
487
|
}
|
|
386
488
|
|
|
489
|
+
// Bundle multiple validators into a single JS file for fast startup.
|
|
490
|
+
// Usage:
|
|
491
|
+
// const bundle = Validator.bundle([schema1, schema2, ...]);
|
|
492
|
+
// fs.writeFileSync('validators.js', bundle);
|
|
493
|
+
// // On startup:
|
|
494
|
+
// const validators = Validator.loadBundle(require('./validators.js'), [schema1, schema2, ...]);
|
|
495
|
+
Validator.bundle = function(schemas, opts) {
|
|
496
|
+
const parts = schemas.map(schema => {
|
|
497
|
+
const v = new Validator(schema, opts);
|
|
498
|
+
const standalone = v.toStandalone();
|
|
499
|
+
if (!standalone) return 'null';
|
|
500
|
+
return '(function(){' + standalone.replace("'use strict';", '').replace('module.exports = ', 'return ') + '})()';
|
|
501
|
+
});
|
|
502
|
+
return "'use strict';\nmodule.exports = [\n" + parts.join(',\n') + '\n];\n';
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
// Zero-dependency self-contained bundle — no require('ata-validator') needed at runtime.
|
|
506
|
+
Validator.bundleStandalone = function(schemas, opts) {
|
|
507
|
+
const R = "Object.freeze({valid:true,errors:Object.freeze([])})";
|
|
508
|
+
const fns = schemas.map(schema => {
|
|
509
|
+
const v = new Validator(schema, opts);
|
|
510
|
+
const jsFn = v._jsFn;
|
|
511
|
+
if (!jsFn || !jsFn._hybridSource) return 'null';
|
|
512
|
+
const jsErrFn = compileToJSCodegenWithErrors(
|
|
513
|
+
typeof schema === 'string' ? JSON.parse(schema) : schema
|
|
514
|
+
);
|
|
515
|
+
const errBody = jsErrFn && jsErrFn._errSource
|
|
516
|
+
? jsErrFn._errSource
|
|
517
|
+
: "return{valid:false,errors:[{code:'error',path:'',message:'validation failed'}]}";
|
|
518
|
+
return `(function(R){var E=function(d){var _all=true;${errBody}};return function(d){${jsFn._hybridSource}}})(R)`;
|
|
519
|
+
});
|
|
520
|
+
return `'use strict';\nvar R=${R};\nmodule.exports=[${fns.join(',')}];\n`;
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
// Compact bundle: deduplicated code. Shared template functions + per-schema params.
|
|
524
|
+
// Much smaller file → faster V8 parse → faster startup.
|
|
525
|
+
Validator.bundleCompact = function(schemas, opts) {
|
|
526
|
+
// Analyze schemas and group by structure
|
|
527
|
+
const entries = schemas.map(schema => {
|
|
528
|
+
const v = new Validator(schema, opts);
|
|
529
|
+
const jsFn = v._jsFn;
|
|
530
|
+
if (!jsFn || !jsFn._hybridSource) return null;
|
|
531
|
+
const jsErrFn = compileToJSCodegenWithErrors(
|
|
532
|
+
typeof schema === 'string' ? JSON.parse(schema) : schema
|
|
533
|
+
);
|
|
534
|
+
return {
|
|
535
|
+
hybrid: jsFn._hybridSource,
|
|
536
|
+
err: jsErrFn && jsErrFn._errSource ? jsErrFn._errSource : null,
|
|
537
|
+
};
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
// Deduplicate function bodies — many schemas produce identical or near-identical code
|
|
541
|
+
const bodyMap = new Map(); // body → index
|
|
542
|
+
const bodies = [];
|
|
543
|
+
const errMap = new Map();
|
|
544
|
+
const errBodies = [];
|
|
545
|
+
|
|
546
|
+
const indices = entries.map(e => {
|
|
547
|
+
if (!e) return [-1, -1];
|
|
548
|
+
let hi = bodyMap.get(e.hybrid);
|
|
549
|
+
if (hi === undefined) { hi = bodies.length; bodies.push(e.hybrid); bodyMap.set(e.hybrid, hi); }
|
|
550
|
+
let ei = -1;
|
|
551
|
+
if (e.err) {
|
|
552
|
+
ei = errMap.get(e.err);
|
|
553
|
+
if (ei === undefined) { ei = errBodies.length; errBodies.push(e.err); errMap.set(e.err, ei); }
|
|
554
|
+
}
|
|
555
|
+
return [hi, ei];
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
// Generate compact bundle
|
|
559
|
+
let out = "'use strict';\n";
|
|
560
|
+
out += "var R=Object.freeze({valid:true,errors:Object.freeze([])});\n";
|
|
561
|
+
|
|
562
|
+
// Shared hybrid factories
|
|
563
|
+
out += "var H=[\n";
|
|
564
|
+
out += bodies.map(b => `function(R,E){return function(d){${b}}}`).join(',\n');
|
|
565
|
+
out += "\n];\n";
|
|
566
|
+
|
|
567
|
+
// Shared error functions
|
|
568
|
+
out += "var EF=[\n";
|
|
569
|
+
out += errBodies.map(b => `function(d){var _all=true;${b}}`).join(',\n');
|
|
570
|
+
out += "\n];\n";
|
|
571
|
+
|
|
572
|
+
// Build validators from shared templates
|
|
573
|
+
out += "module.exports=[";
|
|
574
|
+
out += indices.map(([hi, ei]) => {
|
|
575
|
+
if (hi < 0) return 'null';
|
|
576
|
+
if (ei >= 0) return `H[${hi}](R,EF[${ei}])`;
|
|
577
|
+
return `H[${hi}](R,function(){return{valid:false,errors:[]}})`;
|
|
578
|
+
}).join(',');
|
|
579
|
+
out += "];\n";
|
|
580
|
+
|
|
581
|
+
return out;
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
Validator.loadBundle = function(mods, schemas, opts) {
|
|
585
|
+
return schemas.map((schema, i) => {
|
|
586
|
+
if (mods[i]) return Validator.fromStandalone(mods[i], schema, opts);
|
|
587
|
+
return new Validator(schema, opts);
|
|
588
|
+
});
|
|
589
|
+
};
|
|
590
|
+
|
|
387
591
|
module.exports = { Validator, validate, version, createPaddedBuffer, SIMDJSON_PADDING };
|
package/lib/js-compiler.js
CHANGED
|
@@ -531,14 +531,16 @@ function compileToJSCodegen(schema) {
|
|
|
531
531
|
const boolFn = new Function('d', body)
|
|
532
532
|
|
|
533
533
|
// Build hybrid: same body, return R instead of true, return E(d) instead of false.
|
|
534
|
-
// V8 optimizes this identically to jsFn — E(d) is dead code on valid path.
|
|
535
|
-
// 83M ops/sec vs 26M for combined. Invalid path: 34M vs 6M.
|
|
536
534
|
const hybridBody = replaceTopLevel(helperStr + checkStr + '\n return R')
|
|
537
535
|
try {
|
|
538
536
|
const factory = new Function('R', 'E', `return function(d){${hybridBody}}`)
|
|
539
537
|
boolFn._hybridFactory = factory
|
|
540
538
|
} catch {}
|
|
541
539
|
|
|
540
|
+
// Store source for standalone compilation (pre-build to file)
|
|
541
|
+
boolFn._source = body
|
|
542
|
+
boolFn._hybridSource = hybridBody
|
|
543
|
+
|
|
542
544
|
return boolFn
|
|
543
545
|
} catch {
|
|
544
546
|
return null
|
|
@@ -940,7 +942,9 @@ function compileToJSCodegenWithErrors(schema) {
|
|
|
940
942
|
lines.join('\n ') +
|
|
941
943
|
`\n return{valid:_e.length===0,errors:_e}`
|
|
942
944
|
try {
|
|
943
|
-
|
|
945
|
+
const fn = new Function('d', '_all', body)
|
|
946
|
+
fn._errSource = body
|
|
947
|
+
return fn
|
|
944
948
|
} catch {
|
|
945
949
|
return null
|
|
946
950
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ata-validator",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.9",
|
|
4
4
|
"description": "Ultra-fast JSON Schema validator. Beats ajv on every valid-path benchmark: 1.1x–2.7x faster validate(obj), 151x faster compilation, 5.9x faster parallel batch. Speculative validation with V8-optimized JS codegen, simdjson, multi-core. Standard Schema V1 compatible.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|