/* * The `twin-state` channel — **a message says whose twin it is, and the filter asks.** Pure. * * ── What went wrong (2026-09-24) ───────────────────────────────────────────── * A twin is `(domain, instanceId)` (`runtime-key.ts`), and the same instanceId in two tenants is the * expected case — reference source names such as `sap-ewm-1710` are vocabulary tenants share. The * channel had not caught up: publishers sent `{ twinState: { instanceId, … } }` with no domain, and the * subscription filtered on instanceId alone. Ownership was checked once, when subscribing. So a tenant * watching its own `sap-ewm-1710` also received the other tenant's deltas, and the snapshot sent to a * new subscriber went to every subscriber of that id in every tenant. * * The rule is shell's `data` channel's (the message carries its domain, the filter compares it with the * key), compared by id because the twin's key already is. Every sender builds its payload here, and * `twin-state-channel.test.ts` fails if a sender builds one anywhere else. */ export const TWIN_STATE_TOPIC = 'twin-state' export interface TwinStateMessageShape { instanceId: string kind: string revision?: number payload?: unknown } export interface TwinStatePayload { domainId: string twinState: TwinStateMessageShape } export function twinStatePayload(domainId: string, twinState: TwinStateMessageShape): TwinStatePayload { if (!domainId) throw new Error('twinStatePayload: domainId is required — a twin state message always belongs to a tenant') return { domainId, twinState } } /** Passes only this tenant's messages about this twin. */ export function twinStateFilter(domainId: string, instanceId: string) { return (p: TwinStatePayload | undefined): boolean => !!p && p.domainId === domainId && p.twinState?.instanceId === instanceId }