import { computed, onMounted, onScopeDispose, readonly, ref, toValue, type MaybeRefOrGetter, type Ref, } from 'vue' const RUNTIME_RELOAD_KEY_PREFIX = 'mint:runtime-reload:' const REVISION_QUERY_KEY = '__mint_revision' const inMemoryReloadGuards = new Set() interface RuntimeStorage { getItem(key: string): string | null setItem(key: string, value: string): void } interface RuntimeEventTarget { addEventListener(type: 'vite:preloadError', listener: EventListener): void removeEventListener(type: 'vite:preloadError', listener: EventListener): void } interface RuntimeVisibilityTarget { addEventListener(type: 'visibilitychange', listener: EventListener): void removeEventListener(type: 'visibilitychange', listener: EventListener): void } export interface RuntimeRevisionResponse { frontend_revision?: string | null } export interface RuntimeAlignmentOptions { /** Revision embedded in the HTML that booted this tab. */ currentRevision?: MaybeRefOrGetter /** Browser visibility seam. */ visibility?: () => DocumentVisibilityState /** Current location seam. */ currentUrl?: () => string /** Location replacement seam. */ replace?: (url: string) => void /** Per-tab recovery guard storage. */ storage?: RuntimeStorage | null /** Vite preload error source. */ eventTarget?: RuntimeEventTarget | null /** Browser visibility event source. */ visibilityTarget?: RuntimeVisibilityTarget | null } export interface UseRuntimeAlignmentReturn { updateAvailable: Readonly> nextRevision: Readonly> observe: (response: RuntimeRevisionResponse | unknown) => void reload: () => void recoverMissingChunk: () => void } function browserStorage(): RuntimeStorage | null { try { return globalThis.sessionStorage ?? null } catch { return null } } function browserEventTarget(): RuntimeEventTarget | null { return typeof globalThis.window === 'undefined' ? null : globalThis.window } function browserVisibilityTarget(): RuntimeVisibilityTarget | null { return typeof globalThis.document === 'undefined' ? null : globalThis.document } function responseRevision(response: RuntimeRevisionResponse | unknown): string | null { if (!response || typeof response !== 'object') return null const value = (response as RuntimeRevisionResponse).frontend_revision return typeof value === 'string' && value ? value : null } /** Keep a loaded MINT frontend aligned with the backend revision serving its scope. */ export function useRuntimeAlignment( options: RuntimeAlignmentOptions = {}, ): UseRuntimeAlignmentReturn { const storage = options.storage === undefined ? browserStorage() : options.storage const eventTarget = options.eventTarget === undefined ? browserEventTarget() : options.eventTarget const visibilityTarget = options.visibilityTarget === undefined ? browserVisibilityTarget() : options.visibilityTarget const visibility = options.visibility ?? (() => globalThis.document?.visibilityState ?? 'visible') const currentUrl = options.currentUrl ?? (() => globalThis.location?.href ?? '/') const replace = options.replace ?? ((url: string) => globalThis.location?.replace(url)) const initialRevision = computed(() => toValue(options.currentRevision) ?? null) const baselineRevision = ref(initialRevision.value) const nextRevision = ref(null) const updateAvailable = computed(() => nextRevision.value !== null) let wasHidden = false function reloadTo(revision: string, guardKind: 'revision' | 'chunk'): void { const guardKey = `${RUNTIME_RELOAD_KEY_PREFIX}${guardKind}:${revision}` const url = new URL(currentUrl(), globalThis.location?.origin ?? 'http://localhost') if (url.searchParams.get(REVISION_QUERY_KEY) === revision) return if (inMemoryReloadGuards.has(guardKey)) return try { if (storage?.getItem(guardKey)) { inMemoryReloadGuards.add(guardKey) return } storage?.setItem(guardKey, '1') } catch { // A blocked sessionStorage must not block recovery. } inMemoryReloadGuards.add(guardKey) url.searchParams.set(REVISION_QUERY_KEY, revision) replace(url.toString()) } function observe(response: RuntimeRevisionResponse | unknown): void { const revision = responseRevision(response) if (!revision) return const current = initialRevision.value ?? baselineRevision.value if (!current) { baselineRevision.value = revision return } if (revision === current) { wasHidden = false return } if (wasHidden || visibility() === 'hidden') { wasHidden = false reloadTo(revision, 'revision') return } nextRevision.value = revision } function reload(): void { if (nextRevision.value) reloadTo(nextRevision.value, 'revision') } function recoverMissingChunk(): void { reloadTo(baselineRevision.value ?? initialRevision.value ?? 'unknown', 'chunk') } function handlePreloadError(event: Event): void { event.preventDefault() recoverMissingChunk() } function handleVisibilityChange(): void { if (visibility() === 'hidden') wasHidden = true } onMounted(() => { eventTarget?.addEventListener('vite:preloadError', handlePreloadError) visibilityTarget?.addEventListener('visibilitychange', handleVisibilityChange) }) onScopeDispose(() => { eventTarget?.removeEventListener('vite:preloadError', handlePreloadError) visibilityTarget?.removeEventListener('visibilitychange', handleVisibilityChange) }) return { updateAvailable: readonly(updateAvailable), nextRevision: readonly(nextRevision), observe, reload, recoverMissingChunk, } } /** Read the revision injected into the HTML that started this browser tab. */ export function getInjectedFrontendRevision(): string | null { if (typeof globalThis.window === 'undefined') return null const runtimeWindow = globalThis.window as typeof globalThis.window & { __MINT_FRONTEND_REVISION__?: string __MINT_PLATFORM__?: { plugin?: { frontend_revision?: string } } } return runtimeWindow.__MINT_PLATFORM__?.plugin?.frontend_revision ?? runtimeWindow.__MINT_FRONTEND_REVISION__ ?? null }