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