Stage 6: gateway edge (Connect/FlatBuffers over h2c, platform/email/guest auth, sessions, rate-limit, admin passthrough, live push bridge)
New public ingress and the first network edge. Framework + a vertical slice of operations end-to-end; remaining ops reuse the same transcode pattern in Stage 7. Contracts (new module scrabble/pkg): - push.proto (backend->gateway gRPC server-stream) + scrabble.fbs (FlatBuffers edge payloads), committed generated Go; buf/flatc Makefiles (dev-time codegen). Backend: - REST handlers on the /api/v1 groups: internal session endpoints (telegram/guest/email login -> mint, resolve, revoke) and the user slice (profile, submit_play, state, lobby enqueue/poll, chat). - internal/notify in-process Publisher hub + internal/pushgrpc gRPC server (BACKEND_GRPC_ADDR) streaming your_turn/opponent_moved/chat/nudge/match_found; emission in game.commit, social, matchmaker. - migration 00005 accounts.is_guest; guests are durable rows excluded from stats; ProvisionGuest; email-as-login (RequestLoginCode/LoginWithCode). Gateway (new module scrabble/gateway): - Connect Gateway service over h2c (Execute + Subscribe), FlatBuffers<->JSON transcode registry, Telegram initData HMAC validator (seam), session cache, token-bucket rate limiter (3 classes), push fan-out hub, backend REST + push gRPC client, admin Basic-Auth reverse proxy. go.work: use ./pkg, ./gateway + replace scrabble/pkg. CI: gateway/**, pkg/** path filters; unit build/vet/test span all three modules. Docs (PLAN, ARCHITECTURE, FUNCTIONAL+ru, TESTING, READMEs) updated; gateway/pkg unit tests + guest/email-login integration tests.
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"scrabble/backend/internal/engine"
|
||||
"scrabble/backend/internal/game"
|
||||
"scrabble/backend/internal/notify"
|
||||
)
|
||||
|
||||
// Matchmaker is the in-memory auto-match pool: a FIFO queue per variant that pairs
|
||||
@@ -30,6 +31,7 @@ type Matchmaker struct {
|
||||
robots RobotProvider
|
||||
waitDelay time.Duration
|
||||
clock func() time.Time
|
||||
pub notify.Publisher
|
||||
log *zap.Logger
|
||||
|
||||
mu sync.Mutex
|
||||
@@ -51,6 +53,7 @@ func NewMatchmaker(games GameCreator, robots RobotProvider, waitDelay time.Durat
|
||||
robots: robots,
|
||||
waitDelay: waitDelay,
|
||||
clock: func() time.Time { return time.Now().UTC() },
|
||||
pub: notify.Nop{},
|
||||
log: log,
|
||||
queues: make(map[engine.Variant][]uuid.UUID),
|
||||
queued: make(map[uuid.UUID]engine.Variant),
|
||||
@@ -60,6 +63,26 @@ func NewMatchmaker(games GameCreator, robots RobotProvider, waitDelay time.Durat
|
||||
}
|
||||
}
|
||||
|
||||
// SetNotifier installs the live-event publisher used to push match_found to the
|
||||
// seated players when a pairing or robot substitution starts a game. It must be
|
||||
// called during startup wiring, before the reaper runs; the default is
|
||||
// notify.Nop (no live events; waiters still discover the game via Poll).
|
||||
func (m *Matchmaker) SetNotifier(p notify.Publisher) {
|
||||
if p != nil {
|
||||
m.pub = p
|
||||
}
|
||||
}
|
||||
|
||||
// emitMatchFound pushes match_found to every seat of a freshly started game.
|
||||
// Emitting to a robot seat is harmless (no client subscription exists for it).
|
||||
func (m *Matchmaker) emitMatchFound(g game.Game) {
|
||||
intents := make([]notify.Intent, 0, len(g.Seats))
|
||||
for _, s := range g.Seats {
|
||||
intents = append(intents, notify.MatchFound(s.AccountID, g.ID))
|
||||
}
|
||||
m.pub.Publish(intents...)
|
||||
}
|
||||
|
||||
// EnqueueResult reports the outcome of joining the pool: either a started game or a
|
||||
// queued ticket awaiting an opponent.
|
||||
type EnqueueResult struct {
|
||||
@@ -102,6 +125,7 @@ func (m *Matchmaker) Enqueue(ctx context.Context, accountID uuid.UUID, variant e
|
||||
m.mu.Lock()
|
||||
m.results[opponent] = g
|
||||
m.mu.Unlock()
|
||||
m.emitMatchFound(g)
|
||||
return EnqueueResult{Matched: true, Game: g}, nil
|
||||
}
|
||||
|
||||
@@ -197,6 +221,7 @@ func (m *Matchmaker) Reap(ctx context.Context, now time.Time) {
|
||||
m.mu.Lock()
|
||||
m.results[s.human] = g
|
||||
m.mu.Unlock()
|
||||
m.emitMatchFound(g)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user