@genex-ai/multiplayer 0.17.10 → 0.17.11

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.
@@ -9,10 +9,10 @@ import {
9
9
  } from "./chunk-4R7C2TLH.js";
10
10
  import {
11
11
  connectAuthority
12
- } from "./chunk-SQC6WTMF.js";
13
- import "./chunk-42R7NREA.js";
12
+ } from "./chunk-I3DNVGGC.js";
13
+ import "./chunk-BB6OIWQ2.js";
14
14
  import "./chunk-YWQ3ALYY.js";
15
- import "./chunk-HHETHPGL.js";
15
+ import "./chunk-5WENVWVO.js";
16
16
  import "./chunk-OD6NRSX2.js";
17
17
  import {
18
18
  firstpersonWorld
@@ -22,7 +22,7 @@ import {
22
22
  CloseCode,
23
23
  ServerMessage,
24
24
  isReservedEventType
25
- } from "./chunk-7J3YWPCA.js";
25
+ } from "./chunk-2CSX5WA6.js";
26
26
 
27
27
  // src/bridge-authority.ts
28
28
  import { getAuthorityRegions, getAuthorityUrls, getColyseusAuth } from "@genex-ai/embed-sdk";
package/dist/bridge.js CHANGED
@@ -15,14 +15,14 @@ import {
15
15
  joinPrivate,
16
16
  matchmake,
17
17
  roomOf
18
- } from "./chunk-NSQQCLMA.js";
19
- import "./chunk-42R7NREA.js";
18
+ } from "./chunk-OESKZODQ.js";
19
+ import "./chunk-BB6OIWQ2.js";
20
20
  import {
21
21
  Limits,
22
22
  RESERVED_SHARED_PREFIX,
23
23
  ServerMessage,
24
24
  isReservedEventType
25
- } from "./chunk-7J3YWPCA.js";
25
+ } from "./chunk-2CSX5WA6.js";
26
26
 
27
27
  // src/bridge.ts
28
28
  var version = 2;
@@ -55,7 +55,7 @@ var AuthorityJoinOption = {
55
55
  * refused with 400. */
56
56
  SUCCESSOR_CLOSED: "successorClosed"
57
57
  };
