wip: optimizations

This commit is contained in:
2026-05-23 15:28:40 +02:00
parent 5bf506af03
commit 155fe20862
89 changed files with 7431 additions and 392 deletions

View File

@@ -25,12 +25,14 @@ import {
mergeMap,
catchError,
withLatestFrom,
switchMap
switchMap,
filter
} from 'rxjs/operators';
import { v4 as uuidv4 } from 'uuid';
import { MessagesActions } from './messages.actions';
import { selectCurrentUser } from '../users/users.selectors';
import { selectCurrentRoom, selectSavedRooms } from '../rooms/rooms.selectors';
import { RoomsActions } from '../rooms/rooms.actions';
import { selectMessagesEntities } from './messages.selectors';
import { RealtimeSessionFacade } from '../../core/realtime';
import { DatabaseService } from '../../infrastructure/persistence';
@@ -40,6 +42,7 @@ import { AttachmentFacade } from '../../domains/attachment';
import { hasDedicatedChatEmbed } from '../../domains/chat/domain/rules/link-embed.rules';
import { LinkMetadataService } from '../../domains/chat/application/services/link-metadata.service';
import { TimeSyncService } from '../../core/services/time-sync.service';
import { PlatformService } from '../../core/platform';
import {
DELETED_MESSAGE_CONTENT,
Message,
@@ -52,6 +55,8 @@ import { resolveRoomPermission } from '../../domains/access-control';
import { dispatchIncomingMessage, IncomingMessageContext } from './messages-incoming.handlers';
const INITIAL_ROOM_MESSAGE_LIMIT = 30;
/** Cap on simultaneous browser-cache prefetches for apps with many saved rooms. */
const PREFETCH_CONCURRENCY = 3;
@Injectable()
export class MessagesEffects {
@@ -63,14 +68,26 @@ export class MessagesEffects {
private readonly webrtc = inject(RealtimeSessionFacade);
private readonly timeSync = inject(TimeSyncService);
private readonly linkMetadata = inject(LinkMetadataService);
private readonly platform = inject(PlatformService);
/** Loads messages for a room from the local database, hydrating reactions. */
loadMessages$ = createEffect(() =>
this.actions$.pipe(
ofType(MessagesActions.loadMessages),
withLatestFrom(this.store.select(selectCurrentRoom)),
switchMap(([{ roomId }, currentRoom]) =>
from(this.loadInitialMessages(roomId, currentRoom)).pipe(
withLatestFrom(
this.store.select(selectCurrentRoom),
this.store.select(selectSavedRooms)
),
switchMap(([
{ roomId },
currentRoom,
savedRooms
]) => {
const targetRoom = currentRoom?.id === roomId
? currentRoom
: savedRooms.find((room) => room.id === roomId) ?? null;
return from(this.loadInitialMessages(roomId, targetRoom)).pipe(
mergeMap(async (messages) => {
const hydrated = await hydrateMessages(messages, this.db);
@@ -78,18 +95,73 @@ export class MessagesEffects {
this.attachments.rememberMessageRoom(message.id, message.roomId);
}
void this.attachments.requestAutoDownloadsForRoom(roomId);
return MessagesActions.loadMessagesSuccess({ messages: hydrated });
}),
catchError((error) =>
of(MessagesActions.loadMessagesFailure({ error: error.message }))
)
);
})
)
);
/**
* Background-prefetch initial messages for every saved room after the
* rooms list loads in browser. Electron avoids this path because startup
* IPC prefetch competes with foreground room switches. Results are merged
* into the messages slice via `upsertMany`, leaving the active-room loading
* flag untouched.
*/
prefetchSavedRoomsOnLoad$ = createEffect(() =>
this.actions$.pipe(
ofType(RoomsActions.loadRoomsSuccess),
filter(() => this.platform.isBrowser),
mergeMap(({ rooms }) =>
from(rooms).pipe(
mergeMap(
(room) => of(MessagesActions.prefetchRoomMessages({ roomId: room.id })),
PREFETCH_CONCURRENCY
)
)
)
)
);
prefetchRoomMessages$ = createEffect(() =>
this.actions$.pipe(
ofType(MessagesActions.prefetchRoomMessages),
withLatestFrom(this.store.select(selectSavedRooms)),
mergeMap(
([{ roomId }, savedRooms]) =>
from(this.fetchRoomMessagesForPrefetch(roomId, savedRooms.find((room) => room.id === roomId) ?? null)),
PREFETCH_CONCURRENCY
)
)
);
private async fetchRoomMessagesForPrefetch(roomId: string, targetRoom: Room | null) {
try {
const messages = await this.loadInitialMessages(roomId, targetRoom);
const hydrated = await hydrateMessages(messages, this.db);
for (const message of hydrated) {
this.attachments.rememberMessageRoom(message.id, message.roomId);
}
return MessagesActions.prefetchRoomMessagesSuccess({ messages: hydrated });
} catch (error) {
reportDebuggingError(
this.debugging,
'MessagesEffects.prefetchRoomMessages',
'Failed to prefetch room messages',
{ roomId },
error
);
return MessagesActions.prefetchRoomMessagesSuccess({ messages: [] });
}
}
/** Paginates older messages from the local DB for scroll-up history loading. */
loadOlderMessages$ = createEffect(() =>
this.actions$.pipe(
@@ -119,9 +191,9 @@ export class MessagesEffects {
)
);
private async loadInitialMessages(roomId: string, currentRoom: Room | null): Promise<Message[]> {
const textChannels = currentRoom?.id === roomId
? (currentRoom.channels ?? []).filter((channel) => channel.type === 'text')
private async loadInitialMessages(roomId: string, targetRoom: Room | null): Promise<Message[]> {
const textChannels = targetRoom?.id === roomId
? (targetRoom.channels ?? []).filter((channel) => channel.type === 'text')
: [];
if (textChannels.length <= 1) {