Request session info from sw if missing (#2664)

* 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>
This commit is contained in:
Ajay Bura 2026-02-21 12:21:27 +05:30 committed by GitHub
parent 29ec172c8b
commit 170f5cd473
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 105 additions and 36 deletions

View file

@ -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={

View file

@ -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 = () => {

128
src/sw.ts
View file

@ -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<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));
@ -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<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
*/
@ -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);
})
);
});