58
- var AUTHORITY_SDK_LABEL = "ts/0.17.10";
58
+ var AUTHORITY_SDK_LABEL = "ts/0.17.11";
59
59
  var AuthorityCaps = {
60
60
  /** Decodes and draws §17.10's physics bodies. A physics room refuses a join without it (426). An
61
61
  * SDK sends it only once its drawing passes `physics-draw.json` and, for an engine SDK, its own
@@ -19,7 +19,7 @@ import {
19
19
  JoinError,
20
20
  Limits,
21
21
  isReservedEventType
22
- } from "./chunk-7J3YWPCA.js";
22
+ } from "./chunk-2CSX5WA6.js";
23
23
 
24
24
  // src/authority/room-key.ts
25
25
  var AuthorityRoomLimits = {
@@ -31,6 +31,7 @@ var AuthorityRoomLimits = {
31
31
  MAX_WORLDS: 16
32
32
  };
33
33
  var ROOM_NOT_FOUND = 522;
34
+ var ROOM_NOT_FOUND_AS_SENT = 500;
34
35
  var inRange = (c, lo, hi) => c >= lo.charCodeAt(0) && c <= hi.charCodeAt(0);
35
36
  var isLowerOrDigit = (c) => inRange(c, "a", "z") || inRange(c, "0", "9");
36
37
  var KEY_MARKS = "._:-";
@@ -71,7 +72,7 @@ function planAuthorityJoin(target) {
71
72
  return { method: AuthorityJoinMethod.JoinOrCreate, options };
72
73
  }
73
74
  function nextCodeRegion(error, region, regions) {
74
- const elsewhere = error === JoinError.WRONG_REGION || error === ROOM_NOT_FOUND;
75
+ const elsewhere = error === JoinError.WRONG_REGION || error === ROOM_NOT_FOUND || error === ROOM_NOT_FOUND_AS_SENT;
75
76
  return elsewhere && region + 1 < regions ? region + 1 : null;
76
77
  }
77
78
  function selectMatchWorld(worlds, world, key) {
@@ -2,7 +2,7 @@ import {
2
2
  ClientReconnection,
3
3
  JoinError,
4
4
  PROTOCOL_VERSION
5
- } from "./chunk-7J3YWPCA.js";
5
+ } from "./chunk-2CSX5WA6.js";
6
6
 
7
7
  // src/join.ts
8
8
  import { Client as Client2 } from "@colyseus/sdk";
@@ -5,7 +5,7 @@ import {
5
5
  joinOptions,
6
6
  useSdkReconnection,
7
7
  waitForFirstState
8
- } from "./chunk-42R7NREA.js";
8
+ } from "./chunk-BB6OIWQ2.js";
9
9
  import {
10
10
  udpFastOpener
11
11
  } from "./chunk-YWQ3ALYY.js";
@@ -60,7 +60,7 @@ import {
60
60
  serverTick,
61
61
  tickInstant,
62
62
  validRoomCode
63
- } from "./chunk-HHETHPGL.js";
63
+ } from "./chunk-5WENVWVO.js";
64
64
  import {
65
65
  RemotePredict
66
66
  } from "./chunk-OD6NRSX2.js";
@@ -88,11 +88,12 @@ import {
88
88
  RoomType,
89
89
  ServerMessage,
90
90
  authorityCloseCodeOf
91
- } from "./chunk-7J3YWPCA.js";
91
+ } from "./chunk-2CSX5WA6.js";
92
92
 
93
93
  // src/authority/regions.ts
94
94
  var AUTHORITY_REFUSALS_PER_REGION = 2;
95
95
  var AUTHORITY_MAX_PING_MS = 350;
96
+ var AUTHORITY_REGROUP_WAITS_MS = [2e3, 4e3, 9e3, 15e3, 3e4];
96
97
  var RegionRule = {
97
98
  /** A room with players in any region in reach, nearest first; else a new room in the nearest region. */
98
99
  Players: "players",
@@ -171,6 +172,24 @@ function refusalKind(code) {
171
172
  if (typeof code !== "number" || code <= 0 || DEAD_PATH_REFUSALS.has(code)) return RefusalKind.NextRegion;
172
173
  return CAPACITY_REFUSALS.has(code) ? RefusalKind.Capacity : RefusalKind.Final;
173
174
  }
175
+ function planAuthorityRegroup(regions, best, options = {}, joined) {
176
+ if (options.rule === RegionRule.Nearest) return [];
177
+ if (typeof options.region === "string" && regions.some((r) => r.region === options.region)) return [];
178
+ const at2 = regions.findIndex((r) => r.region === joined);
179
+ if (at2 < 0) return [];
180
+ const limit = answered(options.maxPingMs) ? options.maxPingMs : AUTHORITY_MAX_PING_MS;
181
+ return rankWithLatency(regions, best).filter((x) => x.index < at2 && x.ms !== null && x.ms <= limit).map((x) => x.pick);
182
+ }
183
+ function authorityRegionOf(regions, url) {
184
+ return regions.find((r) => r.urls.includes(url))?.region;
185
+ }
186
+ async function measureAuthorityRegions(regions, options = {}) {
187
+ const best = regions.length <= 1 ? [] : await probeAuthorityRegions(regions, options);
188
+ return regions.map((r, i) => {
189
+ const heard = (best[i] ?? []).filter(answered);
190
+ return { region: r.region, pingMs: heard.length === 0 ? null : Math.min(...heard) };
191
+ });
192
+ }
174
193
  var SearchRefusal = {
175
194
  /** No room here (Colyseus's 521, no room matches the criteria), a dead path, a full or draining
176
195
  * region: the next region is asked. */
@@ -194,9 +213,9 @@ function nextAttempt(state, regionCount, delays) {
194
213
  async function selectAuthorityRegions(regions, options = {}) {
195
214
  if (regions.length === 0) return [];
196
215
  if (regions.length === 1) return rankAuthorityRegions(regions, []);
197
- return rankAuthorityRegions(regions, await probeBest(regions, options));
216
+ return rankAuthorityRegions(regions, await probeAuthorityRegions(regions, options));
198
217
  }
199
- async function probeBest(regions, options) {
218
+ async function probeAuthorityRegions(regions, options) {
200
219
  const samples = Math.max(1, options.samples ?? RELAY_SELECT_DEFAULTS.SAMPLES);
201
220
  const plans = regions.map((r) => regionProbePlan(r.urls, samples));
202
221
  const results = await probeLatencies(plans, options);
@@ -213,7 +232,7 @@ async function probeBest(regions, options) {
213
232
  }
214
233
  async function selectAuthoritySearch(regions, search = {}, options = {}) {
215
234
  if (regions.length <= 1) return planAuthoritySearch(regions, [], search);
216
- return planAuthoritySearch(regions, await probeBest(regions, options), search);
235
+ return planAuthoritySearch(regions, await probeAuthorityRegions(regions, options), search);
217
236
  }
218
237
  async function probeLatencies(plans, options = {}) {
219
238
  const probe = options.probe ?? defaultProbe;
@@ -336,6 +355,8 @@ function survivingSession(first, deps) {
336
355
  const listeners = /* @__PURE__ */ new Map();
337
356
  let current = first;
338
357
  let left = false;
358
+ let rejoining = false;
359
+ let endWait = null;
339
360
  let physicsHash = null;
340
361
  let world = null;
341
362
  const fire = (event, ...payload) => {
@@ -348,6 +369,14 @@ function survivingSession(first, deps) {
348
369
  });
349
370
  };
350
371
  const rejoin = async (lostCode, lostReason) => {
372
+ rejoining = true;
373
+ try {
374
+ await replaceLostRoom(lostCode, lostReason);
375
+ } finally {
376
+ rejoining = false;
377
+ }
378
+ };
379
+ const replaceLostRoom = async (lostCode, lostReason) => {
351
380
  const startedAt = now();
352
381
  const lost = { roomId: current.code, matchKey: current.matchKey, closed: deps.closed };
353
382
  const regions = deps.targets.length;
@@ -398,6 +427,46 @@ function survivingSession(first, deps) {
398
427
  });
399
428
  };
400
429
  watch(first);
430
+ const regroupWait = (ms) => {
431
+ if (deps.sleep !== void 0) return deps.sleep(ms);
432
+ return new Promise((resolve) => {
433
+ const timer = setTimeout(done, ms);
434
+ timer.unref?.();
435
+ function done() {
436
+ clearTimeout(timer);
437
+ endWait = null;
438
+ resolve();
439
+ }
440
+ endWait = done;
441
+ });
442
+ };
443
+ const regroup = async (ask) => {
444
+ const waits = AUTHORITY_REGROUP_WAITS_MS;
445
+ let step = 0;
446
+ while (!left) {
447
+ await regroupWait(waits[Math.min(step, waits.length - 1)]);
448
+ if (left) return;
449
+ if (rejoining || current.players().size > 1) {
450
+ step = 0;
451
+ continue;
452
+ }
453
+ step += 1;
454
+ const next = await ask().catch(() => null);
455
+ if (next === null) continue;
456
+ if (left || rejoining || current.players().size > 1) {
457
+ await next.leave().catch(() => {
458
+ });
459
+ continue;
460
+ }
461
+ const empty = current;
462
+ adopt(next);
463
+ fire(RejoinEvent.Rejoined, { code: next.code, url: next.url, moved: true });
464
+ await empty.leave().catch(() => {
465
+ });
466
+ step = 0;
467
+ }
468
+ };
469
+ if (deps.regroup !== void 0) void regroup(deps.regroup.bind(deps));
401
470
  const own = {
402
471
  input,
403
472
  frame(frameNow) {
@@ -428,6 +497,7 @@ function survivingSession(first, deps) {
428
497
  },
429
498
  async leave() {
430
499
  left = true;
500
+ endWait?.();
431
501
  await current.leave();
432
502
  }
433
503
  };
@@ -5325,25 +5395,27 @@ function resimBudgetMs() {
5325
5395
  return /Mobi|Android|iPhone|iPad/i.test(agent) ? PHYS_RESIM_BUDGET_MS.mobile : PHYS_RESIM_BUDGET_MS.desktop;
5326
5396
  }
5327
5397
  async function joinTargets(opts, method) {
5328
- if (opts.url) return { search: [], create: [opts.url] };
5398
+ if (opts.url) return { search: [], create: [opts.url], regroup: () => [] };
5329
5399
  const regions = normalizeAuthorityRegions(opts.regions, opts.urls);
5330
5400
  if (regions.length === 0) throw new Error("genex multiplayer: `urls` must contain at least one authority server URL");
5331
5401
  const pool = method === AuthorityJoinMethod.JoinOrCreate;
5332
- const plan = await selectAuthoritySearch(
5333
- regions,
5334
- {
5335
- rule: pool ? opts.regionRule : RegionRule.Nearest,
5336
- maxPingMs: opts.maxPingMs,
5337
- ...method === AuthorityJoinMethod.JoinById ? {} : { region: opts.region }
5338
- },
5339
- opts.probe === void 0 ? {} : { probe: opts.probe }
5340
- );
5341
- return { search: plan.search.map((pick) => pick.url), create: plan.create.map((pick) => pick.url) };
5402
+ const options = {
5403
+ rule: pool ? opts.regionRule : RegionRule.Nearest,
5404
+ maxPingMs: opts.maxPingMs,
5405
+ ...method === AuthorityJoinMethod.JoinById ? {} : { region: opts.region }
5406
+ };
5407
+ const best = regions.length <= 1 ? [] : await probeAuthorityRegions(regions, opts.probe === void 0 ? {} : { probe: opts.probe });
5408
+ const plan = planAuthoritySearch(regions, best, options);
5409
+ return {
5410
+ search: plan.search.map((pick) => pick.url),
5411
+ create: plan.create.map((pick) => pick.url),
5412
+ regroup: (url) => planAuthorityRegroup(regions, best, options, authorityRegionOf(regions, url) ?? "").map((pick) => pick.url)
5413
+ };
5342
5414
  }
5343
- async function joinWherePlayersAre(opts, plan, search, targets) {
5415
+ async function joinWherePlayersAre(opts, plan, search, where) {
5344
5416
  for (const url of search) {
5345
5417
  try {
5346
- return { room: await joinOnce(new Client(url), opts, { ...plan, method: AuthorityJoinMethod.Join }), url, targets };
5418
+ return { room: await joinOnce(new Client(url), opts, { ...plan, method: AuthorityJoinMethod.Join }), url, ...where };
5347
5419
  } catch (err) {
5348
5420
  if (searchRefusalKind(errorCode(err)) === SearchRefusal.Final) explainAuthorityRefusal(err);
5349
5421
  }
@@ -5353,15 +5425,16 @@ async function joinWherePlayersAre(opts, plan, search, targets) {
5353
5425
  async function joinAuthority(opts, target) {
5354
5426
  const plan = planAuthorityJoin(target);
5355
5427
  if ("refused" in plan) throw new TypeError(`genex multiplayer: this room cannot be asked for (${plan.refused})`);
5356
- const { search, create: targets } = await joinTargets(opts, plan.method);
5357
- const found = await joinWherePlayersAre(opts, plan, search, targets);
5428
+ const { search, create: targets, regroup: regroupOf } = await joinTargets(opts, plan.method);
5429
+ const regroup = plan.method === AuthorityJoinMethod.JoinOrCreate ? regroupOf : () => [];
5430
+ const found = await joinWherePlayersAre(opts, plan, search, { targets, regroup });
5358
5431
  if (found !== null) return found;
5359
5432
  const delays = opts.capacityRetryMs ?? CAPACITY_RETRY_MS;
5360
5433
  let state = { region: 0, refused: 0, step: 0 };
5361
5434
  for (; ; ) {
5362
5435
  const url = targets[state.region];
5363
5436
  try {
5364
- return { room: await joinOnce(new Client(url), opts, plan), url, targets };
5437
+ return { room: await joinOnce(new Client(url), opts, plan), url, targets, regroup };
5365
5438
  } catch (err) {
5366
5439
  if (plan.method === AuthorityJoinMethod.JoinById) {
5367
5440
  const region = nextCodeRegion(errorCode(err), state.region, targets.length);
@@ -5467,15 +5540,33 @@ async function joinPrivateAuthority(code, opts) {
5467
5540
  function comingBack(session, opts, joined, closed) {
5468
5541
  if (opts.rejoin === false) return session;
5469
5542
  const others = joined.targets.filter((url) => url !== joined.url);
5543
+ const seatAt = async (url, plan) => {
5544
+ const room = await joinOnce(new Client(url), opts, plan);
5545
+ const next = attachAuthoritySession(useSdkReconnection(room), opts, url);
5546
+ await waitForFirstState(room, ClientReconnection.firstStateTimeoutMs);
5547
+ return next;
5548
+ };
5549
+ let here = joined.url;
5550
+ const pool = {
5551
+ method: AuthorityJoinMethod.Join,
5552
+ options: opts.matchKey === void 0 || opts.matchKey === "" ? {} : { matchKey: opts.matchKey }
5553
+ };
5554
+ const regroup = async () => {
5555
+ for (const url of joined.regroup(here)) {
5556
+ try {
5557
+ const next = await seatAt(url, pool);
5558
+ here = url;
5559
+ return next;
5560
+ } catch {
5561
+ }
5562
+ }
5563
+ return null;
5564
+ };
5470
5565
  return survivingSession(session, {
5471
5566
  closed,
5472
5567
  targets: [joined.url, ...others],
5473
- join: async (url, way, lost) => {
5474
- const room = await joinOnce(new Client(url), opts, rejoinPlan(way, lost));
5475
- const next = attachAuthoritySession(useSdkReconnection(room), opts, url);
5476
- await waitForFirstState(room, ClientReconnection.firstStateTimeoutMs);
5477
- return next;
5478
- }
5568
+ join: (url, way, lost) => seatAt(url, rejoinPlan(way, lost)),
5569
+ ...opts.regroup === false || joined.regroup(here).length === 0 ? {} : { regroup }
5479
5570
  });
5480
5571
  }
5481
5572
  function rejoinPlan(way, lost) {
@@ -6788,6 +6879,7 @@ function attachAuthoritySession(room, opts, url) {
6788
6879
  export {
6789
6880
  AUTHORITY_REFUSALS_PER_REGION,
6790
6881
  AUTHORITY_MAX_PING_MS,
6882
+ AUTHORITY_REGROUP_WAITS_MS,
6791
6883
  RegionRule,
6792
6884
  normalizeAuthorityRegions,
6793
6885
  regionProbePlan,
@@ -6796,9 +6888,13 @@ export {
6796
6888
  capacityWalk,
6797
6889
  RefusalKind,
6798
6890
  refusalKind,
6891
+ planAuthorityRegroup,
6892
+ authorityRegionOf,
6893
+ measureAuthorityRegions,
6799
6894
  SearchRefusal,
6800
6895
  searchRefusalKind,
6801
6896
  selectAuthorityRegions,
6897
+ probeAuthorityRegions,
6802
6898
  selectAuthoritySearch,
6803
6899
  REJOIN,
6804
6900
  SUCCESSOR_OPEN_MS,
@@ -8,7 +8,7 @@ import {
8
8
  waitForFirstState,
9
9
  warnProtocolRefusal,
10
10
  withoutReconnection
11
- } from "./chunk-42R7NREA.js";
11
+ } from "./chunk-BB6OIWQ2.js";
12
12
  import {
13
13
  ClientMessage,
14
14
  ClientReconnection,
@@ -23,7 +23,7 @@ import {
23
23
  SEND_TIME_KEY,
24
24
  ServerMessage,
25
25
  isReservedEventType
26
- } from "./chunk-7J3YWPCA.js";
26
+ } from "./chunk-2CSX5WA6.js";
27
27
 
28
28
  // src/index.ts
29
29
  import { getStateCallbacks } from "@colyseus/sdk";
package/dist/index.js CHANGED
@@ -3,9 +3,9 @@ import {
3
3
  createPrivate,
4
4
  joinPrivate,
5
5
  matchmake
6
- } from "./chunk-NSQQCLMA.js";
7
- import "./chunk-42R7NREA.js";
8
- import "./chunk-7J3YWPCA.js";
6
+ } from "./chunk-OESKZODQ.js";
7
+ import "./chunk-BB6OIWQ2.js";
8
+ import "./chunk-2CSX5WA6.js";
9
9
  export {
10
10
  connect,
11
11
  createPrivate,
@@ -76,7 +76,7 @@ interface AuthorityDrainNotice {
76
76
  drainMs: number;
77
77
  }
78
78
  /** The SDK label this TypeScript client sends. tests hold it to package.json. */
79
- declare const AUTHORITY_SDK_LABEL = "ts/0.17.10";
79
+ declare const AUTHORITY_SDK_LABEL = "ts/0.17.11";
80
80
  /** Client capabilities (§17.10 *Joining*). */
81
81
  declare const AuthorityCaps: {
82
82
  /** Decodes and draws §17.10's physics bodies. A physics room refuses a join without it (426). An
package/dist/protocol.js CHANGED
@@ -50,7 +50,7 @@ import {
50
50
  isReservedEventType,
51
51
  roomKeyFor,
52
52
  slugOfRoomKey
53
- } from "./chunk-7J3YWPCA.js";
53
+ } from "./chunk-2CSX5WA6.js";
54
54
  export {
55
55
  AUTHORITY_ENCODER_BUFFER_BYTES,
56
56
  AUTHORITY_MAX_FRAME_BYTES,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genex-ai/multiplayer",
3
- "version": "0.17.10",
3
+ "version": "0.17.11",
4
4
  "description": "The multiplayer client wrapper + shared room schema for genex.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",