From a588bbf7d1c5116d9d24083646450fc22370c50d Mon Sep 17 00:00:00 2001 From: Greg Soucy Date: Fri, 20 Mar 2026 18:23:06 -0400 Subject: [PATCH] [runtime] prefer builtin schemas during validator warmup Why: cached-only /verify warmup should avoid CI flakiness from remote schema fetches when builtin schemas already exist. Contract impact: none --- server.mjs | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/server.mjs b/server.mjs index d5af7e5..4e7850b 100644 --- a/server.mjs +++ b/server.mjs @@ -847,15 +847,24 @@ function normalizeSchemaFetchUrl(url) { return u; } -async function fetchJsonWithTimeout(url, timeoutMs) { +async function fetchJsonWithTimeout(url, timeoutMs, options = {}) { if (typeof fetch !== "function") throw new Error("global fetch is not available (requires Node 18+)"); const u = normalizeSchemaFetchUrl(url); + const preferBuiltin = options?.preferBuiltin === true; cachePrune(schemaJsonCache, { ttlMs: JSON_CACHE_TTL_MS, maxEntries: MAX_JSON_CACHE_ENTRIES, tsField: "fetchedAt" }); const cached = schemaJsonCache.get(u); if (cached) return cached.schema; + if (preferBuiltin) { + const builtinSchema = getBuiltinSchema(u); + if (builtinSchema) { + schemaJsonCache.set(u, { fetchedAt: Date.now(), schema: builtinSchema }); + return builtinSchema; + } + } + const ac = new AbortController(); const t = setTimeout(() => ac.abort(), timeoutMs); @@ -882,12 +891,12 @@ async function fetchJsonWithTimeout(url, timeoutMs) { } } -function makeAjv() { +function makeAjv(options = {}) { const ajv = new Ajv({ allErrors: true, strict: false, validateSchema: false, - loadSchema: async (uri) => await fetchJsonWithTimeout(uri, SCHEMA_FETCH_TIMEOUT_MS), + loadSchema: async (uri) => await fetchJsonWithTimeout(uri, SCHEMA_FETCH_TIMEOUT_MS, options), }); addFormats(ajv); return ajv; @@ -897,7 +906,7 @@ function receiptSchemaUrlForVerb(verb) { return `${SCHEMA_HOST}/schemas/v1.1.0/commons/${verb}/receipts/${verb}.receipt.schema.json`; } -async function getValidatorForVerb(verb) { +async function getValidatorForVerb(verb, options = {}) { cachePrune(validatorCache, { ttlMs: VALIDATOR_CACHE_TTL_MS, maxEntries: MAX_VALIDATOR_CACHE_ENTRIES, @@ -909,7 +918,7 @@ async function getValidatorForVerb(verb) { if (inflightValidator.has(verb)) return await inflightValidator.get(verb); const build = (async () => { - const ajv = makeAjv(); + const ajv = makeAjv(options); const url = receiptSchemaUrlForVerb(verb); // Preload shared refs (best effort) @@ -919,12 +928,12 @@ async function getValidatorForVerb(verb) { `${SCHEMA_HOST}/schemas/v1.1.0/_shared/x402.schema.json`, `${SCHEMA_HOST}/schemas/v1.1.0/_shared/identity.schema.json`, ]; - await Promise.all(shared.map((u) => fetchJsonWithTimeout(u, SCHEMA_FETCH_TIMEOUT_MS).catch(() => null))); + await Promise.all(shared.map((u) => fetchJsonWithTimeout(u, SCHEMA_FETCH_TIMEOUT_MS, options).catch(() => null))); } catch { // ignore } - const schema = await fetchJsonWithTimeout(url, SCHEMA_FETCH_TIMEOUT_MS); + const schema = await fetchJsonWithTimeout(url, SCHEMA_FETCH_TIMEOUT_MS, options); const validate = await withTimeout(ajv.compileAsync(schema), SCHEMA_VALIDATE_BUDGET_MS, "ajv_compile_budget_exceeded"); validatorCache.set(verb, { compiledAt: Date.now(), validate }); @@ -973,7 +982,11 @@ function startWarmWorker() { if (hasValidatorCached(verb)) continue; try { - await withTimeout(getValidatorForVerb(verb), PREWARM_PER_VERB_BUDGET_MS, "prewarm_per_verb_timeout"); + await withTimeout( + getValidatorForVerb(verb, { preferBuiltin: true }), + PREWARM_PER_VERB_BUDGET_MS, + "prewarm_per_verb_timeout" + ); } catch (e) { console.warn("[prewarm] verb failed", verb, e?.message || e); }