• 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

89.17
/src/Uwave.js
1
import EventEmitter from 'node:events';
1✔
2
import { promisify } from 'node:util';
1✔
3
import Redis from 'ioredis';
1✔
4
import avvio from 'avvio';
1✔
5
import { pino } from 'pino';
1✔
6
import { CamelCasePlugin, Kysely, SqliteDialect } from 'kysely';
1✔
7
import httpApi, { errorHandling } from './HttpApi.js';
1✔
8
import SocketServer from './SocketServer.js';
1✔
9
import { Source } from './Source.js';
1✔
10
import KeyValue from './KeyValue.js';
1✔
11
import configStore from './plugins/configStore.js';
1✔
12
import booth from './plugins/booth.js';
1✔
13
import chat from './plugins/chat.js';
1✔
14
import motd from './plugins/motd.js';
1✔
15
import playlists from './plugins/playlists.js';
1✔
16
import users from './plugins/users.js';
1✔
17
import bans from './plugins/bans.js';
1✔
18
import history from './plugins/history.js';
1✔
19
import acl from './plugins/acl.js';
1✔
20
import waitlist from './plugins/waitlist.js';
1✔
21
import passport from './plugins/passport.js';
1✔
22
import migrations from './plugins/migrations.js';
1✔
23
import { SqliteDateColumnsPlugin, connect as connectSqlite } from './utils/sqlite.js';
1✔
24
import Emittery from 'emittery';
1✔
25
import SqliteSessionStore from './utils/SqliteSessionStore.js';
1✔
26

1✔
27
const DEFAULT_SQLITE_PATH = './uwave.sqlite';
1✔
28

1✔
29
class UwCamelCasePlugin extends CamelCasePlugin {
1✔
30
  /**
1✔
31
   * @param {string} str
1✔
32
   * @override
1✔
33
   * @protected
1✔
34
   */
1✔
35
  camelCase(str) {
1✔
36
    return super.camelCase(str).replace(/Id$/, 'ID');
23,838✔
37
  }
23,838✔
38
}
1✔
39

1✔
40
/**
1✔
41
 * @typedef {import('./Source.js').SourcePlugin} SourcePlugin
1✔
42
 */
1✔
43

1✔
44
/**
1✔
45
 * @typedef {UwaveServer & avvio.Server<UwaveServer>} Boot
1✔
46
 * @typedef {Pick<
1✔
47
 *   import('ioredis').RedisOptions,
1✔
48
 *   'port' | 'host' | 'family' | 'path' | 'db' | 'password' | 'username' | 'tls'
1✔
49
 * >} RedisOptions
1✔
50
 * @typedef {{
1✔
51
 *   port?: number,
1✔
52
 *   sqlite?: string,
1✔
53
 *   redis?: string | RedisOptions,
1✔
54
 *   logger?: import('pino').LoggerOptions,
1✔
55
 * } & import('./HttpApi.js').HttpApiOptions} Options
1✔
56
 */
1✔
57

