ProgressEndpoint
ProgressEndpoint extends RestEndpoint with download progress tracking
using the ReadableStream API.
extends
ProgressEndpoint extends RestEndpoint
Usage
import { ProgressEndpoint, Entity, resource } from '@data-client/rest';
// Standalone endpoint with progress
const getFile = new ProgressEndpoint({
path: '/files/:id',
schema: FileEntity,
onDownloadProgress({ loaded, total, lengthComputable }) {
if (lengthComputable) {
console.log(`${Math.round((loaded / total!) * 100)}%`);
}
},
});
// With resource()
const FileResource = resource({
path: '/files/:id',
schema: FileEntity,
Endpoint: ProgressEndpoint,
});
// Add progress tracking to a specific endpoint via .extend()
const downloadWithProgress = FileResource.get.extend({
onDownloadProgress({ loaded, total, lengthComputable }) {
setProgress(lengthComputable ? loaded / total! : undefined);
},
});
onDownloadProgress
Called once per chunk as the response body is read via ReadableStream.
onDownloadProgress?: (event: DownloadProgress) => void;
DownloadProgress
interface DownloadProgress {
/** Bytes downloaded so far */
loaded: number;
/** Total bytes (from Content-Length), or undefined if unknown */
total: number | undefined;
/** Whether total is known */
lengthComputable: boolean;
}
lengthComputable is false when:
- The
Content-Lengthheader is missing (e.g., chunked transfer encoding) - The
Content-Encodingheader is set (e.g.,gzip), sinceContent-Lengthreflects the compressed size but chunks are decompressed
Graceful degradation
When response.body is not available (e.g., older polyfills like whatwg-fetch),
progress tracking is silently skipped and the endpoint behaves identically to
RestEndpoint.
How it works
ProgressEndpoint overrides fetchResponse() to intercept the response body stream:
- Calls
super.fetchResponse()to perform the HTTP request - If
onDownloadProgressis set andresponse.bodyexists, reads the body chunk-by-chunk viaReadableStream - Calls
onDownloadProgressafter each chunk with cumulativeloadedbytes - Reconstructs a
Responsefrom the collected chunks forparseResponse()to consume
The rest of the lifecycle (parseResponse, process, schema normalization) is unchanged.
Inheritance
ProgressEndpoint can be subclassed the same way as RestEndpoint:
import { ProgressEndpoint, type RestGenerics } from '@data-client/rest';
class MyProgressEndpoint<
O extends RestGenerics = any,
> extends ProgressEndpoint<O> {
urlPrefix = 'https://api.example.com';
getHeaders(headers: HeadersInit): HeadersInit {
return {
...headers,
Authorization: `Bearer ${getToken()}`,
};
}
}