websocket cleanup

This commit is contained in:
seavor 2026-04-20 00:37:23 -05:00
parent 2aeb1542b1
commit 2afa2922e9
18 changed files with 82 additions and 93 deletions

View file

@ -15,6 +15,14 @@ export class ModeratorRequestImpl implements WebsocketTypes.IModeratorRequest {
ModeratorCommands.banFromServer(minutes, userName, address, reason, visibleReason, clientid, removeMessages);
}
forceActivateUser(usernameToActivate: string, moderatorName: string): void {
ModeratorCommands.forceActivateUser(usernameToActivate, moderatorName);
}
getAdminNotes(userName: string): void {
ModeratorCommands.getAdminNotes(userName);
}
getBanHistory(userName: string): void {
ModeratorCommands.getBanHistory(userName);
}
@ -27,6 +35,14 @@ export class ModeratorRequestImpl implements WebsocketTypes.IModeratorRequest {
ModeratorCommands.getWarnList(modName, userName, userClientid);
}
grantReplayAccess(replayId: number, moderatorName: string): void {
ModeratorCommands.grantReplayAccess(replayId, moderatorName);
}
updateAdminNotes(userName: string, notes: string): void {
ModeratorCommands.updateAdminNotes(userName, notes);
}
viewLogHistory(filters: Data.ViewLogHistoryParams): void {
ModeratorCommands.viewLogHistory(filters);
}

View file

@ -1,5 +1,11 @@
import { createAction } from '@reduxjs/toolkit';
import { roomsSlice } from './rooms.reducer';
export const Actions = roomsSlice.actions;
const SignalActions = {
gameCreated: createAction<{ roomId: number }>('rooms/gameCreated'),
};
export const Actions = { ...roomsSlice.actions, ...SignalActions };
export type RoomsAction = ReturnType<typeof Actions[keyof typeof Actions]>;

View file

