- Bump service worker cache name to v18 to clear stale JS/CSS caches - Bump dashboard.js to ?v=4 so browsers fetch updated script
355 lines
12 KiB
JavaScript
355 lines
12 KiB
JavaScript
// /home/Autopartes/pos/static/pwa/sw.js
|
|
// Nexus POS — Service Worker v17
|
|
// Self-contained vanilla JS. No external imports.
|
|
//
|
|
// Bump CACHE_NAME whenever static assets change significantly.
|
|
// The fetch handler normalizes static asset URLs (strips ?v= query strings)
|
|
// so templates can use cache-busting query params freely.
|
|
|
|
const CACHE_NAME = 'nexus-pos-v18';
|
|
|
|
const APP_SHELL = [
|
|
'/pos/static/css/tokens.css',
|
|
'/pos/static/css/common.css',
|
|
'/pos/static/css/pos-ui.css',
|
|
'/pos/static/js/app-init.js',
|
|
'/pos/static/js/sidebar.js',
|
|
'/pos/static/js/offline-banner.js',
|
|
'/pos/static/js/sync-engine.js',
|
|
'/pos/static/js/i18n.js',
|
|
'/pos/static/js/kiosk.js',
|
|
'/pos/static/js/splash-loader.js',
|
|
'/pos/static/js/pos-utils.js',
|
|
'/pos/static/js/pwa-install.js',
|
|
'/pos/static/js/chat.js',
|
|
'/pos/static/pwa/manifest.json',
|
|
'/pos/static/pwa/icon-192.png',
|
|
'/pos/static/pwa/icon-512.png'
|
|
];
|
|
|
|
// ─── IndexedDB helpers (offline queue) ───────────────────────────
|
|
const DB_NAME = 'nexus-offline';
|
|
const DB_VERSION = 1;
|
|
const STORE_NAME = 'pendingRequests';
|
|
|
|
function openDB() {
|
|
return new Promise(function (resolve, reject) {
|
|
var request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
request.onerror = function () { reject(request.error); };
|
|
request.onsuccess = function () { resolve(request.result); };
|
|
request.onupgradeneeded = function (event) {
|
|
var db = event.target.result;
|
|
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
db.createObjectStore(STORE_NAME, { keyPath: 'id', autoIncrement: true });
|
|
}
|
|
};
|
|
});
|
|
}
|
|
|
|
function savePendingRequest(entry) {
|
|
return openDB().then(function (db) {
|
|
return new Promise(function (resolve, reject) {
|
|
var tx = db.transaction(STORE_NAME, 'readwrite');
|
|
var store = tx.objectStore(STORE_NAME);
|
|
var request = store.add(entry);
|
|
request.onsuccess = function () { resolve(request.result); };
|
|
request.onerror = function () { reject(request.error); };
|
|
});
|
|
});
|
|
}
|
|
|
|
function getPendingRequests() {
|
|
return openDB().then(function (db) {
|
|
return new Promise(function (resolve, reject) {
|
|
var tx = db.transaction(STORE_NAME, 'readonly');
|
|
var store = tx.objectStore(STORE_NAME);
|
|
var request = store.getAll();
|
|
request.onsuccess = function () { resolve(request.result || []); };
|
|
request.onerror = function () { reject(request.error); };
|
|
});
|
|
});
|
|
}
|
|
|
|
function clearPendingRequests() {
|
|
return openDB().then(function (db) {
|
|
return new Promise(function (resolve, reject) {
|
|
var tx = db.transaction(STORE_NAME, 'readwrite');
|
|
var store = tx.objectStore(STORE_NAME);
|
|
var request = store.clear();
|
|
request.onsuccess = function () { resolve(); };
|
|
request.onerror = function () { reject(request.error); };
|
|
});
|
|
});
|
|
}
|
|
|
|
// ─── Install: pre-cache app shell ────────────────────────────────
|
|
self.addEventListener('install', function (event) {
|
|
event.waitUntil(
|
|
caches.open(CACHE_NAME).then(function (cache) {
|
|
return cache.addAll(APP_SHELL);
|
|
}).then(function () {
|
|
return self.skipWaiting();
|
|
})
|
|
);
|
|
});
|
|
|
|
// ─── Activate: purge old caches ──────────────────────────────────
|
|
self.addEventListener('activate', function (event) {
|
|
event.waitUntil(
|
|
caches.keys().then(function (names) {
|
|
return Promise.all(
|
|
names.filter(function (n) { return n !== CACHE_NAME; })
|
|
.map(function (n) { return caches.delete(n); })
|
|
);
|
|
}).then(function () {
|
|
return self.clients.claim();
|
|
}).then(function () {
|
|
return self.clients.matchAll({ type: 'window' }).then(function (clients) {
|
|
clients.forEach(function (client) {
|
|
client.postMessage({ type: 'SW_UPDATED', cacheName: CACHE_NAME });
|
|
});
|
|
});
|
|
})
|
|
);
|
|
});
|
|
|
|
// ─── Fetch strategy ──────────────────────────────────────────────
|
|
self.addEventListener('fetch', function (event) {
|
|
var url = new URL(event.request.url);
|
|
var req = event.request;
|
|
|
|
// Only handle requests within /pos/ scope
|
|
if (url.pathname.indexOf('/pos/') === -1) {
|
|
return;
|
|
}
|
|
|
|
// Normalize cache key for static assets (strip query strings) so
|
|
// catalog.js?v=5 and catalog.js?v=6 share the same cache entry.
|
|
var cacheKey = req;
|
|
if (/\.(js|css|png|jpg|jpeg|webp|svg|gif|ico|woff|woff2|ttf|eot|json)$/.test(url.pathname)) {
|
|
cacheKey = new Request(url.pathname);
|
|
}
|
|
|
|
// Never cache auth endpoints
|
|
if (url.pathname.indexOf('/pos/api/auth/') !== -1) {
|
|
return;
|
|
}
|
|
|
|
// HTML pages -> network-first (always fresh)
|
|
if (req.headers.get('accept') && req.headers.get('accept').indexOf('text/html') !== -1) {
|
|
event.respondWith(networkFirst(req));
|
|
return;
|
|
}
|
|
|
|
// Offline cart queue: POST /pos/api/cart/*
|
|
if (req.method === 'POST' && url.pathname.indexOf('/pos/api/cart/') !== -1) {
|
|
event.respondWith(
|
|
fetch(req.clone()).then(function (response) {
|
|
return response;
|
|
}).catch(function () {
|
|
return req.clone().text().then(function (bodyText) {
|
|
var entry = {
|
|
url: req.url,
|
|
method: req.method,
|
|
headers: Array.from(req.headers.entries()),
|
|
body: bodyText,
|
|
timestamp: Date.now()
|
|
};
|
|
return savePendingRequest(entry);
|
|
}).then(function () {
|
|
return new Response(
|
|
JSON.stringify({ queued: true, message: 'Added to offline queue' }),
|
|
{ status: 200, headers: { 'Content-Type': 'application/json' } }
|
|
);
|
|
}).catch(function (err) {
|
|
console.error('[SW] Failed to queue offline cart request:', err);
|
|
return new Response(
|
|
JSON.stringify({ queued: false, message: 'Failed to queue request' }),
|
|
{ status: 503, headers: { 'Content-Type': 'application/json' } }
|
|
);
|
|
})
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
// WhatsApp endpoints need fresh data
|
|
if (url.pathname.indexOf('/pos/api/whatsapp/') !== -1) {
|
|
return;
|
|
}
|
|
|
|
// API calls -> network-first
|
|
if (url.pathname.indexOf('/pos/api/') !== -1) {
|
|
event.respondWith(networkFirst(req));
|
|
return;
|
|
}
|
|
|
|
// Everything else (JS, CSS, images) -> cache-first
|
|
event.respondWith(cacheFirst(req, cacheKey));
|
|
});
|
|
|
|
function cacheFirst(request, cacheKey) {
|
|
cacheKey = cacheKey || request;
|
|
return caches.match(cacheKey).then(function (cached) {
|
|
if (cached) {
|
|
fetch(request).then(function (response) {
|
|
if (response && response.status === 200 && request.method === 'GET') {
|
|
caches.open(CACHE_NAME).then(function (cache) {
|
|
cache.put(cacheKey, response);
|
|
});
|
|
}
|
|
}).catch(function () {});
|
|
return cached;
|
|
}
|
|
return fetch(request).then(function (response) {
|
|
if (response && response.status === 200 && request.method === 'GET') {
|
|
var clone = response.clone();
|
|
caches.open(CACHE_NAME).then(function (cache) {
|
|
cache.put(cacheKey, clone);
|
|
});
|
|
}
|
|
return response;
|
|
});
|
|
});
|
|
}
|
|
|
|
function networkFirst(request) {
|
|
return fetch(request).then(function (response) {
|
|
if (response && response.status === 200 && request.method === 'GET') {
|
|
var clone = response.clone();
|
|
caches.open(CACHE_NAME).then(function (cache) {
|
|
cache.put(request, clone);
|
|
});
|
|
}
|
|
return response;
|
|
}).catch(function () {
|
|
return caches.match(request);
|
|
});
|
|
}
|
|
|
|
// ─── Background Sync ─────────────────────────────────────────────
|
|
self.addEventListener('sync', function (event) {
|
|
if (event.tag === 'nexus-pos-sync') {
|
|
event.waitUntil(
|
|
self.clients.matchAll().then(function (clients) {
|
|
clients.forEach(function (client) {
|
|
client.postMessage({ type: 'SYNC_REQUESTED' });
|
|
});
|
|
})
|
|
);
|
|
}
|
|
|
|
if (event.tag === 'nexus-cart-sync') {
|
|
event.waitUntil(
|
|
getPendingRequests().then(function (entries) {
|
|
if (!entries || entries.length === 0) {
|
|
console.log('[SW] No pending cart actions to sync.');
|
|
return;
|
|
}
|
|
console.log('[SW] Syncing', entries.length, 'pending cart action(s)...');
|
|
var syncPromises = entries.map(function (entry) {
|
|
return fetch(entry.url, {
|
|
method: entry.method,
|
|
headers: entry.headers.reduce(function (obj, h) {
|
|
obj[h[0]] = h[1];
|
|
return obj;
|
|
}, {}),
|
|
body: entry.body
|
|
}).then(function (response) {
|
|
console.log('[SW] Cart sync success for', entry.url, '- status', response.status);
|
|
return { ok: true, id: entry.id };
|
|
}).catch(function (err) {
|
|
console.error('[SW] Cart sync failed for', entry.url, '-', err);
|
|
return { ok: false, id: entry.id };
|
|
});
|
|
});
|
|
return Promise.all(syncPromises).then(function (results) {
|
|
var allOk = results.every(function (r) { return r.ok; });
|
|
if (allOk) {
|
|
return clearPendingRequests().then(function () {
|
|
console.log('[SW] All cart actions synced. Pending queue cleared.');
|
|
});
|
|
} else {
|
|
var failedIds = results.filter(function (r) { return !r.ok; }).map(function (r) { return r.id; });
|
|
console.warn('[SW] Some cart actions failed. Keeping', failedIds.length, 'entries for retry.');
|
|
}
|
|
});
|
|
}).catch(function (err) {
|
|
console.error('[SW] Error during nexus-cart-sync:', err);
|
|
})
|
|
);
|
|
}
|
|
});
|
|
|
|
// ─── Push Notifications ──────────────────────────────────────────
|
|
self.addEventListener('push', function (event) {
|
|
var data = {};
|
|
if (event.data) {
|
|
try {
|
|
data = event.data.json();
|
|
} catch (e) {
|
|
data = { title: event.data.text() };
|
|
}
|
|
}
|
|
var title = data.title || 'Nexus POS';
|
|
var options = {
|
|
body: data.body || 'Tienes una nueva notificación del POS.',
|
|
icon: '/pos/static/pwa/icon-192.png',
|
|
badge: '/pos/static/pwa/icon-192.png',
|
|
tag: data.tag || 'nexus-pos-general',
|
|
data: data.data || { url: '/pos/sale' },
|
|
requireInteraction: false
|
|
};
|
|
event.waitUntil(
|
|
self.registration.showNotification(title, options)
|
|
);
|
|
});
|
|
|
|
// ─── Notification Click ──────────────────────────────────────────
|
|
self.addEventListener('notificationclick', function (event) {
|
|
event.notification.close();
|
|
var targetUrl = event.notification.data && event.notification.data.url
|
|
? event.notification.data.url
|
|
: '/pos/sale';
|
|
event.waitUntil(
|
|
self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then(function (clientList) {
|
|
for (var i = 0; i < clientList.length; i++) {
|
|
var client = clientList[i];
|
|
if (client.url.indexOf('/pos/') !== -1 && 'focus' in client) {
|
|
return client.focus().then(function (focusedClient) {
|
|
if ('navigate' in focusedClient) {
|
|
return focusedClient.navigate(targetUrl);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
if (self.clients.openWindow) {
|
|
return self.clients.openWindow(targetUrl);
|
|
}
|
|
})
|
|
);
|
|
});
|
|
|
|
// ─── Message handler ─────────────────────────────────────────────
|
|
self.addEventListener('message', function (event) {
|
|
if (event.data && event.data.type === 'SKIP_WAITING') {
|
|
self.skipWaiting();
|
|
}
|
|
if (event.data && event.data.type === 'CLEAR_CACHES') {
|
|
event.waitUntil(
|
|
caches.keys().then(function (names) {
|
|
return Promise.all(
|
|
names.map(function (n) { return caches.delete(n); })
|
|
);
|
|
}).then(function () {
|
|
console.log('[SW] All caches cleared by client request.');
|
|
return self.clients.matchAll().then(function (clients) {
|
|
clients.forEach(function (client) {
|
|
client.postMessage({ type: 'CACHES_CLEARED' });
|
|
});
|
|
});
|
|
})
|
|
);
|
|
}
|
|
});
|