download
raw
9.76 kB
import { getRetryDelay, shouldRetry } from './error-helper';
import { LoadStats } from '../loader/load-stats';
import { logger } from '../utils/logger';
import type { HlsConfig } from '../config';
import type { RetryConfig } from '../config';
import type {
Loader,
LoaderCallbacks,
LoaderConfiguration,
LoaderContext,
LoaderResponse,
LoaderStats,
} from '../types/loader';
const AGE_HEADER_LINE_REGEX = /^age:\s*[\d.]+\s*$/im;
class XhrLoader implements Loader<LoaderContext> {
private xhrSetup:
| ((xhr: XMLHttpRequest, url: string) => Promise<void> | void)
| null;
private requestTimeout?: number;
private retryTimeout?: number;
private retryDelay: number;
private config: LoaderConfiguration | null = null;
private callbacks: LoaderCallbacks<LoaderContext> | null = null;
public context: LoaderContext | null = null;
private loader: XMLHttpRequest | null = null;
public stats: LoaderStats;
constructor(config: HlsConfig) {
this.xhrSetup = config ? config.xhrSetup || null : null;
this.stats = new LoadStats();
this.retryDelay = 0;
}
destroy() {
this.callbacks = null;
this.abortInternal();
this.loader = null;
this.config = null;
this.context = null;
this.xhrSetup = null;
}
abortInternal() {
const loader = this.loader;
self.clearTimeout(this.requestTimeout);
self.clearTimeout(this.retryTimeout);
if (loader) {
loader.onreadystatechange = null;
loader.onprogress = null;
if (loader.readyState !== 4) {
this.stats.aborted = true;
loader.abort();
}
}
}
abort() {
this.abortInternal();
if (this.callbacks?.onAbort) {
this.callbacks.onAbort(
this.stats,
this.context as LoaderContext,
this.loader,
);
}
}
load(
context: LoaderContext,
config: LoaderConfiguration,
callbacks: LoaderCallbacks<LoaderContext>,
) {
if (this.stats.loading.start) {
throw new Error('Loader can only be used once.');
}
this.stats.loading.start = self.performance.now();
this.context = context;
this.config = config;
this.callbacks = callbacks;
this.loadInternal();
}
loadInternal() {
const { config, context } = this;
if (!config || !context) {
return;
}
const xhr = (this.loader = new self.XMLHttpRequest());
const stats = this.stats;
stats.loading.first = 0;
stats.loaded = 0;
stats.aborted = false;
const xhrSetup = this.xhrSetup;
if (xhrSetup) {
Promise.resolve()
.then(() => {
if (this.loader !== xhr || this.stats.aborted) return;
return xhrSetup(xhr, context.url);
})
.catch((error: Error) => {
if (this.loader !== xhr || this.stats.aborted) return;
xhr.open('GET', context.url, true);
return xhrSetup(xhr, context.url);
})
.then(() => {
if (this.loader !== xhr || this.stats.aborted) return;
this.openAndSendXhr(xhr, context, config);
})
.catch((error: Error) => {
// IE11 throws an exception on xhr.open if attempting to access an HTTP resource over HTTPS
this.callbacks?.onError(
{ code: xhr.status, text: error.message },
context,
xhr,
stats,
);
return;
});
} else {
this.openAndSendXhr(xhr, context, config);
}
}
openAndSendXhr(
xhr: XMLHttpRequest,
context: LoaderContext,
config: LoaderConfiguration,
) {
if (!xhr.readyState) {
xhr.open('GET', context.url, true);
}
const headers = context.headers;
const { maxTimeToFirstByteMs, maxLoadTimeMs } = config.loadPolicy;
if (headers) {
for (const header in headers) {
xhr.setRequestHeader(header, headers[header]);
}
}
if (context.rangeEnd) {
xhr.setRequestHeader(
'Range',
'bytes=' + context.rangeStart + '-' + (context.rangeEnd - 1),
);
}
xhr.onreadystatechange = this.readystatechange.bind(this);
xhr.onprogress = this.loadprogress.bind(this);
xhr.responseType = context.responseType as XMLHttpRequestResponseType;
// setup timeout before we perform request
self.clearTimeout(this.requestTimeout);
config.timeout =
maxTimeToFirstByteMs && Number.isFinite(maxTimeToFirstByteMs)
? maxTimeToFirstByteMs
: maxLoadTimeMs;
this.requestTimeout = self.setTimeout(
this.loadtimeout.bind(this),
config.timeout,
);
xhr.send();
}
readystatechange() {
const { context, loader: xhr, stats } = this;
if (!context || !xhr) {
return;
}
const readyState = xhr.readyState;
const config = this.config as LoaderConfiguration;
// don't proceed if xhr has been aborted
if (stats.aborted) {
return;
}
// >= HEADERS_RECEIVED
if (readyState >= 2) {
if (stats.loading.first === 0) {
stats.loading.first = Math.max(
self.performance.now(),
stats.loading.start,
);
// readyState >= 2 AND readyState !==4 (readyState = HEADERS_RECEIVED || LOADING) rearm timeout as xhr not finished yet
if (config.timeout !== config.loadPolicy.maxLoadTimeMs) {
self.clearTimeout(this.requestTimeout);
config.timeout = config.loadPolicy.maxLoadTimeMs;
this.requestTimeout = self.setTimeout(
this.loadtimeout.bind(this),
config.loadPolicy.maxLoadTimeMs -
(stats.loading.first - stats.loading.start),
);
}
}
if (readyState === 4) {
self.clearTimeout(this.requestTimeout);
xhr.onreadystatechange = null;
xhr.onprogress = null;
const status = xhr.status;
// http status between 200 to 299 are all successful
const useResponseText =
xhr.responseType === 'text' ? xhr.responseText : null;
if (status >= 200 && status < 300) {
const data = useResponseText ?? xhr.response;
if (data != null) {
stats.loading.end = Math.max(
self.performance.now(),
stats.loading.first,
);
const len =
xhr.responseType === 'arraybuffer'
? data.byteLength
: data.length;
stats.loaded = stats.total = len;
stats.bwEstimate =
(stats.total * 8000) / (stats.loading.end - stats.loading.first);
const onProgress = this.callbacks?.onProgress;
if (onProgress) {
onProgress(stats, context, data, xhr);
}
const response: LoaderResponse = {
url: xhr.responseURL,
data: data,
code: status,
};
this.callbacks?.onSuccess(response, stats, context, xhr);
return;
}
}
// Handle bad status or nullish response
const retryConfig = config.loadPolicy.errorRetry;
const retryCount = stats.retry;
// if max nb of retries reached or if http status between 400 and 499 (such error cannot be recovered, retrying is useless), return error
const response: LoaderResponse = {
url: context.url,
data: undefined,
code: status,
};
if (shouldRetry(retryConfig, retryCount, false, response)) {
this.retry(retryConfig);
} else {
logger.error(`${status} while loading ${context.url}`);
this.callbacks?.onError(
{ code: status, text: xhr.statusText },
context,
xhr,
stats,
);
}
}
}
}
loadtimeout() {
if (!this.config) return;
const retryConfig = this.config.loadPolicy.timeoutRetry;
const retryCount = this.stats.retry;
if (shouldRetry(retryConfig, retryCount, true)) {
this.retry(retryConfig);
} else {
logger.warn(`timeout while loading ${this.context?.url}`);
const callbacks = this.callbacks;
if (callbacks) {
this.abortInternal();
callbacks.onTimeout(
this.stats,
this.context as LoaderContext,
this.loader,
);
}
}
}
retry(retryConfig: RetryConfig) {
const { context, stats } = this;
this.retryDelay = getRetryDelay(retryConfig, stats.retry);
stats.retry++;
logger.warn(
`${
status ? 'HTTP Status ' + status : 'Timeout'
} while loading ${context?.url}, retrying ${stats.retry}/${
retryConfig.maxNumRetry
} in ${this.retryDelay}ms`,
);
// abort and reset internal state
this.abortInternal();
this.loader = null;
// schedule retry
self.clearTimeout(this.retryTimeout);
this.retryTimeout = self.setTimeout(
this.loadInternal.bind(this),
this.retryDelay,
);
}
loadprogress(event: ProgressEvent) {
const stats = this.stats;
stats.loaded = event.loaded;
if (event.lengthComputable) {
stats.total = event.total;
}
}
getCacheAge(): number | null {
let result: number | null = null;
if (
this.loader &&
AGE_HEADER_LINE_REGEX.test(this.loader.getAllResponseHeaders())
) {
const ageHeader = this.loader.getResponseHeader('age');
result = ageHeader ? parseFloat(ageHeader) : null;
}
return result;
}
getResponseHeader(name: string): string | null {
if (
this.loader &&
new RegExp(`^${name}:\\s*[\\d.]+\\s*$`, 'im').test(
this.loader.getAllResponseHeaders(),
)
) {
return this.loader.getResponseHeader(name);
}
return null;
}
}
export default XhrLoader;

Xet Storage Details

Size:
9.76 kB
·
Xet hash:
a03a68434cc739a9d7878e6228d43be4baa9f36faa733c354a1003d482dd8f3a

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.