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

project-slippi / slippi-js / 19321334711

13 Nov 2025 05:15AM UTC coverage: 81.39% (-2.3%) from 83.652%
19321334711

push

github

web-flow
Fix broken enet loading in CJS bundle and also lazy load reconnect-core import (#149)

* use enet module default if it exists

* load enet module function

* mark enet and reconnect-core as optional dependencies

* dynamically import reconnect-core

* oops add missing file

* fix enet types

688 of 916 branches covered (75.11%)

Branch coverage included in aggregate %.

6 of 59 new or added lines in 4 files covered. (10.17%)

1 existing line in 1 file now uncovered.

1901 of 2265 relevant lines covered (83.93%)

241487.69 hits per line

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

10.06
/src/console/consoleConnection.ts
1
import { EventEmitter } from "events";
24✔
2
import net from "net";
24✔
3
import type { Instance } from "reconnect-core";
4

5
import type { CommunicationMessage } from "./communication";
6
import { CommunicationType, ConsoleCommunication } from "./communication";
24✔
7
import { loadReconnectCoreModule } from "./loadReconnectCoreModule";
24✔
8
import type { Connection, ConnectionDetails, ConnectionSettings } from "./types";
9
import { ConnectionEvent, ConnectionStatus, Ports } from "./types";
24✔
10

11
export const NETWORK_MESSAGE = "HELO\0";
24✔
12

13
const DEFAULT_CONNECTION_TIMEOUT_MS = 20000;
24✔
14

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

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

28
const consoleConnectionOptions = {
24✔
29
  autoReconnect: true,
30
};
31

32
export type ConsoleConnectionOptions = typeof consoleConnectionOptions;
33

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

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

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

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

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

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

119
  private async _connectOnPort(ip: string, port: number, timeout: number): Promise<void> {
NEW
120
    const inject = await loadReconnectCoreModule();
×
121

122
    // set up reconnect
123
    const reconnect = inject(() =>
×
124
      net.connect({
×
125
        host: ip,
126
        port: port,
127
        timeout: timeout,
128
      }),
129
    );
130

131
    // Indicate we are connecting
132
    this._setStatus(ConnectionStatus.CONNECTING);
×
133

134
    // Prepare console communication obj for talking UBJSON
135
    const consoleComms = new ConsoleCommunication();
×
136

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

152
        let commState: CommunicationState = CommunicationState.INITIAL;
×
153
        client.on("data", (data) => {
×
154
          if (commState === CommunicationState.INITIAL) {
×
155
            commState = this._getInitialCommState(data);
×
156
            console.log(`Connected to ${ip}:${port} with type: ${commState}`);
×
157
            this._setStatus(ConnectionStatus.CONNECTED);
×
158
            console.log(data.toString("hex"));
×
159
          }
160

161
          if (commState === CommunicationState.LEGACY) {
×
162
            // If the first message received was not a handshake message, either we
163
            // connected to an old Nintendont version or a relay instance
164
            this._handleReplayData(data);
×
165
            return;
×
166
          }
167

168
          try {
×
169
            consoleComms.receive(data);
×
170
          } catch (err) {
171
            console.error("Failed to process new data from server...", {
×
172
              error: err,
173
              prevDataBuf: consoleComms.getReceiveBuffer(),
174
              rcvData: data,
175
            });
176
            client.destroy();
×
177
            this.emit(ConnectionEvent.ERROR, err);
×
178
            return;
×
179
          }
180
          const messages = consoleComms.getMessages();
×
181

182
          // Process all of the received messages
183
          try {
×
184
            messages.forEach((message) => this._processMessage(message));
×
185
          } catch (err) {
186
            // Disconnect client to send another handshake message
187
            console.error(err);
×
188
            client.destroy();
×
189
            this.emit(ConnectionEvent.ERROR, err);
×
190
          }
191
        });
192

193
        client.on("timeout", () => {
×
194
          // const previouslyConnected = this.connectionStatus === ConnectionStatus.CONNECTED;
195
          console.warn(`Attempted connection to ${ip}:${port} timed out after ${timeout}ms`);
×
196
          client.destroy();
×
197
        });
198

199
        client.on("end", () => {
×
200
          console.log("disconnect");
×
201
          if (!this.shouldReconnect) {
×
202
            client.destroy();
×
203
          }
204
        });
205

206
        client.on("close", () => {
×
207
          console.log("connection was closed");
×
208
        });
209

210
        const handshakeMsgOut = consoleComms.genHandshakeOut(
×
211
          this.connDetails.gameDataCursor as Uint8Array,
212
          this.connDetails.clientToken ?? 0,
×
213
          this.isRealtime,
214
        );
215

216
        client.write(handshakeMsgOut);
×
217
      },
218
    );
