@sweidos/eidos 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"eidos.cjs","names":[],"sources":["../src/types.ts","../src/store-slices.ts","../src/store.ts","../src/sw-bridge.ts","../src/resource.ts","../src/idb.ts","../src/queue-storage.ts","../src/queue-sync.ts","../src/action.ts","../src/replay.ts","../src/runtime.ts","../src/async-storage-adapter.ts","../src/react/Provider.tsx","../src/stores.ts","../src/react/hooks.ts","../src/version.ts"],"sourcesContent":["// ─────────────────────────────────────────────────────────────────────────────\n// Eidos Core Types\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport type CacheStrategy = 'cache-first' | 'stale-while-revalidate' | 'network-first';\n\n// ── Resource ─────────────────────────────────────────────────────────────────\n\nexport interface ResourceConfig {\n /** Make this resource available when the device is offline. */\n offline: boolean;\n /** Override the auto-selected caching strategy. */\n strategy?: CacheStrategy;\n /** Custom cache bucket name. Defaults to 'eidos-resources-v1'. */\n cacheName?: string;\n /**\n * Cache schema version. Bump when the response shape changes so old\n * cached entries (a different shape) aren't served from a stale bucket.\n * Appended to `cacheName` as a suffix (e.g. `eidos-resources-v1-v2`).\n * Old-versioned buckets are left in Cache Storage — clear them via\n * `caches.delete()` if needed.\n */\n version?: string | number;\n /** Max age of cached response in milliseconds. Expired entries trigger a network fetch. */\n maxAge?: number;\n}\n\nexport interface GeneratedStrategy {\n name: string;\n swStrategy: CacheStrategy;\n cacheName: string;\n /** One-line rationale for why this strategy was chosen. */\n reasoning: string;\n /** Human-readable description of each behavioral step. */\n behavior: string[];\n /** Pseudocode showing the equivalent Workbox config. */\n equivalentCode: string;\n}\n\nexport interface ResourceEntry {\n url: string;\n config: ResourceConfig;\n strategy: GeneratedStrategy;\n status: 'idle' | 'fetching' | 'fresh' | 'stale' | 'error' | 'offline';\n cachedAt?: number;\n fetchedAt?: number;\n cacheHits: number;\n cacheMisses: number;\n lastEvent?: 'cache-hit' | 'cache-updated' | 'network-error' | 'cache-cleared';\n}\n\nexport interface ResourceHandle<T = unknown> {\n readonly url: string;\n readonly config: ResourceConfig;\n readonly strategy: GeneratedStrategy;\n fetch: () => Promise<Response>;\n json: () => Promise<T>;\n /** Returns a TanStack Query-compatible options object. */\n query: () => { queryKey: [string, string]; queryFn: () => Promise<T> };\n prefetch: () => Promise<void>;\n invalidate: () => Promise<void>;\n /** Remove from registry and SW. Required before re-registering the same URL with different config. */\n unregister: () => void;\n}\n\n/**\n * Handle for a URL pattern (`/api/products/*`, `/api/users/:id`, `**`).\n * The SW intercepts all matching requests automatically — there is no single\n * URL to fetch/cache directly, so only cache-management methods are exposed.\n * Returned by `resourcePattern()`.\n */\nexport interface PatternResourceHandle {\n readonly url: string;\n readonly config: ResourceConfig;\n readonly strategy: GeneratedStrategy;\n /** Clears all cache entries matching this pattern. */\n invalidate: () => Promise<void>;\n /** Remove from registry and SW. Required before re-registering the same pattern with different config. */\n unregister: () => void;\n}\n\n/** A handle returned by either `resource()` or `resourcePattern()`. */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type AnyResourceHandle = ResourceHandle<any> | PatternResourceHandle;\n\n/** Summary returned by warmCache(). */\nexport interface WarmCacheResult {\n /** Resources that were prefetched successfully. */\n warmed: number;\n /** Resources whose prefetch threw (network error, offline, etc.). */\n failed: number;\n /** The raw errors, in input order, for failed handles. */\n errors: unknown[];\n}\n\n// ── Action ───────────────────────────────────────────────────────────────────\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ninterface ActionConfigBase<TArgs extends any[] = any[]> {\n /** Max retry attempts before marking as failed. Default: 3. */\n maxRetries?: number;\n /** Human-readable name for the action (used in devtools). */\n name?: string;\n /**\n * Prefixes the registered action id (`namespace::name`). Use to avoid\n * collisions when two actions share a name (e.g. across micro-frontends,\n * or two `createOrder` actions in different modules) — without a\n * namespace, the second registration silently overwrites the first.\n */\n namespace?: string;\n /**\n * Replay order when multiple queued actions are pending.\n * `'high'` items replay before `'normal'`, which replay before `'low'`.\n * Each tier completes fully before the next tier begins.\n * Default: `'normal'`.\n */\n priority?: 'high' | 'normal' | 'low';\n /**\n * Called immediately before the async function executes, with the same args\n * plus a trailing `ActionContext`. Use to apply an optimistic UI update (add\n * item, mark as pending, etc.) and to capture `idempotencyKey` for later\n * `handle.cancel(idempotencyKey)` calls. Called on every invocation —\n * online, offline, and during queue replay.\n */\n onOptimistic?: (...args: [...TArgs, ActionContext]) => void;\n /**\n * Called when the action permanently fails and will not be retried.\n * - `best-effort`: called on first throw.\n * - `neverLose`: called when `maxRetries` is exhausted (status → `'failed'`).\n * Use to revert the optimistic update.\n */\n onRollback?: (...args: [...TArgs, ActionContext]) => void;\n /**\n * Declarative conflict-resolution strategy used during queue replay when\n * the server responds with a 4xx status (conflict, gone, unprocessable,\n * etc.). If not provided, 4xx errors are treated identically to other\n * errors (retried until `maxRetries` is exhausted, then `onRollback` is\n * called). See `ConflictConfig`.\n */\n conflict?: ConflictConfig;\n /**\n * When `true`, each invocation gets an `AbortController` whose `signal` is\n * passed via `ActionContext.signal`. Forward it to `fetch`/etc. so\n * `handle.cancel(idempotencyKey)` can abort an in-flight call, or remove a\n * not-yet-replayed queued item.\n */\n cancellable?: boolean;\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type ActionConfig<TArgs extends any[] = any[]> =\n | (ActionConfigBase<TArgs> & {\n /** Call directly, no persistence on failure. */\n reliability: 'best-effort';\n })\n | (ActionConfigBase<TArgs> & {\n /** Persist to IndexedDB before executing; replay on reconnect. */\n reliability: 'neverLose';\n /**\n * Required for `neverLose` — queued items must survive a page reload\n * and be matched back to this action on replay. `fn.name` is not\n * reliable (minifiers rename it, arrow functions may be anonymous).\n */\n name: string;\n });\n\n/**\n * Passed to `ConflictConfig.resolve` (for `'merge'`/`'custom'` strategies)\n * when a queued action's replay receives a 4xx response.\n */\nexport interface ConflictContext {\n /** Whatever `fn` threw — typically a `Response` or an error with `.status`. */\n error: unknown;\n /** The original arguments the action was queued with. */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n args: any[];\n /** Number of replay attempts so far (0 on first replay). */\n attempt: number;\n idempotencyKey: string;\n}\n\n/**\n * Outcome of `ConflictConfig.resolve`:\n * - `'retry'`: keep the item queued, retry per normal backoff.\n * - `'skip'`: silently remove the item (no `onRollback`).\n * - `{ resolved: args }`: replace the queued args and retry immediately\n * on the next replay pass.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type ConflictResolution = 'retry' | 'skip' | { resolved: any[] };\n\nexport interface ConflictConfig {\n /**\n * - `'serverWins'`: drop the queued item, keeping the server's state.\n * - `'clientWins'`: keep retrying — the client's write should eventually\n * be accepted (e.g. once the server-side conflict is cleared).\n * - `'merge'` / `'custom'`: call `resolve` to decide.\n */\n strategy: 'serverWins' | 'clientWins' | 'merge' | 'custom';\n /** Required for `'merge'` and `'custom'`. */\n resolve?: (ctx: ConflictContext) => ConflictResolution;\n}\n\n/** Bump when ActionQueueItem's shape changes. Used to migrate items persisted by older versions. */\nexport const CURRENT_QUEUE_SCHEMA_VERSION = 2;\n\nexport interface ActionQueueItem {\n /** Shape version this item was persisted with. Items from before v2 are migrated on load. */\n schemaVersion: number;\n id: string;\n /** ID of the registered action (maps to the function in the registry). */\n actionId: string;\n actionName: string;\n /**\n * Stable per-invocation key, generated once and reused across every retry/replay\n * of this item. Pass to your server as an idempotency key so retries that reach\n * the server after a dropped response don't double-execute.\n */\n idempotencyKey: string;\n args: unknown[];\n queuedAt: number;\n retryCount: number;\n maxRetries: number;\n status: 'pending' | 'replaying' | 'succeeded' | 'failed';\n /** Replay priority. High items replay before normal, normal before low. Default: 'normal'. */\n priority?: 'high' | 'normal' | 'low';\n error?: string;\n completedAt?: number;\n /** Earliest timestamp at which this item should be retried (exponential backoff). */\n nextRetryAt?: number;\n}\n\nexport interface QueuedResult {\n readonly queued: true;\n readonly id: string;\n readonly message: string;\n}\n\n/** Summary returned by replayQueue(). */\nexport interface ReplayResult {\n /** Items where the registered function was found and called. */\n attempted: number;\n /** Items that resolved successfully. */\n succeeded: number;\n /** Items that failed and have no retries remaining (status: 'failed'). */\n failed: number;\n /** Items that failed but will be retried later (nextRetryAt set). */\n retrying: number;\n /** Items whose actionId had no registered function — likely not yet imported. */\n skipped: number;\n /** Items that received a 4xx response and were dropped via `conflict: { strategy: 'serverWins' }` (or `resolve()` returning `'skip'`). */\n conflicted: number;\n /** Items removed via `handle.cancel(idempotencyKey)` before/during replay. */\n cancelled: number;\n}\n\n/**\n * Passed as an extra argument after the declared params to `neverLose` actions,\n * on every invocation (initial call, offline queue, and replay). The same\n * `idempotencyKey` is reused across all retries of one logical invocation —\n * forward it to your server (e.g. as an `Idempotency-Key` header) so a retry\n * that reaches the server after a dropped response doesn't double-execute.\n */\nexport interface ActionContext {\n idempotencyKey: string;\n /** 0 on the first attempt, incremented on each replay retry. */\n attempt: number;\n /** Set when `config.cancellable` is true. Forward to `fetch`/etc. for cancellation support. */\n signal?: AbortSignal;\n}\n\n/**\n * Every action function receives its declared args plus a trailing\n * `ActionContext` — on every invocation (online, offline, and replay).\n */\nexport type ActionFn<TArgs extends unknown[], TReturn> = (\n ...args: [...TArgs, ActionContext]\n) => Promise<TReturn>;\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport interface ActionHandle<TArgs extends any[], TReturn> {\n (...args: TArgs): Promise<TReturn | QueuedResult>;\n readonly id: string;\n readonly config: ActionConfig;\n /**\n * Cancel an invocation by its `idempotencyKey` (from `ActionContext` /\n * `onOptimistic`). Aborts the in-flight call if `cancellable: true` and\n * still running, otherwise removes a not-yet-replayed queued item.\n * Returns `true` if something was cancelled/removed.\n */\n cancel: (idempotencyKey: string) => Promise<boolean>;\n}\n\n// ── Global State ─────────────────────────────────────────────────────────────\n\nexport interface EidosState {\n isOnline: boolean;\n swStatus: 'idle' | 'registering' | 'active' | 'error' | 'unsupported';\n swError?: string;\n resources: Record<string, ResourceEntry>;\n queue: ActionQueueItem[];\n reliability: ReliabilityStats;\n}\n\n/**\n * Cumulative, session-scoped counters for `neverLose` queue outcomes — opt-in\n * telemetry surfaced via `eidosReliabilityStats` / `useEidosReliabilityStats()`\n * and `EidosConfig.onReliabilityReport`. Reset on page reload (not persisted).\n */\nexport interface ReliabilityStats {\n [key: string]: number;\n /** Actions persisted to the queue (offline, or online call that threw). */\n queued: number;\n /** Queue items that executed successfully (first attempt or a retry). */\n succeeded: number;\n /** Queue items that exhausted `maxRetries` and moved to `'failed'`. */\n failed: number;\n /** Replay attempts that failed but will retry (haven't exhausted `maxRetries`). */\n retried: number;\n /** Queue items dropped by a `serverWins`/`merge`/`custom` conflict resolution. */\n conflicted: number;\n /** Queue items removed via `handle.cancel(idempotencyKey)` before replay completed. */\n cancelled: number;\n}\n\nexport function emptyReliabilityStats(): ReliabilityStats {\n return { queued: 0, succeeded: 0, failed: 0, retried: 0, conflicted: 0, cancelled: 0 };\n}\n\nexport interface QueueStatusCounts {\n [key: string]: number;\n pending: number;\n failed: number;\n replaying: number;\n total: number;\n}\n\n/** Single pass over the queue — avoids separate .filter() calls per status. */\nexport function countQueueByStatus(queue: ActionQueueItem[]): QueueStatusCounts {\n let pending = 0,\n failed = 0,\n replaying = 0;\n for (const q of queue) {\n if (q.status === 'pending') pending++;\n else if (q.status === 'failed') failed++;\n else if (q.status === 'replaying') replaying++;\n }\n return { pending, failed, replaying, total: queue.length };\n}\n","import type { ResourceEntry, ActionQueueItem, ReliabilityStats } from './types';\nimport { emptyReliabilityStats } from './types';\nimport type { EidosStore } from './store';\n\ntype Setter = (updater: (prev: EidosStore) => Partial<EidosStore>) => void;\n\n// ── Resource slice ────────────────────────────────────────────────────────────\n\nexport interface ResourceActions {\n registerResource: (url: string, entry: ResourceEntry) => void;\n updateResource: (url: string, update: Partial<ResourceEntry>) => void;\n unregisterResource: (url: string) => void;\n}\n\nexport function createResourceActions(set: Setter): ResourceActions {\n return {\n registerResource: (url, entry) => set((s) => ({ resources: { ...s.resources, [url]: entry } })),\n\n updateResource: (url, update) =>\n set((s) => ({\n resources: {\n ...s.resources,\n [url]: s.resources[url] ? { ...s.resources[url], ...update } : s.resources[url],\n },\n })),\n\n unregisterResource: (url) =>\n set((s) => ({\n resources: Object.fromEntries(Object.entries(s.resources).filter(([k]) => k !== url)),\n })),\n };\n}\n\n// ── Queue slice ───────────────────────────────────────────────────────────────\n\nexport interface QueueActions {\n addQueueItem: (item: ActionQueueItem) => void;\n updateQueueItem: (id: string, update: Partial<ActionQueueItem>) => void;\n batchUpdateQueueItems: (updates: Array<{ id: string; update: Partial<ActionQueueItem> }>) => void;\n removeQueueItem: (id: string) => void;\n hydrateQueue: (items: ActionQueueItem[]) => void;\n}\n\nexport function createQueueActions(set: Setter): QueueActions {\n return {\n addQueueItem: (item) => set((s) => ({ queue: [...s.queue, item] })),\n\n updateQueueItem: (id, update) =>\n set((s) => ({\n queue: s.queue.map((item) => (item.id === id ? { ...item, ...update } : item)),\n })),\n\n batchUpdateQueueItems: (updates) =>\n set((s) => {\n const map = new Map(updates.map((u) => [u.id, u.update]));\n return {\n queue: s.queue.map((item) => {\n const u = map.get(item.id);\n return u ? { ...item, ...u } : item;\n }),\n };\n }),\n\n removeQueueItem: (id) => set((s) => ({ queue: s.queue.filter((item) => item.id !== id) })),\n\n hydrateQueue: (items) => set(() => ({ queue: items })),\n };\n}\n\n// ── Reliability slice ─────────────────────────────────────────────────────────\n\nexport interface ReliabilityActions {\n recordReliabilityEvent: (event: keyof ReliabilityStats) => void;\n resetReliabilityStats: () => void;\n}\n\nexport function createReliabilityActions(set: Setter): ReliabilityActions {\n return {\n recordReliabilityEvent: (event) =>\n set((s) => ({ reliability: { ...s.reliability, [event]: s.reliability[event] + 1 } })),\n\n resetReliabilityStats: () => set(() => ({ reliability: emptyReliabilityStats() })),\n };\n}\n","import type { EidosState } from './types';\nimport { emptyReliabilityStats } from './types';\nimport {\n createResourceActions,\n createQueueActions,\n createReliabilityActions,\n} from './store-slices';\nimport type { ResourceActions, QueueActions, ReliabilityActions } from './store-slices';\n\nexport interface EidosStore extends EidosState, ResourceActions, QueueActions, ReliabilityActions {\n // Online\n setOnline: (online: boolean) => void;\n // SW\n setSwStatus: (status: EidosState['swStatus'], error?: string) => void;\n}\n\ntype Listener = () => void;\n\nlet _state: EidosStore;\nconst _listeners = new Set<Listener>();\n\nfunction _notify() {\n _listeners.forEach((fn) => fn());\n}\n\nfunction _set(updater: (prev: EidosStore) => Partial<EidosStore>) {\n _state = { ..._state, ...updater(_state) };\n _notify();\n}\n\n_state = {\n // navigator.onLine is undefined in React Native — default to true unless explicitly false\n isOnline: typeof navigator === 'undefined' || navigator.onLine !== false,\n swStatus: 'idle',\n swError: undefined,\n resources: {},\n queue: [],\n reliability: emptyReliabilityStats(),\n\n setOnline: (isOnline) => _set(() => ({ isOnline })),\n\n setSwStatus: (swStatus, swError) => _set(() => ({ swStatus, swError })),\n\n ...createResourceActions(_set),\n ...createQueueActions(_set),\n ...createReliabilityActions(_set),\n};\n\nfunction _getState() {\n return _state;\n}\n\nfunction _subscribe(listener: Listener) {\n _listeners.add(listener);\n return () => {\n _listeners.delete(listener);\n };\n}\n\nexport const useEidosStore = {\n getState: _getState,\n subscribe: _subscribe,\n // Test/devtools helper — merges partial state, preserves action methods.\n setState: (partial: Partial<EidosStore> | ((s: EidosStore) => Partial<EidosStore>)) => {\n const update = typeof partial === 'function' ? partial(_state) : partial;\n _state = { ..._state, ...update };\n _notify();\n },\n};\n","import { useEidosStore } from './store';\n\nlet _registration: ServiceWorkerRegistration | null = null;\n// Messages sent before the SW activates are buffered here and flushed once\n// the SW is ready. Covers resource registrations, cache clears, offline\n// simulation — anything sent at module scope before EidosProvider mounts.\nlet _pendingMessages: Record<string, unknown>[] = [];\n\nexport function getSwRegistration() {\n return _registration;\n}\n\nexport async function registerServiceWorker(swPath: string): Promise<void> {\n if (typeof navigator === 'undefined' || !('serviceWorker' in navigator)) {\n useEidosStore.getState().setSwStatus('unsupported');\n return;\n }\n\n const store = useEidosStore.getState();\n store.setSwStatus('registering');\n\n try {\n _registration = await navigator.serviceWorker.register(swPath, { scope: '/' });\n\n await waitForActivation(_registration);\n\n store.setSwStatus('active');\n\n // Receive messages from SW\n navigator.serviceWorker.addEventListener('message', onSwMessage);\n\n // Track online/offline\n window.addEventListener('online', () => store.setOnline(true));\n window.addEventListener('offline', () => store.setOnline(false));\n\n flushPendingMessages();\n } catch (err) {\n store.setSwStatus('error', String(err));\n }\n}\n\nfunction waitForActivation(reg: ServiceWorkerRegistration): Promise<void> {\n return new Promise((resolve) => {\n if (reg.active) {\n resolve();\n return;\n }\n const sw = reg.installing ?? reg.waiting;\n if (!sw) {\n resolve();\n return;\n }\n\n // Resolve after 10s regardless — another tab may be blocking activation\n const timer = setTimeout(resolve, 10_000);\n\n sw.addEventListener('statechange', function handler() {\n if (sw.state === 'activated') {\n clearTimeout(timer);\n sw.removeEventListener('statechange', handler);\n resolve();\n }\n });\n });\n}\n\nexport function sendToWorker(message: Record<string, unknown>): void {\n const sw = _registration?.active;\n if (sw) {\n sw.postMessage(message);\n } else {\n _pendingMessages.push(message);\n }\n}\n\nlet _bgSyncHandler: (() => void) | null = null;\n\nexport function registerBgSyncHandler(fn: () => void): void {\n _bgSyncHandler = fn;\n}\n\nexport function isBgSyncSupported(): boolean {\n try {\n return (\n typeof navigator !== 'undefined' &&\n 'serviceWorker' in navigator &&\n _registration !== null &&\n 'sync' in _registration\n );\n } catch {\n return false;\n }\n}\n\ninterface PushHandlers {\n onNotificationClick?: (data: unknown) => void;\n onSubscriptionExpired?: (sub: PushSubscriptionJSON) => void;\n}\n\nlet _pushHandlers: PushHandlers = {};\n\nexport function registerPushCallbacks(handlers: PushHandlers): void {\n _pushHandlers = handlers;\n}\n\nfunction onSwMessage(event: MessageEvent): void {\n const data = event.data as {\n type: string;\n url?: string;\n strategy?: string;\n data?: unknown;\n subscription?: unknown;\n };\n if (!data?.type) return;\n\n const store = useEidosStore.getState();\n const { type, url } = data;\n\n if (type === 'EIDOS_BACKGROUND_SYNC') {\n _bgSyncHandler?.();\n return;\n }\n\n if (type === 'EIDOS_NOTIFICATION_CLICK') {\n _pushHandlers.onNotificationClick?.(data.data);\n return;\n }\n\n if (type === 'EIDOS_SUBSCRIPTION_EXPIRED') {\n _pushHandlers.onSubscriptionExpired?.(data.subscription as PushSubscriptionJSON);\n return;\n }\n\n if (!url) return;\n\n switch (type) {\n case 'EIDOS_CACHE_HIT': {\n const current = store.resources[url];\n store.updateResource(url, {\n status: 'fresh',\n lastEvent: 'cache-hit',\n cacheHits: (current?.cacheHits ?? 0) + 1,\n });\n break;\n }\n case 'EIDOS_CACHE_UPDATED': {\n store.updateResource(url, {\n status: 'fresh',\n lastEvent: 'cache-updated',\n cachedAt: Date.now(),\n });\n break;\n }\n case 'EIDOS_NETWORK_ERROR': {\n store.updateResource(url, {\n status: 'error',\n lastEvent: 'network-error',\n });\n break;\n }\n }\n}\n\nexport function setOfflineSimulation(enabled: boolean): void {\n sendToWorker({ type: 'EIDOS_SIMULATE_OFFLINE', enabled });\n useEidosStore.getState().setOnline(!enabled);\n}\n\nfunction flushPendingMessages(): void {\n const sw = _registration?.active;\n if (!sw) return;\n for (const msg of _pendingMessages) sw.postMessage(msg);\n _pendingMessages = [];\n}\n\n/** Test-only: resets module-level state between test cases. */\nexport function _resetSwBridgeForTests(): void {\n _registration = null;\n _pendingMessages = [];\n _bgSyncHandler = null;\n _pushHandlers = {};\n}\n","import { useEidosStore } from './store';\nimport { sendToWorker } from './sw-bridge';\nimport type {\n ResourceConfig,\n ResourceHandle,\n PatternResourceHandle,\n ResourceEntry,\n GeneratedStrategy,\n CacheStrategy,\n WarmCacheResult,\n} from './types';\n\nconst _registry = new Map<string, ResourceHandle | PatternResourceHandle>();\n\n// ── Request deduplication ─────────────────────────────────────────────────────\n// If multiple callers invoke handle.fetch() simultaneously for the same URL,\n// only one network request is made. Each caller gets its own cloned Response.\n// Keyed by URL; entry is deleted when the request settles.\nconst _inflightRequests = /* @__PURE__ */ new Map<string, Promise<Response>>();\n\n// ── TanStack Query bridge (optional) ─────────────────────────────────────────\n// Set by @sweidos/eidos/query when withEidosQueryClient() is called.\n// Lets handle.invalidate() also invalidate the matching TQ cache entry.\ntype QueryInvalidator = (queryKey: [string, string]) => void;\nlet _queryInvalidator: QueryInvalidator | null = null;\n\n/** @internal Called by @sweidos/eidos/query. */\nexport function setQueryInvalidator(fn: QueryInvalidator): void {\n _queryInvalidator = fn;\n}\n\n// ── URL pattern helpers ───────────────────────────────────────────────────────\n\n/** Returns true if `url` contains wildcard or :param segments. */\nfunction isPattern(url: string): boolean {\n return url.includes('*') || /:[^/]+/.test(url);\n}\n\n/**\n * Converts a URL pattern to a regex source string for SW fetch matching.\n * `**` → multi-segment wildcard (`.+`)\n * `*` → single-segment wildcard (`[^/]+`)\n * `:param` → named single segment (`[^/]+`)\n *\n * Special regex characters in the pattern (e.g. `.`) are escaped first so\n * they match literally.\n *\n * @example\n * patternToRegexStr('/api/products/*') // '^/api/products/[^/]+$'\n * patternToRegexStr('/api/products/**') // '^/api/products/.+$'\n * patternToRegexStr('/api/users/:id') // '^/api/users/[^/]+$'\n */\nfunction patternToRegexStr(pattern: string): string {\n // Escape all regex-special chars except `*`, `/`, `:` (handled below)\n const escaped = pattern.replace(/[.+?^${}()|[\\]\\\\]/g, '\\\\$&');\n return (\n '^' +\n escaped\n .replace(/\\*\\*/g, '.+') // ** → multi-segment wildcard\n .replace(/\\*/g, '[^/]+') // * → single-segment wildcard\n .replace(/:[^/]+/g, '[^/]+') + // :param → single-segment wildcard\n '$'\n );\n}\n\n/** Shared setup for resource()/resourcePattern(): strategy derivation, store + SW registration. */\nfunction _register(\n url: string,\n config: ResourceConfig,\n): { strategy: GeneratedStrategy; regexStr: string | undefined } {\n const strategy = deriveStrategy(config);\n const regexStr = isPattern(url) ? patternToRegexStr(url) : undefined;\n\n const entry: ResourceEntry = {\n url,\n config,\n strategy,\n status: 'idle',\n cacheHits: 0,\n cacheMisses: 0,\n };\n\n useEidosStore.getState().registerResource(url, entry);\n\n sendToWorker({\n type: 'EIDOS_REGISTER_RESOURCE',\n url,\n strategy: strategy.swStrategy,\n cacheName: strategy.cacheName,\n ...(regexStr !== undefined && { pattern: regexStr }),\n });\n\n return { strategy, regexStr };\n}\n\nfunction _invalidate(\n url: string,\n strategy: GeneratedStrategy,\n regexStr: string | undefined,\n): () => Promise<void> {\n return async () => {\n sendToWorker({ type: 'EIDOS_CLEAR_CACHE', url });\n const cache = await caches.open(strategy.cacheName).catch(() => null);\n if (cache) {\n const keys = await cache.keys();\n const patternRe = regexStr ? new RegExp(regexStr) : null;\n const isCrossOrigin = url.startsWith('http');\n await Promise.all(\n keys\n .filter((r) => {\n const rUrl = r.url;\n const p = new URL(rUrl).pathname;\n if (patternRe) {\n // Cross-origin patterns were compiled from absolute URLs; test full URL.\n return patternRe.test(isCrossOrigin ? rUrl : p);\n }\n return isCrossOrigin ? rUrl === url : rUrl === url || p === url;\n })\n .map((r) => cache.delete(r)),\n );\n }\n // For exact-URL resources update the store entry; patterns don't have a\n // single entry to update (individual URLs are not tracked per-pattern).\n if (!isPattern(url)) {\n useEidosStore.getState().updateResource(url, {\n status: 'stale',\n cachedAt: undefined,\n lastEvent: 'cache-cleared',\n cacheHits: 0,\n cacheMisses: 0,\n });\n }\n // Notify TanStack Query bridge if registered.\n _queryInvalidator?.(['eidos', url]);\n };\n}\n\nfunction _unregister(url: string): () => void {\n return () => {\n _registry.delete(url);\n sendToWorker({ type: 'EIDOS_UNREGISTER_RESOURCE', url });\n useEidosStore.getState().unregisterResource(url);\n };\n}\n\nfunction _warnIfReregisteredWithDifferentConfig(\n url: string,\n existing: ResourceHandle | PatternResourceHandle,\n config: ResourceConfig,\n factoryName: string,\n): void {\n if (!import.meta.env.DEV) return;\n const existingCfg = existing.config;\n if (\n existingCfg.offline !== config.offline ||\n existingCfg.strategy !== config.strategy ||\n existingCfg.cacheName !== config.cacheName ||\n existingCfg.version !== config.version\n ) {\n console.warn(\n `[eidos] ${factoryName}('${url}') already registered with a different config — returning cached handle. Call handle.unregister() first to re-register.`,\n { registered: existingCfg, ignored: config },\n );\n }\n}\n\n// ── resource() ────────────────────────────────────────────────────────────────\n\n/**\n * Registers a concrete-URL resource. For URL patterns (`/api/products/*`,\n * `/api/users/:id`, `**`), use `resourcePattern()` instead.\n */\nexport function resource<T = unknown>(url: string, config: ResourceConfig): ResourceHandle<T> {\n if (isPattern(url)) {\n throw new Error(\n `[eidos] resource('${url}') is a URL pattern — use resourcePattern('${url}', config) instead. ` +\n `Pattern handles only support invalidate()/unregister(); the SW intercepts matching requests automatically.`,\n );\n }\n\n if (_registry.has(url)) {\n const existing = _registry.get(url)!;\n _warnIfReregisteredWithDifferentConfig(url, existing, config, 'resource');\n return existing as ResourceHandle<T>;\n }\n\n const { strategy } = _register(url, config);\n\n const handle: ResourceHandle<T> = {\n url,\n config,\n strategy,\n\n fetch: async () => {\n // ── Deduplication: coalesce concurrent fetches for the same URL ─────\n // If a request is already in-flight, piggyback on it and return a clone\n // so each caller gets an independent readable Response body.\n const existing = _inflightRequests.get(url);\n if (existing) return existing.then((r) => r.clone());\n\n // Store the raw-response promise. All callers (including the primary)\n // receive a clone — the raw response stays unconsumed in the map so\n // any caller arriving while the promise is still pending can clone it.\n const task = _fetchResource(url, config, strategy);\n _inflightRequests.set(url, task);\n // .catch() silences the unhandled-rejection on the cleanup promise;\n // the error still propagates to callers via task.then() below.\n task.finally(() => _inflightRequests.delete(url)).catch(() => {});\n return task.then((r) => r.clone());\n },\n\n json: async () => {\n const res = await handle.fetch();\n return res.json() as Promise<T>;\n },\n\n query: () => ({\n queryKey: ['eidos', url] as [string, string],\n queryFn: () => handle.json(),\n }),\n\n prefetch: async () => {\n await handle.fetch();\n },\n\n invalidate: _invalidate(url, strategy, undefined),\n unregister: _unregister(url),\n };\n\n _registry.set(url, handle);\n return handle;\n}\n\n// ── resourcePattern() ────────────────────────────────────────────────────────\n\n/**\n * Registers a URL pattern (`/api/products/*`, `/api/users/:id`, `**`). The SW\n * intercepts all matching requests automatically — there is no single URL to\n * fetch/cache directly, so the returned handle only supports cache management\n * (`invalidate`/`unregister`). For a fetchable resource, use `resource()`.\n */\nexport function resourcePattern(url: string, config: ResourceConfig): PatternResourceHandle {\n if (!isPattern(url)) {\n throw new Error(\n `[eidos] resourcePattern('${url}') is not a URL pattern — use resource('${url}', config) instead.`,\n );\n }\n\n if (_registry.has(url)) {\n const existing = _registry.get(url)!;\n _warnIfReregisteredWithDifferentConfig(url, existing, config, 'resourcePattern');\n return existing as PatternResourceHandle;\n }\n\n const { strategy, regexStr } = _register(url, config);\n\n const handle: PatternResourceHandle = {\n url,\n config,\n strategy,\n invalidate: _invalidate(url, strategy, regexStr),\n unregister: _unregister(url),\n };\n\n _registry.set(url, handle);\n return handle;\n}\n\n// ── _fetchResource ─────────────────────────────────────────────────────────────\n// The actual network/cache implementation. Separated from handle.fetch() so the\n// deduplication wrapper can store the Promise and share it across concurrent callers.\n// Returns the raw (unconsumed) Response — callers MUST .clone() before reading body.\nasync function _fetchResource(\n url: string,\n config: ResourceConfig,\n strategy: GeneratedStrategy,\n): Promise<Response> {\n const store = useEidosStore.getState();\n store.updateResource(url, { status: 'fetching', fetchedAt: Date.now() });\n\n // Open cache once and reuse across try/catch — avoids a redundant\n // caches.open() call in the error fallback path.\n const cache = await caches.open(strategy.cacheName).catch(() => null);\n\n try {\n // ── network-first: skip cache check, go straight to network ─────────\n // For cache-first / SWR the cache check below is correct. For\n // network-first, reading cache first and returning early would\n // contradict the strategy — fresh data is the priority.\n if (strategy.swStrategy !== 'network-first') {\n // ── Direct Cache API check ─────────────────────────────────────────\n // We read the cache in the main thread rather than waiting for\n // an async SW postMessage. This gives instant, reliable status\n // updates regardless of SW message timing.\n const cached = cache ? await cache.match(url).catch(() => null) : null;\n\n // Treat cache as miss if maxAge exceeded\n const current = useEidosStore.getState().resources[url];\n const expired =\n config.maxAge !== undefined &&\n current?.cachedAt !== undefined &&\n Date.now() - current.cachedAt > config.maxAge;\n\n if (cached && !expired) {\n store.updateResource(url, {\n status: 'fresh',\n lastEvent: 'cache-hit',\n cacheHits: (current?.cacheHits ?? 0) + 1,\n });\n\n // Background revalidation for SWR (stale-while-revalidate)\n if (strategy.swStrategy === 'stale-while-revalidate') {\n fetch(url, { signal: AbortSignal.timeout(5000) })\n .then(async (resp) => {\n if (resp.ok && cache) {\n await cache.put(url, resp.clone());\n useEidosStore.getState().updateResource(url, {\n cachedAt: Date.now(),\n lastEvent: 'cache-updated',\n });\n }\n })\n .catch(() => {\n /* offline or timed out — cached version stays valid */\n });\n }\n\n return cached;\n }\n\n // Cache miss (or expired)\n const storeEntry = useEidosStore.getState().resources[url];\n store.updateResource(url, {\n cacheMisses: (storeEntry?.cacheMisses ?? 0) + 1,\n });\n }\n\n const response = await fetch(url);\n\n if (response.ok) {\n if (cache) await cache.put(url, response.clone());\n store.updateResource(url, {\n status: 'fresh',\n cachedAt: Date.now(),\n lastEvent: 'cache-updated',\n });\n return response;\n }\n\n // Non-2xx response (e.g. 503 from offline SW) — update status and throw\n // so callers get a proper error instead of a plain-object body they can't use.\n store.updateResource(url, { status: response.status === 503 ? 'offline' : 'error' });\n\n // Check if the SW tagged this as an offline response\n const isOffline = response.headers.get('X-Eidos-Offline') === 'true';\n throw new Error(\n isOffline\n ? `offline: no cached response for ${url}`\n : `${response.status} ${response.statusText}`,\n );\n } catch (err) {\n // Network failure — try cache one more time as fallback\n const fallback = cache ? await cache.match(url).catch(() => null) : null;\n\n if (fallback) {\n const current = useEidosStore.getState().resources[url];\n store.updateResource(url, {\n status: 'fresh',\n lastEvent: 'cache-hit',\n cacheHits: (current?.cacheHits ?? 0) + 1,\n });\n return fallback;\n }\n\n store.updateResource(url, { status: 'error' });\n throw err;\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Strategy derivation — intent → deterministic caching strategy\n// ─────────────────────────────────────────────────────────────────────────────\n\nfunction deriveStrategy(config: ResourceConfig): GeneratedStrategy {\n const explicit = config.strategy;\n if (config.offline) {\n return buildStrategy(explicit ?? 'stale-while-revalidate', config.cacheName, config.version);\n }\n return buildStrategy(explicit ?? 'network-first', config.cacheName, config.version);\n}\n\n// Strategy display names — always included (tiny, used by devtools).\nconst STRATEGY_NAMES: Record<CacheStrategy, string> = {\n 'stale-while-revalidate': 'StaleWhileRevalidate',\n 'cache-first': 'CacheFirst',\n 'network-first': 'NetworkFirst',\n};\n\n// Heavy descriptive strings — stripped from production bundles by Vite's\n// import.meta.env.DEV dead-code elimination. Only the names above ship in prod.\ntype StrategyDevInfo = Pick<GeneratedStrategy, 'reasoning' | 'behavior' | 'equivalentCode'>;\nconst _STRATEGY_DEV_META: Record<CacheStrategy, StrategyDevInfo> = {\n 'stale-while-revalidate': {\n reasoning:\n 'offline: true signals resilience. SWR returns cached data instantly while revalidating in the background — the best tradeoff between speed and freshness for offline-capable resources.',\n behavior: [\n 'Cache hit → return immediately, kick off background revalidation',\n 'Cache miss → fetch from network, cache the response, return it',\n 'Offline → return cached version if available, 503 if not',\n 'Reconnect → next request triggers a background refresh',\n ],\n equivalentCode: `// Workbox equivalent\nnew StaleWhileRevalidate({\n cacheName: 'eidos-resources-v1',\n plugins: [new ExpirationPlugin({ maxEntries: 60 })],\n})`,\n },\n 'cache-first': {\n reasoning:\n 'cache-first maximises speed and offline availability. Network is consulted only on cache miss. Best for static or infrequently-updated data.',\n behavior: [\n 'Cache hit → return immediately, no network request made',\n 'Cache miss → fetch from network, cache the response, return it',\n 'Offline → return cached version, 503 if cache is empty',\n 'Cache never expires unless explicitly invalidated',\n ],\n equivalentCode: `// Workbox equivalent\nnew CacheFirst({\n cacheName: 'eidos-resources-v1',\n plugins: [new ExpirationPlugin({ maxEntries: 60 })],\n})`,\n },\n 'network-first': {\n reasoning:\n 'network-first prioritises fresh data. Cache acts as a safety net when offline. Best for frequently-updated resources where stale data causes problems.',\n behavior: [\n 'Always try network first',\n 'Network success → update cache, return fresh response',\n 'Network failure → fall back to cached version',\n 'Offline with empty cache → return 503 error response',\n ],\n equivalentCode: `// Workbox equivalent\nnew NetworkFirst({\n cacheName: 'eidos-resources-v1',\n networkTimeoutSeconds: 3,\n})`,\n },\n};\n\nfunction buildStrategy(\n swStrategy: CacheStrategy,\n cacheName?: string,\n version?: string | number,\n): GeneratedStrategy {\n const meta = _STRATEGY_DEV_META[swStrategy];\n const baseName = cacheName ?? 'eidos-resources-v1';\n return {\n name: STRATEGY_NAMES[swStrategy],\n swStrategy,\n cacheName: version !== undefined ? `${baseName}-v${version}` : baseName,\n // reasoning + behavior are rendered by the playground UI from live ResourceEntry objects —\n // keep them in all builds. equivalentCode is a static code block only used in DEV tools.\n reasoning: meta.reasoning,\n behavior: meta.behavior,\n equivalentCode: import.meta.env.DEV ? meta.equivalentCode : '',\n };\n}\n\n// ── warmCache ─────────────────────────────────────────────────────────────────\n\n/**\n * Bulk-prefetch an array of resource handles concurrently, warming the cache\n * for each one. Useful on login / app init when you know which resources the\n * user will need offline.\n *\n * Pattern handles (containing `*`, `**`, or `:param`) are silently skipped —\n * they match multiple URLs so there is no single URL to prefetch.\n *\n * @example\n * import { warmCache } from '@sweidos/eidos'\n *\n * // In EidosProvider's onReady, or after login:\n * const { warmed, failed } = await warmCache([products, userProfile, settings])\n */\nexport async function warmCache(handles: ResourceHandle[]): Promise<WarmCacheResult> {\n const results = await Promise.allSettled(handles.map((h) => h.prefetch()));\n const errors = results\n .filter((r): r is PromiseRejectedResult => r.status === 'rejected')\n .map((r) => r.reason);\n\n if (import.meta.env.DEV && errors.length > 0) {\n console.warn(`[eidos] warmCache: ${errors.length} handle(s) failed to prefetch`, errors);\n }\n\n return {\n warmed: results.filter((r) => r.status === 'fulfilled').length,\n failed: errors.length,\n errors,\n };\n}\n","import type { ActionQueueItem } from './types';\nimport type { QueueStorage } from './queue-storage';\n\nconst DB_NAME = 'eidos';\nconst DB_VERSION = 1;\nconst QUEUE_STORE = 'action-queue';\n\nlet _db: IDBDatabase | null = null;\n\nfunction openDB(): Promise<IDBDatabase> {\n if (_db) return Promise.resolve(_db);\n\n return new Promise((resolve, reject) => {\n const req = indexedDB.open(DB_NAME, DB_VERSION);\n\n req.onupgradeneeded = (event) => {\n const db = (event.target as IDBOpenDBRequest).result;\n if (!db.objectStoreNames.contains(QUEUE_STORE)) {\n const store = db.createObjectStore(QUEUE_STORE, { keyPath: 'id' });\n store.createIndex('status', 'status', { unique: false });\n store.createIndex('actionId', 'actionId', { unique: false });\n }\n };\n\n req.onsuccess = () => {\n _db = req.result;\n resolve(req.result);\n };\n\n req.onerror = () => reject(req.error);\n });\n}\n\nexport async function idbAddToQueue(item: ActionQueueItem): Promise<void> {\n const db = await openDB();\n return new Promise((resolve, reject) => {\n const tx = db.transaction(QUEUE_STORE, 'readwrite');\n tx.objectStore(QUEUE_STORE).add(item);\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n });\n}\n\nexport async function idbGetQueue(): Promise<ActionQueueItem[]> {\n const db = await openDB();\n return new Promise((resolve, reject) => {\n const tx = db.transaction(QUEUE_STORE, 'readonly');\n const req = tx.objectStore(QUEUE_STORE).getAll();\n req.onsuccess = () => resolve(req.result as ActionQueueItem[]);\n req.onerror = () => reject(req.error);\n });\n}\n\nexport async function idbUpdateQueueItem(\n id: string,\n update: Partial<ActionQueueItem>,\n): Promise<void> {\n const db = await openDB();\n return new Promise((resolve, reject) => {\n const tx = db.transaction(QUEUE_STORE, 'readwrite');\n const store = tx.objectStore(QUEUE_STORE);\n const get = store.get(id);\n get.onsuccess = () => {\n if (get.result) {\n store.put({ ...get.result, ...update });\n } else if (import.meta.env.DEV) {\n console.warn(\n `[eidos] idbUpdateQueueItem: item \"${id}\" not found — store/IDB may have diverged`,\n );\n }\n };\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n });\n}\n\nexport async function idbRemoveFromQueue(id: string): Promise<void> {\n const db = await openDB();\n return new Promise((resolve, reject) => {\n const tx = db.transaction(QUEUE_STORE, 'readwrite');\n tx.objectStore(QUEUE_STORE).delete(id);\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n });\n}\n\n// Uses the status index to fetch only pending/failed items — avoids a full\n// table scan when the queue has many succeeded/replaying entries.\nexport async function idbGetPendingItems(): Promise<ActionQueueItem[]> {\n const db = await openDB();\n\n function cursorToArray(status: string): Promise<ActionQueueItem[]> {\n return new Promise((resolve, reject) => {\n const tx = db.transaction(QUEUE_STORE, 'readonly');\n const index = tx.objectStore(QUEUE_STORE).index('status');\n const items: ActionQueueItem[] = [];\n const req = index.openCursor(IDBKeyRange.only(status));\n req.onsuccess = (e) => {\n const cursor = (e.target as IDBRequest<IDBCursorWithValue>).result;\n if (cursor) {\n items.push(cursor.value as ActionQueueItem);\n cursor.continue();\n } else resolve(items);\n };\n req.onerror = () => reject(req.error);\n });\n }\n\n const [pending, failed] = await Promise.all([cursorToArray('pending'), cursorToArray('failed')]);\n return [...pending, ...failed];\n}\n\nexport async function idbClearQueue(): Promise<void> {\n const db = await openDB();\n return new Promise((resolve, reject) => {\n const tx = db.transaction(QUEUE_STORE, 'readwrite');\n tx.objectStore(QUEUE_STORE).clear();\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n });\n}\n\n/** IndexedDB-backed QueueStorage implementation (default for browser environments). */\nexport const idbQueueStorage: QueueStorage = {\n add: idbAddToQueue,\n getAll: idbGetQueue,\n getPending: idbGetPendingItems,\n update: idbUpdateQueueItem,\n remove: idbRemoveFromQueue,\n clear: idbClearQueue,\n};\n","import type { ActionQueueItem } from './types';\n\nexport interface QueueStorage {\n add(item: ActionQueueItem): Promise<void>;\n getAll(): Promise<ActionQueueItem[]>;\n getPending(): Promise<ActionQueueItem[]>;\n update(id: string, patch: Partial<ActionQueueItem>): Promise<void>;\n remove(id: string): Promise<void>;\n clear(): Promise<void>;\n}\n\nlet _storage: QueueStorage | null = null;\n\n/** Override the default IndexedDB queue with a custom storage backend (e.g. AsyncStorage for React Native). */\nexport function setQueueStorage(s: QueueStorage): void {\n _storage = s;\n}\n\nexport function _getQueueStorage(): QueueStorage | null {\n return _storage;\n}\n","import { useEidosStore } from './store';\nimport type { ActionQueueItem } from './types';\n\nconst CHANNEL_NAME = 'eidos-queue-sync';\n\ntype QueueSyncMessage =\n | { type: 'update'; id: string; update: Partial<ActionQueueItem> }\n | { type: 'batchUpdate'; updates: Array<{ id: string; update: Partial<ActionQueueItem> }> }\n | { type: 'remove'; id: string };\n\nlet _channel: BroadcastChannel | null | undefined;\n\nfunction getChannel(): BroadcastChannel | null {\n if (_channel !== undefined) return _channel;\n _channel = typeof BroadcastChannel === 'undefined' ? null : new BroadcastChannel(CHANNEL_NAME);\n return _channel;\n}\n\n/**\n * Broadcasts a queue-item status change to other tabs sharing the same\n * IndexedDB queue. The replay-lock holder (see `replayQueue` in action.ts)\n * is the only tab that mutates queue-item status, so non-leader tabs would\n * otherwise show stale status until their own store re-hydrates.\n *\n * No-ops in environments without BroadcastChannel (React Native, old Safari).\n */\nexport function broadcastQueueSync(message: QueueSyncMessage): void {\n getChannel()?.postMessage(message);\n}\n\n/**\n * Applies queue-item status updates broadcast by the replay-lock holder to\n * this tab's store. Returns an unsubscribe function.\n */\nexport function subscribeQueueSync(): () => void {\n const channel = getChannel();\n if (!channel) return () => {};\n\n const handler = (event: MessageEvent<QueueSyncMessage>) => {\n const store = useEidosStore.getState();\n const message = event.data;\n switch (message.type) {\n case 'update':\n store.updateQueueItem(message.id, message.update);\n break;\n case 'batchUpdate':\n store.batchUpdateQueueItems(message.updates);\n break;\n case 'remove':\n store.removeQueueItem(message.id);\n break;\n }\n };\n\n channel.addEventListener('message', handler);\n return () => channel.removeEventListener('message', handler);\n}\n\n/** Test-only: reset the cached channel so each test gets a fresh instance. */\nexport function _resetQueueSyncChannel(): void {\n _channel?.close();\n _channel = undefined;\n}\n","import { useEidosStore } from './store';\nimport { getSwRegistration } from './sw-bridge';\nimport { idbQueueStorage } from './idb';\nimport { _getQueueStorage } from './queue-storage';\nimport { broadcastQueueSync } from './queue-sync';\nimport type { QueueStorage } from './queue-storage';\nimport { CURRENT_QUEUE_SCHEMA_VERSION } from './types';\nimport type {\n ActionConfig,\n ActionContext,\n ActionHandle,\n ActionFn,\n ActionQueueItem,\n ConflictConfig,\n ConflictContext,\n ConflictResolution,\n QueuedResult,\n ReplayResult,\n} from './types';\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst _actionRegistry = new Map<string, ActionFn<any[], any>>();\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst _rollbackRegistry = new Map<string, (...args: any[]) => void>();\nconst _conflictConfigRegistry = new Map<string, ConflictConfig>();\nconst _configRegistry = new Map<string, ActionConfig>();\n\n// In-flight AbortControllers for `cancellable` actions, keyed by idempotencyKey.\n// Populated for direct calls and replays alike; removed once the call settles.\nconst _inflightControllers = new Map<string, AbortController>();\n\nfunction qs(): QueueStorage {\n // idbQueueStorage is the default browser fallback when no custom storage is set.\n return _getQueueStorage() ?? idbQueueStorage;\n}\n\nfunction uid() {\n return crypto.randomUUID();\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction callWithContext<TArgs extends any[], TReturn>(\n fn: ActionFn<TArgs, TReturn>,\n args: TArgs,\n ctx: ActionContext,\n): Promise<TReturn> {\n return fn(...args, ctx);\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function action<TArgs extends any[], TReturn>(\n fn: ActionFn<TArgs, TReturn>,\n config: ActionConfig<TArgs>,\n): ActionHandle<TArgs, TReturn> {\n // || not ?? — fn.name can be '' (anonymous arrow fn) which ?? treats as a\n // valid value, causing all anonymous actions to share actionId ''.\n const baseId = config.name || fn.name || uid();\n const actionId = config.namespace ? `${config.namespace}::${baseId}` : baseId;\n\n if (import.meta.env.DEV && config.reliability === 'neverLose' && !config.name && !fn.name) {\n console.warn(\n `[eidos] action() registered with neverLose but no stable name was found (fn.name=\"${fn.name}\"). Pass config.name so queued items survive a page reload and can be replayed.`,\n );\n }\n\n if (_actionRegistry.has(actionId)) {\n throw new Error(\n `[eidos] duplicate action id \"${actionId}\" — an action with this id is already registered. Pass a unique config.name or config.namespace.`,\n );\n }\n\n // Registering here means the function is available for replay after\n // the user refreshes the page (actions are defined at module scope).\n _actionRegistry.set(actionId, fn as ActionFn<unknown[], unknown>);\n _configRegistry.set(actionId, config as ActionConfig);\n\n if (config.onRollback) {\n _rollbackRegistry.set(actionId, config.onRollback);\n }\n\n if (config.conflict) {\n if (\n import.meta.env.DEV &&\n (config.conflict.strategy === 'merge' || config.conflict.strategy === 'custom') &&\n !config.conflict.resolve\n ) {\n console.error(\n `[eidos] action \"${actionId}\" has conflict.strategy \"${config.conflict.strategy}\" but no resolve() — items will retry indefinitely on 4xx.`,\n );\n }\n _conflictConfigRegistry.set(actionId, config.conflict);\n }\n\n const wrapped = async (...args: TArgs): Promise<TReturn | QueuedResult> => {\n const { isOnline } = useEidosStore.getState();\n\n // Generated for every invocation — reused across every retry/replay of a\n // neverLose item, and used to key handle.cancel() for in-flight cancellable calls.\n const idempotencyKey = uid();\n\n let signal: AbortSignal | undefined;\n if (config.cancellable) {\n const controller = new AbortController();\n _inflightControllers.set(idempotencyKey, controller);\n signal = controller.signal;\n }\n\n const ctx: ActionContext = { idempotencyKey, attempt: 0, signal };\n\n config.onOptimistic?.(...args, ctx);\n\n try {\n if (config.reliability === 'neverLose') {\n if (!isOnline) {\n return persistAndQueue(actionId, actionId, args, config, idempotencyKey);\n }\n // Online + neverLose: execute, queue on failure\n try {\n return await callWithContext(fn, args, ctx);\n } catch (err) {\n if (isAbortError(err)) throw err;\n return persistAndQueue(actionId, actionId, args, config, idempotencyKey);\n }\n }\n\n // best-effort: execute directly, rollback on failure\n try {\n return await callWithContext(fn, args, ctx);\n } catch (err) {\n config.onRollback?.(...args, ctx);\n throw err;\n }\n } finally {\n if (config.cancellable) _inflightControllers.delete(idempotencyKey);\n }\n };\n\n Object.defineProperty(wrapped, 'id', { value: actionId, writable: false });\n Object.defineProperty(wrapped, 'config', { value: config, writable: false });\n Object.defineProperty(wrapped, 'cancel', { value: cancelByIdempotencyKey, writable: false });\n\n return wrapped as unknown as ActionHandle<TArgs, TReturn>;\n}\n\n/**\n * Cancel an invocation by its `idempotencyKey` (from `ActionContext` /\n * `onOptimistic`). Aborts the in-flight call if `cancellable: true` and\n * still running, otherwise removes a not-yet-replayed queued item.\n * Returns `true` if something was cancelled/removed.\n *\n * Shared by every `ActionHandle.cancel()` and by devtools, which can't\n * address a specific handle from a queue item alone.\n */\nexport async function cancelByIdempotencyKey(idempotencyKey: string): Promise<boolean> {\n const controller = _inflightControllers.get(idempotencyKey);\n if (controller) {\n controller.abort();\n return true;\n }\n\n // Not in flight — check for a not-yet-replayed queued item with this key.\n const items = await qs().getAll();\n const item = items.find((i) => i.idempotencyKey === idempotencyKey && i.status === 'pending');\n if (!item) return false;\n\n useEidosStore.getState().removeQueueItem(item.id);\n broadcastQueueSync({ type: 'remove', id: item.id });\n await qs().remove(item.id);\n return true;\n}\n\n/**\n * Reset a `'failed'` queue item back to `'pending'` so the next\n * `replayQueue()` retries it — clears `error`/`nextRetryAt` and resets\n * `retryCount` to 0. Returns `true` if the item existed and was failed.\n * Used by devtools' per-item \"Retry\" action.\n */\nexport async function requeueItem(id: string): Promise<boolean> {\n const items = await qs().getAll();\n const item = items.find((i) => i.id === id);\n if (!item || item.status !== 'failed') return false;\n\n const update: Partial<ActionQueueItem> = {\n status: 'pending',\n error: undefined,\n nextRetryAt: undefined,\n retryCount: 0,\n };\n useEidosStore.getState().updateQueueItem(id, update);\n broadcastQueueSync({ type: 'update', id, update });\n await qs().update(id, update);\n return true;\n}\n\nfunction isJsonSerializable(value: unknown): boolean {\n try {\n JSON.stringify(value);\n return true;\n } catch {\n return false;\n }\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nasync function persistAndQueue<TArgs extends any[]>(\n actionId: string,\n actionName: string,\n args: TArgs,\n config: ActionConfig<TArgs>,\n idempotencyKey: string,\n): Promise<QueuedResult> {\n if (import.meta.env.DEV && !isJsonSerializable(args)) {\n console.warn(\n `[eidos] action \"${actionName}\" queued with non-JSON-serializable args. These args will be lost after a page reload. Use plain JSON values for neverLose actions.`,\n args,\n );\n }\n\n const id = uid();\n const item: ActionQueueItem = {\n schemaVersion: CURRENT_QUEUE_SCHEMA_VERSION,\n id,\n actionId,\n actionName,\n idempotencyKey,\n args,\n queuedAt: Date.now(),\n retryCount: 0,\n maxRetries: config.maxRetries ?? 3,\n status: 'pending',\n priority: config.priority ?? 'normal',\n };\n\n await qs().add(item);\n useEidosStore.getState().addQueueItem(item);\n useEidosStore.getState().recordReliabilityEvent('queued');\n\n // Register Background Sync tag so the browser can wake up open clients\n // when connectivity returns, even if the user navigated away briefly.\n // Graceful no-op when Background Sync is unsupported.\n try {\n const reg = getSwRegistration();\n if (reg && 'sync' in reg) {\n await (reg as unknown as { sync: { register(tag: string): Promise<void> } }).sync.register(\n 'eidos-queue-replay',\n );\n }\n } catch {\n // Background Sync not available — online-event replay remains the fallback\n }\n\n return {\n queued: true,\n id,\n message: `\"${actionName}\" queued — will execute when online`,\n };\n}\n\nfunction isAbortError(err: unknown): boolean {\n return err instanceof DOMException && err.name === 'AbortError';\n}\n\nfunction isClientError(err: unknown): boolean {\n if (err instanceof Response) return err.status >= 400 && err.status < 500;\n if (typeof err === 'object' && err !== null) {\n const s = (err as Record<string, unknown>).status;\n if (typeof s === 'number') return s >= 400 && s < 500;\n }\n return false;\n}\n\n// Base delay 2s, doubles per retry, capped at 5 minutes, ±20% jitter\nfunction backoffMs(retryCount: number): number {\n const base = Math.min(2000 * 2 ** retryCount, 300_000);\n return base * (0.8 + Math.random() * 0.4);\n}\n\nfunction emptyReplayResult(): ReplayResult {\n return {\n attempted: 0,\n succeeded: 0,\n failed: 0,\n retrying: 0,\n skipped: 0,\n conflicted: 0,\n cancelled: 0,\n };\n}\n\nlet _replaying = false;\nconst REPLAY_LOCK_NAME = 'eidos-queue-replay';\n\nexport async function replayQueue(): Promise<ReplayResult> {\n const store = useEidosStore.getState();\n if (!store.isOnline) return emptyReplayResult();\n\n // Web Locks coordinate replay across tabs sharing the same IndexedDB queue —\n // only the lock holder replays; other tabs no-op rather than re-executing\n // the same queued actions in parallel.\n if (typeof navigator !== 'undefined' && navigator.locks) {\n return navigator.locks.request(REPLAY_LOCK_NAME, { ifAvailable: true }, async (lock) => {\n if (!lock) return emptyReplayResult();\n return _doReplayQueue(store);\n });\n }\n\n // Fallback for environments without the Web Locks API (older Safari, React\n // Native, test runners) — guards against concurrent replay within this tab only.\n if (_replaying) return emptyReplayResult();\n _replaying = true;\n try {\n return await _doReplayQueue(store);\n } finally {\n _replaying = false;\n }\n}\n\ntype ItemOutcome = 'succeeded' | 'failed' | 'retrying' | 'skipped' | 'conflicted' | 'cancelled';\n\nasync function _markSucceeded(\n item: ActionQueueItem,\n store: ReturnType<typeof useEidosStore.getState>,\n): Promise<void> {\n const completedAt = Date.now();\n store.updateQueueItem(item.id, { status: 'succeeded', completedAt });\n store.recordReliabilityEvent('succeeded');\n broadcastQueueSync({ type: 'update', id: item.id, update: { status: 'succeeded', completedAt } });\n await qs().update(item.id, { status: 'succeeded', completedAt });\n\n // Remove from queue after a short delay so UI can show the success state briefly\n setTimeout(() => {\n store.removeQueueItem(item.id);\n broadcastQueueSync({ type: 'remove', id: item.id });\n qs().remove(item.id);\n }, 3000);\n}\n\n/**\n * Resolves a 4xx error against the action's conflict strategy.\n * Returns 'conflicted' if the item was dropped, undefined if normal\n * retry/fail logic should run (possibly with `item.args` rewritten by `merge`).\n */\nasync function _resolveConflict(\n item: ActionQueueItem,\n store: ReturnType<typeof useEidosStore.getState>,\n err: unknown,\n): Promise<ItemOutcome | undefined> {\n const conflictConfig = _conflictConfigRegistry.get(item.actionId);\n let resolution: ConflictResolution | undefined;\n\n if (conflictConfig) {\n switch (conflictConfig.strategy) {\n case 'serverWins':\n resolution = 'skip';\n break;\n case 'clientWins':\n resolution = 'retry';\n break;\n case 'merge':\n case 'custom': {\n const ctx: ConflictContext = {\n error: err,\n args: item.args as unknown[],\n attempt: item.retryCount,\n idempotencyKey: item.idempotencyKey,\n };\n resolution = conflictConfig.resolve?.(ctx) ?? 'retry';\n break;\n }\n }\n }\n\n if (resolution === 'skip') {\n store.removeQueueItem(item.id);\n store.recordReliabilityEvent('conflicted');\n broadcastQueueSync({ type: 'remove', id: item.id });\n await qs().remove(item.id);\n return 'conflicted';\n }\n if (resolution && typeof resolution === 'object') {\n item.args = resolution.resolved;\n store.updateQueueItem(item.id, { args: resolution.resolved });\n broadcastQueueSync({ type: 'update', id: item.id, update: { args: resolution.resolved } });\n await qs().update(item.id, { args: resolution.resolved });\n }\n // 'retry' (or merged args) falls through to normal retry/fail logic\n return undefined;\n}\n\nasync function _scheduleRetryOrFail(\n item: ActionQueueItem,\n store: ReturnType<typeof useEidosStore.getState>,\n err: unknown,\n): Promise<ItemOutcome> {\n const retryCount = item.retryCount + 1;\n if (retryCount >= item.maxRetries) {\n const update = { status: 'failed' as const, error: String(err), retryCount };\n store.updateQueueItem(item.id, update);\n store.recordReliabilityEvent('failed');\n broadcastQueueSync({ type: 'update', id: item.id, update });\n await qs().update(item.id, update);\n const ctx: ActionContext = { idempotencyKey: item.idempotencyKey, attempt: retryCount };\n _rollbackRegistry.get(item.actionId)?.(...(item.args as unknown[]), ctx);\n return 'failed';\n }\n\n const nextRetryAt = Date.now() + backoffMs(retryCount);\n const update = { status: 'pending' as const, retryCount, nextRetryAt };\n store.updateQueueItem(item.id, update);\n store.recordReliabilityEvent('retried');\n broadcastQueueSync({ type: 'update', id: item.id, update });\n await qs().update(item.id, update);\n return 'retrying';\n}\n\nasync function _replayItem(\n item: ActionQueueItem,\n store: ReturnType<typeof useEidosStore.getState>,\n): Promise<ItemOutcome> {\n const fn = _actionRegistry.get(item.actionId);\n if (!fn) return 'skipped';\n\n const cancellable = _configRegistry.get(item.actionId)?.cancellable;\n let signal: AbortSignal | undefined;\n if (cancellable) {\n const controller = new AbortController();\n _inflightControllers.set(item.idempotencyKey, controller);\n signal = controller.signal;\n }\n\n const ctx: ActionContext = {\n idempotencyKey: item.idempotencyKey,\n attempt: item.retryCount,\n signal,\n };\n\n try {\n await callWithContext(fn, item.args as unknown[], ctx);\n await _markSucceeded(item, store);\n return 'succeeded';\n } catch (err) {\n // Cancelled via handle.cancel(idempotencyKey) — drop the item, no rollback/retry.\n if (isAbortError(err)) {\n store.removeQueueItem(item.id);\n store.recordReliabilityEvent('cancelled');\n broadcastQueueSync({ type: 'remove', id: item.id });\n await qs().remove(item.id);\n return 'cancelled';\n }\n\n // 4xx: give the conflict strategy a chance to decide before normal retry/fail logic\n if (isClientError(err)) {\n const outcome = await _resolveConflict(item, store, err);\n if (outcome) return outcome;\n }\n\n return _scheduleRetryOrFail(item, store, err);\n } finally {\n if (cancellable) _inflightControllers.delete(item.idempotencyKey);\n }\n}\n\nasync function _replayTier(\n items: ActionQueueItem[],\n store: ReturnType<typeof useEidosStore.getState>,\n result: ReplayResult,\n): Promise<void> {\n if (items.length === 0) return;\n\n // Batch 'replaying' status update — N items → 1 store notify.\n // IDB write is fire-and-forget: on reload items stay 'pending', safe to re-replay.\n const replayable = items.filter((item) => _actionRegistry.has(item.actionId));\n result.skipped += items.length - replayable.length;\n\n if (replayable.length > 0) {\n const updates = replayable.map((item) => ({\n id: item.id,\n update: { status: 'replaying' as const },\n }));\n store.batchUpdateQueueItems(updates);\n broadcastQueueSync({ type: 'batchUpdate', updates });\n for (const item of replayable) {\n qs().update(item.id, { status: 'replaying' });\n }\n }\n\n const outcomes = await Promise.allSettled(replayable.map((item) => _replayItem(item, store)));\n\n for (const o of outcomes) {\n const outcome = o.status === 'fulfilled' ? o.value : 'failed';\n if (outcome === 'skipped') {\n result.skipped++;\n } else if (outcome === 'conflicted') {\n result.conflicted++;\n } else if (outcome === 'cancelled') {\n result.cancelled++;\n } else {\n result.attempted++;\n result[outcome]++;\n }\n }\n}\n\nasync function _doReplayQueue(\n store: ReturnType<typeof useEidosStore.getState>,\n): Promise<ReplayResult> {\n const candidates = await qs().getPending();\n const now = Date.now();\n // getPending() includes 'failed' items (for UI/queue-stats visibility), but\n // items that already exhausted maxRetries must not be auto-replayed again —\n // otherwise every reconnect re-executes the action and re-fires onRollback.\n // Those items stay 'failed' until the host app explicitly clears/re-queues them.\n const pending = candidates.filter(\n (item) => item.retryCount < item.maxRetries && (!item.nextRetryAt || item.nextRetryAt <= now),\n );\n\n const result: ReplayResult = emptyReplayResult();\n\n // Process tiers sequentially: high items complete before normal, normal before low.\n // Within each tier items run in parallel via Promise.allSettled.\n for (const tier of ['high', 'normal', 'low'] as const) {\n const tierItems = pending.filter((item) => (item.priority ?? 'normal') === tier);\n await _replayTier(tierItems, store, result);\n }\n\n return result;\n}\n\n/** Remove all items from the action queue (storage + in-memory store). */\nexport async function clearQueue(): Promise<void> {\n await qs().clear();\n useEidosStore.getState().hydrateQueue([]);\n}\n","import { useEidosStore } from './store';\nimport { replayQueue } from './action';\n\n/**\n * Subscribe to online/offline transitions and trigger replayQueue() on\n * reconnect, plus replay any pending items left over from a previous session.\n *\n * Shared by the web (runtime.ts) and React Native (runtime-rn.ts) init paths.\n *\n * WHY subscribe to the store instead of window.addEventListener('online'):\n * setOfflineSimulation() updates the store directly but never fires a real\n * browser `online` event. Watching the store catches both:\n * • Real network reconnects (sw-bridge updates store on window.online)\n * • Simulation toggled off (setOfflineSimulation(false) → store.setOnline(true))\n *\n * Returns an unsubscribe function.\n */\nexport function subscribeReplayOnReconnect(): () => void {\n let prevIsOnline = useEidosStore.getState().isOnline;\n\n const unsubscribe = useEidosStore.subscribe(() => {\n const { isOnline } = useEidosStore.getState();\n const justCameOnline = isOnline && !prevIsOnline;\n prevIsOnline = isOnline;\n\n if (justCameOnline) {\n // Small delay so the connection (or simulation reset) settles first\n setTimeout(replayQueue, 600);\n }\n });\n\n // Replay any pending items that survived a reload/restart.\n // 'failed' items have already exhausted maxRetries and are never\n // re-replayed (see _doReplayQueue), so they don't count here.\n const store = useEidosStore.getState();\n const hasPending = store.queue.some((q) => q.status === 'pending');\n if (store.isOnline && hasPending) {\n setTimeout(replayQueue, 1200);\n }\n\n return unsubscribe;\n}\n","import { registerServiceWorker, registerBgSyncHandler } from './sw-bridge';\nimport { replayQueue } from './action';\nimport { useEidosStore } from './store';\nimport { idbGetQueue, idbQueueStorage } from './idb';\nimport { _getQueueStorage } from './queue-storage';\nimport { subscribeReplayOnReconnect } from './replay';\nimport { subscribeQueueSync } from './queue-sync';\nimport { CURRENT_QUEUE_SCHEMA_VERSION } from './types';\nimport type { ActionQueueItem, ReliabilityStats } from './types';\n\n// Items persisted before idempotencyKey/schemaVersion existed (v1) are migrated\n// in place: assign a fresh idempotencyKey and bump schemaVersion. A fresh key on\n// first replay after upgrade is correct — these items were never sent with one.\nasync function migrateQueueItem(item: ActionQueueItem): Promise<ActionQueueItem> {\n if (item.schemaVersion === CURRENT_QUEUE_SCHEMA_VERSION && item.idempotencyKey) {\n return item;\n }\n const migrated: ActionQueueItem = {\n ...item,\n schemaVersion: CURRENT_QUEUE_SCHEMA_VERSION,\n idempotencyKey: item.idempotencyKey ?? crypto.randomUUID(),\n };\n const storage = _getQueueStorage() ?? idbQueueStorage;\n await storage\n .update(migrated.id, {\n schemaVersion: migrated.schemaVersion,\n idempotencyKey: migrated.idempotencyKey,\n })\n .catch(() => {\n // Best-effort persist — item still gets the migrated shape in memory this session\n });\n return migrated;\n}\n\nexport interface EidosConfig {\n /** Path to the eidos service worker. Defaults to '/eidos-sw.js'. */\n swPath?: string;\n /** Automatically replay the action queue on reconnect. Default: true. */\n autoReplay?: boolean;\n /**\n * Opt-in reliability telemetry. Called with a snapshot of cumulative\n * `neverLose` queue outcome counters (`ReliabilityStats`) every\n * `reliabilityReportInterval` ms — wire this up to your analytics backend.\n * Not called if omitted.\n */\n onReliabilityReport?: (stats: ReliabilityStats) => void;\n /** Interval (ms) between `onReliabilityReport` calls. Default: 60000. */\n reliabilityReportInterval?: number;\n}\n\nlet _initialized = false;\nlet _unsubscribe: (() => void) | null = null;\nlet _unsubscribeQueueSync: (() => void) | null = null;\nlet _reliabilityReportTimer: ReturnType<typeof setInterval> | null = null;\n\nexport async function initEidos(config: EidosConfig = {}): Promise<void> {\n // Skip silently during SSR — SW, IndexedDB, and window are browser-only.\n if (typeof window === 'undefined') return;\n if (_initialized) return;\n _initialized = true;\n\n const swPath = config.swPath ?? '/eidos-sw.js';\n const autoReplay = config.autoReplay ?? true;\n\n // Restore persisted queue from IndexedDB on startup\n try {\n const persisted = await idbGetQueue();\n if (persisted.length > 0) {\n const migrated = await Promise.all(persisted.map(migrateQueueItem));\n useEidosStore.getState().hydrateQueue(migrated);\n }\n } catch {\n // IndexedDB unavailable (Firefox private browsing) — silent fallback\n }\n\n try {\n await registerServiceWorker(swPath);\n } catch {\n // SW registration failed; app continues without offline support\n }\n\n // When the SW fires the Background Sync tag, replay the queue in the main thread.\n // This path runs even if the user briefly navigated away and back — the browser\n // triggers the sync event on the SW, which wakes up all open clients.\n registerBgSyncHandler(() => {\n if (useEidosStore.getState().isOnline) {\n setTimeout(replayQueue, 200);\n }\n });\n\n if (autoReplay) {\n _unsubscribe = subscribeReplayOnReconnect();\n }\n\n // Apply queue-item status changes broadcast by the replay-lock holder so\n // non-leader tabs reflect live status without waiting for re-hydration.\n _unsubscribeQueueSync = subscribeQueueSync();\n\n if (config.onReliabilityReport) {\n const interval = config.reliabilityReportInterval ?? 60_000;\n const report = config.onReliabilityReport;\n _reliabilityReportTimer = setInterval(() => {\n report(useEidosStore.getState().reliability);\n }, interval);\n }\n\n if (import.meta.env.DEV) {\n const store = useEidosStore.getState();\n console.groupCollapsed('%c⚡ Eidos', 'color:#22C55E;font-weight:bold');\n console.log('SW path :', swPath);\n console.log('Auto-replay:', autoReplay);\n console.log('SW status :', store.swStatus);\n console.groupEnd();\n }\n}\n\nexport function _resetEidos() {\n _unsubscribe?.();\n _unsubscribe = null;\n _unsubscribeQueueSync?.();\n _unsubscribeQueueSync = null;\n if (_reliabilityReportTimer) clearInterval(_reliabilityReportTimer);\n _reliabilityReportTimer = null;\n _initialized = false;\n}\n","import type { ActionQueueItem } from './types';\nimport type { QueueStorage } from './queue-storage';\n\n/** Minimal subset of @react-native-async-storage/async-storage (or any compatible key-value store). */\nexport interface AsyncStorageLike {\n getItem(key: string): Promise<string | null>;\n setItem(key: string, value: string): Promise<void>;\n removeItem(key: string): Promise<void>;\n}\n\nconst QUEUE_KEY = '@eidos:queue';\n\n/**\n * QueueStorage implementation backed by any AsyncStorage-compatible API.\n * Pass the AsyncStorage singleton from @react-native-async-storage/async-storage\n * (or MMKV, SQLite, or any store that satisfies AsyncStorageLike).\n */\nexport class AsyncStorageQueueStorage implements QueueStorage {\n constructor(private readonly storage: AsyncStorageLike) {}\n\n private async readAll(): Promise<ActionQueueItem[]> {\n try {\n const raw = await this.storage.getItem(QUEUE_KEY);\n if (!raw) return [];\n return JSON.parse(raw) as ActionQueueItem[];\n } catch {\n return [];\n }\n }\n\n private async writeAll(items: ActionQueueItem[]): Promise<void> {\n await this.storage.setItem(QUEUE_KEY, JSON.stringify(items));\n }\n\n async add(item: ActionQueueItem): Promise<void> {\n const items = await this.readAll();\n items.push(item);\n await this.writeAll(items);\n }\n\n async getAll(): Promise<ActionQueueItem[]> {\n return this.readAll();\n }\n\n async getPending(): Promise<ActionQueueItem[]> {\n const items = await this.readAll();\n return items.filter((i) => i.status === 'pending' || i.status === 'failed');\n }\n\n async update(id: string, patch: Partial<ActionQueueItem>): Promise<void> {\n const items = await this.readAll();\n const idx = items.findIndex((i) => i.id === id);\n if (idx !== -1) items[idx] = { ...items[idx], ...patch };\n await this.writeAll(items);\n }\n\n async remove(id: string): Promise<void> {\n const items = await this.readAll();\n await this.writeAll(items.filter((i) => i.id !== id));\n }\n\n async clear(): Promise<void> {\n await this.storage.removeItem(QUEUE_KEY);\n }\n}\n","import { useEffect, type ReactNode } from 'react';\nimport { initEidos, type EidosConfig } from '../runtime';\n\ninterface EidosProviderProps extends EidosConfig {\n children: ReactNode;\n}\n\n/**\n * Mount once at the root of your application.\n * Registers the service worker and initialises the Eidos runtime.\n *\n * @example\n * <EidosProvider swPath=\"/eidos-sw.js\">\n * <App />\n * </EidosProvider>\n */\nexport function EidosProvider({ children, swPath, autoReplay }: EidosProviderProps) {\n useEffect(() => {\n initEidos({ swPath, autoReplay });\n // Run once on mount only\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n return <>{children}</>;\n}\n","/**\n * Framework-agnostic reactive stores — compatible with Svelte's store protocol,\n * Vue's watchEffect, RxJS, and vanilla JS. Zero framework dependencies.\n *\n * Svelte: use the `$` prefix — `$eidosQueue`, `$eidosStatus`, etc.\n * Vue: call `.subscribe()` inside a composable with `onUnmounted` cleanup.\n * Vanilla: call `.subscribe(run)` directly; the return value unsubscribes.\n *\n * Each store calls its subscriber whenever any part of the Eidos state changes.\n * For fine-grained subscriptions, use `.getState()` to read the current snapshot\n * and compare manually in the subscriber callback.\n */\n\nimport { useEidosStore } from './store';\nimport type { EidosStore } from './store';\nimport { countQueueByStatus } from './types';\nimport type { ActionQueueItem, ResourceEntry, ReliabilityStats } from './types';\n\n// ── Readable<T> — compatible with Svelte's Readable interface ─────────────────\n\nexport interface EidosReadable<T> {\n /** Subscribe to value changes. Returns an unsubscribe function. */\n subscribe(run: (value: T) => void): () => void;\n /** Read the current value synchronously without subscribing. */\n getState(): T;\n}\n\nfunction shallowEqual<T extends Record<string, unknown>>(a: T, b: T): boolean {\n const keys = Object.keys(a) as (keyof T)[];\n if (keys.length !== Object.keys(b).length) return false;\n for (const k of keys) if (a[k] !== b[k]) return false;\n return true;\n}\n\n// Typed comparator alias so call sites don't need inline casts.\nfunction shallowEq<T extends Record<string, unknown>>(a: T, b: T): boolean {\n return shallowEqual(a, b);\n}\n\nfunction readable<T>(\n selector: (s: EidosStore) => T,\n equal: (a: T, b: T) => boolean = Object.is,\n): EidosReadable<T> {\n return {\n subscribe(run) {\n // Emit current value immediately (Svelte store contract)\n let last = selector(useEidosStore.getState());\n run(last);\n return useEidosStore.subscribe(() => {\n const next = selector(useEidosStore.getState());\n if (!equal(last, next)) {\n last = next;\n run(next);\n }\n });\n },\n getState() {\n return selector(useEidosStore.getState());\n },\n };\n}\n\n// ── Static stores (created once at module scope) ──────────────────────────────\n\n/** Full Eidos state snapshot. Prefer the narrower stores below. */\nexport const eidosStore: EidosReadable<EidosStore> = readable((s) => s);\n\n/** The action queue. Re-notifies on every queue mutation. */\nexport const eidosQueue: EidosReadable<ActionQueueItem[]> = readable((s) => s.queue);\n\n/**\n * Online status + SW lifecycle.\n * Only re-emits when isOnline, swStatus, or swError actually changes.\n */\nexport const eidosStatus: EidosReadable<{\n isOnline: boolean;\n swStatus: EidosStore['swStatus'];\n swError: string | undefined;\n}> = readable(\n (s) => ({ isOnline: s.isOnline, swStatus: s.swStatus, swError: s.swError }),\n shallowEq,\n);\n\n/**\n * Queue counts. Re-emits only when a count actually changes, not on every\n * queue mutation (e.g. a status transition that doesn't change counts is skipped).\n */\nexport const eidosQueueStats: EidosReadable<{\n pending: number;\n failed: number;\n replaying: number;\n total: number;\n}> = readable((s) => countQueueByStatus(s.queue), shallowEq);\n\n/**\n * Cumulative, session-scoped `neverLose` queue outcome counters — opt-in\n * reliability telemetry. Re-emits only when a counter changes. See\n * `EidosConfig.onReliabilityReport` to forward these to an analytics backend.\n */\nexport const eidosReliabilityStats: EidosReadable<ReliabilityStats> = readable(\n (s) => s.reliability,\n shallowEq,\n);\n\n// ── Dynamic stores (created per URL / ID) ─────────────────────────────────────\n\n/**\n * Live cache state for a single registered resource URL.\n * @example\n * // Svelte\n * const entry = eidosResource('/api/products')\n * $: hits = $entry?.cacheHits ?? 0\n */\nexport function eidosResource(url: string): EidosReadable<ResourceEntry | undefined> {\n return readable((s) => s.resources[url]);\n}\n\n/**\n * Live state for a single queue item by ID. Returns `undefined` once the item\n * is removed from the queue (after a successful replay or `clearQueue()`).\n * @example\n * // Svelte\n * const item = eidosAction(queuedResult.id)\n * $: status = $item?.status // 'pending' | 'replaying' | 'succeeded' | 'failed' | undefined\n */\nexport function eidosAction(id: string): EidosReadable<ActionQueueItem | undefined> {\n return readable((s) => s.queue.find((item) => item.id === id));\n}\n\n/**\n * Calls `callback` once each time the action queue drains from non-empty → 0.\n * Framework-agnostic equivalent of `useEidosOnDrain` for Svelte/Vue/vanilla.\n * Returns an unsubscribe function.\n *\n * @example\n * // Svelte\n * onMount(() => onQueueDrain(() => toast.success('All offline actions synced!')))\n */\nexport function onQueueDrain(callback: () => void): () => void {\n let prev = useEidosStore.getState().queue.length;\n return useEidosStore.subscribe(() => {\n const total = useEidosStore.getState().queue.length;\n if (prev > 0 && total === 0) callback();\n prev = total;\n });\n}\n","import { useEffect, useRef, useSyncExternalStore } from 'react';\nimport { useEidosStore } from '../store';\nimport type { EidosStore } from '../store';\nimport { countQueueByStatus } from '../types';\nimport { onQueueDrain } from '../stores';\n\nfunction useStore(): EidosStore;\nfunction useStore<T>(selector: (state: EidosStore) => T): T;\nfunction useStore<T = EidosStore>(selector?: (state: EidosStore) => T): T {\n const fn = selector ?? ((s: EidosStore) => s as unknown as T);\n return useSyncExternalStore(useEidosStore.subscribe, () => fn(useEidosStore.getState()));\n}\n\n/** Full Eidos store — prefer the narrower hooks below for performance. */\nexport function useEidos() {\n return useStore();\n}\n\n/** All registered resources — only re-renders when the resources map changes, not on queue mutations. */\nexport function useEidosResources() {\n return useStore((s) => s.resources);\n}\n\n/** Live state for a single registered resource URL. */\nexport function useEidosResource(url: string) {\n return useStore((s) => s.resources[url]);\n}\n\n/** The current action queue. */\nexport function useEidosQueue() {\n return useStore((s) => s.queue);\n}\n\n/**\n * Live state for a single queue item by ID. Only re-renders when that specific\n * item changes — cheaper than `useEidosQueue().find(id)` which re-renders on\n * any queue mutation.\n */\nexport function useEidosAction(id: string) {\n return useStore((s) => s.queue.find((item) => item.id === id));\n}\n\n/**\n * Online + SW status — cheap subscription, safe to use in header components.\n * Three separate primitive selectors so each only triggers a re-render when\n * its own value changes (no object-reference churn from a combined selector).\n */\nexport function useEidosStatus() {\n const isOnline = useStore((s) => s.isOnline);\n const swStatus = useStore((s) => s.swStatus);\n const swError = useStore((s) => s.swError);\n return { isOnline, swStatus, swError };\n}\n\n/**\n * Queue counts — single subscription, single loop. Re-renders only when a\n * count changes, not on every queue mutation. Use for badges and status bars\n * instead of `useEidosQueue()` when you only need numbers, not full items.\n */\nexport function useEidosQueueStats() {\n // Encode as a comma-separated string so useSyncExternalStore's Object.is\n // comparison bails out correctly when counts haven't changed. One loop,\n // one subscription — cheaper than four separate filter() passes.\n const encoded = useStore((s) => {\n const { pending, failed, replaying, total } = countQueueByStatus(s.queue);\n return `${pending},${failed},${replaying},${total}`;\n });\n const [p, f, r, t] = encoded.split(',');\n return { pending: +p, failed: +f, replaying: +r, total: +t };\n}\n\n/**\n * Calls `callback` once each time the action queue drains from non-empty → 0.\n * Stable callback reference not required — always calls the latest version.\n * Use for \"all offline actions synced!\" toasts.\n *\n * @example\n * useEidosOnDrain(() => toast.success('All offline actions synced!'))\n */\n/**\n * Cumulative, session-scoped `neverLose` queue outcome counters — opt-in\n * reliability telemetry for dashboards/devtools. Re-renders only when a\n * counter changes.\n */\nexport function useEidosReliabilityStats() {\n return useStore((s) => s.reliability);\n}\n\nexport function useEidosOnDrain(callback: () => void) {\n const callbackRef = useRef(callback);\n\n useEffect(() => {\n callbackRef.current = callback;\n });\n\n useEffect(() => onQueueDrain(() => callbackRef.current()), []);\n}\n","export const VERSION = '2.2.0';\n"],"mappings":"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"}
1
+ {"version":3,"file":"eidos.cjs","names":[],"sources":["../src/types.ts","../src/store-slices.ts","../src/store.ts","../src/sw-bridge.ts","../src/resource.ts","../src/idb.ts","../src/queue-storage.ts","../src/queue-sync.ts","../src/action.ts","../src/replay.ts","../src/runtime.ts","../src/async-storage-adapter.ts","../src/react/Provider.tsx","../src/stores.ts","../src/react/hooks.ts","../src/version.ts","../src/debug.ts"],"sourcesContent":["// ─────────────────────────────────────────────────────────────────────────────\n// Eidos Core Types\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport type CacheStrategy = 'cache-first' | 'stale-while-revalidate' | 'network-first';\n\n// ── Resource ─────────────────────────────────────────────────────────────────\n\nexport interface ResourceConfig {\n /** Make this resource available when the device is offline. */\n offline: boolean;\n /** Override the auto-selected caching strategy. */\n strategy?: CacheStrategy;\n /** Custom cache bucket name. Defaults to 'eidos-resources-v1'. */\n cacheName?: string;\n /**\n * Cache schema version. Bump when the response shape changes so old\n * cached entries (a different shape) aren't served from a stale bucket.\n * Appended to `cacheName` as a suffix (e.g. `eidos-resources-v1-v2`).\n * Old-versioned buckets are left in Cache Storage — clear them via\n * `caches.delete()` if needed.\n */\n version?: string | number;\n /** Max age of cached response in milliseconds. Expired entries trigger a network fetch. */\n maxAge?: number;\n /**\n * Maximum number of entries to keep in this resource's cache bucket.\n * When the limit is exceeded after a `cache.put()`, the oldest entry is evicted (FIFO).\n * Useful for list/image endpoints that grow unbounded.\n */\n maxEntries?: number;\n}\n\nexport interface GeneratedStrategy {\n name: string;\n swStrategy: CacheStrategy;\n cacheName: string;\n /** One-line rationale for why this strategy was chosen. */\n reasoning: string;\n /** Human-readable description of each behavioral step. */\n behavior: string[];\n /** Pseudocode showing the equivalent Workbox config. */\n equivalentCode: string;\n}\n\nexport interface ResourceEntry {\n url: string;\n config: ResourceConfig;\n strategy: GeneratedStrategy;\n status: 'idle' | 'fetching' | 'fresh' | 'stale' | 'error' | 'offline';\n cachedAt?: number;\n fetchedAt?: number;\n cacheHits: number;\n cacheMisses: number;\n lastEvent?: 'cache-hit' | 'cache-updated' | 'network-error' | 'cache-cleared';\n}\n\nexport interface ResourceHandle<T = unknown> {\n readonly url: string;\n readonly config: ResourceConfig;\n readonly strategy: GeneratedStrategy;\n fetch: () => Promise<Response>;\n json: () => Promise<T>;\n /** Returns a TanStack Query-compatible options object. */\n query: () => { queryKey: [string, string]; queryFn: () => Promise<T> };\n prefetch: () => Promise<void>;\n invalidate: () => Promise<void>;\n /** Remove from registry and SW. Required before re-registering the same URL with different config. */\n unregister: () => void;\n}\n\n/**\n * Handle for a URL pattern (`/api/products/*`, `/api/users/:id`, `**`).\n * The SW intercepts all matching requests automatically — there is no single\n * URL to fetch/cache directly, so only cache-management methods are exposed.\n * Returned by `resourcePattern()`.\n */\nexport interface PatternResourceHandle {\n readonly url: string;\n readonly config: ResourceConfig;\n readonly strategy: GeneratedStrategy;\n /** Clears all cache entries matching this pattern. */\n invalidate: () => Promise<void>;\n /** Remove from registry and SW. Required before re-registering the same pattern with different config. */\n unregister: () => void;\n}\n\n/** A handle returned by either `resource()` or `resourcePattern()`. */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type AnyResourceHandle = ResourceHandle<any> | PatternResourceHandle;\n\n/** Summary returned by warmCache(). */\nexport interface WarmCacheResult {\n /** Resources that were prefetched successfully. */\n warmed: number;\n /** Resources whose prefetch threw (network error, offline, etc.). */\n failed: number;\n /** The raw errors, in input order, for failed handles. */\n errors: unknown[];\n}\n\n// ── Action ───────────────────────────────────────────────────────────────────\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ninterface ActionConfigBase<TArgs extends any[] = any[]> {\n /** Max retry attempts before marking as failed. Default: 3. */\n maxRetries?: number;\n /** Human-readable name for the action (used in devtools). */\n name?: string;\n /**\n * Prefixes the registered action id (`namespace::name`). Use to avoid\n * collisions when two actions share a name (e.g. across micro-frontends,\n * or two `createOrder` actions in different modules) — without a\n * namespace, the second registration silently overwrites the first.\n */\n namespace?: string;\n /**\n * Replay order when multiple queued actions are pending.\n * `'high'` items replay before `'normal'`, which replay before `'low'`.\n * Each tier completes fully before the next tier begins.\n * Default: `'normal'`.\n */\n priority?: 'high' | 'normal' | 'low';\n /**\n * Called immediately before the async function executes, with the same args\n * plus a trailing `ActionContext`. Use to apply an optimistic UI update (add\n * item, mark as pending, etc.) and to capture `idempotencyKey` for later\n * `handle.cancel(idempotencyKey)` calls. Called on every invocation —\n * online, offline, and during queue replay.\n */\n onOptimistic?: (...args: [...TArgs, ActionContext]) => void;\n /**\n * Called when the action permanently fails and will not be retried.\n * - `best-effort`: called on first throw.\n * - `neverLose`: called when `maxRetries` is exhausted (status → `'failed'`).\n * Use to revert the optimistic update.\n */\n onRollback?: (...args: [...TArgs, ActionContext]) => void;\n /**\n * Declarative conflict-resolution strategy used during queue replay when\n * the server responds with a 4xx status (conflict, gone, unprocessable,\n * etc.). If not provided, 4xx errors are treated identically to other\n * errors (retried until `maxRetries` is exhausted, then `onRollback` is\n * called). See `ConflictConfig`.\n */\n conflict?: ConflictConfig;\n /**\n * When `true`, each invocation gets an `AbortController` whose `signal` is\n * passed via `ActionContext.signal`. Forward it to `fetch`/etc. so\n * `handle.cancel(idempotencyKey)` can abort an in-flight call, or remove a\n * not-yet-replayed queued item.\n */\n cancellable?: boolean;\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type ActionConfig<TArgs extends any[] = any[]> =\n | (ActionConfigBase<TArgs> & {\n /** Call directly, no persistence on failure. */\n reliability: 'best-effort';\n })\n | (ActionConfigBase<TArgs> & {\n /** Persist to IndexedDB before executing; replay on reconnect. */\n reliability: 'neverLose';\n /**\n * Required for `neverLose` — queued items must survive a page reload\n * and be matched back to this action on replay. `fn.name` is not\n * reliable (minifiers rename it, arrow functions may be anonymous).\n */\n name: string;\n });\n\n/**\n * Passed to `ConflictConfig.resolve` (for `'merge'`/`'custom'` strategies)\n * when a queued action's replay receives a 4xx response.\n */\nexport interface ConflictContext {\n /** Whatever `fn` threw — typically a `Response` or an error with `.status`. */\n error: unknown;\n /** The original arguments the action was queued with. */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n args: any[];\n /** Number of replay attempts so far (0 on first replay). */\n attempt: number;\n idempotencyKey: string;\n}\n\n/**\n * Outcome of `ConflictConfig.resolve`:\n * - `'retry'`: keep the item queued, retry per normal backoff.\n * - `'skip'`: silently remove the item (no `onRollback`).\n * - `{ resolved: args }`: replace the queued args and retry immediately\n * on the next replay pass.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type ConflictResolution = 'retry' | 'skip' | { resolved: any[] };\n\nexport interface ConflictConfig {\n /**\n * - `'serverWins'`: drop the queued item, keeping the server's state.\n * - `'clientWins'`: keep retrying — the client's write should eventually\n * be accepted (e.g. once the server-side conflict is cleared).\n * - `'merge'` / `'custom'`: call `resolve` to decide.\n */\n strategy: 'serverWins' | 'clientWins' | 'merge' | 'custom';\n /** Required for `'merge'` and `'custom'`. */\n resolve?: (ctx: ConflictContext) => ConflictResolution;\n}\n\n/** Bump when ActionQueueItem's shape changes. Used to migrate items persisted by older versions. */\nexport const CURRENT_QUEUE_SCHEMA_VERSION = 2;\n\nexport interface ActionQueueItem {\n /** Shape version this item was persisted with. Items from before v2 are migrated on load. */\n schemaVersion: number;\n id: string;\n /** ID of the registered action (maps to the function in the registry). */\n actionId: string;\n actionName: string;\n /**\n * Stable per-invocation key, generated once and reused across every retry/replay\n * of this item. Pass to your server as an idempotency key so retries that reach\n * the server after a dropped response don't double-execute.\n */\n idempotencyKey: string;\n args: unknown[];\n queuedAt: number;\n retryCount: number;\n maxRetries: number;\n status: 'pending' | 'replaying' | 'succeeded' | 'failed';\n /** Replay priority. High items replay before normal, normal before low. Default: 'normal'. */\n priority?: 'high' | 'normal' | 'low';\n error?: string;\n completedAt?: number;\n /** Earliest timestamp at which this item should be retried (exponential backoff). */\n nextRetryAt?: number;\n}\n\nexport interface QueuedResult {\n readonly queued: true;\n readonly id: string;\n readonly message: string;\n}\n\n/** Summary returned by replayQueue(). */\nexport interface ReplayResult {\n /** Items where the registered function was found and called. */\n attempted: number;\n /** Items that resolved successfully. */\n succeeded: number;\n /** Items that failed and have no retries remaining (status: 'failed'). */\n failed: number;\n /** Items that failed but will be retried later (nextRetryAt set). */\n retrying: number;\n /** Items whose actionId had no registered function — likely not yet imported. */\n skipped: number;\n /** Items that received a 4xx response and were dropped via `conflict: { strategy: 'serverWins' }` (or `resolve()` returning `'skip'`). */\n conflicted: number;\n /** Items removed via `handle.cancel(idempotencyKey)` before/during replay. */\n cancelled: number;\n}\n\n/**\n * Passed as an extra argument after the declared params to `neverLose` actions,\n * on every invocation (initial call, offline queue, and replay). The same\n * `idempotencyKey` is reused across all retries of one logical invocation —\n * forward it to your server (e.g. as an `Idempotency-Key` header) so a retry\n * that reaches the server after a dropped response doesn't double-execute.\n */\nexport interface ActionContext {\n idempotencyKey: string;\n /** 0 on the first attempt, incremented on each replay retry. */\n attempt: number;\n /** Set when `config.cancellable` is true. Forward to `fetch`/etc. for cancellation support. */\n signal?: AbortSignal;\n}\n\n/**\n * Every action function receives its declared args plus a trailing\n * `ActionContext` — on every invocation (online, offline, and replay).\n */\nexport type ActionFn<TArgs extends unknown[], TReturn> = (\n ...args: [...TArgs, ActionContext]\n) => Promise<TReturn>;\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport interface ActionHandle<TArgs extends any[], TReturn> {\n (...args: TArgs): Promise<TReturn | QueuedResult>;\n readonly id: string;\n readonly config: ActionConfig;\n /**\n * Cancel an invocation by its `idempotencyKey` (from `ActionContext` /\n * `onOptimistic`). Aborts the in-flight call if `cancellable: true` and\n * still running, otherwise removes a not-yet-replayed queued item.\n * Returns `true` if something was cancelled/removed.\n */\n cancel: (idempotencyKey: string) => Promise<boolean>;\n}\n\n// ── Global State ─────────────────────────────────────────────────────────────\n\nexport interface EidosState {\n isOnline: boolean;\n swStatus: 'idle' | 'registering' | 'active' | 'error' | 'unsupported';\n swError?: string;\n resources: Record<string, ResourceEntry>;\n queue: ActionQueueItem[];\n reliability: ReliabilityStats;\n}\n\n/**\n * Cumulative, session-scoped counters for `neverLose` queue outcomes — opt-in\n * telemetry surfaced via `eidosReliabilityStats` / `useEidosReliabilityStats()`\n * and `EidosConfig.onReliabilityReport`. Reset on page reload (not persisted).\n */\nexport interface ReliabilityStats {\n [key: string]: number;\n /** Actions persisted to the queue (offline, or online call that threw). */\n queued: number;\n /** Queue items that executed successfully (first attempt or a retry). */\n succeeded: number;\n /** Queue items that exhausted `maxRetries` and moved to `'failed'`. */\n failed: number;\n /** Replay attempts that failed but will retry (haven't exhausted `maxRetries`). */\n retried: number;\n /** Queue items dropped by a `serverWins`/`merge`/`custom` conflict resolution. */\n conflicted: number;\n /** Queue items removed via `handle.cancel(idempotencyKey)` before replay completed. */\n cancelled: number;\n}\n\nexport function emptyReliabilityStats(): ReliabilityStats {\n return { queued: 0, succeeded: 0, failed: 0, retried: 0, conflicted: 0, cancelled: 0 };\n}\n\nexport interface QueueStatusCounts {\n [key: string]: number;\n pending: number;\n failed: number;\n replaying: number;\n total: number;\n}\n\n/** Single pass over the queue — avoids separate .filter() calls per status. */\nexport function countQueueByStatus(queue: ActionQueueItem[]): QueueStatusCounts {\n let pending = 0,\n failed = 0,\n replaying = 0;\n for (const q of queue) {\n if (q.status === 'pending') pending++;\n else if (q.status === 'failed') failed++;\n else if (q.status === 'replaying') replaying++;\n }\n return { pending, failed, replaying, total: queue.length };\n}\n","import type { ResourceEntry, ActionQueueItem, ReliabilityStats } from './types';\nimport { emptyReliabilityStats } from './types';\nimport type { EidosStore } from './store';\n\ntype Setter = (updater: (prev: EidosStore) => Partial<EidosStore>) => void;\n\n// ── Resource slice ────────────────────────────────────────────────────────────\n\nexport interface ResourceActions {\n registerResource: (url: string, entry: ResourceEntry) => void;\n updateResource: (url: string, update: Partial<ResourceEntry>) => void;\n unregisterResource: (url: string) => void;\n}\n\nexport function createResourceActions(set: Setter): ResourceActions {\n return {\n registerResource: (url, entry) => set((s) => ({ resources: { ...s.resources, [url]: entry } })),\n\n updateResource: (url, update) =>\n set((s) => ({\n resources: {\n ...s.resources,\n [url]: s.resources[url] ? { ...s.resources[url], ...update } : s.resources[url],\n },\n })),\n\n unregisterResource: (url) =>\n set((s) => ({\n resources: Object.fromEntries(Object.entries(s.resources).filter(([k]) => k !== url)),\n })),\n };\n}\n\n// ── Queue slice ───────────────────────────────────────────────────────────────\n\nexport interface QueueActions {\n addQueueItem: (item: ActionQueueItem) => void;\n updateQueueItem: (id: string, update: Partial<ActionQueueItem>) => void;\n batchUpdateQueueItems: (updates: Array<{ id: string; update: Partial<ActionQueueItem> }>) => void;\n removeQueueItem: (id: string) => void;\n hydrateQueue: (items: ActionQueueItem[]) => void;\n}\n\nexport function createQueueActions(set: Setter): QueueActions {\n return {\n addQueueItem: (item) => set((s) => ({ queue: [...s.queue, item] })),\n\n updateQueueItem: (id, update) =>\n set((s) => ({\n queue: s.queue.map((item) => (item.id === id ? { ...item, ...update } : item)),\n })),\n\n batchUpdateQueueItems: (updates) =>\n set((s) => {\n const map = new Map(updates.map((u) => [u.id, u.update]));\n return {\n queue: s.queue.map((item) => {\n const u = map.get(item.id);\n return u ? { ...item, ...u } : item;\n }),\n };\n }),\n\n removeQueueItem: (id) => set((s) => ({ queue: s.queue.filter((item) => item.id !== id) })),\n\n hydrateQueue: (items) => set(() => ({ queue: items })),\n };\n}\n\n// ── Reliability slice ─────────────────────────────────────────────────────────\n\nexport interface ReliabilityActions {\n recordReliabilityEvent: (event: keyof ReliabilityStats) => void;\n resetReliabilityStats: () => void;\n}\n\nexport function createReliabilityActions(set: Setter): ReliabilityActions {\n return {\n recordReliabilityEvent: (event) =>\n set((s) => ({ reliability: { ...s.reliability, [event]: s.reliability[event] + 1 } })),\n\n resetReliabilityStats: () => set(() => ({ reliability: emptyReliabilityStats() })),\n };\n}\n","import type { EidosState } from './types';\nimport { emptyReliabilityStats } from './types';\nimport {\n createResourceActions,\n createQueueActions,\n createReliabilityActions,\n} from './store-slices';\nimport type { ResourceActions, QueueActions, ReliabilityActions } from './store-slices';\n\nexport interface EidosStore extends EidosState, ResourceActions, QueueActions, ReliabilityActions {\n // Online\n setOnline: (online: boolean) => void;\n // SW\n setSwStatus: (status: EidosState['swStatus'], error?: string) => void;\n}\n\ntype Listener = () => void;\n\nlet _state: EidosStore;\nconst _listeners = new Set<Listener>();\n\nfunction _notify() {\n _listeners.forEach((fn) => fn());\n}\n\nfunction _set(updater: (prev: EidosStore) => Partial<EidosStore>) {\n _state = { ..._state, ...updater(_state) };\n _notify();\n}\n\n_state = {\n // navigator.onLine is undefined in React Native — default to true unless explicitly false\n isOnline: typeof navigator === 'undefined' || navigator.onLine !== false,\n swStatus: 'idle',\n swError: undefined,\n resources: {},\n queue: [],\n reliability: emptyReliabilityStats(),\n\n setOnline: (isOnline) => _set(() => ({ isOnline })),\n\n setSwStatus: (swStatus, swError) => _set(() => ({ swStatus, swError })),\n\n ...createResourceActions(_set),\n ...createQueueActions(_set),\n ...createReliabilityActions(_set),\n};\n\nfunction _getState() {\n return _state;\n}\n\nfunction _subscribe(listener: Listener) {\n _listeners.add(listener);\n return () => {\n _listeners.delete(listener);\n };\n}\n\nexport const useEidosStore = {\n getState: _getState,\n subscribe: _subscribe,\n // Test/devtools helper — merges partial state, preserves action methods.\n setState: (partial: Partial<EidosStore> | ((s: EidosStore) => Partial<EidosStore>)) => {\n const update = typeof partial === 'function' ? partial(_state) : partial;\n _state = { ..._state, ...update };\n _notify();\n },\n};\n","import { useEidosStore } from './store';\n\nlet _registration: ServiceWorkerRegistration | null = null;\n// Messages sent before the SW activates are buffered here and flushed once\n// the SW is ready. Covers resource registrations, cache clears, offline\n// simulation — anything sent at module scope before EidosProvider mounts.\nlet _pendingMessages: Record<string, unknown>[] = [];\n\nexport function getSwRegistration() {\n return _registration;\n}\n\ninterface SwRegistrationOptions {\n skipWaiting: boolean;\n onUpdateAvailable?: (registration: ServiceWorkerRegistration) => void;\n}\n\nexport async function registerServiceWorker(\n swPath: string,\n options: SwRegistrationOptions = { skipWaiting: true },\n): Promise<void> {\n if (typeof navigator === 'undefined' || !('serviceWorker' in navigator)) {\n useEidosStore.getState().setSwStatus('unsupported');\n if (import.meta.env.DEV) {\n console.warn(\n '[eidos] Service workers are not supported in this context. ' +\n 'Offline support and SW-side caching are disabled. ' +\n 'Service workers require a modern browser with a secure context (HTTPS or localhost).',\n );\n }\n return;\n }\n\n // Warn early when the browser will reject registration regardless — saves a round-trip\n // and gives a clearer message than the browser's generic SecurityError.\n if (import.meta.env.DEV && typeof window !== 'undefined' && !window.isSecureContext) {\n console.warn(\n `[eidos] Service workers require a secure context (HTTPS or localhost). ` +\n `initEidos() was called on \"${window.location.origin}\" — ` +\n `the browser will silently disable offline support. ` +\n `Switch to localhost for development or deploy to HTTPS.`,\n );\n }\n\n const store = useEidosStore.getState();\n store.setSwStatus('registering');\n\n try {\n _registration = await navigator.serviceWorker.register(swPath, { scope: '/' });\n\n await waitForActivation(_registration);\n\n store.setSwStatus('active');\n\n // Receive messages from SW\n navigator.serviceWorker.addEventListener('message', onSwMessage);\n\n // Track online/offline\n window.addEventListener('online', () => store.setOnline(true));\n window.addEventListener('offline', () => store.setOnline(false));\n\n flushPendingMessages();\n\n // Handle SW updates — the new SW waits for EIDOS_SKIP_WAITING from the page.\n _watchForUpdate(_registration, options);\n } catch (err) {\n store.setSwStatus('error', String(err));\n if (import.meta.env.DEV) {\n const errMsg = String(err).toLowerCase();\n const isNotFound =\n errMsg.includes('404') ||\n errMsg.includes('bad http response') ||\n errMsg.includes('not found') ||\n errMsg.includes('failed to load');\n if (isNotFound) {\n console.warn(\n `[eidos] Service worker file not found at \"${swPath}\". ` +\n `Did you add the eidos() plugin to your vite.config.ts? ` +\n `If you're not using Vite, copy the file manually: ` +\n `node_modules/@sweidos/eidos/dist/eidos-sw.js → public/eidos-sw.js`,\n );\n } else {\n console.warn(`[eidos] Service worker registration failed: ${err}`);\n }\n }\n }\n}\n\nfunction waitForActivation(reg: ServiceWorkerRegistration): Promise<void> {\n return new Promise((resolve) => {\n if (reg.active) {\n resolve();\n return;\n }\n const sw = reg.installing ?? reg.waiting;\n if (!sw) {\n resolve();\n return;\n }\n\n // Resolve after 10s regardless — another tab may be blocking activation\n const timer = setTimeout(resolve, 10_000);\n\n sw.addEventListener('statechange', function handler() {\n if (sw.state === 'activated') {\n clearTimeout(timer);\n sw.removeEventListener('statechange', handler);\n resolve();\n }\n });\n });\n}\n\nexport function sendToWorker(message: Record<string, unknown>): void {\n const sw = _registration?.active;\n if (sw) {\n sw.postMessage(message);\n } else {\n _pendingMessages.push(message);\n }\n}\n\nlet _bgSyncHandler: (() => void) | null = null;\n\nexport function registerBgSyncHandler(fn: () => void): void {\n _bgSyncHandler = fn;\n}\n\nexport function isBgSyncSupported(): boolean {\n try {\n return (\n typeof navigator !== 'undefined' &&\n 'serviceWorker' in navigator &&\n _registration !== null &&\n 'sync' in _registration\n );\n } catch {\n return false;\n }\n}\n\ninterface PushHandlers {\n onNotificationClick?: (data: unknown) => void;\n onSubscriptionExpired?: (sub: PushSubscriptionJSON) => void;\n}\n\nlet _pushHandlers: PushHandlers = {};\n\nexport function registerPushCallbacks(handlers: PushHandlers): void {\n _pushHandlers = handlers;\n}\n\nfunction onSwMessage(event: MessageEvent): void {\n const data = event.data as {\n type: string;\n url?: string;\n strategy?: string;\n data?: unknown;\n subscription?: unknown;\n };\n if (!data?.type) return;\n\n const store = useEidosStore.getState();\n const { type, url } = data;\n\n if (type === 'EIDOS_BACKGROUND_SYNC') {\n _bgSyncHandler?.();\n return;\n }\n\n if (type === 'EIDOS_NOTIFICATION_CLICK') {\n _pushHandlers.onNotificationClick?.(data.data);\n return;\n }\n\n if (type === 'EIDOS_SUBSCRIPTION_EXPIRED') {\n _pushHandlers.onSubscriptionExpired?.(data.subscription as PushSubscriptionJSON);\n return;\n }\n\n if (!url) return;\n\n switch (type) {\n case 'EIDOS_CACHE_HIT': {\n const current = store.resources[url];\n store.updateResource(url, {\n status: 'fresh',\n lastEvent: 'cache-hit',\n cacheHits: (current?.cacheHits ?? 0) + 1,\n });\n break;\n }\n case 'EIDOS_CACHE_UPDATED': {\n store.updateResource(url, {\n status: 'fresh',\n lastEvent: 'cache-updated',\n cachedAt: Date.now(),\n });\n break;\n }\n case 'EIDOS_NETWORK_ERROR': {\n store.updateResource(url, {\n status: 'error',\n lastEvent: 'network-error',\n });\n break;\n }\n }\n}\n\nexport function setOfflineSimulation(enabled: boolean): void {\n sendToWorker({ type: 'EIDOS_SIMULATE_OFFLINE', enabled });\n useEidosStore.getState().setOnline(!enabled);\n}\n\nfunction flushPendingMessages(): void {\n const sw = _registration?.active;\n if (!sw) return;\n for (const msg of _pendingMessages) sw.postMessage(msg);\n _pendingMessages = [];\n}\n\nfunction _watchForUpdate(reg: ServiceWorkerRegistration, options: SwRegistrationOptions): void {\n const notify = (r: ServiceWorkerRegistration) => {\n if (options.skipWaiting) {\n r.waiting?.postMessage({ type: 'EIDOS_SKIP_WAITING' });\n } else {\n options.onUpdateAvailable?.(r);\n }\n };\n\n // A SW may already be waiting on startup (installed across a previous page load\n // but blocked because another tab held the old SW active).\n if (reg.waiting && navigator.serviceWorker.controller) {\n notify(reg);\n }\n\n reg.addEventListener('updatefound', () => {\n const newSw = reg.installing;\n if (!newSw) return;\n newSw.addEventListener('statechange', () => {\n if (newSw.state === 'installed' && navigator.serviceWorker.controller) {\n notify(reg);\n }\n });\n });\n}\n\n/**\n * Tells the waiting service worker to activate immediately, then reloads the page.\n * Only relevant when `skipWaiting: false` — call this after the user confirms\n * a \"reload to update\" toast shown via `onUpdateAvailable`.\n */\nexport function triggerSwUpdate(): void {\n _registration?.waiting?.postMessage({ type: 'EIDOS_SKIP_WAITING' });\n}\n\n/** Test-only: resets module-level state between test cases. */\nexport function _resetSwBridgeForTests(): void {\n _registration = null;\n _pendingMessages = [];\n _bgSyncHandler = null;\n _pushHandlers = {};\n}\n","import { useEidosStore } from './store';\nimport { sendToWorker } from './sw-bridge';\nimport type {\n ResourceConfig,\n ResourceHandle,\n PatternResourceHandle,\n ResourceEntry,\n GeneratedStrategy,\n CacheStrategy,\n WarmCacheResult,\n} from './types';\n\nconst _registry = new Map<string, ResourceHandle | PatternResourceHandle>();\n\n// ── Request deduplication ─────────────────────────────────────────────────────\n// If multiple callers invoke handle.fetch() simultaneously for the same URL,\n// only one network request is made. Each caller gets its own cloned Response.\n// Keyed by URL; entry is deleted when the request settles.\nconst _inflightRequests = /* @__PURE__ */ new Map<string, Promise<Response>>();\n\n// ── TanStack Query bridge (optional) ─────────────────────────────────────────\n// Set by @sweidos/eidos/query when withEidosQueryClient() is called.\n// Lets handle.invalidate() also invalidate the matching TQ cache entry.\ntype QueryInvalidator = (queryKey: [string, string]) => void;\nlet _queryInvalidator: QueryInvalidator | null = null;\n\n/** @internal Called by @sweidos/eidos/query. */\nexport function setQueryInvalidator(fn: QueryInvalidator): void {\n _queryInvalidator = fn;\n}\n\n// ── URL pattern helpers ───────────────────────────────────────────────────────\n\n/** Returns true if `url` contains wildcard or :param segments. */\nfunction isPattern(url: string): boolean {\n return url.includes('*') || /:[^/]+/.test(url);\n}\n\n/**\n * Converts a URL pattern to a regex source string for SW fetch matching.\n * `**` → multi-segment wildcard (`.+`)\n * `*` → single-segment wildcard (`[^/]+`)\n * `:param` → named single segment (`[^/]+`)\n *\n * Special regex characters in the pattern (e.g. `.`) are escaped first so\n * they match literally.\n *\n * @example\n * patternToRegexStr('/api/products/*') // '^/api/products/[^/]+$'\n * patternToRegexStr('/api/products/**') // '^/api/products/.+$'\n * patternToRegexStr('/api/users/:id') // '^/api/users/[^/]+$'\n */\nfunction patternToRegexStr(pattern: string): string {\n // Escape all regex-special chars except `*`, `/`, `:` (handled below)\n const escaped = pattern.replace(/[.+?^${}()|[\\]\\\\]/g, '\\\\$&');\n return (\n '^' +\n escaped\n .replace(/\\*\\*/g, '.+') // ** → multi-segment wildcard\n .replace(/\\*/g, '[^/]+') // * → single-segment wildcard\n .replace(/:[^/]+/g, '[^/]+') + // :param → single-segment wildcard\n '$'\n );\n}\n\n/** Shared setup for resource()/resourcePattern(): strategy derivation, store + SW registration. */\nfunction _register(\n url: string,\n config: ResourceConfig,\n): { strategy: GeneratedStrategy; regexStr: string | undefined } {\n const strategy = deriveStrategy(config);\n const regexStr = isPattern(url) ? patternToRegexStr(url) : undefined;\n\n const entry: ResourceEntry = {\n url,\n config,\n strategy,\n status: 'idle',\n cacheHits: 0,\n cacheMisses: 0,\n };\n\n useEidosStore.getState().registerResource(url, entry);\n\n sendToWorker({\n type: 'EIDOS_REGISTER_RESOURCE',\n url,\n strategy: strategy.swStrategy,\n cacheName: strategy.cacheName,\n ...(regexStr !== undefined && { pattern: regexStr }),\n ...(config.maxAge !== undefined && { maxAge: config.maxAge }),\n ...(config.maxEntries !== undefined && { maxEntries: config.maxEntries }),\n });\n\n return { strategy, regexStr };\n}\n\nfunction _invalidate(\n url: string,\n strategy: GeneratedStrategy,\n regexStr: string | undefined,\n): () => Promise<void> {\n return async () => {\n sendToWorker({ type: 'EIDOS_CLEAR_CACHE', url });\n const cache = await caches.open(strategy.cacheName).catch(() => null);\n if (cache) {\n const keys = await cache.keys();\n const patternRe = regexStr ? new RegExp(regexStr) : null;\n const isCrossOrigin = url.startsWith('http');\n await Promise.all(\n keys\n .filter((r) => {\n const rUrl = r.url;\n const p = new URL(rUrl).pathname;\n if (patternRe) {\n // Cross-origin patterns were compiled from absolute URLs; test full URL.\n return patternRe.test(isCrossOrigin ? rUrl : p);\n }\n return isCrossOrigin ? rUrl === url : rUrl === url || p === url;\n })\n .map((r) => cache.delete(r)),\n );\n }\n // For exact-URL resources update the store entry; patterns don't have a\n // single entry to update (individual URLs are not tracked per-pattern).\n if (!isPattern(url)) {\n useEidosStore.getState().updateResource(url, {\n status: 'stale',\n cachedAt: undefined,\n lastEvent: 'cache-cleared',\n cacheHits: 0,\n cacheMisses: 0,\n });\n }\n // Notify TanStack Query bridge if registered.\n _queryInvalidator?.(['eidos', url]);\n };\n}\n\nfunction _unregister(url: string): () => void {\n return () => {\n _registry.delete(url);\n sendToWorker({ type: 'EIDOS_UNREGISTER_RESOURCE', url });\n useEidosStore.getState().unregisterResource(url);\n };\n}\n\nfunction _warnIfReregisteredWithDifferentConfig(\n url: string,\n existing: ResourceHandle | PatternResourceHandle,\n config: ResourceConfig,\n factoryName: string,\n): void {\n if (!import.meta.env.DEV) return;\n const existingCfg = existing.config;\n if (\n existingCfg.offline !== config.offline ||\n existingCfg.strategy !== config.strategy ||\n existingCfg.cacheName !== config.cacheName ||\n existingCfg.version !== config.version\n ) {\n console.warn(\n `[eidos] ${factoryName}('${url}') already registered with a different config — returning cached handle. Call handle.unregister() first to re-register.`,\n { registered: existingCfg, ignored: config },\n );\n }\n}\n\n// ── resource() ────────────────────────────────────────────────────────────────\n\n/**\n * Registers a concrete-URL resource. For URL patterns (`/api/products/*`,\n * `/api/users/:id`, `**`), use `resourcePattern()` instead.\n */\nexport function resource<T = unknown>(url: string, config: ResourceConfig): ResourceHandle<T> {\n if (isPattern(url)) {\n throw new Error(\n `[eidos] resource('${url}') is a URL pattern — use resourcePattern('${url}', config) instead. ` +\n `Pattern handles only support invalidate()/unregister(); the SW intercepts matching requests automatically.`,\n );\n }\n\n if (_registry.has(url)) {\n const existing = _registry.get(url)!;\n _warnIfReregisteredWithDifferentConfig(url, existing, config, 'resource');\n return existing as ResourceHandle<T>;\n }\n\n const { strategy } = _register(url, config);\n\n const handle: ResourceHandle<T> = {\n url,\n config,\n strategy,\n\n fetch: async () => {\n // ── Deduplication: coalesce concurrent fetches for the same URL ─────\n // If a request is already in-flight, piggyback on it and return a clone\n // so each caller gets an independent readable Response body.\n const existing = _inflightRequests.get(url);\n if (existing) return existing.then((r) => r.clone());\n\n // Store the raw-response promise. All callers (including the primary)\n // receive a clone — the raw response stays unconsumed in the map so\n // any caller arriving while the promise is still pending can clone it.\n const task = _fetchResource(url, config, strategy);\n _inflightRequests.set(url, task);\n // .catch() silences the unhandled-rejection on the cleanup promise;\n // the error still propagates to callers via task.then() below.\n task.finally(() => _inflightRequests.delete(url)).catch(() => {});\n return task.then((r) => r.clone());\n },\n\n json: async () => {\n const res = await handle.fetch();\n return res.json() as Promise<T>;\n },\n\n query: () => ({\n queryKey: ['eidos', url] as [string, string],\n queryFn: () => handle.json(),\n }),\n\n prefetch: async () => {\n await handle.fetch();\n },\n\n invalidate: _invalidate(url, strategy, undefined),\n unregister: _unregister(url),\n };\n\n _registry.set(url, handle);\n return handle;\n}\n\n// ── resourcePattern() ────────────────────────────────────────────────────────\n\n/**\n * Registers a URL pattern (`/api/products/*`, `/api/users/:id`, `**`). The SW\n * intercepts all matching requests automatically — there is no single URL to\n * fetch/cache directly, so the returned handle only supports cache management\n * (`invalidate`/`unregister`). For a fetchable resource, use `resource()`.\n */\nexport function resourcePattern(url: string, config: ResourceConfig): PatternResourceHandle {\n if (!isPattern(url)) {\n throw new Error(\n `[eidos] resourcePattern('${url}') is not a URL pattern — use resource('${url}', config) instead.`,\n );\n }\n\n if (_registry.has(url)) {\n const existing = _registry.get(url)!;\n _warnIfReregisteredWithDifferentConfig(url, existing, config, 'resourcePattern');\n return existing as PatternResourceHandle;\n }\n\n const { strategy, regexStr } = _register(url, config);\n\n const handle: PatternResourceHandle = {\n url,\n config,\n strategy,\n invalidate: _invalidate(url, strategy, regexStr),\n unregister: _unregister(url),\n };\n\n _registry.set(url, handle);\n return handle;\n}\n\n// ── _fetchResource ─────────────────────────────────────────────────────────────\n// The actual network/cache implementation. Separated from handle.fetch() so the\n// deduplication wrapper can store the Promise and share it across concurrent callers.\n// Returns the raw (unconsumed) Response — callers MUST .clone() before reading body.\nasync function _fetchResource(\n url: string,\n config: ResourceConfig,\n strategy: GeneratedStrategy,\n): Promise<Response> {\n const store = useEidosStore.getState();\n store.updateResource(url, { status: 'fetching', fetchedAt: Date.now() });\n\n // Open cache once and reuse across try/catch — avoids a redundant\n // caches.open() call in the error fallback path.\n const cache = await caches.open(strategy.cacheName).catch(() => null);\n\n try {\n // ── network-first: skip cache check, go straight to network ─────────\n // For cache-first / SWR the cache check below is correct. For\n // network-first, reading cache first and returning early would\n // contradict the strategy — fresh data is the priority.\n if (strategy.swStrategy !== 'network-first') {\n // ── Direct Cache API check ─────────────────────────────────────────\n // We read the cache in the main thread rather than waiting for\n // an async SW postMessage. This gives instant, reliable status\n // updates regardless of SW message timing.\n const cached = cache ? await cache.match(url).catch(() => null) : null;\n\n // Treat cache as miss if maxAge exceeded\n const current = useEidosStore.getState().resources[url];\n const expired =\n config.maxAge !== undefined &&\n current?.cachedAt !== undefined &&\n Date.now() - current.cachedAt > config.maxAge;\n\n if (cached && !expired) {\n store.updateResource(url, {\n status: 'fresh',\n lastEvent: 'cache-hit',\n cacheHits: (current?.cacheHits ?? 0) + 1,\n });\n\n // Background revalidation for SWR (stale-while-revalidate)\n if (strategy.swStrategy === 'stale-while-revalidate') {\n fetch(url, { signal: AbortSignal.timeout(5000) })\n .then(async (resp) => {\n if (resp.ok && cache) {\n await cache.put(url, resp.clone());\n useEidosStore.getState().updateResource(url, {\n cachedAt: Date.now(),\n lastEvent: 'cache-updated',\n });\n }\n })\n .catch(() => {\n /* offline or timed out — cached version stays valid */\n });\n }\n\n return cached;\n }\n\n // Cache miss (or expired)\n const storeEntry = useEidosStore.getState().resources[url];\n store.updateResource(url, {\n cacheMisses: (storeEntry?.cacheMisses ?? 0) + 1,\n });\n }\n\n const response = await fetch(url);\n\n if (response.ok) {\n if (cache) await cache.put(url, response.clone());\n store.updateResource(url, {\n status: 'fresh',\n cachedAt: Date.now(),\n lastEvent: 'cache-updated',\n });\n return response;\n }\n\n // Non-2xx response (e.g. 503 from offline SW) — update status and throw\n // so callers get a proper error instead of a plain-object body they can't use.\n store.updateResource(url, { status: response.status === 503 ? 'offline' : 'error' });\n\n // Check if the SW tagged this as an offline response\n const isOffline = response.headers.get('X-Eidos-Offline') === 'true';\n throw new Error(\n isOffline\n ? `offline: no cached response for ${url}`\n : `${response.status} ${response.statusText}`,\n );\n } catch (err) {\n // Network failure — try cache one more time as fallback\n const fallback = cache ? await cache.match(url).catch(() => null) : null;\n\n if (fallback) {\n const current = useEidosStore.getState().resources[url];\n store.updateResource(url, {\n status: 'fresh',\n lastEvent: 'cache-hit',\n cacheHits: (current?.cacheHits ?? 0) + 1,\n });\n return fallback;\n }\n\n store.updateResource(url, { status: 'error' });\n throw err;\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Strategy derivation — intent → deterministic caching strategy\n// ─────────────────────────────────────────────────────────────────────────────\n\nfunction deriveStrategy(config: ResourceConfig): GeneratedStrategy {\n const explicit = config.strategy;\n if (config.offline) {\n return buildStrategy(explicit ?? 'stale-while-revalidate', config.cacheName, config.version);\n }\n return buildStrategy(explicit ?? 'network-first', config.cacheName, config.version);\n}\n\n// Strategy display names — always included (tiny, used by devtools).\nconst STRATEGY_NAMES: Record<CacheStrategy, string> = {\n 'stale-while-revalidate': 'StaleWhileRevalidate',\n 'cache-first': 'CacheFirst',\n 'network-first': 'NetworkFirst',\n};\n\n// Heavy descriptive strings — stripped from production bundles by Vite's\n// import.meta.env.DEV dead-code elimination. Only the names above ship in prod.\ntype StrategyDevInfo = Pick<GeneratedStrategy, 'reasoning' | 'behavior' | 'equivalentCode'>;\nconst _STRATEGY_DEV_META: Record<CacheStrategy, StrategyDevInfo> = {\n 'stale-while-revalidate': {\n reasoning:\n 'offline: true signals resilience. SWR returns cached data instantly while revalidating in the background — the best tradeoff between speed and freshness for offline-capable resources.',\n behavior: [\n 'Cache hit → return immediately, kick off background revalidation',\n 'Cache miss → fetch from network, cache the response, return it',\n 'Offline → return cached version if available, 503 if not',\n 'Reconnect → next request triggers a background refresh',\n ],\n equivalentCode: `// Workbox equivalent (maxEntries/maxAge are configured via ResourceConfig)\nnew StaleWhileRevalidate({\n cacheName: 'eidos-resources-v1',\n plugins: [new ExpirationPlugin({ maxEntries: config.maxEntries, maxAgeSeconds: config.maxAge && config.maxAge / 1000 })],\n})`,\n },\n 'cache-first': {\n reasoning:\n 'cache-first maximises speed and offline availability. Network is consulted only on cache miss. Best for static or infrequently-updated data.',\n behavior: [\n 'Cache hit → return immediately, no network request made',\n 'Cache miss → fetch from network, cache the response, return it',\n 'Offline → return cached version, 503 if cache is empty',\n 'Cache never expires unless maxAge is set or explicitly invalidated',\n ],\n equivalentCode: `// Workbox equivalent (maxEntries/maxAge are configured via ResourceConfig)\nnew CacheFirst({\n cacheName: 'eidos-resources-v1',\n plugins: [new ExpirationPlugin({ maxEntries: config.maxEntries, maxAgeSeconds: config.maxAge && config.maxAge / 1000 })],\n})`,\n },\n 'network-first': {\n reasoning:\n 'network-first prioritises fresh data. Cache acts as a safety net when offline. Best for frequently-updated resources where stale data causes problems.',\n behavior: [\n 'Always try network first',\n 'Network success → update cache, return fresh response',\n 'Network failure → fall back to cached version',\n 'Offline with empty cache → return 503 error response',\n ],\n equivalentCode: `// Workbox equivalent\nnew NetworkFirst({\n cacheName: 'eidos-resources-v1',\n networkTimeoutSeconds: 3,\n})`,\n },\n};\n\nfunction buildStrategy(\n swStrategy: CacheStrategy,\n cacheName?: string,\n version?: string | number,\n): GeneratedStrategy {\n const meta = _STRATEGY_DEV_META[swStrategy];\n const baseName = cacheName ?? 'eidos-resources-v1';\n return {\n name: STRATEGY_NAMES[swStrategy],\n swStrategy,\n cacheName: version !== undefined ? `${baseName}-v${version}` : baseName,\n // reasoning + behavior are rendered by the playground UI from live ResourceEntry objects —\n // keep them in all builds. equivalentCode is a static code block only used in DEV tools.\n reasoning: meta.reasoning,\n behavior: meta.behavior,\n equivalentCode: import.meta.env.DEV ? meta.equivalentCode : '',\n };\n}\n\n// ── warmCache ─────────────────────────────────────────────────────────────────\n\n/**\n * Bulk-prefetch an array of resource handles concurrently, warming the cache\n * for each one. Useful on login / app init when you know which resources the\n * user will need offline.\n *\n * Pattern handles (containing `*`, `**`, or `:param`) are silently skipped —\n * they match multiple URLs so there is no single URL to prefetch.\n *\n * @example\n * import { warmCache } from '@sweidos/eidos'\n *\n * // In EidosProvider's onReady, or after login:\n * const { warmed, failed } = await warmCache([products, userProfile, settings])\n */\nexport async function warmCache(handles: ResourceHandle[]): Promise<WarmCacheResult> {\n const results = await Promise.allSettled(handles.map((h) => h.prefetch()));\n const errors = results\n .filter((r): r is PromiseRejectedResult => r.status === 'rejected')\n .map((r) => r.reason);\n\n if (import.meta.env.DEV && errors.length > 0) {\n console.warn(`[eidos] warmCache: ${errors.length} handle(s) failed to prefetch`, errors);\n }\n\n return {\n warmed: results.filter((r) => r.status === 'fulfilled').length,\n failed: errors.length,\n errors,\n };\n}\n","import type { ActionQueueItem } from './types';\nimport type { QueueStorage } from './queue-storage';\n\nconst DB_NAME = 'eidos';\nconst DB_VERSION = 1;\nconst QUEUE_STORE = 'action-queue';\n\nlet _db: IDBDatabase | null = null;\n\nfunction openDB(): Promise<IDBDatabase> {\n if (_db) return Promise.resolve(_db);\n\n return new Promise((resolve, reject) => {\n const req = indexedDB.open(DB_NAME, DB_VERSION);\n\n req.onupgradeneeded = (event) => {\n const db = (event.target as IDBOpenDBRequest).result;\n if (!db.objectStoreNames.contains(QUEUE_STORE)) {\n const store = db.createObjectStore(QUEUE_STORE, { keyPath: 'id' });\n store.createIndex('status', 'status', { unique: false });\n store.createIndex('actionId', 'actionId', { unique: false });\n }\n };\n\n req.onsuccess = () => {\n _db = req.result;\n resolve(req.result);\n };\n\n req.onerror = () => reject(req.error);\n });\n}\n\nexport async function idbAddToQueue(item: ActionQueueItem): Promise<void> {\n const db = await openDB();\n return new Promise((resolve, reject) => {\n const tx = db.transaction(QUEUE_STORE, 'readwrite');\n tx.objectStore(QUEUE_STORE).add(item);\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n });\n}\n\nexport async function idbGetQueue(): Promise<ActionQueueItem[]> {\n const db = await openDB();\n return new Promise((resolve, reject) => {\n const tx = db.transaction(QUEUE_STORE, 'readonly');\n const req = tx.objectStore(QUEUE_STORE).getAll();\n req.onsuccess = () => resolve(req.result as ActionQueueItem[]);\n req.onerror = () => reject(req.error);\n });\n}\n\nexport async function idbUpdateQueueItem(\n id: string,\n update: Partial<ActionQueueItem>,\n): Promise<void> {\n const db = await openDB();\n return new Promise((resolve, reject) => {\n const tx = db.transaction(QUEUE_STORE, 'readwrite');\n const store = tx.objectStore(QUEUE_STORE);\n const get = store.get(id);\n get.onsuccess = () => {\n if (get.result) {\n store.put({ ...get.result, ...update });\n } else if (import.meta.env.DEV) {\n console.warn(\n `[eidos] idbUpdateQueueItem: item \"${id}\" not found — store/IDB may have diverged`,\n );\n }\n };\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n });\n}\n\nexport async function idbRemoveFromQueue(id: string): Promise<void> {\n const db = await openDB();\n return new Promise((resolve, reject) => {\n const tx = db.transaction(QUEUE_STORE, 'readwrite');\n tx.objectStore(QUEUE_STORE).delete(id);\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n });\n}\n\n// Uses the status index to fetch only pending/failed items — avoids a full\n// table scan when the queue has many succeeded/replaying entries.\nexport async function idbGetPendingItems(): Promise<ActionQueueItem[]> {\n const db = await openDB();\n\n function cursorToArray(status: string): Promise<ActionQueueItem[]> {\n return new Promise((resolve, reject) => {\n const tx = db.transaction(QUEUE_STORE, 'readonly');\n const index = tx.objectStore(QUEUE_STORE).index('status');\n const items: ActionQueueItem[] = [];\n const req = index.openCursor(IDBKeyRange.only(status));\n req.onsuccess = (e) => {\n const cursor = (e.target as IDBRequest<IDBCursorWithValue>).result;\n if (cursor) {\n items.push(cursor.value as ActionQueueItem);\n cursor.continue();\n } else resolve(items);\n };\n req.onerror = () => reject(req.error);\n });\n }\n\n const [pending, failed] = await Promise.all([cursorToArray('pending'), cursorToArray('failed')]);\n return [...pending, ...failed];\n}\n\nexport async function idbClearQueue(): Promise<void> {\n const db = await openDB();\n return new Promise((resolve, reject) => {\n const tx = db.transaction(QUEUE_STORE, 'readwrite');\n tx.objectStore(QUEUE_STORE).clear();\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n });\n}\n\n/** IndexedDB-backed QueueStorage implementation (default for browser environments). */\nexport const idbQueueStorage: QueueStorage = {\n add: idbAddToQueue,\n getAll: idbGetQueue,\n getPending: idbGetPendingItems,\n update: idbUpdateQueueItem,\n remove: idbRemoveFromQueue,\n clear: idbClearQueue,\n};\n","import type { ActionQueueItem } from './types';\n\nexport interface QueueStorage {\n add(item: ActionQueueItem): Promise<void>;\n getAll(): Promise<ActionQueueItem[]>;\n getPending(): Promise<ActionQueueItem[]>;\n update(id: string, patch: Partial<ActionQueueItem>): Promise<void>;\n remove(id: string): Promise<void>;\n clear(): Promise<void>;\n}\n\nlet _storage: QueueStorage | null = null;\n\n/** Override the default IndexedDB queue with a custom storage backend (e.g. AsyncStorage for React Native). */\nexport function setQueueStorage(s: QueueStorage): void {\n _storage = s;\n}\n\nexport function _getQueueStorage(): QueueStorage | null {\n return _storage;\n}\n","import { useEidosStore } from './store';\nimport type { ActionQueueItem } from './types';\n\nconst CHANNEL_NAME = 'eidos-queue-sync';\n\ntype QueueSyncMessage =\n | { type: 'update'; id: string; update: Partial<ActionQueueItem> }\n | { type: 'batchUpdate'; updates: Array<{ id: string; update: Partial<ActionQueueItem> }> }\n | { type: 'remove'; id: string };\n\nlet _channel: BroadcastChannel | null | undefined;\n\nfunction getChannel(): BroadcastChannel | null {\n if (_channel !== undefined) return _channel;\n _channel = typeof BroadcastChannel === 'undefined' ? null : new BroadcastChannel(CHANNEL_NAME);\n return _channel;\n}\n\n/**\n * Broadcasts a queue-item status change to other tabs sharing the same\n * IndexedDB queue. The replay-lock holder (see `replayQueue` in action.ts)\n * is the only tab that mutates queue-item status, so non-leader tabs would\n * otherwise show stale status until their own store re-hydrates.\n *\n * No-ops in environments without BroadcastChannel (React Native, old Safari).\n */\nexport function broadcastQueueSync(message: QueueSyncMessage): void {\n getChannel()?.postMessage(message);\n}\n\n/**\n * Applies queue-item status updates broadcast by the replay-lock holder to\n * this tab's store. Returns an unsubscribe function.\n */\nexport function subscribeQueueSync(): () => void {\n const channel = getChannel();\n if (!channel) return () => {};\n\n const handler = (event: MessageEvent<QueueSyncMessage>) => {\n const store = useEidosStore.getState();\n const message = event.data;\n switch (message.type) {\n case 'update':\n store.updateQueueItem(message.id, message.update);\n break;\n case 'batchUpdate':\n store.batchUpdateQueueItems(message.updates);\n break;\n case 'remove':\n store.removeQueueItem(message.id);\n break;\n }\n };\n\n channel.addEventListener('message', handler);\n return () => channel.removeEventListener('message', handler);\n}\n\n/** Test-only: reset the cached channel so each test gets a fresh instance. */\nexport function _resetQueueSyncChannel(): void {\n _channel?.close();\n _channel = undefined;\n}\n","import { useEidosStore } from './store';\nimport { getSwRegistration } from './sw-bridge';\nimport { idbQueueStorage } from './idb';\nimport { _getQueueStorage } from './queue-storage';\nimport { broadcastQueueSync } from './queue-sync';\nimport type { QueueStorage } from './queue-storage';\nimport { CURRENT_QUEUE_SCHEMA_VERSION } from './types';\nimport type {\n ActionConfig,\n ActionContext,\n ActionHandle,\n ActionFn,\n ActionQueueItem,\n ConflictConfig,\n ConflictContext,\n ConflictResolution,\n QueuedResult,\n ReplayResult,\n} from './types';\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst _actionRegistry = new Map<string, ActionFn<any[], any>>();\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst _rollbackRegistry = new Map<string, (...args: any[]) => void>();\nconst _conflictConfigRegistry = new Map<string, ConflictConfig>();\nconst _configRegistry = new Map<string, ActionConfig>();\n\n// In-flight AbortControllers for `cancellable` actions, keyed by idempotencyKey.\n// Populated for direct calls and replays alike; removed once the call settles.\nconst _inflightControllers = new Map<string, AbortController>();\n\nfunction qs(): QueueStorage {\n // idbQueueStorage is the default browser fallback when no custom storage is set.\n return _getQueueStorage() ?? idbQueueStorage;\n}\n\nfunction uid() {\n return crypto.randomUUID();\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction callWithContext<TArgs extends any[], TReturn>(\n fn: ActionFn<TArgs, TReturn>,\n args: TArgs,\n ctx: ActionContext,\n): Promise<TReturn> {\n return fn(...args, ctx);\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function action<TArgs extends any[], TReturn>(\n fn: ActionFn<TArgs, TReturn>,\n config: ActionConfig<TArgs>,\n): ActionHandle<TArgs, TReturn> {\n // || not ?? — fn.name can be '' (anonymous arrow fn) which ?? treats as a\n // valid value, causing all anonymous actions to share actionId ''.\n const baseId = config.name || fn.name || uid();\n const actionId = config.namespace ? `${config.namespace}::${baseId}` : baseId;\n\n if (import.meta.env.DEV && config.reliability === 'neverLose' && !config.name && !fn.name) {\n console.warn(\n `[eidos] action() registered with neverLose but no stable name was found (fn.name=\"${fn.name}\"). Pass config.name so queued items survive a page reload and can be replayed.`,\n );\n }\n\n if (_actionRegistry.has(actionId)) {\n throw new Error(\n `[eidos] duplicate action id \"${actionId}\" — an action with this id is already registered. Pass a unique config.name or config.namespace.`,\n );\n }\n\n // Registering here means the function is available for replay after\n // the user refreshes the page (actions are defined at module scope).\n _actionRegistry.set(actionId, fn as ActionFn<unknown[], unknown>);\n _configRegistry.set(actionId, config as ActionConfig);\n\n if (config.onRollback) {\n _rollbackRegistry.set(actionId, config.onRollback);\n }\n\n if (config.conflict) {\n if (\n import.meta.env.DEV &&\n (config.conflict.strategy === 'merge' || config.conflict.strategy === 'custom') &&\n !config.conflict.resolve\n ) {\n console.error(\n `[eidos] action \"${actionId}\" has conflict.strategy \"${config.conflict.strategy}\" but no resolve() — items will retry indefinitely on 4xx.`,\n );\n }\n _conflictConfigRegistry.set(actionId, config.conflict);\n }\n\n const wrapped = async (...args: TArgs): Promise<TReturn | QueuedResult> => {\n const { isOnline } = useEidosStore.getState();\n\n // Generated for every invocation — reused across every retry/replay of a\n // neverLose item, and used to key handle.cancel() for in-flight cancellable calls.\n const idempotencyKey = uid();\n\n let signal: AbortSignal | undefined;\n if (config.cancellable) {\n const controller = new AbortController();\n _inflightControllers.set(idempotencyKey, controller);\n signal = controller.signal;\n }\n\n const ctx: ActionContext = { idempotencyKey, attempt: 0, signal };\n\n config.onOptimistic?.(...args, ctx);\n\n try {\n if (config.reliability === 'neverLose') {\n if (!isOnline) {\n return persistAndQueue(actionId, actionId, args, config, idempotencyKey);\n }\n // Online + neverLose: execute, queue on failure\n try {\n return await callWithContext(fn, args, ctx);\n } catch (err) {\n if (isAbortError(err)) throw err;\n return persistAndQueue(actionId, actionId, args, config, idempotencyKey);\n }\n }\n\n // best-effort: execute directly, rollback on failure\n try {\n return await callWithContext(fn, args, ctx);\n } catch (err) {\n config.onRollback?.(...args, ctx);\n throw err;\n }\n } finally {\n if (config.cancellable) _inflightControllers.delete(idempotencyKey);\n }\n };\n\n Object.defineProperty(wrapped, 'id', { value: actionId, writable: false });\n Object.defineProperty(wrapped, 'config', { value: config, writable: false });\n Object.defineProperty(wrapped, 'cancel', { value: cancelByIdempotencyKey, writable: false });\n\n return wrapped as unknown as ActionHandle<TArgs, TReturn>;\n}\n\n/**\n * Cancel an invocation by its `idempotencyKey` (from `ActionContext` /\n * `onOptimistic`). Aborts the in-flight call if `cancellable: true` and\n * still running, otherwise removes a not-yet-replayed queued item.\n * Returns `true` if something was cancelled/removed.\n *\n * Shared by every `ActionHandle.cancel()` and by devtools, which can't\n * address a specific handle from a queue item alone.\n */\nexport async function cancelByIdempotencyKey(idempotencyKey: string): Promise<boolean> {\n const controller = _inflightControllers.get(idempotencyKey);\n if (controller) {\n controller.abort();\n return true;\n }\n\n // Not in flight — check for a not-yet-replayed queued item with this key.\n const items = await qs().getAll();\n const item = items.find((i) => i.idempotencyKey === idempotencyKey && i.status === 'pending');\n if (!item) return false;\n\n useEidosStore.getState().removeQueueItem(item.id);\n broadcastQueueSync({ type: 'remove', id: item.id });\n await qs().remove(item.id);\n return true;\n}\n\n/**\n * Reset a `'failed'` queue item back to `'pending'` so the next\n * `replayQueue()` retries it — clears `error`/`nextRetryAt` and resets\n * `retryCount` to 0. Returns `true` if the item existed and was failed.\n * Used by devtools' per-item \"Retry\" action.\n */\nexport async function requeueItem(id: string): Promise<boolean> {\n const items = await qs().getAll();\n const item = items.find((i) => i.id === id);\n if (!item || item.status !== 'failed') return false;\n\n const update: Partial<ActionQueueItem> = {\n status: 'pending',\n error: undefined,\n nextRetryAt: undefined,\n retryCount: 0,\n };\n useEidosStore.getState().updateQueueItem(id, update);\n broadcastQueueSync({ type: 'update', id, update });\n await qs().update(id, update);\n return true;\n}\n\nfunction isJsonSerializable(value: unknown): boolean {\n try {\n JSON.stringify(value);\n return true;\n } catch {\n return false;\n }\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nasync function persistAndQueue<TArgs extends any[]>(\n actionId: string,\n actionName: string,\n args: TArgs,\n config: ActionConfig<TArgs>,\n idempotencyKey: string,\n): Promise<QueuedResult> {\n if (import.meta.env.DEV && !isJsonSerializable(args)) {\n console.warn(\n `[eidos] action \"${actionName}\" queued with non-JSON-serializable args. These args will be lost after a page reload. Use plain JSON values for neverLose actions.`,\n args,\n );\n }\n\n const id = uid();\n const item: ActionQueueItem = {\n schemaVersion: CURRENT_QUEUE_SCHEMA_VERSION,\n id,\n actionId,\n actionName,\n idempotencyKey,\n args,\n queuedAt: Date.now(),\n retryCount: 0,\n maxRetries: config.maxRetries ?? 3,\n status: 'pending',\n priority: config.priority ?? 'normal',\n };\n\n await qs().add(item);\n useEidosStore.getState().addQueueItem(item);\n useEidosStore.getState().recordReliabilityEvent('queued');\n\n // Register Background Sync tag so the browser can wake up open clients\n // when connectivity returns, even if the user navigated away briefly.\n // Graceful no-op when Background Sync is unsupported.\n try {\n const reg = getSwRegistration();\n if (reg && 'sync' in reg) {\n await (reg as unknown as { sync: { register(tag: string): Promise<void> } }).sync.register(\n 'eidos-queue-replay',\n );\n }\n } catch {\n // Background Sync not available — online-event replay remains the fallback\n }\n\n return {\n queued: true,\n id,\n message: `\"${actionName}\" queued — will execute when online`,\n };\n}\n\nfunction isAbortError(err: unknown): boolean {\n return err instanceof DOMException && err.name === 'AbortError';\n}\n\nfunction isClientError(err: unknown): boolean {\n if (err instanceof Response) return err.status >= 400 && err.status < 500;\n if (typeof err === 'object' && err !== null) {\n const s = (err as Record<string, unknown>).status;\n if (typeof s === 'number') return s >= 400 && s < 500;\n }\n return false;\n}\n\n// Base delay 2s, doubles per retry, capped at 5 minutes, ±20% jitter\nfunction backoffMs(retryCount: number): number {\n const base = Math.min(2000 * 2 ** retryCount, 300_000);\n return base * (0.8 + Math.random() * 0.4);\n}\n\nfunction emptyReplayResult(): ReplayResult {\n return {\n attempted: 0,\n succeeded: 0,\n failed: 0,\n retrying: 0,\n skipped: 0,\n conflicted: 0,\n cancelled: 0,\n };\n}\n\nlet _replaying = false;\nconst REPLAY_LOCK_NAME = 'eidos-queue-replay';\n\nexport async function replayQueue(): Promise<ReplayResult> {\n const store = useEidosStore.getState();\n if (!store.isOnline) return emptyReplayResult();\n\n // Web Locks coordinate replay across tabs sharing the same IndexedDB queue —\n // only the lock holder replays; other tabs no-op rather than re-executing\n // the same queued actions in parallel.\n if (typeof navigator !== 'undefined' && navigator.locks) {\n return navigator.locks.request(REPLAY_LOCK_NAME, { ifAvailable: true }, async (lock) => {\n if (!lock) return emptyReplayResult();\n return _doReplayQueue(store);\n });\n }\n\n // Fallback for environments without the Web Locks API (older Safari, React\n // Native, test runners) — guards against concurrent replay within this tab only.\n if (_replaying) return emptyReplayResult();\n _replaying = true;\n try {\n return await _doReplayQueue(store);\n } finally {\n _replaying = false;\n }\n}\n\ntype ItemOutcome = 'succeeded' | 'failed' | 'retrying' | 'skipped' | 'conflicted' | 'cancelled';\n\nasync function _markSucceeded(\n item: ActionQueueItem,\n store: ReturnType<typeof useEidosStore.getState>,\n): Promise<void> {\n const completedAt = Date.now();\n store.updateQueueItem(item.id, { status: 'succeeded', completedAt });\n store.recordReliabilityEvent('succeeded');\n broadcastQueueSync({ type: 'update', id: item.id, update: { status: 'succeeded', completedAt } });\n await qs().update(item.id, { status: 'succeeded', completedAt });\n\n // Remove from queue after a short delay so UI can show the success state briefly\n setTimeout(() => {\n store.removeQueueItem(item.id);\n broadcastQueueSync({ type: 'remove', id: item.id });\n qs().remove(item.id);\n }, 3000);\n}\n\n/**\n * Resolves a 4xx error against the action's conflict strategy.\n * Returns 'conflicted' if the item was dropped, undefined if normal\n * retry/fail logic should run (possibly with `item.args` rewritten by `merge`).\n */\nasync function _resolveConflict(\n item: ActionQueueItem,\n store: ReturnType<typeof useEidosStore.getState>,\n err: unknown,\n): Promise<ItemOutcome | undefined> {\n const conflictConfig = _conflictConfigRegistry.get(item.actionId);\n let resolution: ConflictResolution | undefined;\n\n if (conflictConfig) {\n switch (conflictConfig.strategy) {\n case 'serverWins':\n resolution = 'skip';\n break;\n case 'clientWins':\n resolution = 'retry';\n break;\n case 'merge':\n case 'custom': {\n const ctx: ConflictContext = {\n error: err,\n args: item.args as unknown[],\n attempt: item.retryCount,\n idempotencyKey: item.idempotencyKey,\n };\n resolution = conflictConfig.resolve?.(ctx) ?? 'retry';\n break;\n }\n }\n }\n\n if (resolution === 'skip') {\n store.removeQueueItem(item.id);\n store.recordReliabilityEvent('conflicted');\n broadcastQueueSync({ type: 'remove', id: item.id });\n await qs().remove(item.id);\n return 'conflicted';\n }\n if (resolution && typeof resolution === 'object') {\n item.args = resolution.resolved;\n store.updateQueueItem(item.id, { args: resolution.resolved });\n broadcastQueueSync({ type: 'update', id: item.id, update: { args: resolution.resolved } });\n await qs().update(item.id, { args: resolution.resolved });\n }\n // 'retry' (or merged args) falls through to normal retry/fail logic\n return undefined;\n}\n\nasync function _scheduleRetryOrFail(\n item: ActionQueueItem,\n store: ReturnType<typeof useEidosStore.getState>,\n err: unknown,\n): Promise<ItemOutcome> {\n const retryCount = item.retryCount + 1;\n if (retryCount >= item.maxRetries) {\n const update = { status: 'failed' as const, error: String(err), retryCount };\n store.updateQueueItem(item.id, update);\n store.recordReliabilityEvent('failed');\n broadcastQueueSync({ type: 'update', id: item.id, update });\n await qs().update(item.id, update);\n const ctx: ActionContext = { idempotencyKey: item.idempotencyKey, attempt: retryCount };\n _rollbackRegistry.get(item.actionId)?.(...(item.args as unknown[]), ctx);\n return 'failed';\n }\n\n const nextRetryAt = Date.now() + backoffMs(retryCount);\n const update = { status: 'pending' as const, retryCount, nextRetryAt };\n store.updateQueueItem(item.id, update);\n store.recordReliabilityEvent('retried');\n broadcastQueueSync({ type: 'update', id: item.id, update });\n await qs().update(item.id, update);\n return 'retrying';\n}\n\nasync function _replayItem(\n item: ActionQueueItem,\n store: ReturnType<typeof useEidosStore.getState>,\n): Promise<ItemOutcome> {\n const fn = _actionRegistry.get(item.actionId);\n if (!fn) return 'skipped';\n\n const cancellable = _configRegistry.get(item.actionId)?.cancellable;\n let signal: AbortSignal | undefined;\n if (cancellable) {\n const controller = new AbortController();\n _inflightControllers.set(item.idempotencyKey, controller);\n signal = controller.signal;\n }\n\n const ctx: ActionContext = {\n idempotencyKey: item.idempotencyKey,\n attempt: item.retryCount,\n signal,\n };\n\n try {\n await callWithContext(fn, item.args as unknown[], ctx);\n await _markSucceeded(item, store);\n return 'succeeded';\n } catch (err) {\n // Cancelled via handle.cancel(idempotencyKey) — drop the item, no rollback/retry.\n if (isAbortError(err)) {\n store.removeQueueItem(item.id);\n store.recordReliabilityEvent('cancelled');\n broadcastQueueSync({ type: 'remove', id: item.id });\n await qs().remove(item.id);\n return 'cancelled';\n }\n\n // 4xx: give the conflict strategy a chance to decide before normal retry/fail logic\n if (isClientError(err)) {\n const outcome = await _resolveConflict(item, store, err);\n if (outcome) return outcome;\n }\n\n return _scheduleRetryOrFail(item, store, err);\n } finally {\n if (cancellable) _inflightControllers.delete(item.idempotencyKey);\n }\n}\n\nasync function _replayTier(\n items: ActionQueueItem[],\n store: ReturnType<typeof useEidosStore.getState>,\n result: ReplayResult,\n): Promise<void> {\n if (items.length === 0) return;\n\n // Batch 'replaying' status update — N items → 1 store notify.\n // IDB write is fire-and-forget: on reload items stay 'pending', safe to re-replay.\n const replayable = items.filter((item) => _actionRegistry.has(item.actionId));\n result.skipped += items.length - replayable.length;\n\n if (replayable.length > 0) {\n const updates = replayable.map((item) => ({\n id: item.id,\n update: { status: 'replaying' as const },\n }));\n store.batchUpdateQueueItems(updates);\n broadcastQueueSync({ type: 'batchUpdate', updates });\n for (const item of replayable) {\n qs().update(item.id, { status: 'replaying' });\n }\n }\n\n const outcomes = await Promise.allSettled(replayable.map((item) => _replayItem(item, store)));\n\n for (const o of outcomes) {\n const outcome = o.status === 'fulfilled' ? o.value : 'failed';\n if (outcome === 'skipped') {\n result.skipped++;\n } else if (outcome === 'conflicted') {\n result.conflicted++;\n } else if (outcome === 'cancelled') {\n result.cancelled++;\n } else {\n result.attempted++;\n result[outcome]++;\n }\n }\n}\n\nasync function _doReplayQueue(\n store: ReturnType<typeof useEidosStore.getState>,\n): Promise<ReplayResult> {\n const candidates = await qs().getPending();\n const now = Date.now();\n // getPending() includes 'failed' items (for UI/queue-stats visibility), but\n // items that already exhausted maxRetries must not be auto-replayed again —\n // otherwise every reconnect re-executes the action and re-fires onRollback.\n // Those items stay 'failed' until the host app explicitly clears/re-queues them.\n const pending = candidates.filter(\n (item) => item.retryCount < item.maxRetries && (!item.nextRetryAt || item.nextRetryAt <= now),\n );\n\n const result: ReplayResult = emptyReplayResult();\n\n // Process tiers sequentially: high items complete before normal, normal before low.\n // Within each tier items run in parallel via Promise.allSettled.\n for (const tier of ['high', 'normal', 'low'] as const) {\n const tierItems = pending.filter((item) => (item.priority ?? 'normal') === tier);\n await _replayTier(tierItems, store, result);\n }\n\n return result;\n}\n\n/** Remove all items from the action queue (storage + in-memory store). */\nexport async function clearQueue(): Promise<void> {\n await qs().clear();\n useEidosStore.getState().hydrateQueue([]);\n}\n","import { useEidosStore } from './store';\nimport { replayQueue } from './action';\n\n/**\n * Subscribe to online/offline transitions and trigger replayQueue() on\n * reconnect, plus replay any pending items left over from a previous session.\n *\n * Shared by the web (runtime.ts) and React Native (runtime-rn.ts) init paths.\n *\n * WHY subscribe to the store instead of window.addEventListener('online'):\n * setOfflineSimulation() updates the store directly but never fires a real\n * browser `online` event. Watching the store catches both:\n * • Real network reconnects (sw-bridge updates store on window.online)\n * • Simulation toggled off (setOfflineSimulation(false) → store.setOnline(true))\n *\n * Returns an unsubscribe function.\n */\nexport function subscribeReplayOnReconnect(): () => void {\n let prevIsOnline = useEidosStore.getState().isOnline;\n\n const unsubscribe = useEidosStore.subscribe(() => {\n const { isOnline } = useEidosStore.getState();\n const justCameOnline = isOnline && !prevIsOnline;\n prevIsOnline = isOnline;\n\n if (justCameOnline) {\n // Small delay so the connection (or simulation reset) settles first\n setTimeout(replayQueue, 600);\n }\n });\n\n // Replay any pending items that survived a reload/restart.\n // 'failed' items have already exhausted maxRetries and are never\n // re-replayed (see _doReplayQueue), so they don't count here.\n const store = useEidosStore.getState();\n const hasPending = store.queue.some((q) => q.status === 'pending');\n if (store.isOnline && hasPending) {\n setTimeout(replayQueue, 1200);\n }\n\n return unsubscribe;\n}\n","import { registerServiceWorker, registerBgSyncHandler } from './sw-bridge';\nimport { replayQueue } from './action';\nimport { useEidosStore } from './store';\nimport { idbGetQueue, idbQueueStorage } from './idb';\nimport { _getQueueStorage } from './queue-storage';\nimport { subscribeReplayOnReconnect } from './replay';\nimport { subscribeQueueSync } from './queue-sync';\nimport { CURRENT_QUEUE_SCHEMA_VERSION } from './types';\nimport type { ActionQueueItem, ReliabilityStats } from './types';\n\n// Items persisted before idempotencyKey/schemaVersion existed (v1) are migrated\n// in place: assign a fresh idempotencyKey and bump schemaVersion. A fresh key on\n// first replay after upgrade is correct — these items were never sent with one.\nasync function migrateQueueItem(item: ActionQueueItem): Promise<ActionQueueItem> {\n if (item.schemaVersion === CURRENT_QUEUE_SCHEMA_VERSION && item.idempotencyKey) {\n return item;\n }\n const migrated: ActionQueueItem = {\n ...item,\n schemaVersion: CURRENT_QUEUE_SCHEMA_VERSION,\n idempotencyKey: item.idempotencyKey ?? crypto.randomUUID(),\n };\n const storage = _getQueueStorage() ?? idbQueueStorage;\n await storage\n .update(migrated.id, {\n schemaVersion: migrated.schemaVersion,\n idempotencyKey: migrated.idempotencyKey,\n })\n .catch(() => {\n // Best-effort persist — item still gets the migrated shape in memory this session\n });\n return migrated;\n}\n\nexport interface EidosConfig {\n /** Path to the eidos service worker. Defaults to '/eidos-sw.js'. */\n swPath?: string;\n /** Automatically replay the action queue on reconnect. Default: true. */\n autoReplay?: boolean;\n /**\n * When `true` (default), the new service worker activates immediately when an\n * update is available — matching the pre-v2.3 behaviour. Set to `false` to opt\n * into a toast-then-reload pattern: `onUpdateAvailable` fires instead and you\n * call `triggerSwUpdate()` when the user confirms the reload.\n *\n * Note: avoid calling `triggerSwUpdate()` while `neverLose` actions are mid-replay\n * — the BroadcastChannel/Web-Locks replay coordination survives SW activation, but\n * triggering an update during an active replay pass is unnecessary churn.\n */\n skipWaiting?: boolean;\n /**\n * Called when a new service worker version has installed and is waiting to\n * activate. Use this to show a \"reload to update\" toast. Only fires when\n * `skipWaiting: false`; with the default `skipWaiting: true` the update\n * applies automatically and this callback is never needed.\n *\n * Call `triggerSwUpdate()` when the user confirms the reload — it tells the\n * waiting SW to activate, then reload the page.\n */\n onUpdateAvailable?: (registration: ServiceWorkerRegistration) => void;\n /**\n * Opt-in reliability telemetry. Called with a snapshot of cumulative\n * `neverLose` queue outcome counters (`ReliabilityStats`) every\n * `reliabilityReportInterval` ms — wire this up to your analytics backend.\n * Not called if omitted.\n */\n onReliabilityReport?: (stats: ReliabilityStats) => void;\n /** Interval (ms) between `onReliabilityReport` calls. Default: 60000. */\n reliabilityReportInterval?: number;\n}\n\nlet _initialized = false;\nlet _unsubscribe: (() => void) | null = null;\nlet _unsubscribeQueueSync: (() => void) | null = null;\nlet _reliabilityReportTimer: ReturnType<typeof setInterval> | null = null;\n\nexport async function initEidos(config: EidosConfig = {}): Promise<void> {\n // Skip silently during SSR — SW, IndexedDB, and window are browser-only.\n if (typeof window === 'undefined') return;\n if (_initialized) return;\n _initialized = true;\n\n const swPath = config.swPath ?? '/eidos-sw.js';\n const autoReplay = config.autoReplay ?? true;\n const skipWaiting = config.skipWaiting ?? true;\n\n // Restore persisted queue from IndexedDB on startup\n try {\n const persisted = await idbGetQueue();\n if (persisted.length > 0) {\n const migrated = await Promise.all(persisted.map(migrateQueueItem));\n useEidosStore.getState().hydrateQueue(migrated);\n }\n } catch {\n // IndexedDB unavailable (Firefox private browsing) — silent fallback\n }\n\n try {\n await registerServiceWorker(swPath, {\n skipWaiting,\n onUpdateAvailable: config.onUpdateAvailable,\n });\n } catch {\n // SW registration failed; app continues without offline support\n }\n\n // When the SW fires the Background Sync tag, replay the queue in the main thread.\n // This path runs even if the user briefly navigated away and back — the browser\n // triggers the sync event on the SW, which wakes up all open clients.\n registerBgSyncHandler(() => {\n if (useEidosStore.getState().isOnline) {\n setTimeout(replayQueue, 200);\n }\n });\n\n if (autoReplay) {\n _unsubscribe = subscribeReplayOnReconnect();\n }\n\n // Apply queue-item status changes broadcast by the replay-lock holder so\n // non-leader tabs reflect live status without waiting for re-hydration.\n _unsubscribeQueueSync = subscribeQueueSync();\n\n if (config.onReliabilityReport) {\n const interval = config.reliabilityReportInterval ?? 60_000;\n const report = config.onReliabilityReport;\n _reliabilityReportTimer = setInterval(() => {\n report(useEidosStore.getState().reliability);\n }, interval);\n }\n\n if (import.meta.env.DEV) {\n const store = useEidosStore.getState();\n console.groupCollapsed('%c⚡ Eidos', 'color:#22C55E;font-weight:bold');\n console.log('SW path :', swPath);\n console.log('Auto-replay:', autoReplay);\n console.log('SW status :', store.swStatus);\n console.groupEnd();\n }\n}\n\nexport function _resetEidos() {\n _unsubscribe?.();\n _unsubscribe = null;\n _unsubscribeQueueSync?.();\n _unsubscribeQueueSync = null;\n if (_reliabilityReportTimer) clearInterval(_reliabilityReportTimer);\n _reliabilityReportTimer = null;\n _initialized = false;\n}\n","import type { ActionQueueItem } from './types';\nimport type { QueueStorage } from './queue-storage';\n\n/** Minimal subset of @react-native-async-storage/async-storage (or any compatible key-value store). */\nexport interface AsyncStorageLike {\n getItem(key: string): Promise<string | null>;\n setItem(key: string, value: string): Promise<void>;\n removeItem(key: string): Promise<void>;\n}\n\nconst QUEUE_KEY = '@eidos:queue';\n\n/**\n * QueueStorage implementation backed by any AsyncStorage-compatible API.\n * Pass the AsyncStorage singleton from @react-native-async-storage/async-storage\n * (or MMKV, SQLite, or any store that satisfies AsyncStorageLike).\n */\nexport class AsyncStorageQueueStorage implements QueueStorage {\n constructor(private readonly storage: AsyncStorageLike) {}\n\n private async readAll(): Promise<ActionQueueItem[]> {\n try {\n const raw = await this.storage.getItem(QUEUE_KEY);\n if (!raw) return [];\n return JSON.parse(raw) as ActionQueueItem[];\n } catch {\n return [];\n }\n }\n\n private async writeAll(items: ActionQueueItem[]): Promise<void> {\n await this.storage.setItem(QUEUE_KEY, JSON.stringify(items));\n }\n\n async add(item: ActionQueueItem): Promise<void> {\n const items = await this.readAll();\n items.push(item);\n await this.writeAll(items);\n }\n\n async getAll(): Promise<ActionQueueItem[]> {\n return this.readAll();\n }\n\n async getPending(): Promise<ActionQueueItem[]> {\n const items = await this.readAll();\n return items.filter((i) => i.status === 'pending' || i.status === 'failed');\n }\n\n async update(id: string, patch: Partial<ActionQueueItem>): Promise<void> {\n const items = await this.readAll();\n const idx = items.findIndex((i) => i.id === id);\n if (idx !== -1) items[idx] = { ...items[idx], ...patch };\n await this.writeAll(items);\n }\n\n async remove(id: string): Promise<void> {\n const items = await this.readAll();\n await this.writeAll(items.filter((i) => i.id !== id));\n }\n\n async clear(): Promise<void> {\n await this.storage.removeItem(QUEUE_KEY);\n }\n}\n","import { useEffect, type ReactNode } from 'react';\nimport { initEidos, type EidosConfig } from '../runtime';\n\ninterface EidosProviderProps extends EidosConfig {\n children: ReactNode;\n}\n\n/**\n * Mount once at the root of your application.\n * Registers the service worker and initialises the Eidos runtime.\n *\n * @example\n * <EidosProvider swPath=\"/eidos-sw.js\">\n * <App />\n * </EidosProvider>\n */\nexport function EidosProvider({ children, swPath, autoReplay }: EidosProviderProps) {\n useEffect(() => {\n initEidos({ swPath, autoReplay });\n // Run once on mount only\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n return <>{children}</>;\n}\n","/**\n * Framework-agnostic reactive stores — compatible with Svelte's store protocol,\n * Vue's watchEffect, RxJS, and vanilla JS. Zero framework dependencies.\n *\n * Svelte: use the `$` prefix — `$eidosQueue`, `$eidosStatus`, etc.\n * Vue: call `.subscribe()` inside a composable with `onUnmounted` cleanup.\n * Vanilla: call `.subscribe(run)` directly; the return value unsubscribes.\n *\n * Each store calls its subscriber whenever any part of the Eidos state changes.\n * For fine-grained subscriptions, use `.getState()` to read the current snapshot\n * and compare manually in the subscriber callback.\n */\n\nimport { useEidosStore } from './store';\nimport type { EidosStore } from './store';\nimport { countQueueByStatus } from './types';\nimport type { ActionQueueItem, ResourceEntry, ReliabilityStats } from './types';\n\n// ── Readable<T> — compatible with Svelte's Readable interface ─────────────────\n\nexport interface EidosReadable<T> {\n /** Subscribe to value changes. Returns an unsubscribe function. */\n subscribe(run: (value: T) => void): () => void;\n /** Read the current value synchronously without subscribing. */\n getState(): T;\n}\n\nfunction shallowEqual<T extends Record<string, unknown>>(a: T, b: T): boolean {\n const keys = Object.keys(a) as (keyof T)[];\n if (keys.length !== Object.keys(b).length) return false;\n for (const k of keys) if (a[k] !== b[k]) return false;\n return true;\n}\n\n// Typed comparator alias so call sites don't need inline casts.\nfunction shallowEq<T extends Record<string, unknown>>(a: T, b: T): boolean {\n return shallowEqual(a, b);\n}\n\nfunction readable<T>(\n selector: (s: EidosStore) => T,\n equal: (a: T, b: T) => boolean = Object.is,\n): EidosReadable<T> {\n return {\n subscribe(run) {\n // Emit current value immediately (Svelte store contract)\n let last = selector(useEidosStore.getState());\n run(last);\n return useEidosStore.subscribe(() => {\n const next = selector(useEidosStore.getState());\n if (!equal(last, next)) {\n last = next;\n run(next);\n }\n });\n },\n getState() {\n return selector(useEidosStore.getState());\n },\n };\n}\n\n// ── Static stores (created once at module scope) ──────────────────────────────\n\n/** Full Eidos state snapshot. Prefer the narrower stores below. */\nexport const eidosStore: EidosReadable<EidosStore> = readable((s) => s);\n\n/** The action queue. Re-notifies on every queue mutation. */\nexport const eidosQueue: EidosReadable<ActionQueueItem[]> = readable((s) => s.queue);\n\n/**\n * Online status + SW lifecycle.\n * Only re-emits when isOnline, swStatus, or swError actually changes.\n */\nexport const eidosStatus: EidosReadable<{\n isOnline: boolean;\n swStatus: EidosStore['swStatus'];\n swError: string | undefined;\n}> = readable(\n (s) => ({ isOnline: s.isOnline, swStatus: s.swStatus, swError: s.swError }),\n shallowEq,\n);\n\n/**\n * Queue counts. Re-emits only when a count actually changes, not on every\n * queue mutation (e.g. a status transition that doesn't change counts is skipped).\n */\nexport const eidosQueueStats: EidosReadable<{\n pending: number;\n failed: number;\n replaying: number;\n total: number;\n}> = readable((s) => countQueueByStatus(s.queue), shallowEq);\n\n/**\n * Cumulative, session-scoped `neverLose` queue outcome counters — opt-in\n * reliability telemetry. Re-emits only when a counter changes. See\n * `EidosConfig.onReliabilityReport` to forward these to an analytics backend.\n */\nexport const eidosReliabilityStats: EidosReadable<ReliabilityStats> = readable(\n (s) => s.reliability,\n shallowEq,\n);\n\n// ── Dynamic stores (created per URL / ID) ─────────────────────────────────────\n\n/**\n * Live cache state for a single registered resource URL.\n * @example\n * // Svelte\n * const entry = eidosResource('/api/products')\n * $: hits = $entry?.cacheHits ?? 0\n */\nexport function eidosResource(url: string): EidosReadable<ResourceEntry | undefined> {\n return readable((s) => s.resources[url]);\n}\n\n/**\n * Live state for a single queue item by ID. Returns `undefined` once the item\n * is removed from the queue (after a successful replay or `clearQueue()`).\n * @example\n * // Svelte\n * const item = eidosAction(queuedResult.id)\n * $: status = $item?.status // 'pending' | 'replaying' | 'succeeded' | 'failed' | undefined\n */\nexport function eidosAction(id: string): EidosReadable<ActionQueueItem | undefined> {\n return readable((s) => s.queue.find((item) => item.id === id));\n}\n\n/**\n * Calls `callback` once each time the action queue drains from non-empty → 0.\n * Framework-agnostic equivalent of `useEidosOnDrain` for Svelte/Vue/vanilla.\n * Returns an unsubscribe function.\n *\n * @example\n * // Svelte\n * onMount(() => onQueueDrain(() => toast.success('All offline actions synced!')))\n */\nexport function onQueueDrain(callback: () => void): () => void {\n let prev = useEidosStore.getState().queue.length;\n return useEidosStore.subscribe(() => {\n const total = useEidosStore.getState().queue.length;\n if (prev > 0 && total === 0) callback();\n prev = total;\n });\n}\n","import { useEffect, useRef, useSyncExternalStore } from 'react';\nimport { useEidosStore } from '../store';\nimport type { EidosStore } from '../store';\nimport { countQueueByStatus } from '../types';\nimport { onQueueDrain } from '../stores';\n\nfunction useStore(): EidosStore;\nfunction useStore<T>(selector: (state: EidosStore) => T): T;\nfunction useStore<T = EidosStore>(selector?: (state: EidosStore) => T): T {\n const fn = selector ?? ((s: EidosStore) => s as unknown as T);\n return useSyncExternalStore(useEidosStore.subscribe, () => fn(useEidosStore.getState()));\n}\n\n/** Full Eidos store — prefer the narrower hooks below for performance. */\nexport function useEidos() {\n return useStore();\n}\n\n/** All registered resources — only re-renders when the resources map changes, not on queue mutations. */\nexport function useEidosResources() {\n return useStore((s) => s.resources);\n}\n\n/** Live state for a single registered resource URL. */\nexport function useEidosResource(url: string) {\n return useStore((s) => s.resources[url]);\n}\n\n/** The current action queue. */\nexport function useEidosQueue() {\n return useStore((s) => s.queue);\n}\n\n/**\n * Live state for a single queue item by ID. Only re-renders when that specific\n * item changes — cheaper than `useEidosQueue().find(id)` which re-renders on\n * any queue mutation.\n */\nexport function useEidosAction(id: string) {\n return useStore((s) => s.queue.find((item) => item.id === id));\n}\n\n/**\n * Online + SW status — cheap subscription, safe to use in header components.\n * Three separate primitive selectors so each only triggers a re-render when\n * its own value changes (no object-reference churn from a combined selector).\n */\nexport function useEidosStatus() {\n const isOnline = useStore((s) => s.isOnline);\n const swStatus = useStore((s) => s.swStatus);\n const swError = useStore((s) => s.swError);\n return { isOnline, swStatus, swError };\n}\n\n/**\n * Queue counts — single subscription, single loop. Re-renders only when a\n * count changes, not on every queue mutation. Use for badges and status bars\n * instead of `useEidosQueue()` when you only need numbers, not full items.\n */\nexport function useEidosQueueStats() {\n // Encode as a comma-separated string so useSyncExternalStore's Object.is\n // comparison bails out correctly when counts haven't changed. One loop,\n // one subscription — cheaper than four separate filter() passes.\n const encoded = useStore((s) => {\n const { pending, failed, replaying, total } = countQueueByStatus(s.queue);\n return `${pending},${failed},${replaying},${total}`;\n });\n const [p, f, r, t] = encoded.split(',');\n return { pending: +p, failed: +f, replaying: +r, total: +t };\n}\n\n/**\n * Calls `callback` once each time the action queue drains from non-empty → 0.\n * Stable callback reference not required — always calls the latest version.\n * Use for \"all offline actions synced!\" toasts.\n *\n * @example\n * useEidosOnDrain(() => toast.success('All offline actions synced!'))\n */\n/**\n * Cumulative, session-scoped `neverLose` queue outcome counters — opt-in\n * reliability telemetry for dashboards/devtools. Re-renders only when a\n * counter changes.\n */\nexport function useEidosReliabilityStats() {\n return useStore((s) => s.reliability);\n}\n\nexport function useEidosOnDrain(callback: () => void) {\n const callbackRef = useRef(callback);\n\n useEffect(() => {\n callbackRef.current = callback;\n });\n\n useEffect(() => onQueueDrain(() => callbackRef.current()), []);\n}\n","export const VERSION = '2.3.0';\n","import { useEidosStore } from './store';\nimport { getSwRegistration } from './sw-bridge';\nimport { VERSION } from './version';\nimport type { ReliabilityStats } from './types';\n\nexport interface EidosDebugSnapshot {\n version: string;\n swStatus: string;\n swError?: string;\n isOnline: boolean;\n resourceCount: number;\n resources: Record<\n string,\n {\n url: string;\n strategy: string;\n status: string;\n cacheHits: number;\n cacheMisses: number;\n cachedAt?: number;\n }\n >;\n queue: {\n id: string;\n actionId: string;\n actionName: string;\n status: string;\n retryCount: number;\n maxRetries: number;\n idempotencyKey: string;\n schemaVersion: number;\n queuedAt: number;\n }[];\n reliability: ReliabilityStats;\n swRegistration: {\n scope: string;\n scriptURL: string;\n state: 'installing' | 'waiting' | 'active' | null;\n } | null;\n}\n\n/**\n * Returns a plain-object snapshot of the current Eidos runtime state.\n * Safe to serialize with `JSON.stringify`. Useful for bug reports,\n * attaching to error tracking breadcrumbs, and the devtools SW tab.\n *\n * @example\n * // Print for a bug report:\n * console.log(JSON.stringify(eidosDebug(), null, 2));\n *\n * // Attach to a Sentry breadcrumb:\n * Sentry.addBreadcrumb({ data: eidosDebug() });\n */\nexport function eidosDebug(): EidosDebugSnapshot {\n const state = useEidosStore.getState();\n const swReg = getSwRegistration();\n\n return {\n version: VERSION,\n swStatus: state.swStatus,\n ...(state.swError !== undefined && { swError: state.swError }),\n isOnline: state.isOnline,\n resourceCount: Object.keys(state.resources).length,\n resources: Object.fromEntries(\n Object.entries(state.resources).map(([url, entry]) => [\n url,\n {\n url: entry.url,\n strategy: entry.strategy.swStrategy,\n status: entry.status,\n cacheHits: entry.cacheHits,\n cacheMisses: entry.cacheMisses,\n ...(entry.cachedAt !== undefined && { cachedAt: entry.cachedAt }),\n },\n ]),\n ),\n queue: state.queue.map((item) => ({\n id: item.id,\n actionId: item.actionId,\n actionName: item.actionName,\n status: item.status,\n retryCount: item.retryCount,\n maxRetries: item.maxRetries,\n idempotencyKey: item.idempotencyKey,\n schemaVersion: item.schemaVersion,\n queuedAt: item.queuedAt,\n })),\n reliability: { ...state.reliability },\n swRegistration: swReg\n ? {\n scope: swReg.scope,\n scriptURL: (swReg.active ?? swReg.waiting ?? swReg.installing)?.scriptURL ?? '',\n state: swReg.installing\n ? 'installing'\n : swReg.waiting\n ? 'waiting'\n : swReg.active\n ? 'active'\n : null,\n }\n : null,\n };\n}\n"],"mappings":"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"}