git / moonbite-network / static/service-worker.js · raw

/** * MoonBite Service Worker * iOS & Android PWA Support with offline-first strategy * TLS 1.3 compliant, secure headers support */const CACHE_VERSION = 'moonbite-v4-bulletproof-wallet';const STATIC_CACHE = `${CACHE_VERSION}-static`;const DYNAMIC_CACHE = `${CACHE_VERSION}-dynamic`;const API_CACHE = `${CACHE_VERSION}-api`;const IMAGE_CACHE = `${CACHE_VERSION}-images`;// Critical static assets to cache on installconst CRITICAL_ASSETS = [  '/',  '/wallet',  '/index.html',  '/wallet.html',  '/static/manifest.json',  '/static/style.css',  '/static/site.css',  '/static/wallet-security.js',  '/static/moonbite-logo.svg'];// Install Event - Precache critical assetsself.addEventListener('install', event => {  console.log('[SW] Installing service worker v' + CACHE_VERSION);  event.waitUntil(    caches.open(STATIC_CACHE)      .then(cache => {        console.log('[SW] Caching critical assets');        return cache.addAll(CRITICAL_ASSETS).catch(err => {          console.warn('[SW] Some critical assets failed to cache:', err);          // Don't fail install if some assets unavailable        });      })      .then(() => self.skipWaiting())  );});// Activate Event - Clean up old cachesself.addEventListener('activate', event => {  console.log('[SW] Activating service worker');  event.waitUntil(    caches.keys().then(cacheNames => {      return Promise.all(        cacheNames          .filter(name => !name.startsWith(CACHE_VERSION))          .map(name => {            console.log('[SW] Deleting old cache:', name);            return caches.delete(name);          })      );    })      .then(() => self.clients.claim())  );});// Fetch Event - Smart caching strategyself.addEventListener('fetch', event => {  const { request } = event;  const url = new URL(request.url);  // Skip cross-origin requests and non-GET requests  if (!url.origin.includes(self.location.origin) || request.method !== 'GET') {    return;  }  // API requests: Network-first with fallback to cache  if (url.pathname.startsWith('/api/')) {    event.respondWith(networkFirstStrategy(request, API_CACHE));    return;  }  // Images: Cache-first with network fallback  if (request.destination === 'image') {    event.respondWith(cacheFirstStrategy(request, IMAGE_CACHE));    return;  }  // HTML: Network-first for dynamic content  if (request.destination === 'document') {    event.respondWith(networkFirstStrategy(request, DYNAMIC_CACHE, 5000));    return;  }  // CSS, JS, fonts: Cache-first (versioned)  if (['style', 'script', 'font'].includes(request.destination)) {    event.respondWith(cacheFirstStrategy(request, STATIC_CACHE));    return;  }  // Default: Stale-while-revalidate  event.respondWith(staleWhileRevalidateStrategy(request, DYNAMIC_CACHE));});/** * Cache-first strategy: Use cache, fallback to network */function cacheFirstStrategy(request, cacheName) {  return caches.match(request)    .then(cached => {      if (cached) {        return cached;      }      return fetch(request)        .then(response => {          // Only cache successful responses          if (response && response.status === 200) {            const clone = response.clone();            caches.open(cacheName).then(cache => {              cache.put(request, clone);            });          }          return response;        })        .catch(() => {          // Return offline response for specific content types          if (request.destination === 'image') {            return caches.match('/static/offline-image.svg');          }          return new Response('Offline - resource unavailable', {            status: 503,            statusText: 'Service Unavailable',            headers: new Headers({ 'Content-Type': 'text/plain' })          });        });    });}/** * Network-first strategy: Try network, fallback to cache */function networkFirstStrategy(request, cacheName, timeout = 5000) {  return new Promise((resolve, reject) => {    let timeoutId;    // Set timeout for network request    timeoutId = setTimeout(() => {      console.log('[SW] Network timeout, trying cache for:', request.url);      caches.match(request)        .then(cached => {          if (cached) {            resolve(cached);          } else {            resolve(new Response(              JSON.stringify({ error: 'Offline and no cached response' }),              {                status: 503,                statusText: 'Service Unavailable',                headers: new Headers({ 'Content-Type': 'application/json' })              }            ));          }        })        .catch(reject);    }, timeout);    // Try network first    fetch(request)      .then(response => {        clearTimeout(timeoutId);        // Cache successful responses        if (response && response.status === 200) {          const clone = response.clone();          caches.open(cacheName).then(cache => {            cache.put(request, clone);          });        }        resolve(response);      })      .catch(() => {        clearTimeout(timeoutId);        // Network failed, try cache        caches.match(request)          .then(cached => {            if (cached) {              resolve(cached);            } else {              resolve(new Response(                JSON.stringify({ error: 'Network error and no cached response' }),                {                  status: 503,                  statusText: 'Service Unavailable',                  headers: new Headers({ 'Content-Type': 'application/json' })                }              ));            }          })          .catch(reject);      });  });}/** * Stale-while-revalidate: Serve cache immediately, update in background */function staleWhileRevalidateStrategy(request, cacheName) {  return caches.match(request)    .then(cached => {      const fetchPromise = fetch(request)        .then(response => {          if (response && response.status === 200) {            const clone = response.clone();            caches.open(cacheName).then(cache => {              cache.put(request, clone);            });          }          return response;        })        .catch(() => {          // Network failed, return cached or error          if (cached) {            return cached;          }          return new Response('Offline and no cached response', {            status: 503,            statusText: 'Service Unavailable'          });        });      return cached || fetchPromise;    });}// Background Sync for pending transactionsself.addEventListener('sync', event => {  console.log('[SW] Background sync triggered:', event.tag);  if (event.tag === 'sync-transactions') {    event.waitUntil(syncPendingTransactions());  }  if (event.tag === 'sync-wallet') {    event.waitUntil(syncWalletData());  }});async function syncPendingTransactions() {  try {    const db = await openDB();    const pending = await getPendingTransactions(db);    console.log('[SW] Syncing ' + pending.length + ' pending transactions');    for (const tx of pending) {      try {        const response = await fetch('/api/wallet/submit-tx', {          method: 'POST',          headers: { 'Content-Type': 'application/json' },          body: JSON.stringify(tx)        });        if (response.ok) {          await markTransactionSynced(db, tx.id);          notifyClients({            type: 'TRANSACTION_SYNCED',            txId: tx.id,            status: 'success'          });        }      } catch (err) {        console.error('[SW] Failed to sync transaction:', tx.id, err);      }    }  } catch (err) {    console.error('[SW] Sync error:', err);    throw err;  }}async function syncWalletData() {  try {    const response = await fetch('/api/wallet/info');    if (response.ok) {      const data = await response.json();      // Store in cache for offline access      const cache = await caches.open(API_CACHE);      cache.put('/api/wallet/info', response.clone());      // Notify clients of update      notifyClients({        type: 'WALLET_SYNCED',        data: data,        timestamp: Date.now()      });    }  } catch (err) {    console.error('[SW] Wallet sync failed:', err);  }}// Periodic Background Sync (if supported)self.addEventListener('periodicsync', event => {  console.log('[SW] Periodic sync triggered:', event.tag);  if (event.tag === 'update-wallet') {    event.waitUntil(syncWalletData());  }});// Message handling from clientsself.addEventListener('message', event => {  console.log('[SW] Message received:', event.data);  const { type, payload } = event.data;  switch (type) {    case 'SKIP_WAITING':      self.skipWaiting();      break;    case 'GET_VERSION':      event.ports[0].postMessage({        type: 'VERSION',        version: CACHE_VERSION      });      break;    case 'CACHE_STATUS':      caches.keys().then(cacheNames => {        event.ports[0].postMessage({          type: 'CACHE_STATUS',          caches: cacheNames,          timestamp: Date.now()        });      });      break;    case 'CLEAR_CACHE':      caches.delete(payload.cacheName).then(() => {        event.ports[0].postMessage({          type: 'CACHE_CLEARED',          cacheName: payload.cacheName        });      });      break;    case 'SYNC_WALLET':      event.waitUntil(        syncWalletData().then(() => {          event.ports[0].postMessage({            type: 'SYNC_COMPLETE',            success: true          });        })      );      break;  }});// Utility: Notify all clientsfunction notifyClients(message) {  self.clients.matchAll().then(clients => {    clients.forEach(client => {      client.postMessage(message);    });  });}// IndexedDB helpersfunction openDB() {  return new Promise((resolve, reject) => {    const request = indexedDB.open('MoonBiteWallet', 1);    request.onerror = () => reject(request.error);    request.onsuccess = () => resolve(request.result);    request.onupgradeneeded = event => {      const db = event.target.result;      if (!db.objectStoreNames.contains('transactions')) {        const txStore = db.createObjectStore('transactions', { keyPath: 'id' });        txStore.createIndex('status', 'status', { unique: false });        txStore.createIndex('timestamp', 'timestamp', { unique: false });      }    };  });}async function getPendingTransactions(db) {  return new Promise((resolve, reject) => {    const transaction = db.transaction('transactions', 'readonly');    const store = transaction.objectStore('transactions');    const index = store.index('status');    const range = IDBKeyRange.only('pending');    const request = index.getAll(range);    request.onerror = () => reject(request.error);    request.onsuccess = () => resolve(request.result);  });}async function markTransactionSynced(db, txId) {  return new Promise((resolve, reject) => {    const transaction = db.transaction('transactions', 'readwrite');    const store = transaction.objectStore('transactions');    const request = store.get(txId);    request.onsuccess = () => {      const tx = request.result;      if (tx) {        tx.status = 'synced';        const updateRequest = store.put(tx);        updateRequest.onerror = () => reject(updateRequest.error);        updateRequest.onsuccess = () => resolve();      }    };    request.onerror = () => reject(request.error);  });}// Log service worker stateconsole.log('[SW] Service Worker loaded and ready');