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

project-slippi / slippi-js / 20257242476

16 Dec 2025 05:10AM UTC coverage: 80.865% (+0.05%) from 80.813%
20257242476

push

github

web-flow
refactor: prefer undefined over null (#158)

* find and replace null with undefined

* compare against null non type check for checking existence

* simplify undefined declarations

* update tests

* allow Aerials to still have nullable iasa frame data

* make string types more restrictive

* revert back to null in metadata type

* make aerial frame data undefined

* remove unnecessary undefined in optional types

* fix _write() method prototype

* refer T[] over new Array<T>

* revert some typing changes

* revert

* revert type change

* simplify

* remove some comments

690 of 921 branches covered (74.92%)

Branch coverage included in aggregate %.

70 of 90 new or added lines in 21 files covered. (77.78%)

1 existing line in 1 file now uncovered.

1854 of 2225 relevant lines covered (83.33%)

125903.11 hits per line

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

10.34
/src/node/console/dolphinConnection.ts
1
import type { Host, Packet, Peer } from "enet";
2

3
import { TypedEventEmitter } from "../../common/utils/typedEventEmitter";
12✔
4
import { loadEnetModule } from "./loadEnetModule";
12✔
5
import type { Connection, ConnectionDetails, ConnectionEventMap, ConnectionSettings } from "./types";
6
import { ConnectionEvent, ConnectionStatus, Ports } from "./types";
12✔
7

8
const MAX_PEERS = 32;
12✔
9

10
export enum DolphinMessageType {
12✔
11
  CONNECT_REPLY = "connect_reply",
12✔
12
  GAME_EVENT = "game_event",
12✔
13
  START_GAME = "start_game",
12✔
14
  END_GAME = "end_game",
12✔
15
}
16

17
export class DolphinConnection extends TypedEventEmitter<ConnectionEventMap> implements Connection {
12✔
18
  private ipAddress: string;
19
  private port: number;
20
  private connectionStatus = ConnectionStatus.DISCONNECTED;
×
21
  private gameCursor = 0;
×
22
  private nickname = "unknown";
×
23
  private version = "";
×
24
  private peer?: Peer;
25
  private client?: Host;
26

27
  public constructor() {
28
    super();
×
29
    this.ipAddress = "0.0.0.0";
×
30
    this.port = Ports.DEFAULT;
×
31
  }
32

33
  /**
34
   * @returns The current connection status.
35
   */
36
  public getStatus(): ConnectionStatus {
37
    return this.connectionStatus;
×
38
  }
39

40
  /**
41
   * @returns The IP address and port of the current connection.
42
   */
43
  public getSettings(): ConnectionSettings {
44
    return {
×
45
      ipAddress: this.ipAddress,
46
      port: this.port,
47
    };
48
  }
49

50
  public getDetails(): ConnectionDetails {
51
    return {
×
52
      consoleNick: this.nickname,
53
      gameDataCursor: this.gameCursor,
54
      version: this.version,
55
    };
56
  }
57

58
  public async connect(ip: string, port: number): Promise<void> {
59
    console.log(`Connecting to: ${ip}:${port}`);
×
60
    this.ipAddress = ip;
×
61
    this.port = port;
×
62

63
    const enet = await loadEnetModule();
×
64
    // Create the enet client
NEW
65
    let client: Host | undefined = this.client;
×
66
    if (!client) {
×
67
      client = enet.createClient({ peers: MAX_PEERS, channels: 3, down: 0, up: 0 }, (err) => {
×
68
        if (err) {
×
69
          console.error(err);
×
70
          return;
×
71
        }
72
      });
73
      this.client = client;
×
74
    }
75

76
    const peer = client.connect(
×
77
      {
78
        address: this.ipAddress,
79
        port: this.port,
80
      },
81
      3,
82
      1337, // Data to send, not sure what this is or what this represents
83
      (err, newPeer) => {
84
        if (err) {
×
85
          console.error(err);
×
86
          return;
×
87
        }
88

89
        newPeer.ping();
×
90
        this.emit(ConnectionEvent.CONNECT, undefined);
×
91
        this._setStatus(ConnectionStatus.CONNECTED);
×
92
      },
93
    );
94

95
    peer.on("connect", () => {
×
96
      // Reset the game cursor to the beginning of the game. Do we need to do this or
97
      // should it just continue from where it left off?
98
      this.gameCursor = 0;
×
99

100
      const request = {
×
101
        type: "connect_request",
102
        cursor: this.gameCursor,
103
      };
104
      const packet = new enet.Packet(JSON.stringify(request), enet.PACKET_FLAG.RELIABLE);
×
105
      peer.send(0, packet);
×
106
    });
107

108
    peer.on("message", (packet: Packet) => {
×
109
      const data = packet.data();
×
110
      if (data.length === 0) {
×
111
        return;
×
112
      }
113

114
      const dataString = data.toString("ascii");
×
115
      const message = JSON.parse(dataString);
×
116
      const { dolphin_closed } = message;
×
117
      if (dolphin_closed) {
×
118
        // We got a disconnection request
119
        this.disconnect();
×
120
        return;
×
121
      }
122
      this.emit(ConnectionEvent.MESSAGE, message);
×
123
      switch (message.type) {
×
124
        case DolphinMessageType.CONNECT_REPLY:
125
          this.connectionStatus = ConnectionStatus.CONNECTED;
×
126
          this.gameCursor = message.cursor;
×
127
          this.nickname = message.nick;
×
128
          this.version = message.version;
×
129
          this.emit(ConnectionEvent.HANDSHAKE, this.getDetails());
×
130
          break;
×
131
        case DolphinMessageType.GAME_EVENT: {
132
          const { payload } = message;
×
133
          //TODO: remove after game start and end messages have been in stable Ishii for a bit
134
          if (!payload) {
×
135
            // We got a disconnection request
136
            this.disconnect();
×
137
            return;
×
138
          }
139

140
          this._updateCursor(message, dataString);
×
141

142
          const gameData = Buffer.from(payload, "base64");
×
143
          this._handleReplayData(gameData);
×
144
          break;
×
145
        }
146
        case DolphinMessageType.START_GAME: {
147
          this._updateCursor(message, dataString);
×
148
          break;
×
149
        }
150
        case DolphinMessageType.END_GAME: {
151
          this._updateCursor(message, dataString);
×
152
          break;
×
153
        }
154
      }
155
    });
156

157
    peer.on("disconnect", () => {
×
158
      this.onPeerDisconnected();
×
159
    });
160

161
    this.peer = peer;
×
162

163
    this._setStatus(ConnectionStatus.CONNECTING);
×
164
  }
165

166
  private destroyClient(): void {
167
    if (this.client) {
×
168
      this.client.destroy();
×
NEW
169
      this.client = undefined;
×
170
    }
171
  }
172

173
  private onPeerDisconnected(): void {
174
    this._setStatus(ConnectionStatus.DISCONNECTED);
×
175
    if (this.peer) {
×
NEW
176
      this.peer = undefined;
×
177
    }
178
    this.destroyClient();
×
179
  }
180

181
  public disconnect(): void {
182
    if (this.peer) {
×
183
      this.peer.disconnectLater();
×
184
    } else {
185
      this._setStatus(ConnectionStatus.DISCONNECTED);
×
186
      this.destroyClient();
×
187
    }
188
  }
189

190
  private _handleReplayData(data: Uint8Array): void {
191
    this.emit(ConnectionEvent.DATA, data);
×
192
  }
193

194
  private _setStatus(status: ConnectionStatus): void {
195
    // Don't fire the event if the status hasn't actually changed
196
    if (this.connectionStatus !== status) {
×
197
      this.connectionStatus = status;
×
198
      this.emit(ConnectionEvent.STATUS_CHANGE, this.connectionStatus);
×
199
    }
200
  }
201

202
  private _updateCursor(message: { cursor: number; next_cursor: number }, dataString: string): void {
203
    const { cursor, next_cursor } = message;
×
204

205
    if (this.gameCursor !== cursor) {
×
206
      const err = new Error(
×
207
        `Unexpected game data cursor. Expected: ${this.gameCursor} but got: ${cursor}. Payload: ${dataString}`,
208
      );
209
      console.warn(err);
×
210
      this.emit(ConnectionEvent.ERROR, err);
×
211
    }
212

213
    this.gameCursor = next_cursor;
×
214
  }
215
}
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

© 2025 Coveralls, Inc