A modern, zero-dependency TypeScript browser package for file management. Handles chunked uploads (bypassing server body-size limits), batch uploads, file viewing with state management, gallery listing, downloads, and metadata caching — all through a clean singleton API.
- Installation
- Quick Start
- Configuration
- API Reference
- Use Cases
- 1. Single File Upload with Progress
- 2. Upload with Visibility and Metadata
- 3. Pause, Resume, and Abort Upload
- 4. Upload Multiple Files
- 5. View an Image
- 6. View a Video
- 7. View a PDF
- 8. Play Audio
- 9. Reactive Viewer with Loading States
- 10. Browse Gallery (All Files)
- 11. Browse Gallery by User
- 12. Gallery with Filters and Sorting
- 13. Download a File
- 14. Get File Metadata
- 15. Delete a File
- 16. Update Token (Auth Refresh)
- 17. Initialise Without Token, Authenticate Later
- 18. Error Handling
- 19. Cache Management
- 20. Cleanup and Destroy
- Backend API Endpoints
- Types
- License
yarn add @weconjs/client-fmimport { FileManager } from '@weconjs/client-fm';
// 1. Create a singleton instance
const fm = FileManager.create({
baseUrl: 'https://api.example.com/fm',
token: 'your-jwt-token',
});
// 2. Load server config (chunk size, allowed types, etc.)
await fm.ready();
// 3. Upload a file
const uploader = fm.upload(fileInput.files[0], { visibility: 'public' });
uploader.on('progress', ({ percentage }) => console.log(`${percentage}%`));
const result = await uploader.start();
console.log('Uploaded:', result.url);interface ClientConfig {
/** Base URL of the file manager API. */
baseUrl: string;
/** Bearer token for authentication (can be set later via setToken()). */
token?: string;
/** Max retry attempts for failed requests (default: 3). */
maxRetries?: number;
/** Base delay in ms between retries, doubled each attempt (default: 1000). */
retryDelay?: number;
/** Max entries in the LRU metadata cache (default: 100). */
cacheMaxSize?: number;
/** TTL in ms for cached entries (default: 300000 — 5 min). */
cacheTTL?: number;
}The main entry point. Created once, used everywhere.
| Method | Returns | Description |
|---|---|---|
FileManager.create(config) |
FileManager |
Create and register the singleton |
FileManager.getInstance() |
FileManager |
Retrieve the existing singleton |
fm.init() |
Promise<void> |
Eagerly fetch server chunk config |
fm.ready() |
Promise<FileManager> |
Ensure server config is loaded (idempotent) |
fm.setToken(token) |
void |
Update the auth token at any time |
fm.upload(file, options?) |
ChunkedUploader |
Start a chunked upload |
fm.uploadMany(entries) |
BatchUploader |
Start a multi-file batch upload |
fm.download(fileId) |
Promise<Blob> |
Download a file as a Blob |
fm.getFileInfo(fileId) |
Promise<FileRecord> |
Get file metadata (cached) |
fm.gallery(query?) |
Promise<GalleryResponse> |
List files with filters |
fm.userGallery(userId, query?) |
Promise<GalleryResponse> |
List files by user |
fm.view(fileId, params?) |
FileViewer |
Create a headless file viewer |
fm.deleteFile(fileId) |
Promise<void> |
Delete a file |
fm.clearCache() |
void |
Clear all cached metadata |
fm.destroy() |
void |
Destroy the singleton and cleanup |
Returned by fm.upload(). Manages the lifecycle of a single file upload.
| Method / Property | Returns | Description |
|---|---|---|
uploader.start() |
Promise<UploadCompleteResponse> |
Start the upload |
uploader.pause() |
void |
Pause after the current chunk |
uploader.resume() |
void |
Resume a paused upload |
uploader.abort() |
void |
Cancel the upload |
uploader.status |
UploadStatus |
Current status |
uploader.progress |
number |
Percentage 0–100 |
uploader.fileId |
string | null |
Server-assigned file ID |
Events:
| Event | Payload | When |
|---|---|---|
progress |
{ loaded, total, percentage, chunkIndex } |
After each chunk succeeds |
chunk-complete |
{ chunkIndex, totalChunks } |
After each chunk succeeds |
complete |
UploadCompleteResponse |
All chunks uploaded and finalised |
error |
{ error, chunkIndex?, retriesLeft } |
A chunk failed (may be retried) |
abort |
void |
Upload was aborted |
Returned by fm.uploadMany(). Manages multiple sequential file uploads.
| Method / Property | Returns | Description |
|---|---|---|
batch.start() |
Promise<BatchFileResult[]> |
Start all uploads |
batch.abort() |
void |
Abort all uploads |
batch.getUploader(index) |
ChunkedUploader | undefined |
Access individual uploader |
batch.isRunning |
boolean |
Whether the batch is in progress |
batch.totalFiles |
number |
Total number of files |
Events:
| Event | Payload | When |
|---|---|---|
progress |
{ completedFiles, totalFiles, percentage, currentFilePercentage, currentFileIndex } |
Progress on any file |
file-complete |
{ index, result } |
A file finished uploading |
file-error |
{ index, error } |
A file failed (batch continues) |
complete |
BatchFileResult[] |
All files processed |
abort |
void |
Batch was aborted |
Returned by fm.view(). Headless file viewer with state management.
| Method / Property | Returns | Description |
|---|---|---|
viewer.load() |
Promise<ViewResult> |
Fetch metadata, blob, create URL |
viewer.reload() |
Promise<ViewResult> |
Re-fetch bypassing cache |
viewer.dispose() |
void |
Must call — revokes object URL |
viewer.state |
ViewResult |
Full state snapshot |
viewer.status |
ViewStatus |
'idle' | 'loading' | 'loaded' | 'error' |
viewer.src |
string | null |
Object URL ready for src attrs |
viewer.mediaCategory |
MediaCategory |
'image' | 'video' | 'audio' | 'pdf' | 'text' | 'unknown' |
Events:
| Event | Payload | When |
|---|---|---|
state-change |
ViewResult |
On every state transition |
loaded |
ViewResult |
When file is loaded and ready |
error |
{ error, fileId } |
When loading fails |
ViewFileParams:
interface ViewFileParams {
as?: MediaCategory; // Force media category
width?: number; // Image: desired width
height?: number; // Image: desired height
quality?: number; // Image: quality 1–100
stream?: boolean; // Video/Audio: request streamable URL
page?: number; // PDF: specific page
headers?: Record<string, string>; // Custom headers (e.g. Range)
}const fm = FileManager.create({ baseUrl: 'https://api.example.com/fm', token: 'jwt' });
await fm.ready();
const uploader = fm.upload(fileInput.files[0]);
uploader.on('progress', ({ percentage, chunkIndex }) => {
progressBar.style.width = `${percentage}%`;
console.log(`Chunk ${chunkIndex} done — ${percentage}%`);
});
uploader.on('complete', (result) => {
console.log('Done!', result.url);
});
const result = await uploader.start();const uploader = fm.upload(file, {
visibility: 'public',
metadata: {
description: 'Profile photo',
tags: ['avatar', 'user-123'],
uploadedBy: 'user-123',
},
});
const result = await uploader.start();const uploader = fm.upload(largeFile);
uploader.start(); // don't await — control flow independently
// Pause after the current chunk completes
pauseBtn.onclick = () => uploader.pause();
// Resume
resumeBtn.onclick = () => uploader.resume();
// Abort entirely (cancels in-flight request)
cancelBtn.onclick = () => uploader.abort();
// Check status at any time
console.log(uploader.status); // 'uploading' | 'paused' | 'aborted' | ...
console.log(uploader.progress); // 0–100const batch = fm.uploadMany([
{ file: photoFile, options: { visibility: 'public' } },
{ file: documentFile, options: { visibility: 'private', metadata: { type: 'invoice' } } },
{ file: videoFile },
]);
// Aggregate progress across all files
batch.on('progress', ({ completedFiles, totalFiles, percentage }) => {
progressLabel.textContent = `${completedFiles}/${totalFiles} files — ${percentage}%`;
});
// Per-file callbacks
batch.on('file-complete', ({ index, result }) => {
console.log(`File ${index} uploaded:`, result.url);
});
batch.on('file-error', ({ index, error }) => {
console.error(`File ${index} failed:`, error.message);
// Batch continues to the next file automatically
});
const results = await batch.start();
// Inspect results
results.forEach(({ file, result, error }) => {
if (error) console.error(`${file.name} failed:`, error);
else console.log(`${file.name} uploaded:`, result!.url);
});const viewer = fm.view('file-id-123', {
as: 'image',
width: 800,
quality: 85,
});
const { status, src } = await viewer.load();
if (status === 'loaded') {
imgElement.src = src!;
}
// IMPORTANT: dispose when done (e.g. on component unmount)
viewer.dispose();const viewer = fm.view('video-file-id', { stream: true });
const result = await viewer.load();
if (result.status === 'loaded') {
videoElement.src = result.src!;
videoElement.play();
}
// Cleanup
viewer.dispose();const viewer = fm.view('pdf-file-id', { page: 1 });
const { status, src } = await viewer.load();
if (status === 'loaded') {
iframeElement.src = src!;
}
viewer.dispose();const viewer = fm.view('audio-file-id');
const { status, src, mediaCategory } = await viewer.load();
if (status === 'loaded' && mediaCategory === 'audio') {
audioElement.src = src!;
audioElement.play();
}
viewer.dispose();The viewer emits state-change on every transition, making it perfect for reactive UIs:
const viewer = fm.view('file-id-456');
viewer.on('state-change', ({ status, src, mediaCategory, error }) => {
switch (status) {
case 'loading':
spinner.style.display = 'block';
content.style.display = 'none';
errorEl.style.display = 'none';
break;
case 'loaded':
spinner.style.display = 'none';
content.style.display = 'block';
if (mediaCategory === 'image') imgEl.src = src!;
if (mediaCategory === 'video') videoEl.src = src!;
if (mediaCategory === 'audio') audioEl.src = src!;
if (mediaCategory === 'pdf') iframeEl.src = src!;
break;
case 'error':
spinner.style.display = 'none';
errorEl.style.display = 'block';
errorEl.textContent = error!.message;
break;
}
});
viewer.load();
// Reload with fresh data (bypasses cache)
refreshBtn.onclick = () => viewer.reload();
// Cleanup
unmount(() => viewer.dispose());const gallery = await fm.gallery({ page: 1, limit: 20 });
console.log(`${gallery.total} files, page ${gallery.page}/${gallery.totalPages}`);
gallery.data.forEach((file) => {
console.log(file.fileName, file.mimeType, file.size, file.url);
});const userFiles = await fm.userGallery('user-123', {
page: 1,
limit: 10,
});
userFiles.data.forEach((file) => {
console.log(file.fileName, file.visibility);
});// Only public images, sorted by size descending
const images = await fm.gallery({
visibility: 'public',
mimeType: 'image/*',
sortBy: 'size',
sortOrder: 'desc',
page: 1,
limit: 50,
});const blob = await fm.download('file-id-789');
// Trigger browser download
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'my-file.pdf';
a.click();
URL.revokeObjectURL(url);const file = await fm.getFileInfo('file-id-789');
console.log(file.fileName); // "report.pdf"
console.log(file.mimeType); // "application/pdf"
console.log(file.size); // 1048576
console.log(file.visibility); // "private"
console.log(file.url); // "https://..."
console.log(file.metadata); // { description: "..." }
console.log(file.createdAt); // "2025-01-15T10:30:00Z"await fm.deleteFile('file-id-789');// Create instance without a token
const fm = FileManager.create({
baseUrl: 'https://api.example.com/fm',
});
// After user logs in
fm.setToken(authResponse.accessToken);
// After token refresh
fm.setToken(newAccessToken);const fm = FileManager.create({
baseUrl: 'https://api.example.com/fm',
});
// Public gallery works without a token (if server allows)
const gallery = await fm.gallery({ visibility: 'public' });
// After login, set token and unlock protected features
fm.setToken(jwt);
await fm.ready(); // fetch server config for uploads
const uploader = fm.upload(file, { visibility: 'private' });
await uploader.start();import { FileManager } from '@weconjs/client-fm';
const fm = FileManager.create({ baseUrl: 'https://api.example.com/fm', token: 'jwt' });
try {
await fm.ready();
} catch (err) {
console.error('Failed to connect to server:', err);
}
// Upload with granular error handling
const uploader = fm.upload(file);
uploader.on('error', ({ error, chunkIndex, retriesLeft }) => {
console.warn(
`Chunk ${chunkIndex} failed: ${error.message}. ` +
`${retriesLeft} retries remaining.`
);
});
try {
const result = await uploader.start();
} catch (err) {
if (err instanceof DOMException && err.name === 'AbortError') {
console.log('Upload was aborted');
} else {
console.error('Upload failed permanently:', err);
}
}Metadata and gallery results are cached automatically with LRU + TTL:
// First call: fetches from server
const file1 = await fm.getFileInfo('file-id');
// Second call: served from cache (instant)
const file2 = await fm.getFileInfo('file-id');
// Force clear all cached data
fm.clearCache();
// Viewer also supports cache bypass
const viewer = fm.view('file-id');
await viewer.load(); // uses cache
await viewer.reload(); // bypasses cache// Destroy the singleton (clears cache, resets state)
fm.destroy();
// Access anywhere via singleton
const fm = FileManager.getInstance();
// Create a fresh instance
const newFm = FileManager.create({ baseUrl: '...' });Below is the full specification of the API endpoints this package expects your backend to implement.
Returns the server's chunk configuration. Called by fm.init() / fm.ready().
Headers:
Authorization: Bearer <token>
Response 200 OK:
{
"chunkSize": 512000,
"maxFileSize": 5242880,
"allowedMimeTypes": ["image/jpeg", "image/png", "application/pdf", "video/mp4"]
}| Field | Type | Description |
|---|---|---|
chunkSize |
number |
Size of each chunk in bytes |
maxFileSize |
number |
Max total file size allowed |
allowedMimeTypes |
string[]? |
Whitelist of MIME types (empty = all) |
Initialise a new chunked upload. Called once before any chunks are sent.
Headers:
Authorization: Bearer <token>
Content-Type: application/json
Request body:
{
"fileName": "report.pdf",
"fileSize": 15728640,
"mimeType": "application/pdf",
"visibility": "private",
"metadata": {
"description": "Q4 Financial Report",
"department": "finance"
},
"totalChunks": 31
}| Field | Type | Required | Description |
|---|---|---|---|
fileName |
string |
✅ | Original file name |
fileSize |
number |
✅ | Total file size in bytes |
mimeType |
string |
✅ | File MIME type |
visibility |
'public' | 'private' |
❌ | File access level (default: 'private') |
metadata |
object |
❌ | Arbitrary key-value metadata |
totalChunks |
number |
✅ | Expected number of chunks |
Response 201 Created:
{
"fileId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"uploadToken": "scoped-upload-token-xyz"
}| Field | Type | Description |
|---|---|---|
fileId |
string |
Server-assigned file identifier |
uploadToken |
string? |
Optional scoped token for this upload |
Upload a single chunk. Called sequentially for each chunk (0 to N-1).
URL Parameters:
| Param | Type | Description |
|---|---|---|
fileId |
string |
File ID from the init response |
Headers:
Authorization: Bearer <token>
Note: Content-Type is
multipart/form-data— set automatically by the browser.
Request body (FormData):
| Field | Type | Description |
|---|---|---|
chunk |
Blob |
The chunk data |
chunkIndex |
string |
0-based chunk index |
totalChunks |
string |
Total number of chunks |
Response 200 OK:
{
"received": true
}Finalise the upload after all chunks have been received.
URL Parameters:
| Param | Type | Description |
|---|---|---|
fileId |
string |
File ID from the init response |
Headers:
Authorization: Bearer <token>
Response 200 OK:
{
"fileId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"url": "https://cdn.example.com/files/f47ac10b.pdf",
"size": 15728640,
"mimeType": "application/pdf"
}| Field | Type | Description |
|---|---|---|
fileId |
string |
File identifier |
url |
string |
Public/signed URL to the assembled file |
size |
number |
Final file size in bytes |
mimeType |
string |
File MIME type |
List files with optional filtering, sorting, and pagination.
Headers:
Authorization: Bearer <token>
Query Parameters:
| Param | Type | Default | Description |
|---|---|---|---|
page |
number |
1 |
Page number (1-based) |
limit |
number |
20 |
Items per page |
userId |
string |
— | Filter by file owner |
visibility |
'public' | 'private' |
— | Filter by visibility |
mimeType |
string |
— | Filter by MIME (e.g. image/*) |
sortBy |
'createdAt' | 'size' | 'fileName' |
'createdAt' |
Sort field |
sortOrder |
'asc' | 'desc' |
'desc' |
Sort direction |
Example: GET /files?page=1&limit=10&userId=user-123&visibility=public&sortBy=size&sortOrder=desc
Response 200 OK:
{
"data": [
{
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"fileName": "photo.jpg",
"mimeType": "image/jpeg",
"size": 2048576,
"visibility": "public",
"url": "https://cdn.example.com/files/photo.jpg",
"metadata": { "description": "Team photo" },
"createdAt": "2025-01-15T10:30:00Z",
"updatedAt": "2025-01-15T10:30:00Z"
}
],
"total": 42,
"page": 1,
"limit": 10,
"totalPages": 5
}Get metadata for a single file.
URL Parameters:
| Param | Type | Description |
|---|---|---|
fileId |
string |
File identifier |
Headers:
Authorization: Bearer <token>
Response 200 OK:
{
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"fileName": "report.pdf",
"mimeType": "application/pdf",
"size": 15728640,
"visibility": "private",
"url": "https://cdn.example.com/files/report.pdf",
"metadata": { "department": "finance" },
"createdAt": "2025-01-15T10:30:00Z",
"updatedAt": "2025-01-15T10:30:00Z"
}Download the file content as a binary blob.
URL Parameters:
| Param | Type | Description |
|---|---|---|
fileId |
string |
File identifier |
Headers:
Authorization: Bearer <token>
Optional Query Parameters (used by the viewer for server-side transformations):
| Param | Type | Description |
|---|---|---|
w |
number |
Desired width (images) |
h |
number |
Desired height (images) |
q |
number |
Quality 1–100 (images) |
page |
number |
Specific page (PDFs) |
stream |
'true' |
Request streamable format (video/audio) |
Example: GET /files/abc123/download?w=800&h=600&q=85
Response 200 OK:
- Content-Type:
<file mime type> - Body: binary file content
Delete a file from the server.
URL Parameters:
| Param | Type | Description |
|---|---|---|
fileId |
string |
File identifier |
Headers:
Authorization: Bearer <token>
Response 204 No Content
All types are exported from the package for full TypeScript support:
import type {
// Config
ClientConfig,
ServerChunkConfig,
// File
FileRecord,
FileVisibility,
UploadOptions,
UploadInitRequest,
UploadInitResponse,
UploadChunkPayload,
UploadCompleteResponse,
UploadStatus,
UploadEvents,
UploadProgressPayload,
// Gallery
GalleryQuery,
GalleryResponse,
PaginatedResponse,
// Viewer
ViewFileParams,
ViewResult,
ViewStatus,
MediaCategory,
ViewerEvents,
// Batch
BatchFileEntry,
BatchProgressPayload,
BatchFileResult,
BatchUploadEvents,
} from '@weconjs/client-fm';MIT