@tamgl/colyseus-schema
Version:
Binary state serializer with delta encoding for games
271 lines • 11.4 kB
JavaScript
;
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