3.4 KiB
schemas/messages
(no submodules)
Purpose
This module declares the Zod boundary schemas for
@suaveplan/broadcast-channel. ChannelMessageSchema validates every inbound
frame produced by the underlying ChannelLike transport before it is allowed
into the per-type handler registry. ChannelConfigSchema validates the
construction-time config object so consumers see a meaningful Zod issue list
instead of an opaque runtime failure when a field is missing or non-numeric.
Boundary validation is the suaveplan-mandated fix for the enterprise gap "No
Zod validation on incoming ChannelMessage — a rogue tab could send
garbage." A peer that posts a frame missing id or with a negative
timestamp no longer reaches user code; the channel drops the frame,
increments channel.message.dropped, and warns through the injected
LoggerLike.
Features
- Two schemas:
ChannelMessageSchema,ChannelConfigSchema. - Default-import Zod (
import z from "zod") to satisfy the suaveplan Bun-safe rule. payloadis intentionallyz.unknown()so callers can layer their own per-type validation downstream.- Stable required-field set:
{ type, payload, id, senderId, timestamp }. - Inferred input types via
z.infer.
Usage: Basic
import { ChannelMessageSchema } from "@suaveplan/broadcast-channel";
const parsed = ChannelMessageSchema.safeParse(incoming);
if (!parsed.success) {
return; // drop
}
Usage: Advanced
import z from "zod";
import { ChannelMessageSchema } from "@suaveplan/broadcast-channel";
// Layer a per-type payload validator over the boundary schema.
const UserSignedInPayload = z.object({ userId: z.string().min(1) });
const incoming: unknown = receiveOffWire();
const env = ChannelMessageSchema.safeParse(incoming);
if (!env.success) return;
if (env.data.type === "user:signed-in") {
const payload = UserSignedInPayload.safeParse(env.data.payload);
if (!payload.success) return;
// payload.data.userId is typed and validated
}
API Reference
const ChannelMessageSchema—z.object({ type: z.string().min(1), payload: z.unknown(), id: z.string().min(1), senderId: z.string().min(1), timestamp: z.number().int().nonnegative() }).const ChannelConfigSchema—z.object({ name: z.string().min(1), heartbeatInterval: z.number().int().positive(), tabTimeout: z.number().int().positive(), maxMessageBytes: z.number().int().positive() }).type ChannelMessageInput = z.infer<typeof ChannelMessageSchema>.type ChannelConfigInput = z.infer<typeof ChannelConfigSchema>.
Implementation Notes
payload is left as z.unknown() because the channel is type-agnostic on
the wire: per-type validation belongs in the application layer, not in the
transport boundary. Forcing a discriminated union here would push every
caller into either casting or extending the schema, and would force a major
version bump every time an application added a new message type — exactly the
opposite of the loose-coupling story the channel is meant to provide. The
.min(1) constraints on type / id / senderId catch the realistic
failure mode (empty strings produced by a malformed encoder) without the
cost of regex matching. timestamp uses .int().nonnegative() so frames
produced by clocks at zero (a fresh FakeClock) still validate while frames
with garbage negative values are rejected.