view mrjunejune/src/sw.js @ 273:e02e2036ef84 default tip

add Layer 2 JRPG component system Add reusable content and window modals, an isolated component sandbox, shared cyberpunk scroll areas, production-safe cache freshness, and server-rendered JRPG panel state. Co-authored-by: Copilot <[email protected]>
author MrJuneJune <me@mrjunejune.com>
date Sat, 08 Aug 2026 02:08:08 -0700
parents 056790c4fb0d
children
line wrap: on
line source

// Root-scoped Service Worker for MrJuneJune PWA
const CACHE_VERSION = 'v37-network-first';
const CACHE_NAME = `mrjunejune-${CACHE_VERSION}`;
const DEVELOPMENT_SANDBOX_PATHS = [
  '/public/component-sandbox',
  '/public/composer-lab',
  '/public/mjj-composer.',
  '/public/mjj-modal.',
];

// Files to cache immediately on install
const STATIC_CACHE = [
  '/',
  '/offline.html',
  '/index.js',
  '/base.css',
  '/public/design-system/styles/tokens.css',
  '/public/design-system/styles/reference.css',
  '/public/design-system/styles/semantic.css',
  '/public/design-system/styles/density.css',
  '/public/design-system/styles/themes.css',
  '/public/design-system/styles/components.css',
  '/public/design-system/styles/elements.css',
  '/public/design-system/components/index.js',
  '/public/design-system/components/alert.js',
  '/public/design-system/components/button.js',
  '/public/design-system/components/card.js',
  '/public/design-system/components/collections.js',
  '/public/design-system/components/disclosure.js',
  '/public/design-system/components/field.js',
  '/public/design-system/components/forms.js',
  '/public/design-system/components/icon.js',
  '/public/design-system/components/link.js',
  '/public/design-system/components/notifications.js',
  '/public/design-system/components/overlays.js',
  '/public/design-system/components/primitives.js',
  '/public/design-system/components/stack.js',
  '/public/design-system/components/story.js',
  '/public/epi_all_colors.svg',
  '/public/fonts/Roboto-Regular.ttf',
  '/public/fonts/Roboto-Thin.ttf',
  '/public/fonts/more-sugar.regular.otf',
  '/public/fonts/more-sugar.thin.otf',
];

// Install event - cache static assets
self.addEventListener('install', (event) => {
  console.log('[SW] Installing service worker...');

  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => {
      console.log('[SW] Caching static assets');
      return cache.addAll(STATIC_CACHE);
    }).then(() => {
      console.log('[SW] Skip waiting');
      return self.skipWaiting();
    })
  );
});

// Activate event - clean up old caches
self.addEventListener('activate', (event) => {
  console.log('[SW] Activating service worker...');

  event.waitUntil(
    caches.keys().then((cacheNames) => {
      return Promise.all(
        cacheNames.map((cacheName) => {
          if (cacheName !== CACHE_NAME) {
            console.log('[SW] Deleting old cache:', cacheName);
            return caches.delete(cacheName);
          }
        })
      );
    }).then(() => {
      console.log('[SW] Claiming clients');
      return self.clients.claim();
    })
  );
});

function isCacheable(response) {
  return response &&
    response.status === 200 &&
    response.type !== 'error';
}

async function updateCache(request, response) {
  if (!isCacheable(response)) return;
  const cache = await caches.open(CACHE_NAME);
  await cache.put(request, response.clone());
}

async function networkFirst(request, offlineFallback = false) {
  try {
    const response = await fetch(request, { cache: 'no-cache' });
    await updateCache(request, response);
    return response;
  } catch (error) {
    const cached = await caches.match(request);
    if (cached) return cached;
    if (offlineFallback) {
      const offline = await caches.match('/offline.html');
      if (offline) return offline;
    }
    throw error;
  }
}

async function staleWhileRevalidate(event, request) {
  const cached = await caches.match(request);
  const update = fetch(request, { cache: 'no-cache' }).then(async response => {
    await updateCache(request, response);
    return response;
  });
  if (!cached) return update;
  event.waitUntil(update.catch(() => undefined));
  return cached;
}

// Fetch event - current code first, cached media first, offline fallback.
self.addEventListener('fetch', (event) => {
  const { request } = event;
  const url = new URL(request.url);

  // Skip non-GET requests
  if (request.method !== 'GET') {
    return;
  }

  // Skip chrome-extension and other non-http(s) requests
  if (!url.protocol.startsWith('http')) {
    return;
  }

  if (url.origin !== self.location.origin) {
    return;
  }

  // Skip API calls and media uploads (always go to network)
  if (url.pathname.startsWith('/api/')) {
    return;
  }

  // Development servers should always serve current source and assets.
  if (url.hostname === 'localhost' || url.hostname === '127.0.0.1') {
    return;
  }

  // Component workbenches must reflect every ibazel rebuild on any hostname.
  if (DEVELOPMENT_SANDBOX_PATHS.some(path =>
    url.pathname.startsWith(path))) {
    return;
  }

  // The HLS tool and media must always reflect the current player version.
  if (url.pathname.startsWith('/tools/hls_player') ||
      url.pathname.startsWith('/public/hls-sample/')) {
    return;
  }

  const acceptsHtml = request.mode === 'navigate' ||
    request.headers.get('Accept')?.includes('text/html');
  const currentCode = /\.(?:css|js|json|wasm)$/i.test(url.pathname);
  const revalidatedMedia =
    /\.(?:svg|jpe?g|webp|woff2?|ttf|otf|pdf)$/i.test(url.pathname);

  if (acceptsHtml || currentCode) {
    event.respondWith(networkFirst(request, acceptsHtml));
    return;
  }

  if (revalidatedMedia) {
    event.respondWith(staleWhileRevalidate(event, request));
  }
});

// Handle messages from the client
self.addEventListener('message', (event) => {
  if (event.data && event.data.type === 'SKIP_WAITING') {
    self.skipWaiting();
  }

  if (event.data && event.data.type === 'CLEAR_CACHE') {
    event.waitUntil(
      caches.keys().then((cacheNames) => {
        return Promise.all(
          cacheNames.map((cacheName) => caches.delete(cacheName))
        );
      })
    );
  }
});