fix(multiplayer): converge events through REST

This commit is contained in:
Josh Creek
2026-09-02 18:54:10 +01:00
parent 91658fbc13
commit 55b88a3aa5
14 changed files with 128 additions and 18 deletions
+4 -2
View File
@@ -13,8 +13,10 @@ import (
// RunProposalOutboxDispatcher delivers committed proposal changes to the
// authenticated WebSocket subscribers. It only reads proposal_changed rows;
// result and other outbox event types remain owned by their own consumers.
// Delivery is at-least-once because the row is acknowledged only after every
// participant publication succeeds.
// The outbox guarantees after-commit publication into this replica's bounded
// transient hub; WebSocket receipt is deliberately best-effort. Clients use
// owner-scoped periodic REST recovery for correctness across disconnects and
// replicas, so a socket notification is only a latency optimization.
func RunProposalOutboxDispatcher(ctx context.Context, db *sql.DB, service *Service) {
if db == nil || service == nil {
return
+2 -1
View File
@@ -349,6 +349,7 @@ type queueCreateRequest struct {
type queueResponse struct {
TicketID string `json:"ticket_id"`
PlayerID string `json:"player_id"`
ProposalID string `json:"proposal_id,omitempty"`
MatchID string `json:"match_id,omitempty"`
State string `json:"state"`
Revision uint64 `json:"revision"`
@@ -1117,7 +1118,7 @@ func decodeBody(w http.ResponseWriter, r *http.Request, target any) bool {
}
func toQueueResponse(ticket domain.QueueTicket) queueResponse {
return queueResponse{TicketID: ticket.TicketID, PlayerID: ticket.PlayerID, MatchID: ticket.MatchID, Playlist: string(ticket.Playlist), State: string(ticket.State), Revision: ticket.Revision, EnqueuedAt: ticket.EnqueuedAt, ExpiresAt: ticket.ExpiresAt}
return queueResponse{TicketID: ticket.TicketID, PlayerID: ticket.PlayerID, ProposalID: ticket.ProposalID, MatchID: ticket.MatchID, Playlist: string(ticket.Playlist), State: string(ticket.State), Revision: ticket.Revision, EnqueuedAt: ticket.EnqueuedAt, ExpiresAt: ticket.ExpiresAt}
}
func toProposalResponse(proposal domain.Proposal) proposalResponse {
+4 -1
View File
@@ -23,7 +23,10 @@ import (
type queueBackendSpy struct{ createCalls, heartbeatCalls, cancelCalls, getCalls int }
func TestQueueResponseCarriesRecoveredMatchIdentity(t *testing.T) {
response := toQueueResponse(domain.QueueTicket{TicketID: "ticket-1234567890", PlayerID: "player-1234567890", MatchID: "match-1234567890", State: domain.AssignmentReady})
response := toQueueResponse(domain.QueueTicket{TicketID: "ticket-1234567890", PlayerID: "player-1234567890", ProposalID: "proposal-1234567890", MatchID: "match-1234567890", State: domain.AssignmentReady})
if response.ProposalID != "proposal-1234567890" {
t.Fatalf("queue response proposal ID = %q", response.ProposalID)
}
if response.MatchID != "match-1234567890" {
t.Fatalf("queue response match ID = %q", response.MatchID)
}