fix: multiple bug fixes

isolated users, db backup, weird disconnect issues for long voice sessions,
This commit is contained in:
2026-04-24 22:19:57 +02:00
parent 44588e8789
commit bc2fa7de22
56 changed files with 1861 additions and 133 deletions

View File

@@ -235,6 +235,71 @@ describe('users reducer - status', () => {
// The buildPresenceAwareUser function takes incoming status when non-offline
expect(state.entities['u1']?.status).toBe('online');
});
it('preserves omitted live peer presence and voice state during stale server snapshot', () => {
const remoteUser = createUser({
id: 'u2',
oderId: 'u2',
displayName: 'Voice Peer',
presenceServerIds: ['s1'],
voiceState: {
isConnected: true,
isMuted: false,
isDeafened: false,
isSpeaking: true,
roomId: 'voice-1',
serverId: 's1'
},
cameraState: { isEnabled: true },
screenShareState: { isSharing: true }
});
const withUser = usersReducer(baseState, UsersActions.userJoined({ user: remoteUser }));
const state = usersReducer(withUser, UsersActions.syncServerPresence({
roomId: 's1',
users: [],
connectedPeerIds: ['u2']
}));
expect(state.entities['u2']?.presenceServerIds).toEqual(['s1']);
expect(state.entities['u2']?.isOnline).toBe(true);
expect(state.entities['u2']?.voiceState?.isConnected).toBe(true);
expect(state.entities['u2']?.voiceState?.roomId).toBe('voice-1');
expect(state.entities['u2']?.cameraState?.isEnabled).toBe(true);
expect(state.entities['u2']?.screenShareState?.isSharing).toBe(true);
});
it('clears omitted peer live state when transport is gone', () => {
const remoteUser = createUser({
id: 'u3',
oderId: 'u3',
displayName: 'Dropped Peer',
presenceServerIds: ['s1'],
voiceState: {
isConnected: true,
isMuted: false,
isDeafened: false,
isSpeaking: true,
roomId: 'voice-1',
serverId: 's1'
},
cameraState: { isEnabled: true },
screenShareState: { isSharing: true }
});
const withUser = usersReducer(baseState, UsersActions.userJoined({ user: remoteUser }));
const state = usersReducer(withUser, UsersActions.syncServerPresence({
roomId: 's1',
users: [],
connectedPeerIds: []
}));
expect(state.entities['u3']?.presenceServerIds).toBeUndefined();
expect(state.entities['u3']?.isOnline).toBe(false);
expect(state.entities['u3']?.status).toBe('offline');
expect(state.entities['u3']?.voiceState?.isConnected).toBe(false);
expect(state.entities['u3']?.voiceState?.roomId).toBeUndefined();
expect(state.entities['u3']?.cameraState?.isEnabled).toBe(false);
expect(state.entities['u3']?.screenShareState?.isSharing).toBe(false);
});
});
describe('manual status overrides auto idle', () => {

View File

@@ -18,6 +18,7 @@ import {
export const UsersActions = createActionGroup({
source: 'Users',
events: {
'Authenticate User': props<{ user: User }>(),
'Load Current User': emptyProps(),
'Load Current User Success': props<{ user: User }>(),
'Load Current User Failure': props<{ error: string }>(),
@@ -31,7 +32,7 @@ export const UsersActions = createActionGroup({
'User Joined': props<{ user: User }>(),
'User Left': props<{ userId: string; serverId?: string; serverIds?: string[] }>(),
'Sync Server Presence': props<{ roomId: string; users: User[] }>(),
'Sync Server Presence': props<{ roomId: string; users: User[]; connectedPeerIds?: string[] }>(),
'Update User': props<{ userId: string; updates: Partial<User> }>(),
'Update User Role': props<{ userId: string; role: User['role'] }>(),
@@ -58,6 +59,7 @@ export const UsersActions = createActionGroup({
'Sync Users': props<{ users: User[] }>(),
'Clear Users': emptyProps(),
'Reset Users State': emptyProps(),
'Update Host': props<{ userId: string }>(),
'Update Voice State': props<{ userId: string; voiceState: Partial<VoiceState> }>(),

View File

@@ -23,6 +23,7 @@ import {
switchMap
} from 'rxjs/operators';
import { v4 as uuidv4 } from 'uuid';
import { MessagesActions } from '../messages/messages.actions';
import { UsersActions } from './users.actions';
import { RoomsActions } from '../rooms/rooms.actions';
import {
@@ -46,6 +47,9 @@ import {
Room,
User
} from '../../shared-kernel';
import {
setStoredCurrentUserId
} from '../../core/storage/current-user-storage';
import { findRoomMember, removeRoomMember } from '../rooms/room-members.helpers';
type IncomingModerationExtraAction =
@@ -65,6 +69,27 @@ export class UsersEffects {
private serverDirectory = inject(ServerDirectoryFacade);
private webrtc = inject(RealtimeSessionFacade);
/** Prepares persisted state for a successful login before exposing the user in-memory. */
authenticateUser$ = createEffect(() =>
this.actions$.pipe(
ofType(UsersActions.authenticateUser),
switchMap(({ user }) =>
from(this.prepareAuthenticatedUserStorage(user.id)).pipe(
mergeMap(() => [
MessagesActions.clearMessages(),
UsersActions.resetUsersState(),
RoomsActions.resetRoomsState(),
UsersActions.setCurrentUser({ user }),
RoomsActions.loadRooms()
]),
catchError((error: Error) =>
of(UsersActions.loadCurrentUserFailure({ error: error.message || 'Failed to prepare local user state.' }))
)
)
)
)
);
// Load current user from storage
/** Loads the persisted current user from the local database on startup. */
loadCurrentUser$ = createEffect(() =>
@@ -124,6 +149,11 @@ export class UsersEffects {
};
}
private async prepareAuthenticatedUserStorage(userId: string): Promise<void> {
setStoredCurrentUserId(userId);
await this.db.initialize();
}
/** Loads all users associated with a specific room from the local database. */
loadRoomUsers$ = createEffect(() =>
this.actions$.pipe(

View File

@@ -30,6 +30,16 @@ function mergePresenceServerIds(
return normalizePresenceServerIds([...(existingServerIds ?? []), ...(incomingServerIds ?? [])]);
}
function hasLivePeerTransport(user: User, connectedPeerIds: ReadonlySet<string>): boolean {
if (connectedPeerIds.size === 0) {
return false;
}
return connectedPeerIds.has(user.id)
|| connectedPeerIds.has(user.oderId)
|| (!!user.peerId && connectedPeerIds.has(user.peerId));
}
interface AvatarFields {
avatarUrl?: string;
avatarHash?: string;
@@ -262,6 +272,10 @@ export const initialState: UsersState = usersAdapter.getInitialState({
export const usersReducer = createReducer(
initialState,
on(UsersActions.resetUsersState, () => ({
...initialState
})),
on(UsersActions.loadCurrentUser, (state) => ({
...state,
loading: true,
@@ -344,10 +358,11 @@ export const usersReducer = createReducer(
on(UsersActions.userJoined, (state, { user }) =>
usersAdapter.upsertOne(buildPresenceAwareUser(state.entities[user.id], user), state)
),
on(UsersActions.syncServerPresence, (state, { roomId, users }) => {
on(UsersActions.syncServerPresence, (state, { roomId, users, connectedPeerIds }) => {
let nextState = state;
const seenUserIds = new Set<string>();
const connectedPeerIdSet = new Set(connectedPeerIds ?? []);
for (const user of users) {
seenUserIds.add(user.id);
@@ -363,6 +378,7 @@ export const usersReducer = createReducer(
&& user.id !== nextState.currentUserId
&& user.presenceServerIds?.includes(roomId) === true
&& !seenUserIds.has(user.id)
&& !hasLivePeerTransport(user, connectedPeerIdSet)
)
.map((user) => ({
id: user.id,