Fix animation stale reference

This commit is contained in:
2026-07-21 16:03:28 +10:00
parent d2d82ae6ae
commit 9af84760fa
12 changed files with 225 additions and 38 deletions

View File

@@ -1,18 +1,83 @@
<script setup lang="ts">
import { onBeforeUnmount, ref, watch } from 'vue'
import { useRegisterSW } from 'virtual:pwa-register/vue'
import { useNativeRouter } from '@native-vue-router/core'
import { recordPwaUpdateState } from '../pwa'
const native = useNativeRouter()
const { needRefresh, updateServiceWorker } = useRegisterSW()
const reloadPending = ref(false)
let registration: ServiceWorkerRegistration | undefined
let checkTimer: number | undefined
let reloading = false
let checking = false
function reloadWhenIdle() {
if (!reloadPending.value || native.transaction.value || reloading) return
reloading = true
window.location.reload()
}
async function checkForUpdate() {
if (!registration || checking || document.visibilityState === 'hidden' || !navigator.onLine) return
checking = true
recordPwaUpdateState('checking', true)
try {
await registration.update()
if (!reloadPending.value) recordPwaUpdateState('current')
} catch {
recordPwaUpdateState('error')
} finally {
checking = false
}
}
const { needRefresh, updateServiceWorker } = useRegisterSW({
immediate: true,
onRegisteredSW(_workerUrl, workerRegistration) {
registration = workerRegistration
void checkForUpdate()
if (checkTimer !== undefined) window.clearInterval(checkTimer)
checkTimer = window.setInterval(() => void checkForUpdate(), 60_000)
},
onNeedRefresh() {
recordPwaUpdateState('ready')
},
onNeedReload() {
recordPwaUpdateState('ready')
reloadPending.value = true
reloadWhenIdle()
},
onRegisterError() {
recordPwaUpdateState('error')
},
})
function checkWhenActive() {
if (document.visibilityState === 'visible') void checkForUpdate()
}
window.addEventListener('focus', checkWhenActive)
window.addEventListener('online', checkWhenActive)
document.addEventListener('visibilitychange', checkWhenActive)
watch(() => native.transaction.value, reloadWhenIdle, { flush: 'post' })
function update() {
reloadPending.value = true
if (!native.transaction.value) void updateServiceWorker(true)
reloadWhenIdle()
}
onBeforeUnmount(() => {
if (checkTimer !== undefined) window.clearInterval(checkTimer)
window.removeEventListener('focus', checkWhenActive)
window.removeEventListener('online', checkWhenActive)
document.removeEventListener('visibilitychange', checkWhenActive)
})
</script>
<template>
<aside v-if="needRefresh" class="update-toast" role="status">
<span>A fresh build is ready.</span>
<button type="button" :disabled="Boolean(native.transaction.value)" @click="update">Update</button>
<aside v-if="needRefresh || reloadPending" class="update-toast" role="status">
<span>{{ native.transaction.value ? 'A fresh build will open after this gesture.' : 'A fresh build is ready.' }}</span>
<button v-if="needRefresh" type="button" :disabled="Boolean(native.transaction.value)" @click="update">Update</button>
</aside>
</template>

View File

@@ -6,6 +6,11 @@ interface StandaloneNavigator extends Navigator {
}
type ServiceWorkerState = 'unsupported' | 'installing' | 'ready'
export type PwaUpdateState = 'idle' | 'checking' | 'current' | 'ready' | 'error'
declare const __NVR_BUILD_ID__: string
export const pwaBuildId = __NVR_BUILD_ID__
const state = reactive({
ios: false,
@@ -13,10 +18,19 @@ const state = reactive({
edgeGuard: false,
edgeClaims: 0,
serviceWorker: 'installing' as ServiceWorkerState,
updateState: 'idle' as PwaUpdateState,
updateChecks: 0,
})
export const pwaEnvironment = readonly(state)
export function recordPwaUpdateState(updateState: PwaUpdateState, checked = false) {
state.updateState = updateState
if (checked) state.updateChecks += 1
document.documentElement.dataset.pwaUpdateState = state.updateState
document.documentElement.dataset.pwaUpdateChecks = String(state.updateChecks)
}
export function isIOSWebKit() {
const navigatorWithTouch = navigator as Navigator & { maxTouchPoints?: number }
return /iPad|iPhone|iPod/.test(navigator.userAgent)
@@ -37,6 +51,8 @@ function updateEnvironment() {
document.documentElement.dataset.pwaDisplayMode = state.standalone ? 'standalone' : 'browser'
document.documentElement.dataset.pwaEdgeGuard = state.edgeGuard ? 'active' : 'inactive'
document.documentElement.dataset.pwaEdgeClaims = String(state.edgeClaims)
document.documentElement.dataset.pwaUpdateState = state.updateState
document.documentElement.dataset.pwaUpdateChecks = String(state.updateChecks)
}
export interface PwaAdapterOptions {

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import AppHeader from '../components/AppHeader.vue'
import { useDemoStore } from '../data'
import { pwaEnvironment } from '../pwa'
import { pwaBuildId, pwaEnvironment } from '../pwa'
const store = useDemoStore()
</script>
@@ -17,6 +17,7 @@ const store = useDemoStore()
<h2>PWA environment</h2>
<div><span><strong>Display mode</strong><small>Home Screen installation state</small></span><b :class="{ offline: !pwaEnvironment.standalone }">{{ pwaEnvironment.standalone ? 'Standalone' : 'Browser tab' }}</b></div>
<div><span><strong>Offline worker</strong><small>Cached application shell</small></span><b :class="{ offline: pwaEnvironment.serviceWorker !== 'ready' }">{{ pwaEnvironment.serviceWorker }}</b></div>
<div><span><strong>App build</strong><small>{{ pwaBuildId }}</small></span><b :class="{ offline: pwaEnvironment.updateState === 'error' }">{{ pwaEnvironment.updateState }} · {{ pwaEnvironment.updateChecks }} checks</b></div>
<div><span><strong>iOS edge ownership</strong><small>Leading-edge touches claimed by this app: {{ pwaEnvironment.edgeClaims }}</small></span><b :class="{ offline: !pwaEnvironment.edgeGuard }">{{ pwaEnvironment.edgeGuard ? 'App reserved' : pwaEnvironment.ios ? 'Install required' : 'Not iOS' }}</b></div>
<p v-if="pwaEnvironment.ios && !pwaEnvironment.standalone">In Safari, choose Share Add to Home Screen, then launch the new icon. Edge interception is intentionally disabled inside a normal browser tab.</p>
<p v-else-if="pwaEnvironment.ios">The leading edge is reserved before WebKit navigation begins. Open a conversation and drag from the extreme left edge to verify the live back preview.</p>