import { BackingField, defineSimpleAccessors, setBackingField, WithBackingField } from './backing-field'; import { Array, FieldDescriptorBase, FieldDescriptorBaseOptions, FieldType, FieldTypeDefinition, FixedLengthArray, getFieldTypeDefinition, Number, VariableLengthArray } from './field'; import { StructDefaultOptions, StructDeserializationContext, StructOptions, StructSerializationContext } from './types'; import { Evaluate, Identity, OmitNever, Overwrite } from './utils'; export type StructValueType = T extends { deserialize(context: StructDeserializationContext): Promise; } ? R : never; export type StructInitType> = T extends { create(value: infer R, ...args: any): any; } ? Evaluate : never; interface AddArrayFieldDescriptor< TResult extends object, TInit extends object, TExtra extends object, TAfterParsed > { < TName extends string, TType extends Array.SubType, TTypeScriptType = Array.TypeScriptType >( name: TName, type: TType, options: FixedLengthArray.Options, typescriptType?: () => TTypeScriptType, ): MergeStruct< TResult, TInit, TExtra, TAfterParsed, FixedLengthArray< TName, TType, TTypeScriptType > >; < TName extends string, TType extends Array.SubType, TLengthField extends VariableLengthArray.KeyOfType, TEmptyBehavior extends VariableLengthArray.EmptyBehavior, TTypeScriptType = VariableLengthArray.TypeScriptType >( name: TName, type: TType, options: VariableLengthArray.Options, typescriptType?: () => TTypeScriptType, ): MergeStruct< TResult, TInit, TExtra, TAfterParsed, VariableLengthArray< TName, TType, TInit, TLengthField, TEmptyBehavior, TTypeScriptType > >; } interface AddArraySubTypeFieldDescriptor< TResult extends object, TInit extends object, TExtra extends object, TAfterParsed, TType extends Array.SubType > { < TName extends string, TTypeScriptType = Array.TypeScriptType >( name: TName, options: FixedLengthArray.Options, typescriptType?: () => TTypeScriptType, ): MergeStruct< TResult, TInit, TExtra, TAfterParsed, FixedLengthArray< TName, TType, TTypeScriptType > >; < TName extends string, TLengthField extends VariableLengthArray.KeyOfType, TEmptyBehavior extends VariableLengthArray.EmptyBehavior, TTypeScriptType = VariableLengthArray.TypeScriptType >( name: TName, options: VariableLengthArray.Options, _typescriptType?: TTypeScriptType, ): MergeStruct< TResult, TInit, TExtra, TAfterParsed, VariableLengthArray< TName, TType, TInit, TLengthField, TEmptyBehavior, TTypeScriptType > >; } type MergeStruct< TResult extends object, TInit extends object, TExtra extends object, TAfterParsed, TDescriptor extends FieldDescriptorBase > = Identity>, OmitNever>, TExtra, TAfterParsed >>; export type StructAfterParsed = (this: WithBackingField, object: WithBackingField) => TAfterParsed; export default class Struct< TResult extends object = {}, TInit extends object = {}, TExtra extends object = {}, TAfterParsed = undefined, > { public readonly options: Readonly; private _size = 0; public get size() { return this._size; } private fields: FieldDescriptorBase[] = []; private _extra: PropertyDescriptorMap = {}; private _afterParsed?: StructAfterParsed; public constructor(options: Partial = StructDefaultOptions) { this.options = { ...StructDefaultOptions, ...options }; } private clone(): Struct { const result = new Struct(this.options); result.fields = this.fields.slice(); result._size = this._size; result._extra = this._extra; result._afterParsed = this._afterParsed; return result; } public field( field: TDescriptor, ): MergeStruct { const result = this.clone(); result.fields.push(field); const definition = getFieldTypeDefinition(field.type); const size = definition.getSize({ field, options: this.options }); result._size += size; return result; } private number< TName extends string, TSubType extends Number.SubType = Number.SubType, TTypeScriptType = Number.TypeScriptType >( name: TName, type: TSubType, options: FieldDescriptorBaseOptions = {}, _typescriptType?: TTypeScriptType, ) { return this.field>({ type: FieldType.Number, name, subType: type, options, }); } public uint16< TName extends string, TTypeScriptType = Number.TypeScriptType >( name: TName, options: FieldDescriptorBaseOptions = {}, _typescriptType?: TTypeScriptType, ) { return this.number( name, Number.SubType.Uint16, options, _typescriptType ); } public int32< TName extends string, TTypeScriptType = Number.TypeScriptType >( name: TName, options: FieldDescriptorBaseOptions = {}, _typescriptType?: TTypeScriptType, ) { return this.number( name, Number.SubType.Int32, options, _typescriptType ); } public uint32< TName extends string, TTypeScriptType = Number.TypeScriptType >( name: TName, options: FieldDescriptorBaseOptions = {}, _typescriptType?: TTypeScriptType, ) { return this.number( name, Number.SubType.Uint32, options, _typescriptType ); } public uint64< TName extends string, TTypeScriptType = Number.TypeScriptType >( name: TName, options: FieldDescriptorBaseOptions = {}, _typescriptType?: TTypeScriptType, ) { return this.number( name, Number.SubType.Uint64, options, _typescriptType ); } public int64< TName extends string, TTypeScriptType = Number.TypeScriptType >( name: TName, options: FieldDescriptorBaseOptions = {}, _typescriptType?: TTypeScriptType, ) { return this.number( name, Number.SubType.Int64, options, _typescriptType ); } private array: AddArrayFieldDescriptor = ( name: string, type: Array.SubType, options: FixedLengthArray.Options | VariableLengthArray.Options ): Struct => { if ('length' in options) { return this.field({ type: FieldType.FixedLengthArray, name, subType: type, options: options, }); } else { return this.field({ type: FieldType.VariableLengthArray, name, subType: type, options: options, }); } }; public arrayBuffer: AddArraySubTypeFieldDescriptor< TResult, TInit, TExtra, TAfterParsed, Array.SubType.ArrayBuffer > = ( name: TName, options: any ) => { return this.array(name, Array.SubType.ArrayBuffer, options); }; public string: AddArraySubTypeFieldDescriptor< TResult, TInit, TExtra, TAfterParsed, Array.SubType.String > = ( name: TName, options: any ) => { return this.array(name, Array.SubType.String, options); }; public extra>, never >>( value: TValue & ThisType, TResult>>> ): Struct< TResult, TInit, Overwrite, TAfterParsed > { const result = this.clone(); result._extra = { ...result._extra, ...Object.getOwnPropertyDescriptors(value) }; return result; } public afterParsed( callback: StructAfterParsed ): Struct; public afterParsed( callback?: StructAfterParsed ): Struct; public afterParsed( callback?: StructAfterParsed ): Struct; public afterParsed( callback?: StructAfterParsed ) { const result = this.clone(); result._afterParsed = callback; return result; } private initializeField( context: StructSerializationContext, field: FieldDescriptorBase, fieldTypeDefinition: FieldTypeDefinition, object: any, value: any, extra?: any ) { if (fieldTypeDefinition.initialize) { fieldTypeDefinition.initialize({ context, extra, field, object, options: this.options, value, }); } else { setBackingField(object, field.name, value); defineSimpleAccessors(object, field.name); } } public create(init: TInit, context: StructSerializationContext): Overwrite { const object: any = { [BackingField]: {}, }; Object.defineProperties(object, this._extra); for (const field of this.fields) { const fieldTypeDefinition = getFieldTypeDefinition(field.type); this.initializeField( context, field, fieldTypeDefinition, object, (init as any)[field.name] ); } return object; } public async deserialize( context: StructDeserializationContext ): Promise : TAfterParsed> { const object: any = { [BackingField]: {}, }; Object.defineProperties(object, this._extra); for (const field of this.fields) { const fieldTypeDefinition = getFieldTypeDefinition(field.type); const { value, extra } = await fieldTypeDefinition.deserialize({ context, field, object, options: this.options, }); this.initializeField( context, field, fieldTypeDefinition, object, value, extra ); } if (this._afterParsed) { const result = this._afterParsed.call(object, object); if (result) { return result; } } return object; } public serialize(init: TInit, context: StructSerializationContext): ArrayBuffer { const object = this.create(init, context) as any; let size = this._size; let fieldSize: number[] = []; for (let i = 0; i < this.fields.length; i += 1) { const field = this.fields[i]; const type = getFieldTypeDefinition(field.type); if (type.getDynamicSize) { fieldSize[i] = type.getDynamicSize({ context, field, object, options: this.options, }); size += fieldSize[i]; } else { fieldSize[i] = type.getSize({ field, options: this.options }); } } const buffer = new ArrayBuffer(size); const dataView = new DataView(buffer); let offset = 0; for (let i = 0; i < this.fields.length; i += 1) { const field = this.fields[i]; const type = getFieldTypeDefinition(field.type); type.serialize({ context, dataView, field, object, offset, options: this.options, }); offset += fieldSize[i]; } return buffer; } }