import { registerSW } from 'virtual:pwa-register' const ACTIVATION_RETRY_DELAY = 5_000 const ACTIVATION_TIMEOUT = 30_000 let registrationStarted = false let activationInProgress = false let reloadStarted = false let currentRegistration: ServiceWorkerRegistration | undefined let activateUpdateHandler: ((reloadPage?: boolean) => Promise) | undefined let registrationInitialization: Promise | undefined let activationRetryTimer: number | undefined let activationTimeoutTimer: number | undefined const watchedInstallingWorkers = new WeakSet() export type ManualUpdateResult = 'up-to-date' | 'updating' | 'unsupported' function showUpdateOverlay() { if (document.getElementById('sw-update-overlay')) return const style = document.createElement('style') style.id = 'sw-update-overlay-style' style.textContent = ` #sw-update-overlay { position: fixed; inset: 0; z-index: 99999; display: flex; align-items: center; justify-content: center; background: rgba(0, 0, 0, 0.65); backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); } #sw-update-overlay .sw-update-card { display: flex; flex-direction: column; align-items: center; gap: 20px; background: #fff; border-radius: 16px; padding: 40px 56px; box-shadow: 0 8px 40px rgba(0,0,0,0.25); text-align: center; max-width: 360px; width: 90%; } #sw-update-overlay .sw-update-spinner { width: 56px; height: 56px; border: 5px solid #e5e7eb; border-top-color: #6366f1; border-radius: 50%; animation: sw-spin 0.8s linear infinite; } @keyframes sw-spin { to { transform: rotate(360deg); } } #sw-update-overlay .sw-update-title { font-size: 18px; font-weight: 700; color: #1f2937; margin: 0; } #sw-update-overlay .sw-update-desc { font-size: 14px; color: #6b7280; margin: 0; } ` document.head.appendChild(style) const overlay = document.createElement('div') overlay.id = 'sw-update-overlay' overlay.innerHTML = `

System Updating

Loading new version, please wait...
The page will reload automatically.

` document.body.appendChild(overlay) } function hideUpdateOverlay() { document.getElementById('sw-update-overlay')?.remove() document.getElementById('sw-update-overlay-style')?.remove() } function clearActivationTimers() { window.clearTimeout(activationRetryTimer) window.clearTimeout(activationTimeoutTimer) activationRetryTimer = undefined activationTimeoutTimer = undefined } function reloadForUpdate() { if (reloadStarted) return reloadStarted = true clearActivationTimers() window.location.reload() } function activateWaitingWorker( registration: ServiceWorkerRegistration | undefined, activateUpdate: (reloadPage?: boolean) => Promise, showOverlay: boolean, ) { if (activationInProgress) { if (showOverlay) showUpdateOverlay() return } activationInProgress = true if (showOverlay) { showUpdateOverlay() } // Workbox'ın controlling olayı herhangi bir nedenle kaçarsa native olay da // yeni worker kontrolü aldığı anda sayfayı yeniler. navigator.serviceWorker.addEventListener('controllerchange', reloadForUpdate, { once: true, }) // Workbox callback sırasına bağlı kalmadan waiting worker'ı doğrudan geçir. registration?.waiting?.postMessage({ type: 'SKIP_WAITING' }) void activateUpdate(true).catch((error: unknown) => { console.error('Service worker activation failed.', error) activationInProgress = false clearActivationTimers() hideUpdateOverlay() }) // Bazı tarayıcılarda Workbox mesajı bekleyen worker'a ulaşmayabiliyor. // Kısa bir süre sonra native mesajla bir kez daha aktivasyon istenir. activationRetryTimer = window.setTimeout(() => { registration?.waiting?.postMessage({ type: 'SKIP_WAITING' }) }, ACTIVATION_RETRY_DELAY) // Aktivasyon yine de tamamlanamazsa uygulamayı sonsuza kadar kilitleme. // Eski worker çalışmaya devam eder; bir sonraki açılışta tekrar denenir. activationTimeoutTimer = window.setTimeout(() => { activationInProgress = false clearActivationTimers() // Worker waiting durumundan çıktıysa aktivasyon gerçekleşmiş fakat // controllerchange olayı kaçmış olabilir. Yeni precache'i almak için yenile. if (!registration?.waiting && registration?.active) { reloadForUpdate() return } hideUpdateOverlay() console.warn('Service worker activation timed out; continuing with current version.') }, ACTIVATION_TIMEOUT) } function showUpdateWhileInstalling(registration: ServiceWorkerRegistration) { const installingWorker = registration.installing if ( !installingWorker || !navigator.serviceWorker.controller || watchedInstallingWorkers.has(installingWorker) ) { return } watchedInstallingWorkers.add(installingWorker) showUpdateOverlay() installingWorker.addEventListener('statechange', () => { // Precache dosyalarından biri indirilemezse worker redundant olur. Eski // uygulama çalışmaya devam eder ve kullanıcı kilitli ekranda bırakılmaz. if (installingWorker.state === 'redundant' && !activationInProgress) { hideUpdateOverlay() } }) } function watchForUpdate(registration: ServiceWorkerRegistration) { registration.addEventListener('updatefound', () => { // Gerçek worker güncellemesi bulunduğu anda, precache indirmesi başlamadan // Loading'i göster ve aktivasyon/reload tamamlanana kadar açık tut. showUpdateWhileInstalling(registration) }) // Listener bağlanmadan hemen önce başlamış bir kurulumu da kaçırma. showUpdateWhileInstalling(registration) } function checkForUpdate(registration: ServiceWorkerRegistration) { // Bir deploy versiyon numarası/tag değişmeden yapılsa bile sw.js içindeki // precache manifest'i değişir. Uygulama açılırken yapılan bu tek kontrol // yeni build'i algılar; açık uygulamada periyodik bir arka plan işi çalışmaz. void registration.update().catch((error: unknown) => { console.warn('Service worker update check failed.', error) }) } export const registerServiceWorker = () => { if (registrationStarted || !('serviceWorker' in navigator)) return registrationStarted = true registrationInitialization = navigator.serviceWorker .getRegistration() .catch(() => undefined) .then((existingRegistration) => { if (existingRegistration) { watchForUpdate(existingRegistration) } const activateUpdate = registerSW({ immediate: true, onRegisteredSW(_swUrl, registration) { if (!registration) return if (registration !== existingRegistration) { watchForUpdate(registration) } currentRegistration = registration checkForUpdate(registration) // waiting yalnızca aktif worker'dan farklı, kurulumu tamamlanmış gerçek // bir worker olduğunda bulunur. Onu doğrudan etkinleştir. if (registration.waiting && navigator.serviceWorker.controller) { activateWaitingWorker(registration, activateUpdate, true) } }, onNeedRefresh() { // Bu callback worker'ın bütün yeni JS/CSS dosyalarını başarıyla precache'e // almasından sonra çalışır; aktivasyon ve reload atomik yapılır. activateWaitingWorker(currentRegistration, activateUpdate, true) }, onNeedReload() { reloadForUpdate() }, onOfflineReady() { console.log('📦 App offline ready') }, onRegisterError(error) { activationInProgress = false clearActivationTimers() hideUpdateOverlay() console.error('Service worker registration failed.', error) }, }) activateUpdateHandler = activateUpdate }) } export const checkForAppUpdate = async (): Promise => { if (!('serviceWorker' in navigator)) return 'unsupported' // App içindeki normal kayıt işlemi henüz bitmediyse önce onu tamamla. await registrationInitialization const registration = currentRegistration ?? (await navigator.serviceWorker.ready.catch(() => undefined)) if (!registration) return 'unsupported' let updateFound = false const handleUpdateFound = () => { updateFound = true } registration.addEventListener('updatefound', handleUpdateFound) try { await registration.update() } finally { registration.removeEventListener('updatefound', handleUpdateFound) } const updateAvailable = Boolean(updateFound || registration.installing || registration.waiting) if (!updateAvailable) return 'up-to-date' if (registration.waiting && activateUpdateHandler) { activateWaitingWorker(registration, activateUpdateHandler, true) } // installing durumundaysa gerçek onNeedRefresh callback'i indirme bittikten // sonra aynı overlay/aktivasyon akışını başlatır. return 'updating' }