@ -318,14 +318,6 @@ describe('REMOVE_MESSAGES', () => {
});
describe('GAME_CREATED', () => {
it('returns state unchanged', () => {
const state = makeRoomsState();
const result = roomsReducer(state, Actions.gameCreated({ roomId: 1 }));
expect(result).toEqual(state);
});
});
describe('JOINED_GAME', () => {
it('sets joinedGameIds[roomId][gameId] = true', () => {

View file

@ -190,9 +190,6 @@ export const roomsSlice = createSlice({
state.joinedGameIds[roomId][gameId] = true;
},
// Signal-only; kept for discriminated-union exhaustiveness.
gameCreated: (_state, _action: PayloadAction<{ roomId: number }>) => {},
selectGame: (state, action: PayloadAction<{ roomId: number; gameId: number | undefined }>) => {
const { roomId, gameId } = action.payload;
state.selectedGameIds[roomId] = gameId;

View file

@ -1,6 +1,6 @@
import { roomsSlice } from './rooms.reducer';
import { Actions } from './rooms.actions';
const a = roomsSlice.actions;
const a = Actions;
export const Types = {
CLEAR_STORE: a.clearStore.type,

View file

@ -1,5 +1,36 @@
import { createAction } from '@reduxjs/toolkit';
import { WebsocketTypes } from '@app/websocket/types';
import { serverSlice } from './server.reducer';
export const Actions = serverSlice.actions;
const SignalActions = {
accountAwaitingActivation: createAction<{ options: WebsocketTypes.PendingActivationContext }>('server/accountAwaitingActivation'),
accountActivationFailed: createAction('server/accountActivationFailed'),
accountActivationSuccess: createAction('server/accountActivationSuccess'),
loginSuccessful: createAction<{ options: WebsocketTypes.LoginSuccessContext }>('server/loginSuccessful'),
loginFailed: createAction('server/loginFailed'),
connectionFailed: createAction('server/connectionFailed'),
testConnectionSuccessful: createAction('server/testConnectionSuccessful'),
testConnectionFailed: createAction('server/testConnectionFailed'),
registrationRequiresEmail: createAction('server/registrationRequiresEmail'),
registrationSuccess: createAction('server/registrationSuccess'),
registrationEmailError: createAction<{ error: string }>('server/registrationEmailError'),
registrationPasswordError: createAction<{ error: string }>('server/registrationPasswordError'),
registrationUserNameError: createAction<{ error: string }>('server/registrationUserNameError'),
resetPassword: createAction('server/resetPassword'),
resetPasswordFailed: createAction('server/resetPasswordFailed'),
resetPasswordChallenge: createAction('server/resetPasswordChallenge'),
resetPasswordSuccess: createAction('server/resetPasswordSuccess'),
reloadConfig: createAction('server/reloadConfig'),
shutdownServer: createAction('server/shutdownServer'),
updateServerMessage: createAction('server/updateServerMessage'),
accountPasswordChange: createAction('server/accountPasswordChange'),
addToList: createAction<{ list: string; userName: string }>('server/addToList'),
removeFromList: createAction<{ list: string; userName: string }>('server/removeFromList'),
grantReplayAccess: createAction<{ replayId: number; moderatorName: string }>('server/grantReplayAccess'),
forceActivateUser: createAction<{ usernameToActivate: string; moderatorName: string }>('server/forceActivateUser'),
};
export const Actions = { ...serverSlice.actions, ...SignalActions };
export type ServerAction = ReturnType<typeof Actions[keyof typeof Actions]>;

View file

@ -5,7 +5,6 @@ import { serverReducer, MAX_USER_MESSAGES } from './server.reducer';
import { Actions } from './server.actions';
import {
makeBanHistoryItem,
makePendingActivationContext,
makeDeckList,
makeDeckTreeItem,
makeGame,
@ -62,24 +61,6 @@ describe('Account & Connection', () => {
expect(result.status.connectionAttemptMade).toBe(true);
});
it('ACCOUNT_AWAITING_ACTIVATION → returns state unchanged', () => {
const options = makePendingActivationContext();
const state = makeServerState();
const result = serverReducer(state, Actions.accountAwaitingActivation({ options }));
expect(result).toEqual(state);
});
it('ACCOUNT_ACTIVATION_SUCCESS → returns state unchanged', () => {
const state = makeServerState();
const result = serverReducer(state, Actions.accountActivationSuccess());
expect(result).toEqual(state);
});
it('ACCOUNT_ACTIVATION_FAILED → returns state unchanged', () => {
const state = makeServerState();
const result = serverReducer(state, Actions.accountActivationFailed());
expect(result).toEqual(state);
});
});

View file

@ -185,7 +185,7 @@ export const serverSlice = createSlice({
updateUser: (state, action: PayloadAction<{ user: Partial<Data.ServerInfo_User> }>) => {
if (state.user) {
Object.assign(state.user, action.payload.user);
state.user = create(Data.ServerInfo_UserSchema, { ...state.user, ...action.payload.user });
} else {
state.user = action.payload.user as Data.ServerInfo_User;
}
@ -393,42 +393,15 @@ export const serverSlice = createSlice({
accountEditChanged: (state, action: PayloadAction<{ user: Partial<Data.ServerInfo_User> }>) => {
if (state.user) {
Object.assign(state.user, action.payload.user);
state.user = create(Data.ServerInfo_UserSchema, { ...state.user, ...action.payload.user });
}
},
accountImageChanged: (state, action: PayloadAction<{ user: Partial<Data.ServerInfo_User> }>) => {
if (state.user) {
Object.assign(state.user, action.payload.user);
state.user = create(Data.ServerInfo_UserSchema, { ...state.user, ...action.payload.user });
}
},
// Signal-only action types — no state mutation, defined so type strings are generated
accountAwaitingActivation: (_state, _action: PayloadAction<{ options: WebsocketTypes.PendingActivationContext }>) => {},
accountActivationFailed: (_state) => {},
accountActivationSuccess: (_state) => {},
loginSuccessful: (_state, _action: PayloadAction<{ options: WebsocketTypes.LoginSuccessContext }>) => {},
loginFailed: (_state) => {},
connectionFailed: (_state) => {},
testConnectionSuccessful: (_state) => {},
testConnectionFailed: (_state) => {},
registrationRequiresEmail: (_state) => {},
registrationSuccess: (_state) => {},
registrationEmailError: (_state, _action: PayloadAction<{ error: string }>) => {},
registrationPasswordError: (_state, _action: PayloadAction<{ error: string }>) => {},
registrationUserNameError: (_state, _action: PayloadAction<{ error: string }>) => {},
resetPassword: (_state) => {},
resetPasswordFailed: (_state) => {},
resetPasswordChallenge: (_state) => {},
resetPasswordSuccess: (_state) => {},
reloadConfig: (_state) => {},
shutdownServer: (_state) => {},
updateServerMessage: (_state) => {},
accountPasswordChange: (_state) => {},
addToList: (_state, _action: PayloadAction<{ list: string; userName: string }>) => {},
removeFromList: (_state, _action: PayloadAction<{ list: string; userName: string }>) => {},
grantReplayAccess: (_state, _action: PayloadAction<{ replayId: number; moderatorName: string }>) => {},
forceActivateUser: (_state, _action: PayloadAction<{ usernameToActivate: string; moderatorName: string }>) => {},
},
});

View file

@ -1,6 +1,6 @@
import { serverSlice } from './server.reducer';
import { Actions } from './server.actions';
const a = serverSlice.actions;
const a = Actions;
export const Types = {
INITIALIZED: a.initialized.type,

View file

@ -13,7 +13,7 @@ const ERROR_MESSAGES: Record<number, string> = {
[Response_ResponseCode.RespGameFull]: 'The game is already full.',
[Response_ResponseCode.RespWrongPassword]: 'Wrong password.',
[Response_ResponseCode.RespSpectatorsNotAllowed]: 'Spectators are not allowed in this game.',
[Response_ResponseCode.RespOnlyBuddies]: "This game is only open to its creator's buddies.",
[Response_ResponseCode.RespOnlyBuddies]: 'This game is only open to its creator\'s buddies.',
[Response_ResponseCode.RespUserLevelTooLow]: 'This game is only open to registered users.',
[Response_ResponseCode.RespInIgnoreList]: 'You are being ignored by the creator of this game.',
};

View file

@ -75,7 +75,7 @@ describe('joinGame', () => {
[Response_ResponseCode.RespGameFull, 'The game is already full.'],
[Response_ResponseCode.RespWrongPassword, 'Wrong password.'],
[Response_ResponseCode.RespSpectatorsNotAllowed, 'Spectators are not allowed in this game.'],
[Response_ResponseCode.RespOnlyBuddies, "This game is only open to its creator's buddies."],
[Response_ResponseCode.RespOnlyBuddies, 'This game is only open to its creator\'s buddies.'],
[Response_ResponseCode.RespUserLevelTooLow, 'This game is only open to registered users.'],
[Response_ResponseCode.RespInIgnoreList, 'You are being ignored by the creator of this game.'],
];

View file

@ -3,6 +3,8 @@ import { WebClient } from '../../WebClient';
import { Command_Ping_ext, Command_PingSchema } from '@app/generated';
export function ping(pingReceived: () => void): void {
// Uses `onResponse` (not `onSuccess`) so KeepAliveService treats any server
// reply as proof of life, independent of responseCode.
WebClient.instance.protobuf.sendSessionCommand(Command_Ping_ext, create(Command_PingSchema), {
onResponse: () => pingReceived(),
});

View file

@ -4,15 +4,15 @@ import { Command_ReplaySubmitCode_ext, Command_ReplaySubmitCodeSchema } from '@a
export function replaySubmitCode(
replayCode: string,
onSuccess?: () => void,
onError?: (responseCode: number) => void,
onSubmitted?: () => void,
onFailure?: (responseCode: number) => void,
): void {
WebClient.instance.protobuf.sendSessionCommand(
Command_ReplaySubmitCode_ext,
create(Command_ReplaySubmitCodeSchema, { replayCode }),
{
onSuccess,
onError,
onSuccess: onSubmitted,
onError: onFailure,
}
);
}

View file

@ -468,18 +468,18 @@ describe('replaySubmitCode', () => {
);
});
it('forwards onSuccess callback', () => {
const onSuccess = vi.fn();
replaySubmitCode('42-abc123', onSuccess);
it('forwards onSubmitted callback', () => {
const onSubmitted = vi.fn();
replaySubmitCode('42-abc123', onSubmitted);
invokeOnSuccess();
expect(onSuccess).toHaveBeenCalled();
expect(onSubmitted).toHaveBeenCalled();
});
it('forwards onError callback', () => {
const onError = vi.fn();
replaySubmitCode('42-abc123', undefined, onError);
it('forwards onFailure callback', () => {
const onFailure = vi.fn();
replaySubmitCode('42-abc123', undefined, onFailure);
invokeCallback('onError', 404);
expect(onError).toHaveBeenCalledWith(404);
expect(onFailure).toHaveBeenCalledWith(404);
});
});

View file

@ -1,7 +0,0 @@
import { CommonEvents } from './index';
describe('CommonEvents', () => {
it('is an empty event map (all common events were moved to game/session events)', () => {
expect(CommonEvents).toEqual([]);
});
});

View file

@ -1,3 +0,0 @@
import type { SessionExtensionRegistry } from '../session';
export const CommonEvents: SessionExtensionRegistry = [];

View file

@ -1,3 +0,0 @@
// Event_PlayerPropertiesChanged is handled as a game event in websocket/events/game/playerPropertiesChanged.ts
// This file is retained for reference but is no longer registered in CommonEvents.
export {};

View file

@ -107,9 +107,13 @@ export interface IModeratorRequest {
clientid?: string,
removeMessages?: number
): void;
forceActivateUser(usernameToActivate: string, moderatorName: string): void;
getAdminNotes(userName: string): void;
getBanHistory(userName: string): void;
getWarnHistory(userName: string): void;
getWarnList(modName: string, userName: string, userClientid: string): void;
grantReplayAccess(replayId: number, moderatorName: string): void;
updateAdminNotes(userName: string, notes: string): void;
viewLogHistory(filters: ViewLogHistoryParams): void;
warnUser(userName: string, reason: string, clientid?: string, removeMessages?: number): void;
}