Files
paseo/packages/protocol/scripts/generate-validation-aot.mjs
T
babo-jeongukandClaude Opus 5.5 df82edd218 Split the inbound WebSocket validator per message type (#6847)
* Split the inbound WebSocket validator per message type

zod-aot compiled WSOutboundMessageSchema into one 11 MB function. Every
inbound frame, heartbeats included, ran through it, so V8 hosts (daemon
clients, plugin hosts, the CLI, Electron) eventually Maglev-compiled the
whole function: about 800 MB of compiler memory and multi-second stalls,
again after each deoptimization.

Generate a tag-only envelope validator plus one validator per session
message, and dispatch on the message type. Issue paths, defaults and
passthrough behave as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Use forward slashes in generated validator imports

path.relative() returns backslash paths on Windows, which break the
import specifiers written into the generated files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Declare the session export helper as a function

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 19:08:03 +02:00

168 lines
6.6 KiB
JavaScript

import { mkdir, readFile, writeFile } from "node:fs/promises";
import { createRequire } from "node:module";
import { dirname, relative, resolve, sep } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const envelopeSource = resolve(packageRoot, "src/validation/ws-outbound-envelope.ts");
const source = resolve(packageRoot, "codegen/generated/ws-outbound.compile.ts");
const runtimeSchemaMetadata = resolve(
packageRoot,
"src/generated/validation/ws-outbound-schema-metadata.ts",
);
const output = resolve(packageRoot, "src/generated/validation/ws-outbound.aot.ts");
const require = createRequire(import.meta.url);
const zodAotEntry = require.resolve("zod-aot");
const zodAotRoot = resolve(dirname(zodAotEntry), "..");
const emitterPath = resolve(zodAotRoot, "dist/cli/emitter.js");
const discriminatedUnionPath = resolve(
zodAotRoot,
"dist/core/codegen/schemas/discriminated-union.js",
);
async function ensureZodAotRuntimeImportExtensionPatch() {
const emitter = await readFile(emitterPath, "utf8");
if (emitter.includes('sourceRelPath.endsWith(".js")')) {
return;
}
const before = 'let importPath = sourceRelPath.replace(/\\.[cm]?[jt]sx?$/, "");';
const after =
'let importPath = sourceRelPath.endsWith(".js")\n ? sourceRelPath\n : sourceRelPath.replace(/\\.[cm]?[jt]sx?$/, "");';
if (!emitter.includes(before)) {
throw new Error("zod-aot emitter shape changed; update the runtime import extension patch");
}
await writeFile(emitterPath, emitter.replace(before, after));
}
async function ensureZodAotDiscriminatedUnionOutputPatch() {
let discriminatedUnionEmitter = await readFile(discriminatedUnionPath, "utf8");
if (
discriminatedUnionEmitter.includes(
"const needsOutputPropagation = ir.options.some(hasMutation);",
)
) {
return;
}
const importBefore = 'import { escapeString } from "../context.js";';
const importAfter = 'import { escapeString, hasMutation } from "../context.js";';
const outputFlagBefore = "const discKey = escapeString(ir.discriminator);\n let code = emit `";
const outputFlagAfter =
"const discKey = escapeString(ir.discriminator);\n const needsOutputPropagation = ir.options.some(hasMutation);\n let code = emit `";
const propagationBefore =
" ${g.visit(option, { input: objVar, output: objVar })}\n break;`;";
const propagationAfter =
' ${g.visit(option, { input: objVar, output: objVar })}\n ${needsOutputPropagation ? `${g.output}=${objVar};` : ""}\n break;`;';
if (
!discriminatedUnionEmitter.includes(importBefore) ||
!discriminatedUnionEmitter.includes(outputFlagBefore) ||
!discriminatedUnionEmitter.includes(propagationBefore)
) {
throw new Error("zod-aot discriminated-union emitter shape changed; update the output patch");
}
discriminatedUnionEmitter = discriminatedUnionEmitter
.replace(importBefore, importAfter)
.replace(outputFlagBefore, outputFlagAfter)
.replace(propagationBefore, propagationAfter);
await writeFile(discriminatedUnionPath, discriminatedUnionEmitter);
}
await Promise.all([
ensureZodAotRuntimeImportExtensionPatch(),
ensureZodAotDiscriminatedUnionOutputPatch(),
]);
const [
{ loadSourceFile },
{ discoverSchemas },
{ compileSchemas },
{ generateCompiledFileContent },
] = await Promise.all([
import(pathToFileURL(resolve(zodAotRoot, "dist/loader.js")).href),
import(pathToFileURL(resolve(zodAotRoot, "dist/discovery.js")).href),
import(pathToFileURL(resolve(zodAotRoot, "dist/core/pipeline.js")).href),
import(pathToFileURL(resolve(zodAotRoot, "dist/cli/emitter.js")).href),
]);
// One compiled validator per session message plus a tag-only envelope. zod-aot emits one function
// per compiled schema, so this keeps every generated function small enough for V8 to optimize cheaply.
const { sessionOutboundMessageOptions } = await loadSourceFile(envelopeSource, { cacheBust: true });
const sessionTags = sessionOutboundMessageOptions.map((option) => option.shape.type.value);
function sessionExport(index) {
return `SessionOutboundMessage${index}`;
}
const envelopeImport = relative(dirname(source), envelopeSource)
.replace(/\.ts$/, ".js")
.split(sep)
.join("/");
const metadataImport = relative(dirname(runtimeSchemaMetadata), envelopeSource)
.replace(/\.ts$/, ".js")
.split(sep)
.join("/");
await mkdir(dirname(source), { recursive: true });
await writeFile(
source,
[
"// AUTO-GENERATED by generate-validation-aot.mjs — DO NOT EDIT",
'import { compile } from "zod-aot";',
`import { WSOutboundEnvelopeSchema, sessionOutboundMessageOptions } from "${envelopeImport}";`,
"export const WSOutboundEnvelope = compile(WSOutboundEnvelopeSchema);",
...sessionTags.map(
(_, index) =>
`export const ${sessionExport(index)} = compile(sessionOutboundMessageOptions[${index}]);`,
),
"",
].join("\n"),
);
await mkdir(dirname(runtimeSchemaMetadata), { recursive: true });
await writeFile(
runtimeSchemaMetadata,
[
"// AUTO-GENERATED by generate-validation-aot.mjs — DO NOT EDIT",
`import { WSOutboundEnvelopeSchema, sessionOutboundMessageOptions } from "${metadataImport}";`,
"export const WSOutboundEnvelope = { schema: WSOutboundEnvelopeSchema };",
...sessionTags.map(
(_, index) =>
`export const ${sessionExport(index)} = { schema: sessionOutboundMessageOptions[${index}] };`,
),
"",
].join("\n"),
);
const schemas = await discoverSchemas(source, { cacheBust: true });
if (schemas.length !== sessionTags.length + 1) {
throw new Error(
`Expected ${sessionTags.length + 1} zod-aot compile() exports in ${relative(packageRoot, source)}, found ${schemas.length}`,
);
}
const compiled = compileSchemas(schemas, { mode: "inline" });
const runtimeImportPath = relative(dirname(output), runtimeSchemaMetadata)
.replace(/\.[cm]?[jt]sx?$/, ".js")
.split(sep)
.join("/");
const content = generateCompiledFileContent(compiled, runtimeImportPath, {
zodCompat: false,
}).replace(
"// AUTO-GENERATED by zod-aot — DO NOT EDIT",
"// @ts-nocheck\n// AUTO-GENERATED by zod-aot — DO NOT EDIT",
);
const sessionMessageValidators = [
"export const SessionOutboundMessageValidators = {",
...sessionTags.map((tag, index) => ` ${JSON.stringify(tag)}: ${sessionExport(index)},`),
"};",
"",
].join("\n");
await mkdir(dirname(output), { recursive: true });
await writeFile(output, `${content}\n${sessionMessageValidators}`);
console.info(
`generated ${relative(packageRoot, output)}: envelope + ${sessionTags.length} session message validators`,
);