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

u-wave / core / 23186055882

17 Mar 2026 08:53AM UTC coverage: 87.253% (+0.02%) from 87.235%
23186055882

Pull #755

github

web-flow
Merge f26a7074c into d68af310c
Pull Request #755: Remove Redis

1055 of 1249 branches covered (84.47%)

Branch coverage included in aggregate %.

100 of 111 new or added lines in 8 files covered. (90.09%)

18 existing lines in 2 files now uncovered.

10801 of 12339 relevant lines covered (87.54%)

108.27 hits per line

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

80.0
/src/sockets/GuestConnection.js
1
import Emittery from 'emittery';
1✔
2
import { ulid } from 'ulid';
1✔
3
import Ultron from 'ultron';
1✔
4
import WebSocket from 'ws';
1✔
5

1✔
6
const PING_TIMEOUT = 5_000;
1✔
7
const DEAD_TIMEOUT = 30_000;
1✔
8

1✔
9
/**
1✔
10
 * @augments {Emittery<{
1✔
11
 *  close: undefined,
1✔
12
 *  authenticate: {
1✔
13
 *    user: import('../schema.js').User,
1✔
14
 *    sessionID: string,
1✔
15
 *    lastEventID: string | null,
1✔
16
 *  },
1✔
17
 * }>}
1✔
18
 */
1✔
19
class GuestConnection extends Emittery {
1✔
20
  #events;
29✔
21

29✔
22
  /** @type {import('express-session').Store} */
29✔
23
  #sessionStore;
29✔
24

29✔
25
  #logger;
29✔
26

29✔
27
  #lastMessage = Date.now();
29✔
28

29✔
29
  /**
29✔
30
   * @param {import('../Uwave.js').default} uw
29✔
31
   * @param {import('express-session').Store} store
29✔
32
   * @param {import('ws').WebSocket} socket
29✔
33
   * @param {{ authRegistry: import('../AuthRegistry.js').default }} options
29✔
34
   */
29✔
35
  constructor(uw, store, socket, options) {
29✔
36
    super();
29✔
37
    this.uw = uw;
29✔
38
    this.#sessionStore = store;
29✔
39
    this.socket = socket;
29✔
40
    this.options = options;
29✔
41
    this.#logger = uw.logger.child({ ns: 'uwave:sockets', connectionType: 'GuestConnection', userId: null });
29✔
42

29✔
43
    this.#events = new Ultron(socket);
29✔
44

29✔
45
    this.#events.on('close', () => {
29✔
46
      this.emit('close');
×
47
    });
29✔
48

29✔
49
    this.#events.on('message', /** @param {string|Buffer} token */ (token) => {
29✔
50
      this.attemptAuth(token.toString()).then(() => {
29✔
51
        this.send(ulid(), 'authenticated');
29✔
52
      }).catch((error) => {
29✔
53
        this.send(ulid(), 'error', error.message);
×
54
      });
29✔
55
    });
29✔
56

29✔
57
    this.#events.on('pong', () => {
29✔
58
      this.#lastMessage = Date.now();
×
59
    });
29✔
60
  }
29✔
61

29✔
62
  /**
29✔
63
   * @param {string} token
29✔
64
   * @private
29✔
65
   */
29✔
66
  async attemptAuth(token) {
29✔
67
    // TODO: support a Last-Event-ID style value provided by the client
29✔
68
    const { bans, users } = this.uw;
29✔
69
    const { authRegistry } = this.options;
29✔
70

29✔
71
    const { userID, sessionID } = await authRegistry.getTokenUser(token);
29✔
72
    if (!sessionID || typeof sessionID !== 'string') {
29!
73
      throw new Error('Invalid token');
×
74
    }
×
75
    const userModel = await users.getUser(userID);
29✔
76
    if (!userModel) {
29!
77
      throw new Error('Invalid session');
×
78
    }
×
79

29✔
80
    // Users who are banned can still join as guests, but cannot log in. So we
29✔
81
    // ignore their socket login attempts, and just keep their connections
29✔
82
    // around as guest connections.
29✔
83
    if (await bans.isBanned(userModel)) {
29!
84
      throw new Error('You have been banned');
×
85
    }
×
86

29✔
87
    await this.emit('authenticate', { user: userModel, sessionID, lastEventID: null });
29✔
88
  }
29✔
89

29✔
90
  /**
29✔
91
   * @param {string} sessionID
29✔
92
   */
29✔
93
  isReconnect(sessionID) {
29✔
94
    return new Promise((resolve, reject) => {
29✔
95
      this.#sessionStore.get(sessionID, (err, result) => {
29✔
96
        if (err != null) {
29!
NEW
97
          reject(err);
×
98
        } else {
29✔
99
          resolve(result != null);
29✔
100
        }
29✔
101
      });
29✔
102
    });
29✔
103
  }
29✔
104

29✔
105
  /**
29✔
106
   * @param {string} id
29✔
107
   * @param {string} command
29✔
108
   * @param {import('type-fest').JsonValue} [data]
29✔
109
   */
29✔
110
  send(id, command, data) {
29✔
111
    this.socket.send(JSON.stringify({ id, command, data }));
29✔
112
  }
29✔
113

29✔
114
  #timeSinceLastMessage() {
29✔
115
    return Date.now() - this.#lastMessage;
×
116
  }
×
117

29✔
118
  ping() {
29✔
119
    if (this.socket.readyState !== WebSocket.OPEN) {
×
120
      return;
×
121
    }
×
122
    if (this.#timeSinceLastMessage() > DEAD_TIMEOUT) {
×
123
      this.socket.terminate();
×
124
      return;
×
125
    }
×
126
    if (this.#timeSinceLastMessage() > PING_TIMEOUT) {
×
127
      this.socket.ping();
×
128
    }
×
129
  }
×
130

29✔
131
  close() {
29✔
132
    this.#logger.info('close');
×
133
    this.socket.close();
×
134
  }
×
135

29✔
136
  removed() {
29✔
137
    this.#events.remove();
29✔
138
  }
29✔
139

29✔
140
  toString() {
29✔
141
    return 'Guest';
×
142
  }
×
143
}
29✔
144

1✔
145
export default GuestConnection;
1✔
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