@colyseus/core 0.17.51 → 0.17.53
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/MatchMaker.cjs +54 -3
- package/build/MatchMaker.cjs.map +3 -3
- package/build/MatchMaker.d.ts +9 -0
- package/build/MatchMaker.mjs +54 -4
- package/build/MatchMaker.mjs.map +3 -3
- package/build/Transport.cjs.map +2 -2
- package/build/Transport.d.ts +11 -0
- package/build/Transport.mjs.map +2 -2
- package/build/presence/LocalPresence.cjs +2 -0
- package/build/presence/LocalPresence.cjs.map +2 -2
- package/build/presence/LocalPresence.mjs +2 -0
- package/build/presence/LocalPresence.mjs.map +2 -2
- package/build/serializer/SchemaSerializer.cjs +25 -9
- package/build/serializer/SchemaSerializer.cjs.map +2 -2
- package/build/serializer/SchemaSerializer.d.ts +10 -0
- package/build/serializer/SchemaSerializer.mjs +25 -9
- package/build/serializer/SchemaSerializer.mjs.map +2 -2
- package/build/utils/Utils.cjs +6 -0
- package/build/utils/Utils.cjs.map +2 -2
- package/build/utils/Utils.d.ts +2 -0
- package/build/utils/Utils.mjs +4 -0
- package/build/utils/Utils.mjs.map +2 -2
- package/package.json +5 -5
- package/src/MatchMaker.ts +88 -6
- package/src/Transport.ts +11 -0
- package/src/presence/LocalPresence.ts +2 -0
- package/src/serializer/SchemaSerializer.ts +27 -9
- package/src/utils/Utils.ts +6 -0
package/src/MatchMaker.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { EventEmitter } from 'events';
|
|
|
2
2
|
|
|
3
3
|
import { requestFromIPC, subscribeIPC, subscribeWithTimeout } from './IPC.ts';
|
|
4
4
|
|
|
5
|
-
import { type Type, Deferred, generateId, merge, retry, MAX_CONCURRENT_CREATE_ROOM_WAIT_TIME, REMOTE_ROOM_SHORT_TIMEOUT, type MethodName, type RemoteRoomCallReturn } from './utils/Utils.ts';
|
|
5
|
+
import { type Type, Deferred, generateId, merge, retry, MAX_CONCURRENT_CREATE_ROOM_WAIT_TIME, REMOTE_ROOM_SHORT_TIMEOUT, SWEPT_CHECK_INTERVAL, SWEPT_RECEIPT_TTL, type MethodName, type RemoteRoomCallReturn } from './utils/Utils.ts';
|
|
6
6
|
import { isDevMode, cacheRoomHistory, getPreviousProcessId, getRoomRestoreListKey, reloadFromCache } from './utils/DevMode.ts';
|
|
7
7
|
|
|
8
8
|
import { RegisteredHandler } from './matchmaker/RegisteredHandler.ts';
|
|
@@ -174,6 +174,8 @@ export async function accept(isStandalone: boolean = false) {
|
|
|
174
174
|
* (`stats.excludeProcess()`) by mistake due to health-check failure
|
|
175
175
|
*/
|
|
176
176
|
stats.setAutoPersistInterval();
|
|
177
|
+
|
|
178
|
+
startSweptCheck();
|
|
177
179
|
}
|
|
178
180
|
|
|
179
181
|
state = MatchMakerState.READY;
|
|
@@ -774,6 +776,9 @@ export async function gracefullyShutdown(): Promise<any> {
|
|
|
774
776
|
// unsubscribe from process id channel
|
|
775
777
|
presence.unsubscribe(getProcessChannel());
|
|
776
778
|
|
|
779
|
+
// only now: rooms may take minutes to drain, and must stay joinable meanwhile
|
|
780
|
+
clearInterval(sweptCheckInterval);
|
|
781
|
+
|
|
777
782
|
// make sure all rooms are disposed
|
|
778
783
|
return Promise.all(disconnectAll(
|
|
779
784
|
(isDevMode)
|
|
@@ -1026,13 +1031,24 @@ export function healthCheckProcessId(processId: string) {
|
|
|
1026
1031
|
resolve(true)
|
|
1027
1032
|
|
|
1028
1033
|
} catch (e) {
|
|
1029
|
-
// process failed to respond - remove it from stats
|
|
1030
1034
|
logger.debug(`❌ Process '${processId}' failed to respond. Cleaning it up.`);
|
|
1031
|
-
await stats.excludeProcess(processId);
|
|
1032
1035
|
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
+
try {
|
|
1037
|
+
// stop placing new rooms on it. if it is still alive, it re-adds itself
|
|
1038
|
+
// the next time it persists its stats.
|
|
1039
|
+
await stats.excludeProcess(processId);
|
|
1040
|
+
|
|
1041
|
+
// clean-up possibly stale room ids
|
|
1042
|
+
if (!isDevMode) {
|
|
1043
|
+
await removeRoomsByProcessId(processId);
|
|
1044
|
+
|
|
1045
|
+
// receipt goes AFTER the removal: written before, a merely stalled
|
|
1046
|
+
// process could restore its listings ahead of the delete
|
|
1047
|
+
await presence.setex(getProcessSweptKey(processId), '1', SWEPT_RECEIPT_TTL);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
} catch (e) {
|
|
1051
|
+
debugMatchMaking(e); // presence/driver failing must not leave the caller hanging
|
|
1036
1052
|
}
|
|
1037
1053
|
|
|
1038
1054
|
resolve(false);
|
|
@@ -1056,6 +1072,68 @@ async function removeRoomsByProcessId(processId: string) {
|
|
|
1056
1072
|
await driver.cleanup(processId);
|
|
1057
1073
|
}
|
|
1058
1074
|
|
|
1075
|
+
let sweptCheckInterval: NodeJS.Timeout | undefined;
|
|
1076
|
+
let isCheckingSwept = false;
|
|
1077
|
+
|
|
1078
|
+
function startSweptCheck() {
|
|
1079
|
+
clearInterval(sweptCheckInterval); // accept() may be called more than once
|
|
1080
|
+
|
|
1081
|
+
sweptCheckInterval = setInterval(async () => {
|
|
1082
|
+
if (isCheckingSwept) { return; } // a slow presence would stack them up
|
|
1083
|
+
isCheckingSwept = true;
|
|
1084
|
+
|
|
1085
|
+
try {
|
|
1086
|
+
await restoreIfSwept();
|
|
1087
|
+
|
|
1088
|
+
} catch (e) {
|
|
1089
|
+
debugMatchMaking(e); // a failed presence call must not crash the process
|
|
1090
|
+
|
|
1091
|
+
} finally {
|
|
1092
|
+
isCheckingSwept = false;
|
|
1093
|
+
}
|
|
1094
|
+
}, SWEPT_CHECK_INTERVAL);
|
|
1095
|
+
|
|
1096
|
+
sweptCheckInterval.unref(); // don't keep the process running just for this
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
/**
|
|
1100
|
+
* A peer that failed to reach this process removed our room listings, and
|
|
1101
|
+
* left a receipt. If we are reading it, we were only stalled: re-create them.
|
|
1102
|
+
*/
|
|
1103
|
+
async function restoreIfSwept() {
|
|
1104
|
+
if (!await presence.exists(getProcessSweptKey())) { return; }
|
|
1105
|
+
|
|
1106
|
+
// consume before restoring: a removal landing mid-restore leaves a new receipt
|
|
1107
|
+
await presence.del(getProcessSweptKey());
|
|
1108
|
+
await restoreMissingRoomCaches();
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
/**
|
|
1112
|
+
* Re-create the listings of rooms running on this process that are missing
|
|
1113
|
+
* from the driver.
|
|
1114
|
+
*
|
|
1115
|
+
* Peers remove a process's listings when they believe it is dead. If this
|
|
1116
|
+
* process was only stalled, its rooms are still running, and without their
|
|
1117
|
+
* listings nobody could join them anymore.
|
|
1118
|
+
*/
|
|
1119
|
+
export async function restoreMissingRoomCaches() {
|
|
1120
|
+
const roomIds = Object.keys(rooms);
|
|
1121
|
+
if (roomIds.length === 0) { return; }
|
|
1122
|
+
|
|
1123
|
+
// one lookup per room: 0.17 drivers have no batch lookup, and this only runs after a sweep
|
|
1124
|
+
const listed = await Promise.all(roomIds.map((roomId) => driver.has(roomId)));
|
|
1125
|
+
|
|
1126
|
+
const missing = roomIds
|
|
1127
|
+
.filter((_, i) => !listed[i])
|
|
1128
|
+
.map((roomId) => rooms[roomId])
|
|
1129
|
+
.filter((room) =>
|
|
1130
|
+
room?.['_internalState'] === RoomInternalState.CREATED // still here, not disposing
|
|
1131
|
+
);
|
|
1132
|
+
|
|
1133
|
+
debugMatchMaking('restoring %s missing room listing(s)', missing.length);
|
|
1134
|
+
await Promise.all(missing.map((room) => driver.persist(room['_listing'], true)));
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1059
1137
|
async function createRoomReferences(room: Room, init: boolean = false): Promise<boolean> {
|
|
1060
1138
|
rooms[room.roomId] = room;
|
|
1061
1139
|
|
|
@@ -1212,3 +1290,7 @@ function getConcurrencyHashKey(roomName: string) {
|
|
|
1212
1290
|
function getProcessChannel(id: string = processId) {
|
|
1213
1291
|
return `p:${id}`;
|
|
1214
1292
|
}
|
|
1293
|
+
|
|
1294
|
+
function getProcessSweptKey(id: string = processId) {
|
|
1295
|
+
return `swept:${id}`;
|
|
1296
|
+
}
|
package/src/Transport.ts
CHANGED
|
@@ -132,7 +132,18 @@ export interface Client<T extends { userData?: any, auth?: any, messages?: Recor
|
|
|
132
132
|
reconnectionToken: string;
|
|
133
133
|
|
|
134
134
|
// TODO: move these to ClientPrivate
|
|
135
|
+
/**
|
|
136
|
+
* Send raw bytes over the transport's reliable channel.
|
|
137
|
+
*
|
|
138
|
+
* The transport owns `data` from this call on: it may write it later (a
|
|
139
|
+
* socket with backpressure, permessage-deflate), so callers never mutate it
|
|
140
|
+
* afterwards. The same `data` may be handed to several clients.
|
|
141
|
+
*/
|
|
135
142
|
raw(data: Uint8Array | Buffer, options?: ISendOptions, cb?: (err?: Error) => void): void;
|
|
143
|
+
/**
|
|
144
|
+
* {@link raw}, or held until the client finishes joining (or until after the
|
|
145
|
+
* next patch, with `afterNextPatch`). Owns `data` the same way {@link raw} does.
|
|
146
|
+
*/
|
|
136
147
|
enqueueRaw(data: Uint8Array | Buffer, options?: ISendOptions): void;
|
|
137
148
|
|
|
138
149
|
/**
|
|
@@ -106,6 +106,8 @@ export class LocalPresence implements Presence {
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
public del(key: string) {
|
|
109
|
+
clearTimeout(this.timeouts[key]); // a pending expiry would wipe whatever is set next
|
|
110
|
+
delete this.timeouts[key];
|
|
109
111
|
delete this.keys[key];
|
|
110
112
|
delete this.data[key];
|
|
111
113
|
delete this.hash[key];
|
|
@@ -41,23 +41,38 @@ export class SchemaSerializer<T extends Schema> implements Serializer<T> {
|
|
|
41
41
|
public getFullState(client?: Client) {
|
|
42
42
|
if (this.needFullEncode || this.encoder.root.changes.next !== undefined) {
|
|
43
43
|
this.sharedOffsetCache = { offset: 1 };
|
|
44
|
-
|
|
44
|
+
// copy: the next full encode reuses the buffer (slice(): a pooled Buffer would pin 8KB)
|
|
45
|
+
this.fullEncodeCache = this.encoder.encodeAll(this.sharedOffsetCache, this.fullEncodeBuffer).slice();
|
|
46
|
+
this.fitFullEncodeBuffer(this.sharedOffsetCache.offset);
|
|
45
47
|
this.needFullEncode = false;
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
if (this.hasFilters && client?.view) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
this.fullEncodeBuffer
|
|
54
|
-
);
|
|
51
|
+
const it = { ...this.sharedOffsetCache };
|
|
52
|
+
const encoded = this.encoder.encodeAllView(client.view, this.sharedOffsetCache.offset, it, this.fullEncodeBuffer);
|
|
53
|
+
this.fitFullEncodeBuffer(it.offset);
|
|
54
|
+
return encoded;
|
|
55
55
|
|
|
56
56
|
} else {
|
|
57
57
|
return this.fullEncodeCache;
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Grow {@link fullEncodeBuffer} once an encode outgrew it. The encoder grows
|
|
63
|
+
* a buffer it is handed for that one encode only, so otherwise every full
|
|
64
|
+
* encode overflows again and per-view encodes read a truncated shared part.
|
|
65
|
+
*
|
|
66
|
+
* `byteLength` is the iterator's final offset, not the returned length:
|
|
67
|
+
* before @colyseus/schema 4.0.27, a per-view encode that overflows returns
|
|
68
|
+
* its bytes cut short at the old buffer size.
|
|
69
|
+
*/
|
|
70
|
+
protected fitFullEncodeBuffer(byteLength: number) {
|
|
71
|
+
if (byteLength <= this.fullEncodeBuffer.byteLength) { return; }
|
|
72
|
+
this.fullEncodeBuffer = new Uint8Array(byteLength + Encoder.BUFFER_SIZE);
|
|
73
|
+
this.fullEncodeBuffer.set(this.fullEncodeCache);
|
|
74
|
+
}
|
|
75
|
+
|
|
61
76
|
public applyPatches(clients: Client[]) {
|
|
62
77
|
let numClients = clients.length;
|
|
63
78
|
|
|
@@ -119,6 +134,9 @@ export class SchemaSerializer<T extends Schema> implements Serializer<T> {
|
|
|
119
134
|
const encodedChanges = this.encoder.encode(it);
|
|
120
135
|
|
|
121
136
|
if (!this.hasFilters) {
|
|
137
|
+
// copy once for all clients: a transport may still hold it after the next encode (see Client.raw)
|
|
138
|
+
const frame = Buffer.from(encodedChanges);
|
|
139
|
+
|
|
122
140
|
while (numClients--) {
|
|
123
141
|
const client = clients[numClients];
|
|
124
142
|
|
|
@@ -129,7 +147,7 @@ export class SchemaSerializer<T extends Schema> implements Serializer<T> {
|
|
|
129
147
|
continue;
|
|
130
148
|
}
|
|
131
149
|
|
|
132
|
-
client.raw(
|
|
150
|
+
client.raw(frame);
|
|
133
151
|
}
|
|
134
152
|
|
|
135
153
|
} else {
|
|
@@ -154,7 +172,7 @@ export class SchemaSerializer<T extends Schema> implements Serializer<T> {
|
|
|
154
172
|
// allow to pass the same encoded view for multiple clients
|
|
155
173
|
if (encodedView === undefined) {
|
|
156
174
|
encodedView = (view === SHARED_VIEW)
|
|
157
|
-
? encodedChanges
|
|
175
|
+
? Buffer.from(encodedChanges) // encodeView already returns a copy
|
|
158
176
|
: this.encoder.encodeView(client.view, sharedOffset, it);
|
|
159
177
|
this.encodedViews.set(view, encodedView);
|
|
160
178
|
}
|
package/src/utils/Utils.ts
CHANGED
|
@@ -44,6 +44,12 @@ export type RemoteRoomCallReturn<
|
|
|
44
44
|
export const REMOTE_ROOM_SHORT_TIMEOUT = Number(process.env.COLYSEUS_PRESENCE_SHORT_TIMEOUT || 2000);
|
|
45
45
|
export const MAX_CONCURRENT_CREATE_ROOM_WAIT_TIME = Number(process.env.COLYSEUS_MAX_CONCURRENT_CREATE_ROOM_WAIT_TIME || 0.5);
|
|
46
46
|
|
|
47
|
+
// a peer that removes this process's room listings leaves a receipt. this
|
|
48
|
+
// process looks for it every INTERVAL (ms) and restores them. the TTL (seconds)
|
|
49
|
+
// only bounds what dead processes leave behind.
|
|
50
|
+
export const SWEPT_CHECK_INTERVAL = Number(process.env.COLYSEUS_SWEPT_CHECK_INTERVAL || 5000);
|
|
51
|
+
export const SWEPT_RECEIPT_TTL = Number(process.env.COLYSEUS_SWEPT_RECEIPT_TTL || 600);
|
|
52
|
+
|
|
47
53
|
export function generateId(length: number = 9) {
|
|
48
54
|
return nanoid(length);
|
|
49
55
|
}
|