Files
Clintchiz d402256547
Deploy to Production / Build & Verify (push) Failing after 5m56s
Ping Search Engines / Notify Search Engines (push) Successful in 2s
Deploy to Production / Pre-Deploy Tests (push) Has been skipped
Deploy to Production / Deploy to Railway (push) Has been skipped
Deploy to Production / Deploy to Render (push) Has been skipped
Deploy to Production / Deploy to VPS (PM2) (push) Has been skipped
Deploy to Production / Deploy to Fly.io (push) Has been skipped
Deploy to Production / Post-Deploy Verification (push) Has been skipped
Deploy to Production / Notify on Failure (push) Successful in 2s
E2E Test Suite / Critical User Journeys (push) Has been skipped
E2E Test Suite / API Integration Tests (push) Has been skipped
E2E Test Suite / Smoke Tests (P0) (push) Failing after 11m26s
E2E Test Suite / Form Interaction Tests (push) Failing after 11m42s
E2E Test Suite / Destructive & Chaos Tests (push) Failing after 12m2s
E2E Test Suite / Cross-Browser Regression (chromium) (push) Failing after 16m14s
E2E Test Suite / Cross-Browser Regression (webkit) (push) Failing after 17m45s
E2E Test Suite / Cross-Browser Regression (firefox) (push) Failing after 25m23s
E2E Test Suite / Security Header Tests (push) Failing after 7m55s
E2E Test Suite / Test Report Summary (push) Failing after 20s
E2E Test Suite / Mobile Device Tests (push) Failing after 2h49m9s
Uptime Monitor / Health & Response Time (push) Failing after 2s
Uptime Monitor / SSL Certificate (push) Successful in 2s
Uptime Monitor / Send Alerts (push) Failing after 3s
Uptime Monitor / Record Uptime Success (push) Has been skipped
First Init
2026-03-21 16:46:46 +05:30

248 lines
7.7 KiB
JavaScript

