@colyseus/schema 2.0.31 → 3.0.0-alpha.0
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.
- package/build/cjs/index.js +3614 -2634
- package/build/cjs/index.js.map +1 -1
- package/build/esm/index.mjs +3324 -2445
- package/build/esm/index.mjs.map +1 -1
- package/build/umd/index.js +3614 -2634
- package/lib/Decoder.d.ts +16 -0
- package/lib/Decoder.js +182 -0
- package/lib/Decoder.js.map +1 -0
- package/lib/Encoder.d.ts +13 -0
- package/lib/Encoder.js +79 -0
- package/lib/Encoder.js.map +1 -0
- package/lib/Metadata.d.ts +36 -0
- package/lib/Metadata.js +91 -0
- package/lib/Metadata.js.map +1 -0
- package/lib/Reflection.d.ts +7 -5
- package/lib/Reflection.js +62 -58
- package/lib/Reflection.js.map +1 -1
- package/lib/Schema.d.ts +39 -51
- package/lib/Schema.js +189 -731
- package/lib/Schema.js.map +1 -1
- package/lib/annotations.d.ts +26 -45
- package/lib/annotations.js +363 -194
- package/lib/annotations.js.map +1 -1
- package/lib/changes/ChangeSet.d.ts +12 -0
- package/lib/changes/ChangeSet.js +35 -0
- package/lib/changes/ChangeSet.js.map +1 -0
- package/lib/changes/DecodeOperation.d.ts +15 -0
- package/lib/changes/DecodeOperation.js +186 -0
- package/lib/changes/DecodeOperation.js.map +1 -0
- package/lib/changes/EncodeOperation.d.ts +18 -0
- package/lib/changes/EncodeOperation.js +130 -0
- package/lib/changes/EncodeOperation.js.map +1 -0
- package/lib/changes/consts.d.ts +14 -0
- package/lib/changes/consts.js +18 -0
- package/lib/changes/consts.js.map +1 -0
- package/lib/decoder/DecodeOperation.d.ts +24 -0
- package/lib/decoder/DecodeOperation.js +256 -0
- package/lib/decoder/DecodeOperation.js.map +1 -0
- package/lib/decoder/Decoder.d.ts +21 -0
- package/lib/decoder/Decoder.js +114 -0
- package/lib/decoder/Decoder.js.map +1 -0
- package/lib/decoder/ReferenceTracker.d.ts +26 -0
- package/lib/decoder/ReferenceTracker.js +131 -0
- package/lib/decoder/ReferenceTracker.js.map +1 -0
- package/lib/decoder/strategy/RawChanges.d.ts +3 -0
- package/lib/decoder/strategy/RawChanges.js +8 -0
- package/lib/decoder/strategy/RawChanges.js.map +1 -0
- package/lib/decoder/strategy/StateCallbacks.d.ts +20 -0
- package/lib/decoder/strategy/StateCallbacks.js +240 -0
- package/lib/decoder/strategy/StateCallbacks.js.map +1 -0
- package/lib/decoding/decode.d.ts +48 -0
- package/lib/decoding/decode.js +267 -0
- package/lib/decoding/decode.js.map +1 -0
- package/lib/ecs.d.ts +11 -0
- package/lib/ecs.js +160 -0
- package/lib/ecs.js.map +1 -0
- package/lib/encoder/ChangeTree.d.ts +72 -0
- package/lib/encoder/ChangeTree.js +384 -0
- package/lib/encoder/ChangeTree.js.map +1 -0
- package/lib/encoder/EncodeOperation.d.ts +25 -0
- package/lib/encoder/EncodeOperation.js +156 -0
- package/lib/encoder/EncodeOperation.js.map +1 -0
- package/lib/encoder/Encoder.d.ts +23 -0
- package/lib/encoder/Encoder.js +192 -0
- package/lib/encoder/Encoder.js.map +1 -0
- package/lib/encoder/StateView.d.ts +21 -0
- package/lib/encoder/StateView.js +196 -0
- package/lib/encoder/StateView.js.map +1 -0
- package/lib/encoding/assert.d.ts +9 -0
- package/lib/encoding/assert.js +47 -0
- package/lib/encoding/assert.js.map +1 -0
- package/lib/encoding/decode.js +1 -1
- package/lib/encoding/decode.js.map +1 -1
- package/lib/encoding/encode.d.ts +17 -16
- package/lib/encoding/encode.js +88 -81
- package/lib/encoding/encode.js.map +1 -1
- package/lib/encoding/spec.d.ts +25 -0
- package/lib/encoding/spec.js +30 -0
- package/lib/encoding/spec.js.map +1 -0
- package/lib/index.d.ts +18 -10
- package/lib/index.js +39 -17
- package/lib/index.js.map +1 -1
- package/lib/symbol.shim.d.ts +6 -0
- package/lib/symbol.shim.js +4 -0
- package/lib/symbol.shim.js.map +1 -0
- package/lib/types/ArraySchema.js +0 -7
- package/lib/types/ArraySchema.js.map +1 -1
- package/lib/types/HelperTypes.d.ts +10 -2
- package/lib/types/HelperTypes.js.map +1 -1
- package/lib/types/custom/ArraySchema.d.ts +245 -0
- package/lib/types/custom/ArraySchema.js +659 -0
- package/lib/types/custom/ArraySchema.js.map +1 -0
- package/lib/types/custom/CollectionSchema.d.ts +42 -0
- package/lib/types/custom/CollectionSchema.js +165 -0
- package/lib/types/custom/CollectionSchema.js.map +1 -0
- package/lib/types/custom/MapSchema.d.ts +43 -0
- package/lib/types/custom/MapSchema.js +200 -0
- package/lib/types/custom/MapSchema.js.map +1 -0
- package/lib/types/custom/SetSchema.d.ts +39 -0
- package/lib/types/custom/SetSchema.js +177 -0
- package/lib/types/custom/SetSchema.js.map +1 -0
- package/lib/types/registry.d.ts +6 -0
- package/lib/types/registry.js +19 -0
- package/lib/types/registry.js.map +1 -0
- package/lib/types/symbols.d.ts +29 -0
- package/lib/types/symbols.js +33 -0
- package/lib/types/symbols.js.map +1 -0
- package/lib/types/utils.d.ts +0 -8
- package/lib/types/utils.js +1 -33
- package/lib/types/utils.js.map +1 -1
- package/lib/usage.d.ts +1 -0
- package/lib/usage.js +22 -0
- package/lib/usage.js.map +1 -0
- package/lib/utils.d.ts +13 -2
- package/lib/utils.js +36 -15
- package/lib/utils.js.map +1 -1
- package/lib/v3.d.ts +1 -0
- package/lib/v3.js +427 -0
- package/lib/v3.js.map +1 -0
- package/lib/v3_bench.d.ts +1 -0
- package/lib/v3_bench.js +130 -0
- package/lib/v3_bench.js.map +1 -0
- package/lib/v3_experiment.d.ts +1 -0
- package/lib/v3_experiment.js +407 -0
- package/lib/v3_experiment.js.map +1 -0
- package/package.json +5 -5
- package/src/Metadata.ts +135 -0
- package/src/Reflection.ts +75 -66
- package/src/Schema.ts +213 -931
- package/src/annotations.ts +430 -243
- package/src/decoder/DecodeOperation.ts +372 -0
- package/src/decoder/Decoder.ts +155 -0
- package/src/decoder/ReferenceTracker.ts +151 -0
- package/src/decoder/strategy/RawChanges.ts +9 -0
- package/src/decoder/strategy/StateCallbacks.ts +326 -0
- package/src/encoder/ChangeTree.ts +492 -0
- package/src/encoder/EncodeOperation.ts +237 -0
- package/src/encoder/Encoder.ts +246 -0
- package/src/encoder/StateView.ts +229 -0
- package/src/encoding/assert.ts +58 -0
- package/src/encoding/decode.ts +1 -1
- package/src/encoding/encode.ts +88 -82
- package/src/encoding/spec.ts +29 -0
- package/src/index.ts +22 -19
- package/src/symbol.shim.ts +12 -0
- package/src/types/HelperTypes.ts +16 -2
- package/src/types/{ArraySchema.ts → custom/ArraySchema.ts} +345 -251
- package/src/types/{CollectionSchema.ts → custom/CollectionSchema.ts} +56 -46
- package/src/types/{MapSchema.ts → custom/MapSchema.ts} +88 -115
- package/src/types/{SetSchema.ts → custom/SetSchema.ts} +58 -47
- package/src/types/{typeRegistry.ts → registry.ts} +6 -6
- package/src/types/symbols.ts +36 -0
- package/src/types/utils.ts +0 -46
- package/src/utils.ts +50 -21
- package/src/v3_bench.ts +107 -0
- package/src/changes/ChangeTree.ts +0 -295
- package/src/changes/ReferenceTracker.ts +0 -91
- package/src/filters/index.ts +0 -23
- package/src/spec.ts +0 -49
package/src/Schema.ts
CHANGED
|
@@ -1,179 +1,117 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { OPERATION } from './encoding/spec';
|
|
2
|
+
import { DEFAULT_VIEW_TAG, DefinitionType } from "./annotations";
|
|
3
3
|
|
|
4
|
-
import * as encode from "./encoding/encode";
|
|
5
|
-
import * as decode from "./encoding/decode";
|
|
6
|
-
import type { Iterator } from "./encoding/decode"; // dts-bundle-generator
|
|
7
|
-
|
|
8
|
-
import { ArraySchema } from "./types/ArraySchema";
|
|
9
|
-
import { MapSchema } from "./types/MapSchema";
|
|
10
|
-
import { CollectionSchema } from './types/CollectionSchema';
|
|
11
|
-
import { SetSchema } from './types/SetSchema';
|
|
12
|
-
|
|
13
|
-
import { ChangeTree, Ref, ChangeOperation } from "./changes/ChangeTree";
|
|
14
4
|
import { NonFunctionPropNames, ToJSON } from './types/HelperTypes';
|
|
15
|
-
import { ClientState } from './filters';
|
|
16
|
-
import { getType } from './types/typeRegistry';
|
|
17
|
-
import { ReferenceTracker } from './changes/ReferenceTracker';
|
|
18
|
-
import { addCallback, spliceOne } from './types/utils';
|
|
19
|
-
|
|
20
|
-
export interface DataChange<T=any,F=string> {
|
|
21
|
-
refId: number,
|
|
22
|
-
op: OPERATION,
|
|
23
|
-
field: F;
|
|
24
|
-
dynamicIndex?: number | string;
|
|
25
|
-
value: T;
|
|
26
|
-
previousValue: T;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface SchemaDecoderCallbacks<TValue=any, TKey=any> {
|
|
30
|
-
$callbacks: { [operation: number]: Array<(item: TValue, key: TKey) => void> };
|
|
31
|
-
|
|
32
|
-
onAdd(callback: (item: any, key: any) => void, ignoreExisting?: boolean): () => void;
|
|
33
|
-
onRemove(callback: (item: any, key: any) => void): () => void;
|
|
34
|
-
onChange(callback: (item: any, key: any) => void): () => void;
|
|
35
|
-
|
|
36
|
-
clone(decoding?: boolean): SchemaDecoderCallbacks;
|
|
37
|
-
clear(changes?: DataChange[]);
|
|
38
|
-
decode?(byte, it: Iterator);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
class EncodeSchemaError extends Error {}
|
|
42
|
-
|
|
43
|
-
function assertType(value: any, type: string, klass: Schema, field: string | number) {
|
|
44
|
-
let typeofTarget: string;
|
|
45
|
-
let allowNull: boolean = false;
|
|
46
|
-
|
|
47
|
-
switch (type) {
|
|
48
|
-
case "number":
|
|
49
|
-
case "int8":
|
|
50
|
-
case "uint8":
|
|
51
|
-
case "int16":
|
|
52
|
-
case "uint16":
|
|
53
|
-
case "int32":
|
|
54
|
-
case "uint32":
|
|
55
|
-
case "int64":
|
|
56
|
-
case "uint64":
|
|
57
|
-
case "float32":
|
|
58
|
-
case "float64":
|
|
59
|
-
typeofTarget = "number";
|
|
60
|
-
if (isNaN(value)) {
|
|
61
|
-
console.log(`trying to encode "NaN" in ${klass.constructor.name}#${field}`);
|
|
62
|
-
}
|
|
63
|
-
break;
|
|
64
|
-
case "string":
|
|
65
|
-
typeofTarget = "string";
|
|
66
|
-
allowNull = true;
|
|
67
|
-
break;
|
|
68
|
-
case "boolean":
|
|
69
|
-
// boolean is always encoded as true/false based on truthiness
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (typeof (value) !== typeofTarget && (!allowNull || (allowNull && value !== null))) {
|
|
74
|
-
let foundValue = `'${JSON.stringify(value)}'${(value && value.constructor && ` (${value.constructor.name})`) || ''}`;
|
|
75
|
-
throw new EncodeSchemaError(`a '${typeofTarget}' was expected, but ${foundValue} was provided in ${klass.constructor.name}#${field}`);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function assertInstanceType(
|
|
80
|
-
value: Schema,
|
|
81
|
-
type: typeof Schema
|
|
82
|
-
| typeof ArraySchema
|
|
83
|
-
| typeof MapSchema
|
|
84
|
-
| typeof CollectionSchema
|
|
85
|
-
| typeof SetSchema,
|
|
86
|
-
klass: Schema,
|
|
87
|
-
field: string | number,
|
|
88
|
-
) {
|
|
89
|
-
if (!(value instanceof type)) {
|
|
90
|
-
throw new EncodeSchemaError(`a '${type.name}' was expected, but '${(value as any).constructor.name}' was provided in ${klass.constructor.name}#${field}`);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function encodePrimitiveType(
|
|
95
|
-
type: PrimitiveType,
|
|
96
|
-
bytes: number[],
|
|
97
|
-
value: any,
|
|
98
|
-
klass: Schema,
|
|
99
|
-
field: string | number,
|
|
100
|
-
) {
|
|
101
|
-
assertType(value, type as string, klass, field);
|
|
102
|
-
|
|
103
|
-
const encodeFunc = encode[type as string];
|
|
104
5
|
|
|
105
|
-
|
|
106
|
-
|
|
6
|
+
import { ChangeTree, Ref } from './encoder/ChangeTree';
|
|
7
|
+
import { $changes, $decoder, $deleteByIndex, $encoder, $filter, $getByIndex, $track } from './types/symbols';
|
|
8
|
+
import { StateView } from './encoder/StateView';
|
|
107
9
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function decodePrimitiveType (type: string, bytes: number[], it: Iterator) {
|
|
114
|
-
return decode[type as string](bytes, it);
|
|
115
|
-
}
|
|
10
|
+
import { encodeSchemaOperation } from './encoder/EncodeOperation';
|
|
11
|
+
import { decodeSchemaOperation } from './decoder/DecodeOperation';
|
|
12
|
+
import type { Metadata } from './Metadata';
|
|
13
|
+
import { getIndent } from './utils';
|
|
116
14
|
|
|
117
15
|
/**
|
|
118
16
|
* Schema encoder / decoder
|
|
119
17
|
*/
|
|
120
18
|
export abstract class Schema {
|
|
121
|
-
static _typeid: number;
|
|
122
|
-
static _context: Context;
|
|
123
19
|
|
|
124
|
-
static
|
|
20
|
+
static [$encoder] = encodeSchemaOperation;
|
|
21
|
+
static [$decoder] = decodeSchemaOperation;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Assign the property descriptors required to track changes on this instance.
|
|
25
|
+
* @param instance
|
|
26
|
+
*/
|
|
27
|
+
static initialize(instance: any) {
|
|
28
|
+
Object.defineProperty(instance, $changes, {
|
|
29
|
+
value: new ChangeTree(instance),
|
|
30
|
+
enumerable: false,
|
|
31
|
+
writable: true
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const metadata = instance.constructor[Symbol.metadata];
|
|
35
|
+
|
|
36
|
+
// Define property descriptors
|
|
37
|
+
for (const field in metadata) {
|
|
38
|
+
if (metadata[field].descriptor) {
|
|
39
|
+
// for encoder
|
|
40
|
+
Object.defineProperty(instance, `_${field}`, {
|
|
41
|
+
value: undefined,
|
|
42
|
+
writable: true,
|
|
43
|
+
enumerable: false,
|
|
44
|
+
configurable: true,
|
|
45
|
+
});
|
|
46
|
+
Object.defineProperty(instance, field, metadata[field].descriptor);
|
|
125
47
|
|
|
126
|
-
|
|
127
|
-
|
|
48
|
+
} else {
|
|
49
|
+
// for decoder
|
|
50
|
+
Object.defineProperty(instance, field, {
|
|
51
|
+
value: undefined,
|
|
52
|
+
writable: true,
|
|
53
|
+
enumerable: true,
|
|
54
|
+
configurable: true,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Object.defineProperty(instance, field, {
|
|
59
|
+
// ...instance.constructor[Symbol.metadata][field].descriptor
|
|
60
|
+
// });
|
|
61
|
+
// if (args[0]?.hasOwnProperty(field)) {
|
|
62
|
+
// instance[field] = args[0][field];
|
|
63
|
+
// }
|
|
64
|
+
}
|
|
128
65
|
}
|
|
129
66
|
|
|
130
67
|
static is(type: DefinitionType) {
|
|
131
|
-
return (
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
);
|
|
68
|
+
return typeof(type[Symbol.metadata]) === "object";
|
|
69
|
+
// const metadata = type[Symbol.metadata];
|
|
70
|
+
// return metadata && Object.prototype.hasOwnProperty.call(metadata, -1);
|
|
135
71
|
}
|
|
136
72
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
public onChange(callback: () => void): () => void {
|
|
144
|
-
return addCallback((this.$callbacks || (this.$callbacks = {})), OPERATION.REPLACE, callback);
|
|
73
|
+
/**
|
|
74
|
+
* Track property changes
|
|
75
|
+
*/
|
|
76
|
+
static [$track] (changeTree: ChangeTree, index: number, operation: OPERATION = OPERATION.ADD) {
|
|
77
|
+
changeTree.change(index, operation);
|
|
145
78
|
}
|
|
146
|
-
|
|
147
|
-
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Determine if a property must be filtered.
|
|
82
|
+
* - If returns false, the property is NOT going to be encoded.
|
|
83
|
+
* - If returns true, the property is going to be encoded.
|
|
84
|
+
*
|
|
85
|
+
* Encoding with "filters" happens in two steps:
|
|
86
|
+
* - First, the encoder iterates over all "not owned" properties and encodes them.
|
|
87
|
+
* - Then, the encoder iterates over all "owned" properties per instance and encodes them.
|
|
88
|
+
*/
|
|
89
|
+
static [$filter] (ref: Schema, index: number, view: StateView) {
|
|
90
|
+
const metadata: Metadata = ref.constructor[Symbol.metadata];
|
|
91
|
+
const tag = metadata[metadata[index]].tag;
|
|
92
|
+
|
|
93
|
+
if (view === undefined) {
|
|
94
|
+
// shared pass/encode: encode if doesn't have a tag
|
|
95
|
+
return tag === undefined;
|
|
96
|
+
|
|
97
|
+
} else if (tag === undefined) {
|
|
98
|
+
// view pass: no tag
|
|
99
|
+
return true;
|
|
100
|
+
|
|
101
|
+
} else if (tag === DEFAULT_VIEW_TAG) {
|
|
102
|
+
// view pass: default tag
|
|
103
|
+
return view.items.has(ref[$changes]);
|
|
104
|
+
|
|
105
|
+
} else {
|
|
106
|
+
// view pass: custom tag
|
|
107
|
+
const tags = view.tags?.get(ref[$changes]);
|
|
108
|
+
return tags && tags.has(tag);
|
|
109
|
+
}
|
|
148
110
|
}
|
|
149
111
|
|
|
150
112
|
// allow inherited classes to have a constructor
|
|
151
113
|
constructor(...args: any[]) {
|
|
152
|
-
|
|
153
|
-
Object.defineProperties(this, {
|
|
154
|
-
$changes: {
|
|
155
|
-
value: new ChangeTree(this, undefined, new ReferenceTracker()),
|
|
156
|
-
enumerable: false,
|
|
157
|
-
writable: true
|
|
158
|
-
},
|
|
159
|
-
|
|
160
|
-
// $listeners: {
|
|
161
|
-
// value: undefined,
|
|
162
|
-
// enumerable: false,
|
|
163
|
-
// writable: true
|
|
164
|
-
// },
|
|
165
|
-
|
|
166
|
-
$callbacks: {
|
|
167
|
-
value: undefined,
|
|
168
|
-
enumerable: false,
|
|
169
|
-
writable: true
|
|
170
|
-
},
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
const descriptors = this._definition.descriptors;
|
|
174
|
-
if (descriptors) {
|
|
175
|
-
Object.defineProperties(this, descriptors);
|
|
176
|
-
}
|
|
114
|
+
Schema.initialize(this);
|
|
177
115
|
|
|
178
116
|
//
|
|
179
117
|
// Assign initial values
|
|
@@ -190,8 +128,6 @@ export abstract class Schema {
|
|
|
190
128
|
return this;
|
|
191
129
|
}
|
|
192
130
|
|
|
193
|
-
protected get _definition () { return (this.constructor as typeof Schema)._definition; }
|
|
194
|
-
|
|
195
131
|
/**
|
|
196
132
|
* (Server-side): Flag a property to be encoded for the next patch.
|
|
197
133
|
* @param instance Schema instance
|
|
@@ -199,686 +135,21 @@ export abstract class Schema {
|
|
|
199
135
|
* @param operation OPERATION to perform (detected automatically)
|
|
200
136
|
*/
|
|
201
137
|
public setDirty<K extends NonFunctionPropNames<this>>(property: K | number, operation?: OPERATION) {
|
|
202
|
-
this
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
* Client-side: listen for changes on property.
|
|
207
|
-
* @param prop the property name
|
|
208
|
-
* @param callback callback to be triggered on property change
|
|
209
|
-
* @param immediate trigger immediatelly if property has been already set.
|
|
210
|
-
*/
|
|
211
|
-
public listen<K extends NonFunctionPropNames<this>>(
|
|
212
|
-
prop: K,
|
|
213
|
-
callback: (value: this[K], previousValue: this[K]) => void,
|
|
214
|
-
immediate: boolean = true,
|
|
215
|
-
) {
|
|
216
|
-
if (!this.$callbacks) { this.$callbacks = {}; }
|
|
217
|
-
if (!this.$callbacks[prop as string]) { this.$callbacks[prop as string] = []; }
|
|
218
|
-
|
|
219
|
-
this.$callbacks[prop as string].push(callback);
|
|
220
|
-
|
|
221
|
-
if (immediate && this[prop] !== undefined) {
|
|
222
|
-
callback(this[prop], undefined);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// return un-register callback.
|
|
226
|
-
return () => spliceOne(this.$callbacks[prop as string], this.$callbacks[prop as string].indexOf(callback));
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
decode(
|
|
230
|
-
bytes: number[],
|
|
231
|
-
it: Iterator = { offset: 0 },
|
|
232
|
-
ref: Ref = this,
|
|
233
|
-
) {
|
|
234
|
-
const allChanges: DataChange[] = [];
|
|
235
|
-
|
|
236
|
-
const $root = this.$changes.root;
|
|
237
|
-
const totalBytes = bytes.length;
|
|
238
|
-
|
|
239
|
-
let refId: number = 0;
|
|
240
|
-
$root.refs.set(refId, this);
|
|
241
|
-
|
|
242
|
-
while (it.offset < totalBytes) {
|
|
243
|
-
let byte = bytes[it.offset++];
|
|
244
|
-
|
|
245
|
-
if (byte == SWITCH_TO_STRUCTURE) {
|
|
246
|
-
refId = decode.number(bytes, it);
|
|
247
|
-
const nextRef = $root.refs.get(refId) as Schema;
|
|
248
|
-
|
|
249
|
-
//
|
|
250
|
-
// Trying to access a reference that haven't been decoded yet.
|
|
251
|
-
//
|
|
252
|
-
if (!nextRef) { throw new Error(`"refId" not found: ${refId}`); }
|
|
253
|
-
ref = nextRef;
|
|
254
|
-
|
|
255
|
-
continue;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
const changeTree: ChangeTree = ref['$changes'];
|
|
259
|
-
const isSchema = (ref['_definition'] !== undefined);
|
|
260
|
-
|
|
261
|
-
const operation = (isSchema)
|
|
262
|
-
? (byte >> 6) << 6 // "compressed" index + operation
|
|
263
|
-
: byte; // "uncompressed" index + operation (array/map items)
|
|
264
|
-
|
|
265
|
-
if (operation === OPERATION.CLEAR) {
|
|
266
|
-
//
|
|
267
|
-
// TODO: refactor me!
|
|
268
|
-
// The `.clear()` method is calling `$root.removeRef(refId)` for
|
|
269
|
-
// each item inside this collection
|
|
270
|
-
//
|
|
271
|
-
(ref as SchemaDecoderCallbacks).clear(allChanges);
|
|
272
|
-
continue;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const fieldIndex = (isSchema)
|
|
276
|
-
? byte % (operation || 255) // if "REPLACE" operation (0), use 255
|
|
277
|
-
: decode.number(bytes, it);
|
|
278
|
-
|
|
279
|
-
const fieldName = (isSchema)
|
|
280
|
-
? (ref['_definition'].fieldsByIndex[fieldIndex])
|
|
281
|
-
: "";
|
|
282
|
-
|
|
283
|
-
let type = changeTree.getType(fieldIndex);
|
|
284
|
-
let value: any;
|
|
285
|
-
let previousValue: any;
|
|
286
|
-
|
|
287
|
-
let dynamicIndex: number | string;
|
|
288
|
-
|
|
289
|
-
if (!isSchema) {
|
|
290
|
-
previousValue = ref['getByIndex'](fieldIndex);
|
|
291
|
-
|
|
292
|
-
if ((operation & OPERATION.ADD) === OPERATION.ADD) { // ADD or DELETE_AND_ADD
|
|
293
|
-
dynamicIndex = (ref instanceof MapSchema)
|
|
294
|
-
? decode.string(bytes, it)
|
|
295
|
-
: fieldIndex;
|
|
296
|
-
ref['setIndex'](fieldIndex, dynamicIndex);
|
|
297
|
-
|
|
298
|
-
} else {
|
|
299
|
-
// here
|
|
300
|
-
dynamicIndex = ref['getIndex'](fieldIndex);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
} else {
|
|
304
|
-
previousValue = ref[`_${fieldName}`];
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
//
|
|
308
|
-
// Delete operations
|
|
309
|
-
//
|
|
310
|
-
if ((operation & OPERATION.DELETE) === OPERATION.DELETE)
|
|
311
|
-
{
|
|
312
|
-
if (operation !== OPERATION.DELETE_AND_ADD) {
|
|
313
|
-
ref['deleteByIndex'](fieldIndex);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// Flag `refId` for garbage collection.
|
|
317
|
-
if (previousValue && previousValue['$changes']) {
|
|
318
|
-
$root.removeRef(previousValue['$changes'].refId);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
value = null;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
if (fieldName === undefined) {
|
|
325
|
-
console.warn("@colyseus/schema: definition mismatch");
|
|
326
|
-
|
|
327
|
-
//
|
|
328
|
-
// keep skipping next bytes until reaches a known structure
|
|
329
|
-
// by local decoder.
|
|
330
|
-
//
|
|
331
|
-
const nextIterator: Iterator = { offset: it.offset };
|
|
332
|
-
while (it.offset < totalBytes) {
|
|
333
|
-
if (decode.switchStructureCheck(bytes, it)) {
|
|
334
|
-
nextIterator.offset = it.offset + 1;
|
|
335
|
-
if ($root.refs.has(decode.number(bytes, nextIterator))) {
|
|
336
|
-
break;
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
it.offset++;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
continue;
|
|
344
|
-
|
|
345
|
-
} else if (operation === OPERATION.DELETE) {
|
|
346
|
-
//
|
|
347
|
-
// FIXME: refactor me.
|
|
348
|
-
// Don't do anything.
|
|
349
|
-
//
|
|
350
|
-
|
|
351
|
-
} else if (Schema.is(type)) {
|
|
352
|
-
const refId = decode.number(bytes, it);
|
|
353
|
-
value = $root.refs.get(refId);
|
|
354
|
-
|
|
355
|
-
if (operation !== OPERATION.REPLACE) {
|
|
356
|
-
const childType = this.getSchemaType(bytes, it, type);
|
|
357
|
-
|
|
358
|
-
if (!value) {
|
|
359
|
-
value = this.createTypeInstance(childType);
|
|
360
|
-
value.$changes.refId = refId;
|
|
361
|
-
|
|
362
|
-
if (previousValue) {
|
|
363
|
-
value.$callbacks = previousValue.$callbacks;
|
|
364
|
-
// value.$listeners = previousValue.$listeners;
|
|
365
|
-
|
|
366
|
-
if (
|
|
367
|
-
previousValue['$changes'].refId &&
|
|
368
|
-
refId !== previousValue['$changes'].refId
|
|
369
|
-
) {
|
|
370
|
-
$root.removeRef(previousValue['$changes'].refId);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
$root.addRef(refId, value, (value !== previousValue));
|
|
376
|
-
}
|
|
377
|
-
} else if (typeof(type) === "string") {
|
|
378
|
-
//
|
|
379
|
-
// primitive value (number, string, boolean, etc)
|
|
380
|
-
//
|
|
381
|
-
value = decodePrimitiveType(type as string, bytes, it);
|
|
382
|
-
|
|
383
|
-
} else {
|
|
384
|
-
const typeDef = getType(Object.keys(type)[0]);
|
|
385
|
-
const refId = decode.number(bytes, it);
|
|
386
|
-
|
|
387
|
-
const valueRef: SchemaDecoderCallbacks = ($root.refs.has(refId))
|
|
388
|
-
? previousValue || $root.refs.get(refId)
|
|
389
|
-
: new typeDef.constructor();
|
|
390
|
-
|
|
391
|
-
value = valueRef.clone(true);
|
|
392
|
-
value.$changes.refId = refId;
|
|
393
|
-
|
|
394
|
-
// preserve schema callbacks
|
|
395
|
-
if (previousValue) {
|
|
396
|
-
value['$callbacks'] = previousValue['$callbacks'];
|
|
397
|
-
|
|
398
|
-
if (
|
|
399
|
-
previousValue['$changes'].refId &&
|
|
400
|
-
refId !== previousValue['$changes'].refId
|
|
401
|
-
) {
|
|
402
|
-
$root.removeRef(previousValue['$changes'].refId);
|
|
403
|
-
|
|
404
|
-
//
|
|
405
|
-
// Trigger onRemove if structure has been replaced.
|
|
406
|
-
//
|
|
407
|
-
const entries: IterableIterator<[any, any]> = previousValue.entries();
|
|
408
|
-
let iter: IteratorResult<[any, any]>;
|
|
409
|
-
while ((iter = entries.next()) && !iter.done) {
|
|
410
|
-
const [key, value] = iter.value;
|
|
411
|
-
allChanges.push({
|
|
412
|
-
refId,
|
|
413
|
-
op: OPERATION.DELETE,
|
|
414
|
-
field: key,
|
|
415
|
-
value: undefined,
|
|
416
|
-
previousValue: value,
|
|
417
|
-
});
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
$root.addRef(refId, value, (valueRef !== previousValue));
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
if (
|
|
426
|
-
value !== null &&
|
|
427
|
-
value !== undefined
|
|
428
|
-
) {
|
|
429
|
-
if (value['$changes']) {
|
|
430
|
-
value['$changes'].setParent(
|
|
431
|
-
changeTree.ref,
|
|
432
|
-
changeTree.root,
|
|
433
|
-
fieldIndex,
|
|
434
|
-
);
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
if (ref instanceof Schema) {
|
|
438
|
-
ref[fieldName] = value;
|
|
439
|
-
// ref[`_${fieldName}`] = value;
|
|
440
|
-
|
|
441
|
-
} else if (ref instanceof MapSchema) {
|
|
442
|
-
// const key = ref['$indexes'].get(field);
|
|
443
|
-
const key = dynamicIndex as string;
|
|
444
|
-
|
|
445
|
-
// ref.set(key, value);
|
|
446
|
-
ref['$items'].set(key, value);
|
|
447
|
-
ref['$changes'].allChanges.add(fieldIndex);
|
|
448
|
-
|
|
449
|
-
} else if (ref instanceof ArraySchema) {
|
|
450
|
-
// const key = ref['$indexes'][field];
|
|
451
|
-
// console.log("SETTING FOR ArraySchema =>", { field, key, value });
|
|
452
|
-
// ref[key] = value;
|
|
453
|
-
ref.setAt(fieldIndex, value);
|
|
454
|
-
|
|
455
|
-
} else if (ref instanceof CollectionSchema) {
|
|
456
|
-
const index = ref.add(value);
|
|
457
|
-
ref['setIndex'](fieldIndex, index);
|
|
458
|
-
|
|
459
|
-
} else if (ref instanceof SetSchema) {
|
|
460
|
-
const index = ref.add(value);
|
|
461
|
-
if (index !== false) {
|
|
462
|
-
ref['setIndex'](fieldIndex, index);
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
if (previousValue !== value) {
|
|
468
|
-
allChanges.push({
|
|
469
|
-
refId,
|
|
470
|
-
op: operation,
|
|
471
|
-
field: fieldName,
|
|
472
|
-
dynamicIndex,
|
|
473
|
-
value,
|
|
474
|
-
previousValue,
|
|
475
|
-
});
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
this._triggerChanges(allChanges);
|
|
480
|
-
|
|
481
|
-
// drop references of unused schemas
|
|
482
|
-
$root.garbageCollectDeletedRefs();
|
|
483
|
-
|
|
484
|
-
return allChanges;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
encode(
|
|
488
|
-
encodeAll = false,
|
|
489
|
-
bytes: number[] = [],
|
|
490
|
-
useFilters: boolean = false,
|
|
491
|
-
) {
|
|
492
|
-
const rootChangeTree = this.$changes;
|
|
493
|
-
const refIdsVisited = new WeakSet<ChangeTree>();
|
|
494
|
-
|
|
495
|
-
const changeTrees: ChangeTree[] = [rootChangeTree];
|
|
496
|
-
let numChangeTrees = 1;
|
|
497
|
-
|
|
498
|
-
for (let i = 0; i < numChangeTrees; i++) {
|
|
499
|
-
const changeTree = changeTrees[i];
|
|
500
|
-
const ref = changeTree.ref;
|
|
501
|
-
const isSchema = (ref instanceof Schema);
|
|
502
|
-
|
|
503
|
-
// Generate unique refId for the ChangeTree.
|
|
504
|
-
changeTree.ensureRefId();
|
|
505
|
-
|
|
506
|
-
// mark this ChangeTree as visited.
|
|
507
|
-
refIdsVisited.add(changeTree);
|
|
508
|
-
|
|
509
|
-
// root `refId` is skipped.
|
|
510
|
-
if (
|
|
511
|
-
changeTree !== rootChangeTree &&
|
|
512
|
-
(changeTree.changed || encodeAll)
|
|
513
|
-
) {
|
|
514
|
-
encode.uint8(bytes, SWITCH_TO_STRUCTURE);
|
|
515
|
-
encode.number(bytes, changeTree.refId);
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
const changes: ChangeOperation[] | number[] = (encodeAll)
|
|
519
|
-
? Array.from(changeTree.allChanges)
|
|
520
|
-
: Array.from(changeTree.changes.values());
|
|
521
|
-
|
|
522
|
-
for (let j = 0, cl = changes.length; j < cl; j++) {
|
|
523
|
-
const operation: ChangeOperation = (encodeAll)
|
|
524
|
-
? { op: OPERATION.ADD, index: changes[j] as number }
|
|
525
|
-
: changes[j] as ChangeOperation;
|
|
526
|
-
|
|
527
|
-
const fieldIndex = operation.index;
|
|
528
|
-
|
|
529
|
-
const field = (isSchema)
|
|
530
|
-
? ref['_definition'].fieldsByIndex && ref['_definition'].fieldsByIndex[fieldIndex]
|
|
531
|
-
: fieldIndex;
|
|
532
|
-
|
|
533
|
-
// cache begin index if `useFilters`
|
|
534
|
-
const beginIndex = bytes.length;
|
|
535
|
-
|
|
536
|
-
// encode field index + operation
|
|
537
|
-
if (operation.op !== OPERATION.TOUCH) {
|
|
538
|
-
if (isSchema) {
|
|
539
|
-
//
|
|
540
|
-
// Compress `fieldIndex` + `operation` into a single byte.
|
|
541
|
-
// This adds a limitaion of 64 fields per Schema structure
|
|
542
|
-
//
|
|
543
|
-
encode.uint8(bytes, (fieldIndex | operation.op));
|
|
544
|
-
|
|
545
|
-
} else {
|
|
546
|
-
encode.uint8(bytes, operation.op);
|
|
547
|
-
|
|
548
|
-
// custom operations
|
|
549
|
-
if (operation.op === OPERATION.CLEAR) {
|
|
550
|
-
continue;
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
// indexed operations
|
|
554
|
-
encode.number(bytes, fieldIndex);
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
//
|
|
559
|
-
// encode "alias" for dynamic fields (maps)
|
|
560
|
-
//
|
|
561
|
-
if (
|
|
562
|
-
!isSchema &&
|
|
563
|
-
(operation.op & OPERATION.ADD) == OPERATION.ADD // ADD or DELETE_AND_ADD
|
|
564
|
-
) {
|
|
565
|
-
if (ref instanceof MapSchema) {
|
|
566
|
-
//
|
|
567
|
-
// MapSchema dynamic key
|
|
568
|
-
//
|
|
569
|
-
const dynamicIndex = changeTree.ref['$indexes'].get(fieldIndex);
|
|
570
|
-
encode.string(bytes, dynamicIndex);
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
if (operation.op === OPERATION.DELETE) {
|
|
575
|
-
//
|
|
576
|
-
// TODO: delete from filter cache data.
|
|
577
|
-
//
|
|
578
|
-
// if (useFilters) {
|
|
579
|
-
// delete changeTree.caches[fieldIndex];
|
|
580
|
-
// }
|
|
581
|
-
continue;
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
// const type = changeTree.childType || ref._schema[field];
|
|
585
|
-
const type = changeTree.getType(fieldIndex);
|
|
586
|
-
|
|
587
|
-
// const type = changeTree.getType(fieldIndex);
|
|
588
|
-
const value = changeTree.getValue(fieldIndex);
|
|
589
|
-
|
|
590
|
-
// Enqueue ChangeTree to be visited
|
|
591
|
-
if (
|
|
592
|
-
value &&
|
|
593
|
-
value['$changes'] &&
|
|
594
|
-
!refIdsVisited.has(value['$changes'])
|
|
595
|
-
) {
|
|
596
|
-
changeTrees.push(value['$changes']);
|
|
597
|
-
value['$changes'].ensureRefId();
|
|
598
|
-
numChangeTrees++;
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
if (operation.op === OPERATION.TOUCH) {
|
|
602
|
-
continue;
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
if (Schema.is(type)) {
|
|
606
|
-
assertInstanceType(value, type as typeof Schema, ref as Schema, field);
|
|
607
|
-
|
|
608
|
-
//
|
|
609
|
-
// Encode refId for this instance.
|
|
610
|
-
// The actual instance is going to be encoded on next `changeTree` iteration.
|
|
611
|
-
//
|
|
612
|
-
encode.number(bytes, value.$changes.refId);
|
|
613
|
-
|
|
614
|
-
// Try to encode inherited TYPE_ID if it's an ADD operation.
|
|
615
|
-
if ((operation.op & OPERATION.ADD) === OPERATION.ADD) {
|
|
616
|
-
this.tryEncodeTypeId(bytes, type as typeof Schema, value.constructor as typeof Schema);
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
} else if (typeof(type) === "string") {
|
|
620
|
-
//
|
|
621
|
-
// Primitive values
|
|
622
|
-
//
|
|
623
|
-
encodePrimitiveType(type as PrimitiveType, bytes, value, ref as Schema, field);
|
|
624
|
-
|
|
625
|
-
} else {
|
|
626
|
-
//
|
|
627
|
-
// Custom type (MapSchema, ArraySchema, etc)
|
|
628
|
-
//
|
|
629
|
-
const definition = getType(Object.keys(type)[0]);
|
|
630
|
-
|
|
631
|
-
//
|
|
632
|
-
// ensure a ArraySchema has been provided
|
|
633
|
-
//
|
|
634
|
-
assertInstanceType(ref[`_${field}`], definition.constructor, ref as Schema, field);
|
|
635
|
-
|
|
636
|
-
//
|
|
637
|
-
// Encode refId for this instance.
|
|
638
|
-
// The actual instance is going to be encoded on next `changeTree` iteration.
|
|
639
|
-
//
|
|
640
|
-
encode.number(bytes, value.$changes.refId);
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
if (useFilters) {
|
|
644
|
-
// cache begin / end index
|
|
645
|
-
changeTree.cache(fieldIndex as number, bytes.slice(beginIndex));
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
if (!encodeAll && !useFilters) {
|
|
650
|
-
changeTree.discard();
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
return bytes;
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
encodeAll (useFilters?: boolean) {
|
|
658
|
-
return this.encode(true, [], useFilters);
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
applyFilters(client: ClientWithSessionId, encodeAll: boolean = false) {
|
|
662
|
-
const root = this;
|
|
663
|
-
const refIdsDissallowed = new Set<number>();
|
|
664
|
-
|
|
665
|
-
const $filterState = ClientState.get(client);
|
|
666
|
-
|
|
667
|
-
const changeTrees = [this.$changes];
|
|
668
|
-
let numChangeTrees = 1;
|
|
669
|
-
|
|
670
|
-
let filteredBytes: number[] = [];
|
|
671
|
-
|
|
672
|
-
for (let i = 0; i < numChangeTrees; i++) {
|
|
673
|
-
const changeTree = changeTrees[i];
|
|
674
|
-
|
|
675
|
-
if (refIdsDissallowed.has(changeTree.refId)) {
|
|
676
|
-
// console.log("REFID IS NOT ALLOWED. SKIP.", { refId: changeTree.refId })
|
|
677
|
-
continue;
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
const ref = changeTree.ref as Ref;
|
|
681
|
-
const isSchema: boolean = ref instanceof Schema;
|
|
682
|
-
|
|
683
|
-
encode.uint8(filteredBytes, SWITCH_TO_STRUCTURE);
|
|
684
|
-
encode.number(filteredBytes, changeTree.refId);
|
|
685
|
-
|
|
686
|
-
const clientHasRefId = $filterState.refIds.has(changeTree);
|
|
687
|
-
const isEncodeAll = (encodeAll || !clientHasRefId);
|
|
688
|
-
|
|
689
|
-
// console.log("REF:", ref.constructor.name);
|
|
690
|
-
// console.log("Encode all?", isEncodeAll);
|
|
691
|
-
|
|
692
|
-
//
|
|
693
|
-
// include `changeTree` on list of known refIds by this client.
|
|
694
|
-
//
|
|
695
|
-
$filterState.addRefId(changeTree);
|
|
696
|
-
|
|
697
|
-
const containerIndexes = $filterState.containerIndexes.get(changeTree)
|
|
698
|
-
const changes = (isEncodeAll)
|
|
699
|
-
? Array.from(changeTree.allChanges)
|
|
700
|
-
: Array.from(changeTree.changes.values());
|
|
701
|
-
|
|
702
|
-
//
|
|
703
|
-
// WORKAROUND: tries to re-evaluate previously not included @filter() attributes
|
|
704
|
-
// - see "DELETE a field of Schema" test case.
|
|
705
|
-
//
|
|
706
|
-
if (
|
|
707
|
-
!encodeAll &&
|
|
708
|
-
isSchema &&
|
|
709
|
-
(ref as Schema)._definition.indexesWithFilters
|
|
710
|
-
) {
|
|
711
|
-
const indexesWithFilters = (ref as Schema)._definition.indexesWithFilters;
|
|
712
|
-
indexesWithFilters.forEach(indexWithFilter => {
|
|
713
|
-
if (
|
|
714
|
-
!containerIndexes.has(indexWithFilter) &&
|
|
715
|
-
changeTree.allChanges.has(indexWithFilter)
|
|
716
|
-
) {
|
|
717
|
-
if (isEncodeAll) {
|
|
718
|
-
changes.push(indexWithFilter as any);
|
|
719
|
-
|
|
720
|
-
} else {
|
|
721
|
-
changes.push({ op: OPERATION.ADD, index: indexWithFilter, } as any);
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
});
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
for (let j = 0, cl = changes.length; j < cl; j++) {
|
|
728
|
-
const change: ChangeOperation = (isEncodeAll)
|
|
729
|
-
? { op: OPERATION.ADD, index: changes[j] as number }
|
|
730
|
-
: changes[j] as ChangeOperation;
|
|
731
|
-
|
|
732
|
-
// custom operations
|
|
733
|
-
if (change.op === OPERATION.CLEAR) {
|
|
734
|
-
encode.uint8(filteredBytes, change.op);
|
|
735
|
-
continue;
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
const fieldIndex = change.index;
|
|
739
|
-
|
|
740
|
-
//
|
|
741
|
-
// Deleting fields: encode the operation + field index
|
|
742
|
-
//
|
|
743
|
-
if (change.op === OPERATION.DELETE) {
|
|
744
|
-
//
|
|
745
|
-
// DELETE operations also need to go through filtering.
|
|
746
|
-
//
|
|
747
|
-
// TODO: cache the previous value so we can access the value (primitive or `refId`)
|
|
748
|
-
// (check against `$filterState.refIds`)
|
|
749
|
-
//
|
|
750
|
-
|
|
751
|
-
if (isSchema) {
|
|
752
|
-
encode.uint8(filteredBytes, change.op | fieldIndex);
|
|
753
|
-
|
|
754
|
-
} else {
|
|
755
|
-
encode.uint8(filteredBytes, change.op);
|
|
756
|
-
encode.number(filteredBytes, fieldIndex);
|
|
757
|
-
|
|
758
|
-
}
|
|
759
|
-
continue;
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
// indexed operation
|
|
763
|
-
const value = changeTree.getValue(fieldIndex);
|
|
764
|
-
const type = changeTree.getType(fieldIndex);
|
|
765
|
-
|
|
766
|
-
if (isSchema) {
|
|
767
|
-
// Is a Schema!
|
|
768
|
-
const filter = (
|
|
769
|
-
(ref as Schema)._definition.filters &&
|
|
770
|
-
(ref as Schema)._definition.filters[fieldIndex]
|
|
771
|
-
);
|
|
772
|
-
|
|
773
|
-
if (filter && !filter.call(ref, client, value, root)) {
|
|
774
|
-
if (value && value['$changes']) {
|
|
775
|
-
refIdsDissallowed.add(value['$changes'].refId);;
|
|
776
|
-
}
|
|
777
|
-
continue;
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
} else {
|
|
781
|
-
// Is a collection! (map, array, etc.)
|
|
782
|
-
const parent = changeTree.parent as Ref;
|
|
783
|
-
const filter = changeTree.getChildrenFilter();
|
|
784
|
-
|
|
785
|
-
if (filter && !filter.call(parent, client, ref['$indexes'].get(fieldIndex), value, root)) {
|
|
786
|
-
if (value && value['$changes']) {
|
|
787
|
-
refIdsDissallowed.add(value['$changes'].refId);
|
|
788
|
-
}
|
|
789
|
-
continue;
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
// visit child ChangeTree on further iteration.
|
|
794
|
-
if (value['$changes']) {
|
|
795
|
-
changeTrees.push(value['$changes']);
|
|
796
|
-
numChangeTrees++;
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
//
|
|
800
|
-
// Copy cached bytes
|
|
801
|
-
//
|
|
802
|
-
if (change.op !== OPERATION.TOUCH) {
|
|
803
|
-
|
|
804
|
-
//
|
|
805
|
-
// TODO: refactor me!
|
|
806
|
-
//
|
|
807
|
-
|
|
808
|
-
if (change.op === OPERATION.ADD || isSchema) {
|
|
809
|
-
//
|
|
810
|
-
// use cached bytes directly if is from Schema type.
|
|
811
|
-
//
|
|
812
|
-
filteredBytes.push.apply(filteredBytes, changeTree.caches[fieldIndex] ?? []);
|
|
813
|
-
containerIndexes.add(fieldIndex);
|
|
814
|
-
|
|
815
|
-
} else {
|
|
816
|
-
if (containerIndexes.has(fieldIndex)) {
|
|
817
|
-
//
|
|
818
|
-
// use cached bytes if already has the field
|
|
819
|
-
//
|
|
820
|
-
filteredBytes.push.apply(filteredBytes, changeTree.caches[fieldIndex] ?? []);
|
|
821
|
-
|
|
822
|
-
} else {
|
|
823
|
-
//
|
|
824
|
-
// force ADD operation if field is not known by this client.
|
|
825
|
-
//
|
|
826
|
-
containerIndexes.add(fieldIndex);
|
|
827
|
-
|
|
828
|
-
encode.uint8(filteredBytes, OPERATION.ADD);
|
|
829
|
-
encode.number(filteredBytes, fieldIndex);
|
|
830
|
-
|
|
831
|
-
if (ref instanceof MapSchema) {
|
|
832
|
-
//
|
|
833
|
-
// MapSchema dynamic key
|
|
834
|
-
//
|
|
835
|
-
const dynamicIndex = changeTree.ref['$indexes'].get(fieldIndex);
|
|
836
|
-
encode.string(filteredBytes, dynamicIndex);
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
if (value['$changes']) {
|
|
840
|
-
encode.number(filteredBytes, value['$changes'].refId);
|
|
841
|
-
|
|
842
|
-
} else {
|
|
843
|
-
// "encodePrimitiveType" without type checking.
|
|
844
|
-
// the type checking has been done on the first .encode() call.
|
|
845
|
-
encode[type as string](filteredBytes, value);
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
} else if (value['$changes'] && !isSchema) {
|
|
851
|
-
//
|
|
852
|
-
// TODO:
|
|
853
|
-
// - track ADD/REPLACE/DELETE instances on `$filterState`
|
|
854
|
-
// - do NOT always encode dynamicIndex for MapSchema.
|
|
855
|
-
// (If client already has that key, only the first index is necessary.)
|
|
856
|
-
//
|
|
857
|
-
|
|
858
|
-
encode.uint8(filteredBytes, OPERATION.ADD);
|
|
859
|
-
encode.number(filteredBytes, fieldIndex);
|
|
860
|
-
|
|
861
|
-
if (ref instanceof MapSchema) {
|
|
862
|
-
//
|
|
863
|
-
// MapSchema dynamic key
|
|
864
|
-
//
|
|
865
|
-
const dynamicIndex = changeTree.ref['$indexes'].get(fieldIndex);
|
|
866
|
-
encode.string(filteredBytes, dynamicIndex);
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
encode.number(filteredBytes, value['$changes'].refId);
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
};
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
return filteredBytes;
|
|
138
|
+
this[$changes].change(
|
|
139
|
+
this.constructor[Symbol.metadata][property as string].index,
|
|
140
|
+
operation
|
|
141
|
+
);
|
|
876
142
|
}
|
|
877
143
|
|
|
878
144
|
clone (): this {
|
|
879
145
|
const cloned = new ((this as any).constructor);
|
|
880
|
-
const
|
|
881
|
-
|
|
146
|
+
const metadata = this.constructor[Symbol.metadata];
|
|
147
|
+
|
|
148
|
+
//
|
|
149
|
+
// TODO: clone all properties, not only annotated ones
|
|
150
|
+
//
|
|
151
|
+
// for (const field in this) {
|
|
152
|
+
for (const field in metadata) {
|
|
882
153
|
if (
|
|
883
154
|
typeof (this[field]) === "object" &&
|
|
884
155
|
typeof (this[field]?.clone) === "function"
|
|
@@ -895,144 +166,155 @@ export abstract class Schema {
|
|
|
895
166
|
}
|
|
896
167
|
|
|
897
168
|
toJSON () {
|
|
898
|
-
const
|
|
899
|
-
const deprecated = this._definition.deprecated;
|
|
169
|
+
const metadata = this.constructor[Symbol.metadata];
|
|
900
170
|
|
|
901
171
|
const obj: unknown = {};
|
|
902
|
-
for (
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
172
|
+
for (const fieldName in metadata) {
|
|
173
|
+
const field = metadata[fieldName];
|
|
174
|
+
if (!field.deprecated && this[fieldName] !== null && typeof (this[fieldName]) !== "undefined") {
|
|
175
|
+
obj[fieldName] = (typeof (this[fieldName]['toJSON']) === "function")
|
|
176
|
+
? this[fieldName]['toJSON']()
|
|
177
|
+
: this[fieldName];
|
|
907
178
|
}
|
|
908
179
|
}
|
|
909
180
|
return obj as ToJSON<typeof this>;
|
|
910
181
|
}
|
|
911
182
|
|
|
912
183
|
discardAllChanges() {
|
|
913
|
-
this
|
|
184
|
+
this[$changes].discardAll();
|
|
914
185
|
}
|
|
915
186
|
|
|
916
|
-
protected getByIndex(index: number) {
|
|
917
|
-
return this[this.
|
|
187
|
+
protected [$getByIndex](index: number) {
|
|
188
|
+
return this[this.constructor[Symbol.metadata][index]];
|
|
918
189
|
}
|
|
919
190
|
|
|
920
|
-
protected deleteByIndex(index: number) {
|
|
921
|
-
this[this.
|
|
191
|
+
protected [$deleteByIndex](index: number) {
|
|
192
|
+
this[this.constructor[Symbol.metadata][index]] = undefined;
|
|
922
193
|
}
|
|
923
194
|
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
encode.number(bytes, targetType._typeid);
|
|
928
|
-
}
|
|
929
|
-
}
|
|
195
|
+
static debugRefIds(instance: Ref, jsonContents: boolean = true, level: number = 0) {
|
|
196
|
+
const ref = instance;
|
|
197
|
+
const changeTree = ref[$changes];
|
|
930
198
|
|
|
931
|
-
|
|
932
|
-
let type: typeof Schema;
|
|
199
|
+
const contents = (jsonContents) ? ` - ${JSON.stringify(ref.toJSON())}` : "";
|
|
933
200
|
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
201
|
+
let output = "";
|
|
202
|
+
output += `${getIndent(level)}${ref.constructor.name} (${ref[$changes].refId})${contents}\n`;
|
|
203
|
+
|
|
204
|
+
changeTree.forEachChild((childChangeTree) =>
|
|
205
|
+
output += this.debugRefIds(childChangeTree.ref, jsonContents, level + 1));
|
|
938
206
|
|
|
939
|
-
return
|
|
207
|
+
return output;
|
|
940
208
|
}
|
|
941
209
|
|
|
942
|
-
|
|
943
|
-
|
|
210
|
+
/**
|
|
211
|
+
* Return a string representation of the changes on a Schema instance.
|
|
212
|
+
* The list of changes is cleared after each encode.
|
|
213
|
+
*
|
|
214
|
+
* @param instance Schema instance
|
|
215
|
+
* @param isEncodeAll Return "full encode" instead of current change set.
|
|
216
|
+
* @returns
|
|
217
|
+
*/
|
|
218
|
+
static debugChanges(instance: Ref, isEncodeAll: boolean = false) {
|
|
219
|
+
const changeTree = instance[$changes];
|
|
944
220
|
|
|
945
|
-
|
|
946
|
-
|
|
221
|
+
const changeSet = (isEncodeAll) ? changeTree.allChanges : changeTree.changes;
|
|
222
|
+
const changeSetName = (isEncodeAll) ? "allChanges" : "changes";
|
|
947
223
|
|
|
948
|
-
|
|
949
|
-
}
|
|
224
|
+
let output = `${instance.constructor.name} (${changeTree.refId}) -> .${changeSetName}:\n`;
|
|
950
225
|
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
226
|
+
function dumpChangeSet(changeSet: Map<number, OPERATION>) {
|
|
227
|
+
Array.from(changeSet)
|
|
228
|
+
.sort((a, b) => a[0] - b[0])
|
|
229
|
+
.forEach(([index, operation]) =>
|
|
230
|
+
output += `- [${index}]: ${OPERATION[operation]} (${JSON.stringify(changeTree.getValue(index, isEncodeAll))})\n`
|
|
231
|
+
);
|
|
232
|
+
}
|
|
954
233
|
|
|
955
|
-
|
|
956
|
-
const change = changes[i];
|
|
957
|
-
const refId = change.refId;
|
|
958
|
-
const ref = $refs.get(refId);
|
|
959
|
-
const $callbacks: Schema['$callbacks'] | SchemaDecoderCallbacks['$callbacks'] = ref['$callbacks'];
|
|
234
|
+
dumpChangeSet(changeSet);
|
|
960
235
|
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
change.previousValue instanceof Schema
|
|
967
|
-
) {
|
|
968
|
-
change.previousValue['$callbacks']?.[OPERATION.DELETE]?.forEach(callback => callback());
|
|
969
|
-
}
|
|
236
|
+
// display filtered changes
|
|
237
|
+
if (!isEncodeAll && changeTree.filteredChanges?.size > 0) {
|
|
238
|
+
output += `${instance.constructor.name} (${changeTree.refId}) -> .filteredChanges:\n`;
|
|
239
|
+
dumpChangeSet(changeTree.filteredChanges);
|
|
240
|
+
}
|
|
970
241
|
|
|
971
|
-
|
|
972
|
-
|
|
242
|
+
// display filtered changes
|
|
243
|
+
if (isEncodeAll && changeTree.allFilteredChanges?.size > 0) {
|
|
244
|
+
output += `${instance.constructor.name} (${changeTree.refId}) -> .allFilteredChanges:\n`;
|
|
245
|
+
dumpChangeSet(changeTree.allFilteredChanges);
|
|
246
|
+
}
|
|
973
247
|
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
try {
|
|
977
|
-
// trigger onChange
|
|
978
|
-
($callbacks as Schema['$callbacks'])?.[OPERATION.REPLACE]?.forEach(callback =>
|
|
979
|
-
callback());
|
|
248
|
+
return output;
|
|
249
|
+
}
|
|
980
250
|
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
}
|
|
984
|
-
}
|
|
251
|
+
static debugChangesDeep(ref: Ref) {
|
|
252
|
+
let output = "";
|
|
985
253
|
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
$callbacks[change.field]?.forEach((callback) =>
|
|
989
|
-
callback(change.value, change.previousValue));
|
|
990
|
-
}
|
|
254
|
+
const rootChangeTree = ref[$changes];
|
|
255
|
+
const changeTrees: Map<ChangeTree, ChangeTree[]> = new Map();
|
|
991
256
|
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
}
|
|
257
|
+
let totalInstances = 0;
|
|
258
|
+
let totalOperations = 0;
|
|
995
259
|
|
|
996
|
-
|
|
997
|
-
|
|
260
|
+
for (const [changeTree, changes] of (rootChangeTree.root.changes.entries())) {
|
|
261
|
+
let includeChangeTree = false;
|
|
262
|
+
let parentChangeTrees: ChangeTree[] = [];
|
|
263
|
+
let parentChangeTree = changeTree.parent?.[$changes];
|
|
998
264
|
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
$callbacks[OPERATION.ADD]?.forEach(callback =>
|
|
1002
|
-
callback(change.value, change.dynamicIndex ?? change.field));
|
|
265
|
+
if (changeTree === rootChangeTree) {
|
|
266
|
+
includeChangeTree = true;
|
|
1003
267
|
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
// triger onRemove
|
|
1011
|
-
$callbacks[OPERATION.DELETE]?.forEach(callback =>
|
|
1012
|
-
callback(change.previousValue, change.dynamicIndex ?? change.field));
|
|
268
|
+
} else {
|
|
269
|
+
while (parentChangeTree !== undefined) {
|
|
270
|
+
parentChangeTrees.push(parentChangeTree);
|
|
271
|
+
if (parentChangeTree.ref === ref) {
|
|
272
|
+
includeChangeTree = true;
|
|
273
|
+
break;
|
|
1013
274
|
}
|
|
275
|
+
parentChangeTree = parentChangeTree.parent?.[$changes];
|
|
276
|
+
}
|
|
277
|
+
}
|
|
1014
278
|
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
279
|
+
if (includeChangeTree) {
|
|
280
|
+
totalInstances += 1;
|
|
281
|
+
totalOperations += changes.size;
|
|
282
|
+
changeTrees.set(changeTree, parentChangeTrees.reverse());
|
|
283
|
+
}
|
|
284
|
+
}
|
|
1021
285
|
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
286
|
+
output += "---\n"
|
|
287
|
+
output += `root refId: ${rootChangeTree.refId}\n`;
|
|
288
|
+
output += `Total instances: ${totalInstances}\n`;
|
|
289
|
+
output += `Total changes: ${totalOperations}\n`;
|
|
290
|
+
output += "---\n"
|
|
291
|
+
|
|
292
|
+
// based on root.changes, display a tree of changes that has the "ref" instance as parent
|
|
293
|
+
const visitedParents = new WeakSet<ChangeTree>();
|
|
294
|
+
for (const [changeTree, parentChangeTrees] of changeTrees.entries()) {
|
|
295
|
+
parentChangeTrees.forEach((parentChangeTree, level) => {
|
|
296
|
+
if (!visitedParents.has(parentChangeTree)) {
|
|
297
|
+
output += `${getIndent(level)}${parentChangeTree.ref.constructor.name} (refId: ${parentChangeTree.refId})\n`;
|
|
298
|
+
visitedParents.add(parentChangeTree);
|
|
1025
299
|
}
|
|
300
|
+
});
|
|
1026
301
|
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
302
|
+
const changes = changeTree.changes;
|
|
303
|
+
const level = parentChangeTrees.length;
|
|
304
|
+
const indent = getIndent(level);
|
|
305
|
+
|
|
306
|
+
const parentIndex = (level > 0) ? `(${changeTree.parentIndex}) ` : "";
|
|
307
|
+
output += `${indent}${parentIndex}${changeTree.ref.constructor.name} (refId: ${changeTree.refId}) - changes: ${changes.size}\n`;
|
|
308
|
+
|
|
309
|
+
for (const [index, operation] of changes) {
|
|
310
|
+
output += `${getIndent(level + 1)}${OPERATION[operation]}: ${index}\n`;
|
|
1032
311
|
}
|
|
1033
312
|
|
|
1034
|
-
uniqueRefIds.add(refId);
|
|
1035
313
|
}
|
|
1036
314
|
|
|
315
|
+
return `${output}`;
|
|
1037
316
|
}
|
|
317
|
+
|
|
318
|
+
|
|
1038
319
|
}
|
|
320
|
+
|