feat(server): Room.proposalState scaffolding for T3 consent flow

Adds the optional `proposalState` field on `Room` holding the
in-flight two-player consent proposal per T2-ADR-2. Includes
profile, proposer color + token, timestamps, and the active
setTimeout handle so supersession / consent can cancel it cleanly.

Pure type-only addition \u2014 no runtime behavior change; handlers
land in the next commit.
This commit is contained in:
Joey Yakimowich-Payne 2026-04-19 09:17:22 -06:00
commit 9af78ab5e2
No known key found for this signature in database

View file

@ -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<typeof setTimeout>;
};
}
export type JoinResult =