• 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

91.23
/src/common/utils/slpStream.ts
1
import type { EventPayloadTypes } from "../types";
2
import { Command } from "../types";
12✔
3
import { parseMessage } from "./slpReader";
12✔
4
import { TypedEventEmitter } from "./typedEventEmitter";
12✔
5

6
export const NETWORK_MESSAGE = "HELO\0";
12✔
7

8
export enum SlpStreamMode {
12✔
9
  AUTO = "AUTO", // Always reading data, but errors on invalid command
12✔
10
  MANUAL = "MANUAL", // Stops parsing inputs after a valid game end command, requires manual restarting
12✔
11
}
12

13
const defaultSettings = {
12✔
14
  suppressErrors: false,
15
  mode: SlpStreamMode.AUTO,
16
};
17

18
export type SlpStreamSettings = typeof defaultSettings;
19

20
export type MessageSizes = Map<Command, number>;
21

22
export type SlpCommandEventPayload = {
23
  command: Command;
24
  payload: EventPayloadTypes | MessageSizes;
25
};
26

27
export type SlpRawEventPayload = {
28
  command: Command;
29
  payload: Uint8Array;
30
};
31

32
export enum SlpStreamEvent {
12✔
33
  RAW = "slp-raw",
12✔
34
  COMMAND = "slp-command",
12✔
35
}
36

37
type SlpStreamEventMap = {
38
  [SlpStreamEvent.RAW]: SlpRawEventPayload;
39
  [SlpStreamEvent.COMMAND]: SlpCommandEventPayload;
40
};
41

42
/**
43
 * SlpStream processes a stream of Slippi data and emits events based on the commands received.
44
 *
45
 * SlpStream emits two events: "slp-raw" and "slp-command". The "slp-raw" event emits the raw buffer
46
 * bytes whenever it processes each command. You can manually parse this or write it to a
47
 * file. The "slp-command" event returns the parsed payload which you can access the attributes.
48
 *
49
 * @class SlpStream
50
 * @extends {TypedEventEmitter}
51
 */
