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:
@@ -0,0 +1,209 @@
|
||||
package transcode
|
||||
|
||||
import (
|
||||
flatbuffers "github.com/google/flatbuffers/go"
|
||||
|
||||
"scrabble/gateway/internal/backendclient"
|
||||
fb "scrabble/pkg/fbs/scrabblefb"
|
||||
)
|
||||
|
||||
// The encoders build the FlatBuffers response payloads from the backend's typed
|
||||
// responses. FlatBuffers is built bottom-up: every string and child vector is
|
||||
// created before the table that references it, and no two tables/vectors are
|
||||
// under construction at once.
|
||||
|
||||
// encodeSession builds a Session payload.
|
||||
func encodeSession(s backendclient.SessionResp) []byte {
|
||||
b := flatbuffers.NewBuilder(128)
|
||||
token := b.CreateString(s.Token)
|
||||
uid := b.CreateString(s.UserID)
|
||||
name := b.CreateString(s.DisplayName)
|
||||
fb.SessionStart(b)
|
||||
fb.SessionAddToken(b, token)
|
||||
fb.SessionAddUserId(b, uid)
|
||||
fb.SessionAddIsGuest(b, s.IsGuest)
|
||||
fb.SessionAddDisplayName(b, name)
|
||||
b.Finish(fb.SessionEnd(b))
|
||||
return b.FinishedBytes()
|
||||
}
|
||||
|
||||
// encodeAck builds an Ack payload.
|
||||
func encodeAck(ok bool) []byte {
|
||||
b := flatbuffers.NewBuilder(16)
|
||||
fb.AckStart(b)
|
||||
fb.AckAddOk(b, ok)
|
||||
b.Finish(fb.AckEnd(b))
|
||||
return b.FinishedBytes()
|
||||
}
|
||||
|
||||
// encodeProfile builds a Profile payload.
|
||||
func encodeProfile(p backendclient.ProfileResp) []byte {
|
||||
b := flatbuffers.NewBuilder(192)
|
||||
uid := b.CreateString(p.UserID)
|
||||
name := b.CreateString(p.DisplayName)
|
||||
lang := b.CreateString(p.PreferredLanguage)
|
||||
tz := b.CreateString(p.TimeZone)
|
||||
fb.ProfileStart(b)
|
||||
fb.ProfileAddUserId(b, uid)
|
||||
fb.ProfileAddDisplayName(b, name)
|
||||
fb.ProfileAddPreferredLanguage(b, lang)
|
||||
fb.ProfileAddTimeZone(b, tz)
|
||||
fb.ProfileAddHintBalance(b, int32(p.HintBalance))
|
||||
fb.ProfileAddBlockChat(b, p.BlockChat)
|
||||
fb.ProfileAddBlockFriendRequests(b, p.BlockFriendRequests)
|
||||
fb.ProfileAddIsGuest(b, p.IsGuest)
|
||||
b.Finish(fb.ProfileEnd(b))
|
||||
return b.FinishedBytes()
|
||||
}
|
||||
|
||||
// encodeMoveResult builds a MoveResult payload.
|
||||
func encodeMoveResult(r backendclient.MoveResultResp) []byte {
|
||||
b := flatbuffers.NewBuilder(512)
|
||||
move := buildMoveRecord(b, r.Move)
|
||||
game := buildGameView(b, r.Game)
|
||||
fb.MoveResultStart(b)
|
||||
fb.MoveResultAddMove(b, move)
|
||||
fb.MoveResultAddGame(b, game)
|
||||
b.Finish(fb.MoveResultEnd(b))
|
||||
return b.FinishedBytes()
|
||||
}
|
||||
|
||||
// encodeState builds a StateView payload.
|
||||
func encodeState(s backendclient.StateResp) []byte {
|
||||
b := flatbuffers.NewBuilder(512)
|
||||
game := buildGameView(b, s.Game)
|
||||
rack := buildStringVector(b, s.Rack, fb.StateViewStartRackVector)
|
||||
fb.StateViewStart(b)
|
||||
fb.StateViewAddGame(b, game)
|
||||
fb.StateViewAddSeat(b, int32(s.Seat))
|
||||
fb.StateViewAddRack(b, rack)
|
||||
fb.StateViewAddBagLen(b, int32(s.BagLen))
|
||||
fb.StateViewAddHintsRemaining(b, int32(s.HintsRemaining))
|
||||
b.Finish(fb.StateViewEnd(b))
|
||||
return b.FinishedBytes()
|
||||
}
|
||||
|
||||
// encodeMatch builds a MatchResult payload.
|
||||
func encodeMatch(m backendclient.MatchResp) []byte {
|
||||
b := flatbuffers.NewBuilder(512)
|
||||
matched := m.Matched && m.Game != nil
|
||||
var game flatbuffers.UOffsetT
|
||||
if matched {
|
||||
game = buildGameView(b, *m.Game)
|
||||
}
|
||||
fb.MatchResultStart(b)
|
||||
fb.MatchResultAddMatched(b, matched)
|
||||
if matched {
|
||||
fb.MatchResultAddGame(b, game)
|
||||
}
|
||||
b.Finish(fb.MatchResultEnd(b))
|
||||
return b.FinishedBytes()
|
||||
}
|
||||
|
||||
// encodeChat builds a ChatMessage payload.
|
||||
func encodeChat(c backendclient.ChatResp) []byte {
|
||||
b := flatbuffers.NewBuilder(192)
|
||||
id := b.CreateString(c.ID)
|
||||
gid := b.CreateString(c.GameID)
|
||||
sid := b.CreateString(c.SenderID)
|
||||
kind := b.CreateString(c.Kind)
|
||||
body := b.CreateString(c.Body)
|
||||
fb.ChatMessageStart(b)
|
||||
fb.ChatMessageAddId(b, id)
|
||||
fb.ChatMessageAddGameId(b, gid)
|
||||
fb.ChatMessageAddSenderId(b, sid)
|
||||
fb.ChatMessageAddKind(b, kind)
|
||||
fb.ChatMessageAddBody(b, body)
|
||||
fb.ChatMessageAddCreatedAtUnix(b, c.CreatedAtUnix)
|
||||
b.Finish(fb.ChatMessageEnd(b))
|
||||
return b.FinishedBytes()
|
||||
}
|
||||
|
||||
// buildGameView builds a GameView table and returns its offset.
|
||||
func buildGameView(b *flatbuffers.Builder, g backendclient.GameResp) flatbuffers.UOffsetT {
|
||||
seatOffs := make([]flatbuffers.UOffsetT, len(g.Seats))
|
||||
for i, s := range g.Seats {
|
||||
aid := b.CreateString(s.AccountID)
|
||||
fb.SeatViewStart(b)
|
||||
fb.SeatViewAddSeat(b, int32(s.Seat))
|
||||
fb.SeatViewAddAccountId(b, aid)
|
||||
fb.SeatViewAddScore(b, int32(s.Score))
|
||||
fb.SeatViewAddHintsUsed(b, int32(s.HintsUsed))
|
||||
fb.SeatViewAddIsWinner(b, s.IsWinner)
|
||||
seatOffs[i] = fb.SeatViewEnd(b)
|
||||
}
|
||||
fb.GameViewStartSeatsVector(b, len(seatOffs))
|
||||
for i := len(seatOffs) - 1; i >= 0; i-- {
|
||||
b.PrependUOffsetT(seatOffs[i])
|
||||
}
|
||||
seats := b.EndVector(len(seatOffs))
|
||||
|
||||
id := b.CreateString(g.ID)
|
||||
variant := b.CreateString(g.Variant)
|
||||
dictVer := b.CreateString(g.DictVersion)
|
||||
status := b.CreateString(g.Status)
|
||||
endReason := b.CreateString(g.EndReason)
|
||||
|
||||
fb.GameViewStart(b)
|
||||
fb.GameViewAddId(b, id)
|
||||
fb.GameViewAddVariant(b, variant)
|
||||
fb.GameViewAddDictVersion(b, dictVer)
|
||||
fb.GameViewAddStatus(b, status)
|
||||
fb.GameViewAddPlayers(b, int32(g.Players))
|
||||
fb.GameViewAddToMove(b, int32(g.ToMove))
|
||||
fb.GameViewAddTurnTimeoutSecs(b, int32(g.TurnTimeoutSecs))
|
||||
fb.GameViewAddMoveCount(b, int32(g.MoveCount))
|
||||
fb.GameViewAddEndReason(b, endReason)
|
||||
fb.GameViewAddSeats(b, seats)
|
||||
return fb.GameViewEnd(b)
|
||||
}
|
||||
|
||||
// buildMoveRecord builds a MoveRecord table and returns its offset.
|
||||
func buildMoveRecord(b *flatbuffers.Builder, m backendclient.MoveRecordResp) flatbuffers.UOffsetT {
|
||||
tileOffs := make([]flatbuffers.UOffsetT, len(m.Tiles))
|
||||
for i, t := range m.Tiles {
|
||||
letter := b.CreateString(t.Letter)
|
||||
fb.TileRecordStart(b)
|
||||
fb.TileRecordAddRow(b, int32(t.Row))
|
||||
fb.TileRecordAddCol(b, int32(t.Col))
|
||||
fb.TileRecordAddLetter(b, letter)
|
||||
fb.TileRecordAddBlank(b, t.Blank)
|
||||
tileOffs[i] = fb.TileRecordEnd(b)
|
||||
}
|
||||
fb.MoveRecordStartTilesVector(b, len(tileOffs))
|
||||
for i := len(tileOffs) - 1; i >= 0; i-- {
|
||||
b.PrependUOffsetT(tileOffs[i])
|
||||
}
|
||||
tiles := b.EndVector(len(tileOffs))
|
||||
|
||||
words := buildStringVector(b, m.Words, fb.MoveRecordStartWordsVector)
|
||||
|
||||
action := b.CreateString(m.Action)
|
||||
dir := b.CreateString(m.Dir)
|
||||
fb.MoveRecordStart(b)
|
||||
fb.MoveRecordAddPlayer(b, int32(m.Player))
|
||||
fb.MoveRecordAddAction(b, action)
|
||||
fb.MoveRecordAddDir(b, dir)
|
||||
fb.MoveRecordAddMainRow(b, int32(m.MainRow))
|
||||
fb.MoveRecordAddMainCol(b, int32(m.MainCol))
|
||||
fb.MoveRecordAddTiles(b, tiles)
|
||||
fb.MoveRecordAddWords(b, words)
|
||||
fb.MoveRecordAddCount(b, int32(m.Count))
|
||||
fb.MoveRecordAddScore(b, int32(m.Score))
|
||||
fb.MoveRecordAddTotal(b, int32(m.Total))
|
||||
return fb.MoveRecordEnd(b)
|
||||
}
|
||||
|
||||
// buildStringVector builds a vector of strings using the table-specific
|
||||
// StartXVector function and returns the vector offset.
|
||||
func buildStringVector(b *flatbuffers.Builder, items []string, start func(*flatbuffers.Builder, int) flatbuffers.UOffsetT) flatbuffers.UOffsetT {
|
||||
offs := make([]flatbuffers.UOffsetT, len(items))
|
||||
for i, s := range items {
|
||||
offs[i] = b.CreateString(s)
|
||||
}
|
||||
start(b, len(offs))
|
||||
for i := len(offs) - 1; i >= 0; i-- {
|
||||
b.PrependUOffsetT(offs[i])
|
||||
}
|
||||
return b.EndVector(len(offs))
|
||||
}
|
||||
Reference in New Issue
Block a user