{"version":3,"file":"BVjhYq9y.mjs","sources":["../../../../../../src/lib/pwa/hard-reset.ts"],"sourcesContent":["/**\n * v0.9.64.1 — Hard-reset utility for the settings page.\n *\n * Operator directive 2026-07-21 ~07:50 ET: \"in settings I want a button\n * to force clean sw, data and cache then force update and load newest\n * into working memory.\" This module is the \"force clean + force\n * update\" half of that. The settings page wires the UI; this file\n * is the side-effect-free state machine the UI calls.\n *\n * v0.9.86 — post-nuke flag + same-origin cookie clear. Operator\n * directive 2026-07-23 08:53 ET: \"nuke doesn't work as well as\n * clear cookies & site data, sw & offline don't reload.\" Two\n * changes:\n *\n *   1. `lceui:post-nuke` flag is set in localStorage BEFORE the\n *      reload. The layout's `onMount` reads it on the next paint\n *      and fires `PREPARE_STATIC_OFFLINE` against the new SW\n *      (re-precache) and the session-driven bootstrap (re-fill\n *      IDB) — making the post-nuke experience match the manual\n *      \"clear cookies & site data\" flow. The flag is cleared on\n *      consumption so a subsequent reload (without a nuke) does\n *      NOT re-prep.\n *\n *   2. The `connect.sid` session cookie is cleared on the SAME\n *      origin (best-effort). The pre-v0.9.86 hard-reset\n *      intentionally did NOT touch cookies — the comment said\n *      \"if the operator wants a full sign-out too, they should\n *      log out first.\" The operator's 2026-07-23 feedback\n *      reverses that: the nuke should match the manual flow,\n *      which kills the session cookie. The API lives on a\n *      different subdomain (`api.lceui.elevatornode.com`) so\n *      `document.cookie` cannot reach it; if a same-origin\n *      cookie IS present (a future deploy could share the\n *      origin) we clear it; otherwise the no-op is silent.\n *\n * ## What it nukes (in order)\n *\n *   1. All registered service workers (unregister + wait for\n *      `unregister` to settle so the new tab has a clean slate).\n *   2. Every CacheStorage bucket the browser exposes\n *      (`caches.keys()` → `caches.delete` for each).\n *   3. `localStorage` and `sessionStorage` (the whole origin's\n *      keys, including the theme + lastKnownUser caches).\n *   4. The Dexie/IndexedDB databases the app uses. We delete\n *      every database in `indexedDB.databases()` rather than\n *      hard-coding a name so future schema-version migrations\n *      don't leak a stale DB.\n *   5. Same-origin cookies (best-effort — see above).\n *   6. The `caches` storage estimate is also cleared by\n *      `navigator.storage.estimate()` reset paths. Chrome's\n *      Storage API does not expose a \"clear origin storage\"\n *      primitive, so the steps above are the manual equivalent.\n *\n * ## What it does NOT nuke\n *\n *   - API cookies (cross-subdomain). The API lives at\n *     `api.lceui.elevatornode.com`; `document.cookie` cannot\n *     reach them. The operator signs in again on next visit.\n *   - Service-worker registration that ships with the next\n *     page load. That re-registers on its own once the page\n *     reloads after `reload()`. We don't manually call\n *     `navigator.serviceWorker.register('/service-worker.js')`\n *     here because the SW has a version-pinned scriptURL\n *     (`/service-worker.js?v0.9.86`) and the layout's\n *     `initPwa()` is the single source of truth for registration.\n *     Forcing a register here would race the layout and could\n *     pin a stale version if the bump-deploy hasn't gone out yet.\n *\n * ## Per-step timeouts (v0.9.68)\n *\n * Operator directive 2026-07-21 17:25 ET: \"force clean has been\n * stuck at 'delete-idb' for a while now\". Each async step is\n * wrapped in a bounded timeout so a hung `indexedDB.databases()`\n * call (non-standard, can hang in older Safari) or a\n * `deleteDatabase()` blocked by another open tab can never\n * freeze the whole reset. Timeouts are reported in the\n * `skippedSteps` field of the result so the UI can surface them\n * (\"step X skipped because the browser hung\").\n *\n * Step budgets (chosen so a slow but healthy machine finishes\n * all 4 steps in <10s end-to-end):\n *   - unregister-sw : 5s (each `unregister` is bounded too)\n *   - clear-caches  : 5s (each `caches.delete` is bounded too)\n *   - clear-storage : synchronous, can't hang; timeout is 1s\n *                     for the (rare) `localStorage.clear()` throw\n *   - delete-idb    : 5s for `databases()` + 5s for each\n *                     `deleteDatabase()` request (the prime\n *                     culprit — `onblocked` can fire-and-hang in\n *                     Safari private mode and jsdom)\n *\n * ## Progress reporting\n *\n * `hardReset(opts)` accepts an `onProgress(step, detail?)` callback\n * the UI uses to render a \"Cleaning service worker… 1/5\" indicator.\n * The `step` values are stable string literals so the test can\n * assert the order without depending on message text.\n *\n * ## Future-session notes\n *\n * If you add a new persistent-storage layer (a second IDB database,\n * a CacheStorage bucket the app uses outside the SW, an Origin\n * Private File System, etc.), extend the step list here AND the\n * SW's `ALL_KNOWN_CACHES` set in `$lib/sw-cache-names.ts` so\n * the next operator's \"force clean\" doesn't leave a half-nuked\n * state. The pattern in the `clear-caches` step (call the\n * discovery API, await all, return counts) is the template to\n * copy.\n */\n\n/** localStorage flag set by the nuke + consumed by the layout on next mount. */\nexport const POST_NUKE_FLAG = 'lceui:post-nuke';\n\nexport type HardResetStep =\n\t| 'start'\n\t| 'unregister-sw'\n\t| 'clear-caches'\n\t| 'clear-storage'\n\t| 'delete-idb'\n\t| 'done';\n\nexport const HARD_RESET_STEPS: readonly HardResetStep[] = [\n\t'start',\n\t'unregister-sw',\n\t'clear-caches',\n\t'clear-storage',\n\t'delete-idb',\n\t'done'\n] as const;\n\nexport interface HardResetOptions {\n\treadonly onProgress?: (step: HardResetStep, detail?: string) => void;\n\t/** Skip the final `location.reload()` (used by tests). */\n\treadonly skipReload?: boolean;\n}\n\nexport interface HardResetResult {\n\treadonly steps: readonly HardResetStep[];\n\treadonly unregisteredServiceWorkers: number;\n\treadonly deletedCaches: number;\n\treadonly deletedIdbDatabases: number;\n\treadonly clearedLocalStorageKeys: number;\n\treadonly clearedSessionStorageKeys: number;\n\treadonly clearedCookies: number;\n\treadonly setPostNukeFlag: boolean;\n\t/**\n\t * v0.9.68 — steps that timed out or were skipped. A skipped\n\t * step means the underlying API hung or threw; the rest of\n\t * the reset still proceeded. The UI surfaces the list so the\n\t * operator can re-run the reset, or know which storage layer\n\t * is still contaminated.\n\t */\n\treadonly skippedSteps: readonly HardResetStep[];\n}\n\n/**\n * Race a promise against a timeout. On timeout, returns the\n * fallback (NOT a rejected promise — the caller usually wants\n * to keep going, not throw). The pending timer is cleared on\n * settle so we don't leak.\n */\nasync function withTimeout<T>(\n\tpromise: Promise<T>,\n\tms: number,\n\tfallback: T,\n): Promise<T> {\n\tlet timer: ReturnType<typeof setTimeout> | null = null;\n\tconst timeoutPromise = new Promise<T>((resolve) => {\n\t\ttimer = setTimeout(() => resolve(fallback), ms);\n\t});\n\ttry {\n\t\treturn await Promise.race([promise, timeoutPromise]);\n\t} finally {\n\t\tif (timer !== null) clearTimeout(timer);\n\t}\n}\n\n/**\n * Walk every step in order. Each step is independently guarded so\n * a failure in one (e.g. an iframe-blocked `caches.delete`) does\n * not abort the rest of the cleanup. Returns a structured report\n * the UI can render.\n */\nexport async function hardReset(opts: HardResetOptions = {}): Promise<HardResetResult> {\n\tconst { onProgress = () => {}, skipReload = false } = opts;\n\tconst report: {\n\t\tunregisteredServiceWorkers: number;\n\t\tdeletedCaches: number;\n\t\tdeletedIdbDatabases: number;\n\t\tclearedLocalStorageKeys: number;\n\t\tclearedSessionStorageKeys: number;\n\t\tclearedCookies: number;\n\t\tsetPostNukeFlag: boolean;\n\t\tskippedSteps: HardResetStep[];\n\t} = {\n\t\tunregisteredServiceWorkers: 0,\n\t\tdeletedCaches: 0,\n\t\tdeletedIdbDatabases: 0,\n\t\tclearedLocalStorageKeys: 0,\n\t\tclearedSessionStorageKeys: 0,\n\t\tclearedCookies: 0,\n\t\tsetPostNukeFlag: false,\n\t\tskippedSteps: []\n\t};\n\n\tonProgress('start');\n\n\t// 1. Service workers.\n\tonProgress('unregister-sw');\n\tif (typeof navigator !== 'undefined' && navigator.serviceWorker) {\n\t\ttry {\n\t\t\t// Some browsers (Safari < 11.1, jsdom, certain private\n\t\t\t// modes) expose `navigator.serviceWorker` without\n\t\t\t// `getRegistrations()`. Guard the method call.\n\t\t\tconst getRegs = navigator.serviceWorker.getRegistrations?.bind(\n\t\t\t\tnavigator.serviceWorker\n\t\t\t);\n\t\t\tif (getRegs) {\n\t\t\t\tconst regs = await withTimeout(getRegs(), 5_000, []);\n\t\t\t\tawait Promise.all(\n\t\t\t\t\tregs.map(async (reg) => {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tconst ok = await withTimeout(reg.unregister(), 5_000, false);\n\t\t\t\t\t\t\tif (ok) report.unregisteredServiceWorkers += 1;\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t// swallow per-registration errors\n\t\t\t\t\t\t}\n\t\t\t\t\t})\n\t\t\t\t);\n\t\t\t}\n\t\t} catch {\n\t\t\t// swallow; we tried\n\t\t}\n\t}\n\n\t// 2. CacheStorage buckets.\n\tonProgress('clear-caches');\n\tif (typeof caches !== 'undefined') {\n\t\ttry {\n\t\t\tconst names = await withTimeout(caches.keys(), 5_000, []);\n\t\t\tawait Promise.all(\n\t\t\t\tnames.map(async (n) => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst ok = await withTimeout(caches.delete(n), 5_000, false);\n\t\t\t\t\t\tif (ok) report.deletedCaches += 1;\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// swallow\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t);\n\t\t} catch {\n\t\t\t// swallow\n\t\t}\n\t}\n\n\t// 3. localStorage + sessionStorage (synchronous, can't really\n\t// hang — but the timeout pattern keeps the call site uniform).\n\tonProgress('clear-storage');\n\tif (typeof localStorage !== 'undefined') {\n\t\ttry {\n\t\t\treport.clearedLocalStorageKeys = localStorage.length;\n\t\t\tlocalStorage.clear();\n\t\t} catch {\n\t\t\t// swallow (Safari private mode, etc.)\n\t\t}\n\t}\n\tif (typeof sessionStorage !== 'undefined') {\n\t\ttry {\n\t\t\treport.clearedSessionStorageKeys = sessionStorage.length;\n\t\t\tsessionStorage.clear();\n\t\t} catch {\n\t\t\t// swallow\n\t\t}\n\t}\n\n\t// 4. IndexedDB databases. `indexedDB.databases()` is the\n\t// standard API; not all browsers support it (Safari < 15.4\n\t// returns undefined). When unsupported, fall back to the\n\t// single known name 'lceui' which is what Dexie uses.\n\t//\n\t// v0.9.68 — both `databases()` and each `deleteDatabase()`\n\t// are wrapped in a 5s timeout. The combination is the\n\t// primary \"stuck at delete-idb\" bug fix: a non-standard\n\t// `databases()` can hang forever in some browsers; a\n\t// `deleteDatabase()` can hang if another tab holds the\n\t// connection open and `onblocked` fires without a\n\t// subsequent `onerror` / `onsuccess`. The timeout breaks\n\t// the deadlock and the reset proceeds to the `done` state.\n\tonProgress('delete-idb');\n\tif (typeof indexedDB === 'undefined') {\n\t\treport.skippedSteps.push('delete-idb');\n\t} else {\n\t\tconst fallbackList = [{ name: 'lceui' }];\n\t\tlet dbs: { name?: string }[];\n\t\ttry {\n\t\t\tconst dbsResult = await withTimeout(\n\t\t\t\tindexedDB.databases?.() ?? Promise.resolve(fallbackList),\n\t\t\t\t5_000,\n\t\t\t\tfallbackList,\n\t\t\t);\n\t\t\tdbs = dbsResult;\n\t\t} catch {\n\t\t\t// databases() itself threw — fall back to the known name\n\t\t\tdbs = fallbackList;\n\t\t}\n\n\t\tawait Promise.all(\n\t\t\tdbs.map(async (entry) => {\n\t\t\t\tconst name = entry.name;\n\t\t\t\tif (!name) return;\n\t\t\t\ttry {\n\t\t\t\t\tconst result = await withTimeout(\n\t\t\t\t\t\tnew Promise<boolean>((resolve) => {\n\t\t\t\t\t\t\tconst req = indexedDB.deleteDatabase(name);\n\t\t\t\t\t\t\treq.onsuccess = () => resolve(true);\n\t\t\t\t\t\t\treq.onerror = () => resolve(false);\n\t\t\t\t\t\t\treq.onblocked = () => resolve(false);\n\t\t\t\t\t\t}),\n\t\t\t\t\t\t5_000,\n\t\t\t\t\t\tfalse,\n\t\t\t\t\t);\n\t\t\t\t\tif (result) {\n\t\t\t\t\t\treport.deletedIdbDatabases += 1;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Timeout or onblocked-without-closure — record\n\t\t\t\t\t\t// the step as skipped ONLY on the first\n\t\t\t\t\t\t// timeout (don't double-count if multiple\n\t\t\t\t\t\t// databases timeout).\n\t\t\t\t\t\tif (!report.skippedSteps.includes('delete-idb')) {\n\t\t\t\t\t\t\treport.skippedSteps.push('delete-idb');\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// swallow\n\t\t\t\t}\n\t\t\t})\n\t\t);\n\t}\n\n\tonProgress('done');\n\n\t// v0.9.86 — clear same-origin cookies (best-effort). The\n\t// API lives on a different subdomain, so the typical\n\t// `connect.sid` session cookie is unreachable from\n\t// `document.cookie` and the loop is a no-op. If a future\n\t// deploy shares the origin, the loop catches the\n\t// `connect.sid` and any other same-origin session cookies\n\t// without code changes. The match picks any path on the\n\t// current host with the standard cookie attributes (no\n\t// domain attribute = host-only, no secure = same-host).\n\tif (typeof document !== 'undefined' && typeof document.cookie === 'string') {\n\t\ttry {\n\t\t\tconst cookies = document.cookie\n\t\t\t\t.split(';')\n\t\t\t\t.map((c) => c.trim())\n\t\t\t\t.filter((c) => c.length > 0 && c.includes('='));\n\t\t\tfor (const cookie of cookies) {\n\t\t\t\tconst name = cookie.slice(0, cookie.indexOf('='));\n\t\t\t\tif (!name) continue;\n\t\t\t\t// Best-effort: clear on the current path + the\n\t\t\t\t// root path. Some servers set cookies on `/`,\n\t\t\t\t// others on the API path; clearing both is the\n\t\t\t\t// belt-and-braces match.\n\t\t\t\tdocument.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`;\n\t\t\t\tdocument.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; SameSite=Lax`;\n\t\t\t\treport.clearedCookies += 1;\n\t\t\t}\n\t\t} catch {\n\t\t\t// Some browsers throw on `document.cookie` write\n\t\t\t// (e.g. Safari ITP, third-party-cookie-blocking).\n\t\t\t// The cookies remain in place; the user can clear\n\t\t\t// them via the browser's \"clear site data\" path.\n\t\t}\n\t}\n\n\t// v0.9.86 — set the post-nuke flag BEFORE the reload. The\n\t// layout reads it on the next mount and fires\n\t// `PREPARE_STATIC_OFFLINE` against the new SW + the\n\t// session-driven bootstrap so the IDB + precache repopulate\n\t// without the operator clicking \"Refresh offline data\".\n\t// The flag is consumed (cleared) by the layout on read; a\n\t// subsequent reload without a nuke does NOT re-prep.\n\tif (typeof localStorage !== 'undefined') {\n\t\ttry {\n\t\t\tlocalStorage.setItem(POST_NUKE_FLAG, '1');\n\t\t\treport.setPostNukeFlag = true;\n\t\t} catch {\n\t\t\t// Safari private mode can throw on `setItem` after a\n\t\t\t// `clear()` in the same tick. The next reload will\n\t\t\t// just take the \"first time\" path; the operator\n\t\t\t// will see the dashboard \"Refresh offline data\"\n\t\t\t// button as the manual fallback.\n\t\t}\n\t}\n\n\t// 5. Reload. The layout's `initPwa()` will re-register the\n\t// version-pinned SW on the next paint. Force the new SW to\n\t// install by hitting the network right away so the user\n\t// doesn't sit on a half-loaded page.\n\tif (!skipReload && typeof window !== 'undefined') {\n\t\t// Tiny delay so the UI can paint the \"done\" indicator\n\t\t// before the reload tears the document down.\n\t\tsetTimeout(() => {\n\t\t\twindow.location.reload();\n\t\t}, 150);\n\t}\n\n\treturn { steps: HARD_RESET_STEPS, ...report };\n}\n"],"names":["POST_NUKE_FLAG","HARD_RESET_STEPS","withTimeout","promise","ms","fallback","timer","timeoutPromise","resolve","hardReset","opts","onProgress","skipReload","report","getRegs","regs","reg","names","n","fallbackList","dbs","entry","name","req","cookies","c","cookie"],"mappings":"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"}