@yume-chan/struct 0.0.5 → 0.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/CHANGELOG.json +23 -0
  2. package/CHANGELOG.md +16 -0
  3. package/LICENSE +21 -21
  4. package/README.md +720 -643
  5. package/dts/basic/context.d.ts +10 -17
  6. package/dts/basic/context.d.ts.map +1 -1
  7. package/dts/basic/definition.d.ts +23 -23
  8. package/dts/basic/definition.d.ts.map +1 -1
  9. package/dts/basic/field-value.d.ts +38 -0
  10. package/dts/basic/field-value.d.ts.map +1 -0
  11. package/dts/basic/index.d.ts +2 -2
  12. package/dts/basic/index.d.ts.map +1 -1
  13. package/dts/basic/struct-value.d.ts +24 -0
  14. package/dts/basic/struct-value.d.ts.map +1 -0
  15. package/dts/index.d.ts +1 -0
  16. package/dts/index.d.ts.map +1 -1
  17. package/dts/struct.d.ts +44 -47
  18. package/dts/struct.d.ts.map +1 -1
  19. package/dts/syncbird.d.ts +20 -0
  20. package/dts/syncbird.d.ts.map +1 -0
  21. package/dts/types/array-buffer.d.ts +22 -20
  22. package/dts/types/array-buffer.d.ts.map +1 -1
  23. package/dts/types/fixed-length-array-buffer.d.ts +1 -3
  24. package/dts/types/fixed-length-array-buffer.d.ts.map +1 -1
  25. package/dts/types/index.d.ts +0 -1
  26. package/dts/types/index.d.ts.map +1 -1
  27. package/dts/types/number.d.ts +11 -10
  28. package/dts/types/number.d.ts.map +1 -1
  29. package/dts/types/variable-length-array-buffer.d.ts +21 -19
  30. package/dts/types/variable-length-array-buffer.d.ts.map +1 -1
  31. package/dts/{types/utils.d.ts → utils.d.ts} +3 -0
  32. package/dts/utils.d.ts.map +1 -0
  33. package/esm/basic/context.js +1 -1
  34. package/esm/basic/context.js.map +1 -1
  35. package/esm/basic/definition.js +9 -11
  36. package/esm/basic/definition.js.map +1 -1
  37. package/esm/basic/field-value.js +35 -0
  38. package/esm/basic/field-value.js.map +1 -0
  39. package/esm/basic/index.js +2 -2
  40. package/esm/basic/index.js.map +1 -1
  41. package/esm/basic/struct-value.js +36 -0
  42. package/esm/basic/struct-value.js.map +1 -0
  43. package/esm/index.js +1 -0
  44. package/esm/index.js.map +1 -1
  45. package/esm/struct.js +103 -176
  46. package/esm/struct.js.map +1 -1
  47. package/esm/syncbird.js +25 -0
  48. package/esm/syncbird.js.map +1 -0
  49. package/esm/types/array-buffer.js +83 -107
  50. package/esm/types/array-buffer.js.map +1 -1
  51. package/esm/types/fixed-length-array-buffer.js +5 -15
  52. package/esm/types/fixed-length-array-buffer.js.map +1 -1
  53. package/esm/types/index.js +0 -1
  54. package/esm/types/index.js.map +1 -1
  55. package/esm/types/number.js +35 -53
  56. package/esm/types/number.js.map +1 -1
  57. package/esm/types/variable-length-array-buffer.js +57 -68
  58. package/esm/types/variable-length-array-buffer.js.map +1 -1
  59. package/esm/utils.js +17 -0
  60. package/esm/utils.js.map +1 -0
  61. package/package.json +47 -50
  62. package/src/basic/context.ts +32 -40
  63. package/src/basic/definition.ts +72 -69
  64. package/src/basic/{runtime-value.ts → field-value.ts} +67 -72
  65. package/src/basic/index.ts +4 -4
  66. package/src/basic/struct-value.ts +39 -0
  67. package/src/index.ts +5 -4
  68. package/src/struct.ts +595 -579
  69. package/src/syncbird.ts +53 -0
  70. package/src/types/array-buffer.ts +194 -171
  71. package/src/types/fixed-length-array-buffer.ts +17 -27
  72. package/src/types/index.ts +4 -5
  73. package/src/types/number.ts +114 -101
  74. package/src/types/variable-length-array-buffer.ts +142 -121
  75. package/src/{types/utils.ts → utils.ts} +62 -47
  76. package/cjs/basic/context.js +0 -7
  77. package/cjs/basic/context.js.map +0 -1
  78. package/cjs/basic/definition.js +0 -26
  79. package/cjs/basic/definition.js.map +0 -1
  80. package/cjs/basic/index.js +0 -8
  81. package/cjs/basic/index.js.map +0 -1
  82. package/cjs/basic/runtime-object.js +0 -34
  83. package/cjs/basic/runtime-object.js.map +0 -1
  84. package/cjs/basic/runtime-value.js +0 -41
  85. package/cjs/basic/runtime-value.js.map +0 -1
  86. package/cjs/index.js +0 -10
  87. package/cjs/index.js.map +0 -1
  88. package/cjs/struct.js +0 -259
  89. package/cjs/struct.js.map +0 -1
  90. package/cjs/types/array-buffer.js +0 -137
  91. package/cjs/types/array-buffer.js.map +0 -1
  92. package/cjs/types/fixed-length-array-buffer.js +0 -21
  93. package/cjs/types/fixed-length-array-buffer.js.map +0 -1
  94. package/cjs/types/index.js +0 -9
  95. package/cjs/types/index.js.map +0 -1
  96. package/cjs/types/number.js +0 -68
  97. package/cjs/types/number.js.map +0 -1
  98. package/cjs/types/utils.js +0 -11
  99. package/cjs/types/utils.js.map +0 -1
  100. package/cjs/types/variable-length-array-buffer.js +0 -85
  101. package/cjs/types/variable-length-array-buffer.js.map +0 -1
  102. package/dts/basic/runtime-object.d.ts +0 -17
  103. package/dts/basic/runtime-object.d.ts.map +0 -1
  104. package/dts/basic/runtime-value.d.ts +0 -39
  105. package/dts/basic/runtime-value.d.ts.map +0 -1
  106. package/dts/types/utils.d.ts.map +0 -1
  107. package/esm/basic/runtime-object.js +0 -28
  108. package/esm/basic/runtime-object.js.map +0 -1
  109. package/esm/basic/runtime-value.js +0 -38
  110. package/esm/basic/runtime-value.js.map +0 -1
  111. package/esm/types/utils.js +0 -7
  112. package/esm/types/utils.js.map +0 -1
  113. package/src/basic/context.spec.ts +0 -9
  114. package/src/basic/definition.spec.ts +0 -26
  115. package/src/basic/runtime-object.spec.ts +0 -38
  116. package/src/basic/runtime-object.ts +0 -36
  117. package/src/basic/runtime-value.spec.ts +0 -71
  118. package/src/types/array-buffer.spec.ts +0 -88
  119. package/src/types/number.spec.ts +0 -262
  120. package/tsconfig.cjs.json +0 -11
  121. package/tsconfig.esm.json +0 -15
