Files
ai-code-app/src/sw.js

212 lines
7.3 KiB
JavaScript
Executable File

/// <reference lib="webworker" />
import { clientsClaim } from 'workbox-core';
import { precacheAndRoute, cleanupOutdatedCaches, createHandlerBoundToURL } from 'workbox-precaching';
import { NavigationRoute, registerRoute } from 'workbox-routing';
clientsClaim();
cleanupOutdatedCaches();
const manifest = self.__WB_MANIFEST;
if (Array.isArray(manifest) && manifest.length > 0) {
precacheAndRoute(manifest);
const navigationHandler = createHandlerBoundToURL('/index.html');
registerRoute(new NavigationRoute(navigationHandler));
} else {
registerRoute(
new NavigationRoute(({ request }) => {
return fetch(request);
}),
);
}
self.addEventListener('message', (event) => {
if (event.data?.type === 'SKIP_WAITING') {
void self.skipWaiting();
}
});
function normalizeNotificationValue(value) {
return typeof value === 'string' ? value.trim() : '';
}
function normalizeNotificationAliases(value) {
if (!Array.isArray(value)) {
return [];
}
return value.map((item) => normalizeNotificationValue(item)).filter(Boolean);
}
function shouldCloseExistingNotification(notification, payload) {
const data = payload.data ?? {};
const notificationScope = normalizeNotificationValue(data.notificationScope);
const notificationSource = normalizeNotificationValue(data.source);
const notificationKey = normalizeNotificationValue(data.notificationKey);
const notificationAliases = normalizeNotificationAliases(data.notificationAliases);
const replaceExistingScope =
data.replaceExistingScope === true || normalizeNotificationValue(data.replaceExistingScope).toLowerCase() === 'true';
if (!notificationScope || (!replaceExistingScope && notificationScope !== 'automation')) {
return false;
}
const existingData = notification.data && typeof notification.data === 'object' ? notification.data : {};
const existingScope = normalizeNotificationValue(existingData.notificationScope);
const existingSource = normalizeNotificationValue(existingData.source);
const existingNotificationKey = normalizeNotificationValue(existingData.notificationKey);
const existingTag = normalizeNotificationValue(notification.tag);
const replaceTargets = new Set([
notificationScope,
notificationSource,
notificationKey,
...notificationAliases,
].filter(Boolean));
return (
replaceTargets.has(existingScope) ||
replaceTargets.has(existingSource) ||
replaceTargets.has(existingNotificationKey) ||
replaceTargets.has(existingTag)
);
}
self.addEventListener('push', (event) => {
if (!event.data) {
return;
}
let payload = {};
try {
payload = event.data.json();
} catch {
payload = {
title: '알림',
body: event.data.text(),
data: {},
};
}
const title = payload.title || 'AI Code App';
const body = payload.body || '새 알림이 도착했습니다.';
const notificationKey =
payload.data?.notificationKey ||
[payload.threadId ?? 'ai-code-app-notification', payload.data?.eventType ?? 'event', Date.now()].join(':');
event.waitUntil(
self.registration.getNotifications().then((notifications) => {
notifications.filter((notification) => shouldCloseExistingNotification(notification, payload)).forEach((notification) => notification.close());
return self.registration.showNotification(title, {
body,
data: payload.data ?? {},
tag: notificationKey,
badge: '/pwa-192x192.svg',
icon: '/pwa-192x192.svg',
});
}),
);
});
self.addEventListener('notificationclick', (event) => {
const notificationData = event.notification.data ?? {};
const notificationSessionId = typeof notificationData.sessionId === 'string' ? notificationData.sessionId.trim() : '';
const notificationCategory = typeof notificationData.category === 'string' ? notificationData.category.trim() : '';
const notificationType = typeof notificationData.type === 'string' ? notificationData.type.trim() : '';
const notificationThreadId = typeof notificationData.threadId === 'string' ? notificationData.threadId.trim() : '';
const isChatNotification =
notificationCategory === 'chat' ||
notificationType.startsWith('chat') ||
notificationThreadId.startsWith('chat:');
event.notification.close();
let targetUrl;
try {
targetUrl = notificationData.targetUrl ? new URL(String(notificationData.targetUrl)) : new URL('/', self.location.origin);
} catch {
targetUrl = new URL('/', self.location.origin);
}
if (targetUrl.origin !== self.location.origin) {
const fallbackUrl = new URL('/', self.location.origin);
targetUrl.searchParams.forEach((value, key) => {
fallbackUrl.searchParams.set(key, value);
});
targetUrl = fallbackUrl;
}
if (!targetUrl.searchParams.has('topMenu')) {
targetUrl.searchParams.set('topMenu', notificationData.category === 'chat' ? 'chat' : 'plans');
}
if (isChatNotification) {
targetUrl.pathname = '/chat/live';
targetUrl.searchParams.set('topMenu', 'chat');
targetUrl.searchParams.delete('chatView');
targetUrl.searchParams.delete('runtimeRequestId');
if (notificationSessionId) {
targetUrl.searchParams.set('sessionId', notificationSessionId);
}
}
if (notificationData.planId && !targetUrl.searchParams.has('planId')) {
targetUrl.searchParams.set('planId', String(notificationData.planId));
}
if (notificationData.workId && !targetUrl.searchParams.has('workId')) {
targetUrl.searchParams.set('workId', String(notificationData.workId));
}
event.waitUntil(
Promise.all([
self.registration.getNotifications().then((notifications) => {
if (!notificationSessionId || !isChatNotification) {
return;
}
notifications.forEach((notification) => {
const candidateData = notification.data && typeof notification.data === 'object' ? notification.data : null;
const candidateSessionId =
candidateData && typeof candidateData.sessionId === 'string' ? candidateData.sessionId.trim() : '';
const candidateCategory =
candidateData && typeof candidateData.category === 'string' ? candidateData.category.trim() : '';
const candidateType =
candidateData && typeof candidateData.type === 'string' ? candidateData.type.trim() : '';
const candidateThreadId =
candidateData && typeof candidateData.threadId === 'string' ? candidateData.threadId.trim() : '';
const isCandidateChatNotification =
candidateCategory === 'chat' ||
candidateType.startsWith('chat') ||
candidateThreadId.startsWith('chat:');
if (isCandidateChatNotification && candidateSessionId === notificationSessionId) {
notification.close();
}
});
}),
self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clientList) => {
const existingClient = clientList[0];
if (existingClient) {
existingClient.focus();
try {
const clientUrl = new URL(existingClient.url);
if (clientUrl.origin === targetUrl.origin) {
return existingClient.navigate(targetUrl.toString());
}
} catch {
return existingClient.navigate(targetUrl.toString());
}
}
return self.clients.openWindow(targetUrl.toString());
}),
]),
);
});