move service worker code in bundle, and support closing sessions

This commit is contained in:
Bruno Windels 2020-10-16 12:49:42 +02:00
parent 788bce7904
commit 101c7015f2
4 changed files with 161 additions and 46 deletions

View file

@ -29,46 +29,5 @@
} }
}); });
</script> </script>
<script id="service-worker" type="module">
function workerMessage(worker, type) {
const body = {type, id: Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)};
let resolve;
const promise = new Promise(r => resolve = r);
const onMessage = function(event) {
if (event.data.replyTo === body.id) {
navigator.serviceWorker.removeEventListener("message", onMessage);
resolve(event.data.content);
}
}
navigator.serviceWorker.addEventListener("message", onMessage);
worker.postMessage(body);
return promise;
}
if('serviceWorker' in navigator) {
navigator.serviceWorker.register('sw.js').then(function(registration) {
console.log("Service Worker registered");
async function tryActivateUpdate() {
if (registration.waiting && registration.active) {
const version = await workerMessage(registration.waiting, "version");
if (confirm(`Version ${version.version} (${version.buildHash}) is ready to install. Apply now?`)) {
registration.waiting.postMessage({type: "skipWaiting"}); // will trigger controllerchange event
}
}
}
tryActivateUpdate();
registration.onupdatefound = function() {
const newWorker = registration.installing;
newWorker.onstatechange = function() {
tryActivateUpdate();
}
};
});
navigator.serviceWorker.addEventListener("controllerchange", function() {
document.location.reload();
});
}
</script>
</body> </body>
</html> </html>

View file

@ -140,6 +140,7 @@ async function buildHtml(doc, version, globalHash, modernOnly, assets) {
}); });
const pathsJSON = JSON.stringify({ const pathsJSON = JSON.stringify({
worker: assets.has("worker.js") ? assets.resolve(`worker.js`) : null, worker: assets.has("worker.js") ? assets.resolve(`worker.js`) : null,
serviceWorker: "sw.js",
olm: { olm: {
wasm: assets.resolve("olm.wasm"), wasm: assets.resolve("olm.wasm"),
legacyBundle: assets.resolve("olm_legacy.js"), legacyBundle: assets.resolve("olm_legacy.js"),
@ -156,7 +157,6 @@ async function buildHtml(doc, version, globalHash, modernOnly, assets) {
); );
} }
doc("script#main").replaceWith(mainScripts.join("")); doc("script#main").replaceWith(mainScripts.join(""));
doc("script#service-worker").attr("type", "text/javascript");
const versionScript = doc("script#version"); const versionScript = doc("script#version");
versionScript.attr("type", "text/javascript"); versionScript.attr("type", "text/javascript");

View file

@ -25,6 +25,7 @@ import {RootViewModel} from "./domain/RootViewModel.js";
import {createNavigation, createRouter} from "./domain/navigation/index.js"; import {createNavigation, createRouter} from "./domain/navigation/index.js";
import {RootView} from "./ui/web/RootView.js"; import {RootView} from "./ui/web/RootView.js";
import {Clock} from "./ui/web/dom/Clock.js"; import {Clock} from "./ui/web/dom/Clock.js";
import {ServiceWorkerHandler} from "./ui/web/dom/ServiceWorkerHandler.js";
import {History} from "./ui/web/dom/History.js"; import {History} from "./ui/web/dom/History.js";
import {OnlineStatus} from "./ui/web/dom/OnlineStatus.js"; import {OnlineStatus} from "./ui/web/dom/OnlineStatus.js";
import {CryptoDriver} from "./ui/web/dom/CryptoDriver.js"; import {CryptoDriver} from "./ui/web/dom/CryptoDriver.js";
@ -106,8 +107,14 @@ export async function main(container, paths, legacyExtras) {
} else { } else {
request = xhrRequest; request = xhrRequest;
} }
const navigation = createNavigation();
const sessionInfoStorage = new SessionInfoStorage("hydrogen_sessions_v1"); const sessionInfoStorage = new SessionInfoStorage("hydrogen_sessions_v1");
const storageFactory = new StorageFactory(); let serviceWorkerHandler;
if (paths.serviceWorker && "serviceWorker" in navigator) {
serviceWorkerHandler = new ServiceWorkerHandler({navigation});
serviceWorkerHandler.registerAndStart(paths.serviceWorker);
}
const storageFactory = new StorageFactory(serviceWorkerHandler);
const olmPromise = loadOlm(paths.olm); const olmPromise = loadOlm(paths.olm);
// if wasm is not supported, we'll want // if wasm is not supported, we'll want
@ -116,8 +123,6 @@ export async function main(container, paths, legacyExtras) {
if (!window.WebAssembly) { if (!window.WebAssembly) {
workerPromise = loadOlmWorker(paths); workerPromise = loadOlmWorker(paths);
} }
const navigation = createNavigation();
const urlRouter = createRouter({navigation, history: new History()}); const urlRouter = createRouter({navigation, history: new History()});
urlRouter.attach(); urlRouter.attach();
@ -139,7 +144,8 @@ export async function main(container, paths, legacyExtras) {
storageFactory, storageFactory,
clock, clock,
urlRouter, urlRouter,
navigation navigation,
updateService: serviceWorkerHandler
}); });
window.__brawlViewModel = vm; window.__brawlViewModel = vm;
await vm.load(); await vm.load();

