aboutsummaryrefslogtreecommitdiff
path: root/client-web/source/sw
diff options
context:
space:
mode:
Diffstat (limited to 'client-web/source/sw')
-rw-r--r--client-web/source/sw/client.ts (renamed from client-web/source/sw/init.ts)35
-rw-r--r--client-web/source/sw/download.ts36
-rw-r--r--client-web/source/sw/download_stream.ts81
-rw-r--r--client-web/source/sw/protocol.ts9
-rw-r--r--client-web/source/sw/worker.ts115
5 files changed, 149 insertions, 127 deletions
diff --git a/client-web/source/sw/init.ts b/client-web/source/sw/client.ts
index d082038..1396826 100644
--- a/client-web/source/sw/init.ts
+++ b/client-web/source/sw/client.ts
@@ -6,25 +6,34 @@
/// <reference lib="dom" />
import { log } from "../logger.ts"
+import { SWMessage } from "./protocol.ts"
+
+export let SW_ENABLED = false
-export let SW: ServiceWorker | undefined
export async function init_serviceworker() {
let reg = await globalThis.navigator.serviceWorker.getRegistration()
if (reg) {
log("sw", "service worker already installed")
+ SW_ENABLED = true
} else {
log("sw", "registering service worker")
await globalThis.navigator.serviceWorker.register("/sw.js", { scope: "/", type: "module" })
log("sw", "worker installed")
reg = await globalThis.navigator.serviceWorker.getRegistration();
if (!reg) throw new Error("we just registered the sw!?");
+ SW_ENABLED = !!reg
}
- const i = setInterval(() => {
- if (reg!.active) {
- SW = reg!.active
- clearInterval(i)
- }
- }, 100)
+ start_handler()
+ log("sw", "checking for updates")
+ send_sw_message({ check_version: true })
+}
+
+export async function send_sw_message(message: SWMessage, transfer?: Transferable[]) {
+ const reg = await globalThis.navigator.serviceWorker.getRegistration();
+ if (!reg) throw new Error("no sw");
+ if (!reg.active) throw new Error("no sw");
+ if (transfer) reg.active.postMessage(message, transfer)
+ else reg.active.postMessage(message, transfer)
}
export async function update_serviceworker() {
@@ -33,3 +42,15 @@ export async function update_serviceworker() {
log("sw", "cleared all workers")
setTimeout(() => window.location.reload(), 500)
}
+
+function start_handler() {
+ globalThis.navigator.serviceWorker.addEventListener("message", event => {
+ const message: SWMessage = event.data;
+ if (message.version_info) {
+ log("sw", JSON.stringify(message.version_info))
+ }
+ if (message.updated) {
+ log("*", "updated")
+ }
+ })
+}
diff --git a/client-web/source/sw/download.ts b/client-web/source/sw/download.ts
new file mode 100644
index 0000000..02c5e1a
--- /dev/null
+++ b/client-web/source/sw/download.ts
@@ -0,0 +1,36 @@
+export const streams = new Map<string, { readable: ReadableStream, size: number }>()
+
+export function handle_download_request(path: string, event: FetchEvent) {
+ const stream = streams.get(path)
+ if (stream) {
+ streams.delete(path)
+ console.log(`-> stream response`);
+ event.respondWith(
+ new Response(
+ stream.readable,
+ {
+ headers: new Headers({
+ "content-type": "application/octet-stream; charset=utf-8", // TODO transmit and set accordingly
+ "content-security-policy": "default-src 'none'",
+ "content-length": `${stream.size}`,
+ })
+ }
+ )
+ )
+ }
+ event.respondWith(new Response("download failed", { status: 400, headers: new Headers({ "content-type": "text/plain" }) }))
+}
+
+export function port_to_readable(port: MessagePort): ReadableStream {
+ return new ReadableStream({
+ start(controller) {
+ console.log("ReadableStream started");
+ port.onmessage = event => {
+ if (event.data === "end") controller.close()
+ else if (event.data === "abort") controller.error("aborted")
+ else controller.enqueue(event.data)
+ }
+ },
+ cancel() { console.log("ReadableStream cancelled"); port.postMessage({ abort: true }) },
+ })
+}
diff --git a/client-web/source/sw/download_stream.ts b/client-web/source/sw/download_stream.ts
deleted file mode 100644
index 35a3c7e..0000000
--- a/client-web/source/sw/download_stream.ts
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- This file is part of keks-meet (https://codeberg.org/metamuffin/keks-meet)
- which is licensed under the GNU Affero General Public License (version 3); see /COPYING.
- Copyright (C) 2022 metamuffin <metamuffin@disroot.org>
-*/
-/// <reference lib="dom" />
-import { log } from "../logger.ts"
-import { SW } from "./init.ts"
-
-function FallbackStreamDownload(size: number, filename?: string, progress?: (position: number) => void) {
- log({ scope: "*", warn: true }, "downloading to memory because serviceworker is not available")
- let position = 0
- let buffer = new Uint8Array(size)
- return {
- close() {
- const a = document.createElement("a")
- a.href = URL.createObjectURL(new Blob([buffer], { type: "text/plain" }))
- a.download = filename ?? "file"
- a.click()
- },
- abort() { buffer = new Uint8Array(); /* have fun gc */ },
- write(chunk: Blob) {
- const reader = new FileReader();
- reader.onload = function (event) {
- const arr = new Uint8Array(event.target!.result as ArrayBuffer);
- for (let i = 0; i < arr.length; i++, position++) {
- buffer[position] = arr[i]
- }
- if (progress) progress(position)
- };
- reader.readAsArrayBuffer(chunk);
- }
- }
-}
-
-export function StreamDownload({ size, filename, cancel, progress }: {
- size: number,
- filename: string,
- cancel: () => void,
- progress: (position: number) => void
-}) {
- if (!SW) FallbackStreamDownload(size, filename, progress)
- let position = 0
-
- const path = `/download/${encodeURIComponent(filename ?? "file")}`
-
- const { port1, port2 } = new MessageChannel()
- SW!.postMessage({ path, size }, [port2])
-
- const a = document.createElement("a")
- a.href = path
- a.download = filename ?? "file"
- a.target = "_blank"
- a.click()
-
- port1.onmessage = ev => {
- if (ev.data.abort) {
- cancel()
- port1.close()
- }
- }
-
- return {
- close() {
- port1.postMessage("end")
- },
- abort() {
- port1.postMessage("abort")
- },
- write(chunk: Blob) {
- const reader = new FileReader();
- reader.onload = function (event) {
- const arr = new Uint8Array(event.target!.result as ArrayBuffer);
- port1.postMessage(arr)
- position += arr.length
- if (progress) progress(position)
- };
- reader.readAsArrayBuffer(chunk);
- }
- }
-}
diff --git a/client-web/source/sw/protocol.ts b/client-web/source/sw/protocol.ts
new file mode 100644
index 0000000..7cca220
--- /dev/null
+++ b/client-web/source/sw/protocol.ts
@@ -0,0 +1,9 @@
+
+export interface SWMessage {
+ download?: { path: string, size: number }
+ check_version?: boolean,
+ update?: boolean,
+
+ version_info?: { installed_version: string, available_version: string }
+ updated?: boolean
+}
diff --git a/client-web/source/sw/worker.ts b/client-web/source/sw/worker.ts
index a06f3db..442f08a 100644
--- a/client-web/source/sw/worker.ts
+++ b/client-web/source/sw/worker.ts
@@ -4,9 +4,12 @@
Copyright (C) 2022 metamuffin <metamuffin@disroot.org>
*/
/// <reference no-default-lib="true"/>
-
/// <reference lib="esnext" />
/// <reference lib="webworker" />
+
+import { handle_download_request, port_to_readable, streams } from "./download.ts";
+import { SWMessage } from "./protocol.ts";
+
declare const self: ServiceWorkerGlobalScope; export { };
console.log("hello from the keks-meet service worker");
@@ -25,53 +28,87 @@ self.addEventListener("unload", () => {
console.log("unload")
})
-const streams = new Map<string, { readable: ReadableStream, size: number }>()
-
-self.addEventListener("message", ev => {
- const { path, size } = ev.data, port = ev.ports[0]
- const readable = port_to_readable(port)
- streams.set(path, { readable, size })
+self.addEventListener("message", async ev => {
+ const message: SWMessage = ev.data;
+ console.log("incoming message", message);
+ if (message.download) {
+ const { path, size } = message.download, port = ev.ports[0]
+ const readable = port_to_readable(port)
+ streams.set(path, { readable, size })
+ }
+ if (message.check_version) {
+ broadcast_response(await check_for_updates())
+ }
+ if (message.update) {
+ broadcast_response(await update())
+ }
})
-function port_to_readable(port: MessagePort): ReadableStream {
- return new ReadableStream({
- start(controller) {
- console.log("ReadableStream started");
- port.onmessage = event => {
- if (event.data === "end") controller.close()
- else if (event.data === "abort") controller.error("aborted")
- else controller.enqueue(event.data)
- }
- },
- cancel() { console.log("ReadableStream cancelled"); port.postMessage({ abort: true }) },
- })
+async function broadcast_response(message: SWMessage) {
+ const clients = await self.clients.matchAll({})
+ console.log(clients);
+ clients.forEach(c => c.postMessage(message))
}
-self.addEventListener("fetch", event => {
+self.addEventListener("fetch", async event => {
const { request } = event;
if (!request.url.startsWith(self.origin)) return
const path = request.url.substring(self.origin.length)
console.log(request.method, path);
- const stream = streams.get(path)
- if (stream) {
- streams.delete(path)
- console.log(`-> stream response`);
- return event.respondWith(
- new Response(
- stream.readable,
- {
- headers: new Headers({
- "content-type": "application/octet-stream; charset=utf-8", // TODO transmit and set accordingly
- "content-security-policy": "default-src 'none'",
- "content-length": `${stream.size}`,
- })
- }
- )
- )
- }
-
+ if (path.startsWith("/download")) return handle_download_request(path, event)
+ if (path.startsWith("/signaling")) return event.respondWith(fetch(request))
if (path == "/swtest") return event.respondWith(new Response("works!", { headers: new Headers({ "content-type": "text/plain" }) }))
- event.respondWith(fetch(request))
+ const cache = await caches.open("v1")
+ // const cached = await cache.match(request)
+ // if (cached) {
+ // console.log("-> cached");
+ // return cached
+ // }
+ console.log("-> forwarding to the server");
+ const response = await fetch(request);
+ cache.put(request, response.clone())
+ event.respondWith(response.clone())
})
+
+
+async function update(): Promise<SWMessage> {
+ console.log("updating...");
+ await caches.delete("v1")
+ await Promise.all(
+ [
+ "/",
+ "/room",
+ "/config.json",
+ "/assets/bundle.js",
+ "/favicon.ico"
+ ]
+ .map(cache_preload)
+ )
+ return { updated: true }
+}
+
+async function cache_preload(path: string) {
+ const cache = await caches.open("v1")
+ const req = new Request(path)
+ const res = await fetch(req)
+ await cache.put(req, res)
+}
+
+async function check_for_updates(): Promise<SWMessage> {
+ console.log("checking for updates");
+ const cache = await caches.open("v1")
+ const res = await fetch("/version")
+ const res2 = await cache.match(new Request("/version"));
+ const available_version = await res.text()
+ const installed_version = res2 ? await res2.text() : "none";
+ console.log({ available_version, installed_version });
+ return {
+ version_info: {
+ available_version,
+ installed_version
+ }
+ }
+}
+