@magicworks/client-party-game 0.1.0-rc.4 → 0.1.0-rc.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +18 -3
  2. package/dist/client.d.ts +37 -39
  3. package/dist/client.d.ts.map +1 -1
  4. package/dist/client.js +160 -183
  5. package/dist/client.js.map +1 -1
  6. package/dist/connection.d.ts +158 -0
  7. package/dist/connection.d.ts.map +1 -0
  8. package/dist/connection.js +375 -0
  9. package/dist/connection.js.map +1 -0
  10. package/dist/protocol/codes.d.ts +29 -0
  11. package/dist/protocol/codes.d.ts.map +1 -0
  12. package/dist/protocol/codes.js +56 -0
  13. package/dist/protocol/codes.js.map +1 -0
  14. package/dist/protocol/events.d.ts +129 -0
  15. package/dist/protocol/events.d.ts.map +1 -0
  16. package/dist/protocol/events.js +131 -0
  17. package/dist/protocol/events.js.map +1 -0
  18. package/dist/protocol/index.d.ts +25 -0
  19. package/dist/protocol/index.d.ts.map +1 -0
  20. package/dist/protocol/index.js +41 -0
  21. package/dist/protocol/index.js.map +1 -0
  22. package/dist/protocol/limits.d.ts +40 -0
  23. package/dist/protocol/limits.d.ts.map +1 -0
  24. package/dist/protocol/limits.js +43 -0
  25. package/dist/protocol/limits.js.map +1 -0
  26. package/dist/protocol/messages.d.ts +294 -0
  27. package/dist/protocol/messages.d.ts.map +1 -0
  28. package/dist/protocol/messages.js +3 -0
  29. package/dist/protocol/messages.js.map +1 -0
  30. package/dist/protocol/placement.d.ts +63 -0
  31. package/dist/protocol/placement.d.ts.map +1 -0
  32. package/dist/protocol/placement.js +82 -0
  33. package/dist/protocol/placement.js.map +1 -0
  34. package/dist/protocol/routing.d.ts +26 -0
  35. package/dist/protocol/routing.d.ts.map +1 -0
  36. package/dist/protocol/routing.js +19 -0
  37. package/dist/protocol/routing.js.map +1 -0
  38. package/dist/protocol/scoring.d.ts +7 -0
  39. package/dist/protocol/scoring.d.ts.map +1 -0
  40. package/dist/protocol/scoring.js +13 -0
  41. package/dist/protocol/scoring.js.map +1 -0
  42. package/dist/protocol/speedtest.d.ts +9 -0
  43. package/dist/protocol/speedtest.d.ts.map +1 -0
  44. package/dist/protocol/speedtest.js +22 -0
  45. package/dist/protocol/speedtest.js.map +1 -0
  46. package/dist/socket.d.ts +4 -2
  47. package/dist/socket.d.ts.map +1 -1
  48. package/package.json +1 -1
  49. package/dist/protocol.d.ts +0 -509
  50. package/dist/protocol.d.ts.map +0 -1
  51. package/dist/protocol.js +0 -262
  52. package/dist/protocol.js.map +0 -1
package/dist/client.js CHANGED
@@ -1,12 +1,26 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PartyGameClient = void 0;
4
+ const connection_1 = require("./connection");
4
5
  const protocol_1 = require("./protocol");
6
+ /**
7
+ * The global fetch, looked up when called. Called as a plain function, as
8
+ * browsers insist, and without `globalThis`, which the app's oldest web
9
+ * engine (Chrome 56) does not have.
10
+ */
11
+ const globalFetch = url => typeof fetch === 'function'
12
+ ? fetch(url)
13
+ : Promise.reject(new Error('no fetch: pass one to PartyGameClient'));
5
14
  const defaultAckTimeoutMs = 8000;
15
+ const defaultPlacementTimeoutMs = 4000;
6
16
  const defaultClockSyncIntervalMs = 60 * 1000;
17
+ /**
18
+ * A server that has gone keeps its TVs waiting no more than about 15s for a
19
+ * new room, and one that stalls for a few seconds keeps them: a TV that gives
20
+ * up on its room mid-game leaves its phones behind in it.
21
+ */
22
+ const defaultRoomRetryMs = [1000, 3000, 5000, 10000, 15000];
7
23
  const clockSyncSamples = 5;
