first commit
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
# schemas/messages
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## 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
|
||||
|
||||
```ts
|
||||
import { ChannelMessageSchema } from "@suaveplan/broadcast-channel";
|
||||
|
||||
const parsed = ChannelMessageSchema.safeParse(incoming);
|
||||
if (!parsed.success) {
|
||||
return; // drop
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
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.
|
||||
Reference in New Issue
Block a user