@colyseus/core 0.18.24 → 0.18.26
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 +9 -3
- package/build/Room.cjs.map +2 -2
- package/build/Room.d.ts +33 -0
- package/build/Room.mjs +9 -3
- package/build/Room.mjs.map +2 -2
- package/build/RoomMessages.cjs +31 -2
- package/build/RoomMessages.cjs.map +2 -2
- package/build/RoomMessages.d.ts +2 -2
- package/build/RoomMessages.mjs +31 -2
- package/build/RoomMessages.mjs.map +2 -2
- package/build/index.cjs +2 -0
- package/build/index.cjs.map +2 -2
- package/build/index.d.ts +1 -1
- package/build/index.mjs +2 -1
- package/build/index.mjs.map +2 -2
- package/build/input/InputBuffer.cjs +52 -1
- package/build/input/InputBuffer.cjs.map +3 -3
- package/build/input/InputBuffer.d.ts +13 -0
- package/build/input/InputBuffer.mjs +51 -1
- package/build/input/InputBuffer.mjs.map +3 -3
- package/build/input/RoomInput.cjs +3 -2
- package/build/input/RoomInput.cjs.map +2 -2
- package/build/input/RoomInput.d.ts +1 -1
- package/build/input/RoomInput.mjs +4 -2
- package/build/input/RoomInput.mjs.map +2 -2
- package/build/input/types.cjs.map +1 -1
- package/build/input/types.d.ts +6 -1
- package/package.json +3 -3
- package/src/Room.ts +45 -5
- package/src/RoomMessages.ts +41 -4
- package/src/index.ts +1 -1
- package/src/input/InputBuffer.ts +58 -1
- package/src/input/RoomInput.ts +6 -5
- package/src/input/types.ts +7 -1
package/src/RoomMessages.ts
CHANGED
|
@@ -7,8 +7,9 @@ import { createNanoEvents } from './utils/nanoevents.ts';
|
|
|
7
7
|
import { standardValidate, type StandardSchemaV1 } from './utils/StandardSchema.ts';
|
|
8
8
|
import { isDevMode } from './utils/DevMode.ts';
|
|
9
9
|
import { debugMessage, debugAndPrintError } from './Debug.ts';
|
|
10
|
+
import { logger } from './Logger.ts';
|
|
10
11
|
import { OnMessageException } from './errors/RoomExceptions.ts';
|
|
11
|
-
import type
|
|
12
|
+
import { ClientState, type Client, type ClientPrivate } from './Transport.ts';
|
|
12
13
|
import type { Room } from './Room.ts';
|
|
13
14
|
|
|
14
15
|
/** Normalize a thrown value into the `{ name, message, code? }` shape echoed in a
|
|
@@ -74,7 +75,7 @@ const SEND_CONTEXT: MessageContext = Object.freeze({
|
|
|
74
75
|
* ROOM_DATA_BYTES). Room's `_onMessage` keeps the protocol/lifecycle frames
|
|
75
76
|
* (JOIN/PING/LEAVE/input) and calls one of these per frame, preserving the
|
|
76
77
|
* original dispatch order. Reads back into its Room only for `onUncaughtException`
|
|
77
|
-
* (handler wrapping) and `roomId`/`roomName` (logging).
|
|
78
|
+
* (handler wrapping), `onUnhandledMessage` and `roomId`/`roomName` (logging).
|
|
78
79
|
*
|
|
79
80
|
* @internal
|
|
80
81
|
*/
|
|
@@ -263,6 +264,10 @@ export class RoomMessages {
|
|
|
263
264
|
const handler = this.events.events[messageType as string]?.[0];
|
|
264
265
|
|
|
265
266
|
if (handler === undefined) {
|
|
267
|
+
// no warning without the hook: the client already gets an explicit error
|
|
268
|
+
if (this.room.onUnhandledMessage !== undefined) {
|
|
269
|
+
this.#reportUnhandled(client, messageType, message);
|
|
270
|
+
}
|
|
266
271
|
this.#replyToRequest(client, requestId, ResponseStatus.ERROR, {
|
|
267
272
|
name: "no_handler",
|
|
268
273
|
message: `room "${this.room.roomName}" has no onMessage("${messageType}") handler to answer this request.`,
|
|
@@ -351,13 +356,45 @@ export class RoomMessages {
|
|
|
351
356
|
}
|
|
352
357
|
|
|
353
358
|
/** No handler for the type: error the client in dev, drop it in production. */
|
|
354
|
-
#noHandler(client: Client, messageType: string | number,
|
|
359
|
+
#noHandler(client: Client, messageType: string | number, message: unknown): void {
|
|
355
360
|
const errorMessage = `room onMessage for "${messageType}" not registered.`;
|
|
356
|
-
|
|
361
|
+
|
|
362
|
+
if (this.room.onUnhandledMessage !== undefined) {
|
|
363
|
+
this.#reportUnhandled(client, messageType, message);
|
|
364
|
+
|
|
365
|
+
} else {
|
|
366
|
+
logger.warn(`room "${this.room.roomName}" (roomId: ${this.room.roomId}): client '${client.sessionId}' sent "${messageType}", which has no onMessage() handler. ${(isDevMode)
|
|
367
|
+
? "Replied with an INVALID_PAYLOAD error (outside devMode, the client is disconnected)."
|
|
368
|
+
: "Disconnecting the client."}`);
|
|
369
|
+
}
|
|
370
|
+
|
|
357
371
|
if (isDevMode) {
|
|
358
372
|
client.error(ErrorCode.INVALID_PAYLOAD, errorMessage);
|
|
359
373
|
} else {
|
|
374
|
+
// frames already received still arrive after leave(); Room#_onMessage skips them
|
|
375
|
+
client.state = ClientState.LEAVING;
|
|
360
376
|
client.leave(CloseCode.WITH_ERROR, errorMessage);
|
|
361
377
|
}
|
|
362
378
|
}
|
|
379
|
+
|
|
380
|
+
/** Call `room.onUnhandledMessage`. A throw or rejection goes to
|
|
381
|
+
* `onUncaughtException` when set, or is logged; it never stops the default. */
|
|
382
|
+
#reportUnhandled(client: Client, messageType: string | number, message: unknown): void {
|
|
383
|
+
const report = (e: any) => {
|
|
384
|
+
if (this.room.onUncaughtException !== undefined) {
|
|
385
|
+
this.room.onUncaughtException(new OnMessageException<Room<any>, any>(e, e?.message, client, message, messageType), 'onMessage');
|
|
386
|
+
} else {
|
|
387
|
+
debugAndPrintError(e);
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
try {
|
|
392
|
+
const result = this.room.onUnhandledMessage!(client, messageType, message) as any;
|
|
393
|
+
if (typeof result?.catch === 'function') {
|
|
394
|
+
result.catch(report);
|
|
395
|
+
}
|
|
396
|
+
} catch (e: any) {
|
|
397
|
+
report(e);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
363
400
|
}
|
package/src/index.ts
CHANGED
|
@@ -20,7 +20,7 @@ export {
|
|
|
20
20
|
// Core classes
|
|
21
21
|
export { Server, defineRoom, defineServer, registerRoomDefinitions, unregisterRoomDefinitions, applySimulatedLatency, parseLatencyEnv, type RoomDefinitions, type ServerOptions, type SDKTypes } from './Server.ts';
|
|
22
22
|
export { Room, RoomInternalState, validate, type RoomOptions, type DefineInputOptions, type SimulationCallback, type FixedTimestepCallback, type StepContext, type MessageHandlerWithFormat, type Messages, type ExtractRoomState, type ExtractRoomMetadata, type ExtractRoomClient } from './Room.ts';
|
|
23
|
-
export { InputBufferImpl, compileSanitizer } from './input/InputBuffer.ts';
|
|
23
|
+
export { InputBufferImpl, compileSanitizer, compileSnapshot } from './input/InputBuffer.ts';
|
|
24
24
|
export { type InputAccessor, type InputAPI, type NormalizedInputOptions, type ConsumeOptions, type IdleInput, type IdleContext, type SanitizeInput, type NumericFieldsOf } from './input/types.ts';
|
|
25
25
|
export { Rewind, RewindView, type RewindOptions, type RewindMode } from './Rewind.ts';
|
|
26
26
|
export {
|
package/src/input/InputBuffer.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { $values } from '@colyseus/schema';
|
|
2
|
+
|
|
1
3
|
import type { ClientPrivate } from '../Transport.ts';
|
|
2
4
|
import { $METADATA } from '../utils/Utils.ts';
|
|
3
5
|
|
|
@@ -43,7 +45,9 @@ export function compileSanitizer<I>(spec: SanitizeInput<I>): (input: I) => void
|
|
|
43
45
|
// never assigned; the range floor turned "no input" into "held stick").
|
|
44
46
|
if (v === undefined) { continue; }
|
|
45
47
|
// NaN-safe: NaN fails both comparisons → clamp floor.
|
|
46
|
-
|
|
48
|
+
const clamped = v >= mins[i] ? (v <= maxs[i] ? v : maxs[i]) : mins[i];
|
|
49
|
+
// Honest frames clamp nothing: a keyed write through the setter costs more than the walk.
|
|
50
|
+
if (clamped !== v) { inst[names[i]] = clamped; }
|
|
47
51
|
}
|
|
48
52
|
};
|
|
49
53
|
}
|
|
@@ -63,6 +67,59 @@ function fieldNamesOf(ctor: new () => any): string[] {
|
|
|
63
67
|
return names;
|
|
64
68
|
}
|
|
65
69
|
|
|
70
|
+
/** One per input schema, so every room's frames share a hidden class. */
|
|
71
|
+
const _snapshots = new WeakMap<new () => any, (input: any) => any>();
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @internal Compile how a decoded input is copied into the buffer. `clone()`
|
|
75
|
+
* builds a Schema and its ChangeTree per input, for a frame that's never
|
|
76
|
+
* encoded, so a schema of plain values gets a lighter frame: its own array of
|
|
77
|
+
* values behind the ctor's prototype. Methods, getters, `instanceof`, `clone()`
|
|
78
|
+
* and `toJSON()` all hold, and a field write lands on the copy, untracked. A
|
|
79
|
+
* nested schema or collection keeps `clone()`, whose copy is deep.
|
|
80
|
+
*
|
|
81
|
+
* Fields are read by name, as `clone()` reads them, never by copying `$values`:
|
|
82
|
+
* native class fields (`useDefineForClassFields`) shadow the schema's accessors,
|
|
83
|
+
* so the decoder's values sit on the instance and `$values` stays empty.
|
|
84
|
+
*/
|
|
85
|
+
export function compileSnapshot<I>(ctor: new () => I): (input: I) => I {
|
|
86
|
+
let snapshot = _snapshots.get(ctor);
|
|
87
|
+
if (snapshot !== undefined) { return snapshot; }
|
|
88
|
+
|
|
89
|
+
const md = (ctor as any)[$METADATA] as Record<number, { type?: unknown }> | undefined;
|
|
90
|
+
const names = fieldNamesOf(ctor);
|
|
91
|
+
const plain = md !== undefined && names.every((_, i) => {
|
|
92
|
+
const type = md[i].type;
|
|
93
|
+
return typeof type === "string" || (type !== null && typeof type === "object" && "quantized" in type);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (plain) {
|
|
97
|
+
const fields: PropertyDescriptorMap = {};
|
|
98
|
+
names.forEach((name, i) => {
|
|
99
|
+
fields[name] = {
|
|
100
|
+
get(this: any) { return this[$values][i]; },
|
|
101
|
+
set(this: any, value: unknown) { this[$values][i] = value; },
|
|
102
|
+
enumerable: true,
|
|
103
|
+
configurable: true,
|
|
104
|
+
};
|
|
105
|
+
});
|
|
106
|
+
// A function, not a class: `constructor` must stay the schema's (clone/toJSON read its metadata).
|
|
107
|
+
function Frame(this: any, values: unknown[]) { this[$values] = values; }
|
|
108
|
+
Frame.prototype = Object.create(ctor.prototype, fields);
|
|
109
|
+
const count = names.length;
|
|
110
|
+
snapshot = (input) => {
|
|
111
|
+
const values = new Array(count);
|
|
112
|
+
for (let i = 0; i < count; i++) { values[i] = input[names[i]]; }
|
|
113
|
+
return new (Frame as any)(values);
|
|
114
|
+
};
|
|
115
|
+
} else {
|
|
116
|
+
snapshot = (input) => input.clone();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
_snapshots.set(ctor, snapshot!);
|
|
120
|
+
return snapshot!;
|
|
121
|
+
}
|
|
122
|
+
|
|
66
123
|
/**
|
|
67
124
|
* @internal Seed every declared field that has no construction default with
|
|
68
125
|
* its wire-neutral zero value (0 / false / ""; quantized: 0 clamped into the
|
package/src/input/RoomInput.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { HandshakeSection, InputFlags, ProtocolModifier } from '@colyseus/shared
|
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
6
|
InputAccessorImpl, InputBufferImpl, NO_OP_INPUT_ACCESSOR,
|
|
7
|
-
compileSanitizer, seedInputZeroValues, validateSubSteps,
|
|
7
|
+
compileSanitizer, compileSnapshot, seedInputZeroValues, validateSubSteps,
|
|
8
8
|
} from './InputBuffer.ts';
|
|
9
9
|
import type {
|
|
10
10
|
InputAccessor, InputAPI, NormalizedInputOptions,
|
|
@@ -148,6 +148,7 @@ export class RoomInput {
|
|
|
148
148
|
idle: opts?.idle,
|
|
149
149
|
// Compiled once (map → dense min/max walk); applied per decoded frame.
|
|
150
150
|
sanitize: opts?.sanitize !== undefined ? compileSanitizer(opts.sanitize) : undefined,
|
|
151
|
+
snapshot: compileSnapshot(type),
|
|
151
152
|
tickRate,
|
|
152
153
|
subSteps: validateSubSteps(opts?.subSteps, 'defineInput'),
|
|
153
154
|
};
|
|
@@ -220,15 +221,15 @@ export class RoomInput {
|
|
|
220
221
|
|
|
221
222
|
/**
|
|
222
223
|
* Sanitize the freshly-decoded `client._input`, then (when buffering is on)
|
|
223
|
-
* push a
|
|
224
|
+
* push a snapshot into the per-client buffer. Honors the framework seq
|
|
224
225
|
* (unreliable) / a user `inputOptions.seqField` (reliable) to dedupe the
|
|
225
226
|
* redundancy ring.
|
|
226
227
|
*/
|
|
227
228
|
capture(client: ClientPrivate, renderTime: number = 0, reckonTime: number = 0, seq?: number): void {
|
|
228
|
-
// Sanitize before anything reads it (latest, the
|
|
229
|
+
// Sanitize before anything reads it (latest, the snapshot below, the idle ctx).
|
|
229
230
|
this.options.sanitize?.(client._input);
|
|
230
231
|
const buf = client._inputBuffer;
|
|
231
|
-
if (!buf) { return; } // no consumer registered — skip the
|
|
232
|
+
if (!buf) { return; } // no consumer registered — skip the snapshot allocation
|
|
232
233
|
const inst = client._input!;
|
|
233
234
|
if (seq !== undefined) {
|
|
234
235
|
// Unreliable: dedup the ring by the framework wire seq (user seqField is for .at() only).
|
|
@@ -241,7 +242,7 @@ export class RoomInput {
|
|
|
241
242
|
if (typeof value === 'number' && !buf.accept(value)) { return; }
|
|
242
243
|
}
|
|
243
244
|
}
|
|
244
|
-
buf.push(
|
|
245
|
+
buf.push(this.options.snapshot(inst), renderTime, reckonTime, seq);
|
|
245
246
|
}
|
|
246
247
|
|
|
247
248
|
/** Decode a `ROOM_INPUT_RELIABLE` frame: an optional TIMED stamp prefix (shape
|
package/src/input/types.ts
CHANGED
|
@@ -193,7 +193,7 @@ export interface NormalizedInputOptions {
|
|
|
193
193
|
seqField?: string;
|
|
194
194
|
|
|
195
195
|
/**
|
|
196
|
-
* > 0 enables per-client buffering of
|
|
196
|
+
* > 0 enables per-client buffering of input snapshots — required for
|
|
197
197
|
* `room.inputs.get(sessionId).drain() / .peek() / .at()` to return populated
|
|
198
198
|
* data. Oldest drops on overflow. Set to `0` to disable (`.latest` still
|
|
199
199
|
* works).
|
|
@@ -229,6 +229,12 @@ export interface NormalizedInputOptions {
|
|
|
229
229
|
* `latest` / the buffer / the idle ctx.
|
|
230
230
|
*/
|
|
231
231
|
sanitize?: (instance: any) => void;
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* COMPILED copy of a decoded frame for the buffer (see `compileSnapshot`).
|
|
235
|
+
* Reads and writes like the schema, without its change tracking.
|
|
236
|
+
*/
|
|
237
|
+
snapshot: (instance: any) => any;
|
|
232
238
|
}
|
|
233
239
|
|
|
234
240
|
/**
|