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