feat(struct): reuse initialize in deserialize

This commit is contained in:
Simon Chan 2020-09-26 04:13:15 +08:00
parent 13526e9020
commit b1cc2ce9a2
13 changed files with 522 additions and 317 deletions

View file

@ -1,13 +1,16 @@
# @yume-chan/struct # @yume-chan/struct
Serialize and deserialize C-style structures. C-style structure serializer and deserializer.
Fully compatible with TypeScript. Fully compatible with TypeScript.
- [Compatibility](#compatibility)
- [Quick Start](#quick-start) - [Quick Start](#quick-start)
- [API](#api) - [API](#api)
- [`placeholder` method](#placeholder-method)
- [`Struct` constructor](#struct-constructor) - [`Struct` constructor](#struct-constructor)
- [`int32`/`uint32` methods](#int32uint32-methods) - [`Struct#int32`/`Struct#uint32` methods](#structint32structuint32-methods)
- [`Struct#uint64` method](#structuint64-method)
- [`extra` function](#extra-function) - [`extra` function](#extra-function)
- [`afterParsed` method](#afterparsed-method) - [`afterParsed` method](#afterparsed-method)
- [`deserialize` method](#deserialize-method) - [`deserialize` method](#deserialize-method)
@ -26,6 +29,39 @@ Fully compatible with TypeScript.
- [`registerFieldTypeDefinition` method](#registerfieldtypedefinition-method) - [`registerFieldTypeDefinition` method](#registerfieldtypedefinition-method)
- [Data flow](#data-flow) - [Data flow](#data-flow)
## Compatibility
Basic usage requires [`Promise`][MDN_Promise], [`ArrayBuffer`][MDN_ArrayBuffer], [`Uint8Array`][MDN_Uint8Array] and [`DataView`][MDN_DataView]. All can be globally polyfilled to support older runtime.
[MDN_Promise]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
[MDN_ArrayBuffer]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer
[MDN_Uint8Array]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array
[MDN_DataView]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView
| Runtime | Minimal Supported Version | Note |
| --------------------- | ------------------------- | ------------------------------- |
| **Chrome** | 32 | |
| **Edge** | 12 | |
| **Firefox** | 29 | |
| **Internet Explorer** | 10 | Requires polyfill for `Promise` |
| **Safari** | 8 | |
| **Node.js** | 0.12 | |
Usage of `uint64` requires [`BigInt`][MDN_BigInt] (**can't** be polyfilled), [`DataView#getBigUint64`][MDN_DataView_getBigUint64] and [`DataView#setBigUint64`][MDN_DataView_setBigUint64] (can be polyfilled).
[MDN_BigInt]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt
[MDN_DataView_getBigUint64]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/getBigUint64
[MDN_DataView_setBigUint64]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/setBigUint64
| Runtime | Minimal Supported Version | Note |
| --------------------- | ------------------------- | ---------------------------------------------------------------------- |
| **Chrome** | 67 | |
| **Edge** | 79 | |
| **Firefox** | 68 | |
| **Internet Explorer** | *N/A* | Doesn't support `BigInt`, can't be polyfilled. |
| **Safari** | 14 | Requires polyfills for `DataView#getBigUint64`/`DataView#setBigUint64` |
| **Node.js** | 10.4.0 | |
## Quick Start ## Quick Start
```ts ```ts
@ -37,39 +73,62 @@ const MyStruct =
.int32('bar'); .int32('bar');
const value = MyStruct.deserialize(someStream); const value = MyStruct.deserialize(someStream);
// TypeScript can infer type of the result object.
const { foo, bar } = value; const { foo, bar } = value;
``` ```
## API ## API
**While all APIs heavily rely on generic, DO NOT PASS ANY GENERIC ARGUMENTS MANUALLY!** ### `placeholder` method
```ts
export function placeholder<T>(): T {
return undefined as unknown as T;
}
```
Return a (fake) value of the given type.
Because TypeScript only supports supply all or none type arguments, this library allows all type parameters to be inferred from arguments.
This method can be used where an argument is only used to infer a type parameter.
**While all following APIs heavily rely on generic, DO NOT PASS ANY GENERIC ARGUMENTS MANUALLY!**
### `Struct` constructor ### `Struct` constructor
```ts ```ts
declare class Struct<TObject = {}, TAfterParsed = undefined, TInit = {}> { export default class Struct<
TResult extends object = {},
TInit extends object = {},
TExtra extends object = {},
TAfterParsed = undefined,
> {
public constructor(options: Partial<StructOptions> = StructDefaultOptions); public constructor(options: Partial<StructOptions> = StructDefaultOptions);
} }
``` ```
Create a new structure definition. Creates a new structure definition.
**Generic Parameters** **Generic Parameters**
1. `TObject`: Type of the result object. 1. `TResult`: Type of the result object.
1. `TAfterParsed`: Special case for the `afterParsed` function. 2. `TInit`: Type requirement to create such a structure. (Because some fields may implies other fields)
1. `TInit`: Type requirement to create such a structure. (Because some fields may implies other fields, and there is the `extra` function) 3. `TExtra`: Type of extra fields.
4. `TAfterParsed`: State of the `afterParsed` function.
**DO NOT PASS ANY GENERIC ARGUMENTS MANUALLY!** **DO NOT PASS ANY GENERIC ARGUMENTS MANUALLY!**
These are considered "internal state" of the `Struct` and it will take care of them by itself. These are considered "internal state" of the `Struct` and will be taken care of by methods below.
**Parameters** **Parameters**
1. `options`: 1. `options`:
* `littleEndian:boolean = false`: Whether all multi-byte fields are little-endian encoded. * `littleEndian:boolean = false`: Whether all multi-byte fields are [little-endian encoded][Wikipeida_Endianess].
### `int32`/`uint32` methods [Wikipeida_Endianess]: https://en.wikipedia.org/wiki/Endianness
### `Struct#int32`/`Struct#uint32` methods
```ts ```ts
public int32< public int32<
@ -77,12 +136,13 @@ public int32<
TTypeScriptType = number TTypeScriptType = number
>( >(
name: TName, name: TName,
options: {} = {}, options: FieldDescriptorBaseOptions = {},
_typescriptType?: () => TTypeScriptType, _typescriptType?: TTypeScriptType,
): Struct< ): Struct<
TObject & Record<TName, TTypeScriptType>, TResult & Record<TName, TTypeScriptType>,
TInit & Record<TName, TTypeScriptType>,
TExtra,
TAfterParsed, TAfterParsed,
TInit & Record<TName, TTypeScriptType>
>; >;
public uint32< public uint32<
@ -91,20 +151,25 @@ public uint32<
>( >(
name: TName, name: TName,
options: {} = {}, options: {} = {},
_typescriptType?: () => TTypeScriptType, _typescriptType?: TTypeScriptType,
): Struct< ): Struct<
TObject & Record<TName, TTypeScriptType>, TResult & Record<TName, TTypeScriptType>,
TInit & Record<TName, TTypeScriptType>,
TExtra,
TAfterParsed, TAfterParsed,
TInit & Record<TName, TTypeScriptType>
>; >;
``` ```
Append an int32/uint32 field into the `Struct`. Return a new `Struct` instance with an `int32`/`uint32` field appended to the end.
The original `Struct` instance will not be changed.
TypeScript will also append a `name: TTypeScriptType` field into the result object and the init object.
**Generic Parameters** **Generic Parameters**
1. `TName`: Literal type of the field's name. 1. `TName`: Literal type of the field's name.
1. `TTypeScriptType = number`: Type of the field in the result object. 2. `TTypeScriptType = number`: Type of the field in the result object.
**DO NOT PASS ANY GENERIC ARGUMENTS MANUALLY!** **DO NOT PASS ANY GENERIC ARGUMENTS MANUALLY!**
@ -114,15 +179,19 @@ TypeScript will infer them from arguments. See examples below.
1. `name`: (Required) Field name. Should be a string literal to make types work. 1. `name`: (Required) Field name. Should be a string literal to make types work.
1. `options`: currently unused. 1. `options`: currently unused.
1. `_typescriptType`: Supply a function to change the field's type. See examples below. 2. `_typescriptType`: Set field's type. See examples below.
**Returns** **Note**
A new instance of `Struct`, with `{ [name]: TTypeScriptType }` added to its result object type. There is no generic constraints on the `TTypeScriptType` type because TypeScript doesn't allow casting enum types to `number`.
So it's technically possible to pass in an incompatible type (e.g. `string`).
But obviously, it's a bad idea.
**Examples** **Examples**
1. Add an int32 field named `foo` 1. Append an int32 field named `foo`
```ts ```ts
const struct = new Struct() const struct = new Struct()
@ -136,7 +205,7 @@ A new instance of `Struct`, with `{ [name]: TTypeScriptType }` added to its resu
struct.create({ foo: 42 }) // ok struct.create({ foo: 42 }) // ok
``` ```
2. Set `foo`'s type 2. Set `foo`'s type (can be used with the [`placeholder` method](#placeholder-method))
```ts ```ts
enum MyEnum { enum MyEnum {
@ -145,50 +214,79 @@ A new instance of `Struct`, with `{ [name]: TTypeScriptType }` added to its resu
} }
const struct = new Struct() const struct = new Struct()
.int32('foo', () => MyEnum.a); // The inferred return type is `MyEnum` .int32('foo', placeholder<MyEnum>())
.int32('bar', MyEnum.a as const);
const value = await struct.deserialize(stream); const value = await struct.deserialize(stream);
value.foo; // MyEnum value.foo; // MyEnum
value.bar; // MyEnum.a
struct.create({ foo: 42 }); // error: 'foo' must be of type `MyEnum` struct.create({ foo: 42, bar: MyEnum.a }); // error: 'foo' must be of type `MyEnum`
struct.create({ foo: MyEnum.b }); // ok struct.create({ foo: MyEnum.a, bar: MyEnum.b }); // error: 'bar' must be of type `MyEnum.a`
struct.create({ foo: MyEnum.a, bar: MyEnum.b }); // ok
// Although there is no generic constraints on the `TTypeScriptType`
// So it's possible to set it to an incompatible type, like `string`
// But obviously, it's a bad idea
``` ```
3. Create a new struct by extending exist one 3. Create a new struct by extending existing one
```ts ```ts
const struct1 = new Struct() const struct1 = new Struct()
.int32('foo'); .int32('foo');
const struct2 = struct1 const struct2 = struct1
.int32('bar'); // `struct1` was not changed .int32('bar');
assert(struct2 !== struct1);
// `struct1` will not be changed
``` ```
### `Struct#uint64` method
```ts
public uint64<
TName extends string,
TTypeScriptType = bigint
>(
name: TName,
options: FieldDescriptorBaseOptions = {},
_typescriptType?: TTypeScriptType,
): Struct<
TResult & Record<TName, TTypeScriptType>,
TInit & Record<TName, TTypeScriptType>,
TExtra,
TAfterParsed,
>;
```
Return a new `Struct` instance with an `uint64` field appended to the end.
The original `Struct` instance will not be changed.
TypeScript will also append a `name: TTypeScriptType` field into the result object and the init object.
Require native `BigInt` support in runtime. See [compatibility](#compatibility).
### `extra` function ### `extra` function
```ts ```ts
public extra<TExtra extends object>( public extra<TValue extends object>(
value: TExtra & ThisType<Omit<TObject, keyof TExtra> & TExtra> value: TValue & ThisType<WithBackingField<Overwrite<Overwrite<TExtra, TValue>, TResult>>>
): Struct< ): Struct<
StructExtraResult<TObject, TExtra>, TResult,
TAfterParsed, TInit,
Omit<TInit, keyof TExtra> Overwrite<TExtra, TValue>,
TAfterParsed
>; >;
``` ```
Add custom extra fields to the structure. Return a new `Struct` instance adding some extra fields.
Extra fields will be added onto the result object after deserializing all fields, overwriting exist fields with same name. The original `Struct` instance will not be changed.
If an extra field doesn't overlap the existing fields, it will not be serialized. However if it does, the value of extra field will be serialized instead of the parsed value. TypeScript will also append all extra fields into the result object (if not already exited).
**Generic Parameters** **Generic Parameters**
1. `TExtra`: Type of your extra fields. 1. `TValue`: Type of the extra fields.
**DO NOT PASS ANY GENERIC ARGUMENTS MANUALLY!** **DO NOT PASS ANY GENERIC ARGUMENTS MANUALLY!**
@ -196,11 +294,13 @@ TypeScript will infer them from arguments. See examples below.
**Parameters** **Parameters**
1. `extra`: An object containing anything you want to add to the result object. Getters/setters and methods are also allowed. 1. `value`: An object containing anything you want to add to the result object. Accessors and methods are also allowed.
**Returns** **Note**
A new instance of `Struct`, with `extra` merged with existing ones. 1. If the current `Struct` already has some extra fields, it will be merged with `value`, with `value` taking precedence.
2. Extra fields will not be serialized.
3. Extra fields will be ignored if it has the same name with some defined fields.
**Examples** **Examples**
@ -247,30 +347,27 @@ A new instance of `Struct`, with `extra` merged with existing ones.
```ts ```ts
public afterParsed( public afterParsed(
callback: StructAfterParsed<TObject, never> callback?: StructAfterParsed<TResult, void>
): Struct<TObject, never, TInit> ): Struct<TResult, TInit, TExtra, undefined>;
public afterParsed(
callback?: StructAfterParsed<TObject, void>
): Struct<TObject, undefined, TInit>;
``` ```
Run custom logic after deserializing. Return a new `Struct` instance, registering (or replacing) a custom callback to be run after deserialized.
Call `afterParsed` again will replace existing callback. The original `Struct` instance will not be changed.
```ts ```ts
public afterParsed<TResult>( public afterParsed<TAfterParsed>(
callback?: StructAfterParsed<TObject, TResult> callback?: StructAfterParsed<TResult, TAfterParsed>
): Struct<TObject, TResult, TInit>; ): Struct<TResult, TInit, TExtra, TAfterParsed>;
``` ```
Run custom logic after deserializing and replace the result object with the returned value. Return a new `Struct` instance, registering (or replacing) a custom callback to be run after deserialized, and replacing the result object with the returned value.
Call `afterParsed` again will replace existing callback. The original `Struct` instance will not be changed.
**Generic Parameters** **Generic Parameters**
1. `TResult`: Type of the result object 1. `TAfterParsed`: Type of the new result object.
**DO NOT PASS ANY GENERIC ARGUMENTS MANUALLY!** **DO NOT PASS ANY GENERIC ARGUMENTS MANUALLY!**
@ -280,10 +377,6 @@ TypeScript will infer them from arguments. See examples below.
1. `callback`: An function contains the custom logic to be run, optionally returns a new result object. Or `undefined`, to clear the previously set `afterParsed` callback. 1. `callback`: An function contains the custom logic to be run, optionally returns a new result object. Or `undefined`, to clear the previously set `afterParsed` callback.
**Returns**
A new instance of `Struct`, with `afterParsed` callback replaced.
**Examples** **Examples**
1. Handle an "error" packet 1. Handle an "error" packet
@ -343,7 +436,7 @@ A new instance of `Struct`, with `afterParsed` callback replaced.
```ts ```ts
public async deserialize( public async deserialize(
context: StructDeserializationContext context: StructDeserializationContext
): Promise<TAfterParsed extends undefined ? TObject : TAfterParsed> ): Promise<TAfterParsed extends undefined ? Overwrite<TExtra, TResult> : TAfterParsed>;
``` ```
Deserialize one structure from the `context`. Deserialize one structure from the `context`.
@ -366,28 +459,25 @@ There are two concepts around the type plugin system.
### Backing Field ### Backing Field
This library defines a common method to hide implementation details of each field types on the result object. The result object has a hidden backing field, containing implementation details of each field.
It can be accessed with the exported `BackingField` symbol.
```ts ```ts
import { BackingField } from '@yume-chan/struct'; import { getBackingField, setBackingField } from '@yume-chan/struct';
const backingField = resultObject[BackingField]; const value = getBackingField<number>(resultObject, 'foo');
setBackingField(resultObject, 'foo', value);
``` ```
The backing field is a map between field key and arbitrary data each type definition want to store. Normally type definitions will store info on the backing field and then define getter/setter on the result object to access them. It's possible to access other fields' data if you know the type. But it's not recommended to modify them.
It's also possible to access other fields' data if you know the data type. But it's not recommended to modify them.
### `FieldDescriptorBase` interface ### `FieldDescriptorBase` interface
This interface describe one field in the struct, and will be stored in `Struct` class. This interface describes one field, and will be stored in `Struct` class.
**Generic Parameters** **Generic Parameters**
* `TName extends string = string`: Name of the field. Although `FieldDescriptorBase` doesn't need it to be generic, derived types will need it. So marking this way helps TypeScript infer the type. * `TName extends string = string`: Name of the field. Although `FieldDescriptorBase` doesn't need it to be generic, derived types will need it. So marking this way helps TypeScript infer the type.
* `TResultObject = {}`: Type that will be merged into the result object (`TObject`). Any key that has `never` type will be removed. * `TResultObject = {}`: Type that will be merged into the result object (`TResult`). Any key that has `never` type will be removed.
* `TInitObject = {}`: Type that will be merged into the init object (`TInit`). Any key that has `never` type will be removed. Normally you only need to add the current field into `TInit`, but sometimes one field will imply other fields, so you may want to also remove those implied fields from `TInit`. * `TInitObject = {}`: Type that will be merged into the init object (`TInit`). Any key that has `never` type will be removed. Normally you only need to add the current field into `TInit`, but sometimes one field will imply other fields, so you may want to also remove those implied fields from `TInit`.
* `TOptions extends FieldDescriptorBaseOptions = FieldDescriptorBaseOptions`: Type of the `options`. currently `FieldDescriptorBaseOptions` is empty but maybe something will happen later. * `TOptions extends FieldDescriptorBaseOptions = FieldDescriptorBaseOptions`: Type of the `options`. currently `FieldDescriptorBaseOptions` is empty but maybe something will happen later.

View file

@ -1,7 +1,7 @@
{ {
"name": "@yume-chan/struct", "name": "@yume-chan/struct",
"version": "0.0.0", "version": "0.0.0",
"description": "Easy to use C structure serializer and deserializer", "description": "C-style structure serializer and deserializer.",
"keywords": [ "keywords": [
"structure", "structure",
"typescript" "typescript"

View file

@ -0,0 +1,20 @@
export const BackingField = Symbol('BackingField');
export function getBackingField<T = unknown>(object: unknown, field: string): T {
return (object as any)[BackingField][field] as T;
}
export function setBackingField(object: unknown, field: string, value: any): void {
(object as any)[BackingField][field] = value;
}
export function defineSimpleAccessors(object: unknown, field: string): void {
Object.defineProperty(object, field, {
configurable: true,
enumerable: true,
get() { return getBackingField(object, field); },
set(value) { setBackingField(object, field, value); },
});
}
export type WithBackingField<T> = T & { [BackingField]: any; };

View file

@ -1,4 +1,5 @@
import { BackingField, FieldDescriptorBase, FieldDescriptorBaseOptions } from './descriptor'; import { getBackingField, setBackingField } from '../backing-field';
import { FieldDescriptorBase, FieldDescriptorBaseOptions } from './descriptor';
export namespace Array { export namespace Array {
export const enum SubType { export const enum SubType {
@ -17,14 +18,6 @@ export namespace Array {
string?: string; string?: string;
} }
export function getBackingField(object: any, name: string): BackingField {
return object[BackingField][name];
}
export function setBackingField(object: any, name: string, value: BackingField): void {
object[BackingField][name] = value;
}
export function initialize(object: any, field: Array, value: BackingField): void { export function initialize(object: any, field: Array, value: BackingField): void {
switch (field.subType) { switch (field.subType) {
case SubType.ArrayBuffer: case SubType.ArrayBuffer:
@ -32,7 +25,7 @@ export namespace Array {
configurable: true, configurable: true,
enumerable: true, enumerable: true,
get(): ArrayBuffer { get(): ArrayBuffer {
return getBackingField(object, field.name).buffer!; return getBackingField<BackingField>(object, field.name).buffer!;
}, },
set(buffer: ArrayBuffer) { set(buffer: ArrayBuffer) {
setBackingField(object, field.name, { buffer }); setBackingField(object, field.name, { buffer });
@ -44,7 +37,7 @@ export namespace Array {
configurable: true, configurable: true,
enumerable: true, enumerable: true,
get(): string { get(): string {
return getBackingField(object, field.name).string!; return getBackingField<BackingField>(object, field.name).string!;
}, },
set(string: string) { set(string: string) {
setBackingField(object, field.name, { string }); setBackingField(object, field.name, { string });

View file

@ -1,21 +1,9 @@
import { StructDeserializationContext, StructOptions, StructSerializationContext } from '../types';
import { FieldDescriptorBase, FieldType } from './descriptor'; import { FieldDescriptorBase, FieldType } from './descriptor';
export interface StructSerializationContext {
encodeUtf8(input: string): ArrayBuffer;
}
export interface StructDeserializationContext extends StructSerializationContext {
decodeUtf8(buffer: ArrayBuffer): string;
read(length: number): ArrayBuffer | Promise<ArrayBuffer>;
}
export interface StructOptions {
littleEndian: boolean;
}
export interface FieldTypeDefinition< export interface FieldTypeDefinition<
TDescriptor extends FieldDescriptorBase = FieldDescriptorBase, TDescriptor extends FieldDescriptorBase = FieldDescriptorBase,
TInitExtra = undefined,
> { > {
type: FieldType | string; type: FieldType | string;
@ -24,7 +12,7 @@ export interface FieldTypeDefinition<
field: TDescriptor; field: TDescriptor;
object: any; object: any;
options: StructOptions; options: StructOptions;
}): Promise<void>; }): Promise<{ value: any; extra?: TInitExtra; }>;
getSize(options: { getSize(options: {
field: TDescriptor; field: TDescriptor;
@ -32,16 +20,17 @@ export interface FieldTypeDefinition<
}): number; }): number;
getDynamicSize?(options: { getDynamicSize?(options: {
context: StructSerializationContext, context: StructSerializationContext;
field: TDescriptor, field: TDescriptor;
object: any, object: any;
options: StructOptions, options: StructOptions;
}): number; }): number;
initialize?(options: { initialize?(options: {
context: StructSerializationContext; context: StructSerializationContext;
field: TDescriptor; field: TDescriptor;
init: any; value: any;
extra?: TInitExtra;
object: any; object: any;
options: StructOptions; options: StructOptions;
}): void; }): void;
@ -56,17 +45,19 @@ export interface FieldTypeDefinition<
}): void; }): void;
} }
const registry: Record<number | string, FieldTypeDefinition> = {}; const registry: Record<number | string, FieldTypeDefinition<any, any>> = {};
export function getFieldTypeDefinition(type: FieldType | string): FieldTypeDefinition { export function getFieldTypeDefinition(type: FieldType | string): FieldTypeDefinition<any, any> {
return registry[type]; return registry[type];
} }
export function registerFieldTypeDefinition< export function registerFieldTypeDefinition<
TDescriptor extends FieldDescriptorBase, TDescriptor extends FieldDescriptorBase,
TDefinition extends FieldTypeDefinition<TDescriptor> TInitExtra,
TDefinition extends FieldTypeDefinition<TDescriptor, TInitExtra>
>( >(
_field: TDescriptor, _field: TDescriptor,
_initExtra: TInitExtra,
methods: TDefinition methods: TDefinition
): void { ): void {
registry[methods.type] = methods; registry[methods.type] = methods;

View file

@ -1,5 +1,3 @@
export const BackingField = Symbol('BackingField');
export const enum FieldType { export const enum FieldType {
Number, Number,
FixedLengthArray, FixedLengthArray,

View file

@ -1,3 +1,5 @@
import { getBackingField } from '../backing-field';
import { placeholder } from '../utils';
import { Array } from './array'; import { Array } from './array';
import { registerFieldTypeDefinition } from './definition'; import { registerFieldTypeDefinition } from './definition';
import { FieldDescriptorBaseOptions, FieldType } from './descriptor'; import { FieldDescriptorBaseOptions, FieldType } from './descriptor';
@ -25,38 +27,49 @@ export interface FixedLengthArray<
options: TOptions; options: TOptions;
}; };
registerFieldTypeDefinition(undefined as unknown as FixedLengthArray, { registerFieldTypeDefinition(
placeholder<FixedLengthArray>(),
placeholder<ArrayBuffer>(),
{
type: FieldType.FixedLengthArray, type: FieldType.FixedLengthArray,
async deserialize({ context, field, object, }) { async deserialize(
const value: Array.BackingField = { { context, field }
buffer: await context.read(field.options.length), ): Promise<{ value: string | ArrayBuffer, extra?: ArrayBuffer; }> {
}; const buffer = await context.read(field.options.length);
switch (field.subType) { switch (field.subType) {
case Array.SubType.ArrayBuffer: case Array.SubType.ArrayBuffer:
break; return { value: buffer };
case Array.SubType.String: case Array.SubType.String:
value.string = context.decodeUtf8(value.buffer!); return {
break; value: context.decodeUtf8(buffer),
extra: buffer
};
default: default:
throw new Error('Unknown type'); throw new Error('Unknown type');
} }
Array.initialize(object, field, value);
}, },
getSize({ field }) { getSize({ field }) {
return field.options.length; return field.options.length;
}, },
initialize({ field, init, object }) { initialize({ extra, field, object, value }) {
Array.initialize(object, field, {}); const backingField: Array.BackingField = {};
object[field.name] = init[field.name]; if (typeof value === 'string') {
backingField.string = value;
if (extra) {
backingField.buffer = extra;
}
} else {
backingField.buffer = value;
}
Array.initialize(object, field, backingField);
}, },
serialize({ context, dataView, field, object, offset }) { serialize({ context, dataView, field, object, offset }) {
const backingField = Array.getBackingField(object, field.name); const backingField = getBackingField<Array.BackingField>(object, field.name);
backingField.buffer ??= backingField.buffer ??=
context.encodeUtf8(backingField.string!); context.encodeUtf8(backingField.string!);
@ -65,4 +78,5 @@ registerFieldTypeDefinition(undefined as unknown as FixedLengthArray, {
offset offset
); );
} }
}); }
);

View file

@ -1,33 +1,40 @@
import { placeholder } from '../utils';
import { registerFieldTypeDefinition } from './definition'; import { registerFieldTypeDefinition } from './definition';
import { BackingField, FieldDescriptorBase, FieldDescriptorBaseOptions, FieldType } from './descriptor'; import { FieldDescriptorBase, FieldDescriptorBaseOptions, FieldType } from './descriptor';
export namespace Number { export namespace Number {
export type TypeScriptType = number; export type TypeScriptType<T extends SubType> =
T extends SubType.Uint64 ? bigint : number;
export const enum SubType { export const enum SubType {
Int32, Int32,
Uint32, Uint32,
Uint64,
} }
export const SizeMap: Record<SubType, number> = { export const SizeMap: Record<SubType, number> = {
[SubType.Int32]: 4, [SubType.Int32]: 4,
[SubType.Uint32]: 4, [SubType.Uint32]: 4,
[SubType.Uint64]: 8,
}; };
export const DataViewGetterMap = { export const DataViewGetterMap = {
[SubType.Int32]: 'getInt32', [SubType.Int32]: 'getInt32',
[SubType.Uint32]: 'getUint32', [SubType.Uint32]: 'getUint32',
[SubType.Uint64]: 'getBigUint64',
} as const; } as const;
export const DataViewSetterMap = { export const DataViewSetterMap = {
[SubType.Int32]: 'setInt32', [SubType.Int32]: 'setInt32',
[SubType.Uint32]: 'setUint32', [SubType.Uint32]: 'setUint32',
[SubType.Uint64]: 'setBigUint64',
} as const; } as const;
} }
export interface Number< export interface Number<
TName extends string = string, TName extends string = string,
TTypeScriptType = Number.TypeScriptType, TSubType extends Number.SubType = Number.SubType,
TTypeScriptType = Number.TypeScriptType<TSubType>,
TOptions extends FieldDescriptorBaseOptions = FieldDescriptorBaseOptions TOptions extends FieldDescriptorBaseOptions = FieldDescriptorBaseOptions
> extends FieldDescriptorBase< > extends FieldDescriptorBase<
TName, TName,
@ -37,37 +44,35 @@ export interface Number<
> { > {
type: FieldType.Number; type: FieldType.Number;
subType: Number.SubType; subType: TSubType;
} }
registerFieldTypeDefinition(undefined as unknown as Number, { registerFieldTypeDefinition(
placeholder<Number>(),
undefined,
{
type: FieldType.Number, type: FieldType.Number,
getSize({ field }) { getSize({ field }) {
return Number.SizeMap[field.subType]; return Number.SizeMap[field.subType];
}, },
async deserialize({ context, field, object, options }) { async deserialize({ context, field, options }) {
const buffer = await context.read(Number.SizeMap[field.subType]); const buffer = await context.read(Number.SizeMap[field.subType]);
const view = new DataView(buffer); const view = new DataView(buffer);
const value = view[Number.DataViewGetterMap[field.subType]]( const value = view[Number.DataViewGetterMap[field.subType]](
0, 0,
options.littleEndian options.littleEndian
); );
object[BackingField][field.name] = value; return { value };
Object.defineProperty(object, field.name, {
configurable: true,
enumerable: true,
get() { return object[BackingField][field.name]; },
set(value) { object[BackingField][field.name] = value; },
});
}, },
serialize({ dataView, field, object, offset, options }) { serialize({ dataView, field, object, offset, options }) {
dataView[Number.DataViewSetterMap[field.subType]]( (dataView[Number.DataViewSetterMap[field.subType]] as any)(
offset, offset,
object[field.name], object[field.name],
options.littleEndian options.littleEndian
); );
}, },
}); }
);

View file

@ -1,7 +1,9 @@
import { Identity } from '../utils'; import { getBackingField, setBackingField } from '../backing-field';
import { StructSerializationContext } from '../types';
import { Identity, placeholder } from '../utils';
import { Array } from './array'; import { Array } from './array';
import { registerFieldTypeDefinition, StructSerializationContext } from './definition'; import { registerFieldTypeDefinition } from './definition';
import { BackingField, FieldDescriptorBaseOptions, FieldType } from './descriptor'; import { FieldDescriptorBaseOptions, FieldType } from './descriptor';
export namespace VariableLengthArray { export namespace VariableLengthArray {
export type TypeScriptTypeCanBeUndefined< export type TypeScriptTypeCanBeUndefined<
@ -40,8 +42,11 @@ export namespace VariableLengthArray {
emptyBehavior?: TEmptyBehavior; emptyBehavior?: TEmptyBehavior;
} }
export function getLengthBackingField(object: any, field: VariableLengthArray): number | undefined { export function getLengthBackingField(
return object[BackingField][field.options.lengthField]; object: any,
field: VariableLengthArray
): number | undefined {
return getBackingField<number>(object, field.options.lengthField);
} }
export function setLengthBackingField( export function setLengthBackingField(
@ -49,7 +54,7 @@ export namespace VariableLengthArray {
field: VariableLengthArray, field: VariableLengthArray,
value: number | undefined value: number | undefined
) { ) {
object[BackingField][field.options.lengthField] = value; setBackingField(object, field.options.lengthField, value);
} }
export function initialize( export function initialize(
@ -101,7 +106,7 @@ export namespace VariableLengthArray {
get() { get() {
let value = getLengthBackingField(object, field); let value = getLengthBackingField(object, field);
if (value === undefined) { if (value === undefined) {
const backingField = Array.getBackingField(object, field.name); const backingField = getBackingField<Array.BackingField>(object, field.name);
const buffer = context.encodeUtf8(backingField.string!); const buffer = context.encodeUtf8(backingField.string!);
backingField.buffer = buffer; backingField.buffer = buffer;
@ -115,7 +120,7 @@ export namespace VariableLengthArray {
default: default:
throw new Error('Unknown type'); throw new Error('Unknown type');
} }
Array.setBackingField(object, field.name, value); setBackingField(object, field.name, value);
if (value.buffer) { if (value.buffer) {
setLengthBackingField(object, field, value.buffer.byteLength); setLengthBackingField(object, field, value.buffer.byteLength);
} }
@ -142,33 +147,43 @@ export interface VariableLengthArray<
options: TOptions; options: TOptions;
} }
registerFieldTypeDefinition(undefined as unknown as VariableLengthArray, { registerFieldTypeDefinition(
placeholder<VariableLengthArray>(),
placeholder<ArrayBuffer>(),
{
type: FieldType.VariableLengthArray, type: FieldType.VariableLengthArray,
async deserialize({ context, field, object }) { async deserialize(
const value: Array.BackingField = {}; { context, field, object }
): Promise<{ value: string | ArrayBuffer | undefined, extra?: ArrayBuffer; }> {
const length = object[field.options.lengthField]; const length = object[field.options.lengthField];
if (length === 0) { if (length === 0) {
if (field.options.emptyBehavior === VariableLengthArray.EmptyBehavior.Empty) { if (field.options.emptyBehavior === VariableLengthArray.EmptyBehavior.Empty) {
value.buffer = new ArrayBuffer(0);
value.string = '';
}
VariableLengthArray.initialize(object, field, value, context);
return;
}
value.buffer = await context.read(length);
switch (field.subType) { switch (field.subType) {
case Array.SubType.ArrayBuffer: case Array.SubType.ArrayBuffer:
break; return { value: new ArrayBuffer(0) };
case Array.SubType.String: case Array.SubType.String:
value.string = context.decodeUtf8(value.buffer); return { value: '', extra: new ArrayBuffer(0) };
break; default:
throw new Error('Unknown type');
}
} else {
return { value: undefined };
}
}
const buffer = await context.read(length);
switch (field.subType) {
case Array.SubType.ArrayBuffer:
return { value: buffer };
case Array.SubType.String:
return {
value: context.decodeUtf8(buffer),
extra: buffer
};
default: default:
throw new Error('Unknown type'); throw new Error('Unknown type');
} }
VariableLengthArray.initialize(object, field, value, context);
}, },
getSize() { return 0; }, getSize() { return 0; },
@ -177,16 +192,26 @@ registerFieldTypeDefinition(undefined as unknown as VariableLengthArray, {
return object[field.options.lengthField]; return object[field.options.lengthField];
}, },
initialize({ context, field, init, object }) { initialize({ context, extra, field, object, value }) {
VariableLengthArray.initialize(object, field, {}, context); const backingField: Array.BackingField = {};
object[field.name] = init[field.name]; if (typeof value === 'string') {
backingField.string = value;
if (extra) {
backingField.buffer = extra;
}
} else {
backingField.buffer = value;
}
Array.initialize(object, field, backingField);
VariableLengthArray.initialize(object, field, backingField, context);
}, },
serialize({ dataView, field, object, offset }) { serialize({ dataView, field, object, offset }) {
const backingField = Array.getBackingField(object, field.name); const backingField = getBackingField<Array.BackingField>(object, field.name);
new Uint8Array(dataView.buffer).set( new Uint8Array(dataView.buffer).set(
new Uint8Array(backingField.buffer!), new Uint8Array(backingField.buffer!),
offset offset
); );
}, },
}); }
);

View file

@ -1,4 +1,6 @@
export * from './backing-field';
export * from './field'; export * from './field';
export * from './struct'; export * from './struct';
export { default as Struct } from './struct'; export { default as Struct } from './struct';
export * from './types';
export * from './utils'; export * from './utils';

View file

@ -1,19 +1,18 @@
import { Array, BackingField, FieldDescriptorBase, FieldDescriptorBaseOptions, FieldType, FixedLengthArray, getFieldTypeDefinition, Number, StructDeserializationContext, StructOptions, StructSerializationContext, VariableLengthArray } from './field'; import { BackingField, defineSimpleAccessors, setBackingField, WithBackingField } from './backing-field';
import { Evaluate, Identity } from './utils'; 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 Struct<object, object, unknown>> = export type StructValueType<T extends Struct<object, object, object, unknown>> =
T extends { deserialize(reader: StructDeserializationContext): Promise<infer R>; } ? R : never; T extends { deserialize(reader: StructDeserializationContext): Promise<infer R>; } ? R : never;
export type StructInitType<T extends Struct<object, object, unknown>> = export type StructInitType<T extends Struct<object, object, object, unknown>> =
T extends { create(value: infer R, ...args: any): any; } ? R : never; T extends { create(value: infer R, ...args: any): any; } ? R : never;
export const StructDefaultOptions: Readonly<StructOptions> = {
littleEndian: false,
};
interface AddArrayFieldDescriptor< interface AddArrayFieldDescriptor<
TResult extends object, TResult extends object,
TInit extends object, TInit extends object,
TExtra extends object,
TAfterParsed TAfterParsed
> { > {
< <
@ -28,6 +27,7 @@ interface AddArrayFieldDescriptor<
): MergeStruct< ): MergeStruct<
TResult, TResult,
TInit, TInit,
TExtra,
TAfterParsed, TAfterParsed,
FixedLengthArray< FixedLengthArray<
TName, TName,
@ -50,6 +50,7 @@ interface AddArrayFieldDescriptor<
): MergeStruct< ): MergeStruct<
TResult, TResult,
TInit, TInit,
TExtra,
TAfterParsed, TAfterParsed,
VariableLengthArray< VariableLengthArray<
TName, TName,
@ -65,6 +66,7 @@ interface AddArrayFieldDescriptor<
interface AddArraySubTypeFieldDescriptor< interface AddArraySubTypeFieldDescriptor<
TResult extends object, TResult extends object,
TInit extends object, TInit extends object,
TExtra extends object,
TAfterParsed, TAfterParsed,
TType extends Array.SubType TType extends Array.SubType
> { > {
@ -78,6 +80,7 @@ interface AddArraySubTypeFieldDescriptor<
): MergeStruct< ): MergeStruct<
TResult, TResult,
TInit, TInit,
TExtra,
TAfterParsed, TAfterParsed,
FixedLengthArray< FixedLengthArray<
TName, TName,
@ -94,10 +97,11 @@ interface AddArraySubTypeFieldDescriptor<
>( >(
name: TName, name: TName,
options: VariableLengthArray.Options<TInit, TLengthField, TEmptyBehavior>, options: VariableLengthArray.Options<TInit, TLengthField, TEmptyBehavior>,
_typescriptType?: () => TTypeScriptType, _typescriptType?: TTypeScriptType,
): MergeStruct< ): MergeStruct<
TResult, TResult,
TInit, TInit,
TExtra,
TAfterParsed, TAfterParsed,
VariableLengthArray< VariableLengthArray<
TName, TName,
@ -110,31 +114,27 @@ interface AddArraySubTypeFieldDescriptor<
>; >;
} }
export type OmitNever<T> = Pick<T, { [K in keyof T]: [T[K]] extends [never] ? never : K }[keyof T]>;
type MergeStruct< type MergeStruct<
TResult extends object, TResult extends object,
TInit extends object, TInit extends object,
TExtra extends object,
TAfterParsed, TAfterParsed,
TDescriptor extends FieldDescriptorBase TDescriptor extends FieldDescriptorBase
> = > =
Identity<Struct< Identity<Struct<
Evaluate<TResult & Exclude<TDescriptor['resultObject'], undefined>>, Evaluate<TResult & Exclude<TDescriptor['resultObject'], undefined>>,
Evaluate<OmitNever<TInit & Exclude<TDescriptor['initObject'], undefined>>>, Evaluate<OmitNever<TInit & Exclude<TDescriptor['initObject'], undefined>>>,
TExtra,
TAfterParsed TAfterParsed
>>; >>;
type WithBackingField<T> = T & { [BackingField]: any; };
export type StructExtraResult<TResult, TExtra> =
Evaluate<Omit<TResult, keyof TExtra> & TExtra>;
export type StructAfterParsed<TResult, TAfterParsed> = export type StructAfterParsed<TResult, TAfterParsed> =
(this: WithBackingField<TResult>, object: WithBackingField<TResult>) => TAfterParsed; (this: WithBackingField<TResult>, object: WithBackingField<TResult>) => TAfterParsed;
export default class Struct< export default class Struct<
TResult extends object = {}, TResult extends object = {},
TInit extends object = {}, TInit extends object = {},
TExtra extends object = {},
TAfterParsed = undefined, TAfterParsed = undefined,
> { > {
public readonly options: Readonly<StructOptions>; public readonly options: Readonly<StructOptions>;
@ -152,8 +152,8 @@ export default class Struct<
this.options = { ...StructDefaultOptions, ...options }; this.options = { ...StructDefaultOptions, ...options };
} }
private clone(): Struct<any, any, any> { private clone(): Struct<any, any, any, any> {
const result = new Struct<any, any, any>(this.options); const result = new Struct<any, any, any, any>(this.options);
result.fields = this.fields.slice(); result.fields = this.fields.slice();
result._size = this._size; result._size = this._size;
result._extra = this._extra; result._extra = this._extra;
@ -163,7 +163,7 @@ export default class Struct<
public field<TDescriptor extends FieldDescriptorBase>( public field<TDescriptor extends FieldDescriptorBase>(
field: TDescriptor, field: TDescriptor,
): MergeStruct<TResult, TInit, TAfterParsed, TDescriptor> { ): MergeStruct<TResult, TInit, TExtra, TAfterParsed, TDescriptor> {
const result = this.clone(); const result = this.clone();
result.fields.push(field); result.fields.push(field);
@ -176,14 +176,15 @@ export default class Struct<
private number< private number<
TName extends string, TName extends string,
TTypeScriptType = Number.TypeScriptType TSubType extends Number.SubType = Number.SubType,
TTypeScriptType = Number.TypeScriptType<TSubType>
>( >(
name: TName, name: TName,
type: Number.SubType, type: TSubType,
options: FieldDescriptorBaseOptions = {}, options: FieldDescriptorBaseOptions = {},
_typescriptType?: () => TTypeScriptType, _typescriptType?: TTypeScriptType,
) { ) {
return this.field<Number<TName, TTypeScriptType>>({ return this.field<Number<TName, TSubType, TTypeScriptType>>({
type: FieldType.Number, type: FieldType.Number,
name, name,
subType: type, subType: type,
@ -193,11 +194,11 @@ export default class Struct<
public int32< public int32<
TName extends string, TName extends string,
TTypeScriptType = Number.TypeScriptType TTypeScriptType = Number.TypeScriptType<Number.SubType.Int32>
>( >(
name: TName, name: TName,
options: FieldDescriptorBaseOptions = {}, options: FieldDescriptorBaseOptions = {},
_typescriptType?: () => TTypeScriptType, _typescriptType?: TTypeScriptType,
) { ) {
return this.number( return this.number(
name, name,
@ -209,11 +210,11 @@ export default class Struct<
public uint32< public uint32<
TName extends string, TName extends string,
TTypeScriptType = Number.TypeScriptType TTypeScriptType = Number.TypeScriptType<Number.SubType.Uint32>
>( >(
name: TName, name: TName,
options: FieldDescriptorBaseOptions = {}, options: FieldDescriptorBaseOptions = {},
_typescriptType?: () => TTypeScriptType, _typescriptType?: TTypeScriptType,
) { ) {
return this.number( return this.number(
name, name,
@ -223,11 +224,27 @@ export default class Struct<
); );
} }
private array: AddArrayFieldDescriptor<TResult, TInit, TAfterParsed> = ( public uint64<
TName extends string,
TTypeScriptType = Number.TypeScriptType<Number.SubType.Uint64>
>(
name: TName,
options: FieldDescriptorBaseOptions = {},
_typescriptType?: TTypeScriptType,
) {
return this.number(
name,
Number.SubType.Uint64,
options,
_typescriptType
);
}
private array: AddArrayFieldDescriptor<TResult, TInit, TExtra, TAfterParsed> = (
name: string, name: string,
type: Array.SubType, type: Array.SubType,
options: FixedLengthArray.Options | VariableLengthArray.Options options: FixedLengthArray.Options | VariableLengthArray.Options
): Struct<any, any, any> => { ): Struct<any, any, any, any> => {
if ('length' in options) { if ('length' in options) {
return this.field<FixedLengthArray>({ return this.field<FixedLengthArray>({
type: FieldType.FixedLengthArray, type: FieldType.FixedLengthArray,
@ -248,6 +265,7 @@ export default class Struct<
public arrayBuffer: AddArraySubTypeFieldDescriptor< public arrayBuffer: AddArraySubTypeFieldDescriptor<
TResult, TResult,
TInit, TInit,
TExtra,
TAfterParsed, TAfterParsed,
Array.SubType.ArrayBuffer Array.SubType.ArrayBuffer
> = <TName extends string>( > = <TName extends string>(
@ -260,6 +278,7 @@ export default class Struct<
public string: AddArraySubTypeFieldDescriptor< public string: AddArraySubTypeFieldDescriptor<
TResult, TResult,
TInit, TInit,
TExtra,
TAfterParsed, TAfterParsed,
Array.SubType.String Array.SubType.String
> = <TName extends string>( > = <TName extends string>(
@ -269,11 +288,12 @@ export default class Struct<
return this.array(name, Array.SubType.String, options); return this.array(name, Array.SubType.String, options);
}; };
public extra<TExtra extends object>( public extra<TValue extends object>(
value: TExtra & ThisType<WithBackingField<StructExtraResult<TResult, TExtra>>> value: TValue & ThisType<WithBackingField<Overwrite<Overwrite<TExtra, TValue>, TResult>>>
): Struct< ): Struct<
StructExtraResult<TResult, TExtra>, TResult,
Evaluate<Omit<TInit, keyof TExtra>>, TInit,
Overwrite<TExtra, TValue>,
TAfterParsed TAfterParsed
> { > {
const result = this.clone(); const result = this.clone();
@ -283,69 +303,90 @@ export default class Struct<
public afterParsed( public afterParsed(
callback: StructAfterParsed<TResult, never> callback: StructAfterParsed<TResult, never>
): Struct<TResult, TInit, never>; ): Struct<TResult, TInit, TExtra, never>;
public afterParsed( public afterParsed(
callback?: StructAfterParsed<TResult, void> callback?: StructAfterParsed<TResult, void>
): Struct<TResult, TInit, undefined>; ): Struct<TResult, TInit, TExtra, undefined>;
public afterParsed<TAfterParsed>( public afterParsed<TAfterParsed>(
callback?: StructAfterParsed<TResult, TAfterParsed> callback?: StructAfterParsed<TResult, TAfterParsed>
): Struct<TResult, TInit, TAfterParsed>; ): Struct<TResult, TInit, TExtra, TAfterParsed>;
public afterParsed( public afterParsed(
callback?: StructAfterParsed<TResult, any> callback?: StructAfterParsed<TResult, any>
): Struct<any, any, any> { ) {
const result = this.clone(); const result = this.clone();
result._afterParsed = callback; result._afterParsed = callback;
return result; return result;
} }
public create(init: TInit, context: StructSerializationContext): TResult { private initializeField(
context: StructSerializationContext,
field: FieldDescriptorBase,
fieldTypeDefinition: FieldTypeDefinition<any, any>,
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<TExtra, TResult> {
const object: any = { const object: any = {
[BackingField]: {}, [BackingField]: {},
}; };
Object.defineProperties(object, this._extra);
for (const field of this.fields) { for (const field of this.fields) {
const type = getFieldTypeDefinition(field.type); const fieldTypeDefinition = getFieldTypeDefinition(field.type);
if (type.initialize) { this.initializeField(
type.initialize({
context, context,
field, field,
init, fieldTypeDefinition,
object, object,
options: this.options, (init as any)[field.name]
}); );
} else {
object[BackingField][field.name] = (init as any)[field.name];
Object.defineProperty(object, field.name, {
configurable: true,
enumerable: true,
get() { return object[BackingField][field.name]; },
set(value) { object[BackingField][field.name] = value; },
});
}
} }
Object.defineProperties(object, this._extra);
return object; return object;
} }
public async deserialize( public async deserialize(
context: StructDeserializationContext context: StructDeserializationContext
): Promise<TAfterParsed extends undefined ? TResult : TAfterParsed> { ): Promise<TAfterParsed extends undefined ? Overwrite<TExtra, TResult> : TAfterParsed> {
const object: any = { const object: any = {
[BackingField]: {}, [BackingField]: {},
}; };
Object.defineProperties(object, this._extra);
for (const field of this.fields) { for (const field of this.fields) {
await getFieldTypeDefinition(field.type).deserialize({ const fieldTypeDefinition = getFieldTypeDefinition(field.type);
const { value, extra } = await fieldTypeDefinition.deserialize({
context, context,
field, field,
object, object,
options: this.options, options: this.options,
}); });
this.initializeField(
context,
field,
fieldTypeDefinition,
object,
value,
extra
);
} }
Object.defineProperties(object, this._extra);
if (this._afterParsed) { if (this._afterParsed) {
const result = this._afterParsed.call(object, object); const result = this._afterParsed.call(object, object);
if (result) { if (result) {

View file

@ -0,0 +1,17 @@
export interface StructSerializationContext {
encodeUtf8(input: string): ArrayBuffer;
}
export interface StructDeserializationContext extends StructSerializationContext {
decodeUtf8(buffer: ArrayBuffer): string;
read(length: number): ArrayBuffer | Promise<ArrayBuffer>;
}
export interface StructOptions {
littleEndian: boolean;
}
export const StructDefaultOptions: Readonly<StructOptions> = {
littleEndian: false,
};

View file

@ -1,3 +1,12 @@
export type Identity<T> = T; export type Identity<T> = T;
export type Evaluate<T> = T extends infer U ? { [K in keyof U]: U[K] } : never; export type Evaluate<T> = T extends infer U ? { [K in keyof U]: U[K] } : never;
export type Overwrite<TBase extends object, TNew extends object> =
Evaluate<Omit<TBase, keyof TNew> & TNew>;
export type OmitNever<T> = Pick<T, { [K in keyof T]: [T[K]] extends [never] ? never : K }[keyof T]>;
export function placeholder<T>(): T {
return undefined as unknown as T;
}