Files
v10/dist/dev/network/fetch.js
T

115 lines
4.4 KiB
JavaScript

import { sampleBandwidth } from "./bandwidth-estimator.js";
import { ChunkedStreamIterable } from "./chunked-stream-iterable.js";
//#region src/network/fetch.ts
/**
* HTTP Fetch Wrapper
*
* Composable building blocks:
* - fetchResolvable() — fetch a Resource (handles byte ranges); returns Response
* - getResponseText() — extract text from Response
* - fetchResolvableStream() — single-stage async generator over body chunks
* - fetchStream() — two-stage: await connection establishment, then lazily
* iterate body chunks. Use when timing the connection start independently
* of body consumption matters (e.g., observable fetch timing for ABR).
* - createTrackedFetch() — factory for a fetchStream-shape function that
* samples bandwidth (via EWMA) per chunk and notifies via callback.
*/
/**
* Fetch resolvable from a Resource.
*
* Handles byte range requests if byteRange is present.
* Returns native fetch Response for composability (can extract text, stream, etc.).
*
* @param addressable - Resource to fetch (url + optional byteRange)
* @returns Promise resolving to Response
*
* @example
* const response = await fetchResolvable({ url: 'https://example.com/segment.m4s' });
* const text = await getResponseText(response);
*
* @example
* // With byte range
* const response = await fetchResolvable({
* url: 'https://example.com/file.mp4',
* byteRange: { start: 1000, end: 1999 }
* });
*/
async function fetchResolvable(addressable, options) {
const headers = new Headers(options?.headers);
if (addressable.byteRange) {
const { start, end } = addressable.byteRange;
headers.set("Range", `bytes=${start}-${end}`);
}
const request = new Request(addressable.url, {
method: "GET",
headers,
...options
});
return fetch(request);
}
/**
* Extract text from Response.
*
* Accepts minimal Response-like object (just needs text() method).
* Returns promise from response.text().
*
* @param response - Response-like object with text() method
* @returns Promise resolving to text content
*
* @example
* const response = await fetchResolvable(addressable);
* const text = await getResponseText(response);
*/
function getResponseText(response) {
return response.text();
}
/** Default {@link FetchText}: fetch the resource, reject on non-OK, return text. */
const fetchResolvableText = async (addressable, options) => {
const response = await fetchResolvable(addressable, options);
if (!response.ok) throw new Error(`fetchResolvableText: ${response.status} ${response.statusText} for ${addressable.url}`);
return getResponseText(response);
};
async function fetchStream(addressable, options) {
const { minChunkSize, ...fetchOptions } = options ?? {};
const response = await fetchResolvable(addressable, fetchOptions);
if (!response.body) throw new Error("Response has no body");
return new ChunkedStreamIterable(response.body, ...minChunkSize !== void 0 ? [{ minChunkSize }] : []);
}
/**
* Returns a {@link FetchBytes} function that samples bandwidth via EWMA
* per body chunk. The factory captures the running bandwidth state
* internally; per chunk it computes the next state and notifies the
* supplied `onSample` callback.
*
* The factory's internal accumulator is seeded from `initial` and updated
* on every chunk; callers don't need to thread it back in. `onSample`
* receives the *new* state after each chunk — typical use is to bridge
* samples back into engine state for ABR consumers.
*
* @param initial - Starting `BandwidthState` (commonly zeros or the
* engine's current accumulator).
* @param onSample - Called with the new `BandwidthState` after each chunk.
*/
function createTrackedFetch(initial, onSample) {
let state = initial;
return async (addressable, options) => {
const { minChunkSize, ...fetchOptions } = options ?? {};
const response = await fetchResolvable(addressable, fetchOptions);
if (!response.body) throw new Error("Response has no body");
const body = response.body;
return { [Symbol.asyncIterator]: async function* () {
let chunkStart = performance.now();
for await (const chunk of new ChunkedStreamIterable(body, ...minChunkSize !== void 0 ? [{ minChunkSize }] : [])) {
const elapsed = performance.now() - chunkStart;
state = sampleBandwidth(state, elapsed, chunk.byteLength);
onSample(state);
yield chunk;
chunkStart = performance.now();
}
} };
};
}
//#endregion
export { createTrackedFetch, fetchResolvable, fetchResolvableText, fetchStream, getResponseText };
//# sourceMappingURL=fetch.js.map