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"> <script lang="ts">
import { browser } from '$app/environment'; import { browser } from '$app/environment';
import { TriangleAlert, CircleCheck, Loader } from '@lucide/svelte'; 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'; type StepStatus = 'pending' | 'running' | 'done' | 'error';
@@ -12,101 +14,105 @@ interface ClearStep {
let steps: ClearStep[] = $state([ let steps: ClearStep[] = $state([
{ label: 'Service workers', status: 'pending', detail: '' }, { label: 'Service workers', status: 'pending', detail: '' },
{ label: 'Service worker caches', status: 'pending', detail: '' }, { label: 'App file cache', status: 'pending', detail: '' },
{ label: 'IndexedDB databases', status: 'pending', detail: '' }, { label: 'Saved app data', status: 'pending', detail: '' },
{ label: 'Local storage', status: 'pending', detail: '' }, { label: 'Local settings', status: 'pending', detail: '' },
{ label: 'Session storage', status: 'pending', detail: '' } { label: 'Session data', status: 'pending', detail: '' }
]); ]);
let overall_done = $state(false); let overall_done = $state(false);
let had_error = $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() { async function clear_all_caches() {
// Service workers // core_func.clear_all_storage handles:
steps[0].status = 'running'; // SW unregister → Cache Storage → IDB (with known-names fallback) → localStorage → sessionStorage
try { // All IDB deletes are properly awaited (the original inline implementation did not await them).
if ('serviceWorker' in navigator) { await core_func.clear_all_storage(on_step);
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;
}
// Cache Storage (SW asset caches) // Any steps that received no matching messages (edge case) are marked done.
steps[1].status = 'running'; for (const step of steps) {
try { if (step.status === 'pending' || step.status === 'running') {
if ('caches' in window) { step.status = 'done';
const cache_keys = await caches.keys(); if (!step.detail) step.detail = 'Done';
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';
} }
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; overall_done = true;
// Notify parent window (Novi page) that the clear is complete. // Notify the parent Novi page that the clear is complete.
// The Novi page can optionally listen for this to show a confirmation to the member. // Novi can optionally listen to show a confirmation or trigger a page reload.
try { try {
window.parent.postMessage({ ae_cache_cleared: true, had_error }, '*'); window.parent.postMessage({ ae_cache_cleared: true, had_error }, '*');
} catch (_) { } 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(() => { $effect(() => {
if (!browser) return; if (!browser) return;
clear_all_caches(); clear_all_caches();
@@ -127,13 +133,13 @@ $effect(() => {
Cleared with some errors Cleared with some errors
</div> </div>
<p class="text-sm text-gray-600 dark:text-gray-400"> <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. get the latest version.
</p> </p>
{:else} {:else}
<CircleCheck size={40} class="text-success-500" /> <CircleCheck size={40} class="text-success-500" />
<div class="text-xl font-semibold"> <div class="text-xl font-semibold">
All caches cleared All data cleared
</div> </div>
<p class="text-sm text-gray-600 dark:text-gray-400"> <p class="text-sm text-gray-600 dark:text-gray-400">
Close this tab and reload the IDAA pages to get the latest version. Close this tab and reload the IDAA pages to get the latest version.
@@ -168,13 +174,5 @@ $effect(() => {
Meeting List, Archives, or Bulletin Board, 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! 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>
<!-- <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} {/if}
</div> </div>