diff --git a/apps/web/public/stream-saver-sw.js b/apps/web/public/stream-saver-sw.js
deleted file mode 100644
index 79161736c..000000000
--- a/apps/web/public/stream-saver-sw.js
+++ /dev/null
@@ -1,154 +0,0 @@
-/*
-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 .
-*/
-/* eslint-disable no-restricted-globals */
-
-self.addEventListener("install", () => {
- self.skipWaiting();
-});
-
-self.addEventListener("activate", (event) => {
- event.waitUntil(self.clients.claim());
-});
-
-const map = new Map();
-
-// This should be called once per download
-// Each event has a dataChannel that the data will be piped through
-self.onmessage = (event) => {
- // We send a heartbeat every x second to keep the
- // service worker alive if a transferable stream is not sent
- if (event.data === "ping") {
- return;
- }
-
- const data = event.data;
- const downloadUrl =
- data.url ||
- self.registration.scope +
- Math.random() +
- "/" +
- (typeof data === "string" ? data : data.filename);
- const port = event.ports[0];
- const metadata = new Array(3); // [stream, data, port]
-
- metadata[1] = data;
- metadata[2] = port;
-
- if (event.data.transferringReadable) {
- port.onmessage = (evt) => {
- port.onmessage = null;
- metadata[0] = evt.data.readableStream;
- };
- } else {
- metadata[0] = createStream(port);
- }
-
- map.set(downloadUrl, metadata);
- port.postMessage({ download: downloadUrl });
-};
-
-function createStream(port) {
- // ReadableStream is only supported by chrome 52
- return new ReadableStream({
- start(controller) {
- // When we receive data on the messageChannel, we write
- port.onmessage = ({ data }) => {
- if (data === "end") {
- return controller.close();
- }
-
- if (data === "abort") {
- controller.error("Aborted the download");
- return;
- }
-
- controller.enqueue(data);
- };
- },
- cancel(reason) {
- console.log("user aborted", reason);
- port.postMessage({ abort: true });
- }
- });
-}
-
-self.onfetch = (event) => {
- const url = event.request.url;
-
- // this only works for Firefox
- if (url.endsWith("/ping")) {
- return event.respondWith(new Response("pong"));
- }
-
- const metadata = map.get(url);
- if (!metadata) return null;
-
- const [stream, data, port] = metadata;
-
- map.delete(url);
-
- // Not comfortable letting any user control all headers
- // so we only copy over the length & disposition
- const responseHeaders = new Headers({
- "Content-Type": "application/octet-stream; charset=utf-8",
-
- // To be on the safe side, The link can be opened in a iframe.
- // but octet-stream should stop it.
- "Content-Security-Policy": "default-src 'none'",
- "X-Content-Security-Policy": "default-src 'none'",
- "X-WebKit-CSP": "default-src 'none'",
- "X-XSS-Protection": "1; mode=block"
- });
-
- let headers = new Headers(data.headers || {});
-
- if (headers.has("Content-Length")) {
- responseHeaders.set("Content-Length", headers.get("Content-Length"));
- }
-
- if (headers.has("Content-Disposition")) {
- responseHeaders.set(
- "Content-Disposition",
- headers.get("Content-Disposition")
- );
- }
-
- // data, data.filename and size should not be used anymore
- if (data.size) {
- console.warn("Depricated");
- responseHeaders.set("Content-Length", data.size);
- }
-
- let fileName = typeof data === "string" ? data : data.filename;
- if (fileName) {
- console.warn("Depricated");
- // Make filename RFC5987 compatible
- fileName = encodeURIComponent(fileName)
- .replace(/['()]/g, escape)
- .replace(/\*/g, "%2A");
- responseHeaders.set(
- "Content-Disposition",
- "attachment; filename*=UTF-8''" + fileName
- );
- }
-
- event.respondWith(new Response(stream, { headers: responseHeaders }));
-
- port.postMessage({ debug: "Download started" });
-};
diff --git a/apps/web/src/index.tsx b/apps/web/src/index.tsx
index 9e8e9d0ab..ad8a3fa6e 100644
--- a/apps/web/src/index.tsx
+++ b/apps/web/src/index.tsx
@@ -51,8 +51,6 @@ async function renderApp() {
const serviceWorkerWhitelist: Routes[] = ["default"];
async function initializeServiceWorker() {
if (!IS_DESKTOP_APP) {
- await register();
-
logger.info("Initializing service worker...");
const serviceWorker = await import("./service-worker-registration");
@@ -69,6 +67,9 @@ async function initializeServiceWorker() {
AppEventManager.publish(AppEvents.updateDownloadCompleted, {
version: formatted
});
+ },
+ onSuccess(registration) {
+ register(registration);
}
});
// window.addEventListener("beforeinstallprompt", () => showInstallNotice());
diff --git a/apps/web/src/service-worker-registration.ts b/apps/web/src/service-worker-registration.ts
index 5e439b3a6..a454e31f6 100644
--- a/apps/web/src/service-worker-registration.ts
+++ b/apps/web/src/service-worker-registration.ts
@@ -86,9 +86,11 @@ function registerValidSW(
navigator.serviceWorker
.register(swUrl)
.then((registration) => {
+ if (config.onSuccess) config.onSuccess(registration);
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
+ if (config.onSuccess) config.onSuccess(registration);
return;
}
installingWorker.onstatechange = () => {
diff --git a/apps/web/src/service-worker.ts b/apps/web/src/service-worker.ts
index 91a62b9a8..e85d92ca9 100644
--- a/apps/web/src/service-worker.ts
+++ b/apps/web/src/service-worker.ts
@@ -80,6 +80,8 @@ registerRoute(
})
);
+const downloads = new Map();
+
// This allows the web app to trigger skipWaiting via
// registration.waiting.postMessage({type: 'SKIP_WAITING'})
self.addEventListener("message", (event) => {
@@ -87,6 +89,10 @@ self.addEventListener("message", (event) => {
if (!data) return;
switch (data.type) {
+ // We send a heartbeat every x second to keep the
+ // service worker alive if a transferable stream is not sent
+ case "PING":
+ break;
case "SKIP_WAITING":
self.skipWaiting();
break;
@@ -100,7 +106,125 @@ self.addEventListener("message", (event) => {
});
}
break;
+ case "REGISTER_DOWNLOAD":
+ {
+ console.log("register download", data);
+ const downloadUrl =
+ data.url ||
+ self.registration.scope +
+ Math.random() +
+ "/" +
+ (typeof data === "string" ? data : data.filename);
+ const port = event.ports[0];
+ const metadata = new Array(3); // [stream, data, port]
+
+ metadata[1] = data;
+ metadata[2] = port;
+
+ if (event.data.transferringReadable) {
+ port.onmessage = (evt) => {
+ port.onmessage = null;
+ metadata[0] = evt.data.readableStream;
+ };
+ } else {
+ metadata[0] = createStream(port);
+ }
+
+ downloads.set(downloadUrl, metadata);
+ port.postMessage({ download: downloadUrl });
+ }
+ break;
default:
break;
}
});
+
+self.addEventListener("fetch", (event) => {
+ const url = event.request.url;
+
+ // this only works for Firefox
+ if (url.endsWith("/ping")) {
+ return event.respondWith(new Response("pong"));
+ }
+
+ const metadata = downloads.get(url);
+ if (!metadata) return null;
+
+ const [stream, data, port] = metadata;
+
+ downloads.delete(url);
+
+ // Not comfortable letting any user control all headers
+ // so we only copy over the length & disposition
+ const responseHeaders = new Headers({
+ "Content-Type": "application/octet-stream; charset=utf-8",
+
+ // To be on the safe side, The link can be opened in a iframe.
+ // but octet-stream should stop it.
+ "Content-Security-Policy": "default-src 'none'",
+ "X-Content-Security-Policy": "default-src 'none'",
+ "X-WebKit-CSP": "default-src 'none'",
+ "X-XSS-Protection": "1; mode=block"
+ });
+
+ const headers = new Headers(data.headers || {});
+
+ if (headers.has("Content-Length")) {
+ responseHeaders.set("Content-Length", headers.get("Content-Length")!);
+ }
+
+ if (headers.has("Content-Disposition")) {
+ responseHeaders.set(
+ "Content-Disposition",
+ headers.get("Content-Disposition")!
+ );
+ }
+
+ // data, data.filename and size should not be used anymore
+ if (data.size) {
+ console.warn("Depricated");
+ responseHeaders.set("Content-Length", data.size);
+ }
+
+ let fileName = typeof data === "string" ? data : data.filename;
+ if (fileName) {
+ console.warn("Depricated");
+ // Make filename RFC5987 compatible
+ fileName = encodeURIComponent(fileName)
+ .replace(/['()]/g, escape)
+ .replace(/\*/g, "%2A");
+ responseHeaders.set(
+ "Content-Disposition",
+ "attachment; filename*=UTF-8''" + fileName
+ );
+ }
+
+ event.respondWith(new Response(stream, { headers: responseHeaders }));
+
+ port.postMessage({ debug: "Download started" });
+});
+
+function createStream(port: MessagePort) {
+ // ReadableStream is only supported by chrome 52
+ return new ReadableStream({
+ start(controller) {
+ // When we receive data on the messageChannel, we write
+ port.onmessage = ({ data }) => {
+ if (data === "end") {
+ return controller.close();
+ }
+
+ if (data === "abort") {
+ controller.error("Aborted the download");
+ return;
+ }
+
+ controller.enqueue(data);
+ };
+ },
+ cancel(reason) {
+ console.log("user aborted", reason);
+ port.postMessage({ abort: true });
+ }
+ });
+}
diff --git a/apps/web/src/utils/stream-saver/mitm.ts b/apps/web/src/utils/stream-saver/mitm.ts
index a869d6b65..4ea325179 100644
--- a/apps/web/src/utils/stream-saver/mitm.ts
+++ b/apps/web/src/utils/stream-saver/mitm.ts
@@ -24,7 +24,7 @@ let keepAlive = () => {
keepAlive = () => {};
const interval = setInterval(() => {
if (sw) {
- sw.postMessage("ping");
+ sw.postMessage({ type: "PING" });
} else {
const ping =
location.href.substr(0, location.href.lastIndexOf("/")) + "/ping";
@@ -36,39 +36,6 @@ let keepAlive = () => {
}, 10000);
};
-function registerWorker() {
- return navigator.serviceWorker
- .getRegistration("./")
- .then((swReg) => {
- return (
- swReg ||
- navigator.serviceWorker.register("stream-saver-sw.js", { scope: "./" })
- );
- })
- .then((swReg) => {
- console.log("Stream saver service worker registered!");
- const swRegTmp = swReg.installing || swReg.waiting;
-
- scope = swReg.scope;
- let fn: () => void;
- return (
- (sw = swReg.active) ||
- new Promise((resolve) => {
- swRegTmp?.addEventListener(
- "statechange",
- (fn = () => {
- if (swRegTmp.state === "activated") {
- swRegTmp.removeEventListener("statechange", fn);
- sw = swReg.active;
- resolve(undefined);
- }
- })
- );
- })
- );
- });
-}
-
// Now that we have the Service Worker registered we can process messages
export function postMessage(
data: {
@@ -81,6 +48,7 @@ export function postMessage(
},
ports: MessagePort[]
) {
+ if (!sw) throw new Error("No service worker registered.");
// It's important to have a messageChannel, don't want to interfere
// with other simultaneous downloads
if (!ports || !ports.length) {
@@ -120,19 +88,32 @@ export function postMessage(
// messageChannel.port2 to the service worker. The service worker can
// then use the transferred port to reply via postMessage(), which
// will in turn trigger the onmessage handler on messageChannel.port1.
+
const transferable = [ports[0]];
if (!data.transferringReadable) {
keepAlive();
}
- return sw?.postMessage(data, transferable);
+ return sw.postMessage({ type: "REGISTER_DOWNLOAD", ...data }, transferable);
}
-export async function register() {
- if (navigator.serviceWorker) {
- await registerWorker();
+export async function register(registration: ServiceWorkerRegistration) {
+ sw = registration.active;
+ scope = registration.scope;
+ if (!sw) {
+ const registrations =
+ (await navigator.serviceWorker?.getRegistrations()) || [];
+ for (const registration of registrations) {
+ if (registration.active) {
+ sw = registration.active;
+ scope = registration.scope;
+ }
+ }
}
+ if (sw) console.log("Registered stream saver!");
+ else console.error("Failed to register stream saver!");
+
// FF v102 just started to supports transferable streams, but still needs to ping sw.js
// even tough the service worker dose not have to do any kind of work and listen to any
// messages... #305