mirror of
https://github.com/vector-im/hydrogen-web.git
synced 2024-12-22 19:14:52 +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 {Platform} from "./Platform";
|
||||||
import configURL from "./assets/config.json?url";
|
import configURL from "./assets/config.json?url";
|
||||||
import assetPaths from "./sdk/paths/vite";
|
import assetPaths from "./sdk/paths/vite";
|
||||||
if (import.meta.env.PROD) {
|
|
||||||
assetPaths.serviceWorker = "sw.js";
|
assetPaths.serviceWorker = "sw.js";
|
||||||
}
|
|
||||||
const platform = new Platform({
|
const platform = new Platform({
|
||||||
container: document.body,
|
container: document.body,
|
||||||
assetPaths,
|
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.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import NOTIFICATION_BADGE_ICON from "./assets/icon.png?url";
|
const NOTIFICATION_BADGE_ICON = "icon.png";
|
||||||
// replaced by the service worker build plugin
|
|
||||||
|
// These are replaced by rollup plugins
|
||||||
const UNHASHED_PRECACHED_ASSETS = DEFINE_UNHASHED_PRECACHED_ASSETS;
|
const UNHASHED_PRECACHED_ASSETS = DEFINE_UNHASHED_PRECACHED_ASSETS;
|
||||||
const HASHED_PRECACHED_ASSETS = DEFINE_HASHED_PRECACHED_ASSETS;
|
const HASHED_PRECACHED_ASSETS = DEFINE_HASHED_PRECACHED_ASSETS;
|
||||||
const HASHED_CACHED_ON_REQUEST_ASSETS = DEFINE_HASHED_CACHED_ON_REQUEST_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 hashedCacheName = `hydrogen-assets`;
|
||||||
const mediaThumbnailCacheName = `hydrogen-media-thumbnails-v2`;
|
const mediaThumbnailCacheName = `hydrogen-media-thumbnails-v2`;
|
||||||
|
|
||||||
self.addEventListener('install', function(e) {
|
self.addEventListener("install", function (e) {
|
||||||
e.waitUntil((async () => {
|
e.waitUntil(
|
||||||
|
(async () => {
|
||||||
const unhashedCache = await caches.open(unhashedCacheName);
|
const unhashedCache = await caches.open(unhashedCacheName);
|
||||||
await unhashedCache.addAll(UNHASHED_PRECACHED_ASSETS);
|
await unhashedCache.addAll(UNHASHED_PRECACHED_ASSETS);
|
||||||
const hashedCache = await caches.open(hashedCacheName);
|
const hashedCache = await caches.open(hashedCacheName);
|
||||||
await Promise.all(HASHED_PRECACHED_ASSETS.map(async asset => {
|
await Promise.all(
|
||||||
if (!await hashedCache.match(asset)) {
|
HASHED_PRECACHED_ASSETS.map(async (asset) => {
|
||||||
|
if (!(await hashedCache.match(asset))) {
|
||||||
await hashedCache.add(asset);
|
await hashedCache.add(asset);
|
||||||
}
|
}
|
||||||
}));
|
})
|
||||||
})());
|
);
|
||||||
|
})()
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
self.addEventListener('activate', (event) => {
|
self.addEventListener("activate", (event) => {
|
||||||
// on a first page load/sw install,
|
// on a first page load/sw install,
|
||||||
// start using the service worker on all pages straight away
|
// start using the service worker on all pages straight away
|
||||||
self.clients.claim();
|
self.clients.claim();
|
||||||
@ -49,26 +54,29 @@ async function purgeOldCaches() {
|
|||||||
// remove any caches we don't know about
|
// remove any caches we don't know about
|
||||||
const keyList = await caches.keys();
|
const keyList = await caches.keys();
|
||||||
for (const key of keyList) {
|
for (const key of keyList) {
|
||||||
if (key !== unhashedCacheName && key !== hashedCacheName && key !== mediaThumbnailCacheName) {
|
if (
|
||||||
|
key !== unhashedCacheName &&
|
||||||
|
key !== hashedCacheName &&
|
||||||
|
key !== mediaThumbnailCacheName
|
||||||
|
) {
|
||||||
await caches.delete(key);
|
await caches.delete(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// remove the cache for any old hashed resource
|
// remove the cache for any old hashed resource
|
||||||
const hashedCache = await caches.open(hashedCacheName);
|
const hashedCache = await caches.open(hashedCacheName);
|
||||||
const keys = await hashedCache.keys();
|
const keys = await hashedCache.keys();
|
||||||
const hashedAssetURLs =
|
const hashedAssetURLs = HASHED_PRECACHED_ASSETS.concat(
|
||||||
HASHED_PRECACHED_ASSETS
|
HASHED_CACHED_ON_REQUEST_ASSETS
|
||||||
.concat(HASHED_CACHED_ON_REQUEST_ASSETS)
|
).map((a) => new URL(a, self.registration.scope).href);
|
||||||
.map(a => new URL(a, self.registration.scope).href);
|
|
||||||
|
|
||||||
for (const request of keys) {
|
for (const request of keys) {
|
||||||
if (!hashedAssetURLs.some(url => url === request.url)) {
|
if (!hashedAssetURLs.some((url) => url === request.url)) {
|
||||||
hashedCache.delete(request);
|
hashedCache.delete(request);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.addEventListener('fetch', (event) => {
|
self.addEventListener("fetch", (event) => {
|
||||||
/*
|
/*
|
||||||
service worker shouldn't handle xhr uploads because otherwise
|
service worker shouldn't handle xhr uploads because otherwise
|
||||||
the progress events won't fire.
|
the progress events won't fire.
|
||||||
@ -95,12 +103,18 @@ let pendingFetchAbortController = new AbortController();
|
|||||||
|
|
||||||
async function handleRequest(request) {
|
async function handleRequest(request) {
|
||||||
try {
|
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);
|
return handleStaleWhileRevalidateRequest(request);
|
||||||
}
|
}
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
// rewrite / to /index.html so it hits the cache
|
// 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));
|
request = new Request(new URL("index.html", baseURL.href));
|
||||||
}
|
}
|
||||||
let response = await readCache(request);
|
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
|
// 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
|
// https://developers.google.com/web/tools/chrome-devtools/progressive-web-apps?utm_source=devtools#opaque-responses
|
||||||
if (isCacheableThumbnail(url)) {
|
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 {
|
} else {
|
||||||
response = await fetch(request, {signal: pendingFetchAbortController.signal});
|
response = await fetch(request, {
|
||||||
|
signal: pendingFetchAbortController.signal,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
await updateCache(request, response);
|
await updateCache(request, response);
|
||||||
}
|
}
|
||||||
@ -198,9 +218,10 @@ async function readCache(request) {
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.addEventListener('message', (event) => {
|
self.addEventListener("message", (event) => {
|
||||||
const reply = payload => event.source.postMessage({replyTo: event.data.id, payload});
|
const reply = (payload) =>
|
||||||
const {replyTo} = event.data;
|
event.source.postMessage({ replyTo: event.data.id, payload });
|
||||||
|
const { replyTo } = event.data;
|
||||||
if (replyTo) {
|
if (replyTo) {
|
||||||
const resolve = pendingReplies.get(replyTo);
|
const resolve = pendingReplies.get(replyTo);
|
||||||
if (resolve) {
|
if (resolve) {
|
||||||
@ -210,7 +231,10 @@ self.addEventListener('message', (event) => {
|
|||||||
} else {
|
} else {
|
||||||
switch (event.data?.type) {
|
switch (event.data?.type) {
|
||||||
case "version":
|
case "version":
|
||||||
reply({version: DEFINE_VERSION, buildHash: DEFINE_GLOBAL_HASH});
|
reply({
|
||||||
|
version: DEFINE_VERSION,
|
||||||
|
buildHash: DEFINE_GLOBAL_HASH,
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case "skipWaiting":
|
case "skipWaiting":
|
||||||
self.skipWaiting();
|
self.skipWaiting();
|
||||||
@ -220,8 +244,10 @@ self.addEventListener('message', (event) => {
|
|||||||
break;
|
break;
|
||||||
case "closeSession":
|
case "closeSession":
|
||||||
event.waitUntil(
|
event.waitUntil(
|
||||||
closeSession(event.data.payload.sessionId, event.source.id)
|
closeSession(
|
||||||
.finally(() => reply())
|
event.data.payload.sessionId,
|
||||||
|
event.source.id
|
||||||
|
).finally(() => reply())
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -235,29 +261,40 @@ async function openClientFromNotif(event) {
|
|||||||
console.log("clicked notif with tag", event.notification.tag);
|
console.log("clicked notif with tag", event.notification.tag);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const {sessionId, roomId} = event.notification.data;
|
const { sessionId, roomId } = event.notification.data;
|
||||||
const sessionHash = `#/session/${sessionId}`;
|
const sessionHash = `#/session/${sessionId}`;
|
||||||
const roomHash = `${sessionHash}/room/${roomId}`;
|
const roomHash = `${sessionHash}/room/${roomId}`;
|
||||||
const clientWithSession = await findClient(async client => {
|
const clientWithSession = await findClient(async (client) => {
|
||||||
return await sendAndWaitForReply(client, "hasSessionOpen", {sessionId});
|
return await sendAndWaitForReply(client, "hasSessionOpen", {
|
||||||
|
sessionId,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
if (clientWithSession) {
|
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
|
// use a message rather than clientWithSession.navigate here as this refreshes the page on chrome
|
||||||
clientWithSession.postMessage({type: "openRoom", payload: {roomId}});
|
clientWithSession.postMessage({
|
||||||
if ('focus' in clientWithSession) {
|
type: "openRoom",
|
||||||
|
payload: { roomId },
|
||||||
|
});
|
||||||
|
if ("focus" in clientWithSession) {
|
||||||
try {
|
try {
|
||||||
await clientWithSession.focus();
|
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) {
|
} 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;
|
const roomURL = new URL(`./${roomHash}`, baseURL).href;
|
||||||
await self.clients.openWindow(roomURL);
|
await self.clients.openWindow(roomURL);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.addEventListener('notificationclick', event => {
|
self.addEventListener("notificationclick", (event) => {
|
||||||
event.notification.close();
|
event.notification.close();
|
||||||
event.waitUntil(openClientFromNotif(event));
|
event.waitUntil(openClientFromNotif(event));
|
||||||
});
|
});
|
||||||
@ -268,19 +305,30 @@ async function handlePushNotification(n) {
|
|||||||
let sender = n.sender_display_name || n.sender;
|
let sender = n.sender_display_name || n.sender;
|
||||||
if (sender && n.event_id) {
|
if (sender && n.event_id) {
|
||||||
const roomId = n.room_id;
|
const roomId = n.room_id;
|
||||||
const hasFocusedClientOnRoom = !!await findClient(async client => {
|
const hasFocusedClientOnRoom = !!(await findClient(async (client) => {
|
||||||
if (client.visibilityState === "visible" && client.focused) {
|
if (client.visibilityState === "visible" && client.focused) {
|
||||||
return await sendAndWaitForReply(client, "hasRoomOpen", {sessionId, roomId});
|
return await sendAndWaitForReply(client, "hasRoomOpen", {
|
||||||
}
|
sessionId,
|
||||||
|
roomId,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
}));
|
||||||
if (hasFocusedClientOnRoom) {
|
if (hasFocusedClientOnRoom) {
|
||||||
console.log("client is focused, room is open, don't show notif");
|
console.log("client is focused, room is open, don't show notif");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const newMessageNotifs = Array.from(await self.registration.getNotifications({tag: NOTIF_TAG_NEW_MESSAGE}));
|
const newMessageNotifs = Array.from(
|
||||||
const notifsForRoom = newMessageNotifs.filter(n => n.data.roomId === roomId);
|
await self.registration.getNotifications({
|
||||||
const hasMultiNotification = notifsForRoom.some(n => n.data.multi);
|
tag: NOTIF_TAG_NEW_MESSAGE,
|
||||||
const hasSingleNotifsForRoom = newMessageNotifs.some(n => !n.data.multi);
|
})
|
||||||
|
);
|
||||||
|
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;
|
const roomName = n.room_name || n.room_alias;
|
||||||
let multi = false;
|
let multi = false;
|
||||||
let label;
|
let label;
|
||||||
@ -304,9 +352,9 @@ async function handlePushNotification(n) {
|
|||||||
}
|
}
|
||||||
await self.registration.showNotification(label, {
|
await self.registration.showNotification(label, {
|
||||||
body,
|
body,
|
||||||
data: {sessionId, roomId, multi},
|
data: { sessionId, roomId, multi },
|
||||||
tag: NOTIF_TAG_NEW_MESSAGE,
|
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
|
// 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
|
// when no client is visible, see https://goo.gl/yqv4Q4
|
||||||
}
|
}
|
||||||
|
|
||||||
self.addEventListener('push', event => {
|
self.addEventListener("push", (event) => {
|
||||||
event.waitUntil(handlePushNotification(event.data.json()));
|
event.waitUntil(handlePushNotification(event.data.json()));
|
||||||
});
|
});
|
||||||
|
|
||||||
async function closeSession(sessionId, requestingClientId) {
|
async function closeSession(sessionId, requestingClientId) {
|
||||||
const clients = await self.clients.matchAll();
|
const clients = await self.clients.matchAll();
|
||||||
await Promise.all(clients.map(async client => {
|
await Promise.all(
|
||||||
|
clients.map(async (client) => {
|
||||||
if (client.id !== requestingClientId) {
|
if (client.id !== requestingClientId) {
|
||||||
await sendAndWaitForReply(client, "closeSession", {sessionId});
|
await sendAndWaitForReply(client, "closeSession", {
|
||||||
|
sessionId,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function haltRequests() {
|
async function haltRequests() {
|
||||||
// first ask all clients to block sending any more requests
|
// first ask all clients to block sending any more requests
|
||||||
const clients = await self.clients.matchAll({type: "window"});
|
const clients = await self.clients.matchAll({ type: "window" });
|
||||||
await Promise.all(clients.map(client => {
|
await Promise.all(
|
||||||
|
clients.map((client) => {
|
||||||
return sendAndWaitForReply(client, "haltRequests");
|
return sendAndWaitForReply(client, "haltRequests");
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
// and only then abort the current requests
|
// and only then abort the current requests
|
||||||
pendingFetchAbortController.abort();
|
pendingFetchAbortController.abort();
|
||||||
}
|
}
|
||||||
@ -343,15 +397,15 @@ let messageIdCounter = 0;
|
|||||||
function sendAndWaitForReply(client, type, payload) {
|
function sendAndWaitForReply(client, type, payload) {
|
||||||
messageIdCounter += 1;
|
messageIdCounter += 1;
|
||||||
const id = messageIdCounter;
|
const id = messageIdCounter;
|
||||||
const promise = new Promise(resolve => {
|
const promise = new Promise((resolve) => {
|
||||||
pendingReplies.set(id, resolve);
|
pendingReplies.set(id, resolve);
|
||||||
});
|
});
|
||||||
client.postMessage({type, id, payload});
|
client.postMessage({ type, id, payload });
|
||||||
return promise;
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function findClient(predicate) {
|
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) {
|
for (const client of clientList) {
|
||||||
if (await predicate(client)) {
|
if (await predicate(client)) {
|
||||||
return client;
|
return client;
|
||||||
|
Loading…
Reference in New Issue
Block a user