@colyseus/schema
Version:
Binary state serializer with delta encoding for games
225 lines (174 loc) • 6.19 kB
text/typescript
import { $changes, $childType, $decoder, $deleteByIndex, $encoder, $filter, $getByIndex, $onEncodeEnd } from "../symbols";
import { ChangeTree, type IRef } from "../../encoder/ChangeTree";
import { OPERATION } from "../../encoding/spec";
import { registerType } from "../registry";
import { Collection } from "../HelperTypes";
import { decodeKeyValueOperation } from "../../decoder/DecodeOperation";
import { encodeKeyValueOperation } from "../../encoder/EncodeOperation";
import type { StateView } from "../../encoder/StateView";
import type { Schema } from "../../Schema";
type K = number; // TODO: allow to specify K generic on MapSchema.
export class CollectionSchema<V=any> implements Collection<K, V>, IRef {
public [$changes]: ChangeTree;
protected [$childType]: string | typeof Schema;
protected $items: Map<number, V> = new Map<number, V>();
protected $indexes: Map<number, number> = new Map<number, number>();
protected deletedItems: { [field: string]: V } = {};
protected $refId: number = 0;
static [$encoder] = encodeKeyValueOperation;
static [$decoder] = decodeKeyValueOperation;
/**
* 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 [$filter] (ref: CollectionSchema, index: number, view: StateView) {
return (
!view ||
typeof (ref[$childType]) === "string" ||
view.isChangeTreeVisible((ref[$getByIndex](index) ?? ref.deletedItems[index])[$changes])
);
}
static is(type: any) {
return type['collection'] !== undefined;
}
constructor (initialValues?: Array<V>) {
this[$changes] = new ChangeTree(this);
this[$changes].indexes = {};
if (initialValues) {
initialValues.forEach((v) => this.add(v));
}
Object.defineProperty(this, $childType, {
value: undefined,
enumerable: false,
writable: true,
configurable: true,
});
}
add(value: V) {
// set "index" for reference.
const index = this.$refId++;
const isRef = (value[$changes]) !== undefined;
if (isRef) {
value[$changes].setParent(this, this[$changes].root, index);
}
this[$changes].indexes[index] = index;
this.$indexes.set(index, index);
this.$items.set(index, value);
this[$changes].change(index);
return index;
}
at(index: number): V | undefined {
const key = Array.from(this.$items.keys())[index];
return this.$items.get(key);
}
entries() {
return this.$items.entries();
}
delete(item: V) {
const entries = this.$items.entries();
let index: K;
let entry: IteratorResult<[number, V]>;
while (entry = entries.next()) {
if (entry.done) { break; }
if (item === entry.value[1]) {
index = entry.value[0];
break;
}
}
if (index === undefined) {
return false;
}
this.deletedItems[index] = this[$changes].delete(index);
this.$indexes.delete(index);
return this.$items.delete(index);
}
clear() {
const changeTree = this[$changes];
// discard previous operations.
changeTree.discard(true);
changeTree.indexes = {};
// remove children references
changeTree.forEachChild((childChangeTree, _) => {
changeTree.root?.remove(childChangeTree);
});
// clear previous indexes
this.$indexes.clear();
// clear items
this.$items.clear();
changeTree.operation(OPERATION.CLEAR);
}
has (value: V): boolean {
return Array.from(this.$items.values()).some((v) => v === value);
}
forEach(callbackfn: (value: V, key: K, collection: CollectionSchema<V>) => void) {
this.$items.forEach((value, key, _) => callbackfn(value, key, this));
}
values() {
return this.$items.values();
}
get size () {
return this.$items.size;
}
/** Iterator */
[Symbol.iterator](): IterableIterator<V> {
return this.$items.values();
}
protected setIndex(index: number, key: number) {
this.$indexes.set(index, key);
}
protected getIndex(index: number) {
return this.$indexes.get(index);
}
[$getByIndex](index: number): any {
return this.$items.get(this.$indexes.get(index));
}
[$deleteByIndex](index: number): void {
const key = this.$indexes.get(index);
this.$items.delete(key);
this.$indexes.delete(index);
}
protected [$onEncodeEnd]() {
this.deletedItems = {};
}
toArray() {
return Array.from(this.$items.values());
}
toJSON() {
const values: V[] = [];
this.forEach((value: any, key: K) => {
values.push(
(typeof (value['toJSON']) === "function")
? value['toJSON']()
: value
);
});
return values;
}
//
// Decoding utilities
//
clone(isDecoding?: boolean): CollectionSchema<V> {
let cloned: CollectionSchema;
if (isDecoding) {
// client-side
cloned = Object.assign(new CollectionSchema(), this);
} else {
// server-side
cloned = new CollectionSchema();
this.forEach((value: any) => {
if (value[$changes]) {
cloned.add(value['clone']());
} else {
cloned.add(value);
}
})
}
return cloned;
}
}
registerType("collection", { constructor: CollectionSchema, });