Stage 17 #2: Connecting indicator + auto-retry, instead of red toasts
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 36s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 55s
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 36s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 55s
Connectivity failures become state, not a toast on every attempt. A global online signal (lib/connection.svelte.ts) flips on a transport unavailable / rate_limited and on the live stream's drop, driving a pure-CSS header spinner + 'Connecting…' in place of the title and softly disabling the in-game server actions (commit / exchange / pass / hint; local board/rack/reset stay live). - transport: exec auto-retries with capped exponential backoff — every op on a rate-limit (rejected before processing, safe), reads only on unavailable (a mutation is never blindly re-sent, to avoid double-applying one whose response was lost; its button is disabled while offline so the player re-issues on reconnect). A reachability watcher (profile.get probe) and any successful traffic clear the signal. - the old red error.unavailable toast is gone (handleError suppresses connection codes; the indicator replaces it). A server-data screen still opens with the spinner and fills on reconnect (global indicator + read auto-retry), so navigation is never dead. - pure retry policy unit-tested (retry.ts); a mock-only window.__conn hook drives a Chromium+WebKit e2e (indicator shows offline, the action disables, both clear on reconnect). Full suite + build green. - docs: ARCHITECTURE transport note, FUNCTIONAL (+ _ru), PLAN tracker (incl. #1 — the bot already drains all updates, no change). Also records #1 as investigated/no-change in PLAN. Other server-action buttons (chat send, profile save, …) still degrade to a safe no-op offline; visual disable is easy to extend.
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { backoffMs, isConnectionCode, retryable } from './retry';
|
||||
|
||||
describe('retryable', () => {
|
||||
it('retries any op on a rate-limit rejection (it never reached the backend)', () => {
|
||||
expect(retryable('rate_limited', 'game.submit_play')).toBe(true);
|
||||
expect(retryable('rate_limited', 'games.list')).toBe(true);
|
||||
expect(retryable('rate_limited', 'chat.post')).toBe(true);
|
||||
});
|
||||
|
||||
it('retries only read-only ops on a transport unavailable (a mutation could double-apply)', () => {
|
||||
expect(retryable('unavailable', 'games.list')).toBe(true);
|
||||
expect(retryable('unavailable', 'game.state')).toBe(true);
|
||||
expect(retryable('unavailable', 'draft.get')).toBe(true);
|
||||
// mutations are not auto-resent on a dropped connection
|
||||
expect(retryable('unavailable', 'game.submit_play')).toBe(false);
|
||||
expect(retryable('unavailable', 'chat.post')).toBe(false);
|
||||
expect(retryable('unavailable', 'game.hide')).toBe(false);
|
||||
});
|
||||
|
||||
it('never retries a domain rejection or an unknown code', () => {
|
||||
expect(retryable('not_your_turn', 'game.submit_play')).toBe(false);
|
||||
expect(retryable('illegal_play', 'game.submit_play')).toBe(false);
|
||||
expect(retryable('not_found', 'game.state')).toBe(false);
|
||||
expect(retryable('internal', 'games.list')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isConnectionCode', () => {
|
||||
it('flags the transport/rate-limit codes the indicator covers', () => {
|
||||
expect(isConnectionCode('unavailable')).toBe(true);
|
||||
expect(isConnectionCode('rate_limited')).toBe(true);
|
||||
expect(isConnectionCode('not_your_turn')).toBe(false);
|
||||
expect(isConnectionCode('internal')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('backoffMs', () => {
|
||||
it('grows exponentially and caps at 8s (+ jitter under 250ms)', () => {
|
||||
// attempt 1 ~ 500, 2 ~ 1000, 3 ~ 2000, 4 ~ 4000, 5 ~ 8000, then capped.
|
||||
expect(backoffMs(1)).toBeGreaterThanOrEqual(500);
|
||||
expect(backoffMs(1)).toBeLessThan(750);
|
||||
expect(backoffMs(3)).toBeGreaterThanOrEqual(2000);
|
||||
expect(backoffMs(3)).toBeLessThan(2250);
|
||||
for (const n of [5, 6, 10, 20]) {
|
||||
expect(backoffMs(n)).toBeGreaterThanOrEqual(8000);
|
||||
expect(backoffMs(n)).toBeLessThan(8250);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user