@irtio/client 0.7.0 → 0.9.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-XWVXZRBS.js → chunk-5OHONPVG.js} +142 -0
- package/dist/{chunk-DABQDR3S.js → chunk-VZFJXPVF.js} +7 -8
- package/dist/index.d.ts +439 -11
- package/dist/index.js +291 -8
- package/dist/{physics-RT5T36P5.js → physics-D65WIHPR.js} +2 -2
- package/dist/physics-rapier2d-O53E6BHO.js +186 -0
- package/dist/{physics2d-HOFMWPZV.js → physics2d-GIL6YYAY.js} +2 -2
- package/package.json +8 -3
|
@@ -11,6 +11,7 @@ function emptyPredictionStats() {
|
|
|
11
11
|
resimSteps: 0,
|
|
12
12
|
rebases: 0,
|
|
13
13
|
snaps: 0,
|
|
14
|
+
leadClamped: 0,
|
|
14
15
|
suppressed: 0,
|
|
15
16
|
overCap: 0,
|
|
16
17
|
proxies: 0,
|
|
@@ -120,6 +121,13 @@ var ClientStore = class {
|
|
|
120
121
|
tracked;
|
|
121
122
|
descs = /* @__PURE__ */ new Map();
|
|
122
123
|
frozen = /* @__PURE__ */ new WeakMap();
|
|
124
|
+
/**
|
|
125
|
+
* Per-collection add/remove subscribers (`room.onAdd` / `room.onRemove`). Kept here rather than
|
|
126
|
+
* in the session because this is the only place that sees a frame's ops against the state the
|
|
127
|
+
* frame is about to change: a removed row's last values exist for exactly as long as it takes
|
|
128
|
+
* `applyDelta` to run.
|
|
129
|
+
*/
|
|
130
|
+
entityListeners = /* @__PURE__ */ new Map();
|
|
123
131
|
/** Live collection facades by name, so a D50 swap can retarget rather than replace them. */
|
|
124
132
|
facades = /* @__PURE__ */ new Map();
|
|
125
133
|
/** The object handed out as `room.state`; identity survives a resync. */
|
|
@@ -160,11 +168,13 @@ var ClientStore = class {
|
|
|
160
168
|
}
|
|
161
169
|
// -- snapshot / resync -----------------------------------------------------
|
|
162
170
|
loadSnapshot(bytes) {
|
|
171
|
+
const before = this.captureRows();
|
|
163
172
|
this.plain = decodeSnapshot(this.ext, bytes).state;
|
|
164
173
|
this.tracked = track(this.ext, this.plain);
|
|
165
174
|
this.pendingWrites.length = 0;
|
|
166
175
|
this.evictedThroughTick = 0;
|
|
167
176
|
this.baselineIntents.clear();
|
|
177
|
+
this.emitSnapshotDiff(before);
|
|
168
178
|
}
|
|
169
179
|
/**
|
|
170
180
|
* Captures the field values of every pending owned write, keyed by collection then id then
|
|
@@ -293,14 +303,144 @@ var ClientStore = class {
|
|
|
293
303
|
singletonHint(desc) {
|
|
294
304
|
return `${desc.name} is a singleton and never client-owned; change it with an RPC (room.call.\u2026)`;
|
|
295
305
|
}
|
|
306
|
+
// -- entity add/remove events ---------------------------------------------
|
|
307
|
+
listenersFor(name) {
|
|
308
|
+
let set = this.entityListeners.get(name);
|
|
309
|
+
if (!set) {
|
|
310
|
+
set = { add: /* @__PURE__ */ new Set(), remove: /* @__PURE__ */ new Set() };
|
|
311
|
+
this.entityListeners.set(name, set);
|
|
312
|
+
}
|
|
313
|
+
return set;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Subscribes to rows appearing in one entity collection. Rows that are **already present** are
|
|
317
|
+
* announced synchronously as the subscription is made — the join snapshot has usually landed
|
|
318
|
+
* before user code runs, and a listener that had to reconcile the initial set by hand would
|
|
319
|
+
* make the event useless for exactly the case (a spawn effect) it exists for.
|
|
320
|
+
*/
|
|
321
|
+
onEntityAdd(name, cb) {
|
|
322
|
+
const desc = this.desc(name);
|
|
323
|
+
if (desc?.kind === "entity") {
|
|
324
|
+
const coll = entityOf(this.plain, name);
|
|
325
|
+
for (const id of [...coll.ids()]) cb(id, this.instance(desc, id));
|
|
326
|
+
}
|
|
327
|
+
const set = this.listenersFor(name);
|
|
328
|
+
set.add.add(cb);
|
|
329
|
+
return () => {
|
|
330
|
+
set.add.delete(cb);
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
/** Subscribes to rows disappearing from one entity collection (never fires for past removals). */
|
|
334
|
+
onEntityRemove(name, cb) {
|
|
335
|
+
const set = this.listenersFor(name);
|
|
336
|
+
set.remove.add(cb);
|
|
337
|
+
return () => {
|
|
338
|
+
set.remove.delete(cb);
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
emitAdd(name, id) {
|
|
342
|
+
const set = this.entityListeners.get(name);
|
|
343
|
+
if (!set || set.add.size === 0) return;
|
|
344
|
+
const desc = this.desc(name);
|
|
345
|
+
if (!desc) return;
|
|
346
|
+
const row = this.instance(desc, id);
|
|
347
|
+
for (const cb of [...set.add]) cb(id, row);
|
|
348
|
+
}
|
|
349
|
+
emitRemove(name, id, value, hint) {
|
|
350
|
+
const set = this.entityListeners.get(name);
|
|
351
|
+
if (!set || set.remove.size === 0) return;
|
|
352
|
+
const row = this.freeze(value, hint);
|
|
353
|
+
for (const cb of [...set.remove]) cb(id, row);
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* The adds and removes a delta is about to make, captured against the state as it stands now:
|
|
357
|
+
* a remove's last values are only readable before `applyDelta` drops the row.
|
|
358
|
+
*/
|
|
359
|
+
captureOps(delta) {
|
|
360
|
+
if (this.entityListeners.size === 0) return void 0;
|
|
361
|
+
const adds = [];
|
|
362
|
+
const removes = [];
|
|
363
|
+
for (const dc of delta.collections) {
|
|
364
|
+
const set = this.entityListeners.get(dc.name);
|
|
365
|
+
if (!set || set.add.size === 0 && set.remove.size === 0) continue;
|
|
366
|
+
const desc = this.desc(dc.name);
|
|
367
|
+
if (!desc || desc.kind !== "entity") continue;
|
|
368
|
+
const coll = entityOf(this.plain, dc.name);
|
|
369
|
+
for (const op of dc.ops) {
|
|
370
|
+
if (op.op === "add") {
|
|
371
|
+
if (!coll.has(op.id)) adds.push([dc.name, op.id]);
|
|
372
|
+
} else if (op.op === "remove") {
|
|
373
|
+
const value = coll.get(op.id);
|
|
374
|
+
if (value) removes.push([dc.name, op.id, value]);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return adds.length === 0 && removes.length === 0 ? void 0 : { adds, removes };
|
|
379
|
+
}
|
|
380
|
+
flushOps(captured) {
|
|
381
|
+
if (!captured) return;
|
|
382
|
+
for (const [name, id, value] of captured.removes) {
|
|
383
|
+
const desc = this.desc(name);
|
|
384
|
+
this.emitRemove(
|
|
385
|
+
name,
|
|
386
|
+
id,
|
|
387
|
+
value,
|
|
388
|
+
desc ? `${desc.name}[${id}] has been removed; this is its last value` : name
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
for (const [name, id] of captured.adds) this.emitAdd(name, id);
|
|
392
|
+
}
|
|
393
|
+
/** Fires adds and removes for the difference a wholesale snapshot load made. */
|
|
394
|
+
emitSnapshotDiff(before) {
|
|
395
|
+
if (this.entityListeners.size === 0) return;
|
|
396
|
+
for (const [name, set] of this.entityListeners) {
|
|
397
|
+
const desc = this.desc(name);
|
|
398
|
+
if (!desc || desc.kind !== "entity") continue;
|
|
399
|
+
const prev = before.get(name) ?? /* @__PURE__ */ new Map();
|
|
400
|
+
const coll = entityOf(this.plain, name);
|
|
401
|
+
if (set.remove.size > 0) {
|
|
402
|
+
for (const [id, value] of prev) {
|
|
403
|
+
if (!coll.has(id)) {
|
|
404
|
+
this.emitRemove(
|
|
405
|
+
name,
|
|
406
|
+
id,
|
|
407
|
+
value,
|
|
408
|
+
`${name}[${id}] has been removed; this is its last value`
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
if (set.add.size > 0) {
|
|
414
|
+
for (const id of [...coll.ids()]) {
|
|
415
|
+
if (!prev.has(id)) this.emitAdd(name, id);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
/** The rows every subscribed collection holds right now, for the snapshot diff. */
|
|
421
|
+
captureRows() {
|
|
422
|
+
const out = /* @__PURE__ */ new Map();
|
|
423
|
+
if (this.entityListeners.size === 0) return out;
|
|
424
|
+
for (const name of this.entityListeners.keys()) {
|
|
425
|
+
const desc = this.desc(name);
|
|
426
|
+
if (!desc || desc.kind !== "entity") continue;
|
|
427
|
+
const coll = entityOf(this.plain, name);
|
|
428
|
+
const rows = /* @__PURE__ */ new Map();
|
|
429
|
+
for (const [id, value] of coll) rows.set(id, value);
|
|
430
|
+
out.set(name, rows);
|
|
431
|
+
}
|
|
432
|
+
return out;
|
|
433
|
+
}
|
|
296
434
|
// -- inbound frames -------------------------------------------------------
|
|
297
435
|
/**
|
|
298
436
|
* Applies a server `DELTA`. Update ops for instances this client owns are dropped field-wise
|
|
299
437
|
* (server wins only through `CORRECT`); adds, removes and owner changes always apply.
|
|
300
438
|
*/
|
|
301
439
|
applyServerDelta(delta) {
|
|
440
|
+
const captured = this.captureOps(delta);
|
|
302
441
|
applyDelta(this.ext, this.plain, this.withoutOwnedUpdates(delta));
|
|
303
442
|
this.replayOverAddEchoes(delta);
|
|
443
|
+
this.flushOps(captured);
|
|
304
444
|
}
|
|
305
445
|
/**
|
|
306
446
|
* The flushed-write half of the §7.2 in-flight own-write fix: an `add` op for an instance this
|
|
@@ -453,7 +593,9 @@ var ClientStore = class {
|
|
|
453
593
|
});
|
|
454
594
|
}
|
|
455
595
|
}
|
|
596
|
+
const captured = this.captureOps(delta);
|
|
456
597
|
applyDelta(this.ext, this.plain, delta);
|
|
598
|
+
this.flushOps(captured);
|
|
457
599
|
let replayed = 0;
|
|
458
600
|
if (replay) {
|
|
459
601
|
for (const pw of [...this.pendingWrites, unflushed]) {
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
RESIM_DEPTH,
|
|
6
6
|
SMOOTHING_HALF_LIFE_MS,
|
|
7
7
|
SMOOTHING_SNAP_UNITS
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-5OHONPVG.js";
|
|
9
9
|
|
|
10
10
|
// src/predictor.ts
|
|
11
11
|
var MAX_FREE_STEPS_PER_FRAME = 5;
|
|
@@ -133,6 +133,7 @@ var Predictor = class _Predictor {
|
|
|
133
133
|
resimSteps: 0,
|
|
134
134
|
rebases: 0,
|
|
135
135
|
snaps: 0,
|
|
136
|
+
leadClamped: 0,
|
|
136
137
|
suppressed: 0,
|
|
137
138
|
overCap: 0,
|
|
138
139
|
proxies: 0,
|
|
@@ -951,8 +952,8 @@ var Predictor = class _Predictor {
|
|
|
951
952
|
* written, so plain state holds exactly what the server said), then re-step the world by the
|
|
952
953
|
* client's lead, applying to each re-stepped tick the intent that was in force *at that tick*:
|
|
953
954
|
* the newest buffered unjudged write stamped at or before it, or the baseline (the newest
|
|
954
|
-
* judged write) before the first of them. Bounded by `MAX_LEAD`: an outrun lead
|
|
955
|
-
*
|
|
955
|
+
* judged write) before the first of them. Bounded by `MAX_LEAD`: an outrun lead re-steps that
|
|
956
|
+
* many and counts (`stats.leadClamped`, bugs.md #71).
|
|
956
957
|
*/
|
|
957
958
|
rebase() {
|
|
958
959
|
if (!this.worldReady) return;
|
|
@@ -966,7 +967,9 @@ var Predictor = class _Predictor {
|
|
|
966
967
|
const k = key(entry.desc.name, entry.id);
|
|
967
968
|
this.predictedTicks.set(k, Math.max(this.authorityTick, this.authorityTicks.get(k) ?? 0));
|
|
968
969
|
}
|
|
969
|
-
const
|
|
970
|
+
const wanted = this.leadTicks();
|
|
971
|
+
const lead = Math.min(wanted, MAX_LEAD);
|
|
972
|
+
if (wanted > MAX_LEAD) this.stats.leadClamped++;
|
|
970
973
|
if (this.lastLead !== void 0 && lead !== this.lastLead && this.gapMeasured) {
|
|
971
974
|
const delta = lead - this.lastLead;
|
|
972
975
|
this.stampGap = Math.min(RESIM_DEPTH, Math.max(0, this.stampGap + delta));
|
|
@@ -975,10 +978,6 @@ var Predictor = class _Predictor {
|
|
|
975
978
|
}
|
|
976
979
|
this.lastLead = lead;
|
|
977
980
|
this.headTick = this.authorityTick;
|
|
978
|
-
if (lead > MAX_LEAD) {
|
|
979
|
-
this.stats.snaps++;
|
|
980
|
-
return;
|
|
981
|
-
}
|
|
982
981
|
const replays = /* @__PURE__ */ new Map();
|
|
983
982
|
for (const entry of this.bodies.values()) {
|
|
984
983
|
if (!entry.owned) continue;
|