@colyseus/schema 4.0.24 → 4.0.26

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
@@ -1375,7 +1390,8 @@
1375
1390
  key += `-${this.root.types.schemas.get(parentConstructor)}`;
1376
1391
  }
1377
1392
  key += `-${parentIndex}`;
1378
- const fieldHasViewTag = Metadata.hasViewTagAtIndex(parentConstructor?.[Symbol.metadata], parentIndex);
1393
+ const parentMetadata = parentConstructor?.[Symbol.metadata];
1394
+ const fieldHasViewTag = Metadata.hasViewTagAtIndex(parentMetadata, parentIndex);
1379
1395
  this.isFiltered = parent[$changes].isFiltered // in case parent is already filtered
1380
1396
  || this.root.types.parentFiltered[key]
1381
1397
  || fieldHasViewTag;
@@ -1384,9 +1400,22 @@
1384
1400
  // when it's available, we need to enqueue the "changes" changeset into the "filteredChanges" changeset.
1385
1401
  //
1386
1402
  if (this.isFiltered) {
1403
+ //
1404
+ // Children of a `@view(N)` collection (non-default tag) inherit
1405
+ // visibility from their parent, so items pushed/set after the
1406
+ // initial `view.add(state, N)` show up automatically.
1407
+ //
1408
+ // Default-tag `@view()` collections deliberately keep per-item
1409
+ // gating — `view.add(item)` is required to opt each one in.
1410
+ //
1411
+ // The `parentMetadata[parentIndex].tag` access is safe inside
1412
+ // this branch: the OR's short-circuit means we only reach it
1413
+ // when `fieldHasViewTag` is true, which guarantees the metadata
1414
+ // entry and its `tag` property exist.
1415
+ //
1387
1416
  this.isVisibilitySharedWithParent = (parentChangeTree.isFiltered &&
1388
1417
  typeof (refType) !== "string" &&
1389
- !fieldHasViewTag);
1418
+ (!fieldHasViewTag || (parentIsCollection && parentMetadata[parentIndex].tag !== DEFAULT_VIEW_TAG)));
1390
1419
  if (!this.filteredChanges) {
1391
1420
  this.filteredChanges = createChangeSet();
1392
1421
  this.allFilteredChanges = createChangeSet();
@@ -1666,17 +1695,25 @@
1666
1695
  if (previousValue) {
1667
1696
  let previousRefId = previousValue[$refId];
1668
1697
  if (previousRefId !== undefined && refId !== previousRefId) {
1698
+ // Collection field replaced by a different instance.
1669
1699
  //
1670
- // enqueue onRemove if structure has been replaced.
1671
- //
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.
1672
1711
  const entries = previousValue.entries();
1673
1712
  let iter;
1674
1713
  while ((iter = entries.next()) && !iter.done) {
1675
1714
  const [key, value] = iter.value;
1676
- // if value is a schema, remove its reference
1677
1715
  if (typeof (value) === "object") {
1678
1716
  previousRefId = value[$refId];
1679
- $root.removeRef(previousRefId);
1680
1717
  }
1681
1718
  allChanges.push({
1682
1719
  ref: previousValue,
@@ -3754,9 +3791,10 @@
3754
3791
  return view.isChangeTreeVisible(ref[$changes]);
3755
3792
  }
3756
3793
  else {
3757
- // 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().
3758
3796
  const tags = view.tags?.get(ref[$changes]);
3759
- return tags && tags.has(tag);
3797
+ return tags != null && (tag & tags) !== 0;
3760
3798
  }
3761
3799
  }
3762
3800
  // allow inherited classes to have a constructor
@@ -5570,7 +5608,7 @@
5570
5608
  * List of ChangeTree's that are invisible to this view
5571
5609
  */
5572
5610
  invisible = new WeakSet();
5573
- tags; // TODO: use bit manipulation instead of Set<number> ()
5611
+ tags; // bitmask of tags used to add each ChangeTree
5574
5612
  /**
5575
5613
  * Manual "ADD" operations for changes per ChangeTree, specific to this view.
5576
5614
  * (This is used to force encoding a property, even if it was not changed)
@@ -5658,9 +5696,16 @@
5658
5696
  changeTree.forEachChild((change, index) => {
5659
5697
  // Do not ADD children that don't have the same tag
5660
5698
  if (metadata &&
5661
- metadata[index].tag !== undefined &&
5662
- metadata[index].tag !== tag) {
5663
- return;
5699
+ metadata[index].tag !== undefined) {
5700
+ const fieldTag = metadata[index].tag;
5701
+ // DEFAULT_VIEW_TAG fields are visible to all clients.
5702
+ // Custom-tagged fields are only visible when bits overlap,
5703
+ // and never to default-tag clients.
5704
+ const tagMatch = fieldTag === DEFAULT_VIEW_TAG ||
5705
+ (tag !== DEFAULT_VIEW_TAG && (fieldTag & tag) !== 0);
5706
+ if (!tagMatch) {
5707
+ return;
5708
+ }
5664
5709
  }
5665
5710
  if (this.add(change.ref, tag, false)) {
5666
5711
  isChildAdded = true;
@@ -5671,15 +5716,9 @@
5671
5716
  if (!this.tags) {
5672
5717
  this.tags = new WeakMap();
5673
5718
  }
5674
- let tags;
5675
- if (!this.tags.has(changeTree)) {
5676
- tags = new Set();
5677
- this.tags.set(changeTree, tags);
5678
- }
5679
- else {
5680
- tags = this.tags.get(changeTree);
5681
- }
5682
- tags.add(tag);
5719
+ // Add tag bits into the bitmask stored for this ChangeTree.
5720
+ const currentMask = this.tags.get(changeTree) ?? 0;
5721
+ this.tags.set(changeTree, currentMask | tag);
5683
5722
  // Ref: add tagged properties
5684
5723
  metadata?.[$fieldIndexesByViewTag]?.[tag]?.forEach((index) => {
5685
5724
  if (changeTree.getChange(index) !== exports.OPERATION.DELETE) {
@@ -5703,7 +5742,7 @@
5703
5742
  if (op !== exports.OPERATION.DELETE &&
5704
5743
  (isInvisible || // if "invisible", include all
5705
5744
  tagAtIndex === undefined || // "all change" with no tag
5706
- tagAtIndex === tag // tagged property
5745
+ (tagAtIndex === DEFAULT_VIEW_TAG || (tag !== DEFAULT_VIEW_TAG && (tagAtIndex & tag) !== 0)) // tagged property
5707
5746
  )) {
5708
5747
  changes[index] = op;
5709
5748
  isChildAdded = true; // FIXME: assign only once
@@ -5729,18 +5768,16 @@
5729
5768
  // add parent's tag properties
5730
5769
  if (changeTree.getChange(parentIndex) !== exports.OPERATION.DELETE) {
5731
5770
  const changes = this.touchChanges(changeTree.ref[$refId]);
5732
- if (!this.tags) {
5733
- this.tags = new WeakMap();
5734
- }
5735
- let tags;
5736
- if (!this.tags.has(changeTree)) {
5737
- tags = new Set();
5738
- this.tags.set(changeTree, tags);
5739
- }
5740
- else {
5741
- tags = this.tags.get(changeTree);
5771
+ // Only accumulate positive (custom) tags in the bitmask.
5772
+ // DEFAULT_VIEW_TAG = -1 has all bits set and must not be OR'd in,
5773
+ // as it would make every custom-tagged field appear visible.
5774
+ if (tag !== DEFAULT_VIEW_TAG) {
5775
+ if (!this.tags) {
5776
+ this.tags = new WeakMap();
5777
+ }
5778
+ const currentMask = this.tags.has(changeTree) ? this.tags.get(changeTree) : 0;
5779
+ this.tags.set(changeTree, currentMask | tag);
5742
5780
  }
5743
- tags.add(tag);
5744
5781
  changes[parentIndex] = exports.OPERATION.ADD;
5745
5782
  }
5746
5783
  }
@@ -5810,18 +5847,19 @@
5810
5847
  }
5811
5848
  // remove tag
5812
5849
  if (this.tags && this.tags.has(changeTree)) {
5813
- const tags = this.tags.get(changeTree);
5814
5850
  if (tag === undefined) {
5815
5851
  // delete all tags
5816
5852
  this.tags.delete(changeTree);
5817
5853
  }
5818
5854
  else {
5819
- // delete specific tag
5820
- tags.delete(tag);
5821
- // if tag set is empty, delete it entirely
5822
- if (tags.size === 0) {
5855
+ // clear the tag's bits from the bitmask
5856
+ const newMask = this.tags.get(changeTree) & ~tag;
5857
+ if (newMask === 0) {
5823
5858
  this.tags.delete(changeTree);
5824
5859
  }
5860
+ else {
5861
+ this.tags.set(changeTree, newMask);
5862
+ }
5825
5863
  }
5826
5864
  }
5827
5865
  return this;
@@ -5831,7 +5869,7 @@
5831
5869
  }
5832
5870
  hasTag(ob, tag = DEFAULT_VIEW_TAG) {
5833
5871
  const tags = this.tags?.get(ob[$changes]);
5834
- return tags?.has(tag) ?? false;
5872
+ return tags != null && (tags & tag) !== 0;
5835
5873
  }
5836
5874
  clear() {
5837
5875
  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
@@ -1369,7 +1384,8 @@ class ChangeTree {
1369
1384
  key += `-${this.root.types.schemas.get(parentConstructor)}`;
1370
1385
  }
1371
1386
  key += `-${parentIndex}`;
1372
- const fieldHasViewTag = Metadata.hasViewTagAtIndex(parentConstructor?.[Symbol.metadata], parentIndex);
1387
+ const parentMetadata = parentConstructor?.[Symbol.metadata];
1388
+ const fieldHasViewTag = Metadata.hasViewTagAtIndex(parentMetadata, parentIndex);
1373
1389
  this.isFiltered = parent[$changes].isFiltered // in case parent is already filtered
1374
1390
  || this.root.types.parentFiltered[key]
1375
1391
  || fieldHasViewTag;
@@ -1378,9 +1394,22 @@ class ChangeTree {
1378
1394
  // when it's available, we need to enqueue the "changes" changeset into the "filteredChanges" changeset.
1379
1395
  //
1380
1396
  if (this.isFiltered) {
1397
+ //
1398
+ // Children of a `@view(N)` collection (non-default tag) inherit
1399
+ // visibility from their parent, so items pushed/set after the
1400
+ // initial `view.add(state, N)` show up automatically.
1401
+ //
1402
+ // Default-tag `@view()` collections deliberately keep per-item
1403
+ // gating — `view.add(item)` is required to opt each one in.
1404
+ //
1405
+ // The `parentMetadata[parentIndex].tag` access is safe inside
1406
+ // this branch: the OR's short-circuit means we only reach it
1407
+ // when `fieldHasViewTag` is true, which guarantees the metadata
1408
+ // entry and its `tag` property exist.
1409
+ //
1381
1410
  this.isVisibilitySharedWithParent = (parentChangeTree.isFiltered &&
1382
1411
  typeof (refType) !== "string" &&
1383
- !fieldHasViewTag);
1412
+ (!fieldHasViewTag || (parentIsCollection && parentMetadata[parentIndex].tag !== DEFAULT_VIEW_TAG)));
1384
1413
  if (!this.filteredChanges) {
1385
1414
  this.filteredChanges = createChangeSet();
1386
1415
  this.allFilteredChanges = createChangeSet();
@@ -1660,17 +1689,25 @@ function decodeValue(decoder, operation, ref, index, type, bytes, it, allChanges
1660
1689
  if (previousValue) {
1661
1690
  let previousRefId = previousValue[$refId];
1662
1691
  if (previousRefId !== undefined && refId !== previousRefId) {
1692
+ // Collection field replaced by a different instance.
1663
1693
  //
1664
- // enqueue onRemove if structure has been replaced.
1665
- //
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.
1666
1705
  const entries = previousValue.entries();
1667
1706
  let iter;
1668
1707
  while ((iter = entries.next()) && !iter.done) {
1669
1708
  const [key, value] = iter.value;
1670
- // if value is a schema, remove its reference
1671
1709
  if (typeof (value) === "object") {
1672
1710
  previousRefId = value[$refId];
1673
- $root.removeRef(previousRefId);
1674
1711
  }
1675
1712
  allChanges.push({
1676
1713
  ref: previousValue,
@@ -3748,9 +3785,10 @@ class Schema {
3748
3785
  return view.isChangeTreeVisible(ref[$changes]);
3749
3786
  }
3750
3787
  else {
3751
- // 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().
3752
3790
  const tags = view.tags?.get(ref[$changes]);
3753
- return tags && tags.has(tag);
3791
+ return tags != null && (tag & tags) !== 0;
3754
3792
  }
3755
3793
  }
3756
3794
  // allow inherited classes to have a constructor
@@ -5564,7 +5602,7 @@ class StateView {
5564
5602
  * List of ChangeTree's that are invisible to this view
5565
5603
  */
5566
5604
  invisible = new WeakSet();
5567
- tags; // TODO: use bit manipulation instead of Set<number> ()
5605
+ tags; // bitmask of tags used to add each ChangeTree
5568
5606
  /**
5569
5607
  * Manual "ADD" operations for changes per ChangeTree, specific to this view.
5570
5608
  * (This is used to force encoding a property, even if it was not changed)
@@ -5652,9 +5690,16 @@ class StateView {
5652
5690
  changeTree.forEachChild((change, index) => {
5653
5691
  // Do not ADD children that don't have the same tag
5654
5692
  if (metadata &&
5655
- metadata[index].tag !== undefined &&
5656
- metadata[index].tag !== tag) {
5657
- return;
5693
+ metadata[index].tag !== undefined) {
5694
+ const fieldTag = metadata[index].tag;
5695
+ // DEFAULT_VIEW_TAG fields are visible to all clients.
5696
+ // Custom-tagged fields are only visible when bits overlap,
5697
+ // and never to default-tag clients.
5698
+ const tagMatch = fieldTag === DEFAULT_VIEW_TAG ||
5699
+ (tag !== DEFAULT_VIEW_TAG && (fieldTag & tag) !== 0);
5700
+ if (!tagMatch) {
5701
+ return;
5702
+ }
5658
5703
  }
5659
5704
  if (this.add(change.ref, tag, false)) {
5660
5705
  isChildAdded = true;
@@ -5665,15 +5710,9 @@ class StateView {
5665
5710
  if (!this.tags) {
5666
5711
  this.tags = new WeakMap();
5667
5712
  }
5668
- let tags;
5669
- if (!this.tags.has(changeTree)) {
5670
- tags = new Set();
5671
- this.tags.set(changeTree, tags);
5672
- }
5673
- else {
5674
- tags = this.tags.get(changeTree);
5675
- }
5676
- tags.add(tag);
5713
+ // Add tag bits into the bitmask stored for this ChangeTree.
5714
+ const currentMask = this.tags.get(changeTree) ?? 0;
5715
+ this.tags.set(changeTree, currentMask | tag);
5677
5716
  // Ref: add tagged properties
5678
5717
  metadata?.[$fieldIndexesByViewTag]?.[tag]?.forEach((index) => {
5679
5718
  if (changeTree.getChange(index) !== OPERATION.DELETE) {
@@ -5697,7 +5736,7 @@ class StateView {
5697
5736
  if (op !== OPERATION.DELETE &&
5698
5737
  (isInvisible || // if "invisible", include all
5699
5738
  tagAtIndex === undefined || // "all change" with no tag
5700
- tagAtIndex === tag // tagged property
5739
+ (tagAtIndex === DEFAULT_VIEW_TAG || (tag !== DEFAULT_VIEW_TAG && (tagAtIndex & tag) !== 0)) // tagged property
5701
5740
  )) {
5702
5741
  changes[index] = op;
5703
5742
  isChildAdded = true; // FIXME: assign only once
@@ -5723,18 +5762,16 @@ class StateView {
5723
5762
  // add parent's tag properties
5724
5763
  if (changeTree.getChange(parentIndex) !== OPERATION.DELETE) {
5725
5764
  const changes = this.touchChanges(changeTree.ref[$refId]);
5726
- if (!this.tags) {
5727
- this.tags = new WeakMap();
5728
- }
5729
- let tags;
5730
- if (!this.tags.has(changeTree)) {
5731
- tags = new Set();
5732
- this.tags.set(changeTree, tags);
5733
- }
5734
- else {
5735
- tags = this.tags.get(changeTree);
5765
+ // Only accumulate positive (custom) tags in the bitmask.
5766
+ // DEFAULT_VIEW_TAG = -1 has all bits set and must not be OR'd in,
5767
+ // as it would make every custom-tagged field appear visible.
5768
+ if (tag !== DEFAULT_VIEW_TAG) {
5769
+ if (!this.tags) {
5770
+ this.tags = new WeakMap();
5771
+ }
5772
+ const currentMask = this.tags.has(changeTree) ? this.tags.get(changeTree) : 0;
5773
+ this.tags.set(changeTree, currentMask | tag);
5736
5774
  }
5737
- tags.add(tag);
5738
5775
  changes[parentIndex] = OPERATION.ADD;
5739
5776
  }
5740
5777
  }
@@ -5804,18 +5841,19 @@ class StateView {
5804
5841
  }
5805
5842
  // remove tag
5806
5843
  if (this.tags && this.tags.has(changeTree)) {
5807
- const tags = this.tags.get(changeTree);
5808
5844
  if (tag === undefined) {
5809
5845
  // delete all tags
5810
5846
  this.tags.delete(changeTree);
5811
5847
  }
5812
5848
  else {
5813
- // delete specific tag
5814
- tags.delete(tag);
5815
- // if tag set is empty, delete it entirely
5816
- if (tags.size === 0) {
5849
+ // clear the tag's bits from the bitmask
5850
+ const newMask = this.tags.get(changeTree) & ~tag;
5851
+ if (newMask === 0) {
5817
5852
  this.tags.delete(changeTree);
5818
5853
  }
5854
+ else {
5855
+ this.tags.set(changeTree, newMask);
5856
+ }
5819
5857
  }
5820
5858
  }
5821
5859
  return this;
@@ -5825,7 +5863,7 @@ class StateView {
5825
5863
  }
5826
5864
  hasTag(ob, tag = DEFAULT_VIEW_TAG) {
5827
5865
  const tags = this.tags?.get(ob[$changes]);
5828
- return tags?.has(tag) ?? false;
5866
+ return tags != null && (tags & tag) !== 0;
5829
5867
  }
5830
5868
  clear() {
5831
5869
  if (!this.iterable) {