@tamgl/colyseus-schema
Version:
Binary state serializer with delta encoding for games
411 lines • 17.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.DEFAULT_VIEW_TAG = void 0;
exports.entity = entity;
exports.view = view;
exports.unreliable = unreliable;
exports.type = type;
exports.getPropertyDescriptor = getPropertyDescriptor;
exports.deprecated = deprecated;
exports.defineTypes = defineTypes;
exports.schema = schema;
require("./symbol.shim");
const Schema_1 = require("./Schema");
const ArraySchema_1 = require("./types/custom/ArraySchema");
const MapSchema_1 = require("./types/custom/MapSchema");
const Metadata_1 = require("./Metadata");
const symbols_1 = require("./types/symbols");
const registry_1 = require("./types/registry");
const spec_1 = require("./encoding/spec");
const TypeContext_1 = require("./types/TypeContext");
const assert_1 = require("./encoding/assert");
exports.DEFAULT_VIEW_TAG = -1;
function entity(constructor) {
TypeContext_1.TypeContext.register(constructor);
return constructor;
}
/**
* [See documentation](https://docs.colyseus.io/state/schema/)
*
* Annotate a Schema property to be serializeable.
* \@type()'d fields are automatically flagged as "dirty" for the next patch.
*
* @example Standard usage, with automatic change tracking.
* ```
* \@type("string") propertyName: string;
* ```
*
* @example You can provide the "manual" option if you'd like to manually control your patches via .setDirty().
* ```
* \@type("string", { manual: true })
* ```
*/
// export function type(type: DefinitionType, options?: TypeOptions) {
// return function ({ get, set }, context: ClassAccessorDecoratorContext): ClassAccessorDecoratorResult<Schema, any> {
// if (context.kind !== "accessor") {
// throw new Error("@type() is only supported for class accessor properties");
// }
// const field = context.name.toString();
// //
// // detect index for this field, considering inheritance
// //
// const parent = Object.getPrototypeOf(context.metadata);
// let fieldIndex: number = context.metadata[$numFields] // current structure already has fields defined
// ?? (parent && parent[$numFields]) // parent structure has fields defined
// ?? -1; // no fields defined
// fieldIndex++;
// if (
// !parent && // the parent already initializes the `$changes` property
// !Metadata.hasFields(context.metadata)
// ) {
// context.addInitializer(function (this: Ref) {
// Object.defineProperty(this, $changes, {
// value: new ChangeTree(this),
// enumerable: false,
// writable: true
// });
// });
// }
// Metadata.addField(context.metadata, fieldIndex, field, type);
// const isArray = ArraySchema.is(type);
// const isMap = !isArray && MapSchema.is(type);
// // if (options && options.manual) {
// // // do not declare getter/setter descriptor
// // definition.descriptors[field] = {
// // enumerable: true,
// // configurable: true,
// // writable: true,
// // };
// // return;
// // }
// return {
// init(value) {
// // TODO: may need to convert ArraySchema/MapSchema here
// // do not flag change if value is undefined.
// if (value !== undefined) {
// this[$changes].change(fieldIndex);
// // automaticallty transform Array into ArraySchema
// if (isArray) {
// if (!(value instanceof ArraySchema)) {
// value = new ArraySchema(...value);
// }
// value[$childType] = Object.values(type)[0];
// }
// // automaticallty transform Map into MapSchema
// if (isMap) {
// if (!(value instanceof MapSchema)) {
// value = new MapSchema(value);
// }
// value[$childType] = Object.values(type)[0];
// }
// // try to turn provided structure into a Proxy
// if (value['$proxy'] === undefined) {
// if (isMap) {
// value = getMapProxy(value);
// }
// }
// }
// return value;
// },
// get() {
// return get.call(this);
// },
// set(value: any) {
// /**
// * Create Proxy for array or map items
// */
// // skip if value is the same as cached.
// if (value === get.call(this)) {
// return;
// }
// if (
// value !== undefined &&
// value !== null
// ) {
// // automaticallty transform Array into ArraySchema
// if (isArray) {
// if (!(value instanceof ArraySchema)) {
// value = new ArraySchema(...value);
// }
// value[$childType] = Object.values(type)[0];
// }
// // automaticallty transform Map into MapSchema
// if (isMap) {
// if (!(value instanceof MapSchema)) {
// value = new MapSchema(value);
// }
// value[$childType] = Object.values(type)[0];
// }
// // try to turn provided structure into a Proxy
// if (value['$proxy'] === undefined) {
// if (isMap) {
// value = getMapProxy(value);
// }
// }
// // flag the change for encoding.
// this[$changes].change(fieldIndex);
// //
// // call setParent() recursively for this and its child
// // structures.
// //
// if (value[$changes]) {
// value[$changes].setParent(
// this,
// this[$changes].root,
// Metadata.getIndex(context.metadata, field),
// );
// }
// } else if (get.call(this)) {
// //
// // Setting a field to `null` or `undefined` will delete it.
// //
// this[$changes].delete(field);
// }
// set.call(this, value);
// },
// };
// }
// }
function view(tag = exports.DEFAULT_VIEW_TAG) {
return function (target, fieldName) {
const constructor = target.constructor;
const parentClass = Object.getPrototypeOf(constructor);
const parentMetadata = parentClass[Symbol.metadata];
// TODO: use Metadata.initialize()
const metadata = (constructor[Symbol.metadata] ??= Object.assign({}, constructor[Symbol.metadata], parentMetadata ?? Object.create(null)));
// const fieldIndex = metadata[fieldName];
// if (!metadata[fieldIndex]) {
// //
// // detect index for this field, considering inheritance
// //
// metadata[fieldIndex] = {
// type: undefined,
// index: (metadata[$numFields] // current structure already has fields defined
// ?? (parentMetadata && parentMetadata[$numFields]) // parent structure has fields defined
// ?? -1) + 1 // no fields defined
// }
// }
Metadata_1.Metadata.setTag(metadata, fieldName, tag);
};
}
function unreliable(target, field) {
//
// FIXME: the following block of code is repeated across `@type()`, `@deprecated()` and `@unreliable()` decorators.
//
const constructor = target.constructor;
const parentClass = Object.getPrototypeOf(constructor);
const parentMetadata = parentClass[Symbol.metadata];
// TODO: use Metadata.initialize()
const metadata = (constructor[Symbol.metadata] ??= Object.assign({}, constructor[Symbol.metadata], parentMetadata ?? Object.create(null)));
// if (!metadata[field]) {
// //
// // detect index for this field, considering inheritance
// //
// metadata[field] = {
// type: undefined,
// index: (metadata[$numFields] // current structure already has fields defined
// ?? (parentMetadata && parentMetadata[$numFields]) // parent structure has fields defined
// ?? -1) + 1 // no fields defined
// }
// }
// add owned flag to the field
metadata[metadata[field]].unreliable = true;
}
function type(type, options) {
return function (target, field) {
const constructor = target.constructor;
if (!type) {
throw new Error(`${constructor.name}: @type() reference provided for "${field}" is undefined. Make sure you don't have any circular dependencies.`);
}
// for inheritance support
TypeContext_1.TypeContext.register(constructor);
const parentClass = Object.getPrototypeOf(constructor);
const parentMetadata = parentClass[Symbol.metadata];
const metadata = Metadata_1.Metadata.initialize(constructor);
let fieldIndex = metadata[field];
/**
* skip if descriptor already exists for this field (`@deprecated()`)
*/
if (metadata[fieldIndex] !== undefined) {
if (metadata[fieldIndex].deprecated) {
// do not create accessors for deprecated properties.
return;
}
else if (metadata[fieldIndex].type !== undefined) {
// trying to define same property multiple times across inheritance.
// https://github.com/colyseus/colyseus-unity3d/issues/131#issuecomment-814308572
try {
throw new Error(`@colyseus/schema: Duplicate '${field}' definition on '${constructor.name}'.\nCheck @type() annotation`);
}
catch (e) {
const definitionAtLine = e.stack.split("\n")[4].trim();
throw new Error(`${e.message} ${definitionAtLine}`);
}
}
}
else {
//
// detect index for this field, considering inheritance
//
fieldIndex = metadata[symbols_1.$numFields] // current structure already has fields defined
?? (parentMetadata && parentMetadata[symbols_1.$numFields]) // parent structure has fields defined
?? -1; // no fields defined
fieldIndex++;
}
if (options && options.manual) {
Metadata_1.Metadata.addField(metadata, fieldIndex, field, type, {
// do not declare getter/setter descriptor
enumerable: true,
configurable: true,
writable: true,
});
}
else {
const complexTypeKlass = (Array.isArray(type))
? (0, registry_1.getType)("array")
: (typeof (Object.keys(type)[0]) === "string") && (0, registry_1.getType)(Object.keys(type)[0]);
const childType = (complexTypeKlass)
? Object.values(type)[0]
: type;
Metadata_1.Metadata.addField(metadata, fieldIndex, field, type, getPropertyDescriptor(`_${field}`, fieldIndex, childType, complexTypeKlass));
}
};
}
function getPropertyDescriptor(fieldCached, fieldIndex, type, complexTypeKlass) {
return {
get: function () { return this[fieldCached]; },
set: function (value) {
const previousValue = this[fieldCached] ?? undefined;
// skip if value is the same as cached.
if (value === previousValue) {
return;
}
if (value !== undefined &&
value !== null) {
if (complexTypeKlass) {
// automaticallty transform Array into ArraySchema
if (complexTypeKlass.constructor === ArraySchema_1.ArraySchema && !(value instanceof ArraySchema_1.ArraySchema)) {
value = new ArraySchema_1.ArraySchema(...value);
}
// automaticallty transform Map into MapSchema
if (complexTypeKlass.constructor === MapSchema_1.MapSchema && !(value instanceof MapSchema_1.MapSchema)) {
value = new MapSchema_1.MapSchema(value);
}
value[symbols_1.$childType] = type;
}
else if (typeof (type) !== "string") {
(0, assert_1.assertInstanceType)(value, type, this, fieldCached.substring(1));
}
else {
(0, assert_1.assertType)(value, type, this, fieldCached.substring(1));
}
const changeTree = this[symbols_1.$changes];
//
// Replacing existing "ref", remove it from root.
//
if (previousValue !== undefined && previousValue[symbols_1.$changes]) {
changeTree.root?.remove(previousValue[symbols_1.$changes]);
this.constructor[symbols_1.$track](changeTree, fieldIndex, spec_1.OPERATION.DELETE_AND_ADD);
}
else {
this.constructor[symbols_1.$track](changeTree, fieldIndex, spec_1.OPERATION.ADD);
}
//
// call setParent() recursively for this and its child
// structures.
//
value[symbols_1.$changes]?.setParent(this, changeTree.root, fieldIndex);
}
else if (previousValue !== undefined) {
//
// Setting a field to `null` or `undefined` will delete it.
//
this[symbols_1.$changes].delete(fieldIndex);
}
this[fieldCached] = value;
},
enumerable: true,
configurable: true
};
}
/**
* `@deprecated()` flag a field as deprecated.
* The previous `@type()` annotation should remain along with this one.
*/
function deprecated(throws = true) {
return function (klass, field) {
//
// FIXME: the following block of code is repeated across `@type()`, `@deprecated()` and `@unreliable()` decorators.
//
const constructor = klass.constructor;
const parentClass = Object.getPrototypeOf(constructor);
const parentMetadata = parentClass[Symbol.metadata];
const metadata = (constructor[Symbol.metadata] ??= Object.assign({}, constructor[Symbol.metadata], parentMetadata ?? Object.create(null)));
const fieldIndex = metadata[field];
// if (!metadata[field]) {
// //
// // detect index for this field, considering inheritance
// //
// metadata[field] = {
// type: undefined,
// index: (metadata[$numFields] // current structure already has fields defined
// ?? (parentMetadata && parentMetadata[$numFields]) // parent structure has fields defined
// ?? -1) + 1 // no fields defined
// }
// }
metadata[fieldIndex].deprecated = true;
if (throws) {
metadata[symbols_1.$descriptors] ??= {};
metadata[symbols_1.$descriptors][field] = {
get: function () { throw new Error(`${field} is deprecated.`); },
set: function (value) { },
enumerable: false,
configurable: true
};
}
// flag metadata[field] as non-enumerable
Object.defineProperty(metadata, fieldIndex, {
value: metadata[fieldIndex],
enumerable: false,
configurable: true
});
};
}
function defineTypes(target, fields, options) {
for (let field in fields) {
type(fields[field], options)(target.prototype, field);
}
return target;
}
function schema(fields, name, inherits = Schema_1.Schema) {
const defaultValues = {};
const viewTagFields = {};
for (let fieldName in fields) {
const field = fields[fieldName];
if (typeof (field) === "object") {
if (field['default'] !== undefined) {
defaultValues[fieldName] = field['default'];
}
if (field['view'] !== undefined) {
viewTagFields[fieldName] = (typeof (field['view']) === "boolean")
? exports.DEFAULT_VIEW_TAG
: field['view'];
}
}
}
const klass = Metadata_1.Metadata.setFields(class extends inherits {
constructor(...args) {
args[0] = Object.assign({}, defaultValues, args[0]);
super(...args);
}
}, fields);
for (let fieldName in viewTagFields) {
view(viewTagFields[fieldName])(klass.prototype, fieldName);
}
if (name) {
Object.defineProperty(klass, "name", { value: name });
}
klass.extends = (fields, name) => schema(fields, name, klass);
return klass;
}
//# sourceMappingURL=annotations.js.map