View file

@ -0,0 +1,150 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// 3 (imaginary) interfaces are implemented here:
// - OfflineAvailability (done by registering the sw)
// - UpdateService (see checkForUpdate method, and should also emit events rather than showing confirm dialog here)
// - ConcurrentAccessBlocker (see preventConcurrentSessionAccess method)
export class ServiceWorkerHandler {
constructor({navigation}) {
this._waitingForReply = new Map();
this._messageIdCounter = 0;
this._registration = null;
this._navigation = navigation;
this._registrationPromise = null;
}
registerAndStart(path) {
this._registrationPromise = (async () => {
navigator.serviceWorker.addEventListener("message", this);
navigator.serviceWorker.addEventListener("controllerchange", this);
this._registration = await navigator.serviceWorker.register(path);
this._registrationPromise = null;
console.log("Service Worker registered");
this._registration.addEventListener("updatefound", this);
this._tryActivateUpdate();
})();
}
_onMessage(event) {
const {data} = event;
const replyTo = data.replyTo;
if (replyTo) {
const resolve = this._waitingForReply.get(replyTo);
if (resolve) {
this._waitingForReply.delete(replyTo);
resolve(data.payload);
}
}
if (data.type === "closeSession") {
const {sessionId} = data.payload;
this._closeSessionIfNeeded(sessionId).finally(() => {
event.source.postMessage({replyTo: data.id});
});
}
}
_closeSessionIfNeeded(sessionId) {
const currentSession = this._navigation.path.get("session");
if (sessionId && currentSession?.value === sessionId) {
return new Promise(resolve => {
const unsubscribe = this._navigation.pathObservable.subscribe(path => {
const session = path.get("session");
if (!session || session.value !== sessionId) {
unsubscribe();
resolve();
}
});
this._navigation.push("session");
});
} else {
return Promise.resolve();
}
}
async _tryActivateUpdate() {
if (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
}
}
}
handleEvent(event) {
switch (event.type) {
case "message":
this._onMessage(event);
break;
case "updatefound":
this._registration.installing.addEventListener("statechange", this);
this._tryActivateUpdate();
break;
case "statechange":
this._tryActivateUpdate();
break;
case "controllerchange":
// active service worker changed,
// refresh, so we can get all assets
// (and not some if we would not refresh)
// up to date from it
document.location.reload();
break;
}
}
async _send(type, payload, worker = undefined) {
if (this._registrationPromise) {
await this._registrationPromise;
}
if (!worker) {
worker = this._registration.active;
}
worker.postMessage({type, payload});
}
async _sendAndWaitForReply(type, payload, worker = undefined) {
if (this._registrationPromise) {
await this._registrationPromise;
}
if (!worker) {
worker = this._registration.active;
}
this._messageIdCounter += 1;
const id = this._messageIdCounter;
const promise = new Promise(resolve => {
this._waitingForReply.set(id, resolve);
});
worker.postMessage({type, id, payload});
return await promise;
}
async checkForUpdate() {
if (this._registrationPromise) {
await this._registrationPromise;
}
this._registration.update();
}
async preventConcurrentSessionAccess(sessionId) {
// don't block if we didn't manage to install service worker
if (!this._registration) {
return Promise.resolve();
}
return this._sendAndWaitForReply("closeSession", {sessionId});
}
}