@colyseus/schema 2.0.4 → 2.0.6
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/README.md +0 -4
- package/build/cjs/index.js +48 -48
- package/build/cjs/index.js.map +1 -1
- package/build/esm/index.mjs +130 -104
- package/build/esm/index.mjs.map +1 -1
- package/build/umd/index.js +50 -50
- package/lib/Reflection.js +87 -119
- package/lib/Reflection.js.map +1 -1
- package/lib/Schema.js +195 -257
- package/lib/Schema.js.map +1 -1
- package/lib/annotations.d.ts +6 -6
- package/lib/annotations.js +64 -92
- package/lib/annotations.js.map +1 -1
- package/lib/changes/ChangeTree.d.ts +1 -1
- package/lib/changes/ChangeTree.js +63 -70
- package/lib/changes/ChangeTree.js.map +1 -1
- package/lib/changes/ReferenceTracker.js +24 -27
- package/lib/changes/ReferenceTracker.js.map +1 -1
- package/lib/codegen/api.js +9 -9
- package/lib/codegen/api.js.map +1 -1
- package/lib/codegen/argv.d.ts +1 -1
- package/lib/codegen/argv.js +11 -11
- package/lib/codegen/argv.js.map +1 -1
- package/lib/codegen/cli.js +21 -10
- package/lib/codegen/cli.js.map +1 -1
- package/lib/codegen/languages/cpp.js +126 -77
- package/lib/codegen/languages/cpp.js.map +1 -1
- package/lib/codegen/languages/csharp.js +121 -62
- package/lib/codegen/languages/csharp.js.map +1 -1
- package/lib/codegen/languages/haxe.js +34 -26
- package/lib/codegen/languages/haxe.js.map +1 -1
- package/lib/codegen/languages/java.js +39 -27
- package/lib/codegen/languages/java.js.map +1 -1
- package/lib/codegen/languages/js.js +48 -32
- package/lib/codegen/languages/js.js.map +1 -1
- package/lib/codegen/languages/lua.js +35 -24
- package/lib/codegen/languages/lua.js.map +1 -1
- package/lib/codegen/languages/ts.js +63 -68
- package/lib/codegen/languages/ts.js.map +1 -1
- package/lib/codegen/parser.d.ts +9 -1
- package/lib/codegen/parser.js +88 -46
- package/lib/codegen/parser.js.map +1 -1
- package/lib/codegen/types.d.ts +8 -0
- package/lib/codegen/types.js +64 -54
- package/lib/codegen/types.js.map +1 -1
- package/lib/encoding/decode.js +15 -15
- package/lib/encoding/decode.js.map +1 -1
- package/lib/encoding/encode.js +14 -14
- package/lib/encoding/encode.js.map +1 -1
- package/lib/events/EventEmitter.d.ts +1 -1
- package/lib/events/EventEmitter.js +16 -47
- package/lib/events/EventEmitter.js.map +1 -1
- package/lib/filters/index.js +7 -8
- package/lib/filters/index.js.map +1 -1
- package/lib/index.js +11 -11
- package/lib/index.js.map +1 -1
- package/lib/types/ArraySchema.d.ts +1 -1
- package/lib/types/ArraySchema.js +161 -219
- package/lib/types/ArraySchema.js.map +1 -1
- package/lib/types/CollectionSchema.d.ts +1 -1
- package/lib/types/CollectionSchema.js +63 -71
- package/lib/types/CollectionSchema.js.map +1 -1
- package/lib/types/HelperTypes.d.ts +9 -9
- package/lib/types/MapSchema.d.ts +16 -16
- package/lib/types/MapSchema.js +68 -78
- package/lib/types/MapSchema.js.map +1 -1
- package/lib/types/SetSchema.js +62 -71
- package/lib/types/SetSchema.js.map +1 -1
- package/lib/types/index.js +1 -1
- package/lib/types/index.js.map +1 -1
- package/lib/types/typeRegistry.js +1 -1
- package/lib/types/typeRegistry.js.map +1 -1
- package/lib/types/utils.js +9 -10
- package/lib/types/utils.js.map +1 -1
- package/lib/utils.js +10 -13
- package/lib/utils.js.map +1 -1
- package/package.json +18 -15
- package/src/Reflection.ts +159 -0
- package/src/Schema.ts +1024 -0
- package/src/annotations.ts +400 -0
- package/src/changes/ChangeTree.ts +295 -0
- package/src/changes/ReferenceTracker.ts +81 -0
- package/src/codegen/api.ts +46 -0
- package/src/codegen/argv.ts +40 -0
- package/src/codegen/cli.ts +65 -0
- package/src/codegen/languages/cpp.ts +297 -0
- package/src/codegen/languages/csharp.ts +208 -0
- package/src/codegen/languages/haxe.ts +110 -0
- package/src/codegen/languages/java.ts +115 -0
- package/src/codegen/languages/js.ts +115 -0
- package/src/codegen/languages/lua.ts +125 -0
- package/src/codegen/languages/ts.ts +129 -0
- package/src/codegen/parser.ts +299 -0
- package/src/codegen/types.ts +177 -0
- package/src/encoding/decode.ts +278 -0
- package/src/encoding/encode.ts +283 -0
- package/src/filters/index.ts +23 -0
- package/src/index.ts +59 -0
- package/src/spec.ts +49 -0
- package/src/types/ArraySchema.ts +612 -0
- package/src/types/CollectionSchema.ts +199 -0
- package/src/types/HelperTypes.ts +34 -0
- package/src/types/MapSchema.ts +268 -0
- package/src/types/SetSchema.ts +208 -0
- package/src/types/typeRegistry.ts +19 -0
- package/src/types/utils.ts +62 -0
- package/src/utils.ts +28 -0
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import { ChangeTree } from './changes/ChangeTree';
|
|
2
|
+
import { Schema } from './Schema';
|
|
3
|
+
import { ArraySchema, getArrayProxy } from './types/ArraySchema';
|
|
4
|
+
import { MapSchema, getMapProxy } from './types/MapSchema';
|
|
5
|
+
import { getType } from './types/typeRegistry';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Data types
|
|
9
|
+
*/
|
|
10
|
+
export type PrimitiveType =
|
|
11
|
+
"string" |
|
|
12
|
+
"number" |
|
|
13
|
+
"boolean" |
|
|
14
|
+
"int8" |
|
|
15
|
+
"uint8" |
|
|
16
|
+
"int16" |
|
|
17
|
+
"uint16" |
|
|
18
|
+
"int32" |
|
|
19
|
+
"uint32" |
|
|
20
|
+
"int64" |
|
|
21
|
+
"uint64" |
|
|
22
|
+
"float32" |
|
|
23
|
+
"float64" |
|
|
24
|
+
typeof Schema;
|
|
25
|
+
|
|
26
|
+
export type DefinitionType = PrimitiveType
|
|
27
|
+
| PrimitiveType[]
|
|
28
|
+
| { array: PrimitiveType }
|
|
29
|
+
| { map: PrimitiveType }
|
|
30
|
+
| { collection: PrimitiveType }
|
|
31
|
+
| { set: PrimitiveType };
|
|
32
|
+
|
|
33
|
+
export type Definition = { [field: string]: DefinitionType };
|
|
34
|
+
export type FilterCallback<
|
|
35
|
+
T extends Schema = any,
|
|
36
|
+
V = any,
|
|
37
|
+
R extends Schema = any
|
|
38
|
+
> = (
|
|
39
|
+
((this: T, client: ClientWithSessionId, value: V) => boolean) |
|
|
40
|
+
((this: T, client: ClientWithSessionId, value: V, root: R) => boolean)
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
export type FilterChildrenCallback<
|
|
44
|
+
T extends Schema = any,
|
|
45
|
+
K = any,
|
|
46
|
+
V = any,
|
|
47
|
+
R extends Schema = any
|
|
48
|
+
> = (
|
|
49
|
+
((this: T, client: ClientWithSessionId, key: K, value: V) => boolean) |
|
|
50
|
+
((this: T, client: ClientWithSessionId, key: K, value: V, root: R) => boolean)
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
export class SchemaDefinition {
|
|
54
|
+
schema: Definition;
|
|
55
|
+
|
|
56
|
+
//
|
|
57
|
+
// TODO: use a "field" structure combining all these properties per-field.
|
|
58
|
+
//
|
|
59
|
+
|
|
60
|
+
indexes: { [field: string]: number } = {};
|
|
61
|
+
fieldsByIndex: { [index: number]: string } = {};
|
|
62
|
+
|
|
63
|
+
filters: { [field: string]: FilterCallback };
|
|
64
|
+
indexesWithFilters: number[];
|
|
65
|
+
childFilters: { [field: string]: FilterChildrenCallback }; // childFilters are used on Map, Array, Set items.
|
|
66
|
+
|
|
67
|
+
deprecated: { [field: string]: boolean } = {};
|
|
68
|
+
descriptors: PropertyDescriptorMap & ThisType<any> = {};
|
|
69
|
+
|
|
70
|
+
static create(parent?: SchemaDefinition) {
|
|
71
|
+
const definition = new SchemaDefinition();
|
|
72
|
+
|
|
73
|
+
// support inheritance
|
|
74
|
+
definition.schema = Object.assign({}, parent && parent.schema || {});
|
|
75
|
+
definition.indexes = Object.assign({}, parent && parent.indexes || {});
|
|
76
|
+
definition.fieldsByIndex = Object.assign({}, parent && parent.fieldsByIndex || {});
|
|
77
|
+
definition.descriptors = Object.assign({}, parent && parent.descriptors || {});
|
|
78
|
+
definition.deprecated = Object.assign({}, parent && parent.deprecated || {});
|
|
79
|
+
|
|
80
|
+
return definition;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
addField(field: string, type: DefinitionType) {
|
|
84
|
+
const index = this.getNextFieldIndex();
|
|
85
|
+
this.fieldsByIndex[index] = field;
|
|
86
|
+
this.indexes[field] = index;
|
|
87
|
+
this.schema[field] = (Array.isArray(type))
|
|
88
|
+
? { array: type[0] }
|
|
89
|
+
: type;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
hasField(field: string) {
|
|
93
|
+
return this.indexes[field] !== undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
addFilter(field: string, cb: FilterCallback) {
|
|
97
|
+
if (!this.filters) {
|
|
98
|
+
this.filters = {};
|
|
99
|
+
this.indexesWithFilters = [];
|
|
100
|
+
}
|
|
101
|
+
this.filters[this.indexes[field]] = cb;
|
|
102
|
+
this.indexesWithFilters.push(this.indexes[field]);
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
addChildrenFilter(field: string, cb: FilterChildrenCallback) {
|
|
107
|
+
const index = this.indexes[field];
|
|
108
|
+
const type = this.schema[field];
|
|
109
|
+
|
|
110
|
+
if (getType(Object.keys(type)[0])) {
|
|
111
|
+
if (!this.childFilters) { this.childFilters = {}; }
|
|
112
|
+
|
|
113
|
+
this.childFilters[index] = cb;
|
|
114
|
+
return true;
|
|
115
|
+
|
|
116
|
+
} else {
|
|
117
|
+
console.warn(`@filterChildren: field '${field}' can't have children. Ignoring filter.`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
getChildrenFilter(field: string) {
|
|
122
|
+
return this.childFilters && this.childFilters[this.indexes[field]];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
getNextFieldIndex() {
|
|
126
|
+
return Object.keys(this.schema || {}).length;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function hasFilter(klass: typeof Schema) {
|
|
131
|
+
return klass._context && klass._context.useFilters;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Colyseus integration
|
|
135
|
+
export type ClientWithSessionId = { sessionId: string } & any;
|
|
136
|
+
|
|
137
|
+
export interface TypeOptions {
|
|
138
|
+
manual?: boolean,
|
|
139
|
+
context?: Context,
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export class Context {
|
|
143
|
+
types: {[id: number]: typeof Schema} = {};
|
|
144
|
+
schemas = new Map<typeof Schema, number>();
|
|
145
|
+
useFilters = false;
|
|
146
|
+
|
|
147
|
+
has(schema: typeof Schema) {
|
|
148
|
+
return this.schemas.has(schema);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
get(typeid: number) {
|
|
152
|
+
return this.types[typeid];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
add(schema: typeof Schema, typeid: number = this.schemas.size) {
|
|
156
|
+
// FIXME: move this to somewhere else?
|
|
157
|
+
// support inheritance
|
|
158
|
+
schema._definition = SchemaDefinition.create(schema._definition);
|
|
159
|
+
|
|
160
|
+
schema._typeid = typeid;
|
|
161
|
+
this.types[typeid] = schema;
|
|
162
|
+
this.schemas.set(schema, typeid);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
static create(options: TypeOptions = {}) {
|
|
167
|
+
return function (definition: DefinitionType) {
|
|
168
|
+
if (!options.context) {
|
|
169
|
+
options.context = new Context();
|
|
170
|
+
}
|
|
171
|
+
return type(definition, options);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export const globalContext = new Context();
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* [See documentation](https://docs.colyseus.io/state/schema/)
|
|
180
|
+
*
|
|
181
|
+
* Annotate a Schema property to be serializeable.
|
|
182
|
+
* \@type()'d fields are automatically flagged as "dirty" for the next patch.
|
|
183
|
+
*
|
|
184
|
+
* @example Standard usage, with automatic change tracking.
|
|
185
|
+
* ```
|
|
186
|
+
* \@type("string") propertyName: string;
|
|
187
|
+
* ```
|
|
188
|
+
*
|
|
189
|
+
* @example You can provide the "manual" option if you'd like to manually control your patches via .setDirty().
|
|
190
|
+
* ```
|
|
191
|
+
* \@type("string", { manual: true })
|
|
192
|
+
* ```
|
|
193
|
+
*/
|
|
194
|
+
export function type (
|
|
195
|
+
type: DefinitionType,
|
|
196
|
+
options: TypeOptions = {}
|
|
197
|
+
): PropertyDecorator {
|
|
198
|
+
return function (target: typeof Schema, field: string) {
|
|
199
|
+
const context = options.context || globalContext;
|
|
200
|
+
const constructor = target.constructor as typeof Schema;
|
|
201
|
+
constructor._context = context;
|
|
202
|
+
|
|
203
|
+
if (!type) {
|
|
204
|
+
throw new Error(`${constructor.name}: @type() reference provided for "${field}" is undefined. Make sure you don't have any circular dependencies.`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/*
|
|
208
|
+
* static schema
|
|
209
|
+
*/
|
|
210
|
+
if (!context.has(constructor)) {
|
|
211
|
+
context.add(constructor);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const definition = constructor._definition;
|
|
215
|
+
definition.addField(field, type);
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* skip if descriptor already exists for this field (`@deprecated()`)
|
|
219
|
+
*/
|
|
220
|
+
if (definition.descriptors[field]) {
|
|
221
|
+
if (definition.deprecated[field]) {
|
|
222
|
+
// do not create accessors for deprecated properties.
|
|
223
|
+
return;
|
|
224
|
+
|
|
225
|
+
} else {
|
|
226
|
+
// trying to define same property multiple times across inheritance.
|
|
227
|
+
// https://github.com/colyseus/colyseus-unity3d/issues/131#issuecomment-814308572
|
|
228
|
+
try {
|
|
229
|
+
throw new Error(`@colyseus/schema: Duplicate '${field}' definition on '${constructor.name}'.\nCheck @type() annotation`);
|
|
230
|
+
|
|
231
|
+
} catch (e) {
|
|
232
|
+
const definitionAtLine = e.stack.split("\n")[4].trim();
|
|
233
|
+
throw new Error(`${e.message} ${definitionAtLine}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const isArray = ArraySchema.is(type);
|
|
239
|
+
const isMap = !isArray && MapSchema.is(type);
|
|
240
|
+
|
|
241
|
+
// TODO: refactor me.
|
|
242
|
+
// Allow abstract intermediary classes with no fields to be serialized
|
|
243
|
+
// (See "should support an inheritance with a Schema type without fields" test)
|
|
244
|
+
if (typeof (type) !== "string" && !Schema.is(type)) {
|
|
245
|
+
const childType = Object.values(type)[0];
|
|
246
|
+
if (typeof (childType) !== "string" && !context.has(childType)) {
|
|
247
|
+
context.add(childType);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (options.manual) {
|
|
252
|
+
// do not declare getter/setter descriptor
|
|
253
|
+
definition.descriptors[field] = {
|
|
254
|
+
enumerable: true,
|
|
255
|
+
configurable: true,
|
|
256
|
+
writable: true,
|
|
257
|
+
};
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const fieldCached = `_${field}`;
|
|
262
|
+
definition.descriptors[fieldCached] = {
|
|
263
|
+
enumerable: false,
|
|
264
|
+
configurable: false,
|
|
265
|
+
writable: true,
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
definition.descriptors[field] = {
|
|
269
|
+
get: function () {
|
|
270
|
+
return this[fieldCached];
|
|
271
|
+
},
|
|
272
|
+
|
|
273
|
+
set: function (this: Schema, value: any) {
|
|
274
|
+
/**
|
|
275
|
+
* Create Proxy for array or map items
|
|
276
|
+
*/
|
|
277
|
+
|
|
278
|
+
// skip if value is the same as cached.
|
|
279
|
+
if (value === this[fieldCached]) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (
|
|
284
|
+
value !== undefined &&
|
|
285
|
+
value !== null
|
|
286
|
+
) {
|
|
287
|
+
// automaticallty transform Array into ArraySchema
|
|
288
|
+
if (isArray && !(value instanceof ArraySchema)) {
|
|
289
|
+
value = new ArraySchema(...value);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// automaticallty transform Map into MapSchema
|
|
293
|
+
if (isMap && !(value instanceof MapSchema)) {
|
|
294
|
+
value = new MapSchema(value);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// try to turn provided structure into a Proxy
|
|
298
|
+
if (value['$proxy'] === undefined) {
|
|
299
|
+
if (isMap) {
|
|
300
|
+
value = getMapProxy(value);
|
|
301
|
+
|
|
302
|
+
} else if (isArray) {
|
|
303
|
+
value = getArrayProxy(value);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// flag the change for encoding.
|
|
308
|
+
this.$changes.change(field);
|
|
309
|
+
|
|
310
|
+
//
|
|
311
|
+
// call setParent() recursively for this and its child
|
|
312
|
+
// structures.
|
|
313
|
+
//
|
|
314
|
+
if (value['$changes']) {
|
|
315
|
+
(value['$changes'] as ChangeTree).setParent(
|
|
316
|
+
this,
|
|
317
|
+
this.$changes.root,
|
|
318
|
+
this._definition.indexes[field],
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
} else if (this[fieldCached]) {
|
|
323
|
+
//
|
|
324
|
+
// Setting a field to `null` or `undefined` will delete it.
|
|
325
|
+
//
|
|
326
|
+
this.$changes.delete(field);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
this[fieldCached] = value;
|
|
330
|
+
},
|
|
331
|
+
|
|
332
|
+
enumerable: true,
|
|
333
|
+
configurable: true
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* `@filter()` decorator for defining data filters per client
|
|
340
|
+
*/
|
|
341
|
+
|
|
342
|
+
export function filter<T extends Schema, V, R extends Schema>(cb: FilterCallback<T, V, R>): PropertyDecorator {
|
|
343
|
+
return function (target: any, field: string) {
|
|
344
|
+
const constructor = target.constructor as typeof Schema;
|
|
345
|
+
const definition = constructor._definition;
|
|
346
|
+
|
|
347
|
+
if (definition.addFilter(field, cb)) {
|
|
348
|
+
constructor._context.useFilters = true;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export function filterChildren<T extends Schema, K, V, R extends Schema>(cb: FilterChildrenCallback<T, K, V, R>): PropertyDecorator {
|
|
354
|
+
return function (target: any, field: string) {
|
|
355
|
+
const constructor = target.constructor as typeof Schema;
|
|
356
|
+
const definition = constructor._definition;
|
|
357
|
+
if (definition.addChildrenFilter(field, cb)) {
|
|
358
|
+
constructor._context.useFilters = true;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* `@deprecated()` flag a field as deprecated.
|
|
366
|
+
* The previous `@type()` annotation should remain along with this one.
|
|
367
|
+
*/
|
|
368
|
+
|
|
369
|
+
export function deprecated(throws: boolean = true): PropertyDecorator {
|
|
370
|
+
return function (target: typeof Schema, field: string) {
|
|
371
|
+
const constructor = target.constructor as typeof Schema;
|
|
372
|
+
const definition = constructor._definition;
|
|
373
|
+
|
|
374
|
+
definition.deprecated[field] = true;
|
|
375
|
+
|
|
376
|
+
if (throws) {
|
|
377
|
+
definition.descriptors[field] = {
|
|
378
|
+
get: function () { throw new Error(`${field} is deprecated.`); },
|
|
379
|
+
set: function (this: Schema, value: any) { /* throw new Error(`${field} is deprecated.`); */ },
|
|
380
|
+
enumerable: false,
|
|
381
|
+
configurable: true
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export function defineTypes(
|
|
388
|
+
target: typeof Schema,
|
|
389
|
+
fields: { [property: string]: DefinitionType },
|
|
390
|
+
options: TypeOptions = {}
|
|
391
|
+
) {
|
|
392
|
+
if (!options.context) {
|
|
393
|
+
options.context = target._context || options.context || globalContext;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
for (let field in fields) {
|
|
397
|
+
type(fields[field], options)(target.prototype, field);
|
|
398
|
+
}
|
|
399
|
+
return target;
|
|
400
|
+
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { OPERATION } from "../spec";
|
|
2
|
+
import { Schema } from "../Schema";
|
|
3
|
+
import { SchemaDefinition, FilterChildrenCallback } from "../annotations";
|
|
4
|
+
|
|
5
|
+
import { MapSchema } from "../types/MapSchema";
|
|
6
|
+
import { ArraySchema } from "../types/ArraySchema";
|
|
7
|
+
import { CollectionSchema } from "../types/CollectionSchema";
|
|
8
|
+
import { SetSchema } from "../types/SetSchema";
|
|
9
|
+
import { ReferenceTracker } from "./ReferenceTracker";
|
|
10
|
+
|
|
11
|
+
export type Ref = Schema
|
|
12
|
+
| ArraySchema
|
|
13
|
+
| MapSchema
|
|
14
|
+
| CollectionSchema
|
|
15
|
+
| SetSchema;
|
|
16
|
+
|
|
17
|
+
export interface ChangeOperation {
|
|
18
|
+
op: OPERATION,
|
|
19
|
+
index: number,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
//
|
|
23
|
+
// FieldCache is used for @filter()
|
|
24
|
+
//
|
|
25
|
+
export interface FieldCache {
|
|
26
|
+
beginIndex: number;
|
|
27
|
+
endIndex: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
export class ChangeTree {
|
|
32
|
+
ref: Ref;
|
|
33
|
+
refId: number;
|
|
34
|
+
|
|
35
|
+
root?: ReferenceTracker;
|
|
36
|
+
|
|
37
|
+
parent?: Ref;
|
|
38
|
+
parentIndex?: number;
|
|
39
|
+
|
|
40
|
+
indexes: {[index: string]: any};
|
|
41
|
+
|
|
42
|
+
changed: boolean = false;
|
|
43
|
+
changes = new Map<number, ChangeOperation>();
|
|
44
|
+
allChanges = new Set<number>();
|
|
45
|
+
|
|
46
|
+
// cached indexes for filtering
|
|
47
|
+
caches: {[field: number]: number[]} = {};
|
|
48
|
+
|
|
49
|
+
currentCustomOperation: number = 0;
|
|
50
|
+
|
|
51
|
+
constructor(ref: Ref, parent?: Ref, root?: ReferenceTracker) {
|
|
52
|
+
this.ref = ref;
|
|
53
|
+
this.setParent(parent, root);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
setParent(
|
|
57
|
+
parent: Ref,
|
|
58
|
+
root?: ReferenceTracker,
|
|
59
|
+
parentIndex?: number,
|
|
60
|
+
) {
|
|
61
|
+
if (!this.indexes) {
|
|
62
|
+
this.indexes = (this.ref instanceof Schema)
|
|
63
|
+
? this.ref['_definition'].indexes
|
|
64
|
+
: {};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
this.parent = parent;
|
|
68
|
+
this.parentIndex = parentIndex;
|
|
69
|
+
|
|
70
|
+
// avoid setting parents with empty `root`
|
|
71
|
+
if (!root) { return; }
|
|
72
|
+
this.root = root;
|
|
73
|
+
|
|
74
|
+
//
|
|
75
|
+
// assign same parent on child structures
|
|
76
|
+
//
|
|
77
|
+
if (this.ref instanceof Schema) {
|
|
78
|
+
const definition: SchemaDefinition = this.ref['_definition'];
|
|
79
|
+
|
|
80
|
+
for (let field in definition.schema) {
|
|
81
|
+
const value = this.ref[field];
|
|
82
|
+
|
|
83
|
+
if (value && value['$changes']) {
|
|
84
|
+
const parentIndex = definition.indexes[field];
|
|
85
|
+
|
|
86
|
+
(value['$changes'] as ChangeTree).setParent(
|
|
87
|
+
this.ref,
|
|
88
|
+
root,
|
|
89
|
+
parentIndex,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
} else if (typeof (this.ref) === "object") {
|
|
95
|
+
this.ref.forEach((value, key) => {
|
|
96
|
+
if (value instanceof Schema) {
|
|
97
|
+
const changeTreee = value['$changes'];
|
|
98
|
+
const parentIndex = this.ref['$changes'].indexes[key];
|
|
99
|
+
|
|
100
|
+
changeTreee.setParent(
|
|
101
|
+
this.ref,
|
|
102
|
+
this.root,
|
|
103
|
+
parentIndex,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
operation(op: ChangeOperation) {
|
|
111
|
+
this.changes.set(--this.currentCustomOperation, op);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
change(fieldName: string | number, operation: OPERATION = OPERATION.ADD) {
|
|
115
|
+
const index = (typeof (fieldName) === "number")
|
|
116
|
+
? fieldName
|
|
117
|
+
: this.indexes[fieldName];
|
|
118
|
+
|
|
119
|
+
this.assertValidIndex(index, fieldName);
|
|
120
|
+
|
|
121
|
+
const previousChange = this.changes.get(index);
|
|
122
|
+
|
|
123
|
+
if (
|
|
124
|
+
!previousChange ||
|
|
125
|
+
previousChange.op === OPERATION.DELETE ||
|
|
126
|
+
previousChange.op === OPERATION.TOUCH // (mazmorra.io's BattleAction issue)
|
|
127
|
+
) {
|
|
128
|
+
this.changes.set(index, {
|
|
129
|
+
op: (!previousChange)
|
|
130
|
+
? operation
|
|
131
|
+
: (previousChange.op === OPERATION.DELETE)
|
|
132
|
+
? OPERATION.DELETE_AND_ADD
|
|
133
|
+
: operation,
|
|
134
|
+
// : OPERATION.REPLACE,
|
|
135
|
+
index
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
this.allChanges.add(index);
|
|
140
|
+
|
|
141
|
+
this.changed = true;
|
|
142
|
+
this.touchParents();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
touch(fieldName: string | number) {
|
|
146
|
+
const index = (typeof (fieldName) === "number")
|
|
147
|
+
? fieldName
|
|
148
|
+
: this.indexes[fieldName];
|
|
149
|
+
|
|
150
|
+
this.assertValidIndex(index, fieldName);
|
|
151
|
+
|
|
152
|
+
if (!this.changes.has(index)) {
|
|
153
|
+
this.changes.set(index, { op: OPERATION.TOUCH, index });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
this.allChanges.add(index);
|
|
157
|
+
|
|
158
|
+
// ensure touch is placed until the $root is found.
|
|
159
|
+
this.touchParents();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
touchParents() {
|
|
163
|
+
if (this.parent) {
|
|
164
|
+
(this.parent['$changes'] as ChangeTree).touch(this.parentIndex);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
getType(index?: number) {
|
|
169
|
+
if (this.ref['_definition']) {
|
|
170
|
+
const definition = (this.ref as Schema)['_definition'];
|
|
171
|
+
return definition.schema[ definition.fieldsByIndex[index] ];
|
|
172
|
+
|
|
173
|
+
} else {
|
|
174
|
+
const definition = (this.parent as Schema)['_definition'];
|
|
175
|
+
const parentType = definition.schema[ definition.fieldsByIndex[this.parentIndex] ];
|
|
176
|
+
|
|
177
|
+
//
|
|
178
|
+
// Get the child type from parent structure.
|
|
179
|
+
// - ["string"] => "string"
|
|
180
|
+
// - { map: "string" } => "string"
|
|
181
|
+
// - { set: "string" } => "string"
|
|
182
|
+
//
|
|
183
|
+
return Object.values(parentType)[0];
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
getChildrenFilter(): FilterChildrenCallback {
|
|
188
|
+
const childFilters = (this.parent as Schema)['_definition'].childFilters;
|
|
189
|
+
return childFilters && childFilters[this.parentIndex];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
//
|
|
193
|
+
// used during `.encode()`
|
|
194
|
+
//
|
|
195
|
+
getValue(index: number) {
|
|
196
|
+
return this.ref['getByIndex'](index);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
delete(fieldName: string | number) {
|
|
200
|
+
const index = (typeof (fieldName) === "number")
|
|
201
|
+
? fieldName
|
|
202
|
+
: this.indexes[fieldName];
|
|
203
|
+
|
|
204
|
+
if (index === undefined) {
|
|
205
|
+
console.warn(`@colyseus/schema ${this.ref.constructor.name}: trying to delete non-existing index: ${fieldName} (${index})`);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const previousValue = this.getValue(index);
|
|
210
|
+
// console.log("$changes.delete =>", { fieldName, index, previousValue });
|
|
211
|
+
|
|
212
|
+
this.changes.set(index, { op: OPERATION.DELETE, index });
|
|
213
|
+
|
|
214
|
+
this.allChanges.delete(index);
|
|
215
|
+
|
|
216
|
+
// delete cache
|
|
217
|
+
delete this.caches[index];
|
|
218
|
+
|
|
219
|
+
// remove `root` reference
|
|
220
|
+
if (previousValue && previousValue['$changes']) {
|
|
221
|
+
previousValue['$changes'].parent = undefined;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
this.changed = true;
|
|
225
|
+
this.touchParents();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
discard(changed: boolean = false, discardAll: boolean = false) {
|
|
229
|
+
//
|
|
230
|
+
// Map, Array, etc:
|
|
231
|
+
// Remove cached key to ensure ADD operations is unsed instead of
|
|
232
|
+
// REPLACE in case same key is used on next patches.
|
|
233
|
+
//
|
|
234
|
+
// TODO: refactor this. this is not relevant for Collection and Set.
|
|
235
|
+
//
|
|
236
|
+
if (!(this.ref instanceof Schema)) {
|
|
237
|
+
this.changes.forEach((change) => {
|
|
238
|
+
if (change.op === OPERATION.DELETE) {
|
|
239
|
+
const index = this.ref['getIndex'](change.index)
|
|
240
|
+
delete this.indexes[index];
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
this.changes.clear();
|
|
246
|
+
this.changed = changed;
|
|
247
|
+
|
|
248
|
+
if (discardAll) {
|
|
249
|
+
this.allChanges.clear();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// re-set `currentCustomOperation`
|
|
253
|
+
this.currentCustomOperation = 0;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Recursively discard all changes from this, and child structures.
|
|
258
|
+
*/
|
|
259
|
+
discardAll() {
|
|
260
|
+
this.changes.forEach((change) => {
|
|
261
|
+
const value = this.getValue(change.index);
|
|
262
|
+
|
|
263
|
+
if (value && value['$changes']) {
|
|
264
|
+
value['$changes'].discardAll();
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
this.discard();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// cache(field: number, beginIndex: number, endIndex: number) {
|
|
272
|
+
cache(field: number, cachedBytes: number[]) {
|
|
273
|
+
this.caches[field] = cachedBytes;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
clone() {
|
|
277
|
+
return new ChangeTree(this.ref, this.parent, this.root);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
ensureRefId() {
|
|
281
|
+
// skip if refId is already set.
|
|
282
|
+
if (this.refId !== undefined) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
this.refId = this.root.getNextUniqueId();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
protected assertValidIndex(index: number, fieldName: string | number) {
|
|
290
|
+
if (index === undefined) {
|
|
291
|
+
throw new Error(`ChangeTree: missing index for field "${fieldName}"`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
}
|