@colyseus/sdk 0.17.1 → 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.
- package/build/cjs/3rd_party/discord.js +1 -1
- package/build/cjs/Auth.js +1 -1
- package/build/cjs/Client.js +1 -1
- package/build/cjs/Connection.js +1 -1
- package/build/cjs/HTTP.js +1 -1
- package/build/cjs/Protocol.js +1 -1
- package/build/cjs/Room.js +1 -1
- package/build/cjs/Storage.js +1 -1
- package/build/cjs/core/nanoevents.js +1 -1
- package/build/cjs/core/signal.js +1 -1
- package/build/cjs/core/utils.js +1 -1
- package/build/cjs/errors/Errors.js +1 -1
- package/build/cjs/index.js +1 -1
- package/build/cjs/legacy.js +1 -1
- package/build/cjs/serializer/NoneSerializer.js +1 -1
- package/build/cjs/serializer/SchemaSerializer.js +1 -1
- package/build/cjs/serializer/Serializer.js +1 -1
- package/build/cjs/transport/H3Transport.js +1 -1
- package/build/cjs/transport/WebSocketTransport.js +1 -1
- package/build/esm/3rd_party/discord.mjs +1 -1
- package/build/esm/Auth.mjs +1 -1
- package/build/esm/Client.mjs +1 -1
- package/build/esm/Connection.mjs +1 -1
- package/build/esm/HTTP.mjs +1 -1
- package/build/esm/Protocol.mjs +1 -1
- package/build/esm/Room.mjs +1 -1
- package/build/esm/Storage.mjs +1 -1
- package/build/esm/core/nanoevents.mjs +1 -1
- package/build/esm/core/signal.mjs +1 -1
- package/build/esm/core/utils.mjs +1 -1
- package/build/esm/errors/Errors.mjs +1 -1
- package/build/esm/index.mjs +1 -1
- package/build/esm/legacy.mjs +1 -1
- package/build/esm/serializer/NoneSerializer.mjs +1 -1
- package/build/esm/serializer/SchemaSerializer.mjs +1 -1
- package/build/esm/serializer/Serializer.mjs +1 -1
- package/build/esm/transport/H3Transport.mjs +1 -1
- package/build/esm/transport/WebSocketTransport.mjs +1 -1
- package/dist/colyseus-cocos-creator.js +1 -1
- package/dist/colyseus.js +1 -1
- package/dist/debug.js +1 -1
- package/lib/core/http_bkp.d.ts +10 -10
- package/package.json +7 -6
- package/src/3rd_party/discord.ts +48 -0
- package/src/Auth.ts +177 -0
- package/src/Client.ts +459 -0
- package/src/Connection.ts +51 -0
- package/src/HTTP.ts +545 -0
- package/src/HTTP_bkp.ts +67 -0
- package/src/Protocol.ts +25 -0
- package/src/Room.ts +505 -0
- package/src/Storage.ts +94 -0
- package/src/core/http_bkp.ts +358 -0
- package/src/core/nanoevents.ts +38 -0
- package/src/core/signal.ts +62 -0
- package/src/core/utils.ts +3 -0
- package/src/debug.ts +2743 -0
- package/src/errors/Errors.ts +29 -0
- package/src/index.ts +18 -0
- package/src/legacy.ts +29 -0
- package/src/serializer/FossilDeltaSerializer.ts +39 -0
- package/src/serializer/NoneSerializer.ts +9 -0
- package/src/serializer/SchemaSerializer.ts +61 -0
- package/src/serializer/Serializer.ts +23 -0
- package/src/transport/H3Transport.ts +199 -0
- package/src/transport/ITransport.ts +18 -0
- package/src/transport/WebSocketTransport.ts +53 -0
package/src/Room.ts
ADDED
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
import { type Room as ServerRoom, type ExtractMessageType } from '@colyseus/core';
|
|
2
|
+
import { decode, Decoder, encode, Iterator, schema, Schema, SchemaType } from '@colyseus/schema';
|
|
3
|
+
|
|
4
|
+
import { Packr, unpack } from '@colyseus/msgpackr';
|
|
5
|
+
|
|
6
|
+
import { Connection } from './Connection.ts';
|
|
7
|
+
import { Protocol } from './Protocol.ts';
|
|
8
|
+
import { getSerializer, Serializer } from './serializer/Serializer.ts';
|
|
9
|
+
|
|
10
|
+
// The unused imports here are important for better `.d.ts` file generation
|
|
11
|
+
// (Later merged with `dts-bundle-generator`)
|
|
12
|
+
import { createNanoEvents } from './core/nanoevents.ts';
|
|
13
|
+
import { createSignal } from './core/signal.ts';
|
|
14
|
+
|
|
15
|
+
import { SchemaConstructor, SchemaSerializer } from './serializer/SchemaSerializer.ts';
|
|
16
|
+
|
|
17
|
+
import { CloseCode } from './errors/Errors.ts';
|
|
18
|
+
import { now } from './core/utils.ts';
|
|
19
|
+
|
|
20
|
+
// Infer serializer type based on State: SchemaSerializer for Schema types, Serializer otherwise
|
|
21
|
+
export type InferSerializer<State> = [State] extends [Schema]
|
|
22
|
+
? SchemaSerializer<State>
|
|
23
|
+
: Serializer<State>;
|
|
24
|
+
|
|
25
|
+
export interface RoomAvailable<Metadata = any> {
|
|
26
|
+
name: string;
|
|
27
|
+
roomId: string;
|
|
28
|
+
clients: number;
|
|
29
|
+
maxClients: number;
|
|
30
|
+
metadata?: Metadata;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ReconnectionOptions {
|
|
34
|
+
/**
|
|
35
|
+
* The maximum number of reconnection attempts.
|
|
36
|
+
*/
|
|
37
|
+
maxRetries: number;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The minimum delay between reconnection attempts.
|
|
41
|
+
*/
|
|
42
|
+
minDelay: number;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The maximum delay between reconnection attempts.
|
|
46
|
+
*/
|
|
47
|
+
maxDelay: number;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The minimum uptime of the room before reconnection attempts can be made.
|
|
51
|
+
*/
|
|
52
|
+
minUptime: number;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The current number of reconnection attempts.
|
|
56
|
+
*/
|
|
57
|
+
retryCount: number;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The initial delay between reconnection attempts.
|
|
61
|
+
*/
|
|
62
|
+
delay: number;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The function to calculate the delay between reconnection attempts.
|
|
66
|
+
* @param attempt - The current attempt number.
|
|
67
|
+
* @param delay - The initial delay between reconnection attempts.
|
|
68
|
+
* @returns The delay between reconnection attempts.
|
|
69
|
+
*/
|
|
70
|
+
backoff: (attempt: number, delay: number) => number;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The maximum number of enqueued messages to buffer.
|
|
74
|
+
*/
|
|
75
|
+
maxEnqueuedMessages: number;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Buffer for messages sent while connection is not open.
|
|
79
|
+
* These messages will be sent once the connection is re-established.
|
|
80
|
+
*/
|
|
81
|
+
enqueuedMessages: Array<{ data: Uint8Array }>;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Whether the room is currently reconnecting.
|
|
85
|
+
*/
|
|
86
|
+
isReconnecting: boolean;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export class Room<
|
|
90
|
+
RoomType extends typeof ServerRoom = any,
|
|
91
|
+
State = RoomType['prototype']['state'],
|
|
92
|
+
> {
|
|
93
|
+
public roomId: string;
|
|
94
|
+
public sessionId: string;
|
|
95
|
+
public reconnectionToken: string;
|
|
96
|
+
|
|
97
|
+
public name: string;
|
|
98
|
+
public connection: Connection;
|
|
99
|
+
|
|
100
|
+
// Public signals
|
|
101
|
+
public onStateChange = createSignal<(state: State) => void>();
|
|
102
|
+
public onError = createSignal<(code: number, message?: string) => void>();
|
|
103
|
+
public onLeave = createSignal<(code: number, reason?: string) => void>();
|
|
104
|
+
|
|
105
|
+
public onReconnect = createSignal<() => void>();
|
|
106
|
+
public onDrop = createSignal<(code: number, reason?: string) => void>();
|
|
107
|
+
|
|
108
|
+
protected onJoin = createSignal();
|
|
109
|
+
|
|
110
|
+
public serializerId: string;
|
|
111
|
+
public serializer: InferSerializer<State>;
|
|
112
|
+
|
|
113
|
+
// reconnection logic
|
|
114
|
+
public reconnection: ReconnectionOptions = {
|
|
115
|
+
retryCount: 0,
|
|
116
|
+
maxRetries: 8,
|
|
117
|
+
delay: 100,
|
|
118
|
+
minDelay: 100,
|
|
119
|
+
maxDelay: 5000,
|
|
120
|
+
minUptime: 5000,
|
|
121
|
+
backoff: exponentialBackoff,
|
|
122
|
+
maxEnqueuedMessages: 10,
|
|
123
|
+
enqueuedMessages: [],
|
|
124
|
+
isReconnecting: false,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
protected joinedAtTime: number = 0;
|
|
128
|
+
|
|
129
|
+
protected onMessageHandlers = createNanoEvents();
|
|
130
|
+
|
|
131
|
+
protected packr: Packr;
|
|
132
|
+
|
|
133
|
+
#lastPingTime: number = 0;
|
|
134
|
+
#pingCallback: (ms: number) => void;
|
|
135
|
+
|
|
136
|
+
constructor(name: string, rootSchema?: SchemaConstructor<State>) {
|
|
137
|
+
this.name = name;
|
|
138
|
+
|
|
139
|
+
this.packr = new Packr();
|
|
140
|
+
|
|
141
|
+
// msgpackr workaround: force buffer to be created.
|
|
142
|
+
this.packr.encode(undefined);
|
|
143
|
+
|
|
144
|
+
if (rootSchema) {
|
|
145
|
+
this.serializer = new (getSerializer("schema"));
|
|
146
|
+
(this.serializer as SchemaSerializer).state = new rootSchema();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
this.onError((code, message) => console.warn?.(`colyseus.js - onError => (${code}) ${message}`));
|
|
150
|
+
this.onLeave(() => this.removeAllListeners());
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
public connect(endpoint: string, options?: any, headers?: any) {
|
|
154
|
+
this.connection = new Connection(options.protocol);
|
|
155
|
+
this.connection.events.onmessage = this.onMessageCallback.bind(this);
|
|
156
|
+
this.connection.events.onclose = (e: CloseEvent) => {
|
|
157
|
+
if (this.joinedAtTime === 0) {
|
|
158
|
+
console.warn?.(`Room connection was closed unexpectedly (${e.code}): ${e.reason}`);
|
|
159
|
+
this.onError.invoke(e.code, e.reason);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (
|
|
164
|
+
e.code === CloseCode.NO_STATUS_RECEIVED ||
|
|
165
|
+
e.code === CloseCode.ABNORMAL_CLOSURE ||
|
|
166
|
+
e.code === CloseCode.GOING_AWAY ||
|
|
167
|
+
e.code === CloseCode.DEVMODE_RESTART
|
|
168
|
+
) {
|
|
169
|
+
this.onDrop.invoke(e.code, e.reason);
|
|
170
|
+
this.handleReconnection();
|
|
171
|
+
|
|
172
|
+
} else {
|
|
173
|
+
this.onLeave.invoke(e.code, e.reason);
|
|
174
|
+
this.destroy();
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
this.connection.events.onerror = (e: CloseEvent) => {
|
|
179
|
+
console.warn?.(`Room, onError (${e.code}): ${e.reason}`);
|
|
180
|
+
this.onError.invoke(e.code, e.reason);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* if local serializer has state, it means we don't need to receive the
|
|
185
|
+
* handshake from the server
|
|
186
|
+
*/
|
|
187
|
+
const skipHandshake = (this.serializer?.getState() !== undefined);
|
|
188
|
+
|
|
189
|
+
if (options.protocol === "h3") {
|
|
190
|
+
// FIXME: refactor this.
|
|
191
|
+
const url = new URL(endpoint);
|
|
192
|
+
this.connection.connect(url.origin, { ...options, skipHandshake });
|
|
193
|
+
|
|
194
|
+
} else {
|
|
195
|
+
this.connection.connect(`${endpoint}${skipHandshake ? "?skipHandshake=1" : ""}`, headers);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
public leave(consented: boolean = true): Promise<number> {
|
|
201
|
+
return new Promise((resolve) => {
|
|
202
|
+
this.onLeave((code) => resolve(code));
|
|
203
|
+
|
|
204
|
+
if (this.connection) {
|
|
205
|
+
if (consented) {
|
|
206
|
+
this.packr.buffer[0] = Protocol.LEAVE_ROOM;
|
|
207
|
+
this.connection.send(this.packr.buffer.subarray(0, 1));
|
|
208
|
+
|
|
209
|
+
} else {
|
|
210
|
+
this.connection.close();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
} else {
|
|
214
|
+
this.onLeave.invoke(CloseCode.CONSENTED);
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
public onMessage<MessageType extends keyof RoomType['prototype']['~client']['~messages']>(
|
|
220
|
+
message: MessageType,
|
|
221
|
+
callback: (payload: RoomType['prototype']['~client']['~messages'][MessageType]) => void
|
|
222
|
+
)
|
|
223
|
+
public onMessage<T = any>(type: "*", callback: (messageType: string | number, payload: T) => void)
|
|
224
|
+
public onMessage<T = any>(type: string | number, callback: (payload: T) => void)
|
|
225
|
+
public onMessage(type: '*' | string | number, callback: (...args: any[]) => void) {
|
|
226
|
+
return this.onMessageHandlers.on(this.getMessageHandlerKey(type), callback);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
public ping(callback: (ms: number) => void) {
|
|
230
|
+
this.#lastPingTime = now();
|
|
231
|
+
this.#pingCallback = callback;
|
|
232
|
+
this.packr.buffer[0] = Protocol.PING;
|
|
233
|
+
this.connection.send(this.packr.buffer.subarray(0, 1));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
public send<MessageType extends keyof RoomType['prototype']['messages']>(
|
|
237
|
+
messageType: MessageType,
|
|
238
|
+
payload?: ExtractMessageType<RoomType['prototype']['messages'][MessageType]>
|
|
239
|
+
)
|
|
240
|
+
public send<T = any>(messageType: string | number, payload?: T): void {
|
|
241
|
+
const it: Iterator = { offset: 1 };
|
|
242
|
+
this.packr.buffer[0] = Protocol.ROOM_DATA;
|
|
243
|
+
|
|
244
|
+
if (typeof(messageType) === "string") {
|
|
245
|
+
encode.string(this.packr.buffer as Buffer, messageType, it);
|
|
246
|
+
|
|
247
|
+
} else {
|
|
248
|
+
encode.number(this.packr.buffer as Buffer, messageType, it);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// force packr to use beginning of the buffer
|
|
252
|
+
this.packr.position = 0;
|
|
253
|
+
|
|
254
|
+
const data = (payload !== undefined)
|
|
255
|
+
? this.packr.pack(payload, 2048 + it.offset) // 2048 = RESERVE_START_SPACE
|
|
256
|
+
: this.packr.buffer.subarray(0, it.offset);
|
|
257
|
+
|
|
258
|
+
// If connection is not open, buffer the message
|
|
259
|
+
if (!this.connection.isOpen) {
|
|
260
|
+
enqueueMessage(this, new Uint8Array(data));
|
|
261
|
+
} else {
|
|
262
|
+
this.connection.send(data);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
public sendUnreliable<T = any>(type: string | number, message?: T): void {
|
|
267
|
+
// If connection is not open, skip
|
|
268
|
+
if (!this.connection.isOpen) { return; }
|
|
269
|
+
|
|
270
|
+
const it: Iterator = { offset: 1 };
|
|
271
|
+
this.packr.buffer[0] = Protocol.ROOM_DATA;
|
|
272
|
+
|
|
273
|
+
if (typeof(type) === "string") {
|
|
274
|
+
encode.string(this.packr.buffer as Buffer, type, it);
|
|
275
|
+
|
|
276
|
+
} else {
|
|
277
|
+
encode.number(this.packr.buffer as Buffer, type, it);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// force packr to use beginning of the buffer
|
|
281
|
+
this.packr.position = 0;
|
|
282
|
+
|
|
283
|
+
const data = (message !== undefined)
|
|
284
|
+
? this.packr.pack(message, 2048 + it.offset) // 2048 = RESERVE_START_SPACE
|
|
285
|
+
: this.packr.buffer.subarray(0, it.offset);
|
|
286
|
+
|
|
287
|
+
this.connection.sendUnreliable(data);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
public sendBytes(type: string | number, bytes: Uint8Array) {
|
|
291
|
+
const it: Iterator = { offset: 1 };
|
|
292
|
+
this.packr.buffer[0] = Protocol.ROOM_DATA_BYTES;
|
|
293
|
+
|
|
294
|
+
if (typeof(type) === "string") {
|
|
295
|
+
encode.string(this.packr.buffer as Buffer, type, it);
|
|
296
|
+
|
|
297
|
+
} else {
|
|
298
|
+
encode.number(this.packr.buffer as Buffer, type, it);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// check if buffer needs to be resized
|
|
302
|
+
// TODO: can we avoid this?
|
|
303
|
+
if (bytes.byteLength + it.offset > this.packr.buffer.byteLength) {
|
|
304
|
+
const newBuffer = new Uint8Array(it.offset + bytes.byteLength);
|
|
305
|
+
newBuffer.set(this.packr.buffer);
|
|
306
|
+
this.packr.useBuffer(newBuffer);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
this.packr.buffer.set(bytes, it.offset);
|
|
310
|
+
|
|
311
|
+
// If connection is not open, buffer the message
|
|
312
|
+
if (!this.connection.isOpen) {
|
|
313
|
+
enqueueMessage(this, this.packr.buffer.subarray(0, it.offset + bytes.byteLength));
|
|
314
|
+
} else {
|
|
315
|
+
this.connection.send(this.packr.buffer.subarray(0, it.offset + bytes.byteLength));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
public get state (): State {
|
|
321
|
+
return this.serializer.getState();
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
public removeAllListeners() {
|
|
325
|
+
this.onJoin.clear();
|
|
326
|
+
this.onStateChange.clear();
|
|
327
|
+
this.onError.clear();
|
|
328
|
+
this.onLeave.clear();
|
|
329
|
+
this.onMessageHandlers.events = {};
|
|
330
|
+
|
|
331
|
+
if (this.serializer instanceof SchemaSerializer) {
|
|
332
|
+
// Remove callback references
|
|
333
|
+
this.serializer.decoder.root.callbacks = {};
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
protected onMessageCallback(event: MessageEvent) {
|
|
338
|
+
const buffer = new Uint8Array(event.data);
|
|
339
|
+
|
|
340
|
+
const it: Iterator = { offset: 1 };
|
|
341
|
+
const code = buffer[0];
|
|
342
|
+
|
|
343
|
+
if (code === Protocol.JOIN_ROOM) {
|
|
344
|
+
const reconnectionToken = decode.utf8Read(buffer as Buffer, it, buffer[it.offset++]);
|
|
345
|
+
this.serializerId = decode.utf8Read(buffer as Buffer, it, buffer[it.offset++]);
|
|
346
|
+
|
|
347
|
+
// Instantiate serializer if not locally available.
|
|
348
|
+
if (!this.serializer) {
|
|
349
|
+
const serializer = getSerializer(this.serializerId);
|
|
350
|
+
this.serializer = new serializer();
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// apply handshake on first join (no need to do this on reconnect)
|
|
354
|
+
if (buffer.byteLength > it.offset && this.serializer.handshake) {
|
|
355
|
+
this.serializer.handshake(buffer, it);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (this.joinedAtTime === 0) {
|
|
359
|
+
this.joinedAtTime = Date.now();
|
|
360
|
+
this.onJoin.invoke();
|
|
361
|
+
|
|
362
|
+
} else {
|
|
363
|
+
console.info(`[Colyseus reconnection]: ${String.fromCodePoint(0x2705)} reconnection successful!`); // ✅
|
|
364
|
+
this.reconnection.isReconnecting = false;
|
|
365
|
+
this.onReconnect.invoke();
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
this.reconnectionToken = `${this.roomId}:${reconnectionToken}`;
|
|
369
|
+
|
|
370
|
+
// acknowledge successfull JOIN_ROOM
|
|
371
|
+
this.packr.buffer[0] = Protocol.JOIN_ROOM;
|
|
372
|
+
this.connection.send(this.packr.buffer.subarray(0, 1));
|
|
373
|
+
|
|
374
|
+
// Send any enqueued messages that were buffered while disconnected
|
|
375
|
+
if (this.reconnection.enqueuedMessages.length > 0) {
|
|
376
|
+
for (const message of this.reconnection.enqueuedMessages) {
|
|
377
|
+
this.connection.send(message.data);
|
|
378
|
+
}
|
|
379
|
+
// Clear the buffer after sending
|
|
380
|
+
this.reconnection.enqueuedMessages = [];
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
} else if (code === Protocol.ERROR) {
|
|
384
|
+
const code = decode.number(buffer as Buffer, it);
|
|
385
|
+
const message = decode.string(buffer as Buffer, it);
|
|
386
|
+
|
|
387
|
+
this.onError.invoke(code, message);
|
|
388
|
+
|
|
389
|
+
} else if (code === Protocol.LEAVE_ROOM) {
|
|
390
|
+
this.leave();
|
|
391
|
+
|
|
392
|
+
} else if (code === Protocol.ROOM_STATE) {
|
|
393
|
+
this.serializer.setState(buffer, it);
|
|
394
|
+
this.onStateChange.invoke(this.serializer.getState());
|
|
395
|
+
|
|
396
|
+
} else if (code === Protocol.ROOM_STATE_PATCH) {
|
|
397
|
+
this.serializer.patch(buffer, it);
|
|
398
|
+
this.onStateChange.invoke(this.serializer.getState());
|
|
399
|
+
|
|
400
|
+
} else if (code === Protocol.ROOM_DATA) {
|
|
401
|
+
const type = (decode.stringCheck(buffer as Buffer, it))
|
|
402
|
+
? decode.string(buffer as Buffer, it)
|
|
403
|
+
: decode.number(buffer as Buffer, it);
|
|
404
|
+
|
|
405
|
+
const message = (buffer.byteLength > it.offset)
|
|
406
|
+
? unpack(buffer as Buffer, { start: it.offset })
|
|
407
|
+
: undefined;
|
|
408
|
+
|
|
409
|
+
this.dispatchMessage(type, message);
|
|
410
|
+
|
|
411
|
+
} else if (code === Protocol.ROOM_DATA_BYTES) {
|
|
412
|
+
const type = (decode.stringCheck(buffer as Buffer, it))
|
|
413
|
+
? decode.string(buffer as Buffer, it)
|
|
414
|
+
: decode.number(buffer as Buffer, it);
|
|
415
|
+
|
|
416
|
+
this.dispatchMessage(type, buffer.subarray(it.offset));
|
|
417
|
+
} else if (code === Protocol.PING) {
|
|
418
|
+
this.#pingCallback?.(now() - this.#lastPingTime);
|
|
419
|
+
this.#pingCallback = undefined;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
private dispatchMessage(type: string | number, message: any) {
|
|
424
|
+
const messageType = this.getMessageHandlerKey(type);
|
|
425
|
+
|
|
426
|
+
if (this.onMessageHandlers.events[messageType]) {
|
|
427
|
+
this.onMessageHandlers.emit(messageType, message);
|
|
428
|
+
|
|
429
|
+
} else if (this.onMessageHandlers.events['*']) {
|
|
430
|
+
this.onMessageHandlers.emit('*', type, message);
|
|
431
|
+
|
|
432
|
+
} else if (!messageType.startsWith("__")) { // ignore internal messages
|
|
433
|
+
console.warn?.(`colyseus.js: onMessage() not registered for type '${type}'.`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
private destroy () {
|
|
438
|
+
if (this.serializer) {
|
|
439
|
+
this.serializer.teardown();
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
private getMessageHandlerKey(type: string | number): string {
|
|
444
|
+
switch (typeof(type)) {
|
|
445
|
+
// string
|
|
446
|
+
case "string": return type;
|
|
447
|
+
|
|
448
|
+
// number
|
|
449
|
+
case "number": return `i${type}`;
|
|
450
|
+
|
|
451
|
+
default: throw new Error("invalid message type.");
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
private handleReconnection() {
|
|
456
|
+
if (Date.now() - this.joinedAtTime < this.reconnection.minUptime) {
|
|
457
|
+
console.info(`[Colyseus reconnection]: ${String.fromCodePoint(0x274C)} Room has not been up for long enough for automatic reconnection. (min uptime: ${this.reconnection.minUptime}ms)`); // ❌
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (!this.reconnection.isReconnecting) {
|
|
462
|
+
console.info(`[Colyseus reconnection]: ${String.fromCodePoint(0x1F504)} Re-establishing connection with roomId '${this.roomId}'...`); // 🔄
|
|
463
|
+
this.reconnection.retryCount = 0;
|
|
464
|
+
this.reconnection.isReconnecting = true;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
this.retryReconnection();
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
private retryReconnection() {
|
|
471
|
+
this.reconnection.retryCount++;
|
|
472
|
+
|
|
473
|
+
const delay = Math.min(this.reconnection.maxDelay, Math.max(this.reconnection.minDelay, this.reconnection.backoff(this.reconnection.retryCount, this.reconnection.delay)));
|
|
474
|
+
|
|
475
|
+
console.info(`[Colyseus reconnection]: ${String.fromCodePoint(0x1F504)} will retry in ${delay}ms... (${this.reconnection.retryCount} out of ${this.reconnection.maxRetries})`); // 🔄
|
|
476
|
+
|
|
477
|
+
// Wait before attempting reconnection
|
|
478
|
+
setTimeout(() => {
|
|
479
|
+
try {
|
|
480
|
+
this.connection.reconnect({
|
|
481
|
+
reconnectionToken: this.reconnectionToken.split(":")[1],
|
|
482
|
+
skipHandshake: true, // we already applied the handshake on first join
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
} catch (e) {
|
|
486
|
+
if (this.reconnection.retryCount < this.reconnection.maxRetries) {
|
|
487
|
+
this.retryReconnection();
|
|
488
|
+
} else {
|
|
489
|
+
console.info(`[Colyseus reconnection]: ${String.fromCodePoint(0x274C)} Failed to reconnect. Is your server running? Please check server logs.`); // ❌
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}, delay);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const exponentialBackoff = (attempt: number, delay: number) => {
|
|
497
|
+
return Math.floor(Math.pow(2, attempt) * delay);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function enqueueMessage(room: Room, message: Uint8Array) {
|
|
501
|
+
room.reconnection.enqueuedMessages.push({ data: message });
|
|
502
|
+
if (room.reconnection.enqueuedMessages.length > room.reconnection.maxEnqueuedMessages) {
|
|
503
|
+
room.reconnection.enqueuedMessages.shift();
|
|
504
|
+
}
|
|
505
|
+
}
|
package/src/Storage.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/// <reference path="../typings/cocos-creator.d.ts" />
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* We do not assign 'storage' to window.localStorage immediatelly for React
|
|
5
|
+
* Native compatibility. window.localStorage is not present when this module is
|
|
6
|
+
* loaded.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
let storage: any;
|
|
10
|
+
|
|
11
|
+
function getStorage(): Storage {
|
|
12
|
+
if (!storage) {
|
|
13
|
+
try {
|
|
14
|
+
storage = (typeof (cc) !== 'undefined' && cc.sys && cc.sys.localStorage)
|
|
15
|
+
? cc.sys.localStorage // compatibility with cocos creator
|
|
16
|
+
: window.localStorage; // RN does have window object at this point, but localStorage is not defined
|
|
17
|
+
|
|
18
|
+
} catch (e) {
|
|
19
|
+
// ignore error
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!storage && typeof (globalThis.indexedDB) !== 'undefined') {
|
|
24
|
+
storage = new IndexedDBStorage();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!storage) {
|
|
28
|
+
// mock localStorage if not available (Node.js or RN environment)
|
|
29
|
+
storage = {
|
|
30
|
+
cache: {},
|
|
31
|
+
setItem: function (key, value) { this.cache[key] = value; },
|
|
32
|
+
getItem: function (key) { this.cache[key]; },
|
|
33
|
+
removeItem: function (key) { delete this.cache[key]; },
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return storage;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function setItem(key: string, value: string) {
|
|
41
|
+
getStorage().setItem(key, value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function removeItem(key: string) {
|
|
45
|
+
getStorage().removeItem(key);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function getItem(key: string, callback: Function) {
|
|
49
|
+
const value: any = getStorage().getItem(key);
|
|
50
|
+
|
|
51
|
+
if (
|
|
52
|
+
typeof (Promise) === 'undefined' || // old browsers
|
|
53
|
+
!(value instanceof Promise)
|
|
54
|
+
) {
|
|
55
|
+
// browser has synchronous return
|
|
56
|
+
callback(value);
|
|
57
|
+
|
|
58
|
+
} else {
|
|
59
|
+
// react-native is asynchronous
|
|
60
|
+
value.then((id) => callback(id));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* When running in a Web Worker, we need to use IndexedDB to store data.
|
|
66
|
+
*/
|
|
67
|
+
class IndexedDBStorage {
|
|
68
|
+
private dbPromise: Promise<IDBDatabase> = new Promise((resolve) => {
|
|
69
|
+
const request = indexedDB.open('_colyseus_storage', 1);
|
|
70
|
+
request.onupgradeneeded = () => request.result.createObjectStore('store');
|
|
71
|
+
request.onsuccess = () => resolve(request.result);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
private async tx(mode: IDBTransactionMode, fn: (store: IDBObjectStore) => IDBRequest) {
|
|
75
|
+
const db = await this.dbPromise;
|
|
76
|
+
const store = db.transaction('store', mode).objectStore('store');
|
|
77
|
+
return fn(store);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
setItem(key: string, value: string) {
|
|
81
|
+
return this.tx('readwrite', store => store.put(value, key)).then();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async getItem(key: string) {
|
|
85
|
+
const request = await this.tx('readonly', store => store.get(key));
|
|
86
|
+
return new Promise<string | undefined>((resolve) => {
|
|
87
|
+
request.onsuccess = () => resolve(request.result);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
removeItem(key: string) {
|
|
92
|
+
return this.tx('readwrite', store => store.delete(key)).then();
|
|
93
|
+
}
|
|
94
|
+
}
|