High-performance file uploads with Web Streams API, automatic fallbacks, and resumable uploads. < 10 kB gzipped, zero dependencies, 100% TypeScript.
universal-upload/
├── server.ts # Express test/upload server
├── Dockerfile # Multi-stage Docker build
├── docker-compose.yml # Docker Compose service
├── tsconfig.server.json # TypeScript config for server
├── packages/
│ ├── core/ # @universal-uploader/core — upload engine
│ │ ├── src/ # TypeScript source
│ │ ├── __tests__/ # 71 vitest tests (5 files)
│ │ └── vitest.config.ts
│ └── react/ # @universal-uploader/react — React hook
│ ├── src/ # TypeScript source
│ ├── __tests__/ # 11 vitest tests (1 file)
│ └── vitest.config.ts
|├── apps/demo/ # Vite demo app (한국어, English)
|└── configs/ # Shared build configs
| Method | HTTP | Memory | Progress | Resumable | Legacy |
|---|---|---|---|---|---|
| Fetch Stream | 1 | O(1) ✅ | ❌ | ❌ | |
| Fetch Chunked | N | O(chunk) ✅ | ✅ | ✅ | ❌ |
| XHR Chunked | N | O(chunk) ✅ | ✅ | ✅ | ✅ |
Auto-selects the optimal method for your browser: Fetch Stream where supported, XHR Chunked otherwise (Safari, legacy browsers). Fetch Chunked is available as a manual strategy (UPLOAD_WITH_FETCH_STREAM_CHUNKED).
npm install @universal-uploader/core
# or use @universal-uploader/react for React hook version
yarn add @universal-uploader/core
pnpm add @universal-uploader/coreNPM Packages:
// Full bundle (auto strategy selection)
import upload from '@universal-uploader/core';
// Tree-shakable — only the stream strategy (no XHR/chunked overhead)
import upload from '@universal-uploader/core/base';
import { UPLOAD_WITH_STREAM } from '@universal-uploader/core/stream';
await upload({ url, file, options: { strategy: UPLOAD_WITH_STREAM } });import upload from '@universal-uploader/core';
const { result, actions } = await upload({
url: '/api/upload',
file,
options: {
retryCount: 3,
onProgress: ({ percentage }) => console.log(`${percentage}%`),
onRetry: () => console.log('Retrying...'),
},
});import { useState } from 'react';
import { useUniversalUpload } from '@universal-uploader/react';
import { UPLOAD_AUTO } from '@universal-uploader/core';
export const Upload = () => {
const [progress, setProgress] = useState(0);
const { upload, pause, resume, abort, refresh, status, uploadMethod, result, error } = useUniversalUpload({
url: '/api/upload',
options: {
strategy: UPLOAD_AUTO,
retryCount: 3,
onProgress: (p) => setProgress(Math.round(p.percentage))
}
});
return (
<div>
<input type="file" onChange={(e) => e.target.files && upload(e.target.files[0])} />
<progress value={progress} max={100} />
<p>Status: {status}</p>
<p>Method: {uploadMethod ?? 'pending'}</p>
{status === 'uploading' && (
<>
<button onClick={pause}>Pause</button>
<button onClick={abort}>Abort</button>
</>
)}
{status === 'paused' && <button onClick={resume}>Resume</button>}
{(status === 'error' || status === 'aborted') && (
<button onClick={refresh}>Restart</button>
)}
{error && <p style={{ color: 'red' }}>{error.message}</p>}
</div>
);
};- ✅ Constant Memory - Web Streams keeps memory O(1) even for multi-GB files
- ✅ Smart Fallback - Auto-detects Fetch capabilities, falls back to XHR
- ✅ Resumable - Pause and resume with chunked methods
- ✅ Progress - Detailed callbacks for all methods
- ✅ Retry - Automatic exponential backoff (
retryCount,retryDelay,onRetry) - ✅ Controls -
abort(),pause(),resume(),refresh()(retrydeprecated in React hook) - ✅ Type-Safe - Full TypeScript support
- ✅ Tiny - < 10 kB gzipped, zero dependencies
📚 Interactive Guide & Documentation - Full documentation with examples (한국어, English)
const { result, actions } = await upload({ url, file, options });
const { abort, pause, resume, refresh } = actions;
pause(); // Status: "paused" (resumable)
resume(); // Continue from offset
abort(); // Status: "aborted" (terminal)
refresh(); // Full restart from initial optionsinterface UploadOptions {
chunkSize?: number; // Default: 512KB
retryCount?: number; // Default: 3
retryDelay?: number | ((attempt: number) => number);
customHeaders?: Record<string, string>;
withCredentials?: boolean;
offset?: number;
strategy?: (args: UploadParamsInternal) => Promise<UploadResponse>; // v2: inject strategy directly
onProgress?: (p: { loaded: number; total: number; percentage: number }) => void;
onComplete?: (response?: Response) => void; // Response for 'stream'/'stream chunked'; undefined for 'xhr chunked' & empty files
onError?: (error: Error) => void;
onRetry?: () => void;
onPause?: () => void;
onResume?: () => void;
onAbort?: (error: DOMException) => void;
throwOnError?: boolean | ((error: unknown) => boolean); // Default: false
}| Browser | Support |
|---|---|
| Chrome / Edge | ✅ Fetch Stream |
| Firefox | ✅ Fetch Stream |
| Safari | ✅ XHR Fallback |
| IE 11+ | ✅ XHR Fallback |
The included Express server (server.ts) runs the upload endpoints used for integration testing. Run it in Docker for consistent environments:
# Build and start
docker compose up -d
# Check health
curl http://localhost:3000/health
# → {"status":"ok"}
# View logs
docker compose logs -f
# Stop
docker compose downRuntime config (all environment-variable driven):
| Var | Default | Description |
|---|---|---|
PORT |
3000 |
Server port |
UPLOAD_DIR |
/uploads |
Directory for uploaded files |
Endpoints:
| Method | Path | Description |
|---|---|---|
| GET | /health |
Health check |
| POST | /upload |
Standard upload |
| POST | /upload/fail-always |
Always returns 500 |
| POST | /upload/fail-twice-then-success |
Fails twice, succeeds on 3rd attempt |
| POST | /upload/fail-at-chunk-3 |
Fails on 3rd chunk attempt |
| POST | /upload/reset-test-counters |
Reset failure counters |
The Docker image uses a multi-stage build — TypeScript compilation in stage 1, slim production runtime in stage 2. HEALTHCHECK monitors the /health endpoint every 30 seconds.
127 vitest tests across 2 packages, zero failures.
# Core: 111 tests (6 files) — upload engine, helpers, orchestrator, edge cases
pnpm --filter @universal-uploader/core test
# React: 16 tests (1 file) — hook lifecycle, state transitions, race conditions
pnpm --filter @universal-uploader/react test
# Watch mode
pnpm --filter @universal-uploader/core test:watchTest coverage by package:
| Package | Files | Tests | Covers |
|---|---|---|---|
@universal-uploader/core |
6 | 111 | helper utils, orchestrator, edge cases (zero-byte, abort, retry), stream upload, XHR chunked, stream chunked |
@universal-uploader/react |
1 | 16 | idle state, success/error/abort/pause transitions, uploadMethod, throwOnError, stale-upload cleanup, unmount abort |
Test environment: jsdom via vitest. No browser required.
# Install deps
pnpm install
# Run server locally
npx tsx server.ts
# Run all tests
pnpm --filter @universal-uploader/core test && pnpm --filter @universal-uploader/react test
# Build
pnpm build-libs- Online Documentation - Interactive guide (한국어, English)
- @universal-uploader/core - Core upload engine API
- @universal-uploader/react - React Hook guide
- Demo Source - Demo app code
MIT