@colyseus/schema 3.0.51 → 3.0.53

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.
@@ -963,27 +963,13 @@ const Metadata = {
963
963
  }
964
964
  };
965
965
 
966
- function createChangeSet() {
967
- return { indexes: {}, operations: [] };
966
+ function createChangeSet(queueRootNode) {
967
+ return { indexes: {}, operations: [], queueRootNode };
968
968
  }
969
969
  // Linked list helper functions
970
970
  function createChangeTreeList() {
971
971
  return { next: undefined, tail: undefined, length: 0 };
972
972
  }
973
- function addToChangeTreeList(list, changeTree) {
974
- const node = { changeTree, next: undefined, prev: undefined };
975
- if (!list.next) {
976
- list.next = node;
977
- list.tail = node;
978
- }
979
- else {
980
- node.prev = list.tail;
981
- list.tail.next = node;
982
- list.tail = node;
983
- }
984
- list.length++;
985
- return node;
986
- }
987
973
  function setOperationAtIndex(changeSet, index) {
988
974
  const operationsIndex = changeSet.indexes[index];
989
975
  if (operationsIndex === undefined) {
@@ -1008,17 +994,6 @@ function deleteOperationAtIndex(changeSet, index) {
1008
994
  changeSet.operations[operationsIndex] = undefined;
1009
995
  delete changeSet.indexes[index];
1010
996
  }
1011
- function enqueueChangeTree(root, changeTree, changeSet, queueRootNode = changeTree[changeSet].queueRootNode) {
1012
- // skip
1013
- if (!root) {
1014
- return;
1015
- }
1016
- if (queueRootNode) ;
1017
- else {
1018
- // Add to linked list if not already present
1019
- changeTree[changeSet].queueRootNode = addToChangeTreeList(root[changeSet], changeTree);
1020
- }
1021
- }
1022
997
  class ChangeTree {
1023
998
  constructor(ref) {
1024
999
  /**
@@ -1124,11 +1099,11 @@ class ChangeTree {
1124
1099
  // this is checked during .encode() time.
1125
1100
  if (this.filteredChanges !== undefined) {
1126
1101
  this.filteredChanges.operations.push(-op);
1127
- enqueueChangeTree(this.root, this, 'filteredChanges');
1102
+ this.root?.enqueueChangeTree(this, 'filteredChanges');
1128
1103
  }
1129
1104
  else {
1130
1105
  this.changes.operations.push(-op);
1131
- enqueueChangeTree(this.root, this, 'changes');
1106
+ this.root?.enqueueChangeTree(this, 'changes');
1132
1107
  }
1133
1108
  }
1134
1109
  change(index, operation = exports.OPERATION.ADD) {
@@ -1152,13 +1127,13 @@ class ChangeTree {
1152
1127
  if (isFiltered) {
1153
1128
  setOperationAtIndex(this.allFilteredChanges, index);
1154
1129
  if (this.root) {
1155
- enqueueChangeTree(this.root, this, 'filteredChanges');
1156
- enqueueChangeTree(this.root, this, 'allFilteredChanges');
1130
+ this.root.enqueueChangeTree(this, 'filteredChanges');
1131
+ this.root.enqueueChangeTree(this, 'allFilteredChanges');
1157
1132
  }
1158
1133
  }
1159
1134
  else {
1160
1135
  setOperationAtIndex(this.allChanges, index);
1161
- enqueueChangeTree(this.root, this, 'changes');
1136
+ this.root?.enqueueChangeTree(this, 'changes');
1162
1137
  }
1163
1138
  }
1164
1139
  shiftChangeIndexes(shiftIndex) {
@@ -1213,12 +1188,12 @@ class ChangeTree {
1213
1188
  if (this.filteredChanges !== undefined) {
1214
1189
  setOperationAtIndex(this.allFilteredChanges, allChangesIndex);
1215
1190
  setOperationAtIndex(this.filteredChanges, index);
1216
- enqueueChangeTree(this.root, this, 'filteredChanges');
1191
+ this.root?.enqueueChangeTree(this, 'filteredChanges');
1217
1192
  }
1218
1193
  else {
1219
1194
  setOperationAtIndex(this.allChanges, allChangesIndex);
1220
1195
  setOperationAtIndex(this.changes, index);
1221
- enqueueChangeTree(this.root, this, 'changes');
1196
+ this.root?.enqueueChangeTree(this, 'changes');
1222
1197
  }
1223
1198
  }
1224
1199
  getType(index) {
@@ -1281,10 +1256,10 @@ class ChangeTree {
1281
1256
  //
1282
1257
  if (this.filteredChanges !== undefined) {
1283
1258
  deleteOperationAtIndex(this.allFilteredChanges, allChangesIndex);
1284
- enqueueChangeTree(this.root, this, 'filteredChanges');
1259
+ this.root?.enqueueChangeTree(this, 'filteredChanges');
1285
1260
  }
1286
1261
  else {
1287
- enqueueChangeTree(this.root, this, 'changes');
1262
+ this.root?.enqueueChangeTree(this, 'changes');
1288
1263
  }
1289
1264
  return previousValue;
1290
1265
  }
@@ -1310,9 +1285,10 @@ class ChangeTree {
1310
1285
  this.filteredChanges = createChangeSet();
1311
1286
  }
1312
1287
  if (discardAll) {
1313
- this.allChanges = createChangeSet();
1288
+ // preserve queueRootNode references
1289
+ this.allChanges = createChangeSet(this.allChanges.queueRootNode);
1314
1290
  if (this.allFilteredChanges !== undefined) {
1315
- this.allFilteredChanges = createChangeSet();
1291
+ this.allFilteredChanges = createChangeSet(this.allFilteredChanges.queueRootNode);
1316
1292
  }
1317
1293
  }
1318
1294
  }
@@ -1343,16 +1319,16 @@ class ChangeTree {
1343
1319
  //
1344
1320
  this._checkFilteredByParent(parent, parentIndex);
1345
1321
  if (this.filteredChanges !== undefined) {
1346
- enqueueChangeTree(this.root, this, 'filteredChanges');
1322
+ this.root?.enqueueChangeTree(this, 'filteredChanges');
1347
1323
  if (isNewChangeTree) {
1348
- enqueueChangeTree(this.root, this, 'allFilteredChanges');
1324
+ this.root?.enqueueChangeTree(this, 'allFilteredChanges');
1349
1325
  }
1350
1326
  }
1351
1327
  }
1352
1328
  if (!this.isFiltered) {
1353
- enqueueChangeTree(this.root, this, 'changes');
1329
+ this.root?.enqueueChangeTree(this, 'changes');
1354
1330
  if (isNewChangeTree) {
1355
- enqueueChangeTree(this.root, this, 'allChanges');
1331
+ this.root?.enqueueChangeTree(this, 'allChanges');
1356
1332
  }
1357
1333
  }
1358
1334
  }
@@ -2209,7 +2185,7 @@ class ArraySchema {
2209
2185
  const allChangesIndex = this.items.findIndex(item => item === this.items[0]);
2210
2186
  changeTree.delete(index, exports.OPERATION.DELETE, allChangesIndex);
2211
2187
  changeTree.shiftAllChangeIndexes(-1, allChangesIndex);
2212
- // this.deletedIndexes[index] = true;
2188
+ this.deletedIndexes[index] = true;
2213
2189
  return this.items.shift();
2214
2190
  }
2215
2191
  /**
@@ -2304,10 +2280,10 @@ class ArraySchema {
2304
2280
  // FIXME: this code block is duplicated on ChangeTree
2305
2281
  //
2306
2282
  if (changeTree.filteredChanges !== undefined) {
2307
- enqueueChangeTree(changeTree.root, changeTree, 'filteredChanges');
2283
+ changeTree.root?.enqueueChangeTree(changeTree, 'filteredChanges');
2308
2284
  }
2309
2285
  else {
2310
- enqueueChangeTree(changeTree.root, changeTree, 'changes');
2286
+ changeTree.root?.enqueueChangeTree(changeTree, 'changes');
2311
2287
  }
2312
2288
  return this.items.splice(start, deleteCount, ...insertItems);
2313
2289
  }
@@ -2722,6 +2698,9 @@ class MapSchema {
2722
2698
  previousValue[$changes].root?.remove(previousValue[$changes]);
2723
2699
  }
2724
2700
  }
2701
+ if (this.deletedItems[index]) {
2702
+ delete this.deletedItems[index];
2703
+ }
2725
2704
  }
2726
2705
  else {
2727
2706
  index = changeTree.indexes[$numFields] ?? 0;
@@ -2841,448 +2820,174 @@ class MapSchema {
2841
2820
  }
2842
2821
  registerType("map", { constructor: MapSchema });
2843
2822
 
2844
- const DEFAULT_VIEW_TAG = -1;
2845
- function entity(constructor) {
2846
- TypeContext.register(constructor);
2847
- return constructor;
2848
- }
2849
- /**
2850
- * [See documentation](https://docs.colyseus.io/state/schema/)
2851
- *
2852
- * Annotate a Schema property to be serializeable.
2853
- * \@type()'d fields are automatically flagged as "dirty" for the next patch.
2854
- *
2855
- * @example Standard usage, with automatic change tracking.
2856
- * ```
2857
- * \@type("string") propertyName: string;
2858
- * ```
2859
- *
2860
- * @example You can provide the "manual" option if you'd like to manually control your patches via .setDirty().
2861
- * ```
2862
- * \@type("string", { manual: true })
2863
- * ```
2864
- */
2865
- // export function type(type: DefinitionType, options?: TypeOptions) {
2866
- // return function ({ get, set }, context: ClassAccessorDecoratorContext): ClassAccessorDecoratorResult<Schema, any> {
2867
- // if (context.kind !== "accessor") {
2868
- // throw new Error("@type() is only supported for class accessor properties");
2869
- // }
2870
- // const field = context.name.toString();
2871
- // //
2872
- // // detect index for this field, considering inheritance
2873
- // //
2874
- // const parent = Object.getPrototypeOf(context.metadata);
2875
- // let fieldIndex: number = context.metadata[$numFields] // current structure already has fields defined
2876
- // ?? (parent && parent[$numFields]) // parent structure has fields defined
2877
- // ?? -1; // no fields defined
2878
- // fieldIndex++;
2879
- // if (
2880
- // !parent && // the parent already initializes the `$changes` property
2881
- // !Metadata.hasFields(context.metadata)
2882
- // ) {
2883
- // context.addInitializer(function (this: Ref) {
2884
- // Object.defineProperty(this, $changes, {
2885
- // value: new ChangeTree(this),
2886
- // enumerable: false,
2887
- // writable: true
2888
- // });
2889
- // });
2890
- // }
2891
- // Metadata.addField(context.metadata, fieldIndex, field, type);
2892
- // const isArray = ArraySchema.is(type);
2893
- // const isMap = !isArray && MapSchema.is(type);
2894
- // // if (options && options.manual) {
2895
- // // // do not declare getter/setter descriptor
2896
- // // definition.descriptors[field] = {
2897
- // // enumerable: true,
2898
- // // configurable: true,
2899
- // // writable: true,
2900
- // // };
2901
- // // return;
2902
- // // }
2903
- // return {
2904
- // init(value) {
2905
- // // TODO: may need to convert ArraySchema/MapSchema here
2906
- // // do not flag change if value is undefined.
2907
- // if (value !== undefined) {
2908
- // this[$changes].change(fieldIndex);
2909
- // // automaticallty transform Array into ArraySchema
2910
- // if (isArray) {
2911
- // if (!(value instanceof ArraySchema)) {
2912
- // value = new ArraySchema(...value);
2913
- // }
2914
- // value[$childType] = Object.values(type)[0];
2915
- // }
2916
- // // automaticallty transform Map into MapSchema
2917
- // if (isMap) {
2918
- // if (!(value instanceof MapSchema)) {
2919
- // value = new MapSchema(value);
2920
- // }
2921
- // value[$childType] = Object.values(type)[0];
2922
- // }
2923
- // // try to turn provided structure into a Proxy
2924
- // if (value['$proxy'] === undefined) {
2925
- // if (isMap) {
2926
- // value = getMapProxy(value);
2927
- // }
2928
- // }
2929
- // }
2930
- // return value;
2931
- // },
2932
- // get() {
2933
- // return get.call(this);
2934
- // },
2935
- // set(value: any) {
2936
- // /**
2937
- // * Create Proxy for array or map items
2938
- // */
2939
- // // skip if value is the same as cached.
2940
- // if (value === get.call(this)) {
2941
- // return;
2942
- // }
2943
- // if (
2944
- // value !== undefined &&
2945
- // value !== null
2946
- // ) {
2947
- // // automaticallty transform Array into ArraySchema
2948
- // if (isArray) {
2949
- // if (!(value instanceof ArraySchema)) {
2950
- // value = new ArraySchema(...value);
2951
- // }
2952
- // value[$childType] = Object.values(type)[0];
2953
- // }
2954
- // // automaticallty transform Map into MapSchema
2955
- // if (isMap) {
2956
- // if (!(value instanceof MapSchema)) {
2957
- // value = new MapSchema(value);
2958
- // }
2959
- // value[$childType] = Object.values(type)[0];
2960
- // }
2961
- // // try to turn provided structure into a Proxy
2962
- // if (value['$proxy'] === undefined) {
2963
- // if (isMap) {
2964
- // value = getMapProxy(value);
2965
- // }
2966
- // }
2967
- // // flag the change for encoding.
2968
- // this[$changes].change(fieldIndex);
2969
- // //
2970
- // // call setParent() recursively for this and its child
2971
- // // structures.
2972
- // //
2973
- // if (value[$changes]) {
2974
- // value[$changes].setParent(
2975
- // this,
2976
- // this[$changes].root,
2977
- // Metadata.getIndex(context.metadata, field),
2978
- // );
2979
- // }
2980
- // } else if (get.call(this)) {
2981
- // //
2982
- // // Setting a field to `null` or `undefined` will delete it.
2983
- // //
2984
- // this[$changes].delete(field);
2985
- // }
2986
- // set.call(this, value);
2987
- // },
2988
- // };
2989
- // }
2990
- // }
2991
- function view(tag = DEFAULT_VIEW_TAG) {
2992
- return function (target, fieldName) {
2993
- const constructor = target.constructor;
2994
- const parentClass = Object.getPrototypeOf(constructor);
2995
- const parentMetadata = parentClass[Symbol.metadata];
2996
- // TODO: use Metadata.initialize()
2997
- const metadata = (constructor[Symbol.metadata] ??= Object.assign({}, constructor[Symbol.metadata], parentMetadata ?? Object.create(null)));
2998
- // const fieldIndex = metadata[fieldName];
2999
- // if (!metadata[fieldIndex]) {
3000
- // //
3001
- // // detect index for this field, considering inheritance
3002
- // //
3003
- // metadata[fieldIndex] = {
3004
- // type: undefined,
3005
- // index: (metadata[$numFields] // current structure already has fields defined
3006
- // ?? (parentMetadata && parentMetadata[$numFields]) // parent structure has fields defined
3007
- // ?? -1) + 1 // no fields defined
3008
- // }
3009
- // }
3010
- Metadata.setTag(metadata, fieldName, tag);
3011
- };
3012
- }
3013
- function type(type, options) {
3014
- return function (target, field) {
3015
- const constructor = target.constructor;
3016
- if (!type) {
3017
- throw new Error(`${constructor.name}: @type() reference provided for "${field}" is undefined. Make sure you don't have any circular dependencies.`);
2823
+ var _a$2, _b$2;
2824
+ class CollectionSchema {
2825
+ static { this[_a$2] = encodeKeyValueOperation; }
2826
+ static { this[_b$2] = decodeKeyValueOperation; }
2827
+ /**
2828
+ * Determine if a property must be filtered.
2829
+ * - If returns false, the property is NOT going to be encoded.
2830
+ * - If returns true, the property is going to be encoded.
2831
+ *
2832
+ * Encoding with "filters" happens in two steps:
2833
+ * - First, the encoder iterates over all "not owned" properties and encodes them.
2834
+ * - Then, the encoder iterates over all "owned" properties per instance and encodes them.
2835
+ */
2836
+ static [(_a$2 = $encoder, _b$2 = $decoder, $filter)](ref, index, view) {
2837
+ return (!view ||
2838
+ typeof (ref[$childType]) === "string" ||
2839
+ view.isChangeTreeVisible((ref[$getByIndex](index) ?? ref.deletedItems[index])[$changes]));
2840
+ }
2841
+ static is(type) {
2842
+ return type['collection'] !== undefined;
2843
+ }
2844
+ constructor(initialValues) {
2845
+ this.$items = new Map();
2846
+ this.$indexes = new Map();
2847
+ this.deletedItems = {};
2848
+ this.$refId = 0;
2849
+ this[$changes] = new ChangeTree(this);
2850
+ this[$changes].indexes = {};
2851
+ if (initialValues) {
2852
+ initialValues.forEach((v) => this.add(v));
3018
2853
  }
3019
- // for inheritance support
3020
- TypeContext.register(constructor);
3021
- const parentClass = Object.getPrototypeOf(constructor);
3022
- const parentMetadata = parentClass[Symbol.metadata];
3023
- const metadata = Metadata.initialize(constructor);
3024
- let fieldIndex = metadata[field];
3025
- /**
3026
- * skip if descriptor already exists for this field (`@deprecated()`)
3027
- */
3028
- if (metadata[fieldIndex] !== undefined) {
3029
- if (metadata[fieldIndex].deprecated) {
3030
- // do not create accessors for deprecated properties.
3031
- return;
3032
- }
3033
- else if (metadata[fieldIndex].type !== undefined) {
3034
- // trying to define same property multiple times across inheritance.
3035
- // https://github.com/colyseus/colyseus-unity3d/issues/131#issuecomment-814308572
3036
- try {
3037
- throw new Error(`@colyseus/schema: Duplicate '${field}' definition on '${constructor.name}'.\nCheck @type() annotation`);
3038
- }
3039
- catch (e) {
3040
- const definitionAtLine = e.stack.split("\n")[4].trim();
3041
- throw new Error(`${e.message} ${definitionAtLine}`);
3042
- }
3043
- }
2854
+ Object.defineProperty(this, $childType, {
2855
+ value: undefined,
2856
+ enumerable: false,
2857
+ writable: true,
2858
+ configurable: true,
2859
+ });
2860
+ }
2861
+ add(value) {
2862
+ // set "index" for reference.
2863
+ const index = this.$refId++;
2864
+ const isRef = (value[$changes]) !== undefined;
2865
+ if (isRef) {
2866
+ value[$changes].setParent(this, this[$changes].root, index);
3044
2867
  }
3045
- else {
3046
- //
3047
- // detect index for this field, considering inheritance
3048
- //
3049
- fieldIndex = metadata[$numFields] // current structure already has fields defined
3050
- ?? (parentMetadata && parentMetadata[$numFields]) // parent structure has fields defined
3051
- ?? -1; // no fields defined
3052
- fieldIndex++;
3053
- }
3054
- if (options && options.manual) {
3055
- Metadata.addField(metadata, fieldIndex, field, type, {
3056
- // do not declare getter/setter descriptor
3057
- enumerable: true,
3058
- configurable: true,
3059
- writable: true,
3060
- });
3061
- }
3062
- else {
3063
- const complexTypeKlass = (Array.isArray(type))
3064
- ? getType("array")
3065
- : (typeof (Object.keys(type)[0]) === "string") && getType(Object.keys(type)[0]);
3066
- const childType = (complexTypeKlass)
3067
- ? Object.values(type)[0]
3068
- : type;
3069
- Metadata.addField(metadata, fieldIndex, field, type, getPropertyDescriptor(`_${field}`, fieldIndex, childType, complexTypeKlass));
3070
- }
3071
- };
3072
- }
3073
- function getPropertyDescriptor(fieldCached, fieldIndex, type, complexTypeKlass) {
3074
- return {
3075
- get: function () { return this[fieldCached]; },
3076
- set: function (value) {
3077
- const previousValue = this[fieldCached] ?? undefined;
3078
- // skip if value is the same as cached.
3079
- if (value === previousValue) {
3080
- return;
3081
- }
3082
- if (value !== undefined &&
3083
- value !== null) {
3084
- if (complexTypeKlass) {
3085
- // automaticallty transform Array into ArraySchema
3086
- if (complexTypeKlass.constructor === ArraySchema && !(value instanceof ArraySchema)) {
3087
- value = new ArraySchema(...value);
3088
- }
3089
- // automaticallty transform Map into MapSchema
3090
- if (complexTypeKlass.constructor === MapSchema && !(value instanceof MapSchema)) {
3091
- value = new MapSchema(value);
3092
- }
3093
- value[$childType] = type;
3094
- }
3095
- else if (typeof (type) !== "string") {
3096
- assertInstanceType(value, type, this, fieldCached.substring(1));
3097
- }
3098
- else {
3099
- assertType(value, type, this, fieldCached.substring(1));
3100
- }
3101
- const changeTree = this[$changes];
3102
- //
3103
- // Replacing existing "ref", remove it from root.
3104
- //
3105
- if (previousValue !== undefined && previousValue[$changes]) {
3106
- changeTree.root?.remove(previousValue[$changes]);
3107
- this.constructor[$track](changeTree, fieldIndex, exports.OPERATION.DELETE_AND_ADD);
3108
- }
3109
- else {
3110
- this.constructor[$track](changeTree, fieldIndex, exports.OPERATION.ADD);
3111
- }
3112
- //
3113
- // call setParent() recursively for this and its child
3114
- // structures.
3115
- //
3116
- value[$changes]?.setParent(this, changeTree.root, fieldIndex);
3117
- }
3118
- else if (previousValue !== undefined) {
3119
- //
3120
- // Setting a field to `null` or `undefined` will delete it.
3121
- //
3122
- this[$changes].delete(fieldIndex);
3123
- }
3124
- this[fieldCached] = value;
3125
- },
3126
- enumerable: true,
3127
- configurable: true
3128
- };
3129
- }
3130
- /**
3131
- * `@deprecated()` flag a field as deprecated.
3132
- * The previous `@type()` annotation should remain along with this one.
3133
- */
3134
- function deprecated(throws = true) {
3135
- return function (klass, field) {
3136
- //
3137
- // FIXME: the following block of code is repeated across `@type()`, `@deprecated()` and `@unreliable()` decorators.
3138
- //
3139
- const constructor = klass.constructor;
3140
- const parentClass = Object.getPrototypeOf(constructor);
3141
- const parentMetadata = parentClass[Symbol.metadata];
3142
- const metadata = (constructor[Symbol.metadata] ??= Object.assign({}, constructor[Symbol.metadata], parentMetadata ?? Object.create(null)));
3143
- const fieldIndex = metadata[field];
3144
- // if (!metadata[field]) {
3145
- // //
3146
- // // detect index for this field, considering inheritance
3147
- // //
3148
- // metadata[field] = {
3149
- // type: undefined,
3150
- // index: (metadata[$numFields] // current structure already has fields defined
3151
- // ?? (parentMetadata && parentMetadata[$numFields]) // parent structure has fields defined
3152
- // ?? -1) + 1 // no fields defined
3153
- // }
3154
- // }
3155
- metadata[fieldIndex].deprecated = true;
3156
- if (throws) {
3157
- metadata[$descriptors] ??= {};
3158
- metadata[$descriptors][field] = {
3159
- get: function () { throw new Error(`${field} is deprecated.`); },
3160
- set: function (value) { },
3161
- enumerable: false,
3162
- configurable: true
3163
- };
3164
- }
3165
- // flag metadata[field] as non-enumerable
3166
- Object.defineProperty(metadata, fieldIndex, {
3167
- value: metadata[fieldIndex],
3168
- enumerable: false,
3169
- configurable: true
3170
- });
3171
- };
3172
- }
3173
- function defineTypes(target, fields, options) {
3174
- for (let field in fields) {
3175
- type(fields[field], options)(target.prototype, field);
2868
+ this[$changes].indexes[index] = index;
2869
+ this.$indexes.set(index, index);
2870
+ this.$items.set(index, value);
2871
+ this[$changes].change(index);
2872
+ return index;
3176
2873
  }
3177
- return target;
3178
- }
3179
- function schema(fieldsAndMethods, name, inherits = Schema) {
3180
- const fields = {};
3181
- const methods = {};
3182
- const defaultValues = {};
3183
- const viewTagFields = {};
3184
- for (let fieldName in fieldsAndMethods) {
3185
- const value = fieldsAndMethods[fieldName];
3186
- if (typeof (value) === "object") {
3187
- if (value['default'] !== undefined) {
3188
- defaultValues[fieldName] = value['default'];
2874
+ at(index) {
2875
+ const key = Array.from(this.$items.keys())[index];
2876
+ return this.$items.get(key);
2877
+ }
2878
+ entries() {
2879
+ return this.$items.entries();
2880
+ }
2881
+ delete(item) {
2882
+ const entries = this.$items.entries();
2883
+ let index;
2884
+ let entry;
2885
+ while (entry = entries.next()) {
2886
+ if (entry.done) {
2887
+ break;
3189
2888
  }
3190
- if (value['view'] !== undefined) {
3191
- viewTagFields[fieldName] = (typeof (value['view']) === "boolean")
3192
- ? DEFAULT_VIEW_TAG
3193
- : value['view'];
2889
+ if (item === entry.value[1]) {
2890
+ index = entry.value[0];
2891
+ break;
3194
2892
  }
3195
- fields[fieldName] = value;
3196
- }
3197
- else if (typeof (value) === "function") {
3198
- methods[fieldName] = value;
3199
2893
  }
3200
- else {
3201
- fields[fieldName] = value;
2894
+ if (index === undefined) {
2895
+ return false;
3202
2896
  }
2897
+ this.deletedItems[index] = this[$changes].delete(index);
2898
+ this.$indexes.delete(index);
2899
+ return this.$items.delete(index);
3203
2900
  }
3204
- const klass = Metadata.setFields(class extends inherits {
3205
- constructor(...args) {
3206
- args[0] = Object.assign({}, defaultValues, args[0]);
3207
- super(...args);
3208
- }
3209
- }, fields);
3210
- for (let fieldName in viewTagFields) {
3211
- view(viewTagFields[fieldName])(klass.prototype, fieldName);
2901
+ clear() {
2902
+ const changeTree = this[$changes];
2903
+ // discard previous operations.
2904
+ changeTree.discard(true);
2905
+ changeTree.indexes = {};
2906
+ // remove children references
2907
+ changeTree.forEachChild((childChangeTree, _) => {
2908
+ changeTree.root?.remove(childChangeTree);
2909
+ });
2910
+ // clear previous indexes
2911
+ this.$indexes.clear();
2912
+ // clear items
2913
+ this.$items.clear();
2914
+ changeTree.operation(exports.OPERATION.CLEAR);
3212
2915
  }
3213
- for (let methodName in methods) {
3214
- klass.prototype[methodName] = methods[methodName];
2916
+ has(value) {
2917
+ return Array.from(this.$items.values()).some((v) => v === value);
3215
2918
  }
3216
- if (name) {
3217
- Object.defineProperty(klass, "name", { value: name });
2919
+ forEach(callbackfn) {
2920
+ this.$items.forEach((value, key, _) => callbackfn(value, key, this));
3218
2921
  }
3219
- klass.extends = (fields, name) => schema(fields, name, klass);
3220
- return klass;
3221
- }
3222
-
3223
- function getIndent(level) {
3224
- return (new Array(level).fill(0)).map((_, i) => (i === level - 1) ? `└─ ` : ` `).join("");
3225
- }
3226
- function dumpChanges(schema) {
3227
- const $root = schema[$changes].root;
3228
- const dump = {
3229
- ops: {},
3230
- refs: []
3231
- };
3232
- // for (const refId in $root.changes) {
3233
- let current = $root.changes.next;
3234
- while (current) {
3235
- const changeTree = current.changeTree;
3236
- // skip if ChangeTree is undefined
3237
- if (changeTree === undefined) {
3238
- current = current.next;
3239
- continue;
3240
- }
3241
- const changes = changeTree.indexedOperations;
3242
- dump.refs.push(`refId#${changeTree.refId}`);
3243
- for (const index in changes) {
3244
- const op = changes[index];
3245
- const opName = exports.OPERATION[op];
3246
- if (!dump.ops[opName]) {
3247
- dump.ops[opName] = 0;
3248
- }
3249
- dump.ops[exports.OPERATION[op]]++;
3250
- }
3251
- current = current.next;
2922
+ values() {
2923
+ return this.$items.values();
3252
2924
  }
3253
- return dump;
3254
- }
3255
-
3256
- var _a$2, _b$2;
3257
- /**
3258
- * Schema encoder / decoder
3259
- */
3260
- class Schema {
3261
- static { this[_a$2] = encodeSchemaOperation; }
3262
- static { this[_b$2] = decodeSchemaOperation; }
3263
- /**
3264
- * Assign the property descriptors required to track changes on this instance.
3265
- * @param instance
3266
- */
3267
- static initialize(instance) {
3268
- Object.defineProperty(instance, $changes, {
3269
- value: new ChangeTree(instance),
3270
- enumerable: false,
3271
- writable: true
3272
- });
3273
- Object.defineProperties(instance, instance.constructor[Symbol.metadata]?.[$descriptors] || {});
2925
+ get size() {
2926
+ return this.$items.size;
3274
2927
  }
3275
- static is(type) {
3276
- return typeof (type[Symbol.metadata]) === "object";
3277
- // const metadata = type[Symbol.metadata];
3278
- // return metadata && Object.prototype.hasOwnProperty.call(metadata, -1);
2928
+ /** Iterator */
2929
+ [Symbol.iterator]() {
2930
+ return this.$items.values();
3279
2931
  }
3280
- /**
3281
- * Track property changes
3282
- */
3283
- static [(_a$2 = $encoder, _b$2 = $decoder, $track)](changeTree, index, operation = exports.OPERATION.ADD) {
3284
- changeTree.change(index, operation);
2932
+ setIndex(index, key) {
2933
+ this.$indexes.set(index, key);
2934
+ }
2935
+ getIndex(index) {
2936
+ return this.$indexes.get(index);
2937
+ }
2938
+ [$getByIndex](index) {
2939
+ return this.$items.get(this.$indexes.get(index));
2940
+ }
2941
+ [$deleteByIndex](index) {
2942
+ const key = this.$indexes.get(index);
2943
+ this.$items.delete(key);
2944
+ this.$indexes.delete(index);
2945
+ }
2946
+ [$onEncodeEnd]() {
2947
+ this.deletedItems = {};
2948
+ }
2949
+ toArray() {
2950
+ return Array.from(this.$items.values());
2951
+ }
2952
+ toJSON() {
2953
+ const values = [];
2954
+ this.forEach((value, key) => {
2955
+ values.push((typeof (value['toJSON']) === "function")
2956
+ ? value['toJSON']()
2957
+ : value);
2958
+ });
2959
+ return values;
2960
+ }
2961
+ //
2962
+ // Decoding utilities
2963
+ //
2964
+ clone(isDecoding) {
2965
+ let cloned;
2966
+ if (isDecoding) {
2967
+ // client-side
2968
+ cloned = Object.assign(new CollectionSchema(), this);
2969
+ }
2970
+ else {
2971
+ // server-side
2972
+ cloned = new CollectionSchema();
2973
+ this.forEach((value) => {
2974
+ if (value[$changes]) {
2975
+ cloned.add(value['clone']());
2976
+ }
2977
+ else {
2978
+ cloned.add(value);
2979
+ }
2980
+ });
2981
+ }
2982
+ return cloned;
3285
2983
  }
2984
+ }
2985
+ registerType("collection", { constructor: CollectionSchema, });
2986
+
2987
+ var _a$1, _b$1;
2988
+ class SetSchema {
2989
+ static { this[_a$1] = encodeKeyValueOperation; }
2990
+ static { this[_b$1] = decodeKeyValueOperation; }
3286
2991
  /**
3287
2992
  * Determine if a property must be filtered.
3288
2993
  * - If returns false, the property is NOT going to be encoded.
@@ -3292,418 +2997,652 @@ class Schema {
3292
2997
  * - First, the encoder iterates over all "not owned" properties and encodes them.
3293
2998
  * - Then, the encoder iterates over all "owned" properties per instance and encodes them.
3294
2999
  */
3295
- static [$filter](ref, index, view) {
3296
- const metadata = ref.constructor[Symbol.metadata];
3297
- const tag = metadata[index]?.tag;
3298
- if (view === undefined) {
3299
- // shared pass/encode: encode if doesn't have a tag
3300
- return tag === undefined;
3301
- }
3302
- else if (tag === undefined) {
3303
- // view pass: no tag
3304
- return true;
3305
- }
3306
- else if (tag === DEFAULT_VIEW_TAG) {
3307
- // view pass: default tag
3308
- return view.isChangeTreeVisible(ref[$changes]);
3309
- }
3310
- else {
3311
- // view pass: custom tag
3312
- const tags = view.tags?.get(ref[$changes]);
3313
- return tags && tags.has(tag);
3314
- }
3000
+ static [(_a$1 = $encoder, _b$1 = $decoder, $filter)](ref, index, view) {
3001
+ return (!view ||
3002
+ typeof (ref[$childType]) === "string" ||
3003
+ view.visible.has((ref[$getByIndex](index) ?? ref.deletedItems[index])[$changes]));
3315
3004
  }
3316
- // allow inherited classes to have a constructor
3317
- constructor(...args) {
3318
- //
3319
- // inline
3320
- // Schema.initialize(this);
3321
- //
3322
- Schema.initialize(this);
3323
- //
3324
- // Assign initial values
3325
- //
3326
- if (args[0]) {
3327
- Object.assign(this, args[0]);
3005
+ static is(type) {
3006
+ return type['set'] !== undefined;
3007
+ }
3008
+ constructor(initialValues) {
3009
+ this.$items = new Map();
3010
+ this.$indexes = new Map();
3011
+ this.deletedItems = {};
3012
+ this.$refId = 0;
3013
+ this[$changes] = new ChangeTree(this);
3014
+ this[$changes].indexes = {};
3015
+ if (initialValues) {
3016
+ initialValues.forEach((v) => this.add(v));
3328
3017
  }
3018
+ Object.defineProperty(this, $childType, {
3019
+ value: undefined,
3020
+ enumerable: false,
3021
+ writable: true,
3022
+ configurable: true,
3023
+ });
3329
3024
  }
3330
- assign(props) {
3331
- Object.assign(this, props);
3332
- return this;
3025
+ add(value) {
3026
+ // immediatelly return false if value already added.
3027
+ if (this.has(value)) {
3028
+ return false;
3029
+ }
3030
+ // set "index" for reference.
3031
+ const index = this.$refId++;
3032
+ if ((value[$changes]) !== undefined) {
3033
+ value[$changes].setParent(this, this[$changes].root, index);
3034
+ }
3035
+ const operation = this[$changes].indexes[index]?.op ?? exports.OPERATION.ADD;
3036
+ this[$changes].indexes[index] = index;
3037
+ this.$indexes.set(index, index);
3038
+ this.$items.set(index, value);
3039
+ this[$changes].change(index, operation);
3040
+ return index;
3333
3041
  }
3334
- /**
3335
- * (Server-side): Flag a property to be encoded for the next patch.
3336
- * @param instance Schema instance
3337
- * @param property string representing the property name, or number representing the index of the property.
3338
- * @param operation OPERATION to perform (detected automatically)
3339
- */
3340
- setDirty(property, operation) {
3341
- const metadata = this.constructor[Symbol.metadata];
3342
- this[$changes].change(metadata[metadata[property]].index, operation);
3042
+ entries() {
3043
+ return this.$items.entries();
3343
3044
  }
3344
- clone() {
3345
- const cloned = new (this.constructor);
3346
- const metadata = this.constructor[Symbol.metadata];
3347
- //
3348
- // TODO: clone all properties, not only annotated ones
3349
- //
3350
- // for (const field in this) {
3351
- for (const fieldIndex in metadata) {
3352
- // const field = metadata[metadata[fieldIndex]].name;
3353
- const field = metadata[fieldIndex].name;
3354
- if (typeof (this[field]) === "object" &&
3355
- typeof (this[field]?.clone) === "function") {
3356
- // deep clone
3357
- cloned[field] = this[field].clone();
3045
+ delete(item) {
3046
+ const entries = this.$items.entries();
3047
+ let index;
3048
+ let entry;
3049
+ while (entry = entries.next()) {
3050
+ if (entry.done) {
3051
+ break;
3358
3052
  }
3359
- else {
3360
- // primitive values
3361
- cloned[field] = this[field];
3053
+ if (item === entry.value[1]) {
3054
+ index = entry.value[0];
3055
+ break;
3362
3056
  }
3363
3057
  }
3364
- return cloned;
3058
+ if (index === undefined) {
3059
+ return false;
3060
+ }
3061
+ this.deletedItems[index] = this[$changes].delete(index);
3062
+ this.$indexes.delete(index);
3063
+ return this.$items.delete(index);
3365
3064
  }
3366
- toJSON() {
3367
- const obj = {};
3368
- const metadata = this.constructor[Symbol.metadata];
3369
- for (const index in metadata) {
3370
- const field = metadata[index];
3371
- const fieldName = field.name;
3372
- if (!field.deprecated && this[fieldName] !== null && typeof (this[fieldName]) !== "undefined") {
3373
- obj[fieldName] = (typeof (this[fieldName]['toJSON']) === "function")
3374
- ? this[fieldName]['toJSON']()
3375
- : this[fieldName];
3065
+ clear() {
3066
+ const changeTree = this[$changes];
3067
+ // discard previous operations.
3068
+ changeTree.discard(true);
3069
+ changeTree.indexes = {};
3070
+ // clear previous indexes
3071
+ this.$indexes.clear();
3072
+ // clear items
3073
+ this.$items.clear();
3074
+ changeTree.operation(exports.OPERATION.CLEAR);
3075
+ }
3076
+ has(value) {
3077
+ const values = this.$items.values();
3078
+ let has = false;
3079
+ let entry;
3080
+ while (entry = values.next()) {
3081
+ if (entry.done) {
3082
+ break;
3083
+ }
3084
+ if (value === entry.value) {
3085
+ has = true;
3086
+ break;
3376
3087
  }
3377
3088
  }
3378
- return obj;
3089
+ return has;
3379
3090
  }
3380
- /**
3381
- * Used in tests only
3382
- * @internal
3383
- */
3384
- discardAllChanges() {
3385
- this[$changes].discardAll();
3091
+ forEach(callbackfn) {
3092
+ this.$items.forEach((value, key, _) => callbackfn(value, key, this));
3093
+ }
3094
+ values() {
3095
+ return this.$items.values();
3096
+ }
3097
+ get size() {
3098
+ return this.$items.size;
3099
+ }
3100
+ /** Iterator */
3101
+ [Symbol.iterator]() {
3102
+ return this.$items.values();
3103
+ }
3104
+ setIndex(index, key) {
3105
+ this.$indexes.set(index, key);
3106
+ }
3107
+ getIndex(index) {
3108
+ return this.$indexes.get(index);
3386
3109
  }
3387
3110
  [$getByIndex](index) {
3388
- const metadata = this.constructor[Symbol.metadata];
3389
- return this[metadata[index].name];
3111
+ return this.$items.get(this.$indexes.get(index));
3390
3112
  }
3391
3113
  [$deleteByIndex](index) {
3392
- const metadata = this.constructor[Symbol.metadata];
3393
- this[metadata[index].name] = undefined;
3114
+ const key = this.$indexes.get(index);
3115
+ this.$items.delete(key);
3116
+ this.$indexes.delete(index);
3394
3117
  }
3395
- /**
3396
- * Inspect the `refId` of all Schema instances in the tree. Optionally display the contents of the instance.
3397
- *
3398
- * @param ref Schema instance
3399
- * @param showContents display JSON contents of the instance
3400
- * @returns
3401
- */
3402
- static debugRefIds(ref, showContents = false, level = 0, decoder, keyPrefix = "") {
3403
- const contents = (showContents) ? ` - ${JSON.stringify(ref.toJSON())}` : "";
3404
- const changeTree = ref[$changes];
3405
- const refId = (decoder) ? decoder.root.refIds.get(ref) : changeTree.refId;
3406
- const root = (decoder) ? decoder.root : changeTree.root;
3407
- // log reference count if > 1
3408
- const refCount = (root?.refCount?.[refId] > 1)
3409
- ? ` [×${root.refCount[refId]}]`
3410
- : '';
3411
- let output = `${getIndent(level)}${keyPrefix}${ref.constructor.name} (refId: ${refId})${refCount}${contents}\n`;
3412
- changeTree.forEachChild((childChangeTree, indexOrKey) => {
3413
- let key = indexOrKey;
3414
- if (typeof indexOrKey === 'number' && ref['$indexes']) {
3415
- // MapSchema
3416
- key = ref['$indexes'].get(indexOrKey) ?? indexOrKey;
3417
- }
3418
- const keyPrefix = (ref['forEach'] !== undefined && key !== undefined) ? `["${key}"]: ` : "";
3419
- output += this.debugRefIds(childChangeTree.ref, showContents, level + 1, decoder, keyPrefix);
3420
- });
3421
- return output;
3118
+ [$onEncodeEnd]() {
3119
+ this.deletedItems = {};
3422
3120
  }
3423
- static debugRefIdEncodingOrder(ref, changeSet = 'allChanges') {
3424
- let encodeOrder = [];
3425
- let current = ref[$changes].root[changeSet].next;
3426
- while (current) {
3427
- if (current.changeTree) {
3428
- encodeOrder.push(current.changeTree.refId);
3429
- }
3430
- current = current.next;
3431
- }
3432
- return encodeOrder;
3121
+ toArray() {
3122
+ return Array.from(this.$items.values());
3433
3123
  }
3434
- static debugRefIdsFromDecoder(decoder) {
3435
- return this.debugRefIds(decoder.state, false, 0, decoder);
3124
+ toJSON() {
3125
+ const values = [];
3126
+ this.forEach((value, key) => {
3127
+ values.push((typeof (value['toJSON']) === "function")
3128
+ ? value['toJSON']()
3129
+ : value);
3130
+ });
3131
+ return values;
3436
3132
  }
3437
- /**
3438
- * Return a string representation of the changes on a Schema instance.
3439
- * The list of changes is cleared after each encode.
3440
- *
3441
- * @param instance Schema instance
3442
- * @param isEncodeAll Return "full encode" instead of current change set.
3443
- * @returns
3444
- */
3445
- static debugChanges(instance, isEncodeAll = false) {
3446
- const changeTree = instance[$changes];
3447
- const changeSet = (isEncodeAll) ? changeTree.allChanges : changeTree.changes;
3448
- const changeSetName = (isEncodeAll) ? "allChanges" : "changes";
3449
- let output = `${instance.constructor.name} (${changeTree.refId}) -> .${changeSetName}:\n`;
3450
- function dumpChangeSet(changeSet) {
3451
- changeSet.operations
3452
- .filter(op => op)
3453
- .forEach((index) => {
3454
- const operation = changeTree.indexedOperations[index];
3455
- console.log({ index, operation });
3456
- output += `- [${index}]: ${exports.OPERATION[operation]} (${JSON.stringify(changeTree.getValue(Number(index), isEncodeAll))})\n`;
3133
+ //
3134
+ // Decoding utilities
3135
+ //
3136
+ clone(isDecoding) {
3137
+ let cloned;
3138
+ if (isDecoding) {
3139
+ // client-side
3140
+ cloned = Object.assign(new SetSchema(), this);
3141
+ }
3142
+ else {
3143
+ // server-side
3144
+ cloned = new SetSchema();
3145
+ this.forEach((value) => {
3146
+ if (value[$changes]) {
3147
+ cloned.add(value['clone']());
3148
+ }
3149
+ else {
3150
+ cloned.add(value);
3151
+ }
3457
3152
  });
3458
3153
  }
3459
- dumpChangeSet(changeSet);
3460
- // display filtered changes
3461
- if (!isEncodeAll &&
3462
- changeTree.filteredChanges &&
3463
- (changeTree.filteredChanges.operations).filter(op => op).length > 0) {
3464
- output += `${instance.constructor.name} (${changeTree.refId}) -> .filteredChanges:\n`;
3465
- dumpChangeSet(changeTree.filteredChanges);
3154
+ return cloned;
3155
+ }
3156
+ }
3157
+ registerType("set", { constructor: SetSchema });
3158
+
3159
+ const DEFAULT_VIEW_TAG = -1;
3160
+ function entity(constructor) {
3161
+ TypeContext.register(constructor);
3162
+ return constructor;
3163
+ }
3164
+ /**
3165
+ * [See documentation](https://docs.colyseus.io/state/schema/)
3166
+ *
3167
+ * Annotate a Schema property to be serializeable.
3168
+ * \@type()'d fields are automatically flagged as "dirty" for the next patch.
3169
+ *
3170
+ * @example Standard usage, with automatic change tracking.
3171
+ * ```
3172
+ * \@type("string") propertyName: string;
3173
+ * ```
3174
+ *
3175
+ * @example You can provide the "manual" option if you'd like to manually control your patches via .setDirty().
3176
+ * ```
3177
+ * \@type("string", { manual: true })
3178
+ * ```
3179
+ */
3180
+ // export function type(type: DefinitionType, options?: TypeOptions) {
3181
+ // return function ({ get, set }, context: ClassAccessorDecoratorContext): ClassAccessorDecoratorResult<Schema, any> {
3182
+ // if (context.kind !== "accessor") {
3183
+ // throw new Error("@type() is only supported for class accessor properties");
3184
+ // }
3185
+ // const field = context.name.toString();
3186
+ // //
3187
+ // // detect index for this field, considering inheritance
3188
+ // //
3189
+ // const parent = Object.getPrototypeOf(context.metadata);
3190
+ // let fieldIndex: number = context.metadata[$numFields] // current structure already has fields defined
3191
+ // ?? (parent && parent[$numFields]) // parent structure has fields defined
3192
+ // ?? -1; // no fields defined
3193
+ // fieldIndex++;
3194
+ // if (
3195
+ // !parent && // the parent already initializes the `$changes` property
3196
+ // !Metadata.hasFields(context.metadata)
3197
+ // ) {
3198
+ // context.addInitializer(function (this: Ref) {
3199
+ // Object.defineProperty(this, $changes, {
3200
+ // value: new ChangeTree(this),
3201
+ // enumerable: false,
3202
+ // writable: true
3203
+ // });
3204
+ // });
3205
+ // }
3206
+ // Metadata.addField(context.metadata, fieldIndex, field, type);
3207
+ // const isArray = ArraySchema.is(type);
3208
+ // const isMap = !isArray && MapSchema.is(type);
3209
+ // // if (options && options.manual) {
3210
+ // // // do not declare getter/setter descriptor
3211
+ // // definition.descriptors[field] = {
3212
+ // // enumerable: true,
3213
+ // // configurable: true,
3214
+ // // writable: true,
3215
+ // // };
3216
+ // // return;
3217
+ // // }
3218
+ // return {
3219
+ // init(value) {
3220
+ // // TODO: may need to convert ArraySchema/MapSchema here
3221
+ // // do not flag change if value is undefined.
3222
+ // if (value !== undefined) {
3223
+ // this[$changes].change(fieldIndex);
3224
+ // // automaticallty transform Array into ArraySchema
3225
+ // if (isArray) {
3226
+ // if (!(value instanceof ArraySchema)) {
3227
+ // value = new ArraySchema(...value);
3228
+ // }
3229
+ // value[$childType] = Object.values(type)[0];
3230
+ // }
3231
+ // // automaticallty transform Map into MapSchema
3232
+ // if (isMap) {
3233
+ // if (!(value instanceof MapSchema)) {
3234
+ // value = new MapSchema(value);
3235
+ // }
3236
+ // value[$childType] = Object.values(type)[0];
3237
+ // }
3238
+ // // try to turn provided structure into a Proxy
3239
+ // if (value['$proxy'] === undefined) {
3240
+ // if (isMap) {
3241
+ // value = getMapProxy(value);
3242
+ // }
3243
+ // }
3244
+ // }
3245
+ // return value;
3246
+ // },
3247
+ // get() {
3248
+ // return get.call(this);
3249
+ // },
3250
+ // set(value: any) {
3251
+ // /**
3252
+ // * Create Proxy for array or map items
3253
+ // */
3254
+ // // skip if value is the same as cached.
3255
+ // if (value === get.call(this)) {
3256
+ // return;
3257
+ // }
3258
+ // if (
3259
+ // value !== undefined &&
3260
+ // value !== null
3261
+ // ) {
3262
+ // // automaticallty transform Array into ArraySchema
3263
+ // if (isArray) {
3264
+ // if (!(value instanceof ArraySchema)) {
3265
+ // value = new ArraySchema(...value);
3266
+ // }
3267
+ // value[$childType] = Object.values(type)[0];
3268
+ // }
3269
+ // // automaticallty transform Map into MapSchema
3270
+ // if (isMap) {
3271
+ // if (!(value instanceof MapSchema)) {
3272
+ // value = new MapSchema(value);
3273
+ // }
3274
+ // value[$childType] = Object.values(type)[0];
3275
+ // }
3276
+ // // try to turn provided structure into a Proxy
3277
+ // if (value['$proxy'] === undefined) {
3278
+ // if (isMap) {
3279
+ // value = getMapProxy(value);
3280
+ // }
3281
+ // }
3282
+ // // flag the change for encoding.
3283
+ // this[$changes].change(fieldIndex);
3284
+ // //
3285
+ // // call setParent() recursively for this and its child
3286
+ // // structures.
3287
+ // //
3288
+ // if (value[$changes]) {
3289
+ // value[$changes].setParent(
3290
+ // this,
3291
+ // this[$changes].root,
3292
+ // Metadata.getIndex(context.metadata, field),
3293
+ // );
3294
+ // }
3295
+ // } else if (get.call(this)) {
3296
+ // //
3297
+ // // Setting a field to `null` or `undefined` will delete it.
3298
+ // //
3299
+ // this[$changes].delete(field);
3300
+ // }
3301
+ // set.call(this, value);
3302
+ // },
3303
+ // };
3304
+ // }
3305
+ // }
3306
+ function view(tag = DEFAULT_VIEW_TAG) {
3307
+ return function (target, fieldName) {
3308
+ const constructor = target.constructor;
3309
+ const parentClass = Object.getPrototypeOf(constructor);
3310
+ const parentMetadata = parentClass[Symbol.metadata];
3311
+ // TODO: use Metadata.initialize()
3312
+ const metadata = (constructor[Symbol.metadata] ??= Object.assign({}, constructor[Symbol.metadata], parentMetadata ?? Object.create(null)));
3313
+ // const fieldIndex = metadata[fieldName];
3314
+ // if (!metadata[fieldIndex]) {
3315
+ // //
3316
+ // // detect index for this field, considering inheritance
3317
+ // //
3318
+ // metadata[fieldIndex] = {
3319
+ // type: undefined,
3320
+ // index: (metadata[$numFields] // current structure already has fields defined
3321
+ // ?? (parentMetadata && parentMetadata[$numFields]) // parent structure has fields defined
3322
+ // ?? -1) + 1 // no fields defined
3323
+ // }
3324
+ // }
3325
+ Metadata.setTag(metadata, fieldName, tag);
3326
+ };
3327
+ }
3328
+ function type(type, options) {
3329
+ return function (target, field) {
3330
+ const constructor = target.constructor;
3331
+ if (!type) {
3332
+ throw new Error(`${constructor.name}: @type() reference provided for "${field}" is undefined. Make sure you don't have any circular dependencies.`);
3466
3333
  }
3467
- // display filtered changes
3468
- if (isEncodeAll &&
3469
- changeTree.allFilteredChanges &&
3470
- (changeTree.allFilteredChanges.operations).filter(op => op).length > 0) {
3471
- output += `${instance.constructor.name} (${changeTree.refId}) -> .allFilteredChanges:\n`;
3472
- dumpChangeSet(changeTree.allFilteredChanges);
3334
+ // for inheritance support
3335
+ TypeContext.register(constructor);
3336
+ const parentClass = Object.getPrototypeOf(constructor);
3337
+ const parentMetadata = parentClass[Symbol.metadata];
3338
+ const metadata = Metadata.initialize(constructor);
3339
+ let fieldIndex = metadata[field];
3340
+ /**
3341
+ * skip if descriptor already exists for this field (`@deprecated()`)
3342
+ */
3343
+ if (metadata[fieldIndex] !== undefined) {
3344
+ if (metadata[fieldIndex].deprecated) {
3345
+ // do not create accessors for deprecated properties.
3346
+ return;
3347
+ }
3348
+ else if (metadata[fieldIndex].type !== undefined) {
3349
+ // trying to define same property multiple times across inheritance.
3350
+ // https://github.com/colyseus/colyseus-unity3d/issues/131#issuecomment-814308572
3351
+ try {
3352
+ throw new Error(`@colyseus/schema: Duplicate '${field}' definition on '${constructor.name}'.\nCheck @type() annotation`);
3353
+ }
3354
+ catch (e) {
3355
+ const definitionAtLine = e.stack.split("\n")[4].trim();
3356
+ throw new Error(`${e.message} ${definitionAtLine}`);
3357
+ }
3358
+ }
3359
+ }
3360
+ else {
3361
+ //
3362
+ // detect index for this field, considering inheritance
3363
+ //
3364
+ fieldIndex = metadata[$numFields] // current structure already has fields defined
3365
+ ?? (parentMetadata && parentMetadata[$numFields]) // parent structure has fields defined
3366
+ ?? -1; // no fields defined
3367
+ fieldIndex++;
3368
+ }
3369
+ if (options && options.manual) {
3370
+ Metadata.addField(metadata, fieldIndex, field, type, {
3371
+ // do not declare getter/setter descriptor
3372
+ enumerable: true,
3373
+ configurable: true,
3374
+ writable: true,
3375
+ });
3376
+ }
3377
+ else {
3378
+ const complexTypeKlass = (Array.isArray(type))
3379
+ ? getType("array")
3380
+ : (typeof (Object.keys(type)[0]) === "string") && getType(Object.keys(type)[0]);
3381
+ const childType = (complexTypeKlass)
3382
+ ? Object.values(type)[0]
3383
+ : type;
3384
+ Metadata.addField(metadata, fieldIndex, field, type, getPropertyDescriptor(`_${field}`, fieldIndex, childType, complexTypeKlass));
3473
3385
  }
3474
- return output;
3475
- }
3476
- static debugChangesDeep(ref, changeSetName = "changes") {
3477
- let output = "";
3478
- const rootChangeTree = ref[$changes];
3479
- const root = rootChangeTree.root;
3480
- const changeTrees = new Map();
3481
- const instanceRefIds = [];
3482
- let totalOperations = 0;
3483
- // TODO: FIXME: this method is not working as expected
3484
- for (const [refId, changes] of Object.entries(root[changeSetName])) {
3485
- const changeTree = root.changeTrees[refId];
3486
- if (!changeTree) {
3487
- continue;
3488
- }
3489
- let includeChangeTree = false;
3490
- let parentChangeTrees = [];
3491
- let parentChangeTree = changeTree.parent?.[$changes];
3492
- if (changeTree === rootChangeTree) {
3493
- includeChangeTree = true;
3386
+ };
3387
+ }
3388
+ function getPropertyDescriptor(fieldCached, fieldIndex, type, complexTypeKlass) {
3389
+ return {
3390
+ get: function () { return this[fieldCached]; },
3391
+ set: function (value) {
3392
+ const previousValue = this[fieldCached] ?? undefined;
3393
+ // skip if value is the same as cached.
3394
+ if (value === previousValue) {
3395
+ return;
3494
3396
  }
3495
- else {
3496
- while (parentChangeTree !== undefined) {
3497
- parentChangeTrees.push(parentChangeTree);
3498
- if (parentChangeTree.ref === ref) {
3499
- includeChangeTree = true;
3500
- break;
3397
+ if (value !== undefined &&
3398
+ value !== null) {
3399
+ if (complexTypeKlass) {
3400
+ // automaticallty transform Array into ArraySchema
3401
+ if (complexTypeKlass.constructor === ArraySchema && !(value instanceof ArraySchema)) {
3402
+ value = new ArraySchema(...value);
3501
3403
  }
3502
- parentChangeTree = parentChangeTree.parent?.[$changes];
3404
+ // automaticallty transform Map into MapSchema
3405
+ if (complexTypeKlass.constructor === MapSchema && !(value instanceof MapSchema)) {
3406
+ value = new MapSchema(value);
3407
+ }
3408
+ value[$childType] = type;
3503
3409
  }
3504
- }
3505
- if (includeChangeTree) {
3506
- instanceRefIds.push(changeTree.refId);
3507
- totalOperations += Object.keys(changes).length;
3508
- changeTrees.set(changeTree, parentChangeTrees.reverse());
3509
- }
3510
- }
3511
- output += "---\n";
3512
- output += `root refId: ${rootChangeTree.refId}\n`;
3513
- output += `Total instances: ${instanceRefIds.length} (refIds: ${instanceRefIds.join(", ")})\n`;
3514
- output += `Total changes: ${totalOperations}\n`;
3515
- output += "---\n";
3516
- // based on root.changes, display a tree of changes that has the "ref" instance as parent
3517
- const visitedParents = new WeakSet();
3518
- for (const [changeTree, parentChangeTrees] of changeTrees.entries()) {
3519
- parentChangeTrees.forEach((parentChangeTree, level) => {
3520
- if (!visitedParents.has(parentChangeTree)) {
3521
- output += `${getIndent(level)}${parentChangeTree.ref.constructor.name} (refId: ${parentChangeTree.refId})\n`;
3522
- visitedParents.add(parentChangeTree);
3410
+ else if (typeof (type) !== "string") {
3411
+ assertInstanceType(value, type, this, fieldCached.substring(1));
3523
3412
  }
3524
- });
3525
- const changes = changeTree.indexedOperations;
3526
- const level = parentChangeTrees.length;
3527
- const indent = getIndent(level);
3528
- const parentIndex = (level > 0) ? `(${changeTree.parentIndex}) ` : "";
3529
- output += `${indent}${parentIndex}${changeTree.ref.constructor.name} (refId: ${changeTree.refId}) - changes: ${Object.keys(changes).length}\n`;
3530
- for (const index in changes) {
3531
- const operation = changes[index];
3532
- output += `${getIndent(level + 1)}${exports.OPERATION[operation]}: ${index}\n`;
3413
+ else {
3414
+ assertType(value, type, this, fieldCached.substring(1));
3415
+ }
3416
+ const changeTree = this[$changes];
3417
+ //
3418
+ // Replacing existing "ref", remove it from root.
3419
+ //
3420
+ if (previousValue !== undefined && previousValue[$changes]) {
3421
+ changeTree.root?.remove(previousValue[$changes]);
3422
+ this.constructor[$track](changeTree, fieldIndex, exports.OPERATION.DELETE_AND_ADD);
3423
+ }
3424
+ else {
3425
+ this.constructor[$track](changeTree, fieldIndex, exports.OPERATION.ADD);
3426
+ }
3427
+ //
3428
+ // call setParent() recursively for this and its child
3429
+ // structures.
3430
+ //
3431
+ value[$changes]?.setParent(this, changeTree.root, fieldIndex);
3533
3432
  }
3534
- }
3535
- return `${output}`;
3536
- }
3433
+ else if (previousValue !== undefined) {
3434
+ //
3435
+ // Setting a field to `null` or `undefined` will delete it.
3436
+ //
3437
+ this[$changes].delete(fieldIndex);
3438
+ }
3439
+ this[fieldCached] = value;
3440
+ },
3441
+ enumerable: true,
3442
+ configurable: true
3443
+ };
3537
3444
  }
3538
-
3539
- var _a$1, _b$1;
3540
- class CollectionSchema {
3541
- static { this[_a$1] = encodeKeyValueOperation; }
3542
- static { this[_b$1] = decodeKeyValueOperation; }
3543
- /**
3544
- * Determine if a property must be filtered.
3545
- * - If returns false, the property is NOT going to be encoded.
3546
- * - If returns true, the property is going to be encoded.
3547
- *
3548
- * Encoding with "filters" happens in two steps:
3549
- * - First, the encoder iterates over all "not owned" properties and encodes them.
3550
- * - Then, the encoder iterates over all "owned" properties per instance and encodes them.
3551
- */
3552
- static [(_a$1 = $encoder, _b$1 = $decoder, $filter)](ref, index, view) {
3553
- return (!view ||
3554
- typeof (ref[$childType]) === "string" ||
3555
- view.isChangeTreeVisible((ref[$getByIndex](index) ?? ref.deletedItems[index])[$changes]));
3556
- }
3557
- static is(type) {
3558
- return type['collection'] !== undefined;
3559
- }
3560
- constructor(initialValues) {
3561
- this.$items = new Map();
3562
- this.$indexes = new Map();
3563
- this.deletedItems = {};
3564
- this.$refId = 0;
3565
- this[$changes] = new ChangeTree(this);
3566
- this[$changes].indexes = {};
3567
- if (initialValues) {
3568
- initialValues.forEach((v) => this.add(v));
3445
+ /**
3446
+ * `@deprecated()` flag a field as deprecated.
3447
+ * The previous `@type()` annotation should remain along with this one.
3448
+ */
3449
+ function deprecated(throws = true) {
3450
+ return function (klass, field) {
3451
+ //
3452
+ // FIXME: the following block of code is repeated across `@type()`, `@deprecated()` and `@unreliable()` decorators.
3453
+ //
3454
+ const constructor = klass.constructor;
3455
+ const parentClass = Object.getPrototypeOf(constructor);
3456
+ const parentMetadata = parentClass[Symbol.metadata];
3457
+ const metadata = (constructor[Symbol.metadata] ??= Object.assign({}, constructor[Symbol.metadata], parentMetadata ?? Object.create(null)));
3458
+ const fieldIndex = metadata[field];
3459
+ // if (!metadata[field]) {
3460
+ // //
3461
+ // // detect index for this field, considering inheritance
3462
+ // //
3463
+ // metadata[field] = {
3464
+ // type: undefined,
3465
+ // index: (metadata[$numFields] // current structure already has fields defined
3466
+ // ?? (parentMetadata && parentMetadata[$numFields]) // parent structure has fields defined
3467
+ // ?? -1) + 1 // no fields defined
3468
+ // }
3469
+ // }
3470
+ metadata[fieldIndex].deprecated = true;
3471
+ if (throws) {
3472
+ metadata[$descriptors] ??= {};
3473
+ metadata[$descriptors][field] = {
3474
+ get: function () { throw new Error(`${field} is deprecated.`); },
3475
+ set: function (value) { },
3476
+ enumerable: false,
3477
+ configurable: true
3478
+ };
3569
3479
  }
3570
- Object.defineProperty(this, $childType, {
3571
- value: undefined,
3480
+ // flag metadata[field] as non-enumerable
3481
+ Object.defineProperty(metadata, fieldIndex, {
3482
+ value: metadata[fieldIndex],
3572
3483
  enumerable: false,
3573
- writable: true,
3574
- configurable: true,
3484
+ configurable: true
3575
3485
  });
3486
+ };
3487
+ }
3488
+ function defineTypes(target, fields, options) {
3489
+ for (let field in fields) {
3490
+ type(fields[field], options)(target.prototype, field);
3576
3491
  }
3577
- add(value) {
3578
- // set "index" for reference.
3579
- const index = this.$refId++;
3580
- const isRef = (value[$changes]) !== undefined;
3581
- if (isRef) {
3582
- value[$changes].setParent(this, this[$changes].root, index);
3492
+ return target;
3493
+ }
3494
+ function schema(fieldsAndMethods, name, inherits = Schema) {
3495
+ const fields = {};
3496
+ const methods = {};
3497
+ const defaultValues = {};
3498
+ const viewTagFields = {};
3499
+ for (let fieldName in fieldsAndMethods) {
3500
+ const value = fieldsAndMethods[fieldName];
3501
+ if (typeof (value) === "object") {
3502
+ if (value['view'] !== undefined) {
3503
+ viewTagFields[fieldName] = (typeof (value['view']) === "boolean")
3504
+ ? DEFAULT_VIEW_TAG
3505
+ : value['view'];
3506
+ }
3507
+ fields[fieldName] = value;
3508
+ // If no explicit default provided, handle automatic instantiation for collection types
3509
+ if (!Object.prototype.hasOwnProperty.call(value, 'default')) {
3510
+ if (Array.isArray(value) || value['array'] !== undefined) {
3511
+ // Collection: Array → new ArraySchema()
3512
+ defaultValues[fieldName] = new ArraySchema();
3513
+ }
3514
+ else if (value['map'] !== undefined) {
3515
+ // Collection: Map → new MapSchema()
3516
+ defaultValues[fieldName] = new MapSchema();
3517
+ }
3518
+ else if (value['collection'] !== undefined) {
3519
+ // Collection: Collection → new CollectionSchema()
3520
+ defaultValues[fieldName] = new CollectionSchema();
3521
+ }
3522
+ else if (value['set'] !== undefined) {
3523
+ // Collection: Set → new SetSchema()
3524
+ defaultValues[fieldName] = new SetSchema();
3525
+ }
3526
+ else if (value['type'] !== undefined && Schema.is(value['type'])) {
3527
+ // Direct Schema type: Type → new Type()
3528
+ defaultValues[fieldName] = new value['type']();
3529
+ }
3530
+ }
3531
+ else {
3532
+ defaultValues[fieldName] = value['default'];
3533
+ }
3583
3534
  }
3584
- this[$changes].indexes[index] = index;
3585
- this.$indexes.set(index, index);
3586
- this.$items.set(index, value);
3587
- this[$changes].change(index);
3588
- return index;
3589
- }
3590
- at(index) {
3591
- const key = Array.from(this.$items.keys())[index];
3592
- return this.$items.get(key);
3593
- }
3594
- entries() {
3595
- return this.$items.entries();
3596
- }
3597
- delete(item) {
3598
- const entries = this.$items.entries();
3599
- let index;
3600
- let entry;
3601
- while (entry = entries.next()) {
3602
- if (entry.done) {
3603
- break;
3535
+ else if (typeof (value) === "function") {
3536
+ if (Schema.is(value)) {
3537
+ // Direct Schema type: Type → new Type()
3538
+ defaultValues[fieldName] = new value();
3539
+ fields[fieldName] = value;
3540
+ }
3541
+ else {
3542
+ methods[fieldName] = value;
3543
+ }
3544
+ }
3545
+ else {
3546
+ fields[fieldName] = value;
3547
+ }
3548
+ }
3549
+ const getDefaultValues = () => {
3550
+ const defaults = {};
3551
+ for (const fieldName in defaultValues) {
3552
+ const defaultValue = defaultValues[fieldName];
3553
+ // If the default value has a clone method, use it to get a fresh instance
3554
+ if (defaultValue && typeof defaultValue.clone === 'function') {
3555
+ defaults[fieldName] = defaultValue.clone();
3604
3556
  }
3605
- if (item === entry.value[1]) {
3606
- index = entry.value[0];
3607
- break;
3557
+ else {
3558
+ // Otherwise, use the value as-is (for primitives and non-cloneable objects)
3559
+ defaults[fieldName] = defaultValue;
3608
3560
  }
3609
3561
  }
3610
- if (index === undefined) {
3611
- return false;
3562
+ return defaults;
3563
+ };
3564
+ const klass = Metadata.setFields(class extends inherits {
3565
+ constructor(...args) {
3566
+ args[0] = Object.assign({}, getDefaultValues(), args[0]);
3567
+ super(...args);
3612
3568
  }
3613
- this.deletedItems[index] = this[$changes].delete(index);
3614
- this.$indexes.delete(index);
3615
- return this.$items.delete(index);
3616
- }
3617
- clear() {
3618
- const changeTree = this[$changes];
3619
- // discard previous operations.
3620
- changeTree.discard(true);
3621
- changeTree.indexes = {};
3622
- // remove children references
3623
- changeTree.forEachChild((childChangeTree, _) => {
3624
- changeTree.root?.remove(childChangeTree);
3625
- });
3626
- // clear previous indexes
3627
- this.$indexes.clear();
3628
- // clear items
3629
- this.$items.clear();
3630
- changeTree.operation(exports.OPERATION.CLEAR);
3631
- }
3632
- has(value) {
3633
- return Array.from(this.$items.values()).some((v) => v === value);
3634
- }
3635
- forEach(callbackfn) {
3636
- this.$items.forEach((value, key, _) => callbackfn(value, key, this));
3637
- }
3638
- values() {
3639
- return this.$items.values();
3640
- }
3641
- get size() {
3642
- return this.$items.size;
3643
- }
3644
- /** Iterator */
3645
- [Symbol.iterator]() {
3646
- return this.$items.values();
3647
- }
3648
- setIndex(index, key) {
3649
- this.$indexes.set(index, key);
3650
- }
3651
- getIndex(index) {
3652
- return this.$indexes.get(index);
3653
- }
3654
- [$getByIndex](index) {
3655
- return this.$items.get(this.$indexes.get(index));
3656
- }
3657
- [$deleteByIndex](index) {
3658
- const key = this.$indexes.get(index);
3659
- this.$items.delete(key);
3660
- this.$indexes.delete(index);
3661
- }
3662
- [$onEncodeEnd]() {
3663
- this.deletedItems = {};
3569
+ }, fields);
3570
+ for (let fieldName in viewTagFields) {
3571
+ view(viewTagFields[fieldName])(klass.prototype, fieldName);
3664
3572
  }
3665
- toArray() {
3666
- return Array.from(this.$items.values());
3573
+ for (let methodName in methods) {
3574
+ klass.prototype[methodName] = methods[methodName];
3667
3575
  }
3668
- toJSON() {
3669
- const values = [];
3670
- this.forEach((value, key) => {
3671
- values.push((typeof (value['toJSON']) === "function")
3672
- ? value['toJSON']()
3673
- : value);
3674
- });
3675
- return values;
3576
+ if (name) {
3577
+ Object.defineProperty(klass, "name", { value: name });
3676
3578
  }
3677
- //
3678
- // Decoding utilities
3679
- //
3680
- clone(isDecoding) {
3681
- let cloned;
3682
- if (isDecoding) {
3683
- // client-side
3684
- cloned = Object.assign(new CollectionSchema(), this);
3579
+ klass.extends = (fields, name) => schema(fields, name, klass);
3580
+ return klass;
3581
+ }
3582
+
3583
+ function getIndent(level) {
3584
+ return (new Array(level).fill(0)).map((_, i) => (i === level - 1) ? `└─ ` : ` `).join("");
3585
+ }
3586
+ function dumpChanges(schema) {
3587
+ const $root = schema[$changes].root;
3588
+ const dump = {
3589
+ ops: {},
3590
+ refs: []
3591
+ };
3592
+ // for (const refId in $root.changes) {
3593
+ let current = $root.changes.next;
3594
+ while (current) {
3595
+ const changeTree = current.changeTree;
3596
+ // skip if ChangeTree is undefined
3597
+ if (changeTree === undefined) {
3598
+ current = current.next;
3599
+ continue;
3685
3600
  }
3686
- else {
3687
- // server-side
3688
- cloned = new CollectionSchema();
3689
- this.forEach((value) => {
3690
- if (value[$changes]) {
3691
- cloned.add(value['clone']());
3692
- }
3693
- else {
3694
- cloned.add(value);
3695
- }
3696
- });
3601
+ const changes = changeTree.indexedOperations;
3602
+ dump.refs.push(`refId#${changeTree.refId}`);
3603
+ for (const index in changes) {
3604
+ const op = changes[index];
3605
+ const opName = exports.OPERATION[op];
3606
+ if (!dump.ops[opName]) {
3607
+ dump.ops[opName] = 0;
3608
+ }
3609
+ dump.ops[exports.OPERATION[op]]++;
3697
3610
  }
3698
- return cloned;
3611
+ current = current.next;
3699
3612
  }
3613
+ return dump;
3700
3614
  }
3701
- registerType("collection", { constructor: CollectionSchema, });
3702
3615
 
3703
3616
  var _a, _b;
3704
- class SetSchema {
3705
- static { this[_a] = encodeKeyValueOperation; }
3706
- static { this[_b] = decodeKeyValueOperation; }
3617
+ /**
3618
+ * Schema encoder / decoder
3619
+ */
3620
+ class Schema {
3621
+ static { this[_a] = encodeSchemaOperation; }
3622
+ static { this[_b] = decodeSchemaOperation; }
3623
+ /**
3624
+ * Assign the property descriptors required to track changes on this instance.
3625
+ * @param instance
3626
+ */
3627
+ static initialize(instance) {
3628
+ Object.defineProperty(instance, $changes, {
3629
+ value: new ChangeTree(instance),
3630
+ enumerable: false,
3631
+ writable: true
3632
+ });
3633
+ Object.defineProperties(instance, instance.constructor[Symbol.metadata]?.[$descriptors] || {});
3634
+ }
3635
+ static is(type) {
3636
+ return typeof (type[Symbol.metadata]) === "object";
3637
+ // const metadata = type[Symbol.metadata];
3638
+ // return metadata && Object.prototype.hasOwnProperty.call(metadata, -1);
3639
+ }
3640
+ /**
3641
+ * Track property changes
3642
+ */
3643
+ static [(_a = $encoder, _b = $decoder, $track)](changeTree, index, operation = exports.OPERATION.ADD) {
3644
+ changeTree.change(index, operation);
3645
+ }
3707
3646
  /**
3708
3647
  * Determine if a property must be filtered.
3709
3648
  * - If returns false, the property is NOT going to be encoded.
@@ -3713,164 +3652,248 @@ class SetSchema {
3713
3652
  * - First, the encoder iterates over all "not owned" properties and encodes them.
3714
3653
  * - Then, the encoder iterates over all "owned" properties per instance and encodes them.
3715
3654
  */
3716
- static [(_a = $encoder, _b = $decoder, $filter)](ref, index, view) {
3717
- return (!view ||
3718
- typeof (ref[$childType]) === "string" ||
3719
- view.visible.has((ref[$getByIndex](index) ?? ref.deletedItems[index])[$changes]));
3720
- }
3721
- static is(type) {
3722
- return type['set'] !== undefined;
3723
- }
3724
- constructor(initialValues) {
3725
- this.$items = new Map();
3726
- this.$indexes = new Map();
3727
- this.deletedItems = {};
3728
- this.$refId = 0;
3729
- this[$changes] = new ChangeTree(this);
3730
- this[$changes].indexes = {};
3731
- if (initialValues) {
3732
- initialValues.forEach((v) => this.add(v));
3655
+ static [$filter](ref, index, view) {
3656
+ const metadata = ref.constructor[Symbol.metadata];
3657
+ const tag = metadata[index]?.tag;
3658
+ if (view === undefined) {
3659
+ // shared pass/encode: encode if doesn't have a tag
3660
+ return tag === undefined;
3733
3661
  }
3734
- Object.defineProperty(this, $childType, {
3735
- value: undefined,
3736
- enumerable: false,
3737
- writable: true,
3738
- configurable: true,
3739
- });
3740
- }
3741
- add(value) {
3742
- // immediatelly return false if value already added.
3743
- if (this.has(value)) {
3744
- return false;
3662
+ else if (tag === undefined) {
3663
+ // view pass: no tag
3664
+ return true;
3745
3665
  }
3746
- // set "index" for reference.
3747
- const index = this.$refId++;
3748
- if ((value[$changes]) !== undefined) {
3749
- value[$changes].setParent(this, this[$changes].root, index);
3666
+ else if (tag === DEFAULT_VIEW_TAG) {
3667
+ // view pass: default tag
3668
+ return view.isChangeTreeVisible(ref[$changes]);
3669
+ }
3670
+ else {
3671
+ // view pass: custom tag
3672
+ const tags = view.tags?.get(ref[$changes]);
3673
+ return tags && tags.has(tag);
3750
3674
  }
3751
- const operation = this[$changes].indexes[index]?.op ?? exports.OPERATION.ADD;
3752
- this[$changes].indexes[index] = index;
3753
- this.$indexes.set(index, index);
3754
- this.$items.set(index, value);
3755
- this[$changes].change(index, operation);
3756
- return index;
3757
3675
  }
3758
- entries() {
3759
- return this.$items.entries();
3676
+ // allow inherited classes to have a constructor
3677
+ constructor(...args) {
3678
+ //
3679
+ // inline
3680
+ // Schema.initialize(this);
3681
+ //
3682
+ Schema.initialize(this);
3683
+ //
3684
+ // Assign initial values
3685
+ //
3686
+ if (args[0]) {
3687
+ Object.assign(this, args[0]);
3688
+ }
3760
3689
  }
3761
- delete(item) {
3762
- const entries = this.$items.entries();
3763
- let index;
3764
- let entry;
3765
- while (entry = entries.next()) {
3766
- if (entry.done) {
3767
- break;
3690
+ assign(props) {
3691
+ Object.assign(this, props);
3692
+ return this;
3693
+ }
3694
+ /**
3695
+ * (Server-side): Flag a property to be encoded for the next patch.
3696
+ * @param instance Schema instance
3697
+ * @param property string representing the property name, or number representing the index of the property.
3698
+ * @param operation OPERATION to perform (detected automatically)
3699
+ */
3700
+ setDirty(property, operation) {
3701
+ const metadata = this.constructor[Symbol.metadata];
3702
+ this[$changes].change(metadata[metadata[property]].index, operation);
3703
+ }
3704
+ clone() {
3705
+ const cloned = new (this.constructor);
3706
+ const metadata = this.constructor[Symbol.metadata];
3707
+ //
3708
+ // TODO: clone all properties, not only annotated ones
3709
+ //
3710
+ // for (const field in this) {
3711
+ for (const fieldIndex in metadata) {
3712
+ // const field = metadata[metadata[fieldIndex]].name;
3713
+ const field = metadata[fieldIndex].name;
3714
+ if (typeof (this[field]) === "object" &&
3715
+ typeof (this[field]?.clone) === "function") {
3716
+ // deep clone
3717
+ cloned[field] = this[field].clone();
3768
3718
  }
3769
- if (item === entry.value[1]) {
3770
- index = entry.value[0];
3771
- break;
3719
+ else {
3720
+ // primitive values
3721
+ cloned[field] = this[field];
3772
3722
  }
3773
3723
  }
3774
- if (index === undefined) {
3775
- return false;
3776
- }
3777
- this.deletedItems[index] = this[$changes].delete(index);
3778
- this.$indexes.delete(index);
3779
- return this.$items.delete(index);
3780
- }
3781
- clear() {
3782
- const changeTree = this[$changes];
3783
- // discard previous operations.
3784
- changeTree.discard(true);
3785
- changeTree.indexes = {};
3786
- // clear previous indexes
3787
- this.$indexes.clear();
3788
- // clear items
3789
- this.$items.clear();
3790
- changeTree.operation(exports.OPERATION.CLEAR);
3724
+ return cloned;
3791
3725
  }
3792
- has(value) {
3793
- const values = this.$items.values();
3794
- let has = false;
3795
- let entry;
3796
- while (entry = values.next()) {
3797
- if (entry.done) {
3798
- break;
3799
- }
3800
- if (value === entry.value) {
3801
- has = true;
3802
- break;
3726
+ toJSON() {
3727
+ const obj = {};
3728
+ const metadata = this.constructor[Symbol.metadata];
3729
+ for (const index in metadata) {
3730
+ const field = metadata[index];
3731
+ const fieldName = field.name;
3732
+ if (!field.deprecated && this[fieldName] !== null && typeof (this[fieldName]) !== "undefined") {
3733
+ obj[fieldName] = (typeof (this[fieldName]['toJSON']) === "function")
3734
+ ? this[fieldName]['toJSON']()
3735
+ : this[fieldName];
3803
3736
  }
3804
3737
  }
3805
- return has;
3806
- }
3807
- forEach(callbackfn) {
3808
- this.$items.forEach((value, key, _) => callbackfn(value, key, this));
3809
- }
3810
- values() {
3811
- return this.$items.values();
3812
- }
3813
- get size() {
3814
- return this.$items.size;
3815
- }
3816
- /** Iterator */
3817
- [Symbol.iterator]() {
3818
- return this.$items.values();
3819
- }
3820
- setIndex(index, key) {
3821
- this.$indexes.set(index, key);
3738
+ return obj;
3822
3739
  }
3823
- getIndex(index) {
3824
- return this.$indexes.get(index);
3740
+ /**
3741
+ * Used in tests only
3742
+ * @internal
3743
+ */
3744
+ discardAllChanges() {
3745
+ this[$changes].discardAll();
3825
3746
  }
3826
3747
  [$getByIndex](index) {
3827
- return this.$items.get(this.$indexes.get(index));
3748
+ const metadata = this.constructor[Symbol.metadata];
3749
+ return this[metadata[index].name];
3828
3750
  }
3829
3751
  [$deleteByIndex](index) {
3830
- const key = this.$indexes.get(index);
3831
- this.$items.delete(key);
3832
- this.$indexes.delete(index);
3752
+ const metadata = this.constructor[Symbol.metadata];
3753
+ this[metadata[index].name] = undefined;
3833
3754
  }
3834
- [$onEncodeEnd]() {
3835
- this.deletedItems = {};
3755
+ /**
3756
+ * Inspect the `refId` of all Schema instances in the tree. Optionally display the contents of the instance.
3757
+ *
3758
+ * @param ref Schema instance
3759
+ * @param showContents display JSON contents of the instance
3760
+ * @returns
3761
+ */
3762
+ static debugRefIds(ref, showContents = false, level = 0, decoder, keyPrefix = "") {
3763
+ const contents = (showContents) ? ` - ${JSON.stringify(ref.toJSON())}` : "";
3764
+ const changeTree = ref[$changes];
3765
+ const refId = (decoder) ? decoder.root.refIds.get(ref) : changeTree.refId;
3766
+ const root = (decoder) ? decoder.root : changeTree.root;
3767
+ // log reference count if > 1
3768
+ const refCount = (root?.refCount?.[refId] > 1)
3769
+ ? ` [×${root.refCount[refId]}]`
3770
+ : '';
3771
+ let output = `${getIndent(level)}${keyPrefix}${ref.constructor.name} (refId: ${refId})${refCount}${contents}\n`;
3772
+ changeTree.forEachChild((childChangeTree, indexOrKey) => {
3773
+ let key = indexOrKey;
3774
+ if (typeof indexOrKey === 'number' && ref['$indexes']) {
3775
+ // MapSchema
3776
+ key = ref['$indexes'].get(indexOrKey) ?? indexOrKey;
3777
+ }
3778
+ const keyPrefix = (ref['forEach'] !== undefined && key !== undefined) ? `["${key}"]: ` : "";
3779
+ output += this.debugRefIds(childChangeTree.ref, showContents, level + 1, decoder, keyPrefix);
3780
+ });
3781
+ return output;
3836
3782
  }
3837
- toArray() {
3838
- return Array.from(this.$items.values());
3783
+ static debugRefIdEncodingOrder(ref, changeSet = 'allChanges') {
3784
+ let encodeOrder = [];
3785
+ let current = ref[$changes].root[changeSet].next;
3786
+ while (current) {
3787
+ if (current.changeTree) {
3788
+ encodeOrder.push(current.changeTree.refId);
3789
+ }
3790
+ current = current.next;
3791
+ }
3792
+ return encodeOrder;
3839
3793
  }
3840
- toJSON() {
3841
- const values = [];
3842
- this.forEach((value, key) => {
3843
- values.push((typeof (value['toJSON']) === "function")
3844
- ? value['toJSON']()
3845
- : value);
3846
- });
3847
- return values;
3794
+ static debugRefIdsFromDecoder(decoder) {
3795
+ return this.debugRefIds(decoder.state, false, 0, decoder);
3848
3796
  }
3849
- //
3850
- // Decoding utilities
3851
- //
3852
- clone(isDecoding) {
3853
- let cloned;
3854
- if (isDecoding) {
3855
- // client-side
3856
- cloned = Object.assign(new SetSchema(), this);
3797
+ /**
3798
+ * Return a string representation of the changes on a Schema instance.
3799
+ * The list of changes is cleared after each encode.
3800
+ *
3801
+ * @param instance Schema instance
3802
+ * @param isEncodeAll Return "full encode" instead of current change set.
3803
+ * @returns
3804
+ */
3805
+ static debugChanges(instance, isEncodeAll = false) {
3806
+ const changeTree = instance[$changes];
3807
+ const changeSet = (isEncodeAll) ? changeTree.allChanges : changeTree.changes;
3808
+ const changeSetName = (isEncodeAll) ? "allChanges" : "changes";
3809
+ let output = `${instance.constructor.name} (${changeTree.refId}) -> .${changeSetName}:\n`;
3810
+ function dumpChangeSet(changeSet) {
3811
+ changeSet.operations
3812
+ .filter(op => op)
3813
+ .forEach((index) => {
3814
+ const operation = changeTree.indexedOperations[index];
3815
+ output += `- [${index}]: ${exports.OPERATION[operation]} (${JSON.stringify(changeTree.getValue(Number(index), isEncodeAll))})\n`;
3816
+ });
3857
3817
  }
3858
- else {
3859
- // server-side
3860
- cloned = new SetSchema();
3861
- this.forEach((value) => {
3862
- if (value[$changes]) {
3863
- cloned.add(value['clone']());
3818
+ dumpChangeSet(changeSet);
3819
+ // display filtered changes
3820
+ if (!isEncodeAll &&
3821
+ changeTree.filteredChanges &&
3822
+ (changeTree.filteredChanges.operations).filter(op => op).length > 0) {
3823
+ output += `${instance.constructor.name} (${changeTree.refId}) -> .filteredChanges:\n`;
3824
+ dumpChangeSet(changeTree.filteredChanges);
3825
+ }
3826
+ // display filtered changes
3827
+ if (isEncodeAll &&
3828
+ changeTree.allFilteredChanges &&
3829
+ (changeTree.allFilteredChanges.operations).filter(op => op).length > 0) {
3830
+ output += `${instance.constructor.name} (${changeTree.refId}) -> .allFilteredChanges:\n`;
3831
+ dumpChangeSet(changeTree.allFilteredChanges);
3832
+ }
3833
+ return output;
3834
+ }
3835
+ static debugChangesDeep(ref, changeSetName = "changes") {
3836
+ let output = "";
3837
+ const rootChangeTree = ref[$changes];
3838
+ const root = rootChangeTree.root;
3839
+ const changeTrees = new Map();
3840
+ const instanceRefIds = [];
3841
+ let totalOperations = 0;
3842
+ // TODO: FIXME: this method is not working as expected
3843
+ for (const [refId, changes] of Object.entries(root[changeSetName])) {
3844
+ const changeTree = root.changeTrees[refId];
3845
+ if (!changeTree) {
3846
+ continue;
3847
+ }
3848
+ let includeChangeTree = false;
3849
+ let parentChangeTrees = [];
3850
+ let parentChangeTree = changeTree.parent?.[$changes];
3851
+ if (changeTree === rootChangeTree) {
3852
+ includeChangeTree = true;
3853
+ }
3854
+ else {
3855
+ while (parentChangeTree !== undefined) {
3856
+ parentChangeTrees.push(parentChangeTree);
3857
+ if (parentChangeTree.ref === ref) {
3858
+ includeChangeTree = true;
3859
+ break;
3860
+ }
3861
+ parentChangeTree = parentChangeTree.parent?.[$changes];
3864
3862
  }
3865
- else {
3866
- cloned.add(value);
3863
+ }
3864
+ if (includeChangeTree) {
3865
+ instanceRefIds.push(changeTree.refId);
3866
+ totalOperations += Object.keys(changes).length;
3867
+ changeTrees.set(changeTree, parentChangeTrees.reverse());
3868
+ }
3869
+ }
3870
+ output += "---\n";
3871
+ output += `root refId: ${rootChangeTree.refId}\n`;
3872
+ output += `Total instances: ${instanceRefIds.length} (refIds: ${instanceRefIds.join(", ")})\n`;
3873
+ output += `Total changes: ${totalOperations}\n`;
3874
+ output += "---\n";
3875
+ // based on root.changes, display a tree of changes that has the "ref" instance as parent
3876
+ const visitedParents = new WeakSet();
3877
+ for (const [changeTree, parentChangeTrees] of changeTrees.entries()) {
3878
+ parentChangeTrees.forEach((parentChangeTree, level) => {
3879
+ if (!visitedParents.has(parentChangeTree)) {
3880
+ output += `${getIndent(level)}${parentChangeTree.ref.constructor.name} (refId: ${parentChangeTree.refId})\n`;
3881
+ visitedParents.add(parentChangeTree);
3867
3882
  }
3868
3883
  });
3884
+ const changes = changeTree.indexedOperations;
3885
+ const level = parentChangeTrees.length;
3886
+ const indent = getIndent(level);
3887
+ const parentIndex = (level > 0) ? `(${changeTree.parentIndex}) ` : "";
3888
+ output += `${indent}${parentIndex}${changeTree.ref.constructor.name} (refId: ${changeTree.refId}) - changes: ${Object.keys(changes).length}\n`;
3889
+ for (const index in changes) {
3890
+ const operation = changes[index];
3891
+ output += `${getIndent(level + 1)}${exports.OPERATION[operation]}: ${index}\n`;
3892
+ }
3869
3893
  }
3870
- return cloned;
3894
+ return `${output}`;
3871
3895
  }
3872
3896
  }
3873
- registerType("set", { constructor: SetSchema });
3874
3897
 
3875
3898
  /******************************************************************************
3876
3899
  Copyright (c) Microsoft Corporation.
@@ -3940,11 +3963,12 @@ class Root {
3940
3963
  }
3941
3964
  }
3942
3965
  this.refCount[changeTree.refId] = (previousRefCount || 0) + 1;
3943
- // console.log("ADD", { refId: changeTree.refId, refCount: this.refCount[changeTree.refId] });
3966
+ // console.log("ADD", { refId: changeTree.refId, ref: changeTree.ref.constructor.name, refCount: this.refCount[changeTree.refId], isNewChangeTree });
3944
3967
  return isNewChangeTree;
3945
3968
  }
3946
3969
  remove(changeTree) {
3947
3970
  const refCount = (this.refCount[changeTree.refId]) - 1;
3971
+ // console.log("REMOVE", { refId: changeTree.refId, ref: changeTree.ref.constructor.name, refCount, needRemove: refCount <= 0 });
3948
3972
  if (refCount <= 0) {
3949
3973
  //
3950
3974
  // Only remove "root" reference if it's the last reference
@@ -4027,6 +4051,28 @@ class Root {
4027
4051
  }
4028
4052
  changeSet.tail = node;
4029
4053
  }
4054
+ enqueueChangeTree(changeTree, changeSet, queueRootNode = changeTree[changeSet].queueRootNode) {
4055
+ // skip
4056
+ if (queueRootNode) {
4057
+ return;
4058
+ }
4059
+ // Add to linked list if not already present
4060
+ changeTree[changeSet].queueRootNode = this.addToChangeTreeList(this[changeSet], changeTree);
4061
+ }
4062
+ addToChangeTreeList(list, changeTree) {
4063
+ const node = { changeTree, next: undefined, prev: undefined };
4064
+ if (!list.next) {
4065
+ list.next = node;
4066
+ list.tail = node;
4067
+ }
4068
+ else {
4069
+ node.prev = list.tail;
4070
+ list.tail.next = node;
4071
+ list.tail = node;
4072
+ }
4073
+ list.length++;
4074
+ return node;
4075
+ }
4030
4076
  removeChangeFromChangeSet(changeSetName, changeTree) {
4031
4077
  const changeSet = this[changeSetName];
4032
4078
  const node = changeTree[changeSetName].queueRootNode;