mirror of
https://github.com/vector-im/hydrogen-web.git
synced 2024-12-23 11:35:04 +01:00
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
@ -107,7 +107,7 @@ export class Platform {
|
|||||||
this.sessionInfoStorage = new SessionInfoStorage("hydrogen_sessions_v1");
|
this.sessionInfoStorage = new SessionInfoStorage("hydrogen_sessions_v1");
|
||||||
this.estimateStorageUsage = estimateStorageUsage;
|
this.estimateStorageUsage = estimateStorageUsage;
|
||||||
if (typeof fetch === "function") {
|
if (typeof fetch === "function") {
|
||||||
this.request = createFetchRequest(this.clock.createTimeout);
|
this.request = createFetchRequest(this.clock.createTimeout, this._serviceWorkerHandler);
|
||||||
} else {
|
} else {
|
||||||
this.request = xhrRequest;
|
this.request = xhrRequest;
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,7 @@ export class ServiceWorkerHandler {
|
|||||||
this._registration = null;
|
this._registration = null;
|
||||||
this._registrationPromise = null;
|
this._registrationPromise = null;
|
||||||
this._currentController = null;
|
this._currentController = null;
|
||||||
|
this.haltRequests = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
setNavigation(navigation) {
|
setNavigation(navigation) {
|
||||||
@ -39,10 +40,12 @@ export class ServiceWorkerHandler {
|
|||||||
this._registration = await navigator.serviceWorker.register(path);
|
this._registration = await navigator.serviceWorker.register(path);
|
||||||
await navigator.serviceWorker.ready;
|
await navigator.serviceWorker.ready;
|
||||||
this._currentController = navigator.serviceWorker.controller;
|
this._currentController = navigator.serviceWorker.controller;
|
||||||
this._registrationPromise = null;
|
|
||||||
console.log("Service Worker registered");
|
|
||||||
this._registration.addEventListener("updatefound", this);
|
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(() => {
|
this._closeSessionIfNeeded(sessionId).finally(() => {
|
||||||
event.source.postMessage({replyTo: data.id});
|
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() {
|
async _proposeUpdate() {
|
||||||
// we don't do confirm when the tab is hidden because it will block the event loop and prevent
|
if (document.hidden) {
|
||||||
// events from the service worker to be processed (like controllerchange when the visible tab applies the update).
|
return;
|
||||||
if (!document.hidden && this._registration.waiting && this._registration.active) {
|
}
|
||||||
this._registration.waiting.removeEventListener("statechange", this);
|
const version = await this._sendAndWaitForReply("version");
|
||||||
const version = await this._sendAndWaitForReply("version", null, this._registration.waiting);
|
if (confirm(`Version ${version.version} (${version.buildHash}) is available. Reload to apply?`)) {
|
||||||
if (confirm(`Version ${version.version} (${version.buildHash}) is ready to install. Apply now?`)) {
|
// prevent any fetch requests from going to the service worker
|
||||||
this._registration.waiting.postMessage({type: "skipWaiting"}); // will trigger controllerchange event
|
// 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;
|
break;
|
||||||
case "updatefound":
|
case "updatefound":
|
||||||
this._registration.installing.addEventListener("statechange", this);
|
this._registration.installing.addEventListener("statechange", this);
|
||||||
this._tryActivateUpdate();
|
|
||||||
break;
|
break;
|
||||||
case "statechange":
|
case "statechange": {
|
||||||
this._tryActivateUpdate();
|
if (event.target.state === "installed") {
|
||||||
|
this._proposeUpdate();
|
||||||
|
event.target.removeEventListener("statechange", this);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case "controllerchange":
|
case "controllerchange":
|
||||||
if (!this._currentController) {
|
if (!this._currentController) {
|
||||||
// Clients.claim() in the SW can trigger a controllerchange event
|
// Clients.claim() in the SW can trigger a controllerchange event
|
||||||
@ -115,7 +129,7 @@ export class ServiceWorkerHandler {
|
|||||||
} else {
|
} else {
|
||||||
// active service worker changed,
|
// active service worker changed,
|
||||||
// refresh, so we can get all assets
|
// 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
|
// up to date from it
|
||||||
document.location.reload();
|
document.location.reload();
|
||||||
}
|
}
|
||||||
|
@ -51,8 +51,15 @@ class RequestResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createFetchRequest(createTimeout) {
|
export function createFetchRequest(createTimeout, serviceWorkerHandler) {
|
||||||
return function fetchRequest(url, requestOptions) {
|
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
|
// fetch doesn't do upload progress yet, delegate to xhr
|
||||||
if (requestOptions?.uploadProgress) {
|
if (requestOptions?.uploadProgress) {
|
||||||
return xhrRequest(url, requestOptions);
|
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() {
|
async function purgeOldCaches() {
|
||||||
// remove any caches we don't know about
|
// remove any caches we don't know about
|
||||||
const keyList = await caches.keys();
|
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) => {
|
self.addEventListener('fetch', (event) => {
|
||||||
event.respondWith(handleRequest(event.request));
|
event.respondWith(handleRequest(event.request));
|
||||||
});
|
});
|
||||||
@ -85,9 +83,11 @@ function isCacheableThumbnail(url) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const baseURL = new URL(self.registration.scope);
|
const baseURL = new URL(self.registration.scope);
|
||||||
|
let pendingFetchAbortController = new AbortController();
|
||||||
async function handleRequest(request) {
|
async function handleRequest(request) {
|
||||||
try {
|
try {
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
|
// rewrite / to /index.html so it hits the cache
|
||||||
if (url.origin === baseURL.origin && url.pathname === baseURL.pathname) {
|
if (url.origin === baseURL.origin && url.pathname === baseURL.pathname) {
|
||||||
request = new Request(new URL("index.html", baseURL.href));
|
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
|
// 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
|
// https://developers.google.com/web/tools/chrome-devtools/progressive-web-apps?utm_source=devtools#opaque-responses
|
||||||
if (isCacheableThumbnail(url)) {
|
if (isCacheableThumbnail(url)) {
|
||||||
response = await fetch(request, {mode: "cors", credentials: "omit"});
|
response = await fetch(request, {signal: pendingFetchAbortController.signal, mode: "cors", credentials: "omit"});
|
||||||
} else {
|
} else {
|
||||||
response = await fetch(request);
|
response = await fetch(request, {signal: pendingFetchAbortController.signal});
|
||||||
}
|
}
|
||||||
await updateCache(request, response);
|
await updateCache(request, response);
|
||||||
}
|
}
|
||||||
return response;
|
return response;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!(err instanceof TypeError)) {
|
if (err.name !== "TypeError" && err.name !== "AbortError") {
|
||||||
console.error("error in service worker", err);
|
console.error("error in service worker", err);
|
||||||
}
|
}
|
||||||
throw err;
|
throw err;
|
||||||
@ -172,6 +172,9 @@ self.addEventListener('message', (event) => {
|
|||||||
case "skipWaiting":
|
case "skipWaiting":
|
||||||
self.skipWaiting();
|
self.skipWaiting();
|
||||||
break;
|
break;
|
||||||
|
case "haltRequests":
|
||||||
|
event.waitUntil(haltRequests().then(() => reply()));
|
||||||
|
break;
|
||||||
case "closeSession":
|
case "closeSession":
|
||||||
event.waitUntil(
|
event.waitUntil(
|
||||||
closeSession(event.data.payload.sessionId, event.source.id)
|
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();
|
const pendingReplies = new Map();
|
||||||
let messageIdCounter = 0;
|
let messageIdCounter = 0;
|
||||||
function sendAndWaitForReply(client, type, payload) {
|
function sendAndWaitForReply(client, type, payload) {
|
||||||
|
Loading…
Reference in New Issue
Block a user