@tamgl/colyseus-schema
Version:
Binary state serializer with delta encoding for games
239 lines (188 loc) • 9.11 kB
text/typescript
import { type, PrimitiveType } from "./annotations";
import { TypeContext } from "./types/TypeContext";
import { Metadata } from "./Metadata";
import { ArraySchema } from "./types/custom/ArraySchema";
import { Iterator } from "./encoding/decode";
import { Encoder } from "./encoder/Encoder";
import { Decoder } from "./decoder/Decoder";
import { Schema } from "./Schema";
/**
* Reflection
*/
export class ReflectionField extends Schema {
("string") name: string;
("string") type: string;
("number") referencedType: number;
}
export class ReflectionType extends Schema {
("number") id: number;
("number") extendsId: number;
([ ReflectionField ]) fields = new ArraySchema<ReflectionField>();
}
export class Reflection extends Schema {
([ReflectionType]) types: ArraySchema<ReflectionType> = new ArraySchema<ReflectionType>();
("number") rootType: number;
/**
* Encodes the TypeContext of an Encoder into a buffer.
*
* @param encoder Encoder instance
* @param it
* @returns
*/
static encode(encoder: Encoder, it: Iterator = { offset: 0 }) {
const context = encoder.context;
const reflection = new Reflection();
const reflectionEncoder = new Encoder(reflection);
// rootType is usually the first schema passed to the Encoder
// (unless it inherits from another schema)
const rootType = context.schemas.get(encoder.state.constructor);
if (rootType > 0) { reflection.rootType = rootType; }
const includedTypeIds = new Set<number>();
const pendingReflectionTypes: { [typeid: number]: ReflectionType[] } = {};
// add type to reflection in a way that respects inheritance
// (parent types should be added before their children)
const addType = (type: ReflectionType) => {
if (type.extendsId === undefined || includedTypeIds.has(type.extendsId)) {
includedTypeIds.add(type.id);
reflection.types.push(type);
const deps = pendingReflectionTypes[type.id];
if (deps !== undefined) {
delete pendingReflectionTypes[type.id];
deps.forEach((childType) => addType(childType));
}
} else {
if (pendingReflectionTypes[type.extendsId] === undefined) {
pendingReflectionTypes[type.extendsId] = [];
}
pendingReflectionTypes[type.extendsId].push(type);
}
};
context.schemas.forEach((typeid, klass) => {
const type = new ReflectionType();
type.id = Number(typeid);
// support inheritance
const inheritFrom = Object.getPrototypeOf(klass);
if (inheritFrom !== Schema) {
type.extendsId = context.schemas.get(inheritFrom);
}
const metadata = klass[Symbol.metadata];
//
// FIXME: this is a workaround for inherited types without additional fields
// if metadata is the same reference as the parent class - it means the class has no own metadata
//
if (metadata !== inheritFrom[Symbol.metadata]) {
for (const fieldIndex in metadata) {
const index = Number(fieldIndex);
const fieldName = metadata[index].name;
// skip fields from parent classes
if (!Object.prototype.hasOwnProperty.call(metadata, fieldName)) {
continue;
}
const reflectionField = new ReflectionField();
reflectionField.name = fieldName;
let fieldType: string;
const field = metadata[index];
if (typeof (field.type) === "string") {
fieldType = field.type;
} else {
let childTypeSchema: typeof Schema;
//
// TODO: refactor below.
//
if (Schema.is(field.type)) {
fieldType = "ref";
childTypeSchema = field.type as typeof Schema;
} else {
fieldType = Object.keys(field.type)[0];
if (typeof (field.type[fieldType]) === "string") {
fieldType += ":" + field.type[fieldType]; // array:string
} else {
childTypeSchema = field.type[fieldType];
}
}
reflectionField.referencedType = (childTypeSchema)
? context.getTypeId(childTypeSchema)
: -1;
}
reflectionField.type = fieldType;
type.fields.push(reflectionField);
}
}
addType(type);
});
// in case there are types that were not added due to inheritance
for (const typeid in pendingReflectionTypes) {
pendingReflectionTypes[typeid].forEach((type) =>
reflection.types.push(type))
}
const buf = reflectionEncoder.encodeAll(it);
return Buffer.from(buf, 0, it.offset);
}
/**
* Decodes the TypeContext from a buffer into a Decoder instance.
*
* @param bytes Reflection.encode() output
* @param it
* @returns Decoder instance
*/
static decode<T extends Schema = Schema>(bytes: Buffer, it?: Iterator): Decoder<T> {
const reflection = new Reflection();
const reflectionDecoder = new Decoder(reflection);
reflectionDecoder.decode(bytes, it);
const typeContext = new TypeContext();
// 1st pass, initialize metadata + inheritance
reflection.types.forEach((reflectionType) => {
const parentClass: typeof Schema = typeContext.get(reflectionType.extendsId) ?? Schema;
const schema: typeof Schema = class _ extends parentClass {};
// register for inheritance support
TypeContext.register(schema);
// // for inheritance support
// Metadata.initialize(schema);
typeContext.add(schema, reflectionType.id);
}, {});
// define fields
const addFields = (metadata: Metadata, reflectionType: ReflectionType, parentFieldIndex: number) => {
reflectionType.fields.forEach((field, i) => {
const fieldIndex = parentFieldIndex + i;
if (field.referencedType !== undefined) {
let fieldType = field.type;
let refType: PrimitiveType = typeContext.get(field.referencedType);
// map or array of primitive type (-1)
if (!refType) {
const typeInfo = field.type.split(":");
fieldType = typeInfo[0];
refType = typeInfo[1] as PrimitiveType; // string
}
if (fieldType === "ref") {
Metadata.addField(metadata, fieldIndex, field.name, refType);
} else {
Metadata.addField(metadata, fieldIndex, field.name, { [fieldType]: refType });
}
} else {
Metadata.addField(metadata, fieldIndex, field.name, field.type as PrimitiveType);
}
});
};
// 2nd pass, set fields
reflection.types.forEach((reflectionType) => {
const schema = typeContext.get(reflectionType.id);
// for inheritance support
const metadata = Metadata.initialize(schema);
const inheritedTypes: ReflectionType[] = [];
let parentType: ReflectionType = reflectionType;
do {
inheritedTypes.push(parentType);
parentType = reflection.types.find((t) => t.id === parentType.extendsId);
} while (parentType);
let parentFieldIndex = 0;
inheritedTypes.reverse().forEach((reflectionType) => {
// add fields from all inherited classes
// TODO: refactor this to avoid adding fields from parent classes
addFields(metadata, reflectionType, parentFieldIndex);
parentFieldIndex += reflectionType.fields.length;
});
});
const state: T = new (typeContext.get(reflection.rootType || 0) as unknown as any)();
return new Decoder<T>(state, typeContext);
}
}