@colyseus/schema 3.0.52 → 3.0.54

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.
@@ -689,15 +689,7 @@
689
689
  if (typeof (fieldType) === "string") {
690
690
  continue;
691
691
  }
692
- if (Array.isArray(fieldType)) {
693
- const type = fieldType[0];
694
- // skip primitive types
695
- if (type === "string") {
696
- continue;
697
- }
698
- this.discoverTypes(type, klass, index, parentHasViewTag || fieldHasViewTag);
699
- }
700
- else if (typeof (fieldType) === "function") {
692
+ if (typeof (fieldType) === "function") {
701
693
  this.discoverTypes(fieldType, klass, index, parentHasViewTag || fieldHasViewTag);
702
694
  }
703
695
  else {
@@ -747,11 +739,43 @@
747
739
  }
748
740
 
749
741
  function getNormalizedType(type) {
750
- return (Array.isArray(type))
751
- ? { array: type[0] }
752
- : (typeof (type['type']) !== "undefined")
753
- ? type['type']
754
- : type;
742
+ if (Array.isArray(type)) {
743
+ return { array: getNormalizedType(type[0]) };
744
+ }
745
+ else if (typeof (type['type']) !== "undefined") {
746
+ return type['type'];
747
+ }
748
+ else if (isTSEnum(type)) {
749
+ // Detect TS Enum type (either string or number)
750
+ return Object.keys(type).every(key => typeof type[key] === "string")
751
+ ? "string"
752
+ : "number";
753
+ }
754
+ else if (typeof type === "object" && type !== null) {
755
+ // Handle collection types
756
+ const collectionType = Object.keys(type).find(k => registeredTypes[k] !== undefined);
757
+ if (collectionType) {
758
+ type[collectionType] = getNormalizedType(type[collectionType]);
759
+ return type;
760
+ }
761
+ }
762
+ return type;
763
+ }
764
+ function isTSEnum(_enum) {
765
+ if (typeof _enum === 'function' && _enum[Symbol.metadata]) {
766
+ return false;
767
+ }
768
+ const keys = Object.keys(_enum);
769
+ const numericFields = keys.filter(k => /\d+/.test(k));
770
+ // Check for number enum (has numeric keys and reverse mapping)
771
+ if (numericFields.length > 0 && numericFields.length === (keys.length / 2) && _enum[_enum[numericFields[0]]] == numericFields[0]) {
772
+ return true;
773
+ }
774
+ // Check for string enum (all values are strings and keys match values)
775
+ if (keys.length > 0 && keys.every(key => typeof _enum[key] === 'string' && _enum[key] === key)) {
776
+ return true;
777
+ }
778
+ return false;
755
779
  }
756
780
  const Metadata = {
757
781
  addField(metadata, index, name, type, descriptor) {
@@ -866,14 +890,12 @@
866
890
  ?? -1; // no fields defined
867
891
  fieldIndex++;
868
892
  for (const field in fields) {
869
- const type = fields[field];
893
+ const type = getNormalizedType(fields[field]);
870
894
  // FIXME: this code is duplicated from @type() annotation
871
- const complexTypeKlass = (Array.isArray(type))
872
- ? getType("array")
873
- : (typeof (Object.keys(type)[0]) === "string") && getType(Object.keys(type)[0]);
895
+ const complexTypeKlass = typeof (Object.keys(type)[0]) === "string" && getType(Object.keys(type)[0]);
874
896
  const childType = (complexTypeKlass)
875
897
  ? Object.values(type)[0]
876
- : getNormalizedType(type);
898
+ : type;
877
899
  Metadata.addField(metadata, fieldIndex, field, type, getPropertyDescriptor(`_${field}`, fieldIndex, childType, complexTypeKlass));
878
900
  fieldIndex++;
879
901
  }
@@ -2189,7 +2211,7 @@
2189
2211
  const allChangesIndex = this.items.findIndex(item => item === this.items[0]);
2190
2212
  changeTree.delete(index, exports.OPERATION.DELETE, allChangesIndex);
2191
2213
  changeTree.shiftAllChangeIndexes(-1, allChangesIndex);
2192
- // this.deletedIndexes[index] = true;
2214
+ this.deletedIndexes[index] = true;
2193
2215
  return this.items.shift();
2194
2216
  }
2195
2217
  /**
@@ -2824,448 +2846,174 @@
2824
2846
  }
2825
2847
  registerType("map", { constructor: MapSchema });
2826
2848
 
2827
- const DEFAULT_VIEW_TAG = -1;
2828
- function entity(constructor) {
2829
- TypeContext.register(constructor);
2830
- return constructor;
2831
- }
2832
- /**
2833
- * [See documentation](https://docs.colyseus.io/state/schema/)
2834
- *
2835
- * Annotate a Schema property to be serializeable.
2836
- * \@type()'d fields are automatically flagged as "dirty" for the next patch.
2837
- *
2838
- * @example Standard usage, with automatic change tracking.
2839
- * ```
2840
- * \@type("string") propertyName: string;
2841
- * ```
2842
- *
2843
- * @example You can provide the "manual" option if you'd like to manually control your patches via .setDirty().
2844
- * ```
2845
- * \@type("string", { manual: true })
2846
- * ```
2847
- */
2848
- // export function type(type: DefinitionType, options?: TypeOptions) {
2849
- // return function ({ get, set }, context: ClassAccessorDecoratorContext): ClassAccessorDecoratorResult<Schema, any> {
2850
- // if (context.kind !== "accessor") {
2851
- // throw new Error("@type() is only supported for class accessor properties");
2852
- // }
2853
- // const field = context.name.toString();
2854
- // //
2855
- // // detect index for this field, considering inheritance
2856
- // //
2857
- // const parent = Object.getPrototypeOf(context.metadata);
2858
- // let fieldIndex: number = context.metadata[$numFields] // current structure already has fields defined
2859
- // ?? (parent && parent[$numFields]) // parent structure has fields defined
2860
- // ?? -1; // no fields defined
2861
- // fieldIndex++;
2862
- // if (
2863
- // !parent && // the parent already initializes the `$changes` property
2864
- // !Metadata.hasFields(context.metadata)
2865
- // ) {
2866
- // context.addInitializer(function (this: Ref) {
2867
- // Object.defineProperty(this, $changes, {
2868
- // value: new ChangeTree(this),
2869
- // enumerable: false,
2870
- // writable: true
2871
- // });
2872
- // });
2873
- // }
2874
- // Metadata.addField(context.metadata, fieldIndex, field, type);
2875
- // const isArray = ArraySchema.is(type);
2876
- // const isMap = !isArray && MapSchema.is(type);
2877
- // // if (options && options.manual) {
2878
- // // // do not declare getter/setter descriptor
2879
- // // definition.descriptors[field] = {
2880
- // // enumerable: true,
2881
- // // configurable: true,
2882
- // // writable: true,
2883
- // // };
2884
- // // return;
2885
- // // }
2886
- // return {
2887
- // init(value) {
2888
- // // TODO: may need to convert ArraySchema/MapSchema here
2889
- // // do not flag change if value is undefined.
2890
- // if (value !== undefined) {
2891
- // this[$changes].change(fieldIndex);
2892
- // // automaticallty transform Array into ArraySchema
2893
- // if (isArray) {
2894
- // if (!(value instanceof ArraySchema)) {
2895
- // value = new ArraySchema(...value);
2896
- // }
2897
- // value[$childType] = Object.values(type)[0];
2898
- // }
2899
- // // automaticallty transform Map into MapSchema
2900
- // if (isMap) {
2901
- // if (!(value instanceof MapSchema)) {
2902
- // value = new MapSchema(value);
2903
- // }
2904
- // value[$childType] = Object.values(type)[0];
2905
- // }
2906
- // // try to turn provided structure into a Proxy
2907
- // if (value['$proxy'] === undefined) {
2908
- // if (isMap) {
2909
- // value = getMapProxy(value);
2910
- // }
2911
- // }
2912
- // }
2913
- // return value;
2914
- // },
2915
- // get() {
2916
- // return get.call(this);
2917
- // },
2918
- // set(value: any) {
2919
- // /**
2920
- // * Create Proxy for array or map items
2921
- // */
2922
- // // skip if value is the same as cached.
2923
- // if (value === get.call(this)) {
2924
- // return;
2925
- // }
2926
- // if (
2927
- // value !== undefined &&
2928
- // value !== null
2929
- // ) {
2930
- // // automaticallty transform Array into ArraySchema
2931
- // if (isArray) {
2932
- // if (!(value instanceof ArraySchema)) {
2933
- // value = new ArraySchema(...value);
2934
- // }
2935
- // value[$childType] = Object.values(type)[0];
2936
- // }
2937
- // // automaticallty transform Map into MapSchema
2938
- // if (isMap) {
2939
- // if (!(value instanceof MapSchema)) {
2940
- // value = new MapSchema(value);
2941
- // }
2942
- // value[$childType] = Object.values(type)[0];
2943
- // }
2944
- // // try to turn provided structure into a Proxy
2945
- // if (value['$proxy'] === undefined) {
2946
- // if (isMap) {
2947
- // value = getMapProxy(value);
2948
- // }
2949
- // }
2950
- // // flag the change for encoding.
2951
- // this[$changes].change(fieldIndex);
2952
- // //
2953
- // // call setParent() recursively for this and its child
2954
- // // structures.
2955
- // //
2956
- // if (value[$changes]) {
2957
- // value[$changes].setParent(
2958
- // this,
2959
- // this[$changes].root,
2960
- // Metadata.getIndex(context.metadata, field),
2961
- // );
2962
- // }
2963
- // } else if (get.call(this)) {
2964
- // //
2965
- // // Setting a field to `null` or `undefined` will delete it.
2966
- // //
2967
- // this[$changes].delete(field);
2968
- // }
2969
- // set.call(this, value);
2970
- // },
2971
- // };
2972
- // }
2973
- // }
2974
- function view(tag = DEFAULT_VIEW_TAG) {
2975
- return function (target, fieldName) {
2976
- const constructor = target.constructor;
2977
- const parentClass = Object.getPrototypeOf(constructor);
2978
- const parentMetadata = parentClass[Symbol.metadata];
2979
- // TODO: use Metadata.initialize()
2980
- const metadata = (constructor[Symbol.metadata] ??= Object.assign({}, constructor[Symbol.metadata], parentMetadata ?? Object.create(null)));
2981
- // const fieldIndex = metadata[fieldName];
2982
- // if (!metadata[fieldIndex]) {
2983
- // //
2984
- // // detect index for this field, considering inheritance
2985
- // //
2986
- // metadata[fieldIndex] = {
2987
- // type: undefined,
2988
- // index: (metadata[$numFields] // current structure already has fields defined
2989
- // ?? (parentMetadata && parentMetadata[$numFields]) // parent structure has fields defined
2990
- // ?? -1) + 1 // no fields defined
2991
- // }
2992
- // }
2993
- Metadata.setTag(metadata, fieldName, tag);
2994
- };
2995
- }
2996
- function type(type, options) {
2997
- return function (target, field) {
2998
- const constructor = target.constructor;
2999
- if (!type) {
3000
- throw new Error(`${constructor.name}: @type() reference provided for "${field}" is undefined. Make sure you don't have any circular dependencies.`);
3001
- }
3002
- // for inheritance support
3003
- TypeContext.register(constructor);
3004
- const parentClass = Object.getPrototypeOf(constructor);
3005
- const parentMetadata = parentClass[Symbol.metadata];
3006
- const metadata = Metadata.initialize(constructor);
3007
- let fieldIndex = metadata[field];
3008
- /**
3009
- * skip if descriptor already exists for this field (`@deprecated()`)
3010
- */
3011
- if (metadata[fieldIndex] !== undefined) {
3012
- if (metadata[fieldIndex].deprecated) {
3013
- // do not create accessors for deprecated properties.
3014
- return;
3015
- }
3016
- else if (metadata[fieldIndex].type !== undefined) {
3017
- // trying to define same property multiple times across inheritance.
3018
- // https://github.com/colyseus/colyseus-unity3d/issues/131#issuecomment-814308572
3019
- try {
3020
- throw new Error(`@colyseus/schema: Duplicate '${field}' definition on '${constructor.name}'.\nCheck @type() annotation`);
3021
- }
3022
- catch (e) {
3023
- const definitionAtLine = e.stack.split("\n")[4].trim();
3024
- throw new Error(`${e.message} ${definitionAtLine}`);
3025
- }
3026
- }
3027
- }
3028
- else {
3029
- //
3030
- // detect index for this field, considering inheritance
3031
- //
3032
- fieldIndex = metadata[$numFields] // current structure already has fields defined
3033
- ?? (parentMetadata && parentMetadata[$numFields]) // parent structure has fields defined
3034
- ?? -1; // no fields defined
3035
- fieldIndex++;
2849
+ var _a$2, _b$2;
2850
+ class CollectionSchema {
2851
+ static { this[_a$2] = encodeKeyValueOperation; }
2852
+ static { this[_b$2] = decodeKeyValueOperation; }
2853
+ /**
2854
+ * Determine if a property must be filtered.
2855
+ * - If returns false, the property is NOT going to be encoded.
2856
+ * - If returns true, the property is going to be encoded.
2857
+ *
2858
+ * Encoding with "filters" happens in two steps:
2859
+ * - First, the encoder iterates over all "not owned" properties and encodes them.
2860
+ * - Then, the encoder iterates over all "owned" properties per instance and encodes them.
2861
+ */
2862
+ static [(_a$2 = $encoder, _b$2 = $decoder, $filter)](ref, index, view) {
2863
+ return (!view ||
2864
+ typeof (ref[$childType]) === "string" ||
2865
+ view.isChangeTreeVisible((ref[$getByIndex](index) ?? ref.deletedItems[index])[$changes]));
2866
+ }
2867
+ static is(type) {
2868
+ return type['collection'] !== undefined;
2869
+ }
2870
+ constructor(initialValues) {
2871
+ this.$items = new Map();
2872
+ this.$indexes = new Map();
2873
+ this.deletedItems = {};
2874
+ this.$refId = 0;
2875
+ this[$changes] = new ChangeTree(this);
2876
+ this[$changes].indexes = {};
2877
+ if (initialValues) {
2878
+ initialValues.forEach((v) => this.add(v));
3036
2879
  }
3037
- if (options && options.manual) {
3038
- Metadata.addField(metadata, fieldIndex, field, type, {
3039
- // do not declare getter/setter descriptor
3040
- enumerable: true,
3041
- configurable: true,
3042
- writable: true,
3043
- });
3044
- }
3045
- else {
3046
- const complexTypeKlass = (Array.isArray(type))
3047
- ? getType("array")
3048
- : (typeof (Object.keys(type)[0]) === "string") && getType(Object.keys(type)[0]);
3049
- const childType = (complexTypeKlass)
3050
- ? Object.values(type)[0]
3051
- : type;
3052
- Metadata.addField(metadata, fieldIndex, field, type, getPropertyDescriptor(`_${field}`, fieldIndex, childType, complexTypeKlass));
3053
- }
3054
- };
3055
- }
3056
- function getPropertyDescriptor(fieldCached, fieldIndex, type, complexTypeKlass) {
3057
- return {
3058
- get: function () { return this[fieldCached]; },
3059
- set: function (value) {
3060
- const previousValue = this[fieldCached] ?? undefined;
3061
- // skip if value is the same as cached.
3062
- if (value === previousValue) {
3063
- return;
3064
- }
3065
- if (value !== undefined &&
3066
- value !== null) {
3067
- if (complexTypeKlass) {
3068
- // automaticallty transform Array into ArraySchema
3069
- if (complexTypeKlass.constructor === ArraySchema && !(value instanceof ArraySchema)) {
3070
- value = new ArraySchema(...value);
3071
- }
3072
- // automaticallty transform Map into MapSchema
3073
- if (complexTypeKlass.constructor === MapSchema && !(value instanceof MapSchema)) {
3074
- value = new MapSchema(value);
3075
- }
3076
- value[$childType] = type;
3077
- }
3078
- else if (typeof (type) !== "string") {
3079
- assertInstanceType(value, type, this, fieldCached.substring(1));
3080
- }
3081
- else {
3082
- assertType(value, type, this, fieldCached.substring(1));
3083
- }
3084
- const changeTree = this[$changes];
3085
- //
3086
- // Replacing existing "ref", remove it from root.
3087
- //
3088
- if (previousValue !== undefined && previousValue[$changes]) {
3089
- changeTree.root?.remove(previousValue[$changes]);
3090
- this.constructor[$track](changeTree, fieldIndex, exports.OPERATION.DELETE_AND_ADD);
3091
- }
3092
- else {
3093
- this.constructor[$track](changeTree, fieldIndex, exports.OPERATION.ADD);
3094
- }
3095
- //
3096
- // call setParent() recursively for this and its child
3097
- // structures.
3098
- //
3099
- value[$changes]?.setParent(this, changeTree.root, fieldIndex);
3100
- }
3101
- else if (previousValue !== undefined) {
3102
- //
3103
- // Setting a field to `null` or `undefined` will delete it.
3104
- //
3105
- this[$changes].delete(fieldIndex);
3106
- }
3107
- this[fieldCached] = value;
3108
- },
3109
- enumerable: true,
3110
- configurable: true
3111
- };
3112
- }
3113
- /**
3114
- * `@deprecated()` flag a field as deprecated.
3115
- * The previous `@type()` annotation should remain along with this one.
3116
- */
3117
- function deprecated(throws = true) {
3118
- return function (klass, field) {
3119
- //
3120
- // FIXME: the following block of code is repeated across `@type()`, `@deprecated()` and `@unreliable()` decorators.
3121
- //
3122
- const constructor = klass.constructor;
3123
- const parentClass = Object.getPrototypeOf(constructor);
3124
- const parentMetadata = parentClass[Symbol.metadata];
3125
- const metadata = (constructor[Symbol.metadata] ??= Object.assign({}, constructor[Symbol.metadata], parentMetadata ?? Object.create(null)));
3126
- const fieldIndex = metadata[field];
3127
- // if (!metadata[field]) {
3128
- // //
3129
- // // detect index for this field, considering inheritance
3130
- // //
3131
- // metadata[field] = {
3132
- // type: undefined,
3133
- // index: (metadata[$numFields] // current structure already has fields defined
3134
- // ?? (parentMetadata && parentMetadata[$numFields]) // parent structure has fields defined
3135
- // ?? -1) + 1 // no fields defined
3136
- // }
3137
- // }
3138
- metadata[fieldIndex].deprecated = true;
3139
- if (throws) {
3140
- metadata[$descriptors] ??= {};
3141
- metadata[$descriptors][field] = {
3142
- get: function () { throw new Error(`${field} is deprecated.`); },
3143
- set: function (value) { },
3144
- enumerable: false,
3145
- configurable: true
3146
- };
3147
- }
3148
- // flag metadata[field] as non-enumerable
3149
- Object.defineProperty(metadata, fieldIndex, {
3150
- value: metadata[fieldIndex],
2880
+ Object.defineProperty(this, $childType, {
2881
+ value: undefined,
3151
2882
  enumerable: false,
3152
- configurable: true
2883
+ writable: true,
2884
+ configurable: true,
3153
2885
  });
3154
- };
3155
- }
3156
- function defineTypes(target, fields, options) {
3157
- for (let field in fields) {
3158
- type(fields[field], options)(target.prototype, field);
3159
- }
3160
- return target;
3161
- }
3162
- function schema(fieldsAndMethods, name, inherits = Schema) {
3163
- const fields = {};
3164
- const methods = {};
3165
- const defaultValues = {};
3166
- const viewTagFields = {};
3167
- for (let fieldName in fieldsAndMethods) {
3168
- const value = fieldsAndMethods[fieldName];
3169
- if (typeof (value) === "object") {
3170
- if (value['default'] !== undefined) {
3171
- defaultValues[fieldName] = value['default'];
3172
- }
3173
- if (value['view'] !== undefined) {
3174
- viewTagFields[fieldName] = (typeof (value['view']) === "boolean")
3175
- ? DEFAULT_VIEW_TAG
3176
- : value['view'];
3177
- }
3178
- fields[fieldName] = value;
3179
- }
3180
- else if (typeof (value) === "function") {
3181
- methods[fieldName] = value;
3182
- }
3183
- else {
3184
- fields[fieldName] = value;
3185
- }
3186
2886
  }
3187
- const klass = Metadata.setFields(class extends inherits {
3188
- constructor(...args) {
3189
- args[0] = Object.assign({}, defaultValues, args[0]);
3190
- super(...args);
2887
+ add(value) {
2888
+ // set "index" for reference.
2889
+ const index = this.$refId++;
2890
+ const isRef = (value[$changes]) !== undefined;
2891
+ if (isRef) {
2892
+ value[$changes].setParent(this, this[$changes].root, index);
3191
2893
  }
3192
- }, fields);
3193
- for (let fieldName in viewTagFields) {
3194
- view(viewTagFields[fieldName])(klass.prototype, fieldName);
2894
+ this[$changes].indexes[index] = index;
2895
+ this.$indexes.set(index, index);
2896
+ this.$items.set(index, value);
2897
+ this[$changes].change(index);
2898
+ return index;
3195
2899
  }
3196
- for (let methodName in methods) {
3197
- klass.prototype[methodName] = methods[methodName];
2900
+ at(index) {
2901
+ const key = Array.from(this.$items.keys())[index];
2902
+ return this.$items.get(key);
3198
2903
  }
3199
- if (name) {
3200
- Object.defineProperty(klass, "name", { value: name });
2904
+ entries() {
2905
+ return this.$items.entries();
3201
2906
  }
3202
- klass.extends = (fields, name) => schema(fields, name, klass);
3203
- return klass;
3204
- }
3205
-
3206
- function getIndent(level) {
3207
- return (new Array(level).fill(0)).map((_, i) => (i === level - 1) ? `└─ ` : ` `).join("");
3208
- }
3209
- function dumpChanges(schema) {
3210
- const $root = schema[$changes].root;
3211
- const dump = {
3212
- ops: {},
3213
- refs: []
3214
- };
3215
- // for (const refId in $root.changes) {
3216
- let current = $root.changes.next;
3217
- while (current) {
3218
- const changeTree = current.changeTree;
3219
- // skip if ChangeTree is undefined
3220
- if (changeTree === undefined) {
3221
- current = current.next;
3222
- continue;
3223
- }
3224
- const changes = changeTree.indexedOperations;
3225
- dump.refs.push(`refId#${changeTree.refId}`);
3226
- for (const index in changes) {
3227
- const op = changes[index];
3228
- const opName = exports.OPERATION[op];
3229
- if (!dump.ops[opName]) {
3230
- dump.ops[opName] = 0;
2907
+ delete(item) {
2908
+ const entries = this.$items.entries();
2909
+ let index;
2910
+ let entry;
2911
+ while (entry = entries.next()) {
2912
+ if (entry.done) {
2913
+ break;
2914
+ }
2915
+ if (item === entry.value[1]) {
2916
+ index = entry.value[0];
2917
+ break;
3231
2918
  }
3232
- dump.ops[exports.OPERATION[op]]++;
3233
2919
  }
3234
- current = current.next;
2920
+ if (index === undefined) {
2921
+ return false;
2922
+ }
2923
+ this.deletedItems[index] = this[$changes].delete(index);
2924
+ this.$indexes.delete(index);
2925
+ return this.$items.delete(index);
3235
2926
  }
3236
- return dump;
3237
- }
3238
-
3239
- var _a$2, _b$2;
3240
- /**
3241
- * Schema encoder / decoder
3242
- */
3243
- class Schema {
3244
- static { this[_a$2] = encodeSchemaOperation; }
3245
- static { this[_b$2] = decodeSchemaOperation; }
3246
- /**
3247
- * Assign the property descriptors required to track changes on this instance.
3248
- * @param instance
3249
- */
3250
- static initialize(instance) {
3251
- Object.defineProperty(instance, $changes, {
3252
- value: new ChangeTree(instance),
3253
- enumerable: false,
3254
- writable: true
2927
+ clear() {
2928
+ const changeTree = this[$changes];
2929
+ // discard previous operations.
2930
+ changeTree.discard(true);
2931
+ changeTree.indexes = {};
2932
+ // remove children references
2933
+ changeTree.forEachChild((childChangeTree, _) => {
2934
+ changeTree.root?.remove(childChangeTree);
3255
2935
  });
3256
- Object.defineProperties(instance, instance.constructor[Symbol.metadata]?.[$descriptors] || {});
2936
+ // clear previous indexes
2937
+ this.$indexes.clear();
2938
+ // clear items
2939
+ this.$items.clear();
2940
+ changeTree.operation(exports.OPERATION.CLEAR);
3257
2941
  }
3258
- static is(type) {
3259
- return typeof (type[Symbol.metadata]) === "object";
3260
- // const metadata = type[Symbol.metadata];
3261
- // return metadata && Object.prototype.hasOwnProperty.call(metadata, -1);
2942
+ has(value) {
2943
+ return Array.from(this.$items.values()).some((v) => v === value);
3262
2944
  }
3263
- /**
3264
- * Track property changes
3265
- */
3266
- static [(_a$2 = $encoder, _b$2 = $decoder, $track)](changeTree, index, operation = exports.OPERATION.ADD) {
3267
- changeTree.change(index, operation);
2945
+ forEach(callbackfn) {
2946
+ this.$items.forEach((value, key, _) => callbackfn(value, key, this));
2947
+ }
2948
+ values() {
2949
+ return this.$items.values();
2950
+ }
2951
+ get size() {
2952
+ return this.$items.size;
2953
+ }
2954
+ /** Iterator */
2955
+ [Symbol.iterator]() {
2956
+ return this.$items.values();
2957
+ }
2958
+ setIndex(index, key) {
2959
+ this.$indexes.set(index, key);
2960
+ }
2961
+ getIndex(index) {
2962
+ return this.$indexes.get(index);
2963
+ }
2964
+ [$getByIndex](index) {
2965
+ return this.$items.get(this.$indexes.get(index));
2966
+ }
2967
+ [$deleteByIndex](index) {
2968
+ const key = this.$indexes.get(index);
2969
+ this.$items.delete(key);
2970
+ this.$indexes.delete(index);
2971
+ }
2972
+ [$onEncodeEnd]() {
2973
+ this.deletedItems = {};
2974
+ }
2975
+ toArray() {
2976
+ return Array.from(this.$items.values());
2977
+ }
2978
+ toJSON() {
2979
+ const values = [];
2980
+ this.forEach((value, key) => {
2981
+ values.push((typeof (value['toJSON']) === "function")
2982
+ ? value['toJSON']()
2983
+ : value);
2984
+ });
2985
+ return values;
2986
+ }
2987
+ //
2988
+ // Decoding utilities
2989
+ //
2990
+ clone(isDecoding) {
2991
+ let cloned;
2992
+ if (isDecoding) {
2993
+ // client-side
2994
+ cloned = Object.assign(new CollectionSchema(), this);
2995
+ }
2996
+ else {
2997
+ // server-side
2998
+ cloned = new CollectionSchema();
2999
+ this.forEach((value) => {
3000
+ if (value[$changes]) {
3001
+ cloned.add(value['clone']());
3002
+ }
3003
+ else {
3004
+ cloned.add(value);
3005
+ }
3006
+ });
3007
+ }
3008
+ return cloned;
3268
3009
  }
3010
+ }
3011
+ registerType("collection", { constructor: CollectionSchema, });
3012
+
3013
+ var _a$1, _b$1;
3014
+ class SetSchema {
3015
+ static { this[_a$1] = encodeKeyValueOperation; }
3016
+ static { this[_b$1] = decodeKeyValueOperation; }
3269
3017
  /**
3270
3018
  * Determine if a property must be filtered.
3271
3019
  * - If returns false, the property is NOT going to be encoded.
@@ -3275,418 +3023,653 @@
3275
3023
  * - First, the encoder iterates over all "not owned" properties and encodes them.
3276
3024
  * - Then, the encoder iterates over all "owned" properties per instance and encodes them.
3277
3025
  */
3278
- static [$filter](ref, index, view) {
3279
- const metadata = ref.constructor[Symbol.metadata];
3280
- const tag = metadata[index]?.tag;
3281
- if (view === undefined) {
3282
- // shared pass/encode: encode if doesn't have a tag
3283
- return tag === undefined;
3284
- }
3285
- else if (tag === undefined) {
3286
- // view pass: no tag
3287
- return true;
3288
- }
3289
- else if (tag === DEFAULT_VIEW_TAG) {
3290
- // view pass: default tag
3291
- return view.isChangeTreeVisible(ref[$changes]);
3292
- }
3293
- else {
3294
- // view pass: custom tag
3295
- const tags = view.tags?.get(ref[$changes]);
3296
- return tags && tags.has(tag);
3297
- }
3026
+ static [(_a$1 = $encoder, _b$1 = $decoder, $filter)](ref, index, view) {
3027
+ return (!view ||
3028
+ typeof (ref[$childType]) === "string" ||
3029
+ view.visible.has((ref[$getByIndex](index) ?? ref.deletedItems[index])[$changes]));
3298
3030
  }
3299
- // allow inherited classes to have a constructor
3300
- constructor(...args) {
3301
- //
3302
- // inline
3303
- // Schema.initialize(this);
3304
- //
3305
- Schema.initialize(this);
3306
- //
3307
- // Assign initial values
3308
- //
3309
- if (args[0]) {
3310
- Object.assign(this, args[0]);
3031
+ static is(type) {
3032
+ return type['set'] !== undefined;
3033
+ }
3034
+ constructor(initialValues) {
3035
+ this.$items = new Map();
3036
+ this.$indexes = new Map();
3037
+ this.deletedItems = {};
3038
+ this.$refId = 0;
3039
+ this[$changes] = new ChangeTree(this);
3040
+ this[$changes].indexes = {};
3041
+ if (initialValues) {
3042
+ initialValues.forEach((v) => this.add(v));
3311
3043
  }
3044
+ Object.defineProperty(this, $childType, {
3045
+ value: undefined,
3046
+ enumerable: false,
3047
+ writable: true,
3048
+ configurable: true,
3049
+ });
3312
3050
  }
3313
- assign(props) {
3314
- Object.assign(this, props);
3315
- return this;
3051
+ add(value) {
3052
+ // immediatelly return false if value already added.
3053
+ if (this.has(value)) {
3054
+ return false;
3055
+ }
3056
+ // set "index" for reference.
3057
+ const index = this.$refId++;
3058
+ if ((value[$changes]) !== undefined) {
3059
+ value[$changes].setParent(this, this[$changes].root, index);
3060
+ }
3061
+ const operation = this[$changes].indexes[index]?.op ?? exports.OPERATION.ADD;
3062
+ this[$changes].indexes[index] = index;
3063
+ this.$indexes.set(index, index);
3064
+ this.$items.set(index, value);
3065
+ this[$changes].change(index, operation);
3066
+ return index;
3316
3067
  }
3317
- /**
3318
- * (Server-side): Flag a property to be encoded for the next patch.
3319
- * @param instance Schema instance
3320
- * @param property string representing the property name, or number representing the index of the property.
3321
- * @param operation OPERATION to perform (detected automatically)
3322
- */
3323
- setDirty(property, operation) {
3324
- const metadata = this.constructor[Symbol.metadata];
3325
- this[$changes].change(metadata[metadata[property]].index, operation);
3068
+ entries() {
3069
+ return this.$items.entries();
3326
3070
  }
3327
- clone() {
3328
- const cloned = new (this.constructor);
3329
- const metadata = this.constructor[Symbol.metadata];
3330
- //
3331
- // TODO: clone all properties, not only annotated ones
3332
- //
3333
- // for (const field in this) {
3334
- for (const fieldIndex in metadata) {
3335
- // const field = metadata[metadata[fieldIndex]].name;
3336
- const field = metadata[fieldIndex].name;
3337
- if (typeof (this[field]) === "object" &&
3338
- typeof (this[field]?.clone) === "function") {
3339
- // deep clone
3340
- cloned[field] = this[field].clone();
3071
+ delete(item) {
3072
+ const entries = this.$items.entries();
3073
+ let index;
3074
+ let entry;
3075
+ while (entry = entries.next()) {
3076
+ if (entry.done) {
3077
+ break;
3341
3078
  }
3342
- else {
3343
- // primitive values
3344
- cloned[field] = this[field];
3079
+ if (item === entry.value[1]) {
3080
+ index = entry.value[0];
3081
+ break;
3345
3082
  }
3346
3083
  }
3347
- return cloned;
3084
+ if (index === undefined) {
3085
+ return false;
3086
+ }
3087
+ this.deletedItems[index] = this[$changes].delete(index);
3088
+ this.$indexes.delete(index);
3089
+ return this.$items.delete(index);
3348
3090
  }
3349
- toJSON() {
3350
- const obj = {};
3351
- const metadata = this.constructor[Symbol.metadata];
3352
- for (const index in metadata) {
3353
- const field = metadata[index];
3354
- const fieldName = field.name;
3355
- if (!field.deprecated && this[fieldName] !== null && typeof (this[fieldName]) !== "undefined") {
3356
- obj[fieldName] = (typeof (this[fieldName]['toJSON']) === "function")
3357
- ? this[fieldName]['toJSON']()
3358
- : this[fieldName];
3091
+ clear() {
3092
+ const changeTree = this[$changes];
3093
+ // discard previous operations.
3094
+ changeTree.discard(true);
3095
+ changeTree.indexes = {};
3096
+ // clear previous indexes
3097
+ this.$indexes.clear();
3098
+ // clear items
3099
+ this.$items.clear();
3100
+ changeTree.operation(exports.OPERATION.CLEAR);
3101
+ }
3102
+ has(value) {
3103
+ const values = this.$items.values();
3104
+ let has = false;
3105
+ let entry;
3106
+ while (entry = values.next()) {
3107
+ if (entry.done) {
3108
+ break;
3109
+ }
3110
+ if (value === entry.value) {
3111
+ has = true;
3112
+ break;
3359
3113
  }
3360
3114
  }
3361
- return obj;
3115
+ return has;
3362
3116
  }
3363
- /**
3364
- * Used in tests only
3365
- * @internal
3366
- */
3367
- discardAllChanges() {
3368
- this[$changes].discardAll();
3117
+ forEach(callbackfn) {
3118
+ this.$items.forEach((value, key, _) => callbackfn(value, key, this));
3119
+ }
3120
+ values() {
3121
+ return this.$items.values();
3122
+ }
3123
+ get size() {
3124
+ return this.$items.size;
3125
+ }
3126
+ /** Iterator */
3127
+ [Symbol.iterator]() {
3128
+ return this.$items.values();
3129
+ }
3130
+ setIndex(index, key) {
3131
+ this.$indexes.set(index, key);
3132
+ }
3133
+ getIndex(index) {
3134
+ return this.$indexes.get(index);
3369
3135
  }
3370
3136
  [$getByIndex](index) {
3371
- const metadata = this.constructor[Symbol.metadata];
3372
- return this[metadata[index].name];
3137
+ return this.$items.get(this.$indexes.get(index));
3373
3138
  }
3374
3139
  [$deleteByIndex](index) {
3375
- const metadata = this.constructor[Symbol.metadata];
3376
- this[metadata[index].name] = undefined;
3377
- }
3378
- /**
3379
- * Inspect the `refId` of all Schema instances in the tree. Optionally display the contents of the instance.
3380
- *
3381
- * @param ref Schema instance
3382
- * @param showContents display JSON contents of the instance
3383
- * @returns
3384
- */
3385
- static debugRefIds(ref, showContents = false, level = 0, decoder, keyPrefix = "") {
3386
- const contents = (showContents) ? ` - ${JSON.stringify(ref.toJSON())}` : "";
3387
- const changeTree = ref[$changes];
3388
- const refId = (decoder) ? decoder.root.refIds.get(ref) : changeTree.refId;
3389
- const root = (decoder) ? decoder.root : changeTree.root;
3390
- // log reference count if > 1
3391
- const refCount = (root?.refCount?.[refId] > 1)
3392
- ? ` [×${root.refCount[refId]}]`
3393
- : '';
3394
- let output = `${getIndent(level)}${keyPrefix}${ref.constructor.name} (refId: ${refId})${refCount}${contents}\n`;
3395
- changeTree.forEachChild((childChangeTree, indexOrKey) => {
3396
- let key = indexOrKey;
3397
- if (typeof indexOrKey === 'number' && ref['$indexes']) {
3398
- // MapSchema
3399
- key = ref['$indexes'].get(indexOrKey) ?? indexOrKey;
3400
- }
3401
- const keyPrefix = (ref['forEach'] !== undefined && key !== undefined) ? `["${key}"]: ` : "";
3402
- output += this.debugRefIds(childChangeTree.ref, showContents, level + 1, decoder, keyPrefix);
3403
- });
3404
- return output;
3140
+ const key = this.$indexes.get(index);
3141
+ this.$items.delete(key);
3142
+ this.$indexes.delete(index);
3405
3143
  }
3406
- static debugRefIdEncodingOrder(ref, changeSet = 'allChanges') {
3407
- let encodeOrder = [];
3408
- let current = ref[$changes].root[changeSet].next;
3409
- while (current) {
3410
- if (current.changeTree) {
3411
- encodeOrder.push(current.changeTree.refId);
3412
- }
3413
- current = current.next;
3414
- }
3415
- return encodeOrder;
3144
+ [$onEncodeEnd]() {
3145
+ this.deletedItems = {};
3416
3146
  }
3417
- static debugRefIdsFromDecoder(decoder) {
3418
- return this.debugRefIds(decoder.state, false, 0, decoder);
3147
+ toArray() {
3148
+ return Array.from(this.$items.values());
3419
3149
  }
3420
- /**
3421
- * Return a string representation of the changes on a Schema instance.
3422
- * The list of changes is cleared after each encode.
3423
- *
3424
- * @param instance Schema instance
3425
- * @param isEncodeAll Return "full encode" instead of current change set.
3426
- * @returns
3427
- */
3428
- static debugChanges(instance, isEncodeAll = false) {
3429
- const changeTree = instance[$changes];
3430
- const changeSet = (isEncodeAll) ? changeTree.allChanges : changeTree.changes;
3431
- const changeSetName = (isEncodeAll) ? "allChanges" : "changes";
3432
- let output = `${instance.constructor.name} (${changeTree.refId}) -> .${changeSetName}:\n`;
3433
- function dumpChangeSet(changeSet) {
3434
- changeSet.operations
3435
- .filter(op => op)
3436
- .forEach((index) => {
3437
- const operation = changeTree.indexedOperations[index];
3438
- console.log({ index, operation });
3439
- output += `- [${index}]: ${exports.OPERATION[operation]} (${JSON.stringify(changeTree.getValue(Number(index), isEncodeAll))})\n`;
3150
+ toJSON() {
3151
+ const values = [];
3152
+ this.forEach((value, key) => {
3153
+ values.push((typeof (value['toJSON']) === "function")
3154
+ ? value['toJSON']()
3155
+ : value);
3156
+ });
3157
+ return values;
3158
+ }
3159
+ //
3160
+ // Decoding utilities
3161
+ //
3162
+ clone(isDecoding) {
3163
+ let cloned;
3164
+ if (isDecoding) {
3165
+ // client-side
3166
+ cloned = Object.assign(new SetSchema(), this);
3167
+ }
3168
+ else {
3169
+ // server-side
3170
+ cloned = new SetSchema();
3171
+ this.forEach((value) => {
3172
+ if (value[$changes]) {
3173
+ cloned.add(value['clone']());
3174
+ }
3175
+ else {
3176
+ cloned.add(value);
3177
+ }
3440
3178
  });
3441
3179
  }
3442
- dumpChangeSet(changeSet);
3443
- // display filtered changes
3444
- if (!isEncodeAll &&
3445
- changeTree.filteredChanges &&
3446
- (changeTree.filteredChanges.operations).filter(op => op).length > 0) {
3447
- output += `${instance.constructor.name} (${changeTree.refId}) -> .filteredChanges:\n`;
3448
- dumpChangeSet(changeTree.filteredChanges);
3180
+ return cloned;
3181
+ }
3182
+ }
3183
+ registerType("set", { constructor: SetSchema });
3184
+
3185
+ const DEFAULT_VIEW_TAG = -1;
3186
+ function entity(constructor) {
3187
+ TypeContext.register(constructor);
3188
+ return constructor;
3189
+ }
3190
+ /**
3191
+ * [See documentation](https://docs.colyseus.io/state/schema/)
3192
+ *
3193
+ * Annotate a Schema property to be serializeable.
3194
+ * \@type()'d fields are automatically flagged as "dirty" for the next patch.
3195
+ *
3196
+ * @example Standard usage, with automatic change tracking.
3197
+ * ```
3198
+ * \@type("string") propertyName: string;
3199
+ * ```
3200
+ *
3201
+ * @example You can provide the "manual" option if you'd like to manually control your patches via .setDirty().
3202
+ * ```
3203
+ * \@type("string", { manual: true })
3204
+ * ```
3205
+ */
3206
+ // export function type(type: DefinitionType, options?: TypeOptions) {
3207
+ // return function ({ get, set }, context: ClassAccessorDecoratorContext): ClassAccessorDecoratorResult<Schema, any> {
3208
+ // if (context.kind !== "accessor") {
3209
+ // throw new Error("@type() is only supported for class accessor properties");
3210
+ // }
3211
+ // const field = context.name.toString();
3212
+ // //
3213
+ // // detect index for this field, considering inheritance
3214
+ // //
3215
+ // const parent = Object.getPrototypeOf(context.metadata);
3216
+ // let fieldIndex: number = context.metadata[$numFields] // current structure already has fields defined
3217
+ // ?? (parent && parent[$numFields]) // parent structure has fields defined
3218
+ // ?? -1; // no fields defined
3219
+ // fieldIndex++;
3220
+ // if (
3221
+ // !parent && // the parent already initializes the `$changes` property
3222
+ // !Metadata.hasFields(context.metadata)
3223
+ // ) {
3224
+ // context.addInitializer(function (this: Ref) {
3225
+ // Object.defineProperty(this, $changes, {
3226
+ // value: new ChangeTree(this),
3227
+ // enumerable: false,
3228
+ // writable: true
3229
+ // });
3230
+ // });
3231
+ // }
3232
+ // Metadata.addField(context.metadata, fieldIndex, field, type);
3233
+ // const isArray = ArraySchema.is(type);
3234
+ // const isMap = !isArray && MapSchema.is(type);
3235
+ // // if (options && options.manual) {
3236
+ // // // do not declare getter/setter descriptor
3237
+ // // definition.descriptors[field] = {
3238
+ // // enumerable: true,
3239
+ // // configurable: true,
3240
+ // // writable: true,
3241
+ // // };
3242
+ // // return;
3243
+ // // }
3244
+ // return {
3245
+ // init(value) {
3246
+ // // TODO: may need to convert ArraySchema/MapSchema here
3247
+ // // do not flag change if value is undefined.
3248
+ // if (value !== undefined) {
3249
+ // this[$changes].change(fieldIndex);
3250
+ // // automaticallty transform Array into ArraySchema
3251
+ // if (isArray) {
3252
+ // if (!(value instanceof ArraySchema)) {
3253
+ // value = new ArraySchema(...value);
3254
+ // }
3255
+ // value[$childType] = Object.values(type)[0];
3256
+ // }
3257
+ // // automaticallty transform Map into MapSchema
3258
+ // if (isMap) {
3259
+ // if (!(value instanceof MapSchema)) {
3260
+ // value = new MapSchema(value);
3261
+ // }
3262
+ // value[$childType] = Object.values(type)[0];
3263
+ // }
3264
+ // // try to turn provided structure into a Proxy
3265
+ // if (value['$proxy'] === undefined) {
3266
+ // if (isMap) {
3267
+ // value = getMapProxy(value);
3268
+ // }
3269
+ // }
3270
+ // }
3271
+ // return value;
3272
+ // },
3273
+ // get() {
3274
+ // return get.call(this);
3275
+ // },
3276
+ // set(value: any) {
3277
+ // /**
3278
+ // * Create Proxy for array or map items
3279
+ // */
3280
+ // // skip if value is the same as cached.
3281
+ // if (value === get.call(this)) {
3282
+ // return;
3283
+ // }
3284
+ // if (
3285
+ // value !== undefined &&
3286
+ // value !== null
3287
+ // ) {
3288
+ // // automaticallty transform Array into ArraySchema
3289
+ // if (isArray) {
3290
+ // if (!(value instanceof ArraySchema)) {
3291
+ // value = new ArraySchema(...value);
3292
+ // }
3293
+ // value[$childType] = Object.values(type)[0];
3294
+ // }
3295
+ // // automaticallty transform Map into MapSchema
3296
+ // if (isMap) {
3297
+ // if (!(value instanceof MapSchema)) {
3298
+ // value = new MapSchema(value);
3299
+ // }
3300
+ // value[$childType] = Object.values(type)[0];
3301
+ // }
3302
+ // // try to turn provided structure into a Proxy
3303
+ // if (value['$proxy'] === undefined) {
3304
+ // if (isMap) {
3305
+ // value = getMapProxy(value);
3306
+ // }
3307
+ // }
3308
+ // // flag the change for encoding.
3309
+ // this[$changes].change(fieldIndex);
3310
+ // //
3311
+ // // call setParent() recursively for this and its child
3312
+ // // structures.
3313
+ // //
3314
+ // if (value[$changes]) {
3315
+ // value[$changes].setParent(
3316
+ // this,
3317
+ // this[$changes].root,
3318
+ // Metadata.getIndex(context.metadata, field),
3319
+ // );
3320
+ // }
3321
+ // } else if (get.call(this)) {
3322
+ // //
3323
+ // // Setting a field to `null` or `undefined` will delete it.
3324
+ // //
3325
+ // this[$changes].delete(field);
3326
+ // }
3327
+ // set.call(this, value);
3328
+ // },
3329
+ // };
3330
+ // }
3331
+ // }
3332
+ function view(tag = DEFAULT_VIEW_TAG) {
3333
+ return function (target, fieldName) {
3334
+ const constructor = target.constructor;
3335
+ const parentClass = Object.getPrototypeOf(constructor);
3336
+ const parentMetadata = parentClass[Symbol.metadata];
3337
+ // TODO: use Metadata.initialize()
3338
+ const metadata = (constructor[Symbol.metadata] ??= Object.assign({}, constructor[Symbol.metadata], parentMetadata ?? Object.create(null)));
3339
+ // const fieldIndex = metadata[fieldName];
3340
+ // if (!metadata[fieldIndex]) {
3341
+ // //
3342
+ // // detect index for this field, considering inheritance
3343
+ // //
3344
+ // metadata[fieldIndex] = {
3345
+ // type: undefined,
3346
+ // index: (metadata[$numFields] // current structure already has fields defined
3347
+ // ?? (parentMetadata && parentMetadata[$numFields]) // parent structure has fields defined
3348
+ // ?? -1) + 1 // no fields defined
3349
+ // }
3350
+ // }
3351
+ Metadata.setTag(metadata, fieldName, tag);
3352
+ };
3353
+ }
3354
+ function type(type, options) {
3355
+ return function (target, field) {
3356
+ const constructor = target.constructor;
3357
+ if (!type) {
3358
+ throw new Error(`${constructor.name}: @type() reference provided for "${field}" is undefined. Make sure you don't have any circular dependencies.`);
3359
+ }
3360
+ // Normalize type (enum/collection/etc)
3361
+ type = getNormalizedType(type);
3362
+ // for inheritance support
3363
+ TypeContext.register(constructor);
3364
+ const parentClass = Object.getPrototypeOf(constructor);
3365
+ const parentMetadata = parentClass[Symbol.metadata];
3366
+ const metadata = Metadata.initialize(constructor);
3367
+ let fieldIndex = metadata[field];
3368
+ /**
3369
+ * skip if descriptor already exists for this field (`@deprecated()`)
3370
+ */
3371
+ if (metadata[fieldIndex] !== undefined) {
3372
+ if (metadata[fieldIndex].deprecated) {
3373
+ // do not create accessors for deprecated properties.
3374
+ return;
3375
+ }
3376
+ else if (metadata[fieldIndex].type !== undefined) {
3377
+ // trying to define same property multiple times across inheritance.
3378
+ // https://github.com/colyseus/colyseus-unity3d/issues/131#issuecomment-814308572
3379
+ try {
3380
+ throw new Error(`@colyseus/schema: Duplicate '${field}' definition on '${constructor.name}'.\nCheck @type() annotation`);
3381
+ }
3382
+ catch (e) {
3383
+ const definitionAtLine = e.stack.split("\n")[4].trim();
3384
+ throw new Error(`${e.message} ${definitionAtLine}`);
3385
+ }
3386
+ }
3387
+ }
3388
+ else {
3389
+ //
3390
+ // detect index for this field, considering inheritance
3391
+ //
3392
+ fieldIndex = metadata[$numFields] // current structure already has fields defined
3393
+ ?? (parentMetadata && parentMetadata[$numFields]) // parent structure has fields defined
3394
+ ?? -1; // no fields defined
3395
+ fieldIndex++;
3396
+ }
3397
+ if (options && options.manual) {
3398
+ Metadata.addField(metadata, fieldIndex, field, type, {
3399
+ // do not declare getter/setter descriptor
3400
+ enumerable: true,
3401
+ configurable: true,
3402
+ writable: true,
3403
+ });
3449
3404
  }
3450
- // display filtered changes
3451
- if (isEncodeAll &&
3452
- changeTree.allFilteredChanges &&
3453
- (changeTree.allFilteredChanges.operations).filter(op => op).length > 0) {
3454
- output += `${instance.constructor.name} (${changeTree.refId}) -> .allFilteredChanges:\n`;
3455
- dumpChangeSet(changeTree.allFilteredChanges);
3405
+ else {
3406
+ const complexTypeKlass = typeof (Object.keys(type)[0]) === "string" && getType(Object.keys(type)[0]);
3407
+ const childType = (complexTypeKlass)
3408
+ ? Object.values(type)[0]
3409
+ : type;
3410
+ Metadata.addField(metadata, fieldIndex, field, type, getPropertyDescriptor(`_${field}`, fieldIndex, childType, complexTypeKlass));
3456
3411
  }
3457
- return output;
3458
- }
3459
- static debugChangesDeep(ref, changeSetName = "changes") {
3460
- let output = "";
3461
- const rootChangeTree = ref[$changes];
3462
- const root = rootChangeTree.root;
3463
- const changeTrees = new Map();
3464
- const instanceRefIds = [];
3465
- let totalOperations = 0;
3466
- // TODO: FIXME: this method is not working as expected
3467
- for (const [refId, changes] of Object.entries(root[changeSetName])) {
3468
- const changeTree = root.changeTrees[refId];
3469
- if (!changeTree) {
3470
- continue;
3471
- }
3472
- let includeChangeTree = false;
3473
- let parentChangeTrees = [];
3474
- let parentChangeTree = changeTree.parent?.[$changes];
3475
- if (changeTree === rootChangeTree) {
3476
- includeChangeTree = true;
3412
+ };
3413
+ }
3414
+ function getPropertyDescriptor(fieldCached, fieldIndex, type, complexTypeKlass) {
3415
+ return {
3416
+ get: function () { return this[fieldCached]; },
3417
+ set: function (value) {
3418
+ const previousValue = this[fieldCached] ?? undefined;
3419
+ // skip if value is the same as cached.
3420
+ if (value === previousValue) {
3421
+ return;
3477
3422
  }
3478
- else {
3479
- while (parentChangeTree !== undefined) {
3480
- parentChangeTrees.push(parentChangeTree);
3481
- if (parentChangeTree.ref === ref) {
3482
- includeChangeTree = true;
3483
- break;
3423
+ if (value !== undefined &&
3424
+ value !== null) {
3425
+ if (complexTypeKlass) {
3426
+ // automaticallty transform Array into ArraySchema
3427
+ if (complexTypeKlass.constructor === ArraySchema && !(value instanceof ArraySchema)) {
3428
+ value = new ArraySchema(...value);
3484
3429
  }
3485
- parentChangeTree = parentChangeTree.parent?.[$changes];
3430
+ // automaticallty transform Map into MapSchema
3431
+ if (complexTypeKlass.constructor === MapSchema && !(value instanceof MapSchema)) {
3432
+ value = new MapSchema(value);
3433
+ }
3434
+ value[$childType] = type;
3486
3435
  }
3487
- }
3488
- if (includeChangeTree) {
3489
- instanceRefIds.push(changeTree.refId);
3490
- totalOperations += Object.keys(changes).length;
3491
- changeTrees.set(changeTree, parentChangeTrees.reverse());
3492
- }
3493
- }
3494
- output += "---\n";
3495
- output += `root refId: ${rootChangeTree.refId}\n`;
3496
- output += `Total instances: ${instanceRefIds.length} (refIds: ${instanceRefIds.join(", ")})\n`;
3497
- output += `Total changes: ${totalOperations}\n`;
3498
- output += "---\n";
3499
- // based on root.changes, display a tree of changes that has the "ref" instance as parent
3500
- const visitedParents = new WeakSet();
3501
- for (const [changeTree, parentChangeTrees] of changeTrees.entries()) {
3502
- parentChangeTrees.forEach((parentChangeTree, level) => {
3503
- if (!visitedParents.has(parentChangeTree)) {
3504
- output += `${getIndent(level)}${parentChangeTree.ref.constructor.name} (refId: ${parentChangeTree.refId})\n`;
3505
- visitedParents.add(parentChangeTree);
3436
+ else if (typeof (type) !== "string") {
3437
+ assertInstanceType(value, type, this, fieldCached.substring(1));
3506
3438
  }
3507
- });
3508
- const changes = changeTree.indexedOperations;
3509
- const level = parentChangeTrees.length;
3510
- const indent = getIndent(level);
3511
- const parentIndex = (level > 0) ? `(${changeTree.parentIndex}) ` : "";
3512
- output += `${indent}${parentIndex}${changeTree.ref.constructor.name} (refId: ${changeTree.refId}) - changes: ${Object.keys(changes).length}\n`;
3513
- for (const index in changes) {
3514
- const operation = changes[index];
3515
- output += `${getIndent(level + 1)}${exports.OPERATION[operation]}: ${index}\n`;
3439
+ else {
3440
+ assertType(value, type, this, fieldCached.substring(1));
3441
+ }
3442
+ const changeTree = this[$changes];
3443
+ //
3444
+ // Replacing existing "ref", remove it from root.
3445
+ //
3446
+ if (previousValue !== undefined && previousValue[$changes]) {
3447
+ changeTree.root?.remove(previousValue[$changes]);
3448
+ this.constructor[$track](changeTree, fieldIndex, exports.OPERATION.DELETE_AND_ADD);
3449
+ }
3450
+ else {
3451
+ this.constructor[$track](changeTree, fieldIndex, exports.OPERATION.ADD);
3452
+ }
3453
+ //
3454
+ // call setParent() recursively for this and its child
3455
+ // structures.
3456
+ //
3457
+ value[$changes]?.setParent(this, changeTree.root, fieldIndex);
3516
3458
  }
3517
- }
3518
- return `${output}`;
3519
- }
3459
+ else if (previousValue !== undefined) {
3460
+ //
3461
+ // Setting a field to `null` or `undefined` will delete it.
3462
+ //
3463
+ this[$changes].delete(fieldIndex);
3464
+ }
3465
+ this[fieldCached] = value;
3466
+ },
3467
+ enumerable: true,
3468
+ configurable: true
3469
+ };
3520
3470
  }
3521
-
3522
- var _a$1, _b$1;
3523
- class CollectionSchema {
3524
- static { this[_a$1] = encodeKeyValueOperation; }
3525
- static { this[_b$1] = decodeKeyValueOperation; }
3526
- /**
3527
- * Determine if a property must be filtered.
3528
- * - If returns false, the property is NOT going to be encoded.
3529
- * - If returns true, the property is going to be encoded.
3530
- *
3531
- * Encoding with "filters" happens in two steps:
3532
- * - First, the encoder iterates over all "not owned" properties and encodes them.
3533
- * - Then, the encoder iterates over all "owned" properties per instance and encodes them.
3534
- */
3535
- static [(_a$1 = $encoder, _b$1 = $decoder, $filter)](ref, index, view) {
3536
- return (!view ||
3537
- typeof (ref[$childType]) === "string" ||
3538
- view.isChangeTreeVisible((ref[$getByIndex](index) ?? ref.deletedItems[index])[$changes]));
3539
- }
3540
- static is(type) {
3541
- return type['collection'] !== undefined;
3542
- }
3543
- constructor(initialValues) {
3544
- this.$items = new Map();
3545
- this.$indexes = new Map();
3546
- this.deletedItems = {};
3547
- this.$refId = 0;
3548
- this[$changes] = new ChangeTree(this);
3549
- this[$changes].indexes = {};
3550
- if (initialValues) {
3551
- initialValues.forEach((v) => this.add(v));
3471
+ /**
3472
+ * `@deprecated()` flag a field as deprecated.
3473
+ * The previous `@type()` annotation should remain along with this one.
3474
+ */
3475
+ function deprecated(throws = true) {
3476
+ return function (klass, field) {
3477
+ //
3478
+ // FIXME: the following block of code is repeated across `@type()`, `@deprecated()` and `@unreliable()` decorators.
3479
+ //
3480
+ const constructor = klass.constructor;
3481
+ const parentClass = Object.getPrototypeOf(constructor);
3482
+ const parentMetadata = parentClass[Symbol.metadata];
3483
+ const metadata = (constructor[Symbol.metadata] ??= Object.assign({}, constructor[Symbol.metadata], parentMetadata ?? Object.create(null)));
3484
+ const fieldIndex = metadata[field];
3485
+ // if (!metadata[field]) {
3486
+ // //
3487
+ // // detect index for this field, considering inheritance
3488
+ // //
3489
+ // metadata[field] = {
3490
+ // type: undefined,
3491
+ // index: (metadata[$numFields] // current structure already has fields defined
3492
+ // ?? (parentMetadata && parentMetadata[$numFields]) // parent structure has fields defined
3493
+ // ?? -1) + 1 // no fields defined
3494
+ // }
3495
+ // }
3496
+ metadata[fieldIndex].deprecated = true;
3497
+ if (throws) {
3498
+ metadata[$descriptors] ??= {};
3499
+ metadata[$descriptors][field] = {
3500
+ get: function () { throw new Error(`${field} is deprecated.`); },
3501
+ set: function (value) { },
3502
+ enumerable: false,
3503
+ configurable: true
3504
+ };
3552
3505
  }
3553
- Object.defineProperty(this, $childType, {
3554
- value: undefined,
3506
+ // flag metadata[field] as non-enumerable
3507
+ Object.defineProperty(metadata, fieldIndex, {
3508
+ value: metadata[fieldIndex],
3555
3509
  enumerable: false,
3556
- writable: true,
3557
- configurable: true,
3510
+ configurable: true
3558
3511
  });
3512
+ };
3513
+ }
3514
+ function defineTypes(target, fields, options) {
3515
+ for (let field in fields) {
3516
+ type(fields[field], options)(target.prototype, field);
3559
3517
  }
3560
- add(value) {
3561
- // set "index" for reference.
3562
- const index = this.$refId++;
3563
- const isRef = (value[$changes]) !== undefined;
3564
- if (isRef) {
3565
- value[$changes].setParent(this, this[$changes].root, index);
3566
- }
3567
- this[$changes].indexes[index] = index;
3568
- this.$indexes.set(index, index);
3569
- this.$items.set(index, value);
3570
- this[$changes].change(index);
3571
- return index;
3572
- }
3573
- at(index) {
3574
- const key = Array.from(this.$items.keys())[index];
3575
- return this.$items.get(key);
3576
- }
3577
- entries() {
3578
- return this.$items.entries();
3579
- }
3580
- delete(item) {
3581
- const entries = this.$items.entries();
3582
- let index;
3583
- let entry;
3584
- while (entry = entries.next()) {
3585
- if (entry.done) {
3586
- break;
3518
+ return target;
3519
+ }
3520
+ function schema(fieldsAndMethods, name, inherits = Schema) {
3521
+ const fields = {};
3522
+ const methods = {};
3523
+ const defaultValues = {};
3524
+ const viewTagFields = {};
3525
+ for (let fieldName in fieldsAndMethods) {
3526
+ const value = fieldsAndMethods[fieldName];
3527
+ if (typeof (value) === "object") {
3528
+ if (value['view'] !== undefined) {
3529
+ viewTagFields[fieldName] = (typeof (value['view']) === "boolean")
3530
+ ? DEFAULT_VIEW_TAG
3531
+ : value['view'];
3587
3532
  }
3588
- if (item === entry.value[1]) {
3589
- index = entry.value[0];
3590
- break;
3533
+ fields[fieldName] = getNormalizedType(value);
3534
+ // If no explicit default provided, handle automatic instantiation for collection types
3535
+ if (!Object.prototype.hasOwnProperty.call(value, 'default')) {
3536
+ // TODO: remove Array.isArray() check. Use ['array'] !== undefined only.
3537
+ if (Array.isArray(value) || value['array'] !== undefined) {
3538
+ // Collection: Array → new ArraySchema()
3539
+ defaultValues[fieldName] = new ArraySchema();
3540
+ }
3541
+ else if (value['map'] !== undefined) {
3542
+ // Collection: Map → new MapSchema()
3543
+ defaultValues[fieldName] = new MapSchema();
3544
+ }
3545
+ else if (value['collection'] !== undefined) {
3546
+ // Collection: Collection → new CollectionSchema()
3547
+ defaultValues[fieldName] = new CollectionSchema();
3548
+ }
3549
+ else if (value['set'] !== undefined) {
3550
+ // Collection: Set → new SetSchema()
3551
+ defaultValues[fieldName] = new SetSchema();
3552
+ }
3553
+ else if (value['type'] !== undefined && Schema.is(value['type'])) {
3554
+ // Direct Schema type: Type → new Type()
3555
+ defaultValues[fieldName] = new value['type']();
3556
+ }
3557
+ }
3558
+ else {
3559
+ defaultValues[fieldName] = value['default'];
3560
+ }
3561
+ }
3562
+ else if (typeof (value) === "function") {
3563
+ if (Schema.is(value)) {
3564
+ // Direct Schema type: Type → new Type()
3565
+ defaultValues[fieldName] = new value();
3566
+ fields[fieldName] = getNormalizedType(value);
3567
+ }
3568
+ else {
3569
+ methods[fieldName] = value;
3591
3570
  }
3592
3571
  }
3593
- if (index === undefined) {
3594
- return false;
3572
+ else {
3573
+ fields[fieldName] = getNormalizedType(value);
3595
3574
  }
3596
- this.deletedItems[index] = this[$changes].delete(index);
3597
- this.$indexes.delete(index);
3598
- return this.$items.delete(index);
3599
- }
3600
- clear() {
3601
- const changeTree = this[$changes];
3602
- // discard previous operations.
3603
- changeTree.discard(true);
3604
- changeTree.indexes = {};
3605
- // remove children references
3606
- changeTree.forEachChild((childChangeTree, _) => {
3607
- changeTree.root?.remove(childChangeTree);
3608
- });
3609
- // clear previous indexes
3610
- this.$indexes.clear();
3611
- // clear items
3612
- this.$items.clear();
3613
- changeTree.operation(exports.OPERATION.CLEAR);
3614
- }
3615
- has(value) {
3616
- return Array.from(this.$items.values()).some((v) => v === value);
3617
- }
3618
- forEach(callbackfn) {
3619
- this.$items.forEach((value, key, _) => callbackfn(value, key, this));
3620
- }
3621
- values() {
3622
- return this.$items.values();
3623
3575
  }
3624
- get size() {
3625
- return this.$items.size;
3626
- }
3627
- /** Iterator */
3628
- [Symbol.iterator]() {
3629
- return this.$items.values();
3630
- }
3631
- setIndex(index, key) {
3632
- this.$indexes.set(index, key);
3633
- }
3634
- getIndex(index) {
3635
- return this.$indexes.get(index);
3636
- }
3637
- [$getByIndex](index) {
3638
- return this.$items.get(this.$indexes.get(index));
3639
- }
3640
- [$deleteByIndex](index) {
3641
- const key = this.$indexes.get(index);
3642
- this.$items.delete(key);
3643
- this.$indexes.delete(index);
3644
- }
3645
- [$onEncodeEnd]() {
3646
- this.deletedItems = {};
3576
+ const getDefaultValues = () => {
3577
+ const defaults = {};
3578
+ for (const fieldName in defaultValues) {
3579
+ const defaultValue = defaultValues[fieldName];
3580
+ // If the default value has a clone method, use it to get a fresh instance
3581
+ if (defaultValue && typeof defaultValue.clone === 'function') {
3582
+ defaults[fieldName] = defaultValue.clone();
3583
+ }
3584
+ else {
3585
+ // Otherwise, use the value as-is (for primitives and non-cloneable objects)
3586
+ defaults[fieldName] = defaultValue;
3587
+ }
3588
+ }
3589
+ return defaults;
3590
+ };
3591
+ const klass = Metadata.setFields(class extends inherits {
3592
+ constructor(...args) {
3593
+ args[0] = Object.assign({}, getDefaultValues(), args[0]);
3594
+ super(...args);
3595
+ }
3596
+ }, fields);
3597
+ for (let fieldName in viewTagFields) {
3598
+ view(viewTagFields[fieldName])(klass.prototype, fieldName);
3647
3599
  }
3648
- toArray() {
3649
- return Array.from(this.$items.values());
3600
+ for (let methodName in methods) {
3601
+ klass.prototype[methodName] = methods[methodName];
3650
3602
  }
3651
- toJSON() {
3652
- const values = [];
3653
- this.forEach((value, key) => {
3654
- values.push((typeof (value['toJSON']) === "function")
3655
- ? value['toJSON']()
3656
- : value);
3657
- });
3658
- return values;
3603
+ if (name) {
3604
+ Object.defineProperty(klass, "name", { value: name });
3659
3605
  }
3660
- //
3661
- // Decoding utilities
3662
- //
3663
- clone(isDecoding) {
3664
- let cloned;
3665
- if (isDecoding) {
3666
- // client-side
3667
- cloned = Object.assign(new CollectionSchema(), this);
3606
+ klass.extends = (fields, name) => schema(fields, name, klass);
3607
+ return klass;
3608
+ }
3609
+
3610
+ function getIndent(level) {
3611
+ return (new Array(level).fill(0)).map((_, i) => (i === level - 1) ? `└─ ` : ` `).join("");
3612
+ }
3613
+ function dumpChanges(schema) {
3614
+ const $root = schema[$changes].root;
3615
+ const dump = {
3616
+ ops: {},
3617
+ refs: []
3618
+ };
3619
+ // for (const refId in $root.changes) {
3620
+ let current = $root.changes.next;
3621
+ while (current) {
3622
+ const changeTree = current.changeTree;
3623
+ // skip if ChangeTree is undefined
3624
+ if (changeTree === undefined) {
3625
+ current = current.next;
3626
+ continue;
3668
3627
  }
3669
- else {
3670
- // server-side
3671
- cloned = new CollectionSchema();
3672
- this.forEach((value) => {
3673
- if (value[$changes]) {
3674
- cloned.add(value['clone']());
3675
- }
3676
- else {
3677
- cloned.add(value);
3678
- }
3679
- });
3628
+ const changes = changeTree.indexedOperations;
3629
+ dump.refs.push(`refId#${changeTree.refId}`);
3630
+ for (const index in changes) {
3631
+ const op = changes[index];
3632
+ const opName = exports.OPERATION[op];
3633
+ if (!dump.ops[opName]) {
3634
+ dump.ops[opName] = 0;
3635
+ }
3636
+ dump.ops[exports.OPERATION[op]]++;
3680
3637
  }
3681
- return cloned;
3638
+ current = current.next;
3682
3639
  }
3640
+ return dump;
3683
3641
  }
3684
- registerType("collection", { constructor: CollectionSchema, });
3685
3642
 
3686
3643
  var _a, _b;
3687
- class SetSchema {
3688
- static { this[_a] = encodeKeyValueOperation; }
3689
- static { this[_b] = decodeKeyValueOperation; }
3644
+ /**
3645
+ * Schema encoder / decoder
3646
+ */
3647
+ class Schema {
3648
+ static { this[_a] = encodeSchemaOperation; }
3649
+ static { this[_b] = decodeSchemaOperation; }
3650
+ /**
3651
+ * Assign the property descriptors required to track changes on this instance.
3652
+ * @param instance
3653
+ */
3654
+ static initialize(instance) {
3655
+ Object.defineProperty(instance, $changes, {
3656
+ value: new ChangeTree(instance),
3657
+ enumerable: false,
3658
+ writable: true
3659
+ });
3660
+ Object.defineProperties(instance, instance.constructor[Symbol.metadata]?.[$descriptors] || {});
3661
+ }
3662
+ static is(type) {
3663
+ return typeof (type[Symbol.metadata]) === "object";
3664
+ // const metadata = type[Symbol.metadata];
3665
+ // return metadata && Object.prototype.hasOwnProperty.call(metadata, -1);
3666
+ }
3667
+ /**
3668
+ * Track property changes
3669
+ */
3670
+ static [(_a = $encoder, _b = $decoder, $track)](changeTree, index, operation = exports.OPERATION.ADD) {
3671
+ changeTree.change(index, operation);
3672
+ }
3690
3673
  /**
3691
3674
  * Determine if a property must be filtered.
3692
3675
  * - If returns false, the property is NOT going to be encoded.
@@ -3696,164 +3679,248 @@
3696
3679
  * - First, the encoder iterates over all "not owned" properties and encodes them.
3697
3680
  * - Then, the encoder iterates over all "owned" properties per instance and encodes them.
3698
3681
  */
3699
- static [(_a = $encoder, _b = $decoder, $filter)](ref, index, view) {
3700
- return (!view ||
3701
- typeof (ref[$childType]) === "string" ||
3702
- view.visible.has((ref[$getByIndex](index) ?? ref.deletedItems[index])[$changes]));
3703
- }
3704
- static is(type) {
3705
- return type['set'] !== undefined;
3706
- }
3707
- constructor(initialValues) {
3708
- this.$items = new Map();
3709
- this.$indexes = new Map();
3710
- this.deletedItems = {};
3711
- this.$refId = 0;
3712
- this[$changes] = new ChangeTree(this);
3713
- this[$changes].indexes = {};
3714
- if (initialValues) {
3715
- initialValues.forEach((v) => this.add(v));
3716
- }
3717
- Object.defineProperty(this, $childType, {
3718
- value: undefined,
3719
- enumerable: false,
3720
- writable: true,
3721
- configurable: true,
3722
- });
3723
- }
3724
- add(value) {
3725
- // immediatelly return false if value already added.
3726
- if (this.has(value)) {
3727
- return false;
3728
- }
3729
- // set "index" for reference.
3730
- const index = this.$refId++;
3731
- if ((value[$changes]) !== undefined) {
3732
- value[$changes].setParent(this, this[$changes].root, index);
3733
- }
3734
- const operation = this[$changes].indexes[index]?.op ?? exports.OPERATION.ADD;
3735
- this[$changes].indexes[index] = index;
3736
- this.$indexes.set(index, index);
3737
- this.$items.set(index, value);
3738
- this[$changes].change(index, operation);
3739
- return index;
3740
- }
3741
- entries() {
3742
- return this.$items.entries();
3743
- }
3744
- delete(item) {
3745
- const entries = this.$items.entries();
3746
- let index;
3747
- let entry;
3748
- while (entry = entries.next()) {
3749
- if (entry.done) {
3750
- break;
3751
- }
3752
- if (item === entry.value[1]) {
3753
- index = entry.value[0];
3754
- break;
3755
- }
3682
+ static [$filter](ref, index, view) {
3683
+ const metadata = ref.constructor[Symbol.metadata];
3684
+ const tag = metadata[index]?.tag;
3685
+ if (view === undefined) {
3686
+ // shared pass/encode: encode if doesn't have a tag
3687
+ return tag === undefined;
3756
3688
  }
3757
- if (index === undefined) {
3758
- return false;
3689
+ else if (tag === undefined) {
3690
+ // view pass: no tag
3691
+ return true;
3759
3692
  }
3760
- this.deletedItems[index] = this[$changes].delete(index);
3761
- this.$indexes.delete(index);
3762
- return this.$items.delete(index);
3763
- }
3764
- clear() {
3765
- const changeTree = this[$changes];
3766
- // discard previous operations.
3767
- changeTree.discard(true);
3768
- changeTree.indexes = {};
3769
- // clear previous indexes
3770
- this.$indexes.clear();
3771
- // clear items
3772
- this.$items.clear();
3773
- changeTree.operation(exports.OPERATION.CLEAR);
3774
- }
3775
- has(value) {
3776
- const values = this.$items.values();
3777
- let has = false;
3778
- let entry;
3779
- while (entry = values.next()) {
3780
- if (entry.done) {
3781
- break;
3782
- }
3783
- if (value === entry.value) {
3784
- has = true;
3785
- break;
3786
- }
3693
+ else if (tag === DEFAULT_VIEW_TAG) {
3694
+ // view pass: default tag
3695
+ return view.isChangeTreeVisible(ref[$changes]);
3696
+ }
3697
+ else {
3698
+ // view pass: custom tag
3699
+ const tags = view.tags?.get(ref[$changes]);
3700
+ return tags && tags.has(tag);
3787
3701
  }
3788
- return has;
3789
3702
  }
3790
- forEach(callbackfn) {
3791
- this.$items.forEach((value, key, _) => callbackfn(value, key, this));
3703
+ // allow inherited classes to have a constructor
3704
+ constructor(...args) {
3705
+ //
3706
+ // inline
3707
+ // Schema.initialize(this);
3708
+ //
3709
+ Schema.initialize(this);
3710
+ //
3711
+ // Assign initial values
3712
+ //
3713
+ if (args[0]) {
3714
+ Object.assign(this, args[0]);
3715
+ }
3792
3716
  }
3793
- values() {
3794
- return this.$items.values();
3717
+ assign(props) {
3718
+ Object.assign(this, props);
3719
+ return this;
3795
3720
  }
3796
- get size() {
3797
- return this.$items.size;
3721
+ /**
3722
+ * (Server-side): Flag a property to be encoded for the next patch.
3723
+ * @param instance Schema instance
3724
+ * @param property string representing the property name, or number representing the index of the property.
3725
+ * @param operation OPERATION to perform (detected automatically)
3726
+ */
3727
+ setDirty(property, operation) {
3728
+ const metadata = this.constructor[Symbol.metadata];
3729
+ this[$changes].change(metadata[metadata[property]].index, operation);
3798
3730
  }
3799
- /** Iterator */
3800
- [Symbol.iterator]() {
3801
- return this.$items.values();
3731
+ clone() {
3732
+ const cloned = new (this.constructor);
3733
+ const metadata = this.constructor[Symbol.metadata];
3734
+ //
3735
+ // TODO: clone all properties, not only annotated ones
3736
+ //
3737
+ // for (const field in this) {
3738
+ for (const fieldIndex in metadata) {
3739
+ // const field = metadata[metadata[fieldIndex]].name;
3740
+ const field = metadata[fieldIndex].name;
3741
+ if (typeof (this[field]) === "object" &&
3742
+ typeof (this[field]?.clone) === "function") {
3743
+ // deep clone
3744
+ cloned[field] = this[field].clone();
3745
+ }
3746
+ else {
3747
+ // primitive values
3748
+ cloned[field] = this[field];
3749
+ }
3750
+ }
3751
+ return cloned;
3802
3752
  }
3803
- setIndex(index, key) {
3804
- this.$indexes.set(index, key);
3753
+ toJSON() {
3754
+ const obj = {};
3755
+ const metadata = this.constructor[Symbol.metadata];
3756
+ for (const index in metadata) {
3757
+ const field = metadata[index];
3758
+ const fieldName = field.name;
3759
+ if (!field.deprecated && this[fieldName] !== null && typeof (this[fieldName]) !== "undefined") {
3760
+ obj[fieldName] = (typeof (this[fieldName]['toJSON']) === "function")
3761
+ ? this[fieldName]['toJSON']()
3762
+ : this[fieldName];
3763
+ }
3764
+ }
3765
+ return obj;
3805
3766
  }
3806
- getIndex(index) {
3807
- return this.$indexes.get(index);
3767
+ /**
3768
+ * Used in tests only
3769
+ * @internal
3770
+ */
3771
+ discardAllChanges() {
3772
+ this[$changes].discardAll();
3808
3773
  }
3809
3774
  [$getByIndex](index) {
3810
- return this.$items.get(this.$indexes.get(index));
3775
+ const metadata = this.constructor[Symbol.metadata];
3776
+ return this[metadata[index].name];
3811
3777
  }
3812
3778
  [$deleteByIndex](index) {
3813
- const key = this.$indexes.get(index);
3814
- this.$items.delete(key);
3815
- this.$indexes.delete(index);
3779
+ const metadata = this.constructor[Symbol.metadata];
3780
+ this[metadata[index].name] = undefined;
3816
3781
  }
3817
- [$onEncodeEnd]() {
3818
- this.deletedItems = {};
3782
+ /**
3783
+ * Inspect the `refId` of all Schema instances in the tree. Optionally display the contents of the instance.
3784
+ *
3785
+ * @param ref Schema instance
3786
+ * @param showContents display JSON contents of the instance
3787
+ * @returns
3788
+ */
3789
+ static debugRefIds(ref, showContents = false, level = 0, decoder, keyPrefix = "") {
3790
+ const contents = (showContents) ? ` - ${JSON.stringify(ref.toJSON())}` : "";
3791
+ const changeTree = ref[$changes];
3792
+ const refId = (decoder) ? decoder.root.refIds.get(ref) : changeTree.refId;
3793
+ const root = (decoder) ? decoder.root : changeTree.root;
3794
+ // log reference count if > 1
3795
+ const refCount = (root?.refCount?.[refId] > 1)
3796
+ ? ` [×${root.refCount[refId]}]`
3797
+ : '';
3798
+ let output = `${getIndent(level)}${keyPrefix}${ref.constructor.name} (refId: ${refId})${refCount}${contents}\n`;
3799
+ changeTree.forEachChild((childChangeTree, indexOrKey) => {
3800
+ let key = indexOrKey;
3801
+ if (typeof indexOrKey === 'number' && ref['$indexes']) {
3802
+ // MapSchema
3803
+ key = ref['$indexes'].get(indexOrKey) ?? indexOrKey;
3804
+ }
3805
+ const keyPrefix = (ref['forEach'] !== undefined && key !== undefined) ? `["${key}"]: ` : "";
3806
+ output += this.debugRefIds(childChangeTree.ref, showContents, level + 1, decoder, keyPrefix);
3807
+ });
3808
+ return output;
3819
3809
  }
3820
- toArray() {
3821
- return Array.from(this.$items.values());
3810
+ static debugRefIdEncodingOrder(ref, changeSet = 'allChanges') {
3811
+ let encodeOrder = [];
3812
+ let current = ref[$changes].root[changeSet].next;
3813
+ while (current) {
3814
+ if (current.changeTree) {
3815
+ encodeOrder.push(current.changeTree.refId);
3816
+ }
3817
+ current = current.next;
3818
+ }
3819
+ return encodeOrder;
3822
3820
  }
3823
- toJSON() {
3824
- const values = [];
3825
- this.forEach((value, key) => {
3826
- values.push((typeof (value['toJSON']) === "function")
3827
- ? value['toJSON']()
3828
- : value);
3829
- });
3830
- return values;
3821
+ static debugRefIdsFromDecoder(decoder) {
3822
+ return this.debugRefIds(decoder.state, false, 0, decoder);
3831
3823
  }
3832
- //
3833
- // Decoding utilities
3834
- //
3835
- clone(isDecoding) {
3836
- let cloned;
3837
- if (isDecoding) {
3838
- // client-side
3839
- cloned = Object.assign(new SetSchema(), this);
3824
+ /**
3825
+ * Return a string representation of the changes on a Schema instance.
3826
+ * The list of changes is cleared after each encode.
3827
+ *
3828
+ * @param instance Schema instance
3829
+ * @param isEncodeAll Return "full encode" instead of current change set.
3830
+ * @returns
3831
+ */
3832
+ static debugChanges(instance, isEncodeAll = false) {
3833
+ const changeTree = instance[$changes];
3834
+ const changeSet = (isEncodeAll) ? changeTree.allChanges : changeTree.changes;
3835
+ const changeSetName = (isEncodeAll) ? "allChanges" : "changes";
3836
+ let output = `${instance.constructor.name} (${changeTree.refId}) -> .${changeSetName}:\n`;
3837
+ function dumpChangeSet(changeSet) {
3838
+ changeSet.operations
3839
+ .filter(op => op)
3840
+ .forEach((index) => {
3841
+ const operation = changeTree.indexedOperations[index];
3842
+ output += `- [${index}]: ${exports.OPERATION[operation]} (${JSON.stringify(changeTree.getValue(Number(index), isEncodeAll))})\n`;
3843
+ });
3840
3844
  }
3841
- else {
3842
- // server-side
3843
- cloned = new SetSchema();
3844
- this.forEach((value) => {
3845
- if (value[$changes]) {
3846
- cloned.add(value['clone']());
3845
+ dumpChangeSet(changeSet);
3846
+ // display filtered changes
3847
+ if (!isEncodeAll &&
3848
+ changeTree.filteredChanges &&
3849
+ (changeTree.filteredChanges.operations).filter(op => op).length > 0) {
3850
+ output += `${instance.constructor.name} (${changeTree.refId}) -> .filteredChanges:\n`;
3851
+ dumpChangeSet(changeTree.filteredChanges);
3852
+ }
3853
+ // display filtered changes
3854
+ if (isEncodeAll &&
3855
+ changeTree.allFilteredChanges &&
3856
+ (changeTree.allFilteredChanges.operations).filter(op => op).length > 0) {
3857
+ output += `${instance.constructor.name} (${changeTree.refId}) -> .allFilteredChanges:\n`;
3858
+ dumpChangeSet(changeTree.allFilteredChanges);
3859
+ }
3860
+ return output;
3861
+ }
3862
+ static debugChangesDeep(ref, changeSetName = "changes") {
3863
+ let output = "";
3864
+ const rootChangeTree = ref[$changes];
3865
+ const root = rootChangeTree.root;
3866
+ const changeTrees = new Map();
3867
+ const instanceRefIds = [];
3868
+ let totalOperations = 0;
3869
+ // TODO: FIXME: this method is not working as expected
3870
+ for (const [refId, changes] of Object.entries(root[changeSetName])) {
3871
+ const changeTree = root.changeTrees[refId];
3872
+ if (!changeTree) {
3873
+ continue;
3874
+ }
3875
+ let includeChangeTree = false;
3876
+ let parentChangeTrees = [];
3877
+ let parentChangeTree = changeTree.parent?.[$changes];
3878
+ if (changeTree === rootChangeTree) {
3879
+ includeChangeTree = true;
3880
+ }
3881
+ else {
3882
+ while (parentChangeTree !== undefined) {
3883
+ parentChangeTrees.push(parentChangeTree);
3884
+ if (parentChangeTree.ref === ref) {
3885
+ includeChangeTree = true;
3886
+ break;
3887
+ }
3888
+ parentChangeTree = parentChangeTree.parent?.[$changes];
3847
3889
  }
3848
- else {
3849
- cloned.add(value);
3890
+ }
3891
+ if (includeChangeTree) {
3892
+ instanceRefIds.push(changeTree.refId);
3893
+ totalOperations += Object.keys(changes).length;
3894
+ changeTrees.set(changeTree, parentChangeTrees.reverse());
3895
+ }
3896
+ }
3897
+ output += "---\n";
3898
+ output += `root refId: ${rootChangeTree.refId}\n`;
3899
+ output += `Total instances: ${instanceRefIds.length} (refIds: ${instanceRefIds.join(", ")})\n`;
3900
+ output += `Total changes: ${totalOperations}\n`;
3901
+ output += "---\n";
3902
+ // based on root.changes, display a tree of changes that has the "ref" instance as parent
3903
+ const visitedParents = new WeakSet();
3904
+ for (const [changeTree, parentChangeTrees] of changeTrees.entries()) {
3905
+ parentChangeTrees.forEach((parentChangeTree, level) => {
3906
+ if (!visitedParents.has(parentChangeTree)) {
3907
+ output += `${getIndent(level)}${parentChangeTree.ref.constructor.name} (refId: ${parentChangeTree.refId})\n`;
3908
+ visitedParents.add(parentChangeTree);
3850
3909
  }
3851
3910
  });
3911
+ const changes = changeTree.indexedOperations;
3912
+ const level = parentChangeTrees.length;
3913
+ const indent = getIndent(level);
3914
+ const parentIndex = (level > 0) ? `(${changeTree.parentIndex}) ` : "";
3915
+ output += `${indent}${parentIndex}${changeTree.ref.constructor.name} (refId: ${changeTree.refId}) - changes: ${Object.keys(changes).length}\n`;
3916
+ for (const index in changes) {
3917
+ const operation = changes[index];
3918
+ output += `${getIndent(level + 1)}${exports.OPERATION[operation]}: ${index}\n`;
3919
+ }
3852
3920
  }
3853
- return cloned;
3921
+ return `${output}`;
3854
3922
  }
3855
3923
  }
3856
- registerType("set", { constructor: SetSchema });
3857
3924
 
3858
3925
  /******************************************************************************
3859
3926
  Copyright (c) Microsoft Corporation.