forked from mystiq/hydrogen-web
move service worker code in bundle, and support closing sessions
This commit is contained in:
parent
788bce7904
commit
101c7015f2
4 changed files with 161 additions and 46 deletions
41
index.html
41
index.html
|
@ -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>
|
||||||
|
|
|
@ -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");
|
||||||
|
|
14
src/main.js
14
src/main.js
|
@ -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();
|
||||||
|
|
150
src/ui/web/dom/ServiceWorkerHandler.js
Normal file
150
src/ui/web/dom/ServiceWorkerHandler.js
Normal 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});
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue