@irtio/client 0.6.0 → 0.8.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/dist/index.js CHANGED
@@ -2,12 +2,13 @@ import {
2
2
  ClientStore,
3
3
  E_CONNECT_FAILED,
4
4
  MAX_PREDICTED_BODIES,
5
+ MAX_PROXY_BODIES,
5
6
  PREDICTION_EPSILON,
6
7
  RESIM_DEPTH,
7
8
  SMOOTHING_HALF_LIFE_MS,
8
9
  SMOOTHING_SNAP_UNITS,
9
10
  emptyPredictionStats
10
- } from "./chunk-6J6PKFUS.js";
11
+ } from "./chunk-5Z4DHUA3.js";
11
12
 
12
13
  // src/index.ts
13
14
  import { EMPTY_PROFILE as EMPTY_PROFILE2 } from "@irtio/protocol";
@@ -495,6 +496,23 @@ async function errorFrom(res) {
495
496
  return new IdentityError(code, message);
496
497
  }
497
498
 
499
+ // src/messages.ts
500
+ import { encodeFields } from "@irtio/schema";
501
+ function buildMessages(schema, session) {
502
+ const out = {};
503
+ for (const desc of schema?.messages ?? []) {
504
+ out[desc.name] = {
505
+ send(target, value) {
506
+ session.typedMessage(desc.index, target, encodeFields(desc.fields, value));
507
+ },
508
+ on(cb) {
509
+ return session.onTypedMessage(desc.index, cb);
510
+ }
511
+ };
512
+ }
513
+ return out;
514
+ }
515
+
498
516
  // src/session.ts
499
517
  import {
500
518
  FrameType,
@@ -529,7 +547,7 @@ import {
529
547
  decodeDelta,
530
548
  decodeDeltaFrom,
531
549
  decodeFields,
532
- encodeFields,
550
+ encodeFields as encodeFields2,
533
551
  schemaFromCanonical
534
552
  } from "@irtio/schema";
535
553
 
@@ -764,6 +782,57 @@ var RenderStore = class {
764
782
  const alpha = span > 0 ? (renderT - a.t) / span : 1;
765
783
  return lerpRecord(desc, a.value, b.value, Math.min(1, Math.max(0, alpha)));
766
784
  }
785
+ /**
786
+ * M6 lane D (D71): the body-channel values the renderer is drawing for one instance right now,
787
+ * written into `into`. `true` when there is a pose to draw at all.
788
+ *
789
+ * This is what a kinematic proxy is moved to before every local step, and it deliberately does
790
+ * **not** go through `get`. `get` asks the predictor first — which would recurse, since it calls
791
+ * `frame()` — and for an owned or predicted instance it answers out of the local world, which is
792
+ * the one answer a proxy must never be given (a proxy driven by the local world is a body driving
793
+ * itself). So this is the D20 buffer and nothing else: the authoritative pose interpolated at
794
+ * `interpDelayMs` behind arrival, held at the newest delta and never extrapolated past it.
795
+ *
796
+ * Allocation-free, and only the physics channels: it runs once per proxy per frame.
797
+ *
798
+ * Two things `get` does are left out on purpose. `starved` is not counted, because that number
799
+ * belongs to the render path and a physics read landing in it would double it. And a buffer whose
800
+ * entity has aged out is not `gc`'d here, because a read that drives physics must not decide when
801
+ * the render path's buffers die.
802
+ */
803
+ drawn(desc, id, into) {
804
+ const physics = desc.physics;
805
+ if (!physics) return false;
806
+ const coll = this.store.plain[desc.name];
807
+ const fill = (value) => {
808
+ if (value === void 0) return false;
809
+ for (const [, field] of physics.channels) {
810
+ const v = value[field];
811
+ if (typeof v === "number") into[field] = v;
812
+ }
813
+ return true;
814
+ };
815
+ if (!desc.interpolate) return fill(coll.get(id));
816
+ const buffer = this.buffers.get(desc.name)?.get(id);
817
+ if (!buffer || buffer.frames.length === 0) return fill(coll.get(id));
818
+ const renderT = this.renderTime();
819
+ if (buffer.removedAt !== void 0 && renderT >= buffer.removedAt) return false;
820
+ this.prune(buffer, renderT);
821
+ const frames = buffer.frames;
822
+ const a = frames[0];
823
+ if (renderT < a.t) return false;
824
+ const b = frames[1];
825
+ if (!b) return fill(a.value);
826
+ const span = b.t - a.t;
827
+ const alpha = span > 0 ? Math.min(1, Math.max(0, (renderT - a.t) / span)) : 1;
828
+ for (const [, field] of physics.channels) {
829
+ const av = a.value[field];
830
+ if (typeof av !== "number") continue;
831
+ const bv = b.value[field];
832
+ into[field] = typeof bv === "number" ? av + (bv - av) * alpha : av;
833
+ }
834
+ return true;
835
+ }
767
836
  /** Is `collection[id]` visible at the render clock? */
768
837
  has(desc, id) {
769
838
  const coll = this.store.plain[desc.name];
@@ -1038,6 +1107,7 @@ var Session = class {
1038
1107
  if (this.left) return;
1039
1108
  this.predictor = predictor;
1040
1109
  this.render.attachPredictor(predictor);
1110
+ predictor.setDrawnReader((desc, id, into) => this.render.drawn(desc, id, into));
1041
1111
  if (this.joined) {
1042
1112
  predictor.reset();
1043
1113
  void predictor.start();
@@ -1047,7 +1117,7 @@ var Session = class {
1047
1117
  const physics2d = options.physics2d;
1048
1118
  if (physics && hasPhysics) {
1049
1119
  this.predictionRequested = true;
1050
- void import("./physics-S5LHL3HK.js").then(({ PhysicsPredictor }) => {
1120
+ void import("./physics-4SGKNBAC.js").then(({ PhysicsPredictor }) => {
1051
1121
  if (this.left) return;
1052
1122
  attach(
1053
1123
  new PhysicsPredictor(
@@ -1060,15 +1130,32 @@ var Session = class {
1060
1130
  )
1061
1131
  );
1062
1132
  });
1063
- } else if (physics2d && hasPhysics) {
1133
+ } else if (physics2d !== void 0 && physics2d.engine === "rapier2d" && hasPhysics) {
1134
+ const rapier2d = physics2d;
1135
+ this.predictionRequested = true;
1136
+ void import("./physics-rapier2d-TBOAJMVM.js").then(({ Rapier2dPredictor }) => {
1137
+ if (this.left) return;
1138
+ attach(
1139
+ new Rapier2dPredictor(
1140
+ this.ext,
1141
+ this.store,
1142
+ rapier2d,
1143
+ () => this.me,
1144
+ () => this.rtt,
1145
+ () => this.tickIntervalMs
1146
+ )
1147
+ );
1148
+ });
1149
+ } else if (physics2d !== void 0 && physics2d.engine !== "rapier2d" && hasPhysics) {
1150
+ const matter2d = physics2d;
1064
1151
  this.predictionRequested = true;
1065
- void import("./physics2d-ZV2IRRRZ.js").then(({ Physics2dPredictor }) => {
1152
+ void import("./physics2d-CNSEWKP4.js").then(({ Physics2dPredictor }) => {
1066
1153
  if (this.left) return;
1067
1154
  attach(
1068
1155
  new Physics2dPredictor(
1069
1156
  this.ext,
1070
1157
  this.store,
1071
- physics2d,
1158
+ matter2d,
1072
1159
  () => this.me,
1073
1160
  () => this.rtt,
1074
1161
  () => this.tickIntervalMs
@@ -1102,6 +1189,9 @@ var Session = class {
1102
1189
  * whatever a `SCHEMA` frame replaced it with. Read instead of `options.schema` everywhere, so a
1103
1190
  * swapped session announces its *current* hash when it reconnects rather than the one it was
1104
1191
  * born with — otherwise a resume after a swap would be refused as a mismatch.
1192
+ *
1193
+ * D70 made it internally readable rather than private: `buildMessages` reads the declared
1194
+ * shapes off it when the room object is built. Still not on the public `Room` type.
1105
1195
  */
1106
1196
  schema;
1107
1197
  /** D50: how many times this session has swapped schema. Test seam and a diagnostic. */
@@ -1165,6 +1255,20 @@ var Session = class {
1165
1255
  * `INTERNAL_SESSION`, the same route the D50 tests use for the session itself.
1166
1256
  */
1167
1257
  voiceListeners = /* @__PURE__ */ new Set();
1258
+ /**
1259
+ * D70: typed-message listeners, by wire index, in their own map for the same structural reason
1260
+ * `voiceListeners` is its own set: a raw `onMessage` callback must never be handed a typed
1261
+ * frame and a typed callback must never be handed raw bytes. Splitting the registries makes
1262
+ * that a fact about the shape of the code rather than a filter somewhere in the fan-out.
1263
+ */
1264
+ typedListeners = /* @__PURE__ */ new Map();
1265
+ /** D70: one warning per session for dropped typed messages — a flood must stay one line. */
1266
+ typedDropWarned = false;
1267
+ /**
1268
+ * D70: what `room.stats.messages` reads. `dropped` is the one worth watching: it counts typed
1269
+ * frames this client could not decode, which is a peer on a schema this one does not have.
1270
+ */
1271
+ messageCounts = { sent: 0, received: 0, dropped: 0 };
1168
1272
  cancelFlush;
1169
1273
  cancelPing;
1170
1274
  cancelRetry;
@@ -1628,15 +1732,62 @@ var Session = class {
1628
1732
  if (pong.serverTick > this.tick) this.tick = pong.serverTick;
1629
1733
  }
1630
1734
  onMsg(payload) {
1631
- const msg = decodeMsg(payload);
1735
+ let msg;
1736
+ try {
1737
+ msg = decodeMsg(payload);
1738
+ } catch (err) {
1739
+ this.dropTypedMessage(err);
1740
+ return;
1741
+ }
1632
1742
  const bytes = msg.payload.slice();
1633
1743
  if (msg.target.kind === "voice") {
1634
1744
  for (const cb of [...this.voiceListeners]) cb(bytes);
1635
1745
  return;
1636
1746
  }
1637
1747
  const from = msg.target.kind === "client" ? msg.target.clientId : "server";
1748
+ if (msg.typed) {
1749
+ this.deliverTyped(msg.typed.index, from, bytes);
1750
+ return;
1751
+ }
1752
+ this.messageCounts.received++;
1638
1753
  for (const cb of [...this.messageListeners]) cb(from, bytes);
1639
1754
  }
1755
+ /**
1756
+ * D70: decode one typed payload against the current schema and hand it to that shape's
1757
+ * listeners.
1758
+ *
1759
+ * Everything a hostile peer can do here ends in the same place: a counter and a dropped frame.
1760
+ * An index past the schema's list, a truncated payload, an oversize `str`, an over-max `list`,
1761
+ * an unknown enum member, or a well-formed value of the wrong shape — each is caught, none
1762
+ * throws out of the frame loop, none closes the socket, and the next frame is delivered
1763
+ * normally. The `console.warn` fires once per session so a flood stays one line.
1764
+ */
1765
+ deliverTyped(index, from, bytes) {
1766
+ const desc = this.schema?.messages?.[index];
1767
+ if (!desc) {
1768
+ this.dropTypedMessage(new Error(`no message with index ${index} in this schema`));
1769
+ return;
1770
+ }
1771
+ let value;
1772
+ try {
1773
+ value = decodeFields(desc.fields, bytes);
1774
+ } catch (err) {
1775
+ this.dropTypedMessage(err);
1776
+ return;
1777
+ }
1778
+ this.messageCounts.received++;
1779
+ const set = this.typedListeners.get(index);
1780
+ if (!set) return;
1781
+ for (const cb of [...set]) cb(from, value);
1782
+ }
1783
+ dropTypedMessage(err) {
1784
+ this.messageCounts.dropped++;
1785
+ if (this.typedDropWarned) return;
1786
+ this.typedDropWarned = true;
1787
+ console.warn(
1788
+ `irtio: dropped a typed message this client could not read (${err instanceof Error ? err.message : String(err)}). Read room.stats.messages.dropped for the count; further drops are silent.`
1789
+ );
1790
+ }
1640
1791
  // -------------------------------------------------------------------------
1641
1792
  // Timers
1642
1793
  // -------------------------------------------------------------------------
@@ -1707,7 +1858,7 @@ var Session = class {
1707
1858
  let encoded;
1708
1859
  try {
1709
1860
  desc = this.descOf(name);
1710
- encoded = encodeFields(desc.params, params);
1861
+ encoded = encodeFields2(desc.params, params);
1711
1862
  } catch (err) {
1712
1863
  return Promise.reject(err instanceof Error ? err : new Error(String(err)));
1713
1864
  }
@@ -1728,7 +1879,10 @@ var Session = class {
1728
1879
  reject,
1729
1880
  cancelTimeout
1730
1881
  });
1731
- this.send(FrameType.CALL, encodeCall({ reqId, rpcId: desc.index, params: encoded }));
1882
+ this.send(
1883
+ FrameType.CALL,
1884
+ encodeCall({ reqId, rpcId: desc.index, clientTick: this.tick, params: encoded })
1885
+ );
1732
1886
  });
1733
1887
  }
1734
1888
  async requestOwnership(entity, id) {
@@ -1802,7 +1956,7 @@ var Session = class {
1802
1956
  replyOk(desc, reqId, result) {
1803
1957
  let bytes;
1804
1958
  try {
1805
- bytes = desc.returns ? encodeFields(desc.returns, result ?? {}) : new Uint8Array(0);
1959
+ bytes = desc.returns ? encodeFields2(desc.returns, result ?? {}) : new Uint8Array(0);
1806
1960
  } catch (err) {
1807
1961
  this.replyError(reqId, err instanceof Error ? err.message : String(err));
1808
1962
  return;
@@ -1824,6 +1978,7 @@ var Session = class {
1824
1978
  // -------------------------------------------------------------------------
1825
1979
  message(target, bytes) {
1826
1980
  const wire = target === "all" ? { kind: "all" } : typeof target === "string" ? { kind: "client", clientId: target } : { kind: "role", role: target.role };
1981
+ this.messageCounts.sent++;
1827
1982
  this.send(FrameType.MSG, encodeMsg({ target: wire, payload: bytes }));
1828
1983
  }
1829
1984
  onMessage(cb) {
@@ -1832,6 +1987,27 @@ var Session = class {
1832
1987
  this.messageListeners.delete(cb);
1833
1988
  };
1834
1989
  }
1990
+ /**
1991
+ * D70: send one typed message. `payload` is already `encodeFields`'d by the caller
1992
+ * (`buildMessages`), which is where the shape and the throw on a bad value belong.
1993
+ */
1994
+ typedMessage(index, target, payload) {
1995
+ const wire = target === "all" ? { kind: "all" } : typeof target === "string" ? { kind: "client", clientId: target } : { kind: "role", role: target.role };
1996
+ this.messageCounts.sent++;
1997
+ this.send(FrameType.MSG, encodeMsg({ target: wire, payload, typed: { index } }));
1998
+ }
1999
+ /** D70: subscribe to one message shape by wire index. Raw listeners never see these frames. */
2000
+ onTypedMessage(index, cb) {
2001
+ let set = this.typedListeners.get(index);
2002
+ if (!set) {
2003
+ set = /* @__PURE__ */ new Set();
2004
+ this.typedListeners.set(index, set);
2005
+ }
2006
+ set.add(cb);
2007
+ return () => {
2008
+ set?.delete(cb);
2009
+ };
2010
+ }
1835
2011
  /**
1836
2012
  * D51: write one voice signaling message. Internal — reached only through `INTERNAL_SESSION`,
1837
2013
  * so it is not on the `Room` type and a game cannot call it.
@@ -2032,6 +2208,233 @@ function isRoomFull(err) {
2032
2208
  const message = err instanceof Error ? err.message : String(err);
2033
2209
  return message.startsWith("E_ROOM_FULL");
2034
2210
  }
2211
+ async function findPublic(project, options = {}) {
2212
+ const controlUrl = (options.controlUrl ?? DEFAULT_CONTROL_URL).replace(/\/+$/, "");
2213
+ const fetchImpl = options.fetch ?? fetch;
2214
+ let res;
2215
+ try {
2216
+ res = await fetchImpl(`${controlUrl}/match`, {
2217
+ method: "POST",
2218
+ headers: { "content-type": "application/json" },
2219
+ body: JSON.stringify({
2220
+ project,
2221
+ mode: "public",
2222
+ ...options.queue !== void 0 ? { queue: options.queue } : {},
2223
+ ...options.identity !== void 0 ? { identity: options.identity } : {},
2224
+ ...options.exclude !== void 0 ? { exclude: options.exclude } : {}
2225
+ })
2226
+ });
2227
+ } catch (err) {
2228
+ throw new MatchError(
2229
+ "E_MATCH_UNREACHABLE",
2230
+ `cannot reach the matchmaker at ${controlUrl}: ${err instanceof Error ? err.message : String(err)}`
2231
+ );
2232
+ }
2233
+ const text = await res.text().catch(() => "");
2234
+ let body = {};
2235
+ try {
2236
+ body = JSON.parse(text);
2237
+ } catch {
2238
+ }
2239
+ if (!res.ok) {
2240
+ throw new MatchError(
2241
+ typeof body.code === "string" ? body.code : `E_HTTP_${res.status}`,
2242
+ typeof body.message === "string" ? body.message : `match failed with status ${res.status}`
2243
+ );
2244
+ }
2245
+ if (body.status !== "matched" || typeof body.room !== "string") {
2246
+ throw new MatchError("E_MATCH_MALFORMED", "the matchmaker answered something unexpected");
2247
+ }
2248
+ return {
2249
+ room: body.room,
2250
+ queue: typeof body.queue === "string" ? body.queue : "default",
2251
+ size: typeof body.size === "number" ? body.size : 0,
2252
+ created: body.created === true
2253
+ };
2254
+ }
2255
+ async function joinPublic(schema, options = {}) {
2256
+ const project = schema.project;
2257
+ const key = options.key ?? (typeof project === "string" ? project : void 0);
2258
+ if (key === void 0 || key === "") {
2259
+ throw new MatchError(
2260
+ "E_MATCH_NO_PROJECT",
2261
+ "joinPublic needs a project key: build your schema with `irtio` so it carries one, or pass { key }"
2262
+ );
2263
+ }
2264
+ const identity = options.identity === true ? new Identity({
2265
+ project: key,
2266
+ controlUrl: options.controlUrl,
2267
+ fetch: options.fetch
2268
+ }) : void 0;
2269
+ const ask = async (exclude) => findPublic(key, {
2270
+ ...options.queue !== void 0 ? { queue: options.queue } : {},
2271
+ ...options.controlUrl !== void 0 ? { controlUrl: options.controlUrl } : {},
2272
+ ...options.fetch !== void 0 ? { fetch: options.fetch } : {},
2273
+ ...identity !== void 0 ? { identity: await identity.ensure() } : {},
2274
+ ...exclude !== void 0 ? { exclude } : {}
2275
+ });
2276
+ const ticket = await ask();
2277
+ try {
2278
+ return await joinRoom(schema, { ...options, room: ticket.room });
2279
+ } catch (err) {
2280
+ if (!isRetryablePublicJoin(err)) throw err;
2281
+ const second = await ask(ticket.room);
2282
+ return joinRoom(schema, { ...options, room: second.room });
2283
+ }
2284
+ }
2285
+ function isRetryablePublicJoin(err) {
2286
+ const message = err instanceof Error ? err.message : String(err);
2287
+ return message.startsWith("E_ROOM_FULL") || message.startsWith("E_ROOM_NOT_FOUND");
2288
+ }
2289
+
2290
+ // src/lobby.ts
2291
+ var LOBBY_STATE = "irtLobby";
2292
+ var LOBBY_MEMBERS = "irtLobbyMembers";
2293
+ var LOBBY_POLL_MS = 120;
2294
+ function hasLobby(room) {
2295
+ const state = room?.state;
2296
+ return typeof state === "object" && state !== null && state[LOBBY_STATE] !== void 0 && state[LOBBY_MEMBERS] !== void 0;
2297
+ }
2298
+ function lobbyOf(room, options = {}) {
2299
+ if (!hasLobby(room)) return void 0;
2300
+ const r = room;
2301
+ const singleton = () => r.state[LOBBY_STATE] ?? {};
2302
+ const members = () => r.state[LOBBY_MEMBERS];
2303
+ const players = () => r.clients.map((c) => ({
2304
+ clientId: c.clientId,
2305
+ ready: members().get(c.clientId)?.ready === true,
2306
+ connected: c.connected !== false,
2307
+ me: c.clientId === r.me
2308
+ }));
2309
+ const signature = () => {
2310
+ const s = singleton();
2311
+ let out = `${s.phase ?? ""}|${s.public === true ? 1 : 0}|${s.readyUi === true ? 1 : 0}|${s.capacity ?? 0}`;
2312
+ for (const p of players()) out += `|${p.clientId}${p.ready ? 1 : 0}${p.connected ? 1 : 0}`;
2313
+ return out;
2314
+ };
2315
+ return {
2316
+ get phase() {
2317
+ return singleton().phase === "started" ? "started" : "lobby";
2318
+ },
2319
+ get players() {
2320
+ return players();
2321
+ },
2322
+ get public() {
2323
+ return singleton().public === true;
2324
+ },
2325
+ get readyUi() {
2326
+ return singleton().readyUi === true;
2327
+ },
2328
+ get capacity() {
2329
+ return singleton().capacity ?? 0;
2330
+ },
2331
+ ready(value) {
2332
+ const mine = members().get(r.me);
2333
+ if (!mine) return;
2334
+ mine.ready = value === true;
2335
+ r.flush();
2336
+ },
2337
+ ...options.setPublic !== void 0 ? { setPublic: options.setPublic } : {},
2338
+ on(event, cb) {
2339
+ if (event !== "change") return () => {
2340
+ };
2341
+ let last = signature();
2342
+ const sample = () => {
2343
+ const next = signature();
2344
+ if (next === last) return;
2345
+ last = next;
2346
+ cb();
2347
+ };
2348
+ const offClients = r.on("clients", sample);
2349
+ const timer = setInterval(sample, LOBBY_POLL_MS);
2350
+ timer.unref?.();
2351
+ return () => {
2352
+ offClients();
2353
+ clearInterval(timer);
2354
+ };
2355
+ }
2356
+ };
2357
+ }
2358
+ function attachable(room, view) {
2359
+ return {
2360
+ get id() {
2361
+ return room.id;
2362
+ },
2363
+ get link() {
2364
+ return room.link;
2365
+ },
2366
+ get status() {
2367
+ return room.status;
2368
+ },
2369
+ get clients() {
2370
+ return room.clients;
2371
+ },
2372
+ get maxClients() {
2373
+ return room.maxClients;
2374
+ },
2375
+ get rtt() {
2376
+ return room.rtt;
2377
+ },
2378
+ on: (event, cb) => room.on(event, cb),
2379
+ ...view !== void 0 ? { lobby: view } : {}
2380
+ };
2381
+ }
2382
+ function attachLobby(element, schema, options = {}) {
2383
+ let detachRoom;
2384
+ let busy = false;
2385
+ const fail = (err) => {
2386
+ element.removeAttribute("searching");
2387
+ if (options.onError) {
2388
+ options.onError(err);
2389
+ return;
2390
+ }
2391
+ queueMicrotask(() => {
2392
+ throw err;
2393
+ });
2394
+ };
2395
+ const landed = (room) => {
2396
+ element.removeAttribute("searching");
2397
+ const view = lobbyOf(room, {
2398
+ ...options.setPublic !== void 0 ? { setPublic: (value) => options.setPublic?.(room, value) } : {}
2399
+ });
2400
+ detachRoom = element.attach(
2401
+ attachable(room, view)
2402
+ );
2403
+ options.onRoom?.(room);
2404
+ };
2405
+ const run = async (make) => {
2406
+ if (busy) return;
2407
+ busy = true;
2408
+ try {
2409
+ landed(await make());
2410
+ } catch (err) {
2411
+ fail(err);
2412
+ } finally {
2413
+ busy = false;
2414
+ }
2415
+ };
2416
+ const onPrivate = () => {
2417
+ void run(() => joinRoom(schema, { ...options.join ?? {} }));
2418
+ };
2419
+ const onQuickMatch = (event) => {
2420
+ const detail = event.detail;
2421
+ element.setAttribute("searching", "");
2422
+ void run(
2423
+ () => joinPublic(schema, {
2424
+ ...options.join ?? {},
2425
+ ...options.match ?? {},
2426
+ ...detail?.queue !== void 0 ? { queue: detail.queue } : {}
2427
+ })
2428
+ );
2429
+ };
2430
+ element.addEventListener("private", onPrivate);
2431
+ element.addEventListener("quickmatch", onQuickMatch);
2432
+ return () => {
2433
+ element.removeEventListener("private", onPrivate);
2434
+ element.removeEventListener("quickmatch", onQuickMatch);
2435
+ detachRoom?.();
2436
+ };
2437
+ }
2035
2438
 
2036
2439
  // src/voice.ts
2037
2440
  import {
@@ -2403,6 +2806,8 @@ async function joinRoom(schema, options = {}) {
2403
2806
  }
2404
2807
  function makeRoom(session) {
2405
2808
  const call = callProxy(session);
2809
+ const messages = buildMessages(session.schema, session);
2810
+ const stats = { messages: session.messageCounts };
2406
2811
  const room = {
2407
2812
  get me() {
2408
2813
  return session.me;
@@ -2446,6 +2851,8 @@ function makeRoom(session) {
2446
2851
  return session.predictor?.ready ?? false;
2447
2852
  },
2448
2853
  predicts: (collection, id) => session.predictor?.has(collection, id) ?? false,
2854
+ // ---- M6 lane D: proxies ----
2855
+ proxied: (collection, id) => session.predictor?.hasProxy(collection, id) ?? false,
2449
2856
  get stats() {
2450
2857
  return session.predictor?.stats ?? emptyPredictionStats();
2451
2858
  }
@@ -2455,6 +2862,8 @@ function makeRoom(session) {
2455
2862
  requestOwnership: (entity, id) => session.requestOwnership(entity, id),
2456
2863
  message: (target, bytes) => session.message(target, bytes),
2457
2864
  onMessage: (cb) => session.onMessage(cb),
2865
+ messages,
2866
+ stats,
2458
2867
  on: (event, cb) => session.on(event, cb),
2459
2868
  flush: () => session.flush(),
2460
2869
  leave: () => session.leave()
@@ -2468,7 +2877,11 @@ async function joinRelay(options = {}) {
2468
2877
  const fromLocation = !explicitRoom && currentLocation() !== void 0;
2469
2878
  const roomId = explicitRoom ? roomIdFrom(options.room ?? "") : fromLocation ? roomIdFromLocation() : "";
2470
2879
  const session = new Session({
2471
- schema: void 0,
2880
+ // ---- M6 lane C: typed messages ----
2881
+ // D70: a relay join may now bring the project's deployed schema. This is a type change and a
2882
+ // hash change, not a transport change: the session already chose between the builder's schema
2883
+ // and `relaySchema`, and already put `schema.hash8` or `RELAY_HASH8` in the HELLO.
2884
+ schema: options.schema,
2472
2885
  url,
2473
2886
  key: options.key ?? resolveKey(void 0, void 0, url),
2474
2887
  roomId,
@@ -2505,6 +2918,8 @@ async function joinRelay(options = {}) {
2505
2918
  },
2506
2919
  message: (target, bytes) => session.message(target, bytes),
2507
2920
  onMessage: (cb) => session.onMessage(cb),
2921
+ messages: buildMessages(session.schema, session),
2922
+ stats: { messages: session.messageCounts },
2508
2923
  on: (event, cb) => session.on(event, cb),
2509
2924
  leave: () => session.leave()
2510
2925
  };
@@ -2522,8 +2937,12 @@ export {
2522
2937
  INTERNAL_VOICE_TRACKS,
2523
2938
  Identity,
2524
2939
  IdentityError,
2940
+ LOBBY_MEMBERS,
2941
+ LOBBY_POLL_MS,
2942
+ LOBBY_STATE,
2525
2943
  MAX_IDENTITY_RETRY_WAIT_MS,
2526
2944
  MAX_PREDICTED_BODIES,
2945
+ MAX_PROXY_BODIES,
2527
2946
  MatchError,
2528
2947
  PING_INTERVAL_MS,
2529
2948
  PREDICTION_EPSILON,
@@ -2532,14 +2951,20 @@ export {
2532
2951
  SMOOTHING_HALF_LIFE_MS,
2533
2952
  SMOOTHING_SNAP_UNITS,
2534
2953
  Session,
2954
+ attachLobby,
2955
+ attachable,
2535
2956
  createParty,
2536
2957
  defaultScheduler,
2537
2958
  findMatch,
2959
+ findPublic,
2960
+ hasLobby,
2538
2961
  identityStorageKey,
2962
+ joinPublic,
2539
2963
  joinRelay,
2540
2964
  joinRoom,
2541
2965
  joinVoice,
2542
2966
  linkForUrl,
2967
+ lobbyOf,
2543
2968
  matchRoom,
2544
2969
  resolveUrl,
2545
2970
  roomIdFrom,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  Predictor
3
- } from "./chunk-TE66EDBB.js";
4
- import "./chunk-6J6PKFUS.js";
3
+ } from "./chunk-XFD6WYQW.js";
4
+ import "./chunk-5Z4DHUA3.js";
5
5
 
6
6
  // src/physics.ts
7
7
  function readPose(body, into) {
@@ -100,6 +100,25 @@ var RapierAdapter = class {
100
100
  removeBody(body) {
101
101
  this.world?.removeRigidBody(body);
102
102
  }
103
+ /**
104
+ * D71: a proxy is a `KinematicPositionBased` body — driven by the position it is told to be at
105
+ * next, never by a force, an impulse or a contact. Rapier derives the velocity its contacts see
106
+ * from the move itself, which is what carries a predicted body standing on a moving platform.
107
+ *
108
+ * The body is *switched* rather than built kinematic, so its collider, mass properties and axis
109
+ * locks are the ones the shared factory produced. A proxy has to be the server's body, and the
110
+ * only way to keep that true is to run the same factory call the predicted path runs.
111
+ */
112
+ makeKinematic(handle) {
113
+ const rapier = this.rapier;
114
+ if (!rapier) return;
115
+ handle.setBodyType(rapier.RigidBodyType.KinematicPositionBased, true);
116
+ }
117
+ moveKinematic(handle, pose) {
118
+ const body = handle;
119
+ body.setNextKinematicTranslation(pose.t);
120
+ body.setNextKinematicRotation(pose.r);
121
+ }
103
122
  step() {
104
123
  this.world?.step();
105
124
  }
@@ -191,6 +210,10 @@ var PhysicsPredictor = class extends Predictor {
191
210
  super(ext, store, new RapierAdapter(options), options, meOf, rttOf, tickIntervalOf, log);
192
211
  }
193
212
  };
213
+ function createRapierAdapter(options) {
214
+ return new RapierAdapter(options);
215
+ }
194
216
  export {
195
- PhysicsPredictor
217
+ PhysicsPredictor,
218
+ createRapierAdapter
196
219
  };