UNPKG

@tamgl/colyseus-schema

Version:

Binary state serializer with delta encoding for games

411 lines 17.9 kB
"use strict"; 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