2026-02-24 20:44:16 +00:00
|
|
|
|
import { registerSW } from 'virtual:pwa-register'
|
2026-07-14 14:16:08 +00:00
|
|
|
|
import { store } from '@/store'
|
2026-02-24 20:44:16 +00:00
|
|
|
|
|
2026-08-07 11:59:47 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* Service worker güncelleme akışı
|
|
|
|
|
|
* ------------------------------------------------------------------
|
|
|
|
|
|
* 1. Kayıt + açılışta bir kez `update()` kontrolü.
|
|
|
|
|
|
* 2. Sekme görünürken periyodik, sekmeye dönüldüğünde ve internet geri
|
|
|
|
|
|
* geldiğinde tekrar kontrol -> yeni deploy açık uygulamada da yakalanır.
|
|
|
|
|
|
* 3. Yeni worker kurulmaya başlayınca (`installing`) engellemeyen bir bant
|
|
|
|
|
|
* ile "yeni sürüm indiriliyor" bilgisi gösterilir.
|
|
|
|
|
|
* 4. Kurulum bitince (`installed`) tam ekran overlay + sürüm notları gösterilir,
|
|
|
|
|
|
* SKIP_WAITING gönderilir ve `controllerchange` ile sayfa yenilenir.
|
|
|
|
|
|
* 5. Yenileme sonrası `consumeAppUpdatedVersion()` ile "güncellendi" bildirimi
|
|
|
|
|
|
* ve changelog yönlendirmesi yapılır.
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
export type ManualUpdateResult =
|
|
|
|
|
|
| 'up-to-date'
|
|
|
|
|
|
| 'updating'
|
|
|
|
|
|
/** Tarayıcı service worker desteklemiyor. */
|
|
|
|
|
|
| 'unsupported'
|
|
|
|
|
|
/** Destekleniyor ama bu ortamda kapalı (dev; VITE_PWA_DEV=true ile açılır). */
|
|
|
|
|
|
| 'disabled'
|
|
|
|
|
|
|
|
|
|
|
|
const PERIODIC_CHECK_INTERVAL = 15 * 60 * 1000
|
|
|
|
|
|
const MIN_CHECK_GAP = 60 * 1000
|
|
|
|
|
|
const ACTIVATION_RETRY_DELAY = 8_000
|
|
|
|
|
|
const ACTIVATION_TIMEOUT = 30_000
|
|
|
|
|
|
// `virtual:pwa-register` dev'de (devOptions kapalıyken) no-op'tur ve hiçbir
|
|
|
|
|
|
// callback çalışmaz. Kayıt promise'inin süresiz beklememesi için üst sınır.
|
|
|
|
|
|
const REGISTRATION_TIMEOUT = 10_000
|
|
|
|
|
|
// `registration.update()` yavaş/kopuk ağda süresiz askıda kalabiliyor.
|
|
|
|
|
|
const UPDATE_CALL_TIMEOUT = 20_000
|
|
|
|
|
|
|
|
|
|
|
|
const withTimeout = <T>(promise: Promise<T>, ms: number) =>
|
|
|
|
|
|
Promise.race([
|
|
|
|
|
|
promise,
|
|
|
|
|
|
new Promise<undefined>((resolve) => window.setTimeout(() => resolve(undefined), ms)),
|
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
|
|
const UPDATED_VERSION_KEY = 'sozsoft:app-updated-version'
|
|
|
|
|
|
const CHUNK_RELOAD_GUARD_KEY = 'sozsoft:chunk-reload'
|
2026-07-14 07:37:13 +00:00
|
|
|
|
|
2026-07-14 14:16:08 +00:00
|
|
|
|
type Release = {
|
|
|
|
|
|
version: string
|
|
|
|
|
|
changeLog?: string[]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-07 11:59:47 +00:00
|
|
|
|
let started = false
|
|
|
|
|
|
let registration: ServiceWorkerRegistration | undefined
|
|
|
|
|
|
let initialization: Promise<void> | undefined
|
|
|
|
|
|
let activating = false
|
|
|
|
|
|
let reloading = false
|
|
|
|
|
|
let lastCheckAt = 0
|
|
|
|
|
|
let periodicTimer: number | undefined
|
|
|
|
|
|
let activationRetryTimer: number | undefined
|
|
|
|
|
|
let activationTimeoutTimer: number | undefined
|
|
|
|
|
|
let storeUnsubscribe: (() => void) | undefined
|
|
|
|
|
|
let latestVersion: string | undefined
|
|
|
|
|
|
let releasePromise: Promise<Release | undefined> | undefined
|
|
|
|
|
|
|
|
|
|
|
|
const watchedRegistrations = new WeakSet<ServiceWorkerRegistration>()
|
|
|
|
|
|
const watchedWorkers = new WeakSet<ServiceWorker>()
|
|
|
|
|
|
|
|
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
|
|
/* Sürüm bilgisi */
|
|
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
|
|
|
|
|
|
|
|
const loadLatestRelease = () =>
|
|
|
|
|
|
(releasePromise ??= fetch(`/version.json?ts=${Date.now()}`, { cache: 'no-store' })
|
|
|
|
|
|
.then((response) => {
|
|
|
|
|
|
if (!response.ok) throw new Error(`version.json isteği ${response.status} döndü`)
|
|
|
|
|
|
return response.json() as Promise<{ releases?: Release[] }>
|
|
|
|
|
|
})
|
|
|
|
|
|
.then((data) => {
|
|
|
|
|
|
const release = data.releases?.[0]
|
|
|
|
|
|
latestVersion = release?.version ?? latestVersion
|
|
|
|
|
|
return release
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch((error: unknown) => {
|
|
|
|
|
|
console.warn('Sürüm bilgisi okunamadı.', error)
|
|
|
|
|
|
releasePromise = undefined
|
|
|
|
|
|
return undefined
|
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
|
|
/** Yenileme sonrası bir kez okunur; yeni sürüm numarasını döner. */
|
|
|
|
|
|
export const consumeAppUpdatedVersion = (): string | undefined => {
|
2026-07-14 14:16:08 +00:00
|
|
|
|
try {
|
2026-08-07 11:59:47 +00:00
|
|
|
|
const value = sessionStorage.getItem(UPDATED_VERSION_KEY)
|
|
|
|
|
|
if (value) sessionStorage.removeItem(UPDATED_VERSION_KEY)
|
|
|
|
|
|
return value ?? undefined
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return undefined
|
2026-07-14 14:16:08 +00:00
|
|
|
|
}
|
2026-08-07 11:59:47 +00:00
|
|
|
|
}
|
2026-07-14 14:16:08 +00:00
|
|
|
|
|
2026-08-07 11:59:47 +00:00
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
|
|
/* Arayüz */
|
|
|
|
|
|
/* ------------------------------------------------------------------ */
|
2026-07-14 14:16:08 +00:00
|
|
|
|
|
2026-08-07 11:59:47 +00:00
|
|
|
|
const STYLE_ID = 'sw-update-style'
|
|
|
|
|
|
const BANNER_ID = 'sw-update-banner'
|
|
|
|
|
|
const OVERLAY_ID = 'sw-update-overlay'
|
2026-07-14 14:16:08 +00:00
|
|
|
|
|
2026-08-07 11:59:47 +00:00
|
|
|
|
function ensureStyles() {
|
|
|
|
|
|
if (document.getElementById(STYLE_ID)) return
|
2026-05-09 07:17:25 +00:00
|
|
|
|
|
|
|
|
|
|
const style = document.createElement('style')
|
2026-08-07 11:59:47 +00:00
|
|
|
|
style.id = STYLE_ID
|
2026-05-09 07:17:25 +00:00
|
|
|
|
style.textContent = `
|
2026-08-07 11:59:47 +00:00
|
|
|
|
#${BANNER_ID} {
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
right: 16px;
|
|
|
|
|
|
bottom: 16px;
|
|
|
|
|
|
z-index: 99998;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
max-width: 320px;
|
|
|
|
|
|
padding: 12px 16px;
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
background: #111827;
|
|
|
|
|
|
color: #f9fafb;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
line-height: 1.4;
|
|
|
|
|
|
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
|
|
|
|
|
}
|
|
|
|
|
|
#${OVERLAY_ID} {
|
2026-05-09 07:17:25 +00:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
2026-08-07 11:59:47 +00:00
|
|
|
|
.sw-update-card {
|
2026-05-09 07:17:25 +00:00
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
2026-07-14 14:16:08 +00:00
|
|
|
|
gap: 16px;
|
2026-05-09 07:17:25 +00:00
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-radius: 16px;
|
2026-07-14 14:16:08 +00:00
|
|
|
|
padding: 32px 40px;
|
2026-05-09 07:17:25 +00:00
|
|
|
|
box-shadow: 0 8px 40px rgba(0,0,0,0.25);
|
|
|
|
|
|
text-align: center;
|
2026-07-14 14:16:08 +00:00
|
|
|
|
max-width: 560px;
|
|
|
|
|
|
max-height: min(90vh, 720px);
|
2026-05-09 07:17:25 +00:00
|
|
|
|
width: 90%;
|
|
|
|
|
|
}
|
2026-08-07 11:59:47 +00:00
|
|
|
|
.sw-update-spinner {
|
|
|
|
|
|
flex: 0 0 auto;
|
2026-05-09 07:17:25 +00:00
|
|
|
|
width: 56px;
|
|
|
|
|
|
height: 56px;
|
|
|
|
|
|
border: 5px solid #e5e7eb;
|
|
|
|
|
|
border-top-color: #6366f1;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
animation: sw-spin 0.8s linear infinite;
|
|
|
|
|
|
}
|
2026-08-07 11:59:47 +00:00
|
|
|
|
#${BANNER_ID} .sw-update-spinner {
|
|
|
|
|
|
width: 20px;
|
|
|
|
|
|
height: 20px;
|
|
|
|
|
|
border-width: 3px;
|
|
|
|
|
|
border-color: rgba(255,255,255,0.25);
|
|
|
|
|
|
border-top-color: #a5b4fc;
|
2026-05-09 07:17:25 +00:00
|
|
|
|
}
|
2026-08-07 11:59:47 +00:00
|
|
|
|
@keyframes sw-spin { to { transform: rotate(360deg); } }
|
|
|
|
|
|
.sw-update-title { font-size: 18px; font-weight: 700; color: #1f2937; margin: 0; }
|
|
|
|
|
|
.sw-update-desc { font-size: 14px; color: #6b7280; margin: 0; }
|
|
|
|
|
|
.sw-update-versions {
|
2026-07-14 14:16:08 +00:00
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
padding: 10px 12px;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
background: #f3f4f6;
|
|
|
|
|
|
color: #4b5563;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
}
|
2026-08-07 11:59:47 +00:00
|
|
|
|
.sw-update-separator { color: #9ca3af; margin: 0 4px; }
|
|
|
|
|
|
.sw-update-release {
|
2026-07-14 14:16:08 +00:00
|
|
|
|
width: 100%;
|
|
|
|
|
|
min-height: 0;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
text-align: left;
|
|
|
|
|
|
border-top: 1px solid #e5e7eb;
|
|
|
|
|
|
padding-top: 14px;
|
|
|
|
|
|
}
|
2026-08-07 11:59:47 +00:00
|
|
|
|
.sw-update-release-title { margin: 0 0 10px; color: #374151; font-size: 14px; font-weight: 700; }
|
|
|
|
|
|
.sw-update-list {
|
2026-07-14 14:16:08 +00:00
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
list-style: none;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
color: #6b7280;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
line-height: 1.45;
|
|
|
|
|
|
}
|
2026-08-07 11:59:47 +00:00
|
|
|
|
.sw-update-list li { display: flex; align-items: flex-start; gap: 8px; }
|
|
|
|
|
|
.sw-update-check {
|
2026-07-14 14:16:08 +00:00
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
flex: 0 0 16px;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
width: 16px;
|
|
|
|
|
|
height: 16px;
|
|
|
|
|
|
margin-top: 1px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
background: #d1fae5;
|
|
|
|
|
|
color: #059669;
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
}
|
2026-08-07 11:59:47 +00:00
|
|
|
|
.sw-update-empty { display: block; color: #9ca3af; }
|
|
|
|
|
|
.sw-update-action {
|
|
|
|
|
|
appearance: none;
|
|
|
|
|
|
border: 0;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
padding: 10px 20px;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
background: #4f46e5;
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 600;
|
2026-07-14 14:16:08 +00:00
|
|
|
|
}
|
2026-08-07 11:59:47 +00:00
|
|
|
|
.sw-update-action:hover { background: #4338ca; }
|
2026-07-14 14:16:08 +00:00
|
|
|
|
@media (max-width: 640px) {
|
2026-08-07 11:59:47 +00:00
|
|
|
|
.sw-update-card { padding: 24px 20px; }
|
|
|
|
|
|
#${BANNER_ID} { left: 16px; right: 16px; max-width: none; }
|
2026-07-14 14:16:08 +00:00
|
|
|
|
}
|
2026-05-09 07:17:25 +00:00
|
|
|
|
`
|
|
|
|
|
|
document.head.appendChild(style)
|
2026-08-07 11:59:47 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function removeStyles() {
|
|
|
|
|
|
document.getElementById(STYLE_ID)?.remove()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** İndirme sürerken kullanıcıyı engellemeyen bilgilendirme. */
|
|
|
|
|
|
function showDownloadBanner() {
|
|
|
|
|
|
if (document.getElementById(BANNER_ID) || document.getElementById(OVERLAY_ID)) return
|
|
|
|
|
|
ensureStyles()
|
|
|
|
|
|
|
|
|
|
|
|
const banner = document.createElement('div')
|
|
|
|
|
|
banner.id = BANNER_ID
|
|
|
|
|
|
banner.setAttribute('role', 'status')
|
|
|
|
|
|
banner.setAttribute('aria-live', 'polite')
|
|
|
|
|
|
|
|
|
|
|
|
const spinner = document.createElement('div')
|
|
|
|
|
|
spinner.className = 'sw-update-spinner'
|
|
|
|
|
|
|
|
|
|
|
|
const text = document.createElement('span')
|
|
|
|
|
|
text.textContent = 'Yeni sürüm indiriliyor, lütfen sekmeyi kapatmayın...'
|
|
|
|
|
|
|
|
|
|
|
|
banner.append(spinner, text)
|
|
|
|
|
|
document.body.appendChild(banner)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function removeDownloadBanner() {
|
|
|
|
|
|
document.getElementById(BANNER_ID)?.remove()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getText(value: unknown) {
|
|
|
|
|
|
return value === undefined || value === null ? undefined : String(value)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function applyApiVersion(overlay: HTMLElement) {
|
|
|
|
|
|
const apiConfig = store.getState().abpConfig.config?.extraProperties
|
|
|
|
|
|
const environment = getText(apiConfig?.environment)
|
|
|
|
|
|
const version = getText(apiConfig?.version)
|
|
|
|
|
|
if (!environment && !version) return false
|
|
|
|
|
|
|
|
|
|
|
|
const target = overlay.querySelector<HTMLElement>('[data-sw-api-version]')
|
|
|
|
|
|
if (target) target.textContent = `${environment ?? '-'}:${version ?? '-'}`
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function watchApiVersion(overlay: HTMLElement) {
|
|
|
|
|
|
storeUnsubscribe?.()
|
|
|
|
|
|
storeUnsubscribe = undefined
|
|
|
|
|
|
if (applyApiVersion(overlay)) return
|
|
|
|
|
|
|
|
|
|
|
|
storeUnsubscribe = store.subscribe(() => {
|
|
|
|
|
|
if (!overlay.isConnected || applyApiVersion(overlay)) {
|
|
|
|
|
|
storeUnsubscribe?.()
|
|
|
|
|
|
storeUnsubscribe = undefined
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function fillOverlayDetails(overlay: HTMLElement) {
|
|
|
|
|
|
const release = await loadLatestRelease()
|
|
|
|
|
|
if (!overlay.isConnected) return
|
|
|
|
|
|
|
|
|
|
|
|
const uiVersion = release?.version ?? store.getState().locale.currentUiVersion
|
|
|
|
|
|
const uiValue = overlay.querySelector<HTMLElement>('[data-sw-ui-version]')
|
|
|
|
|
|
if (uiValue) uiValue.textContent = `${import.meta.env.MODE}:${uiVersion ?? '-'}`
|
|
|
|
|
|
watchApiVersion(overlay)
|
|
|
|
|
|
|
|
|
|
|
|
const releaseTitle = overlay.querySelector<HTMLElement>('[data-sw-release-title]')
|
|
|
|
|
|
const releaseList = overlay.querySelector<HTMLUListElement>('[data-sw-release-list]')
|
|
|
|
|
|
if (!releaseTitle || !releaseList) return
|
|
|
|
|
|
|
|
|
|
|
|
releaseTitle.textContent = release?.version
|
|
|
|
|
|
? `v${release.version} ile gelen yenilikler`
|
|
|
|
|
|
: 'Sürüm notları'
|
|
|
|
|
|
releaseList.replaceChildren()
|
|
|
|
|
|
|
|
|
|
|
|
const changes = release?.changeLog?.filter(Boolean) ?? []
|
|
|
|
|
|
if (changes.length === 0) {
|
|
|
|
|
|
const item = document.createElement('li')
|
|
|
|
|
|
item.className = 'sw-update-empty'
|
|
|
|
|
|
item.textContent = 'Bu sürüm için not bulunamadı.'
|
|
|
|
|
|
releaseList.appendChild(item)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
changes.forEach((change) => {
|
|
|
|
|
|
const item = document.createElement('li')
|
|
|
|
|
|
const icon = document.createElement('span')
|
|
|
|
|
|
const text = document.createElement('span')
|
|
|
|
|
|
icon.className = 'sw-update-check'
|
|
|
|
|
|
icon.setAttribute('aria-hidden', 'true')
|
|
|
|
|
|
icon.textContent = '✓'
|
|
|
|
|
|
text.textContent = change.replace(/^\s*-\s*/, '')
|
|
|
|
|
|
item.append(icon, text)
|
|
|
|
|
|
releaseList.appendChild(item)
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function showActivationOverlay() {
|
|
|
|
|
|
removeDownloadBanner()
|
|
|
|
|
|
if (document.getElementById(OVERLAY_ID)) return
|
|
|
|
|
|
ensureStyles()
|
2026-05-09 07:17:25 +00:00
|
|
|
|
|
|
|
|
|
|
const overlay = document.createElement('div')
|
2026-08-07 11:59:47 +00:00
|
|
|
|
overlay.id = OVERLAY_ID
|
2026-07-14 14:16:08 +00:00
|
|
|
|
overlay.setAttribute('role', 'status')
|
|
|
|
|
|
overlay.setAttribute('aria-live', 'polite')
|
2026-05-09 07:17:25 +00:00
|
|
|
|
overlay.innerHTML = `
|
|
|
|
|
|
<div class="sw-update-card">
|
|
|
|
|
|
<div class="sw-update-spinner"></div>
|
2026-08-07 11:59:47 +00:00
|
|
|
|
<p class="sw-update-title" data-sw-title>Sistem güncelleniyor</p>
|
|
|
|
|
|
<p class="sw-update-desc" data-sw-desc>Yeni sürüm yükleniyor, lütfen bekleyin...<br/>Sayfa otomatik olarak yenilenecek.</p>
|
2026-07-14 14:16:08 +00:00
|
|
|
|
<div class="sw-update-versions">
|
2026-08-07 11:59:47 +00:00
|
|
|
|
<span><strong>UI:</strong> <span data-sw-ui-version>Yükleniyor...</span></span>
|
2026-07-14 14:16:08 +00:00
|
|
|
|
<span class="sw-update-separator">|</span>
|
2026-08-07 11:59:47 +00:00
|
|
|
|
<span><strong>API:</strong> <span data-sw-api-version>Yükleniyor...</span></span>
|
2026-07-14 14:16:08 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="sw-update-release">
|
2026-08-07 11:59:47 +00:00
|
|
|
|
<p class="sw-update-release-title" data-sw-release-title>Sürüm notları</p>
|
2026-07-14 14:16:08 +00:00
|
|
|
|
<ul class="sw-update-list" data-sw-release-list>
|
2026-08-07 11:59:47 +00:00
|
|
|
|
<li class="sw-update-empty">Sürüm notları yükleniyor...</li>
|
2026-07-14 14:16:08 +00:00
|
|
|
|
</ul>
|
|
|
|
|
|
</div>
|
2026-05-09 07:17:25 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
`
|
|
|
|
|
|
document.body.appendChild(overlay)
|
2026-08-07 11:59:47 +00:00
|
|
|
|
void fillOverlayDetails(overlay)
|
2026-05-09 07:17:25 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-07 11:59:47 +00:00
|
|
|
|
/** Aktivasyon tamamlanamazsa kullanıcıyı belirsizlikte bırakma. */
|
|
|
|
|
|
function showManualReloadPrompt() {
|
|
|
|
|
|
const overlay = document.getElementById(OVERLAY_ID)
|
|
|
|
|
|
if (!overlay) return
|
|
|
|
|
|
|
|
|
|
|
|
overlay.querySelector<HTMLElement>('.sw-update-spinner')?.remove()
|
|
|
|
|
|
|
|
|
|
|
|
const title = overlay.querySelector<HTMLElement>('[data-sw-title]')
|
|
|
|
|
|
const desc = overlay.querySelector<HTMLElement>('[data-sw-desc]')
|
|
|
|
|
|
if (title) title.textContent = 'Güncelleme tamamlanamadı'
|
|
|
|
|
|
if (desc) {
|
|
|
|
|
|
desc.textContent = 'Yeni sürüme geçmek için sayfayı yenileyin.'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (overlay.querySelector('.sw-update-action')) return
|
|
|
|
|
|
const button = document.createElement('button')
|
|
|
|
|
|
button.type = 'button'
|
|
|
|
|
|
button.className = 'sw-update-action'
|
|
|
|
|
|
button.textContent = 'Sayfayı yenile'
|
|
|
|
|
|
button.addEventListener('click', () => reloadForUpdate())
|
|
|
|
|
|
overlay.querySelector('.sw-update-card')?.appendChild(button)
|
2026-07-14 07:37:13 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-07 11:59:47 +00:00
|
|
|
|
function teardownUpdateUi() {
|
|
|
|
|
|
storeUnsubscribe?.()
|
|
|
|
|
|
storeUnsubscribe = undefined
|
|
|
|
|
|
removeDownloadBanner()
|
|
|
|
|
|
document.getElementById(OVERLAY_ID)?.remove()
|
|
|
|
|
|
removeStyles()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
|
|
/* Aktivasyon */
|
|
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
|
|
|
2026-07-14 07:37:13 +00:00
|
|
|
|
function clearActivationTimers() {
|
|
|
|
|
|
window.clearTimeout(activationRetryTimer)
|
|
|
|
|
|
window.clearTimeout(activationTimeoutTimer)
|
|
|
|
|
|
activationRetryTimer = undefined
|
|
|
|
|
|
activationTimeoutTimer = undefined
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function reloadForUpdate() {
|
2026-08-07 11:59:47 +00:00
|
|
|
|
if (reloading) return
|
|
|
|
|
|
reloading = true
|
2026-07-14 07:37:13 +00:00
|
|
|
|
clearActivationTimers()
|
2026-08-07 11:59:47 +00:00
|
|
|
|
try {
|
|
|
|
|
|
if (latestVersion) sessionStorage.setItem(UPDATED_VERSION_KEY, latestVersion)
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
/* sessionStorage kullanılamıyorsa bildirim atlanır */
|
|
|
|
|
|
}
|
2026-07-14 07:37:13 +00:00
|
|
|
|
window.location.reload()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-07 11:59:47 +00:00
|
|
|
|
function postSkipWaiting() {
|
|
|
|
|
|
registration?.waiting?.postMessage({ type: 'SKIP_WAITING' })
|
|
|
|
|
|
}
|
2026-07-14 07:37:13 +00:00
|
|
|
|
|
2026-08-07 11:59:47 +00:00
|
|
|
|
function activateWaitingWorker() {
|
|
|
|
|
|
if (activating) {
|
|
|
|
|
|
showActivationOverlay()
|
|
|
|
|
|
return
|
2026-07-14 07:37:13 +00:00
|
|
|
|
}
|
2026-08-07 11:59:47 +00:00
|
|
|
|
activating = true
|
|
|
|
|
|
showActivationOverlay()
|
|
|
|
|
|
// Sürüm numarasını yenileme öncesinde hazır tut.
|
|
|
|
|
|
void loadLatestRelease()
|
2026-07-14 07:37:13 +00:00
|
|
|
|
|
2026-08-07 11:59:47 +00:00
|
|
|
|
navigator.serviceWorker.addEventListener('controllerchange', reloadForUpdate, { once: true })
|
|
|
|
|
|
postSkipWaiting()
|
2026-07-14 07:37:13 +00:00
|
|
|
|
|
2026-08-07 11:59:47 +00:00
|
|
|
|
// Bazı tarayıcılarda ilk mesaj bekleyen worker'a ulaşmayabiliyor.
|
|
|
|
|
|
activationRetryTimer = window.setTimeout(postSkipWaiting, ACTIVATION_RETRY_DELAY)
|
2026-07-14 07:37:13 +00:00
|
|
|
|
|
|
|
|
|
|
activationTimeoutTimer = window.setTimeout(() => {
|
|
|
|
|
|
clearActivationTimers()
|
2026-08-07 11:59:47 +00:00
|
|
|
|
activating = false
|
2026-07-14 07:37:13 +00:00
|
|
|
|
|
2026-08-07 11:59:47 +00:00
|
|
|
|
// Worker waiting'den çıktıysa aktivasyon olmuş, sadece controllerchange
|
|
|
|
|
|
// kaçmış olabilir; yeni precache'i almak için yenile.
|
2026-07-14 07:37:13 +00:00
|
|
|
|
if (!registration?.waiting && registration?.active) {
|
|
|
|
|
|
reloadForUpdate()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-07 11:59:47 +00:00
|
|
|
|
console.warn('Service worker aktivasyonu zaman aşımına uğradı.')
|
|
|
|
|
|
showManualReloadPrompt()
|
2026-07-14 07:37:13 +00:00
|
|
|
|
}, ACTIVATION_TIMEOUT)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-07 11:59:47 +00:00
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
|
|
/* Güncelleme takibi */
|
|
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
|
|
|
|
|
|
|
|
function trackInstallingWorker(reg: ServiceWorkerRegistration) {
|
|
|
|
|
|
const worker = reg.installing
|
|
|
|
|
|
// Controller yoksa bu ilk kurulumdur; kullanıcıya güncelleme demek yanlış olur.
|
|
|
|
|
|
if (!worker || !navigator.serviceWorker.controller || watchedWorkers.has(worker)) return
|
|
|
|
|
|
|
|
|
|
|
|
watchedWorkers.add(worker)
|
|
|
|
|
|
showDownloadBanner()
|
|
|
|
|
|
|
|
|
|
|
|
worker.addEventListener('statechange', () => {
|
|
|
|
|
|
if (worker.state === 'installed') {
|
|
|
|
|
|
activateWaitingWorker()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
// Precache dosyalarından biri indirilemezse worker redundant olur; eski
|
|
|
|
|
|
// sürüm çalışmaya devam eder ve kullanıcı kilitli ekranda kalmaz.
|
|
|
|
|
|
if (worker.state === 'redundant' && !activating) {
|
|
|
|
|
|
console.warn('Yeni service worker kurulamadı, mevcut sürümle devam ediliyor.')
|
|
|
|
|
|
teardownUpdateUi()
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function watchRegistration(reg: ServiceWorkerRegistration) {
|
|
|
|
|
|
if (watchedRegistrations.has(reg)) return
|
|
|
|
|
|
watchedRegistrations.add(reg)
|
|
|
|
|
|
reg.addEventListener('updatefound', () => trackInstallingWorker(reg))
|
|
|
|
|
|
trackInstallingWorker(reg)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Kayıtlı worker'ın script'i artık sunulmuyorsa (ör. PWA kapatıldı, dosya adı
|
|
|
|
|
|
* değişti) sunucu SPA fallback'i yüzünden index.html döner ve tarayıcı
|
|
|
|
|
|
* "unsupported MIME type" SecurityError'ı fırlatır. Bu durumda eski kayıt
|
|
|
|
|
|
* kalıcı olarak bozuktur; temizlenmezse her kontrol aynı hatayı verir.
|
|
|
|
|
|
*/
|
|
|
|
|
|
async function unregisterIfStale(reg: ServiceWorkerRegistration, error: unknown) {
|
|
|
|
|
|
const isStaleScript =
|
|
|
|
|
|
error instanceof Error && (error.name === 'SecurityError' || /MIME type/i.test(error.message))
|
|
|
|
|
|
if (!isStaleScript) return false
|
|
|
|
|
|
|
|
|
|
|
|
console.warn('Geçersiz service worker kaydı temizleniyor.', error)
|
|
|
|
|
|
try {
|
|
|
|
|
|
await reg.unregister()
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
/* temizlenemediyse bir sonraki açılışta tekrar denenir */
|
2026-07-14 07:37:13 +00:00
|
|
|
|
}
|
2026-08-07 11:59:47 +00:00
|
|
|
|
if (registration === reg) registration = undefined
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function runUpdateCheck(force = false) {
|
|
|
|
|
|
if (!registration || activating || reloading) return
|
|
|
|
|
|
if (!force && !navigator.onLine) return
|
2026-07-14 07:37:13 +00:00
|
|
|
|
|
2026-08-07 11:59:47 +00:00
|
|
|
|
const now = Date.now()
|
|
|
|
|
|
if (!force && now - lastCheckAt < MIN_CHECK_GAP) return
|
|
|
|
|
|
lastCheckAt = now
|
2026-07-14 07:37:13 +00:00
|
|
|
|
|
2026-08-07 11:59:47 +00:00
|
|
|
|
const reg = registration
|
|
|
|
|
|
try {
|
|
|
|
|
|
await withTimeout(reg.update(), UPDATE_CALL_TIMEOUT)
|
|
|
|
|
|
} catch (error: unknown) {
|
|
|
|
|
|
if (!(await unregisterIfStale(reg, error))) {
|
|
|
|
|
|
console.warn('Service worker güncelleme kontrolü başarısız.', error)
|
2026-07-14 07:37:13 +00:00
|
|
|
|
}
|
2026-08-07 11:59:47 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function startUpdateWatchers() {
|
|
|
|
|
|
window.clearInterval(periodicTimer)
|
|
|
|
|
|
periodicTimer = window.setInterval(() => {
|
|
|
|
|
|
if (document.visibilityState === 'visible') void runUpdateCheck()
|
|
|
|
|
|
}, PERIODIC_CHECK_INTERVAL)
|
|
|
|
|
|
|
|
|
|
|
|
document.addEventListener('visibilitychange', () => {
|
|
|
|
|
|
if (document.visibilityState === 'visible') void runUpdateCheck()
|
2026-07-14 07:37:13 +00:00
|
|
|
|
})
|
2026-08-07 11:59:47 +00:00
|
|
|
|
window.addEventListener('online', () => void runUpdateCheck())
|
2026-07-14 07:37:13 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-07 11:59:47 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* Deploy sonrası sunucudan kaldırılmış bir lazy chunk istenirse Vite
|
|
|
|
|
|
* `vite:preloadError` fırlatır. Beyaz ekran yerine bir kez sayfayı yenile.
|
|
|
|
|
|
*/
|
|
|
|
|
|
function handleChunkLoadFailures() {
|
|
|
|
|
|
window.addEventListener('vite:preloadError', (event) => {
|
|
|
|
|
|
if (reloading) return
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (sessionStorage.getItem(CHUNK_RELOAD_GUARD_KEY)) return
|
|
|
|
|
|
sessionStorage.setItem(CHUNK_RELOAD_GUARD_KEY, '1')
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
event.preventDefault()
|
|
|
|
|
|
console.warn('Eksik chunk tespit edildi, sayfa yenileniyor.')
|
|
|
|
|
|
reloading = true
|
|
|
|
|
|
window.location.reload()
|
2026-07-14 07:37:13 +00:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-08-07 11:59:47 +00:00
|
|
|
|
// Sayfa sorunsuz açıldıysa koruma bayrağını temizle.
|
|
|
|
|
|
window.setTimeout(() => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
sessionStorage.removeItem(CHUNK_RELOAD_GUARD_KEY)
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
/* yoksay */
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 10_000)
|
2026-07-14 07:37:13 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-07 11:59:47 +00:00
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
|
|
/* Genel API */
|
|
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
|
|
|
|
|
|
|
|
/** Service worker yalnızca prod'da, dev'de ise VITE_PWA_DEV=true ile aktiftir. */
|
|
|
|
|
|
const isServiceWorkerEnabled = import.meta.env.PROD || import.meta.env.VITE_PWA_DEV === 'true'
|
|
|
|
|
|
|
|
|
|
|
|
/** Kapalıyken önceki oturumlardan kalan kayıtları temizler. */
|
|
|
|
|
|
async function unregisterAll() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const registrations = await navigator.serviceWorker.getRegistrations()
|
|
|
|
|
|
await Promise.all(registrations.map((reg) => reg.unregister().catch(() => false)))
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
/* yoksay */
|
|
|
|
|
|
}
|
2026-07-14 07:37:13 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 20:44:16 +00:00
|
|
|
|
export const registerServiceWorker = () => {
|
2026-08-07 11:59:47 +00:00
|
|
|
|
if (started || !('serviceWorker' in navigator)) return
|
|
|
|
|
|
started = true
|
|
|
|
|
|
|
|
|
|
|
|
handleChunkLoadFailures()
|
2026-07-14 07:37:13 +00:00
|
|
|
|
|
2026-08-07 11:59:47 +00:00
|
|
|
|
if (!isServiceWorkerEnabled) {
|
|
|
|
|
|
// Dev'de SW kapalı. Daha önce kaydedilmiş dev worker'ı bırakırsak script'i
|
|
|
|
|
|
// (/dev-sw.js) artık üretilmediği için sunucu index.html döner ve her
|
|
|
|
|
|
// güncelleme kontrolü "unsupported MIME type" hatası verir.
|
|
|
|
|
|
initialization = unregisterAll()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
initialization = new Promise<void>((resolve) => {
|
|
|
|
|
|
// Kayıt callback'i hiç çalışmazsa bile promise'i serbest bırak; aksi halde
|
|
|
|
|
|
// `checkForAppUpdate()` "Kontrol ediliyor..." durumunda takılı kalır.
|
|
|
|
|
|
const registrationTimeout = window.setTimeout(resolve, REGISTRATION_TIMEOUT)
|
|
|
|
|
|
const settle = () => {
|
|
|
|
|
|
window.clearTimeout(registrationTimeout)
|
|
|
|
|
|
resolve()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
registerSW({
|
|
|
|
|
|
immediate: true,
|
|
|
|
|
|
onRegisteredSW(_swUrl, reg) {
|
|
|
|
|
|
if (reg) {
|
|
|
|
|
|
registration = reg
|
|
|
|
|
|
watchRegistration(reg)
|
|
|
|
|
|
startUpdateWatchers()
|
|
|
|
|
|
|
|
|
|
|
|
// Kurulumu tamamlanmış, aktif olandan farklı bir worker bekliyorsa
|
|
|
|
|
|
// doğrudan devreye al.
|
|
|
|
|
|
if (reg.waiting && navigator.serviceWorker.controller) {
|
|
|
|
|
|
activateWaitingWorker()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
void runUpdateCheck(true)
|
2026-07-14 07:37:13 +00:00
|
|
|
|
}
|
2026-08-07 11:59:47 +00:00
|
|
|
|
}
|
|
|
|
|
|
settle()
|
|
|
|
|
|
},
|
|
|
|
|
|
onNeedRefresh() {
|
|
|
|
|
|
// Yeni worker tüm kabuk dosyalarını precache'e aldıktan sonra çalışır.
|
|
|
|
|
|
activateWaitingWorker()
|
|
|
|
|
|
},
|
|
|
|
|
|
onRegisterError(error) {
|
|
|
|
|
|
activating = false
|
|
|
|
|
|
clearActivationTimers()
|
|
|
|
|
|
teardownUpdateUi()
|
|
|
|
|
|
console.error('Service worker kaydı başarısız.', error)
|
|
|
|
|
|
settle()
|
|
|
|
|
|
},
|
2026-05-09 07:17:25 +00:00
|
|
|
|
})
|
2026-08-07 11:59:47 +00:00
|
|
|
|
})
|
2026-07-14 07:37:13 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export const checkForAppUpdate = async (): Promise<ManualUpdateResult> => {
|
|
|
|
|
|
if (!('serviceWorker' in navigator)) return 'unsupported'
|
2026-08-07 11:59:47 +00:00
|
|
|
|
if (!isServiceWorkerEnabled) return 'disabled'
|
2026-07-14 07:37:13 +00:00
|
|
|
|
|
2026-08-07 11:59:47 +00:00
|
|
|
|
await initialization
|
|
|
|
|
|
// `navigator.serviceWorker.ready` kayıt yokken (ör. dev ortamı) hiçbir zaman
|
|
|
|
|
|
// resolve etmez; getRegistration() kayıt yoksa undefined döner.
|
|
|
|
|
|
const reg =
|
|
|
|
|
|
registration ?? (await navigator.serviceWorker.getRegistration().catch(() => undefined))
|
|
|
|
|
|
if (!reg) return 'unsupported'
|
2026-07-14 07:37:13 +00:00
|
|
|
|
|
2026-08-07 11:59:47 +00:00
|
|
|
|
registration ??= reg
|
|
|
|
|
|
watchRegistration(reg)
|
|
|
|
|
|
|
|
|
|
|
|
if (reg.waiting && navigator.serviceWorker.controller) {
|
|
|
|
|
|
activateWaitingWorker()
|
|
|
|
|
|
return 'updating'
|
|
|
|
|
|
}
|
2026-07-14 07:37:13 +00:00
|
|
|
|
|
|
|
|
|
|
let updateFound = false
|
|
|
|
|
|
const handleUpdateFound = () => {
|
|
|
|
|
|
updateFound = true
|
|
|
|
|
|
}
|
2026-08-07 11:59:47 +00:00
|
|
|
|
|
|
|
|
|
|
let staleRegistrationCleared = false
|
|
|
|
|
|
reg.addEventListener('updatefound', handleUpdateFound)
|
2026-07-14 07:37:13 +00:00
|
|
|
|
try {
|
2026-08-07 11:59:47 +00:00
|
|
|
|
await withTimeout(reg.update(), UPDATE_CALL_TIMEOUT)
|
|
|
|
|
|
} catch (error: unknown) {
|
|
|
|
|
|
staleRegistrationCleared = await unregisterIfStale(reg, error)
|
|
|
|
|
|
if (!staleRegistrationCleared) {
|
|
|
|
|
|
console.warn('Manuel güncelleme kontrolü başarısız.', error)
|
|
|
|
|
|
}
|
2026-07-14 07:37:13 +00:00
|
|
|
|
} finally {
|
2026-08-07 11:59:47 +00:00
|
|
|
|
reg.removeEventListener('updatefound', handleUpdateFound)
|
|
|
|
|
|
lastCheckAt = Date.now()
|
2026-05-09 07:17:25 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-07 11:59:47 +00:00
|
|
|
|
// Bozuk kayıt temizlendi; bir sonraki açılışta yeni worker kaydedilecek.
|
|
|
|
|
|
if (staleRegistrationCleared) return 'up-to-date'
|
2026-07-14 07:37:13 +00:00
|
|
|
|
|
2026-08-07 11:59:47 +00:00
|
|
|
|
if (!updateFound && !reg.installing && !reg.waiting) return 'up-to-date'
|
2026-07-14 07:37:13 +00:00
|
|
|
|
|
2026-08-07 11:59:47 +00:00
|
|
|
|
if (reg.waiting && navigator.serviceWorker.controller) activateWaitingWorker()
|
|
|
|
|
|
// `installing` durumundaysa statechange dinleyicisi aynı akışı sürdürür.
|
2026-07-14 07:37:13 +00:00
|
|
|
|
return 'updating'
|
2026-02-24 20:44:16 +00:00
|
|
|
|
}
|