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