feat(service): a workspace's chat is sealed for following over Neon Realtime

Merged
CH
8d ago
+658-9

Part of making workspace chat live over Neon Realtime. On main (#3906 has merged). One change: a workspace's chat is sealed for following.

What changes

  • New live read: live.workspaceChat (GET /api/live/workspace-chat?workspaceId=&userId=) in live-procedures.ts. It answers the window's chat read as five sealed statements:
PartWhat it holds
selfthe reader's own member and how far it has read
membersthe roster, each member named as the held read names it (a person by their account's name, an agent by its title or model)
channelsthe channels, with their purpose
messageseach channel's newest 200 messages that visibleTo lets the reader see (LIVE_CHANNEL_MESSAGES, a row_number() per channel), plus every ask still open however far back it is, so For you keeps listing it
readsthe reader's per-channel read cursors
  • Who can read it. Every part returns nothing once the account leaves the workspace or turns Chat off.
    • Like the other live reads from #4228, it checks the session against ?userId=. Another account's session gets 401, which the client reads as signed out.
  • Polling. Like main's other live reads, it answers polled when the reader asks to poll or Realtime can't seal it.
  • Sealing writes nothing. Someone who hasn't joined (#3906) is not found.
  • Building the answer. workspaceChatOfLiveRows in live-wire.ts sorts the rows the way the held read does and builds the held read's ChatAnswer, with no cursor.

The window can't scroll back, so a channel past 200 messages shows its newest 200.

Tests

  • PGlite parity (workspace-chat.test.ts). The test chat has:

    • a reply in a thread
    • a DM
    • a topic with a purpose
    • an answered ask
    • a per-channel read
    • more messages in one channel than the bound

    For a member, the live rows build exactly what the held read returns from the start, kept to each channel's newest 200. For an outsider, and for someone with Chat off, every part is empty.

  • Immutability. live-statements.test.ts holds the five parts to mutableFunctionsOf (none found) and to Realtime's Drizzle adapter.

  • Procedure (live-procedures.test.ts):

    • sealed for the bearer
    • not found for someone who hasn't joined, a stranger, or someone with Chat off
    • signed out for another account's session
  • New JSON Schema goldens for workspaceChatLiveAnswerSchema and workspaceChatLiveRowsSchema.

  • Checks run one at a time on the stack's top: tsc per touched package, biome, oxlint, knip, discarded-effect, repository checks, and the affected vitest suites.