diff --git a/sw.js b/sw.js index e0b7092..1b979c1 100755 --- a/sw.js +++ b/sw.js @@ -1,129 +1,221 @@ -const VERSION = 'v14' +const VERSION = 'v15' const API_ORIGIN = 'https://scraper.infotren.dcdev.ro/' const API_TRAINS = `${API_ORIGIN}v3/trains` const API_STATIONS = `${API_ORIGIN}v3/stations` -self.addEventListener('install', (event) => { - event.waitUntil( - caches - .open(VERSION) - .then((cache) => - cache.addAll([ - // Root - '/', +const CACHE_FIRST = [ + // Root + '/', + + // Utility JS + '/common/worker.js', + '/common/items.js', + '/common/back.js', + '/common/tabs.js', - // Utility JS - '/common/worker.js', - '/common/items.js', - '/common/back.js', - '/common/tabs.js', + // Base + '/base.css', - // Base - '/base.css', + // Pages + '/index.html', - // Pages - '/index.html', + '/about.html', - '/about.html', + '/train.html', + '/train.js', - '/train.html', - '/train.js', + '/view-train.html', + '/view-train.js', + '/view-train.css', - '/view-train.html', - '/view-train.js', - '/view-train.css', + '/station.html', + '/station.js', - '/station.html', - '/station.js', + '/view-station.html', + '/view-station.js', + '/view-station.css', - '/view-station.html', - '/view-station.js', - '/view-station.css', + // API + API_TRAINS, + API_STATIONS, +]; - // API - API_TRAINS, - API_STATIONS, +/** + * @param {string} url + * @returns {boolean} + */ +function shouldReturnFromCacheFirst(url) { + return CACHE_FIRST.map(u => u.includes('://') ? u : self.location.origin + u).includes(url.split('?')[0]) +} - ]) - ) - ) +self.addEventListener('install', (event) => { + event.waitUntil( + caches + .open(VERSION) + .then((cache) => + cache.addAll(CACHE_FIRST) + ) + ) }) const deleteCache = (/** @type {string} */ key) => caches.delete(key) const deleteOldCaches = async () => { - const cacheKeepList = [VERSION] - const keyList = await caches.keys() - const cachesToDelete = keyList.filter((key) => !cacheKeepList.includes(key)) - await Promise.all(cachesToDelete.map(deleteCache)) + const cacheKeepList = [VERSION] + const keyList = await caches.keys() + const cachesToDelete = keyList.filter((key) => !cacheKeepList.includes(key)) + await Promise.all(cachesToDelete.map(deleteCache)) } // Enable navigation preload const enableNavigationPreload = async () => { - if (self.registration.navigationPreload) { - // Enable navigation preloads! - await self.registration.navigationPreload.enable() - } + if (self.registration.navigationPreload) { + // Enable navigation preloads! + await self.registration.navigationPreload.enable() + } } self.addEventListener('activate', (event) => { - event.waitUntil(Promise.all([deleteOldCaches(), enableNavigationPreload()])) + event.waitUntil(Promise.all([deleteOldCaches(), enableNavigationPreload()])) }) +/** + * @param {RequestInfo | URL} request + * @param {Response} response + * @returns {Promise} + */ const putInCache = async (request, response) => { - const cache = await caches.open(VERSION) - await cache.put(request, response) + const cache = await caches.open(VERSION) + // try { + // response.headers.set('SW-Cached-At', new Date().toISOString()) + // } + // catch { + const headers = new Headers(response.headers) + headers.set('SW-Cached-At', new Date().toISOString()) + response = new Response(response.body, { + status: response.status, + statusText: response.statusText, + headers, + }) + // } + await cache.put(request, response) } const cacheFirst = async ({ request, preloadResponsePromise, refreshAnyway }) => { - // First try to get the resource from the cache - const responseFromCache = await caches.match(request) - if (responseFromCache) { - if (refreshAnyway) { - console.log('request in cache, refreshing anyway but returning cache', request); - ((async () => { - try { - const response = await fetch(request) - if (response.ok) { - putInCache(request, response) - } - } - catch {} - })()) - } - return responseFromCache - } - - // Next try to use (and cache) the preloaded response, if it's there - if (preloadResponsePromise) { - const preloadResponse = await preloadResponsePromise - if (preloadResponse && preloadResponse.ok) { - console.info('using preload response', preloadResponse) - putInCache(request, preloadResponse.clone()) - return preloadResponse - } - else { - console.log('got not ok preloadResponse, ignoring', preloadResponse) - } - } - - // Next try to get the resource from the network - const responseFromNetwork = await fetch(request) - // response may be used only once - // we need to save clone to put one copy in cache - // and serve second one - if (responseFromNetwork.ok) { - putInCache(request, responseFromNetwork.clone()) - } - return responseFromNetwork; + // First try to get the resource from the cache + const responseFromCache = await caches.match(request) + if (responseFromCache) { + if (refreshAnyway) { + console.log('[cf] using cache response; refreshing anyway but returning cache', responseFromCache); + ((async () => { + try { + const response = await fetch(request) + if (response.ok) { + await putInCache(request, response) + } + } + catch {} + })()) + } + else { + console.log('[cf] using cache response', responseFromCache) + } + return responseFromCache + } + + // Next try to use (and cache) the preloaded response, if it's there + if (preloadResponsePromise) { + const preloadResponse = await preloadResponsePromise + if (preloadResponse && preloadResponse.ok) { + console.info('[cf] using preload response', preloadResponse) + await putInCache(request, preloadResponse.clone()) + return preloadResponse + } + else { + console.log('[cf] got not ok preloadResponse, ignoring', preloadResponse) + } + } + + // Next try to get the resource from the network + const responseFromNetwork = await fetch(request) + // response may be used only once + // we need to save clone to put one copy in cache + // and serve second one + if (responseFromNetwork.ok) { + console.log('[cf] using network response; ok so also cache', responseFromNetwork) + await putInCache(request, responseFromNetwork.clone()) + } + else { + console.log('[cf] using network not ok response', responseFromNetwork) + } + return responseFromNetwork; +} + +const networkFirst = async ({ request, preloadResponsePromise }) => { + // First try to use (and cache) the preloaded response, if it's there + if (preloadResponsePromise) { + const preloadResponse = await preloadResponsePromise + if (preloadResponse && preloadResponse.ok) { + console.info('[nf] using preload response', preloadResponse) + await putInCache(request, preloadResponse.clone()) + return preloadResponse + } + else { + console.log('[nf] got not ok preloadResponse, ignoring', preloadResponse) + } + } + + // Next try to get the resource from the network + let responseFromNetwork + let errorFromNetwork + try { + responseFromNetwork = await fetch(request) + } + catch (e) { + responseFromNetwork = null + errorFromNetwork = e + } + // If the response is ok, put it in cache and respond + if (responseFromNetwork && responseFromNetwork.ok) { + console.log('[nf] using ok network response', responseFromNetwork) + await putInCache(request, responseFromNetwork.clone()) + return responseFromNetwork + } + + // Response from network wasn't ok, try to find in cache + const responseFromCache = await caches.match(request) + if (responseFromCache) { + console.log('[nf] using cache response', responseFromCache) + return responseFromCache + } + + // If we didn't find a cached response, return the fresh network error + if (responseFromNetwork) { + console.log('[nf] using network not ok response', responseFromNetwork) + return responseFromNetwork + } + else { + console.log('[nf] throwing network error', errorFromNetwork) + throw errorFromNetwork + } } self.addEventListener('fetch', (event) => { - event.respondWith( - cacheFirst({ - request: event.request, - preloadResponsePromise: event.preloadResponse, - refreshAnyway: [API_STATIONS, API_TRAINS].includes(event.request.url.split('?')[0]), - }) - ) + if (shouldReturnFromCacheFirst(event.request.url)) { + event.respondWith( + cacheFirst({ + request: event.request, + preloadResponsePromise: event.preloadResponse, + refreshAnyway: [API_STATIONS, API_TRAINS].includes(event.request.url.split('?')[0]), + }) + ) + } + else { + event.respondWith( + networkFirst({ + request: event.request, + preloadResponsePromise: event.preloadResponse, + }) + ) + } }) diff --git a/view-train.js b/view-train.js index ba5d01a..2c649bf 100755 --- a/view-train.js +++ b/view-train.js @@ -1,4 +1,10 @@ +/** + * @type {string} + */ var trainNumber +/** + * @type {Date} + */ var date var groupIndex = null @@ -6,6 +12,9 @@ var yesterday = false var showKm = false var trainData = null +/** + * @type {?Date} + */ var lastSuccessfulFetch = null /** @@ -55,8 +64,9 @@ var lastSuccessfulFetch = null /** * @param {{ rank: string; number: string; operator: string; date: string; groups: Group[]; }} data + * @param {?Date} fetchDate */ -function onTrainData(data) { +function onTrainData(data, fetchDate) { var title = document.getElementById('title') title.textContent = '' title.appendChild(document.createTextNode('Train ')) @@ -384,10 +394,14 @@ function onTrainData(data) { } }) - lastSuccessfulFetch = new Date() + lastSuccessfulFetch = fetchDate || new Date() } var refreshStopToken = null + +/** + * @returns {Promise} + */ function refresh() { function reschedule(timeout) { if (refreshStopToken != null) { @@ -415,14 +429,25 @@ function refresh() { reschedule(10000) return } - return response.json() + var cacheDate = response.headers.get('SW-Cached-At') + return response.json().then(function (data) { + data['$cacheDate'] = cacheDate + return data + }) }).then(function (response) { if (!response) { return } + var cacheDate = response['$cacheDate'] + if (cacheDate) { + cacheDate = new Date(cacheDate) + } + var success = !cacheDate trainData = response - onTrainData(response) - reschedule() + onTrainData(response, cacheDate) + // Check in 1 seconds if network error + reschedule(success ? undefined : 1000) + return success }).catch(function (e) { // Check in 1 second if network error reschedule(1000) @@ -437,7 +462,24 @@ window.addEventListener('unload', function (e) { }) function rsk() { - refresh() + function changeRskText(newText) { + document.querySelectorAll('.rsk').forEach(function (elem) { + elem.textContent = newText + }) + } + + changeRskText('Refreshing...') + refresh().catch(function () { return false}).then(function (success) { + if (!success) { + changeRskText('Refreshing failed') + setTimeout(function (_) { + changeRskText('Refresh') + }, 3000) + } + else { + changeRskText('Refresh') + } + }) } window.addEventListener('popstate', function (e) {