@sweidos/eidos 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"resource.js","names":[],"sources":["../src/resource.ts"],"sourcesContent":["import { useEidosStore } from './store';\nimport { sendToWorker } from './sw-bridge';\nimport type {\n ResourceConfig,\n ResourceHandle,\n PatternResourceHandle,\n ResourceEntry,\n GeneratedStrategy,\n CacheStrategy,\n WarmCacheResult,\n} from './types';\n\nconst _registry = new Map<string, ResourceHandle | PatternResourceHandle>();\n\n// ── Request deduplication ─────────────────────────────────────────────────────\n// If multiple callers invoke handle.fetch() simultaneously for the same URL,\n// only one network request is made. Each caller gets its own cloned Response.\n// Keyed by URL; entry is deleted when the request settles.\nconst _inflightRequests = /* @__PURE__ */ new Map<string, Promise<Response>>();\n\n// ── TanStack Query bridge (optional) ─────────────────────────────────────────\n// Set by @sweidos/eidos/query when withEidosQueryClient() is called.\n// Lets handle.invalidate() also invalidate the matching TQ cache entry.\ntype QueryInvalidator = (queryKey: [string, string]) => void;\nlet _queryInvalidator: QueryInvalidator | null = null;\n\n/** @internal Called by @sweidos/eidos/query. */\nexport function setQueryInvalidator(fn: QueryInvalidator): void {\n _queryInvalidator = fn;\n}\n\n// ── URL pattern helpers ───────────────────────────────────────────────────────\n\n/** Returns true if `url` contains wildcard or :param segments. */\nfunction isPattern(url: string): boolean {\n return url.includes('*') || /:[^/]+/.test(url);\n}\n\n/**\n * Converts a URL pattern to a regex source string for SW fetch matching.\n * `**` → multi-segment wildcard (`.+`)\n * `*` → single-segment wildcard (`[^/]+`)\n * `:param` → named single segment (`[^/]+`)\n *\n * Special regex characters in the pattern (e.g. `.`) are escaped first so\n * they match literally.\n *\n * @example\n * patternToRegexStr('/api/products/*') // '^/api/products/[^/]+$'\n * patternToRegexStr('/api/products/**') // '^/api/products/.+$'\n * patternToRegexStr('/api/users/:id') // '^/api/users/[^/]+$'\n */\nfunction patternToRegexStr(pattern: string): string {\n // Escape all regex-special chars except `*`, `/`, `:` (handled below)\n const escaped = pattern.replace(/[.+?^${}()|[\\]\\\\]/g, '\\\\$&');\n return (\n '^' +\n escaped\n .replace(/\\*\\*/g, '.+') // ** → multi-segment wildcard\n .replace(/\\*/g, '[^/]+') // * → single-segment wildcard\n .replace(/:[^/]+/g, '[^/]+') + // :param → single-segment wildcard\n '$'\n );\n}\n\n/** Shared setup for resource()/resourcePattern(): strategy derivation, store + SW registration. */\nfunction _register(\n url: string,\n config: ResourceConfig,\n): { strategy: GeneratedStrategy; regexStr: string | undefined } {\n const strategy = deriveStrategy(config);\n const regexStr = isPattern(url) ? patternToRegexStr(url) : undefined;\n\n const entry: ResourceEntry = {\n url,\n config,\n strategy,\n status: 'idle',\n cacheHits: 0,\n cacheMisses: 0,\n };\n\n useEidosStore.getState().registerResource(url, entry);\n\n sendToWorker({\n type: 'EIDOS_REGISTER_RESOURCE',\n url,\n strategy: strategy.swStrategy,\n cacheName: strategy.cacheName,\n ...(regexStr !== undefined && { pattern: regexStr }),\n });\n\n return { strategy, regexStr };\n}\n\nfunction _invalidate(\n url: string,\n strategy: GeneratedStrategy,\n regexStr: string | undefined,\n): () => Promise<void> {\n return async () => {\n sendToWorker({ type: 'EIDOS_CLEAR_CACHE', url });\n const cache = await caches.open(strategy.cacheName).catch(() => null);\n if (cache) {\n const keys = await cache.keys();\n const patternRe = regexStr ? new RegExp(regexStr) : null;\n const isCrossOrigin = url.startsWith('http');\n await Promise.all(\n keys\n .filter((r) => {\n const rUrl = r.url;\n const p = new URL(rUrl).pathname;\n if (patternRe) {\n // Cross-origin patterns were compiled from absolute URLs; test full URL.\n return patternRe.test(isCrossOrigin ? rUrl : p);\n }\n return isCrossOrigin ? rUrl === url : rUrl === url || p === url;\n })\n .map((r) => cache.delete(r)),\n );\n }\n // For exact-URL resources update the store entry; patterns don't have a\n // single entry to update (individual URLs are not tracked per-pattern).\n if (!isPattern(url)) {\n useEidosStore.getState().updateResource(url, {\n status: 'stale',\n cachedAt: undefined,\n lastEvent: 'cache-cleared',\n cacheHits: 0,\n cacheMisses: 0,\n });\n }\n // Notify TanStack Query bridge if registered.\n _queryInvalidator?.(['eidos', url]);\n };\n}\n\nfunction _unregister(url: string): () => void {\n return () => {\n _registry.delete(url);\n sendToWorker({ type: 'EIDOS_UNREGISTER_RESOURCE', url });\n useEidosStore.getState().unregisterResource(url);\n };\n}\n\nfunction _warnIfReregisteredWithDifferentConfig(\n url: string,\n existing: ResourceHandle | PatternResourceHandle,\n config: ResourceConfig,\n factoryName: string,\n): void {\n if (!import.meta.env.DEV) return;\n const existingCfg = existing.config;\n if (\n existingCfg.offline !== config.offline ||\n existingCfg.strategy !== config.strategy ||\n existingCfg.cacheName !== config.cacheName ||\n existingCfg.version !== config.version\n ) {\n console.warn(\n `[eidos] ${factoryName}('${url}') already registered with a different config — returning cached handle. Call handle.unregister() first to re-register.`,\n { registered: existingCfg, ignored: config },\n );\n }\n}\n\n// ── resource() ────────────────────────────────────────────────────────────────\n\n/**\n * Registers a concrete-URL resource. For URL patterns (`/api/products/*`,\n * `/api/users/:id`, `**`), use `resourcePattern()` instead.\n */\nexport function resource<T = unknown>(url: string, config: ResourceConfig): ResourceHandle<T> {\n if (isPattern(url)) {\n throw new Error(\n `[eidos] resource('${url}') is a URL pattern — use resourcePattern('${url}', config) instead. ` +\n `Pattern handles only support invalidate()/unregister(); the SW intercepts matching requests automatically.`,\n );\n }\n\n if (_registry.has(url)) {\n const existing = _registry.get(url)!;\n _warnIfReregisteredWithDifferentConfig(url, existing, config, 'resource');\n return existing as ResourceHandle<T>;\n }\n\n const { strategy } = _register(url, config);\n\n const handle: ResourceHandle<T> = {\n url,\n config,\n strategy,\n\n fetch: async () => {\n // ── Deduplication: coalesce concurrent fetches for the same URL ─────\n // If a request is already in-flight, piggyback on it and return a clone\n // so each caller gets an independent readable Response body.\n const existing = _inflightRequests.get(url);\n if (existing) return existing.then((r) => r.clone());\n\n // Store the raw-response promise. All callers (including the primary)\n // receive a clone — the raw response stays unconsumed in the map so\n // any caller arriving while the promise is still pending can clone it.\n const task = _fetchResource(url, config, strategy);\n _inflightRequests.set(url, task);\n // .catch() silences the unhandled-rejection on the cleanup promise;\n // the error still propagates to callers via task.then() below.\n task.finally(() => _inflightRequests.delete(url)).catch(() => {});\n return task.then((r) => r.clone());\n },\n\n json: async () => {\n const res = await handle.fetch();\n return res.json() as Promise<T>;\n },\n\n query: () => ({\n queryKey: ['eidos', url] as [string, string],\n queryFn: () => handle.json(),\n }),\n\n prefetch: async () => {\n await handle.fetch();\n },\n\n invalidate: _invalidate(url, strategy, undefined),\n unregister: _unregister(url),\n };\n\n _registry.set(url, handle);\n return handle;\n}\n\n// ── resourcePattern() ────────────────────────────────────────────────────────\n\n/**\n * Registers a URL pattern (`/api/products/*`, `/api/users/:id`, `**`). The SW\n * intercepts all matching requests automatically — there is no single URL to\n * fetch/cache directly, so the returned handle only supports cache management\n * (`invalidate`/`unregister`). For a fetchable resource, use `resource()`.\n */\nexport function resourcePattern(url: string, config: ResourceConfig): PatternResourceHandle {\n if (!isPattern(url)) {\n throw new Error(\n `[eidos] resourcePattern('${url}') is not a URL pattern — use resource('${url}', config) instead.`,\n );\n }\n\n if (_registry.has(url)) {\n const existing = _registry.get(url)!;\n _warnIfReregisteredWithDifferentConfig(url, existing, config, 'resourcePattern');\n return existing as PatternResourceHandle;\n }\n\n const { strategy, regexStr } = _register(url, config);\n\n const handle: PatternResourceHandle = {\n url,\n config,\n strategy,\n invalidate: _invalidate(url, strategy, regexStr),\n unregister: _unregister(url),\n };\n\n _registry.set(url, handle);\n return handle;\n}\n\n// ── _fetchResource ─────────────────────────────────────────────────────────────\n// The actual network/cache implementation. Separated from handle.fetch() so the\n// deduplication wrapper can store the Promise and share it across concurrent callers.\n// Returns the raw (unconsumed) Response — callers MUST .clone() before reading body.\nasync function _fetchResource(\n url: string,\n config: ResourceConfig,\n strategy: GeneratedStrategy,\n): Promise<Response> {\n const store = useEidosStore.getState();\n store.updateResource(url, { status: 'fetching', fetchedAt: Date.now() });\n\n // Open cache once and reuse across try/catch — avoids a redundant\n // caches.open() call in the error fallback path.\n const cache = await caches.open(strategy.cacheName).catch(() => null);\n\n try {\n // ── network-first: skip cache check, go straight to network ─────────\n // For cache-first / SWR the cache check below is correct. For\n // network-first, reading cache first and returning early would\n // contradict the strategy — fresh data is the priority.\n if (strategy.swStrategy !== 'network-first') {\n // ── Direct Cache API check ─────────────────────────────────────────\n // We read the cache in the main thread rather than waiting for\n // an async SW postMessage. This gives instant, reliable status\n // updates regardless of SW message timing.\n const cached = cache ? await cache.match(url).catch(() => null) : null;\n\n // Treat cache as miss if maxAge exceeded\n const current = useEidosStore.getState().resources[url];\n const expired =\n config.maxAge !== undefined &&\n current?.cachedAt !== undefined &&\n Date.now() - current.cachedAt > config.maxAge;\n\n if (cached && !expired) {\n store.updateResource(url, {\n status: 'fresh',\n lastEvent: 'cache-hit',\n cacheHits: (current?.cacheHits ?? 0) + 1,\n });\n\n // Background revalidation for SWR (stale-while-revalidate)\n if (strategy.swStrategy === 'stale-while-revalidate') {\n fetch(url, { signal: AbortSignal.timeout(5000) })\n .then(async (resp) => {\n if (resp.ok && cache) {\n await cache.put(url, resp.clone());\n useEidosStore.getState().updateResource(url, {\n cachedAt: Date.now(),\n lastEvent: 'cache-updated',\n });\n }\n })\n .catch(() => {\n /* offline or timed out — cached version stays valid */\n });\n }\n\n return cached;\n }\n\n // Cache miss (or expired)\n const storeEntry = useEidosStore.getState().resources[url];\n store.updateResource(url, {\n cacheMisses: (storeEntry?.cacheMisses ?? 0) + 1,\n });\n }\n\n const response = await fetch(url);\n\n if (response.ok) {\n if (cache) await cache.put(url, response.clone());\n store.updateResource(url, {\n status: 'fresh',\n cachedAt: Date.now(),\n lastEvent: 'cache-updated',\n });\n return response;\n }\n\n // Non-2xx response (e.g. 503 from offline SW) — update status and throw\n // so callers get a proper error instead of a plain-object body they can't use.\n store.updateResource(url, { status: response.status === 503 ? 'offline' : 'error' });\n\n // Check if the SW tagged this as an offline response\n const isOffline = response.headers.get('X-Eidos-Offline') === 'true';\n throw new Error(\n isOffline\n ? `offline: no cached response for ${url}`\n : `${response.status} ${response.statusText}`,\n );\n } catch (err) {\n // Network failure — try cache one more time as fallback\n const fallback = cache ? await cache.match(url).catch(() => null) : null;\n\n if (fallback) {\n const current = useEidosStore.getState().resources[url];\n store.updateResource(url, {\n status: 'fresh',\n lastEvent: 'cache-hit',\n cacheHits: (current?.cacheHits ?? 0) + 1,\n });\n return fallback;\n }\n\n store.updateResource(url, { status: 'error' });\n throw err;\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Strategy derivation — intent → deterministic caching strategy\n// ─────────────────────────────────────────────────────────────────────────────\n\nfunction deriveStrategy(config: ResourceConfig): GeneratedStrategy {\n const explicit = config.strategy;\n if (config.offline) {\n return buildStrategy(explicit ?? 'stale-while-revalidate', config.cacheName, config.version);\n }\n return buildStrategy(explicit ?? 'network-first', config.cacheName, config.version);\n}\n\n// Strategy display names — always included (tiny, used by devtools).\nconst STRATEGY_NAMES: Record<CacheStrategy, string> = {\n 'stale-while-revalidate': 'StaleWhileRevalidate',\n 'cache-first': 'CacheFirst',\n 'network-first': 'NetworkFirst',\n};\n\n// Heavy descriptive strings — stripped from production bundles by Vite's\n// import.meta.env.DEV dead-code elimination. Only the names above ship in prod.\ntype StrategyDevInfo = Pick<GeneratedStrategy, 'reasoning' | 'behavior' | 'equivalentCode'>;\nconst _STRATEGY_DEV_META: Record<CacheStrategy, StrategyDevInfo> = {\n 'stale-while-revalidate': {\n reasoning:\n 'offline: true signals resilience. SWR returns cached data instantly while revalidating in the background — the best tradeoff between speed and freshness for offline-capable resources.',\n behavior: [\n 'Cache hit → return immediately, kick off background revalidation',\n 'Cache miss → fetch from network, cache the response, return it',\n 'Offline → return cached version if available, 503 if not',\n 'Reconnect → next request triggers a background refresh',\n ],\n equivalentCode: `// Workbox equivalent\nnew StaleWhileRevalidate({\n cacheName: 'eidos-resources-v1',\n plugins: [new ExpirationPlugin({ maxEntries: 60 })],\n})`,\n },\n 'cache-first': {\n reasoning:\n 'cache-first maximises speed and offline availability. Network is consulted only on cache miss. Best for static or infrequently-updated data.',\n behavior: [\n 'Cache hit → return immediately, no network request made',\n 'Cache miss → fetch from network, cache the response, return it',\n 'Offline → return cached version, 503 if cache is empty',\n 'Cache never expires unless explicitly invalidated',\n ],\n equivalentCode: `// Workbox equivalent\nnew CacheFirst({\n cacheName: 'eidos-resources-v1',\n plugins: [new ExpirationPlugin({ maxEntries: 60 })],\n})`,\n },\n 'network-first': {\n reasoning:\n 'network-first prioritises fresh data. Cache acts as a safety net when offline. Best for frequently-updated resources where stale data causes problems.',\n behavior: [\n 'Always try network first',\n 'Network success → update cache, return fresh response',\n 'Network failure → fall back to cached version',\n 'Offline with empty cache → return 503 error response',\n ],\n equivalentCode: `// Workbox equivalent\nnew NetworkFirst({\n cacheName: 'eidos-resources-v1',\n networkTimeoutSeconds: 3,\n})`,\n },\n};\n\nfunction buildStrategy(\n swStrategy: CacheStrategy,\n cacheName?: string,\n version?: string | number,\n): GeneratedStrategy {\n const meta = _STRATEGY_DEV_META[swStrategy];\n const baseName = cacheName ?? 'eidos-resources-v1';\n return {\n name: STRATEGY_NAMES[swStrategy],\n swStrategy,\n cacheName: version !== undefined ? `${baseName}-v${version}` : baseName,\n // reasoning + behavior are rendered by the playground UI from live ResourceEntry objects —\n // keep them in all builds. equivalentCode is a static code block only used in DEV tools.\n reasoning: meta.reasoning,\n behavior: meta.behavior,\n equivalentCode: import.meta.env.DEV ? meta.equivalentCode : '',\n };\n}\n\n// ── warmCache ─────────────────────────────────────────────────────────────────\n\n/**\n * Bulk-prefetch an array of resource handles concurrently, warming the cache\n * for each one. Useful on login / app init when you know which resources the\n * user will need offline.\n *\n * Pattern handles (containing `*`, `**`, or `:param`) are silently skipped —\n * they match multiple URLs so there is no single URL to prefetch.\n *\n * @example\n * import { warmCache } from '@sweidos/eidos'\n *\n * // In EidosProvider's onReady, or after login:\n * const { warmed, failed } = await warmCache([products, userProfile, settings])\n */\nexport async function warmCache(handles: ResourceHandle[]): Promise<WarmCacheResult> {\n const results = await Promise.allSettled(handles.map((h) => h.prefetch()));\n const errors = results\n .filter((r): r is PromiseRejectedResult => r.status === 'rejected')\n .map((r) => r.reason);\n\n if (import.meta.env.DEV && errors.length > 0) {\n console.warn(`[eidos] warmCache: ${errors.length} handle(s) failed to prefetch`, errors);\n }\n\n return {\n warmed: results.filter((r) => r.status === 'fulfilled').length,\n failed: errors.length,\n errors,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"resource.js","names":[],"sources":["../src/resource.ts"],"sourcesContent":["import { useEidosStore } from './store';\nimport { sendToWorker } from './sw-bridge';\nimport type {\n ResourceConfig,\n ResourceHandle,\n PatternResourceHandle,\n ResourceEntry,\n GeneratedStrategy,\n CacheStrategy,\n WarmCacheResult,\n} from './types';\n\nconst _registry = new Map<string, ResourceHandle | PatternResourceHandle>();\n\n// ── Request deduplication ─────────────────────────────────────────────────────\n// If multiple callers invoke handle.fetch() simultaneously for the same URL,\n// only one network request is made. Each caller gets its own cloned Response.\n// Keyed by URL; entry is deleted when the request settles.\nconst _inflightRequests = /* @__PURE__ */ new Map<string, Promise<Response>>();\n\n// ── TanStack Query bridge (optional) ─────────────────────────────────────────\n// Set by @sweidos/eidos/query when withEidosQueryClient() is called.\n// Lets handle.invalidate() also invalidate the matching TQ cache entry.\ntype QueryInvalidator = (queryKey: [string, string]) => void;\nlet _queryInvalidator: QueryInvalidator | null = null;\n\n/** @internal Called by @sweidos/eidos/query. */\nexport function setQueryInvalidator(fn: QueryInvalidator): void {\n _queryInvalidator = fn;\n}\n\n// ── URL pattern helpers ───────────────────────────────────────────────────────\n\n/** Returns true if `url` contains wildcard or :param segments. */\nfunction isPattern(url: string): boolean {\n return url.includes('*') || /:[^/]+/.test(url);\n}\n\n/**\n * Converts a URL pattern to a regex source string for SW fetch matching.\n * `**` → multi-segment wildcard (`.+`)\n * `*` → single-segment wildcard (`[^/]+`)\n * `:param` → named single segment (`[^/]+`)\n *\n * Special regex characters in the pattern (e.g. `.`) are escaped first so\n * they match literally.\n *\n * @example\n * patternToRegexStr('/api/products/*') // '^/api/products/[^/]+$'\n * patternToRegexStr('/api/products/**') // '^/api/products/.+$'\n * patternToRegexStr('/api/users/:id') // '^/api/users/[^/]+$'\n */\nfunction patternToRegexStr(pattern: string): string {\n // Escape all regex-special chars except `*`, `/`, `:` (handled below)\n const escaped = pattern.replace(/[.+?^${}()|[\\]\\\\]/g, '\\\\$&');\n return (\n '^' +\n escaped\n .replace(/\\*\\*/g, '.+') // ** → multi-segment wildcard\n .replace(/\\*/g, '[^/]+') // * → single-segment wildcard\n .replace(/:[^/]+/g, '[^/]+') + // :param → single-segment wildcard\n '$'\n );\n}\n\n/** Shared setup for resource()/resourcePattern(): strategy derivation, store + SW registration. */\nfunction _register(\n url: string,\n config: ResourceConfig,\n): { strategy: GeneratedStrategy; regexStr: string | undefined } {\n const strategy = deriveStrategy(config);\n const regexStr = isPattern(url) ? patternToRegexStr(url) : undefined;\n\n const entry: ResourceEntry = {\n url,\n config,\n strategy,\n status: 'idle',\n cacheHits: 0,\n cacheMisses: 0,\n };\n\n useEidosStore.getState().registerResource(url, entry);\n\n sendToWorker({\n type: 'EIDOS_REGISTER_RESOURCE',\n url,\n strategy: strategy.swStrategy,\n cacheName: strategy.cacheName,\n ...(regexStr !== undefined && { pattern: regexStr }),\n ...(config.maxAge !== undefined && { maxAge: config.maxAge }),\n ...(config.maxEntries !== undefined && { maxEntries: config.maxEntries }),\n });\n\n return { strategy, regexStr };\n}\n\nfunction _invalidate(\n url: string,\n strategy: GeneratedStrategy,\n regexStr: string | undefined,\n): () => Promise<void> {\n return async () => {\n sendToWorker({ type: 'EIDOS_CLEAR_CACHE', url });\n const cache = await caches.open(strategy.cacheName).catch(() => null);\n if (cache) {\n const keys = await cache.keys();\n const patternRe = regexStr ? new RegExp(regexStr) : null;\n const isCrossOrigin = url.startsWith('http');\n await Promise.all(\n keys\n .filter((r) => {\n const rUrl = r.url;\n const p = new URL(rUrl).pathname;\n if (patternRe) {\n // Cross-origin patterns were compiled from absolute URLs; test full URL.\n return patternRe.test(isCrossOrigin ? rUrl : p);\n }\n return isCrossOrigin ? rUrl === url : rUrl === url || p === url;\n })\n .map((r) => cache.delete(r)),\n );\n }\n // For exact-URL resources update the store entry; patterns don't have a\n // single entry to update (individual URLs are not tracked per-pattern).\n if (!isPattern(url)) {\n useEidosStore.getState().updateResource(url, {\n status: 'stale',\n cachedAt: undefined,\n lastEvent: 'cache-cleared',\n cacheHits: 0,\n cacheMisses: 0,\n });\n }\n // Notify TanStack Query bridge if registered.\n _queryInvalidator?.(['eidos', url]);\n };\n}\n\nfunction _unregister(url: string): () => void {\n return () => {\n _registry.delete(url);\n sendToWorker({ type: 'EIDOS_UNREGISTER_RESOURCE', url });\n useEidosStore.getState().unregisterResource(url);\n };\n}\n\nfunction _warnIfReregisteredWithDifferentConfig(\n url: string,\n existing: ResourceHandle | PatternResourceHandle,\n config: ResourceConfig,\n factoryName: string,\n): void {\n if (!import.meta.env.DEV) return;\n const existingCfg = existing.config;\n if (\n existingCfg.offline !== config.offline ||\n existingCfg.strategy !== config.strategy ||\n existingCfg.cacheName !== config.cacheName ||\n existingCfg.version !== config.version\n ) {\n console.warn(\n `[eidos] ${factoryName}('${url}') already registered with a different config — returning cached handle. Call handle.unregister() first to re-register.`,\n { registered: existingCfg, ignored: config },\n );\n }\n}\n\n// ── resource() ────────────────────────────────────────────────────────────────\n\n/**\n * Registers a concrete-URL resource. For URL patterns (`/api/products/*`,\n * `/api/users/:id`, `**`), use `resourcePattern()` instead.\n */\nexport function resource<T = unknown>(url: string, config: ResourceConfig): ResourceHandle<T> {\n if (isPattern(url)) {\n throw new Error(\n `[eidos] resource('${url}') is a URL pattern — use resourcePattern('${url}', config) instead. ` +\n `Pattern handles only support invalidate()/unregister(); the SW intercepts matching requests automatically.`,\n );\n }\n\n if (_registry.has(url)) {\n const existing = _registry.get(url)!;\n _warnIfReregisteredWithDifferentConfig(url, existing, config, 'resource');\n return existing as ResourceHandle<T>;\n }\n\n const { strategy } = _register(url, config);\n\n const handle: ResourceHandle<T> = {\n url,\n config,\n strategy,\n\n fetch: async () => {\n // ── Deduplication: coalesce concurrent fetches for the same URL ─────\n // If a request is already in-flight, piggyback on it and return a clone\n // so each caller gets an independent readable Response body.\n const existing = _inflightRequests.get(url);\n if (existing) return existing.then((r) => r.clone());\n\n // Store the raw-response promise. All callers (including the primary)\n // receive a clone — the raw response stays unconsumed in the map so\n // any caller arriving while the promise is still pending can clone it.\n const task = _fetchResource(url, config, strategy);\n _inflightRequests.set(url, task);\n // .catch() silences the unhandled-rejection on the cleanup promise;\n // the error still propagates to callers via task.then() below.\n task.finally(() => _inflightRequests.delete(url)).catch(() => {});\n return task.then((r) => r.clone());\n },\n\n json: async () => {\n const res = await handle.fetch();\n return res.json() as Promise<T>;\n },\n\n query: () => ({\n queryKey: ['eidos', url] as [string, string],\n queryFn: () => handle.json(),\n }),\n\n prefetch: async () => {\n await handle.fetch();\n },\n\n invalidate: _invalidate(url, strategy, undefined),\n unregister: _unregister(url),\n };\n\n _registry.set(url, handle);\n return handle;\n}\n\n// ── resourcePattern() ────────────────────────────────────────────────────────\n\n/**\n * Registers a URL pattern (`/api/products/*`, `/api/users/:id`, `**`). The SW\n * intercepts all matching requests automatically — there is no single URL to\n * fetch/cache directly, so the returned handle only supports cache management\n * (`invalidate`/`unregister`). For a fetchable resource, use `resource()`.\n */\nexport function resourcePattern(url: string, config: ResourceConfig): PatternResourceHandle {\n if (!isPattern(url)) {\n throw new Error(\n `[eidos] resourcePattern('${url}') is not a URL pattern — use resource('${url}', config) instead.`,\n );\n }\n\n if (_registry.has(url)) {\n const existing = _registry.get(url)!;\n _warnIfReregisteredWithDifferentConfig(url, existing, config, 'resourcePattern');\n return existing as PatternResourceHandle;\n }\n\n const { strategy, regexStr } = _register(url, config);\n\n const handle: PatternResourceHandle = {\n url,\n config,\n strategy,\n invalidate: _invalidate(url, strategy, regexStr),\n unregister: _unregister(url),\n };\n\n _registry.set(url, handle);\n return handle;\n}\n\n// ── _fetchResource ─────────────────────────────────────────────────────────────\n// The actual network/cache implementation. Separated from handle.fetch() so the\n// deduplication wrapper can store the Promise and share it across concurrent callers.\n// Returns the raw (unconsumed) Response — callers MUST .clone() before reading body.\nasync function _fetchResource(\n url: string,\n config: ResourceConfig,\n strategy: GeneratedStrategy,\n): Promise<Response> {\n const store = useEidosStore.getState();\n store.updateResource(url, { status: 'fetching', fetchedAt: Date.now() });\n\n // Open cache once and reuse across try/catch — avoids a redundant\n // caches.open() call in the error fallback path.\n const cache = await caches.open(strategy.cacheName).catch(() => null);\n\n try {\n // ── network-first: skip cache check, go straight to network ─────────\n // For cache-first / SWR the cache check below is correct. For\n // network-first, reading cache first and returning early would\n // contradict the strategy — fresh data is the priority.\n if (strategy.swStrategy !== 'network-first') {\n // ── Direct Cache API check ─────────────────────────────────────────\n // We read the cache in the main thread rather than waiting for\n // an async SW postMessage. This gives instant, reliable status\n // updates regardless of SW message timing.\n const cached = cache ? await cache.match(url).catch(() => null) : null;\n\n // Treat cache as miss if maxAge exceeded\n const current = useEidosStore.getState().resources[url];\n const expired =\n config.maxAge !== undefined &&\n current?.cachedAt !== undefined &&\n Date.now() - current.cachedAt > config.maxAge;\n\n if (cached && !expired) {\n store.updateResource(url, {\n status: 'fresh',\n lastEvent: 'cache-hit',\n cacheHits: (current?.cacheHits ?? 0) + 1,\n });\n\n // Background revalidation for SWR (stale-while-revalidate)\n if (strategy.swStrategy === 'stale-while-revalidate') {\n fetch(url, { signal: AbortSignal.timeout(5000) })\n .then(async (resp) => {\n if (resp.ok && cache) {\n await cache.put(url, resp.clone());\n useEidosStore.getState().updateResource(url, {\n cachedAt: Date.now(),\n lastEvent: 'cache-updated',\n });\n }\n })\n .catch(() => {\n /* offline or timed out — cached version stays valid */\n });\n }\n\n return cached;\n }\n\n // Cache miss (or expired)\n const storeEntry = useEidosStore.getState().resources[url];\n store.updateResource(url, {\n cacheMisses: (storeEntry?.cacheMisses ?? 0) + 1,\n });\n }\n\n const response = await fetch(url);\n\n if (response.ok) {\n if (cache) await cache.put(url, response.clone());\n store.updateResource(url, {\n status: 'fresh',\n cachedAt: Date.now(),\n lastEvent: 'cache-updated',\n });\n return response;\n }\n\n // Non-2xx response (e.g. 503 from offline SW) — update status and throw\n // so callers get a proper error instead of a plain-object body they can't use.\n store.updateResource(url, { status: response.status === 503 ? 'offline' : 'error' });\n\n // Check if the SW tagged this as an offline response\n const isOffline = response.headers.get('X-Eidos-Offline') === 'true';\n throw new Error(\n isOffline\n ? `offline: no cached response for ${url}`\n : `${response.status} ${response.statusText}`,\n );\n } catch (err) {\n // Network failure — try cache one more time as fallback\n const fallback = cache ? await cache.match(url).catch(() => null) : null;\n\n if (fallback) {\n const current = useEidosStore.getState().resources[url];\n store.updateResource(url, {\n status: 'fresh',\n lastEvent: 'cache-hit',\n cacheHits: (current?.cacheHits ?? 0) + 1,\n });\n return fallback;\n }\n\n store.updateResource(url, { status: 'error' });\n throw err;\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Strategy derivation — intent → deterministic caching strategy\n// ─────────────────────────────────────────────────────────────────────────────\n\nfunction deriveStrategy(config: ResourceConfig): GeneratedStrategy {\n const explicit = config.strategy;\n if (config.offline) {\n return buildStrategy(explicit ?? 'stale-while-revalidate', config.cacheName, config.version);\n }\n return buildStrategy(explicit ?? 'network-first', config.cacheName, config.version);\n}\n\n// Strategy display names — always included (tiny, used by devtools).\nconst STRATEGY_NAMES: Record<CacheStrategy, string> = {\n 'stale-while-revalidate': 'StaleWhileRevalidate',\n 'cache-first': 'CacheFirst',\n 'network-first': 'NetworkFirst',\n};\n\n// Heavy descriptive strings — stripped from production bundles by Vite's\n// import.meta.env.DEV dead-code elimination. Only the names above ship in prod.\ntype StrategyDevInfo = Pick<GeneratedStrategy, 'reasoning' | 'behavior' | 'equivalentCode'>;\nconst _STRATEGY_DEV_META: Record<CacheStrategy, StrategyDevInfo> = {\n 'stale-while-revalidate': {\n reasoning:\n 'offline: true signals resilience. SWR returns cached data instantly while revalidating in the background — the best tradeoff between speed and freshness for offline-capable resources.',\n behavior: [\n 'Cache hit → return immediately, kick off background revalidation',\n 'Cache miss → fetch from network, cache the response, return it',\n 'Offline → return cached version if available, 503 if not',\n 'Reconnect → next request triggers a background refresh',\n ],\n equivalentCode: `// Workbox equivalent (maxEntries/maxAge are configured via ResourceConfig)\nnew StaleWhileRevalidate({\n cacheName: 'eidos-resources-v1',\n plugins: [new ExpirationPlugin({ maxEntries: config.maxEntries, maxAgeSeconds: config.maxAge && config.maxAge / 1000 })],\n})`,\n },\n 'cache-first': {\n reasoning:\n 'cache-first maximises speed and offline availability. Network is consulted only on cache miss. Best for static or infrequently-updated data.',\n behavior: [\n 'Cache hit → return immediately, no network request made',\n 'Cache miss → fetch from network, cache the response, return it',\n 'Offline → return cached version, 503 if cache is empty',\n 'Cache never expires unless maxAge is set or explicitly invalidated',\n ],\n equivalentCode: `// Workbox equivalent (maxEntries/maxAge are configured via ResourceConfig)\nnew CacheFirst({\n cacheName: 'eidos-resources-v1',\n plugins: [new ExpirationPlugin({ maxEntries: config.maxEntries, maxAgeSeconds: config.maxAge && config.maxAge / 1000 })],\n})`,\n },\n 'network-first': {\n reasoning:\n 'network-first prioritises fresh data. Cache acts as a safety net when offline. Best for frequently-updated resources where stale data causes problems.',\n behavior: [\n 'Always try network first',\n 'Network success → update cache, return fresh response',\n 'Network failure → fall back to cached version',\n 'Offline with empty cache → return 503 error response',\n ],\n equivalentCode: `// Workbox equivalent\nnew NetworkFirst({\n cacheName: 'eidos-resources-v1',\n networkTimeoutSeconds: 3,\n})`,\n },\n};\n\nfunction buildStrategy(\n swStrategy: CacheStrategy,\n cacheName?: string,\n version?: string | number,\n): GeneratedStrategy {\n const meta = _STRATEGY_DEV_META[swStrategy];\n const baseName = cacheName ?? 'eidos-resources-v1';\n return {\n name: STRATEGY_NAMES[swStrategy],\n swStrategy,\n cacheName: version !== undefined ? `${baseName}-v${version}` : baseName,\n // reasoning + behavior are rendered by the playground UI from live ResourceEntry objects —\n // keep them in all builds. equivalentCode is a static code block only used in DEV tools.\n reasoning: meta.reasoning,\n behavior: meta.behavior,\n equivalentCode: import.meta.env.DEV ? meta.equivalentCode : '',\n };\n}\n\n// ── warmCache ─────────────────────────────────────────────────────────────────\n\n/**\n * Bulk-prefetch an array of resource handles concurrently, warming the cache\n * for each one. Useful on login / app init when you know which resources the\n * user will need offline.\n *\n * Pattern handles (containing `*`, `**`, or `:param`) are silently skipped —\n * they match multiple URLs so there is no single URL to prefetch.\n *\n * @example\n * import { warmCache } from '@sweidos/eidos'\n *\n * // In EidosProvider's onReady, or after login:\n * const { warmed, failed } = await warmCache([products, userProfile, settings])\n */\nexport async function warmCache(handles: ResourceHandle[]): Promise<WarmCacheResult> {\n const results = await Promise.allSettled(handles.map((h) => h.prefetch()));\n const errors = results\n .filter((r): r is PromiseRejectedResult => r.status === 'rejected')\n .map((r) => r.reason);\n\n if (import.meta.env.DEV && errors.length > 0) {\n console.warn(`[eidos] warmCache: ${errors.length} handle(s) failed to prefetch`, errors);\n }\n\n return {\n warmed: results.filter((r) => r.status === 'fulfilled').length,\n failed: errors.length,\n errors,\n };\n}\n"],"mappings":"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"}
@@ -1,5 +1,4 @@
1
1
  import { AsyncStorageLike } from './index.ts';
2
-
3
2
  export interface EidosRNConfig {
4
3
  /** AsyncStorage singleton (or any AsyncStorageLike key-value store). */
5
4
  storage: AsyncStorageLike;
@@ -0,0 +1,39 @@
1
+ import { ReliabilityStats } from './types';
2
+ export interface EidosConfig {
3
+ /** Path to the eidos service worker. Defaults to '/eidos-sw.js'. */
4
+ swPath?: string;
5
+ /** Automatically replay the action queue on reconnect. Default: true. */
6
+ autoReplay?: boolean;
7
+ /**
8
+ * When `true` (default), the new service worker activates immediately when an
9
+ * update is available — matching the pre-v2.3 behaviour. Set to `false` to opt
10
+ * into a toast-then-reload pattern: `onUpdateAvailable` fires instead and you
11
+ * call `triggerSwUpdate()` when the user confirms the reload.
12
+ *
13
+ * Note: avoid calling `triggerSwUpdate()` while `neverLose` actions are mid-replay
14
+ * — the BroadcastChannel/Web-Locks replay coordination survives SW activation, but
15
+ * triggering an update during an active replay pass is unnecessary churn.
16
+ */
17
+ skipWaiting?: boolean;
18
+ /**
19
+ * Called when a new service worker version has installed and is waiting to
20
+ * activate. Use this to show a "reload to update" toast. Only fires when
21
+ * `skipWaiting: false`; with the default `skipWaiting: true` the update
22
+ * applies automatically and this callback is never needed.
23
+ *
24
+ * Call `triggerSwUpdate()` when the user confirms the reload — it tells the
25
+ * waiting SW to activate, then reload the page.
26
+ */
27
+ onUpdateAvailable?: (registration: ServiceWorkerRegistration) => void;
28
+ /**
29
+ * Opt-in reliability telemetry. Called with a snapshot of cumulative
30
+ * `neverLose` queue outcome counters (`ReliabilityStats`) every
31
+ * `reliabilityReportInterval` ms — wire this up to your analytics backend.
32
+ * Not called if omitted.
33
+ */
34
+ onReliabilityReport?: (stats: ReliabilityStats) => void;
35
+ /** Interval (ms) between `onReliabilityReport` calls. Default: 60000. */
36
+ reliabilityReportInterval?: number;
37
+ }
38
+ export declare function initEidos(config?: EidosConfig): Promise<void>;
39
+ export declare function _resetEidos(): void;
package/dist/runtime.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import { useEidosStore as n } from "./store.js";
2
- import { registerBgSyncHandler as c, registerServiceWorker as y } from "./sw-bridge.js";
3
- import { idbGetQueue as m, idbQueueStorage as p } from "./idb.js";
4
- import { _getQueueStorage as d } from "./queue-storage.js";
2
+ import { registerBgSyncHandler as p, registerServiceWorker as y } from "./sw-bridge.js";
3
+ import { idbGetQueue as m, idbQueueStorage as d } from "./idb.js";
4
+ import { _getQueueStorage as b } from "./queue-storage.js";
5
5
  import { subscribeQueueSync as f } from "./queue-sync.js";
6
- import { replayQueue as b } from "./action.js";
7
- import { subscribeReplayOnReconnect as h } from "./replay.js";
6
+ import { replayQueue as h } from "./action.js";
7
+ import { subscribeReplayOnReconnect as v } from "./replay.js";
8
8
  async function R(e) {
9
9
  if (e.schemaVersion === 2 && e.idempotencyKey) return e;
10
10
  const t = {
@@ -12,44 +12,47 @@ async function R(e) {
12
12
  schemaVersion: 2,
13
13
  idempotencyKey: e.idempotencyKey ?? crypto.randomUUID()
14
14
  };
15
- return await (d() ?? p).update(t.id, {
15
+ return await (b() ?? d).update(t.id, {
16
16
  schemaVersion: t.schemaVersion,
17
17
  idempotencyKey: t.idempotencyKey
18
18
  }).catch(() => {
19
19
  }), t;
20
20
  }
21
- var o = !1, s = null, u = null, i = null;
22
- async function K(e = {}) {
21
+ var o = !1, s = null, u = null, r = null;
22
+ async function U(e = {}) {
23
23
  if (typeof window > "u" || o) return;
24
24
  o = !0;
25
- const t = e.swPath ?? "/eidos-sw.js", l = e.autoReplay ?? !0;
25
+ const t = e.swPath ?? "/eidos-sw.js", l = e.autoReplay ?? !0, c = e.skipWaiting ?? !0;
26
26
  try {
27
- const r = await m();
28
- if (r.length > 0) {
29
- const a = await Promise.all(r.map(R));
27
+ const i = await m();
28
+ if (i.length > 0) {
29
+ const a = await Promise.all(i.map(R));
30
30
  n.getState().hydrateQueue(a);
31
31
  }
32
32
  } catch {
33
33
  }
34
34
  try {
35
- await y(t);
35
+ await y(t, {
36
+ skipWaiting: c,
37
+ onUpdateAvailable: e.onUpdateAvailable
38
+ });
36
39
  } catch {
37
40
  }
38
- if (c(() => {
39
- n.getState().isOnline && setTimeout(b, 200);
40
- }), l && (s = h()), u = f(), e.onReliabilityReport) {
41
- const r = e.reliabilityReportInterval ?? 6e4, a = e.onReliabilityReport;
42
- i = setInterval(() => {
41
+ if (p(() => {
42
+ n.getState().isOnline && setTimeout(h, 200);
43
+ }), l && (s = v()), u = f(), e.onReliabilityReport) {
44
+ const i = e.reliabilityReportInterval ?? 6e4, a = e.onReliabilityReport;
45
+ r = setInterval(() => {
43
46
  a(n.getState().reliability);
44
- }, r);
47
+ }, i);
45
48
  }
46
49
  }
47
50
  function V() {
48
- s?.(), s = null, u?.(), u = null, i && clearInterval(i), i = null, o = !1;
51
+ s?.(), s = null, u?.(), u = null, r && clearInterval(r), r = null, o = !1;
49
52
  }
50
53
  export {
51
54
  V as _resetEidos,
52
- K as initEidos
55
+ U as initEidos
53
56
  };
54
57
 
55
58
  //# sourceMappingURL=runtime.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"runtime.js","names":[],"sources":["../src/runtime.ts"],"sourcesContent":["import { registerServiceWorker, registerBgSyncHandler } from './sw-bridge';\nimport { replayQueue } from './action';\nimport { useEidosStore } from './store';\nimport { idbGetQueue, idbQueueStorage } from './idb';\nimport { _getQueueStorage } from './queue-storage';\nimport { subscribeReplayOnReconnect } from './replay';\nimport { subscribeQueueSync } from './queue-sync';\nimport { CURRENT_QUEUE_SCHEMA_VERSION } from './types';\nimport type { ActionQueueItem, ReliabilityStats } from './types';\n\n// Items persisted before idempotencyKey/schemaVersion existed (v1) are migrated\n// in place: assign a fresh idempotencyKey and bump schemaVersion. A fresh key on\n// first replay after upgrade is correct — these items were never sent with one.\nasync function migrateQueueItem(item: ActionQueueItem): Promise<ActionQueueItem> {\n if (item.schemaVersion === CURRENT_QUEUE_SCHEMA_VERSION && item.idempotencyKey) {\n return item;\n }\n const migrated: ActionQueueItem = {\n ...item,\n schemaVersion: CURRENT_QUEUE_SCHEMA_VERSION,\n idempotencyKey: item.idempotencyKey ?? crypto.randomUUID(),\n };\n const storage = _getQueueStorage() ?? idbQueueStorage;\n await storage\n .update(migrated.id, {\n schemaVersion: migrated.schemaVersion,\n idempotencyKey: migrated.idempotencyKey,\n })\n .catch(() => {\n // Best-effort persist — item still gets the migrated shape in memory this session\n });\n return migrated;\n}\n\nexport interface EidosConfig {\n /** Path to the eidos service worker. Defaults to '/eidos-sw.js'. */\n swPath?: string;\n /** Automatically replay the action queue on reconnect. Default: true. */\n autoReplay?: boolean;\n /**\n * Opt-in reliability telemetry. Called with a snapshot of cumulative\n * `neverLose` queue outcome counters (`ReliabilityStats`) every\n * `reliabilityReportInterval` ms — wire this up to your analytics backend.\n * Not called if omitted.\n */\n onReliabilityReport?: (stats: ReliabilityStats) => void;\n /** Interval (ms) between `onReliabilityReport` calls. Default: 60000. */\n reliabilityReportInterval?: number;\n}\n\nlet _initialized = false;\nlet _unsubscribe: (() => void) | null = null;\nlet _unsubscribeQueueSync: (() => void) | null = null;\nlet _reliabilityReportTimer: ReturnType<typeof setInterval> | null = null;\n\nexport async function initEidos(config: EidosConfig = {}): Promise<void> {\n // Skip silently during SSR — SW, IndexedDB, and window are browser-only.\n if (typeof window === 'undefined') return;\n if (_initialized) return;\n _initialized = true;\n\n const swPath = config.swPath ?? '/eidos-sw.js';\n const autoReplay = config.autoReplay ?? true;\n\n // Restore persisted queue from IndexedDB on startup\n try {\n const persisted = await idbGetQueue();\n if (persisted.length > 0) {\n const migrated = await Promise.all(persisted.map(migrateQueueItem));\n useEidosStore.getState().hydrateQueue(migrated);\n }\n } catch {\n // IndexedDB unavailable (Firefox private browsing) — silent fallback\n }\n\n try {\n await registerServiceWorker(swPath);\n } catch {\n // SW registration failed; app continues without offline support\n }\n\n // When the SW fires the Background Sync tag, replay the queue in the main thread.\n // This path runs even if the user briefly navigated away and back — the browser\n // triggers the sync event on the SW, which wakes up all open clients.\n registerBgSyncHandler(() => {\n if (useEidosStore.getState().isOnline) {\n setTimeout(replayQueue, 200);\n }\n });\n\n if (autoReplay) {\n _unsubscribe = subscribeReplayOnReconnect();\n }\n\n // Apply queue-item status changes broadcast by the replay-lock holder so\n // non-leader tabs reflect live status without waiting for re-hydration.\n _unsubscribeQueueSync = subscribeQueueSync();\n\n if (config.onReliabilityReport) {\n const interval = config.reliabilityReportInterval ?? 60_000;\n const report = config.onReliabilityReport;\n _reliabilityReportTimer = setInterval(() => {\n report(useEidosStore.getState().reliability);\n }, interval);\n }\n\n if (import.meta.env.DEV) {\n const store = useEidosStore.getState();\n console.groupCollapsed('%c⚡ Eidos', 'color:#22C55E;font-weight:bold');\n console.log('SW path :', swPath);\n console.log('Auto-replay:', autoReplay);\n console.log('SW status :', store.swStatus);\n console.groupEnd();\n }\n}\n\nexport function _resetEidos() {\n _unsubscribe?.();\n _unsubscribe = null;\n _unsubscribeQueueSync?.();\n _unsubscribeQueueSync = null;\n if (_reliabilityReportTimer) clearInterval(_reliabilityReportTimer);\n _reliabilityReportTimer = null;\n _initialized = false;\n}\n"],"mappings":";;;;;;;AAaA,eAAe,EAAiB,GAAiD;AAC/E,MAAI,EAAK,kBAAA,KAAkD,EAAK,eAC9D,QAAO;AAET,QAAM,IAA4B;AAAA,IAChC,GAAG;AAAA,IACH,eAAA;AAAA,IACA,gBAAgB,EAAK,kBAAkB,OAAO,WAAW;AAAA,EAC3D;AAEA,gBADgB,EAAiB,KAAK,GAEnC,OAAO,EAAS,IAAI;AAAA,IACnB,eAAe,EAAS;AAAA,IACxB,gBAAgB,EAAS;AAAA,EAC3B,CAAC,EACA,MAAA,MAAY;AAAA,EAEb,CAAC,GACI;AACT;AAkBA,IAAI,IAAe,IACf,IAAoC,MACpC,IAA6C,MAC7C,IAAiE;AAErE,eAAsB,EAAU,IAAsB,CAAC,GAAkB;AAGvE,MADI,OAAO,SAAW,OAClB,EAAc;AAClB,EAAA,IAAe;AAEf,QAAM,IAAS,EAAO,UAAU,gBAC1B,IAAa,EAAO,cAAc;AAGxC,MAAI;AACF,UAAM,IAAY,MAAM,EAAY;AACpC,QAAI,EAAU,SAAS,GAAG;AACxB,YAAM,IAAW,MAAM,QAAQ,IAAI,EAAU,IAAI,CAAgB,CAAC;AAClE,MAAA,EAAc,SAAS,EAAE,aAAa,CAAQ;AAAA,IAChD;AAAA,EACF,QAAQ;AAAA,EAER;AAEA,MAAI;AACF,UAAM,EAAsB,CAAM;AAAA,EACpC,QAAQ;AAAA,EAER;AAmBA,MAdA,EAAA,MAA4B;AAC1B,IAAI,EAAc,SAAS,EAAE,YAC3B,WAAW,GAAa,GAAG;AAAA,EAE/B,CAAC,GAEG,MACF,IAAe,EAA2B,IAK5C,IAAwB,EAAmB,GAEvC,EAAO,qBAAqB;AAC9B,UAAM,IAAW,EAAO,6BAA6B,KAC/C,IAAS,EAAO;AACtB,IAAA,IAA0B,YAAA,MAAkB;AAC1C,MAAA,EAAO,EAAc,SAAS,EAAE,WAAW;AAAA,IAC7C,GAAG,CAAQ;AAAA,EACb;AAUF;AAEA,SAAgB,IAAc;AAC5B,EAAA,IAAe,GACf,IAAe,MACf,IAAwB,GACxB,IAAwB,MACpB,KAAyB,cAAc,CAAuB,GAClE,IAA0B,MAC1B,IAAe;AACjB"}
1
+ {"version":3,"file":"runtime.js","names":[],"sources":["../src/runtime.ts"],"sourcesContent":["import { registerServiceWorker, registerBgSyncHandler } from './sw-bridge';\nimport { replayQueue } from './action';\nimport { useEidosStore } from './store';\nimport { idbGetQueue, idbQueueStorage } from './idb';\nimport { _getQueueStorage } from './queue-storage';\nimport { subscribeReplayOnReconnect } from './replay';\nimport { subscribeQueueSync } from './queue-sync';\nimport { CURRENT_QUEUE_SCHEMA_VERSION } from './types';\nimport type { ActionQueueItem, ReliabilityStats } from './types';\n\n// Items persisted before idempotencyKey/schemaVersion existed (v1) are migrated\n// in place: assign a fresh idempotencyKey and bump schemaVersion. A fresh key on\n// first replay after upgrade is correct — these items were never sent with one.\nasync function migrateQueueItem(item: ActionQueueItem): Promise<ActionQueueItem> {\n if (item.schemaVersion === CURRENT_QUEUE_SCHEMA_VERSION && item.idempotencyKey) {\n return item;\n }\n const migrated: ActionQueueItem = {\n ...item,\n schemaVersion: CURRENT_QUEUE_SCHEMA_VERSION,\n idempotencyKey: item.idempotencyKey ?? crypto.randomUUID(),\n };\n const storage = _getQueueStorage() ?? idbQueueStorage;\n await storage\n .update(migrated.id, {\n schemaVersion: migrated.schemaVersion,\n idempotencyKey: migrated.idempotencyKey,\n })\n .catch(() => {\n // Best-effort persist — item still gets the migrated shape in memory this session\n });\n return migrated;\n}\n\nexport interface EidosConfig {\n /** Path to the eidos service worker. Defaults to '/eidos-sw.js'. */\n swPath?: string;\n /** Automatically replay the action queue on reconnect. Default: true. */\n autoReplay?: boolean;\n /**\n * When `true` (default), the new service worker activates immediately when an\n * update is available — matching the pre-v2.3 behaviour. Set to `false` to opt\n * into a toast-then-reload pattern: `onUpdateAvailable` fires instead and you\n * call `triggerSwUpdate()` when the user confirms the reload.\n *\n * Note: avoid calling `triggerSwUpdate()` while `neverLose` actions are mid-replay\n * — the BroadcastChannel/Web-Locks replay coordination survives SW activation, but\n * triggering an update during an active replay pass is unnecessary churn.\n */\n skipWaiting?: boolean;\n /**\n * Called when a new service worker version has installed and is waiting to\n * activate. Use this to show a \"reload to update\" toast. Only fires when\n * `skipWaiting: false`; with the default `skipWaiting: true` the update\n * applies automatically and this callback is never needed.\n *\n * Call `triggerSwUpdate()` when the user confirms the reload — it tells the\n * waiting SW to activate, then reload the page.\n */\n onUpdateAvailable?: (registration: ServiceWorkerRegistration) => void;\n /**\n * Opt-in reliability telemetry. Called with a snapshot of cumulative\n * `neverLose` queue outcome counters (`ReliabilityStats`) every\n * `reliabilityReportInterval` ms — wire this up to your analytics backend.\n * Not called if omitted.\n */\n onReliabilityReport?: (stats: ReliabilityStats) => void;\n /** Interval (ms) between `onReliabilityReport` calls. Default: 60000. */\n reliabilityReportInterval?: number;\n}\n\nlet _initialized = false;\nlet _unsubscribe: (() => void) | null = null;\nlet _unsubscribeQueueSync: (() => void) | null = null;\nlet _reliabilityReportTimer: ReturnType<typeof setInterval> | null = null;\n\nexport async function initEidos(config: EidosConfig = {}): Promise<void> {\n // Skip silently during SSR — SW, IndexedDB, and window are browser-only.\n if (typeof window === 'undefined') return;\n if (_initialized) return;\n _initialized = true;\n\n const swPath = config.swPath ?? '/eidos-sw.js';\n const autoReplay = config.autoReplay ?? true;\n const skipWaiting = config.skipWaiting ?? true;\n\n // Restore persisted queue from IndexedDB on startup\n try {\n const persisted = await idbGetQueue();\n if (persisted.length > 0) {\n const migrated = await Promise.all(persisted.map(migrateQueueItem));\n useEidosStore.getState().hydrateQueue(migrated);\n }\n } catch {\n // IndexedDB unavailable (Firefox private browsing) — silent fallback\n }\n\n try {\n await registerServiceWorker(swPath, {\n skipWaiting,\n onUpdateAvailable: config.onUpdateAvailable,\n });\n } catch {\n // SW registration failed; app continues without offline support\n }\n\n // When the SW fires the Background Sync tag, replay the queue in the main thread.\n // This path runs even if the user briefly navigated away and back — the browser\n // triggers the sync event on the SW, which wakes up all open clients.\n registerBgSyncHandler(() => {\n if (useEidosStore.getState().isOnline) {\n setTimeout(replayQueue, 200);\n }\n });\n\n if (autoReplay) {\n _unsubscribe = subscribeReplayOnReconnect();\n }\n\n // Apply queue-item status changes broadcast by the replay-lock holder so\n // non-leader tabs reflect live status without waiting for re-hydration.\n _unsubscribeQueueSync = subscribeQueueSync();\n\n if (config.onReliabilityReport) {\n const interval = config.reliabilityReportInterval ?? 60_000;\n const report = config.onReliabilityReport;\n _reliabilityReportTimer = setInterval(() => {\n report(useEidosStore.getState().reliability);\n }, interval);\n }\n\n if (import.meta.env.DEV) {\n const store = useEidosStore.getState();\n console.groupCollapsed('%c⚡ Eidos', 'color:#22C55E;font-weight:bold');\n console.log('SW path :', swPath);\n console.log('Auto-replay:', autoReplay);\n console.log('SW status :', store.swStatus);\n console.groupEnd();\n }\n}\n\nexport function _resetEidos() {\n _unsubscribe?.();\n _unsubscribe = null;\n _unsubscribeQueueSync?.();\n _unsubscribeQueueSync = null;\n if (_reliabilityReportTimer) clearInterval(_reliabilityReportTimer);\n _reliabilityReportTimer = null;\n _initialized = false;\n}\n"],"mappings":";;;;;;;AAaA,eAAe,EAAiB,GAAiD;AAC/E,MAAI,EAAK,kBAAA,KAAkD,EAAK,eAC9D,QAAO;AAET,QAAM,IAA4B;AAAA,IAChC,GAAG;AAAA,IACH,eAAA;AAAA,IACA,gBAAgB,EAAK,kBAAkB,OAAO,WAAW;AAAA,EAC3D;AAEA,gBADgB,EAAiB,KAAK,GAEnC,OAAO,EAAS,IAAI;AAAA,IACnB,eAAe,EAAS;AAAA,IACxB,gBAAgB,EAAS;AAAA,EAC3B,CAAC,EACA,MAAA,MAAY;AAAA,EAEb,CAAC,GACI;AACT;AAuCA,IAAI,IAAe,IACf,IAAoC,MACpC,IAA6C,MAC7C,IAAiE;AAErE,eAAsB,EAAU,IAAsB,CAAC,GAAkB;AAGvE,MADI,OAAO,SAAW,OAClB,EAAc;AAClB,EAAA,IAAe;AAEf,QAAM,IAAS,EAAO,UAAU,gBAC1B,IAAa,EAAO,cAAc,IAClC,IAAc,EAAO,eAAe;AAG1C,MAAI;AACF,UAAM,IAAY,MAAM,EAAY;AACpC,QAAI,EAAU,SAAS,GAAG;AACxB,YAAM,IAAW,MAAM,QAAQ,IAAI,EAAU,IAAI,CAAgB,CAAC;AAClE,MAAA,EAAc,SAAS,EAAE,aAAa,CAAQ;AAAA,IAChD;AAAA,EACF,QAAQ;AAAA,EAER;AAEA,MAAI;AACF,UAAM,EAAsB,GAAQ;AAAA,MAClC,aAAA;AAAA,MACA,mBAAmB,EAAO;AAAA,IAC5B,CAAC;AAAA,EACH,QAAQ;AAAA,EAER;AAmBA,MAdA,EAAA,MAA4B;AAC1B,IAAI,EAAc,SAAS,EAAE,YAC3B,WAAW,GAAa,GAAG;AAAA,EAE/B,CAAC,GAEG,MACF,IAAe,EAA2B,IAK5C,IAAwB,EAAmB,GAEvC,EAAO,qBAAqB;AAC9B,UAAM,IAAW,EAAO,6BAA6B,KAC/C,IAAS,EAAO;AACtB,IAAA,IAA0B,YAAA,MAAkB;AAC1C,MAAA,EAAO,EAAc,SAAS,EAAE,WAAW;AAAA,IAC7C,GAAG,CAAQ;AAAA,EACb;AAUF;AAEA,SAAgB,IAAc;AAC5B,EAAA,IAAe,GACf,IAAe,MACf,IAAwB,GACxB,IAAwB,MACpB,KAAyB,cAAc,CAAuB,GAClE,IAA0B,MAC1B,IAAe;AACjB"}
@@ -0,0 +1,26 @@
1
+ import { ResourceEntry, ActionQueueItem, ReliabilityStats } from './types';
2
+ import { EidosStore } from './store';
3
+ type Setter = (updater: (prev: EidosStore) => Partial<EidosStore>) => void;
4
+ export interface ResourceActions {
5
+ registerResource: (url: string, entry: ResourceEntry) => void;
6
+ updateResource: (url: string, update: Partial<ResourceEntry>) => void;
7
+ unregisterResource: (url: string) => void;
8
+ }
9
+ export declare function createResourceActions(set: Setter): ResourceActions;
10
+ export interface QueueActions {
11
+ addQueueItem: (item: ActionQueueItem) => void;
12
+ updateQueueItem: (id: string, update: Partial<ActionQueueItem>) => void;
13
+ batchUpdateQueueItems: (updates: Array<{
14
+ id: string;
15
+ update: Partial<ActionQueueItem>;
16
+ }>) => void;
17
+ removeQueueItem: (id: string) => void;
18
+ hydrateQueue: (items: ActionQueueItem[]) => void;
19
+ }
20
+ export declare function createQueueActions(set: Setter): QueueActions;
21
+ export interface ReliabilityActions {
22
+ recordReliabilityEvent: (event: keyof ReliabilityStats) => void;
23
+ resetReliabilityStats: () => void;
24
+ }
25
+ export declare function createReliabilityActions(set: Setter): ReliabilityActions;
26
+ export {};
@@ -0,0 +1,15 @@
1
+ import { EidosState } from './types';
2
+ import { ResourceActions, QueueActions, ReliabilityActions } from './store-slices';
3
+ export interface EidosStore extends EidosState, ResourceActions, QueueActions, ReliabilityActions {
4
+ setOnline: (online: boolean) => void;
5
+ setSwStatus: (status: EidosState['swStatus'], error?: string) => void;
6
+ }
7
+ type Listener = () => void;
8
+ declare function _getState(): EidosStore;
9
+ declare function _subscribe(listener: Listener): () => void;
10
+ export declare const useEidosStore: {
11
+ getState: typeof _getState;
12
+ subscribe: typeof _subscribe;
13
+ setState: (partial: Partial<EidosStore> | ((s: EidosStore) => Partial<EidosStore>)) => void;
14
+ };
15
+ export {};
@@ -0,0 +1,64 @@
1
+ import { EidosStore } from './store';
2
+ import { ActionQueueItem, ResourceEntry, ReliabilityStats } from './types';
3
+ export interface EidosReadable<T> {
4
+ /** Subscribe to value changes. Returns an unsubscribe function. */
5
+ subscribe(run: (value: T) => void): () => void;
6
+ /** Read the current value synchronously without subscribing. */
7
+ getState(): T;
8
+ }
9
+ /** Full Eidos state snapshot. Prefer the narrower stores below. */
10
+ export declare const eidosStore: EidosReadable<EidosStore>;
11
+ /** The action queue. Re-notifies on every queue mutation. */
12
+ export declare const eidosQueue: EidosReadable<ActionQueueItem[]>;
13
+ /**
14
+ * Online status + SW lifecycle.
15
+ * Only re-emits when isOnline, swStatus, or swError actually changes.
16
+ */
17
+ export declare const eidosStatus: EidosReadable<{
18
+ isOnline: boolean;
19
+ swStatus: EidosStore['swStatus'];
20
+ swError: string | undefined;
21
+ }>;
22
+ /**
23
+ * Queue counts. Re-emits only when a count actually changes, not on every
24
+ * queue mutation (e.g. a status transition that doesn't change counts is skipped).
25
+ */
26
+ export declare const eidosQueueStats: EidosReadable<{
27
+ pending: number;
28
+ failed: number;
29
+ replaying: number;
30
+ total: number;
31
+ }>;
32
+ /**
33
+ * Cumulative, session-scoped `neverLose` queue outcome counters — opt-in
34
+ * reliability telemetry. Re-emits only when a counter changes. See
35
+ * `EidosConfig.onReliabilityReport` to forward these to an analytics backend.
36
+ */
37
+ export declare const eidosReliabilityStats: EidosReadable<ReliabilityStats>;
38
+ /**
39
+ * Live cache state for a single registered resource URL.
40
+ * @example
41
+ * // Svelte
42
+ * const entry = eidosResource('/api/products')
43
+ * $: hits = $entry?.cacheHits ?? 0
44
+ */
45
+ export declare function eidosResource(url: string): EidosReadable<ResourceEntry | undefined>;
46
+ /**
47
+ * Live state for a single queue item by ID. Returns `undefined` once the item
48
+ * is removed from the queue (after a successful replay or `clearQueue()`).
49
+ * @example
50
+ * // Svelte
51
+ * const item = eidosAction(queuedResult.id)
52
+ * $: status = $item?.status // 'pending' | 'replaying' | 'succeeded' | 'failed' | undefined
53
+ */
54
+ export declare function eidosAction(id: string): EidosReadable<ActionQueueItem | undefined>;
55
+ /**
56
+ * Calls `callback` once each time the action queue drains from non-empty → 0.
57
+ * Framework-agnostic equivalent of `useEidosOnDrain` for Svelte/Vue/vanilla.
58
+ * Returns an unsubscribe function.
59
+ *
60
+ * @example
61
+ * // Svelte
62
+ * onMount(() => onQueueDrain(() => toast.success('All offline actions synced!')))
63
+ */
64
+ export declare function onQueueDrain(callback: () => void): () => void;
@@ -1,5 +1,4 @@
1
1
  import { EidosConfig } from './index.ts';
2
-
3
2
  /**
4
3
  * Returns an `onMount`-compatible callback that initialises the Eidos runtime
5
4
  * on the client only. Prevents SSR crashes caused by accessing `indexedDB` or
@@ -0,0 +1,24 @@
1
+ export declare function getSwRegistration(): ServiceWorkerRegistration | null;
2
+ interface SwRegistrationOptions {
3
+ skipWaiting: boolean;
4
+ onUpdateAvailable?: (registration: ServiceWorkerRegistration) => void;
5
+ }
6
+ export declare function registerServiceWorker(swPath: string, options?: SwRegistrationOptions): Promise<void>;
7
+ export declare function sendToWorker(message: Record<string, unknown>): void;
8
+ export declare function registerBgSyncHandler(fn: () => void): void;
9
+ export declare function isBgSyncSupported(): boolean;
10
+ interface PushHandlers {
11
+ onNotificationClick?: (data: unknown) => void;
12
+ onSubscriptionExpired?: (sub: PushSubscriptionJSON) => void;
13
+ }
14
+ export declare function registerPushCallbacks(handlers: PushHandlers): void;
15
+ export declare function setOfflineSimulation(enabled: boolean): void;
16
+ /**
17
+ * Tells the waiting service worker to activate immediately, then reloads the page.
18
+ * Only relevant when `skipWaiting: false` — call this after the user confirms
19
+ * a "reload to update" toast shown via `onUpdateAvailable`.
20
+ */
21
+ export declare function triggerSwUpdate(): void;
22
+ /** Test-only: resets module-level state between test cases. */
23
+ export declare function _resetSwBridgeForTests(): void;
24
+ export {};
package/dist/sw-bridge.js CHANGED
@@ -1,78 +1,78 @@
1
1
  import { useEidosStore as a } from "./store.js";
2
- var s = null, o = [];
2
+ var i = null, o = [];
3
3
  function E() {
4
- return s;
4
+ return i;
5
5
  }
6
- async function p(e) {
6
+ async function w(t, e = { skipWaiting: !0 }) {
7
7
  if (typeof navigator > "u" || !("serviceWorker" in navigator)) {
8
8
  a.getState().setSwStatus("unsupported");
9
9
  return;
10
10
  }
11
- const t = a.getState();
12
- t.setSwStatus("registering");
11
+ const n = a.getState();
12
+ n.setSwStatus("registering");
13
13
  try {
14
- s = await navigator.serviceWorker.register(e, { scope: "/" }), await d(s), t.setSwStatus("active"), navigator.serviceWorker.addEventListener("message", g), window.addEventListener("online", () => t.setOnline(!0)), window.addEventListener("offline", () => t.setOnline(!1)), l();
15
- } catch (n) {
16
- t.setSwStatus("error", String(n));
14
+ i = await navigator.serviceWorker.register(t, { scope: "/" }), await d(i), n.setSwStatus("active"), navigator.serviceWorker.addEventListener("message", S), window.addEventListener("online", () => n.setOnline(!0)), window.addEventListener("offline", () => n.setOnline(!1)), l(), v(i, e);
15
+ } catch (r) {
16
+ n.setSwStatus("error", String(r));
17
17
  }
18
18
  }
19
- function d(e) {
20
- return new Promise((t) => {
21
- if (e.active) {
22
- t();
19
+ function d(t) {
20
+ return new Promise((e) => {
21
+ if (t.active) {
22
+ e();
23
23
  return;
24
24
  }
25
- const n = e.installing ?? e.waiting;
25
+ const n = t.installing ?? t.waiting;
26
26
  if (!n) {
27
- t();
27
+ e();
28
28
  return;
29
29
  }
30
- const i = setTimeout(t, 1e4);
31
- n.addEventListener("statechange", function r() {
32
- n.state === "activated" && (clearTimeout(i), n.removeEventListener("statechange", r), t());
30
+ const r = setTimeout(e, 1e4);
31
+ n.addEventListener("statechange", function s() {
32
+ n.state === "activated" && (clearTimeout(r), n.removeEventListener("statechange", s), e());
33
33
  });
34
34
  });
35
35
  }
36
- function S(e) {
37
- const t = s?.active;
38
- t ? t.postMessage(e) : o.push(e);
36
+ function g(t) {
37
+ const e = i?.active;
38
+ e ? e.postMessage(t) : o.push(t);
39
39
  }
40
40
  var u = null;
41
- function w(e) {
42
- u = e;
41
+ function I(t) {
42
+ u = t;
43
43
  }
44
- function h() {
44
+ function _() {
45
45
  try {
46
- return typeof navigator < "u" && "serviceWorker" in navigator && s !== null && "sync" in s;
46
+ return typeof navigator < "u" && "serviceWorker" in navigator && i !== null && "sync" in i;
47
47
  } catch {
48
48
  return !1;
49
49
  }
50
50
  }
51
51
  var c = {};
52
- function O(e) {
53
- c = e;
52
+ function h(t) {
53
+ c = t;
54
54
  }
55
- function g(e) {
56
- const t = e.data;
57
- if (!t?.type) return;
58
- const n = a.getState(), { type: i, url: r } = t;
59
- if (i === "EIDOS_BACKGROUND_SYNC") {
55
+ function S(t) {
56
+ const e = t.data;
57
+ if (!e?.type) return;
58
+ const n = a.getState(), { type: r, url: s } = e;
59
+ if (r === "EIDOS_BACKGROUND_SYNC") {
60
60
  u?.();
61
61
  return;
62
62
  }
63
- if (i === "EIDOS_NOTIFICATION_CLICK") {
64
- c.onNotificationClick?.(t.data);
63
+ if (r === "EIDOS_NOTIFICATION_CLICK") {
64
+ c.onNotificationClick?.(e.data);
65
65
  return;
66
66
  }
67
- if (i === "EIDOS_SUBSCRIPTION_EXPIRED") {
68
- c.onSubscriptionExpired?.(t.subscription);
67
+ if (r === "EIDOS_SUBSCRIPTION_EXPIRED") {
68
+ c.onSubscriptionExpired?.(e.subscription);
69
69
  return;
70
70
  }
71
- if (r)
72
- switch (i) {
71
+ if (s)
72
+ switch (r) {
73
73
  case "EIDOS_CACHE_HIT": {
74
- const f = n.resources[r];
75
- n.updateResource(r, {
74
+ const f = n.resources[s];
75
+ n.updateResource(s, {
76
76
  status: "fresh",
77
77
  lastEvent: "cache-hit",
78
78
  cacheHits: (f?.cacheHits ?? 0) + 1
@@ -80,41 +80,56 @@ function g(e) {
80
80
  break;
81
81
  }
82
82
  case "EIDOS_CACHE_UPDATED":
83
- n.updateResource(r, {
83
+ n.updateResource(s, {
84
84
  status: "fresh",
85
85
  lastEvent: "cache-updated",
86
86
  cachedAt: Date.now()
87
87
  });
88
88
  break;
89
89
  case "EIDOS_NETWORK_ERROR":
90
- n.updateResource(r, {
90
+ n.updateResource(s, {
91
91
  status: "error",
92
92
  lastEvent: "network-error"
93
93
  });
94
94
  break;
95
95
  }
96
96
  }
97
- function _(e) {
98
- S({
97
+ function O(t) {
98
+ g({
99
99
  type: "EIDOS_SIMULATE_OFFLINE",
100
- enabled: e
101
- }), a.getState().setOnline(!e);
100
+ enabled: t
101
+ }), a.getState().setOnline(!t);
102
102
  }
103
103
  function l() {
104
- const e = s?.active;
105
- if (e) {
106
- for (const t of o) e.postMessage(t);
104
+ const t = i?.active;
105
+ if (t) {
106
+ for (const e of o) t.postMessage(e);
107
107
  o = [];
108
108
  }
109
109
  }
110
+ function v(t, e) {
111
+ const n = (r) => {
112
+ e.skipWaiting ? r.waiting?.postMessage({ type: "EIDOS_SKIP_WAITING" }) : e.onUpdateAvailable?.(r);
113
+ };
114
+ t.waiting && navigator.serviceWorker.controller && n(t), t.addEventListener("updatefound", () => {
115
+ const r = t.installing;
116
+ r && r.addEventListener("statechange", () => {
117
+ r.state === "installed" && navigator.serviceWorker.controller && n(t);
118
+ });
119
+ });
120
+ }
121
+ function k() {
122
+ i?.waiting?.postMessage({ type: "EIDOS_SKIP_WAITING" });
123
+ }
110
124
  export {
111
125
  E as getSwRegistration,
112
- h as isBgSyncSupported,
113
- w as registerBgSyncHandler,
114
- O as registerPushCallbacks,
115
- p as registerServiceWorker,
116
- S as sendToWorker,
117
- _ as setOfflineSimulation
126
+ _ as isBgSyncSupported,
127
+ I as registerBgSyncHandler,
128
+ h as registerPushCallbacks,
129
+ w as registerServiceWorker,
130
+ g as sendToWorker,
131
+ O as setOfflineSimulation,
132
+ k as triggerSwUpdate
118
133
  };
119
134
 
120
135
  //# sourceMappingURL=sw-bridge.js.map