plan
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
# Finish Call to Play’s replication seam
|
||||
|
||||
## Summary
|
||||
|
||||
Keep the existing architecture: immutable events, deterministic reduction, live delivery, and handshake history are appropriate for a LAN party. Do not replace it with owner-authoritative state, consensus, persistent storage, or cryptographic peer identities.
|
||||
|
||||
The focused redesign is the delivery/merge/store seam:
|
||||
|
||||
- Merge histories atomically.
|
||||
- Acknowledge live deliveries.
|
||||
- Preserve complete histories while users can still see a call.
|
||||
- Make terminal outcomes understandable.
|
||||
- Never report an action as accepted when it was immediately discarded.
|
||||
|
||||
## User-visible lifecycle
|
||||
|
||||
| State | Meaning | Visibility | Available actions |
|
||||
|---|---|---:|---|
|
||||
| Open / Ready | Call is still coordinating players | Until deadline | Existing participation, chat, creator controls |
|
||||
| Time’s up | Deadline elapsed without a Start or Cancel | 5 minutes | Creator may Start, Add time, or Cancel; history remains complete |
|
||||
| Running | Creator emitted Start; this is a successful outcome | 15 minutes after Start | Read-only card, roster, and complete chat |
|
||||
| Cancelled | Creator emitted Cancel | 15 minutes after Cancel | Read-only card, roster, and complete chat |
|
||||
| Retired | Display period ended | Hidden | None |
|
||||
|
||||
“Time’s up” and “Running” are meaningfully different: a timed-out call is unresolved and recoverable, while Running is a final success receipt. Deadline passage alone never means the game started.
|
||||
|
||||
Running and Cancelled rows remain in the ticker and overlay, sorted after actionable calls. They do not increase the top-bar badge. Their chat history remains readable, but all composers and controls are disabled.
|
||||
|
||||
## Interface and invariant changes
|
||||
|
||||
- Raise `PROTOCOL_VERSION` from 6 to 7; no compatibility path.
|
||||
- Add `Response::CallToPlayAck` with outcomes equivalent to:
|
||||
- `Applied`
|
||||
- `Duplicate`
|
||||
- `NeedHandshake`
|
||||
- `NeedHistory`
|
||||
- `Obsolete`
|
||||
- `Rejected(reason)`
|
||||
- Replace per-event insertion with one atomic `merge_batch(events, now)` operation returning retained UI events, duplicate/obsolete counts, and missing-history information.
|
||||
- Change frontend nomination state to represent `running` and `cancelled`, with a `terminalAt` timestamp.
|
||||
- Change `addTime` to receive the current effective deadline and calculate `max(now, deadline) + duration`.
|
||||
- Preserve these invariants:
|
||||
- Every visible call has its entire event and chat history.
|
||||
- Handshake batches are evaluated as a whole, regardless of event order.
|
||||
- Missing-root actions are not retained alone; they request history.
|
||||
- Event IDs correspond only to retained events.
|
||||
- Terminal tombstones prevent stale histories from resurrecting finished calls.
|
||||
- Local acceptance is immediate user success; remote delivery is acknowledged and healed asynchronously.
|
||||
|
||||
## Commit sequence
|
||||
|
||||
1. `refactor(call-to-play): merge histories atomically`
|
||||
|
||||
- Validate and deduplicate the complete incoming batch before changing the store.
|
||||
- Treat an existing ID with different contents as a conflict and reject the batch.
|
||||
- Evaluate compaction once after all batch events are present, fixing the quadratic handshake path.
|
||||
- Commit the candidate store only when capacity and validation succeed.
|
||||
- Rebuild event IDs from retained events instead of preserving every historical ID.
|
||||
- Return `NeedHistory` without storing an action when neither the store nor batch contains its Create event.
|
||||
- Permit a full `Create + AddTime` history to revive a call atomically.
|
||||
- Mark an event as applied only when it survives compaction; obsolete events are neither broadcast nor emitted to the UI.
|
||||
- Keep the 4,096-event safety cap for unresolved histories, but always permit Start and Cancel. Recently terminal histories and compact tombstones must not prevent new active calls.
|
||||
- Preserve full open/ready/Time’s-up histories; never evict individual chat or participant events.
|
||||
|
||||
2. `fix(call-to-play): acknowledge live replication`
|
||||
|
||||
- Turn the live Call to Play request into a request/response exchange returning `CallToPlayAck`.
|
||||
- Remove source-IP-versus-advertised-IP equality checks.
|
||||
- Under the selected trusted-LAN model, require:
|
||||
- the envelope peer ID to exist in the known peer roster;
|
||||
- every live event’s actor ID to match that envelope peer ID;
|
||||
- local peer-core publication to continue stamping its own actor ID.
|
||||
- Return `NeedHandshake` for unknown peers and `NeedHistory` for missing call roots.
|
||||
- On transport failure, malformed response, `NeedHandshake`, or `NeedHistory`, perform one full Hello/HelloAck resync.
|
||||
- Treat Applied and Duplicate as delivered, Obsolete as finished, and Rejected as a logged non-retriable error.
|
||||
- Keep publication locally successful without waiting for every peer, so an offline machine cannot block a LAN-party action.
|
||||
- Document that shared TLS plus stable peer IDs prevent accidental identity mixing but are not hostile-peer authentication.
|
||||
|
||||
3. `feat(call-to-play): retain terminal outcomes`
|
||||
|
||||
- Preserve complete Running and Cancelled histories in backend snapshots for 15 minutes so late joiners receive the card, roster, and chat.
|
||||
- After 15 minutes, compact each terminal call to its Start or Cancel tombstone for the remainder of the peer session.
|
||||
- Continue deleting unresolved Time’s-up histories after their separate five-minute recovery window, including their event IDs.
|
||||
- Derive and render Running and Cancelled frontend states instead of immediately removing them.
|
||||
- Show their ticker/card status, retain unknown-game degraded rendering, sort them last, and exclude them from the badge.
|
||||
- Prune the frontend’s raw event map after the corresponding display window so a long GUI session does not accumulate invisible history.
|
||||
- Update the feature specification and architecture documentation with these lifecycle and clock-skew assumptions.
|
||||
|
||||
4. `fix(call-to-play): extend from the current deadline`
|
||||
|
||||
- Calculate extensions as `max(Date.now(), nomination.deadline) + five minutes`.
|
||||
- Preserve the existing behavior that an overdue call gets five minutes from now.
|
||||
- Ensure extending a call that became Ready early adds time instead of shortening its remaining deadline.
|
||||
- Keep terminal Running and Cancelled calls non-extendable.
|
||||
|
||||
5. `fix(call-to-play): explain peer startup state`
|
||||
|
||||
- Replace the game-folder advice shown while `actorId` is unavailable with: “Call to Play is still connecting to the LAN. Try again in a moment.”
|
||||
- Do not mark transport unavailable for store-level errors.
|
||||
- Surface distinct messages for:
|
||||
- expired/obsolete call;
|
||||
- full active history;
|
||||
- peer startup;
|
||||
- unexpected update failure.
|
||||
|
||||
## Test and acceptance plan
|
||||
|
||||
- Store unit tests:
|
||||
- `Create + AddTime` succeeds in every input order.
|
||||
- An expired receiver returns NeedHistory for orphan AddTime, then revives after receiving full history.
|
||||
- IDs removed with expired histories do not block revival.
|
||||
- Stale events against terminal tombstones remain obsolete.
|
||||
- Accepted results contain only retained events.
|
||||
- Invalid/conflicting batches leave the store unchanged.
|
||||
- Batch merge compacts once rather than once per event.
|
||||
- Start and Cancel remain possible at the active-history cap.
|
||||
- Visible calls always retain every chat event.
|
||||
|
||||
- Transport tests:
|
||||
- A known peer is accepted when transport and advertised IPs differ.
|
||||
- Unknown peer IDs and mismatched actor IDs receive explicit acknowledgements.
|
||||
- NeedHistory, NeedHandshake, and lost acknowledgements trigger idempotent handshake healing.
|
||||
- Duplicate delivery is harmless.
|
||||
|
||||
- Frontend tests:
|
||||
- Ready, Time’s up, Running, Cancelled, and retired transitions.
|
||||
- Full terminal history remains for 15 minutes and then disappears.
|
||||
- Terminal calls appear in ticker/overlay but not the badge.
|
||||
- Terminal controls and chat composer are disabled.
|
||||
- Both Add-time deadline cases.
|
||||
- Correct startup and store-error messages.
|
||||
|
||||
- Peer CLI:
|
||||
- Keep S48 as the active-call/full-history late-join acceptance test.
|
||||
- Add S49 covering a terminal call whose roster and chat are reconstructed by a late joiner.
|
||||
- Fix any snapshot waiting through the direct reply path rather than observing unrelated generations.
|
||||
|
||||
- Final verification:
|
||||
- `just fmt`
|
||||
- `just clippy`
|
||||
- `just test`
|
||||
- `just frontend-test`
|
||||
- `just build`
|
||||
- `just peer-cli-tests S48 S49`
|
||||
- `git diff --check`
|
||||
|
||||
## Assumptions
|
||||
|
||||
- The LAN is cooperative; deliberate peer-ID impersonation is outside scope.
|
||||
- Wall clocks are assumed reasonably close. Atomic history revival tolerates boundary skew but does not attempt clock synchronization.
|
||||
- Call to Play state remains transient and disappears when the peer process/session ends.
|
||||
- The existing `FABLE_5_FINDINGS.md` commit remains untouched; all implementation work is added as focused forward commits.
|
||||
Reference in New Issue
Block a user