Files
004.erp.generic/frontend/public/service-worker.js
mariomitte e3b3ff90a9
Some checks failed
ERP CI/CD Pipeline / test (push) Has been cancelled
ERP CI/CD Pipeline / Deploy (server git pull + compose) (push) Has been cancelled
manifest manifest.webmanifest
2026-08-10 07:45:08 +02:00

285 lines
8.4 KiB
JavaScript

const CACHE_NAME = 'erp-shell-v3';
const API_CACHE_NAME = 'erp-api-v1';
const WRITE_QUEUE_DB_NAME = 'erp-write-queue-db';
const WRITE_QUEUE_STORE = 'requests';
const WRITE_QUEUE_SYNC_TAG = 'erp-write-queue-sync';
const OFFLINE_URL = '/offline.html';
const PRECACHE_URLS = ['/', '/manifest.webmanifest', '/pwa-icon.svg', OFFLINE_URL];
const API_CACHE_BLOCKLIST = [
'/api/token/',
'/api/token/refresh/',
'/api/fleet/pusher-auth/',
];
function isCacheableApiGet(request, url) {
if (request.method !== 'GET') return false;
if (url.origin !== self.location.origin) return false;
if (!url.pathname.startsWith('/api/')) return false;
return !API_CACHE_BLOCKLIST.some((blocked) => url.pathname.startsWith(blocked));
}
function isQueueableApiWrite(request, url) {
if (!['POST', 'PUT', 'PATCH', 'DELETE'].includes(request.method)) return false;
if (url.origin !== self.location.origin) return false;
if (!url.pathname.startsWith('/api/')) return false;
if (API_CACHE_BLOCKLIST.some((blocked) => url.pathname.startsWith(blocked))) return false;
const contentType = (request.headers.get('content-type') || '').toLowerCase();
if (contentType.includes('multipart/form-data')) return false;
return true;
}
function openWriteQueueDb() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(WRITE_QUEUE_DB_NAME, 1);
request.onupgradeneeded = () => {
const db = request.result;
if (!db.objectStoreNames.contains(WRITE_QUEUE_STORE)) {
db.createObjectStore(WRITE_QUEUE_STORE, { keyPath: 'id', autoIncrement: true });
}
};
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
function serializeHeaders(headers) {
const serialized = {};
headers.forEach((value, key) => {
serialized[key] = value;
});
return serialized;
}
function getAllQueuedRequests(db) {
return new Promise((resolve, reject) => {
const tx = db.transaction(WRITE_QUEUE_STORE, 'readonly');
const store = tx.objectStore(WRITE_QUEUE_STORE);
const request = store.getAll();
request.onsuccess = () => resolve(request.result || []);
request.onerror = () => reject(request.error);
});
}
function addQueuedRequest(db, queuedRequest) {
return new Promise((resolve, reject) => {
const tx = db.transaction(WRITE_QUEUE_STORE, 'readwrite');
const store = tx.objectStore(WRITE_QUEUE_STORE);
const request = store.add(queuedRequest);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
function deleteQueuedRequest(db, id) {
return new Promise((resolve, reject) => {
const tx = db.transaction(WRITE_QUEUE_STORE, 'readwrite');
const store = tx.objectStore(WRITE_QUEUE_STORE);
const request = store.delete(id);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
async function queueWriteRequest(request) {
const body = request.method === 'DELETE' ? null : await request.clone().text();
const db = await openWriteQueueDb();
const payload = {
url: request.url,
method: request.method,
headers: serializeHeaders(request.headers),
body,
createdAt: Date.now(),
};
await addQueuedRequest(db, payload);
}
function createQueuedResponse() {
return new Response(
JSON.stringify({
queued: true,
offline: true,
detail: 'Zahtjev je spremljen i bit će sinkroniziran kad veza bude dostupna.',
}),
{
status: 202,
headers: { 'Content-Type': 'application/json' },
}
);
}
async function scheduleWriteQueueSync() {
if (!self.registration || !self.registration.sync) return;
await self.registration.sync.register(WRITE_QUEUE_SYNC_TAG);
}
async function replayQueuedRequests() {
const db = await openWriteQueueDb();
const queuedRequests = await getAllQueuedRequests(db);
for (const queued of queuedRequests) {
const headers = new Headers(queued.headers || {});
const init = {
method: queued.method,
headers,
credentials: 'same-origin',
};
if (queued.body != null && queued.method !== 'DELETE') {
init.body = queued.body;
}
try {
const response = await fetch(queued.url, init);
if (response.ok) {
await deleteQueuedRequest(db, queued.id);
continue;
}
if (response.status >= 500) {
throw new Error(`Replay failed with status ${response.status}`);
}
await deleteQueuedRequest(db, queued.id);
} catch (error) {
throw error;
}
}
}
self.addEventListener('install', (event) => {
self.addEventListener('install', (event) => {
async function precacheShell() {
const cache = await caches.open(CACHE_NAME);
await Promise.all(
PRECACHE_URLS.map(async (url) => {
try {
const response = await fetch(url, { cache: 'reload' });
if (!response || !response.ok) {
console.warn('Skipping precache for non-OK response:', url, response && response.status);
return;
}
await cache.put(url, response);
} catch (error) {
console.warn('Skipping precache for failed request:', url, error);
}
})
);
}
self.addEventListener('install', (event) => {
event.waitUntil(precacheShell());
self.skipWaiting();
});
self.addEventListener('activate', (event) => {
event.waitUntil(
Promise.all([
caches.keys().then((cacheNames) =>
Promise.all(
cacheNames
.filter((name) => name !== CACHE_NAME && name !== API_CACHE_NAME)
.map((name) => caches.delete(name))
)
),
replayQueuedRequests().catch((error) => {
console.error('Initial write-queue replay failed.', error);
}),
])
);
self.clients.claim();
});
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
if (isQueueableApiWrite(request, url)) {
event.respondWith(
fetch(request.clone()).catch(async () => {
try {
await queueWriteRequest(request);
} catch (error) {
console.error('Failed to queue offline write request.', error);
return new Response(
JSON.stringify({
queued: false,
offline: true,
detail: 'Zahtjev nije moguće spremiti offline.',
}),
{
status: 503,
headers: { 'Content-Type': 'application/json' },
}
);
}
try {
await scheduleWriteQueueSync();
} catch (error) {
console.warn('Background sync registration failed.', error);
}
return createQueuedResponse();
})
);
return;
}
if (request.method !== 'GET') return;
if (isCacheableApiGet(request, url)) {
event.respondWith(
caches.open(API_CACHE_NAME).then(async (cache) => {
const cached = await cache.match(request);
const networkFetch = fetch(request)
.then((response) => {
if (response && response.ok) {
cache.put(request, response.clone());
}
return response;
})
.catch(() => cached);
return cached || networkFetch;
})
);
return;
}
if (url.origin !== self.location.origin) return;
if (request.mode === 'navigate') {
event.respondWith(
fetch(request)
.then((response) => {
const copy = response.clone();
caches.open(CACHE_NAME).then((cache) => cache.put(request, copy));
return response;
})
.catch(async () => {
const cached = await caches.match(request);
return cached || caches.match(OFFLINE_URL);
})
);
return;
}
if (['style', 'script', 'worker', 'font', 'image'].includes(request.destination)) {
event.respondWith(
caches.match(request).then((cached) => {
const networkFetch = fetch(request)
.then((response) => {
const copy = response.clone();
caches.open(CACHE_NAME).then((cache) => cache.put(request, copy));
return response;
})
.catch(() => cached);
return cached || networkFetch;
})
);
}
});
self.addEventListener('sync', (event) => {
if (event.tag !== WRITE_QUEUE_SYNC_TAG) return;
event.waitUntil(replayQueuedRequests());
});
self.addEventListener('message', (event) => {
if (event.data !== 'FLUSH_WRITE_QUEUE') return;
event.waitUntil(replayQueuedRequests());
});