@sweidos/eidos 2.0.0 → 2.2.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":"action.js","names":[],"sources":["../src/action.ts"],"sourcesContent":["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 const cancel = async (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 Object.defineProperty(wrapped, 'id', { value: actionId, writable: false });\n Object.defineProperty(wrapped, 'config', { value: config, writable: false });\n Object.defineProperty(wrapped, 'cancel', { value: cancel, writable: false });\n\n return wrapped as unknown as ActionHandle<TArgs, TReturn>;\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\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 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 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 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 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 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"],"mappings":"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"}
1
+ {"version":3,"file":"action.js","names":[],"sources":["../src/action.ts"],"sourcesContent":["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"],"mappings":"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"}
package/dist/devtools.js CHANGED
@@ -1,6 +1,31 @@
1
1
  "use client";
2
2
  import { useCallback, useState, useSyncExternalStore } from "react";
3
3
  import { jsx, jsxs } from "react/jsx-runtime";
4
+ //#region src/types.ts
5
+ function emptyReliabilityStats() {
6
+ return {
7
+ queued: 0,
8
+ succeeded: 0,
9
+ failed: 0,
10
+ retried: 0,
11
+ conflicted: 0,
12
+ cancelled: 0
13
+ };
14
+ }
15
+ /** Single pass over the queue — avoids separate .filter() calls per status. */
16
+ function countQueueByStatus(queue) {
17
+ let pending = 0, failed = 0, replaying = 0;
18
+ for (const q of queue) if (q.status === "pending") pending++;
19
+ else if (q.status === "failed") failed++;
20
+ else if (q.status === "replaying") replaying++;
21
+ return {
22
+ pending,
23
+ failed,
24
+ replaying,
25
+ total: queue.length
26
+ };
27
+ }
28
+ //#endregion
4
29
  //#region src/store-slices.ts
5
30
  function createResourceActions(set) {
6
31
  return {
@@ -39,6 +64,15 @@ function createQueueActions(set) {
39
64
  hydrateQueue: (items) => set(() => ({ queue: items }))
40
65
  };
41
66
  }
67
+ function createReliabilityActions(set) {
68
+ return {
69
+ recordReliabilityEvent: (event) => set((s) => ({ reliability: {
70
+ ...s.reliability,
71
+ [event]: s.reliability[event] + 1
72
+ } })),
73
+ resetReliabilityStats: () => set(() => ({ reliability: emptyReliabilityStats() }))
74
+ };
75
+ }
42
76
  //#endregion
43
77
  //#region src/store.ts
44
78
  var _state;
@@ -59,13 +93,15 @@ _state = {
59
93
  swError: void 0,
60
94
  resources: {},
61
95
  queue: [],
96
+ reliability: emptyReliabilityStats(),
62
97
  setOnline: (isOnline) => _set(() => ({ isOnline })),
63
98
  setSwStatus: (swStatus, swError) => _set(() => ({
64
99
  swStatus,
65
100
  swError
66
101
  })),
67
102
  ...createResourceActions(_set),
68
- ...createQueueActions(_set)
103
+ ...createQueueActions(_set),
104
+ ...createReliabilityActions(_set)
69
105
  };
70
106
  function _getState() {
71
107
  return _state;
@@ -89,21 +125,6 @@ var useEidosStore = {
89
125
  }
90
126
  };
91
127
  //#endregion
92
- //#region src/types.ts
93
- /** Single pass over the queue — avoids separate .filter() calls per status. */
94
- function countQueueByStatus(queue) {
95
- let pending = 0, failed = 0, replaying = 0;
96
- for (const q of queue) if (q.status === "pending") pending++;
97
- else if (q.status === "failed") failed++;
98
- else if (q.status === "replaying") replaying++;
99
- return {
100
- pending,
101
- failed,
102
- replaying,
103
- total: queue.length
104
- };
105
- }
106
- //#endregion
107
128
  //#region src/react/hooks.ts
108
129
  function useStore(selector) {
109
130
  const fn = selector ?? ((s) => s);
@@ -146,6 +167,22 @@ function useEidosQueueStats() {
146
167
  total: +t
147
168
  };
148
169
  }
170
+ /**
171
+ * Calls `callback` once each time the action queue drains from non-empty → 0.
172
+ * Stable callback reference not required — always calls the latest version.
173
+ * Use for "all offline actions synced!" toasts.
174
+ *
175
+ * @example
176
+ * useEidosOnDrain(() => toast.success('All offline actions synced!'))
177
+ */
178
+ /**
179
+ * Cumulative, session-scoped `neverLose` queue outcome counters — opt-in
180
+ * reliability telemetry for dashboards/devtools. Re-renders only when a
181
+ * counter changes.
182
+ */
183
+ function useEidosReliabilityStats() {
184
+ return useStore((s) => s.reliability);
185
+ }
149
186
  //#endregion
150
187
  //#region src/sw-bridge.ts
151
188
  var _registration = null;
@@ -306,6 +343,55 @@ function qs() {
306
343
  function callWithContext(fn, args, ctx) {
307
344
  return fn(...args, ctx);
308
345
  }
346
+ /**
347
+ * Cancel an invocation by its `idempotencyKey` (from `ActionContext` /
348
+ * `onOptimistic`). Aborts the in-flight call if `cancellable: true` and
349
+ * still running, otherwise removes a not-yet-replayed queued item.
350
+ * Returns `true` if something was cancelled/removed.
351
+ *
352
+ * Shared by every `ActionHandle.cancel()` and by devtools, which can't
353
+ * address a specific handle from a queue item alone.
354
+ */
355
+ async function cancelByIdempotencyKey(idempotencyKey) {
356
+ const controller = _inflightControllers.get(idempotencyKey);
357
+ if (controller) {
358
+ controller.abort();
359
+ return true;
360
+ }
361
+ const item = (await qs().getAll()).find((i) => i.idempotencyKey === idempotencyKey && i.status === "pending");
362
+ if (!item) return false;
363
+ useEidosStore.getState().removeQueueItem(item.id);
364
+ broadcastQueueSync({
365
+ type: "remove",
366
+ id: item.id
367
+ });
368
+ await qs().remove(item.id);
369
+ return true;
370
+ }
371
+ /**
372
+ * Reset a `'failed'` queue item back to `'pending'` so the next
373
+ * `replayQueue()` retries it — clears `error`/`nextRetryAt` and resets
374
+ * `retryCount` to 0. Returns `true` if the item existed and was failed.
375
+ * Used by devtools' per-item "Retry" action.
376
+ */
377
+ async function requeueItem(id) {
378
+ const item = (await qs().getAll()).find((i) => i.id === id);
379
+ if (!item || item.status !== "failed") return false;
380
+ const update = {
381
+ status: "pending",
382
+ error: void 0,
383
+ nextRetryAt: void 0,
384
+ retryCount: 0
385
+ };
386
+ useEidosStore.getState().updateQueueItem(id, update);
387
+ broadcastQueueSync({
388
+ type: "update",
389
+ id,
390
+ update
391
+ });
392
+ await qs().update(id, update);
393
+ return true;
394
+ }
309
395
  function isAbortError(err) {
310
396
  return err instanceof DOMException && err.name === "AbortError";
311
397
  }
@@ -354,6 +440,7 @@ async function _markSucceeded(item, store) {
354
440
  status: "succeeded",
355
441
  completedAt
356
442
  });
443
+ store.recordReliabilityEvent("succeeded");
357
444
  broadcastQueueSync({
358
445
  type: "update",
359
446
  id: item.id,
@@ -404,6 +491,7 @@ async function _resolveConflict(item, store, err) {
404
491
  }
405
492
  if (resolution === "skip") {
406
493
  store.removeQueueItem(item.id);
494
+ store.recordReliabilityEvent("conflicted");
407
495
  broadcastQueueSync({
408
496
  type: "remove",
409
497
  id: item.id
@@ -431,6 +519,7 @@ async function _scheduleRetryOrFail(item, store, err) {
431
519
  retryCount
432
520
  };
433
521
  store.updateQueueItem(item.id, update);
522
+ store.recordReliabilityEvent("failed");
434
523
  broadcastQueueSync({
435
524
  type: "update",
436
525
  id: item.id,
@@ -450,6 +539,7 @@ async function _scheduleRetryOrFail(item, store, err) {
450
539
  nextRetryAt: Date.now() + backoffMs(retryCount)
451
540
  };
452
541
  store.updateQueueItem(item.id, update);
542
+ store.recordReliabilityEvent("retried");
453
543
  broadcastQueueSync({
454
544
  type: "update",
455
545
  id: item.id,
@@ -480,6 +570,7 @@ async function _replayItem(item, store) {
480
570
  } catch (err) {
481
571
  if (isAbortError(err)) {
482
572
  store.removeQueueItem(item.id);
573
+ store.recordReliabilityEvent("cancelled");
483
574
  broadcastQueueSync({
484
575
  type: "remove",
485
576
  id: item.id
@@ -658,7 +749,10 @@ var ICONS = {
658
749
  trash: "M3 6h18M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2m3 0-1 14a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1L4 6h16z",
659
750
  arrowUp: "M12 19V5M5 12l7-7 7 7",
660
751
  arrowDown: "M12 5v14M19 12l-7 7-7-7",
661
- clock: "M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20zM12 6v6l4 2"
752
+ clock: "M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20zM12 6v6l4 2",
753
+ x: "M18 6 6 18M6 6l12 12",
754
+ rotateCcw: "M3 12a9 9 0 1 0 2.6-6.4M3 12V5m0 7h7",
755
+ activity: "M22 12h-4l-3 9L9 3l-3 9H2"
662
756
  };
663
757
  function positionStyle(p) {
664
758
  const base = {
@@ -695,6 +789,7 @@ function EidosDevtools({ position = "bottom-right", defaultOpen = false }) {
695
789
  const { pending, failed, replaying } = useEidosQueueStats();
696
790
  const resources = useEidosResources();
697
791
  const resourceList = Object.values(resources);
792
+ const reliability = useEidosReliabilityStats();
698
793
  const badgeCount = pending + failed + replaying;
699
794
  const toggleOffline = useCallback(() => {
700
795
  const next = !simOffline;
@@ -898,7 +993,11 @@ function EidosDevtools({ position = "bottom-right", defaultOpen = false }) {
898
993
  flexShrink: 0,
899
994
  background: C.surface
900
995
  },
901
- children: ["queue", "cache"].map((t) => /* @__PURE__ */ jsx("button", {
996
+ children: [
997
+ "queue",
998
+ "cache",
999
+ "reliability"
1000
+ ].map((t) => /* @__PURE__ */ jsx("button", {
902
1001
  role: "tab",
903
1002
  "aria-selected": tab === t,
904
1003
  onClick: () => setTab(t),
@@ -919,7 +1018,7 @@ function EidosDevtools({ position = "bottom-right", defaultOpen = false }) {
919
1018
  letterSpacing: "0.05em",
920
1019
  transition: "color 0.15s, border-color 0.15s"
921
1020
  },
922
- children: t === "queue" ? `Queue (${queue.length})` : `Cache (${resourceList.length})`
1021
+ children: t === "queue" ? `Queue (${queue.length})` : t === "cache" ? `Cache (${resourceList.length})` : "Reliability"
923
1022
  }, t))
924
1023
  }),
925
1024
  /* @__PURE__ */ jsx("div", {
@@ -932,7 +1031,7 @@ function EidosDevtools({ position = "bottom-right", defaultOpen = false }) {
932
1031
  queue,
933
1032
  onReplay: handleReplay,
934
1033
  onClear: handleClear
935
- }) : /* @__PURE__ */ jsx(CacheTab, { resources: resourceList })
1034
+ }) : tab === "cache" ? /* @__PURE__ */ jsx(CacheTab, { resources: resourceList }) : /* @__PURE__ */ jsx(ReliabilityTab, { stats: reliability })
936
1035
  })
937
1036
  ]
938
1037
  }), toggleBtn]
@@ -1022,6 +1121,7 @@ function QueueTab({ queue, onReplay, onClear }) {
1022
1121
  children: item.priority
1023
1122
  }),
1024
1123
  /* @__PURE__ */ jsx("span", {
1124
+ title: `idempotencyKey: ${item.idempotencyKey}`,
1025
1125
  style: {
1026
1126
  flex: 1,
1027
1127
  overflow: "hidden",
@@ -1043,6 +1143,38 @@ function QueueTab({ queue, onReplay, onClear }) {
1043
1143
  "/",
1044
1144
  item.maxRetries
1045
1145
  ]
1146
+ }),
1147
+ item.status === "pending" && /* @__PURE__ */ jsx("button", {
1148
+ onClick: () => void cancelByIdempotencyKey(item.idempotencyKey),
1149
+ title: "Cancel — remove this item before it's replayed",
1150
+ "aria-label": `Cancel ${item.actionName}`,
1151
+ ...withFocusRing(),
1152
+ style: {
1153
+ ...btn("ghost"),
1154
+ padding: "2px 4px",
1155
+ minHeight: 18,
1156
+ color: C.red
1157
+ },
1158
+ children: /* @__PURE__ */ jsx(Icon, {
1159
+ path: ICONS.x,
1160
+ size: 11
1161
+ })
1162
+ }),
1163
+ item.status === "failed" && /* @__PURE__ */ jsx("button", {
1164
+ onClick: () => void requeueItem(item.id),
1165
+ title: "Retry — reset for the next replay",
1166
+ "aria-label": `Retry ${item.actionName}`,
1167
+ ...withFocusRing(),
1168
+ style: {
1169
+ ...btn("ghost"),
1170
+ padding: "2px 4px",
1171
+ minHeight: 18,
1172
+ color: C.blue
1173
+ },
1174
+ children: /* @__PURE__ */ jsx(Icon, {
1175
+ path: ICONS.rotateCcw,
1176
+ size: 11
1177
+ })
1046
1178
  })
1047
1179
  ]
1048
1180
  }, item.id))
@@ -1151,5 +1283,106 @@ function CacheTab({ resources }) {
1151
1283
  ]
1152
1284
  }, res.url)) });
1153
1285
  }
1286
+ function ReliabilityTab({ stats }) {
1287
+ const rows = [
1288
+ {
1289
+ label: "Queued",
1290
+ key: "queued",
1291
+ color: C.blue
1292
+ },
1293
+ {
1294
+ label: "Succeeded",
1295
+ key: "succeeded",
1296
+ color: C.green
1297
+ },
1298
+ {
1299
+ label: "Retried",
1300
+ key: "retried",
1301
+ color: C.yellow
1302
+ },
1303
+ {
1304
+ label: "Failed",
1305
+ key: "failed",
1306
+ color: C.red
1307
+ },
1308
+ {
1309
+ label: "Conflicted",
1310
+ key: "conflicted",
1311
+ color: C.purple
1312
+ },
1313
+ {
1314
+ label: "Cancelled",
1315
+ key: "cancelled",
1316
+ color: C.muted
1317
+ }
1318
+ ];
1319
+ const total = stats.queued;
1320
+ const successRate = total > 0 ? Math.round(stats.succeeded / total * 100) : null;
1321
+ return /* @__PURE__ */ jsxs("div", { children: [
1322
+ /* @__PURE__ */ jsxs("div", {
1323
+ style: {
1324
+ display: "flex",
1325
+ alignItems: "center",
1326
+ gap: 8,
1327
+ padding: "8px 12px",
1328
+ borderBottom: `1px solid ${C.border}`
1329
+ },
1330
+ children: [/* @__PURE__ */ jsx("span", {
1331
+ style: {
1332
+ color: C.muted,
1333
+ display: "inline-flex"
1334
+ },
1335
+ children: /* @__PURE__ */ jsx(Icon, {
1336
+ path: ICONS.activity,
1337
+ size: 12
1338
+ })
1339
+ }), /* @__PURE__ */ jsx("span", {
1340
+ style: {
1341
+ color: C.muted,
1342
+ fontSize: 10
1343
+ },
1344
+ children: successRate === null ? "No queued actions yet this session" : `${successRate}% succeeded`
1345
+ })]
1346
+ }),
1347
+ rows.map(({ label, key, color }) => /* @__PURE__ */ jsxs("div", {
1348
+ style: {
1349
+ display: "flex",
1350
+ alignItems: "center",
1351
+ justifyContent: "space-between",
1352
+ padding: "7px 12px",
1353
+ borderBottom: `1px solid ${C.border}`
1354
+ },
1355
+ children: [/* @__PURE__ */ jsx("span", {
1356
+ style: { color: C.text },
1357
+ children: label
1358
+ }), /* @__PURE__ */ jsx("span", {
1359
+ style: pill(color),
1360
+ children: stats[key]
1361
+ })]
1362
+ }, key)),
1363
+ /* @__PURE__ */ jsxs("div", {
1364
+ style: {
1365
+ padding: "8px 12px",
1366
+ color: C.muted,
1367
+ fontSize: 10
1368
+ },
1369
+ children: [
1370
+ "Session-only counters — reset on reload. Wire up",
1371
+ " ",
1372
+ /* @__PURE__ */ jsx("code", {
1373
+ style: { color: C.cyan },
1374
+ children: "onReliabilityReport"
1375
+ }),
1376
+ " in",
1377
+ " ",
1378
+ /* @__PURE__ */ jsx("code", {
1379
+ style: { color: C.cyan },
1380
+ children: "initEidos()"
1381
+ }),
1382
+ " to forward these to analytics."
1383
+ ]
1384
+ })
1385
+ ] });
1386
+ }
1154
1387
  //#endregion
1155
1388
  export { EidosDevtools };
package/dist/eidos.cjs CHANGED
@@ -1,4 +1,4 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});let S=require("react"),H=require("react/jsx-runtime");function pe(e){return{registerResource:(t,r)=>e(n=>({resources:{...n.resources,[t]:r}})),updateResource:(t,r)=>e(n=>({resources:{...n.resources,[t]:n.resources[t]?{...n.resources[t],...r}:n.resources[t]}})),unregisterResource:t=>e(r=>({resources:Object.fromEntries(Object.entries(r.resources).filter(([n])=>n!==t))}))}}function he(e){return{addQueueItem:t=>e(r=>({queue:[...r.queue,t]})),updateQueueItem:(t,r)=>e(n=>({queue:n.queue.map(a=>a.id===t?{...a,...r}:a)})),batchUpdateQueueItems:t=>e(r=>{const n=new Map(t.map(a=>[a.id,a.update]));return{queue:r.queue.map(a=>{const s=n.get(a.id);return s?{...a,...s}:a})}}),removeQueueItem:t=>e(r=>({queue:r.queue.filter(n=>n.id!==t)})),hydrateQueue:t=>e(()=>({queue:t}))}}var w,D=new Set;function G(){D.forEach(e=>e())}function k(e){w={...w,...e(w)},G()}w={isOnline:typeof navigator>"u"||navigator.onLine!==!1,swStatus:"idle",swError:void 0,resources:{},queue:[],setOnline:e=>k(()=>({isOnline:e})),setSwStatus:(e,t)=>k(()=>({swStatus:e,swError:t})),...pe(k),...he(k)};function ye(){return w}function ge(e){return D.add(e),()=>{D.delete(e)}}var o={getState:ye,subscribe:ge,setState:e=>{const t=typeof e=="function"?e(w):e;w={...w,...t},G()}},m=null,T=[];function Y(){return m}async function we(e){if(typeof navigator>"u"||!("serviceWorker"in navigator)){o.getState().setSwStatus("unsupported");return}const t=o.getState();t.setSwStatus("registering");try{m=await navigator.serviceWorker.register(e,{scope:"/"}),await ve(m),t.setSwStatus("active"),navigator.serviceWorker.addEventListener("message",Ee),window.addEventListener("online",()=>t.setOnline(!0)),window.addEventListener("offline",()=>t.setOnline(!1)),_e()}catch(r){t.setSwStatus("error",String(r))}}function ve(e){return new Promise(t=>{if(e.active){t();return}const r=e.installing??e.waiting;if(!r){t();return}const n=setTimeout(t,1e4);r.addEventListener("statechange",function a(){r.state==="activated"&&(clearTimeout(n),r.removeEventListener("statechange",a),t())})})}function _(e){const t=m?.active;t?t.postMessage(e):T.push(e)}var J=null;function me(e){J=e}function Se(){try{return typeof navigator<"u"&&"serviceWorker"in navigator&&m!==null&&"sync"in m}catch{return!1}}var M={};function be(e){M=e}function Ee(e){const t=e.data;if(!t?.type)return;const r=o.getState(),{type:n,url:a}=t;if(n==="EIDOS_BACKGROUND_SYNC"){J?.();return}if(n==="EIDOS_NOTIFICATION_CLICK"){M.onNotificationClick?.(t.data);return}if(n==="EIDOS_SUBSCRIPTION_EXPIRED"){M.onSubscriptionExpired?.(t.subscription);return}if(a)switch(n){case"EIDOS_CACHE_HIT":{const s=r.resources[a];r.updateResource(a,{status:"fresh",lastEvent:"cache-hit",cacheHits:(s?.cacheHits??0)+1});break}case"EIDOS_CACHE_UPDATED":r.updateResource(a,{status:"fresh",lastEvent:"cache-updated",cachedAt:Date.now()});break;case"EIDOS_NETWORK_ERROR":r.updateResource(a,{status:"error",lastEvent:"network-error"});break}}function Re(e){_({type:"EIDOS_SIMULATE_OFFLINE",enabled:e}),o.getState().setOnline(!e)}function _e(){const e=m?.active;if(e){for(const t of T)e.postMessage(t);T=[]}}var v=new Map,C=new Map,X=null;function ke(e){X=e}function q(e){return e.includes("*")||/:[^/]+/.test(e)}function Ie(e){return"^"+e.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*\*/g,".+").replace(/\*/g,"[^/]+").replace(/:[^/]+/g,"[^/]+")+"$"}function z(e,t){const r=qe(t),n=q(e)?Ie(e):void 0,a={url:e,config:t,strategy:r,status:"idle",cacheHits:0,cacheMisses:0};return o.getState().registerResource(e,a),_({type:"EIDOS_REGISTER_RESOURCE",url:e,strategy:r.swStrategy,cacheName:r.cacheName,...n!==void 0&&{pattern:n}}),{strategy:r,regexStr:n}}function Z(e,t,r){return async()=>{_({type:"EIDOS_CLEAR_CACHE",url:e});const n=await caches.open(t.cacheName).catch(()=>null);if(n){const a=await n.keys(),s=r?new RegExp(r):null,i=e.startsWith("http");await Promise.all(a.filter(c=>{const u=c.url,d=new URL(u).pathname;return s?s.test(i?u:d):i?u===e:u===e||d===e}).map(c=>n.delete(c)))}q(e)||o.getState().updateResource(e,{status:"stale",cachedAt:void 0,lastEvent:"cache-cleared",cacheHits:0,cacheMisses:0}),X?.(["eidos",e])}}function ee(e){return()=>{v.delete(e),_({type:"EIDOS_UNREGISTER_RESOURCE",url:e}),o.getState().unregisterResource(e)}}function Oe(e,t){if(q(e))throw new Error(`[eidos] resource('${e}') is a URL pattern — use resourcePattern('${e}', config) instead. Pattern handles only support invalidate()/unregister(); the SW intercepts matching requests automatically.`);if(v.has(e))return v.get(e);const{strategy:r}=z(e,t),n={url:e,config:t,strategy:r,fetch:async()=>{const a=C.get(e);if(a)return a.then(i=>i.clone());const s=Qe(e,t,r);return C.set(e,s),s.finally(()=>C.delete(e)).catch(()=>{}),s.then(i=>i.clone())},json:async()=>(await n.fetch()).json(),query:()=>({queryKey:["eidos",e],queryFn:()=>n.json()}),prefetch:async()=>{await n.fetch()},invalidate:Z(e,r,void 0),unregister:ee(e)};return v.set(e,n),n}function Ae(e,t){if(!q(e))throw new Error(`[eidos] resourcePattern('${e}') is not a URL pattern — use resource('${e}', config) instead.`);if(v.has(e))return v.get(e);const{strategy:r,regexStr:n}=z(e,t),a={url:e,config:t,strategy:r,invalidate:Z(e,r,n),unregister:ee(e)};return v.set(e,a),a}async function Qe(e,t,r){const n=o.getState();n.updateResource(e,{status:"fetching",fetchedAt:Date.now()});const a=await caches.open(r.cacheName).catch(()=>null);try{if(r.swStrategy!=="network-first"){const c=a?await a.match(e).catch(()=>null):null,u=o.getState().resources[e],d=t.maxAge!==void 0&&u?.cachedAt!==void 0&&Date.now()-u.cachedAt>t.maxAge;if(c&&!d)return n.updateResource(e,{status:"fresh",lastEvent:"cache-hit",cacheHits:(u?.cacheHits??0)+1}),r.swStrategy==="stale-while-revalidate"&&fetch(e,{signal:AbortSignal.timeout(5e3)}).then(async l=>{l.ok&&a&&(await a.put(e,l.clone()),o.getState().updateResource(e,{cachedAt:Date.now(),lastEvent:"cache-updated"}))}).catch(()=>{}),c;const g=o.getState().resources[e];n.updateResource(e,{cacheMisses:(g?.cacheMisses??0)+1})}const s=await fetch(e);if(s.ok)return a&&await a.put(e,s.clone()),n.updateResource(e,{status:"fresh",cachedAt:Date.now(),lastEvent:"cache-updated"}),s;n.updateResource(e,{status:s.status===503?"offline":"error"});const i=s.headers.get("X-Eidos-Offline")==="true";throw new Error(i?`offline: no cached response for ${e}`:`${s.status} ${s.statusText}`)}catch(s){const i=a?await a.match(e).catch(()=>null):null;if(i){const c=o.getState().resources[e];return n.updateResource(e,{status:"fresh",lastEvent:"cache-hit",cacheHits:(c?.cacheHits??0)+1}),i}throw n.updateResource(e,{status:"error"}),s}}function qe(e){const t=e.strategy;return e.offline?B(t??"stale-while-revalidate",e.cacheName):B(t??"network-first",e.cacheName)}var Ce={"stale-while-revalidate":"StaleWhileRevalidate","cache-first":"CacheFirst","network-first":"NetworkFirst"},xe={"stale-while-revalidate":{reasoning:"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.",behavior:["Cache hit → return immediately, kick off background revalidation","Cache miss → fetch from network, cache the response, return it","Offline → return cached version if available, 503 if not","Reconnect → next request triggers a background refresh"],equivalentCode:`// Workbox equivalent
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});let R=require("react"),F=require("react/jsx-runtime");function J(){return{queued:0,succeeded:0,failed:0,retried:0,conflicted:0,cancelled:0}}function X(e){let t=0,n=0,r=0;for(const a of e)a.status==="pending"?t++:a.status==="failed"?n++:a.status==="replaying"&&r++;return{pending:t,failed:n,replaying:r,total:e.length}}function we(e){return{registerResource:(t,n)=>e(r=>({resources:{...r.resources,[t]:n}})),updateResource:(t,n)=>e(r=>({resources:{...r.resources,[t]:r.resources[t]?{...r.resources[t],...n}:r.resources[t]}})),unregisterResource:t=>e(n=>({resources:Object.fromEntries(Object.entries(n.resources).filter(([r])=>r!==t))}))}}function ve(e){return{addQueueItem:t=>e(n=>({queue:[...n.queue,t]})),updateQueueItem:(t,n)=>e(r=>({queue:r.queue.map(a=>a.id===t?{...a,...n}:a)})),batchUpdateQueueItems:t=>e(n=>{const r=new Map(t.map(a=>[a.id,a.update]));return{queue:n.queue.map(a=>{const s=r.get(a.id);return s?{...a,...s}:a})}}),removeQueueItem:t=>e(n=>({queue:n.queue.filter(r=>r.id!==t)})),hydrateQueue:t=>e(()=>({queue:t}))}}function me(e){return{recordReliabilityEvent:t=>e(n=>({reliability:{...n.reliability,[t]:n.reliability[t]+1}})),resetReliabilityStats:()=>e(()=>({reliability:J()}))}}var g,T=new Set;function z(){T.forEach(e=>e())}function E(e){g={...g,...e(g)},z()}g={isOnline:typeof navigator>"u"||navigator.onLine!==!1,swStatus:"idle",swError:void 0,resources:{},queue:[],reliability:J(),setOnline:e=>E(()=>({isOnline:e})),setSwStatus:(e,t)=>E(()=>({swStatus:e,swError:t})),...we(E),...ve(E),...me(E)};function Se(){return g}function be(e){return T.add(e),()=>{T.delete(e)}}var o={getState:Se,subscribe:be,setState:e=>{const t=typeof e=="function"?e(g):e;g={...g,...t},z()}},v=null,M=[];function Z(){return v}async function Ee(e){if(typeof navigator>"u"||!("serviceWorker"in navigator)){o.getState().setSwStatus("unsupported");return}const t=o.getState();t.setSwStatus("registering");try{v=await navigator.serviceWorker.register(e,{scope:"/"}),await Re(v),t.setSwStatus("active"),navigator.serviceWorker.addEventListener("message",Oe),window.addEventListener("online",()=>t.setOnline(!0)),window.addEventListener("offline",()=>t.setOnline(!1)),qe()}catch(n){t.setSwStatus("error",String(n))}}function Re(e){return new Promise(t=>{if(e.active){t();return}const n=e.installing??e.waiting;if(!n){t();return}const r=setTimeout(t,1e4);n.addEventListener("statechange",function a(){n.state==="activated"&&(clearTimeout(r),n.removeEventListener("statechange",a),t())})})}function _(e){const t=v?.active;t?t.postMessage(e):M.push(e)}var ee=null;function Ie(e){ee=e}function _e(){try{return typeof navigator<"u"&&"serviceWorker"in navigator&&v!==null&&"sync"in v}catch{return!1}}var j={};function ke(e){j=e}function Oe(e){const t=e.data;if(!t?.type)return;const n=o.getState(),{type:r,url:a}=t;if(r==="EIDOS_BACKGROUND_SYNC"){ee?.();return}if(r==="EIDOS_NOTIFICATION_CLICK"){j.onNotificationClick?.(t.data);return}if(r==="EIDOS_SUBSCRIPTION_EXPIRED"){j.onSubscriptionExpired?.(t.subscription);return}if(a)switch(r){case"EIDOS_CACHE_HIT":{const s=n.resources[a];n.updateResource(a,{status:"fresh",lastEvent:"cache-hit",cacheHits:(s?.cacheHits??0)+1});break}case"EIDOS_CACHE_UPDATED":n.updateResource(a,{status:"fresh",lastEvent:"cache-updated",cachedAt:Date.now()});break;case"EIDOS_NETWORK_ERROR":n.updateResource(a,{status:"error",lastEvent:"network-error"});break}}function Ae(e){_({type:"EIDOS_SIMULATE_OFFLINE",enabled:e}),o.getState().setOnline(!e)}function qe(){const e=v?.active;if(e){for(const t of M)e.postMessage(t);M=[]}}var w=new Map,x=new Map,te=null;function Qe(e){te=e}function C(e){return e.includes("*")||/:[^/]+/.test(e)}function Ce(e){return"^"+e.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*\*/g,".+").replace(/\*/g,"[^/]+").replace(/:[^/]+/g,"[^/]+")+"$"}function ne(e,t){const n=Ne(t),r=C(e)?Ce(e):void 0,a={url:e,config:t,strategy:n,status:"idle",cacheHits:0,cacheMisses:0};return o.getState().registerResource(e,a),_({type:"EIDOS_REGISTER_RESOURCE",url:e,strategy:n.swStrategy,cacheName:n.cacheName,...r!==void 0&&{pattern:r}}),{strategy:n,regexStr:r}}function re(e,t,n){return async()=>{_({type:"EIDOS_CLEAR_CACHE",url:e});const r=await caches.open(t.cacheName).catch(()=>null);if(r){const a=await r.keys(),s=n?new RegExp(n):null,i=e.startsWith("http");await Promise.all(a.filter(c=>{const u=c.url,f=new URL(u).pathname;return s?s.test(i?u:f):i?u===e:u===e||f===e}).map(c=>r.delete(c)))}C(e)||o.getState().updateResource(e,{status:"stale",cachedAt:void 0,lastEvent:"cache-cleared",cacheHits:0,cacheMisses:0}),te?.(["eidos",e])}}function ae(e){return()=>{w.delete(e),_({type:"EIDOS_UNREGISTER_RESOURCE",url:e}),o.getState().unregisterResource(e)}}function xe(e,t){if(C(e))throw new Error(`[eidos] resource('${e}') is a URL pattern — use resourcePattern('${e}', config) instead. Pattern handles only support invalidate()/unregister(); the SW intercepts matching requests automatically.`);if(w.has(e))return w.get(e);const{strategy:n}=ne(e,t),r={url:e,config:t,strategy:n,fetch:async()=>{const a=x.get(e);if(a)return a.then(i=>i.clone());const s=De(e,t,n);return x.set(e,s),s.finally(()=>x.delete(e)).catch(()=>{}),s.then(i=>i.clone())},json:async()=>(await r.fetch()).json(),query:()=>({queryKey:["eidos",e],queryFn:()=>r.json()}),prefetch:async()=>{await r.fetch()},invalidate:re(e,n,void 0),unregister:ae(e)};return w.set(e,r),r}function Pe(e,t){if(!C(e))throw new Error(`[eidos] resourcePattern('${e}') is not a URL pattern — use resource('${e}', config) instead.`);if(w.has(e))return w.get(e);const{strategy:n,regexStr:r}=ne(e,t),a={url:e,config:t,strategy:n,invalidate:re(e,n,r),unregister:ae(e)};return w.set(e,a),a}async function De(e,t,n){const r=o.getState();r.updateResource(e,{status:"fetching",fetchedAt:Date.now()});const a=await caches.open(n.cacheName).catch(()=>null);try{if(n.swStrategy!=="network-first"){const c=a?await a.match(e).catch(()=>null):null,u=o.getState().resources[e],f=t.maxAge!==void 0&&u?.cachedAt!==void 0&&Date.now()-u.cachedAt>t.maxAge;if(c&&!f)return r.updateResource(e,{status:"fresh",lastEvent:"cache-hit",cacheHits:(u?.cacheHits??0)+1}),n.swStrategy==="stale-while-revalidate"&&fetch(e,{signal:AbortSignal.timeout(5e3)}).then(async S=>{S.ok&&a&&(await a.put(e,S.clone()),o.getState().updateResource(e,{cachedAt:Date.now(),lastEvent:"cache-updated"}))}).catch(()=>{}),c;const p=o.getState().resources[e];r.updateResource(e,{cacheMisses:(p?.cacheMisses??0)+1})}const s=await fetch(e);if(s.ok)return a&&await a.put(e,s.clone()),r.updateResource(e,{status:"fresh",cachedAt:Date.now(),lastEvent:"cache-updated"}),s;r.updateResource(e,{status:s.status===503?"offline":"error"});const i=s.headers.get("X-Eidos-Offline")==="true";throw new Error(i?`offline: no cached response for ${e}`:`${s.status} ${s.statusText}`)}catch(s){const i=a?await a.match(e).catch(()=>null):null;if(i){const c=o.getState().resources[e];return r.updateResource(e,{status:"fresh",lastEvent:"cache-hit",cacheHits:(c?.cacheHits??0)+1}),i}throw r.updateResource(e,{status:"error"}),s}}function Ne(e){const t=e.strategy;return e.offline?V(t??"stale-while-revalidate",e.cacheName,e.version):V(t??"network-first",e.cacheName,e.version)}var Te={"stale-while-revalidate":"StaleWhileRevalidate","cache-first":"CacheFirst","network-first":"NetworkFirst"},Me={"stale-while-revalidate":{reasoning:"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.",behavior:["Cache hit → return immediately, kick off background revalidation","Cache miss → fetch from network, cache the response, return it","Offline → return cached version if available, 503 if not","Reconnect → next request triggers a background refresh"],equivalentCode:`// Workbox equivalent
2
2
  new StaleWhileRevalidate({
3
3
  cacheName: 'eidos-resources-v1',
4
4
  plugins: [new ExpirationPlugin({ maxEntries: 60 })],
@@ -10,6 +10,6 @@ new CacheFirst({
10
10
  new NetworkFirst({
11
11
  cacheName: 'eidos-resources-v1',
12
12
  networkTimeoutSeconds: 3,
13
- })`}};function B(e,t){const r=xe[e];return{name:Ce[e],swStrategy:e,cacheName:t??"eidos-resources-v1",reasoning:r.reasoning,behavior:r.behavior,equivalentCode:""}}async function Pe(e){const t=await Promise.allSettled(e.map(n=>n.prefetch())),r=t.filter(n=>n.status==="rejected").map(n=>n.reason);return{warmed:t.filter(n=>n.status==="fulfilled").length,failed:r.length,errors:r}}var Ne="eidos",De=1,f="action-queue",x=null;function b(){return x?Promise.resolve(x):new Promise((e,t)=>{const r=indexedDB.open(Ne,De);r.onupgradeneeded=n=>{const a=n.target.result;if(!a.objectStoreNames.contains(f)){const s=a.createObjectStore(f,{keyPath:"id"});s.createIndex("status","status",{unique:!1}),s.createIndex("actionId","actionId",{unique:!1})}},r.onsuccess=()=>{x=r.result,e(r.result)},r.onerror=()=>t(r.error)})}async function Te(e){const t=await b();return new Promise((r,n)=>{const a=t.transaction(f,"readwrite");a.objectStore(f).add(e),a.oncomplete=()=>r(),a.onerror=()=>n(a.error)})}async function te(){const e=await b();return new Promise((t,r)=>{const n=e.transaction(f,"readonly").objectStore(f).getAll();n.onsuccess=()=>t(n.result),n.onerror=()=>r(n.error)})}async function Me(e,t){const r=await b();return new Promise((n,a)=>{const s=r.transaction(f,"readwrite"),i=s.objectStore(f),c=i.get(e);c.onsuccess=()=>{c.result&&i.put({...c.result,...t})},s.oncomplete=()=>n(),s.onerror=()=>a(s.error)})}async function je(e){const t=await b();return new Promise((r,n)=>{const a=t.transaction(f,"readwrite");a.objectStore(f).delete(e),a.oncomplete=()=>r(),a.onerror=()=>n(a.error)})}async function Ue(){const e=await b();function t(a){return new Promise((s,i)=>{const c=e.transaction(f,"readonly").objectStore(f).index("status"),u=[],d=c.openCursor(IDBKeyRange.only(a));d.onsuccess=g=>{const l=g.target.result;l?(u.push(l.value),l.continue()):s(u)},d.onerror=()=>i(d.error)})}const[r,n]=await Promise.all([t("pending"),t("failed")]);return[...r,...n]}async function Ke(){const e=await b();return new Promise((t,r)=>{const n=e.transaction(f,"readwrite");n.objectStore(f).clear(),n.oncomplete=()=>t(),n.onerror=()=>r(n.error)})}var ne={add:Te,getAll:te,getPending:Ue,update:Me,remove:je,clear:Ke},re=null;function Le(e){re=e}function $(){return re}var We="eidos-queue-sync",I;function ae(){return I!==void 0||(I=typeof BroadcastChannel>"u"?null:new BroadcastChannel(We)),I}function y(e){ae()?.postMessage(e)}function $e(){const e=ae();if(!e)return()=>{};const t=r=>{const n=o.getState(),a=r.data;switch(a.type){case"update":n.updateQueueItem(a.id,a.update);break;case"batchUpdate":n.batchUpdateQueueItems(a.updates);break;case"remove":n.removeQueueItem(a.id);break}};return e.addEventListener("message",t),()=>e.removeEventListener("message",t)}function se(e){let t=0,r=0,n=0;for(const a of e)a.status==="pending"?t++:a.status==="failed"?r++:a.status==="replaying"&&n++;return{pending:t,failed:r,replaying:n,total:e.length}}var A=new Map,ie=new Map,oe=new Map,ce=new Map,R=new Map;function p(){return $()??ne}function j(){return crypto.randomUUID()}function U(e,t,r){return e(...t,r)}function He(e,t){const r=t.name||e.name||j(),n=t.namespace?`${t.namespace}::${r}`:r;if(A.has(n))throw new Error(`[eidos] duplicate action id "${n}" — an action with this id is already registered. Pass a unique config.name or config.namespace.`);A.set(n,e),ce.set(n,t),t.onRollback&&ie.set(n,t.onRollback),t.conflict&&oe.set(n,t.conflict);const a=async(...i)=>{const{isOnline:c}=o.getState(),u=j();let d;if(t.cancellable){const l=new AbortController;R.set(u,l),d=l.signal}const g={idempotencyKey:u,attempt:0,signal:d};t.onOptimistic?.(...i,g);try{if(t.reliability==="neverLose"){if(!c)return F(n,n,i,t,u);try{return await U(e,i,g)}catch(l){if(ue(l))throw l;return F(n,n,i,t,u)}}try{return await U(e,i,g)}catch(l){throw t.onRollback?.(...i,g),l}}finally{t.cancellable&&R.delete(u)}},s=async i=>{const c=R.get(i);if(c)return c.abort(),!0;const u=(await p().getAll()).find(d=>d.idempotencyKey===i&&d.status==="pending");return u?(o.getState().removeQueueItem(u.id),y({type:"remove",id:u.id}),await p().remove(u.id),!0):!1};return Object.defineProperty(a,"id",{value:n,writable:!1}),Object.defineProperty(a,"config",{value:t,writable:!1}),Object.defineProperty(a,"cancel",{value:s,writable:!1}),a}async function F(e,t,r,n,a){const s=j(),i={schemaVersion:2,id:s,actionId:e,actionName:t,idempotencyKey:a,args:r,queuedAt:Date.now(),retryCount:0,maxRetries:n.maxRetries??3,status:"pending",priority:n.priority??"normal"};await p().add(i),o.getState().addQueueItem(i);try{const c=Y();c&&"sync"in c&&await c.sync.register("eidos-queue-replay")}catch{}return{queued:!0,id:s,message:`"${t}" queued — will execute when online`}}function ue(e){return e instanceof DOMException&&e.name==="AbortError"}function Be(e){if(e instanceof Response)return e.status>=400&&e.status<500;if(typeof e=="object"&&e!==null){const t=e.status;if(typeof t=="number")return t>=400&&t<500}return!1}function Fe(e){return Math.min(2e3*2**e,3e5)*(.8+Math.random()*.4)}function O(){return{attempted:0,succeeded:0,failed:0,retrying:0,skipped:0,conflicted:0,cancelled:0}}var P=!1,Ve="eidos-queue-replay";async function Q(){const e=o.getState();if(!e.isOnline)return O();if(typeof navigator<"u"&&navigator.locks)return navigator.locks.request(Ve,{ifAvailable:!0},async t=>t?V(e):O());if(P)return O();P=!0;try{return await V(e)}finally{P=!1}}async function Ge(e,t){const r=Date.now();t.updateQueueItem(e.id,{status:"succeeded",completedAt:r}),y({type:"update",id:e.id,update:{status:"succeeded",completedAt:r}}),await p().update(e.id,{status:"succeeded",completedAt:r}),setTimeout(()=>{t.removeQueueItem(e.id),y({type:"remove",id:e.id}),p().remove(e.id)},3e3)}async function Ye(e,t,r){const n=oe.get(e.actionId);let a;if(n)switch(n.strategy){case"serverWins":a="skip";break;case"clientWins":a="retry";break;case"merge":case"custom":{const s={error:r,args:e.args,attempt:e.retryCount,idempotencyKey:e.idempotencyKey};a=n.resolve?.(s)??"retry";break}}if(a==="skip")return t.removeQueueItem(e.id),y({type:"remove",id:e.id}),await p().remove(e.id),"conflicted";a&&typeof a=="object"&&(e.args=a.resolved,t.updateQueueItem(e.id,{args:a.resolved}),y({type:"update",id:e.id,update:{args:a.resolved}}),await p().update(e.id,{args:a.resolved}))}async function Je(e,t,r){const n=e.retryCount+1;if(n>=e.maxRetries){const s={status:"failed",error:String(r),retryCount:n};t.updateQueueItem(e.id,s),y({type:"update",id:e.id,update:s}),await p().update(e.id,s);const i={idempotencyKey:e.idempotencyKey,attempt:n};return ie.get(e.actionId)?.(...e.args,i),"failed"}const a={status:"pending",retryCount:n,nextRetryAt:Date.now()+Fe(n)};return t.updateQueueItem(e.id,a),y({type:"update",id:e.id,update:a}),await p().update(e.id,a),"retrying"}async function Xe(e,t){const r=A.get(e.actionId);if(!r)return"skipped";const n=ce.get(e.actionId)?.cancellable;let a;if(n){const i=new AbortController;R.set(e.idempotencyKey,i),a=i.signal}const s={idempotencyKey:e.idempotencyKey,attempt:e.retryCount,signal:a};try{return await U(r,e.args,s),await Ge(e,t),"succeeded"}catch(i){if(ue(i))return t.removeQueueItem(e.id),y({type:"remove",id:e.id}),await p().remove(e.id),"cancelled";if(Be(i)){const c=await Ye(e,t,i);if(c)return c}return Je(e,t,i)}finally{n&&R.delete(e.idempotencyKey)}}async function ze(e,t,r){if(e.length===0)return;const n=e.filter(s=>A.has(s.actionId));if(r.skipped+=e.length-n.length,n.length>0){const s=n.map(i=>({id:i.id,update:{status:"replaying"}}));t.batchUpdateQueueItems(s),y({type:"batchUpdate",updates:s});for(const i of n)p().update(i.id,{status:"replaying"})}const a=await Promise.allSettled(n.map(s=>Xe(s,t)));for(const s of a){const i=s.status==="fulfilled"?s.value:"failed";i==="skipped"?r.skipped++:i==="conflicted"?r.conflicted++:i==="cancelled"?r.cancelled++:(r.attempted++,r[i]++)}}async function V(e){const t=await p().getPending(),r=Date.now(),n=t.filter(s=>s.retryCount<s.maxRetries&&(!s.nextRetryAt||s.nextRetryAt<=r)),a=O();for(const s of["high","normal","low"])await ze(n.filter(i=>(i.priority??"normal")===s),e,a);return a}async function Ze(){await p().clear(),o.getState().hydrateQueue([])}function de(){let e=o.getState().isOnline;const t=o.subscribe(()=>{const{isOnline:a}=o.getState(),s=a&&!e;e=a,s&&setTimeout(Q,600)}),r=o.getState(),n=r.queue.some(a=>a.status==="pending");return r.isOnline&&n&&setTimeout(Q,1200),t}async function et(e){if(e.schemaVersion===2&&e.idempotencyKey)return e;const t={...e,schemaVersion:2,idempotencyKey:e.idempotencyKey??crypto.randomUUID()};return await($()??ne).update(t.id,{schemaVersion:t.schemaVersion,idempotencyKey:t.idempotencyKey}).catch(()=>{}),t}var K=!1,L=null,W=null;async function le(e={}){if(typeof window>"u"||K)return;K=!0;const t=e.swPath??"/eidos-sw.js",r=e.autoReplay??!0;try{const n=await te();if(n.length>0){const a=await Promise.all(n.map(et));o.getState().hydrateQueue(a)}}catch{}try{await we(t)}catch{}me(()=>{o.getState().isOnline&&setTimeout(Q,200)}),r&&(L=de()),W=$e()}function tt(){L?.(),L=null,W?.(),W=null,K=!1}var N="@eidos:queue",nt=class{constructor(e){this.storage=e}async readAll(){try{const e=await this.storage.getItem(N);return e?JSON.parse(e):[]}catch{return[]}}async writeAll(e){await this.storage.setItem(N,JSON.stringify(e))}async add(e){const t=await this.readAll();t.push(e),await this.writeAll(t)}async getAll(){return this.readAll()}async getPending(){return(await this.readAll()).filter(e=>e.status==="pending"||e.status==="failed")}async update(e,t){const r=await this.readAll(),n=r.findIndex(a=>a.id===e);n!==-1&&(r[n]={...r[n],...t}),await this.writeAll(r)}async remove(e){const t=await this.readAll();await this.writeAll(t.filter(r=>r.id!==e))}async clear(){await this.storage.removeItem(N)}};function rt({children:e,swPath:t,autoReplay:r}){return(0,S.useEffect)(()=>{le({swPath:t,autoReplay:r})},[]),(0,H.jsx)(H.Fragment,{children:e})}function h(e){const t=e??(r=>r);return(0,S.useSyncExternalStore)(o.subscribe,()=>t(o.getState()))}function at(){return h()}function st(){return h(e=>e.resources)}function it(e){return h(t=>t.resources[e])}function ot(){return h(e=>e.queue)}function ct(e){return h(t=>t.queue.find(r=>r.id===e))}function ut(){return{isOnline:h(e=>e.isOnline),swStatus:h(e=>e.swStatus),swError:h(e=>e.swError)}}function dt(){const[e,t,r,n]=h(a=>{const{pending:s,failed:i,replaying:c,total:u}=se(a.queue);return`${s},${i},${c},${u}`}).split(",");return{pending:+e,failed:+t,replaying:+r,total:+n}}function lt(e){const t=h(a=>a.queue.length),r=(0,S.useRef)(0),n=(0,S.useRef)(e);(0,S.useEffect)(()=>{n.current=e}),(0,S.useEffect)(()=>{r.current>0&&t===0&&n.current(),r.current=t},[t])}var ft="2.0.0";function pt(e,t){const r=Object.keys(e);if(r.length!==Object.keys(t).length)return!1;for(const n of r)if(e[n]!==t[n])return!1;return!0}function fe(e,t){return pt(e,t)}function E(e,t=Object.is){return{subscribe(r){let n=e(o.getState());return r(n),o.subscribe(()=>{const a=e(o.getState());t(n,a)||(n=a,r(a))})},getState(){return e(o.getState())}}}var ht=E(e=>e),yt=E(e=>e.queue),gt=E(e=>({isOnline:e.isOnline,swStatus:e.swStatus,swError:e.swError}),fe),wt=E(e=>se(e.queue),fe);function vt(e){return E(t=>t.resources[e])}function mt(e){return E(t=>t.queue.find(r=>r.id===e))}exports.AsyncStorageQueueStorage=nt;exports.EidosProvider=rt;exports.VERSION=ft;exports._getQueueStorage=$;exports._resetEidos=tt;exports.action=He;exports.clearQueue=Ze;exports.eidosAction=mt;exports.eidosQueue=yt;exports.eidosQueueStats=wt;exports.eidosResource=vt;exports.eidosStatus=gt;exports.eidosStore=ht;exports.getSwRegistration=Y;exports.initEidos=le;exports.isBgSyncSupported=Se;exports.registerPushCallbacks=be;exports.replayQueue=Q;exports.resource=Oe;exports.resourcePattern=Ae;exports.sendToWorker=_;exports.setOfflineSimulation=Re;exports.setQueryInvalidator=ke;exports.setQueueStorage=Le;exports.subscribeReplayOnReconnect=de;exports.useEidos=at;exports.useEidosAction=ct;exports.useEidosOnDrain=lt;exports.useEidosQueue=ot;exports.useEidosQueueStats=dt;exports.useEidosResource=it;exports.useEidosResources=st;exports.useEidosStatus=ut;exports.useEidosStore=o;exports.warmCache=Pe;
13
+ })`}};function V(e,t,n){const r=Me[e],a=t??"eidos-resources-v1";return{name:Te[e],swStrategy:e,cacheName:n!==void 0?`${a}-v${n}`:a,reasoning:r.reasoning,behavior:r.behavior,equivalentCode:""}}async function je(e){const t=await Promise.allSettled(e.map(r=>r.prefetch())),n=t.filter(r=>r.status==="rejected").map(r=>r.reason);return{warmed:t.filter(r=>r.status==="fulfilled").length,failed:n.length,errors:n}}var Ue="eidos",Ke=1,l="action-queue",P=null;function b(){return P?Promise.resolve(P):new Promise((e,t)=>{const n=indexedDB.open(Ue,Ke);n.onupgradeneeded=r=>{const a=r.target.result;if(!a.objectStoreNames.contains(l)){const s=a.createObjectStore(l,{keyPath:"id"});s.createIndex("status","status",{unique:!1}),s.createIndex("actionId","actionId",{unique:!1})}},n.onsuccess=()=>{P=n.result,e(n.result)},n.onerror=()=>t(n.error)})}async function $e(e){const t=await b();return new Promise((n,r)=>{const a=t.transaction(l,"readwrite");a.objectStore(l).add(e),a.oncomplete=()=>n(),a.onerror=()=>r(a.error)})}async function se(){const e=await b();return new Promise((t,n)=>{const r=e.transaction(l,"readonly").objectStore(l).getAll();r.onsuccess=()=>t(r.result),r.onerror=()=>n(r.error)})}async function Le(e,t){const n=await b();return new Promise((r,a)=>{const s=n.transaction(l,"readwrite"),i=s.objectStore(l),c=i.get(e);c.onsuccess=()=>{c.result&&i.put({...c.result,...t})},s.oncomplete=()=>r(),s.onerror=()=>a(s.error)})}async function We(e){const t=await b();return new Promise((n,r)=>{const a=t.transaction(l,"readwrite");a.objectStore(l).delete(e),a.oncomplete=()=>n(),a.onerror=()=>r(a.error)})}async function Be(){const e=await b();function t(a){return new Promise((s,i)=>{const c=e.transaction(l,"readonly").objectStore(l).index("status"),u=[],f=c.openCursor(IDBKeyRange.only(a));f.onsuccess=p=>{const S=p.target.result;S?(u.push(S.value),S.continue()):s(u)},f.onerror=()=>i(f.error)})}const[n,r]=await Promise.all([t("pending"),t("failed")]);return[...n,...r]}async function He(){const e=await b();return new Promise((t,n)=>{const r=e.transaction(l,"readwrite");r.objectStore(l).clear(),r.oncomplete=()=>t(),r.onerror=()=>n(r.error)})}var ie={add:$e,getAll:se,getPending:Be,update:Le,remove:We,clear:He},oe=null;function Fe(e){oe=e}function B(){return oe}var Ve="eidos-queue-sync",k;function ce(){return k!==void 0||(k=typeof BroadcastChannel>"u"?null:new BroadcastChannel(Ve)),k}function h(e){ce()?.postMessage(e)}function Ge(){const e=ce();if(!e)return()=>{};const t=n=>{const r=o.getState(),a=n.data;switch(a.type){case"update":r.updateQueueItem(a.id,a.update);break;case"batchUpdate":r.batchUpdateQueueItems(a.updates);break;case"remove":r.removeQueueItem(a.id);break}};return e.addEventListener("message",t),()=>e.removeEventListener("message",t)}var q=new Map,ue=new Map,de=new Map,le=new Map,I=new Map;function d(){return B()??ie}function U(){return crypto.randomUUID()}function K(e,t,n){return e(...t,n)}function Ye(e,t){const n=t.name||e.name||U(),r=t.namespace?`${t.namespace}::${n}`:n;if(q.has(r))throw new Error(`[eidos] duplicate action id "${r}" — an action with this id is already registered. Pass a unique config.name or config.namespace.`);q.set(r,e),le.set(r,t),t.onRollback&&ue.set(r,t.onRollback),t.conflict&&de.set(r,t.conflict);const a=async(...s)=>{const{isOnline:i}=o.getState(),c=U();let u;if(t.cancellable){const p=new AbortController;I.set(c,p),u=p.signal}const f={idempotencyKey:c,attempt:0,signal:u};t.onOptimistic?.(...s,f);try{if(t.reliability==="neverLose"){if(!i)return G(r,r,s,t,c);try{return await K(e,s,f)}catch(p){if(pe(p))throw p;return G(r,r,s,t,c)}}try{return await K(e,s,f)}catch(p){throw t.onRollback?.(...s,f),p}}finally{t.cancellable&&I.delete(c)}};return Object.defineProperty(a,"id",{value:r,writable:!1}),Object.defineProperty(a,"config",{value:t,writable:!1}),Object.defineProperty(a,"cancel",{value:fe,writable:!1}),a}async function fe(e){const t=I.get(e);if(t)return t.abort(),!0;const n=(await d().getAll()).find(r=>r.idempotencyKey===e&&r.status==="pending");return n?(o.getState().removeQueueItem(n.id),h({type:"remove",id:n.id}),await d().remove(n.id),!0):!1}async function Je(e){const t=(await d().getAll()).find(r=>r.id===e);if(!t||t.status!=="failed")return!1;const n={status:"pending",error:void 0,nextRetryAt:void 0,retryCount:0};return o.getState().updateQueueItem(e,n),h({type:"update",id:e,update:n}),await d().update(e,n),!0}async function G(e,t,n,r,a){const s=U(),i={schemaVersion:2,id:s,actionId:e,actionName:t,idempotencyKey:a,args:n,queuedAt:Date.now(),retryCount:0,maxRetries:r.maxRetries??3,status:"pending",priority:r.priority??"normal"};await d().add(i),o.getState().addQueueItem(i),o.getState().recordReliabilityEvent("queued");try{const c=Z();c&&"sync"in c&&await c.sync.register("eidos-queue-replay")}catch{}return{queued:!0,id:s,message:`"${t}" queued — will execute when online`}}function pe(e){return e instanceof DOMException&&e.name==="AbortError"}function Xe(e){if(e instanceof Response)return e.status>=400&&e.status<500;if(typeof e=="object"&&e!==null){const t=e.status;if(typeof t=="number")return t>=400&&t<500}return!1}function ze(e){return Math.min(2e3*2**e,3e5)*(.8+Math.random()*.4)}function O(){return{attempted:0,succeeded:0,failed:0,retrying:0,skipped:0,conflicted:0,cancelled:0}}var D=!1,Ze="eidos-queue-replay";async function Q(){const e=o.getState();if(!e.isOnline)return O();if(typeof navigator<"u"&&navigator.locks)return navigator.locks.request(Ze,{ifAvailable:!0},async t=>t?Y(e):O());if(D)return O();D=!0;try{return await Y(e)}finally{D=!1}}async function et(e,t){const n=Date.now();t.updateQueueItem(e.id,{status:"succeeded",completedAt:n}),t.recordReliabilityEvent("succeeded"),h({type:"update",id:e.id,update:{status:"succeeded",completedAt:n}}),await d().update(e.id,{status:"succeeded",completedAt:n}),setTimeout(()=>{t.removeQueueItem(e.id),h({type:"remove",id:e.id}),d().remove(e.id)},3e3)}async function tt(e,t,n){const r=de.get(e.actionId);let a;if(r)switch(r.strategy){case"serverWins":a="skip";break;case"clientWins":a="retry";break;case"merge":case"custom":{const s={error:n,args:e.args,attempt:e.retryCount,idempotencyKey:e.idempotencyKey};a=r.resolve?.(s)??"retry";break}}if(a==="skip")return t.removeQueueItem(e.id),t.recordReliabilityEvent("conflicted"),h({type:"remove",id:e.id}),await d().remove(e.id),"conflicted";a&&typeof a=="object"&&(e.args=a.resolved,t.updateQueueItem(e.id,{args:a.resolved}),h({type:"update",id:e.id,update:{args:a.resolved}}),await d().update(e.id,{args:a.resolved}))}async function nt(e,t,n){const r=e.retryCount+1;if(r>=e.maxRetries){const s={status:"failed",error:String(n),retryCount:r};t.updateQueueItem(e.id,s),t.recordReliabilityEvent("failed"),h({type:"update",id:e.id,update:s}),await d().update(e.id,s);const i={idempotencyKey:e.idempotencyKey,attempt:r};return ue.get(e.actionId)?.(...e.args,i),"failed"}const a={status:"pending",retryCount:r,nextRetryAt:Date.now()+ze(r)};return t.updateQueueItem(e.id,a),t.recordReliabilityEvent("retried"),h({type:"update",id:e.id,update:a}),await d().update(e.id,a),"retrying"}async function rt(e,t){const n=q.get(e.actionId);if(!n)return"skipped";const r=le.get(e.actionId)?.cancellable;let a;if(r){const i=new AbortController;I.set(e.idempotencyKey,i),a=i.signal}const s={idempotencyKey:e.idempotencyKey,attempt:e.retryCount,signal:a};try{return await K(n,e.args,s),await et(e,t),"succeeded"}catch(i){if(pe(i))return t.removeQueueItem(e.id),t.recordReliabilityEvent("cancelled"),h({type:"remove",id:e.id}),await d().remove(e.id),"cancelled";if(Xe(i)){const c=await tt(e,t,i);if(c)return c}return nt(e,t,i)}finally{r&&I.delete(e.idempotencyKey)}}async function at(e,t,n){if(e.length===0)return;const r=e.filter(s=>q.has(s.actionId));if(n.skipped+=e.length-r.length,r.length>0){const s=r.map(i=>({id:i.id,update:{status:"replaying"}}));t.batchUpdateQueueItems(s),h({type:"batchUpdate",updates:s});for(const i of r)d().update(i.id,{status:"replaying"})}const a=await Promise.allSettled(r.map(s=>rt(s,t)));for(const s of a){const i=s.status==="fulfilled"?s.value:"failed";i==="skipped"?n.skipped++:i==="conflicted"?n.conflicted++:i==="cancelled"?n.cancelled++:(n.attempted++,n[i]++)}}async function Y(e){const t=await d().getPending(),n=Date.now(),r=t.filter(s=>s.retryCount<s.maxRetries&&(!s.nextRetryAt||s.nextRetryAt<=n)),a=O();for(const s of["high","normal","low"])await at(r.filter(i=>(i.priority??"normal")===s),e,a);return a}async function st(){await d().clear(),o.getState().hydrateQueue([])}function ye(){let e=o.getState().isOnline;const t=o.subscribe(()=>{const{isOnline:a}=o.getState(),s=a&&!e;e=a,s&&setTimeout(Q,600)}),n=o.getState(),r=n.queue.some(a=>a.status==="pending");return n.isOnline&&r&&setTimeout(Q,1200),t}async function it(e){if(e.schemaVersion===2&&e.idempotencyKey)return e;const t={...e,schemaVersion:2,idempotencyKey:e.idempotencyKey??crypto.randomUUID()};return await(B()??ie).update(t.id,{schemaVersion:t.schemaVersion,idempotencyKey:t.idempotencyKey}).catch(()=>{}),t}var $=!1,L=null,W=null,A=null;async function he(e={}){if(typeof window>"u"||$)return;$=!0;const t=e.swPath??"/eidos-sw.js",n=e.autoReplay??!0;try{const r=await se();if(r.length>0){const a=await Promise.all(r.map(it));o.getState().hydrateQueue(a)}}catch{}try{await Ee(t)}catch{}if(Ie(()=>{o.getState().isOnline&&setTimeout(Q,200)}),n&&(L=ye()),W=Ge(),e.onReliabilityReport){const r=e.reliabilityReportInterval??6e4,a=e.onReliabilityReport;A=setInterval(()=>{a(o.getState().reliability)},r)}}function ot(){L?.(),L=null,W?.(),W=null,A&&clearInterval(A),A=null,$=!1}var N="@eidos:queue",ct=class{constructor(e){this.storage=e}async readAll(){try{const e=await this.storage.getItem(N);return e?JSON.parse(e):[]}catch{return[]}}async writeAll(e){await this.storage.setItem(N,JSON.stringify(e))}async add(e){const t=await this.readAll();t.push(e),await this.writeAll(t)}async getAll(){return this.readAll()}async getPending(){return(await this.readAll()).filter(e=>e.status==="pending"||e.status==="failed")}async update(e,t){const n=await this.readAll(),r=n.findIndex(a=>a.id===e);r!==-1&&(n[r]={...n[r],...t}),await this.writeAll(n)}async remove(e){const t=await this.readAll();await this.writeAll(t.filter(n=>n.id!==e))}async clear(){await this.storage.removeItem(N)}};function ut({children:e,swPath:t,autoReplay:n}){return(0,R.useEffect)(()=>{he({swPath:t,autoReplay:n})},[]),(0,F.jsx)(F.Fragment,{children:e})}function dt(e,t){const n=Object.keys(e);if(n.length!==Object.keys(t).length)return!1;for(const r of n)if(e[r]!==t[r])return!1;return!0}function H(e,t){return dt(e,t)}function m(e,t=Object.is){return{subscribe(n){let r=e(o.getState());return n(r),o.subscribe(()=>{const a=e(o.getState());t(r,a)||(r=a,n(a))})},getState(){return e(o.getState())}}}var lt=m(e=>e),ft=m(e=>e.queue),pt=m(e=>({isOnline:e.isOnline,swStatus:e.swStatus,swError:e.swError}),H),yt=m(e=>X(e.queue),H),ht=m(e=>e.reliability,H);function gt(e){return m(t=>t.resources[e])}function wt(e){return m(t=>t.queue.find(n=>n.id===e))}function ge(e){let t=o.getState().queue.length;return o.subscribe(()=>{const n=o.getState().queue.length;t>0&&n===0&&e(),t=n})}function y(e){const t=e??(n=>n);return(0,R.useSyncExternalStore)(o.subscribe,()=>t(o.getState()))}function vt(){return y()}function mt(){return y(e=>e.resources)}function St(e){return y(t=>t.resources[e])}function bt(){return y(e=>e.queue)}function Et(e){return y(t=>t.queue.find(n=>n.id===e))}function Rt(){return{isOnline:y(e=>e.isOnline),swStatus:y(e=>e.swStatus),swError:y(e=>e.swError)}}function It(){const[e,t,n,r]=y(a=>{const{pending:s,failed:i,replaying:c,total:u}=X(a.queue);return`${s},${i},${c},${u}`}).split(",");return{pending:+e,failed:+t,replaying:+n,total:+r}}function _t(){return y(e=>e.reliability)}function kt(e){const t=(0,R.useRef)(e);(0,R.useEffect)(()=>{t.current=e}),(0,R.useEffect)(()=>ge(()=>t.current()),[])}var Ot="2.2.0";exports.AsyncStorageQueueStorage=ct;exports.EidosProvider=ut;exports.VERSION=Ot;exports._getQueueStorage=B;exports._resetEidos=ot;exports.action=Ye;exports.cancelByIdempotencyKey=fe;exports.clearQueue=st;exports.eidosAction=wt;exports.eidosQueue=ft;exports.eidosQueueStats=yt;exports.eidosReliabilityStats=ht;exports.eidosResource=gt;exports.eidosStatus=pt;exports.eidosStore=lt;exports.getSwRegistration=Z;exports.initEidos=he;exports.isBgSyncSupported=_e;exports.onQueueDrain=ge;exports.registerPushCallbacks=ke;exports.replayQueue=Q;exports.requeueItem=Je;exports.resource=xe;exports.resourcePattern=Pe;exports.sendToWorker=_;exports.setOfflineSimulation=Ae;exports.setQueryInvalidator=Qe;exports.setQueueStorage=Fe;exports.subscribeReplayOnReconnect=ye;exports.useEidos=vt;exports.useEidosAction=Et;exports.useEidosOnDrain=kt;exports.useEidosQueue=bt;exports.useEidosQueueStats=It;exports.useEidosReliabilityStats=_t;exports.useEidosResource=St;exports.useEidosResources=mt;exports.useEidosStatus=Rt;exports.useEidosStore=o;exports.warmCache=je;
14
14
 
15
15
  //# sourceMappingURL=eidos.cjs.map