@ckb-ccc/core 1.22.0 → 1.23.0

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 (52) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/dist/advanced.d.mts +1 -1
  3. package/dist/advanced.mjs +1 -1
  4. package/dist/{advancedBarrel-Daj_LJkz.d.mts → advancedBarrel-BBbQZ2OW.d.mts} +480 -379
  5. package/dist/advancedBarrel-BBbQZ2OW.d.mts.map +1 -0
  6. package/dist/advancedBarrel.d.mts +1 -1
  7. package/dist/advancedBarrel.mjs +1 -1
  8. package/dist/barrel-Dz1WLXRX.mjs +7 -0
  9. package/dist/barrel-Dz1WLXRX.mjs.map +1 -0
  10. package/dist/barrel.d.mts +2 -2
  11. package/dist/barrel.mjs +1 -1
  12. package/dist/index.d.mts +2 -2
  13. package/dist/index.mjs +1 -1
  14. package/dist/transformers-BHt442Nd.mjs +2 -0
  15. package/dist/transformers-BHt442Nd.mjs.map +1 -0
  16. package/dist.commonjs/advanced.d.ts +1 -1
  17. package/dist.commonjs/advanced.js +1 -1
  18. package/dist.commonjs/{advancedBarrel-CJL3rfuW.d.ts → advancedBarrel-BS4zFcFI.d.ts} +480 -379
  19. package/dist.commonjs/advancedBarrel-BS4zFcFI.d.ts.map +1 -0
  20. package/dist.commonjs/advancedBarrel.d.ts +1 -1
  21. package/dist.commonjs/advancedBarrel.js +1 -1
  22. package/dist.commonjs/{barrel-DZkApbAR.js → barrel-CZYID7jh.js} +10 -10
  23. package/dist.commonjs/barrel-CZYID7jh.js.map +1 -0
  24. package/dist.commonjs/barrel.d.ts +2 -2
  25. package/dist.commonjs/barrel.js +1 -1
  26. package/dist.commonjs/index.d.ts +2 -2
  27. package/dist.commonjs/index.js +1 -1
  28. package/dist.commonjs/transformers-CYUULrnt.js +2 -0
  29. package/dist.commonjs/transformers-CYUULrnt.js.map +1 -0
  30. package/package.json +1 -1
  31. package/src/address/index.ts +32 -4
  32. package/src/ckb/transaction.ts +79 -31
  33. package/src/client/cache/memory.advanced.ts +18 -3
  34. package/src/client/client.ts +41 -2
  35. package/src/client/clientPublicMainnet.ts +9 -2
  36. package/src/client/clientPublicTestnet.ts +9 -2
  37. package/src/client/jsonRpc/client.ts +8 -8
  38. package/src/codec/codec.ts +51 -27
  39. package/src/codec/entity.ts +26 -16
  40. package/src/fixedPoint/index.ts +17 -8
  41. package/src/jsonRpc/transports/webSocket.ts +3 -1
  42. package/src/molecule/codec.ts +437 -202
  43. package/src/utils/index.ts +3 -1
  44. package/dist/advancedBarrel-Daj_LJkz.d.mts.map +0 -1
  45. package/dist/barrel-CFOWSjcM.mjs +0 -7
  46. package/dist/barrel-CFOWSjcM.mjs.map +0 -1
  47. package/dist/transformers-DAMi6ZOq.mjs +0 -2
  48. package/dist/transformers-DAMi6ZOq.mjs.map +0 -1
  49. package/dist.commonjs/advancedBarrel-CJL3rfuW.d.ts.map +0 -1
  50. package/dist.commonjs/barrel-DZkApbAR.js.map +0 -1
  51. package/dist.commonjs/transformers-C6jbFO7W.js +0 -2
  52. package/dist.commonjs/transformers-C6jbFO7W.js.map +0 -1
@@ -1,14 +1,17 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
2
 
3
3
  import {
4
+ Bytes,
4
5
  bytesConcat,
5
6
  bytesConcatTo,
6
7
  bytesFrom,
7
8
  BytesLike,
8
9
  } from "../bytes/index.js";
9
10
  import {
11
+ ChildContext,
10
12
  Codec,
11
13
  CodecLike,
14
+ ContextType,
12
15
  DecodedType,
13
16
  EncodableType,
14
17
  } from "../codec/codec.js";
@@ -53,19 +56,123 @@ function getMessage(e: unknown): string {
53
56
  return e instanceof Error ? e.message : String(e);
54
57
  }
55
58
 
