@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.
- package/README.md +18 -3
- package/dist/client.d.ts +32 -67
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +180 -272
- package/dist/client.js.map +1 -1
- package/dist/connection.d.ts +159 -0
- package/dist/connection.d.ts.map +1 -0
- package/dist/connection.js +389 -0
- package/dist/connection.js.map +1 -0
- package/dist/protocol/codes.d.ts +33 -0
- package/dist/protocol/codes.d.ts.map +1 -0
- package/dist/protocol/codes.js +60 -0
- package/dist/protocol/codes.js.map +1 -0
- package/dist/protocol/events.d.ts +129 -0
- package/dist/protocol/events.d.ts.map +1 -0
- package/dist/protocol/events.js +131 -0
- package/dist/protocol/events.js.map +1 -0
- package/dist/protocol/index.d.ts +25 -0
- package/dist/protocol/index.d.ts.map +1 -0
- package/dist/protocol/index.js +41 -0
- package/dist/protocol/index.js.map +1 -0
- package/dist/protocol/limits.d.ts +40 -0
- package/dist/protocol/limits.d.ts.map +1 -0
- package/dist/protocol/limits.js +43 -0
- package/dist/protocol/limits.js.map +1 -0
- package/dist/protocol/messages.d.ts +308 -0
- package/dist/protocol/messages.d.ts.map +1 -0
- package/dist/protocol/messages.js +3 -0
- package/dist/protocol/messages.js.map +1 -0
- package/dist/protocol/placement.d.ts +83 -0
- package/dist/protocol/placement.d.ts.map +1 -0
- package/dist/protocol/placement.js +97 -0
- package/dist/protocol/placement.js.map +1 -0
- package/dist/protocol/routing.d.ts +26 -0
- package/dist/protocol/routing.d.ts.map +1 -0
- package/dist/protocol/routing.js +19 -0
- package/dist/protocol/routing.js.map +1 -0
- package/dist/protocol/scoring.d.ts +7 -0
- package/dist/protocol/scoring.d.ts.map +1 -0
- package/dist/protocol/scoring.js +13 -0
- package/dist/protocol/scoring.js.map +1 -0
- package/dist/protocol/speedtest.d.ts +9 -0
- package/dist/protocol/speedtest.d.ts.map +1 -0
- package/dist/protocol/speedtest.js +22 -0
- package/dist/protocol/speedtest.js.map +1 -0
- package/dist/socket.d.ts +4 -2
- package/dist/socket.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/protocol.d.ts +0 -519
- package/dist/protocol.d.ts.map +0 -1
- package/dist/protocol.js +0 -262
- 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
|
-
|
|
8
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
67
|
-
*
|
|
68
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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.
|
|
131
|
-
|
|
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.
|
|
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
|
-
/**
|
|
159
|
-
|
|
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.
|
|
307
|
-
|
|
308
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
218
|
+
this.connection.attachedTo(socket);
|
|
352
219
|
return;
|
|
353
220
|
}
|
|
354
|
-
|
|
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.
|
|
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
|
-
|
|
384
|
-
this.
|
|
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 (
|
|
393
|
-
this.
|
|
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.
|
|
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 (!
|
|
422
|
-
this.
|
|
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
|
-
|
|
439
|
-
await this.whenAttached(event);
|
|
440
|
-
const
|
|
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.
|
|
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.
|
|
458
|
-
*
|
|
459
|
-
*
|
|
460
|
-
* for the phones
|
|
461
|
-
* Host calls wait meanwhile. If it cannot be done
|
|
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
|
-
|
|
467
|
-
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
503
|
-
|
|
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(
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
701
|
-
|
|
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.
|
|
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
|
-
|
|
822
|
-
|
|
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;
|