Files
myprojplanet_vite/src-pwa/custom-service-worker.js
2025-12-30 11:36:37 +01:00

835 lines
24 KiB
JavaScript
Executable File

/* global importScripts */
/* global idbKeyval */
/* global workbox */
/* global cfgenv */
const VITE_APP_VERSION = '1.2.87';
// Costanti di configurazione
const DYNAMIC_CACHE = 'dynamic-cache-v2';
const baseUrl = self.location.origin;
const CACHE_VERSION = VITE_APP_VERSION;
const CACHE_PREFIX = self.location.hostname || 'app';
function extractDomain(url) {
return url.replace(/^https?:\/\//, '');
}
function removeTestPrefix(str) {
return str.startsWith('test.') ? str.slice(5) : str;
}
// Funzione per determinare il dominio API
function determineApiDomain(appDomain) {
if (ISTEST) {
return 'testapi.' + removeTestPrefix(appDomain);
}
return appDomain.includes('localhost') ? 'localhost:3000' : 'api.' + appDomain;
}
const APP_DOMAIN = extractDomain(baseUrl);
const API_DOMAIN = determineApiDomain(APP_DOMAIN);
console.log('API_DOMAIN', API_DOMAIN);
const CACHE_NAME = 'pwa-cache-' + VITE_APP_VERSION; // Nome della cache
importScripts('workbox/workbox-sw.js');
import { clientsClaim } from 'workbox-core';
import {
precacheAndRoute,
cleanupOutdatedCaches,
createHandlerBoundToURL,
} from 'workbox-precaching';
import { registerRoute, NavigationRoute } from 'workbox-routing';
import { setCacheNameDetails } from 'workbox-core';
import {
NetworkOnly,
NetworkFirst,
StaleWhileRevalidate,
CacheFirst,
} from 'workbox-strategies';
import { CacheableResponsePlugin } from 'workbox-cacheable-response';
import { ExpirationPlugin } from 'workbox-expiration';
const debug = false; //process.env.NODE_ENV !== 'production';
if (workbox) {
// Imposta configurazione prima di tutto
workbox.setConfig({ debug });
workbox.loadModule('workbox-strategies');
console.log('Workbox ESISTE ✅ ');
} else {
console.error('Workbox NON CARICATO ! ❌');
}
setCacheNameDetails({
prefix: self.location.hostname,
suffix: 'v2',
precache: 'precache',
runtime: 'runtime',
});
// ✅ SOLUZIONE: Sii più specifico
const precacheList = (self.__WB_MANIFEST || []).filter((entry) => {
const url = entry.url;
// Escludi file grandi, upload, e risorse dinamiche
return (
!url.includes('/upload/') &&
!url.includes('/assets/videos/') &&
!url.match(/\.(mp4|webm|zip|pdf)$/)
);
});
// Precache solo i file filtrati
precacheAndRoute(precacheList);
cleanupOutdatedCaches();
// Installazione del Service Worker
self.addEventListener('install', () => {
console.log('[Service Worker] Installing ...');
self.skipWaiting();
clientsClaim();
// Notifica il frontend che c'è un nuovo SW pronto
self.clients.matchAll().then((clients) => {
clients.forEach((client) => {
client.postMessage({ type: 'SW_UPDATED' });
});
});
});
// Attivazione del Service Worker
self.addEventListener('activate', (event) => {
console.log('[Service Worker] Activating...');
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames
.filter((name) => name !== CACHE_NAME && name !== DYNAMIC_CACHE)
.map((name) => caches.delete(name))
);
})
);
self.clients.claim();
});
const USASYNC = false;
// PER ATTIVARE IL SYNC TOGLI L'AREA COMMENTATA DI 'FETCH' QUI SOTTO ....
/*
// Strategia fetch
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
// ============================================
// IMPORTANTE: NON cachare API /sync o /loadsite
// ============================================
if (url.pathname.includes('/api/') ||
url.pathname.includes('/sync') ||
url.pathname.includes('/loadsite')) {
// Lascia passare normalmente - IndexedDB gestisce cache
return;
}
// ============================================
// Cache Strategy per assets statici
// ============================================
if (request.method === 'GET') {
event.respondWith(
caches.match(request).then((cachedResponse) => {
if (cachedResponse) {
return cachedResponse;
}
return fetch(request).then((response) => {
// Non cachare se non è successo
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// Clona e salva in cache
const responseToCache = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(request, responseToCache);
});
return response;
}).catch(() => {
// Offline fallback
if (request.destination === 'document') {
return caches.match('/offline.html');
}
});
})
);
}
});
*/
console.log(
' [ VER-' +
VITE_APP_VERSION +
' ] _---------________------ PAO: this is my custom service worker: '
);
try {
importScripts('/js/idb.js', '/js/storage.js');
console.log('Local scripts imported successfully.');
} catch (error) {
console.error('Failed to import local scripts ❌:', error);
}
let port = self.location.hostname.startsWith('test') ? 3001 : 3000;
let ISTEST = self.location.hostname.startsWith('test');
let ISLOCALE = self.location.hostname.startsWith('localhost');
console.log('SW- app ver ' + VITE_APP_VERSION);
// Function helpers
async function writeData(table, data) {
console.log('writeData', table, data);
await idbKeyval.setdata(table, data);
}
async function readAllData(table) {
return idbKeyval.getalldata(table);
}
async function clearAllData(table) {
await idbKeyval.clearalldata(table);
}
async function deleteItemFromData(table, id) {
await idbKeyval.deletedata(table, id);
}
if (workbox) {
/*if (process.env.MODE !== 'ssr' || process.env.PROD) {
registerRoute(
new NavigationRoute(
createHandlerBoundToURL(process.env.PWA_FALLBACK_HTML),
{ denylist: [new RegExp(process.env.PWA_SERVICE_WORKER_REGEX), /workbox\workbox-(.)*\.js$/] }
)
)
}*/
// Static assets (JS, CSS, Fonts) - CacheFirst: caricamento rapidissimo
registerRoute(
({ request }) => ['script', 'style', 'font'].includes(request.destination),
new CacheFirst({
cacheName: `${CACHE_PREFIX}-static-assets-${CACHE_VERSION}`,
plugins: [
new CacheableResponsePlugin({ statuses: [0, 200] }),
new ExpirationPlugin({ maxEntries: 50, maxAgeSeconds: 30 * 24 * 60 * 60 }), // 30 giorni
],
})
);
// Immagini - CacheFirst con scadenza e limite
registerRoute(
({ request }) => request.destination === 'image',
new CacheFirst({
cacheName: `${CACHE_PREFIX}-images-${CACHE_VERSION}`,
plugins: [
new CacheableResponsePlugin({ statuses: [0, 200] }),
new ExpirationPlugin({ maxEntries: 60, maxAgeSeconds: 30 * 24 * 60 * 60 }), // 30 giorni
],
})
);
// Google Fonts - StaleWhileRevalidate per aggiornamenti trasparenti
registerRoute(
/^https:\/\/fonts\.(?:googleapis|gstatic)\.com/,
new StaleWhileRevalidate({
cacheName: `${CACHE_PREFIX}-google-fonts-${CACHE_VERSION}`,
plugins: [
new CacheableResponsePlugin({ statuses: [0, 200] }),
new ExpirationPlugin({ maxEntries: 30 }),
],
})
);
// HTML documents - NetworkFirst: garantisce contenuti aggiornati e fallback cache
registerRoute(
({ request }) => request.destination === 'document',
new NetworkFirst({
cacheName: `${CACHE_PREFIX}-html-cache-${CACHE_VERSION}`,
networkTimeoutSeconds: 10, // timeout rapido
plugins: [
new CacheableResponsePlugin({ statuses: [0, 200] }),
new ExpirationPlugin({ maxEntries: 50, maxAgeSeconds: 24 * 60 * 60 }), // 1 giorno
],
})
);
// API calls - NetworkFirst con timeout breve
registerRoute(
({ url }) => url.hostname === API_DOMAIN,
new NetworkFirst({
cacheName: `${CACHE_PREFIX}-api-cache-${CACHE_VERSION}`,
networkTimeoutSeconds: 5,
plugins: [
new CacheableResponsePlugin({ statuses: [0, 200] }),
new ExpirationPlugin({ maxEntries: 100, maxAgeSeconds: 5 * 60 }), // 5 minuti
],
})
);
}
// Generate UUID per sync
function generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
const r = (Math.random() * 16) | 0,
v = c === 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
let syncStore = {};
// Message event handler
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
// Opzionale: rispondi al client
if (event.ports && event.ports[0]) {
event.ports[0].postMessage({ success: true });
}
}
if (event.data.type === 'sync') {
console.log('addEventListener - message');
const id = generateUUID();
syncStore[id] = event.data;
self.registration.sync.register(id);
}
console.log(event.data);
});
// Funzione per gestire richieste API
async function handleApiRequest(request) {
try {
const response = await fetch(request);
// Se la risposta non è valida, restituisci un errore personalizzato
if (!response.ok) {
console.warn('[SW] API Response Error:', response.status, response.statusText);
return new Response(
JSON.stringify({
error: 'API error',
message: `❌ Invalid response from API: ${response.status} ${response.statusText}`,
}),
{ status: response.status, headers: { 'Content-Type': 'application/json' } }
);
}
return response;
} catch (error) {
console.error('[Service Worker] API request error ❌:', error);
// Restituisci una risposta di errore personalizzata
return new Response(
JSON.stringify({
error: 'Network error',
message: '❌ Unable to fetch from API: ' + error.message,
}),
{ status: 503, headers: { 'Content-Type': 'application/json' } }
);
}
}
// Funzione per effettuare una richiesta di rete e memorizzare nella cache
async function fetchAndCache(request) {
const cache = await caches.open(DYNAMIC_CACHE);
try {
const response = await fetch(request);
// Clona e salva la risposta nella cache solo se valida
if (response.ok) {
const responseClone = response.clone();
cache.put(request, responseClone);
}
return response;
} catch (error) {
console.error('[SW] Fetch and cache error ❌:', error);
throw error;
}
}
// Strategia di caching: stale-while-revalidate
async function cacheWithStaleWhileRevalidate(request, event) {
const cache = await caches.open(DYNAMIC_CACHE);
// Prova a recuperare la risorsa dalla cache
const cachedResponse = await cache.match(request);
if (cachedResponse) {
// Aggiorna in background mentre restituisci la risposta in cache
event.waitUntil(
fetchAndCache(request).catch((error) => {
console.error('[SW] Background fetch and cache error ❌:', error);
})
);
return cachedResponse;
}
// Se non è in cache, fai la richiesta di rete
try {
return await fetchAndCache(request);
} catch (error) {
console.error('[SW] Cache miss and network error ❌:', error);
// Restituisci una risposta di fallback personalizzata
return new Response(
JSON.stringify({
error: 'Network error',
message: 'Unable to fetch resource from network or cache.',
}),
{ status: 503, headers: { 'Content-Type': 'application/json' } }
);
}
}
// Listener per gestire tutte le richieste
/*self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
try {
// Ignora richieste non gestibili
if (request.method !== 'GET' || url.protocol !== 'https:') {
return;
}
// Ignora richieste per file di sviluppo (es. /src/)
if (url.pathname.startsWith('/src/') || url.search.includes('vue&type')) {
return;
}
// Gestione richieste API
if (url.hostname === API_DOMAIN) {
if (debug) console.log("E' una RICHIESTA API!");
event.respondWith(handleApiRequest(request));
return;
}
// Gestione risorse statiche e altre richieste
if (debug) console.log("E' una RICHIESTA statica...");
event.respondWith(cacheWithStaleWhileRevalidate(request, event));
} catch (error) {
console.error('[Service Worker] Fetch error ❌:', error);
}
});
*/
// Gestione degli errori non catturati
self.addEventListener('unhandledrejection', (event) => {
console.error('[Service Worker] Unhandled rejection ❌:', event.reason);
});
// Gestione degli errori globali
self.addEventListener('error', (event) => {
console.error('[Service Worker] Global error ❌:', event.error);
});
// Background Sync event
self.addEventListener('sync', (event) => {
console.log('[Service Worker V5] Background syncing', event);
let mystrparam = event.tag;
let multiparams = mystrparam.split('|');
if (multiparams && multiparams.length > 3) {
let [cmd, table, method, token, refreshToken, browser_random] = multiparams;
if (cmd === 'sync-todos') {
console.log('[Service Worker] Syncing', cmd, table, method);
const headers = new Headers();
headers.append('content-Type', 'application/json');
headers.append('Accept', 'application/json');
headers.append('x-auth', token);
headers.append('x-refrtok', refreshToken);
headers.append('x-browser-random', browser_random);
event.waitUntil(
readAllData(table).then((alldata) => {
const myrecs = [...alldata];
let errorfromserver = false;
if (myrecs) {
let promiseChain = Promise.resolve();
for (let rec of myrecs) {
//TODO: Risistemare con calma... per ora non penso venga usato...
let link = cfgenv.serverweb + '/todos';
if (method !== 'POST') link += '/' + rec._id;
promiseChain = promiseChain.then(() =>
fetch(link, {
method: method,
headers: headers,
cache: 'no-cache',
mode: 'cors',
body: JSON.stringify(rec),
})
.then(() => {
deleteItemFromData(table, rec._id);
deleteItemFromData('swmsg', mystrparam);
})
.catch((err) => {
if (err.message === 'Failed to fetch') {
errorfromserver = true;
}
})
);
}
return promiseChain.then(() => {
const mystate = !errorfromserver ? 'online' : 'offline';
writeData('config', { _id: 2, stateconn: mystate });
});
}
})
);
}
}
// Sync per notifiche (se necessario)
if (event.tag === 'sync-notifications') {
event.waitUntil(
Promise.resolve().then(() => {
console.log('[SW] Syncing notifications');
})
);
}
});
// ========================================
// 🔔 PUSH NOTIFICATIONS - BACKWARD COMPATIBLE VERSION
// ========================================
// Push event - BACKWARD COMPATIBLE con formato vecchio E nuovo
self.addEventListener('push', (event) => {
console.log('[Service Worker] 🔔 Push notification received:', event);
// Default data - supporta ENTRAMBI i formati (vecchio e nuovo)
let data = {
title: 'New!',
body: 'Something new happened!',
content: 'Something new happened!', // VECCHIO formato
icon: '/images/android-chrome-192x192.png', // VECCHIO path
badge: '/images/badge-96x96.png', // VECCHIO path
tag: 'default',
url: '/',
requireInteraction: false,
notificationType: null,
rideId: null,
fromUser: null,
actions: []
};
// Parse del payload se presente
if (event.data) {
try {
const payload = event.data.json();
data = { ...data, ...payload };
// BACKWARD COMPATIBILITY: Se c'è 'content' ma non 'body', usa 'content'
if (payload.content && !payload.body) {
data.body = payload.content;
}
// Se c'è 'body' ma non 'content', sincronizza
if (payload.body && !payload.content) {
data.content = payload.body;
}
console.log('[SW] 📦 Parsed notification payload:', data);
} catch (e) {
console.warn('[SW] ⚠️ Failed to parse push payload as JSON, using text');
const textData = event.data.text();
data.body = textData;
data.content = textData;
}
}
// Configura le actions SOLO se c'è un notificationType (nuovo formato)
if (data.notificationType && (!data.actions || data.actions.length === 0)) {
data.actions = getNotificationActions(data.notificationType, data.rideId);
}
// Opzioni della notifica - supporta ENTRAMBI i path delle icone
const options = {
body: data.body || data.content, // Fallback a 'content' se 'body' non c'è
icon: data.icon,
badge: data.badge,
tag: data.tag,
requireInteraction: data.requireInteraction || false,
vibrate: data.vibrate || [200, 100, 200],
data: {
dateOfArrival: Date.now(),
url: data.url || '/',
rideId: data.rideId,
notificationType: data.notificationType,
fromUser: data.fromUser,
// BACKWARD COMPATIBILITY: mantieni anche il formato vecchio
legacyFormat: !data.notificationType // true se è formato vecchio
},
actions: data.actions || []
};
// Mostra la notifica
event.waitUntil(
self.registration.showNotification(data.title, options).then(() => {
console.log('[SW] ✅ Notification displayed successfully');
// Salva la notifica in IndexedDB - BACKWARD COMPATIBLE
const myid = data.id || generateUUID();
self.registration.sync.register(myid);
// Formato compatibile: se è vecchio formato usa solo _id e tag
const notificationData = options.data.legacyFormat
? { _id: myid, tag: options.tag }
: {
_id: myid,
tag: options.tag,
type: data.notificationType,
timestamp: Date.now()
};
writeData('notifications', notificationData).catch(err => {
console.error('[SW] Failed to save notification to IndexedDB:', err);
});
})
);
});
// Funzione helper per generare actions (SOLO per nuovo formato)
function getNotificationActions(notificationType, rideId) {
const actions = [];
switch (notificationType) {
case 'newRideRequest':
actions.push(
{ action: 'view-requests', title: '👀 Visualizza richieste' },
{ action: 'dismiss', title: '❌ Ignora' }
);
break;
case 'requestAccepted':
actions.push(
{ action: 'view-ride', title: '🚗 Vedi viaggio' },
{ action: 'message', title: '💬 Messaggio' }
);
break;
case 'requestRejected':
actions.push(
{ action: 'search-rides', title: '🔍 Cerca altri viaggi' }
);
break;
case 'newMessage':
actions.push(
{ action: 'open-chat', title: '💬 Apri chat' },
{ action: 'dismiss', title: '✓ OK' }
);
break;
case 'rideReminder':
actions.push(
{ action: 'view-ride', title: '📍 Vedi dettagli' },
{ action: 'dismiss', title: '✓ OK' }
);
break;
case 'rideCancelled':
actions.push(
{ action: 'search-rides', title: '🔍 Cerca alternativa' }
);
break;
case 'feedbackRequest':
actions.push(
{ action: 'leave-feedback', title: '⭐ Lascia feedback' },
{ action: 'dismiss', title: 'Dopo' }
);
break;
case 'newFeedbackReceived':
actions.push(
{ action: 'view-feedback', title: '⭐ Vedi feedback' }
);
break;
default:
actions.push(
{ action: 'open-app', title: '📱 Apri app' }
);
}
return actions;
}
// Notification click event - BACKWARD COMPATIBLE
self.addEventListener('notificationclick', (event) => {
console.log('[Service Worker] 🖱️ Notification clicked:', event);
console.log('[SW] Action:', event.action);
console.log('[SW] Notification data:', event.notification.data);
// Chiudi la notifica
event.notification.close();
// Estrai i dati dalla notifica
const notificationData = event.notification.data || {};
const isLegacyFormat = notificationData.legacyFormat || false;
let finalUrl;
// BACKWARD COMPATIBILITY: Se è formato vecchio, usa solo notification.data.url
if (isLegacyFormat || !notificationData.notificationType) {
console.log('[SW] 📜 Using legacy notification format');
finalUrl = notificationData.url || '/';
} else {
// NUOVO formato: usa il routing intelligente
console.log('[SW] 🆕 Using new notification format with smart routing');
const rideId = notificationData.rideId;
const notificationType = notificationData.notificationType;
const action = event.action;
finalUrl = determineUrlFromNotification(action, notificationType, rideId);
}
console.log('[SW] 🎯 Navigating to:', finalUrl);
// Se finalUrl è null (action 'dismiss'), non fare nulla
if (!finalUrl) {
console.log('[SW] Action dismissed, no navigation');
return;
}
// Apri l'URL appropriato
event.waitUntil(
clients.matchAll({ type: 'window', includeUncontrolled: true })
.then((clientList) => {
console.log('[SW] Found', clientList.length, 'open windows');
// Cerca una finestra già aperta con l'URL desiderato
for (const client of clientList) {
if (client.url.includes(finalUrl.split('?')[0]) && 'focus' in client) {
console.log('[SW] ✅ Focusing existing window');
return client.focus();
}
}
// Cerca una finestra dell'app aperta e naviga lì
for (const client of clientList) {
if (client.url.includes(self.location.origin) && 'navigate' in client) {
console.log('[SW] ✅ Navigating existing window to:', finalUrl);
return client.navigate(finalUrl).then(client => client.focus());
}
}
// Altrimenti apri una nuova finestra
if (clients.openWindow) {
console.log('[SW] 🆕 Opening new window');
return clients.openWindow(finalUrl);
}
})
.catch((error) => {
console.error('[SW] ❌ Error handling notification click:', error);
})
);
});
// Funzione per determinare l'URL (SOLO per nuovo formato)
function determineUrlFromNotification(action, notificationType, rideId) {
// Gestisci prima le actions specifiche
if (action) {
switch (action) {
case 'view-requests':
return '/trasporti/miei-viaggi?tab=requests';
case 'view-ride':
return rideId ? `/trasporti/viaggio/${rideId}` : '/trasporti/miei-viaggi';
case 'message':
case 'open-chat':
return rideId ? `/trasporti/chat?ride=${rideId}` : '/trasporti/chat';
case 'search-rides':
return '/trasporti?view=search';
case 'leave-feedback':
return rideId ? `/trasporti/feedback/${rideId}` : '/trasporti/feedback';
case 'view-feedback':
return '/trasporti/miei-feedback';
case 'open-app':
return '/trasporti';
case 'dismiss':
return null; // Non fare nulla
default:
break;
}
}
// Se non c'è action, usa il tipo di notifica
if (notificationType) {
switch (notificationType) {
case 'newRideRequest':
return '/trasporti/miei-viaggi?tab=requests';
case 'requestAccepted':
case 'requestRejected':
case 'rideReminder':
return rideId ? `/trasporti/viaggio/${rideId}` : '/trasporti/miei-viaggi';
case 'newMessage':
return '/trasporti/chat';
case 'rideCancelled':
return '/trasporti/miei-viaggi?tab=cancelled';
case 'feedbackRequest':
return rideId ? `/trasporti/feedback/${rideId}` : '/trasporti/feedback';
case 'newFeedbackReceived':
return '/trasporti/miei-feedback';
default:
return '/trasporti';
}
}
// Fallback
return '/trasporti';
}
// Notification close event - tracking opzionale
self.addEventListener('notificationclose', (event) => {
console.log('[Service Worker] 🔕 Notification closed:', event);
const notificationData = event.notification.data || {};
// Solo per nuovo formato, traccia le dismissioni
if (!notificationData.legacyFormat && notificationData.notificationType) {
writeData('notification-dismissals', {
_id: generateUUID(),
type: notificationData.notificationType,
timestamp: Date.now(),
tag: event.notification.tag
}).catch(err => {
console.error('[SW] Failed to track notification dismissal:', err);
});
}
});
console.log('***** 🚀 CUSTOM-SERVICE-WORKER.JS - BACKWARD COMPATIBLE VERSION ***** ');