8
- /** Servers that can be on their way out at once, during a deploy. */
9
- const maxDrainingRetries = 3;
10
24
  /** Hops a room can have made between servers since we last knew its code. */
11
25
  const maxRedirects = 3;
12
26
  /**
@@ -29,21 +43,14 @@ const moduleRoomMemory = {
29
43
  rememberedCode = code;
30
44
  },
31
45
  };
32
- function withTimeout(promise, ms, what) {
33
- return new Promise((resolve, reject) => {
34
- const timer = setTimeout(() => reject(new Error(`party server did not answer ${what} in ${ms}ms`)), ms);
35
- promise.then(value => {
36
- clearTimeout(timer);
37
- resolve(value);
38
- }, error => {
39
- clearTimeout(timer);
40
- reject(error);
41
- });
46
+ /** `players` with each one's flags in `flags` taken on: a new list, so a screen holding the last one sees the change. */
47
+ function withFlags(players, flags) {
48
+ const changed = new Map(flags.map(entry => [entry.id, entry]));
49
+ return players.map(player => {
50
+ const entry = changed.get(player.id);
51
+ return entry ? { ...player, ...entry } : player;
42
52
  });
43
53
  }
44
- function routesAlike(a, b) {
45
- return (a ? (0, protocol_1.codePrefixOf)(a) : null) === (b ? (0, protocol_1.codePrefixOf)(b) : null);
46
- }
47
54
  /**
48
55
  * Host side of the party games protocol. The TV owns the video and therefore
49
56
  * owns round timing: it tells the server when a round starts and when it ends,
@@ -53,32 +60,18 @@ function routesAlike(a, b) {
53
60
  * so navigating from the lobby into the first round keeps the same room and
54
61
  * the same players.
55
62
  *
56
- * Rooms live in the memory of one server of several, and the load balancer
57
- * finds it by the room code: every connection made for a room carries its
58
- * code (ROUTING_QUERY_KEY). A connection made before there is a room goes to
59
- * any server, so once that server has opened one the client connects again
60
- * with its code - in the lobby, where a moment's reconnect costs nothing,
61
- * rather than leaving the game on a route the load balancer can take away.
63
+ * Rooms live in the memory of one server of several. Reaching the room's -
64
+ * placing each socket, and placing it again when it drops - is the
65
+ * connection's job: see PartyConnection.
62
66
  */
