struct.ts 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631
  1. import { StructAsyncDeserializeStream, StructDefaultOptions, StructDeserializeStream, StructFieldDefinition, StructFieldValue, StructOptions, StructValue, STRUCT_VALUE_SYMBOL } from './basic/index.js';
  2. import { SyncPromise } from "./sync-promise.js";
  3. import { BigIntFieldDefinition, BigIntFieldType, BufferFieldSubType, FixedLengthBufferLikeFieldDefinition, NumberFieldDefinition, NumberFieldType, StringBufferFieldSubType, Uint8ArrayBufferFieldSubType, VariableLengthBufferLikeFieldDefinition, type FixedLengthBufferLikeFieldOptions, type LengthField, type VariableLengthBufferLikeFieldOptions } from './types/index.js';
  4. import type { Evaluate, Identity, Overwrite, ValueOrPromise } from "./utils.js";
  5. export interface StructLike<TValue> {
  6. deserialize(stream: StructDeserializeStream | StructAsyncDeserializeStream): Promise<TValue>;
  7. }
  8. /**
  9. * Extract the value type of the specified `Struct`
  10. */
  11. export type StructValueType<T extends StructLike<any>> =
  12. Awaited<ReturnType<T['deserialize']>>;
  13. /**
  14. * Create a new `Struct` type with `TDefinition` appended
  15. */
  16. type AddFieldDescriptor<
  17. TFields extends object,
  18. TOmitInitKey extends PropertyKey,
  19. TExtra extends object,
  20. TPostDeserialized,
  21. TFieldName extends PropertyKey,
  22. TDefinition extends StructFieldDefinition<any, any, any>
  23. > =
  24. Identity<Struct<
  25. // Merge two types
  26. // Evaluate immediately to optimize editor hover tooltip
  27. Evaluate<TFields & Record<TFieldName, TDefinition['TValue']>>,
  28. // Merge two `TOmitInitKey`s
  29. TOmitInitKey | TDefinition['TOmitInitKey'],
  30. TExtra,
  31. TPostDeserialized
  32. >>;
  33. /**
  34. * Overload methods to add an array buffer like field
  35. */
  36. interface ArrayBufferLikeFieldCreator<
  37. TFields extends object,
  38. TOmitInitKey extends PropertyKey,
  39. TExtra extends object,
  40. TPostDeserialized
  41. > {
  42. /**
  43. * Append a fixed-length array buffer like field to the `Struct`
  44. *
  45. * @param name Name of the field
  46. * @param type `Array.SubType.ArrayBuffer` or `Array.SubType.String`
  47. * @param options Fixed-length array options
  48. * @param typeScriptType Type of the field in TypeScript.
  49. * For example, if this field is a string, you can declare it as a string enum or literal union.
  50. */
  51. <
  52. TName extends PropertyKey,
  53. TType extends BufferFieldSubType<any, any>,
  54. TTypeScriptType = TType['TTypeScriptType'],
  55. >(
  56. name: TName,
  57. type: TType,
  58. options: FixedLengthBufferLikeFieldOptions,
  59. typeScriptType?: TTypeScriptType,
  60. ): AddFieldDescriptor<
  61. TFields,
  62. TOmitInitKey,
  63. TExtra,
  64. TPostDeserialized,
  65. TName,
  66. FixedLengthBufferLikeFieldDefinition<
  67. TType,
  68. FixedLengthBufferLikeFieldOptions
  69. >
  70. >;
  71. /**
  72. * Append a variable-length array buffer like field to the `Struct`
  73. */
  74. <
  75. TName extends PropertyKey,
  76. TType extends BufferFieldSubType<any, any>,
  77. TOptions extends VariableLengthBufferLikeFieldOptions<TFields>,
  78. TTypeScriptType = TType['TTypeScriptType'],
  79. >(
  80. name: TName,
  81. type: TType,
  82. options: TOptions,
  83. typeScriptType?: TTypeScriptType,
  84. ): AddFieldDescriptor<
  85. TFields,
  86. TOmitInitKey,
  87. TExtra,
  88. TPostDeserialized,
  89. TName,
  90. VariableLengthBufferLikeFieldDefinition<
  91. TType,
  92. TOptions
  93. >
  94. >;
  95. }
  96. /**
  97. * Similar to `ArrayBufferLikeFieldCreator`, but bind to `TType`
  98. */
  99. interface BoundArrayBufferLikeFieldDefinitionCreator<
  100. TFields extends object,
  101. TOmitInitKey extends PropertyKey,
  102. TExtra extends object,
  103. TPostDeserialized,
  104. TType extends BufferFieldSubType<any, any>
  105. > {
  106. <
  107. TName extends PropertyKey,
  108. TTypeScriptType = TType['TTypeScriptType'],
  109. >(
  110. name: TName,
  111. options: FixedLengthBufferLikeFieldOptions,
  112. typeScriptType?: TTypeScriptType,
  113. ): AddFieldDescriptor<
  114. TFields,
  115. TOmitInitKey,
  116. TExtra,
  117. TPostDeserialized,
  118. TName,
  119. FixedLengthBufferLikeFieldDefinition<
  120. TType,
  121. FixedLengthBufferLikeFieldOptions,
  122. TTypeScriptType
  123. >
  124. >;
  125. <
  126. TName extends PropertyKey,
  127. TLengthField extends LengthField<TFields>,
  128. TOptions extends VariableLengthBufferLikeFieldOptions<TFields, TLengthField>,
  129. TTypeScriptType = TType['TTypeScriptType'],
  130. >(
  131. name: TName,
  132. options: TOptions,
  133. typeScriptType?: TTypeScriptType,
  134. ): AddFieldDescriptor<
  135. TFields,
  136. TOmitInitKey,
  137. TExtra,
  138. TPostDeserialized,
  139. TName,
  140. VariableLengthBufferLikeFieldDefinition<
  141. TType,
  142. TOptions,
  143. TTypeScriptType
  144. >
  145. >;
  146. }
  147. export type StructPostDeserialized<TFields, TPostDeserialized> =
  148. (this: TFields, object: TFields) => TPostDeserialized;
  149. export type StructDeserializedResult<TFields extends object, TExtra extends object, TPostDeserialized> =
  150. TPostDeserialized extends undefined ? Overwrite<TExtra, TFields> : TPostDeserialized;
  151. export class Struct<
  152. TFields extends object = {},
  153. TOmitInitKey extends PropertyKey = never,
  154. TExtra extends object = {},
  155. TPostDeserialized = undefined,
  156. > implements StructLike<StructDeserializedResult<TFields, TExtra, TPostDeserialized>>{
  157. public readonly TFields!: TFields;
  158. public readonly TOmitInitKey!: TOmitInitKey;
  159. public readonly TExtra!: TExtra;
  160. public readonly TInit!: Evaluate<Omit<TFields, TOmitInitKey>>;
  161. public readonly TDeserializeResult!: StructDeserializedResult<TFields, TExtra, TPostDeserialized>;
  162. public readonly options: Readonly<StructOptions>;
  163. private _size = 0;
  164. /**
  165. * Gets the static size (exclude fields that can change size at runtime)
  166. */
  167. public get size() { return this._size; }
  168. private _fields: [name: PropertyKey, definition: StructFieldDefinition<any, any, any>][] = [];
  169. private _extra: Record<PropertyKey, unknown> = {};
  170. private _postDeserialized?: StructPostDeserialized<any, any> | undefined;
  171. public constructor(options?: Partial<Readonly<StructOptions>>) {
  172. this.options = { ...StructDefaultOptions, ...options };
  173. }
  174. /**
  175. * Appends a `StructFieldDefinition` to the `Struct
  176. */
  177. public field<
  178. TName extends PropertyKey,
  179. TDefinition extends StructFieldDefinition<any, any, any>
  180. >(
  181. name: TName,
  182. definition: TDefinition,
  183. ): AddFieldDescriptor<
  184. TFields,
  185. TOmitInitKey,
  186. TExtra,
  187. TPostDeserialized,
  188. TName,
  189. TDefinition
  190. > {
  191. for (const field of this._fields) {
  192. if (field[0] === name) {
  193. throw new Error(`This struct already have a field with name '${String(name)}'`);
  194. }
  195. }
  196. this._fields.push([name, definition]);
  197. const size = definition.getSize();
  198. this._size += size;
  199. // Force cast `this` to another type
  200. return this as any;
  201. }
  202. /**
  203. * Merges (flats) another `Struct`'s fields and extra fields into this one.
  204. */
  205. public fields<TOther extends Struct<any, any, any, any>>(
  206. other: TOther
  207. ): Struct<
  208. TFields & TOther['TFields'],
  209. TOmitInitKey | TOther['TOmitInitKey'],
  210. TExtra & TOther['TExtra'],
  211. TPostDeserialized
  212. > {
  213. for (const field of other._fields) {
  214. this._fields.push(field);
  215. }
  216. this._size += other._size;
  217. Object.defineProperties(this._extra, Object.getOwnPropertyDescriptors(other._extra));
  218. return this as any;
  219. }
  220. private number<
  221. TName extends PropertyKey,
  222. TType extends NumberFieldType = NumberFieldType,
  223. TTypeScriptType = TType['TTypeScriptType']
  224. >(
  225. name: TName,
  226. type: TType,
  227. typeScriptType?: TTypeScriptType,
  228. ) {
  229. return this.field(
  230. name,
  231. new NumberFieldDefinition(type, typeScriptType),
  232. );
  233. }
  234. /**
  235. * Appends an `int8` field to the `Struct`
  236. */
  237. public int8<
  238. TName extends PropertyKey,
  239. TTypeScriptType = (typeof NumberFieldType)['Uint8']['TTypeScriptType']
  240. >(
  241. name: TName,
  242. typeScriptType?: TTypeScriptType,
  243. ) {
  244. return this.number(
  245. name,
  246. NumberFieldType.Int8,
  247. typeScriptType
  248. );
  249. }
  250. /**
  251. * Appends an `uint8` field to the `Struct`
  252. */
  253. public uint8<
  254. TName extends PropertyKey,
  255. TTypeScriptType = (typeof NumberFieldType)['Uint8']['TTypeScriptType']
  256. >(
  257. name: TName,
  258. typeScriptType?: TTypeScriptType,
  259. ) {
  260. return this.number(
  261. name,
  262. NumberFieldType.Uint8,
  263. typeScriptType
  264. );
  265. }
  266. /**
  267. * Appends an `int16` field to the `Struct`
  268. */
  269. public int16<
  270. TName extends PropertyKey,
  271. TTypeScriptType = (typeof NumberFieldType)['Uint16']['TTypeScriptType']
  272. >(
  273. name: TName,
  274. typeScriptType?: TTypeScriptType,
  275. ) {
  276. return this.number(
  277. name,
  278. NumberFieldType.Int16,
  279. typeScriptType
  280. );
  281. }
  282. /**
  283. * Appends an `uint16` field to the `Struct`
  284. */
  285. public uint16<
  286. TName extends PropertyKey,
  287. TTypeScriptType = (typeof NumberFieldType)['Uint16']['TTypeScriptType']
  288. >(
  289. name: TName,
  290. typeScriptType?: TTypeScriptType,
  291. ) {
  292. return this.number(
  293. name,
  294. NumberFieldType.Uint16,
  295. typeScriptType
  296. );
  297. }
  298. /**
  299. * Appends an `int32` field to the `Struct`
  300. */
  301. public int32<
  302. TName extends PropertyKey,
  303. TTypeScriptType = (typeof NumberFieldType)['Int32']['TTypeScriptType']
  304. >(
  305. name: TName,
  306. typeScriptType?: TTypeScriptType,
  307. ) {
  308. return this.number(
  309. name,
  310. NumberFieldType.Int32,
  311. typeScriptType
  312. );
  313. }
  314. /**
  315. * Appends an `uint32` field to the `Struct`
  316. */
  317. public uint32<
  318. TName extends PropertyKey,
  319. TTypeScriptType = (typeof NumberFieldType)['Uint32']['TTypeScriptType']
  320. >(
  321. name: TName,
  322. typeScriptType?: TTypeScriptType,
  323. ) {
  324. return this.number(
  325. name,
  326. NumberFieldType.Uint32,
  327. typeScriptType
  328. );
  329. }
  330. private bigint<
  331. TName extends PropertyKey,
  332. TType extends BigIntFieldType = BigIntFieldType,
  333. TTypeScriptType = TType['TTypeScriptType']
  334. >(
  335. name: TName,
  336. type: TType,
  337. typeScriptType?: TTypeScriptType,
  338. ) {
  339. return this.field(
  340. name,
  341. new BigIntFieldDefinition(type, typeScriptType),
  342. );
  343. }
  344. /**
  345. * Appends an `int64` field to the `Struct`
  346. *
  347. * Requires native `BigInt` support
  348. */
  349. public int64<
  350. TName extends PropertyKey,
  351. TTypeScriptType = BigIntFieldType['TTypeScriptType']
  352. >(
  353. name: TName,
  354. typeScriptType?: TTypeScriptType,
  355. ) {
  356. return this.bigint(
  357. name,
  358. BigIntFieldType.Int64,
  359. typeScriptType
  360. );
  361. }
  362. /**
  363. * Appends an `uint64` field to the `Struct`
  364. *
  365. * Requires native `BigInt` support
  366. */
  367. public uint64<
  368. TName extends PropertyKey,
  369. TTypeScriptType = BigIntFieldType['TTypeScriptType']
  370. >(
  371. name: TName,
  372. typeScriptType?: TTypeScriptType,
  373. ) {
  374. return this.bigint(
  375. name,
  376. BigIntFieldType.Uint64,
  377. typeScriptType
  378. );
  379. }
  380. private arrayBufferLike: ArrayBufferLikeFieldCreator<
  381. TFields,
  382. TOmitInitKey,
  383. TExtra,
  384. TPostDeserialized
  385. > = (
  386. name: PropertyKey,
  387. type: BufferFieldSubType,
  388. options: FixedLengthBufferLikeFieldOptions | VariableLengthBufferLikeFieldOptions
  389. ): any => {
  390. if ('length' in options) {
  391. return this.field(
  392. name,
  393. new FixedLengthBufferLikeFieldDefinition(type, options),
  394. );
  395. } else {
  396. return this.field(
  397. name,
  398. new VariableLengthBufferLikeFieldDefinition(type, options),
  399. );
  400. }
  401. };
  402. public uint8Array: BoundArrayBufferLikeFieldDefinitionCreator<
  403. TFields,
  404. TOmitInitKey,
  405. TExtra,
  406. TPostDeserialized,
  407. Uint8ArrayBufferFieldSubType
  408. > = (
  409. name: PropertyKey,
  410. options: any,
  411. typeScriptType: any,
  412. ): any => {
  413. return this.arrayBufferLike(name, Uint8ArrayBufferFieldSubType.Instance, options, typeScriptType);
  414. };
  415. public string: BoundArrayBufferLikeFieldDefinitionCreator<
  416. TFields,
  417. TOmitInitKey,
  418. TExtra,
  419. TPostDeserialized,
  420. StringBufferFieldSubType
  421. > = (
  422. name: PropertyKey,
  423. options: any,
  424. typeScriptType: any,
  425. ): any => {
  426. return this.arrayBufferLike(name, StringBufferFieldSubType.Instance, options, typeScriptType);
  427. };
  428. /**
  429. * Adds some extra properties into every `Struct` value.
  430. *
  431. * Extra properties will not affect serialize or deserialize process.
  432. *
  433. * Multiple calls to `extra` will merge all properties together.
  434. *
  435. * @param value
  436. * An object containing properties to be added to the result value. Accessors and methods are also allowed.
  437. */
  438. public extra<T extends Record<
  439. // This trick disallows any keys that are already in `TValue`
  440. Exclude<
  441. keyof T,
  442. Exclude<keyof T, keyof TFields>
  443. >,
  444. never
  445. >>(
  446. value: T & ThisType<Overwrite<Overwrite<TExtra, T>, TFields>>
  447. ): Struct<
  448. TFields,
  449. TOmitInitKey,
  450. Overwrite<TExtra, T>,
  451. TPostDeserialized
  452. > {
  453. Object.defineProperties(
  454. this._extra,
  455. Object.getOwnPropertyDescriptors(value)
  456. );
  457. return this as any;
  458. }
  459. /**
  460. * Registers (or replaces) a custom callback to be run after deserialized.
  461. *
  462. * A callback returning `never` (always throw an error)
  463. * will also change the return type of `deserialize` to `never`.
  464. */
  465. public postDeserialize(
  466. callback: StructPostDeserialized<TFields, never>
  467. ): Struct<TFields, TOmitInitKey, TExtra, never>;
  468. /**
  469. * Registers (or replaces) a custom callback to be run after deserialized.
  470. *
  471. * A callback returning `void` means it modify the result object in-place
  472. * (or doesn't modify it at all), so `deserialize` will still return the result object.
  473. */
  474. public postDeserialize(
  475. callback?: StructPostDeserialized<TFields, void>
  476. ): Struct<TFields, TOmitInitKey, TExtra, undefined>;
  477. /**
  478. * Registers (or replaces) a custom callback to be run after deserialized.
  479. *
  480. * A callback returning anything other than `undefined`
  481. * will `deserialize` to return that object instead.
  482. */
  483. public postDeserialize<TPostSerialize>(
  484. callback?: StructPostDeserialized<TFields, TPostSerialize>
  485. ): Struct<TFields, TOmitInitKey, TExtra, TPostSerialize>;
  486. public postDeserialize(
  487. callback?: StructPostDeserialized<TFields, any>
  488. ) {
  489. this._postDeserialized = callback;
  490. return this as any;
  491. }
  492. /**
  493. * Deserialize a struct value from `stream`.
  494. */
  495. public deserialize(
  496. stream: StructDeserializeStream,
  497. ): StructDeserializedResult<TFields, TExtra, TPostDeserialized>;
  498. public deserialize(
  499. stream: StructAsyncDeserializeStream,
  500. ): Promise<StructDeserializedResult<TFields, TExtra, TPostDeserialized>>;
  501. public deserialize(
  502. stream: StructDeserializeStream | StructAsyncDeserializeStream,
  503. ): ValueOrPromise<StructDeserializedResult<TFields, TExtra, TPostDeserialized>> {
  504. const structValue = new StructValue(this._extra);
  505. let promise = SyncPromise.resolve();
  506. for (const [name, definition] of this._fields) {
  507. promise = promise
  508. .then(() =>
  509. definition.deserialize(this.options, stream as any, structValue)
  510. )
  511. .then(fieldValue => {
  512. structValue.set(name, fieldValue);
  513. });
  514. }
  515. return promise
  516. .then(() => {
  517. const object = structValue.value;
  518. // Run `postDeserialized`
  519. if (this._postDeserialized) {
  520. const override = this._postDeserialized.call(object, object);
  521. // If it returns a new value, use that as result
  522. // Otherwise it only inspects/mutates the object in place.
  523. if (override !== undefined) {
  524. return override;
  525. }
  526. }
  527. return object;
  528. })
  529. .valueOrPromise();
  530. }
  531. public serialize(init: Evaluate<Omit<TFields, TOmitInitKey>>): Uint8Array;
  532. public serialize(init: Evaluate<Omit<TFields, TOmitInitKey>>, output: Uint8Array): number;
  533. public serialize(init: Evaluate<Omit<TFields, TOmitInitKey>>, output?: Uint8Array): Uint8Array | number {
  534. let structValue: StructValue;
  535. if (STRUCT_VALUE_SYMBOL in init) {
  536. structValue = (init as any)[STRUCT_VALUE_SYMBOL];
  537. for (const [key, value] of Object.entries(init)) {
  538. const fieldValue = structValue.get(key);
  539. if (fieldValue) {
  540. fieldValue.set(value);
  541. }
  542. }
  543. } else {
  544. structValue = new StructValue({});
  545. for (const [name, definition] of this._fields) {
  546. const fieldValue = definition.create(
  547. this.options,
  548. structValue,
  549. (init as any)[name]
  550. );
  551. structValue.set(name, fieldValue);
  552. }
  553. }
  554. let structSize = 0;
  555. const fieldsInfo: { fieldValue: StructFieldValue, size: number; }[] = [];
  556. for (const [name] of this._fields) {
  557. const fieldValue = structValue.get(name);
  558. const size = fieldValue.getSize();
  559. fieldsInfo.push({ fieldValue, size });
  560. structSize += size;
  561. }
  562. let outputType = 'number';
  563. if (!output) {
  564. output = new Uint8Array(structSize);
  565. outputType = 'Uint8Array';
  566. }
  567. const dataView = new DataView(output.buffer, output.byteOffset, output.byteLength);
  568. let offset = 0;
  569. for (const { fieldValue, size } of fieldsInfo) {
  570. fieldValue.serialize(dataView, offset);
  571. offset += size;
  572. }
  573. if (outputType === 'number') {
  574. return structSize;
  575. } else {
  576. return output;
  577. }
  578. }
  579. }