@colyseus/sdk 0.17.41 → 0.18.0
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/3rd_party/discord.cjs +1 -1
- package/build/3rd_party/discord.mjs +1 -1
- package/build/Auth.cjs +16 -2
- package/build/Auth.cjs.map +1 -1
- package/build/Auth.mjs +16 -2
- package/build/Auth.mjs.map +1 -1
- package/build/Client.cjs +1 -1
- package/build/Client.mjs +1 -1
- package/build/Connection.cjs +1 -1
- package/build/Connection.mjs +1 -1
- package/build/HTTP.cjs +1 -1
- package/build/HTTP.mjs +1 -1
- package/build/Room.cjs +231 -46
- package/build/Room.cjs.map +1 -1
- package/build/Room.d.ts +62 -2
- package/build/Room.mjs +229 -44
- package/build/Room.mjs.map +1 -1
- package/build/Storage.cjs +1 -1
- package/build/Storage.mjs +1 -1
- package/build/core/nanoevents.cjs +1 -1
- package/build/core/nanoevents.mjs +1 -1
- package/build/core/signal.cjs +1 -1
- package/build/core/signal.cjs.map +1 -1
- package/build/core/signal.mjs +1 -1
- package/build/core/signal.mjs.map +1 -1
- package/build/core/utils.cjs +1 -1
- package/build/core/utils.mjs +1 -1
- package/build/debug.cjs +1 -1
- package/build/debug.cjs.map +1 -1
- package/build/debug.mjs +1 -1
- package/build/debug.mjs.map +1 -1
- package/build/errors/Errors.cjs +1 -1
- package/build/errors/Errors.mjs +1 -1
- package/build/fetchXHR.cjs +1 -1
- package/build/fetchXHR.mjs +1 -1
- package/build/index.cjs +1 -1
- package/build/index.cjs.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.mjs +1 -1
- package/build/index.mjs.map +1 -1
- package/build/input/InputHandle.cjs +47 -0
- package/build/input/InputHandle.cjs.map +1 -0
- package/build/input/InputHandle.d.ts +79 -0
- package/build/input/InputHandle.mjs +48 -0
- package/build/input/InputHandle.mjs.map +1 -0
- package/build/legacy.cjs +1 -1
- package/build/legacy.mjs +1 -1
- package/build/serializer/NoneSerializer.cjs +1 -1
- package/build/serializer/NoneSerializer.mjs +1 -1
- package/build/serializer/SchemaSerializer.cjs +1 -1
- package/build/serializer/SchemaSerializer.mjs +1 -1
- package/build/serializer/Serializer.cjs +1 -1
- package/build/serializer/Serializer.mjs +1 -1
- package/build/transport/H3Transport.cjs +1 -1
- package/build/transport/H3Transport.cjs.map +1 -1
- package/build/transport/H3Transport.mjs +1 -1
- package/build/transport/H3Transport.mjs.map +1 -1
- package/build/transport/WebSocketTransport.cjs +1 -1
- package/build/transport/WebSocketTransport.mjs +1 -1
- package/dist/colyseus.js +13152 -1885
- package/dist/colyseus.js.map +1 -1
- package/dist/debug.js +1 -1
- package/dist/debug.js.map +1 -1
- package/package.json +11 -8
- package/src/Auth.ts +11 -1
- package/src/Room.ts +294 -48
- package/src/core/signal.ts +1 -1
- package/src/debug.ts +8 -8
- package/src/index.ts +1 -1
- package/src/input/InputHandle.ts +115 -0
- package/src/transport/H3Transport.ts +2 -2
- package/build/serializer/FossilDeltaSerializer.d.ts +0 -0
- package/src/serializer/FossilDeltaSerializer.ts +0 -39
package/build/Room.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { type InferState, type NormalizeRoomType, type ExtractRoomMessages, type ExtractRoomClientMessages, type ExtractMessageType } from '@colyseus/shared-types';
|
|
1
|
+
import { type InferState, type InferInput, type NormalizeRoomType, type ExtractRoomMessages, type ExtractRoomClientMessages, type ExtractMessageType, type ExtractResponseType } from '@colyseus/shared-types';
|
|
2
2
|
import { Schema } from '@colyseus/schema';
|
|
3
|
-
import {
|
|
3
|
+
import { type ClientInputHandle, type ClientInputOptions } from './input/InputHandle.ts';
|
|
4
|
+
export { type ClientInputHandle, type ClientInputOptions } from './input/InputHandle.ts';
|
|
5
|
+
import { Packr } from 'msgpackr';
|
|
4
6
|
import { Connection } from './Connection.ts';
|
|
5
7
|
import { Serializer } from './serializer/Serializer.ts';
|
|
6
8
|
import { SchemaConstructor, SchemaSerializer } from './serializer/SchemaSerializer.ts';
|
|
@@ -121,6 +123,13 @@ export declare class Room<T = any, State = InferState<T, never>> {
|
|
|
121
123
|
protected joinedAtTime: number;
|
|
122
124
|
protected onMessageHandlers: import("./core/nanoevents.ts").Emitter<import("./core/nanoevents.ts").EventsMap>;
|
|
123
125
|
protected packr: Packr;
|
|
126
|
+
protected sharedBuffer: Uint8Array;
|
|
127
|
+
/**
|
|
128
|
+
* Default time (ms) a `room.request()` / `room.send(..., callback)` waits
|
|
129
|
+
* for a reply before rejecting. Override per-call with the `timeout`
|
|
130
|
+
* option. Tune globally by assigning to this field after joining.
|
|
131
|
+
*/
|
|
132
|
+
requestTimeout: number;
|
|
124
133
|
constructor(name: string, rootSchema?: SchemaConstructor<State>);
|
|
125
134
|
connect(endpoint: string, options?: any, headers?: any): void;
|
|
126
135
|
leave(consented?: boolean): Promise<number>;
|
|
@@ -129,9 +138,60 @@ export declare class Room<T = any, State = InferState<T, never>> {
|
|
|
129
138
|
onMessage<Payload = any>(type: [keyof ExtractRoomClientMessages<NormalizeRoomType<T>>] extends [never] ? (string | number) : never, callback: (payload: Payload) => void): () => void;
|
|
130
139
|
ping(callback: (ms: number) => void): void;
|
|
131
140
|
send<MessageType extends keyof ExtractRoomMessages<NormalizeRoomType<T>>>(messageType: MessageType, payload?: ExtractMessageType<ExtractRoomMessages<NormalizeRoomType<T>>[MessageType]>): void;
|
|
141
|
+
send<MessageType extends keyof ExtractRoomMessages<NormalizeRoomType<T>>>(messageType: MessageType, payload: ExtractMessageType<ExtractRoomMessages<NormalizeRoomType<T>>[MessageType]>, callback: (response: ExtractResponseType<ExtractRoomMessages<NormalizeRoomType<T>>[MessageType]>, error?: Error) => void): void;
|
|
132
142
|
send<Payload = any>(messageType: [keyof ExtractRoomMessages<NormalizeRoomType<T>>] extends [never] ? (string | number) : never, payload?: Payload): void;
|
|
143
|
+
send<Payload = any, Response = any>(messageType: [keyof ExtractRoomMessages<NormalizeRoomType<T>>] extends [never] ? (string | number) : never, payload: Payload, callback: (response: Response, error?: Error) => void): void;
|
|
144
|
+
/**
|
|
145
|
+
* Send a message and await the server's reply. The server answers by
|
|
146
|
+
* returning a value from its matching `onMessage(type, ...)` handler.
|
|
147
|
+
*
|
|
148
|
+
* Rejects if the handler throws, if no handler is registered, if the
|
|
149
|
+
* connection closes first, or if no reply arrives within `timeout`
|
|
150
|
+
* (defaults to {@link Room.requestTimeout}).
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* ```typescript
|
|
154
|
+
* const profile = await room.request("get-profile", { id: 42 });
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
157
|
+
request<MessageType extends keyof ExtractRoomMessages<NormalizeRoomType<T>>>(messageType: MessageType, payload?: ExtractMessageType<ExtractRoomMessages<NormalizeRoomType<T>>[MessageType]>, options?: {
|
|
158
|
+
timeout?: number;
|
|
159
|
+
}): Promise<ExtractResponseType<ExtractRoomMessages<NormalizeRoomType<T>>[MessageType]>>;
|
|
160
|
+
request<Payload = any, Response = any>(messageType: [keyof ExtractRoomMessages<NormalizeRoomType<T>>] extends [never] ? (string | number) : never, payload?: Payload, options?: {
|
|
161
|
+
timeout?: number;
|
|
162
|
+
}): Promise<Response>;
|
|
133
163
|
sendUnreliable<T = any>(type: string | number, message?: T): void;
|
|
134
164
|
sendBytes(type: string | number, bytes: Uint8Array): void;
|
|
165
|
+
/**
|
|
166
|
+
* Get the per-room input handle. Lazily created on first call and cached;
|
|
167
|
+
* subsequent calls return the same handle (options on later calls are
|
|
168
|
+
* ignored).
|
|
169
|
+
*
|
|
170
|
+
* Schema discovery, in order:
|
|
171
|
+
* 1. `options.type` — explicit constructor (overrides everything).
|
|
172
|
+
* 2. Server-sent reflection from the JOIN handshake — populated when the
|
|
173
|
+
* server room called `defineInput()`. The synthesized class has the
|
|
174
|
+
* same fields as the server's input schema; `instanceof YourInput`
|
|
175
|
+
* won't pass on it.
|
|
176
|
+
*
|
|
177
|
+
* Throws if neither source has produced a constructor.
|
|
178
|
+
*
|
|
179
|
+
* For rollback netcode, prefer `{ mode: "unreliable", delta: true,
|
|
180
|
+
* historySize: 4 }`: tiny per-tick payloads, redundancy across drops,
|
|
181
|
+
* idempotent under reordering.
|
|
182
|
+
*
|
|
183
|
+
* @example
|
|
184
|
+
* ```typescript
|
|
185
|
+
* const conn = await client.joinOrCreate<typeof FpsRoom>("fps");
|
|
186
|
+
* const input = conn.input({ mode: "unreliable" }); // type from server
|
|
187
|
+
* // each simulation tick:
|
|
188
|
+
* input.data.seq++;
|
|
189
|
+
* input.data.vx = vx;
|
|
190
|
+
* input.data.vy = vy;
|
|
191
|
+
* input.send();
|
|
192
|
+
* ```
|
|
193
|
+
*/
|
|
194
|
+
input<I = ([InferInput<T>] extends [never] ? any : InferInput<T>)>(options?: ClientInputOptions<I>): ClientInputHandle<I>;
|
|
135
195
|
get state(): State;
|
|
136
196
|
removeAllListeners(): void;
|
|
137
197
|
protected onMessageCallback(event: MessageEvent): void;
|
package/build/Room.mjs
CHANGED
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
// This software is released under the MIT License.
|
|
4
4
|
// https://opensource.org/license/MIT
|
|
5
5
|
//
|
|
6
|
-
// colyseus.js@0.
|
|
7
|
-
import { CloseCode, Protocol } from '@colyseus/shared-types';
|
|
8
|
-
import { Decoder, encode, decode } from '@colyseus/schema';
|
|
9
|
-
import {
|
|
6
|
+
// colyseus.js@0.18.0
|
|
7
|
+
import { CloseCode, Protocol, HandshakeSection, ResponseStatus } from '@colyseus/shared-types';
|
|
8
|
+
import { Decoder, encode, decode, Reflection } from '@colyseus/schema';
|
|
9
|
+
import { InputEncoder } from '@colyseus/schema/input';
|
|
10
|
+
import { ClientInputHandleImpl } from './input/InputHandle.mjs';
|
|
11
|
+
import { Packr, RESERVE_START_SPACE, unpack } from 'msgpackr';
|
|
10
12
|
import { Connection } from './Connection.mjs';
|
|
11
13
|
import { getSerializer } from './serializer/Serializer.mjs';
|
|
12
14
|
import { createNanoEvents } from './core/nanoevents.mjs';
|
|
@@ -46,13 +48,37 @@ class Room {
|
|
|
46
48
|
joinedAtTime = 0;
|
|
47
49
|
onMessageHandlers = createNanoEvents();
|
|
48
50
|
packr;
|
|
51
|
+
sharedBuffer;
|
|
49
52
|
#lastPingTime = 0;
|
|
50
53
|
#pingCallback = undefined;
|
|
54
|
+
/**
|
|
55
|
+
* Default time (ms) a `room.request()` / `room.send(..., callback)` waits
|
|
56
|
+
* for a reply before rejecting. Override per-call with the `timeout`
|
|
57
|
+
* option. Tune globally by assigning to this field after joining.
|
|
58
|
+
*/
|
|
59
|
+
requestTimeout = 10000;
|
|
60
|
+
/** Monotonic id correlating a {@link Protocol.ROOM_REQUEST} with its reply. @internal */
|
|
61
|
+
#nextRequestId = 0;
|
|
62
|
+
/** In-flight requests awaiting a {@link Protocol.ROOM_RESPONSE}. @internal */
|
|
63
|
+
#pendingRequests = new Map();
|
|
64
|
+
#inputHandle;
|
|
65
|
+
/**
|
|
66
|
+
* Schema constructor recovered via Reflection from the server's
|
|
67
|
+
* handshake (the `INPUT_REFLECTION` tagged section). Populated on JOIN
|
|
68
|
+
* when the server room called `defineInput()`; falls back to `undefined`
|
|
69
|
+
* otherwise. Survives reconnects that skip the handshake — the field is
|
|
70
|
+
* set on the original join and never cleared.
|
|
71
|
+
*
|
|
72
|
+
* Typed as `new () => any` (not `Schema`) on purpose — pinning to this
|
|
73
|
+
* SDK's Schema type would clash with user instances coming from a
|
|
74
|
+
* different copy of `@colyseus/schema` under multi-version installs.
|
|
75
|
+
* @internal
|
|
76
|
+
*/
|
|
77
|
+
#inputCtorFromReflection;
|
|
51
78
|
constructor(name, rootSchema) {
|
|
52
79
|
this.name = name;
|
|
53
80
|
this.packr = new Packr();
|
|
54
|
-
|
|
55
|
-
this.packr.encode(undefined);
|
|
81
|
+
this.sharedBuffer = new Uint8Array(8192);
|
|
56
82
|
if (rootSchema) {
|
|
57
83
|
const serializer = new (getSerializer("schema"));
|
|
58
84
|
this.serializer = serializer;
|
|
@@ -69,6 +95,8 @@ class Room {
|
|
|
69
95
|
this.connection = new Connection(options.protocol);
|
|
70
96
|
this.connection.events.onmessage = this.onMessageCallback.bind(this);
|
|
71
97
|
this.connection.events.onclose = (e) => {
|
|
98
|
+
// the in-flight requests can't be answered on a closed socket
|
|
99
|
+
this.#rejectAllPending("connection closed before a response was received.");
|
|
72
100
|
if (this.joinedAtTime === 0) {
|
|
73
101
|
console.warn?.(`Room connection was closed unexpectedly (${e.code}): ${e.reason}`);
|
|
74
102
|
this.onError.invoke(e.code, e.reason);
|
|
@@ -107,8 +135,8 @@ class Room {
|
|
|
107
135
|
this.onLeave((code) => resolve(code));
|
|
108
136
|
if (this.connection) {
|
|
109
137
|
if (consented) {
|
|
110
|
-
this.
|
|
111
|
-
this.connection.send(this.
|
|
138
|
+
this.sharedBuffer[0] = Protocol.LEAVE_ROOM;
|
|
139
|
+
this.connection.send(this.sharedBuffer.subarray(0, 1));
|
|
112
140
|
}
|
|
113
141
|
else {
|
|
114
142
|
this.connection.close();
|
|
@@ -129,23 +157,35 @@ class Room {
|
|
|
129
157
|
}
|
|
130
158
|
this.#lastPingTime = now();
|
|
131
159
|
this.#pingCallback = callback;
|
|
132
|
-
this.
|
|
133
|
-
this.connection.send(this.
|
|
160
|
+
this.sharedBuffer[0] = Protocol.PING;
|
|
161
|
+
this.connection.send(this.sharedBuffer.subarray(0, 1));
|
|
134
162
|
}
|
|
135
|
-
send(messageType, payload) {
|
|
163
|
+
send(messageType, payload, callback) {
|
|
164
|
+
// Request/response form: defer to `request()` and adapt to a
|
|
165
|
+
// (response, error) callback.
|
|
166
|
+
if (callback !== undefined) {
|
|
167
|
+
this.#request(messageType, payload, this.requestTimeout).then((response) => callback(response, undefined), (error) => callback(undefined, error));
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
136
170
|
const it = { offset: 1 };
|
|
137
|
-
this.
|
|
171
|
+
this.sharedBuffer[0] = Protocol.ROOM_DATA;
|
|
138
172
|
if (typeof (messageType) === "string") {
|
|
139
|
-
encode.string(this.
|
|
173
|
+
encode.string(this.sharedBuffer, messageType, it);
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
encode.number(this.sharedBuffer, messageType, it);
|
|
177
|
+
}
|
|
178
|
+
const headerLength = it.offset;
|
|
179
|
+
let data;
|
|
180
|
+
if (payload !== undefined) {
|
|
181
|
+
// Reserve `headerLength` writable bytes at the front of msgpackr's
|
|
182
|
+
// output and prepend the protocol header into them.
|
|
183
|
+
data = this.packr.pack(payload, RESERVE_START_SPACE | headerLength);
|
|
184
|
+
data.set(this.sharedBuffer.subarray(0, headerLength), 0);
|
|
140
185
|
}
|
|
141
186
|
else {
|
|
142
|
-
|
|
187
|
+
data = this.sharedBuffer.subarray(0, headerLength);
|
|
143
188
|
}
|
|
144
|
-
// force packr to use beginning of the buffer
|
|
145
|
-
this.packr.position = 0;
|
|
146
|
-
const data = (payload !== undefined)
|
|
147
|
-
? this.packr.pack(payload, 2048 + it.offset) // 2048 = RESERVE_START_SPACE
|
|
148
|
-
: this.packr.buffer.subarray(0, it.offset);
|
|
149
189
|
// If connection is not open, buffer the message
|
|
150
190
|
if (!this.connection.isOpen) {
|
|
151
191
|
enqueueMessage(this, new Uint8Array(data));
|
|
@@ -154,51 +194,146 @@ class Room {
|
|
|
154
194
|
this.connection.send(data);
|
|
155
195
|
}
|
|
156
196
|
}
|
|
197
|
+
request(messageType, payload, options) {
|
|
198
|
+
return this.#request(messageType, payload, options?.timeout ?? this.requestTimeout);
|
|
199
|
+
}
|
|
200
|
+
#request(messageType, payload, timeoutMs) {
|
|
201
|
+
return new Promise((resolve, reject) => {
|
|
202
|
+
if (!this.connection.isOpen) {
|
|
203
|
+
reject(new Error(`cannot send request "${messageType}": connection is not open.`));
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const requestId = this.#nextRequestId;
|
|
207
|
+
this.#nextRequestId = (this.#nextRequestId + 1) >>> 0; // keep within uint32
|
|
208
|
+
const it = { offset: 1 };
|
|
209
|
+
this.sharedBuffer[0] = Protocol.ROOM_REQUEST;
|
|
210
|
+
encode.number(this.sharedBuffer, requestId, it);
|
|
211
|
+
if (typeof (messageType) === "string") {
|
|
212
|
+
encode.string(this.sharedBuffer, messageType, it);
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
encode.number(this.sharedBuffer, messageType, it);
|
|
216
|
+
}
|
|
217
|
+
const headerLength = it.offset;
|
|
218
|
+
let data;
|
|
219
|
+
if (payload !== undefined) {
|
|
220
|
+
data = this.packr.pack(payload, RESERVE_START_SPACE | headerLength);
|
|
221
|
+
data.set(this.sharedBuffer.subarray(0, headerLength), 0);
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
data = this.sharedBuffer.subarray(0, headerLength);
|
|
225
|
+
}
|
|
226
|
+
const timer = setTimeout(() => {
|
|
227
|
+
this.#pendingRequests.delete(requestId);
|
|
228
|
+
reject(new Error(`request "${messageType}" timed out after ${timeoutMs}ms.`));
|
|
229
|
+
}, timeoutMs);
|
|
230
|
+
this.#pendingRequests.set(requestId, { resolve, reject, timer });
|
|
231
|
+
this.connection.send(data);
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
#rejectAllPending(reason) {
|
|
235
|
+
if (this.#pendingRequests.size === 0) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const error = new Error(reason);
|
|
239
|
+
for (const pending of this.#pendingRequests.values()) {
|
|
240
|
+
clearTimeout(pending.timer);
|
|
241
|
+
pending.reject(error);
|
|
242
|
+
}
|
|
243
|
+
this.#pendingRequests.clear();
|
|
244
|
+
}
|
|
157
245
|
sendUnreliable(type, message) {
|
|
158
246
|
// If connection is not open, skip
|
|
159
247
|
if (!this.connection.isOpen) {
|
|
160
248
|
return;
|
|
161
249
|
}
|
|
162
250
|
const it = { offset: 1 };
|
|
163
|
-
this.
|
|
251
|
+
this.sharedBuffer[0] = Protocol.ROOM_DATA;
|
|
164
252
|
if (typeof (type) === "string") {
|
|
165
|
-
encode.string(this.
|
|
253
|
+
encode.string(this.sharedBuffer, type, it);
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
encode.number(this.sharedBuffer, type, it);
|
|
257
|
+
}
|
|
258
|
+
const headerLength = it.offset;
|
|
259
|
+
let data;
|
|
260
|
+
if (message !== undefined) {
|
|
261
|
+
data = this.packr.pack(message, RESERVE_START_SPACE | headerLength);
|
|
262
|
+
data.set(this.sharedBuffer.subarray(0, headerLength), 0);
|
|
166
263
|
}
|
|
167
264
|
else {
|
|
168
|
-
|
|
265
|
+
data = this.sharedBuffer.subarray(0, headerLength);
|
|
169
266
|
}
|
|
170
|
-
// force packr to use beginning of the buffer
|
|
171
|
-
this.packr.position = 0;
|
|
172
|
-
const data = (message !== undefined)
|
|
173
|
-
? this.packr.pack(message, 2048 + it.offset) // 2048 = RESERVE_START_SPACE
|
|
174
|
-
: this.packr.buffer.subarray(0, it.offset);
|
|
175
267
|
this.connection.sendUnreliable(data);
|
|
176
268
|
}
|
|
177
269
|
sendBytes(type, bytes) {
|
|
178
270
|
const it = { offset: 1 };
|
|
179
|
-
this.
|
|
271
|
+
this.sharedBuffer[0] = Protocol.ROOM_DATA_BYTES;
|
|
180
272
|
if (typeof (type) === "string") {
|
|
181
|
-
encode.string(this.
|
|
273
|
+
encode.string(this.sharedBuffer, type, it);
|
|
182
274
|
}
|
|
183
275
|
else {
|
|
184
|
-
encode.number(this.
|
|
276
|
+
encode.number(this.sharedBuffer, type, it);
|
|
185
277
|
}
|
|
186
|
-
|
|
187
|
-
//
|
|
188
|
-
if (
|
|
189
|
-
const newBuffer = new Uint8Array(
|
|
190
|
-
newBuffer.set(this.
|
|
191
|
-
this.
|
|
278
|
+
const headerLength = it.offset;
|
|
279
|
+
// grow the scratch buffer if needed, preserving the header bytes
|
|
280
|
+
if (headerLength + bytes.byteLength > this.sharedBuffer.byteLength) {
|
|
281
|
+
const newBuffer = new Uint8Array(headerLength + bytes.byteLength);
|
|
282
|
+
newBuffer.set(this.sharedBuffer.subarray(0, headerLength));
|
|
283
|
+
this.sharedBuffer = newBuffer;
|
|
192
284
|
}
|
|
193
|
-
this.
|
|
285
|
+
this.sharedBuffer.set(bytes, headerLength);
|
|
194
286
|
// If connection is not open, buffer the message
|
|
195
287
|
if (!this.connection.isOpen) {
|
|
196
|
-
enqueueMessage(this, this.
|
|
288
|
+
enqueueMessage(this, this.sharedBuffer.subarray(0, headerLength + bytes.byteLength));
|
|
197
289
|
}
|
|
198
290
|
else {
|
|
199
|
-
this.connection.send(this.
|
|
291
|
+
this.connection.send(this.sharedBuffer.subarray(0, headerLength + bytes.byteLength));
|
|
200
292
|
}
|
|
201
293
|
}
|
|
294
|
+
/**
|
|
295
|
+
* Get the per-room input handle. Lazily created on first call and cached;
|
|
296
|
+
* subsequent calls return the same handle (options on later calls are
|
|
297
|
+
* ignored).
|
|
298
|
+
*
|
|
299
|
+
* Schema discovery, in order:
|
|
300
|
+
* 1. `options.type` — explicit constructor (overrides everything).
|
|
301
|
+
* 2. Server-sent reflection from the JOIN handshake — populated when the
|
|
302
|
+
* server room called `defineInput()`. The synthesized class has the
|
|
303
|
+
* same fields as the server's input schema; `instanceof YourInput`
|
|
304
|
+
* won't pass on it.
|
|
305
|
+
*
|
|
306
|
+
* Throws if neither source has produced a constructor.
|
|
307
|
+
*
|
|
308
|
+
* For rollback netcode, prefer `{ mode: "unreliable", delta: true,
|
|
309
|
+
* historySize: 4 }`: tiny per-tick payloads, redundancy across drops,
|
|
310
|
+
* idempotent under reordering.
|
|
311
|
+
*
|
|
312
|
+
* @example
|
|
313
|
+
* ```typescript
|
|
314
|
+
* const conn = await client.joinOrCreate<typeof FpsRoom>("fps");
|
|
315
|
+
* const input = conn.input({ mode: "unreliable" }); // type from server
|
|
316
|
+
* // each simulation tick:
|
|
317
|
+
* input.data.seq++;
|
|
318
|
+
* input.data.vx = vx;
|
|
319
|
+
* input.data.vy = vy;
|
|
320
|
+
* input.send();
|
|
321
|
+
* ```
|
|
322
|
+
*/
|
|
323
|
+
input(options) {
|
|
324
|
+
if (this.#inputHandle) {
|
|
325
|
+
return this.#inputHandle;
|
|
326
|
+
}
|
|
327
|
+
const Ctor = (options?.type ?? this.#inputCtorFromReflection);
|
|
328
|
+
if (!Ctor) {
|
|
329
|
+
throw new Error("conn.input(): no input schema available. The server room must call " +
|
|
330
|
+
"`defineInput(YourInput)`, or you can pass `{ type: YourInput }` explicitly.");
|
|
331
|
+
}
|
|
332
|
+
const instance = new Ctor();
|
|
333
|
+
const encoder = new InputEncoder(instance, options);
|
|
334
|
+
this.#inputHandle = new ClientInputHandleImpl(this, instance, encoder);
|
|
335
|
+
return this.#inputHandle;
|
|
336
|
+
}
|
|
202
337
|
get state() {
|
|
203
338
|
return this.serializer.getState();
|
|
204
339
|
}
|
|
@@ -227,9 +362,31 @@ class Room {
|
|
|
227
362
|
const serializer = getSerializer(this.serializerId);
|
|
228
363
|
this.serializer = new serializer();
|
|
229
364
|
}
|
|
230
|
-
//
|
|
231
|
-
|
|
232
|
-
|
|
365
|
+
// State reflection is length-prefixed (varint). The schema decoder
|
|
366
|
+
// runs `while (offset < bytes.byteLength)` so without a boundary
|
|
367
|
+
// it would read past the state reflection into the trailing
|
|
368
|
+
// tagged-section bytes — see Protocol.ts for the wire layout.
|
|
369
|
+
const stateReflectionLen = decode.number(buffer, it);
|
|
370
|
+
if (stateReflectionLen > 0 && this.serializer.handshake) {
|
|
371
|
+
const stateReflectionEnd = it.offset + stateReflectionLen;
|
|
372
|
+
this.serializer.handshake(buffer.subarray(0, stateReflectionEnd), it);
|
|
373
|
+
it.offset = stateReflectionEnd;
|
|
374
|
+
}
|
|
375
|
+
// Parse trailing tagged sections (forward-compatible: unknown tags
|
|
376
|
+
// are skipped via length). See HandshakeSection in shared-types.
|
|
377
|
+
while (it.offset < buffer.byteLength) {
|
|
378
|
+
const tag = buffer[it.offset++];
|
|
379
|
+
const sectionLen = decode.number(buffer, it);
|
|
380
|
+
const sectionEnd = it.offset + sectionLen;
|
|
381
|
+
if (tag === HandshakeSection.INPUT_REFLECTION) {
|
|
382
|
+
const inputDecoder = Reflection.decode(buffer.subarray(0, sectionEnd), it);
|
|
383
|
+
// Install schema-builder field descriptors on the
|
|
384
|
+
// reconstructed class so `InputEncoder` can read its
|
|
385
|
+
// `$values` and emit non-empty packets.
|
|
386
|
+
Reflection.makeEncodable(inputDecoder.state.constructor);
|
|
387
|
+
this.#inputCtorFromReflection = inputDecoder.state.constructor;
|
|
388
|
+
}
|
|
389
|
+
it.offset = sectionEnd;
|
|
233
390
|
}
|
|
234
391
|
if (this.joinedAtTime === 0) {
|
|
235
392
|
this.joinedAtTime = Date.now();
|
|
@@ -242,8 +399,8 @@ class Room {
|
|
|
242
399
|
}
|
|
243
400
|
this.reconnectionToken = `${this.roomId}:${reconnectionToken}`;
|
|
244
401
|
// acknowledge successfull JOIN_ROOM
|
|
245
|
-
this.
|
|
246
|
-
this.connection.send(this.
|
|
402
|
+
this.sharedBuffer[0] = Protocol.JOIN_ROOM;
|
|
403
|
+
this.connection.send(this.sharedBuffer.subarray(0, 1));
|
|
247
404
|
// Send any enqueued messages that were buffered while disconnected
|
|
248
405
|
if (this.reconnection.enqueuedMessages.length > 0) {
|
|
249
406
|
for (const message of this.reconnection.enqueuedMessages) {
|
|
@@ -284,6 +441,34 @@ class Room {
|
|
|
284
441
|
: decode.number(buffer, it);
|
|
285
442
|
this.dispatchMessage(type, buffer.subarray(it.offset));
|
|
286
443
|
}
|
|
444
|
+
else if (code === Protocol.ROOM_RESPONSE) {
|
|
445
|
+
// reply to a pending `request()` / `send(..., callback)`
|
|
446
|
+
const requestId = decode.number(buffer, it);
|
|
447
|
+
const status = buffer[it.offset++];
|
|
448
|
+
const pending = this.#pendingRequests.get(requestId);
|
|
449
|
+
// already settled (e.g. timed out) or unknown id — ignore
|
|
450
|
+
if (pending !== undefined) {
|
|
451
|
+
this.#pendingRequests.delete(requestId);
|
|
452
|
+
clearTimeout(pending.timer);
|
|
453
|
+
const payload = (buffer.byteLength > it.offset)
|
|
454
|
+
? unpack(buffer, { start: it.offset })
|
|
455
|
+
: undefined;
|
|
456
|
+
if (status === ResponseStatus.OK) {
|
|
457
|
+
pending.resolve(payload);
|
|
458
|
+
}
|
|
459
|
+
else {
|
|
460
|
+
// payload carries { name, message, code } from the server
|
|
461
|
+
const error = new Error(payload?.message ?? "request failed");
|
|
462
|
+
if (payload?.name) {
|
|
463
|
+
error.name = payload.name;
|
|
464
|
+
}
|
|
465
|
+
if (payload?.code !== undefined) {
|
|
466
|
+
error.code = payload.code;
|
|
467
|
+
}
|
|
468
|
+
pending.reject(error);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
287
472
|
else if (code === Protocol.PING) {
|
|
288
473
|
this.#pingCallback?.(Math.round(now() - this.#lastPingTime));
|
|
289
474
|
this.#pingCallback = undefined;
|