/** * API repository - Simple HTTP data access layer * No business logic - just GET/POST operations * * Server always returns ContentUnit format: { text, styles } */ import type { NetworkRepositoryConfig, ProjectConfig } from '../config/repository.config.js'; import type { ContentUnit } from '../content/types.js'; import type { IAsyncRepository } from '../interfaces/repository.interface.js'; import type { ContentUnitData, TranslationData } from '../types/translation-data.types.js'; /** * API repository implementation * Pure data access - no caching, no business logic */ export declare class NetworkRepository implements IAsyncRepository { readonly name = "NetworkRepository"; readonly type = "network"; readonly config: NetworkRepositoryConfig; /** * Namespace snapshot per key "locale:namespace": the server version TOGETHER * with the payload it describes, persisted as ONE localStorage entry. * * Canonical 304 contract (same as edge KV bundle + WP transient cache): * never claim freshness you can't back. If-None-Match is only sent when the * snapshot is in hand; a 304 answers with that snapshot's payload; version * and payload can never desync because they live and die together. */ private namespaceSnapshots; /** localStorage key prefix for persisted snapshots (version + payload) */ private static readonly SNAPSHOT_PREFIX; /** Legacy bare-version prefix — a version WITHOUT its payload is exactly the * desync the snapshot store exists to prevent; entries are deleted on sight. */ private static readonly LEGACY_VERSION_PREFIX; /** * Miss-path flood protection (READINESS before-scale #1: one cold locale * switch measured ~380 requests in 10 s — every t() miss refired a key GET * and a namespace GET on every re-render, forever on animated pages). */ /** GETs currently on the wire, keyed by `url|if-none-match` — identical * concurrent requests share one fetch; each awaiter gets a clone. The * If-None-Match value is part of the key so the 304-without-snapshot * unconditional refetch never coalesces onto a conditional flight. */ private inflightGets; /** URL → 404/423 seen recently: repeats are answered locally (same typed * error, no network) until `until`. 423 honors the server's Retry-After. * Cleared entirely by any successful write — discovery sync keeps its * miss → batch → served-back round-trip. */ private negativeCache; /** One-way latch: a 401 anywhere (or a 403 on a READ) means the key is * wrong or unauthorized for this project, and retrying cannot heal it — * after the first one nothing touches the network again for this * instance (a page reload starts fresh). Holds the status that tripped * it so short-circuits answer honestly. */ private authFailedStatus; /** Write-only latch (auth-byok Phase 18): a 403 (scope) or 429 (quota) on * a WRITE degrades this instance to read-only — writes stop, silently * and without retries, while reads keep serving the page. */ private writesFailedStatus; private static readonly DEFAULT_NEGATIVE_TTL_MS; /** * Default endpoint templates with placeholder support * Placeholders: :key, :locale, :namespace */ private readonly DEFAULT_ENDPOINTS; constructor(config?: NetworkRepositoryConfig); /** * Get single translation from API * Uses platform-specific integration endpoint * @throws {TranslationNotFoundError} When translation doesn't exist (404) * @throws {TranslationLockedError} When translation is locked/not ready (423) * @throws {ServerError} When server returns 5xx error * @throws {NetworkError} When network request fails */ get(key: string): Promise; /** * Get single translation as full ContentUnit (with styles) * Used by NetworkHandler for individual key lookups that need styles * @throws {TranslationNotFoundError} When translation doesn't exist (404) * @throws {TranslationLockedError} When translation is locked/not ready (423) * @throws {ServerError} When server returns 5xx error * @throws {NetworkError} When network request fails */ getContentUnit(key: string): Promise; /** * Get namespace with all translations * @throws {TranslationNotFoundError} When namespace doesn't exist (404) * @throws {TranslationLockedError} When namespace is locked/not ready (423) * @throws {ServerError} When server returns 5xx error * @throws {NetworkError} When network request fails */ getNamespace(locale: string, namespace: string): Promise; /** * Project-level config for this key — the dashboard's language list, * served under the same trust model as translation reads (docs/20, * companion track). Returns null when the server doesn't expose the * endpoint (404 — older server, feature not deployed) or the payload is * unusable: absence means "keep your local fallback", never an error. */ getProjectConfig(): Promise; /** * Shared namespace fetch with the canonical 304 contract: * - If-None-Match is sent ONLY when a snapshot (version + payload) is in hand. * - 304 answers with that snapshot's payload — never with "nothing". * - A 304 with no snapshot (server/proxy surprise) refetches unconditionally. * - A 200 stores version + payload atomically before either is trusted. */ private fetchNamespaceRaw; /** * Save translation to API (if supported) */ set(key: string, value: string): Promise; setNamespace(locale: string, namespace: string, translations: TranslationData): Promise; /** * Batch sync multiple missing translations * Uses new batch endpoint for efficiency */ setBatch(items: Array<{ key: string; value: string; }>): Promise; /** * Get namespace with all translations as ContentUnit data * * @returns ContentUnitData (Record) * @throws {TranslationNotFoundError} When namespace doesn't exist (404) * @throws {TranslationLockedError} When namespace is locked/not ready (423) * @throws {ServerError} When server returns 5xx error * @throws {NetworkError} When network request fails */ getNamespaceContentUnits(locale: string, namespace: string): Promise; /** * Save ContentUnit to API * * Sends the ContentUnit format to server for storage. * Server should store in ContentUnit format for efficient retrieval. * * @param key - Translation key * @param unit - ContentUnit to save */ setContentUnit(key: string, unit: ContentUnit): Promise; /** * Batch sync multiple ContentUnits * * Sends ContentUnit format to server for batch storage. * * @param items - Array of key-ContentUnit pairs */ setBatchContentUnits(items: Array<{ key: string; value: ContentUnit; }>): Promise; /** * Remove translation from API (if supported) */ remove(key: string): Promise; /** * Clear all translations (if supported) */ clear(): Promise; getConfig(): NetworkRepositoryConfig; /** Read a persisted snapshot (version + payload), falling back to memory. * Legacy bare-version entries and corrupt snapshots are deleted on sight — * a version without its payload must never drive a conditional request. */ private getSnapshot; /** Persist version + payload as ONE entry (memory + localStorage). On quota * failure the previous stored snapshot stays — still a consistent pair. */ private setSnapshot; /** * Handle HTTP error responses by mapping to domain errors * @throws {TranslationNotFoundError} When status is 404 * @throws {TranslationLockedError} When status is 423 * @throws {ServerError} When status is 5xx * @throws {NetworkError} For other client errors */ private handleHttpError; /** * Handle caught errors by wrapping non-domain errors * @throws {TranslationError} Always throws - propagates domain errors or wraps unexpected ones */ private handleCaughtError; /** * Build URL from endpoint template and parameters * Supports placeholders: :key, :locale, :namespace * @param endpoint - Endpoint key from DEFAULT_ENDPOINTS * @param params - Parameters to replace placeholders with * @returns Complete URL with baseUrl + endpoint path */ private buildUrl; /** * Make HTTP request with timeout and headers */ private makeRequest; /** Bookkeeping on every real (non-synthesized) response — runs once per * physical fetch, never once per coalesced awaiter. */ private recordOutcome; /** Test doubles (and exotic runtimes) hand us plain objects — only real * Responses need cloning so each coalesced awaiter can read the body. */ private cloneResponse; private doFetch; } //# sourceMappingURL=network-repository.d.ts.map