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