Spaces:
Sleeping
Sleeping
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;
}
}
|