@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.
@@ -0,0 +1,204 @@
1
+ import { decodeCbor } from "@bcts/dcbor";
2
+ import { InvalidSchemeError, InvalidTypeError, UnexpectedTypeError, URError } from "./error.js";
3
+ import { UR } from "./ur.js";
4
+ import { URType } from "./ur-type.js";
5
+ import { FountainDecoder, type FountainPart } from "./fountain.js";
6
+ import { decodeBytewords, BytewordsStyle } from "./utils.js";
7
+
8
+ /**
9
+ * Decodes multiple UR parts back into a single UR.
10
+ *
11
+ * This reassembles multipart URs that were encoded using fountain codes.
12
+ * The decoder can handle out-of-order reception and packet loss.
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * const decoder = new MultipartDecoder();
17
+ *
18
+ * for (const urPart of urParts) {
19
+ * decoder.receive(urPart);
20
+ * if (decoder.isComplete()) {
21
+ * const ur = decoder.message();
22
+ * break;
23
+ * }
24
+ * }
25
+ * ```
26
+ */
27
+ export class MultipartDecoder {
28
+ private _urType: URType | null = null;
29
+ private _fountainDecoder: FountainDecoder | null = null;
30
+ private _decodedMessage: UR | null = null;
31
+
32
+ /**
33
+ * Receives a UR part string.
34
+ *
35
+ * @param part - A UR part string (e.g., "ur:bytes/1-10/..." or "ur:bytes/...")
36
+ * @throws {InvalidSchemeError} If the part doesn't start with "ur:"
37
+ * @throws {UnexpectedTypeError} If the type doesn't match previous parts
38
+ */
39
+ receive(part: string): void {
40
+ const { urType, partInfo } = this._parsePart(part);
41
+
42
+ // Validate type consistency
43
+ if (this._urType === null) {
44
+ this._urType = urType;
45
+ } else if (!this._urType.equals(urType)) {
46
+ throw new UnexpectedTypeError(this._urType.string(), urType.string());
47
+ }
48
+
49
+ // Handle the part
50
+ if (partInfo.isSinglePart) {
51
+ // Single-part UR - decode immediately
52
+ this._decodedMessage = UR.fromURString(part);
53
+ } else {
54
+ // Multipart UR - use fountain decoder
55
+ this._fountainDecoder ??= new FountainDecoder();
56
+
57
+ const fountainPart = this._decodeFountainPart(partInfo);
58
+ this._fountainDecoder.receive(fountainPart);
59
+
60
+ // Try to get the complete message
61
+ if (this._fountainDecoder.isComplete()) {
62
+ const message = this._fountainDecoder.message();
63
+ if (message !== null) {
64
+ const cbor = decodeCbor(message);
65
+ this._decodedMessage = UR.new(this._urType, cbor);
66
+ }
67
+ }
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Parses a UR part string to extract type and part info.
73
+ */
74
+ private _parsePart(part: string): { urType: URType; partInfo: PartInfo } {
75
+ const lowercased = part.toLowerCase();
76
+
77
+ if (!lowercased.startsWith("ur:")) {
78
+ throw new InvalidSchemeError();
79
+ }
80
+
81
+ const afterScheme = lowercased.substring(3);
82
+ const components = afterScheme.split("/");
83
+
84
+ if (components.length === 0 || components[0] === "") {
85
+ throw new InvalidTypeError();
86
+ }
87
+
88
+ const urType = new URType(components[0]);
89
+
90
+ // Check if this is a multipart UR (format: type/seqNum-seqLen/data)
91
+ if (components.length >= 3) {
92
+ const seqPart = components[1];
93
+ const seqMatch = /^(\d+)-(\d+)$/.exec(seqPart);
94
+
95
+ if (seqMatch !== null) {
96
+ const seqNum = parseInt(seqMatch[1], 10);
97
+ const seqLen = parseInt(seqMatch[2], 10);
98
+ const encodedData = components.slice(2).join("/");
99
+
100
+ return {
101
+ urType,
102
+ partInfo: {
103
+ isSinglePart: false,
104
+ seqNum,
105
+ seqLen,
106
+ encodedData,
107
+ },
108
+ };
109
+ }
110
+ }
111
+
112
+ // Single-part UR
113
+ return {
114
+ urType,
115
+ partInfo: { isSinglePart: true },
116
+ };
117
+ }
118
+
119
+ /**
120
+ * Decodes a multipart UR's fountain part data.
121
+ */
122
+ private _decodeFountainPart(partInfo: MultipartInfo): FountainPart {
123
+ // Decode bytewords
124
+ const rawData = decodeBytewords(partInfo.encodedData, BytewordsStyle.Minimal);
125
+
126
+ if (rawData.length < 8) {
127
+ throw new URError("Invalid multipart data: too short");
128
+ }
129
+
130
+ // Extract metadata
131
+ const messageLen =
132
+ ((rawData[0] << 24) | (rawData[1] << 16) | (rawData[2] << 8) | rawData[3]) >>> 0;
133
+
134
+ const checksum =
135
+ ((rawData[4] << 24) | (rawData[5] << 16) | (rawData[6] << 8) | rawData[7]) >>> 0;
136
+
137
+ const data = rawData.slice(8);
138
+
139
+ return {
140
+ seqNum: partInfo.seqNum,
141
+ seqLen: partInfo.seqLen,
142
+ messageLen,
143
+ checksum,
144
+ data,
145
+ };
146
+ }
147
+
148
+ /**
149
+ * Checks if the message is complete.
150
+ */
151
+ isComplete(): boolean {
152
+ return this._decodedMessage !== null;
153
+ }
154
+
155
+ /**
156
+ * Gets the decoded UR message.
157
+ *
158
+ * @returns The decoded UR, or null if not yet complete
159
+ */
160
+ message(): UR | null {
161
+ return this._decodedMessage;
162
+ }
163
+
164
+ /**
165
+ * Returns the decoding progress as a fraction (0 to 1).
166
+ */
167
+ progress(): number {
168
+ if (this._decodedMessage !== null) {
169
+ return 1;
170
+ }
171
+ if (this._fountainDecoder === null) {
172
+ return 0;
173
+ }
174
+ return this._fountainDecoder.progress();
175
+ }
176
+
177
+ /**
178
+ * Resets the decoder to receive a new message.
179
+ */
180
+ reset(): void {
181
+ this._urType = null;
182
+ this._fountainDecoder = null;
183
+ this._decodedMessage = null;
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Part info for single-part URs.
189
+ */
190
+ interface SinglePartInfo {
191
+ isSinglePart: true;
192
+ }
193
+
194
+ /**
195
+ * Part info for multipart URs.
196
+ */
197
+ interface MultipartInfo {
198
+ isSinglePart: false;
199
+ seqNum: number;
200
+ seqLen: number;
201
+ encodedData: string;
202
+ }
203
+
204
+ type PartInfo = SinglePartInfo | MultipartInfo;
@@ -0,0 +1,166 @@
1
+ import type { UR } from "./ur.js";
2
+ import { URError } from "./error.js";
3
+ import { FountainEncoder, type FountainPart } from "./fountain.js";
4
+ import { encodeBytewords, BytewordsStyle } from "./utils.js";
5
+
6
+ /**
7
+ * Encodes a UR as multiple parts using fountain codes.
8
+ *
9
+ * This allows large CBOR structures to be split into multiple UR strings
10
+ * that can be transmitted separately and reassembled. The encoder uses
11
+ * fountain codes for resilient transmission over lossy channels.
12
+ *
13
+ * For single-part URs (small payloads), use the regular UR.string() method.
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * const ur = UR.new('bytes', cbor);
18
+ * const encoder = new MultipartEncoder(ur, 100);
19
+ *
20
+ * // Generate all pure parts
21
+ * while (!encoder.isComplete()) {
22
+ * const part = encoder.nextPart();
23
+ * console.log(part); // "ur:bytes/1-10/..."
24
+ * }
25
+ *
26
+ * // Generate additional rateless parts for redundancy
27
+ * for (let i = 0; i < 5; i++) {
28
+ * const part = encoder.nextPart();
29
+ * console.log(part); // "ur:bytes/11-10/..."
30
+ * }
31
+ * ```
32
+ */
33
+ export class MultipartEncoder {
34
+ private readonly _ur: UR;
35
+ private readonly _fountainEncoder: FountainEncoder;
36
+ private _currentIndex = 0;
37
+
38
+ /**
39
+ * Creates a new multipart encoder for the given UR.
40
+ *
41
+ * @param ur - The UR to encode
42
+ * @param maxFragmentLen - Maximum length of each fragment in bytes
43
+ * @throws {URError} If encoding fails
44
+ *
45
+ * @example
46
+ * ```typescript
47
+ * const encoder = new MultipartEncoder(ur, 100);
48
+ * ```
49
+ */
50
+ constructor(ur: UR, maxFragmentLen: number) {
51
+ if (maxFragmentLen < 1) {
52
+ throw new URError("Max fragment length must be at least 1");
53
+ }
54
+ this._ur = ur;
55
+
56
+ // Create fountain encoder from CBOR data
57
+ const cborData = ur.cbor().toData();
58
+ this._fountainEncoder = new FountainEncoder(cborData, maxFragmentLen);
59
+ }
60
+
61
+ /**
62
+ * Returns whether the message fits in a single part.
63
+ *
64
+ * For single-part messages, consider using UR.string() directly.
65
+ */
66
+ isSinglePart(): boolean {
67
+ return this._fountainEncoder.isSinglePart();
68
+ }
69
+
70
+ /**
71
+ * Gets the next part of the encoding.
72
+ *
73
+ * Parts 1 through seqLen are "pure" fragments containing one piece each.
74
+ * Parts beyond seqLen are "mixed" fragments using fountain codes for redundancy.
75
+ *
76
+ * @returns The next UR string part
77
+ *
78
+ * @example
79
+ * ```typescript
80
+ * const part = encoder.nextPart();
81
+ * // Returns: "ur:bytes/1-3/lsadaoaxjygonesw"
82
+ * ```
83
+ */
84
+ nextPart(): string {
85
+ const part = this._fountainEncoder.nextPart();
86
+ this._currentIndex++;
87
+ return this._encodePart(part);
88
+ }
89
+
90
+ /**
91
+ * Encodes a fountain part as a UR string.
92
+ */
93
+ private _encodePart(part: FountainPart): string {
94
+ // For single-part messages, use simple format
95
+ if (part.seqLen === 1) {
96
+ return this._ur.string();
97
+ }
98
+
99
+ // Encode the part data as CBOR: [seqNum, seqLen, messageLen, checksum, data]
100
+ // Using a simple format: seqNum-seqLen prefix followed by bytewords-encoded part
101
+ const partData = this._encodePartData(part);
102
+ const encoded = encodeBytewords(partData, BytewordsStyle.Minimal);
103
+
104
+ return `ur:${this._ur.urTypeStr()}/${part.seqNum}-${part.seqLen}/${encoded}`;
105
+ }
106
+
107
+ /**
108
+ * Encodes part metadata and data into bytes for bytewords encoding.
109
+ */
110
+ private _encodePartData(part: FountainPart): Uint8Array {
111
+ // Simple encoding: messageLen (4 bytes) + checksum (4 bytes) + data
112
+ const result = new Uint8Array(8 + part.data.length);
113
+
114
+ // Message length (big-endian)
115
+ result[0] = (part.messageLen >>> 24) & 0xff;
116
+ result[1] = (part.messageLen >>> 16) & 0xff;
117
+ result[2] = (part.messageLen >>> 8) & 0xff;
118
+ result[3] = part.messageLen & 0xff;
119
+
120
+ // Checksum (big-endian)
121
+ result[4] = (part.checksum >>> 24) & 0xff;
122
+ result[5] = (part.checksum >>> 16) & 0xff;
123
+ result[6] = (part.checksum >>> 8) & 0xff;
124
+ result[7] = part.checksum & 0xff;
125
+
126
+ // Fragment data
127
+ result.set(part.data, 8);
128
+
129
+ return result;
130
+ }
131
+
132
+ /**
133
+ * Gets the current part index.
134
+ */
135
+ currentIndex(): number {
136
+ return this._currentIndex;
137
+ }
138
+
139
+ /**
140
+ * Gets the total number of pure parts.
141
+ *
142
+ * Note: Fountain codes can generate unlimited parts beyond this count
143
+ * for additional redundancy.
144
+ */
145
+ partsCount(): number {
146
+ return this._fountainEncoder.seqLen;
147
+ }
148
+
149
+ /**
150
+ * Checks if all pure parts have been emitted.
151
+ *
152
+ * Even after this returns true, you can continue calling nextPart()
153
+ * to generate additional rateless parts for redundancy.
154
+ */
155
+ isComplete(): boolean {
156
+ return this._fountainEncoder.isComplete();
157
+ }
158
+
159
+ /**
160
+ * Resets the encoder to start from the beginning.
161
+ */
162
+ reset(): void {
163
+ this._currentIndex = 0;
164
+ this._fountainEncoder.reset();
165
+ }
166
+ }
@@ -0,0 +1,48 @@
1
+ import type { UREncodable } from "./ur-encodable.js";
2
+ import type { URDecodable } from "./ur-decodable.js";
3
+
4
+ /**
5
+ * A type that can be both encoded to and decoded from a UR.
6
+ *
7
+ * This combines the UREncodable and URDecodable interfaces for types
8
+ * that support bidirectional UR conversion.
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * class MyType implements URCodable {
13
+ * ur(): UR {
14
+ * // Encode to UR
15
+ * }
16
+ *
17
+ * urString(): string {
18
+ * // Return UR string
19
+ * }
20
+ *
21
+ * fromUR(ur: UR): MyType {
22
+ * // Decode from UR
23
+ * }
24
+ *
25
+ * fromURString(urString: string): MyType {
26
+ * // Decode from UR string (convenience method)
27
+ * return this.fromUR(UR.fromURString(urString));
28
+ * }
29
+ * }
30
+ * ```
31
+ */
32
+ export interface URCodable extends UREncodable, URDecodable {}
33
+
34
+ /**
35
+ * Helper function to check if an object implements URCodable.
36
+ */
37
+ export function isURCodable(obj: unknown): obj is URCodable {
38
+ return (
39
+ typeof obj === "object" &&
40
+ obj !== null &&
41
+ "ur" in obj &&
42
+ "urString" in obj &&
43
+ "fromUR" in obj &&
44
+ typeof (obj as Record<string, unknown>)["ur"] === "function" &&
45
+ typeof (obj as Record<string, unknown>)["urString"] === "function" &&
46
+ typeof (obj as Record<string, unknown>)["fromUR"] === "function"
47
+ );
48
+ }
@@ -0,0 +1,56 @@
1
+ import type { UR } from "./ur.js";
2
+
3
+ /**
4
+ * A type that can be decoded from a UR (Uniform Resource).
5
+ *
6
+ * Types implementing this interface should be able to create themselves
7
+ * from a UR containing their data.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * class MyType implements URDecodable {
12
+ * fromUR(ur: UR): MyType {
13
+ * const cbor = ur.cbor();
14
+ * // Decode from CBOR and return MyType instance
15
+ * }
16
+ *
17
+ * fromURString(urString: string): MyType {
18
+ * return this.fromUR(UR.fromURString(urString));
19
+ * }
20
+ * }
21
+ * ```
22
+ */
23
+ export interface URDecodable {
24
+ /**
25
+ * Creates an instance of this type from a UR.
26
+ *
27
+ * @param ur - The UR to decode from
28
+ * @returns An instance of this type
29
+ * @throws If the UR type is wrong or data is malformed
30
+ */
31
+ fromUR(ur: UR): unknown;
32
+
33
+ /**
34
+ * Creates an instance of this type from a UR string.
35
+ *
36
+ * This is a convenience method that parses the UR string and then
37
+ * calls fromUR().
38
+ *
39
+ * @param urString - The UR string to decode from (e.g., "ur:type/...")
40
+ * @returns An instance of this type
41
+ * @throws If the UR string is invalid or data is malformed
42
+ */
43
+ fromURString?(urString: string): unknown;
44
+ }
45
+
46
+ /**
47
+ * Helper function to check if an object implements URDecodable.
48
+ */
49
+ export function isURDecodable(obj: unknown): obj is URDecodable {
50
+ return (
51
+ typeof obj === "object" &&
52
+ obj !== null &&
53
+ "fromUR" in obj &&
54
+ typeof (obj as Record<string, unknown>)["fromUR"] === "function"
55
+ );
56
+ }
@@ -0,0 +1,47 @@
1
+ import type { UR } from "./ur.js";
2
+
3
+ /**
4
+ * A type that can be encoded to a UR (Uniform Resource).
5
+ *
6
+ * Types implementing this interface should be able to convert themselves
7
+ * to CBOR data and associate that with a UR type identifier.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * class MyType implements UREncodable {
12
+ * toCBOR(): CBOR {
13
+ * // Convert to CBOR
14
+ * }
15
+ *
16
+ * ur(): UR {
17
+ * const cbor = this.toCBOR();
18
+ * return UR.new('mytype', cbor);
19
+ * }
20
+ * }
21
+ * ```
22
+ */
23
+ export interface UREncodable {
24
+ /**
25
+ * Returns the UR representation of the object.
26
+ */
27
+ ur(): UR;
28
+
29
+ /**
30
+ * Returns the UR string representation of the object.
31
+ */
32
+ urString(): string;
33
+ }
34
+
35
+ /**
36
+ * Helper function to check if an object implements UREncodable.
37
+ */
38
+ export function isUREncodable(obj: unknown): obj is UREncodable {
39
+ return (
40
+ typeof obj === "object" &&
41
+ obj !== null &&
42
+ "ur" in obj &&
43
+ "urString" in obj &&
44
+ typeof (obj as Record<string, unknown>)["ur"] === "function" &&
45
+ typeof (obj as Record<string, unknown>)["urString"] === "function"
46
+ );
47
+ }
package/src/ur-type.ts ADDED
@@ -0,0 +1,87 @@
1
+ import { InvalidTypeError } from "./error";
2
+ import { isValidURType } from "./utils";
3
+
4
+ /**
5
+ * Represents a UR (Uniform Resource) type identifier.
6
+ *
7
+ * Valid UR types contain only lowercase letters, digits, and hyphens.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * const urType = new URType('test');
12
+ * console.log(urType.string()); // "test"
13
+ * ```
14
+ */
15
+ export class URType {
16
+ private readonly _type: string;
17
+
18
+ /**
19
+ * Creates a new URType from the provided type string.
20
+ *
21
+ * @param urType - The UR type as a string
22
+ * @throws {InvalidTypeError} If the type contains invalid characters
23
+ *
24
+ * @example
25
+ * ```typescript
26
+ * const urType = new URType('test');
27
+ * ```
28
+ */
29
+ constructor(urType: string) {
30
+ if (!isValidURType(urType)) {
31
+ throw new InvalidTypeError();
32
+ }
33
+ this._type = urType;
34
+ }
35
+
36
+ /**
37
+ * Returns the string representation of the URType.
38
+ *
39
+ * @example
40
+ * ```typescript
41
+ * const urType = new URType('test');
42
+ * console.log(urType.string()); // "test"
43
+ * ```
44
+ */
45
+ string(): string {
46
+ return this._type;
47
+ }
48
+
49
+ /**
50
+ * Checks equality with another URType based on the type string.
51
+ */
52
+ equals(other: URType): boolean {
53
+ return this._type === other._type;
54
+ }
55
+
56
+ /**
57
+ * Returns the string representation.
58
+ */
59
+ toString(): string {
60
+ return this._type;
61
+ }
62
+
63
+ /**
64
+ * Creates a URType from a string, throwing an error if invalid.
65
+ *
66
+ * @param value - The UR type string
67
+ * @returns A new URType instance
68
+ * @throws {InvalidTypeError} If the type is invalid
69
+ */
70
+ static from(value: string): URType {
71
+ return new URType(value);
72
+ }
73
+
74
+ /**
75
+ * Safely creates a URType, returning an error if invalid.
76
+ *
77
+ * @param value - The UR type string
78
+ * @returns Either a URType or an error
79
+ */
80
+ static tryFrom(value: string): URType | InvalidTypeError {
81
+ try {
82
+ return new URType(value);
83
+ } catch (error) {
84
+ return error as InvalidTypeError;
85
+ }
86
+ }
87
+ }