-
Notifications
You must be signed in to change notification settings - Fork 107
/
Client.ts
226 lines (186 loc) · 8.31 KB
/
Client.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
import { post, get } from "httpie";
import { ServerError } from './errors/ServerError';
import { Room, RoomAvailable } from './Room';
import { SchemaConstructor } from './serializer/SchemaSerializer';
export type JoinOptions = any;
export class MatchMakeError extends Error {
code: number;
constructor(message: string, code: number) {
super(message);
this.code = code;
Object.setPrototypeOf(this, MatchMakeError.prototype);
}
}
// - React Native does not provide `window.location`
// - Cocos Creator (Native) does not provide `window.location.hostname`
const DEFAULT_ENDPOINT = (typeof (window) !== "undefined" && typeof (window?.location?.hostname) !== "undefined")
? `${window.location.protocol.replace("http", "ws")}//${window.location.hostname}${(window.location.port && `:${window.location.port}`)}`
: "ws://127.0.0.1:2567";
export interface EndpointSettings {
hostname: string,
secure: boolean,
port?: number,
pathname?: string,
}
export class Client {
protected settings: EndpointSettings;
constructor(settings: string | EndpointSettings = DEFAULT_ENDPOINT) {
if (typeof (settings) === "string") {
//
// endpoint by url
//
const url = new URL(settings);
const secure = (url.protocol === "https:" || url.protocol === "wss:");
const port = Number(url.port || (secure ? 443 : 80));
this.settings = {
hostname: url.hostname,
pathname: url.pathname !== "/" ? url.pathname : "",
port,
secure
};
} else {
//
// endpoint by settings
//
if (settings.port === undefined) {
settings.port = (settings.secure) ? 443 : 80;
}
if (settings.pathname === undefined) {
settings.pathname = "";
}
this.settings = settings;
}
}
public async joinOrCreate<T>(roomName: string, options: JoinOptions = {}, rootSchema?: SchemaConstructor<T>) {
return await this.createMatchMakeRequest<T>('joinOrCreate', roomName, options, rootSchema);
}
public async create<T>(roomName: string, options: JoinOptions = {}, rootSchema?: SchemaConstructor<T>) {
return await this.createMatchMakeRequest<T>('create', roomName, options, rootSchema);
}
public async join<T>(roomName: string, options: JoinOptions = {}, rootSchema?: SchemaConstructor<T>) {
return await this.createMatchMakeRequest<T>('join', roomName, options, rootSchema);
}
public async joinById<T>(roomId: string, options: JoinOptions = {}, rootSchema?: SchemaConstructor<T>) {
return await this.createMatchMakeRequest<T>('joinById', roomId, options, rootSchema);
}
/**
* Re-establish connection with a room this client was previously connected to.
*
* @param reconnectionToken The `room.reconnectionToken` from previously connected room.
* @param rootSchema (optional) Concrete root schema definition
* @returns Promise<Room>
*/
public async reconnect<T>(reconnectionToken: string, rootSchema?: SchemaConstructor<T>) {
if (typeof (reconnectionToken) === "string" && typeof (rootSchema) === "string") {
throw new Error("DEPRECATED: .reconnect() now only accepts 'reconnectionToken' as argument.\nYou can get this token from previously connected `room.reconnectionToken`");
}
const [roomId, token] = reconnectionToken.split(":");
return await this.createMatchMakeRequest<T>('reconnect', roomId, { reconnectionToken: token }, rootSchema);
}
public async getAvailableRooms<Metadata = any>(roomName: string = ""): Promise<RoomAvailable<Metadata>[]> {
return (
await get(this.getHttpEndpoint(`${roomName}`), {
headers: {
'Accept': 'application/json'
}
})
).data;
}
public async consumeSeatReservation<T>(
response: any,
rootSchema?: SchemaConstructor<T>,
reuseRoomInstance?: Room // used in devMode
): Promise<Room<T>> {
const room = this.createRoom<T>(response.room.name, rootSchema);
room.roomId = response.room.roomId;
room.sessionId = response.sessionId;
const options: any = { sessionId: room.sessionId };
// forward "reconnection token" in case of reconnection.
if (response.reconnectionToken) {
options.reconnectionToken = response.reconnectionToken;
}
const targetRoom = reuseRoomInstance || room;
room.connect(this.buildEndpoint(response.room, options), response.devMode && (async () => {
console.info(`[Colyseus devMode]: ${String.fromCodePoint(0x1F504)} Re-establishing connection with room id '${room.roomId}'...`); // 🔄
let retryCount = 0;
let retryMaxRetries = 8;
const retryReconnection = async () => {
retryCount++;
try {
await this.consumeSeatReservation(response, rootSchema, targetRoom);
console.info(`[Colyseus devMode]: ${String.fromCodePoint(0x2705)} Successfully re-established connection with room '${room.roomId}'`); // ✅
} catch (e) {
if (retryCount < retryMaxRetries) {
console.info(`[Colyseus devMode]: ${String.fromCodePoint(0x1F504)} retrying... (${retryCount} out of ${retryMaxRetries})`); // 🔄
setTimeout(retryReconnection, 2000);
} else {
console.info(`[Colyseus devMode]: ${String.fromCodePoint(0x274C)} Failed to reconnect. Is your server running? Please check server logs.`); // ❌
}
}
};
setTimeout(retryReconnection, 2000);
}), targetRoom);
return new Promise((resolve, reject) => {
const onError = (code, message) => reject(new ServerError(code, message));
targetRoom.onError.once(onError);
targetRoom['onJoin'].once(() => {
targetRoom.onError.remove(onError);
resolve(targetRoom);
});
});
}
protected async createMatchMakeRequest<T>(
method: string,
roomName: string,
options: JoinOptions = {},
rootSchema?: SchemaConstructor<T>,
reuseRoomInstance?: Room,
) {
const response = (
await post(this.getHttpEndpoint(`${method}/${roomName}`), {
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(options)
})
).data;
if (response.error) {
throw new MatchMakeError(response.error, response.code);
}
// forward reconnection token during "reconnect" methods.
if (method === "reconnect") {
response.reconnectionToken = options.reconnectionToken;
}
return await this.consumeSeatReservation<T>(response, rootSchema, reuseRoomInstance);
}
protected createRoom<T>(roomName: string, rootSchema?: SchemaConstructor<T>) {
return new Room<T>(roomName, rootSchema);
}
protected buildEndpoint(room: any, options: any = {}) {
const params = [];
for (const name in options) {
if (!options.hasOwnProperty(name)) {
continue;
}
params.push(`${name}=${options[name]}`);
}
let endpoint = (this.settings.secure)
? "wss://"
: "ws://"
if (room.publicAddress) {
endpoint += `${room.publicAddress}`;
} else {
endpoint += `${this.settings.hostname}${this.getEndpointPort()}${this.settings.pathname}`;
}
return `${endpoint}/${room.processId}/${room.roomId}?${params.join('&')}`;
}
protected getHttpEndpoint(segments: string = '') {
return `${(this.settings.secure) ? "https" : "http"}://${this.settings.hostname}${this.getEndpointPort()}${this.settings.pathname}/matchmake/${segments}`;
}
protected getEndpointPort() {
return (this.settings.port !== 80 && this.settings.port !== 443)
? `:${this.settings.port}`
: "";
}
}