@irtio/testing 0.5.1 → 0.6.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.
@@ -166,6 +166,15 @@ var DROPPABLE = /* @__PURE__ */ new Set([
166
166
  var MICROTASK_TURNS = 8;
167
167
  var SETTLE_ROUNDS = 64;
168
168
  var JOIN_TIMEOUT_MS = 3e4;
169
+ var HELD_FRAMES_MAX = 16;
170
+ var DROPPED_HELD = new Uint8Array(0);
171
+ function frameNameOf(frame) {
172
+ try {
173
+ return frameTypeName(frame[0] ?? -1);
174
+ } catch {
175
+ return `type ${String(frame[0])}`;
176
+ }
177
+ }
169
178
  async function microtasks(turns = MICROTASK_TURNS) {
170
179
  for (let i = 0; i < turns; i++) await Promise.resolve();
171
180
  }
@@ -182,6 +191,15 @@ var TestHarness = class {
182
191
  links = [];
183
192
  byId = /* @__PURE__ */ new Map();
184
193
  resumeTokens = /* @__PURE__ */ new Map();
194
+ /**
195
+ * Bug #49: frames the core sent a client it knows but this harness has not welcomed yet, by
196
+ * client id. `RoomCore.join` runs `onJoin` before it returns and `byId` is set after, so a
197
+ * `room.call(ctx.clientId)` made from `onJoin` arrives here with no link to deliver to. The
198
+ * client refuses frames before its WELCOME, so they wait for it (`handleHello` drains).
199
+ */
200
+ heldOut = /* @__PURE__ */ new Map();
201
+ /** Client ids a frame was dropped for because nobody knows them, each warned about once. */
202
+ unknownSendWarned = /* @__PURE__ */ new Set();
185
203
  clientList = [];
186
204
  roleById = /* @__PURE__ */ new Map();
187
205
  roleOverrides = /* @__PURE__ */ new Map();
@@ -221,11 +239,16 @@ var TestHarness = class {
221
239
  this.host = new HarnessHost(this.clock);
222
240
  this.host.onSend = (clientId, frame) => {
223
241
  const link = this.byId.get(clientId);
224
- if (link?.connected) this.toClient(link, frame);
242
+ if (link) {
243
+ if (link.connected) this.toClient(link, frame);
244
+ return;
245
+ }
246
+ this.holdOrDrop(clientId, frame);
225
247
  };
226
248
  this.coreRef = new RoomCore(this.definition, this.host, {
227
249
  roomId: this.roomId,
228
- seed: options.seed ?? 1
250
+ seed: options.seed ?? 1,
251
+ ...options.profile === true ? { profile: true } : {}
229
252
  });
230
253
  this.host.core = this.coreRef;
231
254
  this.host.serializeForSave = () => this.coreRef.snapshot();
@@ -281,6 +304,10 @@ var TestHarness = class {
281
304
  get ext() {
282
305
  return this.coreRef.ext;
283
306
  }
307
+ /** D65: the room's bandwidth ledger, or `undefined` unless `profile: true` asked for one. */
308
+ get profile() {
309
+ return this.coreRef.profile();
310
+ }
284
311
  // -------------------------------------------------------------------------
285
312
  // The core is never re-entered
286
313
  // -------------------------------------------------------------------------
@@ -329,11 +356,38 @@ var TestHarness = class {
329
356
  }
330
357
  };
331
358
  }
359
+ /**
360
+ * Bug #49: a frame for a client with no link. Held when the core has the client and counts it
361
+ * connected, which is exactly the window between `RoomCore.join` running `onJoin` and
362
+ * `handleHello` sending the WELCOME; dropped otherwise, with one `warn` per client id so the
363
+ * next silent path is visible from `t.host.logsOf('warn')`.
364
+ */
365
+ holdOrDrop(clientId, frame) {
366
+ const entry = this.coreRef.clients.get(clientId);
367
+ if (entry?.connected) {
368
+ const held = this.heldOut.get(clientId) ?? [];
369
+ if (held.length < HELD_FRAMES_MAX) held.push(frame);
370
+ else if (held.length === HELD_FRAMES_MAX) {
371
+ held.push(DROPPED_HELD);
372
+ this.host.log("warn", [
373
+ `irtio: dropped a ${frameNameOf(frame)} frame for ${clientId}: more than ${HELD_FRAMES_MAX} frames sent while it was still joining`
374
+ ]);
375
+ }
376
+ this.heldOut.set(clientId, held);
377
+ return;
378
+ }
379
+ if (this.unknownSendWarned.has(clientId)) return;
380
+ this.unknownSendWarned.add(clientId);
381
+ this.host.log("warn", [
382
+ `irtio: dropped a ${frameNameOf(frame)} frame for unknown client ${clientId}`
383
+ ]);
384
+ }
332
385
  /** The client end hung up: a deliberate `leave()`, or `t.stop()`. Treated as a graceful leave. */
333
386
  linkClosed(link) {
334
387
  if (!link.connected) return;
335
388
  link.connected = false;
336
389
  this.byId.delete(link.clientId);
390
+ this.heldOut.delete(link.clientId);
337
391
  if (!link.joined || this.stopped) return;
338
392
  link.joined = false;
339
393
  this.enterCore(() => this.coreRef.leave(link.clientId, "left"));
@@ -481,6 +535,10 @@ var TestHarness = class {
481
535
  }
482
536
  });
483
537
  if (!result) {
538
+ this.heldOut.delete(link.clientId);
539
+ if (this.coreRef.clients.has(link.clientId)) {
540
+ this.enterCore(() => this.coreRef.leave(link.clientId, "timeout"));
541
+ }
484
542
  const full = failure instanceof RoomFullError;
485
543
  this.fail(
486
544
  link,
@@ -519,10 +577,16 @@ var TestHarness = class {
519
577
  snapshot: joined.snapshot,
520
578
  resumeToken,
521
579
  roomId: this.roomId,
522
- tickIntervalMs: Math.round(this.intervalMs)
580
+ tickRate: this.definition.config.tickRate,
581
+ maxClients: 0
523
582
  })
524
583
  )
525
584
  );
585
+ const held = this.heldOut.get(link.clientId);
586
+ if (held) {
587
+ this.heldOut.delete(link.clientId);
588
+ for (const frame of held) if (frame !== DROPPED_HELD) this.toClient(link, frame);
589
+ }
526
590
  }
