|
|
|
|
import { parseRetryAfter } from '$libs/http';
|
|
|
|
|
import { HTTP_TIMEOUT_SCOPE_TOTAL } from './consts.ts';
|
Add arts/timr, arts/sess, arts/http; sess actor extension; aapp factory
New artifacts:
arts/timr — runtime timer scheduler (singleton-per-App, never global).
Engine + Active split. Race-safe via (id, key, version) guard against
stale native callbacks, async tasks resolving after cancel/replace,
interval ticks scheduled after dispose, and ack-style timeouts. Per-
entry AbortController; intervals reuse the signal across ticks.
awaitTask:true (default, no overlap) vs awaitTask:false (fire-and-
forget cadence; failures don't stop the interval — semantic frozen).
Recursive setTimeout for intervals. scheduleAt(past) → delay 0 (no
error). Fake-clock injectable for deterministic tests. Pure
computeBackoffDelay helper. 50 server tests + 7 browser tests + full
README + DESIGN_TIMR + interactive test page at /test/timr.
arts/sess — session lifecycle with three generics
(TUser/TCredential/TData) plus optional SessionActor metadata
(kind/source/confidence — orthogonal axis to identity).
Tagged AdoptResult/RefreshResult/RevokeResult; never void.
Generation guard against stale refresh from local cancel, cross-tab
storage events, or re-adopt. onRefresh contract (null=fatal,
throw=transient). onRevoke replaces revokeUrl (consumer controls
fetch; engine just gets boolean). Default scope: 'global' when
onRevoke configured. INITIAL_SESSION sync dispatch on subscribe.
BroadcastChannel payload: {type, event, generation} only — never
tokens. SSR cookie reader validates invariants. withAutoRefresh
helper, 401-retry hook with applyAuth + loop guard, JWT exp helper.
103 tests (60 server + 5 browser + 38 actor/error/etc) + README +
DESIGN.md + test page at /test/sess.
arts/http — HTTP client with Standard Schema body validation, retry
+ Retry-After, attempt + total timeout via AbortSignal composition,
hooks (beforeRequest/beforeRetry/afterResponse/beforeError),
tagged HttpResult. Test page at /test/http.
arts/conn — DESIGN_CONN.md only (no implementation yet).
aapp:
- App.createActiveSession<TUser, TCredential, TData>(opts) factory
auto-injects App.Logger; throws SessAlreadyCreatedError on second
call. App.Sess getter exposes the active session (undefined until
first call). Auto-disposed by App.dispose().
- App.Timers integration deferred to timr Fase 3.
libs/days:
- toEpochMs(value) — permissive coercion (number | Date | string) →
epoch ms. Exposed alongside the existing day/calendar helpers.
Other:
- sium examples moved to _examples/ (excluded from public surface).
- sium-provider.svelte test removed (port pending; tracked elsewhere).
- libs/env.ts — DEV flag single source of truth.
- Various README touch-ups across artifacts.
All artifacts: 1021 server tests + 12 browser tests; svelte-check + ESLint
clean. The 9 server "errors" are pre-existing jsdom missing — unrelated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6 months ago
|
|
|
import { HttpAbortError, HttpTimeoutError } from './errors.ts';
|
|
|
|
|
import type { HttpMethod, RetryConfig } from './types.ts';
|
|
|
|
|
export { parseRetryAfter } from '$libs/http';
|
Add arts/timr, arts/sess, arts/http; sess actor extension; aapp factory
New artifacts:
arts/timr — runtime timer scheduler (singleton-per-App, never global).
Engine + Active split. Race-safe via (id, key, version) guard against
stale native callbacks, async tasks resolving after cancel/replace,
interval ticks scheduled after dispose, and ack-style timeouts. Per-
entry AbortController; intervals reuse the signal across ticks.
awaitTask:true (default, no overlap) vs awaitTask:false (fire-and-
forget cadence; failures don't stop the interval — semantic frozen).
Recursive setTimeout for intervals. scheduleAt(past) → delay 0 (no
error). Fake-clock injectable for deterministic tests. Pure
computeBackoffDelay helper. 50 server tests + 7 browser tests + full
README + DESIGN_TIMR + interactive test page at /test/timr.
arts/sess — session lifecycle with three generics
(TUser/TCredential/TData) plus optional SessionActor metadata
(kind/source/confidence — orthogonal axis to identity).
Tagged AdoptResult/RefreshResult/RevokeResult; never void.
Generation guard against stale refresh from local cancel, cross-tab
storage events, or re-adopt. onRefresh contract (null=fatal,
throw=transient). onRevoke replaces revokeUrl (consumer controls
fetch; engine just gets boolean). Default scope: 'global' when
onRevoke configured. INITIAL_SESSION sync dispatch on subscribe.
BroadcastChannel payload: {type, event, generation} only — never
tokens. SSR cookie reader validates invariants. withAutoRefresh
helper, 401-retry hook with applyAuth + loop guard, JWT exp helper.
103 tests (60 server + 5 browser + 38 actor/error/etc) + README +
DESIGN.md + test page at /test/sess.
arts/http — HTTP client with Standard Schema body validation, retry
+ Retry-After, attempt + total timeout via AbortSignal composition,
hooks (beforeRequest/beforeRetry/afterResponse/beforeError),
tagged HttpResult. Test page at /test/http.
arts/conn — DESIGN_CONN.md only (no implementation yet).
aapp:
- App.createActiveSession<TUser, TCredential, TData>(opts) factory
auto-injects App.Logger; throws SessAlreadyCreatedError on second
call. App.Sess getter exposes the active session (undefined until
first call). Auto-disposed by App.dispose().
- App.Timers integration deferred to timr Fase 3.
libs/days:
- toEpochMs(value) — permissive coercion (number | Date | string) →
epoch ms. Exposed alongside the existing day/calendar helpers.
Other:
- sium examples moved to _examples/ (excluded from public surface).
- sium-provider.svelte test removed (port pending; tracked elsewhere).
- libs/env.ts — DEV flag single source of truth.
- Various README touch-ups across artifacts.
All artifacts: 1021 server tests + 12 browser tests; svelte-check + ESLint
clean. The 9 server "errors" are pre-existing jsdom missing — unrelated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6 months ago
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Decide whether a failed attempt should be retried. Hierarchy:
|
|
|
|
|
* 1. User signal aborted → never retry.
|
|
|
|
|
* 2. Total-timeout fired → never retry.
|
|
|
|
|
* 3. Custom `shouldRetry` predicate → its return value wins.
|
|
|
|
|
* 4. Attempts exhausted → no retry.
|
|
|
|
|
* 5. Method must be in `methods` allow-list.
|
|
|
|
|
* 6. Either: error is a network-class failure (no response) OR response
|
|
|
|
|
* status is in `statusCodes`.
|
|
|
|
|
*/
|
|
|
|
|
export function shouldRetryRequest(
|
|
|
|
|
method: HttpMethod,
|
|
|
|
|
policy: RetryConfig,
|
|
|
|
|
ctx: { error: unknown; response?: Response; attempt: number }
|
|
|
|
|
): boolean {
|
|
|
|
|
// Hard stops independent of policy.
|
|
|
|
|
if (ctx.error instanceof HttpAbortError) return false;
|
|
|
|
|
if (ctx.error instanceof HttpTimeoutError && ctx.error.scope === HTTP_TIMEOUT_SCOPE_TOTAL)
|
|
|
|
|
return false;
|
Add arts/timr, arts/sess, arts/http; sess actor extension; aapp factory
New artifacts:
arts/timr — runtime timer scheduler (singleton-per-App, never global).
Engine + Active split. Race-safe via (id, key, version) guard against
stale native callbacks, async tasks resolving after cancel/replace,
interval ticks scheduled after dispose, and ack-style timeouts. Per-
entry AbortController; intervals reuse the signal across ticks.
awaitTask:true (default, no overlap) vs awaitTask:false (fire-and-
forget cadence; failures don't stop the interval — semantic frozen).
Recursive setTimeout for intervals. scheduleAt(past) → delay 0 (no
error). Fake-clock injectable for deterministic tests. Pure
computeBackoffDelay helper. 50 server tests + 7 browser tests + full
README + DESIGN_TIMR + interactive test page at /test/timr.
arts/sess — session lifecycle with three generics
(TUser/TCredential/TData) plus optional SessionActor metadata
(kind/source/confidence — orthogonal axis to identity).
Tagged AdoptResult/RefreshResult/RevokeResult; never void.
Generation guard against stale refresh from local cancel, cross-tab
storage events, or re-adopt. onRefresh contract (null=fatal,
throw=transient). onRevoke replaces revokeUrl (consumer controls
fetch; engine just gets boolean). Default scope: 'global' when
onRevoke configured. INITIAL_SESSION sync dispatch on subscribe.
BroadcastChannel payload: {type, event, generation} only — never
tokens. SSR cookie reader validates invariants. withAutoRefresh
helper, 401-retry hook with applyAuth + loop guard, JWT exp helper.
103 tests (60 server + 5 browser + 38 actor/error/etc) + README +
DESIGN.md + test page at /test/sess.
arts/http — HTTP client with Standard Schema body validation, retry
+ Retry-After, attempt + total timeout via AbortSignal composition,
hooks (beforeRequest/beforeRetry/afterResponse/beforeError),
tagged HttpResult. Test page at /test/http.
arts/conn — DESIGN_CONN.md only (no implementation yet).
aapp:
- App.createActiveSession<TUser, TCredential, TData>(opts) factory
auto-injects App.Logger; throws SessAlreadyCreatedError on second
call. App.Sess getter exposes the active session (undefined until
first call). Auto-disposed by App.dispose().
- App.Timers integration deferred to timr Fase 3.
libs/days:
- toEpochMs(value) — permissive coercion (number | Date | string) →
epoch ms. Exposed alongside the existing day/calendar helpers.
Other:
- sium examples moved to _examples/ (excluded from public surface).
- sium-provider.svelte test removed (port pending; tracked elsewhere).
- libs/env.ts — DEV flag single source of truth.
- Various README touch-ups across artifacts.
All artifacts: 1021 server tests + 12 browser tests; svelte-check + ESLint
clean. The 9 server "errors" are pre-existing jsdom missing — unrelated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6 months ago
|
|
|
|
|
|
|
|
if (policy.shouldRetry) return policy.shouldRetry(ctx);
|
|
|
|
|
|
|
|
|
|
if (ctx.attempt > policy.limit) return false;
|
|
|
|
|
if (!policy.methods.includes(method)) return false;
|
|
|
|
|
|
|
|
|
|
if (ctx.response !== undefined) {
|
|
|
|
|
return policy.statusCodes.includes(ctx.response.status);
|
|
|
|
|
}
|
|
|
|
|
// No response → network or per-attempt timeout. Both are retry-able.
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Compute the delay (ms) before attempt `n+1`. `Retry-After` from the failed
|
|
|
|
|
* response trumps the policy; otherwise the policy `delay(attempt)` is used,
|
|
|
|
|
* jittered if requested, then capped at `backoffLimit`.
|
|
|
|
|
*/
|
|
|
|
|
export function computeRetryDelay(
|
|
|
|
|
policy: RetryConfig,
|
|
|
|
|
attempt: number,
|
|
|
|
|
response: Response | undefined,
|
|
|
|
|
now: number = Date.now()
|
|
|
|
|
): number {
|
|
|
|
|
const headerDelay = response !== undefined ? parseRetryAfter(response.headers, now) : undefined;
|
|
|
|
|
const baseDelay = headerDelay !== undefined ? headerDelay : policy.delay(attempt);
|
|
|
|
|
const jittered = policy.jitter ? baseDelay * Math.random() : baseDelay;
|
|
|
|
|
return Math.min(Math.max(0, jittered), policy.backoffLimit);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Wait `ms`, resolving early if `signal` aborts. Resolves either way — the
|
|
|
|
|
* caller checks `signal.aborted` after to decide whether to continue.
|
|
|
|
|
*/
|
|
|
|
|
export function delayWithSignal(ms: number, signal: AbortSignal): Promise<void> {
|
|
|
|
|
if (ms <= 0 || signal.aborted) return Promise.resolve();
|
|
|
|
|
|
|
|
|
|
return new Promise<void>((resolve) => {
|
|
|
|
|
const id = setTimeout(() => {
|
|
|
|
|
signal.removeEventListener('abort', onAbort);
|
|
|
|
|
resolve();
|
|
|
|
|
}, ms);
|
|
|
|
|
(id as unknown as { unref?: () => void }).unref?.();
|
|
|
|
|
|
|
|
|
|
function onAbort(): void {
|
|
|
|
|
clearTimeout(id);
|
|
|
|
|
resolve();
|
|
|
|
|
}
|
|
|
|
|
signal.addEventListener('abort', onAbort, { once: true });
|
|
|
|
|
});
|
|
|
|
|
}
|