123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631 |
- import { StructAsyncDeserializeStream, StructDefaultOptions, StructDeserializeStream, StructFieldDefinition, StructFieldValue, StructOptions, StructValue, STRUCT_VALUE_SYMBOL } from './basic/index.js';
- import { SyncPromise } from "./sync-promise.js";
- import { BigIntFieldDefinition, BigIntFieldType, BufferFieldSubType, FixedLengthBufferLikeFieldDefinition, NumberFieldDefinition, NumberFieldType, StringBufferFieldSubType, Uint8ArrayBufferFieldSubType, VariableLengthBufferLikeFieldDefinition, type FixedLengthBufferLikeFieldOptions, type LengthField, type VariableLengthBufferLikeFieldOptions } from './types/index.js';
- import type { Evaluate, Identity, Overwrite, ValueOrPromise } from "./utils.js";
- export interface StructLike<TValue> {
- deserialize(stream: StructDeserializeStream | StructAsyncDeserializeStream): Promise<TValue>;
- }
- /**
- * Extract the value type of the specified `Struct`
- */
- export type StructValueType<T extends StructLike<any>> =
- Awaited<ReturnType<T['deserialize']>>;
- /**
- * Create a new `Struct` type with `TDefinition` appended
- */
- type AddFieldDescriptor<
- TFields extends object,
- TOmitInitKey extends PropertyKey,
- TExtra extends object,
- TPostDeserialized,
- TFieldName extends PropertyKey,
- TDefinition extends StructFieldDefinition<any, any, any>
- > =
- Identity<Struct<
- // Merge two types
- // Evaluate immediately to optimize editor hover tooltip
- Evaluate<TFields & Record<TFieldName, TDefinition['TValue']>>,
- // Merge two `TOmitInitKey`s
- TOmitInitKey | TDefinition['TOmitInitKey'],
- TExtra,
- TPostDeserialized
- >>;
- /**
- * Overload methods to add an array buffer like field
- */
- interface ArrayBufferLikeFieldCreator<
- TFields extends object,
- TOmitInitKey extends PropertyKey,
- TExtra extends object,
- TPostDeserialized
- > {
- /**
- * Append a fixed-length array buffer like field to the `Struct`
- *
- * @param name Name of the field
- * @param type `Array.SubType.ArrayBuffer` or `Array.SubType.String`
- * @param options Fixed-length array options
- * @param typeScriptType Type of the field in TypeScript.
- * For example, if this field is a string, you can declare it as a string enum or literal union.
- */
- <
- TName extends PropertyKey,
- TType extends BufferFieldSubType<any, any>,
- TTypeScriptType = TType['TTypeScriptType'],
- >(
- name: TName,
- type: TType,
- options: FixedLengthBufferLikeFieldOptions,
- typeScriptType?: TTypeScriptType,
- ): AddFieldDescriptor<
- TFields,
- TOmitInitKey,
- TExtra,
- TPostDeserialized,
- TName,
- FixedLengthBufferLikeFieldDefinition<
- TType,
- FixedLengthBufferLikeFieldOptions
- >
- >;
- /**
- * Append a variable-length array buffer like field to the `Struct`
- */
- <
- TName extends PropertyKey,
- TType extends BufferFieldSubType<any, any>,
- TOptions extends VariableLengthBufferLikeFieldOptions<TFields>,
- TTypeScriptType = TType['TTypeScriptType'],
- >(
- name: TName,
- type: TType,
- options: TOptions,
- typeScriptType?: TTypeScriptType,
- ): AddFieldDescriptor<
- TFields,
- TOmitInitKey,
- TExtra,
- TPostDeserialized,
- TName,
- VariableLengthBufferLikeFieldDefinition<
- TType,
- TOptions
- >
- >;
- }
- /**
- * Similar to `ArrayBufferLikeFieldCreator`, but bind to `TType`
- */
- interface BoundArrayBufferLikeFieldDefinitionCreator<
- TFields extends object,
- TOmitInitKey extends PropertyKey,
- TExtra extends object,
- TPostDeserialized,
- TType extends BufferFieldSubType<any, any>
- > {
- <
- TName extends PropertyKey,
- TTypeScriptType = TType['TTypeScriptType'],
- >(
- name: TName,
- options: FixedLengthBufferLikeFieldOptions,
- typeScriptType?: TTypeScriptType,
- ): AddFieldDescriptor<
- TFields,
- TOmitInitKey,
- TExtra,
- TPostDeserialized,
- TName,
- FixedLengthBufferLikeFieldDefinition<
- TType,
- FixedLengthBufferLikeFieldOptions,
- TTypeScriptType
- >
- >;
- <
- TName extends PropertyKey,
- TLengthField extends LengthField<TFields>,
- TOptions extends VariableLengthBufferLikeFieldOptions<TFields, TLengthField>,
- TTypeScriptType = TType['TTypeScriptType'],
- >(
- name: TName,
- options: TOptions,
- typeScriptType?: TTypeScriptType,
- ): AddFieldDescriptor<
- TFields,
- TOmitInitKey,
- TExtra,
- TPostDeserialized,
- TName,
- VariableLengthBufferLikeFieldDefinition<
- TType,
- TOptions,
- TTypeScriptType
- >
- >;
- }
- export type StructPostDeserialized<TFields, TPostDeserialized> =
- (this: TFields, object: TFields) => TPostDeserialized;
- export type StructDeserializedResult<TFields extends object, TExtra extends object, TPostDeserialized> =
- TPostDeserialized extends undefined ? Overwrite<TExtra, TFields> : TPostDeserialized;
- export class Struct<
- TFields extends object = {},
- TOmitInitKey extends PropertyKey = never,
- TExtra extends object = {},
- TPostDeserialized = undefined,
- > implements StructLike<StructDeserializedResult<TFields, TExtra, TPostDeserialized>>{
- public readonly TFields!: TFields;
- public readonly TOmitInitKey!: TOmitInitKey;
- public readonly TExtra!: TExtra;
- public readonly TInit!: Evaluate<Omit<TFields, TOmitInitKey>>;
- public readonly TDeserializeResult!: StructDeserializedResult<TFields, TExtra, TPostDeserialized>;
- public readonly options: Readonly<StructOptions>;
- private _size = 0;
- /**
- * Gets the static size (exclude fields that can change size at runtime)
- */
- public get size() { return this._size; }
- private _fields: [name: PropertyKey, definition: StructFieldDefinition<any, any, any>][] = [];
- private _extra: Record<PropertyKey, unknown> = {};
- private _postDeserialized?: StructPostDeserialized<any, any> | undefined;
- public constructor(options?: Partial<Readonly<StructOptions>>) {
- this.options = { ...StructDefaultOptions, ...options };
- }
- /**
- * Appends a `StructFieldDefinition` to the `Struct
- */
- public field<
- TName extends PropertyKey,
- TDefinition extends StructFieldDefinition<any, any, any>
- >(
- name: TName,
- definition: TDefinition,
- ): AddFieldDescriptor<
- TFields,
- TOmitInitKey,
- TExtra,
- TPostDeserialized,
- TName,
- TDefinition
- > {
- for (const field of this._fields) {
- if (field[0] === name) {
- throw new Error(`This struct already have a field with name '${String(name)}'`);
- }
- }
- this._fields.push([name, definition]);
- const size = definition.getSize();
- this._size += size;
- // Force cast `this` to another type
- return this as any;
- }
- /**
- * Merges (flats) another `Struct`'s fields and extra fields into this one.
- */
- public fields<TOther extends Struct<any, any, any, any>>(
- other: TOther
- ): Struct<
- TFields & TOther['TFields'],
- TOmitInitKey | TOther['TOmitInitKey'],
- TExtra & TOther['TExtra'],
- TPostDeserialized
- > {
- for (const field of other._fields) {
- this._fields.push(field);
- }
- this._size += other._size;
- Object.defineProperties(this._extra, Object.getOwnPropertyDescriptors(other._extra));
- return this as any;
- }
- private number<
- TName extends PropertyKey,
- TType extends NumberFieldType = NumberFieldType,
- TTypeScriptType = TType['TTypeScriptType']
- >(
- name: TName,
- type: TType,
- typeScriptType?: TTypeScriptType,
- ) {
- return this.field(
- name,
- new NumberFieldDefinition(type, typeScriptType),
- );
- }
- /**
- * Appends an `int8` field to the `Struct`
- */
- public int8<
- TName extends PropertyKey,
- TTypeScriptType = (typeof NumberFieldType)['Uint8']['TTypeScriptType']
- >(
- name: TName,
- typeScriptType?: TTypeScriptType,
- ) {
- return this.number(
- name,
- NumberFieldType.Int8,
- typeScriptType
- );
- }
- /**
- * Appends an `uint8` field to the `Struct`
- */
- public uint8<
- TName extends PropertyKey,
- TTypeScriptType = (typeof NumberFieldType)['Uint8']['TTypeScriptType']
- >(
- name: TName,
- typeScriptType?: TTypeScriptType,
- ) {
- return this.number(
- name,
- NumberFieldType.Uint8,
- typeScriptType
- );
- }
- /**
- * Appends an `int16` field to the `Struct`
- */
- public int16<
- TName extends PropertyKey,
- TTypeScriptType = (typeof NumberFieldType)['Uint16']['TTypeScriptType']
- >(
- name: TName,
- typeScriptType?: TTypeScriptType,
- ) {
- return this.number(
- name,
- NumberFieldType.Int16,
- typeScriptType
- );
- }
- /**
- * Appends an `uint16` field to the `Struct`
- */
- public uint16<
- TName extends PropertyKey,
- TTypeScriptType = (typeof NumberFieldType)['Uint16']['TTypeScriptType']
- >(
- name: TName,
- typeScriptType?: TTypeScriptType,
- ) {
- return this.number(
- name,
- NumberFieldType.Uint16,
- typeScriptType
- );
- }
- /**
- * Appends an `int32` field to the `Struct`
- */
- public int32<
- TName extends PropertyKey,
- TTypeScriptType = (typeof NumberFieldType)['Int32']['TTypeScriptType']
- >(
- name: TName,
- typeScriptType?: TTypeScriptType,
- ) {
- return this.number(
- name,
- NumberFieldType.Int32,
- typeScriptType
- );
- }
- /**
- * Appends an `uint32` field to the `Struct`
- */
- public uint32<
- TName extends PropertyKey,
- TTypeScriptType = (typeof NumberFieldType)['Uint32']['TTypeScriptType']
- >(
- name: TName,
- typeScriptType?: TTypeScriptType,
- ) {
- return this.number(
- name,
- NumberFieldType.Uint32,
- typeScriptType
- );
- }
- private bigint<
- TName extends PropertyKey,
- TType extends BigIntFieldType = BigIntFieldType,
- TTypeScriptType = TType['TTypeScriptType']
- >(
- name: TName,
- type: TType,
- typeScriptType?: TTypeScriptType,
- ) {
- return this.field(
- name,
- new BigIntFieldDefinition(type, typeScriptType),
- );
- }
- /**
- * Appends an `int64` field to the `Struct`
- *
- * Requires native `BigInt` support
- */
- public int64<
- TName extends PropertyKey,
- TTypeScriptType = BigIntFieldType['TTypeScriptType']
- >(
- name: TName,
- typeScriptType?: TTypeScriptType,
- ) {
- return this.bigint(
- name,
- BigIntFieldType.Int64,
- typeScriptType
- );
- }
- /**
- * Appends an `uint64` field to the `Struct`
- *
- * Requires native `BigInt` support
- */
- public uint64<
- TName extends PropertyKey,
- TTypeScriptType = BigIntFieldType['TTypeScriptType']
- >(
- name: TName,
- typeScriptType?: TTypeScriptType,
- ) {
- return this.bigint(
- name,
- BigIntFieldType.Uint64,
- typeScriptType
- );
- }
- private arrayBufferLike: ArrayBufferLikeFieldCreator<
- TFields,
- TOmitInitKey,
- TExtra,
- TPostDeserialized
- > = (
- name: PropertyKey,
- type: BufferFieldSubType,
- options: FixedLengthBufferLikeFieldOptions | VariableLengthBufferLikeFieldOptions
- ): any => {
- if ('length' in options) {
- return this.field(
- name,
- new FixedLengthBufferLikeFieldDefinition(type, options),
- );
- } else {
- return this.field(
- name,
- new VariableLengthBufferLikeFieldDefinition(type, options),
- );
- }
- };
- public uint8Array: BoundArrayBufferLikeFieldDefinitionCreator<
- TFields,
- TOmitInitKey,
- TExtra,
- TPostDeserialized,
- Uint8ArrayBufferFieldSubType
- > = (
- name: PropertyKey,
- options: any,
- typeScriptType: any,
- ): any => {
- return this.arrayBufferLike(name, Uint8ArrayBufferFieldSubType.Instance, options, typeScriptType);
- };
- public string: BoundArrayBufferLikeFieldDefinitionCreator<
- TFields,
- TOmitInitKey,
- TExtra,
- TPostDeserialized,
- StringBufferFieldSubType
- > = (
- name: PropertyKey,
- options: any,
- typeScriptType: any,
- ): any => {
- return this.arrayBufferLike(name, StringBufferFieldSubType.Instance, options, typeScriptType);
- };
- /**
- * Adds some extra properties into every `Struct` value.
- *
- * Extra properties will not affect serialize or deserialize process.
- *
- * Multiple calls to `extra` will merge all properties together.
- *
- * @param value
- * An object containing properties to be added to the result value. Accessors and methods are also allowed.
- */
- public extra<T extends Record<
- // This trick disallows any keys that are already in `TValue`
- Exclude<
- keyof T,
- Exclude<keyof T, keyof TFields>
- >,
- never
- >>(
- value: T & ThisType<Overwrite<Overwrite<TExtra, T>, TFields>>
- ): Struct<
- TFields,
- TOmitInitKey,
- Overwrite<TExtra, T>,
- TPostDeserialized
- > {
- Object.defineProperties(
- this._extra,
- Object.getOwnPropertyDescriptors(value)
- );
- return this as any;
- }
- /**
- * Registers (or replaces) a custom callback to be run after deserialized.
- *
- * A callback returning `never` (always throw an error)
- * will also change the return type of `deserialize` to `never`.
- */
- public postDeserialize(
- callback: StructPostDeserialized<TFields, never>
- ): Struct<TFields, TOmitInitKey, TExtra, never>;
- /**
- * Registers (or replaces) a custom callback to be run after deserialized.
- *
- * A callback returning `void` means it modify the result object in-place
- * (or doesn't modify it at all), so `deserialize` will still return the result object.
- */
- public postDeserialize(
- callback?: StructPostDeserialized<TFields, void>
- ): Struct<TFields, TOmitInitKey, TExtra, undefined>;
- /**
- * Registers (or replaces) a custom callback to be run after deserialized.
- *
- * A callback returning anything other than `undefined`
- * will `deserialize` to return that object instead.
- */
- public postDeserialize<TPostSerialize>(
- callback?: StructPostDeserialized<TFields, TPostSerialize>
- ): Struct<TFields, TOmitInitKey, TExtra, TPostSerialize>;
- public postDeserialize(
- callback?: StructPostDeserialized<TFields, any>
- ) {
- this._postDeserialized = callback;
- return this as any;
- }
- /**
- * Deserialize a struct value from `stream`.
- */
- public deserialize(
- stream: StructDeserializeStream,
- ): StructDeserializedResult<TFields, TExtra, TPostDeserialized>;
- public deserialize(
- stream: StructAsyncDeserializeStream,
- ): Promise<StructDeserializedResult<TFields, TExtra, TPostDeserialized>>;
- public deserialize(
- stream: StructDeserializeStream | StructAsyncDeserializeStream,
- ): ValueOrPromise<StructDeserializedResult<TFields, TExtra, TPostDeserialized>> {
- const structValue = new StructValue(this._extra);
- let promise = SyncPromise.resolve();
- for (const [name, definition] of this._fields) {
- promise = promise
- .then(() =>
- definition.deserialize(this.options, stream as any, structValue)
- )
- .then(fieldValue => {
- structValue.set(name, fieldValue);
- });
- }
- return promise
- .then(() => {
- const object = structValue.value;
- // Run `postDeserialized`
- if (this._postDeserialized) {
- const override = this._postDeserialized.call(object, object);
- // If it returns a new value, use that as result
- // Otherwise it only inspects/mutates the object in place.
- if (override !== undefined) {
- return override;
- }
- }
- return object;
- })
- .valueOrPromise();
- }
- public serialize(init: Evaluate<Omit<TFields, TOmitInitKey>>): Uint8Array;
- public serialize(init: Evaluate<Omit<TFields, TOmitInitKey>>, output: Uint8Array): number;
- public serialize(init: Evaluate<Omit<TFields, TOmitInitKey>>, output?: Uint8Array): Uint8Array | number {
- let structValue: StructValue;
- if (STRUCT_VALUE_SYMBOL in init) {
- structValue = (init as any)[STRUCT_VALUE_SYMBOL];
- for (const [key, value] of Object.entries(init)) {
- const fieldValue = structValue.get(key);
- if (fieldValue) {
- fieldValue.set(value);
- }
- }
- } else {
- structValue = new StructValue({});
- for (const [name, definition] of this._fields) {
- const fieldValue = definition.create(
- this.options,
- structValue,
- (init as any)[name]
- );
- structValue.set(name, fieldValue);
- }
- }
- let structSize = 0;
- const fieldsInfo: { fieldValue: StructFieldValue, size: number; }[] = [];
- for (const [name] of this._fields) {
- const fieldValue = structValue.get(name);
- const size = fieldValue.getSize();
- fieldsInfo.push({ fieldValue, size });
- structSize += size;
- }
- let outputType = 'number';
- if (!output) {
- output = new Uint8Array(structSize);
- outputType = 'Uint8Array';
- }
- const dataView = new DataView(output.buffer, output.byteOffset, output.byteLength);
- let offset = 0;
- for (const { fieldValue, size } of fieldsInfo) {
- fieldValue.serialize(dataView, offset);
- offset += size;
- }
- if (outputType === 'number') {
- return structSize;
- } else {
- return output;
- }
- }
- }
|