Rename service worker runtime and scope route persistence
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
+29
-3
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
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
@@ -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:');
|
||||
|
||||
Reference in New Issue
Block a user