refactor(idaa): migrate clear-caches page to core_func.clear_all_storage

Fixes a bug where indexedDB.deleteDatabase() was not awaited, meaning
the page could report success while databases were still being deleted.

Also adds the known-names IDB fallback for older Safari (pre-2021), which
previously caused the page to silently skip all IDB clearing on those browsers.

Removes ~70 lines of duplicate inline logic in favour of core_func.clear_all_storage()
with an on_step callback that maps BrowserResetLogFn messages to the existing
step-by-step progress UI.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-06-24 14:49:05 -04:00
parent 1b8f6efc39
commit 4519e9c21f

View File

@@ -1,6 +1,8 @@
<script lang="ts">
import { browser } from '$app/environment';
import { TriangleAlert, CircleCheck, Loader } from '@lucide/svelte';
import { core_func } from '$lib/ae_core/ae_core_functions';
import type { BrowserResetLogFn } from '$lib/ae_core/ae_core_functions';
type StepStatus = 'pending' | 'running' | 'done' | 'error';
@@ -12,101 +14,105 @@ interface ClearStep {
let steps: ClearStep[] = $state([
{ label: 'Service workers', status: 'pending', detail: '' },
{ label: 'Service worker caches', status: 'pending', detail: '' },
{ label: 'IndexedDB databases', status: 'pending', detail: '' },
{ label: 'Local storage', status: 'pending', detail: '' },
{ label: 'Session storage', status: 'pending', detail: '' }
{ label: 'App file cache', status: 'pending', detail: '' },
{ label: 'Saved app data', status: 'pending', detail: '' },
{ label: 'Local settings', status: 'pending', detail: '' },
{ label: 'Session data', status: 'pending', detail: '' }
]);
let overall_done = $state(false);
let had_error = $state(false);
// Tracks item counts per step — used to build "N databases cleared" summary labels.
const ok_counts: number[] = [0, 0, 0, 0, 0];
// Tracks which step last received a message so the first message for a new step
// transitions it to 'running' before reporting the result.
let current_step_idx = -1;
// Maps a core__browser_reset log message to one of the 5 step indices.
function step_index_for(msg: string): number {
const m = msg.toLowerCase();
if (m.includes('service worker') || (m.includes(' sw') && !m.includes('cache'))) return 0;
if (m.includes('cache storage') || m.includes('cache:')) return 1;
if (m.includes('idb') || m.includes('indexeddb') || m.includes('database')) return 2;
if (m.includes('localstorage')) return 3;
if (m.includes('sessionstorage')) return 4;
return -1; // general info (e.g. "Starting full storage reset...") — not tied to a step
}
function make_ok_detail(idx: number, msg: string): string {
const n = ok_counts[idx];
if (idx === 0) return `${n} unregistered`;
if (idx === 1) return `${n} cache${n !== 1 ? 's' : ''} cleared`;
if (idx === 2) return `${n} database${n !== 1 ? 's' : ''} cleared`;
// localStorage / sessionStorage report "Cleared X (N item(s))." — extract the count.
const count_match = msg.match(/\((\d+)\s+item/);
return count_match ? `${count_match[1]} items cleared` : 'Cleared';
}
const on_step: BrowserResetLogFn = (msg, level = 'info') => {
const idx = step_index_for(msg);
if (idx < 0) return;
// First message for this step: advance to it and mark it running.
if (idx > current_step_idx) {
current_step_idx = idx;
steps[idx].status = 'running';
}
if (level === 'error') {
steps[idx].status = 'error';
steps[idx].detail = msg.replace(/^ERROR\s+/i, '');
had_error = true;
} else if (level === 'ok') {
ok_counts[idx]++;
steps[idx].status = 'done';
steps[idx].detail = make_ok_detail(idx, msg);
} else if (level === 'warn') {
// "Nothing found" and "not supported" warnings close the step.
// Fallback (indexedDB.databases() unavailable) and "blocked" warnings do NOT
// close the step — further 'ok' or 'error' messages will follow.
const lower = msg.toLowerCase();
const is_nothing_found =
lower.includes('no active') ||
lower.includes('not supported') ||
lower.includes('no cache storage') ||
lower.includes('no indexeddb') ||
lower.includes('no idb');
if (is_nothing_found && steps[idx].status === 'running') {
steps[idx].status = 'done';
steps[idx].detail = lower.includes('not supported') ? 'Not supported' : 'Nothing to clear';
}
}
// 'info' messages only trigger the 'running' transition above; no further action.
};
async function clear_all_caches() {
// Service workers
steps[0].status = 'running';
try {
if ('serviceWorker' in navigator) {
const registrations = await navigator.serviceWorker.getRegistrations();
for (const reg of registrations) await reg.unregister();
steps[0].detail = `${registrations.length} unregistered`;
} else {
steps[0].detail = 'Not supported';
}
steps[0].status = 'done';
} catch (e) {
steps[0].status = 'error';
steps[0].detail = String(e);
had_error = true;
}
// core_func.clear_all_storage handles:
// SW unregister → Cache Storage → IDB (with known-names fallback) → localStorage → sessionStorage
// All IDB deletes are properly awaited (the original inline implementation did not await them).
await core_func.clear_all_storage(on_step);
// Cache Storage (SW asset caches)
steps[1].status = 'running';
try {
if ('caches' in window) {
const cache_keys = await caches.keys();
for (const key of cache_keys) await caches.delete(key);
steps[1].detail = `${cache_keys.length} cache${cache_keys.length !== 1 ? 's' : ''} cleared`;
} else {
steps[1].detail = 'Not supported';
// Any steps that received no matching messages (edge case) are marked done.
for (const step of steps) {
if (step.status === 'pending' || step.status === 'running') {
step.status = 'done';
if (!step.detail) step.detail = 'Done';
}
steps[1].status = 'done';
} catch (e) {
steps[1].status = 'error';
steps[1].detail = String(e);
had_error = true;
}
// IDB — enumerate and delete every database on this origin
steps[2].status = 'running';
try {
const db_list = await indexedDB.databases();
for (const db of db_list) {
if (db.name) indexedDB.deleteDatabase(db.name);
}
steps[2].status = 'done';
steps[2].detail = `${db_list.length} database${db_list.length !== 1 ? 's' : ''} cleared`;
} catch (e) {
steps[2].status = 'error';
steps[2].detail = String(e);
had_error = true;
}
// localStorage
steps[3].status = 'running';
try {
localStorage.clear();
steps[3].status = 'done';
steps[3].detail = 'Cleared';
} catch (e) {
steps[3].status = 'error';
steps[3].detail = String(e);
had_error = true;
}
// sessionStorage
steps[4].status = 'running';
try {
sessionStorage.clear();
steps[4].status = 'done';
steps[4].detail = 'Cleared';
} catch (e) {
steps[4].status = 'error';
steps[4].detail = String(e);
had_error = true;
}
overall_done = true;
// Notify parent window (Novi page) that the clear is complete.
// The Novi page can optionally listen for this to show a confirmation to the member.
// Notify the parent Novi page that the clear is complete.
// Novi can optionally listen to show a confirmation or trigger a page reload.
try {
window.parent.postMessage({ ae_cache_cleared: true, had_error }, '*');
} catch (_) {
// not in an iframe — ignore
// Not in an iframe — ignore.
}
}
// Run once on mount. Reads no reactive state so $effect never re-fires.
// Auto-run once on mount. No reactive state is read, so $effect never re-fires.
$effect(() => {
if (!browser) return;
clear_all_caches();
@@ -127,13 +133,13 @@ $effect(() => {
Cleared with some errors
</div>
<p class="text-sm text-gray-600 dark:text-gray-400">
Most caches were cleared. Close this tab and reload the IDAA pages to
Most data was cleared. Close this tab and reload the IDAA pages to
get the latest version.
</p>
{:else}
<CircleCheck size={40} class="text-success-500" />
<div class="text-xl font-semibold">
All caches cleared
All data cleared
</div>
<p class="text-sm text-gray-600 dark:text-gray-400">
Close this tab and reload the IDAA pages to get the latest version.
@@ -168,13 +174,5 @@ $effect(() => {
Meeting List, Archives, or Bulletin Board,
please try once more. We apologize for the inconvenience, and thank you for your patience while we work to improve the IDAA experience!
</p>
<!-- <p class="text-sm text-gray-700 dark:text-gray-300">
If you were having trouble with the IDAA
<a href="https://www.idaa.org/idaa-meetings" style="color:#2563eb;text-decoration:underline;" class="text-blue-600 underline">Meeting List</a>,
<a href="https://www.idaa.org/idaa-archives" style="color:#2563eb;text-decoration:underline;" class="text-blue-600 underline">Archives</a>, or
<a href="https://www.idaa.org/idaa-bulletin-board" style="color:#2563eb;text-decoration:underline;" class="text-blue-600 underline">Bulletin Board</a>,
please try once more. We apologize for the inconvenience, and thank you for your patience while we work to improve the IDAA experience!
</p> -->
{/if}
</div>