@colyseus/schema 3.0.52 → 3.0.53

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