@yume-chan/struct 0.0.13 → 0.0.16

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