219

220
    const setConnectingStatus = (): void => {
×
221
      // Indicate we are connecting
222
      this._setStatus(this.shouldReconnect ? ConnectionStatus.RECONNECT_WAIT : ConnectionStatus.CONNECTING);
×
223
    };
224

225
    connection.on("connect", setConnectingStatus);
×
226
    connection.on("reconnect", setConnectingStatus);
×
227

228
    connection.on("disconnect", () => {
×
229
      if (!this.shouldReconnect) {
×
230
        connection.reconnect = false;
×
231
        connection.disconnect();
×
232
        this._setStatus(ConnectionStatus.DISCONNECTED);
×
233
      }
234
      // TODO: Figure out how to set RECONNECT_WAIT state here. Currently it will stay on
235
      // TODO: Connecting... forever
236
    });
237

238
    connection.on("error", (err) => {
×
239
      console.warn(`Connection on port ${port} encountered an error.`, err);
×
240

241
      this._setStatus(ConnectionStatus.DISCONNECTED);
×
242
      this.emit(ConnectionEvent.ERROR, `Connection on port ${port} encountered an error.\n${err}`);
×
243
    });
244

245
    this.connection = connection;
×
246
    connection.connect(port);
×
247
  }
248

249
  /**
250
   * Terminate the current connection.
251
   */
252
  public disconnect(): void {
253
    // Prevent reconnections and disconnect
254
    if (this.connection) {
×
255
      this.connection.reconnect = false;
×
256
      this.connection.disconnect();
×
257
      this.connection = null;
×
258
    }
259

260
    if (this.client) {
×
261
      this.client.destroy();
×
262
    }
263
  }
264

265
  private _getInitialCommState(data: Buffer): CommunicationState {
266
    if (data.length < 13) {
×
267
      return CommunicationState.LEGACY;
×
268
    }
269

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

272
    const dataStart = data.slice(4, 13);
×
273

274
    return dataStart.equals(openingBytes) ? CommunicationState.NORMAL : CommunicationState.LEGACY;
×
275
  }
276

277
  private _processMessage(message: CommunicationMessage): void {
278
    this.emit(ConnectionEvent.MESSAGE, message);
×
279
    switch (message.type) {
×
280
      case CommunicationType.KEEP_ALIVE:
281
        // console.log("Keep alive message received");
282

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

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

301
        if (message.payload.forcePos) {
×
302
          console.warn(
×
303
            "Overflow occured in Nintendont, data has likely been skipped and replay corrupted. " +
304
              "Expected, Received:",
305
            this.connDetails.gameDataCursor,
306
            readPos,
307
          );
308
        }
309

310
        this.connDetails.gameDataCursor = Uint8Array.from(message.payload.nextPos);
×
311

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

334
  private _handleReplayData(data: Uint8Array): void {
335
    this.emit(ConnectionEvent.DATA, data);
×
336
  }
337

338
  private _setStatus(status: ConnectionStatus): void {
339
    // Don't fire the event if the status hasn't actually changed
340
    if (this.connectionStatus !== status) {
×
341
      this.connectionStatus = status;
×
342
      this.emit(ConnectionEvent.STATUS_CHANGE, this.connectionStatus);
×
343
    }
344
  }
345
}
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