R6(c): share the nested FB builders between notify and gateway transcode

Extract the FlatBuffers builders for the wire tables shared by the backend push
encoder and the gateway edge transcoder — GameView, MoveRecord, StateView,
AccountRef, Invitation and their nested rows — into a new scrabble/pkg/wire
package. Both callers keep their local builder signatures (no call sites move)
but now map their own source types (the backend's notify.* payloads and the
decoded engine.MoveRecord; the gateway's backendclient.* REST DTOs) to neutral
wire.* structs and delegate the construction to package wire, the single
definition of the nested-table layout.

Behaviour-preserving: the verified-identical field sets mean the wire bytes
decode the same, and the notify + transcode round-trip tests pass unchanged. The
fiddly Start/Add/End + reverse-prepend vector boilerplate now lives once; the two
encode files shrink while pkg/wire carries the shared logic.
This commit is contained in:
Ilia Denisov
2026-06-10 17:21:18 +02:00
parent 1079878654
commit b47c47e969
4 changed files with 459 additions and 305 deletions
+54 -100
View File
@@ -5,6 +5,7 @@ import (
"scrabble/gateway/internal/backendclient"
fb "scrabble/pkg/fbs/scrabblefb"
"scrabble/pkg/wire"
)
// The encoders build the FlatBuffers response payloads from the backend's typed
@@ -142,46 +143,24 @@ func encodeMoveResult(r backendclient.MoveResultResp) []byte {
// client requests it on a per-variant cache miss).
func encodeState(s backendclient.StateResp) []byte {
b := flatbuffers.NewBuilder(512)
game := buildGameView(b, s.Game)
rackBytes := make([]byte, len(s.Rack))
for i, v := range s.Rack {
rackBytes[i] = byte(v)
}
rack := b.CreateByteVector(rackBytes)
hasAlphabet := len(s.Alphabet) > 0
var alphabet flatbuffers.UOffsetT
if hasAlphabet {
alphabet = buildAlphabet(b, s.Alphabet)
}
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))
if hasAlphabet {
fb.StateViewAddAlphabet(b, alphabet)
}
b.Finish(fb.StateViewEnd(b))
b.Finish(wire.BuildStateView(b, toWireState(s)))
return b.FinishedBytes()
}
// buildAlphabet builds the AlphabetEntry vector for a StateView and returns its offset.
func buildAlphabet(b *flatbuffers.Builder, entries []backendclient.AlphabetEntryJSON) flatbuffers.UOffsetT {
offs := make([]flatbuffers.UOffsetT, len(entries))
for i, e := range entries {
letter := b.CreateString(e.Letter)
fb.AlphabetEntryStart(b)
fb.AlphabetEntryAddIndex(b, byte(e.Index))
fb.AlphabetEntryAddLetter(b, letter)
fb.AlphabetEntryAddValue(b, int32(e.Value))
offs[i] = fb.AlphabetEntryEnd(b)
// toWireState maps a StateResp to the shared wire.StateView.
func toWireState(s backendclient.StateResp) wire.StateView {
alphabet := make([]wire.AlphabetEntry, len(s.Alphabet))
for i, e := range s.Alphabet {
alphabet[i] = wire.AlphabetEntry{Index: e.Index, Letter: e.Letter, Value: e.Value}
}
fb.StateViewStartAlphabetVector(b, len(offs))
for i := len(offs) - 1; i >= 0; i-- {
b.PrependUOffsetT(offs[i])
return wire.StateView{
Game: toWireGame(s.Game),
Seat: s.Seat,
Rack: s.Rack,
BagLen: s.BagLen,
HintsRemaining: s.HintsRemaining,
Alphabet: alphabet,
}
return b.EndVector(len(offs))
}
// encodeMatch builds a MatchResult payload.
@@ -328,80 +307,55 @@ func encodeChatList(r backendclient.ChatListResp) []byte {
// 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))
return wire.BuildGameView(b, toWireGame(g))
}
// toWireGame maps a GameResp to the shared wire.GameView.
func toWireGame(g backendclient.GameResp) wire.GameView {
seats := make([]wire.SeatView, len(g.Seats))
for i, s := range g.Seats {
aid := b.CreateString(s.AccountID)
dname := b.CreateString(s.DisplayName)
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)
fb.SeatViewAddDisplayName(b, dname)
seatOffs[i] = fb.SeatViewEnd(b)
seats[i] = wire.SeatView{
Seat: s.Seat,
AccountID: s.AccountID,
Score: s.Score,
HintsUsed: s.HintsUsed,
IsWinner: s.IsWinner,
DisplayName: s.DisplayName,
}
}
fb.GameViewStartSeatsVector(b, len(seatOffs))
for i := len(seatOffs) - 1; i >= 0; i-- {
b.PrependUOffsetT(seatOffs[i])
return wire.GameView{
ID: g.ID,
Variant: g.Variant,
DictVersion: g.DictVersion,
Status: g.Status,
Players: g.Players,
ToMove: g.ToMove,
TurnTimeoutSecs: g.TurnTimeoutSecs,
MoveCount: g.MoveCount,
EndReason: g.EndReason,
Seats: seats,
LastActivityUnix: g.LastActivityUnix,
}
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)
fb.GameViewAddLastActivityUnix(b, g.LastActivityUnix)
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))
tiles := make([]wire.TileRecord, 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)
tiles[i] = wire.TileRecord{Row: t.Row, Col: t.Col, Letter: t.Letter, Blank: t.Blank}
}
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)
return wire.BuildMoveRecord(b, wire.MoveRecord{
Player: m.Player,
Action: m.Action,
Dir: m.Dir,
MainRow: m.MainRow,
MainCol: m.MainCol,
Tiles: tiles,
Words: m.Words,
Count: m.Count,
Score: m.Score,
Total: m.Total,
})
}
// buildStringVector builds a vector of strings using the table-specific