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:
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/go-jet/jet/v2/qrm"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"scrabble/backend/internal/notify"
|
||||
"scrabble/backend/internal/postgres/jet/backend/model"
|
||||
"scrabble/backend/internal/postgres/jet/backend/table"
|
||||
)
|
||||
@@ -72,7 +73,12 @@ func (svc *Service) PostMessage(ctx context.Context, gameID, senderID uuid.UUID,
|
||||
if err := Clean(body); err != nil {
|
||||
return Message{}, err
|
||||
}
|
||||
return svc.store.insertChatMessage(ctx, gameID, senderID, kindMessage, body, parseIP(senderIP))
|
||||
msg, err := svc.store.insertChatMessage(ctx, gameID, senderID, kindMessage, body, parseIP(senderIP))
|
||||
if err != nil {
|
||||
return Message{}, err
|
||||
}
|
||||
svc.emitChat(seats, senderID, msg)
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
// Nudge records a nudge from senderID toward the player whose turn is awaited. The
|
||||
@@ -100,7 +106,27 @@ func (svc *Service) Nudge(ctx context.Context, gameID, senderID uuid.UUID) (Mess
|
||||
if ok && svc.now().Sub(last) < nudgeInterval {
|
||||
return Message{}, ErrNudgeTooSoon
|
||||
}
|
||||
return svc.store.insertChatMessage(ctx, gameID, senderID, kindNudge, "", nil)
|
||||
msg, err := svc.store.insertChatMessage(ctx, gameID, senderID, kindNudge, "", nil)
|
||||
if err != nil {
|
||||
return Message{}, err
|
||||
}
|
||||
if toMove >= 0 && toMove < len(seats) {
|
||||
svc.pub.Publish(notify.Nudge(seats[toMove], gameID, senderID))
|
||||
}
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
// emitChat pushes a chat message to every seated player except the sender
|
||||
// (best-effort live delivery; the recipients still read it via Messages).
|
||||
func (svc *Service) emitChat(seats []uuid.UUID, senderID uuid.UUID, m Message) {
|
||||
intents := make([]notify.Intent, 0, len(seats))
|
||||
for _, id := range seats {
|
||||
if id == senderID {
|
||||
continue
|
||||
}
|
||||
intents = append(intents, notify.ChatMessage(id, m.GameID, m.SenderID, m.ID.String(), m.Kind, m.Body, m.CreatedAt))
|
||||
}
|
||||
svc.pub.Publish(intents...)
|
||||
}
|
||||
|
||||
// LastNudgeAt returns the time of the most recent nudge senderID sent in the game
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/google/uuid"
|
||||
|
||||
"scrabble/backend/internal/account"
|
||||
"scrabble/backend/internal/notify"
|
||||
)
|
||||
|
||||
// GameReader is the slice of the game domain the social package needs: the seated
|
||||
@@ -60,6 +61,7 @@ type Service struct {
|
||||
store *Store
|
||||
accounts *account.Store
|
||||
games GameReader
|
||||
pub notify.Publisher
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
@@ -70,6 +72,16 @@ func NewService(store *Store, accounts *account.Store, games GameReader) *Servic
|
||||
store: store,
|
||||
accounts: accounts,
|
||||
games: games,
|
||||
pub: notify.Nop{},
|
||||
now: func() time.Time { return time.Now().UTC() },
|
||||
}
|
||||
}
|
||||
|
||||
// SetNotifier installs the live-event publisher used to push chat messages and
|
||||
// nudges to their recipients. It must be called during startup wiring, before
|
||||
// the service serves traffic; the default is notify.Nop (no live events).
|
||||
func (svc *Service) SetNotifier(p notify.Publisher) {
|
||||
if p != nil {
|
||||
svc.pub = p
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user