/// import { getConfig } from '@scrabble-solver/configs'; import { BLANK } from '@scrabble-solver/constants'; import { solve } from '@scrabble-solver/solver'; import { Board, type Locale, Tile } from '@scrabble-solver/types'; import { type SolveRequestPayload, type VerifiedWord, type VerifyRequestPayload } from '@/types'; import { revalidateDictionary } from './dictionaries'; import { getGaddag } from './getGaddag'; import { type SolverWorkerRequest, type SolverWorkerResponse } from './messages'; declare const self: DedicatedWorkerGlobalScope; /** * Solves run synchronously on the single worker thread, so a burst of requests * would queue stale solves ahead of the latest one. */ let latestSolveId = 0; self.addEventListener('message', ({ data }: MessageEvent) => { if (data.type === 'solve') { latestSolveId = data.id; // eslint-disable-next-line @typescript-eslint/no-floating-promises respond(data.id, () => handleSolve(data.id, data.payload)); } else if (data.type === 'verify') { // eslint-disable-next-line @typescript-eslint/no-floating-promises respond(data.id, () => handleVerify(data.id, data.payload)); } else if (data.type === 'prefetch') { prefetchDictionary(data.locale).catch(() => undefined); } }); async function handleSolve(id: number, payload: SolveRequestPayload): Promise { const { board, characters, game, locale } = payload; const gaddag = await getGaddag(locale); revalidateInBackground(locale); if (!gaddag) { return { id, outcome: 'unavailable' }; } await yieldToQueuedMessages(); if (latestSolveId !== id) { return { id, outcome: 'superseded' }; } const config = getConfig(game, locale); const tiles = characters.map((character) => new Tile({ character, isBlank: character === BLANK })); return { data: solve(gaddag, config, Board.fromJson(board), tiles), id, outcome: 'answered' }; } async function handleVerify( id: number, { board: boardJson, locale }: VerifyRequestPayload, ): Promise { const gaddag = await getGaddag(locale); revalidateInBackground(locale); if (!gaddag) { return { id, outcome: 'unavailable' }; } const board = Board.fromJson(boardJson); const words = board.getWords().sort((a, b) => a.word.localeCompare(b.word, locale)); const invalidWords: VerifiedWord[] = []; const validWords: VerifiedWord[] = []; for (const word of words) { if (gaddag.has(word.word)) { validWords.push({ ...word, isValid: true }); } else { invalidWords.push({ ...word, isValid: false }); } } return { data: { invalidWords, validWords }, id, outcome: 'answered' }; } /** * Deserializes the dictionary ahead of the first solve, so it does not pay the * cold-start cost. Warms even when revalidation fails (e.g. offline) - a * previously cached dictionary can still be deserialized. */ async function prefetchDictionary(locale: Locale): Promise { try { await revalidateDictionary(locale); } catch { // Solving falls back to the server until a later revalidation succeeds. } await getGaddag(locale); } /** * Revalidation failures are expected (offline, server down) and must not * surface as unhandled rejections - solving keeps using the cached dictionary * until a later revalidation succeeds. */ function revalidateInBackground(locale: Locale): void { revalidateDictionary(locale).catch(() => undefined); } async function respond(id: number, handle: () => Promise): Promise { try { self.postMessage(await handle()); } catch { self.postMessage({ id, outcome: 'unavailable' } satisfies SolverWorkerResponse); } } /** * Messages queued behind this handler dispatch only between macrotasks - a * MessageChannel round-trip lets them register before an expensive solve. */ function yieldToQueuedMessages(): Promise { return new Promise((resolve) => { const { port1, port2 } = new MessageChannel(); port1.onmessage = () => resolve(); port2.postMessage(null); }); }