github.com/pbberlin/go-pwa@v0.0.0-20220328105622-7c26e0ca1ab8/app-bucket/tpl/service-worker.tpl.js (about) 1 /* 2 from 3 googlechrome.github.io/samples/service-worker/custom-offline-page/ 4 developers.google.com/web/ilt/pwa/caching-files-with-service-worker 5 web.dev/offline-cookbook 6 7 fetch including cookies: 8 fetch(url, {credentials: 'include'}) 9 10 non CORS fail by default; avoid by 11 new Request(urlToPrefetch, { mode: 'no-cors' } 12 13 */ 14 15 const VS = "{{.Version}}"; // version - also forcing update 16 17 const STATIC_TYPES = { // request.destination 18 "image": true, 19 "style": true, 20 "script": true, 21 // "font": true, 22 // "video": true, 23 "manifest": true, // special for PWA manifest.json 24 } 25 26 27 28 const oldCons = console; // window.console undefined in service worker 29 const newCons = function(oldCons) { 30 31 var prefixes = {}; 32 33 return { 34 log: function (text) { 35 // 'sw-vs-476442 - fetch - sttc stop' 36 // ' static cch -' 37 38 const prefPref = `sw-${VS} -`; 39 const pref1 = text.substring(0, prefPref.length); 40 if (pref1 === prefPref) { 41 text = text.substring(prefPref.length); 42 // oldCons.log("trimmed pref1"); 43 } 44 45 46 const pref2 = text.substring(0, 16); 47 if (pref2 in prefixes) { 48 prefixes[pref2]++; 49 } else { 50 prefixes[pref2] = 1; 51 } 52 53 // avoid escaping of < to < by golang template engine 54 if ( 4>prefixes[pref2] ) { 55 oldCons.log( prefixes[pref2], text); 56 } else { 57 // oldCons.log("swallowed", prefixes[prefix],text); 58 } 59 }, 60 info: function (text) { 61 oldCons.info(text); 62 }, 63 warn: function (text) { 64 oldCons.warn(text); 65 }, 66 error: function (text) { 67 oldCons.error(text); 68 }, 69 70 } 71 72 } 73 74 // console = newCons(oldCons); 75 76 77 78 79 // time of start of program 80 let tmSt = new Date().getTime(); 81 82 const tmSince = () => { 83 const tm = new Date().getTime(); 84 return `${tm - tmSt}`; 85 } 86 87 const tmReset = () => { 88 tmSt = new Date().getTime(); 89 } 90 91 const chopHost = (url) => { 92 url = url.replace(/^(AB)/, ''); // replaces "AB" only if it is at the beginning 93 url = url.replace(/^(https:\/\/localhost)/, ''); 94 return url; 95 } 96 97 98 99 100 const cacheNaviResps = true; // cache navigational responses 101 102 const reqOpts = { 103 cache: "reload", // => force fetching from network; not from html browser cache 104 method: "GET", 105 // headers: new Headers({ 'Content-Type': 'application/json' }), 106 // headers: new Headers({ 'Cache-Control': 'max-age=31536000' }), 107 // headers: new Headers({ 'Cache-Control': 'no-cache' }), 108 109 }; 110 111 const matchOpts = { 112 ignoreVary: true, // ignore differences in Headers 113 ignoreMethod: true, // ignore differences in HTTP methods 114 ignoreSearch: true // ignore differences in query strings 115 }; 116 117 118 const STATIC_RESS = [ 119 {{.ListOfFiles}} 120 ]; 121 122 // on failure: go to chrome://serviceworker-internals and check "Open DevTools window and pause 123 self.addEventListener('install', (evt) => { 124 console.log(`sw-${VS} - install - start ${tmSince()}ms`); 125 126 127 const fc = async () => { 128 const cch = await caches.open(CACHE_KEY); 129 130 let proms = []; 131 STATIC_RESS.forEach( res => { 132 // if (!rsp.ok) throw Error('Not ok'); 133 // return cch.put(url, rsp); 134 proms.push( cch.add( new Request(res, reqOpts) ) ); 135 }); 136 const allPr = await Promise.all(proms); 137 console.log(`sw-${VS} - install - preld ${tmSince()}ms ${allPr}`); 138 139 cch.put('/pets.json', new Response('{"tom": "cat", "jerry": "mouse"}') ); 140 141 }; 142 143 evt.waitUntil( fc() ); 144 145 async function requestBackgroundSync(tag) { 146 try { 147 await self.registration.sync.register(tag); 148 console.log("sync - supported (from service worker)"); 149 } catch (err) { 150 console.error(`sw-${VS} - self.registration.sync failed ${err}`); 151 } 152 } 153 requestBackgroundSync('tag-sync-install'); 154 155 // event.waitUntil( ( async() => { console.log(`payload`); })() ); 156 console.log(`sw-${VS} - install - stop ${tmSince()}ms`); 157 }); 158 159 // cleanup previous service worker version caches 160 // dont block - prevents page loads 161 // www.youtube.com/watch?v=k1eoekN3nkA 162 self.addEventListener('activate', (evt) => { 163 console.log(`sw-${VS} - activate - start ${tmSince()}ms`); 164 165 const fc1 = async () => { 166 // developers.google.com/web/updates/2017/02/navigation-preload 167 if ('navigationPreload' in self.registration) { 168 await self.registration.navigationPreload.enable(); 169 } 170 }; 171 172 // No way for cache TTL: stackoverflow.com/questions/55729284 173 const fc2 = async () => { 174 const keys = await caches.keys(); 175 return await Promise.all( 176 keys 177 .filter( key => key !== CACHE_KEY ) // return true to remove this cache 178 .map( key => caches.delete(key) ) 179 ); 180 }; 181 182 evt.waitUntil( fc1() ); 183 evt.waitUntil( fc2() ); 184 185 // instantly taking control over page 186 self.clients.claim(); 187 188 console.log(`sw-${VS} - activate - stop ${tmSince()}ms`); 189 }); 190 191 self.addEventListener('fetch', (evt) => { 192 193 tmReset(); 194 195 196 // respond documents from net 197 // caching 198 // falling back to cache 199 // falling back offline 200 const fcDoc = async () => { 201 202 if (1>2) { 203 const evtr = evt.request; 204 console.log(evtr.url, evtr.method, evtr.headers, evtr.body); 205 console.log(evtr.url.hostname, evtr.url.origin, evtr.url.pathname); 206 const cch = await caches.open(CACHE_KEY); 207 const rsp = await cch.match('/pets.json'); 208 console.log(` rsp pets is ${rsp}`); 209 } 210 211 try { 212 213 // try navigation preload 214 // developers.google.com/web/updates/2017/02/navigation-preload 215 const preRsp = await evt.preloadResponse; // preload response 216 if (preRsp) { 217 if (!preRsp.ok) throw Error("preRsp status code not 200-299"); 218 console.log(`sw-${VS} - fetch - prel ${tmSince()}ms - preRsp ${preRsp.url}`); 219 if (cacheNaviResps) { 220 const cch = await caches.open(CACHE_KEY); 221 cch.put(evt.request.url, preRsp.clone()); // response is a stream - browser and cache will consume the response 222 } 223 return preRsp; 224 } 225 226 // try network 227 const netRsp = await fetch(evt.request); // network response 228 if (!netRsp.ok) throw Error("netRsp status code not 200-299"); 229 console.log(`sw-${VS} - fetch - net ${tmSince()}ms - netRsp ${netRsp.url}`); 230 if (cacheNaviResps) { 231 const cch = await caches.open(CACHE_KEY); 232 // cch.add(netRsp); 233 cch.put(evt.request.url, netRsp.clone()); 234 } 235 return netRsp; 236 237 } catch (err) { 238 // on network errors 239 // not on resp codes 4xx or 5xx 240 // codes 4xx or 5xx jump here via if (!rsp.ok) throw... 241 console.error(`sw-${VS} - fetch - error ${tmSince()}ms - ${err}`); 242 243 const cch = await caches.open(CACHE_KEY); 244 const rsp = await cch.match(evt.request, matchOpts); 245 if (rsp) { 246 console.log(`sw-${VS} - fetch - cache ${tmSince()}ms - cachedResp ${rsp.url}`); 247 return rsp; 248 } else { 249 if (1>2) { 250 const anotherRsp = new Response('<p>Neither network nor cache available</p>', { headers: { 'Content-Type': 'text/html' } }); 251 return anotherRsp; 252 } 253 return caches.match('/offline.html'); 254 255 } 256 } 257 }; 258 259 // revalidate 260 const fcReval = async () => { 261 try { 262 if (!navigator.onLine) { 263 return; 264 } 265 const cch = await caches.open(CACHE_KEY); 266 const rsp = await fetch(evt.request); 267 cch.put(evt.request.url, rsp); // no cloning necessary for revalidation 268 console.log(` static rvl - ${chopHost(evt.request.url)} - ${tmSince()}ms`); 269 } catch (err) { 270 console.error(`sw-${VS} - reval fetch - error ${tmSince()}ms - ${err} - ${chopHost(evt.request.url)}`); 271 } 272 } 273 274 275 // serve from cache - and revalidate asynchroneously 276 // or serve from net and put into synchroneously 277 // so called "Stale-while-revalidate" - web.dev/offline-cookbook/#stale-while-revalidate 278 // 279 // to see the revalidated response within the same request, we need to call this from the html page 280 const fcSttc = async () => { 281 try { 282 283 // 284 const rspCch = await caches.match(evt.request); 285 if (rspCch) { 286 // Promise.resolve().then( fcReval() ); // rewritten on the next two lines 287 const dummy = await Promise.resolve(); 288 fcReval(); 289 console.log(` static cch - ${chopHost(evt.request.url)} - ${tmSince()}ms`); 290 return rspCch; 291 } 292 293 // this results in chained promises fetch => cache open => cache put => return fetch 294 // we could async the cache open, cache put ops, but it does not save much 295 const rspNet = await fetch(evt.request); 296 const cch = await caches.open(CACHE_KEY); 297 cch.put(evt.request.url, rspNet.clone()); // response is a stream - browser and cache will consume the response 298 console.log(` static net - ${chopHost(evt.request.url)} - ${tmSince()}ms`); 299 return rspNet; 300 301 302 } catch (err) { 303 console.error(`sw-${VS} - fetch static - error ${tmSince()}ms - ${err} - ${chopHost(evt.request.url)}`); 304 } 305 306 }; 307 308 309 310 // medium.com/dev-channel/service-worker-caching-strategies-based-on-request-types-57411dd7652c 311 const dest = evt.request.destination; 312 313 314 if (evt.request.mode === 'navigate') { // only HTML pages 315 // console.log(`sw-${VS} - fetch - navi start ${tmSince()}ms - ${chopHost(evt.request.url)}`); 316 evt.respondWith( fcDoc() ); 317 console.log(`sw-${VS} - fetch - navi stop ${tmSince()}ms - ${chopHost(evt.request.url)}`); 318 } else if ( STATIC_TYPES[dest] ) { 319 evt.respondWith( fcSttc() ); 320 console.log(`sw-${VS} - fetch - sttc stop - dest ${dest} - ${chopHost(evt.request.url)} - mode ${evt.request.mode}`); 321 } else { 322 console.log(`sw-${VS} - fetch - unhandled - dest ${dest} - ${chopHost(evt.request.url)} - mode ${evt.request.mode}`); 323 } 324 325 // ...default browser fetch behaviour without service worker involvement 326 327 328 }); 329 330 331 importScripts(`/js/${VS}/idb.js`); 332 importScripts(`/js/${VS}/db.js`); 333 334 // not triggered by request.mode navigate 335 // https://davidwalsh.name/background-sync 336 self.addEventListener('sync', (evt) => { 337 338 tmReset(); 339 340 console.log(`sw-${VS} - sync tag ${evt.tag} - start `); 341 // console.log(evt); 342 343 344 const pref = "tag-sync-"; 345 const cmp = evt.tag.substring(0, pref.length); 346 if (cmp === pref) { 347 348 console.log(`sw-${VS} - sync tag ${evt.tag} - before waitUntil `); 349 350 evt.waitUntil( async () => { 351 console.log(`sw-${VS} - sync tag ${evt.tag} - after waitUntil `); 352 const cch = await caches.open(CACHE_KEY); 353 cch.add(new Request('/home-sync.html', reqOpts)); 354 }); 355 356 } 357 358 console.log(`sw-${VS} - sync tag ${evt.tag} - stop `); 359 360 361 }); 362 363 364 const CACHE_KEY = `static-resources-${VS}`;