File size: 6,647 Bytes
01e6679
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
import { sum } from "./sum";

/**
 * Represents a single splice operation
 */
interface SpliceOperation {
	insert: Blob;
	start: number;
	end: number;
}

/**
 * @internal
 *
 * A SplicedBlob is a Blob that represents the result of splicing one or more insert blobs
 * into an original blob at specified positions, replacing content between start and end.
 *
 * It is a drop-in replacement for the Blob class, so you can use it as a Blob.
 * The splicing is done virtually without copying data until accessed.
 *
 * @example
 * const originalBlob = new Blob(["Hello, World!"]);
 * const insertBlob = new Blob(["Beautiful "]);
 * const splicedBlob = SplicedBlob.create(originalBlob, insertBlob, 7, 7);
 * // Result represents: "Hello, Beautiful World!"
 */
export class SplicedBlob extends Blob {
	public originalBlob: Blob;
	public spliceOperations: SpliceOperation[];

	private constructor(originalBlob: Blob, spliceOperations: SpliceOperation[]) {
		super();

		this.originalBlob = originalBlob;
		this.spliceOperations = spliceOperations; // Create a copy to prevent external mutation
	}

	static create(originalBlob: Blob, operations: SpliceOperation[]): SplicedBlob {
		// Validate all operations
		for (const op of operations) {
			if (op.start < 0 || op.end < 0) {
				throw new Error("Invalid start/end positions for SplicedBlob");
			}
			if (op.start > originalBlob.size || op.end > originalBlob.size) {
				throw new Error("Invalid start/end positions for SplicedBlob");
			}
			if (op.start > op.end) {
				throw new Error("Invalid start/end positions for SplicedBlob");
			}
		}

		// Sort operations by start position and validate no overlaps
		const sortedOps = [...operations].sort((a, b) => a.start - b.start);
		for (let i = 0; i < sortedOps.length - 1; i++) {
			if (sortedOps[i].end > sortedOps[i + 1].start) {
				throw new Error("Overlapping splice operations are not supported");
			}
		}

		return new SplicedBlob(originalBlob, sortedOps);
	}

	/**
	 * Returns the size of the spliced blob.
	 * Size = original size - total replaced size + total insert size
	 */
	override get size(): number {
		let totalReplacedSize = 0;
		let totalInsertSize = 0;

		for (const op of this.spliceOperations) {
			totalReplacedSize += op.end - op.start;
			totalInsertSize += op.insert.size;
		}

		return this.originalBlob.size - totalReplacedSize + totalInsertSize;
	}

	/**
	 * Returns the MIME type of the original blob.
	 */
	override get type(): string {
		return this.originalBlob.type;
	}

	/**
	 * Returns a new instance of SplicedBlob that is a slice of the current one.
	 *
	 * The slice is inclusive of the start and exclusive of the end.
	 * The slice method does not support negative start/end.
	 *
	 * @param start beginning of the slice
	 * @param end end of the slice
	 */
	override slice(start = 0, end = this.size): Blob {
		if (start < 0 || end < 0) {
			throw new TypeError("Unsupported negative start/end on SplicedBlob.slice");
		}

		start = Math.min(start, this.size);
		end = Math.min(end, this.size);

		if (start >= end) {
			return new Blob([]);
		}

		// Get all segments and calculate their cumulative positions
		const segments = this.segments;
		const segmentBoundaries: number[] = [0];
		let cumulativeSize = 0;

		for (const segment of segments) {
			cumulativeSize += segment.size;
			segmentBoundaries.push(cumulativeSize);
		}

		// Find which segments the slice spans
		const resultSegments: Blob[] = [];

		for (let i = 0; i < segments.length; i++) {
			const segmentStart = segmentBoundaries[i];
			const segmentEnd = segmentBoundaries[i + 1];

			// Skip segments that are entirely before the slice
			if (segmentEnd <= start) {
				continue;
			}

			// Skip segments that are entirely after the slice
			if (segmentStart >= end) {
				break;
			}

			// Calculate slice bounds within this segment
			const sliceStart = Math.max(0, start - segmentStart);
			const sliceEnd = Math.min(segments[i].size, end - segmentStart);

			if (sliceStart < sliceEnd) {
				resultSegments.push(segments[i].slice(sliceStart, sliceEnd));
			}
		}

		return new Blob(resultSegments);
	}

	get firstSpliceIndex(): number {
		return this.spliceOperations[0]?.start ?? Infinity;
	}

	/**
	 * Read the spliced blob content and returns it as an ArrayBuffer.
	 */
	override async arrayBuffer(): Promise<ArrayBuffer> {
		const segments = this.segments;
		const buffers = await Promise.all(segments.map((segment) => segment.arrayBuffer()));

		// Concatenate all buffers
		const totalSize = sum(buffers.map((buffer) => buffer.byteLength));
		const result = new Uint8Array(totalSize);

		let offset = 0;
		for (const buffer of buffers) {
			result.set(new Uint8Array(buffer), offset);
			offset += buffer.byteLength;
		}

		return result.buffer;
	}

	/**
	 * Read the spliced blob content and returns it as a string.
	 */
	override async text(): Promise<string> {
		const buffer = await this.arrayBuffer();
		return new TextDecoder().decode(buffer);
	}

	/**
	 * Returns a stream around the spliced blob content.
	 */
	override stream(): ReturnType<Blob["stream"]> {
		const readable = new ReadableStream({
			start: async (controller) => {
				try {
					const segments = this.segments;

					for (const segment of segments) {
						const reader = segment.stream().getReader();

						try {
							while (true) {
								const { done, value } = await reader.read();
								if (done) {
									break;
								}
								controller.enqueue(value);
							}
						} finally {
							reader.releaseLock();
						}
					}

					controller.close();
				} catch (error) {
					controller.error(error);
				}
			},
		});

		return readable;
	}

	/**
	 * Get all segments that make up the spliced blob.
	 * This includes original blob segments between splice operations and insert blobs.
	 */
	private get segments(): Blob[] {
		const segments: Blob[] = [];
		let currentPosition = 0;

		// Sort operations by start position to ensure correct order
		const sortedOps = [...this.spliceOperations].sort((a, b) => a.start - b.start);

		for (const op of sortedOps) {
			// Add segment from current position to start of this operation
			if (currentPosition < op.start) {
				segments.push(this.originalBlob.slice(currentPosition, op.start));
			}

			// Add the insert blob (if it has content)
			if (op.insert.size > 0) {
				segments.push(op.insert);
			}

			// Move current position to end of this operation
			currentPosition = op.end;
		}

		// Add remaining segment after last operation
		if (currentPosition < this.originalBlob.size) {
			segments.push(this.originalBlob.slice(currentPosition));
		}

		return segments;
	}
}