sozsoft-platform/ui/src/views/version/swRegistration.ts

284 lines
9.1 KiB
TypeScript
Raw Normal View History

2026-02-24 20:44:16 +00:00
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<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'
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)
}
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 = () => {
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
}
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<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)
}
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
}