@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.
- package/dist/{chunk-Q44LJTMZ.js → chunk-TPT2EBEZ.js} +68 -4
- package/dist/index.d.ts +31 -1
- package/dist/index.js +5 -2
- package/dist/matchers.js +1 -1
- package/package.json +6 -6
|
@@ -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
|
|
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
|
-
|
|
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 *
|
|
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-
|
|
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
|
-
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@irtio/testing",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
27
|
-
"@irtio/protocol": "0.
|
|
28
|
-
"@irtio/runtime": "0.
|
|
29
|
-
"@irtio/schema": "0.
|
|
30
|
-
"@irtio/server": "0.
|
|
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"
|