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:
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"scrabble/backend/internal/account"
|
||||
"scrabble/backend/internal/ads"
|
||||
"scrabble/backend/internal/engine"
|
||||
"scrabble/backend/internal/notify"
|
||||
)
|
||||
|
||||
@@ -56,9 +57,34 @@ func (s *Server) profileResponse(ctx context.Context, acc account.Account) profi
|
||||
r := profileResponseFor(acc)
|
||||
r.Banner = s.bannerFor(ctx, acc)
|
||||
s.fillLinkedIdentities(ctx, &r, acc.ID)
|
||||
r.DictVersions = s.currentDictVersions()
|
||||
return r
|
||||
}
|
||||
|
||||
// currentDictVersions reports the current dictionary version of every game variant with a
|
||||
// resident dictionary, as engine.Variant stable labels. It backs profileResponse.DictVersions
|
||||
// so an offline-capable client preloads the matching dawg per variant off the cold-start
|
||||
// profile. A variant without a loaded dictionary is omitted (Registry.Latest reports
|
||||
// ErrUnknownVariant); the returned slice is nil only when no variant is resident.
|
||||
func (s *Server) currentDictVersions() []dictVersion {
|
||||
if s.registry == nil {
|
||||
return nil // no dictionary registry wired (e.g. a minimal test server): advertise none
|
||||
}
|
||||
variants := []engine.Variant{engine.VariantEnglish, engine.VariantRussianScrabble, engine.VariantErudit}
|
||||
out := make([]dictVersion, 0, len(variants))
|
||||
for _, v := range variants {
|
||||
version, _, err := s.registry.Latest(v)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
out = append(out, dictVersion{Variant: v.String(), Version: version})
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// fillLinkedIdentities sets the profile's confirmed email address and platform-linked
|
||||
// flags from the account's identities, so the client offers the right link / unlink /
|
||||
// change-email controls. A read failure leaves them zero (no controls), logged as a
|
||||
|
||||
@@ -63,6 +63,20 @@ type profileResponse struct {
|
||||
Email string `json:"email"`
|
||||
TelegramLinked bool `json:"telegram_linked"`
|
||||
VkLinked bool `json:"vk_linked"`
|
||||
// DictVersions lists the current dictionary version per game variant (engine.Variant
|
||||
// stable labels). An installed PWA preparing for offline play reads it to preload the
|
||||
// right dawg per enabled variant off this cold-start response, without an extra request.
|
||||
// Filled outside the pure projection (it reads the dictionary registry), so it is empty
|
||||
// for callers that build the DTO without a Server. See Server.profileResponse.
|
||||
DictVersions []dictVersion `json:"dict_versions,omitempty"`
|
||||
}
|
||||
|
||||
// dictVersion pairs a game variant's stable label (engine.Variant.String) with its current
|
||||
// dictionary version. profileResponse.DictVersions carries the set so an offline-capable
|
||||
// client preloads the matching dawg per variant.
|
||||
type dictVersion struct {
|
||||
Variant string `json:"variant"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
// tileDTO is one placed (or to-place) tile.
|
||||
|
||||
@@ -42,6 +42,15 @@ type ProfileResp struct {
|
||||
Email string `json:"email"`
|
||||
TelegramLinked bool `json:"telegram_linked"`
|
||||
VkLinked bool `json:"vk_linked"`
|
||||
// DictVersions is the current dictionary version per game variant, forwarded verbatim
|
||||
// into the Profile payload so an offline-capable client preloads the matching dawg.
|
||||
DictVersions []DictVersion `json:"dict_versions,omitempty"`
|
||||
}
|
||||
|
||||
// DictVersion pairs a game variant's stable label with its current dictionary version.
|
||||
type DictVersion struct {
|
||||
Variant string `json:"variant"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
// BannerResp is the advertising-banner block of an eligible viewer's profile: the
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -223,6 +223,13 @@ table BannerInfo {
|
||||
// suppresses out-of-app platform push, leaving only the in-app live stream. banner
|
||||
// carries the advertising-banner block for an eligible viewer, absent otherwise
|
||||
// (all added trailing — backward-compatible).
|
||||
// DictVersion is one variant's current dictionary version, carried on the profile so an offline
|
||||
// client learns it from an existing cold-start request (no extra call for a rarely-used feature).
|
||||
table DictVersion {
|
||||
variant:string;
|
||||
version:string;
|
||||
}
|
||||
|
||||
table Profile {
|
||||
user_id:string;
|
||||
display_name:string;
|
||||
@@ -245,6 +252,10 @@ table Profile {
|
||||
email:string;
|
||||
telegram_linked:bool;
|
||||
vk_linked:bool;
|
||||
// dict_versions carries each variant's current dictionary version so an offline client can
|
||||
// preload the right dictionary and pin a new local game without a separate request (added
|
||||
// trailing — backward-compatible).
|
||||
dict_versions:[DictVersion];
|
||||
}
|
||||
|
||||
// BlockStatus reports the caller's current manual block. The UI fetches it after any operation
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
|
||||
|
||||
package scrabblefb
|
||||
|
||||
import (
|
||||
flatbuffers "github.com/google/flatbuffers/go"
|
||||
)
|
||||
|
||||
type DictVersion struct {
|
||||
_tab flatbuffers.Table
|
||||
}
|
||||
|
||||
func GetRootAsDictVersion(buf []byte, offset flatbuffers.UOffsetT) *DictVersion {
|
||||
n := flatbuffers.GetUOffsetT(buf[offset:])
|
||||
x := &DictVersion{}
|
||||
x.Init(buf, n+offset)
|
||||
return x
|
||||
}
|
||||
|
||||
func FinishDictVersionBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
|
||||
builder.Finish(offset)
|
||||
}
|
||||
|
||||
func GetSizePrefixedRootAsDictVersion(buf []byte, offset flatbuffers.UOffsetT) *DictVersion {
|
||||
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
|
||||
x := &DictVersion{}
|
||||
x.Init(buf, n+offset+flatbuffers.SizeUint32)
|
||||
return x
|
||||
}
|
||||
|
||||
func FinishSizePrefixedDictVersionBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
|
||||
builder.FinishSizePrefixed(offset)
|
||||
}
|
||||
|
||||
func (rcv *DictVersion) Init(buf []byte, i flatbuffers.UOffsetT) {
|
||||
rcv._tab.Bytes = buf
|
||||
rcv._tab.Pos = i
|
||||
}
|
||||
|
||||
func (rcv *DictVersion) Table() flatbuffers.Table {
|
||||
return rcv._tab
|
||||
}
|
||||
|
||||
func (rcv *DictVersion) Variant() []byte {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
|
||||
if o != 0 {
|
||||
return rcv._tab.ByteVector(o + rcv._tab.Pos)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rcv *DictVersion) Version() []byte {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(6))
|
||||
if o != 0 {
|
||||
return rcv._tab.ByteVector(o + rcv._tab.Pos)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func DictVersionStart(builder *flatbuffers.Builder) {
|
||||
builder.StartObject(2)
|
||||
}
|
||||
func DictVersionAddVariant(builder *flatbuffers.Builder, variant flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(variant), 0)
|
||||
}
|
||||
func DictVersionAddVersion(builder *flatbuffers.Builder, version flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(1, flatbuffers.UOffsetT(version), 0)
|
||||
}
|
||||
func DictVersionEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
|
||||
return builder.EndObject()
|
||||
}
|
||||
@@ -211,8 +211,28 @@ func (rcv *Profile) MutateVkLinked(n bool) bool {
|
||||
return rcv._tab.MutateBoolSlot(34, n)
|
||||
}
|
||||
|
||||
func (rcv *Profile) DictVersions(obj *DictVersion, j int) bool {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(36))
|
||||
if o != 0 {
|
||||
x := rcv._tab.Vector(o)
|
||||
x += flatbuffers.UOffsetT(j) * 4
|
||||
x = rcv._tab.Indirect(x)
|
||||
obj.Init(rcv._tab.Bytes, x)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (rcv *Profile) DictVersionsLength() int {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(36))
|
||||
if o != 0 {
|
||||
return rcv._tab.VectorLen(o)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func ProfileStart(builder *flatbuffers.Builder) {
|
||||
builder.StartObject(16)
|
||||
builder.StartObject(17)
|
||||
}
|
||||
func ProfileAddUserId(builder *flatbuffers.Builder, userId flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(userId), 0)
|
||||
@@ -265,6 +285,12 @@ func ProfileAddTelegramLinked(builder *flatbuffers.Builder, telegramLinked bool)
|
||||
func ProfileAddVkLinked(builder *flatbuffers.Builder, vkLinked bool) {
|
||||
builder.PrependBoolSlot(15, vkLinked, false)
|
||||
}
|
||||
func ProfileAddDictVersions(builder *flatbuffers.Builder, dictVersions flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(16, flatbuffers.UOffsetT(dictVersions), 0)
|
||||
}
|
||||
func ProfileStartDictVersionsVector(builder *flatbuffers.Builder, numElems int) flatbuffers.UOffsetT {
|
||||
return builder.StartVector(4, numElems, 4)
|
||||
}
|
||||
func ProfileEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
|
||||
return builder.EndObject()
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ export { ChatPostRequest } from './scrabblefb/chat-post-request.js';
|
||||
export { CheckWordRequest } from './scrabblefb/check-word-request.js';
|
||||
export { ComplaintRequest } from './scrabblefb/complaint-request.js';
|
||||
export { CreateInvitationRequest } from './scrabblefb/create-invitation-request.js';
|
||||
export { DictVersion } from './scrabblefb/dict-version.js';
|
||||
export { DraftRequest } from './scrabblefb/draft-request.js';
|
||||
export { DraftView } from './scrabblefb/draft-view.js';
|
||||
export { EmailConfirmLinkRequest } from './scrabblefb/email-confirm-link-request.js';
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
// automatically generated by the FlatBuffers compiler, do not modify
|
||||
|
||||
import * as flatbuffers from 'flatbuffers';
|
||||
|
||||
export class DictVersion {
|
||||
bb: flatbuffers.ByteBuffer|null = null;
|
||||
bb_pos = 0;
|
||||
__init(i:number, bb:flatbuffers.ByteBuffer):DictVersion {
|
||||
this.bb_pos = i;
|
||||
this.bb = bb;
|
||||
return this;
|
||||
}
|
||||
|
||||
static getRootAsDictVersion(bb:flatbuffers.ByteBuffer, obj?:DictVersion):DictVersion {
|
||||
return (obj || new DictVersion()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
|
||||
}
|
||||
|
||||
static getSizePrefixedRootAsDictVersion(bb:flatbuffers.ByteBuffer, obj?:DictVersion):DictVersion {
|
||||
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
|
||||
return (obj || new DictVersion()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
|
||||
}
|
||||
|
||||
variant():string|null
|
||||
variant(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
variant(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 4);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
version():string|null
|
||||
version(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
version(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 6);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
static startDictVersion(builder:flatbuffers.Builder) {
|
||||
builder.startObject(2);
|
||||
}
|
||||
|
||||
static addVariant(builder:flatbuffers.Builder, variantOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(0, variantOffset, 0);
|
||||
}
|
||||
|
||||
static addVersion(builder:flatbuffers.Builder, versionOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(1, versionOffset, 0);
|
||||
}
|
||||
|
||||
static endDictVersion(builder:flatbuffers.Builder):flatbuffers.Offset {
|
||||
const offset = builder.endObject();
|
||||
return offset;
|
||||
}
|
||||
|
||||
static createDictVersion(builder:flatbuffers.Builder, variantOffset:flatbuffers.Offset, versionOffset:flatbuffers.Offset):flatbuffers.Offset {
|
||||
DictVersion.startDictVersion(builder);
|
||||
DictVersion.addVariant(builder, variantOffset);
|
||||
DictVersion.addVersion(builder, versionOffset);
|
||||
return DictVersion.endDictVersion(builder);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
import * as flatbuffers from 'flatbuffers';
|
||||
|
||||
import { BannerInfo } from '../scrabblefb/banner-info.js';
|
||||
import { DictVersion } from '../scrabblefb/dict-version.js';
|
||||
|
||||
|
||||
export class Profile {
|
||||
@@ -124,8 +125,18 @@ vkLinked():boolean {
|
||||
return offset ? !!this.bb!.readInt8(this.bb_pos + offset) : false;
|
||||
}
|
||||
|
||||
dictVersions(index: number, obj?:DictVersion):DictVersion|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 36);
|
||||
return offset ? (obj || new DictVersion()).__init(this.bb!.__indirect(this.bb!.__vector(this.bb_pos + offset) + index * 4), this.bb!) : null;
|
||||
}
|
||||
|
||||
dictVersionsLength():number {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 36);
|
||||
return offset ? this.bb!.__vector_len(this.bb_pos + offset) : 0;
|
||||
}
|
||||
|
||||
static startProfile(builder:flatbuffers.Builder) {
|
||||
builder.startObject(16);
|
||||
builder.startObject(17);
|
||||
}
|
||||
|
||||
static addUserId(builder:flatbuffers.Builder, userIdOffset:flatbuffers.Offset) {
|
||||
@@ -204,6 +215,22 @@ static addVkLinked(builder:flatbuffers.Builder, vkLinked:boolean) {
|
||||
builder.addFieldInt8(15, +vkLinked, +false);
|
||||
}
|
||||
|
||||
static addDictVersions(builder:flatbuffers.Builder, dictVersionsOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(16, dictVersionsOffset, 0);
|
||||
}
|
||||
|
||||
static createDictVersionsVector(builder:flatbuffers.Builder, data:flatbuffers.Offset[]):flatbuffers.Offset {
|
||||
builder.startVector(4, data.length, 4);
|
||||
for (let i = data.length - 1; i >= 0; i--) {
|
||||
builder.addOffset(data[i]!);
|
||||
}
|
||||
return builder.endVector();
|
||||
}
|
||||
|
||||
static startDictVersionsVector(builder:flatbuffers.Builder, numElems:number) {
|
||||
builder.startVector(4, numElems, 4);
|
||||
}
|
||||
|
||||
static endProfile(builder:flatbuffers.Builder):flatbuffers.Offset {
|
||||
const offset = builder.endObject();
|
||||
return offset;
|
||||
|
||||
@@ -712,6 +712,28 @@ describe('codec', () => {
|
||||
expect(decodeProfile(b.asUint8Array()).variantPreferences).toEqual(['erudit_ru', 'scrabble_en']);
|
||||
});
|
||||
|
||||
it('decodes the profile dictionary versions into a per-variant map', () => {
|
||||
const b = new Builder(128);
|
||||
const uid = b.createString('u-1');
|
||||
const en = fb.DictVersion.createDictVersion(b, b.createString('scrabble_en'), b.createString('v1.3.0'));
|
||||
const er = fb.DictVersion.createDictVersion(b, b.createString('erudit_ru'), b.createString('v1.2.0'));
|
||||
const vec = fb.Profile.createDictVersionsVector(b, [en, er]);
|
||||
fb.Profile.startProfile(b);
|
||||
fb.Profile.addUserId(b, uid);
|
||||
fb.Profile.addDictVersions(b, vec);
|
||||
b.finish(fb.Profile.endProfile(b));
|
||||
expect(decodeProfile(b.asUint8Array()).dictVersions).toEqual({ scrabble_en: 'v1.3.0', erudit_ru: 'v1.2.0' });
|
||||
});
|
||||
|
||||
it('decodes an absent dict_versions vector to an empty map', () => {
|
||||
const b = new Builder(64);
|
||||
const uid = b.createString('u-1');
|
||||
fb.Profile.startProfile(b);
|
||||
fb.Profile.addUserId(b, uid);
|
||||
b.finish(fb.Profile.endProfile(b));
|
||||
expect(decodeProfile(b.asUint8Array()).dictVersions).toEqual({});
|
||||
});
|
||||
|
||||
it('encodes the update-profile variant preferences', () => {
|
||||
const buf = encodeUpdateProfile({
|
||||
displayName: 'Kaya',
|
||||
|
||||
@@ -364,9 +364,25 @@ export function decodeProfile(buf: Uint8Array): Profile {
|
||||
email: s(p.email()),
|
||||
telegramLinked: p.telegramLinked(),
|
||||
vkLinked: p.vkLinked(),
|
||||
dictVersions: decodeDictVersions(p),
|
||||
};
|
||||
}
|
||||
|
||||
// decodeDictVersions reads the profile's per-variant current dictionary versions into a
|
||||
// variant-keyed map, so the offline preloader can look up the version for each enabled
|
||||
// variant. An entry with an unknown variant or empty version is skipped.
|
||||
function decodeDictVersions(p: fb.Profile): Partial<Record<Variant, string>> {
|
||||
const out: Partial<Record<Variant, string>> = {};
|
||||
for (let i = 0; i < p.dictVersionsLength(); i++) {
|
||||
const dv = p.dictVersions(i);
|
||||
if (!dv) continue;
|
||||
const variant = s(dv.variant()) as Variant;
|
||||
const version = s(dv.version());
|
||||
if (variant && version) out[variant] = version;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// decodeVariantPreferences reads the Profile.variant_preferences vector (the variants
|
||||
// the player enabled in Settings).
|
||||
function decodeVariantPreferences(p: fb.Profile): Variant[] {
|
||||
|
||||
@@ -42,6 +42,9 @@ export const PROFILE: Profile = {
|
||||
email: 'you@example.com',
|
||||
telegramLinked: false,
|
||||
vkLinked: false,
|
||||
// Every variant's current dictionary version, as the backend advertises it on the profile —
|
||||
// the offline preloader targets `variant@version`.
|
||||
dictVersions: { erudit_ru: 'v1.3.0', scrabble_ru: 'v1.3.0', scrabble_en: 'v1.3.0' },
|
||||
};
|
||||
|
||||
// Seed social/account data for the mock (pnpm start + Playwright). The mock profile
|
||||
|
||||
@@ -161,6 +161,10 @@ export interface Profile {
|
||||
/** Whether a Telegram / VK identity is attached — drives the Add / Unlink controls. */
|
||||
telegramLinked: boolean;
|
||||
vkLinked: boolean;
|
||||
/** Current dictionary version per game variant (deployment-wide, not per-account). An
|
||||
* offline-capable PWA reads it to preload the matching dawg for each enabled variant off
|
||||
* this cold-start response. Empty only in a degenerate deployment with no resident dictionary. */
|
||||
dictVersions: Partial<Record<Variant, string>>;
|
||||
}
|
||||
|
||||
/** Banner is the advertising-banner block of an eligible viewer's profile. */
|
||||
|
||||
Reference in New Issue
Block a user