Enable offline caching for all web users

Previously, offline caching was conditionally enabled only for Hotwire
Native apps. This removes that restriction and enables offline support
for all users, including PWAs and regular browsers.

This Uses the new `fetchOptions` support in `TurboOffline` handlers
to pass `cache: "no-cache"` for document fetches, which we need
to work around a quite annoying Safari PWA bug
(see https://github.com/basecamp/fizzy/pull/1014).

Also, simplify a bit the cache names and remove the `misc` one.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Rosa Gutierrez
2026-01-28 21:30:50 +01:00
parent a949f1e3ae
commit cf8d92afea
3 changed files with 44 additions and 43 deletions
+1 -1
View File
@@ -13,7 +13,7 @@ GIT
GIT GIT
remote: https://github.com/hotwired/turbo-rails.git remote: https://github.com/hotwired/turbo-rails.git
revision: 45a2a40be0a677808a3a269311f1b5abaa3b30ba revision: 9bcc244043b16ef0202f8fe93aaeb246d841f9eb
branch: offline-cache branch: offline-cache
specs: specs:
turbo-rails (2.0.21) turbo-rails (2.0.21)
+1 -1
View File
@@ -62,7 +62,7 @@ GIT
GIT GIT
remote: https://github.com/hotwired/turbo-rails.git remote: https://github.com/hotwired/turbo-rails.git
revision: 45a2a40be0a677808a3a269311f1b5abaa3b30ba revision: 9bcc244043b16ef0202f8fe93aaeb246d841f9eb
branch: offline-cache branch: offline-cache
specs: specs:
turbo-rails (2.0.21) turbo-rails (2.0.21)
+42 -41
View File
@@ -1,60 +1,61 @@
importScripts("<%= javascript_url("turbo-offline-umd.min") %>") importScripts("<%= javascript_url("turbo-offline-umd.min") %>")
<% if hotwire_native_app? %> // Documents - top-level navigation requests only
TurboOffline.addRule({ // We need `cache: "no-cache"` here to work around
match: /\/assets\/.+[-\.][0-9a-f]+\.(js|css|svg|png|jpg|webp|woff2?|ico)$/, // an annoying Safari PWA bug that predates offline mode
handler: TurboOffline.handlers.cacheFirst({ TurboOffline.addRule({
cacheName: "assets", match: (request) => request.destination === "document",
maxAge: 60 * 60 * 24 * 7 handler: TurboOffline.handlers.networkFirst({
}) cacheName: "main",
maxAge: 60 * 60 * 24 * 3,
networkTimeout: 3,
fetchOptions: { cache: "no-cache" }
}) })
})
TurboOffline.addRule({ TurboOffline.addRule({
match: /\/(boards|cards|users)\//, match: /\/assets\/.+[-\.][0-9a-f]+\.(js|css|svg|png|jpg|webp|woff2?|ico)$/,
except: /\/(edit|pin|watch|new)$/, handler: TurboOffline.handlers.cacheFirst({
handler: TurboOffline.handlers.networkFirst({ cacheName: "assets",
cacheName: "cards", maxAge: 60 * 60 * 24 * 7
maxAge: 60 * 60 * 24 * 3,
networkTimeout: 2
})
}) })
})
TurboOffline.addRule({ TurboOffline.addRule({
match: /\/rails\/active_storage\//, match: /\/(boards|cards|users)\//,
handler: TurboOffline.handlers.networkFirst({ except: /\/(edit|pin|watch|new)$/,
cacheName: "storage", handler: TurboOffline.handlers.networkFirst({
maxAge: 60 * 60 * 24 * 7, cacheName: "main",
networkTimeout: 2 maxAge: 60 * 60 * 24 * 3,
}) networkTimeout: 2
}) })
})
// Everything else TurboOffline.addRule({
TurboOffline.addRule({ match: /\/rails\/active_storage\//,
handler: TurboOffline.handlers.networkFirst({ handler: TurboOffline.handlers.networkFirst({
cacheName: "misc", cacheName: "storage",
maxAge: 60 * 60 * 24, maxAge: 60 * 60 * 24 * 7,
networkTimeout: 3 networkTimeout: 2
})
}) })
})
TurboOffline.start() // Everything else
<% else %> TurboOffline.addRule({
self.addEventListener("fetch", (event) => { handler: TurboOffline.handlers.networkFirst({
if (event.request.method !== "GET") return cacheName: "main",
maxAge: 60 * 60 * 24,
if (event.request.destination === "document") { networkTimeout: 3
event.respondWith(
fetch(event.request, { cache: "no-cache" })
.catch(() => caches.match(event.request))
)
}
}) })
<% end %> })
self.addEventListener("activate", (event) => { self.addEventListener("activate", (event) => {
event.waitUntil(self.clients.claim()) event.waitUntil(self.clients.claim())
}) })
TurboOffline.start()
self.addEventListener("push", (event) => { self.addEventListener("push", (event) => {
const data = event.data.json() const data = event.data.json()
event.waitUntil(Promise.all([ showNotification(data), updateBadgeCount(data.options) ])) event.waitUntil(Promise.all([ showNotification(data), updateBadgeCount(data.options) ]))