/**
* WorkRoot IT Solutions - Service Worker
* Implements:
* - Cache-first strategy for static assets
* - Stale-while-revalidate for HTML pages
* - Offline fallback page
* - Background sync for form submissions
*/
const CACHE_VERSION = 'v1';
const STATIC_CACHE = `workroot-static-${CACHE_VERSION}`;
const PAGE_CACHE = `workroot-pages-${CACHE_VERSION}`;
const SYNC_QUEUE = 'workroot-sync-queue';
// Static assets to pre-cache on install
const PRECACHE_ASSETS = [
'/',
'/offline',
'/manifest.json',
'/favicon.svg',
'/apple-touch-icon.png',
];
// Page routes to cache with stale-while-revalidate
const PAGE_ROUTES = [
'/',
'/about',
'/services',
'/portfolio',
'/contact',
'/blog',
'/privacy',
'/terms',
];
// ─── Install Event ────────────────────────────────────────────────────────────
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(STATIC_CACHE).then((cache) => {
return cache.addAll(PRECACHE_ASSETS);
}).then(() => self.skipWaiting())
);
});
// ─── Activate Event ───────────────────────────────────────────────────────────
self.addEventListener('activate', (event) => {
event.waitUntil(
Promise.all([
// Delete old caches
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames
.filter((name) => name.startsWith('workroot-') && name !== STATIC_CACHE && name !== PAGE_CACHE)
.map((name) => caches.delete(name))
);
}),
// Take control immediately
self.clients.claim(),
])
);
});
// ─── Fetch Event ──────────────────────────────────────────────────────────────
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
// Only handle same-origin requests
if (url.origin !== self.location.origin) return;
// Skip non-GET requests (POST etc. handled via background sync)
if (request.method !== 'GET') return;
// Skip API routes — always go to network
if (url.pathname.startsWith('/api/')) return;
// Static assets: cache-first
if (isStaticAsset(url.pathname)) {
event.respondWith(cacheFirst(request, STATIC_CACHE));
return;
}
// HTML pages: stale-while-revalidate with offline fallback
if (request.headers.get('Accept')?.includes('text/html')) {
event.respondWith(staleWhileRevalidate(request));
return;
}
});
// ─── Background Sync Event ───────────────────────────────────────────────────
self.addEventListener('sync', (event) => {
if (event.tag === 'form-sync') {
event.waitUntil(processSyncQueue());
}
});
// ─── Push Event (for future notifications) ──────────────────────────────────
self.addEventListener('push', (event) => {
if (!event.data) return;
const data = event.data.json();
event.waitUntil(
self.registration.showNotification(data.title || 'WorkRoot', {
body: data.body || '',
icon: '/apple-touch-icon.png',
badge: '/favicon.svg',
data: data.url ? { url: data.url } : undefined,
})
);
});
self.addEventListener('notificationclick', (event) => {
event.notification.close();
if (event.notification.data?.url) {
event.waitUntil(clients.openWindow(event.notification.data.url));
}
});
// ─── Strategy Helpers ─────────────────────────────────────────────────────────
function isStaticAsset(pathname) {
return (
pathname.startsWith('/_assets/') ||
/\.(js|css|woff2?|ttf|otf|eot|png|jpg|jpeg|gif|webp|avif|svg|ico)$/i.test(pathname)
);
}
async function cacheFirst(request, cacheName) {
const cache = await caches.open(cacheName);
const cached = await cache.match(request);
if (cached) return cached;
try {
const response = await fetch(request);
if (response.ok) {
cache.put(request, response.clone());
}
return response;
} catch {
return new Response('Asset unavailable offline', { status: 503 });
}
}
async function staleWhileRevalidate(request) {
const cache = await caches.open(PAGE_CACHE);
const cached = await cache.match(request);
const fetchPromise = fetch(request)
.then((response) => {
if (response.ok) {
cache.put(request, response.clone());
}
return response;
})
.catch(() => null);
if (cached) {
// Serve cached immediately, update in background
fetchPromise.catch(() => {});
return cached;
}
// No cache — await network
const response = await fetchPromise;
if (response) return response;
// Network failed and no cache — serve offline fallback
const offlineFallback = await cache.match('/offline') || await caches.match('/offline');
if (offlineFallback) return offlineFallback;
return new Response(
'<html><body><h1>You are offline</h1><p>Please check your connection.</p></body></html>',
{ status: 503, headers: { 'Content-Type': 'text/html' } }
);
}
// ─── Background Sync Queue ────────────────────────────────────────────────────
async function processSyncQueue() {
const db = await openDB();
const queue = await getAllFromDB(db, SYNC_QUEUE);
for (const item of queue) {
try {
const response = await fetch(item.url, {
method: item.method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(item.body),
});
if (response.ok) {
await deleteFromDB(db, SYNC_QUEUE, item.id);
// Notify any open clients of success
const clients = await self.clients.matchAll();
clients.forEach((client) => {
client.postMessage({ type: 'SYNC_SUCCESS', id: item.id, formType: item.formType });
});
}
} catch {
// Leave in queue for next sync attempt
}
}
}
// ─── IndexedDB Helpers ────────────────────────────────────────────────────────
function openDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('workroot-pwa', 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains(SYNC_QUEUE)) {
const store = db.createObjectStore(SYNC_QUEUE, { keyPath: 'id', autoIncrement: true });
store.createIndex('timestamp', 'timestamp', { unique: false });
}
};
request.onsuccess = (event) => resolve(event.target.result);
request.onerror = () => reject(request.error);
});
}
function getAllFromDB(db, storeName) {
return new Promise((resolve, reject) => {
const tx = db.transaction(storeName, 'readonly');
const store = tx.objectStore(storeName);
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
function deleteFromDB(db, storeName, id) {
return new Promise((resolve, reject) => {
const tx = db.transaction(storeName, 'readwrite');
const store = tx.objectStore(storeName);
const request = store.delete(id);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}