forked from github/cinny
* request session info from sw if missing * fix async session request in fetch * respond fetch synchronously and add early check for non media requests (#2670) * make sure we call respondWith synchronously * simplify isMediaRequest in sw * improve naming in sw * get back baseUrl check into validMediaRequest * pass original request into fetch in sw * extract mediaPath util and performs checks properly --------- Co-authored-by: mmmykhailo <35040944+mmmykhailo@users.noreply.github.com>
159 lines
3.9 KiB
TypeScript
159 lines
3.9 KiB
TypeScript
/// <reference lib="WebWorker" />
|
|
|
|
export type {};
|
|
declare const self: ServiceWorkerGlobalScope;
|
|
|
|
type SessionInfo = {
|
|
accessToken: string;
|
|
baseUrl: string;
|
|
};
|
|
|
|
/**
|
|
* Store session per client (tab)
|
|
*/
|
|
const sessions = new Map<string, SessionInfo>();
|
|
|
|
const clientToResolve = new Map<string, (value: SessionInfo | undefined) => void>();
|
|
const clientToSessionPromise = new Map<string, Promise<SessionInfo | undefined>>();
|
|
|
|
async function cleanupDeadClients() {
|
|
const activeClients = await self.clients.matchAll();
|
|
const activeIds = new Set(activeClients.map((c) => c.id));
|
|
|
|
Array.from(sessions.keys()).forEach((id) => {
|
|
if (!activeIds.has(id)) {
|
|
sessions.delete(id);
|
|
clientToResolve.delete(id);
|
|
clientToSessionPromise.delete(id);
|
|
}
|
|
});
|
|
}
|
|
|
|
function setSession(clientId: string, accessToken: any, baseUrl: any) {
|
|
if (typeof accessToken === 'string' && typeof baseUrl === 'string') {
|
|
sessions.set(clientId, { accessToken, baseUrl });
|
|
} else {
|
|
// Logout or invalid session
|
|
sessions.delete(clientId);
|
|
}
|
|
|
|
const resolveSession = clientToResolve.get(clientId);
|
|
if (resolveSession) {
|
|
resolveSession(sessions.get(clientId));
|
|
clientToResolve.delete(clientId);
|
|
clientToSessionPromise.delete(clientId);
|
|
}
|
|
}
|
|
|
|
function requestSession(client: Client): Promise<SessionInfo | undefined> {
|
|
const promise =
|
|
clientToSessionPromise.get(client.id) ??
|
|
new Promise((resolve) => {
|
|
clientToResolve.set(client.id, resolve);
|
|
client.postMessage({ type: 'requestSession' });
|
|
});
|
|
|
|
if (!clientToSessionPromise.has(client.id)) {
|
|
clientToSessionPromise.set(client.id, promise);
|
|
}
|
|
|
|
return promise;
|
|
}
|
|
|
|
async function requestSessionWithTimeout(
|
|
clientId: string,
|
|
timeoutMs = 3000
|
|
): Promise<SessionInfo | undefined> {
|
|
const client = await self.clients.get(clientId);
|
|
if (!client) return undefined;
|
|
|
|
const sessionPromise = requestSession(client);
|
|
|
|
const timeout = new Promise<undefined>((resolve) => {
|
|
setTimeout(() => resolve(undefined), timeoutMs);
|
|
});
|
|
|
|
return Promise.race([sessionPromise, timeout]);
|
|
}
|
|
|
|
self.addEventListener('install', () => {
|
|
self.skipWaiting();
|
|
});
|
|
|
|
self.addEventListener('activate', (event: ExtendableEvent) => {
|
|
event.waitUntil(
|
|
(async () => {
|
|
await self.clients.claim();
|
|
await cleanupDeadClients();
|
|
})()
|
|
);
|
|
});
|
|
|
|
/**
|
|
* Receive session updates from clients
|
|
*/
|
|
self.addEventListener('message', (event: ExtendableMessageEvent) => {
|
|
const client = event.source as Client | null;
|
|
if (!client) return;
|
|
|
|
const { type, accessToken, baseUrl } = event.data || {};
|
|
|
|
if (type === 'setSession') {
|
|
setSession(client.id, accessToken, baseUrl);
|
|
cleanupDeadClients();
|
|
}
|
|
});
|
|
|
|
const MEDIA_PATHS = ['/_matrix/client/v1/media/download', '/_matrix/client/v1/media/thumbnail'];
|
|
|
|
function mediaPath(url: string): boolean {
|
|
try {
|
|
const { pathname } = new URL(url);
|
|
return MEDIA_PATHS.some((p) => pathname.startsWith(p));
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function validMediaRequest(url: string, baseUrl: string): boolean {
|
|
return MEDIA_PATHS.some((p) => {
|
|
const validUrl = new URL(p, baseUrl);
|
|
return url.startsWith(validUrl.href);
|
|
});
|
|
}
|
|
|
|
function fetchConfig(token: string): RequestInit {
|
|
return {
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
cache: 'default',
|
|
};
|
|
}
|
|
|
|
self.addEventListener('fetch', (event: FetchEvent) => {
|
|
const { url, method } = event.request;
|
|
|
|
if (method !== 'GET' || !mediaPath(url)) return;
|
|
|
|
const { clientId } = event;
|
|
if (!clientId) return;
|
|
|
|
const session = sessions.get(clientId);
|
|
if (session) {
|
|
if (validMediaRequest(url, session.baseUrl)) {
|
|
event.respondWith(fetch(url, fetchConfig(session.accessToken)));
|
|
}
|
|
return;
|
|
}
|
|
|
|
event.respondWith(
|
|
requestSessionWithTimeout(clientId).then((s) => {
|
|
if (s && validMediaRequest(url, s.baseUrl)) {
|
|
return fetch(url, fetchConfig(s.accessToken));
|
|
}
|
|
return fetch(event.request);
|
|
})
|
|
);
|
|
});
|