1✔
58
class UwaveServer extends EventEmitter {
1✔
59
  /** @type {import('ioredis').default | undefined} */
180✔
60
  redis;
180✔
61

180✔
62
  /** @type {import('http').Server} */
180✔
63
  // @ts-expect-error TS2564 Definitely assigned in a plugin
180✔
64
  server;
180✔
65

180✔
66
  /** @type {import('express').Application} */
180✔
67
  // @ts-expect-error TS2564 Definitely assigned in a plugin
180✔
68
  express;
180✔
69

180✔
70
  /** @type {import('./plugins/acl.js').Acl} */
180✔
71
  // @ts-expect-error TS2564 Definitely assigned in a plugin
180✔
72
  acl;
180✔
73

180✔
74
  /** @type {import('./plugins/bans.js').Bans} */
180✔
75
  // @ts-expect-error TS2564 Definitely assigned in a plugin
180✔
76
  bans;
180✔
77

180✔
78
  /** @type {import('./plugins/booth.js').Booth} */
180✔
79
  // @ts-expect-error TS2564 Definitely assigned in a plugin
180✔
80
  booth;
180✔
81

180✔
82
  /** @type {import('./plugins/chat.js').Chat} */
180✔
83
  // @ts-expect-error TS2564 Definitely assigned in a plugin
180✔
84
  chat;
180✔
85

180✔
86
  /** @type {import('./plugins/configStore.js').ConfigStore} */
180✔
87
  // @ts-expect-error TS2564 Definitely assigned in a plugin
180✔
88
  config;
180✔
89

180✔
90
  /** @type {import('./plugins/emotes.js').Emotes|null} */
180✔
91
  emotes = null;
180✔
92

180✔
93
  /** @type {import('./plugins/history.js').HistoryRepository} */
180✔
94
  // @ts-expect-error TS2564 Definitely assigned in a plugin
180✔
95
  history;
180✔
96

180✔
97
  /** @type {import('./plugins/migrations.js').Migrate} */
180✔
98
  // @ts-expect-error TS2564 Definitely assigned in a plugin
180✔
99
  migrate;
180✔
100

180✔
101
  /** @type {import('./plugins/motd.js').MOTD} */
180✔
102
  // @ts-expect-error TS2564 Definitely assigned in a plugin
180✔
103
  motd;
180✔
104

180✔
105
  /** @type {import('./plugins/passport.js').Passport} */
180✔
106
  // @ts-expect-error TS2564 Definitely assigned in a plugin
180✔
107
  passport;
180✔
108

180✔
109
  /** @type {import('./plugins/playlists.js').PlaylistsRepository} */
180✔
110
  // @ts-expect-error TS2564 Definitely assigned in a plugin
180✔
111
  playlists;
180✔
112

180✔
113
  /** @type {import('./plugins/users.js').UsersRepository} */
180✔
114
  // @ts-expect-error TS2564 Definitely assigned in a plugin
180✔
115
  users;
180✔
116

180✔
117
  /** @type {import('./plugins/waitlist.js').Waitlist} */
180✔
118
  // @ts-expect-error TS2564 Definitely assigned in a plugin
180✔
119
  waitlist;
180✔
120

180✔
121
  /** @type {import('./HttpApi.js').HttpApi} */
180✔
122
  // @ts-expect-error TS2564 Definitely assigned in a plugin
180✔
123
  httpApi;
180✔
124

180✔
125
  /** @type {import('./SocketServer.js').default} */
180✔
126
  // @ts-expect-error TS2564 Definitely assigned in a plugin
180✔
127
  socketServer;
180✔
128

180✔
129
  /** @type {Emittery<import('./redisMessages.js').ServerActionParameters>} */
180✔
130
  events = new Emittery({
180✔
131
    debug: { name: 'u-wave-core' },
180✔
132
  });
180✔
133

180✔
134
  /**
180✔
135
   * @type {Map<string, Source>}
180✔
136
   */
180✔
137
  #sources = new Map();
180✔
138

180✔
139
  /**
180✔
140
   * @param {Options} options
180✔
141
   */
180✔
142
  constructor(options) {
180✔
143
    super();
180✔
144

180✔
145
    const boot = avvio(this);
180✔
146

180✔
147
    this.logger = pino({
180✔
148
      ...options.logger,
180✔
149
      redact: ['req.headers.cookie', 'res.headers["set-cookie"]'],
180✔
150
    });
180✔
151

180✔
152
    this.options = {
180✔
153
      sqlite: DEFAULT_SQLITE_PATH,
180✔
154
      ...options,
180✔
155
    };
180✔
156

180✔
157
    /** @type {Kysely<import('./schema.js').Database>} */
180✔
158
    this.db = new Kysely({
180✔
159
      dialect: new SqliteDialect({
180✔
160
        database: () => connectSqlite(options.sqlite ?? DEFAULT_SQLITE_PATH),
180!
161
      }),
180✔
162
      // dialect: new PostgresDialect({
180✔
163
      //   pool: new pg.Pool({
180✔
164
      //     user: 'uwave',
180✔
165
      //     database: 'uwave_test',
180✔
166
      //   }),
180✔
167
      // }),
180✔
168
      plugins: [
180✔
169
        new UwCamelCasePlugin(),
180✔
170
        new SqliteDateColumnsPlugin(['createdAt', 'updatedAt', 'expiresAt', 'playedAt', 'lastSeenAt']),
180✔
171
      ],
180✔
172
    });
180✔
173

180✔
174
    if (typeof options.redis === 'string') {
180!
UNCOV
175
      this.redis = new Redis(options.redis, { lazyConnect: true });
×
176
    } else if (options.redis != null) {
180!
177
      this.redis = new Redis({ ...options.redis, lazyConnect: true });
×
178
    }
×
179

180✔
180
    this.configureRedis();
180✔
181

180✔
182
    boot.onClose(() => Promise.all([
180✔
183
      this.redis?.quit(),
180!
184
      this.db.destroy(),
180✔
185
    ]));
180✔
186

180✔
187
    this.keyv = new KeyValue(this.db);
180✔
188

180✔
189
    boot.use(migrations);
180✔
190
    boot.use(configStore);
180✔
191

180✔
192
    boot.use(passport, {
180✔
193
      secret: this.options.secret,
180✔
194
    });
180✔
195

180✔
196
    const sessionStore = new SqliteSessionStore(this.db, this.logger.child({ ns: 'uwave:sessions' }));
180✔
197

180✔
198
    // Initial API setup
180✔
199
    boot.use(httpApi, {
180✔
200
      secret: this.options.secret,
180✔
201
      sessionStore,
180✔
202
      helmet: this.options.helmet,
180✔
203
      trustProxy: this.options.trustProxy,
180✔
204
      mailTransport: this.options.mailTransport,
180✔
205
      recaptcha: this.options.recaptcha,
180✔
206
      createPasswordResetEmail: this.options.createPasswordResetEmail,
180✔
207
      onError: this.options.onError,
180✔
208
    });
180✔
209
    boot.use(SocketServer.plugin, { secret: this.options.secret, sessionStore });
180✔
210

180✔
211
    boot.use(acl);
180✔
212
    boot.use(chat);
180✔
213
    boot.use(motd);
180✔
214
    boot.use(playlists);
180✔
215
    boot.use(users);
180✔
216
    boot.use(bans);
180✔
217
    boot.use(history);
180✔
218
    boot.use(waitlist);
180✔
219
    boot.use(booth);
180✔
220

180✔
221
    boot.use(errorHandling);
180✔
222
  }
180✔
223

180✔
224
  /**
180✔
225
   * An array of registered sources.
180✔
226
   *
180✔
227
   * @type {Source[]}
180✔
228
   */
180✔
229
  get sources() {
180✔
230
    return [...this.#sources.values()];
×
231
  }
×
232

180✔
233
  /**
180✔
234
   * Get or register a source plugin.
180✔
235
   * If the first parameter is a string, returns an existing source plugin.
180✔
236
   * Else, adds a source plugin and returns its wrapped source plugin.
180✔
237
   *
180✔
238
   * @typedef {((uw: UwaveServer, opts: object) => SourcePlugin)} SourcePluginFactory
180✔
239
   * @typedef {SourcePlugin | SourcePluginFactory} ToSourcePlugin
180✔
240
   * @param {string | Omit<ToSourcePlugin, 'default'> | { default: ToSourcePlugin }} sourcePlugin
180✔
241
   *     Source name or definition.
180✔
242
   *     When a string: Source type name.
180✔
243
   *     Used to signal where a given media item originated from.
180✔
244
   *     When a function or object: Source plugin or plugin factory.
180✔
245
   * @param {object} opts Options to pass to the source plugin. Only used if
180✔
246
   *     a source plugin factory was passed to `sourcePlugin`.
180✔
247
   */
180✔
248
  source(sourcePlugin, opts = {}) {
180✔
249
    if (typeof sourcePlugin === 'string') {
204✔
250
      return this.#sources.get(sourcePlugin);
125✔
251
    }
125✔
252

79✔
253
    const sourceFactory = 'default' in sourcePlugin && sourcePlugin.default ? sourcePlugin.default : sourcePlugin;
204!
254
    if (typeof sourceFactory !== 'function' && typeof sourceFactory !== 'object') {
204!
255
      throw new TypeError(`Source plugin should be a function, got ${typeof sourceFactory}`);
×
256
    }
×
257

79✔
258
    const sourceDefinition = typeof sourceFactory === 'function'
79✔
259
      ? sourceFactory(this, opts)
204✔
260
      : sourceFactory;
204✔
261
    const sourceType = sourceDefinition.name;
204✔
262
    if (typeof sourceType !== 'string') {
204!
263
      throw new TypeError('Source plugin does not specify a name. It may be incompatible with this version of üWave.');
×
264
    }
×
265
    const newSource = new Source(this, sourceType, sourceDefinition);
79✔
266

79✔
267
    this.#sources.set(sourceType, newSource);
79✔
268

79✔
269
    return newSource;
79✔
270
  }
204✔
271

180✔
272
  /**
180✔
273
   * @private
180✔
274
   */
180✔
275
  configureRedis() {
180✔
276
    if (this.redis == null) {
180✔
277
      return;
180✔
278
    }
180✔
NEW
279

×
UNCOV
280
    const log = this.logger.child({ ns: 'uwave:redis' });
×
UNCOV
281

×
UNCOV
282
    this.redis.on('error', (error) => {
×
283
      log.error(error);
×
284
      this.emit('redisError', error);
×
UNCOV
285
    });
×
UNCOV
286
    this.redis.on('reconnecting', () => {
×
287
      log.info('trying to reconnect...');
×
UNCOV
288
    });
×
UNCOV
289

×
UNCOV
290
    this.redis.on('end', () => {
×
UNCOV
291
      log.info('disconnected');
×
UNCOV
292
      this.emit('redisDisconnect');
×
UNCOV
293
    });
×
UNCOV
294

×
UNCOV
295
    this.redis.on('connect', () => {
×
UNCOV
296
      log.info('connected');
×
UNCOV
297
      this.emit('redisConnect');
×
UNCOV
298
    });
×
299
  }
180✔
300

180✔
301
  /**
180✔
302
   * Publish an event to the üWave channel.
180✔
303
   *
180✔
304
   * @template {keyof import('./redisMessages.js').ServerActionParameters} CommandName
180✔
305
   * @param {CommandName} command
180✔
306
   * @param {import('./redisMessages.js').ServerActionParameters[CommandName]} data
180✔
307
   */
180✔
308
  publish(command, data) {
180✔
309
    return this.events.emit(command, data);
263✔
310
  }
263✔
311

180✔
312
  async listen() {
180✔
313
    /** @type {import('avvio').Avvio<this>} */
180✔
314
    // @ts-expect-error TS2322
180✔
315
    const boot = this; // eslint-disable-line @typescript-eslint/no-this-alias
180✔
316
    await boot.ready();
180✔
317

180✔
318
    /** @type {(this: import('net').Server, port?: number) => Promise<void>} */
180✔
319
    const listen = promisify(this.server.listen);
180✔
320
    await listen.call(this.server, this.options.port);
180✔
321
  }
180✔
322
}
180✔
323

1✔
324
export default UwaveServer;
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