@colyseus/core 0.18.18 → 0.18.20
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/Room.cjs +4 -4
- package/build/Room.cjs.map +2 -2
- package/build/Room.mjs +4 -4
- package/build/Room.mjs.map +2 -2
- package/build/RoomPlugin.cjs +34 -17
- package/build/RoomPlugin.cjs.map +2 -2
- package/build/RoomPlugin.mjs +34 -17
- package/build/RoomPlugin.mjs.map +2 -2
- package/build/Transport.cjs.map +2 -2
- package/build/Transport.d.ts +15 -0
- package/build/Transport.mjs.map +2 -2
- package/build/serializer/SchemaSerializer.cjs +16 -16
- package/build/serializer/SchemaSerializer.cjs.map +2 -2
- package/build/serializer/SchemaSerializer.d.ts +1 -3
- package/build/serializer/SchemaSerializer.mjs +16 -16
- package/build/serializer/SchemaSerializer.mjs.map +2 -2
- package/package.json +8 -8
- package/src/Room.ts +8 -8
- package/src/RoomPlugin.ts +46 -21
- package/src/Transport.ts +15 -0
- package/src/serializer/SchemaSerializer.ts +26 -19
package/src/Room.ts
CHANGED
|
@@ -451,13 +451,6 @@ export class Room<T extends RoomOptions = RoomOptions> {
|
|
|
451
451
|
.catch((e) => debugAndPrintError(`onDispose error: ${(e && e.stack || e.message || e || 'promise rejected')} (roomId: ${this.roomId})`))
|
|
452
452
|
.finally(() => this._events.emit('disconnect'));
|
|
453
453
|
});
|
|
454
|
-
|
|
455
|
-
/**
|
|
456
|
-
* If `onUncaughtException` is defined, it will automatically catch exceptions
|
|
457
|
-
*/
|
|
458
|
-
if (this.onUncaughtException !== undefined) {
|
|
459
|
-
this.#registerUncaughtExceptionHandlers();
|
|
460
|
-
}
|
|
461
454
|
}
|
|
462
455
|
|
|
463
456
|
/**
|
|
@@ -592,6 +585,12 @@ export class Room<T extends RoomOptions = RoomOptions> {
|
|
|
592
585
|
setupRoomPlugins(this);
|
|
593
586
|
}
|
|
594
587
|
|
|
588
|
+
// After setupRoomPlugins(), so the try/catch covers plugin hooks too.
|
|
589
|
+
// Not in the constructor: subclass fields aren't assigned until it returns.
|
|
590
|
+
if (this.onUncaughtException !== undefined) {
|
|
591
|
+
this.#registerUncaughtExceptionHandlers();
|
|
592
|
+
}
|
|
593
|
+
|
|
595
594
|
// Bind messages to the room
|
|
596
595
|
if (this.messages !== undefined) {
|
|
597
596
|
|
|
@@ -2487,7 +2486,8 @@ export class Room<T extends RoomOptions = RoomOptions> {
|
|
|
2487
2486
|
this.onCreate = wrapTryCatch(this.onCreate.bind(this), onUncaughtException, OnCreateException, 'onCreate', true);
|
|
2488
2487
|
}
|
|
2489
2488
|
|
|
2490
|
-
|
|
2489
|
+
// _onJoin skips onAuth only while it's still Room's default
|
|
2490
|
+
if (this.onAuth !== Room.prototype.onAuth) {
|
|
2491
2491
|
this.onAuth = wrapTryCatch(this.onAuth.bind(this), onUncaughtException, OnAuthException, 'onAuth', true);
|
|
2492
2492
|
}
|
|
2493
2493
|
|
package/src/RoomPlugin.ts
CHANGED
|
@@ -435,31 +435,52 @@ export function installPluginHookWrappers(
|
|
|
435
435
|
for (const hook of PLUGIN_LIFECYCLE_KEYS) {
|
|
436
436
|
const { before, after } = layout.hooks[hook];
|
|
437
437
|
if (before.length === 0 && after.length === 0) { continue; }
|
|
438
|
+
proto[hook] = composePluginHook(hook, before, after, proto[hook]);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
438
441
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
const p = lookup(k);
|
|
450
|
-
await (p[hook] as Function).call(p, ...args);
|
|
451
|
-
}
|
|
452
|
-
let result: unknown;
|
|
453
|
-
if (original) { result = await original.apply(this, args); }
|
|
454
|
-
for (const k of after) {
|
|
455
|
-
const p = lookup(k);
|
|
456
|
-
await (p[hook] as Function).call(p, ...args);
|
|
457
|
-
}
|
|
458
|
-
return result;
|
|
459
|
-
};
|
|
442
|
+
/**
|
|
443
|
+
* Hooks declared as class fields (`onJoin = () => {}`) are own properties
|
|
444
|
+
* that shadow the prototype wrapper, so wrap them per instance.
|
|
445
|
+
*/
|
|
446
|
+
function wrapOwnHooks(room: RoomPluginHost, layout: PluginLayout): void {
|
|
447
|
+
for (const hook of PLUGIN_LIFECYCLE_KEYS) {
|
|
448
|
+
const { before, after } = layout.hooks[hook];
|
|
449
|
+
if (before.length === 0 && after.length === 0) { continue; }
|
|
450
|
+
if (!Object.hasOwn(room, hook)) { continue; }
|
|
451
|
+
(room as any)[hook] = composePluginHook(hook, before, after, (room as any)[hook]);
|
|
460
452
|
}
|
|
461
453
|
}
|
|
462
454
|
|
|
455
|
+
/** Run the `before` plugins, then `original`, then the `after` plugins. */
|
|
456
|
+
function composePluginHook(
|
|
457
|
+
hook: PluginLifecycleKey,
|
|
458
|
+
before: string[],
|
|
459
|
+
after: string[],
|
|
460
|
+
original: Function | undefined,
|
|
461
|
+
) {
|
|
462
|
+
return async function (
|
|
463
|
+
this: { plugins?: Record<string, RoomPlugin>; _autoPlugins?: Record<string, RoomPlugin> },
|
|
464
|
+
...args: any[]
|
|
465
|
+
) {
|
|
466
|
+
// User plugins live on `this.plugins`, auto-deps (sentinel-keyed)
|
|
467
|
+
// on `this._autoPlugins` — kept separate so user types stay clean.
|
|
468
|
+
const lookup = (k: string): RoomPlugin =>
|
|
469
|
+
(k.startsWith(DEP_KEY_PREFIX) ? this._autoPlugins![k] : this.plugins![k]);
|
|
470
|
+
for (const k of before) {
|
|
471
|
+
const p = lookup(k);
|
|
472
|
+
await (p[hook] as Function).call(p, ...args);
|
|
473
|
+
}
|
|
474
|
+
let result: unknown;
|
|
475
|
+
if (original) { result = await original.apply(this, args); }
|
|
476
|
+
for (const k of after) {
|
|
477
|
+
const p = lookup(k);
|
|
478
|
+
await (p[hook] as Function).call(p, ...args);
|
|
479
|
+
}
|
|
480
|
+
return result;
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
463
484
|
/** Structural shape used by `setupRoomPlugins` — avoids a Room import
|
|
464
485
|
* so the dependency graph stays one-way (Room → RoomPlugin). */
|
|
465
486
|
interface RoomPluginHost {
|
|
@@ -483,6 +504,10 @@ export function setupRoomPlugins(room: RoomPluginHost): void {
|
|
|
483
504
|
|
|
484
505
|
attachRoomReference(room, plugins);
|
|
485
506
|
|
|
507
|
+
if (layout) {
|
|
508
|
+
wrapOwnHooks(room, layout);
|
|
509
|
+
}
|
|
510
|
+
|
|
486
511
|
if (layout && layout.autoDeps.length > 0) {
|
|
487
512
|
room._autoPlugins = instantiateAutoDeps(room, layout);
|
|
488
513
|
}
|
package/src/Transport.ts
CHANGED
|
@@ -252,7 +252,18 @@ export interface Client<T extends { userData?: any, auth?: any, messages?: Recor
|
|
|
252
252
|
reconnectionToken: string;
|
|
253
253
|
|
|
254
254
|
// TODO: move these to ClientPrivate
|
|
255
|
+
/**
|
|
256
|
+
* Send raw bytes over the transport's reliable channel.
|
|
257
|
+
*
|
|
258
|
+
* The transport owns `data` from this call on: it may write it later (a
|
|
259
|
+
* socket with backpressure, permessage-deflate), so callers never mutate it
|
|
260
|
+
* afterwards. The same `data` may be handed to several clients.
|
|
261
|
+
*/
|
|
255
262
|
raw(data: Uint8Array | Buffer, options?: ISendOptions, cb?: (err?: Error) => void): void;
|
|
263
|
+
/**
|
|
264
|
+
* {@link raw}, or held until the client finishes joining (or until after the
|
|
265
|
+
* next patch, with `afterNextPatch`). Owns `data` the same way {@link raw} does.
|
|
266
|
+
*/
|
|
256
267
|
enqueueRaw(data: Uint8Array | Buffer, options?: ISendOptions): void;
|
|
257
268
|
|
|
258
269
|
/**
|
|
@@ -262,6 +273,10 @@ export interface Client<T extends { userData?: any, auth?: any, messages?: Recor
|
|
|
262
273
|
* Absent on transports with no datagram channel (every WebSocket transport).
|
|
263
274
|
* Its presence IS the capability check — callers feature-detect rather than
|
|
264
275
|
* reading a separate flag, and skip clients that can't receive.
|
|
276
|
+
*
|
|
277
|
+
* Unlike {@link raw}, `data` is only valid during this call: the caller
|
|
278
|
+
* reuses its bytes afterwards, so a transport that sends it later copies it
|
|
279
|
+
* first.
|
|
265
280
|
*/
|
|
266
281
|
rawUnreliable?(data: Uint8Array | Buffer, options?: ISendOptions, cb?: (err?: Error) => void): void;
|
|
267
282
|
|
|
@@ -81,6 +81,18 @@ function sendClientFrame(client: Client, encodedBody: Uint8Array, timing?: Patch
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Detach an untimed patch from the encoder's shared buffer: the transport may
|
|
86
|
+
* still hold it when the next encode reuses that buffer (see {@link Client.raw}).
|
|
87
|
+
* Timed frames skip this — {@link buildTimedFrame} already copies per client.
|
|
88
|
+
*/
|
|
89
|
+
function ownedPatch(encoded: Uint8Array, timing?: PatchTimingContext): Uint8Array {
|
|
90
|
+
return timing ? encoded : Buffer.from(encoded);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Empty quiet-tick patch body; sent only timed, so sharing it is safe. */
|
|
94
|
+
const HEARTBEAT_BODY = new Uint8Array([Protocol.ROOM_STATE_PATCH]);
|
|
95
|
+
|
|
84
96
|
/**
|
|
85
97
|
* Size of the {@link ProtocolModifier.UNRELIABLE} header that follows the
|
|
86
98
|
* protocol byte: `[uint16 seq]`. See {@link ProtocolModifier.UNRELIABLE} for
|
|
@@ -126,10 +138,7 @@ export class SchemaSerializer<T extends Schema> implements Serializer<T> {
|
|
|
126
138
|
// flag to avoid re-encoding full state if no changes were made
|
|
127
139
|
protected needFullEncode: boolean = true;
|
|
128
140
|
|
|
129
|
-
// TODO: make this optional. allocating a new buffer for each room may not be always necessary.
|
|
130
|
-
protected fullEncodeBuffer: Uint8Array = new Uint8Array(Encoder.BUFFER_SIZE);
|
|
131
141
|
protected fullEncodeCache: Uint8Array;
|
|
132
|
-
protected sharedOffsetCache: Iterator = { offset: 0 };
|
|
133
142
|
|
|
134
143
|
protected encodedViews: Map<StateView | typeof SHARED_VIEW, Uint8Array>;
|
|
135
144
|
|
|
@@ -150,8 +159,7 @@ export class SchemaSerializer<T extends Schema> implements Serializer<T> {
|
|
|
150
159
|
this.hasFilters = this.encoder.context.hasFilters;
|
|
151
160
|
this.hasUnreliableFields = stateHasUnreliableFields(this.encoder.context);
|
|
152
161
|
|
|
153
|
-
//
|
|
154
|
-
this.fullEncodeBuffer[0] = Protocol.ROOM_STATE;
|
|
162
|
+
this.needFullEncode = true; // the cached full state belongs to the previous encoder
|
|
155
163
|
|
|
156
164
|
if (this.hasFilters) {
|
|
157
165
|
this.encodedViews = new Map();
|
|
@@ -166,19 +174,17 @@ export class SchemaSerializer<T extends Schema> implements Serializer<T> {
|
|
|
166
174
|
|
|
167
175
|
public getFullState(client?: Client, timing?: PatchTimingContext) {
|
|
168
176
|
if (this.needFullEncode || this.encoder.root.changes.next !== undefined) {
|
|
169
|
-
this.
|
|
170
|
-
|
|
177
|
+
this.encoder.sharedBuffer[0] = Protocol.ROOM_STATE;
|
|
178
|
+
// copy: the next encode reuses the shared buffer (slice(): a pooled Buffer would pin 8KB)
|
|
179
|
+
this.fullEncodeCache = this.encoder.encodeAll({ offset: 1 }).slice();
|
|
171
180
|
this.needFullEncode = false;
|
|
172
181
|
}
|
|
173
182
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
this.fullEncodeBuffer
|
|
180
|
-
)
|
|
181
|
-
: this.fullEncodeCache;
|
|
183
|
+
let body = this.fullEncodeCache;
|
|
184
|
+
if (this.hasFilters && client?.view) {
|
|
185
|
+
this.encoder.sharedBuffer.set(body); // patches since the full encode may have overwritten it
|
|
186
|
+
body = this.encoder.encodeAllView(client.view, body.byteLength, { offset: body.byteLength });
|
|
187
|
+
}
|
|
182
188
|
|
|
183
189
|
if (timing && client) {
|
|
184
190
|
return buildTimedFrame(body, timing.sNow, clientProcessedInputSeq(client));
|
|
@@ -232,11 +238,10 @@ export class SchemaSerializer<T extends Schema> implements Serializer<T> {
|
|
|
232
238
|
// body — 1 byte protocol + 8 bytes prefix.
|
|
233
239
|
// No-op for rooms without `defineInput()` (timing is undefined).
|
|
234
240
|
if (timing) {
|
|
235
|
-
const heartbeatBody = Buffer.from([Protocol.ROOM_STATE_PATCH]);
|
|
236
241
|
for (let i = 0; i < clients.length; i++) {
|
|
237
242
|
const client = clients[i];
|
|
238
243
|
if (client.state !== ClientState.JOINED) continue;
|
|
239
|
-
sendClientFrame(client,
|
|
244
|
+
sendClientFrame(client, HEARTBEAT_BODY, timing);
|
|
240
245
|
}
|
|
241
246
|
}
|
|
242
247
|
|
|
@@ -262,6 +267,8 @@ export class SchemaSerializer<T extends Schema> implements Serializer<T> {
|
|
|
262
267
|
const encodedChanges = this.encoder.encode(it);
|
|
263
268
|
|
|
264
269
|
if (!this.hasFilters) {
|
|
270
|
+
const frame = ownedPatch(encodedChanges, timing);
|
|
271
|
+
|
|
265
272
|
while (numClients--) {
|
|
266
273
|
const client = clients[numClients];
|
|
267
274
|
|
|
@@ -272,7 +279,7 @@ export class SchemaSerializer<T extends Schema> implements Serializer<T> {
|
|
|
272
279
|
continue;
|
|
273
280
|
}
|
|
274
281
|
|
|
275
|
-
sendClientFrame(client,
|
|
282
|
+
sendClientFrame(client, frame, timing);
|
|
276
283
|
}
|
|
277
284
|
|
|
278
285
|
} else {
|
|
@@ -297,7 +304,7 @@ export class SchemaSerializer<T extends Schema> implements Serializer<T> {
|
|
|
297
304
|
// allow to pass the same encoded view for multiple clients
|
|
298
305
|
if (encodedView === undefined) {
|
|
299
306
|
encodedView = (view === SHARED_VIEW)
|
|
300
|
-
? encodedChanges
|
|
307
|
+
? ownedPatch(encodedChanges, timing) // encodeView already returns a copy
|
|
301
308
|
: this.encoder.encodeView(client.view, sharedOffset, it);
|
|
302
309
|
this.encodedViews.set(view, encodedView);
|
|
303
310
|
}
|