@irtio/bots 0.5.2 → 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/index.d.ts +561 -55
- package/dist/index.js +651 -46
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -110,29 +110,382 @@ function frameVisibilityLeaks(ext, role, type, payload, spatial) {
|
|
|
110
110
|
return deltaVisibilityLeaks(ext, role, decodeDelta(ext, payload), spatial);
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
// src/conditions.ts
|
|
114
|
+
import { FrameType as FrameType2 } from "@irtio/protocol";
|
|
115
|
+
var STATE_FRAMES = /* @__PURE__ */ new Set([
|
|
116
|
+
FrameType2.WRITE,
|
|
117
|
+
FrameType2.DELTA,
|
|
118
|
+
FrameType2.CORRECT,
|
|
119
|
+
FrameType2.MSG
|
|
120
|
+
]);
|
|
121
|
+
var DUPLICATE_GAP_MS = 1;
|
|
122
|
+
var DEFAULT_REORDER_MS = 50;
|
|
123
|
+
function newConditionCounters() {
|
|
124
|
+
return {
|
|
125
|
+
droppedOut: 0,
|
|
126
|
+
droppedIn: 0,
|
|
127
|
+
duplicatedOut: 0,
|
|
128
|
+
duplicatedIn: 0,
|
|
129
|
+
reorderedOut: 0,
|
|
130
|
+
reorderedIn: 0,
|
|
131
|
+
delayed: 0
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function hasConditions(c) {
|
|
135
|
+
if (!c) return false;
|
|
136
|
+
return (c.rttMs ?? 0) > 0 || (c.jitterMs ?? 0) > 0 || (c.loss ?? 0) > 0 || (c.duplicate ?? 0) > 0 || (c.reorder ?? 0) > 0;
|
|
137
|
+
}
|
|
138
|
+
function describeConditions(c) {
|
|
139
|
+
const parts = [];
|
|
140
|
+
if ((c.rttMs ?? 0) > 0) parts.push(`rtt ${c.rttMs} ms`);
|
|
141
|
+
if ((c.jitterMs ?? 0) > 0) parts.push(`jitter ${c.jitterMs} ms`);
|
|
142
|
+
if ((c.loss ?? 0) > 0) parts.push(`loss ${pct(c.loss ?? 0)}`);
|
|
143
|
+
if ((c.duplicate ?? 0) > 0) parts.push(`duplicate ${pct(c.duplicate ?? 0)}`);
|
|
144
|
+
if ((c.reorder ?? 0) > 0) {
|
|
145
|
+
parts.push(`reorder ${pct(c.reorder ?? 0)} within ${c.reorderMs ?? DEFAULT_REORDER_MS} ms`);
|
|
146
|
+
}
|
|
147
|
+
return parts.length === 0 ? "none" : parts.join(", ");
|
|
148
|
+
}
|
|
149
|
+
function pct(v) {
|
|
150
|
+
return `${Math.round(v * 1e3) / 10}%`;
|
|
151
|
+
}
|
|
152
|
+
var realTimers = {
|
|
153
|
+
now: () => Date.now(),
|
|
154
|
+
setTimeout(fn, ms) {
|
|
155
|
+
const timer = setTimeout(fn, ms);
|
|
156
|
+
timer.unref?.();
|
|
157
|
+
return () => clearTimeout(timer);
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
function conditionedTransport(base, conditions, rng, options = {}) {
|
|
161
|
+
const counters = options.counters ?? newConditionCounters();
|
|
162
|
+
const timers = options.timers ?? realTimers;
|
|
163
|
+
const rttMs = Math.max(0, conditions.rttMs ?? 0);
|
|
164
|
+
const jitterMs = Math.max(0, conditions.jitterMs ?? 0);
|
|
165
|
+
const loss = clamp01(conditions.loss ?? 0);
|
|
166
|
+
const duplicate = clamp01(conditions.duplicate ?? 0);
|
|
167
|
+
const reorder = clamp01(conditions.reorder ?? 0);
|
|
168
|
+
const reorderMs = Math.max(0, conditions.reorderMs ?? DEFAULT_REORDER_MS);
|
|
169
|
+
return {
|
|
170
|
+
connect(url) {
|
|
171
|
+
const socket = base.connect(url);
|
|
172
|
+
const cancels = /* @__PURE__ */ new Set();
|
|
173
|
+
let closed = false;
|
|
174
|
+
let nextIn = 0;
|
|
175
|
+
let nextOut = 0;
|
|
176
|
+
function schedule(delayMs, run) {
|
|
177
|
+
if (closed) return;
|
|
178
|
+
counters.delayed++;
|
|
179
|
+
let cancel = () => void 0;
|
|
180
|
+
const fire = () => {
|
|
181
|
+
cancels.delete(cancel);
|
|
182
|
+
if (!closed) run();
|
|
183
|
+
};
|
|
184
|
+
if (delayMs <= 0) {
|
|
185
|
+
cancel = timers.setTimeout(fire, 0);
|
|
186
|
+
} else {
|
|
187
|
+
cancel = timers.setTimeout(fire, delayMs);
|
|
188
|
+
}
|
|
189
|
+
cancels.add(cancel);
|
|
190
|
+
}
|
|
191
|
+
function deliver(dir, bytes, run) {
|
|
192
|
+
const type = bytes.length > 0 ? bytes[0] ?? -1 : -1;
|
|
193
|
+
const stateful = STATE_FRAMES.has(type);
|
|
194
|
+
if (stateful && loss > 0 && rng.next() < loss) {
|
|
195
|
+
if (dir === "in") counters.droppedIn++;
|
|
196
|
+
else counters.droppedOut++;
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const now = timers.now();
|
|
200
|
+
let at = now + rttMs / 2 + (jitterMs > 0 ? rng.next() * jitterMs : 0);
|
|
201
|
+
const reordered = stateful && reorder > 0 && rng.next() < reorder;
|
|
202
|
+
if (reordered) {
|
|
203
|
+
at += reorderMs > 0 ? rng.next() * reorderMs : 0;
|
|
204
|
+
if (dir === "in") counters.reorderedIn++;
|
|
205
|
+
else counters.reorderedOut++;
|
|
206
|
+
} else {
|
|
207
|
+
const floor = dir === "in" ? nextIn : nextOut;
|
|
208
|
+
at = Math.max(at, floor);
|
|
209
|
+
if (dir === "in") nextIn = at;
|
|
210
|
+
else nextOut = at;
|
|
211
|
+
}
|
|
212
|
+
schedule(at - now, run);
|
|
213
|
+
if (stateful && duplicate > 0 && rng.next() < duplicate) {
|
|
214
|
+
if (dir === "in") counters.duplicatedIn++;
|
|
215
|
+
else counters.duplicatedOut++;
|
|
216
|
+
schedule(at - now + DUPLICATE_GAP_MS, run);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const wrapper = {
|
|
220
|
+
onopen: null,
|
|
221
|
+
onmessage: null,
|
|
222
|
+
onclose: null,
|
|
223
|
+
onerror: null,
|
|
224
|
+
send(bytes) {
|
|
225
|
+
const copy = bytes.slice();
|
|
226
|
+
deliver("out", copy, () => socket.send(copy));
|
|
227
|
+
},
|
|
228
|
+
close() {
|
|
229
|
+
closed = true;
|
|
230
|
+
for (const cancel of cancels) cancel();
|
|
231
|
+
cancels.clear();
|
|
232
|
+
socket.close();
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
socket.onopen = () => wrapper.onopen?.();
|
|
236
|
+
socket.onmessage = (bytes) => {
|
|
237
|
+
const copy = bytes.slice();
|
|
238
|
+
deliver("in", copy, () => wrapper.onmessage?.(copy));
|
|
239
|
+
};
|
|
240
|
+
socket.onclose = (info) => {
|
|
241
|
+
closed = true;
|
|
242
|
+
for (const cancel of cancels) cancel();
|
|
243
|
+
cancels.clear();
|
|
244
|
+
wrapper.onclose?.(info);
|
|
245
|
+
};
|
|
246
|
+
socket.onerror = (error) => wrapper.onerror?.(error);
|
|
247
|
+
return wrapper;
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
function clamp01(v) {
|
|
252
|
+
return Math.min(1, Math.max(0, v));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// src/hits.ts
|
|
256
|
+
function entityValue(frameState, collection, id) {
|
|
257
|
+
const table = frameState[collection];
|
|
258
|
+
if (typeof table !== "object" || table === null) return void 0;
|
|
259
|
+
const record = table[id];
|
|
260
|
+
if (typeof record !== "object" || record === null) return void 0;
|
|
261
|
+
const value = record.value;
|
|
262
|
+
return typeof value === "object" && value !== null ? value : void 0;
|
|
263
|
+
}
|
|
264
|
+
function correlateShots(shots, dump) {
|
|
265
|
+
const frames = dump.frames;
|
|
266
|
+
return shots.map((shot) => {
|
|
267
|
+
const arrivedAt = shot.sentAt + shot.uplinkMs;
|
|
268
|
+
const frame = frames.find((f) => f.at >= arrivedAt);
|
|
269
|
+
if (frame === void 0) {
|
|
270
|
+
return {
|
|
271
|
+
...shot,
|
|
272
|
+
serverTick: void 0,
|
|
273
|
+
estimated: shot.uplinkMs > 0,
|
|
274
|
+
authoritative: void 0,
|
|
275
|
+
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})`
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
const value = entityValue(frame.state, shot.collection, shot.target);
|
|
280
|
+
if (value === void 0) {
|
|
281
|
+
return {
|
|
282
|
+
...shot,
|
|
283
|
+
serverTick: frame.tick,
|
|
284
|
+
estimated: shot.uplinkMs > 0,
|
|
285
|
+
authoritative: void 0,
|
|
286
|
+
missDistance: void 0,
|
|
287
|
+
unresolved: `${shot.collection}.${shot.target} is not in the recording at tick ${frame.tick}`
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
const authoritative = {};
|
|
291
|
+
let sum = 0;
|
|
292
|
+
let missing;
|
|
293
|
+
for (const [field, aimed] of Object.entries(shot.aim)) {
|
|
294
|
+
const actual = value[field];
|
|
295
|
+
if (typeof actual !== "number") {
|
|
296
|
+
missing ??= `${shot.collection}.${shot.target}.${field} is not a number on the timeline`;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
authoritative[field] = actual;
|
|
300
|
+
sum += (actual - aimed) ** 2;
|
|
301
|
+
}
|
|
302
|
+
if (missing !== void 0) {
|
|
303
|
+
return {
|
|
304
|
+
...shot,
|
|
305
|
+
serverTick: frame.tick,
|
|
306
|
+
estimated: shot.uplinkMs > 0,
|
|
307
|
+
authoritative,
|
|
308
|
+
missDistance: void 0,
|
|
309
|
+
unresolved: missing
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
...shot,
|
|
314
|
+
serverTick: frame.tick,
|
|
315
|
+
estimated: shot.uplinkMs > 0,
|
|
316
|
+
authoritative,
|
|
317
|
+
missDistance: Math.sqrt(sum)
|
|
318
|
+
};
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// src/truth.ts
|
|
323
|
+
function captureClientState(schema, state) {
|
|
324
|
+
const view = state ?? {};
|
|
325
|
+
const out = {};
|
|
326
|
+
for (const desc of schema.collections) {
|
|
327
|
+
const raw = view[desc.name];
|
|
328
|
+
if (raw === void 0) continue;
|
|
329
|
+
const collection = asCollection(raw);
|
|
330
|
+
if (collection) {
|
|
331
|
+
const records = {};
|
|
332
|
+
for (const id of collection.ids()) {
|
|
333
|
+
const value = collection.get(id);
|
|
334
|
+
if (value === void 0) continue;
|
|
335
|
+
records[id] = { owner: collection.ownerOf(id), value: plain(value) };
|
|
336
|
+
}
|
|
337
|
+
out[desc.name] = records;
|
|
338
|
+
} else {
|
|
339
|
+
out[desc.name] = plain(raw);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return out;
|
|
343
|
+
}
|
|
344
|
+
function asCollection(value) {
|
|
345
|
+
return value !== null && typeof value === "object" && typeof value.ownerOf === "function" ? value : void 0;
|
|
346
|
+
}
|
|
347
|
+
function plain(value) {
|
|
348
|
+
if (value === null || typeof value !== "object") return value;
|
|
349
|
+
const out = {};
|
|
350
|
+
for (const [k, v] of Object.entries(value)) out[k] = plain(v);
|
|
351
|
+
return out;
|
|
352
|
+
}
|
|
353
|
+
function diffAgainstSave(clients, saveState, meta) {
|
|
354
|
+
const bots = [];
|
|
355
|
+
const all = [];
|
|
356
|
+
let compared = 0;
|
|
357
|
+
for (const client of clients) {
|
|
358
|
+
const differences = [];
|
|
359
|
+
let seen = 0;
|
|
360
|
+
for (const [collection, held] of Object.entries(client.state)) {
|
|
361
|
+
const authoritative = saveState[collection];
|
|
362
|
+
if (isTable(held)) {
|
|
363
|
+
const table = isTable(authoritative) ? authoritative : {};
|
|
364
|
+
for (const [id, record] of Object.entries(held)) {
|
|
365
|
+
seen++;
|
|
366
|
+
const mine = record;
|
|
367
|
+
const theirs = table[id];
|
|
368
|
+
if (theirs === void 0) {
|
|
369
|
+
differences.push({
|
|
370
|
+
bot: client.bot,
|
|
371
|
+
collection,
|
|
372
|
+
id,
|
|
373
|
+
client: mine.value,
|
|
374
|
+
save: void 0,
|
|
375
|
+
detail: `bot ${client.bot} holds ${collection}.${id}, which the save does not have. A client holding an entity authority does not is a desync, not a visibility gap.`
|
|
376
|
+
});
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
if (mine.owner !== theirs.owner) {
|
|
380
|
+
differences.push({
|
|
381
|
+
bot: client.bot,
|
|
382
|
+
collection,
|
|
383
|
+
id,
|
|
384
|
+
field: "owner",
|
|
385
|
+
client: mine.owner,
|
|
386
|
+
save: theirs.owner,
|
|
387
|
+
detail: `bot ${client.bot} has ${collection}.${id} owned by ${String(mine.owner)}, the save says ${String(theirs.owner)}`
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
compareValues(client.bot, collection, id, mine.value, theirs.value, differences);
|
|
391
|
+
}
|
|
392
|
+
} else {
|
|
393
|
+
seen++;
|
|
394
|
+
compareValues(client.bot, collection, void 0, held, authoritative, differences);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
compared += seen;
|
|
398
|
+
bots.push({ bot: client.bot, compared: seen, differences, ok: differences.length === 0 });
|
|
399
|
+
all.push(...differences);
|
|
400
|
+
}
|
|
401
|
+
return {
|
|
402
|
+
ok: all.length === 0,
|
|
403
|
+
bots,
|
|
404
|
+
compared,
|
|
405
|
+
differences: all,
|
|
406
|
+
saveTick: meta.saveTick,
|
|
407
|
+
saveVersion: meta.saveVersion
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
function isTable(value) {
|
|
411
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
412
|
+
}
|
|
413
|
+
function compareValues(bot, collection, id, client, save, out) {
|
|
414
|
+
const where = id === void 0 ? collection : `${collection}.${id}`;
|
|
415
|
+
if (!isTable(client)) {
|
|
416
|
+
if (!same(client, save)) {
|
|
417
|
+
out.push({
|
|
418
|
+
bot,
|
|
419
|
+
collection,
|
|
420
|
+
...id !== void 0 ? { id } : {},
|
|
421
|
+
client,
|
|
422
|
+
save,
|
|
423
|
+
detail: `bot ${bot} has ${where} = ${show(client)}, the save has ${show(save)}`
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
const theirs = isTable(save) ? save : void 0;
|
|
429
|
+
for (const [field, value] of Object.entries(client)) {
|
|
430
|
+
const actual = theirs?.[field];
|
|
431
|
+
if (same(value, actual)) continue;
|
|
432
|
+
out.push({
|
|
433
|
+
bot,
|
|
434
|
+
collection,
|
|
435
|
+
...id !== void 0 ? { id } : {},
|
|
436
|
+
field,
|
|
437
|
+
client: value,
|
|
438
|
+
save: actual,
|
|
439
|
+
detail: `bot ${bot} has ${where}.${field} = ${show(value)}, the save has ${show(actual)}`
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
function same(a, b) {
|
|
444
|
+
if (a === b) return true;
|
|
445
|
+
if (typeof a === "number" && typeof b === "number") {
|
|
446
|
+
return Number.isNaN(a) && Number.isNaN(b);
|
|
447
|
+
}
|
|
448
|
+
if (isTable(a) && isTable(b)) {
|
|
449
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
450
|
+
for (const k of keys) if (!same(a[k], b[k])) return false;
|
|
451
|
+
return true;
|
|
452
|
+
}
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
function show(value) {
|
|
456
|
+
if (value === void 0) return "nothing";
|
|
457
|
+
if (typeof value === "number") return String(Math.round(value * 1e3) / 1e3);
|
|
458
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
459
|
+
if (isTable(value)) return JSON.stringify(value);
|
|
460
|
+
return String(value);
|
|
461
|
+
}
|
|
462
|
+
|
|
113
463
|
// src/observer.ts
|
|
114
464
|
import {
|
|
115
|
-
FrameType as
|
|
465
|
+
FrameType as FrameType4,
|
|
116
466
|
decodeErrorPayload,
|
|
117
467
|
decodeFrame,
|
|
118
468
|
decodeReply,
|
|
119
469
|
decodeWelcome,
|
|
120
470
|
errorByCode,
|
|
121
|
-
readCorrectClientTick
|
|
471
|
+
readCorrectClientTick,
|
|
472
|
+
readSchemaPayload,
|
|
473
|
+
withBuiltins
|
|
122
474
|
} from "@irtio/protocol";
|
|
123
475
|
import {
|
|
124
476
|
ByteReader,
|
|
125
477
|
applyDelta,
|
|
126
478
|
decodeDelta as decodeDelta2,
|
|
127
479
|
decodeDeltaFrom,
|
|
128
|
-
decodeSnapshot
|
|
480
|
+
decodeSnapshot,
|
|
481
|
+
schemaFromCanonical
|
|
129
482
|
} from "@irtio/schema";
|
|
130
483
|
|
|
131
484
|
// src/trace.ts
|
|
132
485
|
import { writeFile } from "fs/promises";
|
|
133
|
-
import { FrameType as
|
|
486
|
+
import { FrameType as FrameType3 } from "@irtio/protocol";
|
|
134
487
|
var FRAME_NAMES = new Map(
|
|
135
|
-
Object.entries(
|
|
488
|
+
Object.entries(FrameType3).map(([name, value]) => [value, name])
|
|
136
489
|
);
|
|
137
490
|
function frameName(type) {
|
|
138
491
|
return FRAME_NAMES.get(type) ?? `UNKNOWN(${type})`;
|
|
@@ -204,9 +557,15 @@ function simulationOnly(ext, delta, predicts) {
|
|
|
204
557
|
const predicted = predicts(dc.name, op.id);
|
|
205
558
|
for (const index of op.mask.fields) {
|
|
206
559
|
const field = desc.fields[index];
|
|
207
|
-
if (!field
|
|
208
|
-
if (
|
|
209
|
-
|
|
560
|
+
if (!field) return "mixed";
|
|
561
|
+
if (physics.bodyFields.has(field.name)) {
|
|
562
|
+
if (predicted) sawPredicted = true;
|
|
563
|
+
else sawSync = true;
|
|
564
|
+
} else if (!predicted && !physics.intents.includes(field.name)) {
|
|
565
|
+
sawSync = true;
|
|
566
|
+
} else {
|
|
567
|
+
return "mixed";
|
|
568
|
+
}
|
|
210
569
|
}
|
|
211
570
|
}
|
|
212
571
|
}
|
|
@@ -262,11 +621,22 @@ var BotObserver = class {
|
|
|
262
621
|
constructor(options) {
|
|
263
622
|
this.options = options;
|
|
264
623
|
this.ring = new TraceRing(options.traceLimit);
|
|
624
|
+
this.ext = options.ext;
|
|
265
625
|
}
|
|
266
626
|
options;
|
|
267
627
|
ring;
|
|
268
628
|
violations = /* @__PURE__ */ new Map();
|
|
269
629
|
counts = /* @__PURE__ */ new Map();
|
|
630
|
+
/**
|
|
631
|
+
* Bug #28: the decode extension, *mutable*. It starts as `options.ext` (the schema the run was
|
|
632
|
+
* spawned with) and is rebuilt in place when an inbound `SCHEMA` frame (13) lands — a D50
|
|
633
|
+
* additive `migrate` swaps the wire mid-session, the real client rebuilds its own decoders
|
|
634
|
+
* (`session.swapSchema`), and an observer still holding the v1 descriptor underruns on the v2
|
|
635
|
+
* resync WELCOME and fails `schema-validity` on a swap that was perfect.
|
|
636
|
+
*/
|
|
637
|
+
ext;
|
|
638
|
+
/** How many `SCHEMA` frames rebuilt {@link ext}. For tests and the trace, like the client's. */
|
|
639
|
+
schemaSwaps = 0;
|
|
270
640
|
id = "";
|
|
271
641
|
role = "";
|
|
272
642
|
roomId = "";
|
|
@@ -403,30 +773,32 @@ var BotObserver = class {
|
|
|
403
773
|
...note !== void 0 ? { note } : {}
|
|
404
774
|
};
|
|
405
775
|
this.ring.push(entry);
|
|
406
|
-
if (dir === "in" && type ===
|
|
776
|
+
if (dir === "in" && type === FrameType4.CORRECT) this.lastCorrectEntry = entry;
|
|
407
777
|
}
|
|
408
778
|
/** Decodes the payload independently. Throws on a bad frame; the caller counts that. */
|
|
409
779
|
inspect(dir, type, bytes, now) {
|
|
410
780
|
const payload = decodeFrame(bytes).payload;
|
|
411
781
|
if (dir === "out") {
|
|
412
|
-
if (type ===
|
|
413
|
-
if (type ===
|
|
782
|
+
if (type === FrameType4.CALL) this.calls++;
|
|
783
|
+
if (type === FrameType4.WRITE) this.recordWrites(decodeDelta2(this.ext, payload), now);
|
|
414
784
|
return void 0;
|
|
415
785
|
}
|
|
416
786
|
switch (type) {
|
|
417
|
-
case
|
|
787
|
+
case FrameType4.WELCOME:
|
|
418
788
|
return this.onWelcome(decodeWelcome(payload));
|
|
419
|
-
case
|
|
420
|
-
return this.onDelta(decodeDelta2(this.
|
|
421
|
-
case
|
|
789
|
+
case FrameType4.DELTA:
|
|
790
|
+
return this.onDelta(decodeDelta2(this.ext, payload), now);
|
|
791
|
+
case FrameType4.CORRECT: {
|
|
422
792
|
const r = new ByteReader(payload);
|
|
423
|
-
const delta = decodeDeltaFrom(this.
|
|
793
|
+
const delta = decodeDeltaFrom(this.ext, r);
|
|
424
794
|
return this.onCorrect(delta, readCorrectClientTick(r), now);
|
|
425
795
|
}
|
|
426
|
-
case
|
|
796
|
+
case FrameType4.ERROR:
|
|
427
797
|
return this.onError(payload);
|
|
428
|
-
case
|
|
798
|
+
case FrameType4.REPLY:
|
|
429
799
|
return this.onReply(payload);
|
|
800
|
+
case FrameType4.SCHEMA:
|
|
801
|
+
return this.onSchema(payload);
|
|
430
802
|
default:
|
|
431
803
|
return void 0;
|
|
432
804
|
}
|
|
@@ -435,12 +807,12 @@ var BotObserver = class {
|
|
|
435
807
|
this.id = welcome.clientId;
|
|
436
808
|
this.role = welcome.role;
|
|
437
809
|
this.roomId = welcome.roomId;
|
|
438
|
-
const snapshot = decodeSnapshot(this.
|
|
810
|
+
const snapshot = decodeSnapshot(this.ext, welcome.snapshot);
|
|
439
811
|
this.view = snapshot.state;
|
|
440
812
|
this.rememberSnapshot(snapshot.state);
|
|
441
813
|
this.countSpatialSnapshot(snapshot.state);
|
|
442
814
|
for (const leak of snapshotVisibilityLeaks(
|
|
443
|
-
this.
|
|
815
|
+
this.ext,
|
|
444
816
|
this.role,
|
|
445
817
|
snapshot.state,
|
|
446
818
|
this.spatialContext()
|
|
@@ -449,6 +821,22 @@ var BotObserver = class {
|
|
|
449
821
|
}
|
|
450
822
|
return `joined ${welcome.roomId} as ${welcome.clientId}/${welcome.role || "default"}`;
|
|
451
823
|
}
|
|
824
|
+
/**
|
|
825
|
+
* Bug #28: a `SCHEMA` frame (D50 additive migrate). Rebuild the decode extension from the
|
|
826
|
+
* descriptor the frame carries, exactly as the real client's `swapSchema` does, so the resync
|
|
827
|
+
* WELCOME about to arrive decodes under the schema it was encoded with. The old view and
|
|
828
|
+
* seen-ids are dropped — they were laid out by descriptors that no longer exist, and the resync
|
|
829
|
+
* WELCOME re-seeds both (`onWelcome`).
|
|
830
|
+
*/
|
|
831
|
+
onSchema(payload) {
|
|
832
|
+
const canonical = readSchemaPayload(payload);
|
|
833
|
+
const next = schemaFromCanonical(canonical);
|
|
834
|
+
this.ext = withBuiltins(next);
|
|
835
|
+
this.schemaSwaps++;
|
|
836
|
+
this.view = void 0;
|
|
837
|
+
this.seen.clear();
|
|
838
|
+
return `schema swapped (hash ${next.hash}) \u2014 decode extension rebuilt`;
|
|
839
|
+
}
|
|
452
840
|
onDelta(delta, now) {
|
|
453
841
|
this.checkVisibility(delta);
|
|
454
842
|
this.matchWrites(delta, now);
|
|
@@ -458,7 +846,7 @@ var BotObserver = class {
|
|
|
458
846
|
this.checkVisibility(delta);
|
|
459
847
|
const names = delta.collections.map((c) => c.name).join(",");
|
|
460
848
|
const kind = simulationOnly(
|
|
461
|
-
this.
|
|
849
|
+
this.ext,
|
|
462
850
|
delta,
|
|
463
851
|
(c, id) => this.predictsBody ? this.predictsBody(c, id) : false
|
|
464
852
|
);
|
|
@@ -479,7 +867,20 @@ var BotObserver = class {
|
|
|
479
867
|
true
|
|
480
868
|
);
|
|
481
869
|
}
|
|
482
|
-
|
|
870
|
+
const fields = /* @__PURE__ */ new Set();
|
|
871
|
+
for (const dc of delta.collections) {
|
|
872
|
+
const desc = this.ext.collections.find(
|
|
873
|
+
(c) => c.name === dc.name
|
|
874
|
+
);
|
|
875
|
+
for (const op of dc.ops) {
|
|
876
|
+
if (op.op !== "update") {
|
|
877
|
+
fields.add(`${op.op} ${op.id}`);
|
|
878
|
+
continue;
|
|
879
|
+
}
|
|
880
|
+
for (const index of op.mask.fields) fields.add(desc?.fields[index]?.name ?? `#${index}`);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
return `corrected ${names} [${[...fields].join(" ")}]${clientTick !== void 0 ? ` (clientTick ${clientTick})` : ""}`;
|
|
483
884
|
}
|
|
484
885
|
/**
|
|
485
886
|
* The room's `correct` event, one per correction op — the only source of `previous` (the local
|
|
@@ -492,7 +893,7 @@ var BotObserver = class {
|
|
|
492
893
|
this.suppressedCorrections++;
|
|
493
894
|
return;
|
|
494
895
|
}
|
|
495
|
-
const desc = this.
|
|
896
|
+
const desc = this.ext.collections.find(
|
|
496
897
|
(c) => c.name === correction.collection
|
|
497
898
|
);
|
|
498
899
|
const predictedBody = desc?.physics !== void 0 && correction.fields.length > 0 && correction.fields.every((f) => desc.physics?.bodyFields.has(f));
|
|
@@ -560,14 +961,9 @@ var BotObserver = class {
|
|
|
560
961
|
* frame — a departing neighbour, not a leak.
|
|
561
962
|
*/
|
|
562
963
|
checkVisibility(delta) {
|
|
563
|
-
if (this.view) applyDelta(this.
|
|
964
|
+
if (this.view) applyDelta(this.ext, this.view, delta);
|
|
564
965
|
this.countSpatialDelta(delta);
|
|
565
|
-
for (const leak of deltaVisibilityLeaks(
|
|
566
|
-
this.options.ext,
|
|
567
|
-
this.role,
|
|
568
|
-
delta,
|
|
569
|
-
this.spatialContext()
|
|
570
|
-
)) {
|
|
966
|
+
for (const leak of deltaVisibilityLeaks(this.ext, this.role, delta, this.spatialContext())) {
|
|
571
967
|
this.violate("visibility-leak", leak);
|
|
572
968
|
}
|
|
573
969
|
this.remember(delta);
|
|
@@ -575,12 +971,12 @@ var BotObserver = class {
|
|
|
575
971
|
countSpatialDelta(delta) {
|
|
576
972
|
if (!this.view || this.id === "") return;
|
|
577
973
|
for (const collection of delta.collections) {
|
|
578
|
-
const desc = this.
|
|
974
|
+
const desc = this.ext.collection(collection.name);
|
|
579
975
|
if (desc.visibility === "spatial-grid") this.spatialOpsJudged += collection.ops.length;
|
|
580
976
|
}
|
|
581
977
|
}
|
|
582
978
|
countSpatialSnapshot(state) {
|
|
583
|
-
for (const desc of this.
|
|
979
|
+
for (const desc of this.ext.collections) {
|
|
584
980
|
if (desc.visibility !== "spatial-grid") continue;
|
|
585
981
|
this.spatialOpsJudged += state[desc.name]?.size ?? 0;
|
|
586
982
|
}
|
|
@@ -594,7 +990,7 @@ var BotObserver = class {
|
|
|
594
990
|
return ids;
|
|
595
991
|
}
|
|
596
992
|
rememberSnapshot(state) {
|
|
597
|
-
for (const desc of this.
|
|
993
|
+
for (const desc of this.ext.collections) {
|
|
598
994
|
if (desc.kind !== "entity") continue;
|
|
599
995
|
const collection = state[desc.name];
|
|
600
996
|
if (!collection) continue;
|
|
@@ -619,6 +1015,7 @@ var BotObserver = class {
|
|
|
619
1015
|
} catch {
|
|
620
1016
|
name = `code ${error.code}`;
|
|
621
1017
|
}
|
|
1018
|
+
if (name === "E_STARTING") return `${name} (room starting \u2014 not counted)`;
|
|
622
1019
|
if (!error.fatal) {
|
|
623
1020
|
this.errors++;
|
|
624
1021
|
this.violate("handler-error", `${name}: ${error.message}`);
|
|
@@ -747,6 +1144,7 @@ function freshValue(rng, desc, ctx) {
|
|
|
747
1144
|
}
|
|
748
1145
|
|
|
749
1146
|
// src/report.ts
|
|
1147
|
+
import { mergeProfiles } from "@irtio/protocol";
|
|
750
1148
|
function percentile(sorted, p) {
|
|
751
1149
|
if (sorted.length === 0) return 0;
|
|
752
1150
|
const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(p / 100 * sorted.length) - 1));
|
|
@@ -901,10 +1299,20 @@ function buildReport(options) {
|
|
|
901
1299
|
bytesOutPerSecondPerBot: round(totals.bytesOut / seconds / bots),
|
|
902
1300
|
convergence,
|
|
903
1301
|
convergenceLagMs: convergence?.p50Ms,
|
|
904
|
-
tracePath: options.tracePath
|
|
1302
|
+
tracePath: options.tracePath,
|
|
1303
|
+
...options.profiles !== void 0 && options.profiles.length > 0 ? { profile: options.profiles.reduce(mergeProfiles) } : {}
|
|
905
1304
|
};
|
|
906
1305
|
}
|
|
907
1306
|
|
|
1307
|
+
// src/scenario.ts
|
|
1308
|
+
function defineScenario(scenario) {
|
|
1309
|
+
return scenario;
|
|
1310
|
+
}
|
|
1311
|
+
function looksLikeScenario(value) {
|
|
1312
|
+
const s = value;
|
|
1313
|
+
return typeof s === "object" && s !== null && Number.isInteger(s.bots) && s.bots > 0 && typeof s.script === "function" && typeof s.assert === "function";
|
|
1314
|
+
}
|
|
1315
|
+
|
|
908
1316
|
// src/script.ts
|
|
909
1317
|
import { rpcTable } from "@irtio/protocol";
|
|
910
1318
|
function ownableCollections(schema) {
|
|
@@ -920,11 +1328,16 @@ function writableFields(desc, cheat) {
|
|
|
920
1328
|
function voidServerRpcs(schema) {
|
|
921
1329
|
return rpcTable(schema).filter((r) => r.direction === "server" && r.returns === void 0);
|
|
922
1330
|
}
|
|
923
|
-
function
|
|
1331
|
+
function asCollection2(value) {
|
|
924
1332
|
return value && typeof value.ownerOf === "function" ? value : void 0;
|
|
925
1333
|
}
|
|
1334
|
+
function cheatPredicate(cheat) {
|
|
1335
|
+
if (cheat === void 0) return () => false;
|
|
1336
|
+
if (typeof cheat === "function") return (index) => cheat(index) === true;
|
|
1337
|
+
return () => cheat;
|
|
1338
|
+
}
|
|
926
1339
|
function randomScript(schema, options = {}) {
|
|
927
|
-
const
|
|
1340
|
+
const cheats = cheatPredicate(options.cheat);
|
|
928
1341
|
const stepMs = options.stepMs ?? 50;
|
|
929
1342
|
const step = options.step ?? 8;
|
|
930
1343
|
const rpcChance = options.rpcChance ?? 0.05;
|
|
@@ -932,24 +1345,25 @@ function randomScript(schema, options = {}) {
|
|
|
932
1345
|
const collections = ownableCollections(schema);
|
|
933
1346
|
const rpcs = voidServerRpcs(schema);
|
|
934
1347
|
return async (bot) => {
|
|
1348
|
+
const cheat = cheats(bot.index);
|
|
935
1349
|
const room = bot.room;
|
|
936
1350
|
const origins = /* @__PURE__ */ new Map();
|
|
937
1351
|
while (!bot.stopped) {
|
|
938
1352
|
for (const desc of collections) {
|
|
939
|
-
const collection =
|
|
1353
|
+
const collection = asCollection2(room.state[desc.name]);
|
|
940
1354
|
if (!collection) continue;
|
|
941
1355
|
for (const id of [...collection.ids()]) {
|
|
942
1356
|
if (collection.ownerOf(id) !== room.me) continue;
|
|
943
1357
|
const instance = collection.get(id);
|
|
944
1358
|
if (!instance) continue;
|
|
945
|
-
writeFields(bot, desc, id, instance, origins);
|
|
1359
|
+
writeFields(bot, desc, id, instance, origins, cheat);
|
|
946
1360
|
}
|
|
947
1361
|
}
|
|
948
1362
|
if (rpcs.length > 0 && bot.rng.chance(rpcChance)) await callRandomRpc(bot, room.call, rpcs);
|
|
949
1363
|
await bot.wait(stepMs);
|
|
950
1364
|
}
|
|
951
1365
|
};
|
|
952
|
-
function writeFields(bot, desc, id, instance, origins) {
|
|
1366
|
+
function writeFields(bot, desc, id, instance, origins, cheat) {
|
|
953
1367
|
const fields = writableFields(desc, cheat);
|
|
954
1368
|
if (fields.length === 0) return;
|
|
955
1369
|
const count = Math.min(fieldsPerStep, fields.length);
|
|
@@ -999,7 +1413,7 @@ import {
|
|
|
999
1413
|
joinRoom,
|
|
1000
1414
|
webSocketTransport
|
|
1001
1415
|
} from "@irtio/client";
|
|
1002
|
-
import { relaySchema, withBuiltins } from "@irtio/protocol";
|
|
1416
|
+
import { relaySchema, withBuiltins as withBuiltins2 } from "@irtio/protocol";
|
|
1003
1417
|
var DEFAULT_SEED = 96016;
|
|
1004
1418
|
var DEFAULT_TRACE_LIMIT = 4096;
|
|
1005
1419
|
var DEFAULT_JOIN_TIMEOUT_MS = 2e4;
|
|
@@ -1010,20 +1424,34 @@ function per(value, index) {
|
|
|
1010
1424
|
return typeof value === "function" ? value(index) : value;
|
|
1011
1425
|
}
|
|
1012
1426
|
var BotImpl = class {
|
|
1013
|
-
constructor(index, room, observer, seed, startedAt) {
|
|
1427
|
+
constructor(index, room, observer, seed, startedAt, conditions, shots) {
|
|
1014
1428
|
this.index = index;
|
|
1015
1429
|
this.room = room;
|
|
1016
1430
|
this.observer = observer;
|
|
1017
1431
|
this.startedAt = startedAt;
|
|
1432
|
+
this.conditions = conditions;
|
|
1433
|
+
this.shots = shots;
|
|
1018
1434
|
this.rng = makeRng(seed);
|
|
1019
1435
|
}
|
|
1020
1436
|
index;
|
|
1021
1437
|
room;
|
|
1022
1438
|
observer;
|
|
1023
1439
|
startedAt;
|
|
1440
|
+
conditions;
|
|
1441
|
+
shots;
|
|
1024
1442
|
stopped = false;
|
|
1025
1443
|
rng;
|
|
1026
1444
|
stopListeners = /* @__PURE__ */ new Set();
|
|
1445
|
+
shot(request) {
|
|
1446
|
+
const tick = this.room.tick;
|
|
1447
|
+
this.shots.push({
|
|
1448
|
+
...request,
|
|
1449
|
+
bot: this.index,
|
|
1450
|
+
sentAt: Date.now(),
|
|
1451
|
+
clientTick: typeof tick === "number" ? tick : void 0,
|
|
1452
|
+
uplinkMs: (this.conditions?.rttMs ?? 0) / 2
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1027
1455
|
get id() {
|
|
1028
1456
|
return this.observer.id;
|
|
1029
1457
|
}
|
|
@@ -1114,6 +1542,7 @@ async function withJoinTimeout(joining, index, timeoutMs, observer, sockets) {
|
|
|
1114
1542
|
if (timer !== void 0) clearTimeout(timer);
|
|
1115
1543
|
}
|
|
1116
1544
|
}
|
|
1545
|
+
var CONDITIONS_SEED_OFFSET = 377022;
|
|
1117
1546
|
async function spawnBots(n, options = {}) {
|
|
1118
1547
|
if (!Number.isInteger(n) || n < 1) {
|
|
1119
1548
|
throw new Error(`irtio bots: spawnBots needs at least one bot, got ${String(n)}`);
|
|
@@ -1127,13 +1556,15 @@ async function spawnBots(n, options = {}) {
|
|
|
1127
1556
|
snapsMax: options.snapsMax ?? DEFAULT_THRESHOLDS.snapsMax,
|
|
1128
1557
|
overrunsMax: options.overrunsMax ?? DEFAULT_THRESHOLDS.overrunsMax
|
|
1129
1558
|
};
|
|
1130
|
-
const ext = options.schema ?
|
|
1559
|
+
const ext = options.schema ? withBuiltins2(options.schema) : relaySchema;
|
|
1131
1560
|
const seed = options.seed ?? DEFAULT_SEED;
|
|
1132
1561
|
const traceLimit = options.traceLimit ?? DEFAULT_TRACE_LIMIT;
|
|
1133
1562
|
const writeLog = new WriteLog();
|
|
1134
1563
|
const lags = [];
|
|
1135
1564
|
const observers = [];
|
|
1136
1565
|
const bots = [];
|
|
1566
|
+
const shots = [];
|
|
1567
|
+
const conditions = [];
|
|
1137
1568
|
const joinTimeoutMs = options.joinTimeoutMs ?? DEFAULT_JOIN_TIMEOUT_MS;
|
|
1138
1569
|
const roomGoneGraceMs = options.roomGoneGraceMs ?? DEFAULT_ROOM_GONE_GRACE_MS;
|
|
1139
1570
|
const baseTransport = options.transport ?? webSocketTransport;
|
|
@@ -1150,13 +1581,25 @@ async function spawnBots(n, options = {}) {
|
|
|
1150
1581
|
const role = per(options.role, index);
|
|
1151
1582
|
const name = per(options.name, index);
|
|
1152
1583
|
const sockets = [];
|
|
1153
|
-
const
|
|
1584
|
+
const asked = per(options.conditions, index);
|
|
1585
|
+
const injected = hasConditions(asked) ? asked : void 0;
|
|
1586
|
+
const counters = newConditionCounters();
|
|
1587
|
+
conditions[index] = { bot: index, conditions: injected, counters };
|
|
1588
|
+
const recording = {
|
|
1154
1589
|
connect(url) {
|
|
1155
1590
|
const socket = baseTransport.connect(url);
|
|
1156
1591
|
sockets.push(socket);
|
|
1157
1592
|
return socket;
|
|
1158
1593
|
}
|
|
1159
1594
|
};
|
|
1595
|
+
const transport = injected === void 0 ? recording : conditionedTransport(
|
|
1596
|
+
recording,
|
|
1597
|
+
injected,
|
|
1598
|
+
makeRng(seed + index + CONDITIONS_SEED_OFFSET),
|
|
1599
|
+
{
|
|
1600
|
+
counters
|
|
1601
|
+
}
|
|
1602
|
+
);
|
|
1160
1603
|
const common = {
|
|
1161
1604
|
room: roomId2,
|
|
1162
1605
|
...options.url !== void 0 ? { url: options.url } : {},
|
|
@@ -1171,14 +1614,23 @@ async function spawnBots(n, options = {}) {
|
|
|
1171
1614
|
...common,
|
|
1172
1615
|
...options.flushMs !== void 0 ? { writeIntervalMs: options.flushMs } : {},
|
|
1173
1616
|
...options.rpc !== void 0 ? { rpc: options.rpc } : {},
|
|
1174
|
-
...options.physics !== void 0 ? { physics: options.physics } : {}
|
|
1617
|
+
...options.physics !== void 0 ? { physics: options.physics } : {},
|
|
1618
|
+
...options.profile === true ? { profile: true } : {}
|
|
1175
1619
|
}) : joinRelay(common);
|
|
1176
1620
|
const room = await withJoinTimeout(joining, index, joinTimeoutMs, observer, sockets);
|
|
1177
1621
|
room.on("correct", (correction) => observer.onCorrection(correction));
|
|
1178
1622
|
const prediction = room.prediction;
|
|
1179
1623
|
if (prediction) observer.predictsBody = (c, id) => prediction.predicts(c, id);
|
|
1180
1624
|
observers[index] = observer;
|
|
1181
|
-
return new BotImpl(
|
|
1625
|
+
return new BotImpl(
|
|
1626
|
+
index,
|
|
1627
|
+
room,
|
|
1628
|
+
observer,
|
|
1629
|
+
seed + index,
|
|
1630
|
+
startedAt,
|
|
1631
|
+
injected,
|
|
1632
|
+
shots
|
|
1633
|
+
);
|
|
1182
1634
|
}
|
|
1183
1635
|
const first = await joinOne(0, options.room ?? "");
|
|
1184
1636
|
bots.push(first);
|
|
@@ -1249,11 +1701,14 @@ async function spawnBots(n, options = {}) {
|
|
|
1249
1701
|
let stoppedAt;
|
|
1250
1702
|
let tickHealth;
|
|
1251
1703
|
const rings = () => observers.map((o) => o.ring);
|
|
1704
|
+
let profiles;
|
|
1252
1705
|
const runner = {
|
|
1253
1706
|
bots,
|
|
1254
1707
|
roomId,
|
|
1255
1708
|
trace: makeTrace(startedAt, rings),
|
|
1256
1709
|
scriptErrors,
|
|
1710
|
+
shots,
|
|
1711
|
+
conditions,
|
|
1257
1712
|
get endedBy() {
|
|
1258
1713
|
return endedBy;
|
|
1259
1714
|
},
|
|
@@ -1266,13 +1721,18 @@ async function spawnBots(n, options = {}) {
|
|
|
1266
1721
|
tickHealth = reading;
|
|
1267
1722
|
},
|
|
1268
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;
|
|
1727
|
+
}
|
|
1269
1728
|
return buildReport({
|
|
1270
1729
|
observers,
|
|
1271
1730
|
roomId,
|
|
1272
1731
|
durationMs: (stoppedAt ?? Date.now()) - startedAt,
|
|
1273
1732
|
lags,
|
|
1274
1733
|
thresholds,
|
|
1275
|
-
tickHealth
|
|
1734
|
+
tickHealth,
|
|
1735
|
+
...profiles !== void 0 ? { profiles } : {}
|
|
1276
1736
|
});
|
|
1277
1737
|
},
|
|
1278
1738
|
async stop() {
|
|
@@ -1288,25 +1748,170 @@ async function spawnBots(n, options = {}) {
|
|
|
1288
1748
|
};
|
|
1289
1749
|
return runner;
|
|
1290
1750
|
}
|
|
1751
|
+
|
|
1752
|
+
// src/timeline.ts
|
|
1753
|
+
var Collection = class {
|
|
1754
|
+
constructor(raw, entity) {
|
|
1755
|
+
this.raw = raw;
|
|
1756
|
+
this.entity = entity;
|
|
1757
|
+
}
|
|
1758
|
+
raw;
|
|
1759
|
+
entity;
|
|
1760
|
+
entry(id) {
|
|
1761
|
+
if (!this.entity) return void 0;
|
|
1762
|
+
const row = this.raw[id];
|
|
1763
|
+
return typeof row === "object" && row !== null ? row : void 0;
|
|
1764
|
+
}
|
|
1765
|
+
get(id) {
|
|
1766
|
+
const value = this.entry(id)?.value;
|
|
1767
|
+
return typeof value === "object" && value !== null ? value : void 0;
|
|
1768
|
+
}
|
|
1769
|
+
has(id) {
|
|
1770
|
+
return this.entity && Object.hasOwn(this.raw, id);
|
|
1771
|
+
}
|
|
1772
|
+
ids() {
|
|
1773
|
+
return this.entity ? Object.keys(this.raw) : [];
|
|
1774
|
+
}
|
|
1775
|
+
get size() {
|
|
1776
|
+
return this.entity ? Object.keys(this.raw).length : 0;
|
|
1777
|
+
}
|
|
1778
|
+
owner(id) {
|
|
1779
|
+
const owner = this.entry(id)?.owner;
|
|
1780
|
+
return typeof owner === "string" && owner !== "" ? owner : void 0;
|
|
1781
|
+
}
|
|
1782
|
+
get value() {
|
|
1783
|
+
return this.entity ? void 0 : this.raw;
|
|
1784
|
+
}
|
|
1785
|
+
};
|
|
1786
|
+
var UnknownCollectionError = class extends Error {
|
|
1787
|
+
name = "UnknownCollectionError";
|
|
1788
|
+
constructor(collection, known) {
|
|
1789
|
+
super(
|
|
1790
|
+
`irtio scenario: this room has no collection named "${collection}". It has: ${known.length > 0 ? known.join(", ") : "none"}.`
|
|
1791
|
+
);
|
|
1792
|
+
}
|
|
1793
|
+
};
|
|
1794
|
+
var TickNotRecordedError = class extends Error {
|
|
1795
|
+
constructor(tick, first, last, dropped) {
|
|
1796
|
+
super(
|
|
1797
|
+
first === void 0 ? `irtio scenario: nothing was recorded, so tick ${tick} cannot be read.` : `irtio scenario: tick ${tick} is not in the recording, which holds ticks ${first} to ${last}` + (dropped > 0 ? `. ${dropped} earlier tick(s) were dropped by the recorder's cap, so raise it or shorten the run.` : ".")
|
|
1798
|
+
);
|
|
1799
|
+
this.tick = tick;
|
|
1800
|
+
}
|
|
1801
|
+
tick;
|
|
1802
|
+
name = "TickNotRecordedError";
|
|
1803
|
+
};
|
|
1804
|
+
function momentOf(dump, index, read) {
|
|
1805
|
+
const frame = dump.frames[index];
|
|
1806
|
+
read(frame.tick);
|
|
1807
|
+
const known = Object.keys(dump.collections);
|
|
1808
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1809
|
+
return new Proxy({}, {
|
|
1810
|
+
get(_target, property) {
|
|
1811
|
+
if (typeof property !== "string") return void 0;
|
|
1812
|
+
const cached = cache.get(property);
|
|
1813
|
+
if (cached) return cached;
|
|
1814
|
+
const kind = dump.collections[property];
|
|
1815
|
+
if (kind === void 0) throw new UnknownCollectionError(property, known);
|
|
1816
|
+
const raw = frame.state[property];
|
|
1817
|
+
const view = new Collection(
|
|
1818
|
+
typeof raw === "object" && raw !== null ? raw : {},
|
|
1819
|
+
kind === "entity"
|
|
1820
|
+
);
|
|
1821
|
+
cache.set(property, view);
|
|
1822
|
+
return view;
|
|
1823
|
+
},
|
|
1824
|
+
has: (_target, property) => typeof property === "string" && property in dump.collections,
|
|
1825
|
+
ownKeys: () => [...known],
|
|
1826
|
+
getOwnPropertyDescriptor: () => ({ enumerable: true, configurable: true })
|
|
1827
|
+
});
|
|
1828
|
+
}
|
|
1829
|
+
function makeTimeline(dump) {
|
|
1830
|
+
const index = /* @__PURE__ */ new Map();
|
|
1831
|
+
dump.frames.forEach((f, i) => index.set(f.tick, i));
|
|
1832
|
+
const ticks = dump.frames.map((f) => f.tick);
|
|
1833
|
+
const results = [];
|
|
1834
|
+
let readTicks = [];
|
|
1835
|
+
const read = (tick) => {
|
|
1836
|
+
readTicks.push(tick);
|
|
1837
|
+
};
|
|
1838
|
+
const at = (tick) => {
|
|
1839
|
+
const i = index.get(tick);
|
|
1840
|
+
if (i === void 0) {
|
|
1841
|
+
throw new TickNotRecordedError(tick, ticks[0], ticks[ticks.length - 1], dump.dropped);
|
|
1842
|
+
}
|
|
1843
|
+
return momentOf(dump, i, read);
|
|
1844
|
+
};
|
|
1845
|
+
return {
|
|
1846
|
+
roomId: dump.roomId,
|
|
1847
|
+
ticks,
|
|
1848
|
+
dropped: dump.dropped,
|
|
1849
|
+
at,
|
|
1850
|
+
find(match) {
|
|
1851
|
+
for (let i = 0; i < dump.frames.length; i++) {
|
|
1852
|
+
const tick = dump.frames[i].tick;
|
|
1853
|
+
const state = momentOf(dump, i, read);
|
|
1854
|
+
if (match(state, tick)) return { tick, state };
|
|
1855
|
+
}
|
|
1856
|
+
return void 0;
|
|
1857
|
+
},
|
|
1858
|
+
check(name, assertion) {
|
|
1859
|
+
readTicks = [];
|
|
1860
|
+
try {
|
|
1861
|
+
assertion();
|
|
1862
|
+
results.push({
|
|
1863
|
+
name,
|
|
1864
|
+
ok: true,
|
|
1865
|
+
...readTicks.length > 0 ? { tick: readTicks[readTicks.length - 1] } : {},
|
|
1866
|
+
detail: readTicks.length === 0 ? "held (read no tick)" : `held, reading tick ${readTicks[readTicks.length - 1]}` + (readTicks.length > 1 ? ` (${readTicks.length} ticks read)` : "")
|
|
1867
|
+
});
|
|
1868
|
+
} catch (err) {
|
|
1869
|
+
results.push({
|
|
1870
|
+
name,
|
|
1871
|
+
ok: false,
|
|
1872
|
+
...readTicks.length > 0 ? { tick: readTicks[readTicks.length - 1] } : {},
|
|
1873
|
+
detail: err instanceof Error ? err.message : String(err)
|
|
1874
|
+
});
|
|
1875
|
+
}
|
|
1876
|
+
},
|
|
1877
|
+
get results() {
|
|
1878
|
+
return results;
|
|
1879
|
+
}
|
|
1880
|
+
};
|
|
1881
|
+
}
|
|
1291
1882
|
export {
|
|
1292
1883
|
BotObserver,
|
|
1293
1884
|
DEFAULT_JOIN_TIMEOUT_MS,
|
|
1885
|
+
DEFAULT_REORDER_MS,
|
|
1294
1886
|
DEFAULT_ROOM_GONE_GRACE_MS,
|
|
1295
1887
|
DEFAULT_SEED,
|
|
1296
1888
|
DEFAULT_THRESHOLDS,
|
|
1297
1889
|
DEFAULT_TRACE_LIMIT,
|
|
1298
1890
|
INVARIANT_NAMES,
|
|
1299
1891
|
JoinTimeoutError,
|
|
1892
|
+
TickNotRecordedError,
|
|
1300
1893
|
TraceRing,
|
|
1894
|
+
UnknownCollectionError,
|
|
1301
1895
|
WriteLog,
|
|
1302
1896
|
buildReport,
|
|
1897
|
+
captureClientState,
|
|
1898
|
+
cheatPredicate,
|
|
1899
|
+
conditionedTransport,
|
|
1303
1900
|
convergenceStats,
|
|
1901
|
+
correlateShots,
|
|
1902
|
+
defineScenario,
|
|
1304
1903
|
deltaVisibilityLeaks,
|
|
1904
|
+
describeConditions,
|
|
1905
|
+
diffAgainstSave,
|
|
1305
1906
|
frameName,
|
|
1306
1907
|
frameVisibilityLeaks,
|
|
1307
1908
|
freshValue,
|
|
1909
|
+
hasConditions,
|
|
1910
|
+
looksLikeScenario,
|
|
1308
1911
|
makeRng,
|
|
1912
|
+
makeTimeline,
|
|
1309
1913
|
makeTrace,
|
|
1914
|
+
newConditionCounters,
|
|
1310
1915
|
nextValue,
|
|
1311
1916
|
randomScript,
|
|
1312
1917
|
relayEchoScript,
|