/** * Live per-seat Frame delta — Phase 2 of `docs/architecture/multiplayer.md`. * * `src/sim/replay.ts`'s `compress()` looked like the thing to reuse here (D2/D3's "2.9 KB per push * with the board omitted when unchanged" cites it) but it solves a different problem: it interns * card/description strings across a WHOLE recorded array of frames, which only pays off when * bundling many frames into one replay file. A live server pushes one frame at a time and has * nothing to intern against. The part that genuinely carries over is much smaller — a one-step-back * "null if unchanged since the last thing sent to THIS seat" check on the three fields that make up * almost all of a Frame's size: `cells`, `facilities`, `division` (`replay.ts`'s own `keys` array). * * Node-free by design, unlike `replay.ts` (which imports `node:fs`) — both the server and a browser * `RemoteSession` import this file directly. */ import type { CellView, DivisionView, FacilityView, Frame } from './view.ts'; /** A `Frame` with the three board-shaped fields replaced by `null` where unchanged since `previous`. */ export type FrameDelta = Omit & { cells: CellView[] | null; facilities: FacilityView[] | null; division: DivisionView[] | null; }; const BOARD_KEYS = ['cells', 'facilities', 'division'] as const; /** * `previous` is the last Frame actually sent to THIS seat, or `null` for a first connect / a * reconnect after a gap — Phase 2 has no persistence to replay a gap against (Phase 3), so a * reconnect always gets a full Frame here rather than a delta. */ export function deltaFrame(previous: Frame | null, next: Frame): FrameDelta { const out = { ...next } as unknown as FrameDelta; for (const key of BOARD_KEYS) { const unchanged = previous !== null && JSON.stringify(previous[key]) === JSON.stringify(next[key]); (out as Record)[key] = unchanged ? null : next[key]; } return out; } /** The receiving side: merges a delta back onto the last full Frame this seat actually has. */ export function applyDelta(previous: Frame | null, delta: FrameDelta): Frame { const out = { ...delta } as unknown as Frame; for (const key of BOARD_KEYS) { if (delta[key] === null) { if (previous === null) { throw new Error(`deltaFrame said "${key}" is unchanged, but there is no previous Frame to merge onto`); } (out as Record)[key] = previous[key]; } } return out; }