@colyseus/core 0.18.1 → 0.18.3
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.map +1 -1
- package/build/Room.mjs.map +1 -1
- package/build/Transport.cjs.map +2 -2
- package/build/Transport.d.ts +7 -6
- package/build/Transport.mjs.map +2 -2
- package/build/errors/RoomExceptions.cjs.map +1 -1
- package/build/errors/RoomExceptions.d.ts +14 -14
- package/build/errors/RoomExceptions.mjs.map +1 -1
- package/build/input/InputBuffer.cjs +26 -0
- package/build/input/InputBuffer.cjs.map +2 -2
- package/build/input/InputBuffer.d.ts +10 -0
- package/build/input/InputBuffer.mjs +25 -0
- package/build/input/InputBuffer.mjs.map +2 -2
- package/build/input/RoomInput.cjs +1 -0
- package/build/input/RoomInput.cjs.map +2 -2
- package/build/input/RoomInput.mjs +2 -0
- package/build/input/RoomInput.mjs.map +2 -2
- package/build/presence/LocalPresence.cjs.map +2 -2
- package/build/presence/LocalPresence.d.ts +1 -1
- package/build/presence/LocalPresence.mjs.map +2 -2
- package/build/router/index.cjs +13 -6
- package/build/router/index.cjs.map +2 -2
- package/build/router/index.d.ts +1 -1
- package/build/router/index.mjs +13 -6
- package/build/router/index.mjs.map +2 -2
- package/build/serializer/SchemaSerializer.cjs.map +1 -1
- package/build/serializer/SchemaSerializer.mjs.map +1 -1
- package/build/serializer/Serializer.cjs.map +1 -1
- package/build/serializer/Serializer.d.ts +4 -4
- package/package.json +7 -4
- package/src/Room.ts +1 -1
- package/src/Transport.ts +7 -6
- package/src/errors/RoomExceptions.ts +18 -18
- package/src/input/InputBuffer.ts +32 -0
- package/src/input/RoomInput.ts +6 -1
- package/src/presence/LocalPresence.ts +1 -1
- package/src/router/index.ts +35 -14
- package/src/serializer/SchemaSerializer.ts +4 -4
- package/src/serializer/Serializer.ts +4 -4
|
@@ -27,11 +27,11 @@ export type RoomException<R extends Room = Room> =
|
|
|
27
27
|
TimedEventException;
|
|
28
28
|
|
|
29
29
|
export class OnCreateException<R extends Room = Room> extends Error {
|
|
30
|
-
options: Parameters<R['onCreate']
|
|
30
|
+
options: Parameters<NonNullable<R['onCreate']>>[0];
|
|
31
31
|
constructor(
|
|
32
32
|
cause: Error,
|
|
33
33
|
message: string,
|
|
34
|
-
options: Parameters<R['onCreate']
|
|
34
|
+
options: Parameters<NonNullable<R['onCreate']>>[0],
|
|
35
35
|
) {
|
|
36
36
|
super(message, { cause });
|
|
37
37
|
this.name = 'OnCreateException';
|
|
@@ -56,15 +56,15 @@ export class OnAuthException<R extends Room = Room> extends Error {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
export class OnJoinException<R extends Room = Room> extends Error {
|
|
59
|
-
client: Parameters<R['onJoin']
|
|
60
|
-
options: Parameters<R['onJoin']
|
|
61
|
-
auth: Parameters<R['onJoin']
|
|
59
|
+
client: Parameters<NonNullable<R['onJoin']>>[0];
|
|
60
|
+
options: Parameters<NonNullable<R['onJoin']>>[1];
|
|
61
|
+
auth: Parameters<NonNullable<R['onJoin']>>[2];
|
|
62
62
|
constructor(
|
|
63
63
|
cause: Error,
|
|
64
64
|
message: string,
|
|
65
|
-
client: Parameters<R['onJoin']
|
|
66
|
-
options: Parameters<R['onJoin']
|
|
67
|
-
auth: Parameters<R['onJoin']
|
|
65
|
+
client: Parameters<NonNullable<R['onJoin']>>[0],
|
|
66
|
+
options: Parameters<NonNullable<R['onJoin']>>[1],
|
|
67
|
+
auth: Parameters<NonNullable<R['onJoin']>>[2],
|
|
68
68
|
) {
|
|
69
69
|
super(message, { cause });
|
|
70
70
|
this.name = 'OnJoinException';
|
|
@@ -75,13 +75,13 @@ export class OnJoinException<R extends Room = Room> extends Error {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
export class OnLeaveException<R extends Room = Room> extends Error {
|
|
78
|
-
client: Parameters<R['onLeave']
|
|
79
|
-
consented: Parameters<R['onLeave']
|
|
78
|
+
client: Parameters<NonNullable<R['onLeave']>>[0];
|
|
79
|
+
consented: Parameters<NonNullable<R['onLeave']>>[1];
|
|
80
80
|
constructor(
|
|
81
81
|
cause: Error,
|
|
82
82
|
message: string,
|
|
83
|
-
client: Parameters<R['onLeave']
|
|
84
|
-
consented: Parameters<R['onLeave']
|
|
83
|
+
client: Parameters<NonNullable<R['onLeave']>>[0],
|
|
84
|
+
consented: Parameters<NonNullable<R['onLeave']>>[1],
|
|
85
85
|
) {
|
|
86
86
|
super(message, { cause });
|
|
87
87
|
this.name = 'OnLeaveException';
|
|
@@ -91,13 +91,13 @@ export class OnLeaveException<R extends Room = Room> extends Error {
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
export class OnDropException<R extends Room = Room> extends Error {
|
|
94
|
-
client: Parameters<R['onDrop']
|
|
95
|
-
code: Parameters<R['onDrop']
|
|
94
|
+
client: Parameters<NonNullable<R['onDrop']>>[0];
|
|
95
|
+
code: Parameters<NonNullable<R['onDrop']>>[1];
|
|
96
96
|
constructor(
|
|
97
97
|
cause: Error,
|
|
98
98
|
message: string,
|
|
99
|
-
client: Parameters<R['onDrop']
|
|
100
|
-
code: Parameters<R['onDrop']
|
|
99
|
+
client: Parameters<NonNullable<R['onDrop']>>[0],
|
|
100
|
+
code: Parameters<NonNullable<R['onDrop']>>[1],
|
|
101
101
|
) {
|
|
102
102
|
super(message, { cause });
|
|
103
103
|
this.name = 'OnDropException';
|
|
@@ -107,11 +107,11 @@ export class OnDropException<R extends Room = Room> extends Error {
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
export class OnReconnectException<R extends Room = Room> extends Error {
|
|
110
|
-
client: Parameters<R['onReconnect']
|
|
110
|
+
client: Parameters<NonNullable<R['onReconnect']>>[0];
|
|
111
111
|
constructor(
|
|
112
112
|
cause: Error,
|
|
113
113
|
message: string,
|
|
114
|
-
client: Parameters<R['onReconnect']
|
|
114
|
+
client: Parameters<NonNullable<R['onReconnect']>>[0],
|
|
115
115
|
) {
|
|
116
116
|
super(message, { cause });
|
|
117
117
|
this.name = 'OnReconnectException';
|
package/src/input/InputBuffer.ts
CHANGED
|
@@ -38,6 +38,10 @@ export function compileSanitizer<I>(spec: SanitizeInput<I>): (input: I) => void
|
|
|
38
38
|
const inst = input as Record<string, number>;
|
|
39
39
|
for (let i = 0; i < names.length; i++) {
|
|
40
40
|
const v = inst[names[i]];
|
|
41
|
+
// Never-received field: no input to sanitize — clamping would INVENT
|
|
42
|
+
// one (delta-encoding clients legitimately never transmit fields they
|
|
43
|
+
// never assigned; the range floor turned "no input" into "held stick").
|
|
44
|
+
if (v === undefined) { continue; }
|
|
41
45
|
// NaN-safe: NaN fails both comparisons → clamp floor.
|
|
42
46
|
inst[names[i]] = v >= mins[i] ? (v <= maxs[i] ? v : maxs[i]) : mins[i];
|
|
43
47
|
}
|
|
@@ -59,6 +63,33 @@ function fieldNamesOf(ctor: new () => any): string[] {
|
|
|
59
63
|
return names;
|
|
60
64
|
}
|
|
61
65
|
|
|
66
|
+
/**
|
|
67
|
+
* @internal Seed every declared field that has no construction default with
|
|
68
|
+
* its wire-neutral zero value (0 / false / ""; quantized: 0 clamped into the
|
|
69
|
+
* descriptor's range). The input contract is "the server holds the last
|
|
70
|
+
* received value" — before anything is received, that value is the ZERO of
|
|
71
|
+
* the field's type, never `undefined`: a sim would NaN-propagate it and a
|
|
72
|
+
* range sanitizer would clamp it into a phantom input. Clients only transmit
|
|
73
|
+
* fields they assign, so an untouched field stays absent on the wire forever.
|
|
74
|
+
*/
|
|
75
|
+
export function seedInputZeroValues(instance: any, ctor: new () => any): void {
|
|
76
|
+
const md = (ctor as any)[$METADATA] as
|
|
77
|
+
Record<number, { name?: string, type?: unknown }> | undefined;
|
|
78
|
+
if (!md) { return; }
|
|
79
|
+
for (let i = 0; ; i++) {
|
|
80
|
+
const f = md[i];
|
|
81
|
+
if (!f || typeof f.name !== "string") break;
|
|
82
|
+
if (instance[f.name] !== undefined) { continue; } // .default() wins
|
|
83
|
+
const type = f.type;
|
|
84
|
+
if (typeof type === "string") {
|
|
85
|
+
instance[f.name] = type === "string" ? "" : type === "boolean" ? false : 0;
|
|
86
|
+
} else if (type !== null && typeof type === "object" && "quantized" in (type as object)) {
|
|
87
|
+
const q = (type as { quantized: { min: number, max: number } }).quantized;
|
|
88
|
+
instance[f.name] = q.min > 0 ? q.min : q.max < 0 ? q.max : 0;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
62
93
|
/** Reclaim the consumed prefix once the read cursor passes this many items —
|
|
63
94
|
* bounds the parallel arrays' slack to O(THRESHOLD) between compactions while
|
|
64
95
|
* keeping per-consume work O(1) (no `shift()` re-index on every input). */
|
|
@@ -184,6 +215,7 @@ export class InputBufferImpl<I = any> {
|
|
|
184
215
|
if (this._idle === undefined) {
|
|
185
216
|
// Lazy mint (cold): the fresh instance doubles as the defaults source.
|
|
186
217
|
this._idle = new this._ctor!();
|
|
218
|
+
seedInputZeroValues(this._idle, this._ctor!);
|
|
187
219
|
this._fieldNames = fieldNamesOf(this._ctor!);
|
|
188
220
|
const fresh = this._idle as Record<string, unknown>;
|
|
189
221
|
this._defaults = this._fieldNames.map((n) => fresh[n]);
|
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, validateSubSteps,
|
|
7
|
+
compileSanitizer, seedInputZeroValues, validateSubSteps,
|
|
8
8
|
} from './InputBuffer.ts';
|
|
9
9
|
import type {
|
|
10
10
|
InputAccessor, InputAPI, NormalizedInputOptions,
|
|
@@ -152,6 +152,11 @@ export class RoomInput {
|
|
|
152
152
|
* via `bufferMaxSize > 0`, for rollback/lockstep), and the accessor. */
|
|
153
153
|
allocate(client: Client & ClientPrivate): void {
|
|
154
154
|
client._input = new this.options.ctor();
|
|
155
|
+
// Wire-neutral zero values for fields with no construction default: a
|
|
156
|
+
// client only transmits fields it assigns, and `undefined` here would
|
|
157
|
+
// NaN-propagate into the sim (or get floor-clamped by a range sanitizer
|
|
158
|
+
// into a PHANTOM held input).
|
|
159
|
+
seedInputZeroValues(client._input, this.options.ctor);
|
|
155
160
|
client._inputDecoder = new InputDecoder(client._input);
|
|
156
161
|
client._reckonBaseline = 0; // mirrors the SDK's delta-coded stamp baseline (reset together on (re)connect)
|
|
157
162
|
const maxSize = this.options.bufferMaxSize;
|
|
@@ -8,7 +8,7 @@ import { hasDevModeCache, isDevMode, getDevModeCache, writeDevModeCache } from '
|
|
|
8
8
|
type Callback = (...args: any[]) => void;
|
|
9
9
|
|
|
10
10
|
export class LocalPresence implements Presence {
|
|
11
|
-
public subscriptions = new EventEmitter();
|
|
11
|
+
public subscriptions: EventEmitter = new EventEmitter();
|
|
12
12
|
|
|
13
13
|
public data: {[roomName: string]: string[]} = {};
|
|
14
14
|
public hash: {[roomName: string]: {[key: string]: string}} = {};
|
package/src/router/index.ts
CHANGED
|
@@ -12,7 +12,9 @@ export {
|
|
|
12
12
|
createMiddleware,
|
|
13
13
|
createInternalContext,
|
|
14
14
|
|
|
15
|
-
// Re-export
|
|
15
|
+
// Re-export every type reachable from an inferred type below — consumers
|
|
16
|
+
// depend on @colyseus/core, not @colyseus/better-call, and cannot name it
|
|
17
|
+
// under pnpm's isolated node_modules (TS2742/TS2883).
|
|
16
18
|
type Router,
|
|
17
19
|
type RouterConfig,
|
|
18
20
|
type Endpoint,
|
|
@@ -20,6 +22,12 @@ export {
|
|
|
20
22
|
type EndpointOptions,
|
|
21
23
|
type EndpointContext,
|
|
22
24
|
type StrictEndpoint,
|
|
25
|
+
type StandardSchemaV1,
|
|
26
|
+
type MiddlewareOptions,
|
|
27
|
+
type MiddlewareInputContext,
|
|
28
|
+
type CookieOptions,
|
|
29
|
+
type CookiePrefixOptions,
|
|
30
|
+
type Status,
|
|
23
31
|
} from "@colyseus/better-call";
|
|
24
32
|
|
|
25
33
|
export { toNodeHandler };
|
|
@@ -41,7 +49,7 @@ export function bindRouterToTransport(transport: Transport, router: Router, useE
|
|
|
41
49
|
// add default "/" route, if not provided.
|
|
42
50
|
const hasRootRoute = (
|
|
43
51
|
// check if express app has a root route
|
|
44
|
-
(expressApp &&
|
|
52
|
+
(expressApp && hasExpressRootRoute(expressApp)) ||
|
|
45
53
|
|
|
46
54
|
// check if router has a root route
|
|
47
55
|
Object.values(router.endpoints).some(endpoint => endpoint.path === "/")
|
|
@@ -105,20 +113,33 @@ export function bindRouterToTransport(transport: Transport, router: Router, useE
|
|
|
105
113
|
});
|
|
106
114
|
}
|
|
107
115
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
116
|
+
/**
|
|
117
|
+
* Whether the express app already handles the root route, in which case
|
|
118
|
+
* Colyseus must not register its own default "/" endpoint over it.
|
|
119
|
+
*/
|
|
120
|
+
function hasExpressRootRoute(expressApp: express.Application) {
|
|
121
|
+
return expressRouterStack(expressApp).some((layer: any) =>
|
|
122
|
+
layer.match('/') && !['query', 'expressInit'].includes(layer.name));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The app's router stack, or an empty stack if it has no routes yet.
|
|
127
|
+
*
|
|
128
|
+
* express v5 exposes the router as `app.router`; v4 exposes it as `app._router`
|
|
129
|
+
* and only creates it once the first route/middleware is registered.
|
|
130
|
+
*/
|
|
131
|
+
function expressRouterStack(expressApp: express.Application): any[] {
|
|
132
|
+
const app = expressApp as any;
|
|
133
|
+
|
|
134
|
+
if (app?._router?.stack) {
|
|
135
|
+
return app._router.stack;
|
|
119
136
|
}
|
|
120
137
|
|
|
121
|
-
|
|
138
|
+
try {
|
|
139
|
+
return app?.router?.stack ?? [];
|
|
140
|
+
} catch (e) {
|
|
141
|
+
return []; // express v4 throws on `app.router` — no routes registered
|
|
142
|
+
}
|
|
122
143
|
}
|
|
123
144
|
|
|
124
145
|
export function createRouter<
|
|
@@ -174,10 +174,10 @@ export class SchemaSerializer<T extends Schema> implements Serializer<T> {
|
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
// Heartbeat:
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
177
|
+
// Heartbeat: `defineInput()` rooms still need periodic timing samples
|
|
178
|
+
// to keep client clock offset / RTT estimates fresh during quiet
|
|
179
|
+
// ticks. Emit a TIMED-prefixed ROOM_STATE_PATCH with an empty schema
|
|
180
|
+
// body — 1 byte protocol + 8 bytes prefix.
|
|
181
181
|
// No-op for rooms without `defineInput()` (timing is undefined).
|
|
182
182
|
if (timing) {
|
|
183
183
|
const heartbeatBody = Buffer.from([Protocol.ROOM_STATE_PATCH]);
|
|
@@ -2,12 +2,12 @@ import type { Client } from '../Transport.ts';
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Per-tick timing context, populated by the Room when
|
|
5
|
-
* {@link Room.defineInput} was called.
|
|
6
|
-
* {@link ProtocolModifier.TIMED} prefix
|
|
7
|
-
*
|
|
5
|
+
* {@link Room.defineInput} was called. Its presence makes the serializer emit
|
|
6
|
+
* a {@link ProtocolModifier.TIMED} prefix on every state/patch frame — the
|
|
7
|
+
* gate is per-room (defineInput), not a per-client capability.
|
|
8
8
|
*/
|
|
9
9
|
export interface PatchTimingContext {
|
|
10
|
-
/** `
|
|
10
|
+
/** Server clock as ms since room start (`clock.elapsedTime`) at patch time. */
|
|
11
11
|
sNow: number;
|
|
12
12
|
}
|
|
13
13
|
|