52
export class SlpStream extends TypedEventEmitter<SlpStreamEventMap> {
12✔
53
  private gameEnded = false; // True only if in manual mode and the game has completed
6✔
54
  private settings: SlpStreamSettings;
55
  private payloadSizes?: MessageSizes;
56
  private previousBuffer: Uint8Array = new Uint8Array(0);
6✔
57
  private readonly utf8Decoder = new TextDecoder("utf-8");
6✔
58

59
  /**
60
   *Creates an instance of SlpStream.
61
   * @param {Partial<SlpStreamSettings>} [slpOptions]
62
   * @memberof SlpStream
63
   */
64
  public constructor(slpOptions?: Partial<SlpStreamSettings>) {
65
    super();
6✔
66
    this.settings = Object.assign({}, defaultSettings, slpOptions);
6✔
67
  }
68

69
  public restart(): void {
70
    this.gameEnded = false;
×
NEW
71
    this.payloadSizes = undefined;
×
72
  }
73

74
  /**
75
   * Process a chunk of data. This is the main entry point for feeding data
76
   * into the stream processor.
77
   */
78
  public process(newData: Uint8Array): void {
79
    // Join the current data with the old data
80
    const combinedLength = this.previousBuffer.length + newData.length;
336,552✔
81
    const data = new Uint8Array(combinedLength);
336,552✔
82
    data.set(this.previousBuffer, 0);
336,552✔
83
    data.set(newData, this.previousBuffer.length);
336,552✔
84

85
    // Clear previous data
86
    this.previousBuffer = new Uint8Array(0);
336,552✔
87

88
    const dataView = new DataView(data.buffer, data.byteOffset, data.byteLength);
336,552✔
89

90
    // Iterate through the data
91
    let index = 0;
336,552✔
92
    while (index < data.length) {
336,552✔
93
      // We want to filter out the network messages
94
      const networkMsgSlice = data.subarray(index, index + 5);
578,078✔
95
      if (this.utf8Decoder.decode(networkMsgSlice) === NETWORK_MESSAGE) {
578,078!
96
        index += 5;
×
97
        continue;
×
98
      }
99

100
      // Make sure we have enough data to read a full payload
101
      const command = dataView.getUint8(index);
578,078✔
102
      let payloadSize = 0;
578,078✔
103
      if (this.payloadSizes) {
578,078✔
104
        payloadSize = this.payloadSizes.get(command) ?? 0;
578,027✔
105
      }
106
      const remainingLen = data.length - index;
578,078✔
107
      if (remainingLen < payloadSize + 1) {
578,078✔
108
        // If remaining length is not long enough for full payload, save the remaining
109
        // data until we receive more data. The data has been split up.
110
        this.previousBuffer = data.slice(index);
177✔
111
        break;
177✔
112
      }
113

114
      // Only process if the game is still going
115
      if (this.settings.mode === SlpStreamMode.MANUAL && this.gameEnded) {
577,901✔
116
        break;
3✔
117
      }
118

119
      // Increment by one for the command byte
120
      index += 1;
577,898✔
121

122
      const payloadPtr = data.subarray(index);
577,898✔
123
      const payloadDataView = new DataView(data.buffer, data.byteOffset + index, data.byteLength - index);
577,898✔
124
      let payloadLen = 0;
577,898✔
125
      try {
577,898✔
126
        payloadLen = this._processCommand(command, payloadPtr, payloadDataView);
577,898✔
127
      } catch (err) {
128
        // Only throw the error if we're not suppressing the errors
129
        if (!this.settings.suppressErrors) {
×
130
          throw err;
×
131
        }
132
        payloadLen = 0;
×
133
      }
134
      index += payloadLen;
577,898✔
135
    }
136
  }
137

138
  private _writeCommand(command: Command, entirePayload: Uint8Array, payloadSize: number): Uint8Array {
139
    const payloadBuf = entirePayload.subarray(0, payloadSize);
577,853✔
140
    // Concatenate command byte with payload
141
    const bufToWrite = new Uint8Array(1 + payloadBuf.length);
577,853✔
142
    bufToWrite[0] = command;
577,853✔
143
    bufToWrite.set(payloadBuf, 1);
577,853✔
144

145
    // Forward the raw buffer onwards
146
    const event: SlpRawEventPayload = {
577,853✔
147
      command: command,
148
      payload: bufToWrite,
149
    };
150
    this.emit(SlpStreamEvent.RAW, event);
577,853✔
151
    return bufToWrite;
577,853✔
152
  }
153

154
  private _processCommand(command: Command, entirePayload: Uint8Array, dataView: DataView): number {
155
    // Handle the message size command
156
    if (command === Command.MESSAGE_SIZES) {
577,898✔
157
      const payloadSize = dataView.getUint8(0);
6✔
158
      // Set the payload sizes
159
      this.payloadSizes = processReceiveCommands(dataView);
6✔
160
      // Emit the raw command event
161
      this._writeCommand(command, entirePayload, payloadSize);
6✔
162
      const eventPayload: SlpCommandEventPayload = {
6✔
163
        command: command,
164
        payload: this.payloadSizes,
165
      };
166
      this.emit(SlpStreamEvent.COMMAND, eventPayload);
6✔
167
      return payloadSize;
6✔
168
    }
169

170
    let payloadSize = 0;
577,892✔
171
    if (this.payloadSizes) {
577,892✔
172
      payloadSize = this.payloadSizes.get(command) ?? 0;
577,847!
173
    }
174

175
    // Fetch the payload and parse it
176
    let payload: Uint8Array;
177
    let parsedPayload: EventPayloadTypes | undefined = undefined;
577,892✔
178
    if (payloadSize > 0) {
577,892✔
179
      payload = this._writeCommand(command, entirePayload, payloadSize);
577,847✔
180
      parsedPayload = parseMessage(command, payload);
577,847✔
181
    }
182
    if (!parsedPayload) {
577,892✔
183
      return payloadSize;
365✔
184
    }
185

186
    switch (command) {
577,527✔
187
      case Command.GAME_END:
188
        // Stop parsing data until we manually restart the stream
189
        if (this.settings.mode === SlpStreamMode.MANUAL) {
6✔
190
          this.gameEnded = true;
3✔
191
        }
192
        break;
6✔
193
    }
194

195
    const eventPayload: SlpCommandEventPayload = {
577,527✔
196
      command: command,
197
      payload: parsedPayload,
198
    };
199
    this.emit(SlpStreamEvent.COMMAND, eventPayload);
577,527✔
200
    return payloadSize;
577,527✔
201
  }
202
}
203

204
const processReceiveCommands = (dataView: DataView): MessageSizes => {
12✔
205
  const payloadSizes = new Map<Command, number>();
6✔
206
  const payloadLen = dataView.getUint8(0);
6✔
207
  for (let i = 1; i < payloadLen; i += 3) {
6✔
208
    const commandByte = dataView.getUint8(i);
49✔
209
    const payloadSize = dataView.getUint16(i + 1);
49✔
210
    payloadSizes.set(commandByte, payloadSize);
49✔
211
  }
212
  return payloadSizes;
6✔
213
};
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