mirror of
https://github.com/yume-chan/ya-webadb.git
synced 2025-10-05 02:39:26 +02:00
feat(struct): reuse initialize in deserialize
This commit is contained in:
parent
13526e9020
commit
b1cc2ce9a2
13 changed files with 522 additions and 317 deletions
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
20
packages/struct/src/backing-field.ts
Normal file
20
packages/struct/src/backing-field.ts
Normal 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; };
|
|
@ -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 });
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
export const BackingField = Symbol('BackingField');
|
|
||||||
|
|
||||||
export const enum FieldType {
|
export const enum FieldType {
|
||||||
Number,
|
Number,
|
||||||
FixedLengthArray,
|
FixedLengthArray,
|
||||||
|
|
|
@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
|
@ -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
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
|
@ -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
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
17
packages/struct/src/types.ts
Normal file
17
packages/struct/src/types.ts
Normal 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,
|
||||||
|
};
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue