@sweidos/eidos 1.2.0 → 2.1.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.
- package/README.md +62 -32
- package/dist/action.js +208 -152
- package/dist/action.js.map +1 -0
- package/dist/async-storage-adapter.js.map +1 -0
- package/dist/devtools.js +247 -86
- package/dist/eidos-sw.js +8 -5
- package/dist/eidos.cjs +2 -2
- package/dist/eidos.cjs.map +1 -0
- package/dist/idb.js.map +1 -0
- package/dist/index.d.ts +101 -35
- package/dist/index.js +44 -41
- package/dist/push.cjs +8 -5
- package/dist/push.js +8 -5
- package/dist/query.cjs +1 -1
- package/dist/query.d.ts +1 -2
- package/dist/query.js +1 -1
- package/dist/queue-storage.js.map +1 -0
- package/dist/queue-sync.js +34 -0
- package/dist/queue-sync.js.map +1 -0
- package/dist/react/Provider.js.map +1 -0
- package/dist/react/hooks.js +23 -23
- package/dist/react/hooks.js.map +1 -0
- package/dist/replay.js.map +1 -0
- package/dist/resource.js +121 -107
- package/dist/resource.js.map +1 -0
- package/dist/runtime.js +23 -22
- package/dist/runtime.js.map +1 -0
- package/dist/store-slices.js.map +1 -0
- package/dist/store.js.map +1 -0
- package/dist/stores.js +23 -31
- package/dist/stores.js.map +1 -0
- package/dist/sw-bridge.js.map +1 -0
- package/dist/types.js +15 -0
- package/dist/types.js.map +1 -0
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -0
- package/package.json +2 -3
|
@@ -0,0 +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 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\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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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"async-storage-adapter.js","names":[],"sources":["../src/async-storage-adapter.ts"],"sourcesContent":["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"],"mappings":"AAUA,IAAM,IAAY,gBAOL,IAAb,MAA8D;AAAA,EAC5D,YAAY,GAA4C;AAA3B,SAAA,UAAA;AAAA,EAA4B;AAAA,EAEzD,MAAc,UAAsC;AAClD,QAAI;AACF,YAAM,IAAM,MAAM,KAAK,QAAQ,QAAQ,CAAS;AAChD,aAAK,IACE,KAAK,MAAM,CAAG,IADJ,CAAC;AAAA,IAEpB,QAAQ;AACN,aAAO,CAAC;AAAA,IACV;AAAA,EACF;AAAA,EAEA,MAAc,SAAS,GAAyC;AAC9D,UAAM,KAAK,QAAQ,QAAQ,GAAW,KAAK,UAAU,CAAK,CAAC;AAAA,EAC7D;AAAA,EAEA,MAAM,IAAI,GAAsC;AAC9C,UAAM,IAAQ,MAAM,KAAK,QAAQ;AACjC,IAAA,EAAM,KAAK,CAAI,GACf,MAAM,KAAK,SAAS,CAAK;AAAA,EAC3B;AAAA,EAEA,MAAM,SAAqC;AACzC,WAAO,KAAK,QAAQ;AAAA,EACtB;AAAA,EAEA,MAAM,aAAyC;AAE7C,YAAO,MADa,KAAK,QAAQ,GACpB,OAAA,CAAQ,MAAM,EAAE,WAAW,aAAa,EAAE,WAAW,QAAQ;AAAA,EAC5E;AAAA,EAEA,MAAM,OAAO,GAAY,GAAgD;AACvE,UAAM,IAAQ,MAAM,KAAK,QAAQ,GAC3B,IAAM,EAAM,UAAA,CAAW,MAAM,EAAE,OAAO,CAAE;AAC9C,IAAI,MAAQ,OAAI,EAAM,CAAA,IAAO;AAAA,MAAE,GAAG,EAAM,CAAA;AAAA,MAAM,GAAG;AAAA,IAAM,IACvD,MAAM,KAAK,SAAS,CAAK;AAAA,EAC3B;AAAA,EAEA,MAAM,OAAO,GAA2B;AACtC,UAAM,IAAQ,MAAM,KAAK,QAAQ;AACjC,UAAM,KAAK,SAAS,EAAM,OAAA,CAAQ,MAAM,EAAE,OAAO,CAAE,CAAC;AAAA,EACtD;AAAA,EAEA,MAAM,QAAuB;AAC3B,UAAM,KAAK,QAAQ,WAAW,CAAS;AAAA,EACzC;AACF"}
|
package/dist/devtools.js
CHANGED
|
@@ -89,6 +89,21 @@ var useEidosStore = {
|
|
|
89
89
|
}
|
|
90
90
|
};
|
|
91
91
|
//#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
|
|
92
107
|
//#region src/react/hooks.ts
|
|
93
108
|
function useStore(selector) {
|
|
94
109
|
const fn = selector ?? ((s) => s);
|
|
@@ -121,11 +136,8 @@ function useEidosStatus() {
|
|
|
121
136
|
*/
|
|
122
137
|
function useEidosQueueStats() {
|
|
123
138
|
const [p, f, r, t] = useStore((s) => {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
else if (q.status === "failed") failed++;
|
|
127
|
-
else if (q.status === "replaying") replaying++;
|
|
128
|
-
return `${pending},${failed},${replaying},${s.queue.length}`;
|
|
139
|
+
const { pending, failed, replaying, total } = countQueueByStatus(s.queue);
|
|
140
|
+
return `${pending},${failed},${replaying},${total}`;
|
|
129
141
|
}).split(",");
|
|
130
142
|
return {
|
|
131
143
|
pending: +p,
|
|
@@ -262,10 +274,29 @@ function _getQueueStorage() {
|
|
|
262
274
|
return _storage;
|
|
263
275
|
}
|
|
264
276
|
//#endregion
|
|
277
|
+
//#region src/queue-sync.ts
|
|
278
|
+
var CHANNEL_NAME = "eidos-queue-sync";
|
|
279
|
+
var _channel;
|
|
280
|
+
function getChannel() {
|
|
281
|
+
if (_channel !== void 0) return _channel;
|
|
282
|
+
_channel = typeof BroadcastChannel === "undefined" ? null : new BroadcastChannel(CHANNEL_NAME);
|
|
283
|
+
return _channel;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Broadcasts a queue-item status change to other tabs sharing the same
|
|
287
|
+
* IndexedDB queue. The replay-lock holder (see `replayQueue` in action.ts)
|
|
288
|
+
* is the only tab that mutates queue-item status, so non-leader tabs would
|
|
289
|
+
* otherwise show stale status until their own store re-hydrates.
|
|
290
|
+
*
|
|
291
|
+
* No-ops in environments without BroadcastChannel (React Native, old Safari).
|
|
292
|
+
*/
|
|
293
|
+
function broadcastQueueSync(message) {
|
|
294
|
+
getChannel()?.postMessage(message);
|
|
295
|
+
}
|
|
296
|
+
//#endregion
|
|
265
297
|
//#region src/action.ts
|
|
266
298
|
var _actionRegistry = /* @__PURE__ */ new Map();
|
|
267
299
|
var _rollbackRegistry = /* @__PURE__ */ new Map();
|
|
268
|
-
var _conflictRegistry = /* @__PURE__ */ new Map();
|
|
269
300
|
var _conflictConfigRegistry = /* @__PURE__ */ new Map();
|
|
270
301
|
var _configRegistry = /* @__PURE__ */ new Map();
|
|
271
302
|
var _inflightControllers = /* @__PURE__ */ new Map();
|
|
@@ -275,6 +306,55 @@ function qs() {
|
|
|
275
306
|
function callWithContext(fn, args, ctx) {
|
|
276
307
|
return fn(...args, ctx);
|
|
277
308
|
}
|
|
309
|
+
/**
|
|
310
|
+
* Cancel an invocation by its `idempotencyKey` (from `ActionContext` /
|
|
311
|
+
* `onOptimistic`). Aborts the in-flight call if `cancellable: true` and
|
|
312
|
+
* still running, otherwise removes a not-yet-replayed queued item.
|
|
313
|
+
* Returns `true` if something was cancelled/removed.
|
|
314
|
+
*
|
|
315
|
+
* Shared by every `ActionHandle.cancel()` and by devtools, which can't
|
|
316
|
+
* address a specific handle from a queue item alone.
|
|
317
|
+
*/
|
|
318
|
+
async function cancelByIdempotencyKey(idempotencyKey) {
|
|
319
|
+
const controller = _inflightControllers.get(idempotencyKey);
|
|
320
|
+
if (controller) {
|
|
321
|
+
controller.abort();
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
const item = (await qs().getAll()).find((i) => i.idempotencyKey === idempotencyKey && i.status === "pending");
|
|
325
|
+
if (!item) return false;
|
|
326
|
+
useEidosStore.getState().removeQueueItem(item.id);
|
|
327
|
+
broadcastQueueSync({
|
|
328
|
+
type: "remove",
|
|
329
|
+
id: item.id
|
|
330
|
+
});
|
|
331
|
+
await qs().remove(item.id);
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Reset a `'failed'` queue item back to `'pending'` so the next
|
|
336
|
+
* `replayQueue()` retries it — clears `error`/`nextRetryAt` and resets
|
|
337
|
+
* `retryCount` to 0. Returns `true` if the item existed and was failed.
|
|
338
|
+
* Used by devtools' per-item "Retry" action.
|
|
339
|
+
*/
|
|
340
|
+
async function requeueItem(id) {
|
|
341
|
+
const item = (await qs().getAll()).find((i) => i.id === id);
|
|
342
|
+
if (!item || item.status !== "failed") return false;
|
|
343
|
+
const update = {
|
|
344
|
+
status: "pending",
|
|
345
|
+
error: void 0,
|
|
346
|
+
nextRetryAt: void 0,
|
|
347
|
+
retryCount: 0
|
|
348
|
+
};
|
|
349
|
+
useEidosStore.getState().updateQueueItem(id, update);
|
|
350
|
+
broadcastQueueSync({
|
|
351
|
+
type: "update",
|
|
352
|
+
id,
|
|
353
|
+
update
|
|
354
|
+
});
|
|
355
|
+
await qs().update(id, update);
|
|
356
|
+
return true;
|
|
357
|
+
}
|
|
278
358
|
function isAbortError(err) {
|
|
279
359
|
return err instanceof DOMException && err.name === "AbortError";
|
|
280
360
|
}
|
|
@@ -317,6 +397,116 @@ async function replayQueue() {
|
|
|
317
397
|
_replaying = false;
|
|
318
398
|
}
|
|
319
399
|
}
|
|
400
|
+
async function _markSucceeded(item, store) {
|
|
401
|
+
const completedAt = Date.now();
|
|
402
|
+
store.updateQueueItem(item.id, {
|
|
403
|
+
status: "succeeded",
|
|
404
|
+
completedAt
|
|
405
|
+
});
|
|
406
|
+
broadcastQueueSync({
|
|
407
|
+
type: "update",
|
|
408
|
+
id: item.id,
|
|
409
|
+
update: {
|
|
410
|
+
status: "succeeded",
|
|
411
|
+
completedAt
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
await qs().update(item.id, {
|
|
415
|
+
status: "succeeded",
|
|
416
|
+
completedAt
|
|
417
|
+
});
|
|
418
|
+
setTimeout(() => {
|
|
419
|
+
store.removeQueueItem(item.id);
|
|
420
|
+
broadcastQueueSync({
|
|
421
|
+
type: "remove",
|
|
422
|
+
id: item.id
|
|
423
|
+
});
|
|
424
|
+
qs().remove(item.id);
|
|
425
|
+
}, 3e3);
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Resolves a 4xx error against the action's conflict strategy.
|
|
429
|
+
* Returns 'conflicted' if the item was dropped, undefined if normal
|
|
430
|
+
* retry/fail logic should run (possibly with `item.args` rewritten by `merge`).
|
|
431
|
+
*/
|
|
432
|
+
async function _resolveConflict(item, store, err) {
|
|
433
|
+
const conflictConfig = _conflictConfigRegistry.get(item.actionId);
|
|
434
|
+
let resolution;
|
|
435
|
+
if (conflictConfig) switch (conflictConfig.strategy) {
|
|
436
|
+
case "serverWins":
|
|
437
|
+
resolution = "skip";
|
|
438
|
+
break;
|
|
439
|
+
case "clientWins":
|
|
440
|
+
resolution = "retry";
|
|
441
|
+
break;
|
|
442
|
+
case "merge":
|
|
443
|
+
case "custom": {
|
|
444
|
+
const ctx = {
|
|
445
|
+
error: err,
|
|
446
|
+
args: item.args,
|
|
447
|
+
attempt: item.retryCount,
|
|
448
|
+
idempotencyKey: item.idempotencyKey
|
|
449
|
+
};
|
|
450
|
+
resolution = conflictConfig.resolve?.(ctx) ?? "retry";
|
|
451
|
+
break;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (resolution === "skip") {
|
|
455
|
+
store.removeQueueItem(item.id);
|
|
456
|
+
broadcastQueueSync({
|
|
457
|
+
type: "remove",
|
|
458
|
+
id: item.id
|
|
459
|
+
});
|
|
460
|
+
await qs().remove(item.id);
|
|
461
|
+
return "conflicted";
|
|
462
|
+
}
|
|
463
|
+
if (resolution && typeof resolution === "object") {
|
|
464
|
+
item.args = resolution.resolved;
|
|
465
|
+
store.updateQueueItem(item.id, { args: resolution.resolved });
|
|
466
|
+
broadcastQueueSync({
|
|
467
|
+
type: "update",
|
|
468
|
+
id: item.id,
|
|
469
|
+
update: { args: resolution.resolved }
|
|
470
|
+
});
|
|
471
|
+
await qs().update(item.id, { args: resolution.resolved });
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
async function _scheduleRetryOrFail(item, store, err) {
|
|
475
|
+
const retryCount = item.retryCount + 1;
|
|
476
|
+
if (retryCount >= item.maxRetries) {
|
|
477
|
+
const update = {
|
|
478
|
+
status: "failed",
|
|
479
|
+
error: String(err),
|
|
480
|
+
retryCount
|
|
481
|
+
};
|
|
482
|
+
store.updateQueueItem(item.id, update);
|
|
483
|
+
broadcastQueueSync({
|
|
484
|
+
type: "update",
|
|
485
|
+
id: item.id,
|
|
486
|
+
update
|
|
487
|
+
});
|
|
488
|
+
await qs().update(item.id, update);
|
|
489
|
+
const ctx = {
|
|
490
|
+
idempotencyKey: item.idempotencyKey,
|
|
491
|
+
attempt: retryCount
|
|
492
|
+
};
|
|
493
|
+
_rollbackRegistry.get(item.actionId)?.(...item.args, ctx);
|
|
494
|
+
return "failed";
|
|
495
|
+
}
|
|
496
|
+
const update = {
|
|
497
|
+
status: "pending",
|
|
498
|
+
retryCount,
|
|
499
|
+
nextRetryAt: Date.now() + backoffMs(retryCount)
|
|
500
|
+
};
|
|
501
|
+
store.updateQueueItem(item.id, update);
|
|
502
|
+
broadcastQueueSync({
|
|
503
|
+
type: "update",
|
|
504
|
+
id: item.id,
|
|
505
|
+
update
|
|
506
|
+
});
|
|
507
|
+
await qs().update(item.id, update);
|
|
508
|
+
return "retrying";
|
|
509
|
+
}
|
|
320
510
|
async function _replayItem(item, store) {
|
|
321
511
|
const fn = _actionRegistry.get(item.actionId);
|
|
322
512
|
if (!fn) return "skipped";
|
|
@@ -334,92 +524,23 @@ async function _replayItem(item, store) {
|
|
|
334
524
|
};
|
|
335
525
|
try {
|
|
336
526
|
await callWithContext(fn, item.args, ctx);
|
|
337
|
-
|
|
338
|
-
store.updateQueueItem(item.id, {
|
|
339
|
-
status: "succeeded",
|
|
340
|
-
completedAt
|
|
341
|
-
});
|
|
342
|
-
await qs().update(item.id, {
|
|
343
|
-
status: "succeeded",
|
|
344
|
-
completedAt
|
|
345
|
-
});
|
|
346
|
-
setTimeout(() => {
|
|
347
|
-
store.removeQueueItem(item.id);
|
|
348
|
-
qs().remove(item.id);
|
|
349
|
-
}, 3e3);
|
|
527
|
+
await _markSucceeded(item, store);
|
|
350
528
|
return "succeeded";
|
|
351
529
|
} catch (err) {
|
|
352
530
|
if (isAbortError(err)) {
|
|
353
531
|
store.removeQueueItem(item.id);
|
|
532
|
+
broadcastQueueSync({
|
|
533
|
+
type: "remove",
|
|
534
|
+
id: item.id
|
|
535
|
+
});
|
|
354
536
|
await qs().remove(item.id);
|
|
355
537
|
return "cancelled";
|
|
356
538
|
}
|
|
357
539
|
if (isClientError(err)) {
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
if (conflictConfig) switch (conflictConfig.strategy) {
|
|
361
|
-
case "serverWins":
|
|
362
|
-
resolution = "skip";
|
|
363
|
-
break;
|
|
364
|
-
case "clientWins":
|
|
365
|
-
case "lastWriteWins":
|
|
366
|
-
resolution = "retry";
|
|
367
|
-
break;
|
|
368
|
-
case "merge":
|
|
369
|
-
case "custom": {
|
|
370
|
-
const ctx = {
|
|
371
|
-
error: err,
|
|
372
|
-
args: item.args,
|
|
373
|
-
attempt: item.retryCount,
|
|
374
|
-
idempotencyKey: item.idempotencyKey
|
|
375
|
-
};
|
|
376
|
-
resolution = conflictConfig.resolve?.(ctx) ?? "retry";
|
|
377
|
-
break;
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
else {
|
|
381
|
-
const onConflict = _conflictRegistry.get(item.actionId);
|
|
382
|
-
if (onConflict) resolution = onConflict(err, item.args);
|
|
383
|
-
}
|
|
384
|
-
if (resolution === "skip") {
|
|
385
|
-
store.removeQueueItem(item.id);
|
|
386
|
-
await qs().remove(item.id);
|
|
387
|
-
return "conflicted";
|
|
388
|
-
}
|
|
389
|
-
if (resolution && typeof resolution === "object") {
|
|
390
|
-
item.args = resolution.resolved;
|
|
391
|
-
store.updateQueueItem(item.id, { args: resolution.resolved });
|
|
392
|
-
await qs().update(item.id, { args: resolution.resolved });
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
const retryCount = item.retryCount + 1;
|
|
396
|
-
if (retryCount >= item.maxRetries) {
|
|
397
|
-
store.updateQueueItem(item.id, {
|
|
398
|
-
status: "failed",
|
|
399
|
-
error: String(err),
|
|
400
|
-
retryCount
|
|
401
|
-
});
|
|
402
|
-
await qs().update(item.id, {
|
|
403
|
-
status: "failed",
|
|
404
|
-
error: String(err),
|
|
405
|
-
retryCount
|
|
406
|
-
});
|
|
407
|
-
_rollbackRegistry.get(item.actionId)?.(...item.args);
|
|
408
|
-
return "failed";
|
|
409
|
-
} else {
|
|
410
|
-
const nextRetryAt = Date.now() + backoffMs(retryCount);
|
|
411
|
-
store.updateQueueItem(item.id, {
|
|
412
|
-
status: "pending",
|
|
413
|
-
retryCount,
|
|
414
|
-
nextRetryAt
|
|
415
|
-
});
|
|
416
|
-
await qs().update(item.id, {
|
|
417
|
-
status: "pending",
|
|
418
|
-
retryCount,
|
|
419
|
-
nextRetryAt
|
|
420
|
-
});
|
|
421
|
-
return "retrying";
|
|
540
|
+
const outcome = await _resolveConflict(item, store, err);
|
|
541
|
+
if (outcome) return outcome;
|
|
422
542
|
}
|
|
543
|
+
return _scheduleRetryOrFail(item, store, err);
|
|
423
544
|
} finally {
|
|
424
545
|
if (cancellable) _inflightControllers.delete(item.idempotencyKey);
|
|
425
546
|
}
|
|
@@ -429,10 +550,15 @@ async function _replayTier(items, store, result) {
|
|
|
429
550
|
const replayable = items.filter((item) => _actionRegistry.has(item.actionId));
|
|
430
551
|
result.skipped += items.length - replayable.length;
|
|
431
552
|
if (replayable.length > 0) {
|
|
432
|
-
|
|
553
|
+
const updates = replayable.map((item) => ({
|
|
433
554
|
id: item.id,
|
|
434
555
|
update: { status: "replaying" }
|
|
435
|
-
}))
|
|
556
|
+
}));
|
|
557
|
+
store.batchUpdateQueueItems(updates);
|
|
558
|
+
broadcastQueueSync({
|
|
559
|
+
type: "batchUpdate",
|
|
560
|
+
updates
|
|
561
|
+
});
|
|
436
562
|
for (const item of replayable) qs().update(item.id, { status: "replaying" });
|
|
437
563
|
}
|
|
438
564
|
const outcomes = await Promise.allSettled(replayable.map((item) => _replayItem(item, store)));
|
|
@@ -581,7 +707,9 @@ var ICONS = {
|
|
|
581
707
|
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",
|
|
582
708
|
arrowUp: "M12 19V5M5 12l7-7 7 7",
|
|
583
709
|
arrowDown: "M12 5v14M19 12l-7 7-7-7",
|
|
584
|
-
clock: "M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20zM12 6v6l4 2"
|
|
710
|
+
clock: "M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20zM12 6v6l4 2",
|
|
711
|
+
x: "M18 6 6 18M6 6l12 12",
|
|
712
|
+
rotateCcw: "M3 12a9 9 0 1 0 2.6-6.4M3 12V5m0 7h7"
|
|
585
713
|
};
|
|
586
714
|
function positionStyle(p) {
|
|
587
715
|
const base = {
|
|
@@ -945,6 +1073,7 @@ function QueueTab({ queue, onReplay, onClear }) {
|
|
|
945
1073
|
children: item.priority
|
|
946
1074
|
}),
|
|
947
1075
|
/* @__PURE__ */ jsx("span", {
|
|
1076
|
+
title: `idempotencyKey: ${item.idempotencyKey}`,
|
|
948
1077
|
style: {
|
|
949
1078
|
flex: 1,
|
|
950
1079
|
overflow: "hidden",
|
|
@@ -966,6 +1095,38 @@ function QueueTab({ queue, onReplay, onClear }) {
|
|
|
966
1095
|
"/",
|
|
967
1096
|
item.maxRetries
|
|
968
1097
|
]
|
|
1098
|
+
}),
|
|
1099
|
+
item.status === "pending" && /* @__PURE__ */ jsx("button", {
|
|
1100
|
+
onClick: () => void cancelByIdempotencyKey(item.idempotencyKey),
|
|
1101
|
+
title: "Cancel — remove this item before it's replayed",
|
|
1102
|
+
"aria-label": `Cancel ${item.actionName}`,
|
|
1103
|
+
...withFocusRing(),
|
|
1104
|
+
style: {
|
|
1105
|
+
...btn("ghost"),
|
|
1106
|
+
padding: "2px 4px",
|
|
1107
|
+
minHeight: 18,
|
|
1108
|
+
color: C.red
|
|
1109
|
+
},
|
|
1110
|
+
children: /* @__PURE__ */ jsx(Icon, {
|
|
1111
|
+
path: ICONS.x,
|
|
1112
|
+
size: 11
|
|
1113
|
+
})
|
|
1114
|
+
}),
|
|
1115
|
+
item.status === "failed" && /* @__PURE__ */ jsx("button", {
|
|
1116
|
+
onClick: () => void requeueItem(item.id),
|
|
1117
|
+
title: "Retry — reset for the next replay",
|
|
1118
|
+
"aria-label": `Retry ${item.actionName}`,
|
|
1119
|
+
...withFocusRing(),
|
|
1120
|
+
style: {
|
|
1121
|
+
...btn("ghost"),
|
|
1122
|
+
padding: "2px 4px",
|
|
1123
|
+
minHeight: 18,
|
|
1124
|
+
color: C.blue
|
|
1125
|
+
},
|
|
1126
|
+
children: /* @__PURE__ */ jsx(Icon, {
|
|
1127
|
+
path: ICONS.rotateCcw,
|
|
1128
|
+
size: 11
|
|
1129
|
+
})
|
|
969
1130
|
})
|
|
970
1131
|
]
|
|
971
1132
|
}, item.id))
|
package/dist/eidos-sw.js
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
//#region ../core/src/internal/url-base64.ts
|
|
2
|
+
/** Decodes a base64url string (e.g. a VAPID public key) into raw bytes. */
|
|
3
|
+
function urlBase64ToUint8Array(base64Url) {
|
|
4
|
+
const base64 = (base64Url + "=".repeat((4 - base64Url.length % 4) % 4)).replace(/-/g, "+").replace(/_/g, "/");
|
|
5
|
+
const raw = atob(base64);
|
|
6
|
+
return Uint8Array.from(raw, (c) => c.charCodeAt(0));
|
|
7
|
+
}
|
|
8
|
+
//#endregion
|
|
1
9
|
//#region src/sw.ts
|
|
2
10
|
var CACHE_VERSION = "v1";
|
|
3
11
|
var CACHE_PREFIX = "eidos";
|
|
@@ -267,11 +275,6 @@ self.addEventListener("pushsubscriptionchange", (event) => {
|
|
|
267
275
|
});
|
|
268
276
|
})());
|
|
269
277
|
});
|
|
270
|
-
function urlBase64ToUint8Array(base64Url) {
|
|
271
|
-
const base64 = (base64Url + "=".repeat((4 - base64Url.length % 4) % 4)).replace(/-/g, "+").replace(/_/g, "/");
|
|
272
|
-
const raw = atob(base64);
|
|
273
|
-
return Uint8Array.from(raw, (c) => c.charCodeAt(0));
|
|
274
|
-
}
|
|
275
278
|
var META_DB = "eidos-sw-meta";
|
|
276
279
|
var META_STORE = "kv";
|
|
277
280
|
function openMetaDb() {
|
package/dist/eidos.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});let
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});let S=require("react"),B=require("react/jsx-runtime");function ye(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 g,D=new Set;function G(){D.forEach(e=>e())}function _(e){g={...g,...e(g)},G()}g={isOnline:typeof navigator>"u"||navigator.onLine!==!1,swStatus:"idle",swError:void 0,resources:{},queue:[],setOnline:e=>_(()=>({isOnline:e})),setSwStatus:(e,t)=>_(()=>({swStatus:e,swError:t})),...ye(_),...he(_)};function ge(){return g}function we(e){return D.add(e),()=>{D.delete(e)}}var o={getState:ge,subscribe:we,setState:e=>{const t=typeof e=="function"?e(g):e;g={...g,...t},G()}},v=null,T=[];function Y(){return v}async function ve(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 me(v),t.setSwStatus("active"),navigator.serviceWorker.addEventListener("message",Re),window.addEventListener("online",()=>t.setOnline(!0)),window.addEventListener("offline",()=>t.setOnline(!1)),_e()}catch(r){t.setSwStatus("error",String(r))}}function me(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 I(e){const t=v?.active;t?t.postMessage(e):T.push(e)}var J=null;function Se(e){J=e}function be(){try{return typeof navigator<"u"&&"serviceWorker"in navigator&&v!==null&&"sync"in v}catch{return!1}}var M={};function Ee(e){M=e}function Re(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 Ie(e){I({type:"EIDOS_SIMULATE_OFFLINE",enabled:e}),o.getState().setOnline(!e)}function _e(){const e=v?.active;if(e){for(const t of T)e.postMessage(t);T=[]}}var w=new Map,C=new Map,X=null;function ke(e){X=e}function Q(e){return e.includes("*")||/:[^/]+/.test(e)}function Oe(e){return"^"+e.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*\*/g,".+").replace(/\*/g,"[^/]+").replace(/:[^/]+/g,"[^/]+")+"$"}function z(e,t){const r=Ce(t),n=Q(e)?Oe(e):void 0,a={url:e,config:t,strategy:r,status:"idle",cacheHits:0,cacheMisses:0};return o.getState().registerResource(e,a),I({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()=>{I({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,f=new URL(u).pathname;return s?s.test(i?u:f):i?u===e:u===e||f===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()=>{w.delete(e),I({type:"EIDOS_UNREGISTER_RESOURCE",url:e}),o.getState().unregisterResource(e)}}function Ae(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(w.has(e))return w.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 w.set(e,n),n}function qe(e,t){if(!Q(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:r,regexStr:n}=z(e,t),a={url:e,config:t,strategy:r,invalidate:Z(e,r,n),unregister:ee(e)};return w.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],f=t.maxAge!==void 0&&u?.cachedAt!==void 0&&Date.now()-u.cachedAt>t.maxAge;if(c&&!f)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 m=>{m.ok&&a&&(await a.put(e,m.clone()),o.getState().updateResource(e,{cachedAt:Date.now(),lastEvent:"cache-updated"}))}).catch(()=>{}),c;const p=o.getState().resources[e];n.updateResource(e,{cacheMisses:(p?.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 Ce(e){const t=e.strategy;return e.offline?H(t??"stale-while-revalidate",e.cacheName,e.version):H(t??"network-first",e.cacheName,e.version)}var xe={"stale-while-revalidate":"StaleWhileRevalidate","cache-first":"CacheFirst","network-first":"NetworkFirst"},Pe={"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 $(e,t){const n=Ie[e];return{name:ke[e],swStrategy:e,cacheName:t??"eidos-resources-v1",reasoning:n.reasoning,behavior:n.behavior,equivalentCode:""}}async function _e(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 Ae="eidos",qe=1,p="action-queue",x=null;function b(){return x?Promise.resolve(x):new Promise((e,t)=>{const n=indexedDB.open(Ae,qe);n.onupgradeneeded=r=>{const s=r.target.result;if(!s.objectStoreNames.contains(p)){const a=s.createObjectStore(p,{keyPath:"id"});a.createIndex("status","status",{unique:!1}),a.createIndex("actionId","actionId",{unique:!1})}},n.onsuccess=()=>{x=n.result,e(n.result)},n.onerror=()=>t(n.error)})}async function Ce(e){const t=await b();return new Promise((n,r)=>{const s=t.transaction(p,"readwrite");s.objectStore(p).add(e),s.oncomplete=()=>n(),s.onerror=()=>r(s.error)})}async function X(){const e=await b();return new Promise((t,n)=>{const r=e.transaction(p,"readonly").objectStore(p).getAll();r.onsuccess=()=>t(r.result),r.onerror=()=>n(r.error)})}async function xe(e,t){const n=await b();return new Promise((r,s)=>{const a=n.transaction(p,"readwrite"),i=a.objectStore(p),o=i.get(e);o.onsuccess=()=>{o.result&&i.put({...o.result,...t})},a.oncomplete=()=>r(),a.onerror=()=>s(a.error)})}async function Qe(e){const t=await b();return new Promise((n,r)=>{const s=t.transaction(p,"readwrite");s.objectStore(p).delete(e),s.oncomplete=()=>n(),s.onerror=()=>r(s.error)})}async function Pe(){const e=await b();function t(s){return new Promise((a,i)=>{const o=e.transaction(p,"readonly").objectStore(p).index("status"),c=[],d=o.openCursor(IDBKeyRange.only(s));d.onsuccess=l=>{const f=l.target.result;f?(c.push(f.value),f.continue()):a(c)},d.onerror=()=>i(d.error)})}const[n,r]=await Promise.all([t("pending"),t("failed")]);return[...n,...r]}async function De(){const e=await b();return new Promise((t,n)=>{const r=e.transaction(p,"readwrite");r.objectStore(p).clear(),r.oncomplete=()=>t(),r.onerror=()=>n(r.error)})}var z={add:Ce,getAll:X,getPending:Pe,update:xe,remove:Qe,clear:De},Z=null;function Ne(e){Z=e}function K(){return Z}var L=new Map,ee=new Map,te=new Map,ne=new Map,re=new Map,O=new Map;function h(){return K()??z}function j(){return crypto.randomUUID()}function M(e,t,n){return e(...t,n)}function Te(e,t){const n=t.name||e.name||j(),r=t.namespace?`${t.namespace}::${n}`:n;L.set(r,e),re.set(r,t),t.onRollback&&ee.set(r,t.onRollback),t.onConflict&&te.set(r,t.onConflict),t.conflict&&ne.set(r,t.conflict);const s=async(...i)=>{const{isOnline:o}=u.getState(),c=t.reliability==="neverLose"||t.cancellable,d=c?j():"";let l;if(t.cancellable){const y=new AbortController;O.set(d,y),l=y.signal}const f={idempotencyKey:d,attempt:0,signal:l};t.onOptimistic?.(...i,f);try{if(t.reliability==="neverLose"){if(!o)return B(r,r,i,t,d);try{return await M(e,i,f)}catch(y){if(se(y))throw y;return B(r,r,i,t,d)}}try{return c?await M(e,i,f):await e(...i)}catch(y){throw t.onRollback?.(...i),y}}finally{t.cancellable&&O.delete(d)}},a=async i=>{const o=O.get(i);if(o)return o.abort(),!0;const c=(await h().getAll()).find(d=>d.idempotencyKey===i&&d.status==="pending");return c?(u.getState().removeQueueItem(c.id),await h().remove(c.id),!0):!1};return Object.defineProperty(s,"id",{value:r,writable:!1}),Object.defineProperty(s,"config",{value:t,writable:!1}),Object.defineProperty(s,"cancel",{value:a,writable:!1}),s}async function B(e,t,n,r,s){const a=j(),i={schemaVersion:2,id:a,actionId:e,actionName:t,idempotencyKey:s,args:n,queuedAt:Date.now(),retryCount:0,maxRetries:r.maxRetries??3,status:"pending",priority:r.priority??"normal"};await h().add(i),u.getState().addQueueItem(i);try{const o=G();o&&"sync"in o&&await o.sync.register("eidos-queue-replay")}catch{}return{queued:!0,id:a,message:`"${t}" queued — will execute when online`}}function se(e){return e instanceof DOMException&&e.name==="AbortError"}function je(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 Me(e){return Math.min(2e3*2**e,3e5)*(.8+Math.random()*.4)}function A(){return{attempted:0,succeeded:0,failed:0,retrying:0,skipped:0,conflicted:0,cancelled:0}}var Q=!1,Ue="eidos-queue-replay";async function q(){const e=u.getState();if(!e.isOnline)return A();if(typeof navigator<"u"&&navigator.locks)return navigator.locks.request(Ue,{ifAvailable:!0},async t=>t?F(e):A());if(Q)return A();Q=!0;try{return await F(e)}finally{Q=!1}}async function We(e,t){const n=L.get(e.actionId);if(!n)return"skipped";const r=re.get(e.actionId)?.cancellable;let s;if(r){const i=new AbortController;O.set(e.idempotencyKey,i),s=i.signal}const a={idempotencyKey:e.idempotencyKey,attempt:e.retryCount,signal:s};try{await M(n,e.args,a);const i=Date.now();return t.updateQueueItem(e.id,{status:"succeeded",completedAt:i}),await h().update(e.id,{status:"succeeded",completedAt:i}),setTimeout(()=>{t.removeQueueItem(e.id),h().remove(e.id)},3e3),"succeeded"}catch(i){if(se(i))return t.removeQueueItem(e.id),await h().remove(e.id),"cancelled";if(je(i)){const c=ne.get(e.actionId);let d;if(c)switch(c.strategy){case"serverWins":d="skip";break;case"clientWins":case"lastWriteWins":d="retry";break;case"merge":case"custom":{const l={error:i,args:e.args,attempt:e.retryCount,idempotencyKey:e.idempotencyKey};d=c.resolve?.(l)??"retry";break}}else{const l=te.get(e.actionId);l&&(d=l(i,e.args))}if(d==="skip")return t.removeQueueItem(e.id),await h().remove(e.id),"conflicted";d&&typeof d=="object"&&(e.args=d.resolved,t.updateQueueItem(e.id,{args:d.resolved}),await h().update(e.id,{args:d.resolved}))}const o=e.retryCount+1;if(o>=e.maxRetries)return t.updateQueueItem(e.id,{status:"failed",error:String(i),retryCount:o}),await h().update(e.id,{status:"failed",error:String(i),retryCount:o}),ee.get(e.actionId)?.(...e.args),"failed";{const c=Date.now()+Me(o);return t.updateQueueItem(e.id,{status:"pending",retryCount:o,nextRetryAt:c}),await h().update(e.id,{status:"pending",retryCount:o,nextRetryAt:c}),"retrying"}}finally{r&&O.delete(e.idempotencyKey)}}async function Ke(e,t,n){if(e.length===0)return;const r=e.filter(a=>L.has(a.actionId));if(n.skipped+=e.length-r.length,r.length>0){t.batchUpdateQueueItems(r.map(a=>({id:a.id,update:{status:"replaying"}})));for(const a of r)h().update(a.id,{status:"replaying"})}const s=await Promise.allSettled(r.map(a=>We(a,t)));for(const a of s){const i=a.status==="fulfilled"?a.value:"failed";i==="skipped"?n.skipped++:i==="conflicted"?n.conflicted++:i==="cancelled"?n.cancelled++:(n.attempted++,n[i]++)}}async function F(e){const t=await h().getPending(),n=Date.now(),r=t.filter(a=>a.retryCount<a.maxRetries&&(!a.nextRetryAt||a.nextRetryAt<=n)),s=A();for(const a of["high","normal","low"])await Ke(r.filter(i=>(i.priority??"normal")===a),e,s);return s}async function Le(){await h().clear(),u.getState().hydrateQueue([])}function ae(){let e=u.getState().isOnline;const t=u.subscribe(()=>{const{isOnline:s}=u.getState(),a=s&&!e;e=s,a&&setTimeout(q,600)}),n=u.getState(),r=n.queue.some(s=>s.status==="pending");return n.isOnline&&r&&setTimeout(q,1200),t}async function He(e){if(e.schemaVersion===2&&e.idempotencyKey)return e;const t={...e,schemaVersion:2,idempotencyKey:e.idempotencyKey??crypto.randomUUID()};return await(K()??z).update(t.id,{schemaVersion:t.schemaVersion,idempotencyKey:t.idempotencyKey}).catch(()=>{}),t}var U=!1,W=null;async function ie(e={}){if(typeof window>"u"||U)return;U=!0;const t=e.swPath??"/eidos-sw.js",n=e.autoReplay??!0;try{const r=await X();if(r.length>0){const s=await Promise.all(r.map(He));u.getState().hydrateQueue(s)}}catch{}try{await fe(t)}catch{}he(()=>{u.getState().isOnline&&setTimeout(q,200)}),n&&(W=ae())}function $e(){W?.(),W=null,U=!1}var P="@eidos:queue",Be=class{constructor(e){this.storage=e}async readAll(){try{const e=await this.storage.getItem(P);return e?JSON.parse(e):[]}catch{return[]}}async writeAll(e){await this.storage.setItem(P,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(s=>s.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(P)}};function Fe({children:e,swPath:t,autoReplay:n}){return(0,m.useEffect)(()=>{ie({swPath:t,autoReplay:n})},[]),(0,H.jsx)(H.Fragment,{children:e})}function g(e){const t=e??(n=>n);return(0,m.useSyncExternalStore)(u.subscribe,()=>t(u.getState()))}function Ve(){return g()}function Ge(){return g(e=>e.resources)}function Ye(e){return g(t=>t.resources[e])}function Je(){return g(e=>e.queue)}function Xe(e){return g(t=>t.queue.find(n=>n.id===e))}function ze(){return{isOnline:g(e=>e.isOnline),swStatus:g(e=>e.swStatus),swError:g(e=>e.swError)}}function Ze(){const[e,t,n,r]=g(s=>{let a=0,i=0,o=0;for(const c of s.queue)c.status==="pending"?a++:c.status==="failed"?i++:c.status==="replaying"&&o++;return`${a},${i},${o},${s.queue.length}`}).split(",");return{pending:+e,failed:+t,replaying:+n,total:+r}}function et(e){const t=g(s=>s.queue.length),n=(0,m.useRef)(0),r=(0,m.useRef)(e);(0,m.useEffect)(()=>{r.current=e}),(0,m.useEffect)(()=>{n.current>0&&t===0&&r.current(),n.current=t},[t])}var tt="1.2.0";function nt(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 oe(e,t){return nt(e,t)}function E(e,t=Object.is){return{subscribe(n){let r=e(u.getState());return n(r),u.subscribe(()=>{const s=e(u.getState());t(r,s)||(r=s,n(s))})},getState(){return e(u.getState())}}}var rt=E(e=>e),st=E(e=>e.queue),at=E(e=>({isOnline:e.isOnline,swStatus:e.swStatus,swError:e.swError}),oe),it=E(e=>{let t=0,n=0,r=0;for(const s of e.queue)s.status==="pending"?t++:s.status==="failed"?n++:s.status==="replaying"&&r++;return{pending:t,failed:n,replaying:r,total:e.queue.length}},oe);function ot(e){return E(t=>t.resources[e])}function ct(e){return E(t=>t.queue.find(n=>n.id===e))}exports.AsyncStorageQueueStorage=Be;exports.EidosProvider=Fe;exports.VERSION=tt;exports._getQueueStorage=K;exports._resetEidos=$e;exports.action=Te;exports.clearQueue=Le;exports.eidosAction=ct;exports.eidosQueue=st;exports.eidosQueueStats=it;exports.eidosResource=ot;exports.eidosStatus=at;exports.eidosStore=rt;exports.getSwRegistration=G;exports.initEidos=ie;exports.isBgSyncSupported=ye;exports.registerPushCallbacks=ge;exports.replayQueue=q;exports.resource=Ee;exports.sendToWorker=R;exports.setOfflineSimulation=ve;exports.setQueryInvalidator=me;exports.setQueueStorage=Ne;exports.subscribeReplayOnReconnect=ae;exports.useEidos=Ve;exports.useEidosAction=Xe;exports.useEidosOnDrain=et;exports.useEidosQueue=Je;exports.useEidosQueueStats=Ze;exports.useEidosResource=Ye;exports.useEidosResources=Ge;exports.useEidosStatus=ze;exports.useEidosStore=u;exports.warmCache=_e;
|
|
13
|
+
})`}};function H(e,t,r){const n=Pe[e],a=t??"eidos-resources-v1";return{name:xe[e],swStrategy:e,cacheName:r!==void 0?`${a}-v${r}`:a,reasoning:n.reasoning,behavior:n.behavior,equivalentCode:""}}async function Ne(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 De="eidos",Te=1,l="action-queue",x=null;function b(){return x?Promise.resolve(x):new Promise((e,t)=>{const r=indexedDB.open(De,Te);r.onupgradeneeded=n=>{const a=n.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})}},r.onsuccess=()=>{x=r.result,e(r.result)},r.onerror=()=>t(r.error)})}async function Me(e){const t=await b();return new Promise((r,n)=>{const a=t.transaction(l,"readwrite");a.objectStore(l).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(l,"readonly").objectStore(l).getAll();n.onsuccess=()=>t(n.result),n.onerror=()=>r(n.error)})}async function je(e,t){const r=await b();return new Promise((n,a)=>{const s=r.transaction(l,"readwrite"),i=s.objectStore(l),c=i.get(e);c.onsuccess=()=>{c.result&&i.put({...c.result,...t})},s.oncomplete=()=>n(),s.onerror=()=>a(s.error)})}async function Ue(e){const t=await b();return new Promise((r,n)=>{const a=t.transaction(l,"readwrite");a.objectStore(l).delete(e),a.oncomplete=()=>r(),a.onerror=()=>n(a.error)})}async function Ke(){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 m=p.target.result;m?(u.push(m.value),m.continue()):s(u)},f.onerror=()=>i(f.error)})}const[r,n]=await Promise.all([t("pending"),t("failed")]);return[...r,...n]}async function $e(){const e=await b();return new Promise((t,r)=>{const n=e.transaction(l,"readwrite");n.objectStore(l).clear(),n.oncomplete=()=>t(),n.onerror=()=>r(n.error)})}var ne={add:Me,getAll:te,getPending:Ke,update:je,remove:Ue,clear:$e},re=null;function Le(e){re=e}function W(){return re}var We="eidos-queue-sync",k;function ae(){return k!==void 0||(k=typeof BroadcastChannel>"u"?null:new BroadcastChannel(We)),k}function h(e){ae()?.postMessage(e)}function Be(){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 d(){return W()??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(...s)=>{const{isOnline:i}=o.getState(),c=j();let u;if(t.cancellable){const p=new AbortController;R.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 F(n,n,s,t,c);try{return await U(e,s,f)}catch(p){if(de(p))throw p;return F(n,n,s,t,c)}}try{return await U(e,s,f)}catch(p){throw t.onRollback?.(...s,f),p}}finally{t.cancellable&&R.delete(c)}};return Object.defineProperty(a,"id",{value:n,writable:!1}),Object.defineProperty(a,"config",{value:t,writable:!1}),Object.defineProperty(a,"cancel",{value:ue,writable:!1}),a}async function ue(e){const t=R.get(e);if(t)return t.abort(),!0;const r=(await d().getAll()).find(n=>n.idempotencyKey===e&&n.status==="pending");return r?(o.getState().removeQueueItem(r.id),h({type:"remove",id:r.id}),await d().remove(r.id),!0):!1}async function Fe(e){const t=(await d().getAll()).find(n=>n.id===e);if(!t||t.status!=="failed")return!1;const r={status:"pending",error:void 0,nextRetryAt:void 0,retryCount:0};return o.getState().updateQueueItem(e,r),h({type:"update",id:e,update:r}),await d().update(e,r),!0}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 d().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 de(e){return e instanceof DOMException&&e.name==="AbortError"}function Ve(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 Ge(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,Ye="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(Ye,{ifAvailable:!0},async t=>t?V(e):O());if(P)return O();P=!0;try{return await V(e)}finally{P=!1}}async function Je(e,t){const r=Date.now();t.updateQueueItem(e.id,{status:"succeeded",completedAt:r}),h({type:"update",id:e.id,update:{status:"succeeded",completedAt:r}}),await d().update(e.id,{status:"succeeded",completedAt:r}),setTimeout(()=>{t.removeQueueItem(e.id),h({type:"remove",id:e.id}),d().remove(e.id)},3e3)}async function Xe(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),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 ze(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),h({type:"update",id:e.id,update:s}),await d().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()+Ge(n)};return t.updateQueueItem(e.id,a),h({type:"update",id:e.id,update:a}),await d().update(e.id,a),"retrying"}async function Ze(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 Je(e,t),"succeeded"}catch(i){if(de(i))return t.removeQueueItem(e.id),h({type:"remove",id:e.id}),await d().remove(e.id),"cancelled";if(Ve(i)){const c=await Xe(e,t,i);if(c)return c}return ze(e,t,i)}finally{n&&R.delete(e.idempotencyKey)}}async function et(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),h({type:"batchUpdate",updates:s});for(const i of n)d().update(i.id,{status:"replaying"})}const a=await Promise.allSettled(n.map(s=>Ze(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 d().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 et(n.filter(i=>(i.priority??"normal")===s),e,a);return a}async function tt(){await d().clear(),o.getState().hydrateQueue([])}function le(){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 nt(e){if(e.schemaVersion===2&&e.idempotencyKey)return e;const t={...e,schemaVersion:2,idempotencyKey:e.idempotencyKey??crypto.randomUUID()};return await(W()??ne).update(t.id,{schemaVersion:t.schemaVersion,idempotencyKey:t.idempotencyKey}).catch(()=>{}),t}var K=!1,$=null,L=null;async function fe(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(nt));o.getState().hydrateQueue(a)}}catch{}try{await ve(t)}catch{}Se(()=>{o.getState().isOnline&&setTimeout(q,200)}),r&&($=le()),L=Be()}function rt(){$?.(),$=null,L?.(),L=null,K=!1}var N="@eidos:queue",at=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 st({children:e,swPath:t,autoReplay:r}){return(0,S.useEffect)(()=>{fe({swPath:t,autoReplay:r})},[]),(0,B.jsx)(B.Fragment,{children:e})}function y(e){const t=e??(r=>r);return(0,S.useSyncExternalStore)(o.subscribe,()=>t(o.getState()))}function it(){return y()}function ot(){return y(e=>e.resources)}function ct(e){return y(t=>t.resources[e])}function ut(){return y(e=>e.queue)}function dt(e){return y(t=>t.queue.find(r=>r.id===e))}function lt(){return{isOnline:y(e=>e.isOnline),swStatus:y(e=>e.swStatus),swError:y(e=>e.swError)}}function ft(){const[e,t,r,n]=y(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 pt(e){const t=y(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 yt="2.1.0";function ht(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 pe(e,t){return ht(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 gt=E(e=>e),wt=E(e=>e.queue),vt=E(e=>({isOnline:e.isOnline,swStatus:e.swStatus,swError:e.swError}),pe),mt=E(e=>se(e.queue),pe);function St(e){return E(t=>t.resources[e])}function bt(e){return E(t=>t.queue.find(r=>r.id===e))}exports.AsyncStorageQueueStorage=at;exports.EidosProvider=st;exports.VERSION=yt;exports._getQueueStorage=W;exports._resetEidos=rt;exports.action=He;exports.cancelByIdempotencyKey=ue;exports.clearQueue=tt;exports.eidosAction=bt;exports.eidosQueue=wt;exports.eidosQueueStats=mt;exports.eidosResource=St;exports.eidosStatus=vt;exports.eidosStore=gt;exports.getSwRegistration=Y;exports.initEidos=fe;exports.isBgSyncSupported=be;exports.registerPushCallbacks=Ee;exports.replayQueue=q;exports.requeueItem=Fe;exports.resource=Ae;exports.resourcePattern=qe;exports.sendToWorker=I;exports.setOfflineSimulation=Ie;exports.setQueryInvalidator=ke;exports.setQueueStorage=Le;exports.subscribeReplayOnReconnect=le;exports.useEidos=it;exports.useEidosAction=dt;exports.useEidosOnDrain=pt;exports.useEidosQueue=ut;exports.useEidosQueueStats=ft;exports.useEidosResource=ct;exports.useEidosResources=ot;exports.useEidosStatus=lt;exports.useEidosStore=o;exports.warmCache=Ne;
|
|
14
14
|
|
|
15
15
|
//# sourceMappingURL=eidos.cjs.map
|