Files
myprojplanet_vite/src-pwa/custom-service-worker.js
2025-03-23 22:53:53 +01:00

507 lines
15 KiB
JavaScript
Executable File

/* global importScripts */
/* global idbKeyval */
/* global workbox */
/* global cfgenv */
const VITE_APP_VERSION = "1.2.29";
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',
});
const precacheList = (self.__WB_MANIFEST || []).filter(entry => {
// Esclude tutto ciò che si trova nella cartella 'upload'
if (entry.url.includes('/upload/')) {
return false;
}
return true;
});
// Precache solo i file filtrati
precacheAndRoute(precacheList);
cleanupOutdatedCaches()
// Installazione del Service Worker
self.addEventListener('install', () => {
console.log('[Service Worker] Installing ...');
self.skipWaiting()
clientsClaim();
});
// 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))
);
})
);
});
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$/] }
)
)
}*/
// Gestione richieste statiche
registerRoute(
({ request }) => request.destination === 'document' || request.destination === 'script' || request.destination === 'style',
new StaleWhileRevalidate({ cacheName: CACHE_NAME })
);
// Cache strategy registrations
registerRoute(
new RegExp(/\.(?:png|gif|jpg|jpeg)$/),
new CacheFirst({
cacheName: `images-upload-${VITE_APP_VERSION}`,
plugins: [
new CacheableResponsePlugin({ statuses: [200] }),
new ExpirationPlugin({ maxEntries: 60, maxAgeSeconds: 30 * 24 * 60 * 60 }), // 30 Days
],
})
);
registerRoute(
new RegExp(/\.(?:svg)$/),
new CacheFirst({
cacheName: `svg-${VITE_APP_VERSION}`,
plugins: [
new CacheableResponsePlugin({ statuses: [200] }),
new ExpirationPlugin({ maxEntries: 60, maxAgeSeconds: 30 * 24 * 60 * 60 }), // 30 Days
],
})
);
registerRoute(
new RegExp(/.*(?:googleapis|gstatic)\.com.*$/),
new StaleWhileRevalidate({
cacheName: `google-fonts-${VITE_APP_VERSION}`,
plugins: [
new CacheableResponsePlugin({ statuses: [200] }),
new ExpirationPlugin({ maxEntries: 30 }),
],
})
);
/*registerRoute(
new RegExp('^/myicons/.*$'), // Corrisponde a percorsi come "/icons/miaicona.ico"
new CacheFirst({
cacheName: `icon-cache-${VITE_APP_VERSION}`,
plugins: [
new CacheableResponsePlugin({ statuses: [200] }),
new ExpirationPlugin({ maxAgeSeconds: 30 * 24 * 60 * 60 }), // 30 Days
],
})
);*/
registerRoute(
new RegExp(/\.(?:js|css|font)$/),
new StaleWhileRevalidate({
cacheName: `js-css-fonts-${VITE_APP_VERSION}`,
plugins: [
new CacheableResponsePlugin({ statuses: [200] }),
],
})
);
// Gestione API
registerRoute(
({ url }) => url.hostname === API_DOMAIN,
new NetworkFirst({ fetchOptions: { credentials: 'include' } })
);
/*registerRoute(
(routeData) => routeData.event.request.headers.get('accept').includes('text/html'),
async (args) => {
let response = await caches.match(args.event.request);
if (response) return response;
try {
response = await fetch(args.event.request);
const cache = await caches.open('dynamic');
cache.put(args.event.request.url, response.clone());
return response;
} catch (err) {
return caches.match('/offline');
}
}
);*/
registerRoute(new RegExp('/admin/'), new NetworkOnly());
function generateUUID() {
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
);
}
const syncStore = {};
self.addEventListener('message', event => {
if (event.data && (event.data.type === 'SKIP_WAITING' || event.data.action === 'skipWaiting')) {
self.skipWaiting();
}
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);
});
function removeTestPrefix(str) {
return str.startsWith('test.') ? str.slice(5) : str;
}
function extractDomain(url) {
return url.replace(/^https?:\/\//, '');
}
// Funzione per verificare se una richiesta è cross-origin
function isCrossOrigin(url) {
try {
const requestUrl = new URL(url);
const baseUrl = self.location.origin;
return requestUrl.origin !== baseUrl;
} catch (e) {
console.error('Error parsing URL: ❌', e);
return true;
}
}
// Costanti di configurazione
const DYNAMIC_CACHE = 'dynamic-cache-v2';
const ENABLE_DYNAMIC_CACHING = true;
const baseUrl = self.location.origin;
const APP_DOMAIN = extractDomain(baseUrl);
const API_DOMAIN = determineApiDomain(APP_DOMAIN);
console.log('API_DOMAIN', API_DOMAIN);
// Funzione per determinare il dominio API
function determineApiDomain(appDomain) {
if (ISTEST) {
return 'testapi.' + removeTestPrefix(appDomain);
}
return appDomain.includes('localhost') ? 'localhost:3000' : 'api.' + appDomain;
}
// 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);
});
// Funzione di utilità per il logging (decommentare se necessario)
// function logFetchDetails(request) {
// console.log('[Service Worker] Fetching:', request.url);
// console.log('Cache mode:', request.cache);
// console.log('Request mode:', request.mode);
// }
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] = 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);
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 });
});
}
})
);
}
}
});
// Notifications
self.addEventListener('notificationclick', (event) => {
const { notification } = event;
const { action } = event;
if (action === 'confirm') {
notification.close();
} else {
event.waitUntil(
self.clients.matchAll().then((clis) => {
const client = clis.find((c) => c.visibilityState === 'visible');
if (client) {
client.navigate(notification.data.url);
client.focus();
} else {
self.clients.openWindow(notification.data.url);
}
notification.close();
}),
);
}
});
self.addEventListener('notificationclose', (event) => {
console.log('Notification was closed', event);
});
self.addEventListener('push', (event) => {
console.log('Push Notification received', event);
let data = event.data ? event.data.json() : { title: 'New!', content: 'Something new happened!', url: '/' };
const options = {
body: data.content,
icon: data.icon ? data.icon : '/images/android-chrome-192x192.png',
badge: data.badge ? data.badge : '/images/badge-96x96.png',
data: { url: data.url },
tag: data.tag,
};
event.waitUntil(self.registration.showNotification(data.title, options));
const myid = data.id || '0';
self.registration.sync.register(myid);
writeData('notifications', { _id: myid, tag: options.tag });
});
} else {
console.warn('Workbox could not be loaded.');
}
console.log('***** FINE CUSTOM-SERVICE-WORKER.JS ***** ');