• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

project-slippi / slippi-js / 26956420963

04 Jun 2026 01:57PM UTC coverage: 81.034%. Remained the same
26956420963

push

github

vinceau
refactor: allow not specifying public member accessibility

871 of 1156 branches covered (75.35%)

Branch coverage included in aggregate %.

3 of 3 new or added lines in 3 files covered. (100.0%)

1 existing line in 1 file now uncovered.

1949 of 2324 relevant lines covered (83.86%)

320353.83 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

10.24
/src/node/console/consoleConnection.ts
1
import net from "net";
26✔
2
import type { Instance } from "reconnect-core";
3

4
import { NETWORK_MESSAGE } from "../../common/utils/slpStream.js";
26✔
5
import { TypedEventEmitter } from "../../common/utils/typedEventEmitter.js";
26✔
6
import type { CommunicationMessage } from "./communication.js";
7
import { CommunicationType, ConsoleCommunication } from "./communication.js";
26✔
8
import { loadReconnectCoreModule } from "./loadReconnectCoreModule.js";
26✔
9
import type { Connection, ConnectionDetails, ConnectionEventMap, ConnectionSettings } from "./types.js";
10
import { ConnectionEvent, ConnectionStatus, Ports } from "./types.js";
26✔
11

12
const DEFAULT_CONNECTION_TIMEOUT_MS = 20000;
26✔
13

14
enum CommunicationState {
26✔
15
  INITIAL = "initial",
26✔
16
  LEGACY = "legacy",
26✔
17
  NORMAL = "normal",
26✔
18
}
19

20
const defaultConnectionDetails: ConnectionDetails = {
26✔
21
  consoleNick: "unknown",
22
  gameDataCursor: Uint8Array.from([0, 0, 0, 0, 0, 0, 0, 0]),
23
  version: "",
24
  clientToken: 0,
25
};
26

27
const consoleConnectionOptions = {
26✔
28
  autoReconnect: true,
29
};
30

31
export type ConsoleConnectionOptions = typeof consoleConnectionOptions;
32

33
/**
34
 * Responsible for maintaining connection to a Slippi relay connection or Wii connection.
35
 * Events are emitted whenever data is received.
36
 *
37
 * Basic usage example:
38
 *
39
 * ```javascript
40
 * const { ConsoleConnection } = require("@slippi/slippi-js");
41
 *
42
 * const connection = new ConsoleConnection();
43
 * connection.connect("localhost", 667); // You should set these values appropriately
44
 *
45
 * connection.on("data", (data) => {
46
 *   // Received data from console
47
 *   console.log(data);
48
 * });
49
 *
50
 * connection.on("statusChange", (status) => {
51
 *   console.log(`status changed: ${status}`);
52
 * });
53
 * ```
54
 */
