UNPKG

@tamgl/colyseus-schema

Version:

Binary state serializer with delta encoding for games

271 lines 11.4 kB
"use strict"; var _a, _b; Object.defineProperty(exports, "__esModule", { value: true }); exports.Schema = void 0; const spec_1 = require("./encoding/spec"); const annotations_1 = require("./annotations"); const ChangeTree_1 = require("./encoder/ChangeTree"); const symbols_1 = require("./types/symbols"); const EncodeOperation_1 = require("./encoder/EncodeOperation"); const DecodeOperation_1 = require("./decoder/DecodeOperation"); const utils_1 = require("./utils"); /** * Schema encoder / decoder */ class Schema { static { this[_a] = EncodeOperation_1.encodeSchemaOperation; } static { this[_b] = DecodeOperation_1.decodeSchemaOperation; } /** * Assign the property descriptors required to track changes on this instance. * @param instance */ static initialize(instance) { Object.defineProperty(instance, symbols_1.$changes, { value: new ChangeTree_1.ChangeTree(instance), enumerable: false, writable: true }); Object.defineProperties(instance, instance.constructor[Symbol.metadata]?.[symbols_1.$descriptors] || {}); } static is(type) { return typeof (type[Symbol.metadata]) === "object"; // const metadata = type[Symbol.metadata]; // return metadata && Object.prototype.hasOwnProperty.call(metadata, -1); } /** * Track property changes */ static [(_a = symbols_1.$encoder, _b = symbols_1.$decoder, symbols_1.$track)](changeTree, index, operation = spec_1.OPERATION.ADD) { changeTree.change(index, operation); } /** * Determine if a property must be filtered. * - If returns false, the property is NOT going to be encoded. * - If returns true, the property is going to be encoded. * * Encoding with "filters" happens in two steps: * - First, the encoder iterates over all "not owned" properties and encodes them. * - Then, the encoder iterates over all "owned" properties per instance and encodes them. */ static [symbols_1.$filter](ref, index, view) { const metadata = ref.constructor[Symbol.metadata]; const tag = metadata[index]?.tag; if (view === undefined) { // shared pass/encode: encode if doesn't have a tag return tag === undefined; } else if (tag === undefined) { // view pass: no tag return true; } else if (tag === annotations_1.DEFAULT_VIEW_TAG) { // view pass: default tag return view.isChangeTreeVisible(ref[symbols_1.$changes]); } else { // view pass: custom tag const tags = view.tags?.get(ref[symbols_1.$changes]); return tags && tags.has(tag); } } // allow inherited classes to have a constructor constructor(...args) { // // inline // Schema.initialize(this); // Schema.initialize(this); // // Assign initial values // if (args[0]) { Object.assign(this, args[0]); } } assign(props) { Object.assign(this, props); return this; } /** * (Server-side): Flag a property to be encoded for the next patch. * @param instance Schema instance * @param property string representing the property name, or number representing the index of the property. * @param operation OPERATION to perform (detected automatically) */ setDirty(property, operation) { const metadata = this.constructor[Symbol.metadata]; this[symbols_1.$changes].change(metadata[metadata[property]].index, operation); } clone() { const cloned = new (this.constructor); const metadata = this.constructor[Symbol.metadata]; // // TODO: clone all properties, not only annotated ones // // for (const field in this) { for (const fieldIndex in metadata) { // const field = metadata[metadata[fieldIndex]].name; const field = metadata[fieldIndex].name; if (typeof (this[field]) === "object" && typeof (this[field]?.clone) === "function") { // deep clone cloned[field] = this[field].clone(); } else { // primitive values cloned[field] = this[field]; } } return cloned; } toJSON() { const obj = {}; const metadata = this.constructor[Symbol.metadata]; for (const index in metadata) { const field = metadata[index]; const fieldName = field.name; if (!field.deprecated && this[fieldName] !== null && typeof (this[fieldName]) !== "undefined") { obj[fieldName] = (typeof (this[fieldName]['toJSON']) === "function") ? this[fieldName]['toJSON']() : this[fieldName]; } } return obj; } /** * Used in tests only * @internal */ discardAllChanges() { this[symbols_1.$changes].discardAll(); } [symbols_1.$getByIndex](index) { const metadata = this.constructor[Symbol.metadata]; return this[metadata[index].name]; } [symbols_1.$deleteByIndex](index) { const metadata = this.constructor[Symbol.metadata]; this[metadata[index].name] = undefined; } /** * Inspect the `refId` of all Schema instances in the tree. Optionally display the contents of the instance. * * @param ref Schema instance * @param showContents display JSON contents of the instance * @returns */ static debugRefIds(ref, showContents = false, level = 0, decoder) { const contents = (showContents) ? ` - ${JSON.stringify(ref.toJSON())}` : ""; const changeTree = ref[symbols_1.$changes]; const refId = (decoder) ? decoder.root.refIds.get(ref) : changeTree.refId; const root = (decoder) ? decoder.root : changeTree.root; // log reference count if > 1 const refCount = (root?.refCount?.[refId] > 1) ? ` [×${root.refCount[refId]}]` : ''; let output = `${(0, utils_1.getIndent)(level)}${ref.constructor.name} (refId: ${refId})${refCount}${contents}\n`; changeTree.forEachChild((childChangeTree) => output += this.debugRefIds(childChangeTree.ref, showContents, level + 1, decoder)); return output; } static debugRefIdsDecoder(decoder) { return this.debugRefIds(decoder.state, false, 0, decoder); } /** * Return a string representation of the changes on a Schema instance. * The list of changes is cleared after each encode. * * @param instance Schema instance * @param isEncodeAll Return "full encode" instead of current change set. * @returns */ static debugChanges(instance, isEncodeAll = false) { const changeTree = instance[symbols_1.$changes]; const changeSet = (isEncodeAll) ? changeTree.allChanges : changeTree.changes; const changeSetName = (isEncodeAll) ? "allChanges" : "changes"; let output = `${instance.constructor.name} (${changeTree.refId}) -> .${changeSetName}:\n`; function dumpChangeSet(changeSet) { changeSet.operations .filter(op => op) .forEach((index) => { const operation = changeTree.indexedOperations[index]; console.log({ index, operation }); output += `- [${index}]: ${spec_1.OPERATION[operation]} (${JSON.stringify(changeTree.getValue(Number(index), isEncodeAll))})\n`; }); } dumpChangeSet(changeSet); // display filtered changes if (!isEncodeAll && changeTree.filteredChanges && (changeTree.filteredChanges.operations).filter(op => op).length > 0) { output += `${instance.constructor.name} (${changeTree.refId}) -> .filteredChanges:\n`; dumpChangeSet(changeTree.filteredChanges); } // display filtered changes if (isEncodeAll && changeTree.allFilteredChanges && (changeTree.allFilteredChanges.operations).filter(op => op).length > 0) { output += `${instance.constructor.name} (${changeTree.refId}) -> .allFilteredChanges:\n`; dumpChangeSet(changeTree.allFilteredChanges); } return output; } static debugChangesDeep(ref, changeSetName = "changes") { let output = ""; const rootChangeTree = ref[symbols_1.$changes]; const root = rootChangeTree.root; const changeTrees = new Map(); const instanceRefIds = []; let totalOperations = 0; for (const [refId, changes] of Object.entries(root[changeSetName])) { const changeTree = root.changeTrees[refId]; let includeChangeTree = false; let parentChangeTrees = []; let parentChangeTree = changeTree.parent?.[symbols_1.$changes]; if (changeTree === rootChangeTree) { includeChangeTree = true; } else { while (parentChangeTree !== undefined) { parentChangeTrees.push(parentChangeTree); if (parentChangeTree.ref === ref) { includeChangeTree = true; break; } parentChangeTree = parentChangeTree.parent?.[symbols_1.$changes]; } } if (includeChangeTree) { instanceRefIds.push(changeTree.refId); totalOperations += Object.keys(changes).length; changeTrees.set(changeTree, parentChangeTrees.reverse()); } } output += "---\n"; output += `root refId: ${rootChangeTree.refId}\n`; output += `Total instances: ${instanceRefIds.length} (refIds: ${instanceRefIds.join(", ")})\n`; output += `Total changes: ${totalOperations}\n`; output += "---\n"; // based on root.changes, display a tree of changes that has the "ref" instance as parent const visitedParents = new WeakSet(); for (const [changeTree, parentChangeTrees] of changeTrees.entries()) { parentChangeTrees.forEach((parentChangeTree, level) => { if (!visitedParents.has(parentChangeTree)) { output += `${(0, utils_1.getIndent)(level)}${parentChangeTree.ref.constructor.name} (refId: ${parentChangeTree.refId})\n`; visitedParents.add(parentChangeTree); } }); const changes = changeTree.indexedOperations; const level = parentChangeTrees.length; const indent = (0, utils_1.getIndent)(level); const parentIndex = (level > 0) ? `(${changeTree.parentIndex}) ` : ""; output += `${indent}${parentIndex}${changeTree.ref.constructor.name} (refId: ${changeTree.refId}) - changes: ${Object.keys(changes).length}\n`; for (const index in changes) { const operation = changes[index]; output += `${(0, utils_1.getIndent)(level + 1)}${spec_1.OPERATION[operation]}: ${index}\n`; } } return `${output}`; } } exports.Schema = Schema; //# sourceMappingURL=Schema.js.map