@irtio/client 0.2.0 → 0.4.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.
@@ -2,6 +2,8 @@
2
2
  var E_CONNECT_FAILED = "E_CONNECT_FAILED";
3
3
  var MAX_PREDICTED_BODIES = 64;
4
4
  var PREDICTION_EPSILON = 0.05;
5
+ var SMOOTHING_HALF_LIFE_MS = 70;
6
+ var SMOOTHING_SNAP_UNITS = 4;
5
7
  function emptyPredictionStats() {
6
8
  return {
7
9
  freeSteps: 0,
@@ -10,7 +12,9 @@ function emptyPredictionStats() {
10
12
  snaps: 0,
11
13
  suppressed: 0,
12
14
  overCap: 0,
13
- lastResimMicros: 0
15
+ lastResimMicros: 0,
16
+ smoothing: 0,
17
+ stampGap: 0
14
18
  };
15
19
  }
16
20
 
@@ -556,6 +560,8 @@ export {
556
560
  E_CONNECT_FAILED,
557
561
  MAX_PREDICTED_BODIES,
558
562
  PREDICTION_EPSILON,
563
+ SMOOTHING_HALF_LIFE_MS,
564
+ SMOOTHING_SNAP_UNITS,
559
565
  emptyPredictionStats,
560
566
  RESIM_DEPTH,
561
567
  ClientStore
package/dist/index.d.ts CHANGED
@@ -195,6 +195,17 @@ declare class ClientStore {
195
195
  * shared resimulation depth (20, D19): beyond it the body snaps to authority and the snap is
196
196
  * counted.
197
197
  *
198
+ * ## What the renderer sees
199
+ *
200
+ * Not the head. The head moves once per server tick — and only when an arrival rebases the world
201
+ * onto it — while a display asks two to four times as often, so handing back the head draws a
202
+ * character running at a steady nine units a second as standing still for half the frames and
203
+ * moving at twenty for the rest. `read()` therefore keeps a short ring of recent poses per body
204
+ * and draws between the two either side of a render clock that runs on wall time, steered to sit
205
+ * about a tick behind the head. Correctness is unaffected: `room.state`, the per-tick history a
206
+ * correction is judged against, and `predictedValues` all still see the head. See bug 7 in
207
+ * `docs/bugs.md` for the measurement.
208
+ *
198
209
  * ## What is deliberately absent
199
210
  *
200
211
  * Non-predicted dynamic bodies do not exist in the local world, and neither do `predicted: true`
@@ -266,6 +277,32 @@ interface ClientPhysicsOptions {
266
277
  * an input frame landing one tick late on the server is invisible, not a storm). Default 0.05.
267
278
  */
268
279
  readonly epsilon?: number;
280
+ /**
281
+ * How fast the drawn position eases back onto the simulation after a re-simulation moved it,
282
+ * as a half-life in milliseconds. `0` turns the smoothing off. Default 70.
283
+ *
284
+ * A rebase can move a body that has already been drawn — because the server disagreed, or
285
+ * because a newly-flushed intent changed what the last few ticks should have been. Handing
286
+ * that straight to the renderer is a step, and a step in the middle of steady motion is what
287
+ * a player calls a yank. Instead the jump is taken out of the drawn pose and put into a
288
+ * per-body offset that decays: the character keeps moving smoothly and arrives at the truth a
289
+ * moment later.
290
+ *
291
+ * This smooths the **error**, not the motion. A rate limiter on the drawn position — the
292
+ * obvious version, and the one a game writes for itself — cannot tell a correction from the
293
+ * character running, so it lags real movement too. Nothing here touches motion the simulation
294
+ * actually produced.
295
+ */
296
+ readonly smoothingHalfLifeMs?: number;
297
+ /**
298
+ * How far the drawn position may be held from the simulation while an offset eases away, in
299
+ * world units. Past it the offset is dropped and the body appears where it is. Default 4.
300
+ *
301
+ * This is the teleport case: a respawn, an area change, a resync. Easing across one of those
302
+ * draws the body sliding through the level, which is worse than the step it avoids. It is the
303
+ * one number here that depends on how big a world unit is in your game.
304
+ */
305
+ readonly smoothingSnapUnits?: number;
269
306
  }
270
307
  /** Counters for the report and `room.prediction.stats`. */
271
308
  interface PredictionStats {
@@ -287,6 +324,21 @@ interface PredictionStats {
287
324
  overCap: number;
288
325
  /** Microseconds spent in the last rebase's re-steps. */
289
326
  lastResimMicros: number;
327
+ /**
328
+ * The largest render-error offset currently being eased away, in world units — how far the
329
+ * furthest-off predicted body is being drawn from where the simulation puts it, so that a
330
+ * re-simulation did not arrive as a step (`smoothingHalfLifeMs`). It rises on a correction and
331
+ * falls back towards zero; what is unhealthy is a value that *stays* up, which means the
332
+ * client is being corrected as fast as it can absorb it.
333
+ */
334
+ smoothing: number;
335
+ /**
336
+ * Ticks between the stamp a `WRITE` carries and the server tick it was applied at, smoothed
337
+ * (bug 1). The client stamps on its predicted clock, which leads authority, so this is how far
338
+ * *early* a naive replay would put every pending intent. 0 until a server that reports the
339
+ * applied tick judges a stamped write.
340
+ */
341
+ stampGap: number;
290
342
  }
291
343
  declare class PhysicsPredictor {
292
344
  private readonly store;
@@ -310,6 +362,27 @@ declare class PhysicsPredictor {
310
362
  private lastNow;
311
363
  private lastFrameNow;
312
364
  private failed;
365
+ /**
366
+ * The local world's head, on the server's tick timeline: the authoritative tick the last
367
+ * rebase started from plus the lead it re-stepped, then one per free-run step.
368
+ */
369
+ private headTick;
370
+ /** The newest server tick any authority has been seen for. `headTick`'s base. */
371
+ private authorityTick;
372
+ /** The newest tick a pose has been stored for. `-1` until the first capture. */
373
+ private curTick;
374
+ /**
375
+ * Where the renderer is, as a fractional tick. Advanced by wall time every frame and steered
376
+ * to sit about a tick behind `curTick`; `read()` draws between the stored poses either side
377
+ * of it.
378
+ */
379
+ private renderTick;
380
+ /** Scratch for one interpolated pose. `read()` is synchronous, so one is enough. */
381
+ private readonly scratch;
382
+ /** Scratch quaternions, so the per-body per-frame offset maths allocates nothing. */
383
+ private readonly qa;
384
+ private readonly qb;
385
+ private readonly qc;
313
386
  /**
314
387
  * Per owned body: the server tick its authoritative record is *from* (the tick of the last
315
388
  * `CORRECT` that touched it), and the per-tick prediction history — after every local step,
@@ -319,6 +392,11 @@ declare class PhysicsPredictor {
319
392
  * against it would report the lead as misprediction).
320
393
  */
321
394
  private readonly authorityTicks;
395
+ /** Smoothed stamp-minus-applied gap; see `noteWriteApplied`. */
396
+ private stampGap;
397
+ private gapMeasured;
398
+ /** The newest stamp already folded into `stampGap`, so a repeated echo is not re-weighted. */
399
+ private gapSampledThrough;
322
400
  private readonly predictedTicks;
323
401
  private readonly history;
324
402
  /** History kept per body — comfortably past the resim depth. */
@@ -335,10 +413,32 @@ declare class PhysicsPredictor {
335
413
  free(): void;
336
414
  /** A resync (`WELCOME`) replaced the authority wholesale: rebase everything, drop banked time. */
337
415
  reset(): void;
338
- /** Authoritative body state arrived (`DELTA` on a non-owned body, `CORRECT` on an owned one). */
339
- noteAuthority(): void;
416
+ /**
417
+ * Authoritative body state arrived (`DELTA` on a non-owned body, `CORRECT` on an owned one).
418
+ *
419
+ * `tick` is the server tick it carried, and it is the base of the predictor's own head clock —
420
+ * which the renderer's interpolation is paced against. It has to be the *world's* clock rather
421
+ * than any one body's: a client with nothing of its own to own still draws a world full of
422
+ * predicted bodies.
423
+ */
424
+ noteAuthority(tick?: number): void;
340
425
  /** The session tells us which server tick one body's authoritative record is from. */
341
426
  noteAuthorityTick(collection: string, id: string, tick: number): void;
427
+ /**
428
+ * A `CORRECT` reported both halves of a judged write: the stamp the client put on it and the
429
+ * server tick it was actually applied at (bug 1). The gap between them is the systematic error
430
+ * in the rebase's replay, and it is not zero even on a loopback: the stamp is the predictor's
431
+ * *head* (authority + lead), while the server applies on arrival, at a tick barely past the
432
+ * authority the client is replaying from. Replaying at the stamp therefore held the previous
433
+ * intent for `gap` ticks the server had already simulated under the new one — the overshoot on
434
+ * every key release, sized `gap x speed`.
435
+ *
436
+ * Smoothed, because it rides on delivery jitter and the measurement is one sample per judged
437
+ * write. Clamped into `[0, RESIM_DEPTH]`: a stamp taken before this client had any physics
438
+ * authority is on the session's bare write counter rather than the server's tick stream, and
439
+ * differencing the two clocks is meaningless — 0 is the old behaviour and the honest default.
440
+ */
441
+ noteWriteApplied(stampTick: number, appliedTick: number): void;
342
442
  /** Does the local world currently simulate `collection[id]`? */
343
443
  has(collection: string, id: string): boolean;
344
444
  /**
@@ -355,11 +455,88 @@ declare class PhysicsPredictor {
355
455
  * draw loop — or a bot's render sampling — is the clock; there is no timer.
356
456
  */
357
457
  frame(now: number): void;
458
+ /**
459
+ * The head moved: store every predicted body's pose under the tick it now holds.
460
+ *
461
+ * A tick can be simulated more than once — a `DELTA` and a `CORRECT` both arrive for the same
462
+ * tick and each rebases — so the slot is simply rewritten with the better answer. It can also
463
+ * move backwards, when a free-run step is discarded by the rebase behind it; `curTick` does
464
+ * not follow it down, because moving the newest label backwards would drag the segment the
465
+ * renderer is crossing backwards with it.
466
+ */
467
+ private capturePoses;
468
+ /**
469
+ * Advance the render clock by one frame of wall time, steering it towards a target distance
470
+ * behind the head rather than clamping it there.
471
+ *
472
+ * `renderTick` and `curTick` agree on rate — one tick per tick period — but not on phase, and
473
+ * the phase error is delivery jitter: authority arrives when the network hands it over, not
474
+ * every 33 ms however steady the server is. Left alone the two would wander apart, so the
475
+ * clock is steered, by rate rather than by position. What the network did to a packet is not
476
+ * something the player's character should be seen doing.
477
+ */
478
+ private advanceRenderClock;
479
+ /**
480
+ * Keep the render clock inside the span there are poses for.
481
+ *
482
+ * Both ends are real. At `curTick` there is nothing further to draw towards, so a late arrival
483
+ * holds the pose rather than extrapolating motion the next tick would have to take back — the
484
+ * same trade that sized the prediction lead (bug 1). At the far end the ring has recycled the
485
+ * slot and the pose is genuinely gone. Between them the clock floats, and the rate trim above
486
+ * is what keeps it from spending its time against either stop.
487
+ */
488
+ private holdRenderTick;
489
+ /** `smoothingHalfLifeMs`, or 0 when the game turned the smoothing off. */
490
+ private halfLifeMs;
491
+ /** Remember where every body is being drawn, before this frame re-simulates anything. */
492
+ private snapshotDrawn;
493
+ /**
494
+ * Take whatever the re-simulation moved the drawn pose by and put it in the offset instead.
495
+ *
496
+ * This is the whole of correction smoothing, and it is smoothing the *error*: the difference
497
+ * measured here is between two answers to the same question — where is this body at the render
498
+ * clock — asked either side of a rebase. Motion the simulation produced is not in it, because
499
+ * the render clock advanced before the snapshot. So real movement is never slowed, which is
500
+ * the thing a rate limiter on the drawn position cannot promise: it sees a position that moved
501
+ * and has no way to know whether the body ran or was corrected.
502
+ *
503
+ * It absorbs more than server disagreement. A newly flushed intent changes what the last few
504
+ * ticks should have been, and the replay hands that over as a jump the same way; so does the
505
+ * render clock being pulled back inside the poses it has. All of it is the same defect from
506
+ * the player's side — the character was drawn somewhere it now turns out it was not — and all
507
+ * of it eases away here.
508
+ */
509
+ private absorbJump;
510
+ /**
511
+ * Ease every offset towards zero. Exponential, so the rate is proportional to the error and
512
+ * there is no world-scale speed to pick: a correction worth a tenth of a unit is worked off
513
+ * gently and one worth a whole unit is not left hanging around for a second.
514
+ */
515
+ private decayOffsets;
516
+ private clearOffset;
517
+ /**
518
+ * The pose to draw one body at: the stored poses either side of the render clock, blended.
519
+ * Falls back to the body's live transform when the ring cannot bracket it — before the first
520
+ * capture, and for a body that appeared this frame.
521
+ */
522
+ private rawPose;
523
+ /** `rawPose` with the render-error offset applied — what the draw loop actually gets. */
524
+ private renderPose;
358
525
  /**
359
526
  * The predicted record for `collection[id]`: the authoritative record (which already carries
360
527
  * local intent writes — `track()` writes through to plain state) with the body-mapped channels
361
528
  * replaced by the local world's values, `Math.fround`ed for f32 fields so what the draw loop
362
529
  * reads is what the wire would carry.
530
+ *
531
+ * Drawn *between* two simulated ticks, not at the head. The local world steps at the room's
532
+ * tick rate, and only when authority arrives to rebase it; a display runs at two to four times
533
+ * that and asks on every frame. Handing back the head means the answer changes on some frames
534
+ * and not others, so a character running at a steady nine units a second is drawn standing
535
+ * still for half of them and moving at twenty for the rest. It is the player's own character
536
+ * answering their own key, and it reads as the game hitching.
537
+ *
538
+ * This is a *render* read. `room.state` and every correctness path — `predictedValues`, the
539
+ * per-tick history a correction is judged against — still see the head exactly as before.
363
540
  */
364
541
  read(desc: CollectionDesc, id: string, base: AnyRecord$2): AnyRecord$2;
365
542
  /**
@@ -388,11 +565,43 @@ declare class PhysicsPredictor {
388
565
  private warnOverCap;
389
566
  private warnOnce;
390
567
  /**
391
- * The client's lead over authority, in ticks: one-way latency plus one tick of margin.
392
- * Measured (dive e2e, 2026-08-27): a full-round-trip lead was tried for the tick-aligned
393
- * replay and made release transitions *worse* (-1.4 to -1.8 u worst backwards step vs -0.6 at
394
- * half), because the extra lead grows the standing overshoot a rest-settling body has to give
395
- * back. Half the trip plus margin is where the stamp clock and the correction stream meet.
568
+ * The client's lead over authority, in ticks: the one-way transit the authority in hand has
569
+ * already spent in flight, rounded to the nearest tick. Nothing more.
570
+ *
571
+ * Every tick of lead beyond that transit is a tick of motion the client draws on the
572
+ * assumption that the input it is holding will still be held when the server gets there. On a
573
+ * release that assumption is wrong by construction, and the invented travel is handed back as
574
+ * a backwards yank — bug 1's "keeps moving after key up and then snaps back". It was
575
+ * `ceil(owd) + 1`, which on a fast link is two whole ticks of margin over a transit of nearly
576
+ * zero. Measured on the arena fixture at no injected latency (release overshoot of a 6 u/s
577
+ * held-input character, `physics-predict.test.ts`):
578
+ *
579
+ * | lead | drawn overshoot past the server's stop | worst backwards step |
580
+ * | --- | --- | --- |
581
+ * | `ceil(owd) + 1` (was) | 0.585 u | -0.585 u |
582
+ * | `max(1, round(owd))` (is) | 0.293 u | -0.292 u |
583
+ * | `round(owd)`, floor 0 | 0.0008 u | -0.0008 u |
584
+ *
585
+ * One tick of running is 0.3 u there: the overshoot is the lead, in ticks, and nothing else.
586
+ * On a link with no transit to cover, every tick of lead is a tick of guessing that the key is
587
+ * still held.
588
+ *
589
+ * The floor of 1 is not margin, it is the smallest lead that is still prediction. At 0 the
590
+ * rebase takes no steps: the local world is pinned to the authority it just received, which is
591
+ * already a tick old, so the client responds to its own input only when the next `CORRECT`
592
+ * arrives and every moving body reads as mispredicted by one tick of motion. Measured: the
593
+ * bot suites' correction-storm invariant fires immediately (21 corrections/s per bot against a
594
+ * threshold of 5 in `packages/cli/test/simulate-physics.test.ts`). One tick of speculation is
595
+ * the price of predicting at all; two was the bug.
596
+ *
597
+ * Erring low is also the cheap direction — a lead shorter than the transit means authority
598
+ * arrives slightly *ahead* of the prediction, and a correction that pulls the character the way
599
+ * it is already going is the one nobody can see.
600
+ *
601
+ * Measured earlier the same day (dive e2e): a *full*-round-trip lead was worse again
602
+ * (-1.4 to -1.8 u worst backwards step vs -0.6 at half), which is the same finding from the
603
+ * other end. `stats.stampGap` is the running check on all of this — it reports how far ahead
604
+ * of the server's application the stamps still land, and it should sit at 0.
396
605
  */
397
606
  private leadTicks;
398
607
  private timestepMs;
@@ -416,6 +625,7 @@ declare class PhysicsPredictor {
416
625
  stampTick(): number | undefined;
417
626
  /** After every step: advance each owned body's prediction clock and remember its channels. */
418
627
  private recordStep;
628
+ /** Returns the number of steps taken, so the caller knows whether the head moved. */
419
629
  private freeRun;
420
630
  /**
421
631
  * Applies intents for every owned predicted body before one step: the intent in force at the
@@ -634,6 +844,20 @@ type MessageTarget = 'all' | string | {
634
844
  declare const MAX_PREDICTED_BODIES = 64;
635
845
  /** Default correction-suppression epsilon, world units (see `ClientPhysicsOptions.epsilon`). */
636
846
  declare const PREDICTION_EPSILON = 0.05;
847
+ /**
848
+ * Default half-life for render-error smoothing, milliseconds (see
849
+ * `ClientPhysicsOptions.smoothingHalfLifeMs`). About two ticks at 30Hz: long enough that a
850
+ * correction is a settle rather than a step, short enough that the drawn position is back on the
851
+ * simulation within a fifth of a second.
852
+ */
853
+ declare const SMOOTHING_HALF_LIFE_MS = 70;
854
+ /**
855
+ * Default distance past which a render-error offset is dropped rather than eased away, world
856
+ * units (see `ClientPhysicsOptions.smoothingSnapUnits`). A respawn, an area change or a session
857
+ * resync moves a body somewhere else entirely, and sliding into it is worse than arriving.
858
+ * Four units is a guess at world scale and the one number here a game may need to change.
859
+ */
860
+ declare const SMOOTHING_SNAP_UNITS = 4;
637
861
  /** `room.prediction` (D22 part 2). */
638
862
  interface PredictionStatus {
639
863
  /** The engine is loaded and the local world exists. */
@@ -655,7 +879,10 @@ interface Room<S, Role extends string = RoleOf<S> & string> {
655
879
  readonly me: string;
656
880
  /** The room code (from `WELCOME`). */
657
881
  readonly id: string;
658
- /** Shareable URL carrying `?room=<id>`. */
882
+ /**
883
+ * Shareable URL carrying `?room=<id>`, with per-client params (`role`) stripped so it is safe
884
+ * to hand to somebody else. The address bar keeps them.
885
+ */
659
886
  readonly link: string;
660
887
  /** The last server tick this client saw. */
661
888
  readonly tick: number;
@@ -666,7 +893,9 @@ interface Room<S, Role extends string = RoleOf<S> & string> {
666
893
  /**
667
894
  * The render read path (D20): same shapes as `room.state`, but non-owned entities are
668
895
  * interpolated at `now − interpDelayMs` (numeric fields lerp, everything else steps) while
669
- * owned entities return the predicted local values. A draw loop opts in by one substitution —
896
+ * predicted bodies are drawn between the two most recent ticks of the local physics world.
897
+ * Either way the value advances with the frame clock rather than with arrivals or with the
898
+ * tick rate. A draw loop opts in by one substitution —
670
899
  * `room.state.players` → `room.render.players`. Tests and game logic keep reading
671
900
  * `room.state`, which stays authoritative.
672
901
  */
@@ -757,8 +986,10 @@ declare const webSocketTransport: Transport;
757
986
  * The interpolated render read path (D20, week 8): `room.render.<collection>` mirrors
758
987
  * `room.state`'s shapes, but non-owned entities are rendered at `now − interpDelayMs`, lerping
759
988
  * numeric fields between the two bracketing DELTAs and stepping everything else (strings, bools,
760
- * enums, refs, lists). Owned entities return the predicted (local) values — the same object
761
- * `room.state` hands out so one read path serves the whole draw loop.
989
+ * enums, refs, lists). Predicted bodies owned or `predicted: true`come from the local
990
+ * physics world instead, drawn between the two most recent simulated ticks rather than at the
991
+ * simulation head, so they too advance with the frame clock and not with whatever rate their
992
+ * source happens to update at (`physics.ts`, `read`). One read path serves the whole draw loop.
762
993
  *
763
994
  * `room.state` stays authoritative and untouched: tests, bots and existing code see exactly what
764
995
  * they saw before. Entities appear and disappear at the buffered time; past the newest delta the
@@ -820,8 +1051,13 @@ type AnyRecord = Record<string, unknown>;
820
1051
  type ClientImpl = (params: AnyRecord) => unknown;
821
1052
  /** Default hard cap on the owned-write flush window. */
822
1053
  declare const DEFAULT_WRITE_INTERVAL_MS = 50;
823
- /** How often the client pings for `room.rtt`. */
824
- declare const PING_INTERVAL_MS = 10000;
1054
+ /**
1055
+ * How often the client pings for `room.rtt`. Two seconds rather than ten (bug 4): the prediction
1056
+ * lead is re-derived from `rtt` on every rebase, so the interval is not just how fresh a
1057
+ * diagnostic number is — it is how long one measurement gets to size the client's simulation.
1058
+ * A PING is a 4-byte payload; five of them a minute is not the reason a room costs anything.
1059
+ */
1060
+ declare const PING_INTERVAL_MS = 2000;
825
1061
  /** How long an outbound RPC waits for its `REPLY` before rejecting. */
826
1062
  declare const CALL_TIMEOUT_MS = 10000;
827
1063
  interface SessionOptions {
@@ -1008,4 +1244,4 @@ declare function joinRoom<S extends AnySchema, Role extends string = RoleOf<S> &
1008
1244
  */
1009
1245
  declare function joinRelay(options?: JoinRelayOptions): Promise<RelayRoom>;
1010
1246
 
1011
- export { CALL_TIMEOUT_MS, type ClientBodySpec, type ClientCollection, type ClientPhysicsOptions, type ClientRapierBody, type ClientRapierModule, type ClientRapierWorld, type ClientState, ClientStore, type ClientVector3, type Correction, DEFAULT_REGION, DEFAULT_WRITE_INTERVAL_MS, DEV_PORT, E_CONNECT_FAILED, type FrameHook, type JoinOptions, type JoinRelayOptions, MAX_PREDICTED_BODIES, type MessageTarget, PING_INTERVAL_MS, PREDICTION_EPSILON, type PredictionStats, type PredictionStatus, RESIM_DEPTH, type RelayRoom, type Room, type RoomCallProxy, type RoomError, type RoomEvents, type Scheduler, Session, type Status, type Transport, type TransportSocket, type Unsubscribe, defaultScheduler, joinRelay, joinRoom, linkForUrl, resolveUrl, roomIdFrom, webSocketTransport };
1247
+ export { CALL_TIMEOUT_MS, type ClientBodySpec, type ClientCollection, type ClientPhysicsOptions, type ClientRapierBody, type ClientRapierModule, type ClientRapierWorld, type ClientState, ClientStore, type ClientVector3, type Correction, DEFAULT_REGION, DEFAULT_WRITE_INTERVAL_MS, DEV_PORT, E_CONNECT_FAILED, type FrameHook, type JoinOptions, type JoinRelayOptions, MAX_PREDICTED_BODIES, type MessageTarget, PING_INTERVAL_MS, PREDICTION_EPSILON, type PredictionStats, type PredictionStatus, RESIM_DEPTH, type RelayRoom, type Room, type RoomCallProxy, type RoomError, type RoomEvents, SMOOTHING_HALF_LIFE_MS, SMOOTHING_SNAP_UNITS, type Scheduler, Session, type Status, type Transport, type TransportSocket, type Unsubscribe, defaultScheduler, joinRelay, joinRoom, linkForUrl, resolveUrl, roomIdFrom, webSocketTransport };
package/dist/index.js CHANGED
@@ -4,8 +4,10 @@ import {
4
4
  MAX_PREDICTED_BODIES,
5
5
  PREDICTION_EPSILON,
6
6
  RESIM_DEPTH,
7
+ SMOOTHING_HALF_LIFE_MS,
8
+ SMOOTHING_SNAP_UNITS,
7
9
  emptyPredictionStats
8
- } from "./chunk-AJHN3YF6.js";
10
+ } from "./chunk-7UTJ7RSF.js";
9
11
 
10
12
  // src/endpoint.ts
11
13
  var DEFAULT_REGION = "eu";
@@ -61,15 +63,19 @@ function roomIdFromLocation() {
61
63
  return "";
62
64
  }
63
65
  }
66
+ var PER_CLIENT_PARAMS = ["role"];
64
67
  function publishRoomToLocation(roomId) {
65
68
  const loc = currentLocation();
66
69
  const history = globalThis.history;
67
70
  if (!loc) return void 0;
68
71
  let href;
72
+ let share;
69
73
  try {
70
74
  const url = new URL(loc.href);
71
75
  url.searchParams.set("room", roomId);
72
76
  href = url.href;
77
+ for (const param of PER_CLIENT_PARAMS) url.searchParams.delete(param);
78
+ share = url.href;
73
79
  } catch {
74
80
  return void 0;
75
81
  }
@@ -79,7 +85,7 @@ function publishRoomToLocation(roomId) {
79
85
  } catch {
80
86
  }
81
87
  }
82
- return href;
88
+ return share;
83
89
  }
84
90
  function linkForUrl(wsUrl, roomId) {
85
91
  try {
@@ -114,6 +120,7 @@ import {
114
120
  encodeReply,
115
121
  errorByCode,
116
122
  formatError,
123
+ readCorrectAppliedTick,
117
124
  readCorrectClientTick,
118
125
  relaySchema,
119
126
  rpcTable,
@@ -479,7 +486,8 @@ var webSocketTransport = {
479
486
 
480
487
  // src/session.ts
481
488
  var DEFAULT_WRITE_INTERVAL_MS = 50;
482
- var PING_INTERVAL_MS = 1e4;
489
+ var PING_INTERVAL_MS = 2e3;
490
+ var RTT_ALPHA = 0.3;
483
491
  var CALL_TIMEOUT_MS = 1e4;
484
492
  var BACKOFF_START_MS = 250;
485
493
  var BACKOFF_MAX_MS = 5e3;
@@ -518,7 +526,7 @@ var Session = class {
518
526
  if (options.physics && hasPhysics) {
519
527
  const physics = options.physics;
520
528
  this.predictionRequested = true;
521
- void import("./physics-BBFSQEYL.js").then(({ PhysicsPredictor }) => {
529
+ void import("./physics-H2VDQLAU.js").then(({ PhysicsPredictor }) => {
522
530
  if (this.left) return;
523
531
  const predictor = new PhysicsPredictor(
524
532
  this.ext,
@@ -836,7 +844,7 @@ var Session = class {
836
844
  this.store.applyServerDelta(delta);
837
845
  this.render.recordDelta(delta);
838
846
  if (this.predictor && delta.collections.some((dc) => this.predictor?.predictsCollection(dc.name))) {
839
- this.predictor.noteAuthority();
847
+ this.predictor.noteAuthority(delta.tick);
840
848
  this.predictor.frame(this.scheduler.now());
841
849
  }
842
850
  }
@@ -844,6 +852,10 @@ var Session = class {
844
852
  const r = new ByteReader(payload);
845
853
  const delta = decodeDeltaFrom(this.ext, r);
846
854
  const clientTick = readCorrectClientTick(r);
855
+ const appliedTick = readCorrectAppliedTick(r);
856
+ if (clientTick !== void 0 && appliedTick !== void 0) {
857
+ this.predictor?.noteWriteApplied(clientTick, appliedTick);
858
+ }
847
859
  this.tick = delta.tick;
848
860
  const predicted = this.capturePredicted(delta);
849
861
  let sawBodyFields = false;
@@ -870,7 +882,7 @@ var Session = class {
870
882
  });
871
883
  }
872
884
  if (sawBodyFields && this.predictor) {
873
- this.predictor.noteAuthority();
885
+ this.predictor.noteAuthority(delta.tick);
874
886
  this.predictor.frame(this.scheduler.now());
875
887
  }
876
888
  }
@@ -916,7 +928,8 @@ var Session = class {
916
928
  }
917
929
  onPong(payload) {
918
930
  const pong = decodePong(payload);
919
- this.rtt = Math.max(1, (this.scheduler.now() >>> 0) - pong.t >>> 0);
931
+ const sample = Math.max(1, (this.scheduler.now() >>> 0) - pong.t >>> 0);
932
+ this.rtt = this.rtt === 0 ? sample : Math.max(1, Math.round(this.rtt + (sample - this.rtt) * RTT_ALPHA));
920
933
  if (pong.serverTick > this.tick) this.tick = pong.serverTick;
921
934
  }
922
935
  onMsg(payload) {
@@ -1310,6 +1323,8 @@ export {
1310
1323
  PING_INTERVAL_MS,
1311
1324
  PREDICTION_EPSILON,
1312
1325
  RESIM_DEPTH,
1326
+ SMOOTHING_HALF_LIFE_MS,
1327
+ SMOOTHING_SNAP_UNITS,
1313
1328
  Session,
1314
1329
  defaultScheduler,
1315
1330
  joinRelay,