@tamgl/colyseus-schema
Version:
Binary state serializer with delta encoding for games
293 lines (248 loc) • 10.7 kB
text/typescript
import { DefinitionType, getPropertyDescriptor } from "./annotations";
import { Schema } from "./Schema";
import { getType } from "./types/registry";
import { $decoder, $descriptors, $encoder, $fieldIndexesByViewTag, $numFields, $refTypeFieldIndexes, $track, $viewFieldIndexes } from "./types/symbols";
import { TypeContext } from "./types/TypeContext";
export type MetadataField = {
type: DefinitionType,
name: string,
index: number,
tag?: number,
unreliable?: boolean,
deprecated?: boolean,
};
export type Metadata =
{ [$numFields]: number; } & // number of fields
{ [$viewFieldIndexes]: number[]; } & // all field indexes with "view" tag
{ [$fieldIndexesByViewTag]: {[tag: number]: number[]}; } & // field indexes by "view" tag
{ [$refTypeFieldIndexes]: number[]; } & // all field indexes containing Ref types (Schema, ArraySchema, MapSchema, etc)
{ [field: number]: MetadataField; } & // index => field name
{ [field: string]: number; } & // field name => field metadata
{ [$descriptors]: { [field: string]: PropertyDescriptor } } // property descriptors
export function getNormalizedType(type: DefinitionType): DefinitionType {
return (Array.isArray(type))
? { array: type[0] }
: (typeof(type['type']) !== "undefined")
? type['type']
: type;
}
// TODO: see test: "should support TypeScript enums"
function isTSEnum(_enum: any) {
const keys = Object.keys(_enum);
const numericFields = keys.filter(k => /\d+/.test(k));
return (numericFields.length === (keys.length / 2) && _enum[_enum[numericFields[0]]] == numericFields[0]);
}
export const Metadata = {
addField(metadata: any, index: number, name: string, type: DefinitionType, descriptor?: PropertyDescriptor) {
if (index > 64) {
throw new Error(`Can't define field '${name}'.\nSchema instances may only have up to 64 fields.`);
}
metadata[index] = Object.assign(
metadata[index] || {}, // avoid overwriting previous field metadata (@owned / @deprecated)
{
type: getNormalizedType(type),
index,
name,
}
);
// create "descriptors" map
Object.defineProperty(metadata, $descriptors, {
value: metadata[$descriptors] || {},
enumerable: false,
configurable: true,
});
if (descriptor) {
// for encoder
metadata[$descriptors][name] = descriptor;
metadata[$descriptors][`_${name}`] = {
value: undefined,
writable: true,
enumerable: false,
configurable: true,
};
} else {
// for decoder
metadata[$descriptors][name] = {
value: undefined,
writable: true,
enumerable: true,
configurable: true,
};
}
// map -1 as last field index
Object.defineProperty(metadata, $numFields, {
value: index,
enumerable: false,
configurable: true
});
// map field name => index (non enumerable)
Object.defineProperty(metadata, name, {
value: index,
enumerable: false,
configurable: true,
});
// if child Ref/complex type, add to -4
if (typeof (metadata[index].type) !== "string") {
if (metadata[$refTypeFieldIndexes] === undefined) {
Object.defineProperty(metadata, $refTypeFieldIndexes, {
value: [],
enumerable: false,
configurable: true,
});
}
metadata[$refTypeFieldIndexes].push(index);
}
},
setTag(metadata: Metadata, fieldName: string, tag: number) {
const index = metadata[fieldName];
const field = metadata[index];
// add 'tag' to the field
field.tag = tag;
if (!metadata[$viewFieldIndexes]) {
// -2: all field indexes with "view" tag
Object.defineProperty(metadata, $viewFieldIndexes, {
value: [],
enumerable: false,
configurable: true
});
// -3: field indexes by "view" tag
Object.defineProperty(metadata, $fieldIndexesByViewTag, {
value: {},
enumerable: false,
configurable: true
});
}
metadata[$viewFieldIndexes].push(index);
if (!metadata[$fieldIndexesByViewTag][tag]) {
metadata[$fieldIndexesByViewTag][tag] = [];
}
metadata[$fieldIndexesByViewTag][tag].push(index);
},
setFields<T extends { new (...args: any[]): InstanceType<T> } = any>(target: T, fields: { [field in keyof InstanceType<T>]?: DefinitionType }) {
// for inheritance support
const constructor = target.prototype.constructor;
TypeContext.register(constructor);
const parentClass = Object.getPrototypeOf(constructor);
const parentMetadata = parentClass && parentClass[Symbol.metadata];
const metadata = Metadata.initialize(constructor);
// Use Schema's methods if not defined in the class
if (!constructor[$track]) { constructor[$track] = Schema[$track]; }
if (!constructor[$encoder]) { constructor[$encoder] = Schema[$encoder]; }
if (!constructor[$decoder]) { constructor[$decoder] = Schema[$decoder]; }
if (!constructor.prototype.toJSON) { constructor.prototype.toJSON = Schema.prototype.toJSON; }
//
// detect index for this field, considering inheritance
//
let fieldIndex = metadata[$numFields] // current structure already has fields defined
?? (parentMetadata && parentMetadata[$numFields]) // parent structure has fields defined
?? -1; // no fields defined
fieldIndex++;
for (const field in fields) {
const type = fields[field];
// FIXME: this code is duplicated from @type() annotation
const complexTypeKlass = (Array.isArray(type))
? getType("array")
: (typeof(Object.keys(type)[0]) === "string") && getType(Object.keys(type)[0]);
const childType = (complexTypeKlass)
? Object.values(type)[0]
: getNormalizedType(type);
Metadata.addField(
metadata,
fieldIndex,
field,
type,
getPropertyDescriptor(`_${field}`, fieldIndex, childType, complexTypeKlass)
);
fieldIndex++;
}
return target;
},
isDeprecated(metadata: any, field: string) {
return metadata[field].deprecated === true;
},
init(klass: any) {
//
// Used only to initialize an empty Schema (Encoder#constructor)
// TODO: remove/refactor this...
//
const metadata = {};
klass[Symbol.metadata] = metadata;
Object.defineProperty(metadata, $numFields, {
value: 0,
enumerable: false,
configurable: true,
});
},
initialize(constructor: any) {
const parentClass = Object.getPrototypeOf(constructor);
const parentMetadata: Metadata = parentClass[Symbol.metadata];
let metadata: Metadata = constructor[Symbol.metadata] ?? Object.create(null);
// make sure inherited classes have their own metadata object.
if (parentClass !== Schema && metadata === parentMetadata) {
metadata = Object.create(null);
if (parentMetadata) {
//
// assign parent metadata to current
//
Object.setPrototypeOf(metadata, parentMetadata);
// $numFields
Object.defineProperty(metadata, $numFields, {
value: parentMetadata[$numFields],
enumerable: false,
configurable: true,
writable: true,
});
// $viewFieldIndexes / $fieldIndexesByViewTag
if (parentMetadata[$viewFieldIndexes] !== undefined) {
Object.defineProperty(metadata, $viewFieldIndexes, {
value: [...parentMetadata[$viewFieldIndexes]],
enumerable: false,
configurable: true,
writable: true,
});
Object.defineProperty(metadata, $fieldIndexesByViewTag, {
value: { ...parentMetadata[$fieldIndexesByViewTag] },
enumerable: false,
configurable: true,
writable: true,
});
}
// $refTypeFieldIndexes
if (parentMetadata[$refTypeFieldIndexes] !== undefined) {
Object.defineProperty(metadata, $refTypeFieldIndexes, {
value: [...parentMetadata[$refTypeFieldIndexes]],
enumerable: false,
configurable: true,
writable: true,
});
}
// $descriptors
Object.defineProperty(metadata, $descriptors, {
value: { ...parentMetadata[$descriptors] },
enumerable: false,
configurable: true,
writable: true,
});
}
}
constructor[Symbol.metadata] = metadata;
return metadata;
},
isValidInstance(klass: any) {
return (
klass.constructor[Symbol.metadata] &&
Object.prototype.hasOwnProperty.call(klass.constructor[Symbol.metadata], $numFields) as boolean
);
},
getFields(klass: any) {
const metadata: Metadata = klass[Symbol.metadata];
const fields = {};
for (let i = 0; i <= metadata[$numFields]; i++) {
fields[metadata[i].name] = metadata[i].type;
}
return fields;
},
hasViewTagAtIndex(metadata: Metadata, index: number) {
return metadata?.[$viewFieldIndexes]?.includes(index);
}
}