// GatewayClient — the typed facade the screens call. Both the real Connect/ // FlatBuffers transport and the in-memory mock implement it. Domain failures (the // gateway's result_code) and edge failures (Connect error codes) are normalised // into a thrown GatewayError carrying a stable `code` the UI maps to an i18n // message. import type { AccountRef, BlockList, BlockStatus, ChatMessage, EvalResult, FeedbackState, FriendCode, GameList, GameView, ExportUrl, GcgExport, History, HintResult, Invitation, InvitationSettings, ConfirmLinkResult, LinkResult, MatchResult, MoveResult, OutgoingList, Profile, ProfileUpdate, PushEvent, Session, StateView, Stats, Tile, Variant, WordCheckResult, } from './model'; /** GatewayError carries a stable code (the gateway result_code, or an edge code). */ export class GatewayError extends Error { readonly code: string; constructor(code: string, message?: string) { super(message ?? code); this.name = 'GatewayError'; this.code = code; } } /** A tile the player is submitting (rack/blank already resolved to a letter). */ export interface PlacedTile { row: number; col: number; letter: string; blank: boolean; } /** Unsubscribe handle for the live stream. */ export type Unsubscribe = () => void; export interface GatewayClient { // --- auth (unauthenticated) --- authTelegram(initData: string): Promise; /** Authenticate a VK Mini App launch: params is the signed vk_* launch query string (the gateway * verifies its sign); displayName is the client-read VKWebAppGetUserInfo name (an unsigned, * cosmetic seed for a brand-new account). */ authVK(params: string, displayName: string): Promise; authGuest(locale?: string): Promise; authEmailRequest(email: string, language: string): Promise; authEmailLogin(email: string, code: string): Promise; /** Confirm a one-tap email deeplink token: a login returns a session to adopt; a * link returns a status ('confirmed' | 'merge_required'). */ confirmEmailLink(token: string): Promise; // --- profile / lists --- profileGet(): Promise; /** The caller's current manual block. Exempt from the backend's suspension gate, so a blocked * client can still fetch it to render the blocked screen. */ blockStatus(): Promise; gamesList(): Promise; // --- lobby --- /** Start a quick game: human auto-match, or an honest-AI game against a robot when vsAi. */ lobbyEnqueue(variant: Variant, multipleWords: boolean, vsAi: boolean): Promise; lobbyPoll(): Promise; /** Leave the auto-match pool (idempotent); a cancelled quick-match must not stay queued. */ lobbyCancel(): Promise; // --- game --- // The play loop exchanges alphabet indices, so submit/evaluate/exchange/ // check-word take the game's variant (to map letters<->indices via the cached alphabet // table), and gameState's includeAlphabet asks the server to embed that table. gameState(gameId: string, includeAlphabet: boolean): Promise; gameHistory(gameId: string): Promise; submitPlay(gameId: string, tiles: PlacedTile[], variant: Variant): Promise; pass(gameId: string): Promise; exchange(gameId: string, tiles: string[], variant: Variant): Promise; resign(gameId: string): Promise; hint(gameId: string): Promise; evaluate(gameId: string, tiles: PlacedTile[], variant: Variant, signal?: AbortSignal): Promise; checkWord(gameId: string, word: string, variant: Variant): Promise; complaint(gameId: string, word: string, note: string): Promise; /** Hide a finished game from the caller's own lobby list; per-account, irreversible. */ hideGame(gameId: string): Promise; // --- dictionary (local move preview) --- /** Fetch the raw serialized dictionary blob for a (variant, version) pair. Session-gated; * lets the client validate and score a move locally instead of a network round trip. * opts.signal aborts a stalled download; opts.reload bypasses the browser HTTP cache (the * debug reset, for testing a cold load). */ fetchDict(variant: Variant, version: string, opts?: { signal?: AbortSignal; reload?: boolean }): Promise; /** Post the client's local move-preview adoption telemetry — a small batch of counter deltas, * session-gated and fire-and-forget (never on the gameplay path). */ reportLocalEval(counts: Record): Promise; // --- draft --- /** The player's server-persisted client-side composition (rack order + board tiles), so a * reload or a second device resumes the same arrangement. The JSON is opaque to the * gateway; the client owns the {rack_order, board_tiles} shape. */ draftGet(gameId: string): Promise; draftSave(gameId: string, json: string): Promise; // --- chat --- chatPost(gameId: string, body: string): Promise; chatList(gameId: string): Promise; nudge(gameId: string): Promise; /** Acknowledge the caller has read the game's chat (sent when they open the move history * or chat), so the backend clears their unread bits and records the read latency. */ markChatRead(gameId: string): Promise; // --- feedback --- /** feedbackSubmit sends a feedback message with an optional single attachment. */ feedbackSubmit(body: string, attachment: Uint8Array | null, attachmentName: string, channel: string): Promise; /** feedbackGet returns the feedback screen state and marks any pending reply delivered. */ feedbackGet(): Promise; /** feedbackUnread reports whether an operator reply awaits delivery (the badge). */ feedbackUnread(): Promise; // --- friends --- friendsList(): Promise; friendsIncoming(): Promise; /** Addressees the caller has already requested (pending or declined; cannot re-request), * plus the per-game disguised-robot requests that keep the in-game 🤝 disabled by seat. */ friendsOutgoing(): Promise; /** Send a friend request. A non-empty gameId marks an in-game request, so a disguised-robot * opponent is recorded as a per-game request rather than against the shared robot account. */ friendRequest(accountId: string, gameId?: string): Promise; friendRespond(requesterId: string, accept: boolean): Promise; friendCancel(accountId: string): Promise; unfriend(accountId: string): Promise; friendCodeIssue(): Promise; friendCodeRedeem(code: string): Promise; // --- blocks --- blocksList(): Promise; block(accountId: string, gameId?: string): Promise; unblock(accountId: string): Promise; // --- invitations --- invitationsList(): Promise; invitationCreate(inviteeIds: string[], settings: InvitationSettings): Promise; invitationAccept(invitationId: string): Promise; invitationDecline(invitationId: string): Promise; invitationCancel(invitationId: string): Promise; // --- profile / stats / history --- profileUpdate(p: ProfileUpdate): Promise; statsGet(): Promise; exportGcg(gameId: string): Promise; /** Mints a signed download URL for a finished game's artifact. dateLocale and the * localized non-play labels (pass/exchange/resign/timeout, in that order) ride the * URL so the server-rendered image matches the player's presentation. */ exportUrl(gameId: string, kind: 'png' | 'gcg', dateLocale: string, actionLabels: string[], timeZone: string): Promise; // --- account linking & merge --- linkEmailRequest(email: string): Promise; linkEmailConfirm(email: string, code: string): Promise; linkEmailMerge(email: string, code: string): Promise; linkTelegram(data: string): Promise; linkTelegramMerge(data: string): Promise; /** Detach a platform identity (kind 'telegram' | 'vk') from the account; email is never * unlinked. Returns the refreshed link result (status 'unlinked'). */ linkUnlink(kind: string): Promise; /** Change the account's confirmed email: mail a code to the new address, then confirm * to atomically switch (status 'changed'). A new address owned by another account is * refused without disclosure. */ changeEmailRequest(email: string): Promise; changeEmailConfirm(email: string, code: string): Promise; // --- live stream --- subscribe(onEvent: (e: PushEvent) => void, onError?: (err: unknown) => void): Unsubscribe; /** Set or clear the bearer token used for authenticated calls and the stream. */ setToken(token: string | null): void; } export type { GameView, Tile };