forked from mystiq/hydrogen-web
halt any fetch request while waiting for new service worker to activate
this make updates apply instantly rather than sometimes being stalled for seconds or minutes.
This commit is contained in:
parent
25cf72a9b6
commit
5d71b655ad
4 changed files with 64 additions and 30 deletions
|
@ -107,7 +107,7 @@ export class Platform {
|
|||
this.sessionInfoStorage = new SessionInfoStorage("hydrogen_sessions_v1");
|
||||
this.estimateStorageUsage = estimateStorageUsage;
|
||||
if (typeof fetch === "function") {
|
||||
this.request = createFetchRequest(this.clock.createTimeout);
|
||||
this.request = createFetchRequest(this.clock.createTimeout, this._serviceWorkerHandler);
|
||||
} else {
|
||||
this.request = xhrRequest;
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ export class ServiceWorkerHandler {
|
|||
this._registration = null;
|
||||
this._registrationPromise = null;
|
||||
this._currentController = null;
|
||||
this.haltRequests = false;
|
||||
}
|
||||
|
||||
setNavigation(navigation) {
|
||||
|
@ -39,10 +40,12 @@ export class ServiceWorkerHandler {
|
|||
this._registration = await navigator.serviceWorker.register(path);
|
||||
await navigator.serviceWorker.ready;
|
||||
this._currentController = navigator.serviceWorker.controller;
|
||||
this._registrationPromise = null;
|
||||
console.log("Service Worker registered");
|
||||
this._registration.addEventListener("updatefound", this);
|
||||
this._tryActivateUpdate();
|
||||
this._registrationPromise = null;
|
||||
if (this._registration.waiting) {
|
||||
this._proposeUpdate();
|
||||
}
|
||||
console.log("Service Worker registered");
|
||||
})();
|
||||
}
|
||||
|
||||
|
@ -61,6 +64,10 @@ export class ServiceWorkerHandler {
|
|||
this._closeSessionIfNeeded(sessionId).finally(() => {
|
||||
event.source.postMessage({replyTo: data.id});
|
||||
});
|
||||
} else if (data.type === "haltRequests") {
|
||||
// this flag is read in fetch.js
|
||||
this.haltRequests = true;
|
||||
event.source.postMessage({replyTo: data.id});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -82,15 +89,19 @@ export class ServiceWorkerHandler {
|
|||
}
|
||||
}
|
||||
|
||||
async _tryActivateUpdate() {
|
||||
// we don't do confirm when the tab is hidden because it will block the event loop and prevent
|
||||
// events from the service worker to be processed (like controllerchange when the visible tab applies the update).
|
||||
if (!document.hidden && this._registration.waiting && this._registration.active) {
|
||||
this._registration.waiting.removeEventListener("statechange", this);
|
||||
const version = await this._sendAndWaitForReply("version", null, this._registration.waiting);
|
||||
if (confirm(`Version ${version.version} (${version.buildHash}) is ready to install. Apply now?`)) {
|
||||
this._registration.waiting.postMessage({type: "skipWaiting"}); // will trigger controllerchange event
|
||||
}
|
||||
async _proposeUpdate() {
|
||||
if (document.hidden) {
|
||||
return;
|
||||
}
|
||||
const version = await this._sendAndWaitForReply("version");
|
||||
if (confirm(`Version ${version.version} (${version.buildHash}) is available. Reload to apply?`)) {
|
||||
// prevent any fetch requests from going to the service worker
|
||||
// from any client, so that it is not kept active
|
||||
// when calling skipWaiting on the new one
|
||||
await this._sendAndWaitForReply("haltRequests");
|
||||
// only once all requests are blocked, ask the new
|
||||
// service worker to skipWaiting
|
||||
this._send("skipWaiting", null, this._registration.waiting);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -101,11 +112,14 @@ export class ServiceWorkerHandler {
|
|||
break;
|
||||
case "updatefound":
|
||||
this._registration.installing.addEventListener("statechange", this);
|
||||
this._tryActivateUpdate();
|
||||
break;
|
||||
case "statechange":
|
||||
this._tryActivateUpdate();
|
||||
case "statechange": {
|
||||
if (event.target.state === "installed") {
|
||||
this._proposeUpdate();
|
||||
event.target.removeEventListener("statechange", this);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "controllerchange":
|
||||
if (!this._currentController) {
|
||||
// Clients.claim() in the SW can trigger a controllerchange event
|
||||
|
@ -115,7 +129,7 @@ export class ServiceWorkerHandler {
|
|||
} else {
|
||||
// active service worker changed,
|
||||
// refresh, so we can get all assets
|
||||
// (and not some if we would not refresh)
|
||||
// (and not only some if we would not refresh)
|
||||
// up to date from it
|
||||
document.location.reload();
|
||||
}
|
||||
|
|
|
@ -51,8 +51,15 @@ class RequestResult {
|
|||
}
|
||||
}
|
||||
|
||||
export function createFetchRequest(createTimeout) {
|
||||
export function createFetchRequest(createTimeout, serviceWorkerHandler) {
|
||||
return function fetchRequest(url, requestOptions) {
|
||||
if (serviceWorkerHandler?.haltRequests) {
|
||||
// prevent any requests while waiting
|
||||
// for the new service worker to get activated.
|
||||
// Once this happens, the page will be reloaded
|
||||
// by the serviceWorkerHandler so this is fine.
|
||||
return new RequestResult(new Promise(() => {}), {});
|
||||
}
|
||||
// fetch doesn't do upload progress yet, delegate to xhr
|
||||
if (requestOptions?.uploadProgress) {
|
||||
return xhrRequest(url, requestOptions);
|
||||
|
|
|
@ -37,6 +37,13 @@ self.addEventListener('install', function(e) {
|
|||
})());
|
||||
});
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
// on a first page load/sw install,
|
||||
// start using the service worker on all pages straight away
|
||||
self.clients.claim();
|
||||
event.waitUntil(purgeOldCaches());
|
||||
});
|
||||
|
||||
async function purgeOldCaches() {
|
||||
// remove any caches we don't know about
|
||||
const keyList = await caches.keys();
|
||||
|
@ -60,15 +67,6 @@ async function purgeOldCaches() {
|
|||
}
|
||||
}
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(Promise.all([
|
||||
purgeOldCaches(),
|
||||
// on a first page load/sw install,
|
||||
// start using the service worker on all pages straight away
|
||||
self.clients.claim()
|
||||
]));
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', (event) => {
|
||||
event.respondWith(handleRequest(event.request));
|
||||
});
|
||||
|
@ -85,9 +83,11 @@ function isCacheableThumbnail(url) {
|
|||
}
|
||||
|
||||
const baseURL = new URL(self.registration.scope);
|
||||
let pendingFetchAbortController = new AbortController();
|
||||
async function handleRequest(request) {
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
// rewrite / to /index.html so it hits the cache
|
||||
if (url.origin === baseURL.origin && url.pathname === baseURL.pathname) {
|
||||
request = new Request(new URL("index.html", baseURL.href));
|
||||
}
|
||||
|
@ -96,15 +96,15 @@ async function handleRequest(request) {
|
|||
// use cors so the resource in the cache isn't opaque and uses up to 7mb
|
||||
// https://developers.google.com/web/tools/chrome-devtools/progressive-web-apps?utm_source=devtools#opaque-responses
|
||||
if (isCacheableThumbnail(url)) {
|
||||
response = await fetch(request, {mode: "cors", credentials: "omit"});
|
||||
response = await fetch(request, {signal: pendingFetchAbortController.signal, mode: "cors", credentials: "omit"});
|
||||
} else {
|
||||
response = await fetch(request);
|
||||
response = await fetch(request, {signal: pendingFetchAbortController.signal});
|
||||
}
|
||||
await updateCache(request, response);
|
||||
}
|
||||
return response;
|
||||
} catch (err) {
|
||||
if (!(err instanceof TypeError)) {
|
||||
if (err.name !== "TypeError" && err.name !== "AbortError") {
|
||||
console.error("error in service worker", err);
|
||||
}
|
||||
throw err;
|
||||
|
@ -172,6 +172,9 @@ self.addEventListener('message', (event) => {
|
|||
case "skipWaiting":
|
||||
self.skipWaiting();
|
||||
break;
|
||||
case "haltRequests":
|
||||
event.waitUntil(haltRequests().then(() => reply()));
|
||||
break;
|
||||
case "closeSession":
|
||||
event.waitUntil(
|
||||
closeSession(event.data.payload.sessionId, event.source.id)
|
||||
|
@ -192,6 +195,16 @@ async function closeSession(sessionId, requestingClientId) {
|
|||
}));
|
||||
}
|
||||
|
||||
async function haltRequests() {
|
||||
// first ask all clients to block sending any more requests
|
||||
const clients = await self.clients.matchAll({type: "window"});
|
||||
await Promise.all(clients.map(client => {
|
||||
return sendAndWaitForReply(client, "haltRequests");
|
||||
}));
|
||||
// and only then abort the current requests
|
||||
pendingFetchAbortController.abort();
|
||||
}
|
||||
|
||||
const pendingReplies = new Map();
|
||||
let messageIdCounter = 0;
|
||||
function sendAndWaitForReply(client, type, payload) {
|
||||
|
|
Loading…
Reference in a new issue