mirror of
https://github.com/vector-im/hydrogen-web.git
synced 2024-12-22 11:05:03 +01:00
Make service worker work in dev
This commit is contained in:
parent
9b68f30aad
commit
3967d26f9d
67
scripts/build-plugins/sw-dev.js
Normal file
67
scripts/build-plugins/sw-dev.js
Normal file
@ -0,0 +1,67 @@
|
||||
/*
|
||||
Copyright 2024 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.
|
||||
*/
|
||||
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
|
||||
/**
|
||||
* This rollup plugin makes it possible to use the serviceworker with the dev server.
|
||||
* The service worker is located in `/src/platform/web/sw.js` and it contains some
|
||||
* fields that need to be replaced with sensible values.
|
||||
*
|
||||
* We have a plugin that does this during build (see `./service-worker.js`).
|
||||
* This plugin does more or less the same but for dev.
|
||||
*/
|
||||
|
||||
export function transformServiceWorkerInDevServer() {
|
||||
// See https://vitejs.dev/config/shared-options.html#define
|
||||
// Comes from vite.config.js
|
||||
let define;
|
||||
|
||||
return {
|
||||
name: "hydrogen:transformServiceWorkerInDevServer",
|
||||
apply: "serve",
|
||||
enforce: "pre",
|
||||
|
||||
configResolved(resolvedConfig) {
|
||||
// store the resolved config
|
||||
define = resolvedConfig.define;
|
||||
},
|
||||
|
||||
async load(id) {
|
||||
if (!id.includes("sw.js")) return null;
|
||||
let code = await readServiceWorkerCode();
|
||||
for (const [key, value] of Object.entries(define)) {
|
||||
code = code.replaceAll(key, value);
|
||||
}
|
||||
return code;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Read service worker code from `src/platform/web/sw.js`
|
||||
* @returns code as string
|
||||
*/
|
||||
async function readServiceWorkerCode() {
|
||||
const resolvedLocation = path.resolve(
|
||||
__dirname,
|
||||
"../../",
|
||||
"./src/platform/web/sw.js"
|
||||
);
|
||||
const data = await fs.readFile(resolvedLocation, { encoding: "utf-8" });
|
||||
return data;
|
||||
}
|
@ -19,9 +19,7 @@
|
||||
import {Platform} from "./Platform";
|
||||
import configURL from "./assets/config.json?url";
|
||||
import assetPaths from "./sdk/paths/vite";
|
||||
if (import.meta.env.PROD) {
|
||||
assetPaths.serviceWorker = "sw.js";
|
||||
}
|
||||
assetPaths.serviceWorker = "sw.js";
|
||||
const platform = new Platform({
|
||||
container: document.body,
|
||||
assetPaths,
|
||||
|
BIN
src/platform/web/public/icon.png
Normal file
BIN
src/platform/web/public/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
@ -15,8 +15,9 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import NOTIFICATION_BADGE_ICON from "./assets/icon.png?url";
|
||||
// replaced by the service worker build plugin
|
||||
const NOTIFICATION_BADGE_ICON = "icon.png";
|
||||
|
||||
// These are replaced by rollup plugins
|
||||
const UNHASHED_PRECACHED_ASSETS = DEFINE_UNHASHED_PRECACHED_ASSETS;
|
||||
const HASHED_PRECACHED_ASSETS = DEFINE_HASHED_PRECACHED_ASSETS;
|
||||
const HASHED_CACHED_ON_REQUEST_ASSETS = DEFINE_HASHED_CACHED_ON_REQUEST_ASSETS;
|
||||
@ -25,20 +26,24 @@ const unhashedCacheName = `hydrogen-assets-${DEFINE_GLOBAL_HASH}`;
|
||||
const hashedCacheName = `hydrogen-assets`;
|
||||
const mediaThumbnailCacheName = `hydrogen-media-thumbnails-v2`;
|
||||
|
||||
self.addEventListener('install', function(e) {
|
||||
e.waitUntil((async () => {
|
||||
const unhashedCache = await caches.open(unhashedCacheName);
|
||||
await unhashedCache.addAll(UNHASHED_PRECACHED_ASSETS);
|
||||
const hashedCache = await caches.open(hashedCacheName);
|
||||
await Promise.all(HASHED_PRECACHED_ASSETS.map(async asset => {
|
||||
if (!await hashedCache.match(asset)) {
|
||||
await hashedCache.add(asset);
|
||||
}
|
||||
}));
|
||||
})());
|
||||
self.addEventListener("install", function (e) {
|
||||
e.waitUntil(
|
||||
(async () => {
|
||||
const unhashedCache = await caches.open(unhashedCacheName);
|
||||
await unhashedCache.addAll(UNHASHED_PRECACHED_ASSETS);
|
||||
const hashedCache = await caches.open(hashedCacheName);
|
||||
await Promise.all(
|
||||
HASHED_PRECACHED_ASSETS.map(async (asset) => {
|
||||
if (!(await hashedCache.match(asset))) {
|
||||
await hashedCache.add(asset);
|
||||
}
|
||||
})
|
||||
);
|
||||
})()
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
self.addEventListener("activate", (event) => {
|
||||
// on a first page load/sw install,
|
||||
// start using the service worker on all pages straight away
|
||||
self.clients.claim();
|
||||
@ -49,26 +54,29 @@ async function purgeOldCaches() {
|
||||
// remove any caches we don't know about
|
||||
const keyList = await caches.keys();
|
||||
for (const key of keyList) {
|
||||
if (key !== unhashedCacheName && key !== hashedCacheName && key !== mediaThumbnailCacheName) {
|
||||
if (
|
||||
key !== unhashedCacheName &&
|
||||
key !== hashedCacheName &&
|
||||
key !== mediaThumbnailCacheName
|
||||
) {
|
||||
await caches.delete(key);
|
||||
}
|
||||
}
|
||||
// remove the cache for any old hashed resource
|
||||
const hashedCache = await caches.open(hashedCacheName);
|
||||
const keys = await hashedCache.keys();
|
||||
const hashedAssetURLs =
|
||||
HASHED_PRECACHED_ASSETS
|
||||
.concat(HASHED_CACHED_ON_REQUEST_ASSETS)
|
||||
.map(a => new URL(a, self.registration.scope).href);
|
||||
const hashedAssetURLs = HASHED_PRECACHED_ASSETS.concat(
|
||||
HASHED_CACHED_ON_REQUEST_ASSETS
|
||||
).map((a) => new URL(a, self.registration.scope).href);
|
||||
|
||||
for (const request of keys) {
|
||||
if (!hashedAssetURLs.some(url => url === request.url)) {
|
||||
if (!hashedAssetURLs.some((url) => url === request.url)) {
|
||||
hashedCache.delete(request);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.addEventListener('fetch', (event) => {
|
||||
self.addEventListener("fetch", (event) => {
|
||||
/*
|
||||
service worker shouldn't handle xhr uploads because otherwise
|
||||
the progress events won't fire.
|
||||
@ -95,12 +103,18 @@ let pendingFetchAbortController = new AbortController();
|
||||
|
||||
async function handleRequest(request) {
|
||||
try {
|
||||
if (request.url.includes("config.json") || /theme-.+\.json/.test(request.url)) {
|
||||
if (
|
||||
request.url.includes("config.json") ||
|
||||
/theme-.+\.json/.test(request.url)
|
||||
) {
|
||||
return handleStaleWhileRevalidateRequest(request);
|
||||
}
|
||||
const url = new URL(request.url);
|
||||
// rewrite / to /index.html so it hits the cache
|
||||
if (url.origin === baseURL.origin && url.pathname === baseURL.pathname) {
|
||||
if (
|
||||
url.origin === baseURL.origin &&
|
||||
url.pathname === baseURL.pathname
|
||||
) {
|
||||
request = new Request(new URL("index.html", baseURL.href));
|
||||
}
|
||||
let response = await readCache(request);
|
||||
@ -108,9 +122,15 @@ async function handleRequest(request) {
|
||||
// use cors so the resource in the cache isn't opaque and uses up to 7mb
|
||||
// https://developers.google.com/web/tools/chrome-devtools/progressive-web-apps?utm_source=devtools#opaque-responses
|
||||
if (isCacheableThumbnail(url)) {
|
||||
response = await fetch(request, {signal: pendingFetchAbortController.signal, mode: "cors", credentials: "omit"});
|
||||
response = await fetch(request, {
|
||||
signal: pendingFetchAbortController.signal,
|
||||
mode: "cors",
|
||||
credentials: "omit",
|
||||
});
|
||||
} else {
|
||||
response = await fetch(request, {signal: pendingFetchAbortController.signal});
|
||||
response = await fetch(request, {
|
||||
signal: pendingFetchAbortController.signal,
|
||||
});
|
||||
}
|
||||
await updateCache(request, response);
|
||||
}
|
||||
@ -184,7 +204,7 @@ async function readCache(request) {
|
||||
if (response) {
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
const url = new URL(request.url);
|
||||
if (isCacheableThumbnail(url)) {
|
||||
const mediaThumbnailCache = await caches.open(mediaThumbnailCacheName);
|
||||
@ -198,9 +218,10 @@ async function readCache(request) {
|
||||
return response;
|
||||
}
|
||||
|
||||
self.addEventListener('message', (event) => {
|
||||
const reply = payload => event.source.postMessage({replyTo: event.data.id, payload});
|
||||
const {replyTo} = event.data;
|
||||
self.addEventListener("message", (event) => {
|
||||
const reply = (payload) =>
|
||||
event.source.postMessage({ replyTo: event.data.id, payload });
|
||||
const { replyTo } = event.data;
|
||||
if (replyTo) {
|
||||
const resolve = pendingReplies.get(replyTo);
|
||||
if (resolve) {
|
||||
@ -210,7 +231,10 @@ self.addEventListener('message', (event) => {
|
||||
} else {
|
||||
switch (event.data?.type) {
|
||||
case "version":
|
||||
reply({version: DEFINE_VERSION, buildHash: DEFINE_GLOBAL_HASH});
|
||||
reply({
|
||||
version: DEFINE_VERSION,
|
||||
buildHash: DEFINE_GLOBAL_HASH,
|
||||
});
|
||||
break;
|
||||
case "skipWaiting":
|
||||
self.skipWaiting();
|
||||
@ -220,8 +244,10 @@ self.addEventListener('message', (event) => {
|
||||
break;
|
||||
case "closeSession":
|
||||
event.waitUntil(
|
||||
closeSession(event.data.payload.sessionId, event.source.id)
|
||||
.finally(() => reply())
|
||||
closeSession(
|
||||
event.data.payload.sessionId,
|
||||
event.source.id
|
||||
).finally(() => reply())
|
||||
);
|
||||
break;
|
||||
}
|
||||
@ -235,29 +261,40 @@ async function openClientFromNotif(event) {
|
||||
console.log("clicked notif with tag", event.notification.tag);
|
||||
return;
|
||||
}
|
||||
const {sessionId, roomId} = event.notification.data;
|
||||
const { sessionId, roomId } = event.notification.data;
|
||||
const sessionHash = `#/session/${sessionId}`;
|
||||
const roomHash = `${sessionHash}/room/${roomId}`;
|
||||
const clientWithSession = await findClient(async client => {
|
||||
return await sendAndWaitForReply(client, "hasSessionOpen", {sessionId});
|
||||
const clientWithSession = await findClient(async (client) => {
|
||||
return await sendAndWaitForReply(client, "hasSessionOpen", {
|
||||
sessionId,
|
||||
});
|
||||
});
|
||||
if (clientWithSession) {
|
||||
console.log("notificationclick: client has session open, showing room there");
|
||||
console.log(
|
||||
"notificationclick: client has session open, showing room there"
|
||||
);
|
||||
// use a message rather than clientWithSession.navigate here as this refreshes the page on chrome
|
||||
clientWithSession.postMessage({type: "openRoom", payload: {roomId}});
|
||||
if ('focus' in clientWithSession) {
|
||||
clientWithSession.postMessage({
|
||||
type: "openRoom",
|
||||
payload: { roomId },
|
||||
});
|
||||
if ("focus" in clientWithSession) {
|
||||
try {
|
||||
await clientWithSession.focus();
|
||||
} catch (err) { console.error(err); } // I've had this throw on me on Android
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} // I've had this throw on me on Android
|
||||
}
|
||||
} else if (self.clients.openWindow) {
|
||||
console.log("notificationclick: no client found with session open, opening new window");
|
||||
console.log(
|
||||
"notificationclick: no client found with session open, opening new window"
|
||||
);
|
||||
const roomURL = new URL(`./${roomHash}`, baseURL).href;
|
||||
await self.clients.openWindow(roomURL);
|
||||
}
|
||||
}
|
||||
|
||||
self.addEventListener('notificationclick', event => {
|
||||
self.addEventListener("notificationclick", (event) => {
|
||||
event.notification.close();
|
||||
event.waitUntil(openClientFromNotif(event));
|
||||
});
|
||||
@ -268,19 +305,30 @@ async function handlePushNotification(n) {
|
||||
let sender = n.sender_display_name || n.sender;
|
||||
if (sender && n.event_id) {
|
||||
const roomId = n.room_id;
|
||||
const hasFocusedClientOnRoom = !!await findClient(async client => {
|
||||
const hasFocusedClientOnRoom = !!(await findClient(async (client) => {
|
||||
if (client.visibilityState === "visible" && client.focused) {
|
||||
return await sendAndWaitForReply(client, "hasRoomOpen", {sessionId, roomId});
|
||||
return await sendAndWaitForReply(client, "hasRoomOpen", {
|
||||
sessionId,
|
||||
roomId,
|
||||
});
|
||||
}
|
||||
});
|
||||
}));
|
||||
if (hasFocusedClientOnRoom) {
|
||||
console.log("client is focused, room is open, don't show notif");
|
||||
return;
|
||||
}
|
||||
const newMessageNotifs = Array.from(await self.registration.getNotifications({tag: NOTIF_TAG_NEW_MESSAGE}));
|
||||
const notifsForRoom = newMessageNotifs.filter(n => n.data.roomId === roomId);
|
||||
const hasMultiNotification = notifsForRoom.some(n => n.data.multi);
|
||||
const hasSingleNotifsForRoom = newMessageNotifs.some(n => !n.data.multi);
|
||||
const newMessageNotifs = Array.from(
|
||||
await self.registration.getNotifications({
|
||||
tag: NOTIF_TAG_NEW_MESSAGE,
|
||||
})
|
||||
);
|
||||
const notifsForRoom = newMessageNotifs.filter(
|
||||
(n) => n.data.roomId === roomId
|
||||
);
|
||||
const hasMultiNotification = notifsForRoom.some((n) => n.data.multi);
|
||||
const hasSingleNotifsForRoom = newMessageNotifs.some(
|
||||
(n) => !n.data.multi
|
||||
);
|
||||
const roomName = n.room_name || n.room_alias;
|
||||
let multi = false;
|
||||
let label;
|
||||
@ -304,9 +352,9 @@ async function handlePushNotification(n) {
|
||||
}
|
||||
await self.registration.showNotification(label, {
|
||||
body,
|
||||
data: {sessionId, roomId, multi},
|
||||
data: { sessionId, roomId, multi },
|
||||
tag: NOTIF_TAG_NEW_MESSAGE,
|
||||
badge: NOTIFICATION_BADGE_ICON
|
||||
badge: NOTIFICATION_BADGE_ICON,
|
||||
});
|
||||
}
|
||||
// we could consider hiding previous notifications here based on the unread count
|
||||
@ -315,25 +363,31 @@ async function handlePushNotification(n) {
|
||||
// when no client is visible, see https://goo.gl/yqv4Q4
|
||||
}
|
||||
|
||||
self.addEventListener('push', event => {
|
||||
self.addEventListener("push", (event) => {
|
||||
event.waitUntil(handlePushNotification(event.data.json()));
|
||||
});
|
||||
|
||||
async function closeSession(sessionId, requestingClientId) {
|
||||
const clients = await self.clients.matchAll();
|
||||
await Promise.all(clients.map(async client => {
|
||||
if (client.id !== requestingClientId) {
|
||||
await sendAndWaitForReply(client, "closeSession", {sessionId});
|
||||
}
|
||||
}));
|
||||
await Promise.all(
|
||||
clients.map(async (client) => {
|
||||
if (client.id !== requestingClientId) {
|
||||
await sendAndWaitForReply(client, "closeSession", {
|
||||
sessionId,
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async function haltRequests() {
|
||||
// first ask all clients to block sending any more requests
|
||||
const clients = await self.clients.matchAll({type: "window"});
|
||||
await Promise.all(clients.map(client => {
|
||||
return sendAndWaitForReply(client, "haltRequests");
|
||||
}));
|
||||
const clients = await self.clients.matchAll({ type: "window" });
|
||||
await Promise.all(
|
||||
clients.map((client) => {
|
||||
return sendAndWaitForReply(client, "haltRequests");
|
||||
})
|
||||
);
|
||||
// and only then abort the current requests
|
||||
pendingFetchAbortController.abort();
|
||||
}
|
||||
@ -343,15 +397,15 @@ let messageIdCounter = 0;
|
||||
function sendAndWaitForReply(client, type, payload) {
|
||||
messageIdCounter += 1;
|
||||
const id = messageIdCounter;
|
||||
const promise = new Promise(resolve => {
|
||||
const promise = new Promise((resolve) => {
|
||||
pendingReplies.set(id, resolve);
|
||||
});
|
||||
client.postMessage({type, id, payload});
|
||||
client.postMessage({ type, id, payload });
|
||||
return promise;
|
||||
}
|
||||
|
||||
async function findClient(predicate) {
|
||||
const clientList = await self.clients.matchAll({type: "window"});
|
||||
const clientList = await self.clients.matchAll({ type: "window" });
|
||||
for (const client of clientList) {
|
||||
if (await predicate(client)) {
|
||||
return client;
|
||||
|
Loading…
Reference in New Issue
Block a user