@magicworks/client-party-game 0.1.0-rc.5 → 0.1.0-rc.7

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 +32 -67
  3. package/dist/client.d.ts.map +1 -1
  4. package/dist/client.js +180 -272
  5. package/dist/client.js.map +1 -1
  6. package/dist/connection.d.ts +159 -0
  7. package/dist/connection.d.ts.map +1 -0
  8. package/dist/connection.js +389 -0
  9. package/dist/connection.js.map +1 -0
  10. package/dist/protocol/codes.d.ts +33 -0
  11. package/dist/protocol/codes.d.ts.map +1 -0
  12. package/dist/protocol/codes.js +60 -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 +308 -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 +83 -0
  31. package/dist/protocol/placement.d.ts.map +1 -0
  32. package/dist/protocol/placement.js +97 -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 -519
  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,14 +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;
7
- const defaultRegionFallbackMs = 12 * 1000;
8
- const defaultRegionDownMs = 5 * 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];
9
23
  const clockSyncSamples = 5;
10
- /** Servers that can be on their way out at once, during a deploy. */
11
- const maxDrainingRetries = 3;
12
24
  /** Hops a room can have made between servers since we last knew its code. */
13
25
  const maxRedirects = 3;
14
26
  /**
@@ -31,29 +43,14 @@ const moduleRoomMemory = {
31
43
  rememberedCode = code;
32
44
  },
33
45
  };
34
- function withTimeout(promise, ms, what) {
35
- return new Promise((resolve, reject) => {
36
- const timer = setTimeout(() => reject(new Error(`party server did not answer ${what} in ${ms}ms`)), ms);
37
- promise.then(value => {
38
- clearTimeout(timer);
39
- resolve(value);
40
- }, error => {
41
- clearTimeout(timer);
42
- reject(error);
43
- });
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;
44
52
  });
45
53
  }
46
- /** URLs without trailing slashes. No Object.entries or fromEntries: older TV browsers lack them, and the app has no polyfill. */
47
- function tidyUrls(urls) {
48
- const tidy = {};
49
- for (const key of Object.keys(urls)) {
50
- tidy[key] = urls[key].replace(/\/+$/, '');
51
- }
52
- return tidy;
53
- }
54
- function routesAlike(a, b) {
55
- return (a ? (0, protocol_1.codePrefixOf)(a) : null) === (b ? (0, protocol_1.codePrefixOf)(b) : null);
56
- }
57
54
  /**
58
55
  * Host side of the party games protocol. The TV owns the video and therefore
59
56
  * owns round timing: it tells the server when a round starts and when it ends,
@@ -63,41 +60,18 @@ function routesAlike(a, b) {
63
60
  * so navigating from the lobby into the first round keeps the same room and
64
61
  * the same players.
65
62
  *
66
- * Rooms live in the memory of one server of several, and the load balancer
67
- * finds it by the room code: every connection made for a room carries its
68
- * code (ROUTING_QUERY_KEY). A connection made before there is a room goes to
69
- * any server, so once that server has opened one the client connects again
70
- * with its code - in the lobby, where a moment's reconnect costs nothing,
71
- * 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.
72
66
  */
73
67
  class PartyGameClient {
74
- url;
75
- /** Built in at first, then as the servers say: see PartyRouting. */
76
- regionUrls;
77
- /** Regions given up on, until when: their rooms are looked for through the global name. */
78
- downRegions = {};
79
- io;
68
+ connection;
80
69
  hostId;
81
70
  roomMemory;
82
71
  logger;
83
- ackTimeoutMs;
84
72
  clockSyncIntervalMs;
85
- regionFallbackMs;
86
- regionDownMs;
87
- /** Runs while the current socket, routed to a room's region, is not connected. */
88
- regionTimer = null;
89
- /** A connection to the global name, seeing whether it is the region that is out of reach. */
90
- regionProbe = null;
91
- socket = null;
92
- /** The room code the current socket was routed by, if any. */
93
- routedCode = null;
94
- /**
95
- * Whether the current connection is attached to the session's room. Host
96
- * calls wait for it: sent while a reconnect is still re-attaching, they
97
- * would reach the server from a socket it does not know the room of.
98
- */
99
- attached = true;
100
- attachedWaiters = [];
73
+ /** Roster changes for a room this TV is about to be handed: see adopt. */
74
+ earlyFlags = null;
101
75
  session = null;
102
76
  /** Where the session's room is now, when a server has said it moved. */
103
77
  redirectCode = null;
@@ -118,17 +92,28 @@ class PartyGameClient {
118
92
  clockOffset = 0;
119
93
  clockSyncTimer = null;
120
94
  constructor(options) {
121
- this.url = options.url.replace(/\/+$/, '');
122
- this.regionUrls = tidyUrls(options.regionUrls || {});
123
- this.io = options.io;
124
95
  this.hostId = options.hostId || processHostId;
125
96
  this.roomMemory = options.roomMemory || moduleRoomMemory;
126
97
  this.logger = options.logger || console;
127
- this.ackTimeoutMs = options.ackTimeoutMs ?? defaultAckTimeoutMs;
128
98
  this.clockSyncIntervalMs =
129
99
  options.clockSyncIntervalMs ?? defaultClockSyncIntervalMs;
130
- this.regionFallbackMs = options.regionFallbackMs ?? defaultRegionFallbackMs;
131
- this.regionDownMs = options.regionDownMs ?? defaultRegionDownMs;
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
+ });
132
117
  }
