|
4 | 4 | })(this, (function(exports) { |
5 | 5 | "use strict"; |
6 | 6 | /*! |
7 | | - Turbo 8.0.20 |
8 | | - Copyright © 2025 37signals LLC |
| 7 | + Turbo 8.0.21 |
| 8 | + Copyright © 2026 37signals LLC |
9 | 9 | */ class Rule { |
10 | | - constructor({handler: handler, match: match = /.*/} = {}) { |
| 10 | + constructor({handler: handler, match: match = /.*/, except: except} = {}) { |
11 | 11 | this.handler = handler; |
12 | 12 | this.match = match; |
| 13 | + this.except = except; |
13 | 14 | } |
14 | 15 | 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 ]; |
17 | 22 | return regexes.some((regex => regex.test(request.url))); |
18 | 23 | } |
19 | 24 | async handle(event) { |
|
22 | 27 | return response; |
23 | 28 | } |
24 | 29 | } |
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 | | - } |
70 | 30 | const DATABASE_NAME = "turbo-offline-database"; |
71 | 31 | const DATABASE_VERSION = 1; |
72 | 32 | 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 | + } |
73 | 42 | class CacheRegistryDatabase { |
74 | | - get(cacheName, key) { |
| 43 | + get(key) { |
75 | 44 | const getOp = store => this.#requestToPromise(store.get(key)); |
76 | 45 | return this.#performOperation(STORE_NAME, getOp, "readonly"); |
77 | 46 | } |
78 | | - has(cacheName, key) { |
| 47 | + has(key) { |
79 | 48 | const countOp = store => this.#requestToPromise(store.count(key)); |
80 | 49 | return this.#performOperation(STORE_NAME, countOp, "readonly").then((result => result === 1)); |
81 | 50 | } |
|
92 | 61 | }; |
93 | 62 | return this.#performOperation(STORE_NAME, putOp, "readwrite"); |
94 | 63 | } |
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)); |
97 | 66 | } |
98 | 67 | getOlderThan(cacheName, timestamp) { |
99 | 68 | const getOlderThanOp = store => { |
|
104 | 73 | }; |
105 | 74 | return this.#performOperation(STORE_NAME, getOlderThanOp, "readonly"); |
106 | 75 | } |
107 | | - delete(cacheName, key) { |
| 76 | + delete(key) { |
108 | 77 | const deleteOp = store => this.#requestToPromise(store.delete(key)); |
109 | 78 | return this.#performOperation(STORE_NAME, deleteOp, "readwrite"); |
110 | 79 | } |
|
160 | 129 | this.database = getDatabase(); |
161 | 130 | } |
162 | 131 | get(key) { |
163 | | - return this.database.get(this.cacheName, key); |
| 132 | + return this.database.get(key); |
164 | 133 | } |
165 | 134 | has(key) { |
166 | | - return this.database.has(this.cacheName, key); |
| 135 | + return this.database.has(key); |
167 | 136 | } |
168 | 137 | put(key, value = {}) { |
169 | 138 | return this.database.put(this.cacheName, key, value); |
170 | 139 | } |
171 | 140 | getTimestamp(key) { |
172 | | - return this.database.getTimestamp(this.cacheName, key); |
| 141 | + return this.database.getTimestamp(key); |
173 | 142 | } |
174 | 143 | getOlderThan(timestamp) { |
175 | 144 | return this.database.getOlderThan(this.cacheName, timestamp); |
176 | 145 | } |
177 | 146 | 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))); |
179 | 198 | } |
180 | 199 | } |
181 | 200 | class CacheTrimmer { |
|
262 | 281 | const cachePromise = cache.put(cacheKeyUrl, response); |
263 | 282 | const registryPromise = this.cacheRegistry.put(cacheKeyUrl); |
264 | 283 | 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 | + })); |
266 | 290 | } |
267 | 291 | } |
268 | 292 | canCacheResponse(response) { |
269 | 293 | return response.status === 200 || response.status === 0; |
270 | 294 | } |
| 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 | + } |
271 | 305 | } |
272 | 306 | function buildCacheKey(requestOrUrl, response) { |
273 | 307 | const request = new Request(requestOrUrl); |
|
0 commit comments