-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsw.js
More file actions
122 lines (110 loc) · 3.65 KB
/
Copy pathsw.js
File metadata and controls
122 lines (110 loc) · 3.65 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
// Paper Dashboard — Service Worker
//
// Strategies:
// 1. Static + CDN assets → cache-first, network fallback
// 2. Navigation (HTML) → network-first so updates show without manual reload
// 3. GAS JSON endpoint → network-first; on network failure, fall back to
// the cached payload and postMessage `data-stale`
// so the UI can show a "from cache" chip. The GAS
// endpoint itself is cheap now (read-only cache
// lookup driven by a 1-min server-side scheduler),
// so we don't need SWR client-side.
//
// Bump CACHE_VERSION when you change static assets to force clients to refresh.
const CACHE_VERSION = "v20";
const STATIC_CACHE = "dashboard-static-" + CACHE_VERSION;
const PRECACHE_URLS = [
"./",
"./index.html",
"./manifest.json",
"./sw.js"
];
const GAS_HOSTS = ["script.google.com", "script.googleusercontent.com"];
self.addEventListener("install", (event) => {
event.waitUntil(
caches.open(STATIC_CACHE).then((cache) => cache.addAll(PRECACHE_URLS))
);
self.skipWaiting();
});
self.addEventListener("activate", (event) => {
event.waitUntil(
caches.keys().then((keys) =>
Promise.all(
keys.filter((k) => k !== STATIC_CACHE).map((k) => caches.delete(k))
)
)
);
self.clients.claim();
});
self.addEventListener("fetch", (event) => {
const req = event.request;
if (req.method !== "GET") return;
const url = new URL(req.url);
if (GAS_HOSTS.includes(url.host)) {
event.respondWith(handleGas(req));
return;
}
if (req.mode === "navigate" || req.destination === "document") {
event.respondWith(handleNetworkFirst(req));
return;
}
event.respondWith(handleStatic(req));
});
async function handleNetworkFirst(req) {
try {
const res = await fetch(req);
if (res && (res.ok || res.type === "opaque")) {
const cache = await caches.open(STATIC_CACHE);
try { await cache.put(req, res.clone()); } catch (e) {}
}
return res;
} catch (e) {
// Offline: try exact match first (preserves ?key=…), then ignore the
// query string so a fresh `?key=abc` install still finds the precached
// shell, then fall through to the bare index.html / scope root.
const cached =
(await caches.match(req)) ||
(await caches.match(req, { ignoreSearch: true })) ||
(await caches.match("./index.html")) ||
(await caches.match("./"));
return cached || Response.error();
}
}
async function handleGas(req) {
const cache = await caches.open(STATIC_CACHE);
try {
const res = await fetch(req, { redirect: "follow" });
if (res && (res.ok || res.type === "opaqueredirect")) {
try { await cache.put(req, res.clone()); } catch (e) {}
}
return res;
} catch (err) {
const cached = await cache.match(req);
if (cached) {
const clients = await self.clients.matchAll();
clients.forEach((c) => c.postMessage({
type: "data-stale",
reason: String((err && err.message) || err) || "network error"
}));
return cached;
}
throw err;
}
}
async function handleStatic(req) {
const cached = await caches.match(req);
if (cached) return cached;
try {
const res = await fetch(req);
if (res && (res.ok || res.type === "opaque")) {
const cache = await caches.open(STATIC_CACHE);
try { await cache.put(req, res.clone()); } catch (e) {}
}
return res;
} catch (e) {
return cached || Response.error();
}
}
self.addEventListener("message", (event) => {
if (event.data && event.data.type === "skip-waiting") self.skipWaiting();
});