Skip to content

Commit 45a2a40

Browse files
committed
Fetch latest changes to turbo-offline
1 parent 22c78d9 commit 45a2a40

9 files changed

Lines changed: 225 additions & 210 deletions

app/assets/javascripts/turbo-offline-umd.js

Lines changed: 94 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,21 @@
44
})(this, (function(exports) {
55
"use strict";
66
/*!
7-
Turbo 8.0.20
8-
Copyright © 2025 37signals LLC
7+
Turbo 8.0.21
8+
Copyright © 2026 37signals LLC
99
*/ class Rule {
10-
constructor({handler: handler, match: match = /.*/} = {}) {
10+
constructor({handler: handler, match: match = /.*/, except: except} = {}) {
1111
this.handler = handler;
1212
this.match = match;
13+
this.except = except;
1314
}
1415
matches(request) {
15-
if (typeof this.match === "function") return this.match(request);
16-
const regexes = Array.isArray(this.match) ? this.match : [ this.match ];
16+
return this.#matchesCondition(request, this.match) && !this.#matchesCondition(request, this.except);
17+
}
18+
#matchesCondition(request, condition) {
19+
if (!condition) return false;
20+
if (typeof condition === "function") return condition(request);
21+
const regexes = Array.isArray(condition) ? condition : [ condition ];
1722
return regexes.some((regex => regex.test(request.url)));
1823
}
1924
async handle(event) {
@@ -22,60 +27,24 @@
2227
return response;
2328
}
2429
}
25-
class ServiceWorker {
26-
#started=false;
27-
#rules=[];
28-
addRule(rule) {
29-
this.#rules.push(new Rule(rule));
30-
}
31-
start() {
32-
this.#warnIfNoRulesConfigured();
33-
if (!this.#started) {
34-
self.addEventListener("install", this.installed);
35-
self.addEventListener("message", this.messageReceived);
36-
self.addEventListener("fetch", this.fetch);
37-
this.#started = true;
38-
}
39-
}
40-
installed=event => {
41-
console.log("Service worker installed");
42-
};
43-
messageReceived=event => {
44-
if (this[event.data.action]) {
45-
const actionCall = this[event.data.action](event.data.params);
46-
event.waitUntil(actionCall);
47-
}
48-
};
49-
fetch=event => {
50-
if (this.#canInterceptRequest(event.request)) {
51-
const rule = this.#findMatchingRule(event.request);
52-
if (!rule) return;
53-
const response = rule.handle(event);
54-
event.respondWith(response);
55-
}
56-
};
57-
#warnIfNoRulesConfigured() {
58-
if (this.#rules.length === 0) {
59-
console.warn("No rules configured for service worker. No requests will be intercepted.");
60-
}
61-
}
62-
#canInterceptRequest(request) {
63-
const url = new URL(request.url, location.href);
64-
return request.method === "GET" && url.protocol.startsWith("http");
65-
}
66-
#findMatchingRule(request) {
67-
return this.#rules.find((rule => rule.matches(request)));
68-
}
69-
}
7030
const DATABASE_NAME = "turbo-offline-database";
7131
const DATABASE_VERSION = 1;
7232
const STORE_NAME = "cache-registry";
33+
function deleteCacheRegistries() {
34+
return new Promise(((resolve, reject) => {
35+
const request = indexedDB.deleteDatabase(DATABASE_NAME);
36+
request.onsuccess = () => resolve();
37+
request.onerror = () => reject(request.error);
38+
})).then((() => {
39+
cacheRegistryDatabase = null;
40+
}));
41+
}
7342
class CacheRegistryDatabase {
74-
get(cacheName, key) {
43+
get(key) {
7544
const getOp = store => this.#requestToPromise(store.get(key));
7645
return this.#performOperation(STORE_NAME, getOp, "readonly");
7746
}
78-
has(cacheName, key) {
47+
has(key) {
7948
const countOp = store => this.#requestToPromise(store.count(key));
8049
return this.#performOperation(STORE_NAME, countOp, "readonly").then((result => result === 1));
8150
}
@@ -92,8 +61,8 @@
9261
};
9362
return this.#performOperation(STORE_NAME, putOp, "readwrite");
9463
}
95-
getTimestamp(cacheName, key) {
96-
return this.get(cacheName, key).then((result => result?.timestamp));
64+
getTimestamp(key) {
65+
return this.get(key).then((result => result?.timestamp));
9766
}
9867
getOlderThan(cacheName, timestamp) {
9968
const getOlderThanOp = store => {
@@ -104,7 +73,7 @@
10473
};
10574
return this.#performOperation(STORE_NAME, getOlderThanOp, "readonly");
10675
}
107-
delete(cacheName, key) {
76+
delete(key) {
10877
const deleteOp = store => this.#requestToPromise(store.delete(key));
10978
return this.#performOperation(STORE_NAME, deleteOp, "readwrite");
11079
}
@@ -160,22 +129,72 @@
160129
this.database = getDatabase();
161130
}
162131
get(key) {
163-
return this.database.get(this.cacheName, key);
132+
return this.database.get(key);
164133
}
165134
has(key) {
166-
return this.database.has(this.cacheName, key);
135+
return this.database.has(key);
167136
}
168137
put(key, value = {}) {
169138
return this.database.put(this.cacheName, key, value);
170139
}
171140
getTimestamp(key) {
172-
return this.database.getTimestamp(this.cacheName, key);
141+
return this.database.getTimestamp(key);
173142
}
174143
getOlderThan(timestamp) {
175144
return this.database.getOlderThan(this.cacheName, timestamp);
176145
}
177146
delete(key) {
178-
return this.database.delete(this.cacheName, key);
147+
return this.database.delete(key);
148+
}
149+
}
150+
class ServiceWorker {
151+
#started=false;
152+
#rules=[];
153+
addRule(rule) {
154+
this.#rules.push(new Rule(rule));
155+
}
156+
start() {
157+
this.#warnIfNoRulesConfigured();
158+
if (!this.#started) {
159+
self.addEventListener("install", this.installed);
160+
self.addEventListener("message", this.messageReceived);
161+
self.addEventListener("fetch", this.fetch);
162+
this.#started = true;
163+
}
164+
}
165+
installed=event => {
166+
console.log("Service worker installed");
167+
};
168+
messageReceived=event => {
169+
if (this[event.data.action]) {
170+
const actionCall = this[event.data.action](event.data.params);
171+
event.waitUntil(actionCall);
172+
}
173+
};
174+
fetch=event => {
175+
if (this.#canInterceptRequest(event.request)) {
176+
const rule = this.#findMatchingRule(event.request);
177+
if (!rule) return;
178+
const response = rule.handle(event);
179+
event.respondWith(response);
180+
}
181+
};
182+
async clearCache() {
183+
const cacheNames = await caches.keys();
184+
await Promise.all(cacheNames.map((name => caches.delete(name))));
185+
await deleteCacheRegistries();
186+
}
187+
#warnIfNoRulesConfigured() {
188+
if (this.#rules.length === 0) {
189+
console.warn("No rules configured for service worker. No requests will be intercepted.");
190+
}
191+
}
192+
#canInterceptRequest(request) {
193+
const url = new URL(request.url, location.href);
194+
return request.method === "GET" && url.protocol.startsWith("http");
195+
}
196+
#findMatchingRule(request) {
197+
return this.#rules.find((rule => rule.matches(request)));
179198
}
180199
}
181200
class CacheTrimmer {
@@ -262,12 +281,27 @@
262281
const cachePromise = cache.put(cacheKeyUrl, response);
263282
const registryPromise = this.cacheRegistry.put(cacheKeyUrl);
264283
const trimPromise = this.cacheTrimmer.trim();
265-
return Promise.all([ cachePromise, registryPromise, trimPromise ]);
284+
return Promise.all([ cachePromise, registryPromise, trimPromise ]).catch((async error => {
285+
if (this.#isQuotaExceededError(error)) {
286+
await this.#clearAllStorage();
287+
}
288+
throw error;
289+
}));
266290
}
267291
}
268292
canCacheResponse(response) {
269293
return response.status === 200 || response.status === 0;
270294
}
295+
#isQuotaExceededError(error) {
296+
return error?.name === "QuotaExceededError" || error?.inner && error.inner.name === "QuotaExceededError";
297+
}
298+
async #clearAllStorage() {
299+
const cacheNames = await caches.keys();
300+
for (const cacheName of cacheNames) {
301+
await caches.delete(cacheName);
302+
}
303+
await deleteCacheRegistries();
304+
}
271305
}
272306
function buildCacheKey(requestOrUrl, response) {
273307
const request = new Request(requestOrUrl);

0 commit comments

Comments
 (0)