133
118
  getSession() {
134
119
  return this.session;
@@ -137,7 +122,7 @@ class PartyGameClient {
137
122
  return this.players;
138
123
  }
139
124
  isConnected() {
140
- return this.socket ? this.socket.connected : false;
125
+ return this.connection.isConnected();
141
126
  }
142
127
  /**
143
128
  * A download of at least `bytes` random bytes for the TV to time in the
@@ -155,147 +140,8 @@ class PartyGameClient {
155
140
  return { url: `${origin}${(0, protocol_1.speedtestPath)(size)}`, bytes: size };
156
141
  }
157
142
  /* ---------------------------------------------------------- connection */
158
- /** The socket for `code`'s room (or for no room), connecting a new one if need be. */
159
- socketFor(code, { fresh = false } = {}) {
160
- if (this.socket && !fresh && routesAlike(this.routedCode, code)) {
161
- return this.socket;
162
- }
163
- if (this.socket) {
164
- this.logger.debug('PartyGames', 'reconnecting to route by', code);
165
- this.socket.close();
166
- }
167
- this.routedCode = code;
168
- this.socket = this.openSocket(code);
169
- return this.socket;
170
- }
171
- currentSocket() {
172
- return this.socket || this.socketFor(this.session?.code || null);
173
- }
174
- setAttached(attached) {
175
- this.attached = attached;
176
- if (attached) {
177
- const waiters = this.attachedWaiters;
178
- this.attachedWaiters = [];
179
- for (const waiter of waiters) {
180
- waiter();
181
- }
182
- }
183
- }
184
- whenAttached(what) {
185
- if (this.attached) {
186
- return Promise.resolve();
187
- }
188
- return withTimeout(new Promise(resolve => this.attachedWaiters.push(resolve)), this.ackTimeoutMs, `${what} (reconnecting)`);
189
- }
190
- /**
191
- * Where `code`'s room is: its region's servers, or for no room - or one in
192
- * a region given up on - the nearest.
193
- */
194
- urlFor(code) {
195
- const region = code ? (0, protocol_1.regionOfCode)(code) : null;
196
- if (!code || (region && (this.downRegions[region] || 0) > Date.now())) {
197
- return this.url;
198
- }
199
- return (0, protocol_1.socketUrlForCode)(code, {
200
- socketUrl: this.url,
201
- regionSocketUrls: this.regionUrls,
202
- });
203
- }
204
- /** A socket routed by `code`, to `url` (by default, where `code`'s room is). */
205
- connectTo(code, url = this.urlFor(code)) {
206
- return this.io(url, {
207
- transports: ['websocket'],
208
- jsonp: false,
209
- query: code ? { [protocol_1.ROUTING_QUERY_KEY]: code } : {},
210
- forceNew: true,
211
- reconnection: true,
212
- reconnectionDelay: 1000,
213
- reconnectionDelayMax: 5000,
214
- timeout: this.ackTimeoutMs,
215
- });
216
- }
217
- /**
218
- * Gives a room's region `regionFallbackMs` to answer `socket`, routed to it
219
- * by `code` (null stops watching). A region that does not is checked
220
- * against the global name: if that answers, the region is down - its rooms
221
- * with it, as a room lives in one server's memory - so the client goes
222
- * through the global name instead, where the room's code gets a new room,
223
- * in a region that is up. If it does not either, it is this TV that is
224
- * offline, and the room is waited for: it may well still be there.
225
- */
226
- watchRegion(socket, code) {
227
- if (this.regionTimer) {
228
- clearTimeout(this.regionTimer);
229
- this.regionTimer = null;
230
- }
231
- this.closeRegionProbe();
232
- if (!code) {
233
- return;
234
- }
235
- this.regionTimer = setTimeout(() => {
236
- this.regionTimer = null;
237
- if (socket !== this.socket || socket.connected) {
238
- return;
239
- }
240
- const probe = this.connectTo(null, this.url);
241
- this.regionProbe = probe;
242
- probe.on('connect', () => {
243
- if (probe !== this.regionProbe) {
244
- return;
245
- }
246
- this.closeRegionProbe();
247
- if (socket !== this.socket || socket.connected) {
248
- return;
249
- }
250
- const region = (0, protocol_1.regionOfCode)(code);
251
- if (region) {
252
- this.downRegions[region] = Date.now() + this.regionDownMs;
253
- }
254
- this.logger.warn('PartyGames', 'region', region, 'out of reach, going through', this.url, 'for', code);
255
- this.socketFor(code, { fresh: true });
256
- });
257
- probe.on('connect_error', () => {
258
- if (probe === this.regionProbe) {
259
- /* offline here too: the room is waited for */
260
- this.watchRegion(socket, code);
261
- }
262
- });
263
- }, this.regionFallbackMs);
264
- }
265
- closeRegionProbe() {
266
- const probe = this.regionProbe;
267
- if (probe) {
268
- this.regionProbe = null;
269
- probe.close();
270
- }
271
- }
272
- openSocket(code) {
273
- this.setAttached(false);
274
- const url = this.urlFor(code);
275
- const socket = this.connectTo(code, url);
276
- /* a room in a region of its own: watched in case that region is down */
277
- const regional = code && url !== this.url ? code : null;
278
- this.watchRegion(socket, regional);
279
- socket.on('connect', () => {
280
- if (socket !== this.socket) {
281
- return;
282
- }
283
- this.logger.debug('PartyGames', 'connected to', url);
284
- this.watchRegion(socket, null);
285
- this.syncClock();
286
- this.reattach(socket);
287
- });
288
- socket.on('disconnect', () => {
289
- if (socket !== this.socket) {
290
- return;
291
- }
292
- this.logger.debug('PartyGames', 'disconnected');
293
- this.setAttached(false);
294
- this.watchRegion(socket, regional);
295
- });
296
- socket.on('connect_error', (error) => {
297
- this.logger.warn('PartyGames', 'connect error', error?.message);
298
- });
143
+ /** Listens for the room's messages on a socket the connection has opened. */
144
+ listen(socket) {
299
145
  socket.on(protocol_1.HostMessage.Players, (payload) => {
300
146
  if (this.session && payload?.code && payload.code !== this.session.code) {
301
147
  /* a room this TV is only now being given - its own went, or moved -
@@ -303,10 +149,26 @@ class PartyGameClient {
303
149
  the roster is the room's it was in */
304
150
  return;
305
151
  }
306
- this.players = payload?.players || [];
307
- for (const listener of this.playersListeners) {
308
- listener(this.players);
152
+ this.setPlayers(payload?.players || []);
153
+ });
154
+ socket.on(protocol_1.HostMessage.PlayersChanged, (payload) => {
155
+ if (!payload?.players) {
156
+ return;
309
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;
170
+ }
171
+ this.setPlayers(withFlags(this.players, payload.players));
310
172
  });
311
173
  socket.on(protocol_1.HostMessage.AllAnswered, (payload) => {
312
174
  for (const listener of this.allAnsweredListeners) {
@@ -322,7 +184,7 @@ class PartyGameClient {
322
184
  }
323
185
  });
324
186
  socket.on(protocol_1.HostMessage.Move, (message) => {
325
- if (socket === this.socket && message?.token) {
187
+ if (socket === this.connection.current() && message?.token) {
326
188
  this.moveRoom(socket, message.token);
327
189
  }
328
190
  });
@@ -338,25 +200,35 @@ class PartyGameClient {
338
200
  /* clocks drift, and so does the route to the server */
339
201
  this.clockSyncTimer = setInterval(() => this.syncClock(), this.clockSyncIntervalMs);
340
202
  }
341
- return socket;
203
+ }
204
+ setPlayers(players) {
205
+ this.players = players;
206
+ for (const listener of this.playersListeners) {
207
+ listener(this.players);
208
+ }
342
209
  }
343
210
  /**
344
211
  * Back on the room we were already hosting, so a dropped connection
345
212
  * mid-party does not lose the players or their scores. If the room is gone
346
213
  * - its server is - the server opens a new one, and the session follows.
347
214
  */
348
- reattach(socket) {
215
+ reattach(socket, prefix) {
349
216
  const session = this.session;
350
217
  if (!session) {
351
- this.setAttached(true);
218
+ this.connection.attachedTo(socket);
352
219
  return;
353
220
  }
354
- this.send(socket, protocol_1.HostEvent.Create, {
221
+ let attached = true;
222
+ this.connection
223
+ .send(socket, protocol_1.HostEvent.Create, {
355
224
  code: this.redirectCode || session.code,
356
225
  hostId: this.hostId,
357
226
  features,
358
227
  })
359
228
  .then(response => {
229
+ if (socket !== this.connection.current()) {
230
+ return;
231
+ }
360
232
  if (response?.ok) {
361
233
  // A re-attach is not a reopen, so it says `reused: false`; the
362
234
  // lobby still is the one it was.
@@ -375,13 +247,14 @@ class PartyGameClient {
375
247
  }
376
248
  else if (response?.reason === protocol_1.HostRefusal.Moved && response.movedTo) {
377
249
  /* it left that server while we were away: follow it */
250
+ attached = false;
378
251
  this.redirectCode = response.movedTo;
379
- this.socketFor(response.movedTo);
252
+ this.connection.connectFor(response.movedTo);
380
253
  }
381
254
  else if (response?.reason === protocol_1.HostRefusal.Draining) {
382
- /* the room is gone and its server opens no new ones: another will,
383
- over a new connection the load balancer can send elsewhere */
384
- 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] });
385
258
  }
386
259
  else {
387
260
  this.logger.warn('PartyGames', 'failed to re-attach', response);
@@ -389,15 +262,15 @@ class PartyGameClient {
389
262
  })
390
263
  .catch(error => this.logger.warn('PartyGames', 'failed to re-attach', error))
391
264
  .finally(() => {
392
- if (socket === this.socket) {
393
- this.setAttached(true);
265
+ if (attached) {
266
+ this.connection.attachedTo(socket);
394
267
  }
395
268
  });
396
269
  }
397
270
  /** Takes on a session the server has handed back, rerouting by its code if need be. */
398
271
  adopt(response) {
399
272
  if (response.regionSocketUrls) {
400
- this.regionUrls = tidyUrls(response.regionSocketUrls);
273
+ this.connection.setRegionUrls(response.regionSocketUrls);
401
274
  }
402
275
  const previous = this.session;
403
276
  this.redirectCode = null;
@@ -411,6 +284,11 @@ class PartyGameClient {
411
284
  players: response.players || [],
412
285
  };
413
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
+ }
414
292
  this.roomMemory.set(response.code);
415
293
  if (previous && previous.code !== response.code) {
416
294
  this.logger.warn('PartyGames', 'room', previous.code, 'is gone, now', response.code);
@@ -418,16 +296,9 @@ class PartyGameClient {
418
296
  listener(this.session);
419
297
  }
420
298
  }
421
- if (!routesAlike(this.routedCode, response.code)) {
422
- this.socketFor(response.code);
299
+ if (!this.connection.routesTo(response.code)) {
300
+ this.connection.connectFor(response.code);
423
301
  }
424
- else {
425
- /* same server: from now on this socket stands for the room */
426
- this.routedCode = response.code;
427
- }
428
- }
429
- send(socket, event, payload = {}) {
430
- return withTimeout(new Promise(resolve => socket.emit(event, payload, resolve)), this.ackTimeoutMs, event);
431
302
  }
432
303
  /**
433
304
  * A host call: once the connection is attached to the room. A call that
@@ -435,43 +306,56 @@ class PartyGameClient {
435
306
  */
436
307
  async emit(event, payload = {}) {
437
308
  for (let attempt = 0;; ++attempt) {
438
- const socket = this.currentSocket();
439
- await this.whenAttached(event);
440
- 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);
441
316
  const reason = response?.reason;
442
317
  if (reason !== protocol_1.HostRefusal.Moving || attempt >= maxMovingRetries) {
443
318
  return response;
444
319
  }
445
- if (this.attached) {
320
+ if (this.connection.isAttached()) {
446
321
  /* refused before the server said where the room is going: the
447
322
  room:move follows, and has us reconnecting - or the move failed
448
323
  and the server lets the room go on here after a while */
449
324
  await new Promise(resolve => setTimeout(resolve, movingRetryMs));
450
325
  }
451
326
  /* the room:move that came before this answer has us reconnecting */
452
- await this.whenAttached(event);
327
+ await this.connection.whenAttached(event);
453
328
  }
454
329
  }
455
330
  /**
456
331
  * Takes the room to another server, which the one it is on asked for: it
457
- * is on its way out, and no game is being played. The room is adopted over
458
- * a connection with no room in its query - so the load balancer hands it
459
- * to a server still taking parties - the old server is told where it went,
460
- * for the phones, and the client then connects for the new code as usual.
461
- * Host calls wait meanwhile. If it cannot be done the room stays where it
462
- * 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.
463
338
  */
464
339
  async moveRoom(oldSocket, token) {
465
- this.setAttached(false);
466
- // The handover is between the servers of one region, whose tables hold
467
- // it, so it goes to the room's region rather than the nearest.
468
- 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();
469
343
  let adopted = null;
470
- for (let attempt = 0; attempt <= maxDrainingRetries && !adopted; ++attempt) {
471
- 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);
472
356
  try {
473
- await withTimeout(new Promise(resolve => socket.on('connect', () => resolve())), this.ackTimeoutMs, 'connecting to move the room');
474
- 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, {
475
359
  token,
476
360
  hostId: this.hostId,
477
361
  features,
@@ -479,28 +363,34 @@ class PartyGameClient {
479
363
  if (response?.ok) {
480
364
  adopted = response;
481
365
  }
482
- 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 {
483
370
  this.logger.warn('PartyGames', 'could not move the room', response);
484
371
  break;
485
372
  }
486
373
  }
487
374
  catch (error) {
488
375
  this.logger.warn('PartyGames', 'could not move the room', error);
376
+ avoid.push(placed.prefix);
489
377
  }
490
378
  finally {
491
379
  socket.close();
492
380
  }
493
381
  }
494
- if (oldSocket !== this.socket) {
382
+ if (oldSocket !== this.connection.current()) {
495
383
  return;
496
384
  }
497
385
  if (!adopted) {
498
- this.setAttached(true);
386
+ this.connection.setAttached(true);
499
387
  return;
500
388
  }
501
389
  this.logger.debug('PartyGames', 'room moved to', adopted.code);
502
- 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));
503
- 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()) {
504
394
  this.adopt(adopted);
505
395
  }
506
396
  }
@@ -514,8 +404,12 @@ class PartyGameClient {
514
404
  let best = null;
515
405
  for (let sample = 0; sample < clockSyncSamples; ++sample) {
516
406
  try {
407
+ const socket = this.connection.current();
408
+ if (!socket) {
409
+ break;
410
+ }
517
411
  const sentAt = Date.now();
518
- const response = await this.send(this.currentSocket(), protocol_1.TimeEvent.Ping);
412
+ const response = await this.connection.send(socket, protocol_1.TimeEvent.Ping);
519
413
  const receivedAt = Date.now();
520
414
  const rtt = receivedAt - sentAt;
521
415
  if (typeof response?.now === 'number' && (!best || rtt < best.rtt)) {
@@ -571,8 +465,8 @@ class PartyGameClient {
571
465
  };
572
466
  /* A fresh room is opened over the connection we have, so the server
573
467
  holding the old one closes it; any other ask goes where its room is. */
574
- if (!fresh) {
575
- this.socketFor(code || null);
468
+ if (!fresh && !this.connection.connectedFor(code || null)) {
469
+ this.connection.connectFor(code || null);
576
470
  }
577
471
  for (let attempt = 0;; ++attempt) {
578
472
  if (!fresh && attempt > 0) {
@@ -582,7 +476,7 @@ class PartyGameClient {
582
476
  const response = await this.emit(protocol_1.HostEvent.Create, request);
583
477
  if (response?.ok) {
584
478
  this.adopt(response);
585
- await this.whenAttached('the room');
479
+ await this.connection.whenAttached('the room');
586
480
  return this.session;
587
481
  }
588
482
  if (response?.reason === protocol_1.HostRefusal.Moved &&
@@ -590,11 +484,11 @@ class PartyGameClient {
590
484
  attempt < maxRedirects) {
591
485
  /* opened again on another server since we last knew it */
592
486
  this.redirectCode = response.movedTo;
593
- this.socketFor(response.movedTo);
487
+ this.connection.connectFor(response.movedTo);
594
488
  continue;
595
489
  }
596
490
  if (response?.reason === protocol_1.HostRefusal.Draining &&
597
- attempt < maxDrainingRetries) {
491
+ attempt < connection_1.maxDrainingRetries) {
598
492
  /* asked a server on its way out: any other server will do */
599
493
  this.logger.debug('PartyGames', 'server draining, asking another');
600
494
  if (fresh) {
@@ -603,7 +497,9 @@ class PartyGameClient {
603
497
  }
604
498
  // A new connection even if this one had no room either: over the
605
499
  // same socket the same draining server would answer again.
606
- this.socketFor(null, { fresh: true });
500
+ this.connection.connectFor(null, {
501
+ avoid: this.connection.avoidingRouted(),
502
+ });
607
503
  continue;
608
504
  }
609
505
  throw new Error(response?.error || 'failed to open a party game room');
@@ -675,6 +571,14 @@ class PartyGameClient {
675
571
  if (!response?.ok) {
676
572
  throw new Error(response?.error || 'failed to open the round');
677
573
  }
574
+ // Nobody has answered the new round, nor been scored on it: the server
575
+ // leaves this to the TV rather than send the whole roster again.
576
+ this.setPlayers(this.players.map(player => ({
577
+ ...player,
578
+ answered: false,
579
+ correct: null,
580
+ gained: 0,
581
+ })));
678
582
  }
679
583
  /** When the server has said a round starts: at once if it already has. */
680
584
  waitForRoundGo(round) {
@@ -697,8 +601,20 @@ class PartyGameClient {
697
601
  if (!response?.ok) {
698
602
  return null;
699
603
  }
700
- this.players = response.players || [];
701
- return response;
604
+ // Each player only as the round changed them: the rest - name,
605
+ // connection, flags - from the roster this TV already has.
606
+ const known = new Map(this.players.map(player => [player.id, player]));
607
+ this.players = (response.players || [])
608
+ .map(standing => ({
609
+ name: '',
610
+ connected: false,
611
+ ready: false,
612
+ answered: false,
613
+ ...known.get(standing.id),
614
+ ...standing,
615
+ }))
616
+ .sort((a, b) => a.rank - b.rank);
617
+ return { ...response, players: this.players };
702
618
  }
703
619
  /**
704
620
  * An ad is playing, between rounds: the phones say so until the next round
@@ -758,7 +674,9 @@ class PartyGameClient {
758
674
  this.lastHost) {
759
675
  const { gameType, genre, genreName } = this.lastHost;
760
676
  this.session = null;
761
- this.socketFor(null, { fresh: true });
677
+ this.connection.connectFor(null, {
678
+ avoid: this.connection.avoidingRouted(),
679
+ });
762
680
  return (await this.host(gameType, genre, genreName, true)).players;
763
681
  }
764
682
  if (!response?.ok) {
@@ -810,21 +728,11 @@ class PartyGameClient {
810
728
  clearInterval(this.clockSyncTimer);
811
729
  this.clockSyncTimer = null;
812
730
  }
813
- if (this.regionTimer) {
814
- clearTimeout(this.regionTimer);
815
- this.regionTimer = null;
816
- }
817
- this.closeRegionProbe();
818
731
  this.session = null;
819
732
  this.redirectCode = null;
820
733
  this.players = [];
821
- if (this.socket) {
822
- const socket = this.socket;
823
- this.socket = null;
824
- this.routedCode = null;
825
- socket.close();
826
- }
827
- this.setAttached(true);
734
+ this.earlyFlags = null;
735
+ this.connection.close();
828
736
  }
829
737
  }
830
738
  exports.PartyGameClient = PartyGameClient;