527
591
  fail(link, code, message) {
528
592
  this.toClient(
@@ -709,7 +773,7 @@ var TestHarness = class {
709
773
  const latency = spec.latency ?? this.latency;
710
774
  this.renderMeta.set(room.me, {
711
775
  // The client's own default: 2 × the tick interval it learned from WELCOME, floored at 50.
712
- interpDelayMs: spec.interpDelayMs ?? Math.max(50, 2 * Math.round(this.intervalMs)),
776
+ interpDelayMs: spec.interpDelayMs ?? Math.max(50, 2 * this.intervalMs),
713
777
  marginMs: (latency?.rttMs ?? 0) + (latency?.jitterMs ?? 0)
714
778
  });
715
779
  const client = Object.create(room);
package/dist/index.d.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import { RoleOf, AnySchema, State, PlainState } from '@irtio/schema';
2
2
  import { RoomDefinition } from '@irtio/server';
3
3
  import { Room, ClientState, RelayRoom, Scheduler } from '@irtio/client';
4
+ import { ProfileSnapshot } from '@irtio/protocol';
4
5
  import { RoomCore } from '@irtio/runtime';
5
6
  import { TraceEntry, HarnessHost, VisibilityLeak, FakeClock } from '@irtio/runtime/test';
6
- export { TraceEntry, VisibilityLeak, initPhysics } from '@irtio/runtime/test';
7
+ export { TraceEntry, VisibilityLeak, initMatter, initPhysics } from '@irtio/runtime/test';
7
8
  export { M as MatcherResult, P as PredictionBounds, T as TRACE_TAIL, m as matchers, t as toHaveConverged, a as toHaveNoVisibilityLeaks, b as toHaveRejected, c as toStayUnderBandwidth, d as toStayWithinPrediction } from './assertions-Cj1Sejhj.js';
8
9
 
9
10
  /**
@@ -69,6 +70,11 @@ interface TestRoomOptions {
69
70
  * either way.
70
71
  */
71
72
  readonly failOnHandlerError?: boolean;
73
+ /**
74
+ * D65: run the room with a bandwidth ledger, readable as `t.profile`. Off by default, and off
75
+ * costs nothing — the room constructs no ledger and walks no frame.
76
+ */
77
+ readonly profile?: boolean;
72
78
  }
73
79
  interface TestJoinSpec<Role extends string = string> {
74
80
  readonly role?: Role;
@@ -192,6 +198,12 @@ interface TestRoom<S extends AnySchema> {
192
198
  until(pred: () => boolean, options?: UntilOptions): Promise<void>;
193
199
  /** `withBuiltins(schema)`: the schema every frame is encoded against. */
194
200
  readonly ext: AnySchema;
201
+ /**
202
+ * D65: the room's bandwidth ledger, or `undefined` unless `testRoom({ profile: true })` asked
203
+ * for one. Cumulative from the room's first frame; the room's own view, so `out` counts what
204
+ * the room sent and the join snapshots it handed the host, and `in` counts what it accepted.
205
+ */
206
+ readonly profile: ProfileSnapshot | undefined;
195
207
  /** Collections a client can see that its role must not. `[]` when the room is clean. */
196
208
  checkVisibility(): VisibilityLeak[];
197
209
  /**
@@ -313,6 +325,15 @@ declare class TestHarness<S extends AnySchema> implements TestRoom<S> {
313
325
  private readonly links;
314
326
  private readonly byId;
315
327
  private readonly resumeTokens;
328
+ /**
329
+ * Bug #49: frames the core sent a client it knows but this harness has not welcomed yet, by
330
+ * client id. `RoomCore.join` runs `onJoin` before it returns and `byId` is set after, so a
331
+ * `room.call(ctx.clientId)` made from `onJoin` arrives here with no link to deliver to. The
332
+ * client refuses frames before its WELCOME, so they wait for it (`handleHello` drains).
333
+ */
334
+ private readonly heldOut;
335
+ /** Client ids a frame was dropped for because nobody knows them, each warned about once. */
336
+ private readonly unknownSendWarned;
316
337
  private readonly clientList;
317
338
  private readonly roleById;
318
339
  private readonly roleOverrides;
@@ -356,9 +377,18 @@ declare class TestHarness<S extends AnySchema> implements TestRoom<S> {
356
377
  private get intervalMs();
357
378
  /** The extended schema every frame is encoded against. */
358
379
  get ext(): AnySchema;
380
+ /** D65: the room's bandwidth ledger, or `undefined` unless `profile: true` asked for one. */
381
+ get profile(): ProfileSnapshot | undefined;
359
382
  private enterCore;
360
383
  /** A `Transport` bound to one `t.join()`; reconnects call `connect` again with the same spec. */
361
384
  private transportFor;
385
+ /**
386
+ * Bug #49: a frame for a client with no link. Held when the core has the client and counts it
387
+ * connected, which is exactly the window between `RoomCore.join` running `onJoin` and
388
+ * `handleHello` sending the WELCOME; dropped otherwise, with one `warn` per client id so the
389
+ * next silent path is visible from `t.host.logsOf('warn')`.
390
+ */
391
+ private holdOrDrop;
362
392
  /** The client end hung up: a deliberate `leave()`, or `t.stop()`. Treated as a graceful leave. */
363
393
  private linkClosed;
364
394
  /**
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  toHaveRejected,
12
12
  toStayUnderBandwidth,
13
13
  toStayWithinPrediction
14
- } from "./chunk-Q44LJTMZ.js";
14
+ } from "./chunk-TPT2EBEZ.js";
15
15
 
16
16
  // src/relay-harness.ts
17
17
  import { joinRelay } from "@irtio/client";
@@ -284,8 +284,9 @@ var TestRelayHarness = class {
284
284
  snapshot: this.relay.snapshot(),
285
285
  resumeToken,
286
286
  roomId: this.roomId,
287
- tickIntervalMs: 0
287
+ tickRate: 0,
288
288
  // relay rooms have no tick
289
+ maxClients: this.maxClients
289
290
  })
290
291
  )
291
292
  );
@@ -448,6 +449,7 @@ var TestRelayHarness = class {
448
449
 
449
450
  // src/index.ts
450
451
  import { initPhysics } from "@irtio/runtime/test";
452
+ import { initMatter } from "@irtio/runtime/test";
451
453
  async function testRoom(definition, options = {}) {
452
454
  return new TestHarness(definition, options);
453
455
  }
@@ -460,6 +462,7 @@ export {
460
462
  TestRelayHarness,
461
463
  clockScheduler,
462
464
  divergence,
465
+ initMatter,
463
466
  initPhysics,
464
467
  matchers,
465
468
  materialize,
package/dist/matchers.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  toHaveRejected,
6
6
  toStayUnderBandwidth,
7
7
  toStayWithinPrediction
8
- } from "./chunk-Q44LJTMZ.js";
8
+ } from "./chunk-TPT2EBEZ.js";
9
9
 
10
10
  // src/matchers.ts
11
11
  import { expect } from "vitest";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@irtio/testing",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "irtio's blessed test API: testRoom() in-process rooms with real client semantics, plus Vitest matchers",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -23,11 +23,11 @@
23
23
  "dist"
24
24
  ],
25
25
  "dependencies": {
26
- "@irtio/client": "0.5.1",
27
- "@irtio/protocol": "0.5.1",
28
- "@irtio/runtime": "0.5.1",
29
- "@irtio/schema": "0.5.1",
30
- "@irtio/server": "0.5.1"
26
+ "@irtio/client": "0.6.0",
27
+ "@irtio/protocol": "0.6.0",
28
+ "@irtio/runtime": "0.6.0",
29
+ "@irtio/schema": "0.6.0",
30
+ "@irtio/server": "0.6.0"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "vitest": ">=3"