@colyseus/schema 4.0.25 → 4.0.27

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/build/index.js CHANGED
@@ -821,10 +821,25 @@
821
821
  });
822
822
  }
823
823
  metadata[$viewFieldIndexes].push(index);
824
- if (!metadata[$fieldIndexesByViewTag][tag]) {
825
- metadata[$fieldIndexesByViewTag][tag] = [];
824
+ // Populate $fieldIndexesByViewTag: for a bitmask tag, register the field
825
+ // index under each individual set bit so that view.add(obj, Tag.ONE) finds
826
+ // fields tagged @view(Tag.ONE|Tag.TWO).
827
+ // Negative tags (i.e. DEFAULT_VIEW_TAG = -1) are stored as-is.
828
+ if (tag < 0) {
829
+ if (!metadata[$fieldIndexesByViewTag][tag]) {
830
+ metadata[$fieldIndexesByViewTag][tag] = [];
831
+ }
832
+ metadata[$fieldIndexesByViewTag][tag].push(index);
833
+ }
834
+ else {
835
+ for (let bits = tag; bits > 0; bits &= bits - 1) {
836
+ const bit = bits & (-bits); // isolate lowest set bit
837
+ if (!metadata[$fieldIndexesByViewTag][bit]) {
838
+ metadata[$fieldIndexesByViewTag][bit] = [];
839
+ }
840
+ metadata[$fieldIndexesByViewTag][bit].push(index);
841
+ }
826
842
  }
827
- metadata[$fieldIndexesByViewTag][tag].push(index);
828
843
  },