55
export class ConsoleConnection extends TypedEventEmitter<ConnectionEventMap> implements Connection {
26✔
56
  private ipAddress: string;
57
  private port: number;
58
  private isRealtime: boolean;
59
  private connectionStatus = ConnectionStatus.DISCONNECTED;
×
60
  private connDetails: ConnectionDetails = { ...defaultConnectionDetails };
×
61
  private client?: net.Socket;
62
  private connection?: Instance<unknown, net.Socket>;
63
  private options: ConsoleConnectionOptions;
64
  private shouldReconnect = false;
×
65

66
  constructor(options?: Partial<ConsoleConnectionOptions>) {
67
    super();
×
68
    this.ipAddress = "0.0.0.0";
×
69
    this.port = Ports.DEFAULT;
×
70
    this.isRealtime = false;
×
71
    this.options = Object.assign({}, consoleConnectionOptions, options);
×
72
  }
73

74
  /**
75
   * @returns The current connection status.
76
   */
77
  getStatus(): ConnectionStatus {
78
    return this.connectionStatus;
×
79
  }
80

81
  /**
82
   * @returns The IP address and port of the current connection.
83
   */
84
  getSettings(): ConnectionSettings {
85
    return {
×
86
      ipAddress: this.ipAddress,
87
      port: this.port,
88
    };
89
  }
90

91
  /**
92
   * @returns The specific details about the connected console.
93
   */
94
  getDetails(): ConnectionDetails {
95
    return { ...this.connDetails };
×
96
  }
97

98
  /**
99
   * Initiate a connection to the Wii or Slippi relay.
100
   * @param ip   The IP address of the Wii or Slippi relay.
101
   * @param port The port to connect to.
102
   * @param isRealtime Optional. A flag to tell the Wii to send data as quickly as possible
103
   * @param timeout Optional. The timeout in milliseconds when attempting to connect
104
   *                to the Wii or relay.
105
   */
106
  async connect(ip: string, port: number, isRealtime = false, timeout = DEFAULT_CONNECTION_TIMEOUT_MS): Promise<void> {
×
UNCOV
107
    this.ipAddress = ip;
×
108
    this.port = port;
×
109
    this.isRealtime = isRealtime;
×
110
    await this._connectOnPort(ip, port, timeout);
×
111
  }
112

113
  private async _connectOnPort(ip: string, port: number, timeout: number): Promise<void> {
114
    const inject = await loadReconnectCoreModule();
×
115

116
    // set up reconnect
117
    const reconnect = inject(() =>
×
118
      net.connect({
×
119
        host: ip,
120
        port: port,
121
        timeout: timeout,
122
      }),
123
    );
124

125
    // Indicate we are connecting
126
    this._setStatus(ConnectionStatus.CONNECTING);
×
127

128
    // Prepare console communication obj for talking UBJSON
129
    const consoleComms = new ConsoleCommunication();
×
130

131
    // TODO: reconnect on failed reconnect, not sure how
132
    // TODO: to do this
133
    const connection = reconnect(
×
134
      {
135
        initialDelay: 2000,
136
        maxDelay: 10000,
137
        strategy: "fibonacci",
138
        failAfter: Infinity,
139
      },
140
      (client) => {
141
        this.emit(ConnectionEvent.CONNECT, undefined);
×
142
        // We successfully connected so turn on auto-reconnect
143
        this.shouldReconnect = this.options.autoReconnect;
×
144
        this.client = client;
×
145

146
        let commState: CommunicationState = CommunicationState.INITIAL;
×
147
        client.on("data", (data) => {
×
148
          if (commState === CommunicationState.INITIAL) {
×
149
            commState = this._getInitialCommState(data);
×
150
            // console.log(`Connected to ${ip}:${port} with type: ${commState}`);
151
            this._setStatus(ConnectionStatus.CONNECTED);
×
152
          }
153

154
          if (commState === CommunicationState.LEGACY) {
×
155
            // If the first message received was not a handshake message, either we
156
            // connected to an old Nintendont version or a relay instance
157
            this._handleReplayData(data);
×
158
            return;
×
159
          }
160

161
          try {
×
162
            consoleComms.receive(data);
×
163
          } catch (err) {
164
            console.error("Failed to process new data from server...", {
×
165
              error: err,
166
              prevDataBuf: consoleComms.getReceiveBuffer(),
167
              rcvData: data,
168
            });
169
            client.destroy();
×
170
            this.emit(ConnectionEvent.ERROR, err);
×
171
            return;
×
172
          }
173
          const messages = consoleComms.getMessages();
×
174

175
          // Process all of the received messages
176
          try {
×
177
            messages.forEach((message) => this._processMessage(message));
×
178
          } catch (err) {
179
            // Disconnect client to send another handshake message
180
            console.error(err);
×
181
            client.destroy();
×
182
            this.emit(ConnectionEvent.ERROR, err);
×
183
          }
184
        });
185

186
        client.on("timeout", () => {
×
187
          // const previouslyConnected = this.connectionStatus === ConnectionStatus.CONNECTED;
188
          console.warn(`Attempted connection to ${ip}:${port} timed out after ${timeout}ms`);
×
189
          client.destroy();
×
190
        });
191

192
        client.on("end", () => {
×
193
          // console.log("disconnect");
194
          if (!this.shouldReconnect) {
×
195
            client.destroy();
×
196
          }
197
        });
198

199
        // client.on("close", () => {
200
        //   console.log("connection was closed");
201
        // });
202

203
        const handshakeMsgOut = consoleComms.genHandshakeOut(
×
204
          this.connDetails.gameDataCursor as Uint8Array,
205
          this.connDetails.clientToken ?? 0,
×
206
          this.isRealtime,
207
        );
208

209
        client.write(handshakeMsgOut);
×
210
      },
211
    );
212

213
    const setConnectingStatus = (): void => {
×
214
      // Indicate we are connecting
215
      this._setStatus(this.shouldReconnect ? ConnectionStatus.RECONNECT_WAIT : ConnectionStatus.CONNECTING);
×
216
    };
217

218
    connection.on("connect", setConnectingStatus);
×
219
    connection.on("reconnect", setConnectingStatus);
×
220

221
    connection.on("disconnect", () => {
×
222
      if (!this.shouldReconnect) {
×
223
        connection.reconnect = false;
×
224
        connection.disconnect();
×
225
        this._setStatus(ConnectionStatus.DISCONNECTED);
×
226
      }
227
      // TODO: Figure out how to set RECONNECT_WAIT state here. Currently it will stay on
228
      // TODO: Connecting... forever
229
    });
230

231
    connection.on("error", (err) => {
×
232
      console.warn(`Connection on port ${port} encountered an error.`, err);
×
233

234
      this._setStatus(ConnectionStatus.DISCONNECTED);
×
235
      this.emit(ConnectionEvent.ERROR, `Connection on port ${port} encountered an error.\n${err}`);
×
236
    });
237

238
    this.connection = connection;
×
239
    connection.connect(port);
×
240
  }
241

242
  /**
243
   * Terminate the current connection.
244
   */
245
  disconnect(): void {
246
    // Prevent reconnections and disconnect
247
    if (this.connection) {
×
248
      this.connection.reconnect = false;
×
249
      this.connection.disconnect();
×
250
      this.connection = undefined;
×
251
    }
252

253
    if (this.client) {
×
254
      this.client.destroy();
×
255
    }
256
  }
257

258
  private _getInitialCommState(data: Buffer): CommunicationState {
259
    if (data.length < 13) {
×
260
      return CommunicationState.LEGACY;
×
261
    }
262

263
    const openingBytes = Buffer.from([0x7b, 0x69, 0x04, 0x74, 0x79, 0x70, 0x65, 0x55, 0x01]);
×
264

265
    const dataStart = data.slice(4, 13);
×
266

267
    return dataStart.equals(openingBytes) ? CommunicationState.NORMAL : CommunicationState.LEGACY;
×
268
  }
269

270
  private _processMessage(message: CommunicationMessage): void {
271
    this.emit(ConnectionEvent.MESSAGE, message);
×
272
    switch (message.type) {
×
273
      case CommunicationType.KEEP_ALIVE:
274
        // console.log("Keep alive message received");
275

276
        // TODO: This is the jankiest shit ever but it will allow for relay connections not
277
        // TODO: to time out as long as the main connection is still receving keep alive messages
278
        // TODO: Need to figure out a better solution for this. There should be no need to have an
279
        // TODO: active Wii connection for the relay connection to keep itself alive
280
        const fakeKeepAlive = Buffer.from(NETWORK_MESSAGE);
×
281
        this._handleReplayData(fakeKeepAlive);
×
282

283
        break;
×
284
      case CommunicationType.REPLAY:
285
        const readPos = Uint8Array.from(message.payload.pos);
×
286
        const cmp = Buffer.compare(this.connDetails.gameDataCursor as Uint8Array, readPos);
×
287
        if (!message.payload.forcePos && cmp !== 0) {
×
288
          // The readPos is not the one we are waiting on, throw error
289
          throw new Error(
×
290
            `Position of received data is incorrect. Expected: ${this.connDetails.gameDataCursor.toString()}, Received: ${readPos.toString()}`,
291
          );
292
        }
293

294
        if (message.payload.forcePos) {
×
295
          console.warn(
×
296
            "Overflow occured in Nintendont, data has likely been skipped and replay corrupted. " +
297
              "Expected, Received:",
298
            this.connDetails.gameDataCursor,
299
            readPos,
300
          );
301
        }
302

303
        this.connDetails.gameDataCursor = Uint8Array.from(message.payload.nextPos);
×
304

305
        const data = Uint8Array.from(message.payload.data);
×
306
        this._handleReplayData(data);
×
307
        break;
×
308
      case CommunicationType.HANDSHAKE:
309
        const { nick, nintendontVersion } = message.payload;
×
310
        if (nick) {
×
311
          this.connDetails.consoleNick = nick;
×
312
        }
313
        const tokenBuf = Buffer.from(message.payload.clientToken);
×
314
        this.connDetails.clientToken = tokenBuf.readUInt32BE(0);
×
315
        if (nintendontVersion) {
×
316
          this.connDetails.version = nintendontVersion;
×
317
        }
318
        this.connDetails.gameDataCursor = Uint8Array.from(message.payload.pos);
×
319
        this.emit(ConnectionEvent.HANDSHAKE, this.connDetails);
×
320
        break;
×
321
      default:
322
        // Should this be an error?
323
        break;
×
324
    }
325
  }
326

327
  private _handleReplayData(data: Uint8Array): void {
328
    this.emit(ConnectionEvent.DATA, data);
×
329
  }
330

331
  private _setStatus(status: ConnectionStatus): void {
332
    // Don't fire the event if the status hasn't actually changed
333
    if (this.connectionStatus !== status) {
×
334
      this.connectionStatus = status;
×
335
      this.emit(ConnectionEvent.STATUS_CHANGE, this.connectionStatus);
×
336
    }
337
  }
338
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc