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
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;
}
+1 -1
View File
@@ -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
+28 -2
View File
@@ -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 {
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);
}
@@ -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,10 +67,11 @@ 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();
}));
}
@@ -91,25 +83,16 @@ 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,
@@ -117,15 +100,12 @@ export async function clearPWACache() {
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;
}
}
/**
* 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
};
+8 -30
View File
@@ -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:');