829
844
  setFields(target, fields) {
830
845
  // for inheritance support
@@ -1680,17 +1695,25 @@
1680
1695
  if (previousValue) {
1681
1696
  let previousRefId = previousValue[$refId];
1682
1697
  if (previousRefId !== undefined && refId !== previousRefId) {
1698
+ // Collection field replaced by a different instance.
1683
1699
  //
1684
- // enqueue onRemove if structure has been replaced.
1685
- //
1700
+ // Don't decrement children here: GC (`garbageCollectDeletedRefs`)
1701
+ // removes them once the previous collection's refId hits zero.
1702
+ // Doing it here too would double-decrement a *shared* child and
1703
+ // drop it while still referenced ("refId not found").
1704
+ if ((operation & exports.OPERATION.DELETE) !== exports.OPERATION.DELETE) {
1705
+ // Replacement not tagged DELETE (e.g. pending ADD not upgraded
1706
+ // to DELETE_AND_ADD), so the previous refId wasn't decremented
1707
+ // above. Release it here, else it never gets GC'd (leak).
1708
+ $root.removeRef(previousRefId);
1709
+ }
1710
+ // enqueue onRemove callbacks for the previous collection's children.
1686
1711
  const entries = previousValue.entries();
1687
1712
  let iter;
1688
1713
  while ((iter = entries.next()) && !iter.done) {
1689
1714
  const [key, value] = iter.value;
1690
- // if value is a schema, remove its reference
1691
1715
  if (typeof (value) === "object") {
1692
1716
  previousRefId = value[$refId];
1693
- $root.removeRef(previousRefId);
1694
1717
  }
1695
1718
  allChanges.push({
1696
1719
  ref: previousValue,
@@ -3768,9 +3791,10 @@
3768
3791
  return view.isChangeTreeVisible(ref[$changes]);
3769
3792
  }
3770
3793
  else {
3771
- // view pass: custom tag
3794
+ // view pass: custom tag (bitmask)
3795
+ // tag is the field's stored bitmask; view.tags stores the accumulated bitmask of tags used in view.add().
3772
3796
  const tags = view.tags?.get(ref[$changes]);
3773
- return tags && tags.has(tag);
3797
+ return tags != null && (tag & tags) !== 0;
3774
3798
  }
3775
3799
  }
3776
3800
  // allow inherited classes to have a constructor
@@ -4380,41 +4404,43 @@
4380
4404
  encoder(this, buffer, changeTree, fieldIndex, operation, it, isEncodeAll, hasView, metadata);
4381
4405
  }
4382
4406
  }
4383
- if (it.offset > buffer.byteLength) {
4384
- // we can assume that n + 1 BUFFER_SIZE will suffice given that we are likely done with encoding at this point
4385
- // multiples of BUFFER_SIZE are faster to allocate than arbitrary sizes
4386
- const newSize = Math.ceil(it.offset / Encoder.BUFFER_SIZE) * Encoder.BUFFER_SIZE;
4387
- console.warn(`@colyseus/schema buffer overflow. Encoded state is higher than default BUFFER_SIZE. Use the following to increase default BUFFER_SIZE:
4407
+ if (it.offset <= buffer.byteLength) {
4408
+ return buffer.subarray(0, it.offset);
4409
+ }
4410
+ // Overflowed: grow and re-encode. Reuse the same iterator so `it.offset`
4411
+ // ends accurate a fresh one strands it at the overflow value and
4412
+ // corrupts the next view's `viewOffset` in a multi-view encode.
4413
+ buffer = this.ensureCapacity(buffer, it.offset);
4414
+ console.warn(`@colyseus/schema buffer overflow. Encoded state is higher than default BUFFER_SIZE. Use the following to increase default BUFFER_SIZE:
4388
4415
 
4389
4416
  import { Encoder } from "@colyseus/schema";
4390
- Encoder.BUFFER_SIZE = ${Math.round(newSize / 1024)} * 1024; // ${Math.round(newSize / 1024)} KB
4417
+ Encoder.BUFFER_SIZE = ${Math.round(buffer.byteLength / 1024)} * 1024; // ${Math.round(buffer.byteLength / 1024)} KB
4391
4418
  `);
4392
- //
4393
- // resize buffer and re-encode (TODO: can we avoid re-encoding here?)
4394
- // -> No we probably can't unless we catch the need for resize before encoding which is likely more computationally expensive than resizing on demand
4395
- //
4396
- const newBuffer = new Uint8Array(newSize);
4397
- newBuffer.set(buffer); // copy previous encoding steps beyond the initialOffset
4398
- buffer = newBuffer;
4399
- // assign resized buffer to local sharedBuffer
4400
- if (buffer === this.sharedBuffer) {
4401
- this.sharedBuffer = buffer;
4402
- }
4403
- return this.encode({ offset: initialOffset }, view, buffer, changeSetName, isEncodeAll);
4404
- }
4405
- else {
4406
- return buffer.subarray(0, it.offset);
4407
- }
4419
+ it.offset = initialOffset;
4420
+ return this.encode(it, view, buffer, changeSetName, isEncodeAll);
4408
4421
  }
4409
4422
  encodeAll(it = { offset: 0 }, buffer = this.sharedBuffer) {
4410
4423
  return this.encode(it, undefined, buffer, "allChanges", true);
4411
4424
  }
4412
4425
  encodeAllView(view, sharedOffset, it, bytes = this.sharedBuffer) {
4413
4426
  const viewOffset = it.offset;
4414
- // try to encode "filtered" changes
4415
- this.encode(it, view, bytes, "allFilteredChanges", true, viewOffset);
4427
+ // encode() may reallocate the buffer — keep its return, not the stale `bytes`.
4428
+ bytes = this.encode(it, view, bytes, "allFilteredChanges", true, viewOffset);
4416
4429
  return concatBytes(bytes.subarray(0, sharedOffset), bytes.subarray(viewOffset, it.offset));
4417
4430
  }
4431
+ /** Grow `buffer` to keep BUFFER_SIZE free bytes past `offset`, preserving `[0, offset)`. */
4432
+ ensureCapacity(buffer, offset) {
4433
+ if (offset + Encoder.BUFFER_SIZE <= buffer.byteLength) {
4434
+ return buffer;
4435
+ }
4436
+ const size = Math.ceil((offset + Encoder.BUFFER_SIZE) / Encoder.BUFFER_SIZE) * Encoder.BUFFER_SIZE;
4437
+ const grown = new Uint8Array(size);
4438
+ grown.set(buffer.subarray(0, offset));
4439
+ if (buffer === this.sharedBuffer) {
4440
+ this.sharedBuffer = grown;
4441
+ }
4442
+ return grown;
4443
+ }
4418
4444
  encodeView(view, sharedOffset, it, bytes = this.sharedBuffer) {
4419
4445
  const viewOffset = it.offset;
4420
4446
  //
@@ -4453,6 +4479,9 @@
4453
4479
  const ctor = ref.constructor;
4454
4480
  const encoder = ctor[$encoder];
4455
4481
  const metadata = ctor[Symbol.metadata];
4482
+ // These writes are unguarded and unrecoverable (view.changes is cleared
4483
+ // below), so unlike encode() they can't re-encode on overflow — grow ahead.
4484
+ bytes = this.ensureCapacity(bytes, it.offset);
4456
4485
  bytes[it.offset++] = SWITCH_TO_STRUCTURE & 255;
4457
4486
  encode.number(bytes, ref[$refId], it);
4458
4487
  for (let i = 0, numChanges = keys.length; i < numChanges; i++) {
@@ -4472,8 +4501,10 @@
4472
4501
  // clear "view" changes after encoding
4473
4502
  view.changes.clear();
4474
4503
  view.changesOutOfOrder = false;
4475
- // try to encode "filtered" changes
4476
- this.encode(it, view, bytes, "filteredChanges", false, viewOffset);
4504
+ // encode() may reallocate the buffer — keep its return, not the stale `bytes`.
4505
+ // Anchor the re-encode at the current offset (default), not `viewOffset`: a
4506
+ // resize must not clobber the view.changes already written at [viewOffset, ).
4507
+ bytes = this.encode(it, view, bytes, "filteredChanges", false);
4477
4508
  return concatBytes(bytes.subarray(0, sharedOffset), bytes.subarray(viewOffset, it.offset));
4478
4509
  }
4479
4510
  /**
@@ -5584,7 +5615,7 @@
5584
5615
  * List of ChangeTree's that are invisible to this view
5585
5616
  */
5586
5617
  invisible = new WeakSet();
5587
- tags; // TODO: use bit manipulation instead of Set<number> ()
5618
+ tags; // bitmask of tags used to add each ChangeTree
5588
5619
  /**
5589
5620
  * Manual "ADD" operations for changes per ChangeTree, specific to this view.
5590
5621
  * (This is used to force encoding a property, even if it was not changed)
@@ -5672,9 +5703,16 @@
5672
5703
  changeTree.forEachChild((change, index) => {
5673
5704
  // Do not ADD children that don't have the same tag
5674
5705
  if (metadata &&
5675
- metadata[index].tag !== undefined &&
5676
- metadata[index].tag !== tag) {
5677
- return;
5706
+ metadata[index].tag !== undefined) {
5707
+ const fieldTag = metadata[index].tag;
5708
+ // DEFAULT_VIEW_TAG fields are visible to all clients.
5709
+ // Custom-tagged fields are only visible when bits overlap,
5710
+ // and never to default-tag clients.
5711
+ const tagMatch = fieldTag === DEFAULT_VIEW_TAG ||
5712
+ (tag !== DEFAULT_VIEW_TAG && (fieldTag & tag) !== 0);
5713
+ if (!tagMatch) {
5714
+ return;
5715
+ }
5678
5716
  }
5679
5717
  if (this.add(change.ref, tag, false)) {
5680
5718
  isChildAdded = true;
@@ -5685,15 +5723,9 @@
5685
5723
  if (!this.tags) {
5686
5724
  this.tags = new WeakMap();
5687
5725
  }
5688
- let tags;
5689
- if (!this.tags.has(changeTree)) {
5690
- tags = new Set();
5691
- this.tags.set(changeTree, tags);
5692
- }
5693
- else {
5694
- tags = this.tags.get(changeTree);
5695
- }
5696
- tags.add(tag);
5726
+ // Add tag bits into the bitmask stored for this ChangeTree.
5727
+ const currentMask = this.tags.get(changeTree) ?? 0;
5728
+ this.tags.set(changeTree, currentMask | tag);
5697
5729
  // Ref: add tagged properties
5698
5730
  metadata?.[$fieldIndexesByViewTag]?.[tag]?.forEach((index) => {
5699
5731
  if (changeTree.getChange(index) !== exports.OPERATION.DELETE) {
@@ -5717,7 +5749,7 @@
5717
5749
  if (op !== exports.OPERATION.DELETE &&
5718
5750
  (isInvisible || // if "invisible", include all
5719
5751
  tagAtIndex === undefined || // "all change" with no tag
5720
- tagAtIndex === tag // tagged property
5752
+ (tagAtIndex === DEFAULT_VIEW_TAG || (tag !== DEFAULT_VIEW_TAG && (tagAtIndex & tag) !== 0)) // tagged property
5721
5753
  )) {
5722
5754
  changes[index] = op;
5723
5755
  isChildAdded = true; // FIXME: assign only once
@@ -5743,18 +5775,16 @@
5743
5775
  // add parent's tag properties
5744
5776
  if (changeTree.getChange(parentIndex) !== exports.OPERATION.DELETE) {
5745
5777
  const changes = this.touchChanges(changeTree.ref[$refId]);
5746
- if (!this.tags) {
5747
- this.tags = new WeakMap();
5748
- }
5749
- let tags;
5750
- if (!this.tags.has(changeTree)) {
5751
- tags = new Set();
5752
- this.tags.set(changeTree, tags);
5753
- }
5754
- else {
5755
- tags = this.tags.get(changeTree);
5778
+ // Only accumulate positive (custom) tags in the bitmask.
5779
+ // DEFAULT_VIEW_TAG = -1 has all bits set and must not be OR'd in,
5780
+ // as it would make every custom-tagged field appear visible.
5781
+ if (tag !== DEFAULT_VIEW_TAG) {
5782
+ if (!this.tags) {
5783
+ this.tags = new WeakMap();
5784
+ }
5785
+ const currentMask = this.tags.has(changeTree) ? this.tags.get(changeTree) : 0;
5786
+ this.tags.set(changeTree, currentMask | tag);
5756
5787
  }
5757
- tags.add(tag);
5758
5788
  changes[parentIndex] = exports.OPERATION.ADD;
5759
5789
  }
5760
5790
  }
@@ -5824,18 +5854,19 @@
5824
5854
  }
5825
5855
  // remove tag
5826
5856
  if (this.tags && this.tags.has(changeTree)) {
5827
- const tags = this.tags.get(changeTree);
5828
5857
  if (tag === undefined) {
5829
5858
  // delete all tags
5830
5859
  this.tags.delete(changeTree);
5831
5860
  }
5832
5861
  else {
5833
- // delete specific tag
5834
- tags.delete(tag);
5835
- // if tag set is empty, delete it entirely
5836
- if (tags.size === 0) {
5862
+ // clear the tag's bits from the bitmask
5863
+ const newMask = this.tags.get(changeTree) & ~tag;
5864
+ if (newMask === 0) {
5837
5865
  this.tags.delete(changeTree);
5838
5866
  }
5867
+ else {
5868
+ this.tags.set(changeTree, newMask);
5869
+ }
5839
5870
  }
5840
5871
  }
5841
5872
  return this;
@@ -5845,7 +5876,7 @@
5845
5876
  }
5846
5877
  hasTag(ob, tag = DEFAULT_VIEW_TAG) {
5847
5878
  const tags = this.tags?.get(ob[$changes]);
5848
- return tags?.has(tag) ?? false;
5879
+ return tags != null && (tags & tag) !== 0;
5849
5880
  }
5850
5881
  clear() {
5851
5882
  if (!this.iterable) {
package/build/index.mjs CHANGED
@@ -815,10 +815,25 @@ const Metadata = {
815
815
  });
816
816
  }
817
817
  metadata[$viewFieldIndexes].push(index);
818
- if (!metadata[$fieldIndexesByViewTag][tag]) {
819
- metadata[$fieldIndexesByViewTag][tag] = [];
818
+ // Populate $fieldIndexesByViewTag: for a bitmask tag, register the field
819
+ // index under each individual set bit so that view.add(obj, Tag.ONE) finds
820
+ // fields tagged @view(Tag.ONE|Tag.TWO).
821
+ // Negative tags (i.e. DEFAULT_VIEW_TAG = -1) are stored as-is.
822
+ if (tag < 0) {
823
+ if (!metadata[$fieldIndexesByViewTag][tag]) {
824
+ metadata[$fieldIndexesByViewTag][tag] = [];
825
+ }
826
+ metadata[$fieldIndexesByViewTag][tag].push(index);
827
+ }
828
+ else {
829
+ for (let bits = tag; bits > 0; bits &= bits - 1) {
830
+ const bit = bits & (-bits); // isolate lowest set bit
831
+ if (!metadata[$fieldIndexesByViewTag][bit]) {
832
+ metadata[$fieldIndexesByViewTag][bit] = [];
833
+ }
834
+ metadata[$fieldIndexesByViewTag][bit].push(index);
835
+ }
820
836
  }
821
- metadata[$fieldIndexesByViewTag][tag].push(index);
822
837
  },
823
838
  setFields(target, fields) {
824
839
  // for inheritance support
@@ -1674,17 +1689,25 @@ function decodeValue(decoder, operation, ref, index, type, bytes, it, allChanges
1674
1689
  if (previousValue) {
1675
1690
  let previousRefId = previousValue[$refId];
1676
1691
  if (previousRefId !== undefined && refId !== previousRefId) {
1692
+ // Collection field replaced by a different instance.
1677
1693
  //
1678
- // enqueue onRemove if structure has been replaced.
1679
- //
1694
+ // Don't decrement children here: GC (`garbageCollectDeletedRefs`)
1695
+ // removes them once the previous collection's refId hits zero.
1696
+ // Doing it here too would double-decrement a *shared* child and
1697
+ // drop it while still referenced ("refId not found").
1698
+ if ((operation & OPERATION.DELETE) !== OPERATION.DELETE) {
1699
+ // Replacement not tagged DELETE (e.g. pending ADD not upgraded
1700
+ // to DELETE_AND_ADD), so the previous refId wasn't decremented
1701
+ // above. Release it here, else it never gets GC'd (leak).
1702
+ $root.removeRef(previousRefId);
1703
+ }
1704
+ // enqueue onRemove callbacks for the previous collection's children.
1680
1705
  const entries = previousValue.entries();
1681
1706
  let iter;
1682
1707
  while ((iter = entries.next()) && !iter.done) {
1683
1708
  const [key, value] = iter.value;
1684
- // if value is a schema, remove its reference
1685
1709
  if (typeof (value) === "object") {
1686
1710
  previousRefId = value[$refId];
1687
- $root.removeRef(previousRefId);
1688
1711
  }
1689
1712
  allChanges.push({
1690
1713
  ref: previousValue,
@@ -3762,9 +3785,10 @@ class Schema {
3762
3785
  return view.isChangeTreeVisible(ref[$changes]);
3763
3786
  }
3764
3787
  else {
3765
- // view pass: custom tag
3788
+ // view pass: custom tag (bitmask)
3789
+ // tag is the field's stored bitmask; view.tags stores the accumulated bitmask of tags used in view.add().
3766
3790
  const tags = view.tags?.get(ref[$changes]);
3767
- return tags && tags.has(tag);
3791
+ return tags != null && (tag & tags) !== 0;
3768
3792
  }
3769
3793
  }
3770
3794
  // allow inherited classes to have a constructor
@@ -4374,41 +4398,43 @@ class Encoder {
4374
4398
  encoder(this, buffer, changeTree, fieldIndex, operation, it, isEncodeAll, hasView, metadata);
4375
4399
  }
4376
4400
  }
4377
- if (it.offset > buffer.byteLength) {
4378
- // we can assume that n + 1 BUFFER_SIZE will suffice given that we are likely done with encoding at this point
4379
- // multiples of BUFFER_SIZE are faster to allocate than arbitrary sizes
4380
- const newSize = Math.ceil(it.offset / Encoder.BUFFER_SIZE) * Encoder.BUFFER_SIZE;
4381
- console.warn(`@colyseus/schema buffer overflow. Encoded state is higher than default BUFFER_SIZE. Use the following to increase default BUFFER_SIZE:
4401
+ if (it.offset <= buffer.byteLength) {
4402
+ return buffer.subarray(0, it.offset);
4403
+ }
4404
+ // Overflowed: grow and re-encode. Reuse the same iterator so `it.offset`
4405
+ // ends accurate a fresh one strands it at the overflow value and
4406
+ // corrupts the next view's `viewOffset` in a multi-view encode.
4407
+ buffer = this.ensureCapacity(buffer, it.offset);
4408
+ console.warn(`@colyseus/schema buffer overflow. Encoded state is higher than default BUFFER_SIZE. Use the following to increase default BUFFER_SIZE:
4382
4409
 
4383
4410
  import { Encoder } from "@colyseus/schema";
4384
- Encoder.BUFFER_SIZE = ${Math.round(newSize / 1024)} * 1024; // ${Math.round(newSize / 1024)} KB
4411
+ Encoder.BUFFER_SIZE = ${Math.round(buffer.byteLength / 1024)} * 1024; // ${Math.round(buffer.byteLength / 1024)} KB
4385
4412
  `);
4386
- //
4387
- // resize buffer and re-encode (TODO: can we avoid re-encoding here?)
4388
- // -> No we probably can't unless we catch the need for resize before encoding which is likely more computationally expensive than resizing on demand
4389
- //
4390
- const newBuffer = new Uint8Array(newSize);
4391
- newBuffer.set(buffer); // copy previous encoding steps beyond the initialOffset
4392
- buffer = newBuffer;
4393
- // assign resized buffer to local sharedBuffer
4394
- if (buffer === this.sharedBuffer) {
4395
- this.sharedBuffer = buffer;
4396
- }
4397
- return this.encode({ offset: initialOffset }, view, buffer, changeSetName, isEncodeAll);
4398
- }
4399
- else {
4400
- return buffer.subarray(0, it.offset);
4401
- }
4413
+ it.offset = initialOffset;
4414
+ return this.encode(it, view, buffer, changeSetName, isEncodeAll);
4402
4415
  }
4403
4416
  encodeAll(it = { offset: 0 }, buffer = this.sharedBuffer) {
4404
4417
  return this.encode(it, undefined, buffer, "allChanges", true);
4405
4418
  }
4406
4419
  encodeAllView(view, sharedOffset, it, bytes = this.sharedBuffer) {
4407
4420
  const viewOffset = it.offset;
4408
- // try to encode "filtered" changes
4409
- this.encode(it, view, bytes, "allFilteredChanges", true, viewOffset);
4421
+ // encode() may reallocate the buffer — keep its return, not the stale `bytes`.
4422
+ bytes = this.encode(it, view, bytes, "allFilteredChanges", true, viewOffset);
4410
4423
  return concatBytes(bytes.subarray(0, sharedOffset), bytes.subarray(viewOffset, it.offset));
4411
4424
  }
4425
+ /** Grow `buffer` to keep BUFFER_SIZE free bytes past `offset`, preserving `[0, offset)`. */
4426
+ ensureCapacity(buffer, offset) {
4427
+ if (offset + Encoder.BUFFER_SIZE <= buffer.byteLength) {
4428
+ return buffer;
4429
+ }
4430
+ const size = Math.ceil((offset + Encoder.BUFFER_SIZE) / Encoder.BUFFER_SIZE) * Encoder.BUFFER_SIZE;
4431
+ const grown = new Uint8Array(size);
4432
+ grown.set(buffer.subarray(0, offset));
4433
+ if (buffer === this.sharedBuffer) {
4434
+ this.sharedBuffer = grown;
4435
+ }
4436
+ return grown;
4437
+ }
4412
4438
  encodeView(view, sharedOffset, it, bytes = this.sharedBuffer) {
4413
4439
  const viewOffset = it.offset;
4414
4440
  //
@@ -4447,6 +4473,9 @@ class Encoder {
4447
4473
  const ctor = ref.constructor;
4448
4474
  const encoder = ctor[$encoder];
4449
4475
  const metadata = ctor[Symbol.metadata];
4476
+ // These writes are unguarded and unrecoverable (view.changes is cleared
4477
+ // below), so unlike encode() they can't re-encode on overflow — grow ahead.
4478
+ bytes = this.ensureCapacity(bytes, it.offset);
4450
4479
  bytes[it.offset++] = SWITCH_TO_STRUCTURE & 255;
4451
4480
  encode.number(bytes, ref[$refId], it);
4452
4481
  for (let i = 0, numChanges = keys.length; i < numChanges; i++) {
@@ -4466,8 +4495,10 @@ class Encoder {
4466
4495
  // clear "view" changes after encoding
4467
4496
  view.changes.clear();
4468
4497
  view.changesOutOfOrder = false;
4469
- // try to encode "filtered" changes
4470
- this.encode(it, view, bytes, "filteredChanges", false, viewOffset);
4498
+ // encode() may reallocate the buffer — keep its return, not the stale `bytes`.
4499
+ // Anchor the re-encode at the current offset (default), not `viewOffset`: a
4500
+ // resize must not clobber the view.changes already written at [viewOffset, ).
4501
+ bytes = this.encode(it, view, bytes, "filteredChanges", false);
4471
4502
  return concatBytes(bytes.subarray(0, sharedOffset), bytes.subarray(viewOffset, it.offset));
4472
4503
  }
4473
4504
  /**
@@ -5578,7 +5609,7 @@ class StateView {
5578
5609
  * List of ChangeTree's that are invisible to this view
5579
5610
  */
5580
5611
  invisible = new WeakSet();
5581
- tags; // TODO: use bit manipulation instead of Set<number> ()
5612
+ tags; // bitmask of tags used to add each ChangeTree
5582
5613
  /**
5583
5614
  * Manual "ADD" operations for changes per ChangeTree, specific to this view.
5584
5615
  * (This is used to force encoding a property, even if it was not changed)
@@ -5666,9 +5697,16 @@ class StateView {
5666
5697
  changeTree.forEachChild((change, index) => {
5667
5698
  // Do not ADD children that don't have the same tag
5668
5699
  if (metadata &&
5669
- metadata[index].tag !== undefined &&
5670
- metadata[index].tag !== tag) {
5671
- return;
5700
+ metadata[index].tag !== undefined) {
5701
+ const fieldTag = metadata[index].tag;
5702
+ // DEFAULT_VIEW_TAG fields are visible to all clients.
5703
+ // Custom-tagged fields are only visible when bits overlap,
5704
+ // and never to default-tag clients.
5705
+ const tagMatch = fieldTag === DEFAULT_VIEW_TAG ||
5706
+ (tag !== DEFAULT_VIEW_TAG && (fieldTag & tag) !== 0);
5707
+ if (!tagMatch) {
5708
+ return;
5709
+ }
5672
5710
  }
5673
5711
  if (this.add(change.ref, tag, false)) {
5674
5712
  isChildAdded = true;
@@ -5679,15 +5717,9 @@ class StateView {
5679
5717
  if (!this.tags) {
5680
5718
  this.tags = new WeakMap();
5681
5719
  }
5682
- let tags;
5683
- if (!this.tags.has(changeTree)) {
5684
- tags = new Set();
5685
- this.tags.set(changeTree, tags);
5686
- }
5687
- else {
5688
- tags = this.tags.get(changeTree);
5689
- }
5690
- tags.add(tag);
5720
+ // Add tag bits into the bitmask stored for this ChangeTree.
5721
+ const currentMask = this.tags.get(changeTree) ?? 0;
5722
+ this.tags.set(changeTree, currentMask | tag);
5691
5723
  // Ref: add tagged properties
5692
5724
  metadata?.[$fieldIndexesByViewTag]?.[tag]?.forEach((index) => {
5693
5725
  if (changeTree.getChange(index) !== OPERATION.DELETE) {
@@ -5711,7 +5743,7 @@ class StateView {
5711
5743
  if (op !== OPERATION.DELETE &&
5712
5744
  (isInvisible || // if "invisible", include all
5713
5745
  tagAtIndex === undefined || // "all change" with no tag
5714
- tagAtIndex === tag // tagged property
5746
+ (tagAtIndex === DEFAULT_VIEW_TAG || (tag !== DEFAULT_VIEW_TAG && (tagAtIndex & tag) !== 0)) // tagged property
5715
5747
  )) {
5716
5748
  changes[index] = op;
5717
5749
  isChildAdded = true; // FIXME: assign only once
@@ -5737,18 +5769,16 @@ class StateView {
5737
5769
  // add parent's tag properties
5738
5770
  if (changeTree.getChange(parentIndex) !== OPERATION.DELETE) {
5739
5771
  const changes = this.touchChanges(changeTree.ref[$refId]);
5740
- if (!this.tags) {
5741
- this.tags = new WeakMap();
5742
- }
5743
- let tags;
5744
- if (!this.tags.has(changeTree)) {
5745
- tags = new Set();
5746
- this.tags.set(changeTree, tags);
5747
- }
5748
- else {
5749
- tags = this.tags.get(changeTree);
5772
+ // Only accumulate positive (custom) tags in the bitmask.
5773
+ // DEFAULT_VIEW_TAG = -1 has all bits set and must not be OR'd in,
5774
+ // as it would make every custom-tagged field appear visible.
5775
+ if (tag !== DEFAULT_VIEW_TAG) {
5776
+ if (!this.tags) {
5777
+ this.tags = new WeakMap();
5778
+ }
5779
+ const currentMask = this.tags.has(changeTree) ? this.tags.get(changeTree) : 0;
5780
+ this.tags.set(changeTree, currentMask | tag);
5750
5781
  }
5751
- tags.add(tag);
5752
5782
  changes[parentIndex] = OPERATION.ADD;
5753
5783
  }
5754
5784
  }
@@ -5818,18 +5848,19 @@ class StateView {
5818
5848
  }
5819
5849
  // remove tag
5820
5850
  if (this.tags && this.tags.has(changeTree)) {
5821
- const tags = this.tags.get(changeTree);
5822
5851
  if (tag === undefined) {
5823
5852
  // delete all tags
5824
5853
  this.tags.delete(changeTree);
5825
5854
  }
5826
5855
  else {
5827
- // delete specific tag
5828
- tags.delete(tag);
5829
- // if tag set is empty, delete it entirely
5830
- if (tags.size === 0) {
5856
+ // clear the tag's bits from the bitmask
5857
+ const newMask = this.tags.get(changeTree) & ~tag;
5858
+ if (newMask === 0) {
5831
5859
  this.tags.delete(changeTree);
5832
5860
  }
5861
+ else {
5862
+ this.tags.set(changeTree, newMask);
5863
+ }
5833
5864
  }
5834
5865
  }
5835
5866
  return this;
@@ -5839,7 +5870,7 @@ class StateView {
5839
5870
  }
5840
5871
  hasTag(ob, tag = DEFAULT_VIEW_TAG) {
5841
5872
  const tags = this.tags?.get(ob[$changes]);
5842
- return tags?.has(tag) ?? false;
5873
+ return tags != null && (tags & tag) !== 0;
5843
5874
  }
5844
5875
  clear() {
5845
5876
  if (!this.iterable) {