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