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 { 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 = {
|
const export_obj = {
|
||||||
check_hosted_file_obj_w_hash: check_hosted_file_obj_w_hash,
|
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,
|
download_export__obj_type: download_export__obj_type,
|
||||||
generate_qr_code: generate_qr_code,
|
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;
|
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;
|
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.
|
* Strips HTML tags from a string.
|
||||||
*/
|
*/
|
||||||
@@ -352,6 +365,7 @@ export const ae_util = {
|
|||||||
to_title_case: to_title_case,
|
to_title_case: to_title_case,
|
||||||
shorten_string: shorten_string,
|
shorten_string: shorten_string,
|
||||||
shorten_filename: shorten_filename,
|
shorten_filename: shorten_filename,
|
||||||
|
obscure_email: obscure_email,
|
||||||
strip_html: strip_html,
|
strip_html: strip_html,
|
||||||
file_extension_icon: file_extension_icon,
|
file_extension_icon: file_extension_icon,
|
||||||
file_extension_icon_lucide: file_extension_icon_lucide,
|
file_extension_icon_lucide: file_extension_icon_lucide,
|
||||||
|
|||||||
Reference in New Issue
Block a user