@absoluteui/services/upload
Chunked, resumable uploads with retry, pause and resume, progress, and queues.
import { ... } from '@absoluteui/services/upload';How to use
Section titled “How to use”Chunked, resumable uploads for large files: slices are sent in parallel, failed chunks are retried with backoff, uploads can pause, resume, and continue after a reload, and progress comes with speed, time remaining, and a sentence for live regions. The server protocol is an adapter, so the same engine works with a simple REST API, S3 multipart, or your own. The Files and downloads guide introduces the pieces; for small files, uploadWithProgress in @absoluteui/services/http is simpler.
How do I upload a large file with progress and resume after a reload?
Section titled “How do I upload a large file with progress and resume after a reload?”Give createUploader an endpoint (the built-in REST convention) and a store for upload ids. A @absoluteui/services/storage store works as the resume store. Announce progress at a calm interval, not on every chunk.
import { createStore } from '@absoluteui/services/storage';import { createUploader, describeUploadProgress, type PersistedUpload } from '@absoluteui/services/upload';
declare const file: File; // a scanned contract chosen by the userconst resume = createStore<PersistedUpload>({ namespace: 'hr-uploads', ttlMs: 7 * 86_400_000 });const uploader = createUploader({ endpoint: '/api/uploads', checksum: 'crc32', store: resume });const bar = document.createElement('progress');bar.max = 100;const status = document.createElement('p');status.setAttribute('aria-live', 'polite');
const upload = uploader.upload(file, { metadata: { employeeId: 42, category: 'contract' } });upload.subscribe((u) => { bar.value = u.progress.percent; });const announce = setInterval(() => { status.textContent = describeUploadProgress(upload.progress, { state: upload.state }); }, 5_000);const result = await upload.promise.finally(() => { clearInterval(announce); });pause(), resume(), and cancel() on the controller map to buttons; cancelling tells the server to discard the parts.
How do I connect to my own upload API?
Section titled “How do I connect to my own upload API?”Implement UploadAdapter: start returns an upload id (and chunks the server already has, to resume), uploadChunk sends one slice, and complete finishes. Chunk retries and ordering are handled for you.
import { createUploader, type UploadAdapter } from '@absoluteui/services/upload';
const received = new Map<number, number>();const adapter: UploadAdapter = { start: async (_file, info) => ({ uploadId: `up-${String(info.size)}` }), uploadChunk: async ({ index, size, onProgress }) => { received.set(index, size); onProgress(size); return { etag: `"part-${String(index)}"` }; }, complete: async ({ uploadId, parts }) => ({ uploadId, parts: parts.length }),};const uploader = createUploader({ adapter });const result = await uploader.upload(new File(['id,name\n42,Ada Lovelace\n'], 'employees.csv')).promise;result.response; // => { uploadId: 'up-24', parts: 1 }result.parts[0]?.etag; // => '"part-0"'received.size; // => 1How do I describe progress for screen readers?
Section titled “How do I describe progress for screen readers?”describeUploadProgress formats the percentage and remaining time with Intl; pass state for queued, paused, and finished uploads and messages to translate.
import { describeUploadProgress } from '@absoluteui/services/upload';
const progress = { loaded: 45, total: 100, percent: 45, speedBps: 1, etaMs: 120_000 };describeUploadProgress(progress, { locale: 'en' }); // => '45% uploaded, about 2 minutes remaining'describeUploadProgress(progress, { locale: 'en', state: 'paused' }); // => 'Upload paused at 45%'What goes wrong?
Section titled “What goes wrong?”- Chunks smaller than
MIN_CHUNK_SIZE(256 KiB) are refused (SVC_UPLOAD_INVALID_OPTION); most object stores need at least 5 MiB parts, which is the default. - The resume key is a hash of name, size, modification time, and type, so a changed file starts a new upload rather than resuming a wrong one.
- Validate files with
validateFilesfrom@absoluteui/services/filebefore uploading; the uploader does not check types or content.
import { createUploader, MIN_CHUNK_SIZE } from '@absoluteui/services/upload';
MIN_CHUNK_SIZE; // => 262144createUploader({ endpoint: '/api/uploads', chunkSize: 64 * 1024 }); // => throws SVC_UPLOAD_INVALID_OPTIONAPI reference
Section titled “API reference”Describe
Section titled “Describe”Functions
Section titled “Functions”describeUploadProgress
Status sentence for a live region, with the percentage and remaining time formatted by Intl
(the UI should throttle announcements). English sentences by default; pass messages to translate.
function describeUploadProgress(progress: UploadProgress, options?: DescribeUploadProgressOptions): string;Example:
describeUploadProgress({ loaded: 45, total: 100, percent: 45, speedBps: 1, etaMs: 120_000 }, { locale: 'en' });// => '45% uploaded, about 2 minutes remaining'Interfaces
Section titled “Interfaces”DescribeUploadProgressOptions
interface DescribeUploadProgressOptions { readonly locale?: string; readonly state?: UploadState; readonly messages?: Partial<UploadProgressMessages>;}UploadProgressMessages
Sentence builders; override to translate. Inputs are already locale-formatted.
interface UploadProgressMessages { readonly queued: () => string; readonly uploading: (percent: string, remaining: string | undefined) => string; readonly paused: (percent: string) => string; readonly completed: () => string; readonly failed: () => string; readonly cancelled: () => string;}Functions
Section titled “Functions”createUploadQueue
Uploads many files with at most concurrency files in flight, with add/remove, retry of
failed files, cancel/pause/resume all, and events for UI and live-region announcements.
function createUploadQueue(options: UploadQueueOptions): UploadQueue;Example:
const uploader = createUploader({ endpoint: '/api/uploads' });const queue = createUploadQueue({ uploader });queue.on('completed', (item) => { console.info(`${item.file.name ?? 'File'} uploaded`); });const input = document.createElement('input');input.type = 'file';queue.add(input.files ?? []);Interfaces
Section titled “Interfaces”UploadQueue
Queue returned by createUploadQueue.
interface UploadQueue { add(files: Iterable<Blob & { readonly name?: string; }> | (Blob & { readonly name?: string; }), options?: { readonly metadata?: Readonly<Record<string, unknown>>; }): readonly UploadQueueItem[]; remove(id: string): boolean; retryFailed(): number; cancelAll(): void; pauseAll(): void; resumeAll(): void; start(): void; readonly items: readonly UploadQueueItem[]; readonly progress: UploadProgress; on<K extends keyof UploadQueueEvents>(event: K, listener: (payload: UploadQueueEvents[K]) => void): () => void; onIdle(): Promise<void>;}UploadQueueEvents
Queue events and their payloads.
interface UploadQueueEvents { added: UploadQueueItem; started: UploadQueueItem; progress: UploadQueueItem; state: UploadQueueItem; completed: UploadQueueItem; failed: UploadQueueItem; cancelled: UploadQueueItem; removed: UploadQueueItem; idle: undefined;}UploadQueueItem
One file in the queue (a live view; read it in event handlers).
interface UploadQueueItem { readonly id: string; readonly file: Blob & { readonly name?: string; }; readonly metadata: Readonly<Record<string, unknown>>; readonly state: UploadState; readonly progress: UploadProgress; readonly error: Error | undefined; readonly result: UploadResult | undefined; readonly controller: UploadController | undefined;}UploadQueueOptions
interface UploadQueueOptions { readonly uploader: Uploader; readonly concurrency?: number; readonly autoStart?: boolean;}Rest adapter
Section titled “Rest adapter”Functions
Section titled “Functions”createRestUploadAdapter
Adapter for this REST convention:
POST {endpoint} (JSON file info) -> { uploadId };
GET {endpoint}/{id} -> { uploadedChunks: number[] } (resume; 404/410 starts over);
PUT {endpoint}/{id}/chunks/{index} (raw bytes, Content-Range: bytes a-b/total, optional checksum header);
POST {endpoint}/{id}/complete (JSON { parts }) -> any JSON;
DELETE {endpoint}/{id} on cancel. Chunk retries are left to the uploader.
function createRestUploadAdapter(options: RestUploadAdapterOptions): UploadAdapter;Example:
const uploader = createUploader({ adapter: createRestUploadAdapter({ endpoint: '/api/uploads', http: { xsrf: true } }) });Interfaces
Section titled “Interfaces”RestUploadAdapterOptions
interface RestUploadAdapterOptions { readonly endpoint: string; readonly headers?: HeadersInit; readonly http?: Omit<HttpClientOptions, 'baseUrl' | 'headers'>; readonly checksumHeader?: string;}Interfaces
Section titled “Interfaces”ChunkChecksum
Per-chunk checksum: crc32 as 8 lowercase hex digits, sha256 as base64.
interface ChunkChecksum { readonly algorithm: 'crc32' | 'sha256'; readonly value: string;}ChunkResult
Optional data an adapter returns for a chunk (for example an S3 part ETag).
interface ChunkResult { readonly etag?: string;}PersistedUpload
State saved per file for resuming.
interface PersistedUpload { readonly uploadId: string; readonly chunkSize: number; readonly size: number; readonly savedAt: number;}UploadAdapter
Server protocol. Implement for S3 multipart, tus-like or custom APIs.
interface UploadAdapter { start(file: Blob, info: UploadFileInfo): Promise<UploadStartResult>; uploadChunk(input: UploadChunkInput): Promise<ChunkResult | undefined>; complete(input: { readonly uploadId: string; readonly parts: readonly UploadedPart[]; readonly signal: AbortSignal; }): Promise<unknown>; abort?(uploadId: string): Promise<void>;}UploadChunkInput
One chunk to send.
interface UploadChunkInput { readonly uploadId: string; readonly index: number; readonly blob: Blob; readonly offset: number; readonly size: number; readonly total: number; readonly checksum: ChunkChecksum | undefined; readonly signal: AbortSignal; readonly onProgress: (loaded: number) => void;}UploadedPart
A finished chunk, passed to complete and onChunkComplete.
interface UploadedPart { readonly index: number; readonly offset: number; readonly size: number; readonly checksum?: ChunkChecksum; readonly etag?: string; readonly resumed: boolean;}UploadFileInfo
What UploadAdapter.start receives.
interface UploadFileInfo { readonly name: string | undefined; readonly size: number; readonly type: string; readonly lastModified: number | undefined; readonly chunkSize: number; readonly totalChunks: number; readonly metadata: Readonly<Record<string, unknown>>; readonly uploadId: string | undefined; readonly signal: AbortSignal;}UploadProgress
File-level progress. speedBps/etaMs are null until enough samples exist.
interface UploadProgress { readonly loaded: number; readonly total: number; readonly percent: number; readonly speedBps: number | null; readonly etaMs: number | null;}UploadResult
Result of a completed upload.
interface UploadResult { readonly uploadId: string; readonly size: number; readonly parts: readonly UploadedPart[]; readonly response: unknown;}UploadStartResult
Result of UploadAdapter.start. Return the same uploadId and uploadedChunks to resume.
interface UploadStartResult { readonly uploadId: string; readonly uploadedChunks?: readonly number[];}UploadStateStore
Key-value persistence for resumable uploads (sync or async; for example services/storage).
interface UploadStateStore { get(key: string): unknown; set(key: string, value: PersistedUpload): unknown; remove(key: string): unknown;}UploadState
Lifecycle of one upload.
type UploadState = 'queued' | 'uploading' | 'paused' | 'completed' | 'failed' | 'cancelled';Uploader
Section titled “Uploader”Functions
Section titled “Functions”chunkChecksum
Checksum of a chunk: CRC-32 computed incrementally over 1 MiB slices (8 hex digits), or SHA-256 through Web Crypto (base64).
function chunkChecksum(blob: Blob, algorithm: 'crc32' | 'sha256'): Promise<ChunkChecksum>;Example:
await chunkChecksum(new Blob(['123456789']), 'crc32'); // => { algorithm: 'crc32', value: 'cbf43926' }createUploader
Creates a chunked, resumable uploader: parallel chunks with per-chunk retry and backoff,
pause/resume/cancel, optional CRC-32 or SHA-256 checksums, file-level progress with speed and
ETA, and resume through an injectable store. The server protocol is an UploadAdapter
(the built-in REST adapter is used when only endpoint is given).
function createUploader(options: UploaderOptions): Uploader;Example:
const store: UploadStateStore = { get: (key) => JSON.parse(localStorage.getItem(key) ?? 'null'), set: (key, value) => { localStorage.setItem(key, JSON.stringify(value)); }, remove: (key) => { localStorage.removeItem(key); },};const uploader = createUploader({ endpoint: '/api/uploads', checksum: 'crc32', store });declare const file: File; // chosen by the userconst bar = document.createElement('progress');const up = uploader.upload(file, { metadata: { folder: 'invoices' } });up.subscribe((u) => (bar.value = u.progress.percent));const result = await up.promise;resumeKey
Storage key for resuming a file: hash of name, size, last-modified time and type (the file name itself is not stored).
function resumeKey(file: Blob & { readonly name?: string; readonly lastModified?: number;}): string;Example:
const file = new File(['hello'], 'a.txt', { type: 'text/plain', lastModified: 0 });resumeKey(file); // => 'aui-upload:b3d68a43-5' // FNV-1a hash, then the size in base 36Constants
Section titled “Constants”DEFAULT_CHUNK_SIZE
Default chunk size (5 MiB).
const DEFAULT_CHUNK_SIZE: number;MIN_CHUNK_SIZE
Smallest allowed chunk (256 KiB).
const MIN_CHUNK_SIZE: number;Interfaces
Section titled “Interfaces”UploadController
Handle for one upload.
interface UploadController { readonly promise: Promise<UploadResult>; readonly file: Blob & { readonly name?: string; }; readonly state: UploadState; readonly progress: UploadProgress; readonly uploadId: string | undefined; readonly error: Error | undefined; pause(): void; resume(): void; cancel(): void; subscribe(listener: (upload: UploadController) => void): () => void;}Uploader
Uploader returned by createUploader.
interface Uploader { upload(file: Blob & { readonly name?: string; }, options?: UploadOptions): UploadController; readonly chunkSize: number;}UploaderOptions
interface UploaderOptions { readonly endpoint?: string; readonly adapter?: UploadAdapter; readonly chunkSize?: number; readonly concurrency?: number; readonly retries?: number; readonly backoff?: { readonly minDelayMs?: number; readonly maxDelayMs?: number; }; readonly checksum?: 'crc32' | 'sha256' | false; readonly headers?: HeadersInit; readonly fetch?: FetchLike; readonly store?: UploadStateStore; readonly onProgress?: (progress: UploadProgress, upload: UploadController) => void; readonly onChunkComplete?: (part: UploadedPart, upload: UploadController) => void; readonly onStateChange?: (state: UploadState, upload: UploadController) => void; readonly signal?: AbortSignal; readonly now?: () => number;}UploadOptions
interface UploadOptions { readonly metadata?: Readonly<Record<string, unknown>>; readonly uploadId?: string; readonly signal?: AbortSignal;}