Files
openclaw/ui/src/lib/json5-runtime.ts
T
Peter Steinberger f98bcb7fa7 perf(ui): drop zod from the Control UI and lazy-load json5 with the config surfaces (#110623)
* perf(ui): drop zod and lazy-load json5 out of Control UI startup

zod's only UI consumer was the custom-theme shape layer, whose deep CSS
validators already re-check every token; replace the two shallow schemas
with a plain record reader and delete the zod jitless CSP shim + test.
Startup keeps one schema library total (typebox, protocol-owned, already
lazy via the approval page).

json5 now loads through a lazy runtime boundary: strict JSON.parse is the
fast path, the parser warms with the config editor and whenever a config
snapshot or raw draft actually needs JSON5 (comments, trailing commas).
The redaction sanitize path parses the authoritative raw once at snapshot
ingestion (async-safe) and submits against the carried parsed fact, so
secret-placeholder handling never races the lazy parser.

Startup JS: 321.8 -> 295.8 KiB gzip, 13 -> 12 requests; budgets ratchet
to 310 KiB / 18 requests.

* fix(ui): gate config submits on pending JSON5 original parse

A JSON5 config racing the first parser load could reach the sanitize step
with a null parsed original and pass redaction placeholders through.
setConfigRawOriginal now parses synchronously whenever the parser is warm
and tracks a pending promise otherwise; submit, auto-save, and teardown
flush paths defer to that promise (teardown keeps its synchronous prefix
when no parse is pending).

* fix(ui): keep teardown config flush synchronous and JSON5 diff cache non-sticky

Teardown flush must dispatch before unload destroys the context; the
gateway's restore-or-reject sentinel contract backs the rare unsanitized
window. Raw-diff parse failures no longer cache while the lazy JSON5
parser is still loading, so a transient cold-parser miss retries on the
next render instead of pinning an empty diff.

* fix(ui): harden lazy JSON5 boundary against double-submit and failed loads

Claim the config busy flag before awaiting a pending JSON5 original parse
so a second click cannot slip past the busy state (autosave overlap is
already serialized by the in-flight registry and drain discipline). A
rejected json5 chunk import now resets the loader for retry, the per-state
pending promise is never-rejecting and self-clearing, and fire-and-forget
warms swallow rejections.

* docs(ui): note autosave entry serialization at the JSON5 parse await

* fix(ui): fill raw pending-changes diff once the lazy JSON5 parser lands

First diff open could race the parser chunk and render an empty list with
nothing scheduling a retry; renderConfig now re-renders when the warm
completes, and the browser test warms the parser in setup to assert the
steady state the view module guarantees in prod.
2026-07-18 12:36:46 +01:00

45 lines
1.3 KiB
TypeScript

// Lazy JSON5 boundary: strict JSON parses without the library, so json5 stays
// out of the startup graph and loads only for config text that needs it.
type Json5Module = { parse: (text: string) => unknown };
let json5: Json5Module | null = null;
let json5Loading: Promise<Json5Module> | null = null;
export function isJson5Warm(): boolean {
return json5 !== null;
}
export function warmJson5(): Promise<Json5Module> {
json5Loading ??= import("json5").then(
(mod) => {
json5 = mod.default;
return json5;
},
(error: unknown) => {
// Transient chunk-load failures must not pin a rejected loader forever;
// the next warm retries the import.
json5Loading = null;
throw error;
},
);
return json5Loading;
}
/**
* Strict-JSON fast path with a JSON5 fallback once the module is warmed.
* Callers on the config surfaces warm the module before raw drafts can exist;
* if a JSON5-only text races the warm-up, this throws like a parse failure and
* the caller's existing invalid-draft handling applies until retry.
*/
export function parseJson5Text(raw: string): unknown {
try {
return JSON.parse(raw) as unknown;
} catch (jsonError) {
if (json5) {
return json5.parse(raw);
}
void warmJson5().catch(() => undefined);
throw jsonError;
}
}