@bcts/uniform-resources 1.0.0-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +48 -0
- package/README.md +17 -0
- package/dist/index.cjs +8373 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +761 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +761 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.iife.js +8377 -0
- package/dist/index.iife.js.map +1 -0
- package/dist/index.mjs +8336 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +74 -0
- package/src/error.ts +88 -0
- package/src/fountain.ts +397 -0
- package/src/index.ts +61 -0
- package/src/multipart-decoder.ts +204 -0
- package/src/multipart-encoder.ts +166 -0
- package/src/ur-codable.ts +48 -0
- package/src/ur-decodable.ts +56 -0
- package/src/ur-encodable.ts +47 -0
- package/src/ur-type.ts +87 -0
- package/src/ur.ts +215 -0
- package/src/utils.ts +802 -0
- package/src/xoshiro.ts +180 -0
package/package.json
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bcts/uniform-resources",
|
|
3
|
+
"version": "1.0.0-alpha.10",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Blockchain Commons Uniform Resources (UR) for TypeScript",
|
|
6
|
+
"license": "BSD-2-Clause-Patent",
|
|
7
|
+
"author": "Leonardo Custodio <leonardo@custodio.me>",
|
|
8
|
+
"homepage": "https://bcts.dev",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/leonardocustodio/bcts",
|
|
12
|
+
"directory": "packages/uniform-resources"
|
|
13
|
+
},
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/leonardocustodio/bcts/issues"
|
|
16
|
+
},
|
|
17
|
+
"main": "dist/index.cjs",
|
|
18
|
+
"module": "dist/index.mjs",
|
|
19
|
+
"types": "dist/index.d.mts",
|
|
20
|
+
"browser": "dist/index.iife.js",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./dist/index.d.mts",
|
|
24
|
+
"import": "./dist/index.mjs",
|
|
25
|
+
"require": "./dist/index.cjs",
|
|
26
|
+
"default": "./dist/index.mjs"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist",
|
|
31
|
+
"src",
|
|
32
|
+
"README.md"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsdown",
|
|
36
|
+
"dev": "tsdown --watch",
|
|
37
|
+
"test": "vitest run",
|
|
38
|
+
"test:watch": "vitest",
|
|
39
|
+
"lint": "eslint 'src/**/*.ts'",
|
|
40
|
+
"lint:fix": "eslint 'src/**/*.ts' --fix",
|
|
41
|
+
"typecheck": "tsc --noEmit",
|
|
42
|
+
"clean": "rm -rf dist",
|
|
43
|
+
"docs": "typedoc"
|
|
44
|
+
},
|
|
45
|
+
"keywords": [
|
|
46
|
+
"uniform-resources",
|
|
47
|
+
"ur",
|
|
48
|
+
"blockchain-commons",
|
|
49
|
+
"cbor",
|
|
50
|
+
"dcbor",
|
|
51
|
+
"encoding",
|
|
52
|
+
"deterministic"
|
|
53
|
+
],
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=18.0.0"
|
|
56
|
+
},
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"@bcts/dcbor": "^1.0.0-alpha.10"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@bcts/eslint": "^0.1.0",
|
|
62
|
+
"@bcts/tsconfig": "^0.1.0",
|
|
63
|
+
"@eslint/js": "^9.39.2",
|
|
64
|
+
"@types/node": "^25.0.3",
|
|
65
|
+
"@types/pako": "^2.0.4",
|
|
66
|
+
"eslint": "^9.39.2",
|
|
67
|
+
"prettier": "^3.7.4",
|
|
68
|
+
"ts-node": "^10.9.2",
|
|
69
|
+
"tsdown": "^0.18.3",
|
|
70
|
+
"typedoc": "^0.28.15",
|
|
71
|
+
"typescript": "^5.9.3",
|
|
72
|
+
"vitest": "^4.0.16"
|
|
73
|
+
}
|
|
74
|
+
}
|
package/src/error.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error type for UR encoding/decoding operations.
|
|
3
|
+
*/
|
|
4
|
+
export class URError extends Error {
|
|
5
|
+
constructor(message: string) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "URError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Error type for invalid UR schemes.
|
|
13
|
+
*/
|
|
14
|
+
export class InvalidSchemeError extends URError {
|
|
15
|
+
constructor() {
|
|
16
|
+
super("Invalid UR scheme");
|
|
17
|
+
this.name = "InvalidSchemeError";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Error type for unspecified UR types.
|
|
23
|
+
*/
|
|
24
|
+
export class TypeUnspecifiedError extends URError {
|
|
25
|
+
constructor() {
|
|
26
|
+
super("No UR type specified");
|
|
27
|
+
this.name = "TypeUnspecifiedError";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Error type for invalid UR types.
|
|
33
|
+
*/
|
|
34
|
+
export class InvalidTypeError extends URError {
|
|
35
|
+
constructor() {
|
|
36
|
+
super("Invalid UR type");
|
|
37
|
+
this.name = "InvalidTypeError";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Error type for non-single-part URs.
|
|
43
|
+
*/
|
|
44
|
+
export class NotSinglePartError extends URError {
|
|
45
|
+
constructor() {
|
|
46
|
+
super("UR is not a single-part");
|
|
47
|
+
this.name = "NotSinglePartError";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Error type for unexpected UR types.
|
|
53
|
+
*/
|
|
54
|
+
export class UnexpectedTypeError extends URError {
|
|
55
|
+
constructor(expected: string, found: string) {
|
|
56
|
+
super(`Expected UR type ${expected}, but found ${found}`);
|
|
57
|
+
this.name = "UnexpectedTypeError";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Error type for Bytewords encoding/decoding errors.
|
|
63
|
+
*/
|
|
64
|
+
export class BytewordsError extends URError {
|
|
65
|
+
constructor(message: string) {
|
|
66
|
+
super(`Bytewords error: ${message}`);
|
|
67
|
+
this.name = "BytewordsError";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Error type for CBOR encoding/decoding errors.
|
|
73
|
+
*/
|
|
74
|
+
export class CBORError extends URError {
|
|
75
|
+
constructor(message: string) {
|
|
76
|
+
super(`CBOR error: ${message}`);
|
|
77
|
+
this.name = "CBORError";
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export type Result<T> = T | Error;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Helper function to check if a result is an error.
|
|
85
|
+
*/
|
|
86
|
+
export function isError(result: unknown): result is Error {
|
|
87
|
+
return result instanceof Error;
|
|
88
|
+
}
|
package/src/fountain.ts
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fountain code implementation for multipart URs.
|
|
3
|
+
*
|
|
4
|
+
* This implements a hybrid fixed-rate and rateless fountain code system
|
|
5
|
+
* as specified in BCR-2020-005 and BCR-2024-001.
|
|
6
|
+
*
|
|
7
|
+
* Key concepts:
|
|
8
|
+
* - Parts 1-seqLen are "pure" fragments (fixed-rate)
|
|
9
|
+
* - Parts > seqLen are "mixed" fragments using XOR (rateless)
|
|
10
|
+
* - Xoshiro256** PRNG ensures encoder/decoder agree on mixing
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { Xoshiro256, createSeed } from "./xoshiro.js";
|
|
14
|
+
import { crc32 } from "./utils.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Represents a fountain code part with metadata.
|
|
18
|
+
*/
|
|
19
|
+
export interface FountainPart {
|
|
20
|
+
/** Sequence number (1-based) */
|
|
21
|
+
seqNum: number;
|
|
22
|
+
/** Total number of pure fragments */
|
|
23
|
+
seqLen: number;
|
|
24
|
+
/** Length of original message */
|
|
25
|
+
messageLen: number;
|
|
26
|
+
/** CRC32 checksum of original message */
|
|
27
|
+
checksum: number;
|
|
28
|
+
/** Fragment data */
|
|
29
|
+
data: Uint8Array;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Splits data into fragments of the specified size.
|
|
34
|
+
*/
|
|
35
|
+
export function splitMessage(message: Uint8Array, fragmentLen: number): Uint8Array[] {
|
|
36
|
+
const fragments: Uint8Array[] = [];
|
|
37
|
+
const fragmentCount = Math.ceil(message.length / fragmentLen);
|
|
38
|
+
|
|
39
|
+
for (let i = 0; i < fragmentCount; i++) {
|
|
40
|
+
const start = i * fragmentLen;
|
|
41
|
+
const end = Math.min(start + fragmentLen, message.length);
|
|
42
|
+
const fragment = new Uint8Array(fragmentLen);
|
|
43
|
+
|
|
44
|
+
// Copy data and pad with zeros if needed
|
|
45
|
+
const sourceSlice = message.slice(start, end);
|
|
46
|
+
fragment.set(sourceSlice);
|
|
47
|
+
|
|
48
|
+
fragments.push(fragment);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return fragments;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* XOR two Uint8Arrays together.
|
|
56
|
+
*/
|
|
57
|
+
export function xorBytes(a: Uint8Array, b: Uint8Array): Uint8Array {
|
|
58
|
+
const len = Math.max(a.length, b.length);
|
|
59
|
+
const result = new Uint8Array(len);
|
|
60
|
+
|
|
61
|
+
for (let i = 0; i < len; i++) {
|
|
62
|
+
result[i] = (a[i] ?? 0) ^ (b[i] ?? 0);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Chooses which fragments to mix for a given sequence number.
|
|
70
|
+
*
|
|
71
|
+
* This uses a seeded Xoshiro256** PRNG to deterministically select fragments,
|
|
72
|
+
* ensuring encoder and decoder agree without explicit coordination.
|
|
73
|
+
*
|
|
74
|
+
* @param seqNum - The sequence number (1-based)
|
|
75
|
+
* @param seqLen - Total number of pure fragments
|
|
76
|
+
* @param checksum - CRC32 checksum of the message
|
|
77
|
+
* @returns Array of fragment indices (0-based)
|
|
78
|
+
*/
|
|
79
|
+
export function chooseFragments(seqNum: number, seqLen: number, checksum: number): number[] {
|
|
80
|
+
// Pure parts (seqNum <= seqLen) contain exactly one fragment
|
|
81
|
+
if (seqNum <= seqLen) {
|
|
82
|
+
return [seqNum - 1];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Mixed parts use PRNG to select fragments
|
|
86
|
+
const seed = createSeed(checksum, seqNum);
|
|
87
|
+
const rng = new Xoshiro256(seed);
|
|
88
|
+
|
|
89
|
+
// Choose degree (number of fragments to mix)
|
|
90
|
+
// Uses a simplified soliton distribution
|
|
91
|
+
const degree = chooseDegree(rng, seqLen);
|
|
92
|
+
|
|
93
|
+
// Choose which fragments to include
|
|
94
|
+
const indices = new Set<number>();
|
|
95
|
+
while (indices.size < degree) {
|
|
96
|
+
const index = rng.nextInt(0, seqLen);
|
|
97
|
+
indices.add(index);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return Array.from(indices).sort((a, b) => a - b);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Chooses the degree (number of fragments to mix) using a simplified
|
|
105
|
+
* robust soliton distribution.
|
|
106
|
+
*
|
|
107
|
+
* This ensures good coverage of fragments for efficient decoding.
|
|
108
|
+
*/
|
|
109
|
+
function chooseDegree(rng: Xoshiro256, seqLen: number): number {
|
|
110
|
+
// Use a simplified distribution that tends toward lower degrees
|
|
111
|
+
// but can occasionally include more fragments
|
|
112
|
+
const r = rng.nextDouble();
|
|
113
|
+
|
|
114
|
+
// Probability distribution favoring lower degrees
|
|
115
|
+
// Based on robust soliton distribution
|
|
116
|
+
if (r < 0.5) {
|
|
117
|
+
return 1;
|
|
118
|
+
} else if (r < 0.75) {
|
|
119
|
+
return 2;
|
|
120
|
+
} else if (r < 0.9) {
|
|
121
|
+
return Math.min(3, seqLen);
|
|
122
|
+
} else {
|
|
123
|
+
// Higher degrees are less common but help with convergence
|
|
124
|
+
return Math.min(rng.nextInt(4, seqLen + 1), seqLen);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Mixes the selected fragments using XOR.
|
|
130
|
+
*/
|
|
131
|
+
export function mixFragments(fragments: Uint8Array[], indices: number[]): Uint8Array {
|
|
132
|
+
if (indices.length === 0) {
|
|
133
|
+
throw new Error("No fragments to mix");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let result: Uint8Array = new Uint8Array(fragments[0].length);
|
|
137
|
+
|
|
138
|
+
for (const index of indices) {
|
|
139
|
+
const fragment = fragments[index];
|
|
140
|
+
if (fragment === undefined) {
|
|
141
|
+
throw new Error(`Fragment at index ${index} not found`);
|
|
142
|
+
}
|
|
143
|
+
result = xorBytes(result, fragment);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Fountain encoder for creating multipart URs.
|
|
151
|
+
*/
|
|
152
|
+
export class FountainEncoder {
|
|
153
|
+
private readonly fragments: Uint8Array[];
|
|
154
|
+
private readonly messageLen: number;
|
|
155
|
+
private readonly checksum: number;
|
|
156
|
+
private seqNum = 0;
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Creates a fountain encoder for the given message.
|
|
160
|
+
*
|
|
161
|
+
* @param message - The message to encode
|
|
162
|
+
* @param maxFragmentLen - Maximum length of each fragment
|
|
163
|
+
*/
|
|
164
|
+
constructor(message: Uint8Array, maxFragmentLen: number) {
|
|
165
|
+
if (maxFragmentLen < 1) {
|
|
166
|
+
throw new Error("Fragment length must be at least 1");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
this.messageLen = message.length;
|
|
170
|
+
this.checksum = crc32(message);
|
|
171
|
+
this.fragments = splitMessage(message, maxFragmentLen);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Returns the number of pure fragments.
|
|
176
|
+
*/
|
|
177
|
+
get seqLen(): number {
|
|
178
|
+
return this.fragments.length;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Returns whether the message fits in a single part.
|
|
183
|
+
*/
|
|
184
|
+
isSinglePart(): boolean {
|
|
185
|
+
return this.fragments.length === 1;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Returns whether all pure parts have been emitted.
|
|
190
|
+
*/
|
|
191
|
+
isComplete(): boolean {
|
|
192
|
+
return this.seqNum >= this.seqLen;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Generates the next fountain part.
|
|
197
|
+
*/
|
|
198
|
+
nextPart(): FountainPart {
|
|
199
|
+
this.seqNum++;
|
|
200
|
+
|
|
201
|
+
const indices = chooseFragments(this.seqNum, this.seqLen, this.checksum);
|
|
202
|
+
const data = mixFragments(this.fragments, indices);
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
seqNum: this.seqNum,
|
|
206
|
+
seqLen: this.seqLen,
|
|
207
|
+
messageLen: this.messageLen,
|
|
208
|
+
checksum: this.checksum,
|
|
209
|
+
data,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Returns the current sequence number.
|
|
215
|
+
*/
|
|
216
|
+
currentSeqNum(): number {
|
|
217
|
+
return this.seqNum;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Resets the encoder to start from the beginning.
|
|
222
|
+
*/
|
|
223
|
+
reset(): void {
|
|
224
|
+
this.seqNum = 0;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Fountain decoder for reassembling multipart URs.
|
|
230
|
+
*/
|
|
231
|
+
export class FountainDecoder {
|
|
232
|
+
private seqLen: number | null = null;
|
|
233
|
+
private messageLen: number | null = null;
|
|
234
|
+
private checksum: number | null = null;
|
|
235
|
+
|
|
236
|
+
// Storage for received data
|
|
237
|
+
private readonly pureFragments = new Map<number, Uint8Array>();
|
|
238
|
+
private readonly mixedParts = new Map<number, { indices: number[]; data: Uint8Array }>();
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Receives a fountain part and attempts to decode.
|
|
242
|
+
*
|
|
243
|
+
* @param part - The fountain part to receive
|
|
244
|
+
* @returns true if the message is now complete
|
|
245
|
+
*/
|
|
246
|
+
receive(part: FountainPart): boolean {
|
|
247
|
+
// Initialize on first part
|
|
248
|
+
if (this.seqLen === null) {
|
|
249
|
+
this.seqLen = part.seqLen;
|
|
250
|
+
this.messageLen = part.messageLen;
|
|
251
|
+
this.checksum = part.checksum;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Validate consistency
|
|
255
|
+
if (
|
|
256
|
+
part.seqLen !== this.seqLen ||
|
|
257
|
+
part.messageLen !== this.messageLen ||
|
|
258
|
+
part.checksum !== this.checksum
|
|
259
|
+
) {
|
|
260
|
+
throw new Error("Inconsistent part metadata");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Determine which fragments this part contains
|
|
264
|
+
const indices = chooseFragments(part.seqNum, this.seqLen, this.checksum);
|
|
265
|
+
|
|
266
|
+
if (indices.length === 1) {
|
|
267
|
+
// Pure fragment
|
|
268
|
+
const index = indices[0];
|
|
269
|
+
if (!this.pureFragments.has(index)) {
|
|
270
|
+
this.pureFragments.set(index, part.data);
|
|
271
|
+
}
|
|
272
|
+
} else {
|
|
273
|
+
// Mixed fragment - store for later reduction
|
|
274
|
+
if (!this.mixedParts.has(part.seqNum)) {
|
|
275
|
+
this.mixedParts.set(part.seqNum, { indices, data: part.data });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Try to reduce mixed parts
|
|
280
|
+
this.reduceMixedParts();
|
|
281
|
+
|
|
282
|
+
return this.isComplete();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Attempts to extract pure fragments from mixed parts.
|
|
287
|
+
*/
|
|
288
|
+
private reduceMixedParts(): void {
|
|
289
|
+
let progress = true;
|
|
290
|
+
|
|
291
|
+
while (progress) {
|
|
292
|
+
progress = false;
|
|
293
|
+
|
|
294
|
+
for (const [seqNum, mixed] of this.mixedParts) {
|
|
295
|
+
// Find which indices we're missing
|
|
296
|
+
const missing: number[] = [];
|
|
297
|
+
let reduced = mixed.data;
|
|
298
|
+
|
|
299
|
+
for (const index of mixed.indices) {
|
|
300
|
+
const pure = this.pureFragments.get(index);
|
|
301
|
+
if (pure !== undefined) {
|
|
302
|
+
// XOR out the known fragment
|
|
303
|
+
reduced = xorBytes(reduced, pure);
|
|
304
|
+
} else {
|
|
305
|
+
missing.push(index);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (missing.length === 0) {
|
|
310
|
+
// All fragments known, remove this mixed part
|
|
311
|
+
this.mixedParts.delete(seqNum);
|
|
312
|
+
progress = true;
|
|
313
|
+
} else if (missing.length === 1) {
|
|
314
|
+
// Can extract the missing fragment
|
|
315
|
+
const missingIndex = missing[0];
|
|
316
|
+
this.pureFragments.set(missingIndex, reduced);
|
|
317
|
+
this.mixedParts.delete(seqNum);
|
|
318
|
+
progress = true;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Returns whether all fragments have been received.
|
|
326
|
+
*/
|
|
327
|
+
isComplete(): boolean {
|
|
328
|
+
if (this.seqLen === null) {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return this.pureFragments.size === this.seqLen;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Reconstructs the original message.
|
|
337
|
+
*
|
|
338
|
+
* @returns The original message, or null if not yet complete
|
|
339
|
+
*/
|
|
340
|
+
message(): Uint8Array | null {
|
|
341
|
+
if (!this.isComplete() || this.seqLen === null || this.messageLen === null) {
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Calculate fragment size from first fragment
|
|
346
|
+
const firstFragment = this.pureFragments.get(0);
|
|
347
|
+
if (firstFragment === undefined) {
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const fragmentLen = firstFragment.length;
|
|
352
|
+
const result = new Uint8Array(this.messageLen);
|
|
353
|
+
|
|
354
|
+
// Assemble fragments
|
|
355
|
+
for (let i = 0; i < this.seqLen; i++) {
|
|
356
|
+
const fragment = this.pureFragments.get(i);
|
|
357
|
+
if (fragment === undefined) {
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const start = i * fragmentLen;
|
|
362
|
+
const end = Math.min(start + fragmentLen, this.messageLen);
|
|
363
|
+
const len = end - start;
|
|
364
|
+
|
|
365
|
+
result.set(fragment.slice(0, len), start);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Verify checksum
|
|
369
|
+
const actualChecksum = crc32(result);
|
|
370
|
+
if (actualChecksum !== this.checksum) {
|
|
371
|
+
throw new Error(`Checksum mismatch: expected ${this.checksum}, got ${actualChecksum}`);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return result;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Returns the progress as a fraction (0 to 1).
|
|
379
|
+
*/
|
|
380
|
+
progress(): number {
|
|
381
|
+
if (this.seqLen === null) {
|
|
382
|
+
return 0;
|
|
383
|
+
}
|
|
384
|
+
return this.pureFragments.size / this.seqLen;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Resets the decoder.
|
|
389
|
+
*/
|
|
390
|
+
reset(): void {
|
|
391
|
+
this.seqLen = null;
|
|
392
|
+
this.messageLen = null;
|
|
393
|
+
this.checksum = null;
|
|
394
|
+
this.pureFragments.clear();
|
|
395
|
+
this.mixedParts.clear();
|
|
396
|
+
}
|
|
397
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Core types
|
|
2
|
+
export { UR } from "./ur";
|
|
3
|
+
export { URType } from "./ur-type";
|
|
4
|
+
|
|
5
|
+
// Error types
|
|
6
|
+
export {
|
|
7
|
+
URError,
|
|
8
|
+
InvalidSchemeError,
|
|
9
|
+
TypeUnspecifiedError,
|
|
10
|
+
InvalidTypeError,
|
|
11
|
+
NotSinglePartError,
|
|
12
|
+
UnexpectedTypeError,
|
|
13
|
+
BytewordsError,
|
|
14
|
+
CBORError,
|
|
15
|
+
isError,
|
|
16
|
+
} from "./error";
|
|
17
|
+
|
|
18
|
+
export type { Result } from "./error";
|
|
19
|
+
|
|
20
|
+
// Traits/Interfaces
|
|
21
|
+
export { isUREncodable } from "./ur-encodable";
|
|
22
|
+
export type { UREncodable } from "./ur-encodable";
|
|
23
|
+
export { isURDecodable } from "./ur-decodable";
|
|
24
|
+
export type { URDecodable } from "./ur-decodable";
|
|
25
|
+
export { isURCodable } from "./ur-codable";
|
|
26
|
+
export type { URCodable } from "./ur-codable";
|
|
27
|
+
|
|
28
|
+
// Multipart encoding/decoding
|
|
29
|
+
export { MultipartEncoder } from "./multipart-encoder";
|
|
30
|
+
export { MultipartDecoder } from "./multipart-decoder";
|
|
31
|
+
|
|
32
|
+
// Fountain codes (for advanced multipart handling)
|
|
33
|
+
export {
|
|
34
|
+
FountainEncoder,
|
|
35
|
+
FountainDecoder,
|
|
36
|
+
splitMessage,
|
|
37
|
+
xorBytes,
|
|
38
|
+
chooseFragments,
|
|
39
|
+
mixFragments,
|
|
40
|
+
} from "./fountain";
|
|
41
|
+
export type { FountainPart } from "./fountain";
|
|
42
|
+
|
|
43
|
+
// PRNG for deterministic fountain code mixing
|
|
44
|
+
export { Xoshiro256, createSeed } from "./xoshiro";
|
|
45
|
+
|
|
46
|
+
// Utilities
|
|
47
|
+
export {
|
|
48
|
+
isURTypeChar,
|
|
49
|
+
isValidURType,
|
|
50
|
+
validateURType,
|
|
51
|
+
BYTEWORDS,
|
|
52
|
+
BYTEWORDS_MAP,
|
|
53
|
+
BYTEMOJIS,
|
|
54
|
+
encodeBytewordsIdentifier,
|
|
55
|
+
encodeBytemojisIdentifier,
|
|
56
|
+
BytewordsStyle,
|
|
57
|
+
encodeBytewords,
|
|
58
|
+
decodeBytewords,
|
|
59
|
+
crc32,
|
|
60
|
+
MINIMAL_BYTEWORDS_MAP,
|
|
61
|
+
} from "./utils";
|