feat(core): add obscure_email to ae_util and browser reset utilities to core_func
- ae_utils: add obscure_email() — first 3 chars + *** + domain; was duplicated in 6 files - core__browser_reset.ts: new module with clear_idb() and clear_all_storage() - clear_idb: indexedDB.databases() with known-names fallback for older browsers - clear_all_storage: mandatory order (SW → Cache Storage → IDB → localStorage → sessionStorage) - optional BrowserResetLogFn callback for step-by-step UI feedback - ae_core_functions: registers both reset functions under core_func; re-exports BrowserResetLogFn Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -47,6 +47,9 @@ import { add_url_params, clean_headers } from '$lib/ae_core/core__api_helpers';
|
||||
|
||||
import { download_export__obj_type } from '$lib/ae_core/core__export';
|
||||
|
||||
import { clear_idb, clear_all_storage } from '$lib/ae_core/core__browser_reset';
|
||||
export type { BrowserResetLogFn } from '$lib/ae_core/core__browser_reset';
|
||||
|
||||
|
||||
const export_obj = {
|
||||
check_hosted_file_obj_w_hash: check_hosted_file_obj_w_hash,
|
||||
@@ -84,6 +87,9 @@ const export_obj = {
|
||||
|
||||
download_export__obj_type: download_export__obj_type,
|
||||
generate_qr_code: generate_qr_code,
|
||||
js_generate_qr_code: js_generate_qr_code
|
||||
js_generate_qr_code: js_generate_qr_code,
|
||||
|
||||
clear_idb: clear_idb,
|
||||
clear_all_storage: clear_all_storage,
|
||||
};
|
||||
export const core_func = export_obj;
|
||||
|
||||
130
src/lib/ae_core/core__browser_reset.ts
Normal file
130
src/lib/ae_core/core__browser_reset.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
/**
|
||||
* Optional progress callback for clear_idb() and clear_all_storage().
|
||||
* level: 'ok' = success, 'warn' = nothing to do / non-fatal, 'error' = failure, 'info' = general step
|
||||
*/
|
||||
export type BrowserResetLogFn = (msg: string, level?: 'info' | 'ok' | 'warn' | 'error') => void;
|
||||
|
||||
// Fallback list for browsers that don't support indexedDB.databases() (pre-2021 Safari etc.)
|
||||
const KNOWN_IDB_NAMES = [
|
||||
'ae_core_db', 'ae_events_db', 'ae_journals_db',
|
||||
'ae_archives_db', 'ae_posts_db', 'ae_idaa_db',
|
||||
'ae_sponsorships_db', 'ae_reports_db',
|
||||
];
|
||||
|
||||
function delete_one_idb(name: string, log?: BrowserResetLogFn): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
const req = indexedDB.deleteDatabase(name);
|
||||
req.onsuccess = () => { log?.(`Deleted IDB database: ${name}`, 'ok'); resolve(); };
|
||||
req.onerror = () => { log?.(`ERROR deleting IDB database: ${name}`, 'error'); resolve(); };
|
||||
// blocked = another tab still has the DB open; we resolve anyway and continue
|
||||
req.onblocked = () => { log?.(`IDB delete blocked (open connections): ${name} — will proceed`, 'warn'); resolve(); };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all IndexedDB databases for this origin.
|
||||
*
|
||||
* Uses indexedDB.databases() when available; falls back to the known Aether DB
|
||||
* name list for older browsers. Does not reload — caller decides navigation.
|
||||
*/
|
||||
export async function clear_idb(log?: BrowserResetLogFn): Promise<void> {
|
||||
if (!browser) return;
|
||||
|
||||
try {
|
||||
if ('databases' in indexedDB) {
|
||||
const db_list = await indexedDB.databases();
|
||||
if (db_list.length === 0) {
|
||||
log?.('No IndexedDB databases found.', 'warn');
|
||||
} else {
|
||||
for (const db_info of db_list) {
|
||||
if (!db_info.name) continue;
|
||||
await delete_one_idb(db_info.name, log);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log?.('indexedDB.databases() not available — deleting known Aether databases by name.', 'warn');
|
||||
for (const name of KNOWN_IDB_NAMES) {
|
||||
await delete_one_idb(name, log);
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
log?.(`ERROR clearing IndexedDB: ${err.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Full browser storage reset for this origin.
|
||||
*
|
||||
* Clears in mandatory order: SW unregister → Cache Storage → IDB → localStorage → sessionStorage.
|
||||
* SW and Cache Storage MUST be cleared before IDB/localStorage — otherwise the SW continues
|
||||
* serving stale JS bundles from its own Cache Storage on the next load, keeping users stuck.
|
||||
*
|
||||
* Does not reload — caller decides whether to navigate after (e.g. window.location.href = '/').
|
||||
*
|
||||
* @param log Optional progress callback. Receives each step message and a severity level.
|
||||
* Use this to surface step-by-step feedback to the user (e.g. the fix-sw page).
|
||||
* Omit for silent operation (e.g. the sys bar reset buttons).
|
||||
*/
|
||||
export async function clear_all_storage(log?: BrowserResetLogFn): Promise<void> {
|
||||
if (!browser) return;
|
||||
|
||||
log?.('Starting full storage reset...', 'info');
|
||||
|
||||
// 1. Unregister all service workers
|
||||
if ('serviceWorker' in navigator) {
|
||||
try {
|
||||
const registrations = await navigator.serviceWorker.getRegistrations();
|
||||
if (registrations.length === 0) {
|
||||
log?.('No active service worker registrations found.', 'warn');
|
||||
} else {
|
||||
log?.(`Found ${registrations.length} service worker registration(s).`, 'info');
|
||||
for (const reg of registrations) {
|
||||
const ok = await reg.unregister();
|
||||
log?.(`Unregistered SW at scope: ${reg.scope} (success: ${ok})`, 'ok');
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
log?.(`ERROR unregistering service workers: ${err.message}`, 'error');
|
||||
}
|
||||
} else {
|
||||
log?.('Service Workers not supported in this browser.', 'warn');
|
||||
}
|
||||
|
||||
// 2. Clear all Cache Storage caches
|
||||
try {
|
||||
const cache_keys = await caches.keys();
|
||||
if (cache_keys.length === 0) {
|
||||
log?.('No Cache Storage entries found.', 'warn');
|
||||
} else {
|
||||
for (const key of cache_keys) {
|
||||
await caches.delete(key);
|
||||
log?.(`Cleared cache: ${key}`, 'ok');
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
log?.(`ERROR clearing Cache Storage: ${err.message}`, 'error');
|
||||
}
|
||||
|
||||
// 3. Clear all IndexedDB databases (handles enumeration + fallback internally)
|
||||
await clear_idb(log);
|
||||
|
||||
// 4. Clear localStorage
|
||||
try {
|
||||
const local_count = localStorage.length;
|
||||
localStorage.clear();
|
||||
log?.(`Cleared localStorage (${local_count} item(s)).`, 'ok');
|
||||
} catch (err: any) {
|
||||
log?.(`ERROR clearing localStorage: ${err.message}`, 'error');
|
||||
}
|
||||
|
||||
// 5. Clear sessionStorage
|
||||
try {
|
||||
const session_count = sessionStorage.length;
|
||||
sessionStorage.clear();
|
||||
log?.(`Cleared sessionStorage (${session_count} item(s)).`, 'ok');
|
||||
} catch (err: any) {
|
||||
log?.(`ERROR clearing sessionStorage: ${err.message}`, 'error');
|
||||
}
|
||||
}
|
||||
@@ -267,6 +267,19 @@ export const shorten_string = function shorten_string({
|
||||
return new_string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a partially-obscured email address for display to non-trusted users.
|
||||
* Shows the first 3 characters of the local part + *** + the full domain.
|
||||
* Example: "john.doe@example.com" → "joh***@example.com"
|
||||
* Trusted staff should receive the full address; pass it through unmodified.
|
||||
*/
|
||||
export function obscure_email(email: string | null | undefined): string {
|
||||
if (!email) return '';
|
||||
const at = email.indexOf('@');
|
||||
if (at < 0) return email;
|
||||
return `${email.slice(0, Math.min(3, at))}***${email.slice(at)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips HTML tags from a string.
|
||||
*/
|
||||
@@ -352,6 +365,7 @@ export const ae_util = {
|
||||
to_title_case: to_title_case,
|
||||
shorten_string: shorten_string,
|
||||
shorten_filename: shorten_filename,
|
||||
obscure_email: obscure_email,
|
||||
strip_html: strip_html,
|
||||
file_extension_icon: file_extension_icon,
|
||||
file_extension_icon_lucide: file_extension_icon_lucide,
|
||||
|
||||
Reference in New Issue
Block a user