@solana/codecs-data-structures 8.3.0 → 8.4.0-canary-20260918134912
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.
- package/README.md +21 -2
- package/dist/index.browser.cjs +48 -4
- package/dist/index.browser.cjs.map +1 -1
- package/dist/index.browser.mjs +50 -6
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.native.mjs +50 -6
- package/dist/index.native.mjs.map +1 -1
- package/dist/index.node.cjs +48 -4
- package/dist/index.node.cjs.map +1 -1
- package/dist/index.node.mjs +50 -6
- package/dist/index.node.mjs.map +1 -1
- package/dist/types/array.d.ts +86 -2
- package/dist/types/array.d.ts.map +1 -1
- package/dist/types/map.d.ts +1 -0
- package/dist/types/map.d.ts.map +1 -1
- package/dist/types/set.d.ts +1 -0
- package/dist/types/set.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/array.ts +155 -4
- package/src/map.ts +1 -0
- package/src/set.ts +1 -0
package/README.md
CHANGED
|
@@ -33,11 +33,12 @@ getArrayCodec(getU8Codec()).encode([1, 2, 3]);
|
|
|
33
33
|
// └-- 4-byte prefix telling us to read 3 items.
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
However, you may use the `size` option to configure this behaviour. It can be one of the following
|
|
36
|
+
However, you may use the `size` option to configure this behaviour. It can be one of the following four strategies:
|
|
37
37
|
|
|
38
38
|
- `Codec<number>`: When a number codec is provided, that codec will be used to encode and decode the size prefix.
|
|
39
39
|
- `number`: When a number is provided, the codec will expect a fixed number of items in the array. An error will be thrown when trying to encode an array of a different length.
|
|
40
|
-
- `"remainder"`: When the string
|
|
40
|
+
- `"remainder"`: When the string `"remainder"` is passed as a size, the codec will use the remainder of the bytes to encode/decode its items. This means the size is not stored or known in advance but simply inferred from the rest of the buffer. For instance, if we have an array of `u16` numbers and 10 bytes remaining, we know there are 5 items in this array.
|
|
41
|
+
- A sentinel object: When an object of the form `{ __kind: 'sentinel', sentinel, strategy? }` is provided, the array ends as soon as the bytes at the next item position match the given `sentinel`. The sentinel is only compared at item boundaries and is never searched for within an item, so — unlike `addCodecSentinel` — its bytes may occur _inside_ an item without terminating the array. The optional `strategy` (`"required"` by default, or `"optional"` / `"omitted"`) controls whether the sentinel is written when encoding and required when decoding.
|
|
41
42
|
|
|
42
43
|
```ts
|
|
43
44
|
getArrayCodec(getU8Codec(), { size: getU16Codec() }).encode([1, 2, 3]);
|
|
@@ -52,8 +53,24 @@ getArrayCodec(getU8Codec(), { size: 3 }).encode([1, 2, 3]);
|
|
|
52
53
|
getArrayCodec(getU8Codec(), { size: 'remainder' }).encode([1, 2, 3]);
|
|
53
54
|
// 0x010203
|
|
54
55
|
// └-- 3 items of 1 byte each. The size is inferred from the remainder of the bytes.
|
|
56
|
+
|
|
57
|
+
getArrayCodec(getU8Codec(), { size: { __kind: 'sentinel', sentinel: new Uint8Array([0]) } }).encode([1, 2, 3]);
|
|
58
|
+
// 0x01020300
|
|
59
|
+
// | └-- The sentinel that marks the end of the array.
|
|
60
|
+
// └-- 3 items of 1 byte each.
|
|
55
61
|
```
|
|
56
62
|
|
|
63
|
+
The sentinel strategy accepts three variants:
|
|
64
|
+
|
|
65
|
+
- `"required"` (default): the sentinel is written after the last item and must be present when decoding. Reaching the end of the byte array without it throws.
|
|
66
|
+
- `"optional"`: the sentinel is written after the last item, but decoding also stops at the end of the byte array. Use this to tolerate tightly sized or legacy data that lacks the sentinel.
|
|
67
|
+
- `"omitted"`: the sentinel is never written; decoding stops at the end of the byte array (and consumes the sentinel if one happens to be present). Only meaningful when the collection is followed by unused space or the end of the byte array.
|
|
68
|
+
|
|
69
|
+
Because the sentinel is only compared at the start of the next item slot, two invariants must hold for the array to round-trip correctly. The codec does **not** enforce them; it is your responsibility to guarantee them:
|
|
70
|
+
|
|
71
|
+
1. **No item may _begin_ with the sentinel's bytes.** A valid item that starts with the sentinel is indistinguishable from the terminator, so decoding would stop early at that item. The sentinel may still appear _inside_ an item, just never at its start. For example, a single `0xff` byte is a poor sentinel for a list of public keys, since roughly one key in 256 starts with `0xff`; a sentinel as wide as an item (such as the all-zero public key) avoids this because only that exact key can match the terminator.
|
|
72
|
+
2. **Under `"optional"` and `"omitted"`, the sentinel must be no wider than the smallest possible item.** Otherwise a trailing region shorter than the sentinel but large enough to hold a valid item would be skipped, since decoding stops as soon as fewer bytes than the sentinel remain. This cannot arise under `"required"` because a terminator is always written.
|
|
73
|
+
|
|
57
74
|
When the size is stored as a prefix, decoding an exhausted byte array yields an empty array instead of failing. This allows arrays to be appended to existing data layouts without breaking the decoding of older data. Use the `requireSizePrefix` option to throw instead.
|
|
58
75
|
|
|
59
76
|
```ts
|
|
@@ -83,6 +100,7 @@ Just like the array codec, it uses a `u32` size prefix by default but can be con
|
|
|
83
100
|
getSetCodec(getU8Codec(), { size: getU16Codec() }).encode(new Set([1, 2, 3]));
|
|
84
101
|
getSetCodec(getU8Codec(), { size: 3 }).encode(new Set([1, 2, 3]));
|
|
85
102
|
getSetCodec(getU8Codec(), { size: 'remainder' }).encode(new Set([1, 2, 3]));
|
|
103
|
+
getSetCodec(getU8Codec(), { size: { __kind: 'sentinel', sentinel: new Uint8Array([0]) } }).encode(new Set([1, 2, 3]));
|
|
86
104
|
```
|
|
87
105
|
|
|
88
106
|
Separate `getSetEncoder` and `getSetDecoder` functions are also available.
|
|
@@ -127,6 +145,7 @@ However, it can be configured using the `size` and `requireSizePrefix` options.
|
|
|
127
145
|
getMapCodec(keyCodec, valueCodec, { size: getU16Codec() }).encode(myMap);
|
|
128
146
|
getMapCodec(keyCodec, valueCodec, { size: 3 }).encode(myMap);
|
|
129
147
|
getMapCodec(keyCodec, valueCodec, { size: 'remainder' }).encode(myMap);
|
|
148
|
+
getMapCodec(keyCodec, valueCodec, { size: { __kind: 'sentinel', sentinel: new Uint8Array([0]) } }).encode(myMap);
|
|
130
149
|
```
|
|
131
150
|
|
|
132
151
|
Separate `getMapEncoder` and `getMapDecoder` functions are also available.
|
package/dist/index.browser.cjs
CHANGED
|
@@ -33,13 +33,15 @@ function getMaxSize(codec) {
|
|
|
33
33
|
// src/array.ts
|
|
34
34
|
function getArrayEncoder(item, config = {}) {
|
|
35
35
|
const size = config.size ?? codecsNumbers.getU32Encoder();
|
|
36
|
+
assertValidSize(size);
|
|
36
37
|
const fixedSize = computeArrayLikeCodecSize(size, getFixedSize(item));
|
|
37
38
|
const maxSize = computeArrayLikeCodecSize(size, getMaxSize(item)) ?? void 0;
|
|
38
39
|
return codecsCore.createEncoder({
|
|
39
40
|
...fixedSize !== null ? { fixedSize } : {
|
|
40
41
|
getSizeFromValue: (array) => {
|
|
41
|
-
const prefixSize =
|
|
42
|
-
|
|
42
|
+
const prefixSize = isPrefixSize(size) ? codecsCore.getEncodedSize(array.length, size) : 0;
|
|
43
|
+
const suffixSize = isSentinelSize(size) && size.strategy !== "omitted" ? size.sentinel.length : 0;
|
|
44
|
+
return prefixSize + suffixSize + [...array].reduce((all, value) => all + codecsCore.getEncodedSize(value, item), 0);
|
|
43
45
|
},
|
|
44
46
|
maxSize
|
|
45
47
|
},
|
|
@@ -47,18 +49,23 @@ function getArrayEncoder(item, config = {}) {
|
|
|
47
49
|
if (typeof size === "number") {
|
|
48
50
|
assertValidNumberOfItemsForCodec(config.description ?? "array", size, array.length);
|
|
49
51
|
}
|
|
50
|
-
if (
|
|
52
|
+
if (isPrefixSize(size)) {
|
|
51
53
|
offset = size.write(array.length, bytes, offset);
|
|
52
54
|
}
|
|
53
55
|
array.forEach((value) => {
|
|
54
56
|
offset = item.write(value, bytes, offset);
|
|
55
57
|
});
|
|
58
|
+
if (isSentinelSize(size) && size.strategy !== "omitted") {
|
|
59
|
+
bytes.set(size.sentinel, offset);
|
|
60
|
+
offset += size.sentinel.length;
|
|
61
|
+
}
|
|
56
62
|
return offset;
|
|
57
63
|
}
|
|
58
64
|
});
|
|
59
65
|
}
|
|
60
66
|
function getArrayDecoder(item, config = {}) {
|
|
61
67
|
const size = config.size ?? codecsNumbers.getU32Decoder();
|
|
68
|
+
assertValidSize(size);
|
|
62
69
|
const itemSize = getFixedSize(item);
|
|
63
70
|
const fixedSize = computeArrayLikeCodecSize(size, itemSize);
|
|
64
71
|
const maxSize = computeArrayLikeCodecSize(size, getMaxSize(item)) ?? void 0;
|
|
@@ -66,7 +73,7 @@ function getArrayDecoder(item, config = {}) {
|
|
|
66
73
|
...fixedSize !== null ? { fixedSize } : { maxSize },
|
|
67
74
|
read: (bytes, offset) => {
|
|
68
75
|
const array = [];
|
|
69
|
-
if (
|
|
76
|
+
if (isPrefixSize(size) && !config.requireSizePrefix && offset >= bytes.length) {
|
|
70
77
|
return [array, offset];
|
|
71
78
|
}
|
|
72
79
|
if (size === "remainder") {
|
|
@@ -77,6 +84,29 @@ function getArrayDecoder(item, config = {}) {
|
|
|
77
84
|
}
|
|
78
85
|
return [array, offset];
|
|
79
86
|
}
|
|
87
|
+
if (isSentinelSize(size)) {
|
|
88
|
+
const { sentinel, strategy = "required" } = size;
|
|
89
|
+
while (true) {
|
|
90
|
+
if (offset + sentinel.length > bytes.length) {
|
|
91
|
+
if (strategy === "required") {
|
|
92
|
+
throw new errors.SolanaError(errors.SOLANA_ERROR__CODECS__SENTINEL_MISSING_AT_END_OF_BYTES, {
|
|
93
|
+
codecDescription: config.description ?? "array",
|
|
94
|
+
hexSentinel: hexBytes(sentinel),
|
|
95
|
+
sentinel
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
if (codecsCore.containsBytes(bytes, sentinel, offset)) {
|
|
101
|
+
offset += sentinel.length;
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
const [value, newOffset2] = item.read(bytes, offset);
|
|
105
|
+
offset = newOffset2;
|
|
106
|
+
array.push(value);
|
|
107
|
+
}
|
|
108
|
+
return [array, offset];
|
|
109
|
+
}
|
|
80
110
|
const [resolvedSize, newOffset] = typeof size === "number" ? [size, offset] : size.read(bytes, offset);
|
|
81
111
|
offset = newOffset;
|
|
82
112
|
for (let i = 0; i < resolvedSize; i += 1) {
|
|
@@ -96,6 +126,20 @@ function computeArrayLikeCodecSize(size, itemSize) {
|
|
|
96
126
|
if (size === 0) return 0;
|
|
97
127
|
return itemSize === null ? null : itemSize * size;
|
|
98
128
|
}
|
|
129
|
+
function isPrefixSize(size) {
|
|
130
|
+
return typeof size === "object" && size !== null && !isSentinelSize(size);
|
|
131
|
+
}
|
|
132
|
+
function isSentinelSize(size) {
|
|
133
|
+
return typeof size === "object" && size !== null && "__kind" in size && size.__kind === "sentinel";
|
|
134
|
+
}
|
|
135
|
+
function assertValidSize(size) {
|
|
136
|
+
if (isSentinelSize(size) && size.sentinel.length === 0) {
|
|
137
|
+
throw new errors.SolanaError(errors.SOLANA_ERROR__CODECS__SENTINEL_MUST_NOT_BE_EMPTY);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function hexBytes(bytes) {
|
|
141
|
+
return bytes.reduce((str, byte) => str + byte.toString(16).padStart(2, "0"), "");
|
|
142
|
+
}
|
|
99
143
|
function getBitArrayEncoder(size, config = {}) {
|
|
100
144
|
const parsedConfig = typeof config === "boolean" ? { backward: config } : config;
|
|
101
145
|
const backward = parsedConfig.backward ?? false;
|