mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-02-23 19:49:56 +01:00
web: use service worker for cross-tab communication only as a fallback
This commit basically reverts 1b63b2dccf.
Using service worker for this by default is a bad idea because:
1. It takes some time for the service worker to be installed and ready
which slows down first launch significantly
2. Service worker is not as reliable when updating from version to
version
We can, however, use it as a fallback i.e for devices that don't yet
support shared worker.
This commit is contained in:
@@ -17,8 +17,15 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import SharedWorker from "./shared-service.worker.ts?sharedworker";
|
||||
import { Mutex } from "async-mutex";
|
||||
|
||||
const sharedWorker = globalThis.SharedWorker
|
||||
? new SharedWorker({
|
||||
name: "SharedService"
|
||||
})
|
||||
: null;
|
||||
|
||||
export class SharedService<T extends object> extends EventTarget {
|
||||
#clientId: Promise<string>;
|
||||
|
||||
@@ -152,7 +159,7 @@ export class SharedService<T extends object> extends EventTarget {
|
||||
}
|
||||
|
||||
try {
|
||||
thisArg.#sendPortToClient(data, requestedPort);
|
||||
await thisArg.#sendPortToClient(data, requestedPort);
|
||||
} catch (e) {
|
||||
console.error(e, providerId, data);
|
||||
// retry if port has been neutered, this can happen when
|
||||
@@ -202,47 +209,69 @@ export class SharedService<T extends object> extends EventTarget {
|
||||
}
|
||||
|
||||
async #sendPortToClient(message: any, port: MessagePort) {
|
||||
// if (!sharedWorker)
|
||||
// throw new Error("Shared worker is not supported in this environment.");
|
||||
// sharedWorker.port.postMessage(message, [port]);
|
||||
|
||||
// Return the port to the client via the service worker.
|
||||
const serviceWorker = await navigator.serviceWorker.ready;
|
||||
serviceWorker.active?.postMessage(message, [port]);
|
||||
if (!sharedWorker) {
|
||||
// Return the port to the client via the service worker.
|
||||
const serviceWorker = await navigator.serviceWorker.ready;
|
||||
serviceWorker.active?.postMessage(message, [port]);
|
||||
} else sharedWorker.port.postMessage(message, [port]);
|
||||
}
|
||||
|
||||
async #getClientId() {
|
||||
// Getting the clientId from the service worker accomplishes two things:
|
||||
// 1. It gets the clientId for this context.
|
||||
// 2. It ensures that the service worker is activated.
|
||||
//
|
||||
// It is possible to do this without polling but it requires about the
|
||||
// same amount of code and using fetch makes 100% certain the service
|
||||
// worker is handling requests.
|
||||
let clientId: string | undefined;
|
||||
while (!clientId) {
|
||||
clientId = await fetch("/clientId").then((response) => {
|
||||
if (response.ok && !!response.headers.get("x-client-id")) {
|
||||
return response.text();
|
||||
}
|
||||
console.warn("service worker not ready, retrying...");
|
||||
return new Promise((resolve) => setTimeout(resolve, 100));
|
||||
if (sharedWorker) {
|
||||
console.time("getting client id");
|
||||
// Use a Web Lock to determine our clientId.
|
||||
const nonce = Math.random().toString();
|
||||
const clientId = await navigator.locks.request(nonce, async () => {
|
||||
console.log("got clientid lock");
|
||||
const { held } = await navigator.locks.query();
|
||||
return held?.find((lock) => lock.name === nonce)?.clientId;
|
||||
});
|
||||
|
||||
// Acquire a Web Lock named after the clientId. This lets other contexts
|
||||
// track this context's lifetime.
|
||||
// TODO: It would be better to lock on the clientId+serviceName (passing
|
||||
// that lock name in the service request). That would allow independent
|
||||
// instance lifetime tracking.
|
||||
await SharedService.#acquireContextLock(clientId);
|
||||
|
||||
// Configure message forwarding via the SharedWorker. This must be
|
||||
// done after acquiring the clientId lock to avoid a race condition
|
||||
// in the SharedWorker.
|
||||
sharedWorker?.port.addEventListener("message", (event) => {
|
||||
event.data.ports = event.ports;
|
||||
this.dispatchEvent(new MessageEvent("message", { data: event.data }));
|
||||
});
|
||||
sharedWorker?.port.start();
|
||||
sharedWorker?.port.postMessage({ clientId });
|
||||
|
||||
console.timeEnd("getting client id");
|
||||
return clientId;
|
||||
} else {
|
||||
// Getting the clientId from the service worker accomplishes two things:
|
||||
// 1. It gets the clientId for this context.
|
||||
// 2. It ensures that the service worker is activated.
|
||||
//
|
||||
// It is possible to do this without polling but it requires about the
|
||||
// same amount of code and using fetch makes 100% certain the service
|
||||
// worker is handling requests.
|
||||
let clientId: string | undefined;
|
||||
while (!clientId) {
|
||||
clientId = await fetch("/clientId").then((response) => {
|
||||
if (response.ok && !!response.headers.get("x-client-id")) {
|
||||
return response.text();
|
||||
}
|
||||
console.warn("service worker not ready, retrying...");
|
||||
return new Promise((resolve) => setTimeout(resolve, 100));
|
||||
});
|
||||
}
|
||||
|
||||
navigator.serviceWorker.addEventListener("message", (event) => {
|
||||
event.data.ports = event.ports;
|
||||
this.dispatchEvent(new MessageEvent("message", { data: event.data }));
|
||||
});
|
||||
|
||||
return clientId;
|
||||
}
|
||||
|
||||
navigator.serviceWorker.addEventListener("message", (event) => {
|
||||
event.data.ports = event.ports;
|
||||
this.dispatchEvent(new MessageEvent("message", { data: event.data }));
|
||||
});
|
||||
|
||||
// Acquire a Web Lock named after the clientId. This lets other contexts
|
||||
// track this context's lifetime.
|
||||
// TODO: It would be better to lock on the clientId+serviceName (passing
|
||||
// that lock name in the service request). That would allow independent
|
||||
// instance lifetime tracking.
|
||||
await SharedService.#acquireContextLock(clientId);
|
||||
|
||||
return clientId;
|
||||
}
|
||||
|
||||
async #providerChange() {
|
||||
|
||||
55
apps/web/src/common/sqlite/shared-service.worker.ts
Normal file
55
apps/web/src/common/sqlite/shared-service.worker.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
/* eslint-disable no-var */
|
||||
|
||||
/// <reference lib="webworker" />
|
||||
|
||||
export default null;
|
||||
|
||||
declare var self: SharedWorkerGlobalScope & typeof globalThis;
|
||||
const mapClientIdToPort: Map<string, MessagePort> = new Map();
|
||||
|
||||
self.addEventListener("connect", (event) => {
|
||||
console.log("connected", event);
|
||||
// The first message from a client associates the clientId with the port.
|
||||
const workerPort = event.ports[0];
|
||||
workerPort.addEventListener(
|
||||
"message",
|
||||
(event) => {
|
||||
console.log("received message", event.data);
|
||||
mapClientIdToPort.set(event.data.clientId, workerPort);
|
||||
|
||||
// Remove the entry when the client goes away, which we detect when
|
||||
// the lock on its name becomes available.
|
||||
navigator.locks.request(event.data.clientId, { mode: "shared" }, () => {
|
||||
mapClientIdToPort.get(event.data.clientId)?.close();
|
||||
mapClientIdToPort.delete(event.data.clientId);
|
||||
});
|
||||
|
||||
// Subsequent messages will be forwarded.
|
||||
workerPort.addEventListener("message", (event) => {
|
||||
const port = mapClientIdToPort.get(event.data.clientId);
|
||||
console.log("sending message to client", event.data.clientId, port);
|
||||
port?.postMessage(event.data, [...event.ports]);
|
||||
});
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
workerPort.start();
|
||||
});
|
||||
@@ -46,7 +46,7 @@ const isLocalhost = Boolean(
|
||||
);
|
||||
|
||||
export function register(config: ServiceWorkerRegistrationConfig) {
|
||||
if ("serviceWorker" in navigator) {
|
||||
if (import.meta.env.PROD && "serviceWorker" in navigator) {
|
||||
// The URL constructor is available in all browsers that support SW.
|
||||
const publicUrl = new URL(PUBLIC_URL, window.location.href);
|
||||
if (publicUrl.origin !== window.location.origin) {
|
||||
@@ -55,11 +55,8 @@ export function register(config: ServiceWorkerRegistrationConfig) {
|
||||
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
|
||||
return;
|
||||
}
|
||||
console.log("registering service worker");
|
||||
|
||||
const swUrl = import.meta.env.PROD
|
||||
? `${PUBLIC_URL}/service-worker.js`
|
||||
: "/dev-sw.js?dev-sw";
|
||||
const swUrl = `${PUBLIC_URL}/service-worker.js`;
|
||||
if (isLocalhost) {
|
||||
// This is running on localhost. Let's check if a service worker still exists or not.
|
||||
checkValidServiceWorker(swUrl, config);
|
||||
|
||||
@@ -147,15 +147,11 @@ export default defineConfig({
|
||||
strategies: "injectManifest",
|
||||
minify: true,
|
||||
manifest: WEB_MANIFEST,
|
||||
injectRegister: false,
|
||||
injectRegister: null,
|
||||
srcDir: "",
|
||||
filename:
|
||||
process.env.NODE_ENV === "production"
|
||||
? "service-worker.ts"
|
||||
: "service-worker.dev.ts",
|
||||
devOptions: {
|
||||
enabled: true
|
||||
},
|
||||
filename: "service-worker.ts",
|
||||
mode: "production",
|
||||
workbox: { mode: "production" },
|
||||
injectManifest: {
|
||||
globPatterns: ["**/*.{js,css,html,wasm}", "**/Inter-*.woff2"],
|
||||
globIgnores: [
|
||||
|
||||
Reference in New Issue
Block a user