Skip to content

@absoluteui/services/upload

Chunked, resumable uploads with retry, pause and resume, progress, and queues.

import { ... } from '@absoluteui/services/upload';

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 user
const 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.

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; // => 1

How 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%'
  • 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 validateFiles from @absoluteui/services/file before uploading; the uploader does not check types or content.
import { createUploader, MIN_CHUNK_SIZE } from '@absoluteui/services/upload';
MIN_CHUNK_SIZE; // => 262144
createUploader({ endpoint: '/api/uploads', chunkSize: 64 * 1024 }); // => throws SVC_UPLOAD_INVALID_OPTION

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'

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;
}

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 ?? []);

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;
}

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 } }) });

RestUploadAdapterOptions

interface RestUploadAdapterOptions {
readonly endpoint: string;
readonly headers?: HeadersInit;
readonly http?: Omit<HttpClientOptions, 'baseUrl' | 'headers'>;
readonly checksumHeader?: string;
}

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';

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 user
const 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 36

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;

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;
}