1//
2// Service worker
3// (◡ ‿ ◡ ✿)
4//
5// This worker is responsible for caching the application
6// so it can be used offline and acts as a proxy that
7// allows for example, authentication through headers
8// when using audio elements.
9//
10/// <reference lib="webworker" />
11
12
13const KEY =
14 /* eslint-disable no-undef *//* @ts-ignore */
15 `diffuse-${BUILD_TIMESTAMP}`
16
17
18const EXCLUDE =
19 [ "_headers"
20 , "_redirects"
21 , "CORS"
22 ]
23
24
25const GOOGLE_DRIVE = "https://www.googleapis.com/drive/"
26
27
28
29// 🙈
30
31
32const isNativeWrapper = location.host === "localhost:44999" || location.host === "127.0.0.1:44999"
33let googleDriveToken
34
35
36
37// 📣
38
39
40self.addEventListener("activate", () => {
41 // Remove all caches except the one with the currently used `KEY`
42 caches.keys().then(keys => {
43 keys.forEach(k => {
44 if (k !== KEY) caches.delete(k)
45 })
46 })
47})
48
49
50self.addEventListener("install", event => {
51 if (isNativeWrapper) {
52 return globalThis.skipWaiting()
53 }
54
55 const href = self.location.href.replace("service-worker.js", "")
56 const promise = fetch("tree.json")
57 .then(response => response.json())
58 .then(tree => {
59 const filteredTree = tree
60 .filter(t => !EXCLUDE.find(u => u === t))
61 .filter(u => u.endsWith(".jpg"))
62 const whatToCache = [ href, `${href.replace(/\/+$/, "")}/about/` ].concat(filteredTree)
63 return caches.open(KEY).then(c => Promise.all(whatToCache.map(x => c.add(x))))
64 })
65
66 event.waitUntil(promise)
67})
68
69
70self.addEventListener("fetch", fetchEvent => {
71 const event = fetchEvent as FetchEvent
72
73 const isInternal =
74 !!event.request.url.match(new RegExp("^" + self.location.origin))
75
76 // Ping
77 if (event.request.url.includes("?ping=1")) {
78 event.respondWith(
79 (async () => {
80 const serverIsOnline = await network(event).then(_ => true).catch(_ => false)
81 return new Response(JSON.stringify(serverIsOnline), {
82 headers: { "Content-Type": "application/json" }
83 })
84 })()
85 )
86
87 // When doing a request with basic authentication in the url, put it in the headers instead
88 } else if (event.request.url.includes("basic_auth=")) {
89 const url = new URL(event.request.url)
90 const token = url.searchParams.get("basic_auth")
91
92 event.respondWith(newRequestWithAuth(
93 event.request,
94 url.toString(),
95 "Basic " + token
96 ))
97
98 // When doing a request with access token in the url, put it in the headers instead
99 } else if (event.request.url.includes("bearer_token=")) {
100 const url = new URL(event.request.url)
101 const token = url.searchParams.get("bearer_token")
102
103 if (url.href.startsWith(GOOGLE_DRIVE)) googleDriveToken = token
104
105 url.searchParams.delete("bearer_token")
106 url.search = "?" + url.searchParams.toString()
107
108 event.respondWith(newRequestWithAuth(
109 event.request,
110 url.toString(),
111 "Bearer " + token
112 ))
113
114 // Use cache if internal request and not using native app
115 } else if (isInternal) {
116 event.respondWith(
117 isNativeWrapper
118 ? network(event)
119 : cacheThenNetwork(event)
120 )
121
122 } else if (event.request.url && event.request.url.startsWith(GOOGLE_DRIVE) && event.request.url.includes("alt=media")) {
123 // For some reason Safari starts using the non bearer-token URL while playing audio
124 event.respondWith(
125 googleDriveToken
126 ? newRequestWithAuth(
127 event.request,
128 event.request.url.toString(),
129 "Bearer " + googleDriveToken
130 )
131 : network(event)
132 )
133
134 }
135})
136
137
138function cacheThenNetwork(event) {
139 const url = new URL(event.request.url)
140 url.search = ""
141
142 return caches
143 .open(KEY)
144 .then(cache => cache.match(url))
145 .then(match => match || fetch(url))
146}
147
148
149function network(event) {
150 return fetch(event.request.url)
151}
152
153
154addEventListener("message", event => {
155 if (event.data === "skipWaiting") {
156 globalThis.skipWaiting()
157 }
158})
159
160
161
162// ⚗️
163
164
165function newRequestWithAuth(request: Request, newUrl: string, authToken: string): Promise<Response> {
166 const newHeaders = new Headers(request.headers)
167 newHeaders.append("authorization", authToken)
168
169 const newRequest = new Request(request, { headers: newHeaders })
170
171 const makeFetch = () => fetch(newRequest).then(async r => {
172 if (r.ok) {
173 return r
174 } else {
175 return r.text().then(text => {
176 throw new Error(text)
177 })
178 }
179 })
180
181 return makeFetch()
182}