Files
talorr cda36918e8 init
2026-03-27 03:36:08 +03:00

208 lines
5.8 KiB
JavaScript

const PUSH_CONTEXT_CACHE = "push-context-v1";
const PUSH_CONTEXT_URL = "/__push-context__";
self.addEventListener("install", () => {
self.skipWaiting();
});
self.addEventListener("message", (event) => {
if (event.data?.type !== "push-context-sync") {
return;
}
event.waitUntil(savePushContext(event.data.payload));
});
self.addEventListener("activate", (event) => {
event.waitUntil(self.clients.claim());
});
self.addEventListener("push", (event) => {
const data = event.data ? event.data.json() : {};
const title = data.title || "Новый сигнал";
const body = data.body || "Появился новый матч в списке сигналов";
const url = data?.data?.url || data.url || "/";
const tag = typeof data.tag === "string" && data.tag.trim().length > 0 ? data.tag : undefined;
const shouldRenotify = Boolean(tag && data.renotify);
const notificationOptions = {
body,
requireInteraction: false,
icon: "/icons/app-icon-192.png",
badge: "/icons/app-badge.png",
data: {
url,
...(data?.data || {})
},
vibrate: [200, 100, 200],
timestamp: Date.now(),
...(tag ? { tag } : {}),
...(tag ? { renotify: shouldRenotify } : {})
};
event.waitUntil(
self.registration.showNotification(title, notificationOptions)
);
});
self.addEventListener("notificationclick", (event) => {
event.notification.close();
const url = event.notification.data?.url || "/";
event.waitUntil(
clients.matchAll({ type: "window", includeUncontrolled: true }).then((windowClients) => {
const sameOriginClients = windowClients.filter((client) => {
try {
return new URL(client.url).origin === self.location.origin;
} catch {
return false;
}
});
const preferredClient =
sameOriginClients.find((client) => client.visibilityState === "visible" || client.focused) ||
sameOriginClients[0];
if (preferredClient) {
preferredClient.postMessage({
type: "push-notification-click",
url
});
return preferredClient.focus();
}
for (const client of windowClients) {
if ("focus" in client) {
client.navigate(url);
return client.focus();
}
}
return clients.openWindow(url);
})
);
});
self.addEventListener("pushsubscriptionchange", (event) => {
event.waitUntil(
resubscribeAfterChange(event)
);
});
async function savePushContext(nextContext) {
const cache = await caches.open(PUSH_CONTEXT_CACHE);
const currentContext = await readPushContext();
const payload = {
...(currentContext || {}),
...(nextContext || {})
};
await cache.put(
PUSH_CONTEXT_URL,
new Response(JSON.stringify(payload), {
headers: {
"Content-Type": "application/json"
}
})
);
}
async function readPushContext() {
const cache = await caches.open(PUSH_CONTEXT_CACHE);
const response = await cache.match(PUSH_CONTEXT_URL);
if (!response) {
return null;
}
try {
return await response.json();
} catch {
return null;
}
}
function urlBase64ToUint8Array(base64String) {
const normalizedInput = typeof base64String === "string" ? base64String.trim() : "";
if (!normalizedInput) {
throw new Error("Missing VAPID public key");
}
const padding = "=".repeat((4 - (normalizedInput.length % 4)) % 4);
const base64 = (normalizedInput + padding).replace(/-/g, "+").replace(/_/g, "/");
const rawData = atob(base64);
return Uint8Array.from([...rawData].map((char) => char.charCodeAt(0)));
}
async function fetchApplicationServerKey(apiBase) {
if (!apiBase) {
throw new Error("Push API base is not configured in service worker context");
}
const response = await fetch(`${apiBase}/vapid-public-key`, {
method: "GET",
credentials: "include"
});
if (!response.ok) {
throw new Error(`Failed to load VAPID public key: ${response.status}`);
}
const payload = await response.json();
return urlBase64ToUint8Array(payload.publicKey);
}
async function syncSubscriptionWithServer(context, subscription, previousEndpoint) {
if (!context?.apiBase) {
throw new Error("Push API base is not configured in service worker context");
}
const serializedSubscription = subscription.toJSON();
if (!serializedSubscription.endpoint || !serializedSubscription.keys?.p256dh || !serializedSubscription.keys?.auth) {
throw new Error("Browser returned an incomplete push subscription");
}
const isAuthenticated = Boolean(context.isAuthenticated);
const endpoint = isAuthenticated ? "/me/push-subscriptions" : "/public/push-subscriptions";
if (!isAuthenticated && !context.anonymousClientId) {
throw new Error("Anonymous push client id is missing in service worker context");
}
const headers = new Headers({
"Content-Type": "application/json"
});
const response = await fetch(`${context.apiBase}${endpoint}`, {
method: "POST",
credentials: "include",
headers,
body: JSON.stringify({
...(isAuthenticated ? {} : { clientId: context.anonymousClientId }),
...serializedSubscription,
...(previousEndpoint && previousEndpoint !== serializedSubscription.endpoint ? { previousEndpoint } : {})
})
});
if (!response.ok) {
throw new Error(`Failed to sync rotated subscription: ${response.status}`);
}
}
async function resubscribeAfterChange(event) {
const context = await readPushContext();
if (!context) {
return;
}
const previousEndpoint = event.oldSubscription?.endpoint;
const subscription =
event.newSubscription ||
(await self.registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: await fetchApplicationServerKey(context.apiBase)
}));
await syncSubscriptionWithServer(context, subscription, previousEndpoint);
}