59
+ function isPositiveSafeInteger(val: unknown): val is number {
60
+ return typeof val === "number" && Number.isSafeInteger(val) && val > 0;
61
+ }
62
+
63
+ export type TableDecodeContext = {
64
+ isExtraFieldIgnored?: boolean;
65
+ };
66
+
67
+ /**
68
+ * Verifies a table or dynvec header and returns item boundaries.
69
+ * For N items/fields, returns an array of length N + 1:
70
+ * [offset_0, offset_1, ..., offset_{N-1}, totalSize].
71
+ */
72
+ function verifyAndExtractOffsets(
73
+ value: Bytes,
74
+ prefix: string,
75
+ fieldCountConstraint?: {
76
+ expected: number;
77
+ allowExtra: boolean;
78
+ },
79
+ ): number[] {
80
+ if (value.byteLength < 4) {
81
+ throw new Error(
82
+ `${prefix}: too short buffer, expected at least 4 bytes, but got ${value.byteLength}`,
83
+ );
84
+ }
85
+ const totalSize = uint32From(value.subarray(0, 4));
86
+ if (totalSize !== value.byteLength) {
87
+ throw new Error(
88
+ `${prefix}: invalid buffer size, expected ${totalSize}, but got ${value.byteLength}`,
89
+ );
90
+ }
91
+
92
+ if (totalSize !== 4 && totalSize < 8) {
93
+ throw new Error(
94
+ `${prefix}: invalid header, buffer too short for first offset, got ${totalSize} bytes`,
95
+ );
96
+ }
97
+
98
+ const firstOffset = totalSize === 4 ? 4 : uint32From(value.subarray(4, 8));
99
+ if (totalSize !== 4) {
100
+ if (firstOffset < 8) {
101
+ throw new Error(
102
+ `${prefix}: invalid first offset, expected at least 8, but got ${firstOffset}`,
103
+ );
104
+ }
105
+ if (firstOffset % 4 !== 0) {
106
+ throw new Error(
107
+ `${prefix}: invalid first offset alignment, expected multiple of 4, but got ${firstOffset}`,
108
+ );
109
+ }
110
+ if (firstOffset > totalSize) {
111
+ throw new Error(
112
+ `${prefix}: invalid first offset, offset ${firstOffset} exceeds total size ${totalSize}`,
113
+ );
114
+ }
115
+ }
116
+
117
+ const fieldCount = (firstOffset - 4) / 4;
118
+ if (
119
+ fieldCountConstraint &&
120
+ (fieldCount < fieldCountConstraint.expected ||
121
+ (fieldCount > fieldCountConstraint.expected &&
122
+ !fieldCountConstraint.allowExtra))
123
+ ) {
124
+ throw new Error(
125
+ `${prefix}: invalid field count, expected ${fieldCountConstraint.expected}, but got ${fieldCount}`,
126
+ );
127
+ }
128
+ if (fieldCount === 0) {
129
+ return [totalSize];
130
+ }
131
+
132
+ const offsets = [firstOffset];
133
+ let previous = firstOffset;
134
+ for (let i = 1; i < fieldCount; i++) {
135
+ const current = uint32From(value.subarray(4 + i * 4, 8 + i * 4));
136
+ if (current > totalSize) {
137
+ throw new Error(
138
+ `${prefix}: invalid offset, offset[${i}] = ${current} exceeds total size ${totalSize}`,
139
+ );
140
+ }
141
+ if (current < previous) {
142
+ throw new Error(
143
+ `${prefix}: invalid offset order, offset[${i}] (${current}) is less than offset[${i - 1}] (${previous})`,
144
+ );
145
+ }
146
+ offsets.push(current);
147
+ previous = current;
148
+ }
149
+
150
+ offsets.push(totalSize);
151
+ return offsets;
152
+ }
153
+
56
154
  /**
57
155
  * Vector with fixed size item codec
58
156
  * @param itemCodec fixed-size vector item codec
59
157
  */