63
67
  class PartyGameClient {
64
- url;
65
- regionUrls;
66
- io;
68
+ connection;
67
69
  hostId;
68
70
  roomMemory;
69
71
  logger;
70
- ackTimeoutMs;
71
72
  clockSyncIntervalMs;
72
- socket = null;
73
- /** The room code the current socket was routed by, if any. */
74
- routedCode = null;
75
- /**
76
- * Whether the current connection is attached to the session's room. Host
77
- * calls wait for it: sent while a reconnect is still re-attaching, they
78
- * would reach the server from a socket it does not know the room of.
79
- */
80
- attached = true;
81
- attachedWaiters = [];
73
+ /** Roster changes for a room this TV is about to be handed: see adopt. */
74
+ earlyFlags = null;
82
75
  session = null;
83
76
  /** Where the session's room is now, when a server has said it moved. */
84
77
  redirectCode = null;
@@ -99,21 +92,28 @@ class PartyGameClient {
99
92
  clockOffset = 0;
100
93
  clockSyncTimer = null;
101
94
  constructor(options) {
102
- this.url = options.url.replace(/\/+$/, '');
103
- /* No Object.entries or fromEntries: older TV browsers lack them, and the
104
- app has no polyfill. */
105
- const regionUrls = options.regionUrls || {};
106
- this.regionUrls = {};
107
- for (const region of Object.keys(regionUrls)) {
108
- this.regionUrls[region] = regionUrls[region].replace(/\/+$/, '');
109
- }
110
- this.io = options.io;
111
95
  this.hostId = options.hostId || processHostId;
112
96
  this.roomMemory = options.roomMemory || moduleRoomMemory;
113
97
  this.logger = options.logger || console;
114
- this.ackTimeoutMs = options.ackTimeoutMs ?? defaultAckTimeoutMs;
115
98
  this.clockSyncIntervalMs =
116
99
  options.clockSyncIntervalMs ?? defaultClockSyncIntervalMs;
100
+ this.connection = new connection_1.PartyConnection({
101
+ url: options.url.replace(/\/+$/, ''),
102
+ regionUrls: options.regionUrls || {},
103
+ io: options.io,
104
+ fetch: options.fetch || globalFetch,
105
+ logger: this.logger,
106
+ ackTimeoutMs: options.ackTimeoutMs ?? defaultAckTimeoutMs,
107
+ placementTimeoutMs: options.placementTimeoutMs ?? defaultPlacementTimeoutMs,
108
+ roomRetryMs: options.roomRetryMs ?? defaultRoomRetryMs,
109
+ }, {
110
+ roomCode: () => this.redirectCode || this.session?.code || null,
111
+ opened: socket => this.listen(socket),
112
+ connected: (socket, prefix) => {
113
+ this.syncClock();
114
+ this.reattach(socket, prefix);
115
+ },
116
+ });
117
117
  }
118
118
  getSession() {
119
119
  return this.session;
@@ -122,7 +122,7 @@ class PartyGameClient {
122
122
  return this.players;
123
123
  }
124
124
  isConnected() {
125
- return this.socket ? this.socket.connected : false;
125
+ return this.connection.isConnected();
126
126
  }
127
127
  /**
128
128
  * A download of at least `bytes` random bytes for the TV to time in the
@@ -140,81 +140,8 @@ class PartyGameClient {
140
140
  return { url: `${origin}${(0, protocol_1.speedtestPath)(size)}`, bytes: size };
141
141
  }
142
142
  /* ---------------------------------------------------------- connection */
143
- /** The socket for `code`'s room (or for no room), connecting a new one if need be. */
144
- socketFor(code, { fresh = false } = {}) {
145
- if (this.socket && !fresh && routesAlike(this.routedCode, code)) {
146
- return this.socket;
147
- }
148
- if (this.socket) {
149
- this.logger.debug('PartyGames', 'reconnecting to route by', code);
150
- this.socket.close();
151
- }
152
- this.routedCode = code;
153
- this.socket = this.openSocket(code);
154
- return this.socket;
155
- }
156
- currentSocket() {
157
- return this.socket || this.socketFor(this.session?.code || null);
158
- }
159
- setAttached(attached) {
160
- this.attached = attached;
161
- if (attached) {
162
- const waiters = this.attachedWaiters;
163
- this.attachedWaiters = [];
164
- for (const waiter of waiters) {
165
- waiter();
166
- }
167
- }
168
- }
169
- whenAttached(what) {
170
- if (this.attached) {
171
- return Promise.resolve();
172
- }
173
- return withTimeout(new Promise(resolve => this.attachedWaiters.push(resolve)), this.ackTimeoutMs, `${what} (reconnecting)`);
174
- }
175
- /** Where `code`'s room is: its region's servers, or for no room, the nearest. */
176
- urlFor(code) {
177
- return code
178
- ? (0, protocol_1.socketUrlForCode)(code, {
179
- socketUrl: this.url,
180
- regionSocketUrls: this.regionUrls,
181
- })
182
- : this.url;
183
- }
184
- /** A socket routed by `code`, to `url` (by default, where `code`'s room is). */
185
- connectTo(code, url = this.urlFor(code)) {
186
- return this.io(url, {
187
- transports: ['websocket'],
188
- jsonp: false,
189
- query: code ? { [protocol_1.ROUTING_QUERY_KEY]: code } : {},
190
- forceNew: true,
191
- reconnection: true,
192
- reconnectionDelay: 1000,
193
- reconnectionDelayMax: 5000,
194
- timeout: this.ackTimeoutMs,
195
- });
196
- }
197
- openSocket(code) {
198
- this.setAttached(false);
199
- const socket = this.connectTo(code);
200
- socket.on('connect', () => {
201
- if (socket !== this.socket) {
202
- return;
203
- }
204
- this.logger.debug('PartyGames', 'connected to', this.url);
205
- this.syncClock();
206
- this.reattach(socket);
207
- });
208
- socket.on('disconnect', () => {
209
- if (socket !== this.socket) {
210
- return;
211
- }
212
- this.logger.debug('PartyGames', 'disconnected');
213
- this.setAttached(false);
214
- });
215
- socket.on('connect_error', (error) => {
216
- this.logger.warn('PartyGames', 'connect error', error?.message);
217
- });
143
+ /** Listens for the room's messages on a socket the connection has opened. */
144
+ listen(socket) {
218
145
  socket.on(protocol_1.HostMessage.Players, (payload) => {
219
146
  if (this.session && payload?.code && payload.code !== this.session.code) {
220
147
  /* a room this TV is only now being given - its own went, or moved -
@@ -222,10 +149,26 @@ class PartyGameClient {
222
149
  the roster is the room's it was in */
223
150
  return;
224
151
  }
225
- this.players = payload?.players || [];
226
- for (const listener of this.playersListeners) {
227
- listener(this.players);
152
+ this.setPlayers(payload?.players || []);
153
+ });
154
+ socket.on(protocol_1.HostMessage.PlayersChanged, (payload) => {
155
+ if (!payload?.players) {
156
+ return;
157
+ }
158
+ if (payload.code !== this.session?.code) {
159
+ /* a room being handed to this TV, whose answer has not been taken
160
+ on yet: kept until it is, as unlike a whole roster these are not
161
+ sent again */
162
+ this.earlyFlags =
163
+ this.earlyFlags?.code === payload.code
164
+ ? {
165
+ code: payload.code,
166
+ players: [...this.earlyFlags.players, ...payload.players],
167
+ }
168
+ : payload;
169
+ return;
228
170
  }
171
+ this.setPlayers(withFlags(this.players, payload.players));
229
172
  });
230
173
  socket.on(protocol_1.HostMessage.AllAnswered, (payload) => {
231
174
  for (const listener of this.allAnsweredListeners) {
@@ -241,7 +184,7 @@ class PartyGameClient {
241
184
  }
242
185
  });
243
186
  socket.on(protocol_1.HostMessage.Move, (message) => {
244
- if (socket === this.socket && message?.token) {
187
+ if (socket === this.connection.current() && message?.token) {
245
188
  this.moveRoom(socket, message.token);
246
189
  }
247
190
  });
@@ -257,25 +200,35 @@ class PartyGameClient {
257
200
  /* clocks drift, and so does the route to the server */
258
201
  this.clockSyncTimer = setInterval(() => this.syncClock(), this.clockSyncIntervalMs);
259
202
  }
260
- return socket;
203
+ }
204
+ setPlayers(players) {
205
+ this.players = players;
206
+ for (const listener of this.playersListeners) {
207
+ listener(this.players);
208
+ }
261
209
  }
262
210
  /**
263
211
  * Back on the room we were already hosting, so a dropped connection
264
212
  * mid-party does not lose the players or their scores. If the room is gone
265
213
  * - its server is - the server opens a new one, and the session follows.
266
214
  */
267
- reattach(socket) {
215
+ reattach(socket, prefix) {
268
216
  const session = this.session;
269
217
  if (!session) {
270
- this.setAttached(true);
218
+ this.connection.attachedTo(socket);
271
219
  return;
272
220
  }
273
- this.send(socket, protocol_1.HostEvent.Create, {
221
+ let attached = true;
222
+ this.connection
223
+ .send(socket, protocol_1.HostEvent.Create, {
274
224
  code: this.redirectCode || session.code,
275
225
  hostId: this.hostId,
276
226
  features,
277
227
  })
278
228
  .then(response => {
229
+ if (socket !== this.connection.current()) {
230
+ return;
231
+ }
279
232
  if (response?.ok) {
280
233
  // A re-attach is not a reopen, so it says `reused: false`; the
281
234
  // lobby still is the one it was.
@@ -294,13 +247,14 @@ class PartyGameClient {
294
247
  }
295
248
  else if (response?.reason === protocol_1.HostRefusal.Moved && response.movedTo) {
296
249
  /* it left that server while we were away: follow it */
250
+ attached = false;
297
251
  this.redirectCode = response.movedTo;
298
- this.socketFor(response.movedTo);
252
+ this.connection.connectFor(response.movedTo);
299
253
  }
300
254
  else if (response?.reason === protocol_1.HostRefusal.Draining) {
301
- /* the room is gone and its server opens no new ones: another will,
302
- over a new connection the load balancer can send elsewhere */
303
- this.socketFor(null, { fresh: true });
255
+ /* the room is gone and its server opens no new ones: another will */
256
+ attached = false;
257
+ this.connection.connectFor(null, { avoid: [prefix] });
304
258
  }
305
259
  else {
306
260
  this.logger.warn('PartyGames', 'failed to re-attach', response);
@@ -308,13 +262,16 @@ class PartyGameClient {
308
262
  })
309
263
  .catch(error => this.logger.warn('PartyGames', 'failed to re-attach', error))
310
264
  .finally(() => {
311
- if (socket === this.socket) {
312
- this.setAttached(true);
265
+ if (attached) {
266
+ this.connection.attachedTo(socket);
313
267
  }
314
268
  });
315
269
  }
316
270
  /** Takes on a session the server has handed back, rerouting by its code if need be. */
317
271
  adopt(response) {
272
+ if (response.regionSocketUrls) {
273
+ this.connection.setRegionUrls(response.regionSocketUrls);
274
+ }
318
275
  const previous = this.session;
319
276
  this.redirectCode = null;
320
277
  this.session = {
@@ -327,6 +284,11 @@ class PartyGameClient {
327
284
  players: response.players || [],
328
285
  };
329
286
  this.players = this.session.players;
287
+ const early = this.earlyFlags;
288
+ this.earlyFlags = null;
289
+ if (early?.code === response.code) {
290
+ this.setPlayers(withFlags(this.players, early.players));
291
+ }
330
292
  this.roomMemory.set(response.code);
331
293
  if (previous && previous.code !== response.code) {
332
294
  this.logger.warn('PartyGames', 'room', previous.code, 'is gone, now', response.code);
@@ -334,60 +296,66 @@ class PartyGameClient {
334
296
  listener(this.session);
335
297
  }
336
298
  }
337
- if (!routesAlike(this.routedCode, response.code)) {
338
- this.socketFor(response.code);
339
- }
340
- else {
341
- /* same server: from now on this socket stands for the room */
342
- this.routedCode = response.code;
299
+ if (!this.connection.routesTo(response.code)) {
300
+ this.connection.connectFor(response.code);
343
301
  }
344
302
  }
345
- send(socket, event, payload = {}) {
346
- return withTimeout(new Promise(resolve => socket.emit(event, payload, resolve)), this.ackTimeoutMs, event);
347
- }
348
303
  /**
349
304
  * A host call: once the connection is attached to the room. A call that
350
305
  * crossed the room's move to another server is sent again from there.
351
306
  */
352
307
  async emit(event, payload = {}) {
353
308
  for (let attempt = 0;; ++attempt) {
354
- const socket = this.currentSocket();
355
- await this.whenAttached(event);
356
- const response = await this.send(this.socket || socket, event, payload);
309
+ this.connection.ensure();
310
+ await this.connection.whenAttached(event);
311
+ const socket = this.connection.current();
312
+ if (!socket) {
313
+ throw new Error(`party server not connected for ${event}`);
314
+ }
315
+ const response = await this.connection.send(socket, event, payload);
357
316
  const reason = response?.reason;
358
317
  if (reason !== protocol_1.HostRefusal.Moving || attempt >= maxMovingRetries) {
359
318
  return response;
360
319
  }
361
- if (this.attached) {
320
+ if (this.connection.isAttached()) {
362
321
  /* refused before the server said where the room is going: the
363
322
  room:move follows, and has us reconnecting - or the move failed
364
323
  and the server lets the room go on here after a while */
365
324
  await new Promise(resolve => setTimeout(resolve, movingRetryMs));
366
325
  }
367
326
  /* the room:move that came before this answer has us reconnecting */
368
- await this.whenAttached(event);
327
+ await this.connection.whenAttached(event);
369
328
  }
370
329
  }
371
330
  /**
372
331
  * Takes the room to another server, which the one it is on asked for: it
373
- * is on its way out, and no game is being played. The room is adopted over
374
- * a connection with no room in its query - so the load balancer hands it
375
- * to a server still taking parties - the old server is told where it went,
376
- * for the phones, and the client then connects for the new code as usual.
377
- * Host calls wait meanwhile. If it cannot be done the room stays where it
378
- * was, and the server asks again later.
332
+ * is on its way out, and no game is being played. Another server of the
333
+ * room's region - the handover is between them, in their tables - is
334
+ * placed for no room and adopts it over a socket of its own; the old server
335
+ * is told where it went, for the phones; and the client then connects for
336
+ * the new code as usual. Host calls wait meanwhile. If it cannot be done
337
+ * the room stays where it was, and the server asks again later.
379
338
  */
380
339
  async moveRoom(oldSocket, token) {
381
- this.setAttached(false);
382
- // The handover is between the servers of one region, whose tables hold
383
- // it, so it goes to the room's region rather than the nearest.
384
- const regionUrl = this.urlFor(this.session?.code || null);
340
+ this.connection.setAttached(false);
341
+ const regionUrl = this.connection.urlFor(this.session?.code || null);
342
+ const avoid = this.connection.avoidingRouted();
385
343
  let adopted = null;
386
- for (let attempt = 0; attempt <= maxDrainingRetries && !adopted; ++attempt) {
387
- const socket = this.connectTo(null, regionUrl);
344
+ for (let attempt = 0; attempt <= connection_1.maxDrainingRetries && !adopted; ++attempt) {
345
+ const placed = await this.connection.place(regionUrl, null, avoid);
346
+ if (!placed) {
347
+ /* no server answered: give it a moment before asking again */
348
+ await new Promise(resolve => setTimeout(resolve, (0, protocol_1.reconnectDelay)(attempt)));
349
+ continue;
350
+ }
351
+ if (placed === connection_1.refused) {
352
+ /* the next ask goes to the next server */
353
+ continue;
354
+ }
355
+ const socket = this.connection.connectTo(placed.prefix, placed.url);
388
356
  try {
389
- await withTimeout(new Promise(resolve => socket.on('connect', () => resolve())), this.ackTimeoutMs, 'connecting to move the room');
390
- const response = await this.send(socket, protocol_1.HostEvent.Adopt, {
357
+ await this.connection.whenConnected(socket, 'connecting to move the room');
358
+ const response = await this.connection.send(socket, protocol_1.HostEvent.Adopt, {
391
359
  token,
392
360
  hostId: this.hostId,
393
361
  features,
@@ -395,28 +363,34 @@ class PartyGameClient {
395
363
  if (response?.ok) {
396
364
  adopted = response;
397
365
  }
398
- else if (response?.reason !== protocol_1.HostRefusal.Draining) {
366
+ else if (response?.reason === protocol_1.HostRefusal.Draining) {
367
+ avoid.push(placed.prefix);
368
+ }
369
+ else {
399
370
  this.logger.warn('PartyGames', 'could not move the room', response);
400
371
  break;
401
372
  }
402
373
  }
403
374
  catch (error) {
404
375
  this.logger.warn('PartyGames', 'could not move the room', error);
376
+ avoid.push(placed.prefix);
405
377
  }
406
378
  finally {
407
379
  socket.close();
408
380
  }
409
381
  }
410
- if (oldSocket !== this.socket) {
382
+ if (oldSocket !== this.connection.current()) {
411
383
  return;
412
384
  }
413
385
  if (!adopted) {
414
- this.setAttached(true);
386
+ this.connection.setAttached(true);
415
387
  return;
416
388
  }
417
389
  this.logger.debug('PartyGames', 'room moved to', adopted.code);
418
- await this.send(oldSocket, protocol_1.HostEvent.Moved, { code: adopted.code }).catch(error => this.logger.warn('PartyGames', 'could not say where the room went', error));
419
- if (oldSocket === this.socket) {
390
+ await this.connection
391
+ .send(oldSocket, protocol_1.HostEvent.Moved, { code: adopted.code })
392
+ .catch(error => this.logger.warn('PartyGames', 'could not say where the room went', error));
393
+ if (oldSocket === this.connection.current()) {
420
394
  this.adopt(adopted);
421
395
  }
422
396
  }
@@ -430,8 +404,12 @@ class PartyGameClient {
430
404
  let best = null;
431
405
  for (let sample = 0; sample < clockSyncSamples; ++sample) {
432
406
  try {
407
+ const socket = this.connection.current();
408
+ if (!socket) {
409
+ break;
410
+ }
433
411
  const sentAt = Date.now();
434
- const response = await this.send(this.currentSocket(), protocol_1.TimeEvent.Ping);
412
+ const response = await this.connection.send(socket, protocol_1.TimeEvent.Ping);
435
413
  const receivedAt = Date.now();
436
414
  const rtt = receivedAt - sentAt;
437
415
  if (typeof response?.now === 'number' && (!best || rtt < best.rtt)) {
@@ -487,8 +465,8 @@ class PartyGameClient {
487
465
  };
488
466
  /* A fresh room is opened over the connection we have, so the server
489
467
  holding the old one closes it; any other ask goes where its room is. */
490
- if (!fresh) {
491
- this.socketFor(code || null);
468
+ if (!fresh && !this.connection.connectedFor(code || null)) {
469
+ this.connection.connectFor(code || null);
492
470
  }
493
471
  for (let attempt = 0;; ++attempt) {
494
472
  if (!fresh && attempt > 0) {
@@ -498,7 +476,7 @@ class PartyGameClient {
498
476
  const response = await this.emit(protocol_1.HostEvent.Create, request);
499
477
  if (response?.ok) {
500
478
  this.adopt(response);
501
- await this.whenAttached('the room');
479
+ await this.connection.whenAttached('the room');
502
480
  return this.session;
503
481
  }
504
482
  if (response?.reason === protocol_1.HostRefusal.Moved &&
@@ -506,11 +484,11 @@ class PartyGameClient {
506
484
  attempt < maxRedirects) {
507
485
  /* opened again on another server since we last knew it */
508
486
  this.redirectCode = response.movedTo;
509
- this.socketFor(response.movedTo);
487
+ this.connection.connectFor(response.movedTo);
510
488
  continue;
511
489
  }
512
490
  if (response?.reason === protocol_1.HostRefusal.Draining &&
513
- attempt < maxDrainingRetries) {
491
+ attempt < connection_1.maxDrainingRetries) {
514
492
  /* asked a server on its way out: any other server will do */
515
493
  this.logger.debug('PartyGames', 'server draining, asking another');
516
494
  if (fresh) {
@@ -519,7 +497,9 @@ class PartyGameClient {
519
497
  }
520
498
  // A new connection even if this one had no room either: over the
521
499
  // same socket the same draining server would answer again.
522
- this.socketFor(null, { fresh: true });
500
+ this.connection.connectFor(null, {
501
+ avoid: this.connection.avoidingRouted(),
502
+ });
523
503
  continue;
524
504
  }
525
505
  throw new Error(response?.error || 'failed to open a party game room');
@@ -674,7 +654,9 @@ class PartyGameClient {
674
654
  this.lastHost) {
675
655
  const { gameType, genre, genreName } = this.lastHost;
676
656
  this.session = null;
677
- this.socketFor(null, { fresh: true });
657
+ this.connection.connectFor(null, {
658
+ avoid: this.connection.avoidingRouted(),
659
+ });
678
660
  return (await this.host(gameType, genre, genreName, true)).players;
679
661
  }
680
662
  if (!response?.ok) {
@@ -729,13 +711,8 @@ class PartyGameClient {
729
711
  this.session = null;
730
712
  this.redirectCode = null;
731
713
  this.players = [];
732
- if (this.socket) {
733
- const socket = this.socket;
734
- this.socket = null;
735
- this.routedCode = null;
736
- socket.close();
737
- }
738
- this.setAttached(true);
714
+ this.earlyFlags = null;
715
+ this.connection.close();
739
716
  }
740
717
  }
741
718
  exports.PartyGameClient = PartyGameClient;