@colyseus/schema 5.0.10 → 5.0.11

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colyseus/schema",
3
- "version": "5.0.10",
3
+ "version": "5.0.11",
4
4
  "description": "Binary state serializer with delta encoding for games",
5
5
  "type": "module",
6
6
  "bin": {
@@ -232,7 +232,6 @@ export class ChangeTree<T extends Ref = any> implements ChangeRecorder {
232
232
  // per-view WeakSet lookups with direct bitwise ops.
233
233
  // Lazy: undefined until the tree participates in any view.
234
234
  visibleViews?: number[];
235
- invisibleViews?: number[];
236
235
 
237
236
  // Per-(view, tag) bitmap, indexed by tag. Custom tags only —
238
237
  // DEFAULT_VIEW_TAG visibility lives in `visibleViews`.
@@ -586,7 +585,6 @@ export class ChangeTree<T extends Ref = any> implements ChangeRecorder {
586
585
  // per-view visibility lives on the tree (NOT keyed by refId), so a
587
586
  // recycled tree must not inherit its previous life's view membership.
588
587
  this.visibleViews = undefined;
589
- this.invisibleViews = undefined;
590
588
  this.tagViews = undefined;
591
589
  this.subscribedViews = undefined;
592
590
  }
@@ -119,16 +119,7 @@ function _fullSyncWalk(ctx: EncodeCtx, changeTree: ChangeTree): void {
119
119
  // Visibility gate: when a view is active, a non-visible tree contributes
120
120
  // nothing itself but we still recurse so descendants (possibly added to
121
121
  // the view explicitly) are reachable.
122
- let visibleHere = true;
123
- if (ctx.hasView) {
124
- const view = ctx.view!;
125
- if (!view.isChangeTreeVisible(changeTree)) {
126
- view.markInvisible(changeTree);
127
- visibleHere = false;
128
- } else {
129
- view.unmarkInvisible(changeTree);
130
- }
131
- }
122
+ const visibleHere = !ctx.hasView || ctx.view!.isChangeTreeVisible(changeTree);
132
123
 
133
124
  if (visibleHere) {
134
125
  const desc = changeTree.encDescriptor;
@@ -313,12 +304,8 @@ export class Encoder<T extends Schema = any> {
313
304
  while (current = current.next) {
314
305
  const changeTree = (current as ChangeTreeNode).changeTree;
315
306
 
316
- if (hasView) {
317
- if (!view.isChangeTreeVisible(changeTree)) {
318
- view.markInvisible(changeTree);
319
- continue;
320
- }
321
- view.unmarkInvisible(changeTree);
307
+ if (hasView && !view.isChangeTreeVisible(changeTree)) {
308
+ continue;
322
309
  }
323
310
 
324
311
  const recorder = unreliable ? changeTree.unreliableRecorder : changeTree;
@@ -28,8 +28,6 @@ function _clearViewBitFromAllTrees(root: Root, slot: number, bit: number): void
28
28
  const tree = trees[refId];
29
29
  const v = tree.visibleViews;
30
30
  if (v !== undefined && slot < v.length) v[slot] &= clearMask;
31
- const i = tree.invisibleViews;
32
- if (i !== undefined && slot < i.length) i[slot] &= clearMask;
33
31
  const s = tree.subscribedViews;
34
32
  if (s !== undefined && slot < s.length) s[slot] &= clearMask;
35
33
  const t = tree.tagViews;
@@ -192,32 +190,6 @@ export class StateView {
192
190
  if (slot < arr.length) arr[slot] &= ~this._bit;
193
191
  }
194
192
 
195
- /** True iff this view has previously marked `tree` as invisible. */
196
- public isInvisible(tree: ChangeTree): boolean {
197
- const arr = tree.invisibleViews;
198
- const slot = this._slot;
199
- return arr !== undefined && slot < arr.length && (arr[slot] & this._bit) !== 0;
200
- }
201
-
202
- /** Mark `tree` as invisible to this view (used by encode loop). */
203
- public markInvisible(tree: ChangeTree): void {
204
- const slot = this._slot;
205
- let arr = tree.invisibleViews;
206
- if (arr === undefined) {
207
- arr = tree.invisibleViews = [];
208
- }
209
- while (arr.length <= slot) arr.push(0);
210
- arr[slot] |= this._bit;
211
- }
212
-
213
- /** Clear invisible bit. */
214
- public unmarkInvisible(tree: ChangeTree): void {
215
- const arr = tree.invisibleViews;
216
- if (arr === undefined) return;
217
- const slot = this._slot;
218
- if (slot < arr.length) arr[slot] &= ~this._bit;
219
- }
220
-
221
193
  // ──────────────────────────────────────────────────────────────────
222
194
  // Per-tag, per-view bitmap. Replaces the legacy
223
195
  // `tags: WeakMap<ChangeTree, Set<number>>` storage. Hot read site is
@@ -371,13 +343,20 @@ export class StateView {
371
343
  // subclasses yield a real Metadata object.
372
344
  const metadata: Metadata = (obj.constructor as typeof Schema)[Symbol.metadata];
373
345
 
374
- this.markVisible(changeTree);
375
-
376
- // add to iterable list (only the explicitly added items)
377
- if (this.iterable && checkIncludeParent) {
346
+ // Add to iterable list (only the explicitly added items), deduping
347
+ // re-adds of an already-visible instance. isVisible must be read
348
+ // BEFORE markVisible; indexOf runs only on the re-add path.
349
+ // NOTE: dedup applies to `items` only — a re-add still re-queues the
350
+ // full snapshot on purpose (shared-view bootstrap re-add: a
351
+ // late-attached client may not have consumed earlier drains).
352
+ // Callers wanting cheap idempotence can guard with `view.has(obj)`.
353
+ if (this.iterable && checkIncludeParent
354
+ && (!this.isVisible(changeTree) || this.items.indexOf(obj) === -1)) {
378
355
  this.items.push(obj);
379
356
  }
380
357
 
358
+ this.markVisible(changeTree);
359
+
381
360
  // add parent ChangeTree's
382
361
  // - if it was invisible to this view
383
362
  // - if it were previously filtered out
@@ -468,7 +447,6 @@ export class StateView {
468
447
 
469
448
  } else if (!changeTree.isNew || isChildAdded) {
470
449
  // new structures will be added as part of .encode() call, no need to force it to .encodeView()
471
- const isInvisible = this.isInvisible(changeTree);
472
450
 
473
451
  // Full-sync snapshot: walk the live ref structurally instead of
474
452
  // iterating a cumulative recorder bucket. Every populated index
@@ -476,11 +454,14 @@ export class StateView {
476
454
  // at encode time). Per-field tags come from the descriptor's
477
455
  // precomputed `tags[]` array — direct index vs a metadata[i].tag
478
456
  // object hop.
457
+ //
458
+ // Non-matching custom-tagged fields are NEVER included here —
459
+ // `view.changes` is drained without a per-field tag re-check,
460
+ // so anything added leaks straight to the wire.
479
461
  const tags = changeTree.encDescriptor.tags;
480
462
  changeTree.forEachLive((index) => {
481
463
  const tagAtIndex = tags[index];
482
464
  if (
483
- isInvisible || // if "invisible", include all
484
465
  tagAtIndex === undefined || // "all change" with no tag
485
466
  tagAtIndex === DEFAULT_VIEW_TAG || // visible to all clients
486
467
  (tag !== DEFAULT_VIEW_TAG && (tagAtIndex & tag) !== 0) // tag bits overlap
@@ -652,7 +633,7 @@ export class StateView {
652
633
 
653
634
  // ── Streamable-collection unsubscribe (the stream itself) ─────
654
635
  // Flush DELETE for every sent position and drop pending. After
655
- // this, the stream is marked invisible to this view — any future
636
+ // this, the stream is no longer visible to this view — any future
656
637
  // `stream.add()` would still seed broadcast pending (if no views)
657
638
  // but would NOT re-seed per-view pending (user must re-subscribe).
658
639
  if (changeTree.isStreamCollection) {