diff --git a/packages/server/src/rooms.ts b/packages/server/src/rooms.ts index 19b772e..b8dbbe9 100644 --- a/packages/server/src/rooms.ts +++ b/packages/server/src/rooms.ts @@ -64,6 +64,45 @@ export interface Room { * returns. */ pendingProposerToken?: string; + /** + * In-flight two-player consent proposal (T2-ADR-2). Populated by + * `modifier-profile.propose` and drained by + * `modifier-profile.consent` (approve → promoted to + * `pendingProfile` via the T2 queue; reject → discarded) or by + * the 60s auto-timeout. + * + * Mutually exclusive with a simultaneous proposal: a second + * `propose` while this is set supersedes the first (the first + * round-trips a `modifier-profile.rejected` with reason + * `"superseded"` before the new proposal registers). This keeps + * the state machine linear — at any instant a room has AT MOST + * one proposal awaiting consent. + */ + proposalState?: { + /** The candidate profile, already validated against the layout + * at receipt time. Stored verbatim; if consent approves, this + * object is handed to `setPendingProfile` unchanged. */ + profile: ModifierProfile; + /** Color of the proposing player. Used in the + * `proposal-pending` broadcast so the opponent UI can phrase + * "White/Black proposes…" correctly. */ + proposedBy: "white" | "black"; + /** Player token of the proposing player. Used to route the + * `consent-received` ack back on approve, and to enforce the + * self-consent guard on `consent`. Token beats socket id + * because it survives reconnect. */ + proposedByToken: string; + /** Unix-ms when the proposal was accepted. */ + proposedAt: number; + /** Unix-ms deadline (proposedAt + 60_000), mirrored to the + * opponent so the UI can animate a countdown without a + * round-trip. */ + expiresAt: number; + /** Active setTimeout handle. Cleared on any state transition + * so a stale timer can't fire after consent/supersession + * has already concluded the proposal. */ + timeoutHandle: ReturnType; + }; } export type JoinResult =