2026-02-24 20:44:16 +00:00
|
|
|
|
import { registerSW } from 'virtual:pwa-register'
|
|
|
|
|
|
|
2026-07-14 07:37:13 +00:00
|
|
|
|
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<void>) | undefined
|
|
|
|
|
|
let registrationInitialization: Promise<void> | undefined
|
|
|
|
|
|
let activationRetryTimer: number | undefined
|
|
|
|
|
|
let activationTimeoutTimer: number | undefined
|
|
|
|
|
|
const watchedInstallingWorkers = new WeakSet<ServiceWorker>()
|
|
|
|
|
|
|
|
|
|
|
|
export type ManualUpdateResult = 'up-to-date' | 'updating' | 'unsupported'
|
|
|
|
|
|
|
2026-05-09 07:17:25 +00:00
|
|
|
|
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 = `
|
|
|
|
|
|
<div class="sw-update-card">
|
|
|
|
|
|
<div class="sw-update-spinner"></div>
|
|
|
|
|
|
<p class="sw-update-title">System Updating</p>
|
|
|
|
|
|
<p class="sw-update-desc">Loading new version, please wait...<br/>The page will reload automatically.</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`
|
|
|
|
|
|
document.body.appendChild(overlay)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-14 07:37:13 +00:00
|
|
|
|
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<void>,
|
|
|
|
|
|
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)
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 20:44:16 +00:00
|
|
|
|
export const registerServiceWorker = () => {
|
2026-07-14 07:37:13 +00:00
|
|
|
|
if (registrationStarted || !('serviceWorker' in navigator)) return
|
|
|
|
|
|
registrationStarted = true
|
|
|
|
|
|
|
|
|
|
|
|
registrationInitialization = navigator.serviceWorker
|
|
|
|
|
|
.getRegistration()
|
|
|
|
|
|
.catch(() => undefined)
|
|
|
|
|
|
.then((existingRegistration) => {
|
|
|
|
|
|
if (existingRegistration) {
|
|
|
|
|
|
watchForUpdate(existingRegistration)
|
2026-05-11 12:47:14 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-14 07:37:13 +00:00
|
|
|
|
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)
|
|
|
|
|
|
},
|
2026-05-09 07:17:25 +00:00
|
|
|
|
})
|
2026-07-14 07:37:13 +00:00
|
|
|
|
activateUpdateHandler = activateUpdate
|
2026-05-09 07:17:25 +00:00
|
|
|
|
})
|
2026-07-14 07:37:13 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export const checkForAppUpdate = async (): Promise<ManualUpdateResult> => {
|
|
|
|
|
|
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)
|
2026-05-09 07:17:25 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-14 07:37:13 +00:00
|
|
|
|
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'
|
2026-02-24 20:44:16 +00:00
|
|
|
|
}
|