@colyseus/core 0.17.44 → 0.18.1
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 +22 -13
- package/build/MatchMaker.cjs.map +2 -2
- package/build/MatchMaker.d.ts +10 -0
- package/build/MatchMaker.mjs +22 -14
- package/build/MatchMaker.mjs.map +2 -2
- package/build/Protocol.cjs +102 -37
- package/build/Protocol.cjs.map +2 -2
- package/build/Protocol.d.ts +33 -2
- package/build/Protocol.mjs +102 -37
- package/build/Protocol.mjs.map +2 -2
- package/build/Rewind.cjs +385 -0
- package/build/Rewind.cjs.map +7 -0
- package/build/Rewind.d.ts +291 -0
- package/build/Rewind.mjs +359 -0
- package/build/Rewind.mjs.map +7 -0
- package/build/Room.cjs +425 -149
- package/build/Room.cjs.map +3 -3
- package/build/Room.d.ts +314 -27
- package/build/Room.mjs +434 -152
- package/build/Room.mjs.map +3 -3
- package/build/RoomMessages.cjs +265 -0
- package/build/RoomMessages.cjs.map +7 -0
- package/build/RoomMessages.d.ts +49 -0
- package/build/RoomMessages.mjs +240 -0
- package/build/RoomMessages.mjs.map +7 -0
- package/build/RoomPlugin.cjs +252 -0
- package/build/RoomPlugin.cjs.map +7 -0
- package/build/RoomPlugin.d.ts +271 -0
- package/build/RoomPlugin.mjs +220 -0
- package/build/RoomPlugin.mjs.map +7 -0
- package/build/Server.cjs +49 -11
- package/build/Server.cjs.map +2 -2
- package/build/Server.d.ts +26 -0
- package/build/Server.mjs +50 -12
- package/build/Server.mjs.map +2 -2
- package/build/Transport.cjs +64 -2
- package/build/Transport.cjs.map +2 -2
- package/build/Transport.d.ts +108 -5
- package/build/Transport.mjs +63 -2
- package/build/Transport.mjs.map +2 -2
- package/build/errors/RoomExceptions.cjs +7 -4
- package/build/errors/RoomExceptions.cjs.map +2 -2
- package/build/errors/RoomExceptions.d.ts +15 -3
- package/build/errors/RoomExceptions.mjs +5 -3
- package/build/errors/RoomExceptions.mjs.map +2 -2
- package/build/index.cjs +30 -4
- package/build/index.cjs.map +2 -2
- package/build/index.d.ts +9 -5
- package/build/index.mjs +29 -6
- package/build/index.mjs.map +2 -2
- package/build/input/InputBuffer.cjs +536 -0
- package/build/input/InputBuffer.cjs.map +7 -0
- package/build/input/InputBuffer.d.ts +226 -0
- package/build/input/InputBuffer.mjs +507 -0
- package/build/input/InputBuffer.mjs.map +7 -0
- package/build/input/RoomInput.cjs +306 -0
- package/build/input/RoomInput.cjs.map +7 -0
- package/build/input/RoomInput.d.ts +91 -0
- package/build/input/RoomInput.mjs +287 -0
- package/build/input/RoomInput.mjs.map +7 -0
- package/build/input/types.cjs +18 -0
- package/build/input/types.cjs.map +7 -0
- package/build/input/types.d.ts +476 -0
- package/build/input/types.mjs +0 -0
- package/build/input/types.mjs.map +7 -0
- package/build/internal.cjs +61 -0
- package/build/internal.cjs.map +7 -0
- package/build/internal.d.ts +9 -0
- package/build/internal.mjs +29 -0
- package/build/internal.mjs.map +7 -0
- package/build/matchmaker/LocalDriver/LocalDriver.cjs +13 -0
- package/build/matchmaker/LocalDriver/LocalDriver.cjs.map +2 -2
- package/build/matchmaker/LocalDriver/LocalDriver.d.ts +1 -0
- package/build/matchmaker/LocalDriver/LocalDriver.mjs +13 -0
- package/build/matchmaker/LocalDriver/LocalDriver.mjs.map +2 -2
- package/build/matchmaker/driver.cjs.map +1 -1
- package/build/matchmaker/driver.d.ts +12 -0
- package/build/matchmaker/driver.mjs.map +1 -1
- package/build/presence/LocalPresence.d.ts +1 -1
- package/build/rooms/LobbyRoom.cjs +8 -10
- package/build/rooms/LobbyRoom.cjs.map +2 -2
- package/build/rooms/LobbyRoom.d.ts +4 -3
- package/build/rooms/LobbyRoom.mjs +8 -10
- package/build/rooms/LobbyRoom.mjs.map +2 -2
- package/build/rooms/QueueRoom.cjs +1 -1
- package/build/rooms/QueueRoom.cjs.map +2 -2
- package/build/rooms/QueueRoom.mjs +1 -1
- package/build/rooms/QueueRoom.mjs.map +2 -2
- package/build/rooms/RelayRoom.cjs +12 -16
- package/build/rooms/RelayRoom.cjs.map +2 -2
- package/build/rooms/RelayRoom.d.ts +32 -11
- package/build/rooms/RelayRoom.mjs +10 -16
- package/build/rooms/RelayRoom.mjs.map +2 -2
- package/build/router/default_routes.cjs +4 -1
- package/build/router/default_routes.cjs.map +2 -2
- package/build/router/default_routes.mjs +4 -1
- package/build/router/default_routes.mjs.map +2 -2
- package/build/router/index.cjs +71 -5
- package/build/router/index.cjs.map +2 -2
- package/build/router/index.d.ts +30 -6
- package/build/router/index.mjs +72 -7
- package/build/router/index.mjs.map +3 -3
- package/build/serializer/NoneSerializer.cjs +2 -2
- package/build/serializer/NoneSerializer.cjs.map +2 -2
- package/build/serializer/NoneSerializer.d.ts +3 -3
- package/build/serializer/NoneSerializer.mjs +2 -2
- package/build/serializer/NoneSerializer.mjs.map +2 -2
- package/build/serializer/SchemaSerializer.cjs +44 -14
- package/build/serializer/SchemaSerializer.cjs.map +2 -2
- package/build/serializer/SchemaSerializer.d.ts +3 -3
- package/build/serializer/SchemaSerializer.mjs +46 -16
- package/build/serializer/SchemaSerializer.mjs.map +2 -2
- package/build/serializer/Serializer.cjs.map +1 -1
- package/build/serializer/Serializer.d.ts +12 -2
- package/build/utils/DevMode.cjs +14 -14
- package/build/utils/DevMode.cjs.map +2 -2
- package/build/utils/DevMode.d.ts +0 -2
- package/build/utils/DevMode.mjs +14 -12
- package/build/utils/DevMode.mjs.map +2 -2
- package/build/utils/UserSessionIndex.cjs +162 -0
- package/build/utils/UserSessionIndex.cjs.map +7 -0
- package/build/utils/UserSessionIndex.d.ts +166 -0
- package/build/utils/UserSessionIndex.mjs +130 -0
- package/build/utils/UserSessionIndex.mjs.map +7 -0
- package/build/utils/Utils.cjs +3 -0
- package/build/utils/Utils.cjs.map +2 -2
- package/build/utils/Utils.d.ts +8 -0
- package/build/utils/Utils.mjs +2 -0
- package/build/utils/Utils.mjs.map +2 -2
- package/package.json +19 -14
- package/src/MatchMaker.ts +47 -15
- package/src/Protocol.ts +130 -59
- package/src/Rewind.ts +572 -0
- package/src/Room.ts +649 -235
- package/src/RoomMessages.ts +342 -0
- package/src/RoomPlugin.ts +563 -0
- package/src/Server.ts +82 -15
- package/src/Transport.ts +172 -9
- package/src/errors/RoomExceptions.ts +18 -4
- package/src/index.ts +23 -3
- package/src/input/InputBuffer.ts +549 -0
- package/src/input/RoomInput.ts +337 -0
- package/src/input/types.ts +503 -0
- package/src/internal.ts +46 -0
- package/src/matchmaker/LocalDriver/LocalDriver.ts +10 -0
- package/src/matchmaker/driver.ts +13 -0
- package/src/rooms/LobbyRoom.ts +12 -8
- package/src/rooms/QueueRoom.ts +1 -1
- package/src/rooms/RelayRoom.ts +9 -15
- package/src/router/default_routes.ts +6 -1
- package/src/router/index.ts +117 -11
- package/src/serializer/NoneSerializer.ts +3 -3
- package/src/serializer/SchemaSerializer.ts +110 -19
- package/src/serializer/Serializer.ts +13 -2
- package/src/utils/DevMode.ts +18 -13
- package/src/utils/UserSessionIndex.ts +311 -0
- package/src/utils/Utils.ts +9 -0
|
@@ -36,6 +36,10 @@ export const postMatchmakeMethod = createEndpoint("/matchmake/:method/:roomName"
|
|
|
36
36
|
if (transport.protocol !== undefined) {
|
|
37
37
|
response.protocol = transport.protocol;
|
|
38
38
|
}
|
|
39
|
+
// WebTransport (h3): client pins the self-signed cert via serverCertificateHashes.
|
|
40
|
+
if (transport.fingerprint !== undefined) {
|
|
41
|
+
response.fingerprint = transport.fingerprint;
|
|
42
|
+
}
|
|
39
43
|
|
|
40
44
|
const json = JSON.stringify(response);
|
|
41
45
|
|
|
@@ -57,5 +61,6 @@ export const postMatchmakeMethod = createEndpoint("/matchmake/:method/:roomName"
|
|
|
57
61
|
});
|
|
58
62
|
|
|
59
63
|
export function getDefaultRouter() {
|
|
60
|
-
|
|
64
|
+
// no public docs page — the docs surface is playground's gated /__apidocs
|
|
65
|
+
return createRouter({ postMatchmakeMethod }, { openapi: { disabled: true } });
|
|
61
66
|
}
|
package/src/router/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type express from "express";
|
|
2
2
|
import type { IncomingMessage, ServerResponse } from "http";
|
|
3
|
-
import {
|
|
3
|
+
import { createHash, timingSafeEqual } from "node:crypto";
|
|
4
|
+
import { type Endpoint, type Router, type RouterConfig, createRouter as createBetterCallRouter, createEndpoint, createMiddleware, APIError } from "@colyseus/better-call";
|
|
4
5
|
import { toNodeHandler, getRequest, setResponse } from "@colyseus/better-call/node";
|
|
5
6
|
import { Transport } from "../Transport.ts";
|
|
6
7
|
import { controller } from "../matchmaker/controller.ts";
|
|
@@ -120,19 +121,124 @@ function expressRootRoute(expressApp: express.Application) {
|
|
|
120
121
|
return stack.find((layer: any) => layer.match('/') && !['query', 'expressInit'].includes(layer.name));
|
|
121
122
|
}
|
|
122
123
|
|
|
123
|
-
/**
|
|
124
|
-
* Do not use this directly. This is used internally by `@colyseus/playground`.
|
|
125
|
-
* TODO: refactor. Avoid using globals.
|
|
126
|
-
* @internal
|
|
127
|
-
*/
|
|
128
|
-
export let __globalEndpoints: Record<string, Endpoint> = {};
|
|
129
|
-
|
|
130
124
|
export function createRouter<
|
|
131
125
|
E extends Record<string, Endpoint>,
|
|
132
126
|
Config extends RouterConfig
|
|
133
127
|
>(endpoints: E, config: Config = {} as Config) {
|
|
134
|
-
|
|
135
|
-
|
|
128
|
+
return createBetterCallRouter({ ...endpoints }, {
|
|
129
|
+
// better-call's /api/reference page dumps the full API surface
|
|
130
|
+
// unauthenticated — opt back in by passing `openapi` explicitly.
|
|
131
|
+
openapi: { disabled: true },
|
|
132
|
+
...config,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface BasicAuthOptions {
|
|
137
|
+
/** username → password. The common static case. */
|
|
138
|
+
users?: Record<string, string>;
|
|
139
|
+
/** Custom validator (e.g. DB-backed). Takes precedence over `users`. */
|
|
140
|
+
validate?: (username: string, password: string) => boolean | Promise<boolean>;
|
|
141
|
+
/** Realm shown in the browser prompt. Default 'Restricted'. */
|
|
142
|
+
realm?: string;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* HTTP Basic Auth middleware. Drop into any endpoint's `use:` slot to gate
|
|
147
|
+
* it behind a browser credentials prompt:
|
|
148
|
+
*
|
|
149
|
+
* playground({ use: [basicAuth({ users: { admin: 's3cret' } })] })
|
|
150
|
+
*/
|
|
151
|
+
export function basicAuth(opts: BasicAuthOptions) {
|
|
152
|
+
const { users, validate } = opts;
|
|
153
|
+
if (!users && !validate) {
|
|
154
|
+
throw new Error('[basicAuth] provide `users` or `validate`');
|
|
155
|
+
}
|
|
156
|
+
// Realm is interpolated into a header — strip `"` so it can't break out.
|
|
157
|
+
const challenge = `Basic realm="${(opts.realm ?? 'Restricted').replace(/"/g, '')}", charset="UTF-8"`;
|
|
158
|
+
|
|
159
|
+
return createMiddleware(async (ctx) => {
|
|
160
|
+
const creds = parseBasicHeader(ctx.getHeader('authorization'));
|
|
161
|
+
const ok = !!creds && (validate
|
|
162
|
+
? await validate(creds.username, creds.password)
|
|
163
|
+
: staticCheck(users!, creds.username, creds.password));
|
|
164
|
+
if (!ok) {
|
|
165
|
+
throw new APIError(401, { message: 'authentication required' }, { 'WWW-Authenticate': challenge });
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function parseBasicHeader(header: string | null | undefined) {
|
|
171
|
+
if (!header) { return null; }
|
|
172
|
+
const sep = header.indexOf(' ');
|
|
173
|
+
if (sep < 0 || header.slice(0, sep).toLowerCase() !== 'basic') { return null; }
|
|
174
|
+
let decoded: string;
|
|
175
|
+
try { decoded = Buffer.from(header.slice(sep + 1), 'base64').toString('utf8'); } catch { return null; }
|
|
176
|
+
const colon = decoded.indexOf(':');
|
|
177
|
+
if (colon < 0) { return null; }
|
|
178
|
+
return { username: decoded.slice(0, colon), password: decoded.slice(colon + 1) };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function staticCheck(users: Record<string, string>, username: string, password: string): boolean {
|
|
182
|
+
const expected = Object.prototype.hasOwnProperty.call(users, username) ? users[username] : undefined;
|
|
183
|
+
// Compare even for an unknown user so reject timing doesn't reveal which
|
|
184
|
+
// usernames exist.
|
|
185
|
+
return safeEqual(password, expected ?? '\0') && expected !== undefined;
|
|
186
|
+
}
|
|
136
187
|
|
|
137
|
-
|
|
188
|
+
// Hash both sides first: equalizes length (timingSafeEqual throws on a
|
|
189
|
+
// length mismatch, which would itself leak the secret's length).
|
|
190
|
+
function safeEqual(a: string, b: string): boolean {
|
|
191
|
+
return timingSafeEqual(
|
|
192
|
+
createHash('sha256').update(a).digest(),
|
|
193
|
+
createHash('sha256').update(b).digest(),
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
// dualModeEndpoints — shared express-compat layer for @colyseus/admin,
|
|
199
|
+
// @colyseus/monitor, @colyseus/playground. Builds the two local routers
|
|
200
|
+
// (specific = no catch-all, full = everything) and the matching node
|
|
201
|
+
// handlers, then packages the express middleware so the return value works
|
|
202
|
+
// both as `{...spread}` into createRouter AND as `app.use("/", x)` middleware.
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
|
|
205
|
+
export type ExpressMiddleware = (
|
|
206
|
+
req: IncomingMessage,
|
|
207
|
+
res: ServerResponse,
|
|
208
|
+
next: (err?: any) => void,
|
|
209
|
+
) => void;
|
|
210
|
+
|
|
211
|
+
export type NodeHandler = (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
212
|
+
|
|
213
|
+
export interface DualModeHelpers {
|
|
214
|
+
specificRouter: Router;
|
|
215
|
+
specificHandler: NodeHandler;
|
|
216
|
+
fullRouter: Router;
|
|
217
|
+
fullHandler: NodeHandler;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function dualModeEndpoints<E extends Record<string, Endpoint>>(
|
|
221
|
+
endpoints: E,
|
|
222
|
+
opts: {
|
|
223
|
+
/** Key in `endpoints` whose path is a catch-all. Excluded from `specificRouter` so it doesn't eat fall-through decisions. */
|
|
224
|
+
catchAllKey?: keyof E;
|
|
225
|
+
/** Build the express middleware given the pre-built routers + node handlers. */
|
|
226
|
+
buildMiddleware: (helpers: DualModeHelpers) => ExpressMiddleware;
|
|
227
|
+
},
|
|
228
|
+
): ExpressMiddleware & E {
|
|
229
|
+
const fullRouter = createRouter(endpoints);
|
|
230
|
+
const fullHandler = toNodeHandler(fullRouter.handler) as NodeHandler;
|
|
231
|
+
|
|
232
|
+
const specificEndpoints = opts.catchAllKey
|
|
233
|
+
? Object.fromEntries(
|
|
234
|
+
Object.entries(endpoints).filter(([k]) => k !== opts.catchAllKey),
|
|
235
|
+
) as Partial<E>
|
|
236
|
+
: endpoints;
|
|
237
|
+
const specificRouter = createRouter(specificEndpoints as E);
|
|
238
|
+
const specificHandler = toNodeHandler(specificRouter.handler) as NodeHandler;
|
|
239
|
+
|
|
240
|
+
const middleware = opts.buildMiddleware({
|
|
241
|
+
specificRouter, specificHandler, fullRouter, fullHandler,
|
|
242
|
+
});
|
|
243
|
+
return Object.assign(middleware, endpoints) as ExpressMiddleware & E;
|
|
138
244
|
}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import type { Client } from '../Transport.ts';
|
|
2
|
-
import type { Serializer } from './Serializer.ts';
|
|
2
|
+
import type { PatchTimingContext, Serializer } from './Serializer.ts';
|
|
3
3
|
|
|
4
4
|
export class NoneSerializer<T= any> implements Serializer<T> {
|
|
5
5
|
public id: string = 'none';
|
|
6
6
|
|
|
7
7
|
public reset(data: any) {}
|
|
8
8
|
|
|
9
|
-
public getFullState(
|
|
9
|
+
public getFullState(_client?: Client, _timing?: PatchTimingContext) {
|
|
10
10
|
return null;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
public applyPatches(
|
|
13
|
+
public applyPatches(_clients: Client[], _state: T, _timing?: PatchTimingContext): boolean {
|
|
14
14
|
return false;
|
|
15
15
|
}
|
|
16
16
|
}
|
|
@@ -1,11 +1,85 @@
|
|
|
1
|
-
import { Protocol } from '@colyseus/shared-types';
|
|
1
|
+
import { Protocol, ProtocolModifier } from '@colyseus/shared-types';
|
|
2
2
|
|
|
3
|
-
import type { Serializer } from './Serializer.ts';
|
|
4
|
-
import { type Client, ClientState } from '../Transport.ts';
|
|
3
|
+
import type { PatchTimingContext, Serializer } from './Serializer.ts';
|
|
4
|
+
import { type Client, type ClientPrivate, ClientState } from '../Transport.ts';
|
|
5
5
|
|
|
6
|
-
import { type Iterator, Encoder, dumpChanges, Reflection, Schema, StateView } from '@colyseus/schema';
|
|
6
|
+
import { type Iterator, encode, Encoder, dumpChanges, Reflection, Schema, StateView } from '@colyseus/schema';
|
|
7
7
|
import { debugPatch } from '../Debug.ts';
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Size of the {@link ProtocolModifier.TIMED} prefix that follows the
|
|
11
|
+
* protocol byte:
|
|
12
|
+
* `[uint32 sNow][uint32 inputSeq]`
|
|
13
|
+
*
|
|
14
|
+
* - `sNow` — server clock as **milliseconds since room start**
|
|
15
|
+
* (`room.clock.elapsedTime`), NOT raw `performance.now()`. A portable,
|
|
16
|
+
* language-agnostic integer (C/C#/Lua have no `performance.now()`); the
|
|
17
|
+
* server's own time-keyed logic uses the SAME `clock.elapsedTime` so the
|
|
18
|
+
* client's reconstructed `serverNow()` stays in phase. Wraps at u32
|
|
19
|
+
* (~49.7 days uptime) — irrelevant per session. Drives the client clock
|
|
20
|
+
* offset / `serverNow()`.
|
|
21
|
+
* - `inputSeq` — seq VALUE of the last input CONSUMED into the authoritative
|
|
22
|
+
* state (the input buffer's `ackSeq`, i.e. last-PROCESSED input). Reliable:
|
|
23
|
+
* equals the consumed count (inputs are sequenced implicitly by receive
|
|
24
|
+
* order). Unreliable: the framework wire seq, so a fully-dropped input
|
|
25
|
+
* doesn't make the ack lag the client's sent seq by the lost count. This
|
|
26
|
+
* single number is the canonical reconciliation ack ("the sequence number
|
|
27
|
+
* of the last input the server processed"); the client prunes its
|
|
28
|
+
* pending-input buffer against it AND derives RTT from its round-trip
|
|
29
|
+
* (`now − sendTime(inputSeq)`). No separate received-seq / received-time —
|
|
30
|
+
* RTT-from-the-processed-ack is round-trip-inclusive, which is the standard.
|
|
31
|
+
*/
|
|
32
|
+
const TIMED_PREFIX_SIZE = 8;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Construct a per-recipient TIMED buffer:
|
|
36
|
+
* `[code|TIMED][sNow][inputSeq][...schema bytes]`
|
|
37
|
+
*
|
|
38
|
+
* `encodedBody` is the shared encoded buffer whose byte 0 already holds the
|
|
39
|
+
* base protocol code (ROOM_STATE or ROOM_STATE_PATCH); bytes 1.. are the
|
|
40
|
+
* schema payload. We construct a fresh per-client Buffer so the transport
|
|
41
|
+
* never sees a buffer that might mutate between queue and send.
|
|
42
|
+
*
|
|
43
|
+
* Uses the schema `encode.*` helpers to keep the wire layout symmetric with
|
|
44
|
+
* the SDK's `decode.*`-driven reader.
|
|
45
|
+
*/
|
|
46
|
+
function buildTimedFrame(
|
|
47
|
+
encodedBody: Uint8Array,
|
|
48
|
+
sNow: number,
|
|
49
|
+
inputSeq: number,
|
|
50
|
+
): Buffer {
|
|
51
|
+
const schemaPayload = encodedBody.subarray(1);
|
|
52
|
+
const out = Buffer.allocUnsafe(1 + TIMED_PREFIX_SIZE + schemaPayload.length);
|
|
53
|
+
out[0] = encodedBody[0] | ProtocolModifier.TIMED;
|
|
54
|
+
const it: Iterator = { offset: 1 };
|
|
55
|
+
encode.uint32(out, sNow >>> 0, it); // ms since room start (clock.elapsedTime)
|
|
56
|
+
encode.uint32(out, inputSeq >>> 0, it);
|
|
57
|
+
out.set(schemaPayload, it.offset);
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Seq VALUE of the last input consumed from the client's buffer — the
|
|
62
|
+
* reconciliation ack (and RTT correlation key). Reliable: equals the consumed
|
|
63
|
+
* count; unreliable: the framework wire seq (so packet loss doesn't skew it).
|
|
64
|
+
* 0 when the room doesn't buffer input. */
|
|
65
|
+
function clientProcessedInputSeq(client: Client): number {
|
|
66
|
+
return (client as Client & ClientPrivate)._inputBuffer?.ackSeq ?? 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Send a per-recipient patch frame: a TIMED-prefixed buffer when `timing` is set,
|
|
71
|
+
* else the shared `encodedBody` passed straight through (no per-client copy). Any
|
|
72
|
+
* `afterNextPatch` frames staged on `client._pendingFrames` are NOT coalesced here
|
|
73
|
+
* — they go out standalone right after the patch via `Room._flushPendingClientFrames`.
|
|
74
|
+
*/
|
|
75
|
+
function sendClientFrame(client: Client, encodedBody: Uint8Array, timing?: PatchTimingContext): void {
|
|
76
|
+
if (timing) {
|
|
77
|
+
client.raw(buildTimedFrame(encodedBody, timing.sNow, clientProcessedInputSeq(client)));
|
|
78
|
+
} else {
|
|
79
|
+
client.raw(encodedBody);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
9
83
|
const SHARED_VIEW = {};
|
|
10
84
|
|
|
11
85
|
export class SchemaSerializer<T extends Schema> implements Serializer<T> {
|
|
@@ -38,27 +112,29 @@ export class SchemaSerializer<T extends Schema> implements Serializer<T> {
|
|
|
38
112
|
}
|
|
39
113
|
}
|
|
40
114
|
|
|
41
|
-
public getFullState(client?: Client) {
|
|
115
|
+
public getFullState(client?: Client, timing?: PatchTimingContext) {
|
|
42
116
|
if (this.needFullEncode || this.encoder.root.changes.next !== undefined) {
|
|
43
117
|
this.sharedOffsetCache = { offset: 1 };
|
|
44
118
|
this.fullEncodeCache = this.encoder.encodeAll(this.sharedOffsetCache, this.fullEncodeBuffer);
|
|
45
119
|
this.needFullEncode = false;
|
|
46
120
|
}
|
|
47
121
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
122
|
+
const body = (this.hasFilters && client?.view)
|
|
123
|
+
? this.encoder.encodeAllView(
|
|
124
|
+
client.view,
|
|
125
|
+
this.sharedOffsetCache.offset,
|
|
126
|
+
{ ...this.sharedOffsetCache },
|
|
127
|
+
this.fullEncodeBuffer
|
|
128
|
+
)
|
|
129
|
+
: this.fullEncodeCache;
|
|
130
|
+
|
|
131
|
+
if (timing && client) {
|
|
132
|
+
return buildTimedFrame(body, timing.sNow, clientProcessedInputSeq(client));
|
|
58
133
|
}
|
|
134
|
+
return body;
|
|
59
135
|
}
|
|
60
136
|
|
|
61
|
-
public applyPatches(clients: Client[]) {
|
|
137
|
+
public applyPatches(clients: Client[], _state?: unknown, timing?: PatchTimingContext) {
|
|
62
138
|
let numClients = clients.length;
|
|
63
139
|
|
|
64
140
|
if (numClients === 0) {
|
|
@@ -92,11 +168,26 @@ export class SchemaSerializer<T extends Schema> implements Serializer<T> {
|
|
|
92
168
|
this.encoder.sharedBuffer[0] = Protocol.ROOM_STATE_PATCH;
|
|
93
169
|
|
|
94
170
|
clientsWithViewChange.forEach((client) => {
|
|
95
|
-
|
|
171
|
+
const encodedView = this.encoder.encodeView(client.view, sharedOffset, it);
|
|
172
|
+
sendClientFrame(client, encodedView, timing);
|
|
96
173
|
});
|
|
97
174
|
}
|
|
98
175
|
}
|
|
99
176
|
|
|
177
|
+
// Heartbeat: clients that advertised CLIENT_TIMING still need
|
|
178
|
+
// periodic timing samples to keep their clock offset / RTT estimate
|
|
179
|
+
// fresh during quiet ticks. Emit a TIMED-prefixed ROOM_STATE_PATCH
|
|
180
|
+
// with an empty schema body — 1 byte protocol + 12 bytes prefix.
|
|
181
|
+
// No-op for rooms without `defineInput()` (timing is undefined).
|
|
182
|
+
if (timing) {
|
|
183
|
+
const heartbeatBody = Buffer.from([Protocol.ROOM_STATE_PATCH]);
|
|
184
|
+
for (let i = 0; i < clients.length; i++) {
|
|
185
|
+
const client = clients[i];
|
|
186
|
+
if (client.state !== ClientState.JOINED) continue;
|
|
187
|
+
sendClientFrame(client, heartbeatBody, timing);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
100
191
|
// skip patching state if:
|
|
101
192
|
// - no clients are connected
|
|
102
193
|
// - no changes were made
|
|
@@ -129,7 +220,7 @@ export class SchemaSerializer<T extends Schema> implements Serializer<T> {
|
|
|
129
220
|
continue;
|
|
130
221
|
}
|
|
131
222
|
|
|
132
|
-
client
|
|
223
|
+
sendClientFrame(client, encodedChanges, timing);
|
|
133
224
|
}
|
|
134
225
|
|
|
135
226
|
} else {
|
|
@@ -159,7 +250,7 @@ export class SchemaSerializer<T extends Schema> implements Serializer<T> {
|
|
|
159
250
|
this.encodedViews.set(view, encodedView);
|
|
160
251
|
}
|
|
161
252
|
|
|
162
|
-
client
|
|
253
|
+
sendClientFrame(client, encodedView, timing);
|
|
163
254
|
}
|
|
164
255
|
|
|
165
256
|
// clear views
|
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import type { Client } from '../Transport.ts';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Per-tick timing context, populated by the Room when
|
|
5
|
+
* {@link Room.defineInput} was called. The serializer uses it to emit a
|
|
6
|
+
* {@link ProtocolModifier.TIMED} prefix for clients that advertised
|
|
7
|
+
* the {@link HandshakeSection.CLIENT_TIMING} capability.
|
|
8
|
+
*/
|
|
9
|
+
export interface PatchTimingContext {
|
|
10
|
+
/** `performance.now()` at the moment the patch is being applied. */
|
|
11
|
+
sNow: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
3
14
|
export interface Serializer<T> {
|
|
4
15
|
id: string;
|
|
5
16
|
reset(data: any): void;
|
|
6
|
-
getFullState(client?: Client): Uint8Array;
|
|
7
|
-
applyPatches(clients: Client[], state: T): boolean;
|
|
17
|
+
getFullState(client?: Client, timing?: PatchTimingContext): Uint8Array;
|
|
18
|
+
applyPatches(clients: Client[], state: T, timing?: PatchTimingContext): boolean;
|
|
8
19
|
handshake?(): Uint8Array;
|
|
9
20
|
}
|
package/src/utils/DevMode.ts
CHANGED
|
@@ -31,11 +31,24 @@ export async function reloadFromCache() {
|
|
|
31
31
|
debugDevMode("rooms to restore: %i", roomHistoryList.length);
|
|
32
32
|
|
|
33
33
|
for (const [roomId, value] of roomHistoryList) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
let roomHistory: any;
|
|
35
|
+
let recreatedRoom: Room;
|
|
36
|
+
let recreatedRoomListing: any;
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
roomHistory = JSON.parse(value);
|
|
40
|
+
debugDevMode("restoring room %s (%s)", roomHistory.roomName, roomId);
|
|
41
|
+
|
|
42
|
+
recreatedRoomListing = await handleCreateRoom(roomHistory.roomName, roomHistory.clientOptions, roomId);
|
|
43
|
+
recreatedRoom = getLocalRoomById(recreatedRoomListing.roomId);
|
|
44
|
+
|
|
45
|
+
} catch (e: any) {
|
|
46
|
+
// unrestorable entry (e.g. room type renamed/removed) — drop it
|
|
47
|
+
// instead of wedging the boot
|
|
48
|
+
debugAndPrintError(`❌ couldn't restore room '${roomId}':\n${e.stack}`);
|
|
49
|
+
await presence.hdel(getRoomRestoreListKey(), roomId);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
39
52
|
|
|
40
53
|
// Restore previous state
|
|
41
54
|
if (roomHistory.hasOwnProperty("state")) {
|
|
@@ -144,14 +157,6 @@ export async function cacheRoomHistory(rooms: { [roomId: string]: Room }) {
|
|
|
144
157
|
}
|
|
145
158
|
}
|
|
146
159
|
|
|
147
|
-
export async function getPreviousProcessId(hostname: string = '') {
|
|
148
|
-
return await presence.hget(getProcessRestoreKey(), hostname);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
160
|
export function getRoomRestoreListKey() {
|
|
152
161
|
return 'roomhistory';
|
|
153
162
|
}
|
|
154
|
-
|
|
155
|
-
export function getProcessRestoreKey() {
|
|
156
|
-
return 'processhistory';
|
|
157
|
-
}
|