UNPKG

@tamgl/colyseus-schema

Version:

Binary state serializer with delta encoding for games

278 lines 11.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.StateView = void 0; exports.createView = createView; const symbols_1 = require("../types/symbols"); const annotations_1 = require("../annotations"); const spec_1 = require("../encoding/spec"); const Metadata_1 = require("../Metadata"); const utils_1 = require("../types/utils"); function createView(iterable = false) { return new StateView(iterable); } class StateView { constructor(iterable = false) { this.iterable = iterable; /** * List of ChangeTree's that are visible to this view */ this.visible = new WeakSet(); /** * List of ChangeTree's that are invisible to this view */ this.invisible = new WeakSet(); /** * Manual "ADD" operations for changes per ChangeTree, specific to this view. * (This is used to force encoding a property, even if it was not changed) */ this.changes = new Map(); if (iterable) { this.items = []; } } // TODO: allow to set multiple tags at once add(obj, tag = annotations_1.DEFAULT_VIEW_TAG, checkIncludeParent = true) { const changeTree = obj?.[symbols_1.$changes]; if (!changeTree) { console.warn("StateView#add(), invalid object:", obj); return this; } else if (!changeTree.parent && changeTree.refId !== 0 // allow root object ) { /** * TODO: can we avoid this? * * When the "parent" structure has the @view() tag, it is currently * not possible to identify it has to be added to the view as well * (this.addParentOf() is not called). */ throw new Error(`Cannot add a detached instance to the StateView. Make sure to assign the "${changeTree.ref.constructor.name}" instance to the state before calling view.add()`); } // FIXME: ArraySchema/MapSchema do not have metadata const metadata = obj.constructor[Symbol.metadata]; this.visible.add(changeTree); // add to iterable list (only the explicitly added items) if (this.iterable && checkIncludeParent) { this.items.push(obj); } // add parent ChangeTree's // - if it was invisible to this view // - if it were previously filtered out if (checkIncludeParent && changeTree.parent) { this.addParentOf(changeTree, tag); } // // TODO: when adding an item of a MapSchema, the changes may not // be set (only the parent's changes are set) // let changes = this.changes.get(changeTree.refId); if (changes === undefined) { changes = {}; this.changes.set(changeTree.refId, changes); } // set tag if (tag !== annotations_1.DEFAULT_VIEW_TAG) { if (!this.tags) { this.tags = new WeakMap(); } let tags; if (!this.tags.has(changeTree)) { tags = new Set(); this.tags.set(changeTree, tags); } else { tags = this.tags.get(changeTree); } tags.add(tag); // Ref: add tagged properties metadata?.[symbols_1.$fieldIndexesByViewTag]?.[tag]?.forEach((index) => { if (changeTree.getChange(index) !== spec_1.OPERATION.DELETE) { changes[index] = spec_1.OPERATION.ADD; } }); } else { const isInvisible = this.invisible.has(changeTree); const changeSet = (changeTree.filteredChanges !== undefined) ? changeTree.allFilteredChanges : changeTree.allChanges; for (let i = 0, len = changeSet.operations.length; i < len; i++) { const index = changeSet.operations[i]; if (index === undefined) { continue; } // skip "undefined" indexes const op = changeTree.indexedOperations[index] ?? spec_1.OPERATION.ADD; const tagAtIndex = metadata?.[index].tag; if (!changeTree.isNew && // new structures will be added as part of .encode() call, no need to force it to .encodeView() (isInvisible || // if "invisible", include all tagAtIndex === undefined || // "all change" with no tag tagAtIndex === tag // tagged property ) && op !== spec_1.OPERATION.DELETE) { changes[index] = op; } } } // Add children of this ChangeTree to this view changeTree.forEachChild((change, index) => { // Do not ADD children that don't have the same tag if (metadata && metadata[index].tag !== undefined && metadata[index].tag !== tag) { return; } this.add(change.ref, tag, false); }); return this; } addParentOf(childChangeTree, tag) { const changeTree = childChangeTree.parent[symbols_1.$changes]; const parentIndex = childChangeTree.parentIndex; if (!this.visible.has(changeTree)) { // view must have all "changeTree" parent tree this.visible.add(changeTree); // add parent's parent const parentChangeTree = changeTree.parent?.[symbols_1.$changes]; if (parentChangeTree && (parentChangeTree.filteredChanges !== undefined)) { this.addParentOf(changeTree, tag); } // // parent is already available, no need to add it! // if (!this.invisible.has(changeTree)) { return; } } // add parent's tag properties if (changeTree.getChange(parentIndex) !== spec_1.OPERATION.DELETE) { let changes = this.changes.get(changeTree.refId); if (changes === undefined) { changes = {}; this.changes.set(changeTree.refId, changes); } if (!this.tags) { this.tags = new WeakMap(); } let tags; if (!this.tags.has(changeTree)) { tags = new Set(); this.tags.set(changeTree, tags); } else { tags = this.tags.get(changeTree); } tags.add(tag); changes[parentIndex] = spec_1.OPERATION.ADD; } } remove(obj, tag = annotations_1.DEFAULT_VIEW_TAG, _isClear = false) { const changeTree = obj[symbols_1.$changes]; if (!changeTree) { console.warn("StateView#remove(), invalid object:", obj); return this; } this.visible.delete(changeTree); // remove from iterable list if (this.iterable && !_isClear // no need to remove during clear(), as it will be cleared entirely ) { (0, utils_1.spliceOne)(this.items, this.items.indexOf(obj)); } const ref = changeTree.ref; const metadata = ref.constructor[Symbol.metadata]; // ArraySchema/MapSchema do not have metadata let changes = this.changes.get(changeTree.refId); if (changes === undefined) { changes = {}; this.changes.set(changeTree.refId, changes); } if (tag === annotations_1.DEFAULT_VIEW_TAG) { // parent is collection (Map/Array) const parent = changeTree.parent; if (!Metadata_1.Metadata.isValidInstance(parent) && changeTree.isFiltered) { const parentChangeTree = parent[symbols_1.$changes]; let changes = this.changes.get(parentChangeTree.refId); if (changes === undefined) { changes = {}; this.changes.set(parentChangeTree.refId, changes); } else if (changes[changeTree.parentIndex] === spec_1.OPERATION.ADD) { // // SAME PATCH ADD + REMOVE: // The 'changes' of deleted structure should be ignored. // this.changes.delete(changeTree.refId); } // DELETE / DELETE BY REF ID changes[changeTree.parentIndex] = spec_1.OPERATION.DELETE; // Remove child schema from visible set this._recursiveDeleteVisibleChangeTree(changeTree); } else { // delete all "tagged" properties. metadata?.[symbols_1.$viewFieldIndexes]?.forEach((index) => changes[index] = spec_1.OPERATION.DELETE); } } else { // delete only tagged properties metadata?.[symbols_1.$fieldIndexesByViewTag][tag].forEach((index) => changes[index] = spec_1.OPERATION.DELETE); } // remove tag if (this.tags && this.tags.has(changeTree)) { const tags = this.tags.get(changeTree); if (tag === undefined) { // delete all tags this.tags.delete(changeTree); } else { // delete specific tag tags.delete(tag); // if tag set is empty, delete it entirely if (tags.size === 0) { this.tags.delete(changeTree); } } } return this; } has(obj) { return this.visible.has(obj[symbols_1.$changes]); } hasTag(ob, tag = annotations_1.DEFAULT_VIEW_TAG) { const tags = this.tags?.get(ob[symbols_1.$changes]); return tags?.has(tag) ?? false; } clear() { if (!this.iterable) { throw new Error("StateView#clear() is only available for iterable StateView's. Use StateView(iterable: true) constructor."); } for (let i = 0, l = this.items.length; i < l; i++) { this.remove(this.items[i], annotations_1.DEFAULT_VIEW_TAG, true); } // clear items array this.items.length = 0; } isChangeTreeVisible(changeTree) { let isVisible = this.visible.has(changeTree); // // TODO: avoid checking for parent visibility, most of the time it's not needed // See test case: 'should not be required to manually call view.add() items to child arrays without @view() tag' // if (!isVisible && changeTree.isVisibilitySharedWithParent) { // console.log("CHECK AGAINST PARENT...", { // ref: changeTree.ref.constructor.name, // refId: changeTree.refId, // parent: changeTree.parent.constructor.name, // }); if (this.visible.has(changeTree.parent[symbols_1.$changes])) { this.visible.add(changeTree); isVisible = true; } } return isVisible; } _recursiveDeleteVisibleChangeTree(changeTree) { changeTree.forEachChild((childChangeTree) => { this.visible.delete(childChangeTree); this._recursiveDeleteVisibleChangeTree(childChangeTree); }); } } exports.StateView = StateView; //# sourceMappingURL=StateView.js.map