@shellui/core 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -0
- package/dist/ContentView-CZG-ro_B.js +146 -0
- package/dist/ContentView-CZG-ro_B.js.map +1 -0
- package/dist/CookiePreferencesView-MhO9FO-4.js +213 -0
- package/dist/CookiePreferencesView-MhO9FO-4.js.map +1 -0
- package/dist/DefaultLayout-Dbb3uJED.js +394 -0
- package/dist/DefaultLayout-Dbb3uJED.js.map +1 -0
- package/dist/FullscreenLayout-1SgPHWw-.js +30 -0
- package/dist/FullscreenLayout-1SgPHWw-.js.map +1 -0
- package/dist/HomeView-DYU-O_Il.js +21 -0
- package/dist/HomeView-DYU-O_Il.js.map +1 -0
- package/dist/NotFoundView-CeYjJNg0.js +52 -0
- package/dist/NotFoundView-CeYjJNg0.js.map +1 -0
- package/dist/OverlayShell-pzbqQW25.js +642 -0
- package/dist/OverlayShell-pzbqQW25.js.map +1 -0
- package/dist/SettingsView-Bndrta44.js +2207 -0
- package/dist/SettingsView-Bndrta44.js.map +1 -0
- package/dist/ViewRoute-ChSPabOy.js +32 -0
- package/dist/ViewRoute-ChSPabOy.js.map +1 -0
- package/dist/WindowsLayout-CXGNPKoY.js +633 -0
- package/dist/WindowsLayout-CXGNPKoY.js.map +1 -0
- package/dist/app.d.ts +3 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/components/ContentView.d.ts +10 -0
- package/dist/components/ContentView.d.ts.map +1 -0
- package/dist/components/HomeView.d.ts +2 -0
- package/dist/components/HomeView.d.ts.map +1 -0
- package/dist/components/LoadingOverlay.d.ts +2 -0
- package/dist/components/LoadingOverlay.d.ts.map +1 -0
- package/dist/components/NotFoundView.d.ts +2 -0
- package/dist/components/NotFoundView.d.ts.map +1 -0
- package/dist/components/RouteErrorBoundary.d.ts +2 -0
- package/dist/components/RouteErrorBoundary.d.ts.map +1 -0
- package/dist/components/ViewRoute.d.ts +7 -0
- package/dist/components/ViewRoute.d.ts.map +1 -0
- package/dist/components/ui/alert-dialog.d.ts +32 -0
- package/dist/components/ui/alert-dialog.d.ts.map +1 -0
- package/dist/components/ui/breadcrumb.d.ts +20 -0
- package/dist/components/ui/breadcrumb.d.ts.map +1 -0
- package/dist/components/ui/button-group.d.ts +7 -0
- package/dist/components/ui/button-group.d.ts.map +1 -0
- package/dist/components/ui/button.d.ts +12 -0
- package/dist/components/ui/button.d.ts.map +1 -0
- package/dist/components/ui/dialog.d.ts +24 -0
- package/dist/components/ui/dialog.d.ts.map +1 -0
- package/dist/components/ui/drawer.d.ts +38 -0
- package/dist/components/ui/drawer.d.ts.map +1 -0
- package/dist/components/ui/select.d.ts +5 -0
- package/dist/components/ui/select.d.ts.map +1 -0
- package/dist/components/ui/sidebar.d.ts +46 -0
- package/dist/components/ui/sidebar.d.ts.map +1 -0
- package/dist/components/ui/sonner.d.ts +6 -0
- package/dist/components/ui/sonner.d.ts.map +1 -0
- package/dist/components/ui/switch.d.ts +8 -0
- package/dist/components/ui/switch.d.ts.map +1 -0
- package/dist/constants/urls.d.ts +6 -0
- package/dist/constants/urls.d.ts.map +1 -0
- package/dist/constants/urls.js +8 -0
- package/dist/constants/urls.js.map +1 -0
- package/dist/features/alertDialog/DialogContext.d.ts +12 -0
- package/dist/features/alertDialog/DialogContext.d.ts.map +1 -0
- package/dist/features/config/ConfigProvider.d.ts +15 -0
- package/dist/features/config/ConfigProvider.d.ts.map +1 -0
- package/dist/features/config/types.d.ts +177 -0
- package/dist/features/config/types.d.ts.map +1 -0
- package/dist/features/config/useConfig.d.ts +8 -0
- package/dist/features/config/useConfig.d.ts.map +1 -0
- package/dist/features/cookieConsent/CookieConsentModal.d.ts +6 -0
- package/dist/features/cookieConsent/CookieConsentModal.d.ts.map +1 -0
- package/dist/features/cookieConsent/CookiePreferencesView.d.ts +2 -0
- package/dist/features/cookieConsent/CookiePreferencesView.d.ts.map +1 -0
- package/dist/features/cookieConsent/cookieConsent.d.ts +22 -0
- package/dist/features/cookieConsent/cookieConsent.d.ts.map +1 -0
- package/dist/features/cookieConsent/useCookieConsent.d.ts +15 -0
- package/dist/features/cookieConsent/useCookieConsent.d.ts.map +1 -0
- package/dist/features/drawer/DrawerContext.d.ts +24 -0
- package/dist/features/drawer/DrawerContext.d.ts.map +1 -0
- package/dist/features/layouts/AppLayout.d.ts +12 -0
- package/dist/features/layouts/AppLayout.d.ts.map +1 -0
- package/dist/features/layouts/DefaultLayout.d.ts +10 -0
- package/dist/features/layouts/DefaultLayout.d.ts.map +1 -0
- package/dist/features/layouts/FullscreenLayout.d.ts +9 -0
- package/dist/features/layouts/FullscreenLayout.d.ts.map +1 -0
- package/dist/features/layouts/LayoutProviders.d.ts +9 -0
- package/dist/features/layouts/LayoutProviders.d.ts.map +1 -0
- package/dist/features/layouts/OverlayShell.d.ts +10 -0
- package/dist/features/layouts/OverlayShell.d.ts.map +1 -0
- package/dist/features/layouts/WindowsLayout.d.ts +24 -0
- package/dist/features/layouts/WindowsLayout.d.ts.map +1 -0
- package/dist/features/layouts/utils.d.ts +16 -0
- package/dist/features/layouts/utils.d.ts.map +1 -0
- package/dist/features/modal/ModalContext.d.ts +20 -0
- package/dist/features/modal/ModalContext.d.ts.map +1 -0
- package/dist/features/sentry/initSentry.d.ts +14 -0
- package/dist/features/sentry/initSentry.d.ts.map +1 -0
- package/dist/features/settings/SettingsContext.d.ts +10 -0
- package/dist/features/settings/SettingsContext.d.ts.map +1 -0
- package/dist/features/settings/SettingsIcons.d.ts +22 -0
- package/dist/features/settings/SettingsIcons.d.ts.map +1 -0
- package/dist/features/settings/SettingsProvider.d.ts +5 -0
- package/dist/features/settings/SettingsProvider.d.ts.map +1 -0
- package/dist/features/settings/SettingsRoutes.d.ts +7 -0
- package/dist/features/settings/SettingsRoutes.d.ts.map +1 -0
- package/dist/features/settings/SettingsView.d.ts +2 -0
- package/dist/features/settings/SettingsView.d.ts.map +1 -0
- package/dist/features/settings/components/Advanced.d.ts +2 -0
- package/dist/features/settings/components/Advanced.d.ts.map +1 -0
- package/dist/features/settings/components/Appearance.d.ts +2 -0
- package/dist/features/settings/components/Appearance.d.ts.map +1 -0
- package/dist/features/settings/components/DataPrivacy.d.ts +2 -0
- package/dist/features/settings/components/DataPrivacy.d.ts.map +1 -0
- package/dist/features/settings/components/Develop.d.ts +2 -0
- package/dist/features/settings/components/Develop.d.ts.map +1 -0
- package/dist/features/settings/components/LanguageAndRegion.d.ts +2 -0
- package/dist/features/settings/components/LanguageAndRegion.d.ts.map +1 -0
- package/dist/features/settings/components/ServiceWorker.d.ts +2 -0
- package/dist/features/settings/components/ServiceWorker.d.ts.map +1 -0
- package/dist/features/settings/components/UpdateApp.d.ts +2 -0
- package/dist/features/settings/components/UpdateApp.d.ts.map +1 -0
- package/dist/features/settings/components/develop/DialogTestButtons.d.ts +2 -0
- package/dist/features/settings/components/develop/DialogTestButtons.d.ts.map +1 -0
- package/dist/features/settings/components/develop/DrawerTestButtons.d.ts +2 -0
- package/dist/features/settings/components/develop/DrawerTestButtons.d.ts.map +1 -0
- package/dist/features/settings/components/develop/ModalTestButtons.d.ts +2 -0
- package/dist/features/settings/components/develop/ModalTestButtons.d.ts.map +1 -0
- package/dist/features/settings/components/develop/ToastTestButtons.d.ts +2 -0
- package/dist/features/settings/components/develop/ToastTestButtons.d.ts.map +1 -0
- package/dist/features/settings/hooks/useSettings.d.ts +2 -0
- package/dist/features/settings/hooks/useSettings.d.ts.map +1 -0
- package/dist/features/sonner/SonnerContext.d.ts +29 -0
- package/dist/features/sonner/SonnerContext.d.ts.map +1 -0
- package/dist/features/theme/ThemeProvider.d.ts +11 -0
- package/dist/features/theme/ThemeProvider.d.ts.map +1 -0
- package/dist/features/theme/themes.d.ts +114 -0
- package/dist/features/theme/themes.d.ts.map +1 -0
- package/dist/features/theme/useTheme.d.ts +10 -0
- package/dist/features/theme/useTheme.d.ts.map +1 -0
- package/dist/i18n/I18nProvider.d.ts +9 -0
- package/dist/i18n/I18nProvider.d.ts.map +1 -0
- package/dist/i18n/config.d.ts +23 -0
- package/dist/i18n/config.d.ts.map +1 -0
- package/dist/i18n/translations/en/common.json.d.ts +19 -0
- package/dist/i18n/translations/en/cookieConsent.json.d.ts +53 -0
- package/dist/i18n/translations/en/settings.json.d.ts +358 -0
- package/dist/i18n/translations/fr/common.json.d.ts +19 -0
- package/dist/i18n/translations/fr/cookieConsent.json.d.ts +53 -0
- package/dist/i18n/translations/fr/settings.json.d.ts +358 -0
- package/dist/index-lmRk5L6z.js +2160 -0
- package/dist/index-lmRk5L6z.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/z-index.d.ts +29 -0
- package/dist/lib/z-index.d.ts.map +1 -0
- package/dist/router/router.d.ts +3 -0
- package/dist/router/router.d.ts.map +1 -0
- package/dist/router/routes.d.ts +4 -0
- package/dist/router/routes.d.ts.map +1 -0
- package/dist/sidebar-ClIeZ2zb.js +303 -0
- package/dist/sidebar-ClIeZ2zb.js.map +1 -0
- package/dist/style.css +1 -0
- package/dist/switch-8SzUJz7Q.js +44 -0
- package/dist/switch-8SzUJz7Q.js.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +93 -0
- package/postcss.config.js +6 -0
- package/src/app.tsx +119 -0
- package/src/components/ContentView.tsx +258 -0
- package/src/components/HomeView.tsx +19 -0
- package/src/components/LoadingOverlay.tsx +12 -0
- package/src/components/NotFoundView.tsx +84 -0
- package/src/components/RouteErrorBoundary.tsx +95 -0
- package/src/components/ViewRoute.tsx +47 -0
- package/src/components/ui/alert-dialog.tsx +181 -0
- package/src/components/ui/breadcrumb.tsx +155 -0
- package/src/components/ui/button-group.tsx +52 -0
- package/src/components/ui/button.tsx +51 -0
- package/src/components/ui/dialog.tsx +160 -0
- package/src/components/ui/drawer.tsx +200 -0
- package/src/components/ui/select.tsx +24 -0
- package/src/components/ui/sidebar.tsx +406 -0
- package/src/components/ui/sonner.tsx +36 -0
- package/src/components/ui/switch.tsx +45 -0
- package/src/constants/urls.ts +4 -0
- package/src/features/alertDialog/DialogContext.tsx +468 -0
- package/src/features/config/ConfigProvider.ts +96 -0
- package/src/features/config/types.ts +195 -0
- package/src/features/config/useConfig.ts +15 -0
- package/src/features/cookieConsent/CookieConsentModal.tsx +122 -0
- package/src/features/cookieConsent/CookiePreferencesView.tsx +328 -0
- package/src/features/cookieConsent/cookieConsent.ts +84 -0
- package/src/features/cookieConsent/useCookieConsent.ts +39 -0
- package/src/features/drawer/DrawerContext.tsx +116 -0
- package/src/features/layouts/AppLayout.tsx +63 -0
- package/src/features/layouts/DefaultLayout.tsx +625 -0
- package/src/features/layouts/FullscreenLayout.tsx +55 -0
- package/src/features/layouts/LayoutProviders.tsx +20 -0
- package/src/features/layouts/OverlayShell.tsx +171 -0
- package/src/features/layouts/WindowsLayout.tsx +860 -0
- package/src/features/layouts/utils.ts +99 -0
- package/src/features/modal/ModalContext.tsx +112 -0
- package/src/features/sentry/initSentry.ts +72 -0
- package/src/features/settings/SettingsContext.tsx +19 -0
- package/src/features/settings/SettingsIcons.tsx +452 -0
- package/src/features/settings/SettingsProvider.tsx +341 -0
- package/src/features/settings/SettingsRoutes.tsx +66 -0
- package/src/features/settings/SettingsView.tsx +327 -0
- package/src/features/settings/components/Advanced.tsx +128 -0
- package/src/features/settings/components/Appearance.tsx +306 -0
- package/src/features/settings/components/DataPrivacy.tsx +142 -0
- package/src/features/settings/components/Develop.tsx +174 -0
- package/src/features/settings/components/LanguageAndRegion.tsx +329 -0
- package/src/features/settings/components/ServiceWorker.tsx +363 -0
- package/src/features/settings/components/UpdateApp.tsx +206 -0
- package/src/features/settings/components/develop/DialogTestButtons.tsx +137 -0
- package/src/features/settings/components/develop/DrawerTestButtons.tsx +67 -0
- package/src/features/settings/components/develop/ModalTestButtons.tsx +30 -0
- package/src/features/settings/components/develop/ToastTestButtons.tsx +179 -0
- package/src/features/settings/hooks/useSettings.tsx +10 -0
- package/src/features/sonner/SonnerContext.tsx +286 -0
- package/src/features/theme/ThemeProvider.tsx +16 -0
- package/src/features/theme/themes.ts +561 -0
- package/src/features/theme/useTheme.tsx +71 -0
- package/src/i18n/I18nProvider.tsx +32 -0
- package/src/i18n/config.ts +107 -0
- package/src/i18n/translations/en/common.json +16 -0
- package/src/i18n/translations/en/cookieConsent.json +50 -0
- package/src/i18n/translations/en/settings.json +355 -0
- package/src/i18n/translations/fr/common.json +16 -0
- package/src/i18n/translations/fr/cookieConsent.json +50 -0
- package/src/i18n/translations/fr/settings.json +355 -0
- package/src/index.css +412 -0
- package/src/index.html +100 -0
- package/src/index.ts +31 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/z-index.ts +29 -0
- package/src/main.tsx +26 -0
- package/src/router/router.tsx +8 -0
- package/src/router/routes.tsx +115 -0
- package/src/service-worker/register.ts +1199 -0
- package/src/service-worker/sw-dev.ts +87 -0
- package/src/service-worker/sw.ts +105 -0
- package/tailwind.config.js +60 -0
|
@@ -0,0 +1,1199 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
import { Workbox } from 'workbox-window';
|
|
3
|
+
import { shellui, getLogger } from '@shellui/sdk';
|
|
4
|
+
|
|
5
|
+
const logger = getLogger('shellcore');
|
|
6
|
+
|
|
7
|
+
let wb: Workbox | null = null;
|
|
8
|
+
let updateAvailable = false;
|
|
9
|
+
let waitingServiceWorker: ServiceWorker | null = null;
|
|
10
|
+
let registrationPromise: Promise<void> | null = null;
|
|
11
|
+
let statusListeners: Array<(status: { registered: boolean; updateAvailable: boolean }) => void> =
|
|
12
|
+
[];
|
|
13
|
+
let isInitialRegistration = false; // Track if this is the first registration (no reload needed)
|
|
14
|
+
let eventListenersAdded = false; // Track if event listeners have been added to prevent duplicates
|
|
15
|
+
let toastShownForServiceWorkerId: string | null = null; // Track which service worker we've shown toast for (prevents duplicates within same page load)
|
|
16
|
+
let isIntentionalUpdate = false; // Track if we're performing an intentional update (user clicked Install Now)
|
|
17
|
+
// CRITICAL: Track registration state to prevent disabling during registration or immediately after page load
|
|
18
|
+
// Initialize start time to page load time to provide grace period immediately after refresh
|
|
19
|
+
// This prevents race conditions where error handlers fire before registration completes
|
|
20
|
+
let isRegistering = false; // Track if registration is currently in progress
|
|
21
|
+
let registrationStartTime = typeof window !== 'undefined' ? Date.now() : 0; // Track when registration started (initialize to page load time)
|
|
22
|
+
const REGISTRATION_GRACE_PERIOD = 5000; // Don't auto-disable within 5 seconds of page load/registration start
|
|
23
|
+
|
|
24
|
+
// Store event handler references so we can remove them if needed
|
|
25
|
+
type EventHandler = (event?: unknown) => void;
|
|
26
|
+
let waitingHandler: EventHandler | null = null;
|
|
27
|
+
let activatedHandler: EventHandler | null = null;
|
|
28
|
+
let controllingHandler: EventHandler | null = null;
|
|
29
|
+
let registeredHandler: EventHandler | null = null;
|
|
30
|
+
let redundantHandler: EventHandler | null = null;
|
|
31
|
+
let serviceWorkerErrorHandler: EventHandler | null = null;
|
|
32
|
+
let messageErrorHandler: EventHandler | null = null;
|
|
33
|
+
|
|
34
|
+
/** Global set by host or by us from config so Tauri can be forced (e.g. when __TAURI__ is not yet injected in dev). */
|
|
35
|
+
declare global {
|
|
36
|
+
interface Window {
|
|
37
|
+
__SHELLUI_TAURI__?: boolean;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function hasTauriOnWindow(w: Window | null): boolean {
|
|
42
|
+
if (!w) return false;
|
|
43
|
+
const o = w as Window & {
|
|
44
|
+
__TAURI__?: unknown;
|
|
45
|
+
__TAURI_INTERNALS__?: unknown;
|
|
46
|
+
__SHELLUI_TAURI__?: boolean;
|
|
47
|
+
};
|
|
48
|
+
if (o.__SHELLUI_TAURI__ === true) return true;
|
|
49
|
+
return !!(o.__TAURI__ ?? o.__TAURI_INTERNALS__);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** True when the app is running inside Tauri (desktop). Service worker is disabled there; a different caching system is used. */
|
|
53
|
+
export function isTauri(): boolean {
|
|
54
|
+
if (typeof window === 'undefined') return false;
|
|
55
|
+
if (hasTauriOnWindow(window)) return true;
|
|
56
|
+
try {
|
|
57
|
+
if (window !== window.top && hasTauriOnWindow(window.top)) return true;
|
|
58
|
+
if (window.parent && window.parent !== window && hasTauriOnWindow(window.parent)) return true;
|
|
59
|
+
} catch {
|
|
60
|
+
// Cross-origin: can't access top/parent
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Cache for service worker file existence check to avoid duplicate fetches
|
|
66
|
+
let swFileExistsCache: Promise<boolean> | null = null;
|
|
67
|
+
let swFileExistsCacheTime = 0;
|
|
68
|
+
const SW_FILE_EXISTS_CACHE_TTL = 5000; // Cache for 5 seconds
|
|
69
|
+
|
|
70
|
+
// Notify all listeners of status changes
|
|
71
|
+
async function notifyStatusListeners() {
|
|
72
|
+
const registered = await isServiceWorkerRegistered();
|
|
73
|
+
const status = {
|
|
74
|
+
registered,
|
|
75
|
+
updateAvailable,
|
|
76
|
+
};
|
|
77
|
+
statusListeners.forEach((listener) => listener(status));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface ServiceWorkerRegistrationOptions {
|
|
81
|
+
enabled: boolean;
|
|
82
|
+
onUpdateAvailable?: () => void;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Disable caching automatically when errors occur
|
|
87
|
+
* This helps prevent hard-to-debug issues
|
|
88
|
+
*/
|
|
89
|
+
async function disableCachingAutomatically(reason: string): Promise<void> {
|
|
90
|
+
// CRITICAL: Don't disable if registration is in progress or just started
|
|
91
|
+
// This prevents race conditions where errors fire during registration
|
|
92
|
+
const timeSinceRegistrationStart = Date.now() - registrationStartTime;
|
|
93
|
+
if (isRegistering || timeSinceRegistrationStart < REGISTRATION_GRACE_PERIOD) {
|
|
94
|
+
console.warn(
|
|
95
|
+
`[Service Worker] NOT disabling - registration in progress or within grace period. Reason: ${reason}, isRegistering: ${isRegistering}, timeSinceStart: ${timeSinceRegistrationStart}ms`,
|
|
96
|
+
);
|
|
97
|
+
logger.warn(
|
|
98
|
+
`Not disabling service worker - registration in progress or within grace period: ${reason}`,
|
|
99
|
+
);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
logger.error(`Auto-disabling caching due to error: ${reason}`);
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
// Unregister service worker first
|
|
107
|
+
await unregisterServiceWorker();
|
|
108
|
+
|
|
109
|
+
// Disable service worker in settings
|
|
110
|
+
// We need to access settings through localStorage since we're in a module
|
|
111
|
+
if (typeof window !== 'undefined') {
|
|
112
|
+
const STORAGE_KEY = 'shellui:settings';
|
|
113
|
+
try {
|
|
114
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
115
|
+
if (stored) {
|
|
116
|
+
const settings = JSON.parse(stored);
|
|
117
|
+
// Only update if service worker is currently enabled to avoid unnecessary updates
|
|
118
|
+
if (settings.serviceWorker?.enabled !== false) {
|
|
119
|
+
settings.serviceWorker = { enabled: false };
|
|
120
|
+
// Migrate legacy key so old cached shape is updated
|
|
121
|
+
if (settings.caching !== undefined) {
|
|
122
|
+
delete settings.caching;
|
|
123
|
+
}
|
|
124
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
|
|
125
|
+
|
|
126
|
+
// Notify the app to reload settings via message system
|
|
127
|
+
shellui.sendMessageToParent({
|
|
128
|
+
type: 'SHELLUI_SETTINGS_UPDATED',
|
|
129
|
+
payload: { settings },
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Also dispatch event for local listeners
|
|
133
|
+
window.dispatchEvent(
|
|
134
|
+
new CustomEvent('shellui:settings-updated', {
|
|
135
|
+
detail: { settings },
|
|
136
|
+
}),
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
// Show a toast notification
|
|
140
|
+
shellui.toast({
|
|
141
|
+
title: 'Service Worker Disabled',
|
|
142
|
+
description: `The service worker has been automatically disabled due to an error: ${reason}`,
|
|
143
|
+
type: 'error',
|
|
144
|
+
duration: 10000,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
// No settings stored, create default with service worker disabled
|
|
149
|
+
const defaultSettings = {
|
|
150
|
+
serviceWorker: { enabled: false },
|
|
151
|
+
};
|
|
152
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(defaultSettings));
|
|
153
|
+
|
|
154
|
+
shellui.toast({
|
|
155
|
+
title: 'Service Worker Disabled',
|
|
156
|
+
description: `The service worker has been automatically disabled due to an error: ${reason}`,
|
|
157
|
+
type: 'error',
|
|
158
|
+
duration: 10000,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
} catch (error) {
|
|
162
|
+
logger.error('Failed to disable service worker in settings:', { error });
|
|
163
|
+
// Still show toast even if settings update fails
|
|
164
|
+
shellui.toast({
|
|
165
|
+
title: 'Service Worker Error',
|
|
166
|
+
description: `Service worker error: ${reason}. Please disable it manually in settings.`,
|
|
167
|
+
type: 'error',
|
|
168
|
+
duration: 10000,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
} catch (error) {
|
|
173
|
+
logger.error('Failed to disable caching automatically:', { error });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Check if service worker file exists
|
|
179
|
+
* Uses caching to prevent duplicate fetches when called concurrently
|
|
180
|
+
*/
|
|
181
|
+
export async function serviceWorkerFileExists(): Promise<boolean> {
|
|
182
|
+
const now = Date.now();
|
|
183
|
+
|
|
184
|
+
// Return cached promise if it's still valid and in progress
|
|
185
|
+
if (swFileExistsCache && now - swFileExistsCacheTime < SW_FILE_EXISTS_CACHE_TTL) {
|
|
186
|
+
return swFileExistsCache;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Create a new fetch promise and cache it
|
|
190
|
+
swFileExistsCache = (async () => {
|
|
191
|
+
try {
|
|
192
|
+
// Use a timestamp to prevent caching
|
|
193
|
+
const response = await fetch(`/sw.js?t=${Date.now()}`, {
|
|
194
|
+
method: 'GET',
|
|
195
|
+
cache: 'no-store',
|
|
196
|
+
headers: {
|
|
197
|
+
'Cache-Control': 'no-cache',
|
|
198
|
+
Pragma: 'no-cache',
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// Handle 500 errors - server error, but don't disable caching immediately
|
|
203
|
+
// This might be a transient server issue, just return false
|
|
204
|
+
if (response.status >= 500) {
|
|
205
|
+
console.warn(
|
|
206
|
+
`[Service Worker] Server error (${response.status}) when fetching service worker - not disabling`,
|
|
207
|
+
);
|
|
208
|
+
logger.warn(
|
|
209
|
+
`Server error (${response.status}) when fetching service worker - not disabling to avoid false positives`,
|
|
210
|
+
);
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// If not ok or 404, file doesn't exist
|
|
215
|
+
if (!response.ok || response.status === 404) {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Check content type - should be JavaScript
|
|
220
|
+
const contentType = response.headers.get('content-type') || '';
|
|
221
|
+
const isJavaScript =
|
|
222
|
+
contentType.includes('javascript') ||
|
|
223
|
+
contentType.includes('application/javascript') ||
|
|
224
|
+
contentType.includes('text/javascript');
|
|
225
|
+
|
|
226
|
+
// If content type is HTML, it's likely Vite's dev server returning index.html
|
|
227
|
+
// which means the file doesn't exist
|
|
228
|
+
if (contentType.includes('text/html')) {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Try to read a bit of the content to verify it's actually a service worker
|
|
233
|
+
const text = await response.text();
|
|
234
|
+
// Service worker files typically start with imports or have workbox/precache references
|
|
235
|
+
const looksLikeServiceWorker =
|
|
236
|
+
text.includes('workbox') ||
|
|
237
|
+
text.includes('precache') ||
|
|
238
|
+
text.includes('serviceWorker') ||
|
|
239
|
+
text.includes('self.addEventListener');
|
|
240
|
+
|
|
241
|
+
// If we got a response but it doesn't look like a service worker, log warning but don't disable
|
|
242
|
+
// This could be a dev server issue or temporary problem
|
|
243
|
+
if (!isJavaScript && !looksLikeServiceWorker) {
|
|
244
|
+
console.warn('[Service Worker] File does not look like a service worker - not disabling');
|
|
245
|
+
logger.warn(
|
|
246
|
+
'Service worker file appears to be invalid or corrupted - not disabling to avoid false positives',
|
|
247
|
+
);
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return isJavaScript || looksLikeServiceWorker;
|
|
252
|
+
} catch (error) {
|
|
253
|
+
// Network errors - don't disable caching, might be offline or transient issue
|
|
254
|
+
if (error instanceof TypeError && error.message.includes('Failed to fetch')) {
|
|
255
|
+
// Don't disable on network errors - might be offline
|
|
256
|
+
console.warn('[Service Worker] Network error when checking file existence - not disabling');
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
// Other errors - log but don't disable, could be transient
|
|
260
|
+
console.warn('[Service Worker] Error checking file existence - not disabling:', error);
|
|
261
|
+
logger.warn(
|
|
262
|
+
'Network error checking service worker file - not disabling to avoid false positives',
|
|
263
|
+
{ error },
|
|
264
|
+
);
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
})();
|
|
268
|
+
|
|
269
|
+
swFileExistsCacheTime = now;
|
|
270
|
+
return swFileExistsCache;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Register the service worker and handle updates
|
|
275
|
+
*/
|
|
276
|
+
export async function registerServiceWorker(
|
|
277
|
+
options: ServiceWorkerRegistrationOptions = { enabled: true },
|
|
278
|
+
): Promise<void> {
|
|
279
|
+
if (!('serviceWorker' in navigator)) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (isTauri()) {
|
|
284
|
+
await unregisterServiceWorker();
|
|
285
|
+
wb = null;
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (!options.enabled) {
|
|
290
|
+
// If disabled, unregister existing service worker
|
|
291
|
+
await unregisterServiceWorker();
|
|
292
|
+
wb = null;
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Check if service worker file exists (works in both dev and production)
|
|
297
|
+
const swExists = await serviceWorkerFileExists();
|
|
298
|
+
if (!swExists) {
|
|
299
|
+
// In dev mode, the service worker might not be ready yet, try again after a short delay
|
|
300
|
+
// Only retry once to avoid infinite loops
|
|
301
|
+
if (!registrationPromise) {
|
|
302
|
+
setTimeout(async () => {
|
|
303
|
+
const retryExists = await serviceWorkerFileExists();
|
|
304
|
+
if (retryExists && !registrationPromise) {
|
|
305
|
+
// Retry registration if file becomes available
|
|
306
|
+
registerServiceWorker(options);
|
|
307
|
+
} else if (!retryExists) {
|
|
308
|
+
logger.warn('Service worker file not found. Service workers may not be available.');
|
|
309
|
+
}
|
|
310
|
+
}, 1000);
|
|
311
|
+
}
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// If already registering, wait for that to complete
|
|
316
|
+
if (registrationPromise) {
|
|
317
|
+
return registrationPromise;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
registrationPromise = (async () => {
|
|
321
|
+
// CRITICAL: Mark that registration is starting to prevent auto-disable during registration
|
|
322
|
+
isRegistering = true;
|
|
323
|
+
registrationStartTime = Date.now();
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
// Check if service worker is already registered
|
|
327
|
+
const existingRegistration = await navigator.serviceWorker.getRegistration();
|
|
328
|
+
|
|
329
|
+
// CRITICAL: If there's a waiting service worker on page load, automatically activate it
|
|
330
|
+
// The user refreshed the page, so they want the new version - activate it automatically
|
|
331
|
+
// This ensures the new version is used without requiring manual "Install Now" click
|
|
332
|
+
if (existingRegistration?.waiting && !existingRegistration.installing) {
|
|
333
|
+
// There's a waiting service worker from before the refresh
|
|
334
|
+
// Since the user refreshed, they want the new version - activate it automatically
|
|
335
|
+
console.info(
|
|
336
|
+
'[Service Worker] Waiting service worker found on page load - automatically activating',
|
|
337
|
+
);
|
|
338
|
+
logger.info(
|
|
339
|
+
'Waiting service worker found on page load - automatically activating since user refreshed',
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
// Store reference to waiting service worker
|
|
343
|
+
const waitingSW = existingRegistration.waiting;
|
|
344
|
+
waitingServiceWorker = waitingSW;
|
|
345
|
+
|
|
346
|
+
// CRITICAL: Mark that we're auto-activating so the controlling handler knows to reload
|
|
347
|
+
// Store in sessionStorage so it survives if there's a reload
|
|
348
|
+
if (typeof window !== 'undefined') {
|
|
349
|
+
sessionStorage.setItem('shellui:service-worker:auto-activated', 'true');
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Automatically activate the waiting service worker
|
|
353
|
+
// This is safe because the user already refreshed, so they want the new version
|
|
354
|
+
// The controlling event handler will detect the auto-activation and reload the page
|
|
355
|
+
try {
|
|
356
|
+
waitingSW.postMessage({ type: 'SKIP_WAITING' });
|
|
357
|
+
console.info(
|
|
358
|
+
'[Service Worker] Sent SKIP_WAITING to waiting service worker - will reload when it takes control',
|
|
359
|
+
);
|
|
360
|
+
} catch (error) {
|
|
361
|
+
logger.error('Failed to activate waiting service worker:', { error });
|
|
362
|
+
// Clear the flag on error
|
|
363
|
+
if (typeof window !== 'undefined') {
|
|
364
|
+
sessionStorage.removeItem('shellui:service-worker:auto-activated');
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (existingRegistration && wb) {
|
|
370
|
+
// Already registered, just update
|
|
371
|
+
isInitialRegistration = false; // This is an update check
|
|
372
|
+
wb.update();
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Check if there's already a service worker controlling the page
|
|
377
|
+
// If not, this is an initial registration (don't reload)
|
|
378
|
+
isInitialRegistration = !navigator.serviceWorker.controller;
|
|
379
|
+
|
|
380
|
+
// Register the service worker
|
|
381
|
+
// Only create new Workbox instance if one doesn't exist
|
|
382
|
+
const isNewWorkbox = !wb;
|
|
383
|
+
if (!wb) {
|
|
384
|
+
// Use updateViaCache: 'none' to ensure service worker file changes are always detected
|
|
385
|
+
// This bypasses the browser cache when checking for updates to sw.js/sw-dev.js
|
|
386
|
+
wb = new Workbox('/sw.js', {
|
|
387
|
+
type: 'classic',
|
|
388
|
+
updateViaCache: 'none', // Always check network for service worker updates
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Remove old listeners if they exist (handles React Strict Mode double-mounting)
|
|
393
|
+
// Always remove listeners if wb exists and we have handler references, regardless of flag
|
|
394
|
+
// This ensures we clean up properly even if the flag was reset
|
|
395
|
+
if (wb) {
|
|
396
|
+
if (waitingHandler) {
|
|
397
|
+
wb.removeEventListener('waiting', waitingHandler);
|
|
398
|
+
waitingHandler = null;
|
|
399
|
+
}
|
|
400
|
+
if (activatedHandler) {
|
|
401
|
+
wb.removeEventListener('activated', activatedHandler);
|
|
402
|
+
activatedHandler = null;
|
|
403
|
+
}
|
|
404
|
+
if (controllingHandler) {
|
|
405
|
+
wb.removeEventListener('controlling', controllingHandler);
|
|
406
|
+
controllingHandler = null;
|
|
407
|
+
}
|
|
408
|
+
if (registeredHandler) {
|
|
409
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
410
|
+
(wb as any).removeEventListener('registered', registeredHandler);
|
|
411
|
+
registeredHandler = null;
|
|
412
|
+
}
|
|
413
|
+
if (redundantHandler) {
|
|
414
|
+
wb.removeEventListener('redundant', redundantHandler);
|
|
415
|
+
redundantHandler = null;
|
|
416
|
+
}
|
|
417
|
+
if (serviceWorkerErrorHandler) {
|
|
418
|
+
navigator.serviceWorker.removeEventListener('error', serviceWorkerErrorHandler);
|
|
419
|
+
serviceWorkerErrorHandler = null;
|
|
420
|
+
}
|
|
421
|
+
if (messageErrorHandler) {
|
|
422
|
+
navigator.serviceWorker.removeEventListener('messageerror', messageErrorHandler);
|
|
423
|
+
messageErrorHandler = null;
|
|
424
|
+
}
|
|
425
|
+
// Reset flag so we can add listeners again
|
|
426
|
+
eventListenersAdded = false;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Only add event listeners once (when creating a new Workbox instance or if they were removed)
|
|
430
|
+
if (isNewWorkbox && !eventListenersAdded) {
|
|
431
|
+
// Set flag IMMEDIATELY to prevent duplicate listener registration
|
|
432
|
+
eventListenersAdded = true;
|
|
433
|
+
|
|
434
|
+
// Handle service worker updates
|
|
435
|
+
// CRITICAL: Use a consistent toast ID to prevent duplicate toasts
|
|
436
|
+
// If the handler is called multiple times (event listener + manual call),
|
|
437
|
+
// using the same ID will update the existing toast instead of creating a new one
|
|
438
|
+
const UPDATE_AVAILABLE_TOAST_ID = 'shellui:update-available';
|
|
439
|
+
|
|
440
|
+
waitingHandler = async () => {
|
|
441
|
+
try {
|
|
442
|
+
// CRITICAL: If we're auto-activating (user refreshed), don't show toast
|
|
443
|
+
// The service worker will activate automatically and page will reload
|
|
444
|
+
const isAutoActivating =
|
|
445
|
+
typeof window !== 'undefined' &&
|
|
446
|
+
sessionStorage.getItem('shellui:service-worker:auto-activated') === 'true';
|
|
447
|
+
|
|
448
|
+
// Get the waiting service worker
|
|
449
|
+
const registration = await navigator.serviceWorker.getRegistration();
|
|
450
|
+
if (!registration || !registration.waiting) {
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const currentWaitingSW = registration.waiting;
|
|
455
|
+
const currentWaitingSWId = currentWaitingSW.scriptURL;
|
|
456
|
+
|
|
457
|
+
// CRITICAL: If auto-activating, update state but skip toast
|
|
458
|
+
if (isAutoActivating) {
|
|
459
|
+
console.info(
|
|
460
|
+
'[Service Worker] Waiting event fired during auto-activation - skipping toast',
|
|
461
|
+
);
|
|
462
|
+
updateAvailable = true;
|
|
463
|
+
waitingServiceWorker = currentWaitingSW;
|
|
464
|
+
notifyStatusListeners();
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// CRITICAL: Check flag BEFORE updating state to prevent race conditions
|
|
469
|
+
// If we've already shown toast for this service worker in this page load, don't show again
|
|
470
|
+
// This prevents duplicate toasts within the same page load, but allows showing after refresh
|
|
471
|
+
if (toastShownForServiceWorkerId === currentWaitingSWId) {
|
|
472
|
+
// Already shown, but ensure state is correct
|
|
473
|
+
updateAvailable = true;
|
|
474
|
+
waitingServiceWorker = currentWaitingSW;
|
|
475
|
+
notifyStatusListeners();
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// CRITICAL: Mark that we've shown toast for this service worker IMMEDIATELY
|
|
480
|
+
// This must happen BEFORE showing the toast to prevent race conditions
|
|
481
|
+
// If the handler is called twice quickly, both might pass the check before the flag is set
|
|
482
|
+
toastShownForServiceWorkerId = currentWaitingSWId;
|
|
483
|
+
|
|
484
|
+
// Update state
|
|
485
|
+
updateAvailable = true;
|
|
486
|
+
waitingServiceWorker = currentWaitingSW;
|
|
487
|
+
notifyStatusListeners();
|
|
488
|
+
|
|
489
|
+
// Show toast notification about update
|
|
490
|
+
if (options.onUpdateAvailable) {
|
|
491
|
+
options.onUpdateAvailable();
|
|
492
|
+
} else {
|
|
493
|
+
// CRITICAL: Use consistent toast ID so duplicate calls update the same toast
|
|
494
|
+
// This prevents multiple toasts from appearing if the handler is called multiple times
|
|
495
|
+
// CRITICAL: Always create fresh action handlers that reference the current waitingServiceWorker
|
|
496
|
+
// This ensures the action handler always has the correct service worker reference
|
|
497
|
+
// even if the toast is updated later
|
|
498
|
+
const actionHandler = () => {
|
|
499
|
+
logger.info('Install Now clicked, updating service worker...');
|
|
500
|
+
// CRITICAL: Get the current waitingServiceWorker at click time, not at toast creation time
|
|
501
|
+
// This ensures it works even if the toast was created earlier and then updated
|
|
502
|
+
if (waitingServiceWorker) {
|
|
503
|
+
updateServiceWorker().catch((error) => {
|
|
504
|
+
logger.error('Failed to update service worker:', { error });
|
|
505
|
+
});
|
|
506
|
+
} else {
|
|
507
|
+
logger.warn('Install Now clicked but no waiting service worker found');
|
|
508
|
+
// Try to get it from registration as fallback
|
|
509
|
+
navigator.serviceWorker.getRegistration().then((swRegistration) => {
|
|
510
|
+
if (swRegistration?.waiting) {
|
|
511
|
+
waitingServiceWorker = swRegistration.waiting;
|
|
512
|
+
updateServiceWorker().catch((error) => {
|
|
513
|
+
logger.error('Failed to update service worker:', { error });
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
shellui.toast({
|
|
521
|
+
id: UPDATE_AVAILABLE_TOAST_ID, // Use consistent ID to prevent duplicates
|
|
522
|
+
title: 'New version available',
|
|
523
|
+
description: 'A new version of the app is available. Install now or later?',
|
|
524
|
+
type: 'info',
|
|
525
|
+
duration: 0, // Don't auto-dismiss
|
|
526
|
+
position: 'bottom-left',
|
|
527
|
+
action: {
|
|
528
|
+
label: 'Install Now',
|
|
529
|
+
onClick: actionHandler, // Use the handler function
|
|
530
|
+
},
|
|
531
|
+
cancel: {
|
|
532
|
+
label: 'Later',
|
|
533
|
+
onClick: () => {
|
|
534
|
+
// User chose to install later, toast will be dismissed
|
|
535
|
+
},
|
|
536
|
+
},
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
} catch (error) {
|
|
540
|
+
logger.error('Error in waiting handler:', { error });
|
|
541
|
+
// On error, reset the flag so we can try again for this service worker
|
|
542
|
+
toastShownForServiceWorkerId = null;
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
wb.addEventListener('waiting', waitingHandler);
|
|
546
|
+
|
|
547
|
+
// Handle service worker activated
|
|
548
|
+
activatedHandler = (event?: unknown) => {
|
|
549
|
+
const evt = (event ?? {}) as Record<string, unknown>;
|
|
550
|
+
console.info('[Service Worker] Service worker activated:', {
|
|
551
|
+
isUpdate: evt.isUpdate,
|
|
552
|
+
isInitialRegistration,
|
|
553
|
+
});
|
|
554
|
+
notifyStatusListeners();
|
|
555
|
+
// Reset flags when service worker is activated (update installed or new registration)
|
|
556
|
+
updateAvailable = false;
|
|
557
|
+
waitingServiceWorker = null;
|
|
558
|
+
toastShownForServiceWorkerId = null; // Reset so we can show toast for the next update
|
|
559
|
+
|
|
560
|
+
// CRITICAL: Only reload if this is an intentional update (user clicked "Install Now")
|
|
561
|
+
// Check both in-memory flag and sessionStorage to ensure we only reload when user explicitly requested it
|
|
562
|
+
const isIntentionalUpdatePersisted =
|
|
563
|
+
typeof window !== 'undefined' &&
|
|
564
|
+
sessionStorage.getItem('shellui:service-worker:intentional-update') === 'true';
|
|
565
|
+
const shouldReload = isIntentionalUpdate || isIntentionalUpdatePersisted;
|
|
566
|
+
|
|
567
|
+
// Clear intentional update flag after activation (update is complete)
|
|
568
|
+
if (typeof window !== 'undefined') {
|
|
569
|
+
sessionStorage.removeItem('shellui:service-worker:intentional-update');
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// CRITICAL: Only reload if this is an intentional update (user clicked "Install Now")
|
|
573
|
+
// Do NOT reload automatically when a new service worker is installed - wait for user action
|
|
574
|
+
// Exception: If we auto-activated on page load, the new version is already active, no reload needed
|
|
575
|
+
if (evt.isUpdate && !isInitialRegistration && shouldReload) {
|
|
576
|
+
// User explicitly clicked "Install Now", so reload to use the new version
|
|
577
|
+
console.info('[Service Worker] Reloading page after intentional update');
|
|
578
|
+
window.location.reload();
|
|
579
|
+
} else if (evt.isUpdate && !isInitialRegistration && !shouldReload) {
|
|
580
|
+
// Auto-activated on page load - new version is now active, UI will update via notifyStatusListeners
|
|
581
|
+
console.info(
|
|
582
|
+
'[Service Worker] Service worker auto-activated on page load - new version is now active',
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
// Reset flag after activation
|
|
586
|
+
isInitialRegistration = false;
|
|
587
|
+
};
|
|
588
|
+
wb.addEventListener('activated', activatedHandler as (event: unknown) => void);
|
|
589
|
+
|
|
590
|
+
// Handle service worker controlling
|
|
591
|
+
controllingHandler = () => {
|
|
592
|
+
notifyStatusListeners();
|
|
593
|
+
|
|
594
|
+
// CRITICAL: Check if this is an auto-activation from page load refresh
|
|
595
|
+
// If user refreshed and we auto-activated, we need to reload to get the new JavaScript
|
|
596
|
+
const wasAutoActivated =
|
|
597
|
+
typeof window !== 'undefined' &&
|
|
598
|
+
sessionStorage.getItem('shellui:service-worker:auto-activated') === 'true';
|
|
599
|
+
|
|
600
|
+
// CRITICAL: Only reload if this is an intentional update (user clicked "Install Now") OR auto-activation
|
|
601
|
+
// The controlling event fires when a service worker takes control
|
|
602
|
+
// Check both in-memory flag and sessionStorage to ensure we only reload when appropriate
|
|
603
|
+
const isIntentionalUpdatePersisted =
|
|
604
|
+
typeof window !== 'undefined' &&
|
|
605
|
+
sessionStorage.getItem('shellui:service-worker:intentional-update') === 'true';
|
|
606
|
+
const shouldReload =
|
|
607
|
+
isIntentionalUpdate || isIntentionalUpdatePersisted || wasAutoActivated;
|
|
608
|
+
|
|
609
|
+
// Clear auto-activation flag if it was set
|
|
610
|
+
if (wasAutoActivated && typeof window !== 'undefined') {
|
|
611
|
+
sessionStorage.removeItem('shellui:service-worker:auto-activated');
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// CRITICAL: Reload if this is an intentional update OR auto-activation from page refresh
|
|
615
|
+
// After reload, the new version will be active and UI will show correct state
|
|
616
|
+
if (!isInitialRegistration && shouldReload) {
|
|
617
|
+
if (wasAutoActivated) {
|
|
618
|
+
console.info(
|
|
619
|
+
'[Service Worker] Auto-activated service worker took control - reloading to use new version',
|
|
620
|
+
);
|
|
621
|
+
} else {
|
|
622
|
+
console.info(
|
|
623
|
+
'[Service Worker] User clicked Install Now - reloading to use new version',
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
// Reload to ensure new JavaScript is loaded
|
|
627
|
+
window.location.reload();
|
|
628
|
+
}
|
|
629
|
+
// Reset flag after controlling
|
|
630
|
+
isInitialRegistration = false;
|
|
631
|
+
};
|
|
632
|
+
wb.addEventListener('controlling', controllingHandler);
|
|
633
|
+
|
|
634
|
+
// Handle service worker registered
|
|
635
|
+
registeredHandler = () => {
|
|
636
|
+
notifyStatusListeners();
|
|
637
|
+
};
|
|
638
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
639
|
+
(wb as any).addEventListener('registered', registeredHandler);
|
|
640
|
+
|
|
641
|
+
// CRITICAL: Handle service worker 'redundant' event
|
|
642
|
+
// This event fires when a service worker is replaced, which is NORMAL during updates
|
|
643
|
+
// The 'redundant' event fires for the OLD service worker when a NEW one takes control
|
|
644
|
+
// During intentional updates (user clicked "Install Now"), this is EXPECTED behavior
|
|
645
|
+
// We MUST check for intentional updates BEFORE disabling, otherwise we'll disable the
|
|
646
|
+
// service worker right after the user explicitly asked to install an update!
|
|
647
|
+
redundantHandler = (event?: unknown) => {
|
|
648
|
+
logger.info('Service worker became redundant:', event as Record<string, unknown>);
|
|
649
|
+
|
|
650
|
+
// CRITICAL CHECK: Verify this is an intentional update BEFORE doing anything
|
|
651
|
+
// Check sessionStorage FIRST (survives page reloads) - this is set BEFORE skip waiting
|
|
652
|
+
// Then check in-memory flag as backup
|
|
653
|
+
// This prevents race conditions where the flag might not be set yet
|
|
654
|
+
const isIntentionalUpdatePersisted =
|
|
655
|
+
typeof window !== 'undefined' &&
|
|
656
|
+
sessionStorage.getItem('shellui:service-worker:intentional-update') === 'true';
|
|
657
|
+
|
|
658
|
+
// CRITICAL: Also check if there's a waiting service worker - if there is, we're in update flow
|
|
659
|
+
// This provides an additional safety check in case sessionStorage check fails
|
|
660
|
+
// A waiting service worker means an update is in progress, so redundant is expected
|
|
661
|
+
// We check synchronously first (waitingServiceWorker variable), then async if needed
|
|
662
|
+
const hasWaitingServiceWorkerSync = !!waitingServiceWorker;
|
|
663
|
+
|
|
664
|
+
// CRITICAL: If ANY of these indicate an intentional update, DO NOT disable
|
|
665
|
+
// The combination of checks prevents false positives from disabling during updates
|
|
666
|
+
// This is the KEY fix - we check multiple signals to be absolutely sure it's an update
|
|
667
|
+
const isUpdateFlow =
|
|
668
|
+
isIntentionalUpdate || isIntentionalUpdatePersisted || hasWaitingServiceWorkerSync;
|
|
669
|
+
|
|
670
|
+
// CRITICAL: Only disable if this is NOT part of a normal update flow
|
|
671
|
+
// Disabling during an update would break the user's explicit "Install Now" action
|
|
672
|
+
// This bug has been seen many times - the redundant event fires during normal updates
|
|
673
|
+
// and without these checks, it would disable the service worker right after install
|
|
674
|
+
if (!isUpdateFlow) {
|
|
675
|
+
// Double-check asynchronously in case the sync check missed it
|
|
676
|
+
// CRITICAL: Be very defensive here - only disable if we're absolutely sure it's an error
|
|
677
|
+
navigator.serviceWorker
|
|
678
|
+
.getRegistration()
|
|
679
|
+
.then((registration) => {
|
|
680
|
+
const hasWaitingAsync = !!registration?.waiting;
|
|
681
|
+
const hasInstallingAsync = !!registration?.installing;
|
|
682
|
+
const hasActiveAsync = !!registration?.active;
|
|
683
|
+
|
|
684
|
+
// CRITICAL: Only disable if there's NO waiting, NO installing, and NO active service worker
|
|
685
|
+
// If any of these exist, it means an update is in progress and redundant is expected
|
|
686
|
+
if (!hasWaitingAsync && !hasInstallingAsync && !hasActiveAsync) {
|
|
687
|
+
// No service worker at all - this is truly unexpected and likely an error
|
|
688
|
+
console.warn(
|
|
689
|
+
'[Service Worker] Redundant event: No service workers found, disabling',
|
|
690
|
+
);
|
|
691
|
+
logger.warn(
|
|
692
|
+
'Service worker became redundant unexpectedly (no service workers found)',
|
|
693
|
+
);
|
|
694
|
+
disableCachingAutomatically(
|
|
695
|
+
'Service worker became redundant (no service workers found)',
|
|
696
|
+
);
|
|
697
|
+
} else {
|
|
698
|
+
// Service workers exist - this is likely part of an update flow, don't disable
|
|
699
|
+
console.info(
|
|
700
|
+
'[Service Worker] Redundant event: Service workers exist, likely update in progress - ignoring',
|
|
701
|
+
);
|
|
702
|
+
logger.info(
|
|
703
|
+
'Service worker became redundant but other service workers exist - likely update in progress, ignoring',
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
})
|
|
707
|
+
.catch((error) => {
|
|
708
|
+
// CRITICAL: If async check fails, DON'T disable - this could be a transient error
|
|
709
|
+
// Log the error but don't disable the service worker
|
|
710
|
+
console.warn(
|
|
711
|
+
'[Service Worker] Redundant event: Async check failed, but NOT disabling:',
|
|
712
|
+
error,
|
|
713
|
+
);
|
|
714
|
+
logger.warn(
|
|
715
|
+
'Service worker redundant event: Async check failed, but NOT disabling to avoid false positives',
|
|
716
|
+
{ error },
|
|
717
|
+
);
|
|
718
|
+
});
|
|
719
|
+
} else {
|
|
720
|
+
console.info(
|
|
721
|
+
'[Service Worker] Redundant event: Intentional update detected - ignoring',
|
|
722
|
+
);
|
|
723
|
+
logger.info(
|
|
724
|
+
'Service worker became redundant as part of normal update - this is expected and safe',
|
|
725
|
+
);
|
|
726
|
+
// CRITICAL: Don't clear the sessionStorage flag immediately - wait until activation
|
|
727
|
+
// Clearing it too early could cause issues if redundant fires multiple times
|
|
728
|
+
// The activated handler will clear it when the update is complete
|
|
729
|
+
}
|
|
730
|
+
};
|
|
731
|
+
wb.addEventListener('redundant', redundantHandler);
|
|
732
|
+
|
|
733
|
+
// Handle external service worker errors
|
|
734
|
+
// Only disable caching on critical errors, not during normal update operations
|
|
735
|
+
serviceWorkerErrorHandler = (event?: unknown) => {
|
|
736
|
+
logger.error('Service worker error event:', event as Record<string, unknown>);
|
|
737
|
+
|
|
738
|
+
// Check if this is an intentional update (check both in-memory flag and sessionStorage)
|
|
739
|
+
const isIntentionalUpdatePersisted =
|
|
740
|
+
typeof window !== 'undefined' &&
|
|
741
|
+
sessionStorage.getItem('shellui:service-worker:intentional-update') === 'true';
|
|
742
|
+
const isUpdateFlow = isIntentionalUpdate || isIntentionalUpdatePersisted;
|
|
743
|
+
|
|
744
|
+
// CRITICAL: Be very defensive - only disable on truly critical errors
|
|
745
|
+
// Many service worker errors are non-fatal and don't require disabling
|
|
746
|
+
if (!isUpdateFlow) {
|
|
747
|
+
// Only disable on actual errors, not warnings or non-critical issues
|
|
748
|
+
const evt = (event ?? {}) as Record<string, unknown>;
|
|
749
|
+
const evtError = evt.error as Record<string, unknown> | undefined;
|
|
750
|
+
const errorMessage = (evt.message as string) || (evtError?.message as string) || 'Unknown error';
|
|
751
|
+
const errorName = (evtError?.name as string) || '';
|
|
752
|
+
|
|
753
|
+
// CRITICAL: Only disable on critical errors, ignore common non-fatal errors
|
|
754
|
+
// Many errors during service worker lifecycle are expected and don't require disabling
|
|
755
|
+
const isCriticalError =
|
|
756
|
+
!errorMessage.includes('update') &&
|
|
757
|
+
!errorMessage.includes('activate') &&
|
|
758
|
+
!errorMessage.includes('install') &&
|
|
759
|
+
!errorName.includes('AbortError') &&
|
|
760
|
+
!errorName.includes('NetworkError');
|
|
761
|
+
|
|
762
|
+
if (isCriticalError) {
|
|
763
|
+
disableCachingAutomatically(`Service worker error: ${errorMessage}`);
|
|
764
|
+
} else {
|
|
765
|
+
console.warn('[Service Worker] Non-critical error, ignoring:', errorMessage);
|
|
766
|
+
logger.warn('Service worker non-critical error, not disabling:', {
|
|
767
|
+
errorMessage,
|
|
768
|
+
errorName,
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
} else {
|
|
772
|
+
console.info('[Service Worker] Error during update flow, ignoring');
|
|
773
|
+
logger.info('Service worker error during update flow, ignoring');
|
|
774
|
+
}
|
|
775
|
+
};
|
|
776
|
+
navigator.serviceWorker.addEventListener('error', serviceWorkerErrorHandler);
|
|
777
|
+
|
|
778
|
+
// Handle message errors from service worker
|
|
779
|
+
messageErrorHandler = (event?: unknown) => {
|
|
780
|
+
logger.error('Service worker message error:', event as Record<string, unknown>);
|
|
781
|
+
// Don't disable on message errors - they're usually not critical
|
|
782
|
+
};
|
|
783
|
+
navigator.serviceWorker.addEventListener('messageerror', messageErrorHandler);
|
|
784
|
+
} // End of event listeners block
|
|
785
|
+
|
|
786
|
+
// Register the service worker
|
|
787
|
+
await wb.register();
|
|
788
|
+
|
|
789
|
+
// Get the underlying registration to set updateViaCache
|
|
790
|
+
// This ensures changes to sw.js/sw-dev.js are always detected (bypasses cache)
|
|
791
|
+
const registration = await navigator.serviceWorker.getRegistration();
|
|
792
|
+
if (registration) {
|
|
793
|
+
// Set updateViaCache to 'none' to ensure service worker file changes are always detected
|
|
794
|
+
// This tells the browser to always check the network for sw.js/sw-dev.js updates
|
|
795
|
+
// Note: This property is read-only in some browsers, but setting it helps where supported
|
|
796
|
+
try {
|
|
797
|
+
// Access the registration's update method to ensure cache-busting
|
|
798
|
+
// The browser will check the service worker file with cache: 'reload' when update() is called
|
|
799
|
+
} catch (_e) {
|
|
800
|
+
// Ignore if updateViaCache can't be set (some browsers don't support it)
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// Check if there's a waiting service worker after registration
|
|
804
|
+
// If we already handled it above (auto-activation on page load), skip showing toast
|
|
805
|
+
// Otherwise, show toast to let user know an update is available
|
|
806
|
+
if (registration.waiting && waitingHandler) {
|
|
807
|
+
const waitingSWId = registration.waiting.scriptURL;
|
|
808
|
+
|
|
809
|
+
// Check if we already auto-activated this waiting service worker above
|
|
810
|
+
// If so, don't show toast - it will activate automatically
|
|
811
|
+
const wasAutoActivated =
|
|
812
|
+
waitingServiceWorker === registration.waiting && updateAvailable === true;
|
|
813
|
+
|
|
814
|
+
if (!wasAutoActivated) {
|
|
815
|
+
// This is a new waiting service worker that appeared after registration
|
|
816
|
+
// Show toast to notify user
|
|
817
|
+
// CRITICAL: Check flag BEFORE calling handler to prevent duplicate toasts
|
|
818
|
+
// The waiting event might have already fired and shown the toast
|
|
819
|
+
if (toastShownForServiceWorkerId !== waitingSWId) {
|
|
820
|
+
// Update state first
|
|
821
|
+
updateAvailable = true;
|
|
822
|
+
waitingServiceWorker = registration.waiting;
|
|
823
|
+
// Trigger the waiting handler to show toast
|
|
824
|
+
// The handler will check the flag again and show toast if needed
|
|
825
|
+
waitingHandler();
|
|
826
|
+
} else {
|
|
827
|
+
// Toast already shown, just update state
|
|
828
|
+
updateAvailable = true;
|
|
829
|
+
waitingServiceWorker = registration.waiting;
|
|
830
|
+
notifyStatusListeners();
|
|
831
|
+
}
|
|
832
|
+
} else {
|
|
833
|
+
// Already auto-activated, just ensure state is correct
|
|
834
|
+
updateAvailable = true;
|
|
835
|
+
waitingServiceWorker = registration.waiting;
|
|
836
|
+
notifyStatusListeners();
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
notifyStatusListeners();
|
|
842
|
+
|
|
843
|
+
// Check for updates periodically (including service worker file changes)
|
|
844
|
+
// This ensures changes to sw.js/sw-dev.js are detected
|
|
845
|
+
/* const _updateInterval = */ setInterval(
|
|
846
|
+
() => {
|
|
847
|
+
if (wb && options.enabled) {
|
|
848
|
+
// wb.update() checks for updates to the service worker file itself
|
|
849
|
+
// The browser will compare the byte-by-byte content of sw.js/sw-dev.js
|
|
850
|
+
wb.update();
|
|
851
|
+
}
|
|
852
|
+
},
|
|
853
|
+
60 * 60 * 1000,
|
|
854
|
+
); // Check every hour
|
|
855
|
+
|
|
856
|
+
// Also check for updates when the page becomes visible (user returns to tab)
|
|
857
|
+
// This helps detect service worker file changes more quickly
|
|
858
|
+
let visibilityHandler: (() => void) | null = null;
|
|
859
|
+
if (typeof document !== 'undefined') {
|
|
860
|
+
visibilityHandler = () => {
|
|
861
|
+
if (!document.hidden && wb && options.enabled) {
|
|
862
|
+
wb.update();
|
|
863
|
+
}
|
|
864
|
+
};
|
|
865
|
+
document.addEventListener('visibilitychange', visibilityHandler);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// CRITICAL: Mark registration as complete only after everything is set up
|
|
869
|
+
// This prevents error handlers from disabling during the registration process
|
|
870
|
+
isRegistering = false;
|
|
871
|
+
|
|
872
|
+
// Store interval and handler for cleanup (though cleanup is best-effort)
|
|
873
|
+
// The interval will continue until page reload, which is acceptable
|
|
874
|
+
} catch (error) {
|
|
875
|
+
// Handle registration errors - be very selective about disabling
|
|
876
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
877
|
+
const errorName = error instanceof Error ? error.name : '';
|
|
878
|
+
logger.error('Registration failed:', { error });
|
|
879
|
+
|
|
880
|
+
// CRITICAL: Only disable on truly critical errors that indicate the service worker is broken
|
|
881
|
+
// Many registration errors are transient or non-fatal
|
|
882
|
+
const isCriticalError =
|
|
883
|
+
(errorMessage.includes('Failed to register') &&
|
|
884
|
+
!errorMessage.includes('already registered')) ||
|
|
885
|
+
(errorMessage.includes('script error') && !errorMessage.includes('network')) ||
|
|
886
|
+
errorMessage.includes('SyntaxError') ||
|
|
887
|
+
(errorMessage.includes('TypeError') && !errorMessage.includes('fetch')) ||
|
|
888
|
+
(error instanceof DOMException &&
|
|
889
|
+
error.name !== 'SecurityError' &&
|
|
890
|
+
error.name !== 'AbortError');
|
|
891
|
+
|
|
892
|
+
if (isCriticalError) {
|
|
893
|
+
await disableCachingAutomatically(`Registration failed: ${errorMessage}`);
|
|
894
|
+
} else {
|
|
895
|
+
console.warn(
|
|
896
|
+
'[Service Worker] Non-critical registration error, NOT disabling:',
|
|
897
|
+
errorMessage,
|
|
898
|
+
);
|
|
899
|
+
logger.warn('Non-critical registration error, not disabling:', { errorMessage, errorName });
|
|
900
|
+
}
|
|
901
|
+
} finally {
|
|
902
|
+
// CRITICAL: Reset registration flag in finally block to ensure it's always reset
|
|
903
|
+
// But keep a grace period to prevent immediate disable after registration completes
|
|
904
|
+
// The grace period is handled in disableCachingAutomatically
|
|
905
|
+
isRegistering = false;
|
|
906
|
+
registrationPromise = null;
|
|
907
|
+
}
|
|
908
|
+
})();
|
|
909
|
+
|
|
910
|
+
return registrationPromise;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/**
|
|
914
|
+
* Update the service worker immediately
|
|
915
|
+
* This will reload the page when the update is installed
|
|
916
|
+
*/
|
|
917
|
+
export async function updateServiceWorker(): Promise<void> {
|
|
918
|
+
if (!wb || !waitingServiceWorker) {
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
try {
|
|
923
|
+
// CRITICAL: Set intentional update flag FIRST, before any other operations
|
|
924
|
+
// This flag MUST be set in sessionStorage BEFORE skip waiting is called
|
|
925
|
+
// The 'redundant' event can fire very quickly after skip waiting, and if this flag
|
|
926
|
+
// isn't set yet, the redundant handler will think it's an error and disable the SW
|
|
927
|
+
// This is the ROOT CAUSE of the bug where service worker gets disabled on "Install Now"
|
|
928
|
+
const INTENTIONAL_UPDATE_KEY = 'shellui:service-worker:intentional-update';
|
|
929
|
+
if (typeof window !== 'undefined') {
|
|
930
|
+
// CRITICAL: Set this IMMEDIATELY - don't wait for anything else
|
|
931
|
+
sessionStorage.setItem(INTENTIONAL_UPDATE_KEY, 'true');
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// CRITICAL: Also set in-memory flag immediately
|
|
935
|
+
// This provides backup protection in case sessionStorage has issues
|
|
936
|
+
isIntentionalUpdate = true;
|
|
937
|
+
|
|
938
|
+
// CRITICAL: Ensure service worker setting is preserved and enabled before reload
|
|
939
|
+
// This prevents the service worker from being disabled after refresh
|
|
940
|
+
// The user explicitly clicked "Install Now", so we must keep the service worker enabled
|
|
941
|
+
if (typeof window !== 'undefined') {
|
|
942
|
+
const STORAGE_KEY = 'shellui:settings';
|
|
943
|
+
try {
|
|
944
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
945
|
+
if (stored) {
|
|
946
|
+
const settings = JSON.parse(stored);
|
|
947
|
+
// CRITICAL: Always ensure service worker is enabled when user clicks Install Now
|
|
948
|
+
// This ensures it stays enabled after the page reloads
|
|
949
|
+
// Without this, the service worker could be disabled after the reload
|
|
950
|
+
settings.serviceWorker = { enabled: true };
|
|
951
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
|
|
952
|
+
|
|
953
|
+
// Notify the app about the settings update (in case it's listening)
|
|
954
|
+
shellui.sendMessageToParent({
|
|
955
|
+
type: 'SHELLUI_SETTINGS_UPDATED',
|
|
956
|
+
payload: { settings },
|
|
957
|
+
});
|
|
958
|
+
} else {
|
|
959
|
+
// No settings stored, create default with service worker enabled
|
|
960
|
+
const defaultSettings = {
|
|
961
|
+
serviceWorker: { enabled: true },
|
|
962
|
+
};
|
|
963
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(defaultSettings));
|
|
964
|
+
}
|
|
965
|
+
} catch (error) {
|
|
966
|
+
logger.warn('Failed to preserve settings before update:', { error });
|
|
967
|
+
// CRITICAL: Even if settings update fails, the intentional update flag is already set
|
|
968
|
+
// This prevents the redundant handler from disabling the service worker
|
|
969
|
+
// The app.tsx will default to enabled anyway, so continue with the update
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// Mark that this is an update (not initial registration)
|
|
974
|
+
isInitialRegistration = false;
|
|
975
|
+
|
|
976
|
+
// Set up reload handler before sending skip waiting
|
|
977
|
+
const reloadApp = () => {
|
|
978
|
+
// Use shellUI refresh message if available, otherwise fallback to window.location.reload
|
|
979
|
+
const sent = shellui.sendMessageToParent({
|
|
980
|
+
type: 'SHELLUI_REFRESH_PAGE',
|
|
981
|
+
payload: {},
|
|
982
|
+
});
|
|
983
|
+
if (!sent) {
|
|
984
|
+
window.location.reload();
|
|
985
|
+
}
|
|
986
|
+
};
|
|
987
|
+
|
|
988
|
+
// Add one-time listener for controlling event
|
|
989
|
+
const updateControllingHandler = () => {
|
|
990
|
+
reloadApp();
|
|
991
|
+
wb?.removeEventListener('controlling', updateControllingHandler);
|
|
992
|
+
// Reset flag after reload is triggered
|
|
993
|
+
setTimeout(() => {
|
|
994
|
+
isIntentionalUpdate = false;
|
|
995
|
+
}, 1000);
|
|
996
|
+
};
|
|
997
|
+
wb.addEventListener('controlling', updateControllingHandler);
|
|
998
|
+
|
|
999
|
+
// Send skip waiting message to the waiting service worker
|
|
1000
|
+
waitingServiceWorker.postMessage({ type: 'SKIP_WAITING' });
|
|
1001
|
+
|
|
1002
|
+
// Fallback: if controlling event doesn't fire within 2 seconds, reload anyway
|
|
1003
|
+
setTimeout(() => {
|
|
1004
|
+
if (controllingHandler) wb?.removeEventListener('controlling', controllingHandler);
|
|
1005
|
+
// Check if service worker is now controlling
|
|
1006
|
+
if (navigator.serviceWorker.controller) {
|
|
1007
|
+
reloadApp();
|
|
1008
|
+
}
|
|
1009
|
+
// Reset flag if fallback is used
|
|
1010
|
+
setTimeout(() => {
|
|
1011
|
+
isIntentionalUpdate = false;
|
|
1012
|
+
}, 1000);
|
|
1013
|
+
}, 2000);
|
|
1014
|
+
} catch (error) {
|
|
1015
|
+
logger.error('Failed to update service worker:', { error });
|
|
1016
|
+
// Reset flag on error
|
|
1017
|
+
isIntentionalUpdate = false;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
/**
|
|
1022
|
+
* Unregister the service worker silently (no page reload)
|
|
1023
|
+
*/
|
|
1024
|
+
export async function unregisterServiceWorker(): Promise<void> {
|
|
1025
|
+
if (!('serviceWorker' in navigator)) {
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
try {
|
|
1030
|
+
// Set flag to prevent any reloads during unregistration
|
|
1031
|
+
isInitialRegistration = true;
|
|
1032
|
+
|
|
1033
|
+
const registration = await navigator.serviceWorker.getRegistration();
|
|
1034
|
+
if (registration) {
|
|
1035
|
+
await registration.unregister();
|
|
1036
|
+
|
|
1037
|
+
// Clear all caches silently
|
|
1038
|
+
if ('caches' in window) {
|
|
1039
|
+
const cacheNames = await caches.keys();
|
|
1040
|
+
await Promise.all(cacheNames.map((name) => caches.delete(name)));
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// Clean up workbox instance and remove all event listeners
|
|
1045
|
+
if (wb) {
|
|
1046
|
+
// Remove all event listeners before cleaning up
|
|
1047
|
+
if (waitingHandler) wb.removeEventListener('waiting', waitingHandler);
|
|
1048
|
+
if (activatedHandler) wb.removeEventListener('activated', activatedHandler);
|
|
1049
|
+
if (controllingHandler) wb.removeEventListener('controlling', controllingHandler);
|
|
1050
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1051
|
+
if (registeredHandler) (wb as any).removeEventListener('registered', registeredHandler);
|
|
1052
|
+
if (redundantHandler) wb.removeEventListener('redundant', redundantHandler);
|
|
1053
|
+
if (serviceWorkerErrorHandler) {
|
|
1054
|
+
navigator.serviceWorker.removeEventListener('error', serviceWorkerErrorHandler);
|
|
1055
|
+
}
|
|
1056
|
+
if (messageErrorHandler) {
|
|
1057
|
+
navigator.serviceWorker.removeEventListener('messageerror', messageErrorHandler);
|
|
1058
|
+
}
|
|
1059
|
+
// Clear handler references
|
|
1060
|
+
waitingHandler = null;
|
|
1061
|
+
activatedHandler = null;
|
|
1062
|
+
controllingHandler = null;
|
|
1063
|
+
registeredHandler = null;
|
|
1064
|
+
redundantHandler = null;
|
|
1065
|
+
serviceWorkerErrorHandler = null;
|
|
1066
|
+
messageErrorHandler = null;
|
|
1067
|
+
// Remove workbox instance
|
|
1068
|
+
wb = null;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
updateAvailable = false;
|
|
1072
|
+
waitingServiceWorker = null;
|
|
1073
|
+
isInitialRegistration = false;
|
|
1074
|
+
toastShownForServiceWorkerId = null; // Reset toast flag on unregister
|
|
1075
|
+
eventListenersAdded = false; // Reset event listeners flag on unregister
|
|
1076
|
+
isIntentionalUpdate = false; // Reset intentional update flag on unregister
|
|
1077
|
+
notifyStatusListeners();
|
|
1078
|
+
} catch (error) {
|
|
1079
|
+
logger.error('Failed to unregister service worker:', { error });
|
|
1080
|
+
isInitialRegistration = false;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
/**
|
|
1085
|
+
* Check if service worker is registered and active
|
|
1086
|
+
*/
|
|
1087
|
+
export async function isServiceWorkerRegistered(): Promise<boolean> {
|
|
1088
|
+
if (!('serviceWorker' in navigator)) {
|
|
1089
|
+
return false;
|
|
1090
|
+
}
|
|
1091
|
+
if (isTauri()) {
|
|
1092
|
+
return false;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// First check if service worker file exists (only in production)
|
|
1096
|
+
const swExists = await serviceWorkerFileExists();
|
|
1097
|
+
if (!swExists) {
|
|
1098
|
+
return false;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
try {
|
|
1102
|
+
const registration = await navigator.serviceWorker.getRegistration();
|
|
1103
|
+
if (!registration) {
|
|
1104
|
+
return false;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// Check if service worker is active (either controlling or installed)
|
|
1108
|
+
// A service worker can be registered but not yet active
|
|
1109
|
+
if (registration.active) {
|
|
1110
|
+
return true;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// Also check if there's a waiting or installing service worker
|
|
1114
|
+
// This means registration is in progress or update is available
|
|
1115
|
+
if (registration.waiting || registration.installing) {
|
|
1116
|
+
return true;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// If navigator.serviceWorker.controller exists, the SW is controlling the page
|
|
1120
|
+
if (navigator.serviceWorker.controller) {
|
|
1121
|
+
return true;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
return false;
|
|
1125
|
+
} catch (_error) {
|
|
1126
|
+
// Silently return false if there's an error checking registration
|
|
1127
|
+
return false;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
/**
|
|
1132
|
+
* Get service worker status
|
|
1133
|
+
*/
|
|
1134
|
+
export async function getServiceWorkerStatus(): Promise<{
|
|
1135
|
+
registered: boolean;
|
|
1136
|
+
updateAvailable: boolean;
|
|
1137
|
+
}> {
|
|
1138
|
+
const registered = await isServiceWorkerRegistered();
|
|
1139
|
+
|
|
1140
|
+
// Actually check if there's a waiting service worker (more reliable than in-memory flag)
|
|
1141
|
+
// This ensures we get the correct state even after page reloads
|
|
1142
|
+
let actuallyUpdateAvailable = false;
|
|
1143
|
+
if (registered && !isTauri()) {
|
|
1144
|
+
try {
|
|
1145
|
+
const registration = await navigator.serviceWorker.getRegistration();
|
|
1146
|
+
if (registration?.waiting) {
|
|
1147
|
+
// There's a waiting service worker, so an update is available
|
|
1148
|
+
actuallyUpdateAvailable = true;
|
|
1149
|
+
// Sync the in-memory flag with the actual state
|
|
1150
|
+
updateAvailable = true;
|
|
1151
|
+
waitingServiceWorker = registration.waiting;
|
|
1152
|
+
} else {
|
|
1153
|
+
// No waiting service worker, so no update is available
|
|
1154
|
+
actuallyUpdateAvailable = false;
|
|
1155
|
+
// Sync the in-memory flag with the actual state
|
|
1156
|
+
updateAvailable = false;
|
|
1157
|
+
waitingServiceWorker = null;
|
|
1158
|
+
}
|
|
1159
|
+
} catch (error) {
|
|
1160
|
+
logger.error('Failed to check service worker registration:', { error });
|
|
1161
|
+
// Fall back to in-memory flag if check fails
|
|
1162
|
+
actuallyUpdateAvailable = updateAvailable;
|
|
1163
|
+
}
|
|
1164
|
+
} else {
|
|
1165
|
+
// Not registered or Tauri, so no update available
|
|
1166
|
+
actuallyUpdateAvailable = false;
|
|
1167
|
+
updateAvailable = false;
|
|
1168
|
+
waitingServiceWorker = null;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
return {
|
|
1172
|
+
registered,
|
|
1173
|
+
updateAvailable: actuallyUpdateAvailable,
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
/**
|
|
1178
|
+
* Manually trigger a check for service worker updates (browser only).
|
|
1179
|
+
* Resolves when the check is complete. Listen to addStatusListener for updateAvailable changes.
|
|
1180
|
+
*/
|
|
1181
|
+
export async function checkForServiceWorkerUpdate(): Promise<void> {
|
|
1182
|
+
if (isTauri() || !wb) return;
|
|
1183
|
+
await wb.update();
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
/**
|
|
1187
|
+
* Add a listener for service worker status changes
|
|
1188
|
+
*/
|
|
1189
|
+
export function addStatusListener(
|
|
1190
|
+
listener: (status: { registered: boolean; updateAvailable: boolean }) => void,
|
|
1191
|
+
): () => void {
|
|
1192
|
+
statusListeners.push(listener);
|
|
1193
|
+
// Don't call immediately - let the component do the initial check to avoid duplicate fetches
|
|
1194
|
+
|
|
1195
|
+
// Return unsubscribe function
|
|
1196
|
+
return () => {
|
|
1197
|
+
statusListeners = statusListeners.filter((l) => l !== listener);
|
|
1198
|
+
};
|
|
1199
|
+
}
|