perf: server navigation

This commit is contained in:
2026-05-18 19:38:08 +02:00
parent 0152ed9dd2
commit afb64520ed
12 changed files with 212 additions and 90 deletions

View File

@@ -111,40 +111,47 @@ export class MessagesSyncEffects {
this.actions$.pipe(
ofType(RoomsActions.joinRoomSuccess, RoomsActions.viewServerSuccess),
withLatestFrom(this.store.select(selectCurrentRoom)),
mergeMap(([{ room }, currentRoom]) => {
const activeRoom = currentRoom || room;
switchMap(([{ room }, currentRoom]) => {
const requestedRoomId = room.id;
if (!activeRoom)
return EMPTY;
return timer(75).pipe(
withLatestFrom(this.store.select(selectCurrentRoom)),
switchMap(([, latestCurrentRoom]) => {
const activeRoom = latestCurrentRoom ?? currentRoom ?? room;
const peers = this.webrtc.getConnectedPeers();
return from(
this.db.getMessages(activeRoom.id, FULL_SYNC_LIMIT, 0)
).pipe(
tap((messages) => {
const count = messages.length;
const lastUpdated = getLatestTimestamp(messages);
for (const pid of this.webrtc.getConnectedPeers()) {
try {
this.webrtc.sendToPeer(pid, {
type: 'chat-sync-summary',
roomId: activeRoom.id,
count,
lastUpdated
});
this.webrtc.sendToPeer(pid, {
type: 'chat-inventory-request',
roomId: activeRoom.id
});
} catch (error) {
this.debugging.warn('messages', 'Failed to kick off room sync for peer', {
error,
peerId: pid,
roomId: activeRoom.id
});
}
if (!activeRoom || activeRoom.id !== requestedRoomId || peers.length === 0) {
return EMPTY;
}
return from(this.db.getMessages(activeRoom.id, FULL_SYNC_LIMIT, 0)).pipe(
tap((messages) => {
const count = messages.length;
const lastUpdated = getLatestTimestamp(messages);
for (const pid of peers) {
try {
this.webrtc.sendToPeer(pid, {
type: 'chat-sync-summary',
roomId: activeRoom.id,
count,
lastUpdated
});
this.webrtc.sendToPeer(pid, {
type: 'chat-inventory-request',
roomId: activeRoom.id
});
} catch (error) {
this.debugging.warn('messages', 'Failed to kick off room sync for peer', {
error,
peerId: pid,
roomId: activeRoom.id
});
}
}
})
);
})
);
})

View File

@@ -50,6 +50,8 @@ import { canEditMessage } from '../../domains/chat/domain/rules/message.rules';
import { resolveRoomPermission } from '../../domains/access-control';
import { dispatchIncomingMessage, IncomingMessageContext } from './messages-incoming.handlers';
const INITIAL_ROOM_MESSAGE_LIMIT = 30;
@Injectable()
export class MessagesEffects {
private readonly actions$ = inject(Actions);
@@ -66,7 +68,7 @@ export class MessagesEffects {
this.actions$.pipe(
ofType(MessagesActions.loadMessages),
switchMap(({ roomId }) =>
from(this.db.getMessages(roomId)).pipe(
from(this.db.getMessages(roomId, INITIAL_ROOM_MESSAGE_LIMIT, 0)).pipe(
mergeMap(async (messages) => {
const hydrated = await hydrateMessages(messages, this.db);

View File

@@ -29,23 +29,20 @@ export type { InventoryItem } from '../../domains/chat/domain/rules/message-sync
/** Hydrates a single message with its reactions from the database. */
export async function hydrateMessage(
msg: Message,
db: DatabaseService
_db: DatabaseService
): Promise<Message> {
if (msg.isDeleted)
return normaliseDeletedMessage(msg);
const reactions = await db.getReactionsForMessage(msg.id);
return reactions.length > 0 ? { ...msg,
reactions } : msg;
return msg;
}
/** Hydrates an array of messages with their reactions. */
export async function hydrateMessages(
messages: Message[],
db: DatabaseService
_db: DatabaseService
): Promise<Message[]> {
return Promise.all(messages.map((msg) => hydrateMessage(msg, db)));
return messages.map((msg) => msg.isDeleted ? normaliseDeletedMessage(msg) : msg);
}
/** Builds a sync inventory item from a message and its reaction count. */

View File

@@ -340,11 +340,12 @@ export class RoomMembersSyncEffects {
const role = room.hostId === currentUser.id
? 'host'
: (isCurrentRoom ? currentUser.role : existingMember?.role ?? 'member');
const seenAt = existingMember?.lastSeenAt ?? currentUser.joinedAt ?? Date.now();
return {
...roomMemberFromUser(currentUser, Date.now(), role),
...roomMemberFromUser(currentUser, seenAt, role),
id: existingMember?.id ?? currentUser.id,
joinedAt: existingMember?.joinedAt ?? currentUser.joinedAt ?? Date.now(),
joinedAt: existingMember?.joinedAt ?? currentUser.joinedAt ?? seenAt,
avatarUrl: currentUser.avatarUrl ?? existingMember?.avatarUrl,
role
};

View File

@@ -12,7 +12,8 @@ import {
of,
from,
EMPTY,
merge
merge,
timer
} from 'rxjs';
import {
map,
@@ -60,6 +61,8 @@ type BlockedRoomAccessAction =
| ReturnType<typeof RoomsActions.forgetRoom>
| ReturnType<typeof RoomsActions.joinRoomFailure>;
const VIEW_SERVER_LOAD_DELAY_MS = 75;
@Injectable()
export class RoomsEffects {
private actions$ = inject(Actions);
@@ -608,7 +611,12 @@ export class RoomsEffects {
navigationRequestVersion
});
this.router.navigate(['/room', room.id]);
window.setTimeout(() => {
if (this.signalingConnection.isCurrentRoomNavigation(room.id, navigationRequestVersion)) {
void this.router.navigate(['/room', room.id]);
}
}, 0);
return of(RoomsActions.viewServerSuccess({ room }));
};
@@ -634,7 +642,9 @@ export class RoomsEffects {
onViewServerSuccess$ = createEffect(() =>
this.actions$.pipe(
ofType(RoomsActions.viewServerSuccess),
mergeMap(({ room }) => [MessagesActions.loadMessages({ roomId: room.id }), UsersActions.loadBans()])
switchMap(({ room }) => timer(VIEW_SERVER_LOAD_DELAY_MS).pipe(
mergeMap(() => [MessagesActions.loadMessages({ roomId: room.id }), UsersActions.loadBans()])
))
)
);

View File

@@ -42,6 +42,20 @@ function getDefaultTextChannelId(room: Room): string {
return resolveActiveTextChannelId(enrichRoom(room).channels, 'general');
}
function activateRoomView(state: RoomsState, room: Room, isConnecting: boolean): RoomsState {
const enriched = enrichRoom(room);
return {
...state,
currentRoom: enriched,
savedRooms: upsertRoom(state.savedRooms, enriched),
isConnecting,
signalServerCompatibilityError: null,
isConnected: true,
activeChannelId: getDefaultTextChannelId(enriched)
};
}
/** Upsert a room into a saved-rooms list (add or replace by id) */
function upsertRoom(savedRooms: Room[], room: Room): Room[] {
const normalizedRoom = enrichRoom(room);
@@ -220,27 +234,24 @@ export const roomsReducer = createReducer(
})),
// View server - just switch the viewed room, stay connected
on(RoomsActions.viewServer, (state) => ({
...state,
isConnecting: true,
signalServerCompatibilityError: null,
error: null
})),
on(RoomsActions.viewServerSuccess, (state, { room }) => {
const enriched = enrichRoom(room);
on(RoomsActions.viewServer, (state, { room, skipBanCheck }) => {
if (skipBanCheck) {
return {
...activateRoomView(state, room, true),
error: null
};
}
return {
...state,
currentRoom: enriched,
savedRooms: upsertRoom(state.savedRooms, enriched),
isConnecting: false,
isConnecting: true,
signalServerCompatibilityError: null,
isConnected: true,
activeChannelId: getDefaultTextChannelId(enriched)
error: null
};
}),
on(RoomsActions.viewServerSuccess, (state, { room }) => activateRoomView(state, room, false)),
// Update room settings
on(RoomsActions.updateRoomSettings, (state) => ({
...state,