@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colyseus/schema",
3
- "version": "4.0.24",
3
+ "version": "4.0.26",
4
4
  "description": "Binary state serializer with delta encoding for games",
5
5
  "type": "module",
6
6
  "bin": {
package/src/Metadata.ts CHANGED
@@ -161,11 +161,24 @@ export const Metadata = {
161
161
 
162
162
  metadata[$viewFieldIndexes].push(index);
163
163
 
164
- if (!metadata[$fieldIndexesByViewTag][tag]) {
165
- metadata[$fieldIndexesByViewTag][tag] = [];
164
+ // Populate $fieldIndexesByViewTag: for a bitmask tag, register the field
165
+ // index under each individual set bit so that view.add(obj, Tag.ONE) finds
166
+ // fields tagged @view(Tag.ONE|Tag.TWO).
167
+ // Negative tags (i.e. DEFAULT_VIEW_TAG = -1) are stored as-is.
168
+ if (tag < 0) {
169
+ if (!metadata[$fieldIndexesByViewTag][tag]) {
170
+ metadata[$fieldIndexesByViewTag][tag] = [];
171
+ }
172
+ metadata[$fieldIndexesByViewTag][tag].push(index);
173
+ } else {
174
+ for (let bits = tag; bits > 0; bits &= bits - 1) {
175
+ const bit = bits & (-bits); // isolate lowest set bit
176
+ if (!metadata[$fieldIndexesByViewTag][bit]) {
177
+ metadata[$fieldIndexesByViewTag][bit] = [];
178
+ }
179
+ metadata[$fieldIndexesByViewTag][bit].push(index);
180
+ }
166
181
  }
167
-
168
- metadata[$fieldIndexesByViewTag][tag].push(index);
169
182
  },
170
183
 
171
184
  setFields<T extends { new (...args: any[]): InstanceType<T> } = any>(target: T, fields: { [field in keyof InstanceType<T>]?: DefinitionType }) {
package/src/Schema.ts CHANGED
@@ -85,9 +85,10 @@ export class Schema<C = any> implements IRef {
85
85
  return view.isChangeTreeVisible(ref[$changes]);
86
86
 
87
87
  } else {
88
- // view pass: custom tag
88
+ // view pass: custom tag (bitmask)
89
+ // tag is the field's stored bitmask; view.tags stores the accumulated bitmask of tags used in view.add().
89
90
  const tags = view.tags?.get(ref[$changes]);
90
- return tags && tags.has(tag);
91
+ return tags != null && (tag & tags) !== 0;
91
92
  }
92
93
  }
93
94
 
@@ -108,18 +108,27 @@ export function decodeValue<T extends Ref>(
108
108
  let previousRefId = previousValue[$refId];
109
109
 
110
110
  if (previousRefId !== undefined && refId !== previousRefId) {
111
+ // Collection field replaced by a different instance.
111
112
  //
112
- // enqueue onRemove if structure has been replaced.
113
- //
113
+ // Don't decrement children here: GC (`garbageCollectDeletedRefs`)
114
+ // removes them once the previous collection's refId hits zero.
115
+ // Doing it here too would double-decrement a *shared* child and
116
+ // drop it while still referenced ("refId not found").
117
+ if ((operation & OPERATION.DELETE) !== OPERATION.DELETE) {
118
+ // Replacement not tagged DELETE (e.g. pending ADD not upgraded
119
+ // to DELETE_AND_ADD), so the previous refId wasn't decremented
120
+ // above. Release it here, else it never gets GC'd (leak).
121
+ $root.removeRef(previousRefId);
122
+ }
123
+
124
+ // enqueue onRemove callbacks for the previous collection's children.
114
125
  const entries: IterableIterator<[any, any]> = (previousValue as any).entries();
115
126
  let iter: IteratorResult<[any, any]>;
116
127
  while ((iter = entries.next()) && !iter.done) {
117
128
  const [key, value] = iter.value;
118
129
 
119
- // if value is a schema, remove its reference
120
130
  if (typeof(value) === "object") {
121
131
  previousRefId = value[$refId];
122
- $root.removeRef(previousRefId);
123
132
  }
124
133
 
125
134
  allChanges.push({
@@ -1,4 +1,5 @@
1
1
  import { OPERATION } from "../encoding/spec.js";
2
+ import { DEFAULT_VIEW_TAG } from "../annotations.js";
2
3
  import { Schema } from "../Schema.js";
3
4
  import { $changes, $childType, $decoder, $onEncodeEnd, $encoder, $getByIndex, $refId, $refTypeFieldIndexes, $viewFieldIndexes, type $deleteByIndex } from "../types/symbols.js";
4
5
 
@@ -570,7 +571,8 @@ export class ChangeTree<T extends Ref = any> {
570
571
  }
571
572
  key += `-${parentIndex}`;
572
573
 
573
- const fieldHasViewTag = Metadata.hasViewTagAtIndex(parentConstructor?.[Symbol.metadata], parentIndex);
574
+ const parentMetadata = parentConstructor?.[Symbol.metadata];
575
+ const fieldHasViewTag = Metadata.hasViewTagAtIndex(parentMetadata, parentIndex);
574
576
 
575
577
  this.isFiltered = parent[$changes].isFiltered // in case parent is already filtered
576
578
  || this.root.types.parentFiltered[key]
@@ -581,11 +583,23 @@ export class ChangeTree<T extends Ref = any> {
581
583
  // when it's available, we need to enqueue the "changes" changeset into the "filteredChanges" changeset.
582
584
  //
583
585
  if (this.isFiltered) {
584
-
586
+ //
587
+ // Children of a `@view(N)` collection (non-default tag) inherit
588
+ // visibility from their parent, so items pushed/set after the
589
+ // initial `view.add(state, N)` show up automatically.
590
+ //
591
+ // Default-tag `@view()` collections deliberately keep per-item
592
+ // gating — `view.add(item)` is required to opt each one in.
593
+ //
594
+ // The `parentMetadata[parentIndex].tag` access is safe inside
595
+ // this branch: the OR's short-circuit means we only reach it
596
+ // when `fieldHasViewTag` is true, which guarantees the metadata
597
+ // entry and its `tag` property exist.
598
+ //
585
599
  this.isVisibilitySharedWithParent = (
586
600
  parentChangeTree.isFiltered &&
587
601
  typeof (refType) !== "string" &&
588
- !fieldHasViewTag
602
+ (!fieldHasViewTag || (parentIsCollection && parentMetadata[parentIndex].tag !== DEFAULT_VIEW_TAG))
589
603
  );
590
604
 
591
605
  if (!this.filteredChanges) {
@@ -27,7 +27,7 @@ export class StateView {
27
27
  */
28
28
  invisible: WeakSet<ChangeTree> = new WeakSet<ChangeTree>();
29
29
 
30
- tags?: WeakMap<ChangeTree, Set<number>>; // TODO: use bit manipulation instead of Set<number> ()
30
+ tags?: WeakMap<ChangeTree, number>; // bitmask of tags used to add each ChangeTree
31
31
 
32
32
  /**
33
33
  * Manual "ADD" operations for changes per ChangeTree, specific to this view.
@@ -130,10 +130,18 @@ export class StateView {
130
130
  // Do not ADD children that don't have the same tag
131
131
  if (
132
132
  metadata &&
133
- metadata[index].tag !== undefined &&
134
- metadata[index].tag !== tag
133
+ metadata[index].tag !== undefined
135
134
  ) {
136
- return;
135
+ const fieldTag = metadata[index].tag;
136
+ // DEFAULT_VIEW_TAG fields are visible to all clients.
137
+ // Custom-tagged fields are only visible when bits overlap,
138
+ // and never to default-tag clients.
139
+ const tagMatch = fieldTag === DEFAULT_VIEW_TAG ||
140
+ (tag !== DEFAULT_VIEW_TAG && (fieldTag & tag) !== 0);
141
+
142
+ if (!tagMatch) {
143
+ return;
144
+ }
137
145
  }
138
146
 
139
147
  if (this.add(change.ref, tag, false)) {
@@ -144,16 +152,11 @@ export class StateView {
144
152
  // set tag
145
153
  if (tag !== DEFAULT_VIEW_TAG) {
146
154
  if (!this.tags) {
147
- this.tags = new WeakMap<ChangeTree, Set<number>>();
148
- }
149
- let tags: Set<number>;
150
- if (!this.tags.has(changeTree)) {
151
- tags = new Set<number>();
152
- this.tags.set(changeTree, tags);
153
- } else {
154
- tags = this.tags.get(changeTree);
155
+ this.tags = new WeakMap<ChangeTree, number>();
155
156
  }
156
- tags.add(tag);
157
+ // Add tag bits into the bitmask stored for this ChangeTree.
158
+ const currentMask = this.tags.get(changeTree) ?? 0;
159
+ this.tags.set(changeTree, currentMask | tag);
157
160
 
158
161
  // Ref: add tagged properties
159
162
  metadata?.[$fieldIndexesByViewTag]?.[tag]?.forEach((index) => {
@@ -181,7 +184,7 @@ export class StateView {
181
184
  (
182
185
  isInvisible || // if "invisible", include all
183
186
  tagAtIndex === undefined || // "all change" with no tag
184
- tagAtIndex === tag // tagged property
187
+ (tagAtIndex === DEFAULT_VIEW_TAG || (tag !== DEFAULT_VIEW_TAG && (tagAtIndex & tag) !== 0)) // tagged property
185
188
  )
186
189
  ) {
187
190
  changes[index] = op;
@@ -215,18 +218,16 @@ export class StateView {
215
218
  if (changeTree.getChange(parentIndex) !== OPERATION.DELETE) {
216
219
  const changes = this.touchChanges(changeTree.ref[$refId]);
217
220
 
218
- if (!this.tags) {
219
- this.tags = new WeakMap<ChangeTree, Set<number>>();
220
- }
221
-
222
- let tags: Set<number>;
223
- if (!this.tags.has(changeTree)) {
224
- tags = new Set<number>();
225
- this.tags.set(changeTree, tags);
226
- } else {
227
- tags = this.tags.get(changeTree);
221
+ // Only accumulate positive (custom) tags in the bitmask.
222
+ // DEFAULT_VIEW_TAG = -1 has all bits set and must not be OR'd in,
223
+ // as it would make every custom-tagged field appear visible.
224
+ if (tag !== DEFAULT_VIEW_TAG) {
225
+ if (!this.tags) {
226
+ this.tags = new WeakMap<ChangeTree, number>();
227
+ }
228
+ const currentMask = this.tags.has(changeTree) ? this.tags.get(changeTree) : 0;
229
+ this.tags.set(changeTree, currentMask | tag);
228
230
  }
229
- tags.add(tag);
230
231
 
231
232
  changes[parentIndex] = OPERATION.ADD;
232
233
  }
@@ -313,17 +314,16 @@ export class StateView {
313
314
 
314
315
  // remove tag
315
316
  if (this.tags && this.tags.has(changeTree)) {
316
- const tags = this.tags.get(changeTree);
317
317
  if (tag === undefined) {
318
318
  // delete all tags
319
319
  this.tags.delete(changeTree);
320
320
  } else {
321
- // delete specific tag
322
- tags.delete(tag);
323
-
324
- // if tag set is empty, delete it entirely
325
- if (tags.size === 0) {
321
+ // clear the tag's bits from the bitmask
322
+ const newMask = this.tags.get(changeTree) & ~tag;
323
+ if (newMask === 0) {
326
324
  this.tags.delete(changeTree);
325
+ } else {
326
+ this.tags.set(changeTree, newMask);
327
327
  }
328
328
  }
329
329
  }
@@ -337,7 +337,7 @@ export class StateView {
337
337
 
338
338
  hasTag(ob: Ref, tag: number = DEFAULT_VIEW_TAG) {
339
339
  const tags = this.tags?.get(ob[$changes]);
340
- return tags?.has(tag) ?? false;
340
+ return tags != null && (tags & tag) !== 0;
341
341
  }
342
342
 
343
343
  clear() {