refactor typescript wiring

This commit is contained in:
seavor 2026-04-15 15:46:17 -05:00
parent cea9ae62d8
commit c62c336a11
286 changed files with 2999 additions and 3053 deletions

View file

@ -1,5 +1,5 @@
vi.mock('@bufbuild/protobuf', () => ({
create: vi.fn((_schema: any, fields?: any) => ({ ...(fields ?? {}) })),
create: vi.fn((_schema: unknown, fields?: Record<string, unknown>) => ({ ...(fields ?? {}) })),
fromBinary: vi.fn(),
toBinary: vi.fn().mockReturnValue(new Uint8Array()),
hasExtension: vi.fn().mockReturnValue(false),
@ -7,11 +7,11 @@ vi.mock('@bufbuild/protobuf', () => ({
setExtension: vi.fn(),
}));
vi.mock('generated/proto/commands_pb', () => ({
vi.mock('../../generated/proto/commands_pb', () => ({
CommandContainerSchema: {},
}));
vi.mock('generated/proto/server_message_pb', () => ({
vi.mock('../../generated/proto/server_message_pb', () => ({
ServerMessageSchema: {},
ServerMessage_MessageType: {
RESPONSE: 1,
@ -32,16 +32,29 @@ vi.mock('../WebClient', () => ({
default: {},
}));
import { fromBinary, hasExtension, getExtension } from '@bufbuild/protobuf';
import { ServerMessage_MessageType } from 'generated/proto/server_message_pb';
import { create, fromBinary, hasExtension, getExtension } from '@bufbuild/protobuf';
import type { GenExtension } from '@bufbuild/protobuf/codegenv2';
import { ProtobufService } from './ProtobufService';
import { GameEvents, RoomEvents, SessionEvents } from '../events';
import type { GameExtensionRegistry } from '../events/game';
import type { RoomExtensionRegistry } from '../events/room';
import type { SessionExtensionRegistry } from '../events/session';
let mockSocket: any;
import { Data } from '@app/types';
type ProtobufInternal = ProtobufService & {
cmdId: number;
pendingCommands: Map<number, (response: Data.Response) => void>;
processGameEvent(container: unknown, extra?: unknown): void;
processRoomEvent(event: unknown): void;
processSessionEvent(event: unknown): void;
processServerResponse(response: unknown): void;
};
let mockSocket: { isOpen: ReturnType<typeof vi.fn>; send: ReturnType<typeof vi.fn> };
beforeEach(() => {
vi.clearAllMocks();
mockSocket = {
isOpen: vi.fn().mockReturnValue(true),
send: vi.fn(),
@ -49,14 +62,21 @@ beforeEach(() => {
});
describe('ProtobufService', () => {
// Mock extensions for send*Command tests — @bufbuild/protobuf is fully mocked so these are never invoked
const sessionExt = {} as GenExtension<Data.SessionCommand, Record<string, never>>;
const roomExt = {} as GenExtension<Data.RoomCommand, Record<string, never>>;
const gameExt = {} as GenExtension<Data.GameCommand, Record<string, never>>;
const moderatorExt = {} as GenExtension<Data.ModeratorCommand, Record<string, never>>;
const adminExt = {} as GenExtension<Data.AdminCommand, Record<string, never>>;
describe('resetCommands', () => {
it('resets cmdId and pendingCommands', () => {
const service = new ProtobufService(mockSocket);
service.sendSessionCommand({} as any, vi.fn());
expect((service as any).cmdId).toBe(1);
service.sendSessionCommand(sessionExt, vi.fn());
expect((service as ProtobufInternal).cmdId).toBe(1);
service.resetCommands();
expect((service as any).cmdId).toBe(0);
expect((service as any).pendingCommands).toEqual(new Map());
expect((service as ProtobufInternal).cmdId).toBe(0);
expect((service as ProtobufInternal).pendingCommands).toEqual(new Map());
});
});
@ -64,22 +84,22 @@ describe('ProtobufService', () => {
it('increments cmdId and stores callback', () => {
const service = new ProtobufService(mockSocket);
const cb = vi.fn();
service.sendCommand({} as any, cb);
expect((service as any).cmdId).toBe(1);
expect((service as any).pendingCommands.get(1)).toBe(cb);
service.sendCommand(create(Data.CommandContainerSchema), cb);
expect((service as ProtobufInternal).cmdId).toBe(1);
expect((service as ProtobufInternal).pendingCommands.get(1)).toBe(cb);
});
it('sends encoded data when socket is OPEN', () => {
const service = new ProtobufService(mockSocket);
mockSocket.isOpen.mockReturnValue(true);
service.sendCommand({} as any, vi.fn());
service.sendCommand(create(Data.CommandContainerSchema), vi.fn());
expect(mockSocket.send).toHaveBeenCalled();
});
it('does not send when socket is not OPEN', () => {
const service = new ProtobufService(mockSocket);
mockSocket.isOpen.mockReturnValue(false);
service.sendCommand({} as any, vi.fn());
service.sendCommand(create(Data.CommandContainerSchema), vi.fn());
expect(mockSocket.send).not.toHaveBeenCalled();
});
});
@ -87,136 +107,136 @@ describe('ProtobufService', () => {
describe('sendSessionCommand', () => {
it('stores callback and increments cmdId', () => {
const service = new ProtobufService(mockSocket);
service.sendSessionCommand({} as any, {});
expect((service as any).cmdId).toBe(1);
expect((service as any).pendingCommands.get(1)).toBeTypeOf('function');
service.sendSessionCommand(sessionExt, {});
expect((service as ProtobufInternal).cmdId).toBe(1);
expect((service as ProtobufInternal).pendingCommands.get(1)).toBeTypeOf('function');
});
it('invokes onResponse with raw response when the pending command is triggered', () => {
const service = new ProtobufService(mockSocket);
const cb = vi.fn();
service.sendSessionCommand({} as any, {}, { onResponse: cb });
service.sendSessionCommand(sessionExt, {}, { onResponse: cb });
const storedCb = (service as any).pendingCommands.get(1);
storedCb({ responseData: true });
const storedCb = (service as ProtobufInternal).pendingCommands.get(1)!;
storedCb(create(Data.ResponseSchema));
expect(cb).toHaveBeenCalledWith({ responseData: true });
expect(cb).toHaveBeenCalledWith(create(Data.ResponseSchema));
});
it('does not throw when no callback is provided and pending command is triggered', () => {
const service = new ProtobufService(mockSocket);
service.sendSessionCommand({} as any, {});
service.sendSessionCommand(sessionExt, {});
const storedCb = (service as any).pendingCommands.get(1);
expect(() => storedCb({ responseData: true })).not.toThrow();
const storedCb = (service as ProtobufInternal).pendingCommands.get(1)!;
expect(() => storedCb(create(Data.ResponseSchema))).not.toThrow();
});
});
describe('sendRoomCommand', () => {
it('stores callback and increments cmdId', () => {
const service = new ProtobufService(mockSocket);
service.sendRoomCommand(42, {} as any, {});
expect((service as any).cmdId).toBe(1);
service.sendRoomCommand(42, roomExt, {});
expect((service as ProtobufInternal).cmdId).toBe(1);
});
it('invokes onResponse with raw response when the pending command is triggered', () => {
const service = new ProtobufService(mockSocket);
const cb = vi.fn();
service.sendRoomCommand(42, {} as any, {}, { onResponse: cb });
service.sendRoomCommand(42, roomExt, {}, { onResponse: cb });
const storedCb = (service as any).pendingCommands.get(1);
storedCb({ responseData: true });
const storedCb = (service as ProtobufInternal).pendingCommands.get(1)!;
storedCb(create(Data.ResponseSchema));
expect(cb).toHaveBeenCalledWith({ responseData: true });
expect(cb).toHaveBeenCalledWith(create(Data.ResponseSchema));
});
it('does not throw when no callback is provided and pending command is triggered', () => {
const service = new ProtobufService(mockSocket);
service.sendRoomCommand(42, {} as any, {});
service.sendRoomCommand(42, roomExt, {});
const storedCb = (service as any).pendingCommands.get(1);
expect(() => storedCb({ responseData: true })).not.toThrow();
const storedCb = (service as ProtobufInternal).pendingCommands.get(1)!;
expect(() => storedCb(create(Data.ResponseSchema))).not.toThrow();
});
});
describe('sendGameCommand', () => {
it('stores callback and increments cmdId', () => {
const service = new ProtobufService(mockSocket);
service.sendGameCommand(7, {} as any, {});
expect((service as any).cmdId).toBe(1);
service.sendGameCommand(7, gameExt, {});
expect((service as ProtobufInternal).cmdId).toBe(1);
});
it('invokes onResponse with raw response when the pending command is triggered', () => {
const service = new ProtobufService(mockSocket);
const cb = vi.fn();
service.sendGameCommand(7, {} as any, {}, { onResponse: cb });
service.sendGameCommand(7, gameExt, {}, { onResponse: cb });
const storedCb = (service as any).pendingCommands.get(1);
storedCb({ responseData: true });
const storedCb = (service as ProtobufInternal).pendingCommands.get(1)!;
storedCb(create(Data.ResponseSchema));
expect(cb).toHaveBeenCalledWith({ responseData: true });
expect(cb).toHaveBeenCalledWith(create(Data.ResponseSchema));
});
it('does not throw when no callback is provided and pending command is triggered', () => {
const service = new ProtobufService(mockSocket);
service.sendGameCommand(7, {} as any, {});
service.sendGameCommand(7, gameExt, {});
const storedCb = (service as any).pendingCommands.get(1);
expect(() => storedCb({ responseData: true })).not.toThrow();
const storedCb = (service as ProtobufInternal).pendingCommands.get(1)!;
expect(() => storedCb(create(Data.ResponseSchema))).not.toThrow();
});
});
describe('sendModeratorCommand', () => {
it('stores callback and increments cmdId', () => {
const service = new ProtobufService(mockSocket);
service.sendModeratorCommand({} as any, {});
expect((service as any).cmdId).toBe(1);
service.sendModeratorCommand(moderatorExt, {});
expect((service as ProtobufInternal).cmdId).toBe(1);
});
it('invokes onResponse with raw response when the pending command is triggered', () => {
const service = new ProtobufService(mockSocket);
const cb = vi.fn();
service.sendModeratorCommand({} as any, {}, { onResponse: cb });
service.sendModeratorCommand(moderatorExt, {}, { onResponse: cb });
const storedCb = (service as any).pendingCommands.get(1);
storedCb({ responseData: true });
const storedCb = (service as ProtobufInternal).pendingCommands.get(1)!;
storedCb(create(Data.ResponseSchema));
expect(cb).toHaveBeenCalledWith({ responseData: true });
expect(cb).toHaveBeenCalledWith(create(Data.ResponseSchema));
});
it('does not throw when no callback is provided and pending command is triggered', () => {
const service = new ProtobufService(mockSocket);
service.sendModeratorCommand({} as any, {});
service.sendModeratorCommand(moderatorExt, {});
const storedCb = (service as any).pendingCommands.get(1);
expect(() => storedCb({ responseData: true })).not.toThrow();
const storedCb = (service as ProtobufInternal).pendingCommands.get(1)!;
expect(() => storedCb(create(Data.ResponseSchema))).not.toThrow();
});
});
describe('sendAdminCommand', () => {
it('stores callback and increments cmdId', () => {
const service = new ProtobufService(mockSocket);
service.sendAdminCommand({} as any, {});
expect((service as any).cmdId).toBe(1);
service.sendAdminCommand(adminExt, {});
expect((service as ProtobufInternal).cmdId).toBe(1);
});
it('invokes onResponse with raw response when the pending command is triggered', () => {
const service = new ProtobufService(mockSocket);
const cb = vi.fn();
service.sendAdminCommand({} as any, {}, { onResponse: cb });
service.sendAdminCommand(adminExt, {}, { onResponse: cb });
const storedCb = (service as any).pendingCommands.get(1);
storedCb({ responseData: true });
const storedCb = (service as ProtobufInternal).pendingCommands.get(1)!;
storedCb(create(Data.ResponseSchema));
expect(cb).toHaveBeenCalledWith({ responseData: true });
expect(cb).toHaveBeenCalledWith(create(Data.ResponseSchema));
});
it('does not throw when no callback is provided and pending command is triggered', () => {
const service = new ProtobufService(mockSocket);
service.sendAdminCommand({} as any, {});
service.sendAdminCommand(adminExt, {});
const storedCb = (service as any).pendingCommands.get(1);
expect(() => storedCb({ responseData: true })).not.toThrow();
const storedCb = (service as ProtobufInternal).pendingCommands.get(1)!;
expect(() => storedCb(create(Data.ResponseSchema))).not.toThrow();
});
});
@ -224,27 +244,30 @@ describe('ProtobufService', () => {
it('routes RESPONSE message to processServerResponse', () => {
const service = new ProtobufService(mockSocket);
const cb = vi.fn();
(service as any).cmdId = 1;
(service as any).pendingCommands.set(1, cb);
(service as ProtobufInternal).cmdId = 1;
(service as ProtobufInternal).pendingCommands.set(1, cb);
vi.mocked(fromBinary).mockReturnValue({
messageType: ServerMessage_MessageType.RESPONSE,
response: { cmdId: BigInt(1) },
} as any);
vi.mocked(fromBinary).mockReturnValue(
create(Data.ServerMessageSchema, {
messageType: Data.ServerMessage_MessageType.RESPONSE,
response: create(Data.ResponseSchema, { cmdId: BigInt(1) }),
})
);
service.handleMessageEvent({ data: new ArrayBuffer(0) } as MessageEvent);
expect(cb).toHaveBeenCalledWith({ cmdId: BigInt(1) });
expect((service as any).pendingCommands.get(1)).toBeUndefined();
expect(cb).toHaveBeenCalledWith(expect.objectContaining({ cmdId: BigInt(1) }));
expect((service as ProtobufInternal).pendingCommands.get(1)).toBeUndefined();
});
it('routes ROOM_EVENT message', () => {
const service = new ProtobufService(mockSocket);
const processRoomEvent = vi.spyOn(service as any, 'processRoomEvent');
const processRoomEvent = vi.spyOn(service as ProtobufInternal, 'processRoomEvent');
vi.mocked(fromBinary).mockReturnValue({
messageType: ServerMessage_MessageType.ROOM_EVENT,
roomEvent: {},
} as any);
vi.mocked(fromBinary).mockReturnValue(
create(Data.ServerMessageSchema, {
messageType: Data.ServerMessage_MessageType.ROOM_EVENT,
})
);
service.handleMessageEvent({ data: new ArrayBuffer(0) } as MessageEvent);
expect(processRoomEvent).toHaveBeenCalled();
@ -252,12 +275,13 @@ describe('ProtobufService', () => {
it('routes SESSION_EVENT message', () => {
const service = new ProtobufService(mockSocket);
const processSessionEvent = vi.spyOn(service as any, 'processSessionEvent');
const processSessionEvent = vi.spyOn(service as ProtobufInternal, 'processSessionEvent');
vi.mocked(fromBinary).mockReturnValue({
messageType: ServerMessage_MessageType.SESSION_EVENT,
sessionEvent: {},
} as any);
vi.mocked(fromBinary).mockReturnValue(
create(Data.ServerMessageSchema, {
messageType: Data.ServerMessage_MessageType.SESSION_EVENT,
})
);
service.handleMessageEvent({ data: new ArrayBuffer(0) } as MessageEvent);
expect(processSessionEvent).toHaveBeenCalled();
@ -265,12 +289,13 @@ describe('ProtobufService', () => {
it('routes GAME_EVENT_CONTAINER message', () => {
const service = new ProtobufService(mockSocket);
const processGameEvent = vi.spyOn(service as any, 'processGameEvent');
const processGameEvent = vi.spyOn(service as ProtobufInternal, 'processGameEvent');
vi.mocked(fromBinary).mockReturnValue({
messageType: ServerMessage_MessageType.GAME_EVENT_CONTAINER,
gameEventContainer: {},
} as any);
vi.mocked(fromBinary).mockReturnValue(
create(Data.ServerMessageSchema, {
messageType: Data.ServerMessage_MessageType.GAME_EVENT_CONTAINER,
})
);
service.handleMessageEvent({ data: new ArrayBuffer(0) } as MessageEvent);
expect(processGameEvent).toHaveBeenCalled();
@ -280,9 +305,11 @@ describe('ProtobufService', () => {
const service = new ProtobufService(mockSocket);
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
vi.mocked(fromBinary).mockReturnValue({
messageType: 999,
} as any);
vi.mocked(fromBinary).mockReturnValue(
create(Data.ServerMessageSchema, {
messageType: 999,
})
);
service.handleMessageEvent({ data: new ArrayBuffer(0) } as MessageEvent);
expect(consoleSpy).toHaveBeenCalled();
@ -291,7 +318,7 @@ describe('ProtobufService', () => {
it('does nothing when decoded message is null', () => {
const service = new ProtobufService(mockSocket);
vi.mocked(fromBinary).mockReturnValue(null as any);
vi.mocked(fromBinary).mockReturnValue(null!);
expect(() => service.handleMessageEvent({ data: new ArrayBuffer(0) } as MessageEvent)).not.toThrow();
});
@ -311,56 +338,56 @@ describe('ProtobufService', () => {
it('returns early when container has no eventList', () => {
const service = new ProtobufService(mockSocket);
vi.mocked(hasExtension).mockReturnValue(false);
(service as any).processGameEvent(null, {});
(service as ProtobufInternal).processGameEvent(null, {});
expect(hasExtension).not.toHaveBeenCalled();
});
it('dispatches to a GameEvents handler when hasExtension returns true', () => {
const service = new ProtobufService(mockSocket);
const handler = vi.fn();
const mockExt = {};
const mockExt = {} as GenExtension<Data.GameEvent, unknown>;
const payload = { someData: 1 };
// Temporarily override GameEvents for this test
(GameEvents as any).push([mockExt, handler]);
(GameEvents as GameExtensionRegistry).push([mockExt, handler]);
vi.mocked(hasExtension).mockReturnValue(true);
vi.mocked(getExtension).mockReturnValue(payload);
(service as any).processGameEvent({
(service as ProtobufInternal).processGameEvent({
gameId: 42,
eventList: [{ playerId: 5 }],
}, {});
expect(handler).toHaveBeenCalledWith(payload, expect.objectContaining({ gameId: 42, playerId: 5 }));
(GameEvents as any).pop();
(GameEvents as GameExtensionRegistry).pop();
});
it('defaults gameId and playerId to -1 when undefined', () => {
const service = new ProtobufService(mockSocket);
const handler = vi.fn();
const mockExt = {};
const mockExt = {} as GenExtension<Data.GameEvent, unknown>;
const payload = { someData: 1 };
(GameEvents as any).push([mockExt, handler]);
(GameEvents as GameExtensionRegistry).push([mockExt, handler]);
vi.mocked(hasExtension).mockReturnValue(true);
vi.mocked(getExtension).mockReturnValue(payload);
(service as any).processGameEvent({
(service as ProtobufInternal).processGameEvent({
gameId: undefined,
eventList: [{ playerId: undefined }],
});
expect(handler).toHaveBeenCalledWith(payload, expect.objectContaining({ gameId: -1, playerId: -1 }));
(GameEvents as any).pop();
(GameEvents as GameExtensionRegistry).pop();
});
});
describe('processServerResponse', () => {
it('returns early when response is undefined', () => {
const service = new ProtobufService(mockSocket);
(service as any).pendingCommands.set(1, vi.fn());
(service as any).processServerResponse(undefined);
expect((service as any).pendingCommands.size).toBe(1);
(service as ProtobufInternal).pendingCommands.set(1, vi.fn());
(service as ProtobufInternal).processServerResponse(undefined);
expect((service as ProtobufInternal).pendingCommands.size).toBe(1);
});
});
@ -368,25 +395,25 @@ describe('ProtobufService', () => {
it('returns early when event is undefined', () => {
const service = new ProtobufService(mockSocket);
vi.mocked(hasExtension).mockReturnValue(false);
(service as any).processRoomEvent(undefined);
(service as ProtobufInternal).processRoomEvent(undefined);
expect(hasExtension).not.toHaveBeenCalled();
});
it('dispatches to a RoomEvents handler when hasExtension returns true', () => {
const service = new ProtobufService(mockSocket);
const handler = vi.fn();
const mockExt = {};
const mockExt = {} as GenExtension<Data.RoomEvent, unknown>;
const payload = { roomData: 1 };
(RoomEvents as any).push([mockExt, handler]);
(RoomEvents as RoomExtensionRegistry).push([mockExt, handler]);
vi.mocked(hasExtension).mockReturnValue(true);
vi.mocked(getExtension).mockReturnValue(payload);
const event = { roomId: 10 };
(service as any).processRoomEvent(event);
(service as ProtobufInternal).processRoomEvent(event);
expect(handler).toHaveBeenCalledWith(payload, event);
(RoomEvents as any).pop();
(RoomEvents as RoomExtensionRegistry).pop();
});
});
@ -394,24 +421,24 @@ describe('ProtobufService', () => {
it('returns early when event is undefined', () => {
const service = new ProtobufService(mockSocket);
vi.mocked(hasExtension).mockReturnValue(false);
(service as any).processSessionEvent(undefined);
(service as ProtobufInternal).processSessionEvent(undefined);
expect(hasExtension).not.toHaveBeenCalled();
});
it('dispatches to a SessionEvents handler when hasExtension returns true', () => {
const service = new ProtobufService(mockSocket);
const handler = vi.fn();
const mockExt = {};
const mockExt = {} as GenExtension<Data.SessionEvent, unknown>;
const payload = { sessionData: 1 };
(SessionEvents as any).push([mockExt, handler]);
(SessionEvents as SessionExtensionRegistry).push([mockExt, handler]);
vi.mocked(hasExtension).mockReturnValue(true);
vi.mocked(getExtension).mockReturnValue(payload);
(service as any).processSessionEvent({ sessionId: 7 });
(service as ProtobufInternal).processSessionEvent({ sessionId: 7 });
expect(handler).toHaveBeenCalledWith(payload);
(SessionEvents as any).pop();
(SessionEvents as SessionExtensionRegistry).pop();
});
});