Files
lanspread/CALL_TO_PLAY_FIXES_PLAN.md
2026-07-23 17:07:32 +02:00

8.7 KiB
Raw Permalink Blame History

Finish Call to Plays 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
Times 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

“Times 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/Times-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 events 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 Times-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 frontends 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, Times 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.