package/README.md CHANGED
@@ -1,643 +1,720 @@
1
- # @yume-chan/struct
2
-
3
- ![license](https://img.shields.io/npm/l/@yume-chan/struct)
4
- ![npm type definitions](https://img.shields.io/npm/types/@yume-chan/struct)
5
- [![npm version](https://img.shields.io/npm/v/@yume-chan/struct)](https://www.npmjs.com/package/@yume-chan/struct)
6
- ![npm bundle size](https://img.shields.io/bundlephobia/min/@yume-chan/struct)
7
-
8
- A C-style structure serializer and deserializer. Written in TypeScript and highly takes advantage of its type system.
9
-
10
- ## Installation
11
-
12
- ```sh
13
- $ npm i @yume-chan/struct
14
- ```
15
-
16
- ## Quick Start
17
-
18
- ```ts
19
- import Struct from '@yume-chan/struct';
20
-
21
- const MyStruct =
22
- new Struct({ littleEndian: true })
23
- .int8('foo')
24
- .int64('bar')
25
- .int32('bazLength')
26
- .string('baz', { lengthField: 'bazLength' });
27
-
28
- const value = await MyStruct.deserialize(stream);
29
- value.foo // number
30
- value.bar // bigint
31
- value.bazLength // number
32
- value.baz // string
33
-
34
- const buffer = MyStruct.serialize({
35
- foo: 42,
36
- bar: 42n,
37
- // `bazLength` automatically set to `baz.length`
38
- baz: 'Hello, World!',
39
- });
40
- ```
41
-
42
- - [Installation](#installation)
43
- - [Quick Start](#quick-start)
44
- - [Compatibility](#compatibility)
45
- - [API](#api)
46
- - [`placeholder`](#placeholder)
47
- - [`Struct`](#struct)
48
- - [`int8`/`uint8`/`int16`/`uint16`/`int32`/`uint32`](#int8uint8int16uint16int32uint32)
49
- - [`int64`/`uint64`](#int64uint64)
50
- - [`arraybuffer`/`uint8ClampedArray`/`string`](#arraybufferuint8clampedarraystring)
51
- - [`fields`](#fields)
52
- - [`deserialize`](#deserialize)
53
- - [`serialize`](#serialize)
54
- - [`extra`](#extra)
55
- - [`postDeserialize`](#postdeserialize)
56
- - [Custom field type](#custom-field-type)
57
- - [`Struct#field` method](#structfield-method)
58
- - [`FieldDefinition`](#fielddefinition)
59
- - [`getSize`](#getsize)
60
- - [`deserialize`](#deserialize-1)
61
- - [`createValue`](#createvalue)
62
- - [`FieldRuntimeValue`](#fieldruntimevalue)
63
-
64
- ## Compatibility
65
-
66
- 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.
67
-
68
- [MDN_Promise]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
69
- [MDN_ArrayBuffer]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer
70
- [MDN_Uint8Array]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array
71
- [MDN_DataView]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView
72
-
73
- | Runtime | Minimal Supported Version | Note |
74
- | --------------------- | ------------------------- | ------------------------------- |
75
- | **Chrome** | 32 | |
76
- | **Edge** | 12 | |
77
- | **Firefox** | 29 | |
78
- | **Internet Explorer** | 10 | Requires polyfill for `Promise` |
79
- | **Safari** | 8 | |
80
- | **Node.js** | 0.12 | |
81
-
82
- Use of `int64`/`uint64` requires [`BigInt`][MDN_BigInt] (**can't** be polyfilled), [`DataView#getBigUint64`][MDN_DataView_getBigUint64] and [`DataView#setBigUint64`][MDN_DataView_setBigUint64] (can be polyfilled).
83
-
84
- [MDN_BigInt]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt
85
- [MDN_DataView_getBigUint64]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/getBigUint64
86
- [MDN_DataView_setBigUint64]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/setBigUint64
87
-
88
- | Runtime | Minimal Supported Version | Note |
89
- | --------------------- | ------------------------- | ---------------------------------------------------------------------- |
90
- | **Chrome** | 67 | |
91
- | **Edge** | 79 | |
92
- | **Firefox** | 68 | |
93
- | **Internet Explorer** | *N/A* | Doesn't support `BigInt`, can't be polyfilled. |
94
- | **Safari** | 14 | Requires polyfills for `DataView#getBigUint64`/`DataView#setBigUint64` |
95
- | **Node.js** | 10.4.0 | |
96
-
97
- ## API
98
-
99
- ### `placeholder`
100
-
101
- ```ts
102
- function placeholder<T>(): T {
103
- return undefined as unknown as T;
104
- }
105
- ```
106
-
107
- Returns a (fake) value of the given type. It's only useful in TypeScript, if you are using JavaScript, you shouldn't care about it.
108
-
109
- Many methods in this library have multiple generic parameters, but TypeScript only allows users to specify none (let TypeScript inference all of them from arguments), or all generic arguments. ([Microsoft/TypeScript#26242](https://github.com/microsoft/TypeScript/issues/26242))
110
-
111
- <details>
112
- <summary>Detail explanation (click to expand)</summary>
113
-
114
- When you have a generic method, where half generic parameters can be inferred.
115
-
116
- ```ts
117
- declare function fn<A, B>(a: A): [A, B];
118
- fn(42); // Expected 2 type arguments, but got 1. ts(2558)
119
- ```
120
-
121
- Rather than force users repeat the type `A`, I declare a parameter for `B`.
122
-
123
- ```ts
124
- declare function fn2<A, B>(a: A, b: B): [A, B];
125
- ```
126
-
127
- I don't really need a value of type `B`, I only require its type information
128
-
129
- ```ts
130
- fn2(42, placeholder<boolean>()) // fn2<number, boolean>
131
- ```
132
- </details>
133
-
134
- To workaround this issue, these methods have an extra `_typescriptType` parameter, to let you specify a generic parameter, without pass all other generic arguments manually. The actual value of `_typescriptType` argument is never used, so you can pass any value, as long as it has the correct type, including values produced by this `placeholder` method.
135
-
136
- **With that said, I don't expect you to specify any generic arguments manually when using this library.**
137
-
138
- ### `Struct`
139
-
140
- ```ts
141
- class Struct<
142
- TValue extends object = {},
143
- TInit extends object = {},
144
- TExtra extends object = {},
145
- TPostDeserialized = undefined
146
- > {
147
- public constructor(options: Partial<StructOptions> = StructDefaultOptions);
148
- }
149
- ```
150
-
151
- Creates a new structure declaration.
152
-
153
- <details>
154
- <summary>Generic parameters (click to expand)</summary>
155
-
156
- This information was added to help you understand how does it work. These are considered as "internal state" so don't specify them manually.
157
-
158
- 1. `TValue`: Type of the Struct value. Modified when new fields are added.
159
- 2. `TInit`: Type requirement to create such a structure. (May not be same as `TValue` because some fields can implies others). Modified when new fields are added.
160
- 3. `TExtra`: Type of extra fields. Modified when `extra` is called.
161
- 4. `TPostDeserialized`: State of the `postDeserialize` function. Modified when `postDeserialize` is called. Affects return type of `deserialize`
162
- </details>
163
-
164
- **Parameters**
165
-
166
- 1. `options`:
167
- * `littleEndian:boolean = false`: Whether all multi-byte fields in this struct are [little-endian encoded][Wikipeida_Endianess].
168
-
169
- [Wikipeida_Endianess]: https://en.wikipedia.org/wiki/Endianness
170
-
171
- #### `int8`/`uint8`/`int16`/`uint16`/`int32`/`uint32`
172
-
173
- ```ts
174
- int32<
175
- TName extends string | number | symbol,
176
- TTypeScriptType = number
177
- >(
178
- name: TName,
179
- _typescriptType?: TTypeScriptType
180
- ): Struct<
181
- Evaluate<TValue & Record<TName, TTypeScriptType>>,
182
- Evaluate<TInit & Record<TName, TTypeScriptType>>,
183
- TExtra,
184
- TPostDeserialized
185
- >;
186
- ```
187
-
188
- Appends an `int8`/`uint8`/`int16`/`uint16`/`int32`/`uint32` field to the `Struct`
189
-
190
- **Generic Parameters**
191
-
192
- 1. `TName`: Literal type of the field's name.
193
- 2. `TTypeScriptType = number`: Type of the field in the result object. For example you can declare it as a number literal type, or some enum type.
194
-
195
- **Parameters**
196
-
197
- 1. `name`: (Required) Field name. Should be a string literal to make types work.
198
- 2. `_typescriptType`: Set field's type. See examples below.
199
-
200
- **Note**
201
-
202
- There is no generic constraints on the `TTypeScriptType` type because TypeScript doesn't allow casting enum types to `number`.
203
-
204
- So it's technically possible to pass in an incompatible type (e.g. `string`). But obviously, it's a bad idea.
205
-
206
- **Examples**
207
-
208
- 1. Append an `int32` field named `foo`
209
-
210
- ```ts
211
- const struct = new Struct()
212
- .int32('foo');
213
-
214
- const value = await struct.deserialize(stream);
215
- value.foo; // number
216
-
217
- struct.serialize({ }, context) // error: 'foo' is required
218
- struct.serialize({ foo: 'bar' }, context) // error: 'foo' must be a number
219
- struct.serialize({ foo: 42 }, context) // ok
220
- ```
221
-
222
- 2. Set `foo`'s type (can be used with [`placeholder` method](#placeholder))
223
-
224
- ```ts
225
- enum MyEnum {
226
- a,
227
- b,
228
- }
229
-
230
- const struct = new Struct()
231
- .int32('foo', placeholder<MyEnum>())
232
- .int32('bar', MyEnum.a as const);
233
-
234
- const value = await struct.deserialize(stream);
235
- value.foo; // MyEnum
236
- value.bar; // MyEnum.a
237
-
238
- struct.serialize({ foo: 42, bar: MyEnum.a }, context); // error: 'foo' must be of type `MyEnum`
239
- struct.serialize({ foo: MyEnum.a, bar: MyEnum.b }, context); // error: 'bar' must be of type `MyEnum.a`
240
- struct.serialize({ foo: MyEnum.a, bar: MyEnum.b }, context); // ok
241
- ```
242
-
243
- #### `int64`/`uint64`
244
-
245
- ```ts
246
- int64<
247
- TName extends PropertyKey,
248
- TTypeScriptType = bigint
249
- >(
250
- name: TName,
251
- _typescriptType?: TTypeScriptType
252
- ): Struct<
253
- Evaluate<TValue & Record<TName, TTypeScriptType>>,
254
- Evaluate<TInit & Record<TName, TTypeScriptType>>,
255
- TExtra,
256
- TPostDeserialized
257
- >;
258
- ```
259
-
260
- Appends an `int64`/`uint64` field to the `Struct`.
261
-
262
- Requires native runtime support for `BigInt`. Check [compatibility table](#compatibility) for more information.
263
-
264
- #### `arraybuffer`/`uint8ClampedArray`/`string`
265
-
266
- ```ts
267
- <
268
- TName extends PropertyKey,
269
- TTypeScriptType = TType['valueType'],
270
- >(
271
- name: TName,
272
- options: FixedLengthArrayBufferLikeFieldOptions,
273
- typescriptType?: TTypeScriptType,
274
- ): AddFieldDescriptor<
275
- TValue,
276
- TInit,
277
- TExtra,
278
- TPostDeserialized,
279
- TName,
280
- FixedLengthArrayBufferLikeFieldDefinition<
281
- TType,
282
- FixedLengthArrayBufferLikeFieldOptions
283
- >
284
- >;
285
-
286
- <
287
- TName extends PropertyKey,
288
- TLengthField extends KeysOfType<TInit, number | string>,
289
- TOptions extends VariableLengthArrayBufferLikeFieldOptions<TInit, TLengthField>,
290
- TTypeScriptType = TType['valueType'],
291
- >(
292
- name: TName,
293
- options: TOptions,
294
- typescriptType?: TTypeScriptType,
295
- ): AddFieldDescriptor<
296
- TValue,
297
- TInit,
298
- TExtra,
299
- TPostDeserialized,
300
- TName,
301
- VariableLengthArrayBufferLikeFieldDefinition<
302
- TType,
303
- TOptions
304
- >
305
- >;
306
- ```
307
-
308
- Appends an array type field to the `Struct`. The second, `options` parameter defines its length in byte.
309
-
310
- * `{ length: number }`: When the `length` option is specified, it's a fixed length array.
311
- * `{ lengthField: string }`: When the `lengthField` option is specified, and pointing to another `number` or `string` typed field that's defined before this one, it's a variable length array. It will use that field's value for its length when deserializing, and write its length to that field when serializing.
312
-
313
- All these three are deserialized as `ArrayBuffer`, then converted to `Uint8ClampedArray` or `string` for ease of use.
314
-
315
- #### `fields`
316
-
317
- ```ts
318
- fields<
319
- TOther extends Struct<any, any, any, any>
320
- >(
321
- other: TOther
322
- ): Struct<
323
- TValue & TOther['valueType'],
324
- TInit & TOther['initType'],
325
- TExtra & TOther['extraType'],
326
- TPostDeserialized
327
- >;
328
- ```
329
-
330
- Merges (flats) another `Struct`'s fields and extra fields into the current one.
331
-
332
- **Examples**
333
-
334
- 1. Extending another `Struct`
335
-
336
- ```ts
337
- const MyStructV1 =
338
- new Struct()
339
- .int32('field1');
340
-
341
- const MyStructV2 =
342
- new Struct()
343
- .fields(MyStructV1)
344
- .int32('field2');
345
-
346
- const structV2 = await MyStructV2.deserialize(context);
347
- structV2.field1; // number
348
- structV2.field2; // number
349
- // Fields are flatten
350
- ```
351
-
352
- 2. Also possible in any order
353
-
354
- ```ts
355
- const MyStructV1 =
356
- new Struct()
357
- .int32('field1');
358
-
359
- const MyStructV2 =
360
- new Struct()
361
- .int32('field2')
362
- .fields(MyStructV1);
363
-
364
- const structV2 = await MyStructV2.deserialize(context);
365
- structV2.field1; // number
366
- structV2.field2; // number
367
- // Same result as above, but serialize/deserialize order is reversed
368
- ```
369
-
370
- #### `deserialize`
371
-
372
- ```ts
373
- export interface StructDeserializationContext {
374
- decodeUtf8(buffer: ArrayBuffer): string;
375
-
376
- read(length: number): ArrayBuffer | Promise<ArrayBuffer>;
377
- }
378
-
379
- deserialize(context: StructDeserializationContext): Promise<TPostDeserialized extends undefined ? Overwrite<TExtra, TValue> : TPostDeserialized>;
380
- ```
381
-
382
- Deserialize a Struct value from `context`.
383
-
384
- As you can see, if your `postDeserialize` callback returns something, that value will be returned by `deserialize`.
385
-
386
- The `context` has a `read` method, that when called, should returns exactly `length` bytes of data (or throw an `Error` if it can't). So data can arrive asynchronously.
387
-
388
- #### `serialize`
389
-
390
- ```ts
391
- interface StructSerializationContext {
392
- encodeUtf8(input: string): ArrayBuffer;
393
- }
394
-
395
- serialize(init: TInit, context: StructSerializationContext): ArrayBuffer;
396
- ```
397
-
398
- Serialize a Struct value into an `ArrayBuffer`.
399
-
400
- #### `extra`
401
-
402
- ```ts
403
- extra<
404
- T extends Record<
405
- Exclude<
406
- keyof T,
407
- Exclude<
408
- keyof T,
409
- keyof TValue
410
- >
411
- >,
412
- never
413
- >
414
- >(
415
- value: T & ThisType<Overwrite<Overwrite<TExtra, T>, TValue>>
416
- ): Struct<
417
- TValue,
418
- TInit,
419
- Overwrite<TExtra, T>,
420
- TPostDeserialized
421
- >;
422
- ```
423
-
424
- Adds some extra fields into every Struct value.
425
-
426
- Extra fields will not affect serialize or deserialize process.
427
-
428
- Multiple calls to `extra` will merge all values together.
429
-
430
- See examples below.
431
-
432
- **Generic Parameters**
433
-
434
- 1. `T`: Type of the extra fields. The scary looking generic constraint is used to forbid overwriting any already existed fields.
435
-
436
- **DO NOT PASS ANY GENERIC ARGUMENTS MANUALLY!**
437
-
438
- TypeScript will infer them from arguments. See examples below.
439
-
440
- **Parameters**
441
-
442
- 1. `value`: An object containing anything you want to add to the result object. Accessors and methods are also allowed.
443
-
444
- **Examples**
445
-
446
- 1. Add an extra field
447
-
448
- ```ts
449
- const struct = new Struct()
450
- .int32('foo')
451
- .extra({
452
- bar: 'hello',
453
- });
454
-
455
- const value = await struct.deserialize(stream);
456
- value.foo; // number
457
- value.bar; // 'hello'
458
-
459
- struct.create({ foo: 42 }); // ok
460
- struct.create({ foo: 42, bar: 'hello' }); // error: 'bar' is redundant
461
- ```
462
-
463
- 2. Add getters and methods. `this` in functions refers to the result object.
464
-
465
- ```ts
466
- const struct = new Struct()
467
- .int32('foo')
468
- .extra({
469
- get bar() {
470
- // `this` is the result Struct value
471
- return this.foo + 1;
472
- },
473
- logBar() {
474
- // `this` also contains other extra fields
475
- console.log(this.bar);
476
- },
477
- });
478
-
479
- const value = await struct.deserialize(stream);
480
- value.foo; // number
481
- value.bar; // number
482
- value.logBar();
483
- ```
484
-
485
- #### `postDeserialize`
486
-
487
- ```ts
488
- postDeserialize(callback: StructPostDeserialized<TValue, never>): Struct<TValue, TInit, TExtra, never>;
489
- postDeserialize(callback?: StructPostDeserialized<TValue, void>): Struct<TValue, TInit, TExtra, undefined>;
490
- ```
491
-
492
- Registers (or replaces) a custom callback to be run after deserialized.
493
-
494
- A callback returning `never` (always throw an error) will also change the return type of `deserialize` to `never`.
495
-
496
- A callback returning `void` means it modify the result object in-place (or doesn't modify it at all), so `deserialize` will still return the result object.
497
-
498
- ```ts
499
- postDeserialize<TPostSerialize>(callback?: StructPostDeserialized<TValue, TPostSerialize>): Struct<TValue, TInit, TExtra, TPostSerialize>;
500
- ```
501
-
502
- Registers (or replaces) a custom callback to be run after deserialized.
503
-
504
- A callback returning anything other than `undefined` will `deserialize` to return that object instead.
505
-
506
- **Generic Parameters**
507
-
508
- 1. `TPostSerialize`: Type of the new result object.
509
-
510
- **DO NOT PASS ANY GENERIC ARGUMENTS MANUALLY!**
511
-
512
- TypeScript will infer them from arguments. See examples below.
513
-
514
- **Parameters**
515
-
516
- 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.
517
-
518
- **Examples**
519
-
520
- 1. Handle an "error" packet
521
-
522
- ```ts
523
- // Say your protocol have an error packet,
524
- // You want to throw a JavaScript Error when received such a packet,
525
- // But you don't want to modify all receiving path
526
-
527
- const struct = new Struct()
528
- .int32('messageLength')
529
- .string('message', { lengthField: 'messageLength' })
530
- .postDeserialize(value => {
531
- throw new Error(value.message);
532
- });
533
- ```
534
-
535
- 2. Do anything you want
536
-
537
- ```ts
538
- // I think this one doesn't need any code example
539
- ```
540
-
541
- 3. Replace result object
542
-
543
- ```ts
544
- const struct1 = new Struct()
545
- .int32('foo')
546
- .postDeserialize(value => {
547
- return {
548
- bar: value.foo,
549
- };
550
- });
551
-
552
- const value = await struct.deserialize(stream);
553
- value.bar; // number
554
- ```
555
-
556
- ## Custom field type
557
-
558
- This library has a plugin system to support adding fields with custom types.
559
-
560
- ### `Struct#field` method
561
-
562
- ```ts
563
- field<
564
- TName extends PropertyKey,
565
- TDefinition extends FieldDefinition<any, any, any>
566
- >(
567
- name: TName,
568
- definition: TDefinition
569
- ): Struct<
570
- Evaluate<TValue & Record<TFieldName, TDefinition['valueType']>>,
571
- Evaluate<Omit<TInit, TDefinition['removeFields']> & Record<TFieldName, TDefinition['valueType']>>,
572
- TExtra,
573
- TPostDeserialized
574
- >;
575
- ```
576
-
577
- Appends a `FieldDefinition` to the `Struct`.
578
-
579
- All above built-in methods are alias of `field`. To add a field of a custom type, let users call `field` with your custom `FieldDefinition` implementation.
580
-
581
- ### `FieldDefinition`
582
-
583
- ```ts
584
- abstract class FieldDefinition<TOptions = void, TValueType = unknown, TOmitInit = never> {
585
- readonly options: TOptions;
586
-
587
- constructor(options: TOptions);
588
- }
589
- ```
590
-
591
- A `FieldDefinition` describes type, size and runtime semantics of a field.
592
-
593
- It's an `abstract` class, means it lacks some method implementations, so it shouldn't be constructed.
594
-
595
- #### `getSize`
596
-
597
- ```ts
598
- abstract getSize(): number;
599
- ```
600
-
601
- Returns the size (or minimal size if it's dynamic) of this field.
602
-
603
- Actual size should been returned from `FieldRuntimeValue#getSize`
604
-
605
- #### `deserialize`
606
-
607
- ```ts
608
- abstract deserialize(
609
- options: Readonly<StructOptions>,
610
- context: StructDeserializationContext,
611
- object: any,
612
- ): ValueOrPromise<FieldRuntimeValue<FieldDefinition<TOptions, TValueType, TRemoveInitFields>>>;
613
- ```
614
-
615
- Defines how to deserialize a value from `context`. Can also return a `Promise`.
616
-
617
- Usually implementations should be:
618
-
619
- 1. Somehow parse the value from `context`
620
- 2. Pass the value into `FieldDefinition#createValue`
621
-
622
- Sometimes, some metadata is present when deserializing, but need to be calculated when serializing, for example a UTF-8 encoded string may have different length between itself (character count) and serialized form (byte length). So `deserialize` can save those metadata on the `FieldRuntimeValue` instance for later use.
623
-
624
- #### `createValue`
625
-
626
- ```ts
627
- abstract createValue(
628
- options: Readonly<StructOptions>,
629
- context: StructSerializationContext,
630
- object: any,
631
- value: TValueType,
632
- ): FieldRuntimeValue<FieldDefinition<TOptions, TValueType, TRemoveInitFields>>;
633
- ```
634
-
635
- Similar to `deserialize`, creates a `FieldRuntimeValue` for this instance.
636
-
637
- The difference is `createValue` will be called when a init value was provided to create a Struct value.
638
-
639
- ### `FieldRuntimeValue`
640
-
641
- One `FieldDefinition` instance represents one field declaration, and one `FieldRuntimeValue` instance represents one value.
642
-
643
- It defines how to get, set, and serialize a value.
1
+ # @yume-chan/struct
2
+
3
+ ![license](https://img.shields.io/npm/l/@yume-chan/struct)
4
+ ![npm type definitions](https://img.shields.io/npm/types/@yume-chan/struct)
5
+ [![npm version](https://img.shields.io/npm/v/@yume-chan/struct)](https://www.npmjs.com/package/@yume-chan/struct)
6
+ ![npm bundle size](https://img.shields.io/bundlephobia/min/@yume-chan/struct)
7
+ ![Codecov](https://img.shields.io/codecov/c/github/yume-chan/ya-webadb?flag=struct&token=2fU3Cx2Edq)
8
+
9
+ A C-style structure serializer and deserializer. Written in TypeScript and highly takes advantage of its type system.
10
+
11
+ **WARNING:** The public API is UNSTABLE. If you have any questions, please open an issue.
12
+
13
+ ## Installation
14
+
15
+ ```sh
16
+ $ npm i @yume-chan/struct
17
+ ```
18
+
19
+ ## Quick Start
20
+
21
+ ```ts
22
+ import Struct from '@yume-chan/struct';
23
+
24
+ const MyStruct =
25
+ new Struct({ littleEndian: true })
26
+ .int8('foo')
27
+ .int64('bar')
28
+ .int32('bazLength')
29
+ .string('baz', { lengthField: 'bazLength' });
30
+
31
+ const value = await MyStruct.deserialize(stream);
32
+ value.foo // number
33
+ value.bar // bigint
34
+ value.bazLength // number
35
+ value.baz // string
36
+
37
+ const buffer = MyStruct.serialize({
38
+ foo: 42,
39
+ bar: 42n,
40
+ // `bazLength` automatically set to `baz.length`
41
+ baz: 'Hello, World!',
42
+ });
43
+ ```
44
+
45
+ - [Installation](#installation)
46
+ - [Quick Start](#quick-start)
47
+ - [Compatibility](#compatibility)
48
+ - [API](#api)
49
+ - [`placeholder`](#placeholder)
50
+ - [`Struct`](#struct)
51
+ - [`int8`/`uint8`/`int16`/`uint16`/`int32`/`uint32`](#int8uint8int16uint16int32uint32)
52
+ - [`int64`/`uint64`](#int64uint64)
53
+ - [`arraybuffer`/`uint8ClampedArray`/`string`](#arraybufferuint8clampedarraystring)
54
+ - [`fields`](#fields)
55
+ - [`extra`](#extra)
56
+ - [`postDeserialize`](#postdeserialize)
57
+ - [`deserialize`](#deserialize)
58
+ - [`serialize`](#serialize)
59
+ - [Custom field type](#custom-field-type)
60
+ - [`Struct#field`](#structfield)
61
+ - [`StructFieldDefinition`](#structfielddefinition)
62
+ - [`TValue`/`TOmitInitKey`](#tvaluetomitinitkey)
63
+ - [`getSize`](#getsize)
64
+ - [`create`](#create)
65
+ - [`deserialize`](#deserialize-1)
66
+ - [`StructFieldValue`](#structfieldvalue)
67
+ - [`getSize`](#getsize-1)
68
+ - [`get`/`set`](#getset)
69
+ - [`serialize`](#serialize-1)
70
+
71
+ ## Compatibility
72
+
73
+ | | Chrome | Edge | Firefox | Internet Explorer | Safari | Node.js |
74
+ | ---------------------------------------------------- | ------ | ---- | ------- | ----------------- | -------------- | ------- |
75
+ | [`Promise`][MDN_Promise] | 32 | 12 | 29 | No<sup>1</sup> | 8 | 0.12 |
76
+ | [`ArrayBuffer`][MDN_ArrayBuffer] | 7 | 12 | 4 | 10 | 5.1 | 0.10 |
77
+ | [`Uint8Array`][MDN_Uint8Array] | 7 | 12 | 4 | 10 | 5.1 | 0.10 |
78
+ | [`DataView`][MDN_DataView] | 9 | 12 | 15 | 10 | 5.1 | 0.10 |
79
+ | **Basic usage** | 32 | 12 | 29 | 10<sup>1</sup> | 8 | 0.12 |
80
+ | [`BigInt`][MDN_BigInt] | 67 | 79 | 68 | No | 14 | 10.4 |
81
+ | [`DataView#getBigUint64`][MDN_DataView_getBigUint64] | 67 | 79 | 68 | No | No<sup>2</sup> | 10.4 |
82
+ | [`DataView#setBigUint64`][MDN_DataView_setBigUint64] | 67 | 79 | 68 | No | No<sup>2</sup> | 10.4 |
83
+ | **Use [`int64/uint64`](#int64uint64) API** | 67 | 79 | 68 | No | 14<sup>2</sup> | 10.4 |
84
+
85
+ <sup>1</sup> Requires a polyfill for Promise (e.g. [promise-polyfill](https://www.npmjs.com/package/promise-polyfill))
86
+
87
+ <sup>2</sup> Requires a polyfill for `DataView#getBigUint64` and `DataView#setBigUint64`
88
+
89
+ [MDN_Promise]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
90
+ [MDN_ArrayBuffer]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer
91
+ [MDN_Uint8Array]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array
92
+ [MDN_DataView]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView
93
+ [MDN_BigInt]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt
94
+ [MDN_DataView_getBigUint64]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/getBigUint64
95
+ [MDN_DataView_setBigUint64]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/setBigUint64
96
+
97
+ ## API
98
+
99
+ ### `placeholder`
100
+
101
+ ```ts
102
+ function placeholder<T>(): T {
103
+ return undefined as unknown as T;
104
+ }
105
+ ```
106
+
107
+ Returns a (fake) value of the given type. It's only useful in TypeScript, if you are using JavaScript, you shouldn't care about it.
108
+
109
+ Many methods in this library have multiple generic parameters, but TypeScript only allows users to specify none (let TypeScript inference all of them from arguments), or all generic arguments. ([Microsoft/TypeScript#26242](https://github.com/microsoft/TypeScript/issues/26242))
110
+
111
+ <details>
112
+ <summary>Detail explanation (click to expand)</summary>
113
+
114
+ When you have a generic method, where half generic parameters can be inferred.
115
+
116
+ ```ts
117
+ declare function fn<A, B>(a: A): [A, B];
118
+ fn(42); // Expected 2 type arguments, but got 1. ts(2558)
119
+ ```
120
+
121
+ Rather than force users repeat the type `A`, I declare a parameter for `B`.
122
+
123
+ ```ts
124
+ declare function fn2<A, B>(a: A, b: B): [A, B];
125
+ ```
126
+
127
+ I don't really need a value of type `B`, I only require its type information
128
+
129
+ ```ts
130
+ fn2(42, placeholder<boolean>()) // fn2<number, boolean>
131
+ ```
132
+ </details>
133
+
134
+ To workaround this issue, these methods have an extra `_typescriptType` parameter, to let you specify a generic parameter, without passing all other generic arguments manually. The actual value of `_typescriptType` argument is never used, so you can pass any value, as long as it has the correct type, including values produced by this `placeholder` method.
135
+
136
+ **With that said, I don't expect you to specify any generic arguments manually when using this library.**
137
+
138
+ ### `Struct`
139
+
140
+ ```ts
141
+ class Struct<
142
+ TFields extends object = {},
143
+ TOmitInitKey extends string | number | symbol = never,
144
+ TExtra extends object = {},
145
+ TPostDeserialized = undefined
146
+ > {
147
+ public constructor(options: Partial<StructOptions> = StructDefaultOptions);
148
+ }
149
+ ```
150
+
151
+ Creates a new structure declaration.
152
+
153
+ <details>
154
+ <summary>Generic parameters (click to expand)</summary>
155
+
156
+ This information was added to help you understand how does it work. These are considered as "internal state" so don't specify them manually.
157
+
158
+ 1. `TFields`: Type of the Struct value. Modified when new fields are added.
159
+ 2. `TOmitInitKey`: When serializing a structure containing variable length arrays, the length field can be calculate from the array field, so they doesn't need to be provided explicitly.
160
+ 3. `TExtra`: Type of extra fields. Modified when `extra` is called.
161
+ 4. `TPostDeserialized`: State of the `postDeserialize` function. Modified when `postDeserialize` is called. Affects return type of `deserialize`
162
+ </details>
163
+
164
+ **Parameters**
165
+
166
+ 1. `options`:
167
+ * `littleEndian:boolean = false`: Whether all multi-byte fields in this struct are [little-endian encoded][Wikipeida_Endianess].
168
+
169
+ [Wikipeida_Endianess]: https://en.wikipedia.org/wiki/Endianness
170
+
171
+ #### `int8`/`uint8`/`int16`/`uint16`/`int32`/`uint32`
172
+
173
+ ```ts
174
+ int32<
175
+ TName extends string | number | symbol,
176
+ TTypeScriptType = number
177
+ >(
178
+ name: TName,
179
+ _typescriptType?: TTypeScriptType
180
+ ): Struct<
181
+ TFields & Record<TName, TTypeScriptType>,
182
+ TOmitInitKey,
183
+ TExtra,
184
+ TPostDeserialized
185
+ >;
186
+ ```
187
+
188
+ Appends an `int8`/`uint8`/`int16`/`uint16`/`int32`/`uint32` field to the `Struct`.
189
+
190
+ <details>
191
+ <summary>Generic parameters (click to expand)</summary>
192
+
193
+ 1. `TName`: Literal type of the field's name.
194
+ 2. `TTypeScriptType = number`: Type of the field in the result object. For example you can declare it as a number literal type, or some enum type.
195
+ </details>
196
+
197
+ **Parameters**
198
+
199
+ 1. `name`: (Required) Field name. Must have a [literal type](https://www.typescriptlang.org/docs/handbook/literal-types.html).
200
+ 2. `_typescriptType`: Set field's type. See examples below.
201
+
202
+ **Note**
203
+
204
+ There is no generic constraints on the `TTypeScriptType`, because TypeScript doesn't allow casting enum types to `number`.
205
+
206
+ So it's technically possible to pass in an incompatible type (e.g. `string`). But obviously, it's a bad idea.
207
+
208
+ **Examples**
209
+
210
+ 1. Append an `int32` field named `foo`
211
+
212
+ ```ts
213
+ const struct = new Struct()
214
+ .int32('foo');
215
+
216
+ const value = await struct.deserialize(stream);
217
+ value.foo; // number
218
+
219
+ struct.serialize({ }) // error: 'foo' is required
220
+ struct.serialize({ foo: 'bar' }) // error: 'foo' must be a number
221
+ struct.serialize({ foo: 42 }) // ok
222
+ ```
223
+
224
+ 2. Set fields' type (can be used with [`placeholder` method](#placeholder))
225
+
226
+ ```ts
227
+ enum MyEnum {
228
+ a,
229
+ b,
230
+ }
231
+
232
+ const struct = new Struct()
233
+ .int32('foo', placeholder<MyEnum>())
234
+ .int32('bar', MyEnum.a as const);
235
+
236
+ const value = await struct.deserialize(stream);
237
+ value.foo; // MyEnum
238
+ value.bar; // MyEnum.a
239
+
240
+ struct.serialize({ foo: 42, bar: MyEnum.a }); // error: 'foo' must be of type `MyEnum`
241
+ struct.serialize({ foo: MyEnum.a, bar: MyEnum.b }); // error: 'bar' must be of type `MyEnum.a`
242
+ struct.serialize({ foo: MyEnum.a, bar: MyEnum.b }); // ok
243
+ ```
244
+
245
+ #### `int64`/`uint64`
246
+
247
+ ```ts
248
+ int64<
249
+ TName extends string | number | symbol,
250
+ TTypeScriptType = bigint
251
+ >(
252
+ name: TName,
253
+ _typescriptType?: TTypeScriptType
254
+ ): Struct<
255
+ TFields & Record<TName, TTypeScriptType>,
256
+ TOmitInitKey,
257
+ TExtra,
258
+ TPostDeserialized
259
+ >;
260
+ ```
261
+
262
+ Appends an `int64`/`uint64` field to the `Struct`.
263
+
264
+ Requires native support for `BigInt`. Check [compatibility table](#compatibility) for more information.
265
+
266
+ #### `arraybuffer`/`uint8ClampedArray`/`string`
267
+
268
+ ```ts
269
+ arraybuffer<
270
+ TName extends string | number | symbol,
271
+ TTypeScriptType = ArrayBuffer
272
+ >(
273
+ name: TName,
274
+ options: FixedLengthArrayBufferLikeFieldOptions,
275
+ _typescriptType?: TTypeScriptType,
276
+ ): Struct<
277
+ TFields & Record<TName, TTypeScriptType>,
278
+ TOmitInitKey,
279
+ TExtra,
280
+ TPostDeserialized
281
+ >;
282
+
283
+ arraybuffer<
284
+ TName extends string | number | symbol,
285
+ TOptions extends VariableLengthArrayBufferLikeFieldOptions<TFields>,
286
+ TTypeScriptType = ArrayBuffer,
287
+ >(
288
+ name: TName,
289
+ options: TOptions,
290
+ _typescriptType?: TTypeScriptType,
291
+ ): Struct<
292
+ TFields & Record<TName, TTypeScriptType>,
293
+ TOmitInitKey | TOptions['lengthField'],
294
+ TExtra,
295
+ TPostDeserialized
296
+ >;
297
+ ```
298
+
299
+ Appends an `ArrayBuffer`/`Uint8ClampedArray`/`string` field to the `Struct`.
300
+
301
+ The `options` parameter defines its length, it can be in two formats:
302
+
303
+ * `{ length: number }`: Presence of the `length` option indicates that it's a fixed length array.
304
+ * `{ lengthField: string }`: Presence of the `lengthField` option indicates it's a variable length array. The `lengthField` options must refers to a `number` or `string` typed field that's already defined in this `Struct`. When deserializing, it will use that field's value as its length. And when serializing, it will write its length to that field.
305
+
306
+ All these three are actually deserialized to `ArrayBuffer`, then converted to `Uint8ClampedArray` or `string` for ease of use.
307
+
308
+ #### `fields`
309
+
310
+ ```ts
311
+ fields<
312
+ TOther extends Struct<any, any, any, any>
313
+ >(
314
+ other: TOther
315
+ ): Struct<
316
+ TFields & TOther['fieldsType'],
317
+ TOmitInitKey | TOther['omitInitType'],
318
+ TExtra & TOther['extraType'],
319
+ TPostDeserialized
320
+ >;
321
+ ```
322
+
323
+ Merges (flats) another `Struct`'s fields and extra fields into the current one.
324
+
325
+ **Examples**
326
+
327
+ 1. Extending another `Struct`
328
+
329
+ ```ts
330
+ const MyStructV1 =
331
+ new Struct()
332
+ .int32('field1');
333
+
334
+ const MyStructV2 =
335
+ new Struct()
336
+ .fields(MyStructV1)
337
+ .int32('field2');
338
+
339
+ const structV2 = await MyStructV2.deserialize(stream);
340
+ structV2.field1; // number
341
+ structV2.field2; // number
342
+ // Fields are flatten
343
+ ```
344
+
345
+ 2. Also possible in any order
346
+
347
+ ```ts
348
+ const MyStructV1 =
349
+ new Struct()
350
+ .int32('field1');
351
+
352
+ const MyStructV2 =
353
+ new Struct()
354
+ .int32('field2')
355
+ .fields(MyStructV1);
356
+
357
+ const structV2 = await MyStructV2.deserialize(stream);
358
+ structV2.field1; // number
359
+ structV2.field2; // number
360
+ // Same result as above, but serialize/deserialize order is reversed
361
+ ```
362
+
363
+ #### `extra`
364
+
365
+ ```ts
366
+ extra<
367
+ T extends Record<
368
+ Exclude<
369
+ keyof T,
370
+ Exclude<
371
+ keyof T,
372
+ keyof TFields
373
+ >
374
+ >,
375
+ never
376
+ >
377
+ >(
378
+ value: T & ThisType<Overwrite<Overwrite<TExtra, T>, TFields>>
379
+ ): Struct<
380
+ TFields,
381
+ TInit,
382
+ Overwrite<TExtra, T>,
383
+ TPostDeserialized
384
+ >;
385
+ ```
386
+
387
+ Adds extra fields into the `Struct`. Extra fields will be defined on prototype of each Struct values, so they don't affect serialize and deserialize process, and deserialized fields will overwrite extra fields.
388
+
389
+ Multiple calls merge all extra fields together.
390
+
391
+ **Generic Parameters**
392
+
393
+ 1. `T`: Type of the extra fields. The scary looking generic constraint is used to forbid overwriting any already existed fields.
394
+
395
+ **Parameters**
396
+
397
+ 1. `value`: An object containing anything you want to add to Struct values. Accessors and methods are also allowed.
398
+
399
+ **Examples**
400
+
401
+ 1. Add an extra field
402
+
403
+ ```ts
404
+ const struct = new Struct()
405
+ .int32('foo')
406
+ .extra({
407
+ bar: 'hello',
408
+ });
409
+
410
+ const value = await struct.deserialize(stream);
411
+ value.foo; // number
412
+ value.bar; // 'hello'
413
+
414
+ struct.serialize({ foo: 42 }); // ok
415
+ struct.serialize({ foo: 42, bar: 'hello' }); // error: 'bar' is redundant
416
+ ```
417
+
418
+ 2. Add getters and methods. `this` in functions refers to the result object.
419
+
420
+ ```ts
421
+ const struct = new Struct()
422
+ .int32('foo')
423
+ .extra({
424
+ get bar() {
425
+ // `this` is the result Struct value
426
+ return this.foo + 1;
427
+ },
428
+ logBar() {
429
+ // `this` also contains other extra fields
430
+ console.log(this.bar);
431
+ },
432
+ });
433
+
434
+ const value = await struct.deserialize(stream);
435
+ value.foo; // number
436
+ value.bar; // number
437
+ value.logBar();
438
+ ```
439
+
440
+ #### `postDeserialize`
441
+
442
+ ```ts
443
+ postDeserialize(): Struct<TFields, TOmitInitKey, TExtra, undefined>;
444
+ ```
445
+
446
+ Remove any registered post deserialization callback.
447
+
448
+ ```ts
449
+ postDeserialize(
450
+ callback: (this: TFields, object: TFields) => never
451
+ ): Struct<TFields, TOmitInitKey, TExtra, never>;
452
+ postDeserialize(
453
+ callback: (this: TFields, object: TFields) => void
454
+ ): Struct<TFields, TOmitInitKey, TExtra, undefined>;
455
+ ```
456
+
457
+ Registers (or replaces) a custom callback to be run after deserialized.
458
+
459
+ `this` in `callback`, along with the first parameter `object` will both be the deserialized Struct value.
460
+
461
+ A callback returning `never` (always throws errors) will change the return type of `deserialize` to `never`.
462
+
463
+ A callback returning `void` means it modify the result object in-place (or doesn't modify it at all), so `deserialize` will still return the result object.
464
+
465
+ ```ts
466
+ postDeserialize<TPostSerialize>(
467
+ callback: (this: TFields, object: TFields) => TPostSerialize
468
+ ): Struct<TFields, TOmitInitKey, TExtra, TPostSerialize>;
469
+ ```
470
+
471
+ Registers (or replaces) a custom callback to be run after deserialized.
472
+
473
+ A callback returning anything other than `undefined` will cause `deserialize` to return that value instead.
474
+
475
+ **Generic Parameters**
476
+
477
+ 1. `TPostSerialize`: Type of the new result.
478
+
479
+ **Parameters**
480
+
481
+ 1. `callback`: An function contains the custom logic to be run, optionally returns a new result. Or `undefined`, to remove any previously set `postDeserialize` callback.
482
+
483
+ **Examples**
484
+
485
+ 1. Handle an "error" packet
486
+
487
+ ```ts
488
+ // Say your protocol have an error packet,
489
+ // You want to throw a JavaScript Error when received such a packet,
490
+ // But you don't want to modify all receiving path
491
+
492
+ const struct = new Struct()
493
+ .int32('messageLength')
494
+ .string('message', { lengthField: 'messageLength' })
495
+ .postDeserialize(value => {
496
+ throw new Error(value.message);
497
+ });
498
+ ```
499
+
500
+ 2. Do anything you want
501
+
502
+ ```ts
503
+ // I think this one doesn't need any code example
504
+ ```
505
+
506
+ 3. Replace result object
507
+
508
+ ```ts
509
+ const struct1 = new Struct()
510
+ .int32('foo')
511
+ .postDeserialize(value => {
512
+ return {
513
+ bar: value.foo,
514
+ };
515
+ });
516
+
517
+ const value = await struct.deserialize(stream);
518
+ value.foo // error: not exist
519
+ value.bar; // number
520
+ ```
521
+
522
+ #### `deserialize`
523
+
524
+ ```ts
525
+ interface StructDeserializeStream {
526
+ /**
527
+ * Read data from the underlying data source.
528
+ *
529
+ * Stream must return exactly `length` bytes or data. If that's not possible
530
+ * (due to end of file or other error condition), it must throw an error.
531
+ */
532
+ read(length: number): ArrayBuffer;
533
+ }
534
+
535
+ deserialize(
536
+ stream: StructDeserializeStream,
537
+ ): TPostDeserialized extends undefined
538
+ ? Overwrite<TExtra, TValue>
539
+ : TPostDeserialized
540
+ >;
541
+ deserialize(
542
+ stream: StructAsyncDeserializeStream,
543
+ ): Promise<
544
+ TPostDeserialized extends undefined
545
+ ? Overwrite<TExtra, TValue>
546
+ : TPostDeserialized
547
+ >
548
+ >;
549
+ ```
550
+
551
+ Deserialize a Struct value from `stream`.
552
+
553
+ As the signature shows, if the `postDeserialize` callback returns any value, `deserialize` will return that value instead.
554
+
555
+ The `read` method of `stream`, when being called, should returns exactly `length` bytes of data (or throw an `Error` if it can't).
556
+
557
+ #### `serialize`
558
+
559
+ ```ts
560
+ serialize(
561
+ init: Omit<TFields, TOmitInitKey>
562
+ ): ArrayBuffer;
563
+ ```
564
+
565
+ Serialize a Struct value into an `ArrayBuffer`.
566
+
567
+ ## Custom field type
568
+
569
+ This library supports adding fields of user defined types.
570
+
571
+ ### `Struct#field`
572
+
573
+ ```ts
574
+ field<
575
+ TName extends string | number | symbol,
576
+ TDefinition extends StructFieldDefinition<any, any, any>
577
+ >(
578
+ name: TName,
579
+ definition: TDefinition
580
+ ): Struct<
581
+ TFields & Record<TName, TDefinition['TValue']>,
582
+ TOmitInitKey | TDefinition['TOmitInitKey'],
583
+ TExtra,
584
+ TPostDeserialized
585
+ >;
586
+ ```
587
+
588
+ Appends a `StructFieldDefinition` to the `Struct`.
589
+
590
+ Actually, all built-in field type methods are aliases of `field`. For example, calling
591
+
592
+ ```ts
593
+ struct.int8('foo')
594
+ ```
595
+
596
+ is same as
597
+
598
+ ```ts
599
+ struct.field(
600
+ 'foo',
601
+ new NumberFieldDefinition(
602
+ NumberFieldType.Int8
603
+ )
604
+ )
605
+ ```
606
+
607
+
608
+ ### `StructFieldDefinition`
609
+
610
+ ```ts
611
+ abstract class StructFieldDefinition<
612
+ TOptions = void,
613
+ TValue = unknown,
614
+ TOmitInitKey extends PropertyKey = never,
615
+ > {
616
+ public readonly options: TOptions;
617
+
618
+ public constructor(options: TOptions);
619
+ }
620
+ ```
621
+
622
+ A `StructFieldDefinition` describes type, size and runtime semantics of a field.
623
+
624
+ It's an `abstract` class, means it lacks some method implementations, so it shouldn't be constructed.
625
+
626
+ #### `TValue`/`TOmitInitKey`
627
+
628
+ These two fields are used to provide type information to TypeScript. Their values will always be `undefined`, but having correct types is enough. You don't need to care about them.
629
+
630
+ #### `getSize`
631
+
632
+ ```ts
633
+ abstract getSize(): number;
634
+ ```
635
+
636
+ Derived classes must implement this method to return size (or minimal size if it's dynamic) of this field.
637
+
638
+ Actual size should be returned from `StructFieldValue#getSize`
639
+
640
+ #### `create`
641
+
642
+ ```ts
643
+ abstract create(
644
+ options: Readonly<StructOptions>,
645
+ struct: StructValue,
646
+ value: TValue,
647
+ ): StructFieldValue<this>;
648
+ ```
649
+
650
+ Derived classes must implement this method to create its own field value instance for the current definition.
651
+
652
+ `Struct#serialize` will call this method, then call `StructFieldValue#serialize` to serialize one field value.
653
+
654
+ #### `deserialize`
655
+
656
+ ```ts
657
+ abstract deserialize(
658
+ options: Readonly<StructOptions>,
659
+ stream: StructDeserializeStream,
660
+ struct: StructValue,
661
+ ): StructFieldValue<this>;
662
+ abstract deserialize(
663
+ options: Readonly<StructOptions>,
664
+ stream: StructAsyncDeserializeStream,
665
+ struct: StructValue,
666
+ ): Promise<StructFieldValue<this>>;
667
+ ```
668
+
669
+ Derived classes must implement this method to define how to deserialize a value from `stream`. Can also return a `Promise`.
670
+
671
+ Usually implementations should be:
672
+
673
+ 1. Somehow parse the value from `stream`
674
+ 2. Pass the value into its `create` method
675
+
676
+ Sometimes, some metadata is present when deserializing, but need to be calculated when serializing, for example a UTF-8 encoded string may have different length between itself (character count) and serialized form (byte length). So `deserialize` can save those metadata on the `StructFieldValue` instance for later use.
677
+
678
+ ### `StructFieldValue`
679
+
680
+ ```ts
681
+ abstract class StructFieldValue<
682
+ TDefinition extends StructFieldDefinition<any, any, any>
683
+ >
684
+ ```
685
+
686
+ To define a custom type, one must create their own `StructFieldValue` type to define the runtime semantics.
687
+
688
+ Each `StructFieldValue` is linked to a `StructFieldDefinition`.
689
+
690
+ #### `getSize`
691
+
692
+ ```ts
693
+ getSize(): number;
694
+ ```
695
+
696
+ Gets size of this field. By default, it returns its `definition`'s size.
697
+
698
+ If this field's size can change based on some criteria, one must override `getSize` to return its actual size.
699
+
700
+ #### `get`/`set`
701
+
702
+ ```ts
703
+ get(): TDefinition['TValue'];
704
+ set(value: TDefinition['TValue']): void;
705
+ ```
706
+
707
+ Defines how to get or set this field's value. By default, it store its value in `value` field.
708
+
709
+ If one needs to manipulate other states when getting/setting values, they can override these methods.
710
+
711
+ #### `serialize`
712
+
713
+ ```ts
714
+ abstract serialize(
715
+ dataView: DataView,
716
+ offset: number
717
+ ): void;
718
+ ```
719
+
720
+ Derived classes must implement this method to serialize current value into `dataView`, from `offset`. It must not write more bytes than what its `getSize` returned.