view mrjunejune/src/sw.js @ 279:b3b547563ec7

Add Google connector service and agent wiki Implement the C/Seobeo Google Drive and Gmail connector with encrypted OAuth storage, Zenbu authentication, browser testing, AI tool discovery, chunked HTTP decoding, and Bazel coverage. Consolidate repository guidance into progressive wiki documentation and enforce arena-first allocation for new first-party C code. Co-authored-by: Copilot <[email protected]> Copilot-Session: 84c338fd-0939-4bb3-b7f3-1062eb213e5d
author MrJuneJune <me@mrjunejune.com>
date Mon, 17 Aug 2026 22:22:36 -0700
parents e02e2036ef84
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))
        );
      })
    );
  }
});