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 &lt; 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}`;