d67e582c03
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 1m4s
CI / conformance (pull_request) Successful in 11s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m39s
The edge caddy 503 carries X-Scrabble-Maintenance during a deploy window, but a user already in the running SPA only sees their calls start failing — the static caddy page catches only a fresh load. Detect that marker (strictly — not any transient 'unavailable', which the Connecting indicator already covers) on the raw ConnectError at the two transport catch sites (unary exec + the live subscribe stream), before toGatewayError discards the response headers, and raise a non-dismissable dimmed overlay that mirrors the static caddy page. It self-clears: a capped-backoff poll (a cheap read, mirroring connection.svelte.ts) lifts it on the first success, and a manual "retry" button forces an immediate re-check — so it can never get stuck. On detection the read retry loop fails fast, so a window doesn't burn the retry budget on every call. - pure detector maintenance.ts (maintenanceRetryMs / parseRetryAfterMs) + unit tests; the store + self-clearing poll in maintenance.svelte.ts (mirrors connection.svelte.ts) - MaintenanceOverlay.svelte (clones Splash's fixed/inset/dimmed shell, non-dismissable), mounted app-global in App.svelte after Coachmark - transport.ts detects + reports at both catch sites, clears on any successful read - i18n RU "Технические работы" / EN "Under maintenance"; a window.__maint mock hook (the mock can't emit a real 503) + a Playwright spec Prod is same-origin (VITE_GATEWAY_URL empty) so the marker header is readable without a CORS expose-header. Verified: pnpm check (0), unit (402), build, e2e (186, Chromium+WebKit).
38 lines
1.8 KiB
TypeScript
38 lines
1.8 KiB
TypeScript
// Detection of a planned edge maintenance window. During a prod deploy roll the edge
|
|
// caddy returns HTTP 503 with an `X-Scrabble-Maintenance: 1` header (and a `Retry-After`)
|
|
// for every gated route. The SPA keys STRICTLY on that marker — not on any transport
|
|
// `unavailable`, which the "Connecting…" indicator already covers — so a planned window
|
|
// raises the maintenance overlay while an ordinary blip stays a soft reconnect.
|
|
//
|
|
// These helpers are pure (no DOM, no runes) so they unit-test in the node vitest env. The
|
|
// maintenance marker + Retry-After ride on the raw ConnectError's `metadata`, which
|
|
// toGatewayError (retry.ts) discards — so detection must run on the raw error.
|
|
|
|
import { ConnectError } from '@connectrpc/connect';
|
|
|
|
const DEFAULT_RETRY_MS = 15_000;
|
|
const MIN_RETRY_MS = 3_000;
|
|
const MAX_RETRY_MS = 120_000;
|
|
|
|
/**
|
|
* parseRetryAfterMs converts a `Retry-After` header (delta-seconds — the edge emits
|
|
* seconds, never an HTTP-date) into a millisecond hint clamped to a sane band. An absent
|
|
* or unparseable value falls back to a default.
|
|
*/
|
|
export function parseRetryAfterMs(header: string | null | undefined): number {
|
|
const secs = header == null ? NaN : Number(header);
|
|
if (!Number.isFinite(secs) || secs <= 0) return DEFAULT_RETRY_MS;
|
|
return Math.min(MAX_RETRY_MS, Math.max(MIN_RETRY_MS, Math.round(secs * 1000)));
|
|
}
|
|
|
|
/**
|
|
* maintenanceRetryMs returns the maintenance `Retry-After` hint in milliseconds when the
|
|
* thrown transport error is the edge maintenance 503 (carries `X-Scrabble-Maintenance: 1`),
|
|
* or null for any other error. Header lookup is case-insensitive (Headers.get).
|
|
*/
|
|
export function maintenanceRetryMs(e: unknown): number | null {
|
|
if (!(e instanceof ConnectError)) return null;
|
|
if (e.metadata?.get('x-scrabble-maintenance') !== '1') return null;
|
|
return parseRetryAfterMs(e.metadata.get('retry-after'));
|
|
}
|