feat: on-device move preview (local eval) with network fallback
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 58s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m17s
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 58s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m17s
Score and validate a tentative move on-device instead of a per-arrangement network
round trip. The dawg reader and the validate/score/direction slice of the
scrabble-solver engine are ported to TypeScript (ui/src/lib/dict), pinned
byte-for-byte to the Go engine by a `conformance` CI job (full-dictionary reader
parity plus a battery of plays across every variant and both cross-word rules,
including the inferred orientation). The server stays authoritative — submit_play
re-validates — so the local result is an advisory accelerator only.
- backend: Registry.DictBytes + an authed GET /api/v1/user/dict/{variant}/{version}
(immutable) streaming the pinned per-game dawg; caddy routes /dict to the gateway.
- gateway: a session-gated /dict edge route proxying it; fetchDict on the transport.
- client: the dictionary loads on game open (low priority so it never starves the
game on a slow link; aborted at a 5s cap or when leaving the game), is cached in
IndexedDB (best-effort, self-healing on a rejected blob) and reused across
sessions; a warm-up overlay covers a cold load, then the network preview is the
fallback; a bad-connection breaker stops warming after repeated misses; the move
preview cancels its in-flight request when the tiles change.
- parity generators backend/cmd/{dictgen,validategen} + gated Vitest suites, run in
CI against the release dictionaries. A hidden debug readout lists the cached
dictionaries + breaker state, and its reset clears the cache.
- docs: ARCHITECTURE §5, TESTING, UI_DESIGN, FUNCTIONAL (+ru).
This commit is contained in:
@@ -125,6 +125,34 @@ func (c *Client) do(ctx context.Context, method, path, userID, clientIP string,
|
||||
return nil
|
||||
}
|
||||
|
||||
// getRaw performs one REST GET, returning the raw (un-decoded) response body and
|
||||
// the value of respHeader (e.g. Cache-Control). userID, when non-empty, is
|
||||
// forwarded as X-User-ID. A non-2xx response is returned as an *APIError. Unlike
|
||||
// do it does not JSON-decode, so it carries a binary payload such as a dictionary
|
||||
// blob.
|
||||
func (c *Client) getRaw(ctx context.Context, path, userID, respHeader string) ([]byte, string, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+path, nil)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("backendclient: new request: %w", err)
|
||||
}
|
||||
if userID != "" {
|
||||
req.Header.Set("X-User-ID", userID)
|
||||
}
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("backendclient: GET %s: %w", path, err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("backendclient: read response: %w", err)
|
||||
}
|
||||
if resp.StatusCode >= http.StatusMultipleChoices {
|
||||
return nil, "", parseAPIError(resp.StatusCode, data)
|
||||
}
|
||||
return data, resp.Header.Get(respHeader), nil
|
||||
}
|
||||
|
||||
// parseAPIError extracts the backend's {error:{code,message}} envelope.
|
||||
func parseAPIError(status int, data []byte) *APIError {
|
||||
var env struct {
|
||||
|
||||
Reference in New Issue
Block a user