feat(pwa): improve service worker with background sync, push, IndexedDB
- Bumped cache version to nexus-pos-v3 - Background sync for cart (nexus-cart-sync): replays pending requests from IndexedDB, clears queue on success - Push notifications: parse payload, show notification, focus/open /pos/sale on click - Offline cart strategy: queue failed POST /pos/api/cart/* in IndexedDB, return queued JSON response - Message handlers: SKIP_WAITING (preserved) + CLEAR_CACHES - Periodic background sync stub commented for future cache warming
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
// /home/Autopartes/pos/static/pwa/sw.js
|
||||
// Nexus POS — Service Worker v1
|
||||
// Nexus POS — Service Worker v3
|
||||
// Self-contained vanilla JS. No external imports.
|
||||
|
||||
const CACHE_NAME = 'nexus-pos-v2';
|
||||
const CACHE_NAME = 'nexus-pos-v3';
|
||||
|
||||
const APP_SHELL = [
|
||||
'/pos/login',
|
||||
@@ -35,6 +36,61 @@ const APP_SHELL = [
|
||||
'/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(
|
||||
@@ -63,6 +119,7 @@ self.addEventListener('activate', function (event) {
|
||||
// ─── 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) {
|
||||
@@ -76,18 +133,57 @@ self.addEventListener('fetch', function (event) {
|
||||
|
||||
// Don't cache login page — always fetch fresh to avoid stale redirects
|
||||
if (url.pathname === '/pos/login' || url.pathname === '/pos/login/') {
|
||||
event.respondWith(networkFirst(event.request));
|
||||
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 () {
|
||||
// Clone request body to store it for later retry
|
||||
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;
|
||||
}
|
||||
|
||||
// API calls → network-first
|
||||
if (url.pathname.indexOf('/pos/api/') !== -1) {
|
||||
event.respondWith(networkFirst(event.request));
|
||||
event.respondWith(networkFirst(req));
|
||||
return;
|
||||
}
|
||||
|
||||
// Everything else → cache-first
|
||||
event.respondWith(cacheFirst(event.request));
|
||||
event.respondWith(cacheFirst(req));
|
||||
});
|
||||
|
||||
function cacheFirst(request) {
|
||||
@@ -130,9 +226,10 @@ function networkFirst(request) {
|
||||
}
|
||||
|
||||
// ─── Background Sync ─────────────────────────────────────────────
|
||||
// Existing sync handler + new cart-specific sync
|
||||
self.addEventListener('sync', function (event) {
|
||||
if (event.tag === 'nexus-pos-sync') {
|
||||
event.respondWith && event.waitUntil(
|
||||
event.waitUntil(
|
||||
self.clients.matchAll().then(function (clients) {
|
||||
clients.forEach(function (client) {
|
||||
client.postMessage({ type: 'SYNC_REQUESTED' });
|
||||
@@ -140,11 +237,144 @@ self.addEventListener('sync', function (event) {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
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)...');
|
||||
|
||||
// Replay each pending request to the server
|
||||
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 {
|
||||
// Remove only successfully synced entries; failed ones will retry next time
|
||||
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) {
|
||||
// Focus existing tab if it matches the target scope
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
// Otherwise open a new window
|
||||
if (self.clients.openWindow) {
|
||||
return self.clients.openWindow(targetUrl);
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// ─── Periodic Background Sync (stub for future use) ──────────────
|
||||
// This can be used to warm the cache daily or refresh catalog data
|
||||
// in the background. Requires user permission and browser support.
|
||||
// self.addEventListener('periodicsync', function (event) {
|
||||
// if (event.tag === 'nexus-daily-sync') {
|
||||
// event.waitUntil(
|
||||
// // e.g. cache warming, catalog refresh, etc.
|
||||
// caches.open(CACHE_NAME).then(function (cache) {
|
||||
// return cache.add('/pos/api/catalog/refresh');
|
||||
// }).catch(function (err) {
|
||||
// console.error('[SW] periodicsync failed:', err);
|
||||
// })
|
||||
// );
|
||||
// }
|
||||
// });
|
||||
|
||||
// ─── 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' });
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user