diff --git a/README.md b/README.md index f3db0c2..4c85672 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ The library provides an `App` component that manages the application shell, rout ```jsx import { App } from '@reliancy/bface/ui/App'; import { CONFIG_KEYS } from '@reliancy/bface/platform/env'; -import { registerServiceWorker } from '@reliancy/bface/platform/sw-register'; +import { sw } from '@reliancy/bface/platform/worker'; async function handleInit(services, { initialProfile } = {}) { // Application profile (camelCase or snake_case field names are accepted where noted in env mapping) @@ -49,7 +49,7 @@ async function handleInit(services, { initialProfile } = {}) { // await loadModule(moduleName, services); } - await registerServiceWorker(); + await sw.registerServiceWorker(); return profile; } @@ -65,12 +65,11 @@ function MyApp() { - **`services.api_client`** — HTTP client (`get`, `post`, …) - **`services.storage`** — storage module (`getProvider`, …) -- **`services.api_router`** — placeholder for service-worker API routing (when available) - **`services.ui_router`** — UI routing helpers - **`services.menu`** — menu registration and queries - **`services.env`** — `initEnv`, `getConfig`, `setConfig`, `CONFIG_KEYS`, tracing helpers, etc. -Service worker registration is **not** on `services`; import `registerServiceWorker` from `@reliancy/bface/platform/sw-register` (or the package root) and call it from `onInit` when you are ready. +Service worker registration is **not** on `services`; import `sw` from `@reliancy/bface/platform/worker` (or the package root) and call `sw.registerServiceWorker()` from `onInit` when you are ready. Service-worker API interception belongs in app/module `sw.js` files that run inside the worker. ```jsx async function handleInit(services) { @@ -138,7 +137,7 @@ const menuItems = queryMenuItems('/primary'); ### Exports (`package.json` → `exports`) - **`@reliancy/bface`** — main entry: platform, `App`, UI components index, general settings, security, and data helpers -- **`@reliancy/bface/platform/*`** — platform modules (`env`, `api`, `storage`, `menu`, `sw-register`, `compat`, `host`, …) +- **`@reliancy/bface/platform/*`** — platform modules (`env`, `api`, `storage`, `menu`, `worker`, `compat`, `host`, …) - **`@reliancy/bface/ui/*`** — UI entry points such as `App` and `components` Security and data types are re-exported from the root entry; there are no separate `exports` subpaths for `./security/*` or `./data/*` today—import them from `@reliancy/bface` or add deep links if your bundler resolves source. @@ -149,7 +148,7 @@ Security and data types are re-exported from the root entry; there are no separa - **`platform/api.js`** — API client - **`platform/storage.js`** — storage abstraction (localStorage, IndexedDB, OPFS) - **`platform/menu.js`** — menu model and queries -- **`platform/sw-register.js`** — service worker registration and cache helpers +- **`platform/worker.js`** — browser worker/runtime helpers, currently exposed through the `sw` namespace - **`platform/compat.js`** — environment detection and compatibility - **`platform/host.js`** — host detection (e.g. Electron) @@ -195,7 +194,7 @@ Here's a complete example of using the library in a project: // app.jsx import { App } from '@reliancy/bface/ui/App'; import { CONFIG_KEYS } from '@reliancy/bface/platform/env'; -import { registerServiceWorker } from '@reliancy/bface/platform/sw-register'; +import { sw } from '@reliancy/bface/platform/worker'; async function loadProfile() { // Load your app profile (from JSON, API, etc.) @@ -225,7 +224,7 @@ async function handleInit(services, { initialProfile } = {}) { } // 4. Register service worker - await registerServiceWorker(); + await sw.registerServiceWorker(); return profile; } diff --git a/src/index.js b/src/index.js index cbceb82..61e932c 100644 --- a/src/index.js +++ b/src/index.js @@ -9,7 +9,7 @@ export * from './platform/compat.js'; export * from './platform/env.js'; export * from './platform/menu.js'; export * from './platform/storage.js'; -export * from './platform/sw-register.js'; +export * from './platform/worker.js'; export * from './data/index.js'; // Re-export UI components diff --git a/src/platform/compat.js b/src/platform/compat.js index 2c88d99..812b2ac 100644 --- a/src/platform/compat.js +++ b/src/platform/compat.js @@ -7,11 +7,20 @@ * added when needed. */ -import { getConfig, setConfig } from './env.js'; +import { CONFIG_KEYS, getConfig, setConfig } from './env.js'; import { getDesktopBridgeSafe, getHostKind, isElectronHost } from './host.js'; // Config key for last visited route path const LAST_PATH_CONFIG_KEY = 'router.lastPath'; +const DEFAULT_APP_NAME = 'default'; + +async function getScopedLastPathConfigKey() { + const appName = await getConfig(CONFIG_KEYS.APP_NAME, DEFAULT_APP_NAME); + const normalizedAppName = + typeof appName === 'string' && appName.trim() ? appName.trim() : DEFAULT_APP_NAME; + + return `${LAST_PATH_CONFIG_KEY}.${normalizedAppName}`; +} // ============================================================================ // Theme Detection (System Color Scheme) @@ -650,10 +659,21 @@ export async function getRouterPath(defaultPath = '/') { // If URL is empty or '/', check config for last visited path try { - const lastPath = await getConfig(LAST_PATH_CONFIG_KEY, null); + const scopedLastPathKey = await getScopedLastPathConfigKey(); + const lastPath = await getConfig(scopedLastPathKey, null); if (lastPath && typeof lastPath === 'string' && lastPath !== '/') { return lastPath; } + + // Keep the old unscoped key only for the default app so existing installs + // continue to reopen their last route after the namespaced migration. + const appName = await getConfig(CONFIG_KEYS.APP_NAME, DEFAULT_APP_NAME); + if (appName === DEFAULT_APP_NAME) { + const legacyLastPath = await getConfig(LAST_PATH_CONFIG_KEY, null); + if (legacyLastPath && typeof legacyLastPath === 'string' && legacyLastPath !== '/') { + return legacyLastPath; + } + } } catch (error) { console.warn('[Compat] Failed to get last path from config:', error); } @@ -694,7 +714,13 @@ export async function setRouterPath(path, replace = false, options = {}) { // Save to config for persistence (works on all platforms) try { - await setConfig(LAST_PATH_CONFIG_KEY, fullPath); + const scopedLastPathKey = await getScopedLastPathConfigKey(); + await setConfig(scopedLastPathKey, fullPath); + + const appName = await getConfig(CONFIG_KEYS.APP_NAME, DEFAULT_APP_NAME); + if (appName === DEFAULT_APP_NAME) { + await setConfig(LAST_PATH_CONFIG_KEY, fullPath); + } } catch (error) { console.warn('[Compat] Failed to save path to config:', error); } diff --git a/src/platform/sw-register.js b/src/platform/worker.js similarity index 72% rename from src/platform/sw-register.js rename to src/platform/worker.js index 3351d92..947e2ee 100644 --- a/src/platform/sw-register.js +++ b/src/platform/worker.js @@ -1,5 +1,7 @@ /** - * Service Worker Registration + * Browser worker/runtime helpers. + * Keep the top-level module compact, then expose focused namespaces that we + * can later split into separate files without changing the import surface. */ import { isElectronHost, isTauriHost } from './host.js'; @@ -8,14 +10,12 @@ import { getConfig, isDevelopment, CONFIG_KEYS } from './env.js'; const SW_PATH = '/sw.js'; const SW_SCOPE = '/'; const DEV_SW_RESET_KEY = '__bface_dev_sw_reset__'; -/** - * Clear all caches - */ -export async function clearAllCaches() { + +async function clearAllCaches() { if ('caches' in window) { try { const cacheNames = await caches.keys(); - await Promise.all(cacheNames.map(name => { + await Promise.all(cacheNames.map((name) => { console.log('[SW] Clearing cache:', name); return caches.delete(name); })); @@ -29,14 +29,11 @@ export async function clearAllCaches() { return 0; } -/** - * Unregister all service workers - */ -export async function unregisterAllServiceWorkers() { +async function unregisterAllServiceWorkers() { if ('serviceWorker' in navigator) { try { const registrations = await navigator.serviceWorker.getRegistrations(); - await Promise.all(registrations.map(reg => { + await Promise.all(registrations.map((reg) => { console.log('[SW] Unregistering service worker:', reg.scope); return reg.unregister(); })); @@ -50,25 +47,19 @@ export async function unregisterAllServiceWorkers() { return 0; } -/** - * Clear all storage (localStorage, sessionStorage, IndexedDB) - */ -export async function clearAllStorage() { +async function clearAllStorage() { try { - // Clear localStorage localStorage.clear(); console.log('[Storage] localStorage cleared'); - - // Clear sessionStorage + sessionStorage.clear(); console.log('[Storage] sessionStorage cleared'); - - // Clear IndexedDB databases + if ('indexedDB' in window) { const databases = await indexedDB.databases(); - await Promise.all(databases.map(db => { + await Promise.all(databases.map((db) => { if (db.name) { - return new Promise((resolve, reject) => { + return new Promise((resolve) => { const deleteReq = indexedDB.deleteDatabase(db.name); deleteReq.onsuccess = () => { console.log(`[Storage] IndexedDB database "${db.name}" deleted`); @@ -76,13 +67,14 @@ export async function clearAllStorage() { }; deleteReq.onerror = () => { console.warn(`[Storage] Failed to delete IndexedDB database "${db.name}"`); - resolve(); // Continue even if one fails + resolve(); }; }); } + return Promise.resolve(); })); } - + console.log('[Storage] All storage cleared'); return true; } catch (error) { @@ -91,41 +83,29 @@ export async function clearAllStorage() { } } -/** - * Clear everything: caches, service workers, and storage - * This is the main utility function for clearing all PWA data - */ -export async function clearPWACache() { - console.log('🧹 Clearing all PWA caches and storage...'); - +async function clearPWACache() { + console.log('Clearing all PWA caches and storage...'); + try { - // 1. Unregister service workers const swCount = await unregisterAllServiceWorkers(); - - // 2. Clear all caches const cacheCount = await clearAllCaches(); - - // 3. Clear all storage await clearAllStorage(); - - console.log(`✅ PWA cache cleared: ${swCount} service worker(s), ${cacheCount} cache(s), and all storage`); - console.log('💡 Reload the page to re-register service workers'); - + + console.log(`PWA cache cleared: ${swCount} service worker(s), ${cacheCount} cache(s), and all storage`); + console.log('Reload the page to re-register service workers'); + return { serviceWorkers: swCount, caches: cacheCount, storage: true }; } catch (error) { - console.error('❌ Failed to clear PWA cache:', error); + console.error('Failed to clear PWA cache:', error); throw error; } } -/** - * Register service worker - */ -export async function registerServiceWorker() { +async function registerServiceWorker() { if (isElectronHost() || isTauriHost()) { await unregisterAllServiceWorkers(); console.log('[SW] Skipping service worker registration in desktop host'); @@ -140,19 +120,14 @@ export async function registerServiceWorker() { if ('serviceWorker' in navigator) { try { - - // Register or get existing service worker const registration = await navigator.serviceWorker.register(SW_PATH, { scope: SW_SCOPE, - updateViaCache: 'none' // Always fetch fresh service worker + updateViaCache: 'none' }); console.log('Service Worker registered:', registration); - - // Force immediate update check to get latest version await registration.update(); - // Handle updates registration.addEventListener('updatefound', () => { const newWorker = registration.installing; if (newWorker) { @@ -160,7 +135,6 @@ export async function registerServiceWorker() { if (newWorker.state === 'installed') { if (navigator.serviceWorker.controller) { console.log('[SW] New service worker available, reloading...'); - // Force reload to activate new worker window.location.reload(); } else { console.log('[SW] Service worker installed for the first time'); @@ -175,17 +149,13 @@ export async function registerServiceWorker() { console.error('Service Worker registration failed:', error); throw error; } - } else { - console.warn('Service Workers are not supported'); - return null; } + + console.warn('Service Workers are not supported'); + return null; } -/** - * In development, stale service workers can break Vite module loading by - * intercepting /@fs and related requests. Clear them once and reload cleanly. - */ -export async function resetServiceWorkers() { +async function resetServiceWorkers() { if (isElectronHost() || isTauriHost()) { await unregisterAllServiceWorkers(); return false; @@ -215,7 +185,7 @@ export async function resetServiceWorkers() { return true; } -export async function getServiceWorkerStatus() { +async function getServiceWorkerStatus() { if (isElectronHost() || isTauriHost()) { return 'Desktop Disabled'; } @@ -243,10 +213,7 @@ export async function getServiceWorkerStatus() { } } -/** - * Unregister service worker - */ -export async function unregisterServiceWorker() { +async function unregisterServiceWorker() { if (isElectronHost() || isTauriHost()) { return; } @@ -260,3 +227,13 @@ export async function unregisterServiceWorker() { } } +export const sw = { + clearAllCaches, + unregisterAllServiceWorkers, + clearAllStorage, + clearPWACache, + registerServiceWorker, + resetServiceWorkers, + getServiceWorkerStatus, + unregisterServiceWorker +}; diff --git a/src/ui/App.jsx b/src/ui/App.jsx index 70e6cab..da63645 100644 --- a/src/ui/App.jsx +++ b/src/ui/App.jsx @@ -7,12 +7,8 @@ import React, { createContext, useContext, useState, useEffect, useCallback, useRef, useMemo } from 'react'; import { TamaguiProvider, Theme, createTamagui, YStack } from 'tamagui'; import { - clearPWACache, - clearAllCaches, - clearAllStorage, - getServiceWorkerStatus, - unregisterAllServiceWorkers -} from '../platform/sw-register.js'; + sw +} from '../platform/worker.js'; import { getProvider } from '../platform/storage.js'; import * as apiClient from '../platform/api.js'; import * as storageModuleRef from '../platform/storage.js'; @@ -215,27 +211,9 @@ function App({ console.warn('[App] Failed to import Router module:', error); } - // Get API router from service worker (if available) - let api_router = null; - if (typeof window !== 'undefined' && 'serviceWorker' in navigator) { - // API Router is in service worker, modules will register via message passing - // or we expose a registration function - api_router = { - register: (path, handler) => { - // Register endpoint in SW router - // Note: For now, we'll need to handle this differently since - // functions can't be serialized. Modules should register routes - // in their routes.js files which get loaded into the SW. - console.log(`[API Router] Register endpoint: ${path}`); - // TODO: Implement proper SW router registration - } - }; - } - return { api_client: apiClient.api, // Renamed from api storage: storageModuleRef, - api_router, ui_router, menu: menuRef, env: envModuleRef // New service @@ -313,7 +291,7 @@ function App({ appLogger.warn('Menu service not available'); } - setSwStatus(await getServiceWorkerStatus()); + setSwStatus(await sw.getServiceWorkerStatus()); setInitialized(true); initTrace.end({ @@ -533,12 +511,12 @@ export { useApp, useTheme, THEME_MODES, themeManager as ThemeManager }; // Expose PWA utilities globally for console access if (typeof window !== 'undefined') { - window.clearPWACache = clearPWACache; + window.clearPWACache = sw.clearPWACache; window.__PWA_UTILS__ = { - clearPWACache, - clearAllCaches, - unregisterAllServiceWorkers, - clearAllStorage + clearPWACache: sw.clearPWACache, + clearAllCaches: sw.clearAllCaches, + unregisterAllServiceWorkers: sw.unregisterAllServiceWorkers, + clearAllStorage: sw.clearAllStorage }; console.log('💡 PWA Utilities available:');