@telorun/kernel 0.68.0 → 0.70.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/dist/bigint-json.d.ts +45 -0
- package/dist/bigint-json.d.ts.map +1 -0
- package/dist/bigint-json.js +65 -0
- package/dist/bigint-json.js.map +1 -0
- package/dist/bigint-schema-view.d.ts +34 -0
- package/dist/bigint-schema-view.d.ts.map +1 -0
- package/dist/bigint-schema-view.js +85 -0
- package/dist/bigint-schema-view.js.map +1 -0
- package/dist/cel-handlers.d.ts.map +1 -1
- package/dist/cel-handlers.js +6 -8
- package/dist/cel-handlers.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/invocation-contract-binding.d.ts +0 -6
- package/dist/invocation-contract-binding.d.ts.map +1 -1
- package/dist/invocation-contract-binding.js +9 -64
- package/dist/invocation-contract-binding.js.map +1 -1
- package/dist/kernel.d.ts.map +1 -1
- package/dist/kernel.js +10 -3
- package/dist/kernel.js.map +1 -1
- package/dist/logging/encode-json.d.ts.map +1 -1
- package/dist/logging/encode-json.js +13 -9
- package/dist/logging/encode-json.js.map +1 -1
- package/dist/logging/encode-pretty.d.ts.map +1 -1
- package/dist/logging/encode-pretty.js +13 -4
- package/dist/logging/encode-pretty.js.map +1 -1
- package/dist/manifest-schemas.d.ts.map +1 -1
- package/dist/manifest-schemas.js +2 -1
- package/dist/manifest-schemas.js.map +1 -1
- package/dist/observed-state.d.ts.map +1 -1
- package/dist/observed-state.js +2 -0
- package/dist/observed-state.js.map +1 -1
- package/dist/resource-context.d.ts +10 -1
- package/dist/resource-context.d.ts.map +1 -1
- package/dist/resource-context.js +42 -11
- package/dist/resource-context.js.map +1 -1
- package/dist/schema-validator.d.ts.map +1 -1
- package/dist/schema-validator.js +42 -16
- package/dist/schema-validator.js.map +1 -1
- package/package.json +3 -3
- package/src/bigint-json.ts +69 -0
- package/src/bigint-schema-view.ts +76 -0
- package/src/cel-handlers.ts +6 -9
- package/src/index.ts +1 -0
- package/src/invocation-contract-binding.ts +9 -55
- package/src/kernel.ts +11 -0
- package/src/logging/encode-json.ts +14 -9
- package/src/logging/encode-pretty.ts +12 -3
- package/src/manifest-schemas.ts +2 -1
- package/src/observed-state.ts +2 -0
- package/src/resource-context.ts +46 -11
- package/src/schema-validator.ts +43 -16
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { formatUnixNano, type AnyValue, type ErrorValue, type LogRecord } from "@telorun/sdk";
|
|
1
|
+
import { bigIntAt, formatUnixNano, type AnyValue, type ErrorValue, type LogRecord } from "@telorun/sdk";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* The `json` encoding — `kernel/specs/logging.md` §11.1. One JSON object per
|
|
@@ -83,16 +83,21 @@ function sortKeysDeep(value: AnyValue): AnyValue {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
function makeReplacer(encodeBytes: BytesEncoder) {
|
|
86
|
-
return function replacer(this: unknown,
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
86
|
+
return function replacer(this: unknown, key: string, value: unknown): unknown {
|
|
87
|
+
// Read the BigInt off the HOLDER, not off `value`: `BigInt.prototype.toJSON`
|
|
88
|
+
// has already rewritten it to the exact-digits form every other JSON boundary
|
|
89
|
+
// wants (see `enableBigIntJson`). A log record is the one destination where
|
|
90
|
+
// that is wrong — a value beyond 2^53 loses precision in a JS receiver, so it
|
|
91
|
+
// degrades to a decimal string rather than to a wrong number, the same
|
|
92
|
+
// reasoning OTLP gives for quoting its 64-bit fields.
|
|
93
|
+
const source = bigIntAt(this, key);
|
|
94
|
+
if (source !== undefined) {
|
|
95
|
+
return source >= MIN_SAFE && source <= MAX_SAFE ? Number(source) : source.toString();
|
|
94
96
|
}
|
|
95
97
|
if (value instanceof Uint8Array) return encodeBytes(value);
|
|
96
98
|
return value;
|
|
97
99
|
};
|
|
98
100
|
}
|
|
101
|
+
|
|
102
|
+
const MIN_SAFE = BigInt(Number.MIN_SAFE_INTEGER);
|
|
103
|
+
const MAX_SAFE = BigInt(Number.MAX_SAFE_INTEGER);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { severityFloor, type AnyValue, type ErrorValue, type LogRecord } from "@telorun/sdk";
|
|
1
|
+
import { bigIntAt, severityFloor, type AnyValue, type ErrorValue, type LogRecord } from "@telorun/sdk";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* The `pretty` encoding — `kernel/specs/logging.md` §11.2. For humans on a
|
|
@@ -90,8 +90,17 @@ function quoteIfNeeded(text: string): string {
|
|
|
90
90
|
return /[\s"=]/.test(text) ? JSON.stringify(text) : text;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
function jsonSafe(
|
|
94
|
-
|
|
93
|
+
function jsonSafe(this: unknown, key: string, value: unknown): unknown {
|
|
94
|
+
// Read the integer off the HOLDER, not off `value`: `BigInt.prototype.toJSON`
|
|
95
|
+
// (see `enableBigIntJson`) runs BEFORE a replacer, so by the time this is
|
|
96
|
+
// called a wide integer is already the exact-digits form every wire encoding
|
|
97
|
+
// wants. This console encoding renders it as a decimal string instead, and
|
|
98
|
+
// matching on `typeof value === "bigint"` would silently stop doing so —
|
|
99
|
+
// a changed encoding, not an error. Reading the holder keeps the rendering
|
|
100
|
+
// identical whether or not the patch is installed, which matters because a
|
|
101
|
+
// test can call this encoder outside a booted kernel.
|
|
102
|
+
const source = bigIntAt(this, key);
|
|
103
|
+
if (source !== undefined) return source.toString();
|
|
95
104
|
if (value instanceof Uint8Array) return `<${value.byteLength} bytes>`;
|
|
96
105
|
return value;
|
|
97
106
|
}
|
package/src/manifest-schemas.ts
CHANGED
|
@@ -2,7 +2,7 @@ import AjvModule from "ajv";
|
|
|
2
2
|
import addFormats from "ajv-formats";
|
|
3
3
|
// One definition of the `status:` block's shape, shared with `telo check` — the
|
|
4
4
|
// `required:` restriction is reported by the analyzer, which can name the fix.
|
|
5
|
-
import { OBSERVED_STATE_SCHEMA } from "@telorun/analyzer";
|
|
5
|
+
import { binaryKeyword, OBSERVED_STATE_SCHEMA } from "@telorun/analyzer";
|
|
6
6
|
const Ajv = AjvModule.default ?? AjvModule;
|
|
7
7
|
|
|
8
8
|
// Re-export the shared ResourceRef fragment from the templating package
|
|
@@ -177,6 +177,7 @@ export const ResourceAbstractSchema = {
|
|
|
177
177
|
};
|
|
178
178
|
|
|
179
179
|
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
180
|
+
ajv.addKeyword(binaryKeyword());
|
|
180
181
|
addFormats.default(ajv);
|
|
181
182
|
|
|
182
183
|
// Lazy-compile validator: the AJV codegen cost (≈10–15 ms for these
|
package/src/observed-state.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { binaryKeyword } from "@telorun/analyzer";
|
|
1
2
|
import AjvModule from "ajv";
|
|
2
3
|
import { detachSnapshotValue, OBSERVED_STATE_KEY, RuntimeError } from "@telorun/sdk";
|
|
3
4
|
|
|
@@ -57,6 +58,7 @@ function mark(target: Record<string, unknown>, info: ObservedStateInfo): void {
|
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
61
|
+
ajv.addKeyword(binaryKeyword());
|
|
60
62
|
// Compiling a status schema costs ~ms and a resource may report repeatedly, so
|
|
61
63
|
// keep the validator keyed on the schema object it came from. The kind's folded
|
|
62
64
|
// `status:` is stamped once at registration, so this hits.
|
package/src/resource-context.ts
CHANGED
|
@@ -28,10 +28,12 @@ import {
|
|
|
28
28
|
type TypeRule,
|
|
29
29
|
type ZoneEntry,
|
|
30
30
|
} from "@telorun/sdk";
|
|
31
|
+
import { binaryKeyword } from "@telorun/analyzer";
|
|
31
32
|
import { isRefSentinel } from "@telorun/templating";
|
|
32
33
|
import { ZoneContext } from "./zone-context.js";
|
|
33
34
|
import * as path from "path";
|
|
34
35
|
import { pathToFileURL } from "url";
|
|
36
|
+
import { withBigIntsAsNumbers } from "./bigint-schema-view.js";
|
|
35
37
|
import type { ModuleArtifact } from "./bundle/module-artifact.js";
|
|
36
38
|
import { hostEnv } from "./host-env.js";
|
|
37
39
|
import type { LoggingHost } from "./logging/logging-host.js";
|
|
@@ -73,6 +75,12 @@ export class ResourceContextImpl implements ResourceContext {
|
|
|
73
75
|
* eagerly for every context would allocate per resource for nothing. */
|
|
74
76
|
#log: Logger | undefined;
|
|
75
77
|
|
|
78
|
+
/** The resolved kind. It cannot come from `metadata`, which is the resource's
|
|
79
|
+
* metadata BLOCK — `kind` is its sibling, not its member, so reading
|
|
80
|
+
* `metadata.kind` yielded `undefined` and every controller record went out
|
|
81
|
+
* with no resource identity at all (§7.3). */
|
|
82
|
+
#resolvedKind: string | undefined;
|
|
83
|
+
|
|
76
84
|
/**
|
|
77
85
|
* The resource's structured logger, stamped with its identity, module, and
|
|
78
86
|
* import-alias scope so a record identifies *which instance* emitted it — the
|
|
@@ -81,8 +89,8 @@ export class ResourceContextImpl implements ResourceContext {
|
|
|
81
89
|
*/
|
|
82
90
|
get log(): Logger {
|
|
83
91
|
if (!this.#log) {
|
|
84
|
-
const kind = this
|
|
85
|
-
const name = this.metadata?.
|
|
92
|
+
const kind = this.#resolvedKind;
|
|
93
|
+
const name = this.metadata?.name as string | undefined;
|
|
86
94
|
const resource =
|
|
87
95
|
kind && name
|
|
88
96
|
? { kind, name, id: `${this.ownerPrefix}${kind}.${name}` }
|
|
@@ -149,7 +157,17 @@ export class ResourceContextImpl implements ResourceContext {
|
|
|
149
157
|
* dispatch fails.
|
|
150
158
|
*/
|
|
151
159
|
private readonly owningContext: IEvaluationContext = moduleContext,
|
|
160
|
+
/**
|
|
161
|
+
* The resolved kind, known long before `create()` runs. Passed here rather
|
|
162
|
+
* than waited for at `bindResourceIdentity` so `ctx.log` carries resource
|
|
163
|
+
* identity from the moment the context exists: a controller that captures
|
|
164
|
+
* `ctx.log` in its constructor — the natural thing to do when the logger is
|
|
165
|
+
* handed to a helper — would otherwise hold an identity-less logger for the
|
|
166
|
+
* resource's whole life, and nothing would report that it had.
|
|
167
|
+
*/
|
|
168
|
+
resolvedKind?: string,
|
|
152
169
|
) {
|
|
170
|
+
this.#resolvedKind = resolvedKind;
|
|
153
171
|
// `ctx.env` is the sanctioned host-env channel for controllers — always the
|
|
154
172
|
// real environment (kernel passes its snapshot), never the locked Proxy.
|
|
155
173
|
this.env = env ?? hostEnv();
|
|
@@ -301,6 +319,7 @@ export class ResourceContextImpl implements ResourceContext {
|
|
|
301
319
|
for (const kw of ["x-telo-ref", "x-telo-scope", "x-telo-context", "x-telo-schema-from"]) {
|
|
302
320
|
ajv.addKeyword(kw);
|
|
303
321
|
}
|
|
322
|
+
ajv.addKeyword(binaryKeyword());
|
|
304
323
|
const validate = ajv.compile(
|
|
305
324
|
"type" in schema && typeof schema.type === "string"
|
|
306
325
|
? schema
|
|
@@ -311,7 +330,11 @@ export class ResourceContextImpl implements ResourceContext {
|
|
|
311
330
|
additionalProperties: false,
|
|
312
331
|
},
|
|
313
332
|
);
|
|
314
|
-
|
|
333
|
+
// A BigInt-normalized view: AJV reads `integer` as `typeof == "number"`, so a
|
|
334
|
+
// CEL integer (int64) would be rejected at a slot it satisfies. This validator
|
|
335
|
+
// runs without `useDefaults` and already checks a derived value, so there is
|
|
336
|
+
// nothing to merge back. See `bigint-schema-view.ts`.
|
|
337
|
+
const isValid = validate(withBigIntsAsNumbers(stripCompiledValues(value)));
|
|
315
338
|
if (!isValid) {
|
|
316
339
|
throw new RuntimeError(
|
|
317
340
|
"ERR_INVALID_VALUE",
|
|
@@ -341,6 +364,10 @@ export class ResourceContextImpl implements ResourceContext {
|
|
|
341
364
|
manifest: Record<string, unknown>,
|
|
342
365
|
): void {
|
|
343
366
|
this.#self = handle;
|
|
367
|
+
// The kind is NOT restated here. It is set at construction — the single
|
|
368
|
+
// production site always has it — and `#log` is memoized on first access, so
|
|
369
|
+
// a late assignment could not reach a logger a controller already holds. A
|
|
370
|
+
// fallback here would read as a guarantee it cannot provide.
|
|
344
371
|
this.#zones = new ZoneContext({
|
|
345
372
|
resourceName: (this.metadata?.name as string) ?? "<unnamed>",
|
|
346
373
|
resolvedKind,
|
|
@@ -418,10 +445,16 @@ export class ResourceContextImpl implements ResourceContext {
|
|
|
418
445
|
// settlement is already observed here.
|
|
419
446
|
const tracked = this.owningContext
|
|
420
447
|
.runDetached(fn) // bare scope-detach primitive
|
|
421
|
-
.catch(
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
448
|
+
.catch((err: unknown) => {
|
|
449
|
+
// A detached task has no caller to throw to, so this record is the only
|
|
450
|
+
// report. It replaces the bus event rather than joining it: `eventName`
|
|
451
|
+
// IS the bridge to the event bus (§4), and a record already reaches every
|
|
452
|
+
// sink including the debug wire, so emitting both would ship two copies
|
|
453
|
+
// of one fact with two payload shapes to keep in step.
|
|
454
|
+
this.log.error("Detached task failed", undefined, {
|
|
455
|
+
error: err,
|
|
456
|
+
eventName: "background.task.error",
|
|
457
|
+
});
|
|
425
458
|
})
|
|
426
459
|
.finally(() => {
|
|
427
460
|
this.pendingDetached.delete(tracked);
|
|
@@ -446,10 +479,12 @@ export class ResourceContextImpl implements ResourceContext {
|
|
|
446
479
|
await Promise.race([Promise.allSettled([...this.pendingDetached]), timeout]);
|
|
447
480
|
if (timer) clearTimeout(timer);
|
|
448
481
|
if (this.pendingDetached.size > 0) {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
482
|
+
this.log.warn(
|
|
483
|
+
`Abandoned ${this.pendingDetached.size} background task(s) after waiting ` +
|
|
484
|
+
`${DETACHED_DRAIN_TIMEOUT_MS}ms for them to drain`,
|
|
485
|
+
{ "telo.detached.abandoned": this.pendingDetached.size },
|
|
486
|
+
{ eventName: "background.task.abandoned" },
|
|
487
|
+
);
|
|
453
488
|
}
|
|
454
489
|
}
|
|
455
490
|
|
package/src/schema-validator.ts
CHANGED
|
@@ -7,17 +7,16 @@ import { createHash } from "node:crypto";
|
|
|
7
7
|
import * as fs from "node:fs";
|
|
8
8
|
import { createRequire } from "node:module";
|
|
9
9
|
import * as path from "node:path";
|
|
10
|
+
import { binaryKeyword, X_TELO_BINARY } from "@telorun/analyzer";
|
|
11
|
+
import { mergeFilledDefaults, withBigIntsAsNumbers } from "./bigint-schema-view.js";
|
|
10
12
|
import { formatAjvErrors } from "./manifest-schemas.js";
|
|
11
13
|
|
|
12
|
-
/** Render a value for an error message without ever throwing
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* so serializing the offending data threw from inside the message template and
|
|
16
|
-
* the thrown stringify error REPLACED the validation failure. The author was
|
|
17
|
-
* told "cannot serialize BigInt" instead of which field was wrong. */
|
|
14
|
+
/** Render a value for an error message without ever throwing — the offending
|
|
15
|
+
* data may be cyclic, and a throw here would REPLACE the validation failure
|
|
16
|
+
* with an unrelated error naming no field. */
|
|
18
17
|
function describeValue(data: unknown): string {
|
|
19
18
|
try {
|
|
20
|
-
return JSON.stringify(data
|
|
19
|
+
return JSON.stringify(data) ?? String(data);
|
|
21
20
|
} catch {
|
|
22
21
|
return String(data);
|
|
23
22
|
}
|
|
@@ -165,13 +164,23 @@ const NAME_KEYED_SCHEMA_KEYWORDS = new Set([
|
|
|
165
164
|
* two schemas that differ only there hash alike. */
|
|
166
165
|
const DATA_VALUE_KEYWORDS = new Set(["const", "default", "enum", "examples"]);
|
|
167
166
|
|
|
167
|
+
/** Annotations that DO emit validation code, and so must survive the strip and stay
|
|
168
|
+
* in the cache key. `x-telo-binary` is the first: bytes have no JSON Schema type,
|
|
169
|
+
* so the keyword is the only thing standing between a byte slot and "accepts any
|
|
170
|
+
* object". Stripping it would silently reduce the slot to an empty schema — the
|
|
171
|
+
* precise regression the annotation was introduced to close — and, because the key
|
|
172
|
+
* is meant to describe the compiled validator, a keyword that changes the validator
|
|
173
|
+
* belongs in it. */
|
|
174
|
+
const VALIDATING_ANNOTATIONS = new Set([X_TELO_BINARY]);
|
|
175
|
+
|
|
168
176
|
/** Deep-clone `schema` without its `x-telo-*` annotations — applied, like
|
|
169
177
|
* {@link collapseSentinelsToSource}, before both AJV compilation and cache
|
|
170
178
|
* hashing.
|
|
171
179
|
*
|
|
172
|
-
*
|
|
173
|
-
* false` and registers the known ones as no-op keywords, so
|
|
174
|
-
*
|
|
180
|
+
* Almost every `x-telo-*` keyword is analyzer/editor metadata: AJV runs `strict:
|
|
181
|
+
* false` and registers the known ones as no-op keywords, so they emit no
|
|
182
|
+
* validation code. {@link VALIDATING_ANNOTATIONS} is the exception and is kept —
|
|
183
|
+
* see the note there. Leaving the rest in the hashed form makes the
|
|
175
184
|
* cache key sensitive to differences that cannot change what the validator
|
|
176
185
|
* does — and one such difference is real and systematic. The analyzer rewrites
|
|
177
186
|
* `x-telo-ref.kind` to its canonical `<module>.<Kind>` in the declaring scope
|
|
@@ -193,7 +202,7 @@ function stripTeloAnnotations(value: unknown, nameKeyed = false): unknown {
|
|
|
193
202
|
if (!value || typeof value !== "object") return value;
|
|
194
203
|
const out: Record<string, unknown> = {};
|
|
195
204
|
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
|
196
|
-
if (!nameKeyed && k.startsWith("x-telo-")) continue;
|
|
205
|
+
if (!nameKeyed && k.startsWith("x-telo-") && !VALIDATING_ANNOTATIONS.has(k)) continue;
|
|
197
206
|
// A data-bearing keyword's value is carried over verbatim; a name-keyed
|
|
198
207
|
// map's VALUES are schema nodes again, so only its keys are exempt.
|
|
199
208
|
out[k] =
|
|
@@ -265,6 +274,11 @@ export class SchemaValidator {
|
|
|
265
274
|
]) {
|
|
266
275
|
this.ajv.addKeyword(kw);
|
|
267
276
|
}
|
|
277
|
+
// Not a no-op like the rest: bytes have no JSON Schema type, so this keyword IS
|
|
278
|
+
// the check. Defined as codegen in the analyzer so it inlines into the
|
|
279
|
+
// standalone validators compiled and cached below, rather than needing the
|
|
280
|
+
// implementation present at load.
|
|
281
|
+
this.ajv.addKeyword(binaryKeyword());
|
|
268
282
|
// Register the shared manifest root so module schemas can
|
|
269
283
|
// `$ref: "telo://manifest#/$defs/ResourceRef"` without each manifest
|
|
270
284
|
// bundling its own copy. Mirrors the analyzer's createAjv().
|
|
@@ -383,19 +397,32 @@ export class SchemaValidator {
|
|
|
383
397
|
const validate = this.compileAjvOrLoadCached(sanitized, hash, persist);
|
|
384
398
|
if (persist) this.persistedHashes.add(hash);
|
|
385
399
|
|
|
400
|
+
// AJV's type check is `typeof data == "number"`, so a CEL integer — a BigInt —
|
|
401
|
+
// is rejected at an `integer` slot no matter what the author writes. Check a
|
|
402
|
+
// normalized VIEW instead and merge the `useDefaults` fills back, so the value
|
|
403
|
+
// that reaches the controller keeps its 64-bit range. `withBigIntsAsNumbers`
|
|
404
|
+
// returns the same reference when there was nothing to normalize, which is what
|
|
405
|
+
// keeps the BigInt-free path byte-identical to a plain `validate(data)`.
|
|
406
|
+
const check = (data: any): boolean => {
|
|
407
|
+
const view = withBigIntsAsNumbers(data);
|
|
408
|
+
const ok = validate(view);
|
|
409
|
+
if (ok && view !== data) mergeFilledDefaults(data, view);
|
|
410
|
+
return ok;
|
|
411
|
+
};
|
|
412
|
+
|
|
386
413
|
const validator = {
|
|
387
414
|
validate: (data: any) => {
|
|
388
|
-
|
|
389
|
-
|
|
415
|
+
if (!check(data)) {
|
|
416
|
+
// Reports `data`, not the normalized view: the view renders a wide
|
|
417
|
+
// integer through a double, so the digits it prints for the offending
|
|
418
|
+
// value would not be the ones the author wrote.
|
|
390
419
|
throw new RuntimeError(
|
|
391
420
|
"ERR_RESOURCE_SCHEMA_VALIDATION_FAILED",
|
|
392
421
|
`Invalid value passed: ${describeValue(data)}. Error: ${formatAjvErrors(validate.errors)}`,
|
|
393
422
|
);
|
|
394
423
|
}
|
|
395
424
|
},
|
|
396
|
-
isValid: (data: any) =>
|
|
397
|
-
return validate(data);
|
|
398
|
-
},
|
|
425
|
+
isValid: (data: any) => check(data),
|
|
399
426
|
};
|
|
400
427
|
|
|
401
428
|
this.hashCache.set(hash, validator);
|