60
- export function fixedItemVec<Encodable, Decoded>(
61
- itemCodec: CodecLike<Encodable, Decoded>,
62
- ): Codec<Array<Encodable>, Array<Decoded>> {
158
+ export function fixedItemVec<Encodable, Decoded, Context>(
159
+ itemCodec: CodecLike<Encodable, Decoded, Context>,
160
+ ): Codec<Array<Encodable>, Array<Decoded>, ContextType<typeof itemCodec>> {
63
161
  const itemByteLength = itemCodec.byteLength;
64
162
  if (itemByteLength === undefined) {
65
163
  throw new Error("fixedItemVec: itemCodec requires a byte length");
66
164
  }
165
+ if (!isPositiveSafeInteger(itemByteLength)) {
166
+ throw new Error(
167
+ `fixedItemVec: itemCodec byteLength must be a positive safe integer, but got ${String(itemByteLength)}`,
168
+ );
169
+ }
67
170
 
68
- return Codec.from({
171
+ return Codec.from<
172
+ Array<Encodable>,
173
+ Array<Decoded>,
174
+ ContextType<typeof itemCodec>
175
+ >({
69
176
  encode(userDefinedItems) {
70
177
  try {
71
178
  const concatted: number[] = [];
@@ -115,10 +222,14 @@ export function fixedItemVec<Encodable, Decoded>(
115
222
  * Vector with dynamic size item codec, you can create a recursive vector with this function
116
223
  * @param itemCodec the vector item codec. It can be fixed-size or dynamic-size.
117
224
  */
118
- export function dynItemVec<Encodable, Decoded>(
119
- itemCodec: CodecLike<Encodable, Decoded>,
120
- ): Codec<Array<Encodable>, Array<Decoded>> {
121
- return Codec.from({
225
+ export function dynItemVec<Encodable, Decoded, Context>(
226
+ itemCodec: CodecLike<Encodable, Decoded, Context>,
227
+ ): Codec<Array<Encodable>, Array<Decoded>, ContextType<typeof itemCodec>> {
228
+ return Codec.from<
229
+ Array<Encodable>,
230
+ Array<Decoded>,
231
+ ContextType<typeof itemCodec>
232
+ >({
122
233
  encode(userDefinedItems) {
123
234
  try {
124
235
  let offset = 4 + userDefinedItems.length * 4;
@@ -140,28 +251,8 @@ export function dynItemVec<Encodable, Decoded>(
140
251
  },
141
252
  decode(buffer, config) {
142
253
  const value = bytesFrom(buffer);
143
- if (value.byteLength < 4) {
144
- throw new Error(
145
- `dynItemVec: too short buffer, expected at least 4 bytes, but got ${value.byteLength}`,
146
- );
147
- }
148
- const byteLength = uint32From(value.subarray(0, 4));
149
- if (byteLength !== value.byteLength) {
150
- throw new Error(
151
- `dynItemVec: invalid buffer size, expected ${byteLength}, but got ${value.byteLength}`,
152
- );
153
- }
254
+ const offsets = verifyAndExtractOffsets(value, "dynItemVec");
154
255
 
155
- if (byteLength === 4) {
156
- return [];
157
- }
158
-
159
- const offset = uint32From(value.subarray(4, 8));
160
- const itemCount = (offset - 4) / 4;
161
- const offsets = Array.from(new Array(itemCount), (_, index) =>
162
- uint32From(value.subarray(4 + index * 4, 8 + index * 4)),
163
- );
164
- offsets.push(byteLength);
165
256
  try {
166
257
  const decodedArray: Array<Decoded> = [];
167
258
  for (let index = 0; index < offsets.length - 1; index++) {
@@ -182,9 +273,9 @@ export function dynItemVec<Encodable, Decoded>(
182
273
  * General vector codec, if `itemCodec` is fixed size type, it will create a fixvec codec, otherwise a dynvec codec will be created.
183
274
  * @param itemCodec
184
275
  */
185
- export function vector<Encodable, Decoded>(
186
- itemCodec: CodecLike<Encodable, Decoded>,
187
- ): Codec<Array<Encodable>, Array<Decoded>> {
276
+ export function vector<Encodable, Decoded, Context>(
277
+ itemCodec: CodecLike<Encodable, Decoded, Context>,
278
+ ): Codec<Array<Encodable>, Array<Decoded>, ContextType<typeof itemCodec>> {
188
279
  if (itemCodec.byteLength !== undefined) {
189
280
  return fixedItemVec(itemCodec);
190
281
  }
@@ -198,10 +289,18 @@ export function vector<Encodable, Decoded>(
198
289
  * - if it's not empty, just serialize the inner item (the size is same as the inner item's size).
199
290
  * @param innerCodec
200
291
  */
201
- export function option<Encodable, Decoded>(
202
- innerCodec: CodecLike<Encodable, Decoded>,
203
- ): Codec<Encodable | undefined | null, Decoded | undefined> {
204
- return Codec.from({
292
+ export function option<Encodable, Decoded, Context>(
293
+ innerCodec: CodecLike<Encodable, Decoded, Context>,
294
+ ): Codec<
295
+ Encodable | undefined | null,
296
+ Decoded | undefined,
297
+ ContextType<typeof innerCodec>
298
+ > {
299
+ return Codec.from<
300
+ Encodable | undefined | null,
301
+ Decoded | undefined,
302
+ ContextType<typeof innerCodec>
303
+ >({
205
304
  encode(userDefinedOrNull) {
206
305
  if (userDefinedOrNull == null) {
207
306
  return bytesFrom([]);
@@ -230,10 +329,10 @@ export function option<Encodable, Decoded>(
230
329
  * Wrap the encoded value with a fixed-length buffer
231
330
  * @param codec
232
331
  */
233
- export function byteVec<Encodable, Decoded>(
234
- codec: CodecLike<Encodable, Decoded>,
235
- ): Codec<Encodable, Decoded> {
236
- return Codec.from({
332
+ export function byteVec<Encodable, Decoded, Context>(
333
+ codec: CodecLike<Encodable, Decoded, Context>,
334
+ ): Codec<Encodable, Decoded, ContextType<typeof codec>> {
335
+ return Codec.from<Encodable, Decoded, ContextType<typeof codec>>({
237
336
  encode(userDefined) {
238
337
  try {
239
338
  const payload = bytesFrom(codec.encode(userDefined));
@@ -266,13 +365,15 @@ export function byteVec<Encodable, Decoded>(
266
365
  }
267
366
 
268
367
  export type EncodableRecordOptionalKeys<
269
- T extends Record<string, CodecLike<any, any>>,
368
+ T extends Record<string, CodecLike<any, any, any>>,
270
369
  > = {
271
370
  [K in keyof T]: Extract<EncodableType<T[K]>, undefined> extends never
272
371
  ? never
273
372
  : K;
274
373
  }[keyof T];
275
- export type EncodableRecord<T extends Record<string, CodecLike<any, any>>> = {
374
+ export type EncodableRecord<
375
+ T extends Record<string, CodecLike<any, any, any>>,
376
+ > = {
276
377
  [key in keyof Pick<T, EncodableRecordOptionalKeys<T>>]+?: EncodableType<
277
378
  T[key]
278
379
  >;
@@ -281,30 +382,33 @@ export type EncodableRecord<T extends Record<string, CodecLike<any, any>>> = {
281
382
  };
282
383
 
283
384
  export type DecodedRecordOptionalKeys<
284
- T extends Record<string, CodecLike<any, any>>,
385
+ T extends Record<string, CodecLike<any, any, any>>,
285
386
  > = {
286
387
  [K in keyof T]: Extract<DecodedType<T[K]>, undefined> extends never
287
388
  ? never
288
389
  : K;
289
390
  }[keyof T];
290
- export type DecodedRecord<T extends Record<string, CodecLike<any, any>>> = {
291
- [key in keyof Pick<T, DecodedRecordOptionalKeys<T>>]+?: DecodedType<T[key]>;
292
- } & {
293
- [key in keyof Omit<T, DecodedRecordOptionalKeys<T>>]: DecodedType<T[key]>;
294
- };
391
+ export type DecodedRecord<T extends Record<string, CodecLike<any, any, any>>> =
392
+ {
393
+ [key in keyof Pick<T, DecodedRecordOptionalKeys<T>>]+?: DecodedType<T[key]>;
394
+ } & {
395
+ [key in keyof Omit<T, DecodedRecordOptionalKeys<T>>]: DecodedType<T[key]>;
396
+ };
295
397
 
296
398
  /**
297
399
  * Table is a dynamic-size type. It can be considered as a dynvec but the length is fixed.
298
400
  * @param codecLayout
299
401
  */
300
402
  export function table<
301
- T extends Record<string, CodecLike<any, any>>,
403
+ T extends Record<string, CodecLike<any, any, any>>,
302
404
  Encodable extends EncodableRecord<T>,
303
405
  Decoded extends DecodedRecord<T>,
304
- >(codecLayout: T): Codec<Encodable, Decoded> {
406
+ >(
407
+ codecLayout: T,
408
+ ): Codec<Encodable, Decoded, ChildContext<T> & TableDecodeContext> {
305
409
  const keys = Object.keys(codecLayout);
306
410
 
307
- return Codec.from({
411
+ return Codec.from<Encodable, Decoded, ChildContext<T> & TableDecodeContext>({
308
412
  encode(object) {
309
413
  let offset = 4 + keys.length * 4;
310
414
  const header: number[] = [];
@@ -324,67 +428,34 @@ export function table<
324
428
  const packedTotalSize = uint32To(header.length + body.length + 4);
325
429
  return bytesConcat(packedTotalSize, header, body);
326
430
  },
327
- decode(buffer, config) {
431
+ decode(buffer, context) {
328
432
  const value = bytesFrom(buffer);
329
- if (value.byteLength < 4) {
330
- throw new Error(
331
- `table: too short buffer, expected at least 4 bytes, but got ${value.byteLength}`,
332
- );
333
- }
334
- const byteLength = uint32From(value.subarray(0, 4));
335
- const headerLength = uint32From(value.subarray(4, 8));
336
- const actualFieldCount = (headerLength - 4) / 4;
337
-
338
- if (byteLength !== value.byteLength) {
339
- throw new Error(
340
- `table: invalid buffer size, expected ${byteLength}, but got ${value.byteLength}`,
341
- );
342
- }
343
-
344
- if (actualFieldCount < keys.length) {
345
- throw new Error(
346
- `table: invalid field count, expected ${keys.length}, but got ${actualFieldCount}`,
347
- );
348
- }
433
+ const schemaKeyCount = keys.length;
434
+ const offsets = verifyAndExtractOffsets(value, "table", {
435
+ expected: schemaKeyCount,
436
+ allowExtra: context?.isExtraFieldIgnored === true,
437
+ });
438
+ const result: Record<string, unknown> = {};
349
439
 
350
- if (actualFieldCount > keys.length && !config?.isExtraFieldIgnored) {
351
- throw new Error(
352
- `table: invalid field count, expected ${keys.length}, but got ${actualFieldCount}, and extra fields are not allowed in the current configuration. If you want to ignore extra fields, set isExtraFieldIgnored to true.`,
353
- );
354
- }
355
- const offsets = keys.map((_, index) =>
356
- uint32From(value.subarray(4 + index * 4, 8 + index * 4)),
357
- );
358
- // If there are extra fields, add the last offset to the offsets array
359
- if (actualFieldCount > keys.length) {
360
- offsets.push(
361
- uint32From(value.subarray(4 + keys.length * 4, 8 + keys.length * 4)),
362
- );
363
- } else {
364
- // If there are no extra fields, add the byte length to the offsets array
365
- offsets.push(byteLength);
366
- }
367
- const object = {};
368
- for (let i = 0; i < offsets.length - 1; i++) {
440
+ for (let i = 0; i < schemaKeyCount; i++) {
441
+ const key = keys[i];
369
442
  const start = offsets[i];
370
443
  const end = offsets[i + 1];
371
- const field = keys[i];
372
- const codec = codecLayout[field];
373
- const payload = value.subarray(start, end);
444
+ const itemBuffer = value.subarray(start, end);
374
445
  try {
375
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
376
- Object.assign(object, { [field]: codec.decode(payload, config) });
446
+ result[key] = codecLayout[key].decode(itemBuffer, context);
377
447
  } catch (e: unknown) {
378
- throw new Error(`table.${field} - ${getMessage(e)}`, { cause: e });
448
+ throw new Error(`table.${key} - ${getMessage(e)}`, { cause: e });
379
449
  }
380
450
  }
381
- return object as Decoded;
451
+
452
+ return result as Decoded;
382
453
  },
383
454
  });
384
455
  }
385
456
 
386
457
  export type UnionEncodable<
387
- T extends Record<string, CodecLike<any, any>>,
458
+ T extends Record<string, CodecLike<any, any, any>>,
388
459
  K extends keyof T = keyof T,
389
460
  > = K extends unknown
390
461
  ? {
@@ -393,7 +464,7 @@ export type UnionEncodable<
393
464
  }
394
465
  : never;
395
466
  export type UnionDecoded<
396
- T extends Record<string, CodecLike<any, any>>,
467
+ T extends Record<string, CodecLike<any, any, any>>,
397
468
  K extends keyof T = keyof T,
398
469
  > = K extends unknown
399
470
  ? {
@@ -403,7 +474,7 @@ export type UnionDecoded<
403
474
  : never;
404
475
 
405
476
  export type UnionMatchHandlers<
406
- CodecType extends CodecLike<any, UnionDecoded<any, any>>,
477
+ CodecType extends CodecLike<any, UnionDecoded<any, any>, any>,
407
478
  Result,
408
479
  > = {
409
480
  [T in DecodedType<CodecType>["type"]]: (
@@ -411,150 +482,295 @@ export type UnionMatchHandlers<
411
482
  ) => Result;
412
483
  };
413
484
 
414
- /**
415
- * Constructs a union codec that can serialize and deserialize values tagged with a type identifier.
416
- *
417
- * If all variants have the same fixed size, the resulting union codec is fixed-size (header + payload).
418
- * Otherwise, it falls back to a dynamic-size codec.
419
- *
420
- * Serialization format:
421
- * 1. 4-byte little-endian unsigned integer for the variant index.
422
- * 2. Encoded bytes of the selected variant.
423
- *
424
- * @typeParam T
425
- * A record mapping variant names to codecs.
426
- * @param codecLayout
427
- * An object whose keys are variant names and values are codecs for each variant.
428
- * @param fields
429
- * Optional mapping from variant names to custom numeric IDs. If omitted, the index
430
- * of each variant in `codecLayout` is used as its ID.
431
- *
432
- *
433
- * @example
434
- * // Dynamic union without custom numeric IDs
435
- * union({ cafe: Uint8, bee: Uint16 })
436
- *
437
- * // Dynamic union with custom numeric IDs
438
- * union({ cafe: Uint8, bee: Uint16 }, { cafe: 0xcafe, bee: 0xbee })
439
- *
440
- * // Fixed-size union without custom numeric IDs
441
- * const PaddedUint8 = struct({ data : u8, padding : u8 })
442
- * union({ cafe: PaddedUint8, bee: Uint16 });
443
- *
444
- * // Fixed-size union with custom numeric IDs
445
- * union({ cafe: PaddedUint8, bee: Uint16 }, { cafe: 0xcafe, bee: 0xbee })
446
- */
447
-
448
- export function union<T extends Record<string, CodecLike<any, any>>>(
485
+ function validateUnionFields<
486
+ T extends Record<string, CodecLike<any, any, any>>,
487
+ >(
488
+ prefix: string,
449
489
  codecLayout: T,
450
490
  fields?: Record<keyof T, number | undefined | null>,
451
- ): Codec<UnionEncodable<T> | { inner: UnionEncodable<T> }, UnionDecoded<T>> {
452
- const entries = Object.entries(codecLayout);
453
-
454
- // Determine if all variants have a fixed and equal byteLength.
455
- let byteLength: number | undefined;
456
- if (entries.length > 0) {
457
- const firstLen = entries[0][1].byteLength;
491
+ ): void {
492
+ if (!fields) {
493
+ return;
494
+ }
495
+ const layoutKeys = new Set(Object.keys(codecLayout));
496
+ const fieldKeys = new Set(Object.keys(fields));
497
+ for (const key of fieldKeys) {
498
+ if (!layoutKeys.has(key)) {
499
+ throw new Error(
500
+ `${prefix}: unexpected field id for unknown key '${key}'`,
501
+ );
502
+ }
503
+ }
504
+ const seenIds = new Map<number, string>();
505
+ for (const [key, id] of Object.entries(fields)) {
506
+ if (id === undefined || id === null) {
507
+ throw new Error(`${prefix}: field id for '${key}' is required`);
508
+ }
458
509
  if (
459
- firstLen !== undefined &&
460
- entries.every(([, { byteLength: len }]) => len === firstLen)
510
+ typeof id !== "number" ||
511
+ !Number.isInteger(id) ||
512
+ id < 0 ||
513
+ id > 0xffffffff
461
514
  ) {
462
- // Add 4 bytes for the type header
463
- byteLength = firstLen + 4;
515
+ throw new Error(`${prefix}: invalid field id ${id} for key '${key}'`);
464
516
  }
517
+ if (seenIds.has(id)) {
518
+ throw new Error(
519
+ `${prefix}: duplicate field id ${id} for keys '${seenIds.get(id)}' and '${key}'`,
520
+ );
521
+ }
522
+ seenIds.set(id, key);
465
523
  }
466
-
467
- function extract(
468
- encodable: UnionEncodable<T> | { inner: UnionEncodable<T> },
469
- ): UnionEncodable<T> {
470
- if ("type" in encodable && "value" in encodable) {
471
- return encodable;
524
+ for (const key of layoutKeys) {
525
+ if (!fieldKeys.has(key)) {
526
+ throw new Error(`${prefix}: missing field id for key '${key}'`);
472
527
  }
528
+ }
529
+ }
473
530
 
474
- return encodable.inner;
531
+ function extractUnionEncodable<
532
+ T extends Record<string, CodecLike<any, any, any>>,
533
+ >(
534
+ encodable: UnionEncodable<T> | { inner: UnionEncodable<T> },
535
+ ): UnionEncodable<T> {
536
+ if ("type" in encodable && "value" in encodable) {
537
+ return encodable;
475
538
  }
476
539
 
477
- return Codec.from({
540
+ return encodable.inner;
541
+ }
542
+
543
+ function createUnionCodec<T extends Record<string, CodecLike<any, any, any>>>(
544
+ prefix: string,
545
+ codecLayout: T,
546
+ fields: Record<keyof T, number | undefined | null> | undefined,
547
+ payloadByteLength?: number,
548
+ ): Codec<
549
+ UnionEncodable<T> | { inner: UnionEncodable<T> },
550
+ UnionDecoded<T>,
551
+ ChildContext<T>
552
+ > {
553
+ const keys = Object.keys(codecLayout);
554
+ const fieldIds = new Map<string, number>();
555
+ const fieldsById = new Map<number, string>();
556
+ for (const [index, key] of keys.entries()) {
557
+ const fieldId = fields ? (fields[key] as number) : index;
558
+ fieldIds.set(key, fieldId);
559
+ fieldsById.set(fieldId, key);
560
+ }
561
+ const byteLength =
562
+ payloadByteLength === undefined ? undefined : payloadByteLength + 4;
563
+
564
+ return Codec.from<
565
+ UnionEncodable<T> | { inner: UnionEncodable<T> },
566
+ UnionDecoded<T>,
567
+ ChildContext<T>
568
+ >({
478
569
  byteLength,
479
570
  encode(encodable) {
480
- const { type, value } = extract(encodable);
571
+ const { type, value } = extractUnionEncodable(encodable);
481
572
  const typeStr = type.toString();
482
573
  const codec = codecLayout[typeStr];
483
574
  if (!codec) {
484
575
  throw new Error(
485
- `union: invalid type, expected ${entries.map((e) => e[0]).toString()}, but got ${typeStr}`,
576
+ `${prefix}: invalid type, expected ${keys.toString()}, but got ${typeStr}`,
486
577
  );
487
578
  }
488
- const fieldId = fields
489
- ? (fields[typeStr] ?? -1)
490
- : entries.findIndex((e) => e[0] === typeStr);
491
- if (fieldId < 0) {
492
- throw new Error(`union: invalid field id ${fieldId} of ${typeStr}`);
579
+ const fieldId = fieldIds.get(typeStr);
580
+ if (fieldId === undefined) {
581
+ throw new Error(`${prefix}: invalid field id -1 of ${typeStr}`);
493
582
  }
494
583
  const header = uint32To(fieldId);
495
584
  try {
496
585
  const body = codec.encode(value);
586
+ if (
587
+ payloadByteLength !== undefined &&
588
+ body.byteLength !== payloadByteLength
589
+ ) {
590
+ throw new Error(
591
+ `${prefix}: variant '${typeStr}' encoded ${body.byteLength} bytes, expected ${payloadByteLength}`,
592
+ );
593
+ }
497
594
  return bytesConcat(header, body);
498
595
  } catch (e: unknown) {
499
- throw new Error(`union.(${typeStr}) - ${getMessage(e)}`, { cause: e });
596
+ throw new Error(`${prefix}.(${typeStr}) - ${getMessage(e)}`, {
597
+ cause: e,
598
+ });
500
599
  }
501
600
  },
502
601
  decode(buffer, config) {
503
602
  const value = bytesFrom(buffer);
504
- const fieldIndex = uint32From(value.subarray(0, 4));
505
- const keys = Object.keys(codecLayout);
506
-
507
- const field = (() => {
508
- if (!fields) {
509
- return keys[fieldIndex];
510
- }
511
- const entry = Object.entries(fields).find(
512
- ([, id]) => id === fieldIndex,
603
+ if (byteLength === undefined && value.byteLength < 4) {
604
+ throw new Error(
605
+ `${prefix}: too short buffer, expected at least 4 bytes for union tag, but got ${value.byteLength}`,
513
606
  );
514
- return entry?.[0];
515
- })();
516
-
607
+ }
608
+ const fieldIndex = uint32From(value.subarray(0, 4));
609
+ const field = fieldsById.get(fieldIndex);
517
610
  if (!field) {
518
- if (!fields) {
519
- throw new Error(
520
- `union: unknown union field index ${fieldIndex}, only ${keys.toString()} are allowed`,
521
- );
522
- }
523
- const fieldKeys = Object.keys(fields);
524
611
  throw new Error(
525
- `union: unknown union field index ${fieldIndex}, only ${fieldKeys.toString()} and ${keys.toString()} are allowed`,
612
+ `${prefix}: unknown union field index ${fieldIndex}, only ${keys.toString()} are allowed`,
526
613
  );
527
614
  }
528
615
 
529
- return {
530
- type: field,
531
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
532
- value: codecLayout[field].decode(value.subarray(4), config),
533
- } as UnionDecoded<T>;
616
+ try {
617
+ return {
618
+ type: field,
619
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
620
+ value: codecLayout[field].decode(value.subarray(4), config),
621
+ } as UnionDecoded<T>;
622
+ } catch (e: unknown) {
623
+ throw new Error(`${prefix}.(${field}) - ${getMessage(e)}`, {
624
+ cause: e,
625
+ });
626
+ }
534
627
  },
535
628
  });
536
629
  }
537
630
 
631
+ /**
632
+ * Constructs a union codec that can serialize and deserialize values tagged with a type identifier.
633
+ *
634
+ * In accordance with the Molecule specification, `union` is always a dynamic-size type,
635
+ * regardless of whether its variants have identical fixed lengths.
636
+ *
637
+ * If you need a fixed-size union type compatible with fixed containers like `struct`,
638
+ * `array`, or `vector` (as FixVec), use {@link fixedUnion} instead.
639
+ *
640
+ * Serialization format:
641
+ * 1. 4-byte little-endian unsigned integer for the variant ItemId (tag).
642
+ * 2. Encoded bytes of the selected variant.
643
+ *
644
+ * @typeParam T
645
+ * A record mapping variant names to codecs.
646
+ * @param codecLayout
647
+ * An object whose keys are variant names and values are codecs for each variant.
648
+ * @param fields
649
+ * Optional mapping from variant names to custom numeric IDs. If omitted, the index
650
+ * of each variant in `codecLayout` is used as its ID.
651
+ *
652
+ * @example
653
+ * // Union without custom numeric IDs
654
+ * union({ cafe: Uint8, bee: Uint16 });
655
+ *
656
+ * // Union with custom numeric IDs
657
+ * union({ cafe: Uint8, bee: Uint16 }, { cafe: 0xcafe, bee: 0xbee });
658
+ *
659
+ * // Fixed-size union (explicit extension):
660
+ * fixedUnion({ cafe: Uint16, bee: Uint16 });
661
+ */
662
+ export function union<T extends Record<string, CodecLike<any, any, any>>>(
663
+ codecLayout: T,
664
+ fields?: Record<keyof T, number | undefined | null>,
665
+ ): Codec<
666
+ UnionEncodable<T> | { inner: UnionEncodable<T> },
667
+ UnionDecoded<T>,
668
+ ChildContext<T>
669
+ > {
670
+ if (Object.keys(codecLayout).length === 0) {
671
+ throw new Error("union: must have at least one variant");
672
+ }
673
+
674
+ validateUnionFields("union", codecLayout, fields);
675
+ return createUnionCodec("union", codecLayout, fields);
676
+ }
677
+
678
+ /**
679
+ * FixedUnion is a fixed-size union type where all variants must be fixed-size types of identical byte length.
680
+ * The total byteLength is `payloadByteLength + 4` (4-byte uint32 ItemId tag).
681
+ *
682
+ * Compatibility note:
683
+ * `fixedUnion` is a CCC extension for scenarios requiring fixed memory layouts (such as embedding
684
+ * inside `mol.struct`, `mol.array`, or `mol.vector` as FixVec). While the standalone wire format
685
+ * (4-byte tag + variant payload) is binary-compatible with a union encoding, composing it into
686
+ * `struct`, `array`, or FixVec is not part of the standard upstream Molecule union schema specification.
687
+ *
688
+ * Migration guide:
689
+ * ```typescript
690
+ * // Preserve legacy equal-length union fixed container layout (FixVec):
691
+ * const Legacy = mol.vector(mol.fixedUnion(layout));
692
+ *
693
+ * // Use canonical Molecule union vector (DynVec):
694
+ * const Canonical = mol.vector(mol.union(layout));
695
+ * ```
696
+ *
697
+ * @param codecLayout an object mapping variant names to fixed-size codecs of identical byteLength
698
+ * @param fields optional mapping from variant names to custom numeric IDs
699
+ */
700
+ export function fixedUnion<T extends Record<string, CodecLike<any, any, any>>>(
701
+ codecLayout: T,
702
+ fields?: Record<keyof T, number | undefined | null>,
703
+ ): Codec<
704
+ UnionEncodable<T> | { inner: UnionEncodable<T> },
705
+ UnionDecoded<T>,
706
+ ChildContext<T>
707
+ > {
708
+ const entries = Object.entries(codecLayout);
709
+ if (entries.length === 0) {
710
+ throw new Error("fixedUnion: must have at least one variant");
711
+ }
712
+
713
+ let payloadByteLength: number | undefined;
714
+ for (const [key, codec] of entries) {
715
+ if (codec.byteLength === undefined) {
716
+ throw new Error(`fixedUnion: variant '${key}' must be a fixed-size type`);
717
+ }
718
+ if (!isPositiveSafeInteger(codec.byteLength)) {
719
+ throw new Error(
720
+ `fixedUnion: variant '${key}' byteLength must be a positive safe integer, but got ${String(codec.byteLength)}`,
721
+ );
722
+ }
723
+ if (payloadByteLength === undefined) {
724
+ payloadByteLength = codec.byteLength;
725
+ } else if (codec.byteLength !== payloadByteLength) {
726
+ throw new Error(
727
+ `fixedUnion: all variants must have the same byteLength (${payloadByteLength}), but variant '${key}' has ${codec.byteLength}`,
728
+ );
729
+ }
730
+ }
731
+
732
+ validateUnionFields("fixedUnion", codecLayout, fields);
733
+
734
+ if (payloadByteLength! > Number.MAX_SAFE_INTEGER - 4) {
735
+ throw new Error("fixedUnion: total byteLength exceeds safe integer limit");
736
+ }
737
+
738
+ return createUnionCodec("fixedUnion", codecLayout, fields, payloadByteLength);
739
+ }
740
+
538
741
  /**
539
742
  * Struct is a fixed-size type: all fields in struct are fixed-size and it has a fixed quantity of fields.
540
743
  * The size of a struct is the sum of all fields' size.
541
744
  * @param codecLayout a object contains all fields' codec
542
745
  */
543
746
  export function struct<
544
- T extends Record<string, CodecLike<any, any>>,
747
+ T extends Record<string, CodecLike<any, any, any>>,
545
748
  Encodable extends EncodableRecord<T>,
546
749
  Decoded extends DecodedRecord<T>,
547
- >(codecLayout: T): Codec<Encodable, Decoded> {
548
- const codecArray = Object.values(codecLayout);
750
+ >(codecLayout: T): Codec<Encodable, Decoded, ChildContext<T>> {
549
751
  const keys = Object.keys(codecLayout);
752
+ if (keys.length === 0) {
753
+ throw new Error("struct: must have at least one field");
754
+ }
550
755
 
551
- return Codec.from({
552
- byteLength: codecArray.reduce((acc, codec) => {
553
- if (codec.byteLength === undefined) {
554
- throw new Error("struct: all fields must be fixed-size");
555
- }
556
- return acc + codec.byteLength;
557
- }, 0),
756
+ let byteLength = 0;
757
+ for (const [key, codec] of Object.entries(codecLayout)) {
758
+ if (codec.byteLength === undefined) {
759
+ throw new Error(`struct: field '${key}' must be fixed-size`);
760
+ }
761
+ if (!isPositiveSafeInteger(codec.byteLength)) {
762
+ throw new Error(
763
+ `struct: field '${key}' byteLength must be a positive safe integer, but got ${String(codec.byteLength)}`,
764
+ );
765
+ }
766
+ byteLength += codec.byteLength;
767
+ if (!Number.isSafeInteger(byteLength)) {
768
+ throw new Error("struct: total byteLength exceeds safe integer limit");
769
+ }
770
+ }
771
+
772
+ return Codec.from<Encodable, Decoded, ChildContext<T>>({
773
+ byteLength,
558
774
  encode(object) {
559
775
  const bytes: number[] = [];
560
776
  for (const key of keys) {
@@ -593,16 +809,35 @@ export function struct<
593
809
  * @param itemCodec the fixed-size array item codec
594
810
  * @param itemCount
595
811
  */
596
- export function array<Encodable, Decoded>(
597
- itemCodec: CodecLike<Encodable, Decoded>,
812
+ export function array<Encodable, Decoded, Context>(
813
+ itemCodec: CodecLike<Encodable, Decoded, Context>,
598
814
  itemCount: number,
599
- ): Codec<Array<Encodable>, Array<Decoded>> {
815
+ ): Codec<Array<Encodable>, Array<Decoded>, ContextType<typeof itemCodec>> {
600
816
  if (itemCodec.byteLength === undefined) {
601
817
  throw new Error("array: itemCodec requires a byte length");
602
818
  }
819
+ if (!isPositiveSafeInteger(itemCodec.byteLength)) {
820
+ throw new Error(
821
+ `array: itemCodec byteLength must be a positive safe integer, but got ${String(itemCodec.byteLength)}`,
822
+ );
823
+ }
824
+ if (!isPositiveSafeInteger(itemCount)) {
825
+ throw new Error(
826
+ `array: itemCount must be a positive safe integer, but got ${String(itemCount)}`,
827
+ );
828
+ }
603
829
  const byteLength = itemCodec.byteLength * itemCount;
830
+ if (!Number.isSafeInteger(byteLength)) {
831
+ throw new Error(
832
+ `array: total byteLength exceeds safe integer limit, got ${String(byteLength)}`,
833
+ );
834
+ }
604
835
 
605
- return Codec.from({
836
+ return Codec.from<
837
+ Array<Encodable>,
838
+ Array<Decoded>,
839
+ ContextType<typeof itemCodec>
840
+ >({
606
841
  byteLength,
607
842
  encode(items) {
608
843
  try {
@@ -618,7 +853,7 @@ export function array<Encodable, Decoded>(
618
853
  },
619
854
  decode(buffer, config) {
620
855
  const value = bytesFrom(buffer);
621
- if (value.byteLength != byteLength) {
856
+ if (value.byteLength !== byteLength) {
622
857
  throw new Error(
623
858
  `array: invalid buffer size, expected ${byteLength}, but got ${value.byteLength}`,
624
859
  );