export const CACHE_VERSION = 'v7';
export const APP_SHELL = '/index.html';
export const PRECACHE_URLS = [
'/',
'/index.html',
'/styles/app.css',
'/scripts/app.js',
'/scripts/vendor.js',
'/fonts/inter-var.woff2',
'/icons/logo.svg',
'/offline.html'
];
import { CACHE_VERSION, PRECACHE_URLS, APP_SHELL } from './assets.js';
const STATIC_CACHE = `static-${CACHE_VERSION}`;
const EXPECTED_CACHES = [STATIC_CACHE];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(STATIC_CACHE).then((cache) => cache.addAll(PRECACHE_URLS))
);
});
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((keys) => {
const stale = keys.filter((key) => !EXPECTED_CACHES.includes(key));
return Promise.all(stale.map((key) => caches.delete(key)));
}).then(() => self.clients.claim())
);
});
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
if (request.method !== 'GET' || url.origin !== self.location.origin) {
return;
}
if (request.mode === 'navigate') {
event.respondWith(handleNavigation(request));
return;
}
event.respondWith(cacheFirst(request));
});
async function handleNavigation(request) {
try {
const fresh = await fetch(request);
const cache = await caches.open(STATIC_CACHE);
cache.put(APP_SHELL, fresh.clone());
return fresh;
} catch (err) {
const cache = await caches.open(STATIC_CACHE);
return (await cache.match(APP_SHELL)) || cache.match('/offline.html');
}
}
async function cacheFirst(request) {
const cache = await caches.open(STATIC_CACHE);
const hit = await cache.match(request);
if (hit) return hit;
const response = await fetch(request);
if (response.ok && response.type === 'basic') {
cache.put(request, response.clone());
}
return response;
}
export function registerServiceWorker() {
if (!('serviceWorker' in navigator)) return;
window.addEventListener('load', async () => {
try {
const reg = await navigator.serviceWorker.register('/sw.js', {
scope: '/'
});
reg.addEventListener('updatefound', () => {
const incoming = reg.installing;
if (!incoming) return;
incoming.addEventListener('statechange', () => {
const hasController = Boolean(navigator.serviceWorker.controller);
if (incoming.state === 'installed' && hasController) {
promptForUpdate(reg);
}
});
});
} catch (err) {
console.error('SW registration failed:', err);
}
});
navigator.serviceWorker.addEventListener('controllerchange', () => {
window.location.reload();
});
}
function promptForUpdate(reg) {
const accepted = window.confirm('A new version is available. Reload now?');
if (accepted && reg.waiting) {
reg.waiting.postMessage({ type: 'SKIP_WAITING' });
}
}
This snippet shows a production-shaped service worker that precaches a fixed set of static assets and serves them offline without ever leaving the app stuck on stale files. The core pattern is a versioned cache name combined with an atomic install and cleanup lifecycle, so each deploy gets its own cache bucket and old buckets are deleted only after the new worker takes control.
In sw.js, CACHE_VERSION is baked into STATIC_CACHE so a bump invalidates everything at once. The install handler opens that cache and calls addAll on the precache list; because addAll rejects if any request fails, a broken URL aborts the whole install and the old worker keeps running — this fail-closed behavior is deliberate and prevents a half-populated cache. self.skipWaiting() is intentionally not called here so an active tab is never swapped out from under the user mid-session.
The activate handler is where cleanup happens: it enumerates caches.keys() and deletes any cache whose name is not in the current allow-list, then calls clients.claim() so the freshly activated worker controls existing pages. Doing deletion in activate rather than install guarantees the new cache is fully built before the old one disappears.
The fetch handler only intercepts same-origin GET requests — cross-origin and non-GET traffic passes straight through, which avoids corrupting POSTs and opaque responses. Navigation requests use a network-first strategy via handleNavigation so users get fresh HTML when online but fall back to the cached app shell offline. Everything else uses cacheFirst, which returns a hit immediately and otherwise fetches, then caches only successful, basic-type responses to avoid poisoning the cache with errors or opaque cross-origin bodies.
assets.js centralizes the precache manifest and the version string, keeping the worker logic clean and making the list easy to generate at build time. register.js wires the worker into the page and, critically, listens for updatefound and statechange to detect when a new worker is installed and waiting, then prompts a reload — the missing half of a safe update flow. Together these files avoid the classic pitfalls: stale assets, partial caches, and abrupt worker takeover.
Related snips
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
module Api
module V1
class UsersController < BaseController
def show
user = User.includes(:profile).find(params[:id])
ETags for conditional requests and caching
require "csv"
class PeopleCsvStream
include Enumerable
HEADERS = %w[id full_name email signed_up_at plan].freeze
Resilient CSV Export as a Streamed Response
Rails.application.configure do
config.after_initialize do
Bullet.enable = true
Bullet.alert = false
Bullet.bullet_logger = true
Bullet.console = true
N+1 query detection with Bullet gem
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["form"]
static values = { delay: { type: Number, default: 250 } }
Debounced live search with Stimulus + Turbo Streams
json.array! @posts do |post|
json.cache! ['v1', post], expires_in: 1.hour do
json.id post.id
json.title post.title
json.excerpt post.excerpt
json.published_at post.published_at
Fragment caching for expensive JSON serialization
Share this code
Here's the card — post it anywhere.