216 行
5.8 KiB
TypeScript
216 行
5.8 KiB
TypeScript
import { registerSW } from "virtual:pwa-register";
|
|
import { canUseWindow, isEnterpriseMessengerContainer, matchesMedia } from "./lib/runtimeContext";
|
|
import { PWA_UPDATE_READY_EVENT } from "./lib/pwaUpdateEvents";
|
|
|
|
const MOBILE_APPROVAL_TASK_PATH = /^\/workflow\/tasks\/([^/]+)$/i;
|
|
const OFFLINE_ALLOWED_PATHS = ["/mobile/approvals"];
|
|
const CLEARABLE_CACHE_PREFIXES = ["app-api-runtime", "app-static-runtime", "workbox-precache"];
|
|
const AUTH_INPUT_PATHS = ["/login", "/change-password", "/forgot-password", "/reset-password"];
|
|
const BUILD_SHA_DISPLAY_LENGTH = 12;
|
|
|
|
type ReadyResponse = {
|
|
data?: {
|
|
buildSha?: unknown;
|
|
};
|
|
};
|
|
|
|
let buildFreshnessMonitorInstalled = false;
|
|
|
|
function isAuthInputPath(pathname: string) {
|
|
return AUTH_INPUT_PATHS.some((path) => pathname === path || pathname.startsWith(`${path}/`));
|
|
}
|
|
|
|
function bindServiceWorkerUpdateCleanup(registration: ServiceWorkerRegistration | undefined) {
|
|
if (!canUseWindow() || !registration) {
|
|
return;
|
|
}
|
|
|
|
const watchInstallingWorker = (worker: ServiceWorker | null) => {
|
|
if (!worker) {
|
|
return;
|
|
}
|
|
|
|
worker.addEventListener("statechange", () => {
|
|
if (worker.state === "installed" && window.navigator.serviceWorker.controller) {
|
|
void clearPwaRuntimeCaches();
|
|
}
|
|
});
|
|
};
|
|
|
|
watchInstallingWorker(registration.installing);
|
|
registration.addEventListener("updatefound", () => {
|
|
watchInstallingWorker(registration.installing);
|
|
});
|
|
}
|
|
|
|
function normalizeBuildSha(value: unknown) {
|
|
const normalized = String(value ?? "").trim().toLowerCase();
|
|
if (
|
|
!normalized ||
|
|
normalized === "unknown" ||
|
|
normalized === "development" ||
|
|
!/^[a-f0-9]{7,64}$/.test(normalized)
|
|
) {
|
|
return null;
|
|
}
|
|
return normalized.slice(0, BUILD_SHA_DISPLAY_LENGTH);
|
|
}
|
|
|
|
function buildUserControlledRefresh() {
|
|
return () => {
|
|
void clearPwaRuntimeCaches()
|
|
.catch(() => undefined)
|
|
.finally(() => {
|
|
window.location.reload();
|
|
});
|
|
};
|
|
}
|
|
|
|
export function installBuildFreshnessMonitor() {
|
|
if (!canUseWindow() || buildFreshnessMonitorInstalled) {
|
|
return;
|
|
}
|
|
|
|
buildFreshnessMonitorInstalled = true;
|
|
let checkInFlight = false;
|
|
let mismatchNotified = false;
|
|
|
|
const checkBuildFreshness = async () => {
|
|
if (document.visibilityState !== "visible" || checkInFlight || mismatchNotified) {
|
|
return;
|
|
}
|
|
|
|
const clientBuildSha = normalizeBuildSha(document.documentElement.dataset.buildSha);
|
|
if (!clientBuildSha) {
|
|
return;
|
|
}
|
|
|
|
checkInFlight = true;
|
|
try {
|
|
const response = await window.fetch("/ready", {
|
|
cache: "no-store",
|
|
credentials: "same-origin",
|
|
headers: { accept: "application/json" },
|
|
});
|
|
if (!response.ok) {
|
|
return;
|
|
}
|
|
|
|
const payload = await response.json() as ReadyResponse;
|
|
const serverBuildSha = normalizeBuildSha(payload.data?.buildSha);
|
|
if (!serverBuildSha || serverBuildSha === clientBuildSha) {
|
|
return;
|
|
}
|
|
|
|
mismatchNotified = true;
|
|
window.dispatchEvent(new CustomEvent(PWA_UPDATE_READY_EVENT, {
|
|
detail: { applyUpdate: buildUserControlledRefresh() },
|
|
}));
|
|
} catch {
|
|
} finally {
|
|
checkInFlight = false;
|
|
}
|
|
};
|
|
|
|
document.addEventListener("visibilitychange", () => {
|
|
if (document.visibilityState === "visible") {
|
|
void checkBuildFreshness();
|
|
}
|
|
});
|
|
}
|
|
|
|
export function registerPwaIfEnabled() {
|
|
if (import.meta.env.DEV) {
|
|
return;
|
|
}
|
|
|
|
if (import.meta.env.VITE_PWA_ENABLED === "false") {
|
|
return;
|
|
}
|
|
|
|
let updateServiceWorker: ((reloadPage?: boolean) => Promise<void>) | null = null;
|
|
|
|
updateServiceWorker = registerSW({
|
|
immediate: true,
|
|
onNeedRefresh() {
|
|
const applyUpdate = () => {
|
|
void clearPwaRuntimeCaches()
|
|
.catch(() => undefined)
|
|
.finally(() => {
|
|
void updateServiceWorker?.(true);
|
|
});
|
|
};
|
|
|
|
if (canUseWindow() && isAuthInputPath(window.location.pathname)) {
|
|
window.dispatchEvent(new CustomEvent(PWA_UPDATE_READY_EVENT, { detail: { applyUpdate } }));
|
|
return;
|
|
}
|
|
|
|
applyUpdate();
|
|
},
|
|
onOfflineReady() {
|
|
if (canUseWindow()) {
|
|
window.dispatchEvent(new CustomEvent("hyb-oa:pwa-offline-ready"));
|
|
}
|
|
},
|
|
onRegisteredSW(_swScriptUrl, registration) {
|
|
bindServiceWorkerUpdateCleanup(registration);
|
|
},
|
|
});
|
|
}
|
|
|
|
export function buildMobileApprovalsPath(taskId?: string | null) {
|
|
const normalizedTaskId = String(taskId ?? "").trim();
|
|
if (!normalizedTaskId) {
|
|
return "/mobile/approvals";
|
|
}
|
|
|
|
return `/mobile/approvals/${encodeURIComponent(normalizedTaskId)}`;
|
|
}
|
|
|
|
export function extractWorkflowTaskId(pathname: string) {
|
|
const match = pathname.match(MOBILE_APPROVAL_TASK_PATH);
|
|
return match?.[1] ?? null;
|
|
}
|
|
|
|
export function isOfflineSnapshotAllowedPath(pathname: string) {
|
|
if (OFFLINE_ALLOWED_PATHS.includes(pathname)) {
|
|
return true;
|
|
}
|
|
|
|
if (/^\/mobile\/approvals\/[^/]+$/.test(pathname)) {
|
|
return true;
|
|
}
|
|
|
|
return Boolean(extractWorkflowTaskId(pathname));
|
|
}
|
|
|
|
export function shouldPreferMobileApprovalExperience() {
|
|
if (!canUseWindow()) {
|
|
return false;
|
|
}
|
|
|
|
const isStandalone = matchesMedia("(display-mode: standalone)") || (window.navigator as Navigator & { standalone?: boolean }).standalone === true;
|
|
const isTouchSmallScreen = window.navigator.maxTouchPoints > 0 && matchesMedia("(max-width: 1024px)");
|
|
|
|
return isStandalone || isEnterpriseMessengerContainer() || isTouchSmallScreen;
|
|
}
|
|
|
|
export async function clearPwaRuntimeCaches() {
|
|
if (!canUseWindow() || !("caches" in window)) {
|
|
return;
|
|
}
|
|
|
|
const cacheNames = await window.caches.keys();
|
|
const cachesToDelete = cacheNames.filter((cacheName) =>
|
|
CLEARABLE_CACHE_PREFIXES.some((prefix) => cacheName.startsWith(prefix)),
|
|
);
|
|
|
|
await Promise.all(cachesToDelete.map(async (cacheName) => {
|
|
try {
|
|
await window.caches.delete(cacheName);
|
|
} catch {
|
|
}
|
|
}));
|
|
}
|