Rename service worker runtime and scope route persistence

This commit is contained in:
Amer Agovic
2026-05-11 13:51:47 -05:00
parent f5a739ee35
commit 6fe23fae86
5 changed files with 87 additions and 107 deletions
+7 -8
View File
@@ -29,7 +29,7 @@ The library provides an `App` component that manages the application shell, rout
```jsx ```jsx
import { App } from '@reliancy/bface/ui/App'; import { App } from '@reliancy/bface/ui/App';
import { CONFIG_KEYS } from '@reliancy/bface/platform/env'; 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 } = {}) { async function handleInit(services, { initialProfile } = {}) {
// Application profile (camelCase or snake_case field names are accepted where noted in env mapping) // 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 loadModule(moduleName, services);
} }
await registerServiceWorker(); await sw.registerServiceWorker();
return profile; return profile;
} }
@@ -65,12 +65,11 @@ function MyApp() {
- **`services.api_client`** — HTTP client (`get`, `post`, …) - **`services.api_client`** — HTTP client (`get`, `post`, …)
- **`services.storage`** — storage module (`getProvider`, …) - **`services.storage`** — storage module (`getProvider`, …)
- **`services.api_router`** — placeholder for service-worker API routing (when available)
- **`services.ui_router`** — UI routing helpers - **`services.ui_router`** — UI routing helpers
- **`services.menu`** — menu registration and queries - **`services.menu`** — menu registration and queries
- **`services.env`** — `initEnv`, `getConfig`, `setConfig`, `CONFIG_KEYS`, tracing helpers, etc. - **`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 ```jsx
async function handleInit(services) { async function handleInit(services) {
@@ -138,7 +137,7 @@ const menuItems = queryMenuItems('/primary');
### Exports (`package.json` → `exports`) ### Exports (`package.json` → `exports`)
- **`@reliancy/bface`** — main entry: platform, `App`, UI components index, general settings, security, and data helpers - **`@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` - **`@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. 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/api.js`** — API client
- **`platform/storage.js`** — storage abstraction (localStorage, IndexedDB, OPFS) - **`platform/storage.js`** — storage abstraction (localStorage, IndexedDB, OPFS)
- **`platform/menu.js`** — menu model and queries - **`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/compat.js`** — environment detection and compatibility
- **`platform/host.js`** — host detection (e.g. Electron) - **`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 // app.jsx
import { App } from '@reliancy/bface/ui/App'; import { App } from '@reliancy/bface/ui/App';
import { CONFIG_KEYS } from '@reliancy/bface/platform/env'; 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() { async function loadProfile() {
// Load your app profile (from JSON, API, etc.) // Load your app profile (from JSON, API, etc.)
@@ -225,7 +224,7 @@ async function handleInit(services, { initialProfile } = {}) {
} }
// 4. Register service worker // 4. Register service worker
await registerServiceWorker(); await sw.registerServiceWorker();
return profile; return profile;
} }
+1 -1
View File
@@ -9,7 +9,7 @@ export * from './platform/compat.js';
export * from './platform/env.js'; export * from './platform/env.js';
export * from './platform/menu.js'; export * from './platform/menu.js';
export * from './platform/storage.js'; export * from './platform/storage.js';
export * from './platform/sw-register.js'; export * from './platform/worker.js';
export * from './data/index.js'; export * from './data/index.js';
// Re-export UI components // Re-export UI components
+29 -3
View File
@@ -7,11 +7,20 @@
* added when needed. * added when needed.
*/ */
import { getConfig, setConfig } from './env.js'; import { CONFIG_KEYS, getConfig, setConfig } from './env.js';
import { getDesktopBridgeSafe, getHostKind, isElectronHost } from './host.js'; import { getDesktopBridgeSafe, getHostKind, isElectronHost } from './host.js';
// Config key for last visited route path // Config key for last visited route path
const LAST_PATH_CONFIG_KEY = 'router.lastPath'; 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) // 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 // If URL is empty or '/', check config for last visited path
try { 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 !== '/') { if (lastPath && typeof lastPath === 'string' && lastPath !== '/') {
return 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) { } catch (error) {
console.warn('[Compat] Failed to get last path from config:', 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) // Save to config for persistence (works on all platforms)
try { 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) { } catch (error) {
console.warn('[Compat] Failed to save path to config:', error); console.warn('[Compat] Failed to save path to config:', error);
} }
@@ -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'; 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_PATH = '/sw.js';
const SW_SCOPE = '/'; const SW_SCOPE = '/';
const DEV_SW_RESET_KEY = '__bface_dev_sw_reset__'; const DEV_SW_RESET_KEY = '__bface_dev_sw_reset__';
/**
* Clear all caches async function clearAllCaches() {
*/
export async function clearAllCaches() {
if ('caches' in window) { if ('caches' in window) {
try { try {
const cacheNames = await caches.keys(); const cacheNames = await caches.keys();
await Promise.all(cacheNames.map(name => { await Promise.all(cacheNames.map((name) => {
console.log('[SW] Clearing cache:', name); console.log('[SW] Clearing cache:', name);
return caches.delete(name); return caches.delete(name);
})); }));
@@ -29,14 +29,11 @@ export async function clearAllCaches() {
return 0; return 0;
} }
/** async function unregisterAllServiceWorkers() {
* Unregister all service workers
*/
export async function unregisterAllServiceWorkers() {
if ('serviceWorker' in navigator) { if ('serviceWorker' in navigator) {
try { try {
const registrations = await navigator.serviceWorker.getRegistrations(); 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); console.log('[SW] Unregistering service worker:', reg.scope);
return reg.unregister(); return reg.unregister();
})); }));
@@ -50,25 +47,19 @@ export async function unregisterAllServiceWorkers() {
return 0; return 0;
} }
/** async function clearAllStorage() {
* Clear all storage (localStorage, sessionStorage, IndexedDB)
*/
export async function clearAllStorage() {
try { try {
// Clear localStorage
localStorage.clear(); localStorage.clear();
console.log('[Storage] localStorage cleared'); console.log('[Storage] localStorage cleared');
// Clear sessionStorage
sessionStorage.clear(); sessionStorage.clear();
console.log('[Storage] sessionStorage cleared'); console.log('[Storage] sessionStorage cleared');
// Clear IndexedDB databases
if ('indexedDB' in window) { if ('indexedDB' in window) {
const databases = await indexedDB.databases(); const databases = await indexedDB.databases();
await Promise.all(databases.map(db => { await Promise.all(databases.map((db) => {
if (db.name) { if (db.name) {
return new Promise((resolve, reject) => { return new Promise((resolve) => {
const deleteReq = indexedDB.deleteDatabase(db.name); const deleteReq = indexedDB.deleteDatabase(db.name);
deleteReq.onsuccess = () => { deleteReq.onsuccess = () => {
console.log(`[Storage] IndexedDB database "${db.name}" deleted`); console.log(`[Storage] IndexedDB database "${db.name}" deleted`);
@@ -76,10 +67,11 @@ export async function clearAllStorage() {
}; };
deleteReq.onerror = () => { deleteReq.onerror = () => {
console.warn(`[Storage] Failed to delete IndexedDB database "${db.name}"`); console.warn(`[Storage] Failed to delete IndexedDB database "${db.name}"`);
resolve(); // Continue even if one fails resolve();
}; };
}); });
} }
return Promise.resolve();
})); }));
} }
@@ -91,25 +83,16 @@ export async function clearAllStorage() {
} }
} }
/** async function clearPWACache() {
* Clear everything: caches, service workers, and storage console.log('Clearing all PWA caches 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...');
try { try {
// 1. Unregister service workers
const swCount = await unregisterAllServiceWorkers(); const swCount = await unregisterAllServiceWorkers();
// 2. Clear all caches
const cacheCount = await clearAllCaches(); const cacheCount = await clearAllCaches();
// 3. Clear all storage
await clearAllStorage(); await clearAllStorage();
console.log(`PWA cache cleared: ${swCount} service worker(s), ${cacheCount} cache(s), and all storage`); 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('Reload the page to re-register service workers');
return { return {
serviceWorkers: swCount, serviceWorkers: swCount,
@@ -117,15 +100,12 @@ export async function clearPWACache() {
storage: true storage: true
}; };
} catch (error) { } catch (error) {
console.error('Failed to clear PWA cache:', error); console.error('Failed to clear PWA cache:', error);
throw error; throw error;
} }
} }
/** async function registerServiceWorker() {
* Register service worker
*/
export async function registerServiceWorker() {
if (isElectronHost() || isTauriHost()) { if (isElectronHost() || isTauriHost()) {
await unregisterAllServiceWorkers(); await unregisterAllServiceWorkers();
console.log('[SW] Skipping service worker registration in desktop host'); console.log('[SW] Skipping service worker registration in desktop host');
@@ -140,19 +120,14 @@ export async function registerServiceWorker() {
if ('serviceWorker' in navigator) { if ('serviceWorker' in navigator) {
try { try {
// Register or get existing service worker
const registration = await navigator.serviceWorker.register(SW_PATH, { const registration = await navigator.serviceWorker.register(SW_PATH, {
scope: SW_SCOPE, scope: SW_SCOPE,
updateViaCache: 'none' // Always fetch fresh service worker updateViaCache: 'none'
}); });
console.log('Service Worker registered:', registration); console.log('Service Worker registered:', registration);
// Force immediate update check to get latest version
await registration.update(); await registration.update();
// Handle updates
registration.addEventListener('updatefound', () => { registration.addEventListener('updatefound', () => {
const newWorker = registration.installing; const newWorker = registration.installing;
if (newWorker) { if (newWorker) {
@@ -160,7 +135,6 @@ export async function registerServiceWorker() {
if (newWorker.state === 'installed') { if (newWorker.state === 'installed') {
if (navigator.serviceWorker.controller) { if (navigator.serviceWorker.controller) {
console.log('[SW] New service worker available, reloading...'); console.log('[SW] New service worker available, reloading...');
// Force reload to activate new worker
window.location.reload(); window.location.reload();
} else { } else {
console.log('[SW] Service worker installed for the first time'); 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); console.error('Service Worker registration failed:', error);
throw error; throw error;
} }
} else {
console.warn('Service Workers are not supported');
return null;
} }
console.warn('Service Workers are not supported');
return null;
} }
/** async function resetServiceWorkers() {
* 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() {
if (isElectronHost() || isTauriHost()) { if (isElectronHost() || isTauriHost()) {
await unregisterAllServiceWorkers(); await unregisterAllServiceWorkers();
return false; return false;
@@ -215,7 +185,7 @@ export async function resetServiceWorkers() {
return true; return true;
} }
export async function getServiceWorkerStatus() { async function getServiceWorkerStatus() {
if (isElectronHost() || isTauriHost()) { if (isElectronHost() || isTauriHost()) {
return 'Desktop Disabled'; return 'Desktop Disabled';
} }
@@ -243,10 +213,7 @@ export async function getServiceWorkerStatus() {
} }
} }
/** async function unregisterServiceWorker() {
* Unregister service worker
*/
export async function unregisterServiceWorker() {
if (isElectronHost() || isTauriHost()) { if (isElectronHost() || isTauriHost()) {
return; return;
} }
@@ -260,3 +227,13 @@ export async function unregisterServiceWorker() {
} }
} }
export const sw = {
clearAllCaches,
unregisterAllServiceWorkers,
clearAllStorage,
clearPWACache,
registerServiceWorker,
resetServiceWorkers,
getServiceWorkerStatus,
unregisterServiceWorker
};
+8 -30
View File
@@ -7,12 +7,8 @@
import React, { createContext, useContext, useState, useEffect, useCallback, useRef, useMemo } from 'react'; import React, { createContext, useContext, useState, useEffect, useCallback, useRef, useMemo } from 'react';
import { TamaguiProvider, Theme, createTamagui, YStack } from 'tamagui'; import { TamaguiProvider, Theme, createTamagui, YStack } from 'tamagui';
import { import {
clearPWACache, sw
clearAllCaches, } from '../platform/worker.js';
clearAllStorage,
getServiceWorkerStatus,
unregisterAllServiceWorkers
} from '../platform/sw-register.js';
import { getProvider } from '../platform/storage.js'; import { getProvider } from '../platform/storage.js';
import * as apiClient from '../platform/api.js'; import * as apiClient from '../platform/api.js';
import * as storageModuleRef from '../platform/storage.js'; import * as storageModuleRef from '../platform/storage.js';
@@ -215,27 +211,9 @@ function App({
console.warn('[App] Failed to import Router module:', error); 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 { return {
api_client: apiClient.api, // Renamed from api api_client: apiClient.api, // Renamed from api
storage: storageModuleRef, storage: storageModuleRef,
api_router,
ui_router, ui_router,
menu: menuRef, menu: menuRef,
env: envModuleRef // New service env: envModuleRef // New service
@@ -313,7 +291,7 @@ function App({
appLogger.warn('Menu service not available'); appLogger.warn('Menu service not available');
} }
setSwStatus(await getServiceWorkerStatus()); setSwStatus(await sw.getServiceWorkerStatus());
setInitialized(true); setInitialized(true);
initTrace.end({ initTrace.end({
@@ -533,12 +511,12 @@ export { useApp, useTheme, THEME_MODES, themeManager as ThemeManager };
// Expose PWA utilities globally for console access // Expose PWA utilities globally for console access
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
window.clearPWACache = clearPWACache; window.clearPWACache = sw.clearPWACache;
window.__PWA_UTILS__ = { window.__PWA_UTILS__ = {
clearPWACache, clearPWACache: sw.clearPWACache,
clearAllCaches, clearAllCaches: sw.clearAllCaches,
unregisterAllServiceWorkers, unregisterAllServiceWorkers: sw.unregisterAllServiceWorkers,
clearAllStorage clearAllStorage: sw.clearAllStorage
}; };
console.log('💡 PWA Utilities available:'); console.log('💡 PWA Utilities available:');