diff --git a/src/app/pages/Router.tsx b/src/app/pages/Router.tsx index 04d14a07..f8647d18 100644 --- a/src/app/pages/Router.tsx +++ b/src/app/pages/Router.tsx @@ -68,7 +68,6 @@ import { Create } from './client/create'; import { CreateSpaceModalRenderer } from '../features/create-space'; import { SearchModalRenderer } from '../features/search'; import { getFallbackSession } from '../state/sessions'; -import { pushSessionToSW } from '../../sw-session'; export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize) => { const { hashRouter } = clientConfig; @@ -116,7 +115,6 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize) if (afterLoginPath) setAfterLoginRedirectPath(afterLoginPath); return redirect(getLoginPath()); } - pushSessionToSW(session.baseUrl, session.accessToken); return null; }} element={ diff --git a/src/index.tsx b/src/index.tsx index 71e723ab..6cc0ca38 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -34,17 +34,14 @@ if ('serviceWorker' in navigator) { navigator.serviceWorker.register(swUrl).then(sendSessionToSW); navigator.serviceWorker.ready.then(sendSessionToSW); - window.addEventListener('load', sendSessionToSW); - // When returning from background - document.addEventListener('visibilitychange', () => { - if (document.visibilityState === 'visible') { + navigator.serviceWorker.addEventListener('message', (ev) => { + const { type } = ev.data ?? {}; + + if (type === 'requestSession') { sendSessionToSW(); } }); - - // When restored from bfcache (important on iOS) - window.addEventListener('pageshow', sendSessionToSW); } const mountApp = () => { diff --git a/src/sw.ts b/src/sw.ts index d4eebc02..69293b1d 100644 --- a/src/sw.ts +++ b/src/sw.ts @@ -3,14 +3,6 @@ export type {}; declare const self: ServiceWorkerGlobalScope; -self.addEventListener('install', () => { - self.skipWaiting(); -}); - -self.addEventListener('activate', (event: ExtendableEvent) => { - event.waitUntil(self.clients.claim()); -}); - type SessionInfo = { accessToken: string; baseUrl: string; @@ -21,6 +13,9 @@ type SessionInfo = { */ const sessions = new Map(); +const clientToResolve = new Map void>(); +const clientToSessionPromise = new Map>(); + async function cleanupDeadClients() { const activeClients = await self.clients.matchAll(); const activeIds = new Set(activeClients.map((c) => c.id)); @@ -28,10 +23,72 @@ async function cleanupDeadClients() { 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 { + 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 { + const client = await self.clients.get(clientId); + if (!client) return undefined; + + const sessionPromise = requestSession(client); + + const timeout = new Promise((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 */ @@ -41,23 +98,28 @@ self.addEventListener('message', (event: ExtendableMessageEvent) => { const { type, accessToken, baseUrl } = event.data || {}; - if (type !== 'setSession') return; - - cleanupDeadClients(); - - if (typeof accessToken === 'string' && typeof baseUrl === 'string') { - sessions.set(client.id, { accessToken, baseUrl }); - } else { - // Logout or invalid session - sessions.delete(client.id); + if (type === 'setSession') { + setSession(client.id, accessToken, baseUrl); + cleanupDeadClients(); } }); -function validMediaRequest(url: string, baseUrl: string): boolean { - const downloadUrl = new URL('/_matrix/client/v1/media/download', baseUrl); - const thumbnailUrl = new URL('/_matrix/client/v1/media/thumbnail', baseUrl); +const MEDIA_PATHS = ['/_matrix/client/v1/media/download', '/_matrix/client/v1/media/thumbnail']; - return url.startsWith(downloadUrl.href) || url.startsWith(thumbnailUrl.href); +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 { @@ -72,13 +134,25 @@ function fetchConfig(token: string): RequestInit { self.addEventListener('fetch', (event: FetchEvent) => { const { url, method } = event.request; - if (method !== 'GET') return; - if (!event.clientId) return; + if (method !== 'GET' || !mediaPath(url)) return; - const session = sessions.get(event.clientId); - if (!session) return; + const { clientId } = event; + if (!clientId) return; - if (!validMediaRequest(url, session.baseUrl)) return; + const session = sessions.get(clientId); + if (session) { + if (validMediaRequest(url, session.baseUrl)) { + event.respondWith(fetch(url, fetchConfig(session.accessToken))); + } + return; + } - event.respondWith(fetch(url, fetchConfig(session.accessToken))); + event.respondWith( + requestSessionWithTimeout(clientId).then((s) => { + if (s && validMediaRequest(url, s.baseUrl)) { + return fetch(url, fetchConfig(s.accessToken)); + } + return fetch(event.request); + }) + ); });