Files
skills/docs/networking/events/broadcast-channel/src/schemas/messages.md
2026-05-29 08:20:56 +01:00

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.
  • payload is intentionally z.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.