Add better error surfacing for decoder iterators, add UrlSource

This commit is contained in:
David Payr
2024-12-29 12:38:55 +01:00
parent cc87dbfcf9
commit f70142e25b
8 changed files with 501 additions and 77 deletions
+124
View File
@@ -67,3 +67,127 @@ export class BlobSource extends Source {
return this._blob.size;
}
}
/** @public */
export class UrlSource extends Source {
/** @internal */
private _url: string;
/** @internal */
private _withCredentials: boolean;
/** @internal */
private _fullData: ArrayBuffer | null = null;
constructor(url: string, options: { withCredentials?: boolean } = {}) {
if (typeof url !== 'string') {
throw new TypeError('url must be a string.');
}
if (!options || typeof options !== 'object') {
throw new TypeError('options must be an object.');
}
if (options.withCredentials !== undefined && typeof options.withCredentials !== 'boolean') {
throw new TypeError('options.withCredentials, when specified, must be a boolean.');
}
super();
this._url = url;
this._withCredentials = options.withCredentials ?? false;
}
/** @internal */
private _makeRequest(
range?: { start: number; end: number },
): Promise<{ response: ArrayBuffer; statusCode: number }> {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest(); // We use XMLHttpRequest instead of fetch since it supports more protocols
xhr.open('GET', this._url, true);
xhr.responseType = 'arraybuffer';
xhr.withCredentials = this._withCredentials;
if (range) {
xhr.setRequestHeader('Range', `bytes=${range.start}-${range.end - 1}`);
}
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
const buffer = xhr.response as ArrayBuffer;
if (!range) {
this._fullData = buffer;
}
resolve({
response: buffer,
statusCode: xhr.status,
});
} else {
reject(new Error(`Error fetching ${this._url}: ${xhr.status} ${xhr.statusText}`));
}
};
xhr.onerror = () => {
reject(new Error('Network error occurred.'));
};
xhr.ontimeout = () => {
reject(new Error('Request timed out.'));
};
xhr.send();
});
}
/** @internal */
override async _read(start: number, end: number): Promise<Uint8Array> {
if (this._fullData) {
return new Uint8Array(this._fullData, start, end - start);
}
const { response, statusCode } = await this._makeRequest({ start, end });
// If server doesn't support range requests, it will return 200 instead of 206. In that case, let's manually
// slice the response.
if (statusCode === 200) {
const fullData = new Uint8Array(response);
return fullData.subarray(start, end);
}
return new Uint8Array(response);
}
/** @internal */
override async _retrieveSize(): Promise<number> {
if (this._fullData) {
return this._fullData.byteLength;
}
const xhr = new XMLHttpRequest();
xhr.open('HEAD', this._url, true);
xhr.withCredentials = this._withCredentials;
await new Promise<void>((resolve, reject) => {
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve();
} else {
reject(new Error(`Error fetching ${this._url} (HEAD): ${xhr.status} ${xhr.statusText}`));
}
};
xhr.onerror = () => {
resolve();
};
xhr.send();
});
const contentLength = xhr.getResponseHeader('Content-Length');
if (!contentLength) {
// If Content-Length is not available, make a GET request to get the full size
const { response } = await this._makeRequest();
return response.byteLength;
}
return parseInt(contentLength, 10);
}
}