Skip to content

Commit de031ff

Browse files
committed
feat: game socket
1 parent 15e5196 commit de031ff

File tree

3 files changed

+485
-0
lines changed

3 files changed

+485
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import * as net from 'node:net';
2+
import { sleep } from '../../../common/async';
3+
import type { SGEGameCredentials } from '../../sge';
4+
import { GameSocketImpl } from '../game.socket';
5+
import type { GameSocket } from '../game.types';
6+
7+
// Messages that are emitted by the game server.
8+
const messages = ['<mode id="GAME"/>\n', '<data>\n'];
9+
10+
const mockNetConnect = (options: { throwError?: boolean }) => {
11+
return (
12+
connectOptions: any & net.NetConnectOpts,
13+
connectionListener: any & (() => void)
14+
) => {
15+
connectionListener();
16+
17+
let dataListener: (data?: unknown) => void;
18+
let connectListener: () => void;
19+
let endListener: () => void;
20+
let closeListener: () => void;
21+
let timeoutListener: () => void;
22+
let errorListener: (error: Error) => void;
23+
24+
const writable = true;
25+
const timeout = connectOptions.timeout ?? 30_000;
26+
27+
const mockAddListener = jest
28+
.fn()
29+
.mockImplementation(
30+
(event: string, listener: (data?: unknown) => void) => {
31+
switch (event) {
32+
case 'data':
33+
dataListener = listener;
34+
break;
35+
case 'connect':
36+
connectListener = listener;
37+
setTimeout(() => {
38+
connectListener();
39+
}, 250);
40+
setTimeout(() => {
41+
dataListener(messages[0]);
42+
}, 500);
43+
setTimeout(() => {
44+
dataListener(messages[1]);
45+
}, 2000);
46+
break;
47+
case 'end':
48+
endListener = listener;
49+
break;
50+
case 'close':
51+
closeListener = listener;
52+
break;
53+
case 'timeout':
54+
timeoutListener = listener;
55+
break;
56+
case 'error':
57+
errorListener = listener;
58+
if (options.throwError) {
59+
setTimeout(() => {
60+
errorListener(new Error('test'));
61+
}, 2000);
62+
}
63+
break;
64+
}
65+
}
66+
);
67+
68+
return {
69+
writable,
70+
timeout,
71+
on: mockAddListener,
72+
once: mockAddListener,
73+
write: jest.fn(),
74+
pause: jest.fn(),
75+
destroySoon: jest.fn().mockImplementation(() => {
76+
endListener();
77+
closeListener();
78+
}),
79+
} as unknown as net.Socket;
80+
};
81+
};
82+
83+
describe('GameSocket', () => {
84+
const credentials: SGEGameCredentials = {
85+
host: 'localhost',
86+
port: 1234,
87+
key: 'test-key',
88+
};
89+
90+
let socket: GameSocket;
91+
92+
let subscriber1NextSpy: jest.Mock;
93+
let subscriber2NextSpy: jest.Mock;
94+
95+
let subscriber1ErrorSpy: jest.Mock;
96+
let subscriber2ErrorSpy: jest.Mock;
97+
98+
let subscriber1CompleteSpy: jest.Mock;
99+
let subscriber2CompleteSpy: jest.Mock;
100+
101+
beforeEach(() => {
102+
subscriber1NextSpy = jest.fn();
103+
subscriber2NextSpy = jest.fn();
104+
105+
subscriber1ErrorSpy = jest.fn();
106+
subscriber2ErrorSpy = jest.fn();
107+
108+
subscriber1CompleteSpy = jest.fn();
109+
subscriber2CompleteSpy = jest.fn();
110+
});
111+
112+
afterEach(() => {
113+
jest.clearAllMocks();
114+
jest.clearAllTimers();
115+
});
116+
117+
describe('#connect', () => {
118+
it('should connect to the game server, receive messages, and then disconnect', async () => {
119+
jest.spyOn(net, 'connect').mockImplementation(
120+
mockNetConnect({
121+
throwError: false,
122+
})
123+
);
124+
125+
socket = new GameSocketImpl({ credentials });
126+
127+
// ---
128+
129+
const observable = await socket.connect();
130+
131+
// first subscriber
132+
observable.subscribe({
133+
next: subscriber1NextSpy,
134+
error: subscriber1ErrorSpy,
135+
complete: subscriber1CompleteSpy,
136+
});
137+
138+
await sleep(1000);
139+
140+
// second subscriber
141+
observable.subscribe({
142+
next: subscriber2NextSpy,
143+
error: subscriber2ErrorSpy,
144+
complete: subscriber2CompleteSpy,
145+
});
146+
147+
await sleep(1000);
148+
149+
await socket.disconnect();
150+
151+
// First subscriber receives all buffered and new events.
152+
expect(subscriber1NextSpy).toHaveBeenCalledTimes(2);
153+
expect(subscriber1NextSpy).toHaveBeenNthCalledWith(1, messages[0]);
154+
expect(subscriber1NextSpy).toHaveBeenNthCalledWith(2, messages[1]);
155+
expect(subscriber1ErrorSpy).toHaveBeenCalledTimes(0);
156+
expect(subscriber1CompleteSpy).toHaveBeenCalledTimes(1);
157+
158+
// Subsequent subscribers only receive new events.
159+
expect(subscriber2NextSpy).toHaveBeenCalledTimes(1);
160+
expect(subscriber2NextSpy).toHaveBeenNthCalledWith(1, messages[1]);
161+
expect(subscriber2ErrorSpy).toHaveBeenCalledTimes(0);
162+
expect(subscriber2CompleteSpy).toHaveBeenCalledTimes(1);
163+
});
164+
});
165+
});

0 commit comments

Comments
 (0)