From 7bbcbb841c1ddb16b56d7dcf297a79ba125bc4ec Mon Sep 17 00:00:00 2001 From: 7w1 Date: Mon, 4 May 2026 18:33:05 -0500 Subject: [PATCH 1/5] fixes to reduce background client syncs + increase poll timeout to 30s increase the timeout for background clients to 60s, separates them into their own store, and makes sliding sync respect the setting instead of the client setting, and hopefully better management adds a startup timeout of 45s to the main loading screen to prevent anything from getting stuck behind that which may or may not actually do anything. so much writing for such little change :( --- .../pages/client/BackgroundNotifications.tsx | 61 +++++++++++---- src/client/initMatrix.ts | 76 ++++++++++++++++--- src/types/matrix-sdk.ts | 2 +- 3 files changed, 114 insertions(+), 25 deletions(-) diff --git a/src/app/pages/client/BackgroundNotifications.tsx b/src/app/pages/client/BackgroundNotifications.tsx index 35c196348..79be8d1b4 100644 --- a/src/app/pages/client/BackgroundNotifications.tsx +++ b/src/app/pages/client/BackgroundNotifications.tsx @@ -3,6 +3,7 @@ import type { MatrixClient, MatrixEvent, Room } from '$types/matrix-sdk'; import { ClientEvent, createClient, + IndexedDBStore, MatrixEventEvent, RoomEvent, SyncState, @@ -47,6 +48,10 @@ import { mobileOrTablet } from '$utils/user-agent'; const log = createLogger('BackgroundNotifications'); const debugLog = createDebugLogger('BackgroundNotifications'); + +const BACKGROUND_SYNC_POLL_TIMEOUT_MS = 60_000; +const BACKGROUND_STAGGER_DELAY_MS = 5_000; + const isClientReadyForNotifications = (state: SyncState | string | null): boolean => state === SyncState.Prepared || state === SyncState.Syncing || state === SyncState.Catchup; @@ -54,18 +59,36 @@ const startBackgroundClient = async ( session: Session, slidingSyncConfig: ReturnType['slidingSync'] ): Promise => { + const storeName = { + sync: `bg-sync${session.userId}`, + crypto: `bg-crypto${session.userId}`, + rustCryptoPrefix: `bg-sync${session.userId}`, + }; + + const indexedDBStore = new IndexedDBStore({ + indexedDB: global.indexedDB, + localStorage: global.localStorage, + dbName: storeName.sync, + }); + const mx = createClient({ baseUrl: session.baseUrl, accessToken: session.accessToken, userId: session.userId, deviceId: session.deviceId, + store: indexedDBStore, timelineSupport: false, }); - await startClient(mx, { + + const startOpts = { baseUrl: session.baseUrl, - slidingSync: slidingSyncConfig, + slidingSync: session.slidingSyncOptIn ? slidingSyncConfig : undefined, sessionSlidingSyncOptIn: session.slidingSyncOptIn, - }); + pollTimeoutMs: BACKGROUND_SYNC_POLL_TIMEOUT_MS, + timelineLimit: 1, + }; + + await startClient(mx, startOpts); return mx; }; @@ -136,9 +159,11 @@ export function BackgroundNotifications() { // listeners before stopping a background client. const clientCleanupRef = useRef(new Map()); + const activeUserId = activeSessionId ?? sessions[0]?.userId; + const inactiveSessions = useMemo( - () => sessions.filter((s) => s.userId !== (activeSessionId ?? sessions[0]?.userId)), - [sessions, activeSessionId] + () => sessions.filter((s) => s.userId !== activeUserId), + [sessions, activeUserId] ); // Ref so retry setTimeout callbacks can access the current session list // without stale closures. @@ -540,20 +565,30 @@ export function BackgroundNotifications() { }); }; - inactiveSessions.forEach((session) => { - if (!current.has(session.userId)) startSession(session); + const pendingSessions = inactiveSessions.filter((s) => !current.has(s.userId)); + const staggerTimers: ReturnType[] = []; + pendingSessions.forEach((session, idx) => { + if (idx === 0) { + startSession(session); + } else { + staggerTimers.push( + setTimeout(() => startSession(session), idx * BACKGROUND_STAGGER_DELAY_MS) + ); + } }); - // Capture the cleanup map for this effect instance so teardown runs against - // the listeners registered while this effect was active. const cleanupMap = clientCleanupRef.current; + const activeUserIds = new Set(inactiveSessions.map((s) => s.userId)); return () => { + staggerTimers.forEach(clearTimeout); current.forEach((mx, userId) => { - cleanupMap.get(userId)?.(); - cleanupMap.delete(userId); - stopClient(mx); + if (!activeUserIds.has(userId)) { + cleanupMap.get(userId)?.(); + cleanupMap.delete(userId); + stopClient(mx); + current.delete(userId); + } }); - current.clear(); }; }, [ clientConfig.slidingSync, diff --git a/src/client/initMatrix.ts b/src/client/initMatrix.ts index 25249718c..6a7ba0043 100644 --- a/src/client/initMatrix.ts +++ b/src/client/initMatrix.ts @@ -2,6 +2,7 @@ import type { CryptoCallbacks, MatrixClient, ISyncStateData } from '$types/matri import { ClientEvent, createClient, + Filter, IndexedDBStore, IndexedDBCryptoStore, SyncState, @@ -26,7 +27,7 @@ const classicSyncObserverByClient = new WeakMap< MatrixClient, (state: SyncState, prevState: SyncState | null, data?: ISyncStateData) => void >(); -const FAST_SYNC_POLL_TIMEOUT_MS = 10000; +const FAST_SYNC_POLL_TIMEOUT_MS = 30_000; const SLIDING_SYNC_POLL_TIMEOUT_MS = 20000; type SyncTransport = 'classic' | 'sliding'; type SyncTransportReason = @@ -315,7 +316,9 @@ export const initClient = async (session: Session): Promise => { const wipeAllStores = async () => { log.warn('initClient: wiping all stores for', session.userId); - debugLog.warn('sync', 'Wiping all stores due to mismatch', { userId: session.userId }); + debugLog.warn('sync', 'Wiping all stores due to mismatch', { + userId: session.userId, + }); Sentry.addBreadcrumb({ category: 'crypto', message: 'Crypto store mismatch — wiping local stores and retrying', @@ -353,18 +356,24 @@ export const initClient = async (session: Session): Promise => { } try { - await mx.initRustCrypto({ cryptoDatabasePrefix: storeName.rustCryptoPrefix }); + await mx.initRustCrypto({ + cryptoDatabasePrefix: storeName.rustCryptoPrefix, + }); } catch (err) { if (!isMismatch(err)) { debugLog.error('sync', 'Failed to initialize crypto', { error: err }); throw err; } log.warn('initClient: mismatch on initRustCrypto — wiping and retrying:', err); - debugLog.warn('sync', 'Crypto init mismatch - wiping stores and retrying', { error: err }); + debugLog.warn('sync', 'Crypto init mismatch - wiping stores and retrying', { + error: err, + }); mx.stopClient(); await wipeAllStores(); mx = await buildClient(session); - await mx.initRustCrypto({ cryptoDatabasePrefix: storeName.rustCryptoPrefix }); + await mx.initRustCrypto({ + cryptoDatabasePrefix: storeName.rustCryptoPrefix, + }); } mx.setMaxListeners(50); @@ -375,6 +384,8 @@ export type StartClientConfig = { baseUrl?: string; slidingSync?: SlidingSyncConfig; sessionSlidingSyncOptIn?: boolean; + pollTimeoutMs?: number; + timelineLimit?: number; }; export type ClientSyncDiagnostics = SyncTransportMeta & { @@ -415,7 +426,12 @@ export const startClient = async (mx: MatrixClient, config?: StartClientConfig): hasProxy: hasSlidingProxy, }); - const startClassicSync = async (fallbackFromSliding: boolean, reason: SyncTransportReason) => { + const CLASSIC_SYNC_STARTUP_TIMEOUT_MS = 45_000; + + const startClassicSync = async ( + fallbackFromSliding: boolean, + reason: SyncTransportReason + ): Promise => { syncTransportByClient.set(mx, { transport: 'classic', slidingConfigured: slidingEnabledOnServer, @@ -426,13 +442,44 @@ export const startClient = async (mx: MatrixClient, config?: StartClientConfig): reason, }); Sentry.metrics.count('sable.sync.transport', 1, { - attributes: { transport: 'classic', reason, fallback: String(fallbackFromSliding) }, + attributes: { + transport: 'classic', + reason, + fallback: String(fallbackFromSliding), + }, }); - await mx.startClient({ + + const startupTimeout = new Promise((resolve) => { + window.setTimeout(() => { + debugLog.warn('sync', 'Classic sync startup timed out', { + userId: mx.getUserId(), + timeoutMs: CLASSIC_SYNC_STARTUP_TIMEOUT_MS, + }); + resolve(); + }, CLASSIC_SYNC_STARTUP_TIMEOUT_MS); + }); + + const effectivePollTimeout = config?.pollTimeoutMs ?? FAST_SYNC_POLL_TIMEOUT_MS; + const effectiveTimelineLimit = config?.timelineLimit ?? 10; + + const classicFilter = new Filter(mx.getUserId() ?? undefined); + classicFilter.setTimelineLimit(effectiveTimelineLimit); + // Ensure lazy loading stays on (carried by buildDefaultFilter but explicit here + // since we replace the filter entirely rather than merging). + const filterDefinition = classicFilter.getDefinition(); + if (filterDefinition.room) { + filterDefinition.room.timeline = filterDefinition.room.timeline ?? {}; + (filterDefinition.room.timeline as { lazy_load_members?: boolean }).lazy_load_members = true; + } + + const syncStarted = mx.startClient({ lazyLoadMembers: true, - pollTimeout: FAST_SYNC_POLL_TIMEOUT_MS, + pollTimeout: effectivePollTimeout, threadSupport: true, + filter: classicFilter, }); + + await Promise.race([syncStarted, startupTimeout]); // Attach an ongoing classic-sync observer — equivalent to SlidingSyncManager's // onLifecycle listener. Tracks state transitions, initial-sync timing, and errors. let classicSyncCount = 0; @@ -584,7 +631,11 @@ export const startClient = async (mx: MatrixClient, config?: StartClientConfig): reason: 'sliding_active', }); Sentry.metrics.count('sable.sync.transport', 1, { - attributes: { transport: 'sliding', reason: 'sliding_active', fallback: 'false' }, + attributes: { + transport: 'sliding', + reason: 'sliding_active', + fallback: 'false', + }, }); try { @@ -649,7 +700,10 @@ export const getClientSyncDiagnostics = (mx: MatrixClient): ClientSyncDiagnostic * so the correct Jotai Provider store is used. */ export const logoutClient = async (mx: MatrixClient, session?: Session) => { - log.log('logoutClient', { userId: mx.getUserId(), sessionUserId: session?.userId }); + log.log('logoutClient', { + userId: mx.getUserId(), + sessionUserId: session?.userId, + }); debugLog.info('general', 'Logging out client', { userId: mx.getUserId() }); pushSessionToSW(); stopClient(mx); diff --git a/src/types/matrix-sdk.ts b/src/types/matrix-sdk.ts index 6a51b180a..d05073f2e 100644 --- a/src/types/matrix-sdk.ts +++ b/src/types/matrix-sdk.ts @@ -12,7 +12,7 @@ export * from 'matrix-js-sdk/lib/sliding-sync'; export * from 'matrix-js-sdk/lib/sync-accumulator'; export * from 'matrix-js-sdk/lib/scheduler'; export * from 'matrix-js-sdk/lib/store/memory'; -export { createClient } from 'matrix-js-sdk/lib/matrix'; +export { createClient, Filter } from 'matrix-js-sdk/lib/matrix'; export * from 'matrix-js-sdk/lib/models/event'; export * from 'matrix-js-sdk/lib/models/room'; From 663aa4f876da42dee91931a8b8242b4a53ec50ee Mon Sep 17 00:00:00 2001 From: 7w1 Date: Mon, 4 May 2026 18:38:45 -0500 Subject: [PATCH 2/5] fix lint issue from other pr --- src/app/utils/msc4459helper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/utils/msc4459helper.ts b/src/app/utils/msc4459helper.ts index d999130d5..426484aad 100644 --- a/src/app/utils/msc4459helper.ts +++ b/src/app/utils/msc4459helper.ts @@ -47,7 +47,7 @@ function getImagePackReferencesForMxcInternal( .map((pack) => { const img = pack.getImages(imageUsage).find((val) => val.url === mxcUrl); const room = matrixClient.getRoom(pack.address?.roomId); - if (!room || (isRoomPrivate(matrixClient, room) && !bypassPrivateFilter)) return; + if (!room || (isRoomPrivate(matrixClient, room) && !bypassPrivateFilter)) return undefined; const viaServers = new SerializableSet(); if (room) getViaServers(room).forEach((via) => { From 377c6fdcb1c2be0b3a83cb7b28ed8cc26a67702e Mon Sep 17 00:00:00 2001 From: 7w1 Date: Mon, 4 May 2026 18:40:29 -0500 Subject: [PATCH 3/5] changeset --- .changeset/fix_background_sync.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fix_background_sync.md diff --git a/.changeset/fix_background_sync.md b/.changeset/fix_background_sync.md new file mode 100644 index 000000000..d60aef4fe --- /dev/null +++ b/.changeset/fix_background_sync.md @@ -0,0 +1,5 @@ +--- +default: patch +--- + +Some fixes to sync requests on loading screen and for multi-account background syncing. From e4cf72ed77e67834e9b504230cec915504f1ba02 Mon Sep 17 00:00:00 2001 From: 7w1 Date: Mon, 4 May 2026 20:03:44 -0500 Subject: [PATCH 4/5] bypass sdk thread root spam on initial load --- src/client/initMatrix.ts | 69 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 63 insertions(+), 6 deletions(-) diff --git a/src/client/initMatrix.ts b/src/client/initMatrix.ts index 6a7ba0043..1e6ca9c84 100644 --- a/src/client/initMatrix.ts +++ b/src/client/initMatrix.ts @@ -48,8 +48,54 @@ type SyncTransportMeta = { reason: SyncTransportReason; }; const syncTransportByClient = new WeakMap(); +const fetchRoomEventStartupCleanupByClient = new WeakMap void>(); const COLD_CACHE_BOOTSTRAP_TIMEOUT_MS = 20000; +type FetchRoomEventResult = Awaited>; +type MatrixClientWithWritableFetchRoomEvent = MatrixClient & { + fetchRoomEvent: (roomId: string, eventId: string) => Promise; +}; + +// Replace fetchRoomEvent for first sync so thread roots don't each trigger GET /event +// Uses cached timeline events when present otherwise a stub that fetches when user opens thread +function installStartupFetchRoomEventPatch(mx: MatrixClient): void { + fetchRoomEventStartupCleanupByClient.get(mx)?.(); + + const mxWritable = mx as MatrixClientWithWritableFetchRoomEvent; + const origFetchRoomEvent = mx.fetchRoomEvent.bind(mx); + let restored = false; + + const restore = () => { + if (restored) return; + restored = true; + fetchRoomEventStartupCleanupByClient.delete(mx); + // Put the real fetchRoomEvent back and detach this + mxWritable.fetchRoomEvent = origFetchRoomEvent; + mx.off(ClientEvent.Sync, onSync); + }; + + const onSync = (state: SyncState) => { + // Initial sync burst is over, let normal server fetches run again + if (state === SyncState.Prepared || state === SyncState.Syncing) { + restore(); + } + }; + + mxWritable.fetchRoomEvent = (roomId: string, eventId: string) => { + if (restored) return origFetchRoomEvent(roomId, eventId); + const cachedEvent = mx.getRoom(roomId)?.findEventById(eventId); + // Reuse sync payload instead of another GET when we already have the root. + const payload: FetchRoomEventResult = cachedEvent?.event ?? { + event_id: eventId, + room_id: roomId, + }; + return Promise.resolve(payload); + }; + + mx.on(ClientEvent.Sync, onSync); + fetchRoomEventStartupCleanupByClient.set(mx, restore); +} + export const resolveSlidingEnabled = (enabled: SlidingSyncConfig['enabled']): boolean => { if (enabled === undefined) return false; if (typeof enabled === 'boolean') return enabled; @@ -472,12 +518,20 @@ export const startClient = async (mx: MatrixClient, config?: StartClientConfig): (filterDefinition.room.timeline as { lazy_load_members?: boolean }).lazy_load_members = true; } - const syncStarted = mx.startClient({ - lazyLoadMembers: true, - pollTimeout: effectivePollTimeout, - threadSupport: true, - filter: classicFilter, - }); + installStartupFetchRoomEventPatch(mx); + + let syncStarted: Promise; + try { + syncStarted = mx.startClient({ + lazyLoadMembers: true, + pollTimeout: effectivePollTimeout, + threadSupport: true, + filter: classicFilter, + }); + } catch (syncErr) { + fetchRoomEventStartupCleanupByClient.get(mx)?.(); + throw syncErr; + } await Promise.race([syncStarted, startupTimeout]); // Attach an ongoing classic-sync observer — equivalent to SlidingSyncManager's @@ -639,12 +693,14 @@ export const startClient = async (mx: MatrixClient, config?: StartClientConfig): }); try { + installStartupFetchRoomEventPatch(mx); await mx.startClient({ lazyLoadMembers: true, slidingSync: manager.slidingSync, threadSupport: true, }); } catch (err) { + fetchRoomEventStartupCleanupByClient.get(mx)?.(); debugLog.error('network', 'Failed to start client with sliding sync', { error: err instanceof Error ? err.message : String(err), userId: mx.getUserId(), @@ -659,6 +715,7 @@ export const startClient = async (mx: MatrixClient, config?: StartClientConfig): export const stopClient = (mx: MatrixClient): void => { log.log('stopClient', mx.getUserId()); debugLog.info('sync', 'Stopping client', { userId: mx.getUserId() }); + fetchRoomEventStartupCleanupByClient.get(mx)?.(); disposeSlidingSync(mx); const classicSyncListener = classicSyncObserverByClient.get(mx); if (classicSyncListener) { From 04c8f40ce6c13e6e07f196855bbaa5350ed40329 Mon Sep 17 00:00:00 2001 From: 7w1 Date: Mon, 4 May 2026 20:09:15 -0500 Subject: [PATCH 5/5] Update fix_background_sync.md --- .changeset/fix_background_sync.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/fix_background_sync.md b/.changeset/fix_background_sync.md index d60aef4fe..076b9a503 100644 --- a/.changeset/fix_background_sync.md +++ b/.changeset/fix_background_sync.md @@ -2,4 +2,4 @@ default: patch --- -Some fixes to sync requests on loading screen and for multi-account background syncing. +Some fixes to sync requests being spammed on loading screen and for multi-account background syncing, it should also load faster now!