-
Notifications
You must be signed in to change notification settings - Fork 0
/
sw.js
265 lines (231 loc) · 9.3 KB
/
sw.js
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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
/* ===========================================================
* sw.js
* ===========================================================
* Copyright 2016 @huxpro
* Licensed under Apache 2.0
* service worker scripting
* ========================================================== */
// CACHE_NAMESPACE
// CacheStorage is shared between all sites under same domain.
// A namespace can prevent potential name conflicts and mis-deletion.
const CACHE_NAMESPACE = 'main-'
const CACHE = CACHE_NAMESPACE + 'precache-then-runtime';
const PRECACHE_LIST = [
"./",
"./offline.html",
"./js/jquery.min.js",
"./js/bootstrap.min.js",
"./js/mritd-blog.min.js",
"./js/snackbar.js",
"./img/avatar.jpg",
"./img/home-bg.jpg",
"./img/404-bg.jpg",
"./css/syntax.css",
"./css/mritd-blog.min.css",
"./css/bootstrap.min.css"
]
const HOSTNAME_WHITELIST = [
self.location.hostname,
"mritd.me",
"mritd.github.io",
"cdnjs.cloudflare.com"
]
const DEPRECATED_CACHES = ['precache-v1', 'runtime', 'main-precache-v1', 'main-runtime']
// The Util Function to hack URLs of intercepted requests
const getCacheBustingUrl = (req) => {
var now = Date.now();
url = new URL(req.url)
// 1. fixed http URL
// Just keep syncing with location.protocol
// fetch(httpURL) belongs to active mixed content.
// And fetch(httpRequest) is not supported yet.
url.protocol = self.location.protocol
// 2. add query for caching-busting.
// Github Pages served with Cache-Control: max-age=600
// max-age on mutable content is error-prone, with SW life of bugs can even extend.
// Until cache mode of Fetch API landed, we have to workaround cache-busting with query string.
// Cache-Control-Bug: https://bugs.chromium.org/p/chromium/issues/detail?id=453190
url.search += (url.search ? '&' : '?') + 'cache-bust=' + now;
return url.href
}
// The Util Function to detect and polyfill req.mode="navigate"
// request.mode of 'navigate' is unfortunately not supported in Chrome
// versions older than 49, so we need to include a less precise fallback,
// which checks for a GET request with an Accept: text/html header.
const isNavigationReq = (req) => (req.mode === 'navigate' || (req.method === 'GET' && req.headers.get('accept').includes('text/html')))
// The Util Function to detect if a req is end with extension
// Accordin to Fetch API spec <https://fetch.spec.whatwg.org/#concept-request-destination>
// Any HTML's navigation has consistently mode="navigate" type="" and destination="document"
// including requesting an img (or any static resources) from URL Bar directly.
// So It ends up with that regExp is still the king of URL routing ;)
// P.S. An url.pathname has no '.' can not indicate it ends with extension (e.g. /api/version/1.2/)
const endWithExtension = (req) => Boolean(new URL(req.url).pathname.match(/\.\w+$/))
// Redirect in SW manually fixed github pages arbitray 404s on things?blah
// what we want:
// repo?blah -> !(gh 404) -> sw 302 -> repo/?blah
// .ext?blah -> !(sw 302 -> .ext/?blah -> gh 404) -> .ext?blah
// If It's a navigation req and it's url.pathname isn't end with '/' or '.ext'
// it should be a dir/repo request and need to be fixed (a.k.a be redirected)
// Tracking https://twitter.com/Huxpro/status/798816417097224193
const shouldRedirect = (req) => (isNavigationReq(req) && new URL(req.url).pathname.substr(-1) !== "/" && !endWithExtension(req))
// The Util Function to get redirect URL
// `${url}/` would mis-add "/" in the end of query, so we use URL object.
// P.P.S. Always trust url.pathname instead of the whole url string.
const getRedirectUrl = (req) => {
url = new URL(req.url)
url.pathname += "/"
return url.href
}
/**
* @Lifecycle Install
* Precache anything static to this version of your app.
* e.g. App Shell, 404, JS/CSS dependencies...
*
* waitUntil() : installing ====> installed
* skipWaiting() : waiting(installed) ====> activating
*/
self.addEventListener('install', e => {
e.waitUntil(
caches.open(CACHE).then(cache => {
return cache.addAll(PRECACHE_LIST)
.then(self.skipWaiting())
.catch(err => console.log(err))
})
)
});
/**
* @Lifecycle Activate
* New one activated when old isnt being used.
*
* waitUntil(): activating ====> activated
*/
self.addEventListener('activate', event => {
// delete old deprecated caches.
caches.keys().then(cacheNames => Promise.all(
cacheNames
.filter(cacheName => DEPRECATED_CACHES.includes(cacheName))
.map(cacheName => caches.delete(cacheName))
))
console.log('service worker activated.')
event.waitUntil(self.clients.claim());
});
var fetchHelper = {
fetchThenCache: function(request){
// Requests with mode "no-cors" can result in Opaque Response,
// Requests to Allow-Control-Cross-Origin: * can't include credentials.
const init = { mode: "cors", credentials: "omit" }
const fetched = fetch(request, init)
const fetchedCopy = fetched.then(resp => resp.clone());
// NOTE: Opaque Responses have no hedaders so [[ok]] make no sense to them
// so Opaque Resp will not be cached in this case.
Promise.all([fetchedCopy, caches.open(CACHE)])
.then(([response, cache]) => response.ok && cache.put(request, response))
.catch(_ => {/* eat any errors */})
return fetched;
},
cacheFirst: function(url){
return caches.match(url)
.then(resp => resp || this.fetchThenCache(url))
.catch(_ => {/* eat any errors */})
}
}
/**
* @Functional Fetch
* All network requests are being intercepted here.
*
* void respondWith(Promise<Response> r);
*/
self.addEventListener('fetch', event => {
// logs for debugging
//console.log(`fetch ${event.request.url}`)
//console.log(` - type: ${event.request.type}; destination: ${event.request.destination}`)
//console.log(` - mode: ${event.request.mode}, accept: ${event.request.headers.get('accept')}`)
// Skip some of cross-origin requests, like those for Google Analytics.
if (HOSTNAME_WHITELIST.indexOf(new URL(event.request.url).hostname) > -1) {
// Redirect in SW manually fixed github pages 404s on repo?blah
if (shouldRedirect(event.request)) {
event.respondWith(Response.redirect(getRedirectUrl(event.request)))
return;
}
// Cache-only Startgies for ys.static resources
if (event.request.url.indexOf('ys.static') > -1){
event.respondWith(fetchHelper.cacheFirst(event.request.url))
return;
}
// Stale-while-revalidate for possiblily dynamic content
// similar to HTTP's stale-while-revalidate: https://www.mnot.net/blog/2007/12/12/stale
// Upgrade from Jake's to Surma's: https://gist.github.com/surma/eb441223daaedf880801ad80006389f1
const cached = caches.match(event.request);
const fetched = fetch(getCacheBustingUrl(event.request), { cache: "no-store" });
const fetchedCopy = fetched.then(resp => resp.clone());
// Call respondWith() with whatever we get first.
// Promise.race() resolves with first one settled (even rejected)
// If the fetch fails (e.g disconnected), wait for the cache.
// If there’s nothing in cache, wait for the fetch.
// If neither yields a response, return offline pages.
event.respondWith(
Promise.race([fetched.catch(_ => cached), cached])
.then(resp => resp || fetched)
.catch(_ => caches.match('offline.html'))
);
// Update the cache with the version we fetched (only for ok status)
event.waitUntil(
Promise.all([fetchedCopy, caches.open(CACHE)])
.then(([response, cache]) => response.ok && cache.put(event.request, response))
.catch(_ => {/* eat any errors */ })
);
// If one request is a HTML naviagtion, checking update!
if (isNavigationReq(event.request)) {
// you need "preserve logs" to see this log
// cuz it happened before navigating
console.log(`fetch ${event.request.url}`)
event.waitUntil(revalidateContent(cached, fetchedCopy))
}
}
});
/**
* Broadcasting all clients with MessageChannel API
*/
function sendMessageToAllClients(msg) {
self.clients.matchAll().then(clients => {
clients.forEach(client => {
console.log(client);
client.postMessage(msg)
})
})
}
/**
* Broadcasting all clients async
*/
function sendMessageToClientsAsync(msg) {
// waiting for new client alive with "async" setTimeout hacking
// https://twitter.com/Huxpro/status/799265578443751424
// https://jakearchibald.com/2016/service-worker-meeting-notes/#fetch-event-clients
setTimeout(() => {
sendMessageToAllClients(msg)
}, 1000)
}
/**
* if content modified, we can notify clients to refresh
* TODO: Gh-pages rebuild everything in each release. should find a workaround (e.g. ETag with cloudflare)
*
* @param {Promise<response>} cachedResp [description]
* @param {Promise<response>} fetchedResp [description]
* @return {Promise}
*/
function revalidateContent(cachedResp, fetchedResp) {
// revalidate when both promise resolved
return Promise.all([cachedResp, fetchedResp])
.then(([cached, fetched]) => {
const cachedVer = cached.headers.get('last-modified')
const fetchedVer = fetched.headers.get('last-modified')
console.log(`"${cachedVer}" vs. "${fetchedVer}"`);
if (cachedVer !== fetchedVer) {
sendMessageToClientsAsync({
'command': 'UPDATE_FOUND',
'url': fetched.url
})
}
})
.catch(err => console.log(err))
}