@colyseus/sdk 0.17.0 → 0.17.2

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 (67) hide show
  1. package/build/cjs/3rd_party/discord.js +1 -1
  2. package/build/cjs/Auth.js +1 -1
  3. package/build/cjs/Client.js +1 -1
  4. package/build/cjs/Connection.js +1 -1
  5. package/build/cjs/HTTP.js +1 -1
  6. package/build/cjs/Protocol.js +1 -1
  7. package/build/cjs/Room.js +1 -1
  8. package/build/cjs/Storage.js +1 -1
  9. package/build/cjs/core/nanoevents.js +1 -1
  10. package/build/cjs/core/signal.js +1 -1
  11. package/build/cjs/core/utils.js +1 -1
  12. package/build/cjs/errors/Errors.js +1 -1
  13. package/build/cjs/index.js +1 -1
  14. package/build/cjs/legacy.js +1 -1
  15. package/build/cjs/serializer/NoneSerializer.js +1 -1
  16. package/build/cjs/serializer/SchemaSerializer.js +1 -1
  17. package/build/cjs/serializer/Serializer.js +1 -1
  18. package/build/cjs/transport/H3Transport.js +1 -1
  19. package/build/cjs/transport/WebSocketTransport.js +1 -1
  20. package/build/esm/3rd_party/discord.mjs +1 -1
  21. package/build/esm/Auth.mjs +1 -1
  22. package/build/esm/Client.mjs +1 -1
  23. package/build/esm/Connection.mjs +1 -1
  24. package/build/esm/HTTP.mjs +1 -1
  25. package/build/esm/Protocol.mjs +1 -1
  26. package/build/esm/Room.mjs +1 -1
  27. package/build/esm/Storage.mjs +1 -1
  28. package/build/esm/core/nanoevents.mjs +1 -1
  29. package/build/esm/core/signal.mjs +1 -1
  30. package/build/esm/core/utils.mjs +1 -1
  31. package/build/esm/errors/Errors.mjs +1 -1
  32. package/build/esm/index.mjs +1 -1
  33. package/build/esm/legacy.mjs +1 -1
  34. package/build/esm/serializer/NoneSerializer.mjs +1 -1
  35. package/build/esm/serializer/SchemaSerializer.mjs +1 -1
  36. package/build/esm/serializer/Serializer.mjs +1 -1
  37. package/build/esm/transport/H3Transport.mjs +1 -1
  38. package/build/esm/transport/WebSocketTransport.mjs +1 -1
  39. package/dist/colyseus-cocos-creator.js +1 -1
  40. package/dist/colyseus.js +1 -1
  41. package/dist/debug.js +1 -1
  42. package/lib/core/http_bkp.d.ts +10 -10
  43. package/package.json +29 -25
  44. package/src/3rd_party/discord.ts +48 -0
  45. package/src/Auth.ts +177 -0
  46. package/src/Client.ts +459 -0
  47. package/src/Connection.ts +51 -0
  48. package/src/HTTP.ts +545 -0
  49. package/src/HTTP_bkp.ts +67 -0
  50. package/src/Protocol.ts +25 -0
  51. package/src/Room.ts +505 -0
  52. package/src/Storage.ts +94 -0
  53. package/src/core/http_bkp.ts +358 -0
  54. package/src/core/nanoevents.ts +38 -0
  55. package/src/core/signal.ts +62 -0
  56. package/src/core/utils.ts +3 -0
  57. package/src/debug.ts +2743 -0
  58. package/src/errors/Errors.ts +29 -0
  59. package/src/index.ts +18 -0
  60. package/src/legacy.ts +29 -0
  61. package/src/serializer/FossilDeltaSerializer.ts +39 -0
  62. package/src/serializer/NoneSerializer.ts +9 -0
  63. package/src/serializer/SchemaSerializer.ts +61 -0
  64. package/src/serializer/Serializer.ts +23 -0
  65. package/src/transport/H3Transport.ts +199 -0
  66. package/src/transport/ITransport.ts +18 -0
  67. package/src/transport/WebSocketTransport.ts +53 -0
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Discord Embedded App SDK
3
+ * https://github.com/colyseus/colyseus/issues/707
4
+ *
5
+ * All URLs must go through the local proxy from
6
+ * https://<app_id>.discordsays.com/.proxy/<mapped_url>/...
7
+ *
8
+ * URL Mapping Examples:
9
+ *
10
+ * 1. Using Colyseus Cloud:
11
+ * - /colyseus/{subdomain} -> {subdomain}.colyseus.cloud
12
+ *
13
+ * Example:
14
+ * const client = new Client("https://xxxx.colyseus.cloud");
15
+ *
16
+ * -------------------------------------------------------------
17
+ *
18
+ * 2. Using `cloudflared` tunnel:
19
+ * - /colyseus/ -> <your-cloudflared-url>.trycloudflare.com
20
+ *
21
+ * Example:
22
+ * const client = new Client("https://<your-cloudflared-url>.trycloudflare.com");
23
+ *
24
+ * -------------------------------------------------------------
25
+ *
26
+ * 3. Providing a manual /.proxy/your-mapping:
27
+ * - /your-mapping/ -> your-endpoint.com
28
+ *
29
+ * Example:
30
+ * const client = new Client("/.proxy/your-mapping");
31
+ *
32
+ */
33
+ export function discordURLBuilder (url: URL): string {
34
+ const localHostname = window?.location?.hostname || "localhost";
35
+
36
+ const remoteHostnameSplitted = url.hostname.split('.');
37
+ const subdomain = (
38
+ !url.hostname.includes("trycloudflare.com") && // ignore cloudflared subdomains
39
+ !url.hostname.includes("discordsays.com") && // ignore discordsays.com subdomains
40
+ remoteHostnameSplitted.length > 2
41
+ )
42
+ ? `/${remoteHostnameSplitted[0]}`
43
+ : '';
44
+
45
+ return (url.pathname.startsWith("/.proxy"))
46
+ ? `${url.protocol}//${localHostname}${subdomain}${url.pathname}${url.search}`
47
+ : `${url.protocol}//${localHostname}/.proxy/colyseus${subdomain}${url.pathname}${url.search}`;
48
+ }
package/src/Auth.ts ADDED
@@ -0,0 +1,177 @@
1
+ import { HTTP } from "./HTTP.ts";
2
+ import { getItem, removeItem, setItem } from "./Storage.ts";
3
+ import { createNanoEvents } from './core/nanoevents.ts';
4
+
5
+ export interface AuthSettings {
6
+ path: string;
7
+ key: string;
8
+ }
9
+
10
+ export interface PopupSettings {
11
+ prefix: string;
12
+ width: number;
13
+ height: number;
14
+ }
15
+
16
+ export interface AuthData {
17
+ user: any;
18
+ token: string;
19
+ }
20
+
21
+ export class Auth {
22
+ settings: AuthSettings = {
23
+ path: "/auth",
24
+ key: "colyseus-auth-token",
25
+ };
26
+
27
+ #_initialized = false;
28
+ #_signInWindow: WindowProxy | null = null;
29
+ #_events = createNanoEvents();
30
+
31
+ protected http: HTTP<any>;
32
+
33
+ constructor(http: HTTP<any>) {
34
+ this.http = http;
35
+ getItem(this.settings.key, (token: string) => this.token = token);
36
+ }
37
+
38
+ public set token(token: string) {
39
+ this.http.authToken = token;
40
+ }
41
+
42
+ public get token(): string | undefined {
43
+ return this.http.authToken;
44
+ }
45
+
46
+ public onChange(callback: (response: AuthData) => void) {
47
+ const unbindChange = this.#_events.on("change", callback);
48
+ if (!this.#_initialized) {
49
+ this.getUserData().then((userData: any) => {
50
+ this.emitChange({ ...userData, token: this.token });
51
+
52
+ }).catch((e) => {
53
+ // user is not logged in, or service is down
54
+ this.emitChange({ user: null, token: undefined });
55
+ });
56
+ }
57
+ this.#_initialized = true;
58
+ return unbindChange;
59
+ }
60
+
61
+ public async getUserData() {
62
+ if (this.token) {
63
+ return (await this.http.get(`${this.settings.path}/userdata`)).data;
64
+ } else {
65
+ throw new Error("missing auth.token");
66
+ }
67
+ }
68
+
69
+ public async registerWithEmailAndPassword(email: string, password: string, options?: any) {
70
+ const data = (await this.http.post(`${this.settings.path}/register`, {
71
+ body: { email, password, options, },
72
+ })).data;
73
+
74
+ this.emitChange(data as any);
75
+
76
+ return data;
77
+ }
78
+
79
+ public async signInWithEmailAndPassword(email: string, password: string) {
80
+ const data = (await this.http.post(`${this.settings.path}/login`, {
81
+ body: { email, password, },
82
+ })).data;
83
+
84
+ this.emitChange(data as any);
85
+
86
+ return data;
87
+ }
88
+
89
+ public async signInAnonymously(options?: any) {
90
+ const data = (await this.http.post(`${this.settings.path}/anonymous`, {
91
+ body: { options, }
92
+ })).data;
93
+
94
+ this.emitChange(data as any);
95
+
96
+ return data;
97
+ }
98
+
99
+ public async sendPasswordResetEmail(email: string) {
100
+ return (await this.http.post(`${this.settings.path}/forgot-password`, {
101
+ body: { email, }
102
+ })).data;
103
+ }
104
+
105
+ public async signInWithProvider(providerName: string, settings: Partial<PopupSettings> = {}) {
106
+ return new Promise((resolve, reject) => {
107
+ const w = settings.width || 480;
108
+ const h = settings.height || 768;
109
+
110
+ // forward existing token for upgrading
111
+ const upgradingToken = this.token ? `?token=${this.token}` : "";
112
+
113
+ // Capitalize first letter of providerName
114
+ const title = `Login with ${(providerName[0].toUpperCase() + providerName.substring(1))}`;
115
+ const url = this.http['sdk']['getHttpEndpoint'](`${(settings.prefix || `${this.settings.path}/provider`)}/${providerName}${upgradingToken}`);
116
+
117
+ const left = (screen.width / 2) - (w / 2);
118
+ const top = (screen.height / 2) - (h / 2);
119
+
120
+ this.#_signInWindow = window.open(url, title, 'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=no, copyhistory=no, width=' + w + ', height=' + h + ', top=' + top + ', left=' + left);
121
+
122
+ const onMessage = (event: MessageEvent) => {
123
+ // TODO: it is a good idea to check if event.origin can be trusted!
124
+ // if (event.origin.indexOf(window.location.hostname) === -1) { return; }
125
+
126
+ // require 'user' and 'token' inside received data.
127
+ if (event.data.user === undefined && event.data.token === undefined) { return; }
128
+
129
+ clearInterval(rejectionChecker);
130
+ this.#_signInWindow?.close();
131
+ this.#_signInWindow = null;
132
+
133
+ window.removeEventListener("message", onMessage);
134
+
135
+ if (event.data.error !== undefined) {
136
+ reject(event.data.error);
137
+
138
+ } else {
139
+ resolve(event.data);
140
+ this.emitChange(event.data);
141
+ }
142
+ }
143
+
144
+ const rejectionChecker = setInterval(() => {
145
+ if (!this.#_signInWindow || this.#_signInWindow.closed) {
146
+ this.#_signInWindow = null;
147
+ reject("cancelled");
148
+ window.removeEventListener("message", onMessage);
149
+ }
150
+ }, 200);
151
+
152
+ window.addEventListener("message", onMessage);
153
+ });
154
+ }
155
+
156
+ public async signOut() {
157
+ // @ts-ignore
158
+ this.emitChange({ user: null, token: null });
159
+ }
160
+
161
+ private emitChange(authData: Partial<AuthData>) {
162
+ if (authData.token !== undefined) {
163
+ this.token = authData.token;
164
+
165
+ if (authData.token === null) {
166
+ removeItem(this.settings.key);
167
+
168
+ } else {
169
+ // store key in localStorage
170
+ setItem(this.settings.key, authData.token);
171
+ }
172
+ }
173
+
174
+ this.#_events.emit("change", authData);
175
+ }
176
+
177
+ }
package/src/Client.ts ADDED
@@ -0,0 +1,459 @@
1
+ import type { matchMaker, SDKTypes, Room as ServerRoom } from '@colyseus/core';
2
+
3
+ import { CloseCode, ServerError } from './errors/Errors.ts';
4
+ import { Room } from './Room.ts';
5
+ import { SchemaConstructor } from './serializer/SchemaSerializer.ts';
6
+ import { HTTP } from './HTTP.ts';
7
+ import { Auth } from './Auth.ts';
8
+ import { Protocol } from './Protocol.ts';
9
+ import { Connection } from './Connection.ts';
10
+ import { discordURLBuilder } from './3rd_party/discord.ts';
11
+
12
+ export type JoinOptions = any;
13
+ export type ISeatReservation = matchMaker.ISeatReservation;
14
+
15
+ export class MatchMakeError extends Error {
16
+ code: number;
17
+ constructor(message: string, code: number) {
18
+ super(message);
19
+ this.code = code;
20
+ this.name = "MatchMakeError";
21
+ Object.setPrototypeOf(this, MatchMakeError.prototype);
22
+ }
23
+ }
24
+
25
+ // - React Native does not provide `window.location`
26
+ // - Cocos Creator (Native) does not provide `window.location.hostname`
27
+ const DEFAULT_ENDPOINT = (typeof (window) !== "undefined" && typeof (window?.location?.hostname) !== "undefined")
28
+ ? `${window.location.protocol.replace("http", "ws")}//${window.location.hostname}${(window.location.port && `:${window.location.port}`)}`
29
+ : "ws://127.0.0.1:2567";
30
+
31
+ export interface EndpointSettings {
32
+ hostname: string,
33
+ secure: boolean,
34
+ port?: number,
35
+ pathname?: string,
36
+ searchParams?: string,
37
+ protocol?: "ws" | "h3";
38
+ }
39
+
40
+ export interface ClientOptions {
41
+ headers?: { [id: string]: string };
42
+ urlBuilder?: (url: URL) => string;
43
+ protocol?: "ws" | "h3";
44
+ }
45
+
46
+ export interface LatencyOptions {
47
+ /** "ws" for WebSocket, "h3" for WebTransport (default: "ws") */
48
+ protocol?: "ws" | "h3";
49
+ /** Number of pings to send (default: 1). Returns the average latency when > 1. */
50
+ pingCount?: number;
51
+ }
52
+
53
+ export class ColyseusSDK<ServerType extends SDKTypes = any> {
54
+ static VERSION = "0.17";
55
+
56
+ /**
57
+ * The HTTP client to make requests to the server.
58
+ */
59
+ public http: HTTP<ServerType['~routes']>;
60
+
61
+ /**
62
+ * The authentication module to authenticate into requests and rooms.
63
+ */
64
+ public auth: Auth;
65
+
66
+ /**
67
+ * The settings used to connect to the server.
68
+ */
69
+ public settings: EndpointSettings;
70
+
71
+ protected urlBuilder: (url: URL) => string;
72
+
73
+ constructor(
74
+ settings: string | EndpointSettings = DEFAULT_ENDPOINT,
75
+ options?: ClientOptions,
76
+ ) {
77
+ if (typeof (settings) === "string") {
78
+
79
+ //
80
+ // endpoint by url
81
+ //
82
+ const url = (settings.startsWith("/"))
83
+ ? new URL(settings, DEFAULT_ENDPOINT)
84
+ : new URL(settings);
85
+
86
+ const secure = (url.protocol === "https:" || url.protocol === "wss:");
87
+ const port = Number(url.port || (secure ? 443 : 80));
88
+
89
+ this.settings = {
90
+ hostname: url.hostname,
91
+ pathname: url.pathname,
92
+ port,
93
+ secure,
94
+ searchParams: url.searchParams.toString() || undefined,
95
+ };
96
+
97
+ } else {
98
+ //
99
+ // endpoint by settings
100
+ //
101
+ if (settings.port === undefined) {
102
+ settings.port = (settings.secure) ? 443 : 80;
103
+ }
104
+ if (settings.pathname === undefined) {
105
+ settings.pathname = "";
106
+ }
107
+ this.settings = settings;
108
+ }
109
+
110
+ // make sure pathname does not end with "/"
111
+ if (this.settings.pathname.endsWith("/")) {
112
+ this.settings.pathname = this.settings.pathname.slice(0, -1);
113
+ }
114
+
115
+ // specify room connection protocol if provided
116
+ if (options?.protocol) {
117
+ this.settings.protocol = options.protocol;
118
+ }
119
+
120
+ this.http = new HTTP(this, {
121
+ headers: options?.headers || {},
122
+ });
123
+ this.auth = new Auth(this.http);
124
+
125
+ this.urlBuilder = options?.urlBuilder;
126
+
127
+ //
128
+ // Discord Embedded SDK requires a custom URL builder
129
+ //
130
+ if (
131
+ !this.urlBuilder &&
132
+ typeof (window) !== "undefined" &&
133
+ window?.location?.hostname?.includes("discordsays.com")
134
+ ) {
135
+ this.urlBuilder = discordURLBuilder;
136
+ console.log("Colyseus SDK: Discord Embedded SDK detected. Using custom URL builder.");
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Select the endpoint with the lowest latency.
142
+ * @param endpoints Array of endpoints to select from.
143
+ * @param options Client options.
144
+ * @param latencyOptions Latency measurement options (protocol, pingCount).
145
+ * @returns The client with the lowest latency.
146
+ */
147
+ static async selectByLatency<ServerType extends SDKTypes = any>(
148
+ endpoints: Array<string | EndpointSettings>,
149
+ options?: ClientOptions,
150
+ latencyOptions: LatencyOptions = {}
151
+ ) {
152
+ const clients = endpoints.map(endpoint => new ColyseusSDK<ServerType>(endpoint, options));
153
+
154
+ const latencies = (await Promise.allSettled(clients.map((client, index) => client.getLatency(latencyOptions).then(latency => {
155
+ const settings = clients[index].settings;
156
+ console.log(`🛜 Endpoint Latency: ${latency}ms - ${settings.hostname}:${settings.port}${settings.pathname}`);
157
+ return [index, latency]
158
+ }))))
159
+ .filter((result) => result.status === 'fulfilled')
160
+ .map(result => result.value);
161
+
162
+ if (latencies.length === 0) {
163
+ throw new Error('All endpoints failed to respond');
164
+ }
165
+
166
+ return clients[latencies.sort((a, b) => a[1] - b[1])[0][0]];
167
+ }
168
+
169
+ // Overload: Use room name from ServerType to infer room type
170
+ public async joinOrCreate<R extends keyof ServerType['~rooms']>(
171
+ roomName: R,
172
+ options?: Parameters<ServerType['~rooms'][R]['~room']['prototype']['onJoin']>[1],
173
+ rootSchema?: SchemaConstructor<ServerType>
174
+ ): Promise<Room<ServerType['~rooms'][R]['~room']>>
175
+ // Overload: Pass RoomType directly to extract state
176
+ public async joinOrCreate<RoomType extends typeof ServerRoom>(
177
+ roomName: string,
178
+ options?: Parameters<RoomType['prototype']['onJoin']>[1],
179
+ rootSchema?: SchemaConstructor<RoomType['prototype']['state']>
180
+ ): Promise<Room<RoomType>>
181
+ // Overload: Pass State type directly
182
+ public async joinOrCreate<State = any>(
183
+ roomName: string,
184
+ options?: JoinOptions,
185
+ rootSchema?: SchemaConstructor<State>
186
+ ): Promise<Room<any, State>>
187
+ // Implementation
188
+ public async joinOrCreate<T = any>(roomName: string, options: JoinOptions = {}, rootSchema?: SchemaConstructor<T>) {
189
+ return await this.createMatchMakeRequest<T>('joinOrCreate', roomName, options, rootSchema);
190
+ }
191
+
192
+ // Overload: Use room name from ServerType to infer room type
193
+ public async create<R extends keyof ServerType['~rooms']>(
194
+ roomName: R,
195
+ options?: Parameters<ServerType['~rooms'][R]['~room']['prototype']['onJoin']>[1],
196
+ rootSchema?: SchemaConstructor<ServerType>
197
+ ): Promise<Room<ServerType['~rooms'][R]['~room']>>
198
+ // Overload: Pass RoomType directly to extract state
199
+ public async create<RoomType extends typeof ServerRoom>(
200
+ roomName: string,
201
+ options?: Parameters<RoomType['prototype']['onJoin']>[1],
202
+ rootSchema?: SchemaConstructor<RoomType['prototype']['state']>
203
+ ): Promise<Room<RoomType>>
204
+ // Overload: Pass State type directly
205
+ public async create<State = any>(
206
+ roomName: string,
207
+ options?: JoinOptions,
208
+ rootSchema?: SchemaConstructor<State>
209
+ ): Promise<Room<any, State>>
210
+ // Implementation
211
+ public async create<T = any>(roomName: string, options: JoinOptions = {}, rootSchema?: SchemaConstructor<T>) {
212
+ return await this.createMatchMakeRequest<T>('create', roomName, options, rootSchema);
213
+ }
214
+
215
+ // Overload: Use room name from ServerType to infer room type
216
+ public async join<R extends keyof ServerType['~rooms']>(
217
+ roomName: R,
218
+ options?: Parameters<ServerType['~rooms'][R]['~room']['prototype']['onJoin']>[1],
219
+ rootSchema?: SchemaConstructor<ServerType>
220
+ ): Promise<Room<ServerType['~rooms'][R]['~room']>>
221
+ // Overload: Pass RoomType directly to extract state
222
+ public async join<RoomType extends typeof ServerRoom>(
223
+ roomName: string,
224
+ options?: Parameters<RoomType['prototype']['onJoin']>[1],
225
+ rootSchema?: SchemaConstructor<RoomType['prototype']['state']>
226
+ ): Promise<Room<RoomType>>
227
+ // Overload: Pass State type directly
228
+ public async join<State = any>(
229
+ roomName: string,
230
+ options?: JoinOptions,
231
+ rootSchema?: SchemaConstructor<State>
232
+ ): Promise<Room<any, State>>
233
+ // Implementation
234
+ public async join<T = any>(roomName: string, options: JoinOptions = {}, rootSchema?: SchemaConstructor<T>) {
235
+ return await this.createMatchMakeRequest<T>('join', roomName, options, rootSchema);
236
+ }
237
+
238
+ // Overload: Use room name from ServerType to infer room type
239
+ public async joinById<R extends keyof ServerType['~rooms']>(
240
+ roomName: R,
241
+ options?: Parameters<ServerType['~rooms'][R]['~room']['prototype']['onJoin']>[1],
242
+ rootSchema?: SchemaConstructor<ServerType>
243
+ ): Promise<Room<ServerType['~rooms'][R]['~room']>>
244
+ // Overload: Pass RoomType directly to extract state
245
+ public async joinById<RoomType extends typeof ServerRoom>(
246
+ roomId: string,
247
+ options?: Parameters<RoomType['prototype']['onJoin']>[1],
248
+ rootSchema?: SchemaConstructor<RoomType['prototype']['state']>
249
+ ): Promise<Room<RoomType>>
250
+ // Overload: Pass State type directly
251
+ public async joinById<State = any>(
252
+ roomId: string,
253
+ options?: JoinOptions,
254
+ rootSchema?: SchemaConstructor<State>
255
+ ): Promise<Room<any, State>>
256
+ // Implementation
257
+ public async joinById<T = any>(roomId: string, options: JoinOptions = {}, rootSchema?: SchemaConstructor<T>) {
258
+ return await this.createMatchMakeRequest<T>('joinById', roomId, options, rootSchema);
259
+ }
260
+
261
+ /**
262
+ * Re-establish connection with a room this client was previously connected to.
263
+ *
264
+ * @param reconnectionToken The `room.reconnectionToken` from previously connected room.
265
+ * @param rootSchema (optional) Concrete root schema definition
266
+ * @returns Promise<Room>
267
+ */
268
+ // Overload: Use room name from ServerType to infer room type
269
+ public async reconnect<R extends keyof ServerType['~rooms']>(reconnectionToken: string, roomName?: R): Promise<Room<ServerType['~rooms'][R]['~room']>>
270
+ // Overload: Pass RoomType directly to extract state
271
+ public async reconnect<RoomType extends typeof ServerRoom>(
272
+ reconnectionToken: string,
273
+ rootSchema?: SchemaConstructor<RoomType['prototype']['state']>
274
+ ): Promise<Room<RoomType>>
275
+ // Overload: Pass State type directly
276
+ public async reconnect<State = any>(
277
+ reconnectionToken: string,
278
+ rootSchema?: SchemaConstructor<State>
279
+ ): Promise<Room<any, State>>
280
+ // Implementation
281
+ public async reconnect<T = any>(reconnectionToken: string, rootSchema?: SchemaConstructor<T>) {
282
+ if (typeof (reconnectionToken) === "string" && typeof (rootSchema) === "string") {
283
+ throw new Error("DEPRECATED: .reconnect() now only accepts 'reconnectionToken' as argument.\nYou can get this token from previously connected `room.reconnectionToken`");
284
+ }
285
+ const [roomId, token] = reconnectionToken.split(":");
286
+ if (!roomId || !token) {
287
+ throw new Error("Invalid reconnection token format.\nThe format should be roomId:reconnectionToken");
288
+ }
289
+ return await this.createMatchMakeRequest<T>('reconnect', roomId, { reconnectionToken: token }, rootSchema);
290
+ }
291
+
292
+ public async consumeSeatReservation<T>(
293
+ response: ISeatReservation,
294
+ rootSchema?: SchemaConstructor<T>
295
+ ): Promise<Room<any, T>> {
296
+ const room = this.createRoom<T>(response.name, rootSchema);
297
+ room.roomId = response.roomId;
298
+ room.sessionId = response.sessionId;
299
+
300
+ const options: any = { sessionId: room.sessionId };
301
+
302
+ // forward "reconnection token" in case of reconnection.
303
+ if (response.reconnectionToken) {
304
+ options.reconnectionToken = response.reconnectionToken;
305
+ }
306
+
307
+ room.connect(
308
+ this.buildEndpoint(response, options),
309
+ response,
310
+ this.http.options.headers
311
+ );
312
+
313
+ return new Promise((resolve, reject) => {
314
+ const onError = (code, message) => reject(new ServerError(code, message));
315
+ room.onError.once(onError);
316
+
317
+ room['onJoin'].once(() => {
318
+ room.onError.remove(onError);
319
+ resolve(room);
320
+ });
321
+ });
322
+ }
323
+
324
+ /**
325
+ * Create a new connection with the server, and measure the latency.
326
+ * @param options Latency measurement options (protocol, pingCount).
327
+ */
328
+ public getLatency(options: LatencyOptions = {}): Promise<number> {
329
+ const protocol = options.protocol ?? "ws";
330
+ const pingCount = options.pingCount ?? 1;
331
+
332
+ return new Promise<number>((resolve, reject) => {
333
+ const conn = new Connection(protocol);
334
+ const latencies: number[] = [];
335
+ let pingStart = 0;
336
+
337
+ conn.events.onopen = () => {
338
+ pingStart = Date.now();
339
+ conn.send(new Uint8Array([Protocol.PING]));
340
+ };
341
+
342
+ conn.events.onmessage = (_: MessageEvent) => {
343
+ latencies.push(Date.now() - pingStart);
344
+
345
+ if (latencies.length < pingCount) {
346
+ // Send another ping
347
+ pingStart = Date.now();
348
+ conn.send(new Uint8Array([Protocol.PING]));
349
+ } else {
350
+ // Done, calculate average and close
351
+ conn.close();
352
+ const average = latencies.reduce((sum, l) => sum + l, 0) / latencies.length;
353
+ resolve(average);
354
+ }
355
+ };
356
+
357
+ conn.events.onerror = (event: ErrorEvent) => {
358
+ reject(new ServerError(CloseCode.ABNORMAL_CLOSURE, `Failed to get latency: ${event.message}`));
359
+ };
360
+
361
+ conn.connect(this.getHttpEndpoint());
362
+ });
363
+ }
364
+
365
+ protected async createMatchMakeRequest<T>(
366
+ method: string,
367
+ roomName: string,
368
+ options: JoinOptions = {},
369
+ rootSchema?: SchemaConstructor<T>,
370
+ ) {
371
+ const httpResponse = await (this.http as HTTP<any>).post(`/matchmake/${method}/${roomName}`, {
372
+ headers: {
373
+ 'Accept': 'application/json',
374
+ 'Content-Type': 'application/json'
375
+ },
376
+ body: options
377
+ });
378
+
379
+ // Handle HTTP error responses
380
+ if (!httpResponse.ok) {
381
+ // @ts-ignore
382
+ throw new MatchMakeError(httpResponse.error.message || httpResponse.error, httpResponse.error.code || httpResponse.status);
383
+ }
384
+
385
+ const response = httpResponse.data as unknown as ISeatReservation;
386
+
387
+ // forward reconnection token during "reconnect" methods.
388
+ if (method === "reconnect") {
389
+ response.reconnectionToken = options.reconnectionToken;
390
+ }
391
+
392
+ return await this.consumeSeatReservation<T>(response, rootSchema);
393
+ }
394
+
395
+ protected createRoom<T>(roomName: string, rootSchema?: SchemaConstructor<T>) {
396
+ return new Room<any, T>(roomName, rootSchema);
397
+ }
398
+
399
+ protected buildEndpoint(seatReservation: ISeatReservation, options: any = {}) {
400
+ let protocol: string = this.settings.protocol || "ws";
401
+ let searchParams = this.settings.searchParams || "";
402
+
403
+ // forward authentication token
404
+ if (this.http.authToken) {
405
+ options['_authToken'] = this.http.authToken;
406
+ }
407
+
408
+ // append provided options
409
+ for (const name in options) {
410
+ if (!options.hasOwnProperty(name)) {
411
+ continue;
412
+ }
413
+ searchParams += (searchParams ? '&' : '') + `${name}=${options[name]}`;
414
+ }
415
+
416
+ if (protocol === "h3") {
417
+ protocol = "http";
418
+ }
419
+
420
+ let endpoint = (this.settings.secure)
421
+ ? `${protocol}s://`
422
+ : `${protocol}://`;
423
+
424
+ if (seatReservation.publicAddress) {
425
+ endpoint += `${seatReservation.publicAddress}`;
426
+
427
+ } else {
428
+ endpoint += `${this.settings.hostname}${this.getEndpointPort()}${this.settings.pathname}`;
429
+ }
430
+
431
+ const endpointURL = `${endpoint}/${seatReservation.processId}/${seatReservation.roomId}?${searchParams}`;
432
+ return (this.urlBuilder)
433
+ ? this.urlBuilder(new URL(endpointURL))
434
+ : endpointURL;
435
+ }
436
+
437
+ protected getHttpEndpoint(segments: string = '') {
438
+ const path = segments.startsWith("/") ? segments : `/${segments}`;
439
+
440
+ let endpointURL = `${(this.settings.secure) ? "https" : "http"}://${this.settings.hostname}${this.getEndpointPort()}${this.settings.pathname}${path}`;
441
+
442
+ if (this.settings.searchParams) {
443
+ endpointURL += `?${this.settings.searchParams}`;
444
+ }
445
+
446
+ return (this.urlBuilder)
447
+ ? this.urlBuilder(new URL(endpointURL))
448
+ : endpointURL;
449
+ }
450
+
451
+ protected getEndpointPort() {
452
+ return (this.settings.port !== 80 && this.settings.port !== 443)
453
+ ? `:${this.settings.port}`
454
+ : "";
455
+ }
456
+ }
457
+
458
+ export const Client = ColyseusSDK;
459
+ export type Client = InstanceType<typeof ColyseusSDK>;