Skip to content

Replace manual SSE event parsing with Zod schemas #579

@brendanlong

Description

@brendanlong

This is related to #580 and a PR fixing this should target the tanstack-db branch.

Problem

The parseEventData function in src/lib/hooks/useRealtimeUpdates.ts:110-403 is ~290 lines of manual type checking with nested typeof checks for each event type:

function parseEventData(data: string): SyncEvent | null {
  try {
    const parsed: unknown = JSON.parse(data);
    if (typeof parsed !== "object" || parsed === null || !("type" in parsed)) return null;
    const event = parsed as Record<string, unknown>;

    if (
      event.type === "new_entry" &&
      (typeof event.subscriptionId === "string" || event.subscriptionId === null) &&
      typeof event.entryId === "string" &&
      typeof event.updatedAt === "string"
    ) {
      return { type: "new_entry" as const, ... };
    }

    // ... 250+ more lines of the same pattern for each event type
  }
}

Why this should change

  1. Error-prone: Easy to miss a field validation or get a type wrong. No compiler help — all runtime typeof checks.
  2. Duplicated validation: The server already has Zod schemas for these exact event types in src/server/trpc/routers/sync.ts:155-170+. The same shapes are validated in two places with two different mechanisms.
  3. Hard to maintain: Adding a field to an event requires updating both the server Zod schema and this manual parser, with no compile-time link between them.

Suggestion

Use Zod (already a dependency) to validate SSE events. The server-side sync router already defines Zod schemas for these events — they could be shared or mirrored:

// Shared event schemas (e.g., src/lib/events/schemas.ts)
const newEntryEventSchema = z.object({
  type: z.literal("new_entry"),
  subscriptionId: z.string().nullable(),
  entryId: z.string(),
  updatedAt: z.string(),
  timestamp: z.string().optional().default(() => new Date().toISOString()),
  feedType: z.enum(["web", "email", "saved"]).optional(),
});

const syncEventSchema = z.discriminatedUnion("type", [
  newEntryEventSchema,
  entryUpdatedEventSchema,
  entryStateChangedEventSchema,
  // ...
]);

// Usage
function parseEventData(data: string): SyncEvent | null {
  try {
    const result = syncEventSchema.safeParse(JSON.parse(data));
    return result.success ? result.data : null;
  } catch {
    return null;
  }
}

This would reduce ~290 lines to ~50, add compile-time type safety, and could share schemas with the server.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions