Skip to content

jdy8739/universal-uploader

Repository files navigation

Universal Stream Uploader

npm version npm downloads GitHub license tests

High-performance file uploads with Web Streams API, automatic fallbacks, and resumable uploads. < 10 kB gzipped, zero dependencies, 100% TypeScript.

Project Structure

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

Three Upload Methods

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).

Installation

npm install @universal-uploader/core
# or use @universal-uploader/react for React hook version
yarn add @universal-uploader/core
pnpm add @universal-uploader/core

NPM Packages:

Tree-Shakable Imports

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

Quick Start

Core

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...'),
  },
});

React Hook

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

Features

  • 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() (retry deprecated in React hook)
  • Type-Safe - Full TypeScript support
  • Tiny - < 10 kB gzipped, zero dependencies

📚 Interactive Guide & Documentation - Full documentation with examples (한국어, English)

Upload Controls

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 options

Configuration

interface 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

Browser Support
Chrome / Edge ✅ Fetch Stream
Firefox ✅ Fetch Stream
Safari ✅ XHR Fallback
IE 11+ ✅ XHR Fallback

Docker (Test Server)

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 down

Runtime 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.

Testing

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:watch

Test 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.

Development Quick Start

# 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

Documentation

License

MIT

About

High-performance file uploads for modern browsers. Consistent memory usage, intelligent stream-XHR fallback, and production-grade resilience.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors