patch oko teksta PDFa i UI tablica
This commit is contained in:
@@ -1,7 +1,147 @@
|
||||
const CACHE_NAME = 'erp-shell-v1';
|
||||
const CACHE_NAME = 'erp-shell-v2';
|
||||
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 = ['/', '/index.html', '/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) => {
|
||||
event.waitUntil(
|
||||
caches.open(CACHE_NAME).then((cache) => cache.addAll(PRECACHE_URLS))
|
||||
@@ -11,22 +151,76 @@ self.addEventListener('install', (event) => {
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(
|
||||
caches.keys().then((cacheNames) =>
|
||||
Promise.all(
|
||||
cacheNames
|
||||
.filter((name) => name !== CACHE_NAME)
|
||||
.map((name) => caches.delete(name))
|
||||
)
|
||||
)
|
||||
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;
|
||||
|
||||
const url = new URL(request.url);
|
||||
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') {
|
||||
@@ -60,3 +254,13 @@ self.addEventListener('fetch', (event) => {
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
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());
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user