-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsw.js
More file actions
151 lines (131 loc) · 4.2 KB
/
sw.js
File metadata and controls
151 lines (131 loc) · 4.2 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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
// v2 — install prompt support
const CACHE_NAME = "qr-generator";
const SHARE_CACHE = "share-data";
const APP_SHELL = [
"/qr/",
"/qr/index.html",
"/qr/style.css",
"/qr/script.js",
"/qr/ce.js",
"/qr/qrcode.js",
"/qr/manifest.json",
"/qr/icons/icon-192.png",
"/qr/icons/icon-512.png"
];
self.addEventListener("install", (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => cache.addAll(APP_SHELL))
);
self.skipWaiting();
});
self.addEventListener("activate", (event) => {
event.waitUntil(
caches.keys().then((keys) =>
Promise.all(
keys
.filter((key) => key !== CACHE_NAME && key !== SHARE_CACHE)
.map((key) => caches.delete(key))
)
).then(() => self.clients.claim())
);
});
self.addEventListener("fetch", (event) => {
const url = new URL(event.request.url);
// Share target POST handler
if (url.pathname === "/qr/share-target" && event.request.method === "POST") {
event.respondWith(handleShareTarget(event.request));
return;
}
// Shared data retrieval endpoint
if (url.pathname === "/qr/get-shared-data") {
event.respondWith(handleGetSharedData());
return;
}
// Only handle same-origin GET requests
if (url.origin !== self.location.origin || event.request.method !== "GET") return;
event.respondWith(
caches.open(CACHE_NAME).then((cache) =>
cache.match(event.request).then((cached) => {
const cachedForCompare = cached?.clone();
const revalidate = fetch(event.request)
.then(async (response) => {
if (!response.ok) return response;
// For text assets, detect content changes before updating cache
if (cachedForCompare && isTextAsset(url.pathname)) {
const [freshText, oldText] = await Promise.all([
response.clone().text(),
cachedForCompare.text()
]);
if (freshText !== oldText) {
await cache.put(event.request, response.clone());
notifyClients();
}
return response;
}
// Binary assets or first-time cache: just update
await cache.put(event.request, response.clone());
return response;
})
.catch(() => null);
if (cached) {
// Stale: return cached immediately, revalidate in background
event.waitUntil(revalidate);
return cached;
}
// No cache — wait for network
return revalidate.then((response) => {
if (response) return response;
// Offline fallback for navigation
if (event.request.mode === "navigate") {
return cache.match("/qr/index.html");
}
return new Response("Offline", { status: 503 });
});
})
)
);
});
function isTextAsset(pathname) {
return /\.(html|css|js|json)$/.test(pathname) || pathname.endsWith("/");
}
async function notifyClients() {
const clients = await self.clients.matchAll({ type: "window" });
clients.forEach((client) => client.postMessage({ type: "UPDATE_AVAILABLE" }));
}
async function handleShareTarget(request) {
const formData = await request.formData();
const data = {
title: formData.get("title") || "",
text: formData.get("text") || "",
url: formData.get("url") || ""
};
const files = formData.getAll("files");
for (const file of files) {
if (file instanceof File && file.size > 0) {
data.fileContent = await file.text();
data.fileName = file.name;
data.fileType = file.type;
}
}
const cache = await caches.open(SHARE_CACHE);
await cache.put("/shared-data", new Response(JSON.stringify(data)));
return Response.redirect("/qr/?shared=1", 303);
}
async function handleGetSharedData() {
try {
const cache = await caches.open(SHARE_CACHE);
const response = await cache.match("/shared-data");
if (response) {
const data = await response.text();
await cache.delete("/shared-data");
return new Response(data, {
headers: { "Content-Type": "application/json" }
});
}
} catch (e) {
// ignore
}
return new Response("null", {
headers: { "Content-Type": "application/json" }
});
}