Stage 7 (wip): wire remaining ops (backend REST, FBS, gateway transcode) + real UI transport
backend: REST handlers for pass/exchange/resign/hint/evaluate/check_word/complaint/history/chat-list/nudge + new game.ListForAccount (my games) + seat display_name resolution pkg/fbs: GameActionRequest/ExchangeRequest/EvalRequest/EvalResult/CheckWordRequest/WordCheckResult/ComplaintRequest/HintResult/History/GameList/ChatList + SeatView.display_name; committed Go regenerated (flatc 23.5.26) gateway: 11 new transcode ops + backendclient methods + FB encoders ui: edge TS codegen (flatc --ts + protoc-gen-es, committed), FlatBuffers<->model codec, real connect-web transport (binary, bearer auth, Subscribe). prod bundle ~69KB gzip JS
This commit is contained in:
@@ -0,0 +1,384 @@
|
||||
// FlatBuffers <-> domain-model codec. The real transport encodes each request table
|
||||
// and decodes each response/event table here, mirroring the gateway's Go encoders in
|
||||
// reverse. The screens only ever see the plain model (lib/model), never these wire
|
||||
// types.
|
||||
|
||||
import { Builder, ByteBuffer, type Offset } from 'flatbuffers';
|
||||
import * as fb from '../gen/fbs/scrabblefb';
|
||||
import type { PlacedTile } from './client';
|
||||
import type {
|
||||
ChatMessage,
|
||||
EvalResult,
|
||||
GameList,
|
||||
GameView,
|
||||
History,
|
||||
HintResult,
|
||||
MatchResult,
|
||||
MoveRecord,
|
||||
MoveResult,
|
||||
Profile,
|
||||
PushEvent,
|
||||
Seat,
|
||||
Session,
|
||||
StateView,
|
||||
Tile,
|
||||
Variant,
|
||||
WordCheckResult,
|
||||
} from './model';
|
||||
|
||||
// --- request encoders ---
|
||||
|
||||
function buildTile(b: Builder, t: PlacedTile): Offset {
|
||||
const letter = b.createString(t.letter);
|
||||
fb.TileRecord.startTileRecord(b);
|
||||
fb.TileRecord.addRow(b, t.row);
|
||||
fb.TileRecord.addCol(b, t.col);
|
||||
fb.TileRecord.addLetter(b, letter);
|
||||
fb.TileRecord.addBlank(b, t.blank);
|
||||
return fb.TileRecord.endTileRecord(b);
|
||||
}
|
||||
|
||||
function finish(b: Builder, root: Offset): Uint8Array {
|
||||
b.finish(root);
|
||||
return b.asUint8Array();
|
||||
}
|
||||
|
||||
export const empty = (): Uint8Array => new Uint8Array();
|
||||
|
||||
export function encodeGameAction(gameId: string): Uint8Array {
|
||||
const b = new Builder(64);
|
||||
const gid = b.createString(gameId);
|
||||
fb.GameActionRequest.startGameActionRequest(b);
|
||||
fb.GameActionRequest.addGameId(b, gid);
|
||||
return finish(b, fb.GameActionRequest.endGameActionRequest(b));
|
||||
}
|
||||
|
||||
export function encodeStateRequest(gameId: string): Uint8Array {
|
||||
const b = new Builder(64);
|
||||
const gid = b.createString(gameId);
|
||||
fb.StateRequest.startStateRequest(b);
|
||||
fb.StateRequest.addGameId(b, gid);
|
||||
return finish(b, fb.StateRequest.endStateRequest(b));
|
||||
}
|
||||
|
||||
export function encodeSubmitPlay(gameId: string, dir: 'H' | 'V', tiles: PlacedTile[]): Uint8Array {
|
||||
const b = new Builder(256);
|
||||
const tileOffs = tiles.map((t) => buildTile(b, t));
|
||||
const vec = fb.SubmitPlayRequest.createTilesVector(b, tileOffs);
|
||||
const gid = b.createString(gameId);
|
||||
const d = b.createString(dir);
|
||||
fb.SubmitPlayRequest.startSubmitPlayRequest(b);
|
||||
fb.SubmitPlayRequest.addGameId(b, gid);
|
||||
fb.SubmitPlayRequest.addDir(b, d);
|
||||
fb.SubmitPlayRequest.addTiles(b, vec);
|
||||
return finish(b, fb.SubmitPlayRequest.endSubmitPlayRequest(b));
|
||||
}
|
||||
|
||||
export function encodeEval(gameId: string, dir: 'H' | 'V', tiles: PlacedTile[]): Uint8Array {
|
||||
const b = new Builder(256);
|
||||
const tileOffs = tiles.map((t) => buildTile(b, t));
|
||||
const vec = fb.EvalRequest.createTilesVector(b, tileOffs);
|
||||
const gid = b.createString(gameId);
|
||||
const d = b.createString(dir);
|
||||
fb.EvalRequest.startEvalRequest(b);
|
||||
fb.EvalRequest.addGameId(b, gid);
|
||||
fb.EvalRequest.addDir(b, d);
|
||||
fb.EvalRequest.addTiles(b, vec);
|
||||
return finish(b, fb.EvalRequest.endEvalRequest(b));
|
||||
}
|
||||
|
||||
export function encodeExchange(gameId: string, tiles: string[]): Uint8Array {
|
||||
const b = new Builder(128);
|
||||
const offs = tiles.map((s) => b.createString(s));
|
||||
const vec = fb.ExchangeRequest.createTilesVector(b, offs);
|
||||
const gid = b.createString(gameId);
|
||||
fb.ExchangeRequest.startExchangeRequest(b);
|
||||
fb.ExchangeRequest.addGameId(b, gid);
|
||||
fb.ExchangeRequest.addTiles(b, vec);
|
||||
return finish(b, fb.ExchangeRequest.endExchangeRequest(b));
|
||||
}
|
||||
|
||||
export function encodeCheckWord(gameId: string, word: string): Uint8Array {
|
||||
const b = new Builder(128);
|
||||
const gid = b.createString(gameId);
|
||||
const w = b.createString(word);
|
||||
fb.CheckWordRequest.startCheckWordRequest(b);
|
||||
fb.CheckWordRequest.addGameId(b, gid);
|
||||
fb.CheckWordRequest.addWord(b, w);
|
||||
return finish(b, fb.CheckWordRequest.endCheckWordRequest(b));
|
||||
}
|
||||
|
||||
export function encodeComplaint(gameId: string, word: string, note: string): Uint8Array {
|
||||
const b = new Builder(256);
|
||||
const gid = b.createString(gameId);
|
||||
const w = b.createString(word);
|
||||
const n = b.createString(note);
|
||||
fb.ComplaintRequest.startComplaintRequest(b);
|
||||
fb.ComplaintRequest.addGameId(b, gid);
|
||||
fb.ComplaintRequest.addWord(b, w);
|
||||
fb.ComplaintRequest.addNote(b, n);
|
||||
return finish(b, fb.ComplaintRequest.endComplaintRequest(b));
|
||||
}
|
||||
|
||||
export function encodeEnqueue(variant: Variant): Uint8Array {
|
||||
const b = new Builder(64);
|
||||
const v = b.createString(variant);
|
||||
fb.EnqueueRequest.startEnqueueRequest(b);
|
||||
fb.EnqueueRequest.addVariant(b, v);
|
||||
return finish(b, fb.EnqueueRequest.endEnqueueRequest(b));
|
||||
}
|
||||
|
||||
export function encodeChatPost(gameId: string, body: string): Uint8Array {
|
||||
const b = new Builder(128);
|
||||
const gid = b.createString(gameId);
|
||||
const bd = b.createString(body);
|
||||
fb.ChatPostRequest.startChatPostRequest(b);
|
||||
fb.ChatPostRequest.addGameId(b, gid);
|
||||
fb.ChatPostRequest.addBody(b, bd);
|
||||
return finish(b, fb.ChatPostRequest.endChatPostRequest(b));
|
||||
}
|
||||
|
||||
export function encodeGuestLogin(locale: string): Uint8Array {
|
||||
const b = new Builder(64);
|
||||
const l = b.createString(locale);
|
||||
fb.GuestLoginRequest.startGuestLoginRequest(b);
|
||||
fb.GuestLoginRequest.addLocale(b, l);
|
||||
return finish(b, fb.GuestLoginRequest.endGuestLoginRequest(b));
|
||||
}
|
||||
|
||||
export function encodeEmailRequest(email: string): Uint8Array {
|
||||
const b = new Builder(128);
|
||||
const e = b.createString(email);
|
||||
fb.EmailRequestRequest.startEmailRequestRequest(b);
|
||||
fb.EmailRequestRequest.addEmail(b, e);
|
||||
return finish(b, fb.EmailRequestRequest.endEmailRequestRequest(b));
|
||||
}
|
||||
|
||||
export function encodeEmailLogin(email: string, code: string): Uint8Array {
|
||||
const b = new Builder(128);
|
||||
const e = b.createString(email);
|
||||
const c = b.createString(code);
|
||||
fb.EmailLoginRequest.startEmailLoginRequest(b);
|
||||
fb.EmailLoginRequest.addEmail(b, e);
|
||||
fb.EmailLoginRequest.addCode(b, c);
|
||||
return finish(b, fb.EmailLoginRequest.endEmailLoginRequest(b));
|
||||
}
|
||||
|
||||
// --- response decoders ---
|
||||
|
||||
function s(v: string | null): string {
|
||||
return v ?? '';
|
||||
}
|
||||
|
||||
function decodeTile(t: fb.TileRecord): Tile {
|
||||
return { row: t.row(), col: t.col(), letter: s(t.letter()), blank: t.blank() };
|
||||
}
|
||||
|
||||
function decodeSeat(v: fb.SeatView): Seat {
|
||||
return {
|
||||
seat: v.seat(),
|
||||
accountId: s(v.accountId()),
|
||||
displayName: s(v.displayName()),
|
||||
score: v.score(),
|
||||
hintsUsed: v.hintsUsed(),
|
||||
isWinner: v.isWinner(),
|
||||
};
|
||||
}
|
||||
|
||||
function decodeGameView(g: fb.GameView): GameView {
|
||||
const seats: Seat[] = [];
|
||||
for (let i = 0; i < g.seatsLength(); i++) {
|
||||
const sv = g.seats(i);
|
||||
if (sv) seats.push(decodeSeat(sv));
|
||||
}
|
||||
return {
|
||||
id: s(g.id()),
|
||||
variant: s(g.variant()) as Variant,
|
||||
dictVersion: s(g.dictVersion()),
|
||||
status: s(g.status()),
|
||||
players: g.players(),
|
||||
toMove: g.toMove(),
|
||||
turnTimeoutSecs: g.turnTimeoutSecs(),
|
||||
moveCount: g.moveCount(),
|
||||
endReason: s(g.endReason()),
|
||||
seats,
|
||||
};
|
||||
}
|
||||
|
||||
function decodeMove(m: fb.MoveRecord): MoveRecord {
|
||||
const tiles: Tile[] = [];
|
||||
for (let i = 0; i < m.tilesLength(); i++) {
|
||||
const t = m.tiles(i);
|
||||
if (t) tiles.push(decodeTile(t));
|
||||
}
|
||||
const words: string[] = [];
|
||||
for (let i = 0; i < m.wordsLength(); i++) words.push(s(m.words(i)));
|
||||
return {
|
||||
player: m.player(),
|
||||
action: s(m.action()),
|
||||
dir: s(m.dir()),
|
||||
mainRow: m.mainRow(),
|
||||
mainCol: m.mainCol(),
|
||||
tiles,
|
||||
words,
|
||||
count: m.count(),
|
||||
score: m.score(),
|
||||
total: m.total(),
|
||||
};
|
||||
}
|
||||
|
||||
function decodeChatMsg(m: fb.ChatMessage): ChatMessage {
|
||||
return {
|
||||
id: s(m.id()),
|
||||
gameId: s(m.gameId()),
|
||||
senderId: s(m.senderId()),
|
||||
kind: s(m.kind()),
|
||||
body: s(m.body()),
|
||||
createdAtUnix: Number(m.createdAtUnix()),
|
||||
};
|
||||
}
|
||||
|
||||
export function decodeSession(buf: Uint8Array): Session {
|
||||
const t = fb.Session.getRootAsSession(new ByteBuffer(buf));
|
||||
return { token: s(t.token()), userId: s(t.userId()), isGuest: t.isGuest(), displayName: s(t.displayName()) };
|
||||
}
|
||||
|
||||
export function decodeProfile(buf: Uint8Array): Profile {
|
||||
const p = fb.Profile.getRootAsProfile(new ByteBuffer(buf));
|
||||
return {
|
||||
userId: s(p.userId()),
|
||||
displayName: s(p.displayName()),
|
||||
preferredLanguage: s(p.preferredLanguage()),
|
||||
timeZone: s(p.timeZone()),
|
||||
hintBalance: p.hintBalance(),
|
||||
blockChat: p.blockChat(),
|
||||
blockFriendRequests: p.blockFriendRequests(),
|
||||
isGuest: p.isGuest(),
|
||||
};
|
||||
}
|
||||
|
||||
export function decodeStateView(buf: Uint8Array): StateView {
|
||||
const v = fb.StateView.getRootAsStateView(new ByteBuffer(buf));
|
||||
const g = v.game();
|
||||
const rack: string[] = [];
|
||||
for (let i = 0; i < v.rackLength(); i++) rack.push(s(v.rack(i)));
|
||||
return {
|
||||
game: g ? decodeGameView(g) : emptyGame(),
|
||||
seat: v.seat(),
|
||||
rack,
|
||||
bagLen: v.bagLen(),
|
||||
hintsRemaining: v.hintsRemaining(),
|
||||
};
|
||||
}
|
||||
|
||||
export function decodeMoveResult(buf: Uint8Array): MoveResult {
|
||||
const r = fb.MoveResult.getRootAsMoveResult(new ByteBuffer(buf));
|
||||
const m = r.move();
|
||||
const g = r.game();
|
||||
return { move: m ? decodeMove(m) : emptyMove(), game: g ? decodeGameView(g) : emptyGame() };
|
||||
}
|
||||
|
||||
export function decodeHintResult(buf: Uint8Array): HintResult {
|
||||
const r = fb.HintResult.getRootAsHintResult(new ByteBuffer(buf));
|
||||
const m = r.move();
|
||||
return { move: m ? decodeMove(m) : emptyMove(), hintsRemaining: r.hintsRemaining() };
|
||||
}
|
||||
|
||||
export function decodeEvalResult(buf: Uint8Array): EvalResult {
|
||||
const r = fb.EvalResult.getRootAsEvalResult(new ByteBuffer(buf));
|
||||
const words: string[] = [];
|
||||
for (let i = 0; i < r.wordsLength(); i++) words.push(s(r.words(i)));
|
||||
return { legal: r.legal(), score: r.score(), words };
|
||||
}
|
||||
|
||||
export function decodeWordCheck(buf: Uint8Array): WordCheckResult {
|
||||
const r = fb.WordCheckResult.getRootAsWordCheckResult(new ByteBuffer(buf));
|
||||
return { word: s(r.word()), legal: r.legal() };
|
||||
}
|
||||
|
||||
export function decodeHistory(buf: Uint8Array): History {
|
||||
const h = fb.History.getRootAsHistory(new ByteBuffer(buf));
|
||||
const moves: MoveRecord[] = [];
|
||||
for (let i = 0; i < h.movesLength(); i++) {
|
||||
const m = h.moves(i);
|
||||
if (m) moves.push(decodeMove(m));
|
||||
}
|
||||
return { gameId: s(h.gameId()), moves };
|
||||
}
|
||||
|
||||
export function decodeGameList(buf: Uint8Array): GameList {
|
||||
const gl = fb.GameList.getRootAsGameList(new ByteBuffer(buf));
|
||||
const games: GameView[] = [];
|
||||
for (let i = 0; i < gl.gamesLength(); i++) {
|
||||
const g = gl.games(i);
|
||||
if (g) games.push(decodeGameView(g));
|
||||
}
|
||||
return { games };
|
||||
}
|
||||
|
||||
export function decodeMatchResult(buf: Uint8Array): MatchResult {
|
||||
const m = fb.MatchResult.getRootAsMatchResult(new ByteBuffer(buf));
|
||||
const g = m.game();
|
||||
return { matched: m.matched(), game: m.matched() && g ? decodeGameView(g) : undefined };
|
||||
}
|
||||
|
||||
export function decodeChatMessage(buf: Uint8Array): ChatMessage {
|
||||
return decodeChatMsg(fb.ChatMessage.getRootAsChatMessage(new ByteBuffer(buf)));
|
||||
}
|
||||
|
||||
export function decodeChatList(buf: Uint8Array): ChatMessage[] {
|
||||
const cl = fb.ChatList.getRootAsChatList(new ByteBuffer(buf));
|
||||
const out: ChatMessage[] = [];
|
||||
for (let i = 0; i < cl.messagesLength(); i++) {
|
||||
const m = cl.messages(i);
|
||||
if (m) out.push(decodeChatMsg(m));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function decodeEvent(kind: string, payload: Uint8Array): PushEvent | null {
|
||||
const bb = new ByteBuffer(payload);
|
||||
switch (kind) {
|
||||
case 'your_turn': {
|
||||
const e = fb.YourTurnEvent.getRootAsYourTurnEvent(bb);
|
||||
return { kind: 'your_turn', gameId: s(e.gameId()), deadlineUnix: Number(e.deadlineUnix()) };
|
||||
}
|
||||
case 'opponent_moved': {
|
||||
const e = fb.OpponentMovedEvent.getRootAsOpponentMovedEvent(bb);
|
||||
return { kind: 'opponent_moved', gameId: s(e.gameId()), seat: e.seat(), action: s(e.action()), score: e.score(), total: e.total() };
|
||||
}
|
||||
case 'chat_message':
|
||||
return { kind: 'chat_message', message: decodeChatMsg(fb.ChatMessage.getRootAsChatMessage(bb)) };
|
||||
case 'nudge': {
|
||||
const e = fb.NudgeEvent.getRootAsNudgeEvent(bb);
|
||||
return { kind: 'nudge', gameId: s(e.gameId()), fromUserId: s(e.fromUserId()) };
|
||||
}
|
||||
case 'match_found': {
|
||||
const e = fb.MatchFoundEvent.getRootAsMatchFoundEvent(bb);
|
||||
return { kind: 'match_found', gameId: s(e.gameId()) };
|
||||
}
|
||||
case 'heartbeat':
|
||||
return { kind: 'heartbeat' };
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function emptyGame(): GameView {
|
||||
return {
|
||||
id: '',
|
||||
variant: 'english',
|
||||
dictVersion: '',
|
||||
status: '',
|
||||
players: 0,
|
||||
toMove: 0,
|
||||
turnTimeoutSecs: 0,
|
||||
moveCount: 0,
|
||||
endReason: '',
|
||||
seats: [],
|
||||
};
|
||||
}
|
||||
|
||||
function emptyMove(): MoveRecord {
|
||||
return { player: 0, action: '', dir: '', mainRow: 0, mainCol: 0, tiles: [], words: [], count: 0, score: 0, total: 0 };
|
||||
}
|
||||
+132
-31
@@ -1,36 +1,137 @@
|
||||
// Placeholder for the real Connect-web + FlatBuffers transport, wired in the edge
|
||||
// codegen task. Until then, selecting a non-mock mode surfaces a clear error instead
|
||||
// of failing silently. The mock (lib/mock) backs `pnpm start`.
|
||||
// The real Connect-RPC + FlatBuffers transport. Every unary op rides the single
|
||||
// Execute envelope (message_type + FlatBuffers payload); the live stream is
|
||||
// Subscribe. The session token rides in the Authorization header; domain outcomes
|
||||
// come back in result_code, edge failures as Connect error codes — both normalised to
|
||||
// a thrown GatewayError. In dev the Vite proxy forwards the RPC path to the h2c
|
||||
// gateway; in a packaged app VITE_GATEWAY_URL points at the real origin.
|
||||
|
||||
import type { GatewayClient } from './client';
|
||||
import { GatewayError } from './client';
|
||||
import { Code, ConnectError, createClient } from '@connectrpc/connect';
|
||||
import { createConnectTransport } from '@connectrpc/connect-web';
|
||||
import { Gateway } from '../gen/edge/v1/edge_pb';
|
||||
import { GatewayError, type GatewayClient } from './client';
|
||||
import * as codec from './codec';
|
||||
|
||||
function toGatewayError(e: unknown): GatewayError {
|
||||
if (e instanceof ConnectError) {
|
||||
switch (e.code) {
|
||||
case Code.Unauthenticated:
|
||||
return new GatewayError('session_invalid', e.message);
|
||||
case Code.ResourceExhausted:
|
||||
return new GatewayError('rate_limited', e.message);
|
||||
case Code.Unavailable:
|
||||
return new GatewayError('unavailable', e.message);
|
||||
case Code.NotFound:
|
||||
return new GatewayError('not_found', e.message);
|
||||
default:
|
||||
return new GatewayError('internal', e.message);
|
||||
}
|
||||
}
|
||||
return new GatewayError('unavailable', String(e));
|
||||
}
|
||||
|
||||
export function createTransport(baseUrl: string): GatewayClient {
|
||||
const origin = baseUrl || (typeof location !== 'undefined' ? location.origin : '');
|
||||
const transport = createConnectTransport({ baseUrl: origin, useBinaryFormat: true });
|
||||
const client = createClient(Gateway, transport);
|
||||
let token: string | null = null;
|
||||
|
||||
const headers = (): Record<string, string> | undefined =>
|
||||
token ? { authorization: `Bearer ${token}` } : undefined;
|
||||
|
||||
async function exec(messageType: string, payload: Uint8Array): Promise<Uint8Array> {
|
||||
let res;
|
||||
try {
|
||||
res = await client.execute({ messageType, payload, requestId: '' }, { headers: headers() });
|
||||
} catch (e) {
|
||||
throw toGatewayError(e);
|
||||
}
|
||||
if (res.resultCode && res.resultCode !== 'ok') throw new GatewayError(res.resultCode);
|
||||
return res.payload;
|
||||
}
|
||||
|
||||
export function createTransport(_baseUrl: string): GatewayClient {
|
||||
const ni = (): never => {
|
||||
throw new GatewayError('unavailable', 'real transport not wired yet');
|
||||
};
|
||||
return {
|
||||
setToken: () => {},
|
||||
authGuest: ni,
|
||||
authEmailRequest: ni,
|
||||
authEmailLogin: ni,
|
||||
profileGet: ni,
|
||||
gamesList: ni,
|
||||
lobbyEnqueue: ni,
|
||||
lobbyPoll: ni,
|
||||
gameState: ni,
|
||||
gameHistory: ni,
|
||||
submitPlay: ni,
|
||||
pass: ni,
|
||||
exchange: ni,
|
||||
resign: ni,
|
||||
hint: ni,
|
||||
evaluate: ni,
|
||||
checkWord: ni,
|
||||
complaint: ni,
|
||||
chatPost: ni,
|
||||
chatList: ni,
|
||||
nudge: ni,
|
||||
subscribe: ni,
|
||||
setToken(t) {
|
||||
token = t;
|
||||
},
|
||||
|
||||
async authGuest(locale) {
|
||||
return codec.decodeSession(await exec('auth.guest', codec.encodeGuestLogin(locale ?? '')));
|
||||
},
|
||||
async authEmailRequest(email) {
|
||||
await exec('auth.email.request', codec.encodeEmailRequest(email));
|
||||
},
|
||||
async authEmailLogin(email, code) {
|
||||
return codec.decodeSession(await exec('auth.email.login', codec.encodeEmailLogin(email, code)));
|
||||
},
|
||||
|
||||
async profileGet() {
|
||||
return codec.decodeProfile(await exec('profile.get', codec.empty()));
|
||||
},
|
||||
async gamesList() {
|
||||
return codec.decodeGameList(await exec('games.list', codec.empty()));
|
||||
},
|
||||
|
||||
async lobbyEnqueue(variant) {
|
||||
return codec.decodeMatchResult(await exec('lobby.enqueue', codec.encodeEnqueue(variant)));
|
||||
},
|
||||
async lobbyPoll() {
|
||||
return codec.decodeMatchResult(await exec('lobby.poll', codec.empty()));
|
||||
},
|
||||
|
||||
async gameState(id) {
|
||||
return codec.decodeStateView(await exec('game.state', codec.encodeStateRequest(id)));
|
||||
},
|
||||
async gameHistory(id) {
|
||||
return codec.decodeHistory(await exec('game.history', codec.encodeGameAction(id)));
|
||||
},
|
||||
async submitPlay(id, dir, tiles) {
|
||||
return codec.decodeMoveResult(await exec('game.submit_play', codec.encodeSubmitPlay(id, dir, tiles)));
|
||||
},
|
||||
async pass(id) {
|
||||
return codec.decodeMoveResult(await exec('game.pass', codec.encodeGameAction(id)));
|
||||
},
|
||||
async exchange(id, tiles) {
|
||||
return codec.decodeMoveResult(await exec('game.exchange', codec.encodeExchange(id, tiles)));
|
||||
},
|
||||
async resign(id) {
|
||||
return codec.decodeMoveResult(await exec('game.resign', codec.encodeGameAction(id)));
|
||||
},
|
||||
async hint(id) {
|
||||
return codec.decodeHintResult(await exec('game.hint', codec.encodeGameAction(id)));
|
||||
},
|
||||
async evaluate(id, dir, tiles) {
|
||||
return codec.decodeEvalResult(await exec('game.evaluate', codec.encodeEval(id, dir, tiles)));
|
||||
},
|
||||
async checkWord(id, word) {
|
||||
return codec.decodeWordCheck(await exec('game.check_word', codec.encodeCheckWord(id, word)));
|
||||
},
|
||||
async complaint(id, word, note) {
|
||||
await exec('game.complaint', codec.encodeComplaint(id, word, note));
|
||||
},
|
||||
|
||||
async chatPost(id, body) {
|
||||
return codec.decodeChatMessage(await exec('chat.post', codec.encodeChatPost(id, body)));
|
||||
},
|
||||
async chatList(id) {
|
||||
return codec.decodeChatList(await exec('chat.list', codec.encodeGameAction(id)));
|
||||
},
|
||||
async nudge(id) {
|
||||
return codec.decodeChatMessage(await exec('chat.nudge', codec.encodeGameAction(id)));
|
||||
},
|
||||
|
||||
subscribe(onEvent, onError) {
|
||||
const ctrl = new AbortController();
|
||||
void (async () => {
|
||||
try {
|
||||
for await (const ev of client.subscribe({}, { headers: headers(), signal: ctrl.signal })) {
|
||||
const pe = codec.decodeEvent(ev.kind, ev.payload);
|
||||
if (pe) onEvent(pe);
|
||||
}
|
||||
} catch (e) {
|
||||
if (!ctrl.signal.aborted) onError?.(toGatewayError(e));
|
||||
}
|
||||
})();
|
||||
return () => ctrl.abort();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user