Files
paseo/packages/protocol/src/validation/ws-outbound.ts
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

55 lines
2.1 KiB
TypeScript

import { ZodRealError } from "zod";
import type { z } from "zod";
import {
SessionOutboundMessageValidators,
WSOutboundEnvelope,
} from "../generated/validation/ws-outbound.aot.js";
import type { SessionOutboundMessage, WSOutboundMessage } from "../messages.js";
type ValidationResult<T> = { success: true; data: T } | { success: false; error: z.ZodError };
interface GeneratedValidator<T> {
safeParse(input: unknown): ValidationResult<T>;
}
// zod-aot emits runtime JavaScript from the envelope and message schemas but not its TypeScript surface.
// Protocol regression tests cover the patched compiler cases behind this boundary.
const envelopeValidator = WSOutboundEnvelope as GeneratedValidator<WSOutboundMessage>;
const sessionMessageValidators = SessionOutboundMessageValidators as Record<
SessionOutboundMessage["type"],
GeneratedValidator<SessionOutboundMessage>
>;
// The message validator reports paths relative to the message, nested issues included.
function underMessage(issue: z.core.$ZodIssue): z.core.$ZodIssue {
const path = ["message", ...issue.path];
switch (issue.code) {
case "invalid_union":
return issue.inclusive === false
? { ...issue, path }
: { ...issue, path, errors: issue.errors.map((branch) => branch.map(underMessage)) };
case "invalid_key":
case "invalid_element":
return { ...issue, path, issues: issue.issues.map(underMessage) };
default:
return { ...issue, path };
}
}
export function validateWSOutboundMessage(input: unknown): ValidationResult<WSOutboundMessage> {
const envelope = envelopeValidator.safeParse(input);
if (!envelope.success || envelope.data.type !== "session") {
return envelope;
}
const message = envelope.data.message;
const result = sessionMessageValidators[message.type].safeParse(message);
if (!result.success) {
return { success: false, error: new ZodRealError(result.error.issues.map(underMessage)) };
}
// Message validators return a new object when they apply defaults.
if (result.data === message) {
return envelope;
}
return { success: true, data: { ...envelope.data, message: result.data } };
}