@irtio/client 0.5.2 → 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.
@@ -1,6 +1,7 @@
1
1
  // src/contract.ts
2
2
  var E_CONNECT_FAILED = "E_CONNECT_FAILED";
3
3
  var MAX_PREDICTED_BODIES = 64;
4
+ var MAX_PROXY_BODIES = 256;
4
5
  var PREDICTION_EPSILON = 0.05;
5
6
  var SMOOTHING_HALF_LIFE_MS = 70;
6
7
  var SMOOTHING_SNAP_UNITS = 4;
@@ -12,9 +13,14 @@ function emptyPredictionStats() {
12
13
  snaps: 0,
13
14
  suppressed: 0,
14
15
  overCap: 0,
16
+ proxies: 0,
17
+ absent: 0,
15
18
  lastResimMicros: 0,
16
19
  smoothing: 0,
17
- stampGap: 0
20
+ stampGap: 0,
21
+ stampGapRaw: 0,
22
+ lastStampTick: 0,
23
+ lastAppliedTick: 0
18
24
  };
19
25
  }
20
26
 
@@ -58,6 +64,10 @@ var ClientCollectionImpl = class {
58
64
  }
59
65
  store;
60
66
  desc;
67
+ /** D50: point this facade at the same collection in a rebuilt schema. */
68
+ retarget(desc) {
69
+ this.desc = desc;
70
+ }
61
71
  get plain() {
62
72
  return entityOf(this.store.plain, this.desc.name);
63
73
  }
@@ -101,7 +111,8 @@ var ClientStore = class {
101
111
  for (const c of ext.collections) this.descs.set(c.name, c);
102
112
  this.plain = createState(ext);
103
113
  this.tracked = track(ext, this.plain);
104
- this.view = this.buildView();
114
+ this.view = {};
115
+ this.buildViewInto(this.view);
105
116
  }
106
117
  ext;
107
118
  meOf;
@@ -109,6 +120,8 @@ var ClientStore = class {
109
120
  tracked;
110
121
  descs = /* @__PURE__ */ new Map();
111
122
  frozen = /* @__PURE__ */ new WeakMap();
123
+ /** Live collection facades by name, so a D50 swap can retarget rather than replace them. */
124
+ facades = /* @__PURE__ */ new Map();
112
125
  /** The object handed out as `room.state`; identity survives a resync. */
113
126
  view;
114
127
  /** Flushed-but-unjudged writes, oldest first, at most `RESIM_DEPTH` entries (D19). */
@@ -122,6 +135,29 @@ var ClientStore = class {
122
135
  * walks the window tick by tick starts from here, not from the newest local value.
123
136
  */
124
137
  baselineIntents = /* @__PURE__ */ new Map();
138
+ /**
139
+ * D50: rebuild every schema-derived handle in this store against `newExt`, keeping the objects
140
+ * user code holds. `view` keeps its identity (contractual, and pinned by test), and so does each
141
+ * collection facade behind it, because a game may well have hoisted `room.state.players` into a
142
+ * local.
143
+ *
144
+ * State itself is *not* carried across: `plain` is re-created empty under the new schema. That
145
+ * is not a loss, because a swap only ever happens immediately before the resync WELCOME that
146
+ * `rejoinAll` sends, and `loadSnapshot` re-seeds everything from it. Carrying the old records
147
+ * forward would be actively wrong — `normalizeRecord` rejects field names the old schema did
148
+ * not have, and a record built under the old descriptor is missing the new fields entirely.
149
+ */
150
+ swapSchema(newExt) {
151
+ this.ext = newExt;
152
+ this.descs.clear();
153
+ for (const c of newExt.collections) this.descs.set(c.name, c);
154
+ this.plain = createState(newExt);
155
+ this.tracked = track(newExt, this.plain);
156
+ this.pendingWrites.length = 0;
157
+ this.evictedThroughTick = 0;
158
+ this.baselineIntents.clear();
159
+ this.buildViewInto(this.view);
160
+ }
125
161
  // -- snapshot / resync -----------------------------------------------------
126
162
  loadSnapshot(bytes) {
127
163
  this.plain = decodeSnapshot(this.ext, bytes).state;
@@ -182,20 +218,48 @@ var ClientStore = class {
182
218
  }
183
219
  this.remarkOwned();
184
220
  }
185
- buildView() {
186
- const view = {};
221
+ /**
222
+ * Defines one accessor per collection on `view`. Called once at construction and again on every
223
+ * schema swap, always against the SAME object, so `room.state`'s identity never changes.
224
+ *
225
+ * A collection that survives the swap keeps its facade and is retargeted at the new descriptor;
226
+ * a collection the new schema adds gets a fresh one; a collection that disappears has its
227
+ * accessor deleted. Only the first two can happen on an additive swap, but a `delete` is one
228
+ * line and a stale accessor reading a collection that no longer exists would throw from inside
229
+ * a getter, which is a miserable thing to debug.
230
+ */
231
+ buildViewInto(view) {
232
+ const names = /* @__PURE__ */ new Set();
187
233
  for (const c of this.ext.collections) {
234
+ names.add(c.name);
188
235
  if (c.kind === "entity") {
189
- const facade = withIndexSugar(new ClientCollectionImpl(this, c));
190
- Object.defineProperty(view, c.name, { get: () => facade, enumerable: true });
236
+ const existing = this.facades.get(c.name);
237
+ if (existing) {
238
+ existing.retarget(c);
239
+ continue;
240
+ }
241
+ const impl = new ClientCollectionImpl(this, c);
242
+ this.facades.set(c.name, impl);
243
+ const facade = withIndexSugar(impl);
244
+ Object.defineProperty(view, c.name, {
245
+ get: () => facade,
246
+ enumerable: true,
247
+ configurable: true
248
+ });
191
249
  } else {
192
250
  Object.defineProperty(view, c.name, {
193
251
  get: () => this.freeze(this.plain[c.name], this.singletonHint(c)),
194
- enumerable: true
252
+ enumerable: true,
253
+ configurable: true
195
254
  });
196
255
  }
197
256
  }
198
- return view;
257
+ for (const name of Object.keys(view)) {
258
+ if (!names.has(name)) {
259
+ delete view[name];
260
+ this.facades.delete(name);
261
+ }
262
+ }
199
263
  }
200
264
  // -- instances ------------------------------------------------------------
201
265
  /** The writable tracked proxy when this client owns `id`, otherwise a frozen one. */
@@ -559,6 +623,7 @@ var ClientStore = class {
559
623
  export {
560
624
  E_CONNECT_FAILED,
561
625
  MAX_PREDICTED_BODIES,
626
+ MAX_PROXY_BODIES,
562
627
  PREDICTION_EPSILON,
563
628
  SMOOTHING_HALF_LIFE_MS,
564
629
  SMOOTHING_SNAP_UNITS,