Skip to main content

threadline-realtime-event

Use when changing the Durable Object message protocol in apps/realtime — adding a message type, changing presence or fan-out behavior, altering ticket verification, or touching room event persistence. Covers the workerd runtime constraints that make this tier different from the API.

Ir para a instalação

Informações da origem

Repositório
hoangsonww/Threadline-RealTime-Collab
Última atividade na origem
17 de agosto de 2026 às 05:57
Idioma detectado do SKILL.md
inglês
Estrelas
3
Forks
1

Opções de instalação

Por padrão, está selecionado o prompt que primeiro revisa a origem. Você pode mudar para um comando direto ou baixar uma cópia local.

Revise os arquivos de origem

Leia o SKILL.md e os arquivos complementares exibidos pelo SkillsMP antes de decidir se vai instalar.

Exibindo SKILL.md

SKILL.md
Instruções da origem · Visualização somente leitura
name
threadline-realtime-event
description
Use when changing the Durable Object message protocol in apps/realtime — adding a message type, changing presence or fan-out behavior, altering ticket verification, or touching room event persistence. Covers the workerd runtime constraints that make this tier different from the API.
# Changing the realtime protocol `apps/realtime` is one Cloudflare Worker with **one `RoomDurableObject` per room**. It is not a second Node service and several things you would reach for reflexively are unavailable or behave differently. ## Runtime constraints — read before writing - **This is workerd, not Node.** No filesystem, no `process`, no Node built-ins beyond what the Workers runtime polyfills. Tests run under `@cloudflare/vitest-pool-workers` in a real workerd instance, which is why `apps/realtime/**` is **excluded** from the root `vitest.config.ts` and invoked separately by the root `test` script. If your new test does not run, that exclusion is why. - **The Durable Object hibernates.** It uses the WebSocket hibernation API with SQLite-backed storage. In-memory state on the instance does **not** survive hibernation. Anything that must outlive an idle period goes in storage, not in a field. - **`worker-configuration.d.ts` is generated** by `wrangler types`, which runs as part of the workspace's `typecheck` script. Never hand-edit it. A diff in it is regeneration noise. - **Room event history is bounded in the database**, not in memory — see the `perf(api)` commit that made that change and the contract recorded alongside it. Do not reintroduce unbounded in-memory accumulation. ## The invariant that matters most **The Durable Object verifies the room ticket itself.** It does not trust that `apps/api` already authorized the connection. This is the central claim of the whole architecture ([`docs/security.md`](../../../docs/security.md)). When you add a message type, ask: *does the ticket that admitted this connection actually authorize this new capability?* If the new message can do something the ticket's scope did not contemplate — write where the ticket only granted read, manage where it granted join — then the check that admitted the socket is not sufficient, and the handler needs its own. Do not remove a verification because it looks redundant with something `apps/api` does. That redundancy is the design. ## Steps ### 1. Define the message Extend the message handling in `apps/realtime/src/index.ts` — `webSocketMessage` for inbound, the fan-out path for outbound. Validate the inbound payload. A message arriving over a WebSocket is untrusted input exactly as much as an HTTP body is, and the fact that the socket was authenticated says nothing about the shape of what arrives on it. ### 2. Authorize it Check inside the handler, against what the connection's ticket actually granted. Presence events are a live example of a check that looks redundant and is not: the code asserts that a presence event's `userId` matches its actor, because a socket authorized as one participant must not be able to emit events attributed to another. ### 3. Decide what persists Not every message should be recorded. Ephemeral signalling — ICE candidates, session descriptions, transient presence — should not accumulate in room history. Durable artefacts should. If it persists, it flows to `apps/api` through the internal ingest path, which is authenticated with its own secret and verified independently on arrival. ### 4. Test it `apps/realtime/src/index.test.ts`, under `@cloudflare/vitest-pool-workers`. Cover the accepted path, the **rejected** path (a socket that should not be permitted to send this message), and a malformed payload. As with the API tier, the rejection path is the one that only ever runs in the suite. ```bash npm run test --workspace=@threadline/realtime ``` ### 5. Document it [`docs/realtime.md`](../../../docs/realtime.md) is the protocol's specification. A message type that exists in code but not there will be re-litigated by the next person who reads either one. If the change alters a trust boundary, [`docs/security.md`](../../../docs/security.md) needs updating too, and it likely warrants an ADR. ## Verify ```bash npm run typecheck # also regenerates worker-configuration.d.ts npm run test --workspace=@threadline/realtime npm run dev:realtime:local # wrangler dev --local on :8787 ``` For an end-to-end check, run the full stack (`npm run dev`) and open two browser sessions in the same room — a protocol change that works against one client and breaks fan-out is not visible with a single tab. ## Checklist - [ ] Inbound payload is validated - [ ] Authorization is re-checked in the handler against what the ticket granted - [ ] Presence and identity claims are verified against the actual actor - [ ] Persistence decision is deliberate — ephemeral signalling is not recorded - [ ] Nothing accumulates unboundedly in Durable Object memory - [ ] Test for the accepted path - [ ] Test for the rejected path - [ ] Test for a malformed payload - [ ] `docs/realtime.md` updated - [ ] `docs/security.md` updated if a trust boundary moved - [ ] Verified with two clients in one room, not one
Ver no GitHub