apply_patch previously duplicated the install event listener, causing a ServiceWorker script evaluation error on load. Deduplicate and hoist precacheShell() before the install listener. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
283 lines
8.3 KiB
JavaScript
283 lines
8.3 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;
|
|
}
|
|
}
|
|
}
|
|
|
|
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());
|
|
});
|