Skip to content

weconjs/client-fm

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

@weconjs/client-fm

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.


Table of Contents


Installation

yarn add @weconjs/client-fm

Quick Start

import { 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);

Configuration

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

API Reference

FileManager

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

ChunkedUploader

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

BatchUploader

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

FileViewer

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

Use Cases

1. Single File Upload with Progress

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

2. Upload with Visibility and Metadata

const uploader = fm.upload(file, {
  visibility: 'public',
  metadata: {
    description: 'Profile photo',
    tags: ['avatar', 'user-123'],
    uploadedBy: 'user-123',
  },
});

const result = await uploader.start();

3. Pause, Resume, and Abort Upload

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–100

4. Upload Multiple Files

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

5. View an Image

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

6. View a Video

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

7. View a PDF

const viewer = fm.view('pdf-file-id', { page: 1 });

const { status, src } = await viewer.load();
if (status === 'loaded') {
  iframeElement.src = src!;
}

viewer.dispose();

8. Play Audio

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

9. Reactive Viewer with Loading States

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

10. Browse Gallery (All Files)

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

11. Browse Gallery by User

const userFiles = await fm.userGallery('user-123', {
  page: 1,
  limit: 10,
});

userFiles.data.forEach((file) => {
  console.log(file.fileName, file.visibility);
});

12. Gallery with Filters and Sorting

// Only public images, sorted by size descending
const images = await fm.gallery({
  visibility: 'public',
  mimeType: 'image/*',
  sortBy: 'size',
  sortOrder: 'desc',
  page: 1,
  limit: 50,
});

13. Download a File

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

14. Get File Metadata

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"

15. Delete a File

await fm.deleteFile('file-id-789');

16. Update Token (Auth Refresh)

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

17. Initialise Without Token, Authenticate Later

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

18. Error Handling

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

19. Cache Management

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

20. Cleanup and Destroy

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

Backend API Endpoints

Below is the full specification of the API endpoints this package expects your backend to implement.


GET /config

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)

POST /files/init

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

POST /files/:fileId/chunks

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
}

POST /files/:fileId/complete

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

GET /files

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 /files/:fileId

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

GET /files/:fileId/download

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 /files/:fileId

Delete a file from the server.

URL Parameters:

Param Type Description
fileId string File identifier

Headers:

Authorization: Bearer <token>

Response 204 No Content


Types

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

License

MIT

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors