import { ClientEvent, createClient, MemoryStore, SyncState, type MatrixClient, type Room, } from 'matrix-js-sdk'; import { getCurrentSession, type Session } from '@cinny/app/state/sessions'; export type LiveConnectMode = 'mock' | 'live'; export type ManualSessionInput = { baseUrl: string; userId: string; deviceId: string; accessToken: string; }; const MANUAL_SESSION_KEY = 'playgroundManualSession'; /** Session already used by Paarrot/Cinny on this origin (localStorage). */ export function peekPaarrotSession(): Session | undefined { return getCurrentSession(); } export function loadManualSession(): ManualSessionInput | null { try { const raw = sessionStorage.getItem(MANUAL_SESSION_KEY); if (!raw) return null; const parsed = JSON.parse(raw) as ManualSessionInput; if (parsed?.baseUrl && parsed?.userId && parsed?.deviceId && parsed?.accessToken) { return parsed; } } catch { /* ignore */ } return null; } export function saveManualSession(input: ManualSessionInput | null) { if (!input) { sessionStorage.removeItem(MANUAL_SESSION_KEY); return; } sessionStorage.setItem(MANUAL_SESSION_KEY, JSON.stringify(input)); } function normalizeSession(input: Session | ManualSessionInput): Session { return { baseUrl: input.baseUrl.replace(/\/$/, ''), userId: input.userId, deviceId: input.deviceId, accessToken: input.accessToken, }; } function resolveSession(explicit?: Session | ManualSessionInput): Session { if (explicit) return normalizeSession(explicit); const fromPaarrot = peekPaarrotSession(); if (fromPaarrot) return normalizeSession(fromPaarrot); const manual = loadManualSession(); if (manual) return normalizeSession(manual); throw new Error( 'No Paarrot session found. Log in at / (same origin), or paste homeserver + token in the playground.' ); } function waitForPrepared(mx: MatrixClient, timeoutMs = 90_000): Promise { return new Promise((resolve, reject) => { const finish = (ok: boolean, err?: Error) => { clearTimeout(timer); mx.off(ClientEvent.Sync, onSync); if (ok) resolve(); else reject(err ?? new Error('Sync failed')); }; const timer = window.setTimeout( () => finish(false, new Error('Timed out waiting for Matrix sync')), timeoutMs ); const onSync = (state: SyncState | null) => { if (state === SyncState.Prepared || state === SyncState.Syncing) { finish(true); } }; mx.on(ClientEvent.Sync, onSync); const cur = mx.getSyncState(); if (cur === SyncState.Prepared || cur === SyncState.Syncing) { finish(true); } }); } /** * Live Matrix client for the playground. * Uses MemoryStore so it won't fight Paarrot's IndexedDB sync/crypto stores. * Skips Rust crypto — encrypted timelines won't decrypt, but profiles/rooms/settings APIs work. */ export async function connectLiveMatrixClient( session?: Session | ManualSessionInput ): Promise { const finalSession = resolveSession(session); const mx = createClient({ baseUrl: finalSession.baseUrl, accessToken: finalSession.accessToken, userId: finalSession.userId, deviceId: finalSession.deviceId, store: new MemoryStore({ localStorage: globalThis.localStorage }), timelineSupport: true, }); mx.setMaxListeners(50); await mx.startClient({ lazyLoadMembers: true, initialSyncLimit: 20, }); await waitForPrepared(mx); return mx; } export function stopLiveMatrixClient(mx: MatrixClient | null | undefined) { if (!mx) return; try { mx.stopClient(); } catch { /* ignore */ } } export function listJoinedRooms(mx: MatrixClient): Room[] { return mx .getRooms() .filter((r) => r.getMyMembership() === 'join') .sort((a, b) => (a.name || a.roomId).localeCompare(b.name || b.roomId)); } export function pickDefaultRoom(mx: MatrixClient, preferredId?: string): Room | undefined { const rooms = listJoinedRooms(mx); if (preferredId) { const hit = rooms.find((r) => r.roomId === preferredId); if (hit) return hit; } return rooms[0]; }