tinyrelayfile
tinyrelay/tinyclient/sw.js/

tinyclient/sw.js raw
1// Service worker: makes the relay installable and keeps navigation working
2// when the network fails. Every request goes to the network first. Successful
3// page loads and the shell scripts are kept as the offline fallback; relay
4// data requested by scripts is never cached.
5const CACHE = "tiny-shell-v1";
6const shellAsset = url => /\/(fixi\.js|signer\.js|webmcp\.js|sw\.js|scripts\/[\w.-]+\.js|icon[^/]*\.(svg|png)|badge-96\.png|apple-touch-icon\.png|manifest\.webmanifest)$/.test(url.pathname);
7self.addEventListener("install", () => self.skipWaiting());
8self.addEventListener("activate", event => {
9 event.waitUntil(caches.keys().then(keys => Promise.all(keys.filter(key => key !== CACHE).map(key => caches.delete(key)))).then(() => self.clients.claim()));
10});
11self.addEventListener("fetch", event => {
12 const request = event.request;
13 const url = new URL(request.url);
14 if (url.origin !== location.origin) return;
15 if (request.method === "POST" && /\/share$/.test(url.pathname)) {
16 event.respondWith(receiveShare(request, url));
17 return;
18 }
19 if (request.method !== "GET") return;
20 const keep = request.mode === "navigate" || shellAsset(url);
21 if (!keep) return;
22 event.respondWith(fetch(request).then(response => {
23 if (response.ok && response.type === "basic") {
24 const copy = response.clone();
25 caches.open(CACHE).then(cache => cache.put(request, copy)).catch(() => {});
26 }
27 return response;
28 }).catch(async () => (await caches.match(request)) || (request.mode === "navigate" ? caches.match(new Request(url.origin + url.pathname.replace(/\/[^/]*$/, "/"))) : undefined)));
29});
30
31// Shares from other apps arrive as a form post. The files are parked in a
32// cache under a fresh id and the Files page picks them up from its fragment,
33// so nothing is uploaded until the person presses Upload there.
34const receiveShare = async (request, url) => {
35 const form = await request.formData();
36 const id = crypto.randomUUID();
37 const cache = await caches.open("tiny-share");
38 const files = form.getAll("files").filter(item => typeof item === "object" && "arrayBuffer" in item);
39 await Promise.all(files.map((file, index) => cache.put(new Request(url.origin + "/share/" + id + "/" + index), new Response(file, {headers: {"content-type": file.type || "application/octet-stream", "x-name": encodeURIComponent(file.name || "shared")}}))));
40 await cache.put(new Request(url.origin + "/share/" + id + "/meta"), Response.json({title: form.get("title") || "", text: form.get("text") || "", url: form.get("url") || "", count: files.length}));
41 return Response.redirect(url.pathname.replace(/\/share$/, "/files") + "#share=" + id, 303);
42};
43
44// Background uploads finish after the page closes. The last response is the
45// blob descriptor; it is parked for the Files page and the person is told.
46const uploadResult = async (registration, ok, detail) => {
47 const cache = await caches.open("tiny-uploads");
48 let response = Response.json({error: detail || "upload failed"}, {status: 500});
49 if (ok) {
50 const records = await registration.matchAll();
51 const last = records[records.length - 1];
52 if (last) response = (await last.responseReady).clone();
53 }
54 await cache.put(new Request(new URL("/uploads/" + registration.id, self.registration.scope).href), response);
55};
56self.addEventListener("backgroundfetchsuccess", event => {
57 event.waitUntil(uploadResult(event.registration, true).then(() => event.updateUI({title: "Upload complete"})));
58});
59self.addEventListener("backgroundfetchfail", event => {
60 event.waitUntil(uploadResult(event.registration, false, event.registration.failureReason).then(() => event.updateUI({title: "Upload failed"})));
61});
62self.addEventListener("backgroundfetchabort", event => {
63 event.waitUntil(uploadResult(event.registration, false, "aborted"));
64});
65self.addEventListener("backgroundfetchclick", event => {
66 event.waitUntil(self.clients.openWindow(new URL("files", self.registration.scope).href));
67});
68
69// Notifications are opt-in per device; a push only arrives after the person
70// enabled them on this browser. The payload is the relay's short summary,
71// with the answers a request for a decision offers as notification actions.
72const notificationActions = list => Array.isArray(list) ? list.filter(item => item && /^[a-z]+$/.test(item.action || "")).map(item => ({action: item.action, title: String(item.title || item.action)})) : [];
73self.addEventListener("push", event => {
74 let data = {};
75 try { data = event.data?.json() || {}; } catch { data = {body: event.data?.text() || ""}; }
76 const base = new URL(self.registration.scope);
77 const badge = Number.isInteger(data.badge) && data.badge > 0 ? navigator.setAppBadge?.(data.badge) : navigator.clearAppBadge?.();
78 event.waitUntil(Promise.all([Promise.resolve(badge).catch(() => {}), self.registration.showNotification(data.title || "tiny", {
79 body: data.body || "", tag: data.tag || "tiny", icon: new URL("icon-192.png", base).href, badge: new URL("badge-96.png", base).href, data: {url: data.url || base.href}, actions: notificationActions(data.actions)
80 })]));
81});
82// An action opens the notification's page with answer=<action>, where the
83// page asks for confirmation before anything is signed.
84const answerURL = (url, action) => /^[a-z]+$/.test(action || "") ? url + (url.includes("?") ? "&" : "?") + "answer=" + action : url;
85self.addEventListener("notificationclick", event => {
86 event.notification.close();
87 const target = answerURL(event.notification.data?.url || self.registration.scope, event.action);
88 event.waitUntil(self.clients.matchAll({type: "window", includeUncontrolled: true}).then(async list => {
89 const open = list.find(client => client.url.startsWith(self.registration.scope));
90 if (open) { const focused = await open.navigate(target); return focused?.focus(); }
91 return self.clients.openWindow(target);
92 }));
93});

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs