@irtio/bots 0.6.0 → 0.7.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.d.ts +370 -22
- package/dist/index.js +483 -118
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -14,7 +14,14 @@ var INVARIANT_NAMES = [
|
|
|
14
14
|
"misprediction",
|
|
15
15
|
"snaps",
|
|
16
16
|
"disconnects",
|
|
17
|
-
"tick-health"
|
|
17
|
+
"tick-health",
|
|
18
|
+
// D70 (M6 lane C): typed peer messages a bot could not read. A shape mismatch between a bot
|
|
19
|
+
// script and the schema was previously a silent nothing-happened; this makes it a red run.
|
|
20
|
+
"typed-message-drops",
|
|
21
|
+
// M6 lane E (D73-c): every party's members share a room, and no room holds more bots than its
|
|
22
|
+
// ticket said it seats. Listed only on a run that queued — a run that built its own room has no
|
|
23
|
+
// parties to be intact, so the row is absent rather than unread. See `partyIntegrityResult`.
|
|
24
|
+
"party-integrity"
|
|
18
25
|
];
|
|
19
26
|
var DEFAULT_THRESHOLDS = {
|
|
20
27
|
budgetBytesPerSec: 128e3,
|
|
@@ -22,7 +29,8 @@ var DEFAULT_THRESHOLDS = {
|
|
|
22
29
|
handlerErrorsMax: 0,
|
|
23
30
|
mispredictionMagnitudeMax: Number.POSITIVE_INFINITY,
|
|
24
31
|
snapsMax: Number.POSITIVE_INFINITY,
|
|
25
|
-
overrunsMax: 0
|
|
32
|
+
overrunsMax: 0,
|
|
33
|
+
typedMessageDropsMax: 0
|
|
26
34
|
};
|
|
27
35
|
var widenedSchemas = /* @__PURE__ */ new WeakMap();
|
|
28
36
|
function widenGrids(ext, slack) {
|
|
@@ -264,16 +272,19 @@ function entityValue(frameState, collection, id) {
|
|
|
264
272
|
function correlateShots(shots, dump) {
|
|
265
273
|
const frames = dump.frames;
|
|
266
274
|
return shots.map((shot) => {
|
|
267
|
-
const
|
|
268
|
-
const frame = frames.find((f) => f.at >=
|
|
275
|
+
const exact = shot.serverTick !== void 0;
|
|
276
|
+
const frame = exact ? frames.find((f) => f.tick === shot.serverTick) : frames.find((f) => f.at >= shot.sentAt + shot.uplinkMs);
|
|
277
|
+
const estimated = !exact && shot.uplinkMs > 0;
|
|
278
|
+
const rewound = shot.rewound;
|
|
269
279
|
if (frame === void 0) {
|
|
270
280
|
return {
|
|
271
281
|
...shot,
|
|
272
|
-
serverTick: void 0,
|
|
273
|
-
estimated
|
|
282
|
+
serverTick: exact ? shot.serverTick : void 0,
|
|
283
|
+
estimated,
|
|
284
|
+
rewound,
|
|
274
285
|
authoritative: void 0,
|
|
275
286
|
missDistance: void 0,
|
|
276
|
-
unresolved: frames.length === 0 ? "nothing was recorded, so there is no tick to judge this shot against" : `the shot lands after the last recorded tick (${frames[frames.length - 1]?.tick})`
|
|
287
|
+
unresolved: frames.length === 0 ? "nothing was recorded, so there is no tick to judge this shot against" : exact ? `the room judged this shot at tick ${shot.serverTick}, which is not in the recording` : `the shot lands after the last recorded tick (${frames[frames.length - 1]?.tick})`
|
|
277
288
|
};
|
|
278
289
|
}
|
|
279
290
|
const value = entityValue(frame.state, shot.collection, shot.target);
|
|
@@ -281,7 +292,8 @@ function correlateShots(shots, dump) {
|
|
|
281
292
|
return {
|
|
282
293
|
...shot,
|
|
283
294
|
serverTick: frame.tick,
|
|
284
|
-
estimated
|
|
295
|
+
estimated,
|
|
296
|
+
rewound,
|
|
285
297
|
authoritative: void 0,
|
|
286
298
|
missDistance: void 0,
|
|
287
299
|
unresolved: `${shot.collection}.${shot.target} is not in the recording at tick ${frame.tick}`
|
|
@@ -303,7 +315,8 @@ function correlateShots(shots, dump) {
|
|
|
303
315
|
return {
|
|
304
316
|
...shot,
|
|
305
317
|
serverTick: frame.tick,
|
|
306
|
-
estimated
|
|
318
|
+
estimated,
|
|
319
|
+
rewound,
|
|
307
320
|
authoritative,
|
|
308
321
|
missDistance: void 0,
|
|
309
322
|
unresolved: missing
|
|
@@ -312,7 +325,8 @@ function correlateShots(shots, dump) {
|
|
|
312
325
|
return {
|
|
313
326
|
...shot,
|
|
314
327
|
serverTick: frame.tick,
|
|
315
|
-
estimated
|
|
328
|
+
estimated,
|
|
329
|
+
rewound,
|
|
316
330
|
authoritative,
|
|
317
331
|
missDistance: Math.sqrt(sum)
|
|
318
332
|
};
|
|
@@ -465,6 +479,7 @@ import {
|
|
|
465
479
|
FrameType as FrameType4,
|
|
466
480
|
decodeErrorPayload,
|
|
467
481
|
decodeFrame,
|
|
482
|
+
decodeMsg,
|
|
468
483
|
decodeReply,
|
|
469
484
|
decodeWelcome,
|
|
470
485
|
errorByCode,
|
|
@@ -477,6 +492,7 @@ import {
|
|
|
477
492
|
applyDelta,
|
|
478
493
|
decodeDelta as decodeDelta2,
|
|
479
494
|
decodeDeltaFrom,
|
|
495
|
+
decodeFields,
|
|
480
496
|
decodeSnapshot,
|
|
481
497
|
schemaFromCanonical
|
|
482
498
|
} from "@irtio/schema";
|
|
@@ -545,21 +561,24 @@ function makeTrace(startedAt, rings) {
|
|
|
545
561
|
}
|
|
546
562
|
|
|
547
563
|
// src/observer.ts
|
|
548
|
-
function simulationOnly(ext, delta,
|
|
564
|
+
function simulationOnly(ext, delta, kindOf) {
|
|
549
565
|
let sawSync = false;
|
|
550
566
|
let sawPredicted = false;
|
|
567
|
+
let sawProxied = false;
|
|
551
568
|
for (const dc of delta.collections) {
|
|
552
569
|
const desc = ext.collections.find((c) => c.name === dc.name);
|
|
553
570
|
const physics = desc?.physics;
|
|
554
571
|
if (!desc || !physics) return "mixed";
|
|
555
572
|
for (const op of dc.ops) {
|
|
556
573
|
if (op.op !== "update") return "mixed";
|
|
557
|
-
const
|
|
574
|
+
const kind = kindOf(dc.name, op.id);
|
|
575
|
+
const predicted = kind === "predicted";
|
|
558
576
|
for (const index of op.mask.fields) {
|
|
559
577
|
const field = desc.fields[index];
|
|
560
578
|
if (!field) return "mixed";
|
|
561
579
|
if (physics.bodyFields.has(field.name)) {
|
|
562
580
|
if (predicted) sawPredicted = true;
|
|
581
|
+
else if (kind === "proxied") sawProxied = true;
|
|
563
582
|
else sawSync = true;
|
|
564
583
|
} else if (!predicted && !physics.intents.includes(field.name)) {
|
|
565
584
|
sawSync = true;
|
|
@@ -569,9 +588,11 @@ function simulationOnly(ext, delta, predicts) {
|
|
|
569
588
|
}
|
|
570
589
|
}
|
|
571
590
|
}
|
|
572
|
-
|
|
573
|
-
if (
|
|
574
|
-
return "
|
|
591
|
+
const kinds = Number(sawPredicted) + Number(sawSync) + Number(sawProxied);
|
|
592
|
+
if (kinds !== 1) return "mixed";
|
|
593
|
+
if (sawPredicted) return "predicted";
|
|
594
|
+
if (sawProxied) return "proxied";
|
|
595
|
+
return "sync";
|
|
575
596
|
}
|
|
576
597
|
var WriteLog = class {
|
|
577
598
|
constructor(limit = 4096) {
|
|
@@ -654,14 +675,29 @@ var BotObserver = class {
|
|
|
654
675
|
bytesOut = 0;
|
|
655
676
|
corrections = 0;
|
|
656
677
|
syncCorrections = 0;
|
|
678
|
+
proxiedCorrections = 0;
|
|
657
679
|
suppressedCorrections = 0;
|
|
658
680
|
mispredictions = 0;
|
|
659
681
|
/**
|
|
660
|
-
*
|
|
661
|
-
* `spawnBots` once the room exists (`room.prediction`);
|
|
662
|
-
* it cannot be derived from the bytes the observer otherwise sticks to.
|
|
682
|
+
* M6 lane E (D73-b): what this bot's client's local world holds for `collection[id]` right now.
|
|
683
|
+
* Assigned by `spawnBots` once the room exists (`room.prediction`); the local world's shape is
|
|
684
|
+
* client-local, so it cannot be derived from the bytes the observer otherwise sticks to.
|
|
685
|
+
*
|
|
686
|
+
* Unset means "no local world at all", which answers `absent` for everything — the behaviour a
|
|
687
|
+
* bot without physics has always had.
|
|
688
|
+
*/
|
|
689
|
+
bodyKind;
|
|
690
|
+
/**
|
|
691
|
+
* M6 lane E (D73-a): the client's own `room.prediction`, when this bot joined with `physics` or
|
|
692
|
+
* `physics2d`. Held rather than copied because `active` flips once the engine has loaded, which
|
|
693
|
+
* happens off the join path — a boolean read at join time would say `false` on every run.
|
|
663
694
|
*/
|
|
664
|
-
|
|
695
|
+
prediction;
|
|
696
|
+
/** D73-a: latched true the first time {@link prediction} reported an active local world. */
|
|
697
|
+
predicted = false;
|
|
698
|
+
/** D73-b: the high-water marks of `prediction.stats.proxies` / `.absent` across the run. */
|
|
699
|
+
proxies = 0;
|
|
700
|
+
absent = 0;
|
|
665
701
|
mispredictionMagnitude = 0;
|
|
666
702
|
mispredictionMax = 0;
|
|
667
703
|
snaps = 0;
|
|
@@ -670,6 +706,10 @@ var BotObserver = class {
|
|
|
670
706
|
disconnects = 0;
|
|
671
707
|
peakBytesInPerSec = 0;
|
|
672
708
|
peakCorrectionsPerSec = 0;
|
|
709
|
+
/** D70: peer-message counters, filled by `inspect`'s MSG case in both directions. */
|
|
710
|
+
messagesSent = 0;
|
|
711
|
+
messagesReceived = 0;
|
|
712
|
+
messagesDropped = 0;
|
|
673
713
|
/**
|
|
674
714
|
* Spatial-grid ops actually put to the AOI policy. Zero on a run whose room has no spatial
|
|
675
715
|
* collection; zero on a run that *does* and would mean the invariant passed vacuously, which is
|
|
@@ -705,6 +745,7 @@ var BotObserver = class {
|
|
|
705
745
|
bytesOut: this.bytesOut,
|
|
706
746
|
corrections: this.corrections,
|
|
707
747
|
syncCorrections: this.syncCorrections,
|
|
748
|
+
proxiedCorrections: this.proxiedCorrections,
|
|
708
749
|
suppressedCorrections: this.suppressedCorrections,
|
|
709
750
|
mispredictions: this.mispredictions,
|
|
710
751
|
mispredictionMagnitude: this.mispredictionMagnitude,
|
|
@@ -714,7 +755,13 @@ var BotObserver = class {
|
|
|
714
755
|
errors: this.errors,
|
|
715
756
|
disconnects: this.disconnects,
|
|
716
757
|
peakBytesInPerSec: this.peakBytesInPerSec,
|
|
717
|
-
peakCorrectionsPerSec: this.peakCorrectionsPerSec
|
|
758
|
+
peakCorrectionsPerSec: this.peakCorrectionsPerSec,
|
|
759
|
+
messagesSent: this.messagesSent,
|
|
760
|
+
messagesReceived: this.messagesReceived,
|
|
761
|
+
messagesDropped: this.messagesDropped,
|
|
762
|
+
predicting: this.predicted,
|
|
763
|
+
proxies: this.proxies,
|
|
764
|
+
absent: this.absent
|
|
718
765
|
};
|
|
719
766
|
}
|
|
720
767
|
/** Counts a violation always; keeps an example, rate-limited per invariant when asked. */
|
|
@@ -740,6 +787,12 @@ var BotObserver = class {
|
|
|
740
787
|
/** The `onFrame` hook. `bytes` is the whole frame, envelope byte included. */
|
|
741
788
|
onFrame(dir, type, bytes) {
|
|
742
789
|
const now = Date.now();
|
|
790
|
+
const prediction = this.prediction;
|
|
791
|
+
if (prediction?.active === true) {
|
|
792
|
+
this.predicted = true;
|
|
793
|
+
if (prediction.stats.proxies > this.proxies) this.proxies = prediction.stats.proxies;
|
|
794
|
+
if (prediction.stats.absent > this.absent) this.absent = prediction.stats.absent;
|
|
795
|
+
}
|
|
743
796
|
let note;
|
|
744
797
|
if (dir === "in") {
|
|
745
798
|
this.framesIn++;
|
|
@@ -781,6 +834,7 @@ var BotObserver = class {
|
|
|
781
834
|
if (dir === "out") {
|
|
782
835
|
if (type === FrameType4.CALL) this.calls++;
|
|
783
836
|
if (type === FrameType4.WRITE) this.recordWrites(decodeDelta2(this.ext, payload), now);
|
|
837
|
+
if (type === FrameType4.MSG) return this.onMsg("out", payload);
|
|
784
838
|
return void 0;
|
|
785
839
|
}
|
|
786
840
|
switch (type) {
|
|
@@ -799,10 +853,64 @@ var BotObserver = class {
|
|
|
799
853
|
return this.onReply(payload);
|
|
800
854
|
case FrameType4.SCHEMA:
|
|
801
855
|
return this.onSchema(payload);
|
|
856
|
+
case FrameType4.MSG:
|
|
857
|
+
return this.onMsg("in", payload);
|
|
802
858
|
default:
|
|
803
859
|
return void 0;
|
|
804
860
|
}
|
|
805
861
|
}
|
|
862
|
+
/**
|
|
863
|
+
* D70: one peer message, in either direction, as a trace note.
|
|
864
|
+
*
|
|
865
|
+
* Before this, `MSG` fell through to `undefined` and a trace showed a frame with a byte count
|
|
866
|
+
* and nothing else — which was tolerable while every message was opaque bytes and is not now
|
|
867
|
+
* that some of them have declared shapes. A raw message notes its target; a typed one notes its
|
|
868
|
+
* name and its value when this bot holds the schema, and its index when it does not.
|
|
869
|
+
*
|
|
870
|
+
* A typed message this bot cannot read is counted as a **drop**, which is what the
|
|
871
|
+
* `typed-message-drops` invariant fails a run on. That is the whole point of the counter: a
|
|
872
|
+
* scenario sending a shape the schema does not describe used to be a message that silently
|
|
873
|
+
* never arrived.
|
|
874
|
+
*/
|
|
875
|
+
onMsg(dir, payload) {
|
|
876
|
+
let msg;
|
|
877
|
+
try {
|
|
878
|
+
msg = decodeMsg(payload);
|
|
879
|
+
} catch (err) {
|
|
880
|
+
const why = err instanceof Error ? err.message : String(err);
|
|
881
|
+
if (dir === "in") this.dropTyped(`a MSG frame did not decode: ${why}`);
|
|
882
|
+
return `msg undecodable (${why})`;
|
|
883
|
+
}
|
|
884
|
+
const who = msg.target.kind === "client" ? msg.target.clientId : msg.target.kind === "role" ? `role:${msg.target.role}` : msg.target.kind;
|
|
885
|
+
if (!msg.typed) {
|
|
886
|
+
if (dir === "out") this.messagesSent++;
|
|
887
|
+
else this.messagesReceived++;
|
|
888
|
+
return `msg raw ${who} (${msg.payload.length}B)`;
|
|
889
|
+
}
|
|
890
|
+
const desc = (this.ext.messages ?? [])[msg.typed.index];
|
|
891
|
+
if (!desc) {
|
|
892
|
+
if (dir === "out") this.messagesSent++;
|
|
893
|
+
else this.dropTyped(`no message with index ${msg.typed.index} in this schema`);
|
|
894
|
+
return `msg typed #${msg.typed.index} (undecodable)`;
|
|
895
|
+
}
|
|
896
|
+
let value;
|
|
897
|
+
try {
|
|
898
|
+
value = decodeFields(desc.fields, msg.payload);
|
|
899
|
+
} catch {
|
|
900
|
+
if (dir === "out") this.messagesSent++;
|
|
901
|
+
else this.dropTyped(`${desc.name} did not decode against this schema`);
|
|
902
|
+
return `msg ${desc.name} (undecodable)`;
|
|
903
|
+
}
|
|
904
|
+
if (dir === "out") this.messagesSent++;
|
|
905
|
+
else this.messagesReceived++;
|
|
906
|
+
const arrow = dir === "out" ? "->" : "<-";
|
|
907
|
+
return `msg ${desc.name} ${arrow} ${who} ${JSON.stringify(value)}`;
|
|
908
|
+
}
|
|
909
|
+
/** D70: one typed message this bot could not read — a counter and an invariant violation. */
|
|
910
|
+
dropTyped(why) {
|
|
911
|
+
this.messagesDropped++;
|
|
912
|
+
this.violate("typed-message-drops", why, true);
|
|
913
|
+
}
|
|
806
914
|
onWelcome(welcome) {
|
|
807
915
|
this.id = welcome.clientId;
|
|
808
916
|
this.role = welcome.role;
|
|
@@ -848,12 +956,16 @@ var BotObserver = class {
|
|
|
848
956
|
const kind = simulationOnly(
|
|
849
957
|
this.ext,
|
|
850
958
|
delta,
|
|
851
|
-
(c, id) => this.
|
|
959
|
+
(c, id) => this.bodyKind ? this.bodyKind(c, id) : "absent"
|
|
852
960
|
);
|
|
853
961
|
if (kind === "sync") {
|
|
854
962
|
this.syncCorrections++;
|
|
855
963
|
return `synced ${names}`;
|
|
856
964
|
}
|
|
965
|
+
if (kind === "proxied") {
|
|
966
|
+
this.proxiedCorrections++;
|
|
967
|
+
return `proxied ${names}`;
|
|
968
|
+
}
|
|
857
969
|
if (kind === "predicted") {
|
|
858
970
|
return `judged ${names}${clientTick !== void 0 ? ` (clientTick ${clientTick})` : ""}`;
|
|
859
971
|
}
|
|
@@ -1145,6 +1257,58 @@ function freshValue(rng, desc, ctx) {
|
|
|
1145
1257
|
|
|
1146
1258
|
// src/report.ts
|
|
1147
1259
|
import { mergeProfiles } from "@irtio/protocol";
|
|
1260
|
+
function matchmakingSummary(reading) {
|
|
1261
|
+
const waits = [...reading.tickets.map((t) => t.waitedMs)].sort((a, b) => a - b);
|
|
1262
|
+
const rooms = /* @__PURE__ */ new Map();
|
|
1263
|
+
for (const t of reading.tickets) {
|
|
1264
|
+
const room = rooms.get(t.room) ?? { bots: 0, size: t.size };
|
|
1265
|
+
room.bots += 1;
|
|
1266
|
+
room.size = Math.max(room.size, t.size);
|
|
1267
|
+
rooms.set(t.room, room);
|
|
1268
|
+
}
|
|
1269
|
+
const byParty = /* @__PURE__ */ new Map();
|
|
1270
|
+
for (const t of reading.tickets) {
|
|
1271
|
+
if (t.party === void 0) continue;
|
|
1272
|
+
const seen = byParty.get(t.party) ?? /* @__PURE__ */ new Set();
|
|
1273
|
+
seen.add(t.room);
|
|
1274
|
+
byParty.set(t.party, seen);
|
|
1275
|
+
}
|
|
1276
|
+
const splitParties = [...byParty].filter(([, seen]) => seen.size > 1).map(([party, seen]) => ({ party, rooms: [...seen] }));
|
|
1277
|
+
const overfullRooms = [...rooms].filter(([, r]) => r.size > 0 && r.bots > r.size).map(([room, r]) => ({ room, bots: r.bots, size: r.size }));
|
|
1278
|
+
return {
|
|
1279
|
+
tickets: reading.tickets.length,
|
|
1280
|
+
rooms: rooms.size,
|
|
1281
|
+
waitP50Ms: percentile(waits, 50),
|
|
1282
|
+
waitP95Ms: percentile(waits, 95),
|
|
1283
|
+
waitMaxMs: waits[waits.length - 1] ?? 0,
|
|
1284
|
+
timeouts: reading.failures.filter((f) => f.code === "E_NO_MATCH").length,
|
|
1285
|
+
failures: reading.failures.filter((f) => f.code !== "E_NO_MATCH"),
|
|
1286
|
+
backfills: reading.tickets.filter((t) => t.backfill).length,
|
|
1287
|
+
partiesIntact: byParty.size - splitParties.length,
|
|
1288
|
+
parties: byParty.size,
|
|
1289
|
+
overfullRooms,
|
|
1290
|
+
splitParties
|
|
1291
|
+
};
|
|
1292
|
+
}
|
|
1293
|
+
function partyIntegrityResult(reading) {
|
|
1294
|
+
const summary = matchmakingSummary(reading);
|
|
1295
|
+
const violations = summary.splitParties.length + summary.overfullRooms.length;
|
|
1296
|
+
const detail = violations === 0 ? `${summary.parties} part${summary.parties === 1 ? "y" : "ies"} landed whole across ${summary.rooms} room(s), and no room held more bots than its ticket seats` : [
|
|
1297
|
+
...summary.splitParties.map(
|
|
1298
|
+
(p) => `party ${p.party} was split across ${p.rooms.join(", ")}`
|
|
1299
|
+
),
|
|
1300
|
+
...summary.overfullRooms.map(
|
|
1301
|
+
(r) => `room ${r.room} holds ${r.bots} bots on a ticket that seats ${r.size}`
|
|
1302
|
+
)
|
|
1303
|
+
].join("; ");
|
|
1304
|
+
return {
|
|
1305
|
+
name: "party-integrity",
|
|
1306
|
+
ok: violations === 0,
|
|
1307
|
+
state: violations === 0 ? "ok" : "violation",
|
|
1308
|
+
violations,
|
|
1309
|
+
detail
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1148
1312
|
function percentile(sorted, p) {
|
|
1149
1313
|
if (sorted.length === 0) return 0;
|
|
1150
1314
|
const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(p / 100 * sorted.length) - 1));
|
|
@@ -1194,9 +1358,12 @@ function detailFor(name, observers, violations, thresholds) {
|
|
|
1194
1358
|
case "correction-storm": {
|
|
1195
1359
|
const corrections = observers.reduce((sum, o) => sum + o.corrections, 0);
|
|
1196
1360
|
const synced = observers.reduce((sum, o) => sum + o.syncCorrections, 0);
|
|
1361
|
+
const proxied = observers.reduce((sum, o) => sum + o.proxiedCorrections, 0);
|
|
1197
1362
|
const suppressed = observers.reduce((sum, o) => sum + o.suppressedCorrections, 0);
|
|
1198
1363
|
const worst = peak(observers, (o) => o.peakCorrectionsPerSec);
|
|
1199
|
-
return `${corrections} correction(s), peak ${worst}/s per bot, threshold ${thresholds.correctionsPerSecMax}/s${synced > 0 ? ` (+${synced} body-sync)` : ""}${
|
|
1364
|
+
return `${corrections} correction(s), peak ${worst}/s per bot, threshold ${thresholds.correctionsPerSecMax}/s${synced > 0 ? ` (+${synced} body-sync)` : ""}${// D73-b: named apart from body-sync, because a proxy correction says something else —
|
|
1365
|
+
// the server disagreeing with what this client drew.
|
|
1366
|
+
proxied > 0 ? ` (+${proxied} proxied)` : ""}${suppressed > 0 ? ` (+${suppressed} within-epsilon)` : ""}${violations === 0 ? "" : examples(observers, name)}`;
|
|
1200
1367
|
}
|
|
1201
1368
|
case "misprediction": {
|
|
1202
1369
|
const count = observers.reduce((sum, o) => sum + o.mispredictions, 0);
|
|
@@ -1216,7 +1383,14 @@ function detailFor(name, observers, violations, thresholds) {
|
|
|
1216
1383
|
const count = observers.reduce((sum, o) => sum + o.disconnects, 0);
|
|
1217
1384
|
return count === 0 ? "every bot stayed connected" : `${count} disconnect(s)${examples(observers, name)}`;
|
|
1218
1385
|
}
|
|
1386
|
+
case "typed-message-drops": {
|
|
1387
|
+
const dropped = observers.reduce((sum, o) => sum + o.messagesDropped, 0);
|
|
1388
|
+
const received = observers.reduce((sum, o) => sum + o.messagesReceived, 0);
|
|
1389
|
+
const sent = observers.reduce((sum, o) => sum + o.messagesSent, 0);
|
|
1390
|
+
return `${sent} message(s) sent, ${received} received, ${dropped} dropped, tolerated ${thresholds.typedMessageDropsMax}` + (violations === 0 ? "" : `${examples(observers, name)} \u2014 a dropped typed message means the sender's shape is not one this schema declares`);
|
|
1391
|
+
}
|
|
1219
1392
|
case "tick-health":
|
|
1393
|
+
case "party-integrity":
|
|
1220
1394
|
return "";
|
|
1221
1395
|
}
|
|
1222
1396
|
}
|
|
@@ -1255,10 +1429,19 @@ function buildReport(options) {
|
|
|
1255
1429
|
const thresholds = options.thresholds ?? DEFAULT_THRESHOLDS;
|
|
1256
1430
|
const durationMs = Math.max(1, options.durationMs);
|
|
1257
1431
|
const seconds = durationMs / 1e3;
|
|
1258
|
-
const
|
|
1432
|
+
const matchmaking = options.matchmaking;
|
|
1433
|
+
const invariants = INVARIANT_NAMES.filter(
|
|
1434
|
+
// D73-c: a run that did not queue has no parties, so the row is absent rather than unread.
|
|
1435
|
+
(name) => name !== "party-integrity" || matchmaking !== void 0
|
|
1436
|
+
).map((name) => {
|
|
1259
1437
|
if (name === "tick-health") return tickHealthResult(options.tickHealth, thresholds);
|
|
1438
|
+
if (name === "party-integrity") return partyIntegrityResult(matchmaking);
|
|
1260
1439
|
const violations = total(observers, name);
|
|
1261
|
-
const ok = name === "handler-error" ? violations <= thresholds.handlerErrorsMax :
|
|
1440
|
+
const ok = name === "handler-error" ? violations <= thresholds.handlerErrorsMax : (
|
|
1441
|
+
// D70: a budget too, for the same reason `handler-error` has one — a scenario that
|
|
1442
|
+
// deliberately sends a shape the room does not declare says so with `--typed-drops-max`.
|
|
1443
|
+
name === "typed-message-drops" ? violations <= thresholds.typedMessageDropsMax : violations === 0
|
|
1444
|
+
);
|
|
1262
1445
|
const state = ok ? "ok" : "violation";
|
|
1263
1446
|
return {
|
|
1264
1447
|
name,
|
|
@@ -1276,16 +1459,26 @@ function buildReport(options) {
|
|
|
1276
1459
|
bytesOut: perBot.reduce((sum, b) => sum + b.bytesOut, 0),
|
|
1277
1460
|
corrections: perBot.reduce((sum, b) => sum + b.corrections, 0),
|
|
1278
1461
|
syncCorrections: perBot.reduce((sum, b) => sum + b.syncCorrections, 0),
|
|
1462
|
+
proxiedCorrections: perBot.reduce((sum, b) => sum + b.proxiedCorrections, 0),
|
|
1279
1463
|
suppressedCorrections: perBot.reduce((sum, b) => sum + b.suppressedCorrections, 0),
|
|
1280
1464
|
mispredictions: perBot.reduce((sum, b) => sum + b.mispredictions, 0),
|
|
1281
1465
|
mispredictionMagnitude: perBot.reduce((sum, b) => sum + b.mispredictionMagnitude, 0),
|
|
1282
1466
|
mispredictionMax: perBot.reduce((max, b) => Math.max(max, b.mispredictionMax), 0),
|
|
1283
1467
|
snaps: perBot.reduce((sum, b) => sum + b.snaps, 0),
|
|
1284
1468
|
calls: perBot.reduce((sum, b) => sum + b.calls, 0),
|
|
1285
|
-
errors: perBot.reduce((sum, b) => sum + b.errors, 0)
|
|
1469
|
+
errors: perBot.reduce((sum, b) => sum + b.errors, 0),
|
|
1470
|
+
messagesSent: perBot.reduce((sum, b) => sum + b.messagesSent, 0),
|
|
1471
|
+
messagesReceived: perBot.reduce((sum, b) => sum + b.messagesReceived, 0),
|
|
1472
|
+
messagesDropped: perBot.reduce((sum, b) => sum + b.messagesDropped, 0)
|
|
1286
1473
|
};
|
|
1287
1474
|
const bots = Math.max(1, perBot.length);
|
|
1288
1475
|
const convergence = convergenceStats(lags);
|
|
1476
|
+
const predicting = perBot.filter((b) => b.predicting);
|
|
1477
|
+
const prediction = predicting.length === 0 ? void 0 : {
|
|
1478
|
+
bots: predicting.length,
|
|
1479
|
+
proxies: predicting.reduce((sum, b) => sum + b.proxies, 0),
|
|
1480
|
+
absent: predicting.reduce((sum, b) => sum + b.absent, 0)
|
|
1481
|
+
};
|
|
1289
1482
|
return {
|
|
1290
1483
|
ok: invariants.every((i) => i.ok),
|
|
1291
1484
|
bots: perBot.length,
|
|
@@ -1300,10 +1493,115 @@ function buildReport(options) {
|
|
|
1300
1493
|
convergence,
|
|
1301
1494
|
convergenceLagMs: convergence?.p50Ms,
|
|
1302
1495
|
tracePath: options.tracePath,
|
|
1303
|
-
...options.profiles !== void 0 && options.profiles.length > 0 ? { profile: options.profiles.reduce(mergeProfiles) } : {}
|
|
1496
|
+
...options.profiles !== void 0 && options.profiles.length > 0 ? { profile: options.profiles.reduce(mergeProfiles) } : {},
|
|
1497
|
+
...prediction !== void 0 ? { prediction } : {},
|
|
1498
|
+
...options.matchmaking !== void 0 ? { matchmaking: matchmakingSummary(options.matchmaking) } : {}
|
|
1304
1499
|
};
|
|
1305
1500
|
}
|
|
1306
1501
|
|
|
1502
|
+
// src/match.ts
|
|
1503
|
+
import { MatchError, createParty, findMatch } from "@irtio/client";
|
|
1504
|
+
var DEFAULT_MATCH_TIMEOUT_MS = 2e4;
|
|
1505
|
+
function describe(err) {
|
|
1506
|
+
if (err instanceof MatchError) return { code: err.code, message: err.message };
|
|
1507
|
+
return { code: "E_MATCH_FAILED", message: err instanceof Error ? err.message : String(err) };
|
|
1508
|
+
}
|
|
1509
|
+
async function matchBots(n, options) {
|
|
1510
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
1511
|
+
throw new Error(`irtio bots: matchBots needs at least one bot, got ${String(n)}`);
|
|
1512
|
+
}
|
|
1513
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1514
|
+
for (let i = 0; i < n; i++) {
|
|
1515
|
+
const key = options.party?.(i);
|
|
1516
|
+
if (key === void 0) continue;
|
|
1517
|
+
const members = groups.get(key) ?? [];
|
|
1518
|
+
members.push(i);
|
|
1519
|
+
groups.set(key, members);
|
|
1520
|
+
}
|
|
1521
|
+
const parties = /* @__PURE__ */ new Map();
|
|
1522
|
+
const partyErrors = /* @__PURE__ */ new Map();
|
|
1523
|
+
for (const [key, members] of groups) {
|
|
1524
|
+
if (members.length < 2) continue;
|
|
1525
|
+
try {
|
|
1526
|
+
const minted = await createParty(options.project, {
|
|
1527
|
+
size: members.length,
|
|
1528
|
+
controlUrl: options.controlUrl,
|
|
1529
|
+
...options.fetch !== void 0 ? { fetch: options.fetch } : {}
|
|
1530
|
+
});
|
|
1531
|
+
parties.set(key, minted.party);
|
|
1532
|
+
} catch (err) {
|
|
1533
|
+
partyErrors.set(key, describe(err));
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_MATCH_TIMEOUT_MS;
|
|
1537
|
+
const attempts = await Promise.all(
|
|
1538
|
+
Array.from({ length: n }, async (_unused, index) => {
|
|
1539
|
+
const key = options.party?.(index);
|
|
1540
|
+
const party = key !== void 0 && parties.has(key) ? parties.get(key) : void 0;
|
|
1541
|
+
const startedAt = Date.now();
|
|
1542
|
+
const failed = key !== void 0 ? partyErrors.get(key) : void 0;
|
|
1543
|
+
if (failed !== void 0) {
|
|
1544
|
+
return { index, waitedMs: 0, error: failed, ...key !== void 0 ? { party: key } : {} };
|
|
1545
|
+
}
|
|
1546
|
+
const identity = options.identity?.(index);
|
|
1547
|
+
try {
|
|
1548
|
+
const ticket = await findMatch(options.project, {
|
|
1549
|
+
controlUrl: options.controlUrl,
|
|
1550
|
+
timeoutMs,
|
|
1551
|
+
...options.queue !== void 0 ? { queue: options.queue } : {},
|
|
1552
|
+
...party !== void 0 ? { party } : {},
|
|
1553
|
+
...identity !== void 0 ? { identity } : {},
|
|
1554
|
+
...options.fetch !== void 0 ? { fetch: options.fetch } : {}
|
|
1555
|
+
});
|
|
1556
|
+
return {
|
|
1557
|
+
index,
|
|
1558
|
+
ticket,
|
|
1559
|
+
waitedMs: Date.now() - startedAt,
|
|
1560
|
+
...key !== void 0 ? { party: key } : {}
|
|
1561
|
+
};
|
|
1562
|
+
} catch (err) {
|
|
1563
|
+
return {
|
|
1564
|
+
index,
|
|
1565
|
+
waitedMs: Date.now() - startedAt,
|
|
1566
|
+
error: describe(err),
|
|
1567
|
+
...key !== void 0 ? { party: key } : {}
|
|
1568
|
+
};
|
|
1569
|
+
}
|
|
1570
|
+
})
|
|
1571
|
+
);
|
|
1572
|
+
const tickets = [];
|
|
1573
|
+
const failures = [];
|
|
1574
|
+
for (const attempt of attempts) {
|
|
1575
|
+
if (attempt.ticket === void 0) {
|
|
1576
|
+
failures.push({
|
|
1577
|
+
requested: attempt.index,
|
|
1578
|
+
code: attempt.error?.code ?? "E_MATCH_FAILED",
|
|
1579
|
+
message: attempt.error?.message ?? "no ticket and no error",
|
|
1580
|
+
waitedMs: attempt.waitedMs,
|
|
1581
|
+
...attempt.party !== void 0 ? { party: attempt.party } : {}
|
|
1582
|
+
});
|
|
1583
|
+
continue;
|
|
1584
|
+
}
|
|
1585
|
+
tickets.push({
|
|
1586
|
+
bot: tickets.length,
|
|
1587
|
+
requested: attempt.index,
|
|
1588
|
+
ticket: attempt.ticket,
|
|
1589
|
+
waitedMs: attempt.waitedMs,
|
|
1590
|
+
...attempt.party !== void 0 ? { party: attempt.party } : {}
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
return { tickets, failures, parties };
|
|
1594
|
+
}
|
|
1595
|
+
function roomsOf(tickets) {
|
|
1596
|
+
const rooms = /* @__PURE__ */ new Map();
|
|
1597
|
+
for (const t of tickets) {
|
|
1598
|
+
const bots = rooms.get(t.ticket.room) ?? [];
|
|
1599
|
+
bots.push(t.bot);
|
|
1600
|
+
rooms.set(t.ticket.room, bots);
|
|
1601
|
+
}
|
|
1602
|
+
return rooms;
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1307
1605
|
// src/scenario.ts
|
|
1308
1606
|
function defineScenario(scenario) {
|
|
1309
1607
|
return scenario;
|
|
@@ -1547,6 +1845,11 @@ async function spawnBots(n, options = {}) {
|
|
|
1547
1845
|
if (!Number.isInteger(n) || n < 1) {
|
|
1548
1846
|
throw new Error(`irtio bots: spawnBots needs at least one bot, got ${String(n)}`);
|
|
1549
1847
|
}
|
|
1848
|
+
if (options.physics !== void 0 && options.physics2d !== void 0) {
|
|
1849
|
+
throw new Error(
|
|
1850
|
+
"irtio: joinRoom was given both { physics } and { physics2d }. A room runs one engine and the client predicts with that one, so pass the option matching the engine your room config declares."
|
|
1851
|
+
);
|
|
1852
|
+
}
|
|
1550
1853
|
const startedAt = Date.now();
|
|
1551
1854
|
const thresholds = {
|
|
1552
1855
|
budgetBytesPerSec: options.budgetBytesPerSec ?? DEFAULT_THRESHOLDS.budgetBytesPerSec,
|
|
@@ -1554,7 +1857,8 @@ async function spawnBots(n, options = {}) {
|
|
|
1554
1857
|
handlerErrorsMax: options.handlerErrorsMax ?? DEFAULT_THRESHOLDS.handlerErrorsMax,
|
|
1555
1858
|
mispredictionMagnitudeMax: options.mispredictionMagnitudeMax ?? DEFAULT_THRESHOLDS.mispredictionMagnitudeMax,
|
|
1556
1859
|
snapsMax: options.snapsMax ?? DEFAULT_THRESHOLDS.snapsMax,
|
|
1557
|
-
overrunsMax: options.overrunsMax ?? DEFAULT_THRESHOLDS.overrunsMax
|
|
1860
|
+
overrunsMax: options.overrunsMax ?? DEFAULT_THRESHOLDS.overrunsMax,
|
|
1861
|
+
typedMessageDropsMax: options.typedMessageDropsMax ?? DEFAULT_THRESHOLDS.typedMessageDropsMax
|
|
1558
1862
|
};
|
|
1559
1863
|
const ext = options.schema ? withBuiltins2(options.schema) : relaySchema;
|
|
1560
1864
|
const seed = options.seed ?? DEFAULT_SEED;
|
|
@@ -1615,12 +1919,16 @@ async function spawnBots(n, options = {}) {
|
|
|
1615
1919
|
...options.flushMs !== void 0 ? { writeIntervalMs: options.flushMs } : {},
|
|
1616
1920
|
...options.rpc !== void 0 ? { rpc: options.rpc } : {},
|
|
1617
1921
|
...options.physics !== void 0 ? { physics: options.physics } : {},
|
|
1922
|
+
...options.physics2d !== void 0 ? { physics2d: options.physics2d } : {},
|
|
1618
1923
|
...options.profile === true ? { profile: true } : {}
|
|
1619
1924
|
}) : joinRelay(common);
|
|
1620
1925
|
const room = await withJoinTimeout(joining, index, joinTimeoutMs, observer, sockets);
|
|
1621
1926
|
room.on("correct", (correction) => observer.onCorrection(correction));
|
|
1622
1927
|
const prediction = room.prediction;
|
|
1623
|
-
if (prediction)
|
|
1928
|
+
if (prediction) {
|
|
1929
|
+
observer.bodyKind = (c, id) => prediction.predicts(c, id) ? "predicted" : prediction.proxied(c, id) ? "proxied" : "absent";
|
|
1930
|
+
observer.prediction = prediction;
|
|
1931
|
+
}
|
|
1624
1932
|
observers[index] = observer;
|
|
1625
1933
|
return new BotImpl(
|
|
1626
1934
|
index,
|
|
@@ -1632,6 +1940,46 @@ async function spawnBots(n, options = {}) {
|
|
|
1632
1940
|
shots
|
|
1633
1941
|
);
|
|
1634
1942
|
}
|
|
1943
|
+
if (options.match !== void 0) {
|
|
1944
|
+
const matched = await matchBots(n, options.match);
|
|
1945
|
+
if (matched.tickets.length === 0) {
|
|
1946
|
+
const first2 = matched.failures[0];
|
|
1947
|
+
throw new Error(
|
|
1948
|
+
`irtio bots: no bot got a ticket from the ${options.match.queue ?? "default"} queue at ${options.match.controlUrl}, so there is no room to run in` + (first2 !== void 0 ? ` (${first2.code}: ${first2.message})` : "")
|
|
1949
|
+
);
|
|
1950
|
+
}
|
|
1951
|
+
const joined = await Promise.allSettled(
|
|
1952
|
+
matched.tickets.map((t) => joinOne(t.bot, t.ticket.room))
|
|
1953
|
+
);
|
|
1954
|
+
const failure = joined.find((r) => r.status === "rejected");
|
|
1955
|
+
if (failure !== void 0) {
|
|
1956
|
+
for (const settled of joined) {
|
|
1957
|
+
if (settled.status === "fulfilled") settled.value.room.leave();
|
|
1958
|
+
}
|
|
1959
|
+
throw failure.reason;
|
|
1960
|
+
}
|
|
1961
|
+
for (const settled of joined) {
|
|
1962
|
+
if (settled.status === "fulfilled") bots.push(settled.value);
|
|
1963
|
+
}
|
|
1964
|
+
return finishRun(bots, {
|
|
1965
|
+
roomId: matched.tickets[0]?.ticket.room ?? "",
|
|
1966
|
+
matches: matched.tickets,
|
|
1967
|
+
rooms: roomsOf(matched.tickets),
|
|
1968
|
+
matchFailures: matched.failures,
|
|
1969
|
+
matchmaking: {
|
|
1970
|
+
tickets: matched.tickets.map((t) => ({
|
|
1971
|
+
bot: t.bot,
|
|
1972
|
+
room: t.ticket.room,
|
|
1973
|
+
queue: t.ticket.queue,
|
|
1974
|
+
size: t.ticket.size,
|
|
1975
|
+
backfill: t.ticket.backfill,
|
|
1976
|
+
waitedMs: t.waitedMs,
|
|
1977
|
+
...t.party !== void 0 ? { party: t.party } : {}
|
|
1978
|
+
})),
|
|
1979
|
+
failures: matched.failures.map((f) => ({ requested: f.requested, code: f.code }))
|
|
1980
|
+
}
|
|
1981
|
+
});
|
|
1982
|
+
}
|
|
1635
1983
|
const first = await joinOne(0, options.room ?? "");
|
|
1636
1984
|
bots.push(first);
|
|
1637
1985
|
const roomId = first.room.id;
|
|
@@ -1651,102 +1999,115 @@ async function spawnBots(n, options = {}) {
|
|
|
1651
1999
|
if (settled.status === "fulfilled") bots.push(settled.value);
|
|
1652
2000
|
}
|
|
1653
2001
|
}
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
await bot.until(() => bot.stopped, { label: "the run to end", timeoutMs: 24 * 36e5 });
|
|
1660
|
-
return;
|
|
1661
|
-
}
|
|
1662
|
-
try {
|
|
1663
|
-
await script(bot);
|
|
1664
|
-
} catch (error) {
|
|
1665
|
-
scriptErrors.push({ bot: bot.index, error });
|
|
1666
|
-
firstError ??= error;
|
|
1667
|
-
}
|
|
2002
|
+
return finishRun(bots, {
|
|
2003
|
+
roomId,
|
|
2004
|
+
matches: [],
|
|
2005
|
+
rooms: /* @__PURE__ */ new Map([[roomId, bots.map((b) => b.index)]]),
|
|
2006
|
+
matchFailures: []
|
|
1668
2007
|
});
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
}
|
|
1678
|
-
let goneSince = 0;
|
|
1679
|
-
let goneTimer;
|
|
1680
|
-
if (roomGoneGraceMs > 0) {
|
|
1681
|
-
goneTimer = setInterval(() => {
|
|
1682
|
-
const down = observers.every(
|
|
1683
|
-
(o) => o.lastStatus === "reconnecting" || o.lastStatus === "closed"
|
|
1684
|
-
);
|
|
1685
|
-
if (!down) {
|
|
1686
|
-
goneSince = 0;
|
|
2008
|
+
function finishRun(bots2, placement) {
|
|
2009
|
+
const roomId2 = placement.roomId;
|
|
2010
|
+
const scriptErrors = [];
|
|
2011
|
+
let firstError;
|
|
2012
|
+
const script = options.script;
|
|
2013
|
+
const scripts = bots2.map(async (bot) => {
|
|
2014
|
+
if (!script) {
|
|
2015
|
+
await bot.until(() => bot.stopped, { label: "the run to end", timeoutMs: 24 * 36e5 });
|
|
1687
2016
|
return;
|
|
1688
2017
|
}
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
goneTimer.unref?.();
|
|
1695
|
-
}
|
|
1696
|
-
void finished.then(() => {
|
|
1697
|
-
endedBy ??= "scripts";
|
|
1698
|
-
if (deadline) clearTimeout(deadline);
|
|
1699
|
-
if (goneTimer) clearInterval(goneTimer);
|
|
1700
|
-
});
|
|
1701
|
-
let stoppedAt;
|
|
1702
|
-
let tickHealth;
|
|
1703
|
-
const rings = () => observers.map((o) => o.ring);
|
|
1704
|
-
let profiles;
|
|
1705
|
-
const runner = {
|
|
1706
|
-
bots,
|
|
1707
|
-
roomId,
|
|
1708
|
-
trace: makeTrace(startedAt, rings),
|
|
1709
|
-
scriptErrors,
|
|
1710
|
-
shots,
|
|
1711
|
-
conditions,
|
|
1712
|
-
get endedBy() {
|
|
1713
|
-
return endedBy;
|
|
1714
|
-
},
|
|
1715
|
-
[Symbol.iterator]: () => bots[Symbol.iterator](),
|
|
1716
|
-
async done() {
|
|
1717
|
-
await finished;
|
|
1718
|
-
if (firstError !== void 0) throw firstError;
|
|
1719
|
-
},
|
|
1720
|
-
recordTickHealth(reading) {
|
|
1721
|
-
tickHealth = reading;
|
|
1722
|
-
},
|
|
1723
|
-
report() {
|
|
1724
|
-
if (options.profile === true) {
|
|
1725
|
-
const live = bots.map((bot) => bot.room.profile?.total()).filter((p) => p !== void 0);
|
|
1726
|
-
if (live.length > 0) profiles = live;
|
|
2018
|
+
try {
|
|
2019
|
+
await script(bot);
|
|
2020
|
+
} catch (error) {
|
|
2021
|
+
scriptErrors.push({ bot: bot.index, error });
|
|
2022
|
+
firstError ??= error;
|
|
1727
2023
|
}
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
});
|
|
1737
|
-
}
|
|
1738
|
-
|
|
2024
|
+
});
|
|
2025
|
+
const finished = Promise.all(scripts).then(() => void 0);
|
|
2026
|
+
let endedBy;
|
|
2027
|
+
let deadline;
|
|
2028
|
+
if (options.durationMs !== void 0) {
|
|
2029
|
+
deadline = setTimeout(() => {
|
|
2030
|
+
endedBy ??= "duration";
|
|
2031
|
+
for (const bot of bots2) bot.stop();
|
|
2032
|
+
}, options.durationMs);
|
|
2033
|
+
}
|
|
2034
|
+
let goneSince = 0;
|
|
2035
|
+
let goneTimer;
|
|
2036
|
+
if (roomGoneGraceMs > 0) {
|
|
2037
|
+
goneTimer = setInterval(() => {
|
|
2038
|
+
const down = observers.every(
|
|
2039
|
+
(o) => o.lastStatus === "reconnecting" || o.lastStatus === "closed"
|
|
2040
|
+
);
|
|
2041
|
+
if (!down) {
|
|
2042
|
+
goneSince = 0;
|
|
2043
|
+
return;
|
|
2044
|
+
}
|
|
2045
|
+
goneSince ||= Date.now();
|
|
2046
|
+
if (Date.now() - goneSince < roomGoneGraceMs) return;
|
|
2047
|
+
endedBy ??= "room-gone";
|
|
2048
|
+
for (const bot of bots2) bot.stop();
|
|
2049
|
+
}, ROOM_GONE_POLL_MS);
|
|
2050
|
+
goneTimer.unref?.();
|
|
2051
|
+
}
|
|
2052
|
+
void finished.then(() => {
|
|
2053
|
+
endedBy ??= "scripts";
|
|
1739
2054
|
if (deadline) clearTimeout(deadline);
|
|
1740
2055
|
if (goneTimer) clearInterval(goneTimer);
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
2056
|
+
});
|
|
2057
|
+
let stoppedAt;
|
|
2058
|
+
let tickHealth;
|
|
2059
|
+
const rings = () => observers.map((o) => o.ring);
|
|
2060
|
+
let profiles;
|
|
2061
|
+
const runner = {
|
|
2062
|
+
bots: bots2,
|
|
2063
|
+
roomId: roomId2,
|
|
2064
|
+
matches: placement.matches,
|
|
2065
|
+
rooms: placement.rooms,
|
|
2066
|
+
matchFailures: placement.matchFailures,
|
|
2067
|
+
trace: makeTrace(startedAt, rings),
|
|
2068
|
+
scriptErrors,
|
|
2069
|
+
shots,
|
|
2070
|
+
conditions,
|
|
2071
|
+
get endedBy() {
|
|
2072
|
+
return endedBy;
|
|
2073
|
+
},
|
|
2074
|
+
[Symbol.iterator]: () => bots2[Symbol.iterator](),
|
|
2075
|
+
async done() {
|
|
2076
|
+
await finished;
|
|
2077
|
+
if (firstError !== void 0) throw firstError;
|
|
2078
|
+
},
|
|
2079
|
+
recordTickHealth(reading) {
|
|
2080
|
+
tickHealth = reading;
|
|
2081
|
+
},
|
|
2082
|
+
report() {
|
|
2083
|
+
if (options.profile === true) {
|
|
2084
|
+
const live = bots2.map((bot) => bot.room.profile?.total()).filter((p) => p !== void 0);
|
|
2085
|
+
if (live.length > 0) profiles = live;
|
|
2086
|
+
}
|
|
2087
|
+
return buildReport({
|
|
2088
|
+
observers,
|
|
2089
|
+
roomId: roomId2,
|
|
2090
|
+
durationMs: (stoppedAt ?? Date.now()) - startedAt,
|
|
2091
|
+
lags,
|
|
2092
|
+
thresholds,
|
|
2093
|
+
tickHealth,
|
|
2094
|
+
...placement.matchmaking !== void 0 ? { matchmaking: placement.matchmaking } : {},
|
|
2095
|
+
...profiles !== void 0 ? { profiles } : {}
|
|
2096
|
+
});
|
|
2097
|
+
},
|
|
2098
|
+
async stop() {
|
|
2099
|
+
if (deadline) clearTimeout(deadline);
|
|
2100
|
+
if (goneTimer) clearInterval(goneTimer);
|
|
2101
|
+
for (const bot of bots2) bot.stop();
|
|
2102
|
+
await finished;
|
|
2103
|
+
stoppedAt ??= Date.now();
|
|
2104
|
+
for (const observer of observers) observer.stopping = true;
|
|
2105
|
+
for (const bot of bots2) bot.room.leave();
|
|
2106
|
+
return this.report();
|
|
2107
|
+
}
|
|
2108
|
+
};
|
|
2109
|
+
return runner;
|
|
2110
|
+
}
|
|
1750
2111
|
}
|
|
1751
2112
|
|
|
1752
2113
|
// src/timeline.ts
|
|
@@ -1882,6 +2243,7 @@ function makeTimeline(dump) {
|
|
|
1882
2243
|
export {
|
|
1883
2244
|
BotObserver,
|
|
1884
2245
|
DEFAULT_JOIN_TIMEOUT_MS,
|
|
2246
|
+
DEFAULT_MATCH_TIMEOUT_MS,
|
|
1885
2247
|
DEFAULT_REORDER_MS,
|
|
1886
2248
|
DEFAULT_ROOM_GONE_GRACE_MS,
|
|
1887
2249
|
DEFAULT_SEED,
|
|
@@ -1911,10 +2273,13 @@ export {
|
|
|
1911
2273
|
makeRng,
|
|
1912
2274
|
makeTimeline,
|
|
1913
2275
|
makeTrace,
|
|
2276
|
+
matchBots,
|
|
2277
|
+
matchmakingSummary,
|
|
1914
2278
|
newConditionCounters,
|
|
1915
2279
|
nextValue,
|
|
1916
2280
|
randomScript,
|
|
1917
2281
|
relayEchoScript,
|
|
2282
|
+
roomsOf,
|
|
1918
2283
|
snapshotVisibilityLeaks,
|
|
1919
2284
|
spawnBots
|
|
1920
2285
|
};
|