Spaces:
Running
Running
/** | |
* WebBlob is a Blob implementation for web resources that supports range requests. | |
*/ | |
interface WebBlobCreateOptions { | |
/** | |
* @default 1_000_000 | |
* | |
* Objects below that size will immediately be fetched and put in RAM, rather | |
* than streamed ad-hoc | |
*/ | |
cacheBelow?: number; | |
/** | |
* Custom fetch function to use instead of the default one, for example to use a proxy or edit headers. | |
*/ | |
fetch?: typeof fetch; | |
} | |
export class WebBlob extends Blob { | |
static async create(url: URL, opts?: WebBlobCreateOptions): Promise<Blob> { | |
const customFetch = opts?.fetch ?? fetch; | |
const response = await customFetch(url, { method: "HEAD" }); | |
const size = Number(response.headers.get("content-length")); | |
const contentType = response.headers.get("content-type") || ""; | |
const supportRange = response.headers.get("accept-ranges") === "bytes"; | |
if (!supportRange || size < (opts?.cacheBelow ?? 1_000_000)) { | |
return await (await customFetch(url)).blob(); | |
} | |
return new WebBlob(url, 0, size, contentType, true, customFetch); | |
} | |
private url: URL; | |
private start: number; | |
private end: number; | |
private contentType: string; | |
private full: boolean; | |
private fetch: typeof fetch; | |
constructor(url: URL, start: number, end: number, contentType: string, full: boolean, customFetch: typeof fetch) { | |
super([]); | |
this.url = url; | |
this.start = start; | |
this.end = end; | |
this.contentType = contentType; | |
this.full = full; | |
this.fetch = customFetch; | |
} | |
override get size(): number { | |
return this.end - this.start; | |
} | |
override get type(): string { | |
return this.contentType; | |
} | |
override slice(start = 0, end = this.size): WebBlob { | |
if (start < 0 || end < 0) { | |
new TypeError("Unsupported negative start/end on FileBlob.slice"); | |
} | |
const slice = new WebBlob( | |
this.url, | |
this.start + start, | |
Math.min(this.start + end, this.end), | |
this.contentType, | |
start === 0 && end === this.size ? this.full : false, | |
this.fetch | |
); | |
return slice; | |
} | |
override async arrayBuffer(): Promise<ArrayBuffer> { | |
const result = await this.fetchRange(); | |
return result.arrayBuffer(); | |
} | |
override async text(): Promise<string> { | |
const result = await this.fetchRange(); | |
return result.text(); | |
} | |
override stream(): ReturnType<Blob["stream"]> { | |
const stream = new TransformStream(); | |
this.fetchRange() | |
.then((response) => response.body?.pipeThrough(stream)) | |
.catch((error) => stream.writable.abort(error.message)); | |
return stream.readable; | |
} | |
private fetchRange(): Promise<Response> { | |
const fetch = this.fetch; // to avoid this.fetch() which is bound to the instance instead of globalThis | |
if (this.full) { | |
return fetch(this.url); | |
} | |
return fetch(this.url, { | |
headers: { | |
Range: `bytes=${this.start}-${this.end - 1}`, | |
}, | |
}); | |
} | |
} | |