feat(profile): advertise per-variant dictionary versions for offline preload

The Profile now carries dict_versions (game variant -> current dictionary
version), populated from the dictionary registry at the profileResponse
choke point, so an installed PWA can preload the matching dawg per enabled
variant off the existing cold-start profile request instead of adding a
round-trip for a rare feature.

Wire path: FBS DictVersion table + Profile.dict_versions (additive,
backward-compatible trailing field) -> backend dto/registry -> gateway
ProfileResp + FBS encoder -> client codec decode into a per-variant map on
model.Profile. Empty in a degenerate no-dictionary deployment; the mock
serves v1.3.0 for all three variants. Codec decode covered by a
bite-tested round-trip unit test.
This commit is contained in:
Ilia Denisov
2026-07-06 10:12:58 +02:00
parent 8349e222fc
commit a692024b4e
14 changed files with 321 additions and 2 deletions
+29
View File
@@ -95,6 +95,7 @@ func encodeProfile(p backendclient.ProfileResp) []byte {
banner = encodeBanner(b, *p.Banner)
}
prefs := buildStringVector(b, p.VariantPreferences, fb.ProfileStartVariantPreferencesVector)
dictVersions := encodeDictVersions(b, p.DictVersions)
fb.ProfileStart(b)
fb.ProfileAddUserId(b, uid)
fb.ProfileAddDisplayName(b, name)
@@ -111,6 +112,9 @@ func encodeProfile(p backendclient.ProfileResp) []byte {
fb.ProfileAddEmail(b, email)
fb.ProfileAddTelegramLinked(b, p.TelegramLinked)
fb.ProfileAddVkLinked(b, p.VkLinked)
if dictVersions != 0 {
fb.ProfileAddDictVersions(b, dictVersions)
}
if p.Banner != nil {
fb.ProfileAddBanner(b, banner)
}
@@ -118,6 +122,31 @@ func encodeProfile(p backendclient.ProfileResp) []byte {
return b.FinishedBytes()
}
// encodeDictVersions builds the Profile's dict_versions vector — one DictVersion table per
// variant/version pair — and returns its offset, or 0 when there are none (the field is then
// omitted). Every child table and its strings are created before the vector is opened, per the
// FlatBuffers rule against nesting a table under one still being built; the caller invokes it
// before ProfileStart for the same reason.
func encodeDictVersions(b *flatbuffers.Builder, dvs []backendclient.DictVersion) flatbuffers.UOffsetT {
if len(dvs) == 0 {
return 0
}
offsets := make([]flatbuffers.UOffsetT, len(dvs))
for i, dv := range dvs {
variant := b.CreateString(dv.Variant)
version := b.CreateString(dv.Version)
fb.DictVersionStart(b)
fb.DictVersionAddVariant(b, variant)
fb.DictVersionAddVersion(b, version)
offsets[i] = fb.DictVersionEnd(b)
}
fb.ProfileStartDictVersionsVector(b, len(offsets))
for i := len(offsets) - 1; i >= 0; i-- {
b.PrependUOffsetT(offsets[i])
}
return b.EndVector(len(offsets))
}
// optString creates a FlatBuffers string for a non-empty value, or 0 to omit the
// optional field (the client then reads it as absent).
func optString(b *flatbuffers.Builder, s string) flatbuffers.UOffsetT {