@triformine/nexis-sdk 0.1.0 → 0.1.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/README.md +48 -0
- package/dist/cjs/client.js +746 -0
- package/dist/cjs/client.js.map +1 -0
- package/dist/cjs/codec.js +57 -0
- package/dist/cjs/codec.js.map +1 -0
- package/dist/cjs/index.js +24 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/patch.js +110 -0
- package/dist/cjs/patch.js.map +1 -0
- package/dist/cjs/rpc.js +69 -0
- package/dist/cjs/rpc.js.map +1 -0
- package/dist/cjs/types.js +6 -0
- package/dist/cjs/types.js.map +1 -0
- package/dist/esm/client.js +719 -0
- package/dist/esm/client.js.map +1 -0
- package/dist/esm/codec.js +36 -0
- package/dist/esm/codec.js.map +1 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/patch.js +86 -0
- package/dist/esm/patch.js.map +1 -0
- package/dist/esm/rpc.js +51 -0
- package/dist/esm/rpc.js.map +1 -0
- package/dist/esm/types.js +3 -0
- package/dist/esm/types.js.map +1 -0
- package/dist/types/client.d.ts +78 -0
- package/dist/types/codec.d.ts +19 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/patch.d.ts +5 -0
- package/dist/types/rpc.d.ts +14 -0
- package/dist/types/types.d.ts +75 -0
- package/package.json +31 -5
- package/src/index.ts +1 -1
- package/bun.lock +0 -111
- package/tsconfig.json +0 -14
|
@@ -0,0 +1,719 @@
|
|
|
1
|
+
import { applyPatch, computeStateChecksum, parsePatchPayload, parseSnapshotPayload } from "./patch";
|
|
2
|
+
import { JsonCodec, MsgpackCodec, codecFor } from "./codec";
|
|
3
|
+
import { RpcClient, UnknownRidError } from "./rpc";
|
|
4
|
+
const DEFAULT_VERSION = 1;
|
|
5
|
+
const DEFAULT_RECONNECT_INITIAL_DELAY_MS = 250;
|
|
6
|
+
const DEFAULT_RECONNECT_MAX_DELAY_MS = 3_000;
|
|
7
|
+
const DEFAULT_RECONNECT_MAX_ATTEMPTS = 20;
|
|
8
|
+
function normalizeConnectOptions(options) {
|
|
9
|
+
const reconnectInitialDelayMs = Math.max(50, options.reconnectInitialDelayMs ?? DEFAULT_RECONNECT_INITIAL_DELAY_MS);
|
|
10
|
+
const reconnectMaxDelayMs = Math.max(reconnectInitialDelayMs, options.reconnectMaxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS);
|
|
11
|
+
const reconnectMaxAttempts = Math.max(1, options.reconnectMaxAttempts ?? DEFAULT_RECONNECT_MAX_ATTEMPTS);
|
|
12
|
+
return {
|
|
13
|
+
...options,
|
|
14
|
+
codecs: options.codecs ?? [
|
|
15
|
+
"msgpack",
|
|
16
|
+
"json"
|
|
17
|
+
],
|
|
18
|
+
autoJoinMatchedRoom: options.autoJoinMatchedRoom ?? false,
|
|
19
|
+
autoReconnect: options.autoReconnect ?? true,
|
|
20
|
+
reconnectInitialDelayMs,
|
|
21
|
+
reconnectMaxDelayMs,
|
|
22
|
+
reconnectMaxAttempts
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function readCodecName(message) {
|
|
26
|
+
const payload = message.p;
|
|
27
|
+
if (payload && typeof payload === "object" && "codec" in payload && payload.codec === "msgpack") {
|
|
28
|
+
return "msgpack";
|
|
29
|
+
}
|
|
30
|
+
return "json";
|
|
31
|
+
}
|
|
32
|
+
function readSessionId(message) {
|
|
33
|
+
const payload = message.p;
|
|
34
|
+
if (payload && typeof payload === "object" && "session_id" in payload && typeof payload.session_id === "string") {
|
|
35
|
+
return payload.session_id;
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
function readErrorReason(message) {
|
|
40
|
+
const payload = message.p;
|
|
41
|
+
if (payload && typeof payload === "object" && "reason" in payload && typeof payload.reason === "string") {
|
|
42
|
+
return payload.reason;
|
|
43
|
+
}
|
|
44
|
+
return "server returned error";
|
|
45
|
+
}
|
|
46
|
+
function isStringArray(value) {
|
|
47
|
+
return Array.isArray(value) && value.every((item)=>typeof item === "string");
|
|
48
|
+
}
|
|
49
|
+
function deepEqual(left, right) {
|
|
50
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
51
|
+
}
|
|
52
|
+
function toJsonValue(bytes) {
|
|
53
|
+
let binary = "";
|
|
54
|
+
for (const value of bytes){
|
|
55
|
+
binary += String.fromCharCode(value);
|
|
56
|
+
}
|
|
57
|
+
return btoa(binary);
|
|
58
|
+
}
|
|
59
|
+
function fromJsonValue(value) {
|
|
60
|
+
if (typeof value !== "string") {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const decoded = atob(value);
|
|
65
|
+
const bytes = new Uint8Array(decoded.length);
|
|
66
|
+
for(let i = 0; i < decoded.length; i += 1){
|
|
67
|
+
bytes[i] = decoded.charCodeAt(i);
|
|
68
|
+
}
|
|
69
|
+
return bytes;
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function readRoomMessagePayload(payload) {
|
|
75
|
+
if (!payload || typeof payload !== "object") {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
const type = payload.type;
|
|
79
|
+
if (typeof type !== "string" && typeof type !== "number") {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
const data = payload.data;
|
|
83
|
+
return {
|
|
84
|
+
type,
|
|
85
|
+
data
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
export function parseMatchFoundPayload(payload) {
|
|
89
|
+
if (!payload || typeof payload !== "object") {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
const room = payload.room;
|
|
93
|
+
const roomType = payload.room_type;
|
|
94
|
+
const size = payload.size;
|
|
95
|
+
const participants = payload.participants;
|
|
96
|
+
if (typeof room !== "string" || typeof roomType !== "string" || typeof size !== "number" || !Number.isFinite(size) || !isStringArray(participants)) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
room,
|
|
101
|
+
roomType,
|
|
102
|
+
size,
|
|
103
|
+
participants
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
export class NexisRoom {
|
|
107
|
+
client;
|
|
108
|
+
id;
|
|
109
|
+
onStateChange;
|
|
110
|
+
constructor(client, roomId){
|
|
111
|
+
this.client = client;
|
|
112
|
+
this.id = roomId;
|
|
113
|
+
this.onStateChange = this.buildStateChangeSubscription();
|
|
114
|
+
}
|
|
115
|
+
get state() {
|
|
116
|
+
return this.client.getRoomState(this.id);
|
|
117
|
+
}
|
|
118
|
+
send(type, message) {
|
|
119
|
+
this.client.sendRoomMessage(this.id, type, message);
|
|
120
|
+
}
|
|
121
|
+
sendBytes(type, bytes) {
|
|
122
|
+
const normalized = bytes instanceof Uint8Array ? bytes : Uint8Array.from(bytes);
|
|
123
|
+
this.client.sendRoomMessageBytes(this.id, type, normalized);
|
|
124
|
+
}
|
|
125
|
+
onMessage(type, callback) {
|
|
126
|
+
return this.client.onRoomMessage(this.id, type, callback);
|
|
127
|
+
}
|
|
128
|
+
buildStateChangeSubscription() {
|
|
129
|
+
const subscribe = (callback)=>this.client.onRoomState(this.id, callback);
|
|
130
|
+
subscribe.once = (callback)=>this.client.onRoomStateOnce(this.id, callback);
|
|
131
|
+
subscribe.select = (path, callback)=>this.client.onRoomStateSelect(this.id, path, callback);
|
|
132
|
+
return subscribe;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
export class NexisClient {
|
|
136
|
+
socket;
|
|
137
|
+
url;
|
|
138
|
+
connectOptions;
|
|
139
|
+
rpc = new RpcClient();
|
|
140
|
+
codec;
|
|
141
|
+
eventHandlers = new Map();
|
|
142
|
+
stateHandlers = new Set();
|
|
143
|
+
roomStateHandlers = new Map();
|
|
144
|
+
roomStateSelectors = new Map();
|
|
145
|
+
roomMessageHandlers = new Map();
|
|
146
|
+
roomStates = new Map();
|
|
147
|
+
roomSeq = new Map();
|
|
148
|
+
roomChecksum = new Map();
|
|
149
|
+
sessionId;
|
|
150
|
+
autoJoinMatchedRoom;
|
|
151
|
+
reconnecting = false;
|
|
152
|
+
disposed = false;
|
|
153
|
+
constructor(url, socket, codec, sessionId, options){
|
|
154
|
+
this.url = url;
|
|
155
|
+
this.socket = socket;
|
|
156
|
+
this.codec = codec;
|
|
157
|
+
this.sessionId = sessionId;
|
|
158
|
+
this.connectOptions = options;
|
|
159
|
+
this.autoJoinMatchedRoom = options.autoJoinMatchedRoom;
|
|
160
|
+
this.attachSocket(socket);
|
|
161
|
+
}
|
|
162
|
+
static connect(url, options) {
|
|
163
|
+
const resolved = normalizeConnectOptions(options);
|
|
164
|
+
return NexisClient.openSocketAndHandshake(url, resolved, resolved.sessionId).then(({ socket, codec, sessionId })=>new NexisClient(url, socket, codec, sessionId, resolved));
|
|
165
|
+
}
|
|
166
|
+
close() {
|
|
167
|
+
this.disposed = true;
|
|
168
|
+
this.socket.close();
|
|
169
|
+
}
|
|
170
|
+
attachSocket(socket) {
|
|
171
|
+
this.socket = socket;
|
|
172
|
+
this.socket.addEventListener("message", (event)=>{
|
|
173
|
+
void this.onRawMessage(event.data);
|
|
174
|
+
});
|
|
175
|
+
this.socket.addEventListener("close", ()=>{
|
|
176
|
+
this.rpc.rejectAll(new Error("socket closed"));
|
|
177
|
+
if (!this.disposed && this.connectOptions.autoReconnect) {
|
|
178
|
+
void this.tryReconnect();
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
async tryReconnect() {
|
|
183
|
+
if (this.reconnecting || this.disposed) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
this.reconnecting = true;
|
|
187
|
+
this.dispatchEvent({
|
|
188
|
+
v: DEFAULT_VERSION,
|
|
189
|
+
t: "reconnect.start",
|
|
190
|
+
p: {
|
|
191
|
+
session_id: this.sessionId
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
let attempt = 0;
|
|
195
|
+
let delayMs = this.connectOptions.reconnectInitialDelayMs;
|
|
196
|
+
while(!this.disposed && attempt < this.connectOptions.reconnectMaxAttempts){
|
|
197
|
+
attempt += 1;
|
|
198
|
+
await NexisClient.wait(delayMs);
|
|
199
|
+
try {
|
|
200
|
+
const reconnect = await NexisClient.openSocketAndHandshake(this.url, this.connectOptions, this.sessionId);
|
|
201
|
+
this.codec = reconnect.codec;
|
|
202
|
+
this.sessionId = reconnect.sessionId ?? this.sessionId;
|
|
203
|
+
this.attachSocket(reconnect.socket);
|
|
204
|
+
this.dispatchEvent({
|
|
205
|
+
v: DEFAULT_VERSION,
|
|
206
|
+
t: "reconnect.ok",
|
|
207
|
+
p: {
|
|
208
|
+
attempt,
|
|
209
|
+
session_id: this.sessionId
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
this.reconnecting = false;
|
|
213
|
+
return;
|
|
214
|
+
} catch {
|
|
215
|
+
this.dispatchEvent({
|
|
216
|
+
v: DEFAULT_VERSION,
|
|
217
|
+
t: "reconnect.retry",
|
|
218
|
+
p: {
|
|
219
|
+
attempt
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
delayMs = Math.min(delayMs * 2, this.connectOptions.reconnectMaxDelayMs);
|
|
224
|
+
}
|
|
225
|
+
this.reconnecting = false;
|
|
226
|
+
this.dispatchEvent({
|
|
227
|
+
v: DEFAULT_VERSION,
|
|
228
|
+
t: "reconnect.failed",
|
|
229
|
+
p: {
|
|
230
|
+
session_id: this.sessionId
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
getSessionId() {
|
|
235
|
+
return this.sessionId;
|
|
236
|
+
}
|
|
237
|
+
room(roomId) {
|
|
238
|
+
return new NexisRoom(this, roomId);
|
|
239
|
+
}
|
|
240
|
+
async joinOrCreate(roomType, options) {
|
|
241
|
+
const roomId = typeof options?.roomId === "string" ? options.roomId : undefined;
|
|
242
|
+
const response = await this.sendRPC("room.join_or_create", {
|
|
243
|
+
roomType,
|
|
244
|
+
roomId,
|
|
245
|
+
options
|
|
246
|
+
}, roomId);
|
|
247
|
+
if (response && typeof response === "object" && typeof response.room === "string") {
|
|
248
|
+
return this.room(response.room);
|
|
249
|
+
}
|
|
250
|
+
if (roomId) {
|
|
251
|
+
return this.room(roomId);
|
|
252
|
+
}
|
|
253
|
+
return this.room(`${roomType}:default`);
|
|
254
|
+
}
|
|
255
|
+
listRooms(roomType) {
|
|
256
|
+
return this.sendRPC("room.list", roomType ? {
|
|
257
|
+
roomType
|
|
258
|
+
} : {});
|
|
259
|
+
}
|
|
260
|
+
enqueueMatchmaking(roomType, size = 2) {
|
|
261
|
+
return this.sendRPC("matchmaking.enqueue", {
|
|
262
|
+
roomType,
|
|
263
|
+
size
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
dequeueMatchmaking() {
|
|
267
|
+
return this.sendRPC("matchmaking.dequeue", {});
|
|
268
|
+
}
|
|
269
|
+
onStateChange(callback) {
|
|
270
|
+
this.stateHandlers.add(callback);
|
|
271
|
+
return ()=>this.stateHandlers.delete(callback);
|
|
272
|
+
}
|
|
273
|
+
onEvent(type, callback) {
|
|
274
|
+
const handlers = this.eventHandlers.get(type) ?? new Set();
|
|
275
|
+
handlers.add(callback);
|
|
276
|
+
this.eventHandlers.set(type, handlers);
|
|
277
|
+
return ()=>{
|
|
278
|
+
const current = this.eventHandlers.get(type);
|
|
279
|
+
if (!current) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
current.delete(callback);
|
|
283
|
+
if (current.size === 0) {
|
|
284
|
+
this.eventHandlers.delete(type);
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
onMatchFound(callback) {
|
|
289
|
+
return this.onEvent("match.found", (message)=>{
|
|
290
|
+
const parsed = parseMatchFoundPayload(message.p);
|
|
291
|
+
if (!parsed) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
callback(parsed, message);
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
sendRPC(type, payload, room) {
|
|
298
|
+
const { message, promise } = this.rpc.createRequest(type, payload, room);
|
|
299
|
+
this.sendEnvelope(message);
|
|
300
|
+
return promise;
|
|
301
|
+
}
|
|
302
|
+
getRoomState(roomId) {
|
|
303
|
+
return this.roomStates.get(roomId) ?? {};
|
|
304
|
+
}
|
|
305
|
+
sendRoomMessage(roomId, type, data) {
|
|
306
|
+
this.sendEnvelope({
|
|
307
|
+
v: DEFAULT_VERSION,
|
|
308
|
+
t: "room.message",
|
|
309
|
+
room: roomId,
|
|
310
|
+
p: {
|
|
311
|
+
type: String(type),
|
|
312
|
+
data
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
sendRoomMessageBytes(roomId, type, data) {
|
|
317
|
+
this.sendEnvelope({
|
|
318
|
+
v: DEFAULT_VERSION,
|
|
319
|
+
t: "room.message.bytes",
|
|
320
|
+
room: roomId,
|
|
321
|
+
p: {
|
|
322
|
+
type: String(type),
|
|
323
|
+
data_b64: toJsonValue(data)
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
onRoomMessage(roomId, type, callback) {
|
|
328
|
+
const key = String(type);
|
|
329
|
+
const byType = this.roomMessageHandlers.get(roomId) ?? new Map();
|
|
330
|
+
const handlers = byType.get(key) ?? new Set();
|
|
331
|
+
handlers.add(callback);
|
|
332
|
+
byType.set(key, handlers);
|
|
333
|
+
this.roomMessageHandlers.set(roomId, byType);
|
|
334
|
+
return ()=>{
|
|
335
|
+
const roomHandlers = this.roomMessageHandlers.get(roomId);
|
|
336
|
+
if (!roomHandlers) {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
const typeHandlers = roomHandlers.get(key);
|
|
340
|
+
if (!typeHandlers) {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
typeHandlers.delete(callback);
|
|
344
|
+
if (typeHandlers.size === 0) {
|
|
345
|
+
roomHandlers.delete(key);
|
|
346
|
+
}
|
|
347
|
+
if (roomHandlers.size === 0) {
|
|
348
|
+
this.roomMessageHandlers.delete(roomId);
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
onRoomState(roomId, callback) {
|
|
353
|
+
const handlers = this.roomStateHandlers.get(roomId) ?? new Set();
|
|
354
|
+
handlers.add(callback);
|
|
355
|
+
this.roomStateHandlers.set(roomId, handlers);
|
|
356
|
+
if (this.roomStates.has(roomId)) {
|
|
357
|
+
callback(this.roomStates.get(roomId) ?? {});
|
|
358
|
+
}
|
|
359
|
+
return ()=>{
|
|
360
|
+
const current = this.roomStateHandlers.get(roomId);
|
|
361
|
+
if (!current) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
current.delete(callback);
|
|
365
|
+
if (current.size === 0) {
|
|
366
|
+
this.roomStateHandlers.delete(roomId);
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
onRoomStateOnce(roomId, callback) {
|
|
371
|
+
let disposed = false;
|
|
372
|
+
const off = this.onRoomState(roomId, (state)=>{
|
|
373
|
+
if (disposed) {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
disposed = true;
|
|
377
|
+
off();
|
|
378
|
+
callback(state);
|
|
379
|
+
});
|
|
380
|
+
return ()=>{
|
|
381
|
+
disposed = true;
|
|
382
|
+
off();
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
onRoomStateSelect(roomId, path, callback) {
|
|
386
|
+
const normalizedPath = path.startsWith("/") ? path.slice(1) : path;
|
|
387
|
+
const currentState = this.getRoomState(roomId);
|
|
388
|
+
const registration = {
|
|
389
|
+
path: normalizedPath,
|
|
390
|
+
callback,
|
|
391
|
+
lastValue: currentState[normalizedPath]
|
|
392
|
+
};
|
|
393
|
+
const selectors = this.roomStateSelectors.get(roomId) ?? new Set();
|
|
394
|
+
selectors.add(registration);
|
|
395
|
+
this.roomStateSelectors.set(roomId, selectors);
|
|
396
|
+
return ()=>{
|
|
397
|
+
const current = this.roomStateSelectors.get(roomId);
|
|
398
|
+
if (!current) {
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
current.delete(registration);
|
|
402
|
+
if (current.size === 0) {
|
|
403
|
+
this.roomStateSelectors.delete(roomId);
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
sendEnvelope(message) {
|
|
408
|
+
const bytes = this.codec.encode(message);
|
|
409
|
+
this.socket.send(bytes);
|
|
410
|
+
}
|
|
411
|
+
dispatchEvent(message) {
|
|
412
|
+
const handlers = this.eventHandlers.get(message.t);
|
|
413
|
+
if (!handlers) {
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
for (const handler of handlers){
|
|
417
|
+
handler(message);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
dispatchState(roomId, nextState, prevState) {
|
|
421
|
+
for (const handler of this.stateHandlers){
|
|
422
|
+
handler(nextState);
|
|
423
|
+
}
|
|
424
|
+
const roomHandlers = this.roomStateHandlers.get(roomId);
|
|
425
|
+
if (roomHandlers) {
|
|
426
|
+
for (const handler of roomHandlers){
|
|
427
|
+
handler(nextState);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
const selectors = this.roomStateSelectors.get(roomId);
|
|
431
|
+
if (selectors) {
|
|
432
|
+
for (const registration of selectors){
|
|
433
|
+
const nextValue = nextState[registration.path];
|
|
434
|
+
const prevValue = prevState[registration.path];
|
|
435
|
+
if (!deepEqual(nextValue, prevValue)) {
|
|
436
|
+
registration.lastValue = nextValue;
|
|
437
|
+
registration.callback(nextValue, nextState);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
dispatchRoomMessage(message) {
|
|
443
|
+
if (!message.room) {
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
const payload = readRoomMessagePayload(message.p);
|
|
447
|
+
if (!payload) {
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
const byType = this.roomMessageHandlers.get(message.room);
|
|
451
|
+
if (!byType) {
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
const handlers = byType.get(String(payload.type));
|
|
455
|
+
if (!handlers) {
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
for (const handler of handlers){
|
|
459
|
+
handler(payload.data, message);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
async onRawMessage(raw) {
|
|
463
|
+
const bytes = await NexisClient.toBytes(raw);
|
|
464
|
+
if (!bytes) {
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
let message;
|
|
468
|
+
try {
|
|
469
|
+
message = this.codec.decode(bytes);
|
|
470
|
+
} catch {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
if (message.t === "rpc.response") {
|
|
474
|
+
try {
|
|
475
|
+
this.rpc.resolveResponse(message);
|
|
476
|
+
} catch (error) {
|
|
477
|
+
if (error instanceof UnknownRidError) {
|
|
478
|
+
this.dispatchEvent({
|
|
479
|
+
v: DEFAULT_VERSION,
|
|
480
|
+
t: "error",
|
|
481
|
+
p: {
|
|
482
|
+
reason: error.message
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
throw error;
|
|
488
|
+
}
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
if (message.t === "state.snapshot") {
|
|
492
|
+
if (!message.room) {
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
const snapshot = parseSnapshotPayload(message.p);
|
|
496
|
+
if (!snapshot) {
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
const computedChecksum = await computeStateChecksum(snapshot.state);
|
|
500
|
+
if (snapshot.checksum && snapshot.checksum !== computedChecksum) {
|
|
501
|
+
this.sendEnvelope({
|
|
502
|
+
v: DEFAULT_VERSION,
|
|
503
|
+
t: "state.resync",
|
|
504
|
+
room: message.room,
|
|
505
|
+
p: {
|
|
506
|
+
since: this.roomSeq.get(message.room) ?? 0
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
const checksum = snapshot.checksum ?? computedChecksum;
|
|
512
|
+
const prevState = this.roomStates.get(message.room) ?? {};
|
|
513
|
+
this.roomStates.set(message.room, snapshot.state);
|
|
514
|
+
this.roomSeq.set(message.room, snapshot.seq);
|
|
515
|
+
this.roomChecksum.set(message.room, checksum);
|
|
516
|
+
this.dispatchState(message.room, snapshot.state, prevState);
|
|
517
|
+
this.sendEnvelope({
|
|
518
|
+
v: DEFAULT_VERSION,
|
|
519
|
+
t: "state.ack",
|
|
520
|
+
room: message.room,
|
|
521
|
+
p: {
|
|
522
|
+
seq: snapshot.seq,
|
|
523
|
+
checksum
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
if (message.t === "state.patch") {
|
|
529
|
+
if (!message.room) {
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
const parsedPatch = parsePatchPayload(message.p);
|
|
533
|
+
if (!parsedPatch) {
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
const currentSeq = this.roomSeq.get(message.room) ?? 0;
|
|
537
|
+
const patchSeq = parsedPatch.seq > 0 ? parsedPatch.seq : currentSeq + 1;
|
|
538
|
+
if (patchSeq <= currentSeq) {
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
if (patchSeq > currentSeq + 1) {
|
|
542
|
+
this.sendEnvelope({
|
|
543
|
+
v: DEFAULT_VERSION,
|
|
544
|
+
t: "state.resync",
|
|
545
|
+
room: message.room,
|
|
546
|
+
p: {
|
|
547
|
+
since: currentSeq
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
const currentState = this.roomStates.get(message.room) ?? {};
|
|
553
|
+
const nextState = applyPatch(currentState, parsedPatch.ops);
|
|
554
|
+
let localChecksum;
|
|
555
|
+
if (parsedPatch.checksum) {
|
|
556
|
+
localChecksum = await computeStateChecksum(nextState);
|
|
557
|
+
if (parsedPatch.checksum !== localChecksum) {
|
|
558
|
+
this.sendEnvelope({
|
|
559
|
+
v: DEFAULT_VERSION,
|
|
560
|
+
t: "state.resync",
|
|
561
|
+
room: message.room,
|
|
562
|
+
p: {
|
|
563
|
+
since: currentSeq,
|
|
564
|
+
checksum: this.roomChecksum.get(message.room)
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
this.roomStates.set(message.room, nextState);
|
|
571
|
+
this.roomSeq.set(message.room, patchSeq);
|
|
572
|
+
if (parsedPatch.checksum) {
|
|
573
|
+
this.roomChecksum.set(message.room, parsedPatch.checksum);
|
|
574
|
+
} else if (localChecksum) {
|
|
575
|
+
this.roomChecksum.set(message.room, localChecksum);
|
|
576
|
+
}
|
|
577
|
+
this.dispatchState(message.room, nextState, currentState);
|
|
578
|
+
this.sendEnvelope({
|
|
579
|
+
v: DEFAULT_VERSION,
|
|
580
|
+
t: "state.ack",
|
|
581
|
+
room: message.room,
|
|
582
|
+
p: parsedPatch.checksum ? {
|
|
583
|
+
seq: patchSeq,
|
|
584
|
+
checksum: this.roomChecksum.get(message.room)
|
|
585
|
+
} : {
|
|
586
|
+
seq: patchSeq
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
if (this.autoJoinMatchedRoom && message.t === "match.found") {
|
|
592
|
+
const parsed = parseMatchFoundPayload(message.p);
|
|
593
|
+
if (parsed) {
|
|
594
|
+
void this.joinOrCreate(parsed.roomType, {
|
|
595
|
+
roomId: parsed.room
|
|
596
|
+
}).catch(()=>undefined);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
if (message.t === "room.message" && message.room) {
|
|
600
|
+
this.dispatchRoomMessage(message);
|
|
601
|
+
}
|
|
602
|
+
this.dispatchEvent(message);
|
|
603
|
+
}
|
|
604
|
+
static openSocketAndHandshake(url, options, sessionIdOverride) {
|
|
605
|
+
const socket = new WebSocket(url);
|
|
606
|
+
const jsonCodec = new JsonCodec();
|
|
607
|
+
const msgpackCodec = new MsgpackCodec();
|
|
608
|
+
return new Promise((resolve, reject)=>{
|
|
609
|
+
let settled = false;
|
|
610
|
+
const handshakeSessionId = sessionIdOverride ?? options.sessionId;
|
|
611
|
+
const onOpen = ()=>{
|
|
612
|
+
const handshake = {
|
|
613
|
+
v: DEFAULT_VERSION,
|
|
614
|
+
codecs: options.codecs,
|
|
615
|
+
project_id: options.projectId?.trim() || "anonymous",
|
|
616
|
+
token: options.token?.trim() || "",
|
|
617
|
+
session_id: handshakeSessionId
|
|
618
|
+
};
|
|
619
|
+
socket.send(JSON.stringify(handshake));
|
|
620
|
+
};
|
|
621
|
+
const onError = ()=>{
|
|
622
|
+
finishReject(new Error("socket connection failed"));
|
|
623
|
+
};
|
|
624
|
+
const onClose = ()=>{
|
|
625
|
+
finishReject(new Error("socket closed before handshake"));
|
|
626
|
+
};
|
|
627
|
+
const onMessage = async (event)=>{
|
|
628
|
+
try {
|
|
629
|
+
const message = await NexisClient.decodeHandshakeMessage(event.data, jsonCodec, msgpackCodec);
|
|
630
|
+
if (!message) {
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
if (message.t === "error") {
|
|
634
|
+
finishReject(new Error(readErrorReason(message)));
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
if (message.t !== "handshake.ok") {
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
const negotiatedCodec = readCodecName(message);
|
|
641
|
+
const sessionId = readSessionId(message) ?? handshakeSessionId;
|
|
642
|
+
finishResolve({
|
|
643
|
+
socket,
|
|
644
|
+
codec: codecFor(negotiatedCodec),
|
|
645
|
+
sessionId
|
|
646
|
+
});
|
|
647
|
+
} catch (error) {
|
|
648
|
+
finishReject(new Error(`handshake decode failed: ${String(error)}`));
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
const cleanup = ()=>{
|
|
652
|
+
socket.removeEventListener("open", onOpen);
|
|
653
|
+
socket.removeEventListener("error", onError);
|
|
654
|
+
socket.removeEventListener("close", onClose);
|
|
655
|
+
socket.removeEventListener("message", onMessage);
|
|
656
|
+
};
|
|
657
|
+
const finishResolve = (result)=>{
|
|
658
|
+
if (settled) {
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
settled = true;
|
|
662
|
+
cleanup();
|
|
663
|
+
resolve(result);
|
|
664
|
+
};
|
|
665
|
+
const finishReject = (error)=>{
|
|
666
|
+
if (settled) {
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
settled = true;
|
|
670
|
+
cleanup();
|
|
671
|
+
reject(error);
|
|
672
|
+
};
|
|
673
|
+
socket.addEventListener("open", onOpen);
|
|
674
|
+
socket.addEventListener("error", onError);
|
|
675
|
+
socket.addEventListener("close", onClose);
|
|
676
|
+
socket.addEventListener("message", onMessage);
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
static wait(ms) {
|
|
680
|
+
return new Promise((resolve)=>setTimeout(resolve, ms));
|
|
681
|
+
}
|
|
682
|
+
static async decodeHandshakeMessage(raw, jsonCodec, msgpackCodec) {
|
|
683
|
+
if (typeof raw === "string") {
|
|
684
|
+
return JSON.parse(raw);
|
|
685
|
+
}
|
|
686
|
+
const bytes = await NexisClient.toBytes(raw);
|
|
687
|
+
if (!bytes) {
|
|
688
|
+
return null;
|
|
689
|
+
}
|
|
690
|
+
try {
|
|
691
|
+
return msgpackCodec.decode(bytes);
|
|
692
|
+
} catch {
|
|
693
|
+
return jsonCodec.decode(bytes);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
static async toBytes(raw) {
|
|
697
|
+
if (raw instanceof Uint8Array) {
|
|
698
|
+
return raw;
|
|
699
|
+
}
|
|
700
|
+
if (raw instanceof ArrayBuffer) {
|
|
701
|
+
return new Uint8Array(raw);
|
|
702
|
+
}
|
|
703
|
+
if (raw instanceof Blob) {
|
|
704
|
+
return new Uint8Array(await raw.arrayBuffer());
|
|
705
|
+
}
|
|
706
|
+
if (typeof raw === "string") {
|
|
707
|
+
return new TextEncoder().encode(raw);
|
|
708
|
+
}
|
|
709
|
+
return null;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
export async function connect(url, options) {
|
|
713
|
+
return NexisClient.connect(url, options);
|
|
714
|
+
}
|
|
715
|
+
export function decodeRoomBytes(payload) {
|
|
716
|
+
return fromJsonValue(payload);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
//# sourceMappingURL=client.js.map
|