@wlindabla/file_uploader 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +2172 -0
- package/dist/cache/index.js +295 -0
- package/dist/cache/index.js.map +1 -0
- package/dist/core/index.js +724 -0
- package/dist/core/index.js.map +1 -0
- package/dist/events/chunk/index.js +43 -0
- package/dist/events/chunk/index.js.map +1 -0
- package/dist/events/complete/index.js +117 -0
- package/dist/events/complete/index.js.map +1 -0
- package/dist/events/index.js +84 -0
- package/dist/events/index.js.map +1 -0
- package/dist/events/initialize/index.js +70 -0
- package/dist/events/initialize/index.js.map +1 -0
- package/dist/events/state/index.js +97 -0
- package/dist/events/state/index.js.map +1 -0
- package/dist/exceptions/index.js +133 -0
- package/dist/exceptions/index.js.map +1 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/dist/subscribers/index.js +162 -0
- package/dist/subscribers/index.js.map +1 -0
- package/dist/types/index.js +30 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/index.js +183 -0
- package/dist/utils/index.js.map +1 -0
- package/package.json +95 -0
package/README.md
ADDED
|
@@ -0,0 +1,2172 @@
|
|
|
1
|
+
# @wlindabla/file_uploader
|
|
2
|
+
|
|
3
|
+
> A powerful, event-driven chunked file uploader for Browser and Node.js — Universal, Resumable, and Production-Ready.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## 📖 Table of Contents
|
|
7
|
+
|
|
8
|
+
- [@wlindabla/file\_uploader](#wlindablafile_uploader)
|
|
9
|
+
- [📖 Table of Contents](#-table-of-contents)
|
|
10
|
+
- [Overview](#overview)
|
|
11
|
+
- [Features](#features)
|
|
12
|
+
- [Installation](#installation)
|
|
13
|
+
- [Requirements](#requirements)
|
|
14
|
+
- [Quick Start](#quick-start)
|
|
15
|
+
- [Browser (Minimal Example)](#browser-minimal-example)
|
|
16
|
+
- [Core Concepts](#core-concepts)
|
|
17
|
+
- [How It Works](#how-it-works)
|
|
18
|
+
- [Upload Lifecycle](#upload-lifecycle)
|
|
19
|
+
- [The Three-Endpoint Pattern](#the-three-endpoint-pattern)
|
|
20
|
+
- [API Reference](#api-reference)
|
|
21
|
+
- [ChunkedFileUploader](#chunkedfileuploader)
|
|
22
|
+
- [Constructor](#constructor)
|
|
23
|
+
- [Methods](#methods)
|
|
24
|
+
- [`.withFile(file: File): this`](#withfilefile-file-this)
|
|
25
|
+
- [`.withEndpoints(endpoints: UploadEndpoints): this`](#withendpointsendpoints-uploadendpoints-this)
|
|
26
|
+
- [`.upload(): Promise<void>`](#upload-promisevoid)
|
|
27
|
+
- [`.pause(): void`](#pause-void)
|
|
28
|
+
- [`.resume(): void`](#resume-void)
|
|
29
|
+
- [`.cancel(): void`](#cancel-void)
|
|
30
|
+
- [`.getState(): UploadState`](#getstate-uploadstate)
|
|
31
|
+
- [`.resumeUpload(resumeData: ResumeData): Promise<void>`](#resumeuploadresumedata-resumedata-promisevoid)
|
|
32
|
+
- [`.loadResumeData(fileName: string): Promise<ResumeData | null>`](#loadresumedatafilename-string-promiseresumedata--null)
|
|
33
|
+
- [HttpFileUploaderEvents](#httpfileuploaderevents)
|
|
34
|
+
- [UploadOptions](#uploadoptions)
|
|
35
|
+
- [Chunk Size Auto-Calculation](#chunk-size-auto-calculation)
|
|
36
|
+
- [UploadEndpoints](#uploadendpoints)
|
|
37
|
+
- [UploadResumeCacheInterface](#uploadresumecacheinterface)
|
|
38
|
+
- [Example Implementations](#example-implementations)
|
|
39
|
+
- [Browser — localStorage](#browser--localstorage)
|
|
40
|
+
- [Browser — IndexedDB](#browser--indexeddb)
|
|
41
|
+
- [Node.js — Redis](#nodejs--redis)
|
|
42
|
+
- [Node.js — Filesystem](#nodejs--filesystem)
|
|
43
|
+
- [Events System](#events-system)
|
|
44
|
+
- [Event Architecture](#event-architecture)
|
|
45
|
+
- [All Available Events](#all-available-events)
|
|
46
|
+
- [Initialize Events](#initialize-events)
|
|
47
|
+
- [Chunk Upload Events](#chunk-upload-events)
|
|
48
|
+
- [Upload State Events](#upload-state-events)
|
|
49
|
+
- [Completion Events](#completion-events)
|
|
50
|
+
- [Metadata Events](#metadata-events)
|
|
51
|
+
- [Event Classes Reference](#event-classes-reference)
|
|
52
|
+
- [`UploadProgressEvent`](#uploadprogressevent)
|
|
53
|
+
- [`UploadStateChangedEvent`](#uploadstatechangedevent)
|
|
54
|
+
- [`UploadMediaCompleteEvent`](#uploadmediacompleteevent)
|
|
55
|
+
- [`UploadCancelledEvent`](#uploadcancelledevent)
|
|
56
|
+
- [`UploadPausedEvent`](#uploadpausedevent)
|
|
57
|
+
- [`UploadResumedEvent`](#uploadresumedevent)
|
|
58
|
+
- [`ChunkUploadHttpErrorResponseEvent`](#chunkuploadhttperrorresponseevent)
|
|
59
|
+
- [`InitializeUploadStartedEvent`](#initializeuploadstartedevent)
|
|
60
|
+
- [`InitializeUploadSuccessEvent`](#initializeuploadsuccessevent)
|
|
61
|
+
- [`InitializeUploadFailureEvent`](#initializeuploadfailureevent)
|
|
62
|
+
- [`ResumeUploadEvent`](#resumeuploadevent)
|
|
63
|
+
- [Subscribers — Upload Lifecycle Handlers](#subscribers--upload-lifecycle-handlers)
|
|
64
|
+
- [⚠️ Important — You Must Register the Subscribers](#️-important--you-must-register-the-subscribers)
|
|
65
|
+
- [Registration](#registration)
|
|
66
|
+
- [Browser Environment](#browser-environment)
|
|
67
|
+
- [Node.js Environment](#nodejs-environment)
|
|
68
|
+
- [Complete Setup Example](#complete-setup-example)
|
|
69
|
+
- [Browser](#browser)
|
|
70
|
+
- [Node.js](#nodejs)
|
|
71
|
+
- [InitializeUploadSubscriber](#initializeuploadsubscriber)
|
|
72
|
+
- [What It Does Internally](#what-it-does-internally)
|
|
73
|
+
- [Events It Dispatches](#events-it-dispatches)
|
|
74
|
+
- [Retry Behavior](#retry-behavior)
|
|
75
|
+
- [Error Cases Handled](#error-cases-handled)
|
|
76
|
+
- [FinalizeUploadSubscriber](#finalizeuploadsubscriber)
|
|
77
|
+
- [What It Does Internally](#what-it-does-internally-1)
|
|
78
|
+
- [Events It Dispatches](#events-it-dispatches-1)
|
|
79
|
+
- [Error Cases Handled](#error-cases-handled-1)
|
|
80
|
+
- [Subscriber Priority](#subscriber-priority)
|
|
81
|
+
- [Common Mistakes](#common-mistakes)
|
|
82
|
+
- [❌ Forgetting to register subscribers](#-forgetting-to-register-subscribers)
|
|
83
|
+
- [❌ Registering subscribers AFTER calling upload()](#-registering-subscribers-after-calling-upload)
|
|
84
|
+
- [✅ Correct order](#-correct-order)
|
|
85
|
+
- [Subscribers](#subscribers)
|
|
86
|
+
- [InitializeUploadSubscriber](#initializeuploadsubscriber-1)
|
|
87
|
+
- [FinalizeUploadSubscriber](#finalizeuploadsubscriber-1)
|
|
88
|
+
- [Types Reference](#types-reference)
|
|
89
|
+
- [`UploadState` (enum)](#uploadstate-enum)
|
|
90
|
+
- [`ChunkInfo`](#chunkinfo)
|
|
91
|
+
- [`UploadProgress`](#uploadprogress)
|
|
92
|
+
- [`ResumeData`](#resumedata)
|
|
93
|
+
- [`UploadResult`](#uploadresult)
|
|
94
|
+
- [`ChunkError`](#chunkerror)
|
|
95
|
+
- [`InitializeUploadResponse`](#initializeuploadresponse)
|
|
96
|
+
- [Exceptions](#exceptions)
|
|
97
|
+
- [`InitializeUploadFailureException`](#initializeuploadfailureexception)
|
|
98
|
+
- [`FileUploadChunkError`](#fileuploadchunkerror)
|
|
99
|
+
- [`ChunkUploadHttpErrorException`](#chunkuploadhttperrorexception)
|
|
100
|
+
- [`UploadCancelledException`](#uploadcancelledexception)
|
|
101
|
+
- [Advanced Usage](#advanced-usage)
|
|
102
|
+
- [Concurrency Control](#concurrency-control)
|
|
103
|
+
- [Pause and Resume](#pause-and-resume)
|
|
104
|
+
- [Cancel Upload](#cancel-upload)
|
|
105
|
+
- [Resumable Upload](#resumable-upload)
|
|
106
|
+
- [Custom Logger](#custom-logger)
|
|
107
|
+
- [Custom Cache Implementation](#custom-cache-implementation)
|
|
108
|
+
- [Environment-Specific Examples](#environment-specific-examples)
|
|
109
|
+
- [Browser Example](#browser-example)
|
|
110
|
+
- [Node.js Example](#nodejs-example)
|
|
111
|
+
- [Server-Side Integration Guide](#server-side-integration-guide)
|
|
112
|
+
- [Init Endpoint](#init-endpoint)
|
|
113
|
+
- [Chunk Upload Endpoint](#chunk-upload-endpoint)
|
|
114
|
+
- [Finalize Endpoint](#finalize-endpoint)
|
|
115
|
+
- [Error Handling](#error-handling)
|
|
116
|
+
- [Complete Error Handling Example](#complete-error-handling-example)
|
|
117
|
+
- [Contributing](#contributing)
|
|
118
|
+
- [Development Setup](#development-setup)
|
|
119
|
+
- [Running Tests](#running-tests)
|
|
120
|
+
- [License](#license)
|
|
121
|
+
- [Support](#support)
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Overview
|
|
126
|
+
|
|
127
|
+
`@wlindabla/file_uploader` is a **universal chunked file upload library** built on top of
|
|
128
|
+
[`@wlindabla/http_client`](https://github.com/Agbokoudjo/http_client) and
|
|
129
|
+
[`@wlindabla/event_dispatcher`](https://github.com/Agbokoudjo/event_dispatcher).
|
|
130
|
+
|
|
131
|
+
It splits large files into small chunks, uploads them in parallel with configurable
|
|
132
|
+
concurrency, and provides a **Symfony-inspired event system** so your application
|
|
133
|
+
can react to every stage of the upload lifecycle — from initialization to completion.
|
|
134
|
+
|
|
135
|
+
The library is designed to work seamlessly in **browsers** (via the File API) and
|
|
136
|
+
in **Node.js** (server-to-server transfers), with full TypeScript support.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Features
|
|
141
|
+
|
|
142
|
+
- 🚀 **Chunked Upload** — Split large files into manageable chunks
|
|
143
|
+
- ⚡ **Concurrent Uploads** — Upload multiple chunks in parallel (configurable)
|
|
144
|
+
- 🔄 **Resumable** — Resume interrupted uploads from where they stopped
|
|
145
|
+
- 🎯 **Event-Driven** — Rich Symfony-style event system for full lifecycle control
|
|
146
|
+
- 🌐 **Universal** — Works in Browser and Node.js
|
|
147
|
+
- 💪 **TypeScript-First** — Full type safety with generics
|
|
148
|
+
- 🔌 **Extensible** — Bring your own cache, logger, and event dispatcher
|
|
149
|
+
- 🛡️ **Robust** — Built-in retry logic, exponential backoff, and error handling
|
|
150
|
+
- 🪶 **Lightweight** — Minimal core dependencies
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Installation
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
# Using yarn (recommended)
|
|
158
|
+
yarn add @wlindabla/file_uploader @wlindabla/http_client @wlindabla/event_dispatcher
|
|
159
|
+
|
|
160
|
+
# Using npm
|
|
161
|
+
npm install @wlindabla/file_uploader @wlindabla/http_client @wlindabla/event_dispatcher
|
|
162
|
+
|
|
163
|
+
# Using pnpm
|
|
164
|
+
pnpm add @wlindabla/file_uploader @wlindabla/http_client @wlindabla/event_dispatcher
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Requirements
|
|
170
|
+
|
|
171
|
+
| Requirement | Version |
|
|
172
|
+
|-------------|---------|
|
|
173
|
+
| Node.js | >= 18.0.0 |
|
|
174
|
+
| TypeScript | >= 5.3.0 |
|
|
175
|
+
| `@wlindabla/http_client` | >=^1.0.0 |
|
|
176
|
+
| `@wlindabla/event_dispatcher` | >=^1.0.0 |
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Quick Start
|
|
181
|
+
|
|
182
|
+
### Browser (Minimal Example)
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
import {
|
|
186
|
+
ChunkedFileUploader,
|
|
187
|
+
HttpFileUploaderEvents,
|
|
188
|
+
UploadStateChangedEvent,
|
|
189
|
+
UploadProgressEvent,
|
|
190
|
+
UploadMediaCompleteEvent
|
|
191
|
+
} from '@wlindabla/file_uploader';
|
|
192
|
+
|
|
193
|
+
import { BrowserEventDispatcher } from '@wlindabla/event_dispatcher';
|
|
194
|
+
|
|
195
|
+
// 1. Implement your cache (localStorage example)
|
|
196
|
+
class LocalStorageCache {
|
|
197
|
+
async getItem(key: string) {
|
|
198
|
+
const item = localStorage.getItem(key);
|
|
199
|
+
return item ? JSON.parse(item) : null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async setItem(key: string, value: any) {
|
|
203
|
+
localStorage.setItem(key, JSON.stringify(value));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async removeItem(key: string) {
|
|
207
|
+
localStorage.removeItem(key);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// 2. Create the event dispatcher
|
|
212
|
+
const dispatcher = new BrowserEventDispatcher();
|
|
213
|
+
|
|
214
|
+
// 3. Listen to upload events
|
|
215
|
+
dispatcher.addListener(
|
|
216
|
+
HttpFileUploaderEvents.MEDIA_CHUNK_UPLOAD_SUCCESS,
|
|
217
|
+
(event: UploadProgressEvent) => {
|
|
218
|
+
console.log(`Progress: ${event.percentage}%`);
|
|
219
|
+
}
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
dispatcher.addListener(
|
|
223
|
+
HttpFileUploaderEvents.DOWNLOAD_MEDIA_COMPLETE,
|
|
224
|
+
(event: UploadMediaCompleteEvent) => {
|
|
225
|
+
console.log('Upload complete! File ID:', event.mediaId);
|
|
226
|
+
}
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
// 4. Create the uploader
|
|
230
|
+
const uploader = new ChunkedFileUploader(
|
|
231
|
+
dispatcher,
|
|
232
|
+
new LocalStorageCache(),
|
|
233
|
+
{
|
|
234
|
+
maxRetries: 3,
|
|
235
|
+
concurrency: 3,
|
|
236
|
+
timeout: 60000
|
|
237
|
+
}
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
// 5. Set the file and endpoints, then upload
|
|
241
|
+
const fileInput = document.getElementById('file') as HTMLInputElement;
|
|
242
|
+
const file = fileInput.files![0];
|
|
243
|
+
|
|
244
|
+
await uploader
|
|
245
|
+
.withFile(file)
|
|
246
|
+
.withEndpoints({
|
|
247
|
+
init: 'https://api.example.com/upload/init',
|
|
248
|
+
upload: 'https://api.example.com/upload/chunk',
|
|
249
|
+
finalize: 'https://api.example.com/upload/finalize'
|
|
250
|
+
})
|
|
251
|
+
.upload();
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## Core Concepts
|
|
257
|
+
|
|
258
|
+
### How It Works
|
|
259
|
+
|
|
260
|
+
```
|
|
261
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
262
|
+
│ YOUR APPLICATION │
|
|
263
|
+
│ dispatcher.addListener(HttpFileUploaderEvents.*, handler) │
|
|
264
|
+
└──────────────────────────────┬──────────────────────────────────┘
|
|
265
|
+
│ listens to
|
|
266
|
+
│
|
|
267
|
+
┌───────────────▼──────────────────┐
|
|
268
|
+
│ UPLOAD EVENTS (public) │
|
|
269
|
+
│ HttpFileUploaderEvents.* │
|
|
270
|
+
│ e.g. MEDIA_CHUNK_UPLOAD_SUCCESS │
|
|
271
|
+
└───────────────▲──────────────────┘
|
|
272
|
+
│ emitted by
|
|
273
|
+
│
|
|
274
|
+
┌───────────────┴──────────────────┐
|
|
275
|
+
│ ChunkedFileUploader │
|
|
276
|
+
│ - Slices file into chunks │
|
|
277
|
+
│ - Manages concurrency (p-limit) │
|
|
278
|
+
│ - Manages state machine │
|
|
279
|
+
└──────┬───────────────┬────────────┘
|
|
280
|
+
│ │
|
|
281
|
+
┌─────────▼───┐ ┌───────▼──────────┐
|
|
282
|
+
│ safeFetch │ │ Subscribers │
|
|
283
|
+
│ (init & │ │ Initialize & │
|
|
284
|
+
│ finalize) │ │ Finalize Upload │
|
|
285
|
+
└─────────────┘ └──────────────────┘
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### Upload Lifecycle
|
|
289
|
+
|
|
290
|
+
```
|
|
291
|
+
IDLE → INITIALIZING → UPLOADING → FINALIZING → COMPLETED
|
|
292
|
+
↓ ↓
|
|
293
|
+
FAILED PAUSED ↔ UPLOADING
|
|
294
|
+
↓
|
|
295
|
+
CANCELLED
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### The Three-Endpoint Pattern
|
|
299
|
+
|
|
300
|
+
Your server must expose **three endpoints**:
|
|
301
|
+
|
|
302
|
+
| Endpoint | Method | Purpose |
|
|
303
|
+
|----------|--------|---------|
|
|
304
|
+
| `init` | POST | Creates an upload session, returns a `mediaId` |
|
|
305
|
+
| `upload` | POST | Receives individual file chunks |
|
|
306
|
+
| `finalize` | POST | Assembles chunks and finalizes the upload |
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## API Reference
|
|
311
|
+
|
|
312
|
+
### ChunkedFileUploader
|
|
313
|
+
|
|
314
|
+
The main class. Implements `ChunkedFileUploaderInterface`.
|
|
315
|
+
|
|
316
|
+
#### Constructor
|
|
317
|
+
|
|
318
|
+
```typescript
|
|
319
|
+
new ChunkedFileUploader(
|
|
320
|
+
uploadEventDispatcher: EventDispatcherInterface,
|
|
321
|
+
uploadResumeData: UploadResumeCacheInterface,
|
|
322
|
+
options: UploadOptions,
|
|
323
|
+
logger?: LoggerInterface
|
|
324
|
+
)
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
| Parameter | Type | Required | Description |
|
|
328
|
+
|-----------|------|----------|-------------|
|
|
329
|
+
| `uploadEventDispatcher` | `EventDispatcherInterface` | ✅ | Dispatcher for upload lifecycle events |
|
|
330
|
+
| `uploadResumeData` | `UploadResumeCacheInterface` | ✅ | Cache implementation for resumable uploads |
|
|
331
|
+
| `options` | `UploadOptions` | ✅ | Upload configuration options |
|
|
332
|
+
| `logger` | `LoggerInterface` | ❌ | Custom logger (default: `NoopLogger`) |
|
|
333
|
+
|
|
334
|
+
#### Methods
|
|
335
|
+
|
|
336
|
+
##### `.withFile(file: File): this`
|
|
337
|
+
|
|
338
|
+
Sets the file to upload. **Must be called before `.upload()`.**
|
|
339
|
+
|
|
340
|
+
```typescript
|
|
341
|
+
uploader.withFile(file);
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
| Throws | Condition |
|
|
345
|
+
|--------|-----------|
|
|
346
|
+
| `Error` | If `file` is null or undefined |
|
|
347
|
+
| `Error` | If `file.size === 0` (empty file) |
|
|
348
|
+
| `TypeError` | If value is not a `File` or `Blob` instance |
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
##### `.withEndpoints(endpoints: UploadEndpoints): this`
|
|
353
|
+
|
|
354
|
+
Sets the server endpoints. **Must be called before `.upload()`.**
|
|
355
|
+
|
|
356
|
+
```typescript
|
|
357
|
+
uploader.withEndpoints({
|
|
358
|
+
init: 'https://api.example.com/upload/init',
|
|
359
|
+
upload: 'https://api.example.com/upload/chunk',
|
|
360
|
+
finalize: 'https://api.example.com/upload/finalize'
|
|
361
|
+
});
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
| Throws | Condition |
|
|
365
|
+
|--------|-----------|
|
|
366
|
+
| `Error` | If any endpoint is missing |
|
|
367
|
+
|
|
368
|
+
---
|
|
369
|
+
|
|
370
|
+
##### `.upload(): Promise<void>`
|
|
371
|
+
|
|
372
|
+
Starts the upload process. Internally:
|
|
373
|
+
1. Hashes the file (SHA-256 of first 1MB)
|
|
374
|
+
2. Dispatches `INITIALIZE_UPLOAD` → handled by `InitializeUploadSubscriber`
|
|
375
|
+
3. Splits file into chunks and uploads them with concurrency control
|
|
376
|
+
4. Dispatches `FINALIZE_UPLOAD` → handled by `FinalizeUploadSubscriber`
|
|
377
|
+
|
|
378
|
+
```typescript
|
|
379
|
+
await uploader
|
|
380
|
+
.withFile(file)
|
|
381
|
+
.withEndpoints(endpoints)
|
|
382
|
+
.upload();
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
| Throws | Condition |
|
|
386
|
+
|--------|-----------|
|
|
387
|
+
| `Error` | If `.withFile()` was not called |
|
|
388
|
+
| `Error` | If `.withEndpoints()` was not called |
|
|
389
|
+
| `InitializeUploadFailureException` | If the server init request fails |
|
|
390
|
+
| `FileUploadChunkError` | If a chunk fails after all retries |
|
|
391
|
+
|
|
392
|
+
---
|
|
393
|
+
|
|
394
|
+
##### `.pause(): void`
|
|
395
|
+
|
|
396
|
+
Pauses the upload. The current chunk finishes uploading before pausing.
|
|
397
|
+
|
|
398
|
+
```typescript
|
|
399
|
+
uploader.pause();
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
Dispatches: `HttpFileUploaderEvents.UPLOAD_PAUSED`
|
|
403
|
+
|
|
404
|
+
---
|
|
405
|
+
|
|
406
|
+
##### `.resume(): void`
|
|
407
|
+
|
|
408
|
+
Resumes a paused upload.
|
|
409
|
+
|
|
410
|
+
```typescript
|
|
411
|
+
uploader.resume();
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
Dispatches: `HttpFileUploaderEvents.UPLOAD_RESUMED`
|
|
415
|
+
|
|
416
|
+
---
|
|
417
|
+
|
|
418
|
+
##### `.cancel(): void`
|
|
419
|
+
|
|
420
|
+
Cancels the upload immediately using `AbortController`.
|
|
421
|
+
|
|
422
|
+
```typescript
|
|
423
|
+
uploader.cancel();
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
Dispatches: `HttpFileUploaderEvents.UPLOAD_CANCELLED`
|
|
427
|
+
|
|
428
|
+
---
|
|
429
|
+
|
|
430
|
+
##### `.getState(): UploadState`
|
|
431
|
+
|
|
432
|
+
Returns the current state of the upload.
|
|
433
|
+
|
|
434
|
+
```typescript
|
|
435
|
+
const state = uploader.getState();
|
|
436
|
+
// 'idle' | 'initializing' | 'uploading' | 'paused' | 'cancelled' | 'finalizing' | 'completed' | 'failed'
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
---
|
|
440
|
+
|
|
441
|
+
##### `.resumeUpload(resumeData: ResumeData): Promise<void>`
|
|
442
|
+
|
|
443
|
+
Resumes an upload from a saved state (after page refresh or network failure).
|
|
444
|
+
|
|
445
|
+
```typescript
|
|
446
|
+
const resumeData = await uploader.loadResumeData('my-video.mp4');
|
|
447
|
+
|
|
448
|
+
if (resumeData) {
|
|
449
|
+
await uploader
|
|
450
|
+
.withFile(file)
|
|
451
|
+
.withEndpoints(endpoints)
|
|
452
|
+
.resumeUpload(resumeData);
|
|
453
|
+
}
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
---
|
|
457
|
+
|
|
458
|
+
##### `.loadResumeData(fileName: string): Promise<ResumeData | null>`
|
|
459
|
+
|
|
460
|
+
Loads previously saved upload progress from the cache.
|
|
461
|
+
|
|
462
|
+
```typescript
|
|
463
|
+
const resumeData = await uploader.loadResumeData('my-video.mp4');
|
|
464
|
+
|
|
465
|
+
if (resumeData) {
|
|
466
|
+
console.log(`${resumeData.uploadedChunks} chunks already uploaded`);
|
|
467
|
+
}
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
---
|
|
471
|
+
|
|
472
|
+
### HttpFileUploaderEvents
|
|
473
|
+
|
|
474
|
+
Abstract class containing all event name constants.
|
|
475
|
+
**This is the single source of truth for event names.**
|
|
476
|
+
|
|
477
|
+
```typescript
|
|
478
|
+
import { HttpFileUploaderEvents } from '@wlindabla/file_uploader';
|
|
479
|
+
|
|
480
|
+
dispatcher.addListener(HttpFileUploaderEvents.MEDIA_CHUNK_UPLOAD_SUCCESS, handler);
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
> See [All Available Events](#all-available-events) for the complete list.
|
|
484
|
+
|
|
485
|
+
---
|
|
486
|
+
|
|
487
|
+
### UploadOptions
|
|
488
|
+
|
|
489
|
+
```typescript
|
|
490
|
+
interface UploadOptions {
|
|
491
|
+
// Chunk configuration
|
|
492
|
+
chunkSize?: number; // Chunk size in bytes (auto-calculated if not set)
|
|
493
|
+
speedMbps?: number; // Connection speed hint for auto chunk size calculation
|
|
494
|
+
config?: ChunkSizeConfig; // Custom chunk size thresholds
|
|
495
|
+
|
|
496
|
+
// Retry & concurrency
|
|
497
|
+
maxRetries?: number; // Max retry attempts per chunk (default: 3)
|
|
498
|
+
concurrency?: number; // Max parallel chunk uploads (default: 3)
|
|
499
|
+
|
|
500
|
+
// Timeouts
|
|
501
|
+
timeout?: number; // Chunk upload timeout in ms (default: 60000)
|
|
502
|
+
initTimeout?: number; // Init request timeout in ms (default: 45000)
|
|
503
|
+
|
|
504
|
+
// Headers
|
|
505
|
+
headers?: HeadersInit; // Headers for chunk upload requests
|
|
506
|
+
headerInitialzingUpload?: HeadersInit; // Headers for init request
|
|
507
|
+
headerFinalezingUpload?: HeadersInit; // Headers for finalize request
|
|
508
|
+
|
|
509
|
+
// Metadata
|
|
510
|
+
metadata?: Record<string, any>; // Extra data sent to the init endpoint
|
|
511
|
+
chunkOtherData?: Record<string, any>; // Extra data appended to each chunk FormData
|
|
512
|
+
|
|
513
|
+
// Resume
|
|
514
|
+
autoSave?: boolean; // Auto-save progress after each chunk (default: false)
|
|
515
|
+
}
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
#### Chunk Size Auto-Calculation
|
|
519
|
+
|
|
520
|
+
When `chunkSize` is not provided, the library automatically calculates the optimal
|
|
521
|
+
chunk size based on `fileSize` and the `config` thresholds:
|
|
522
|
+
|
|
523
|
+
```typescript
|
|
524
|
+
// Default thresholds (DEFAULT_CONFIG)
|
|
525
|
+
{
|
|
526
|
+
defaultChunkSizeMB: 50,
|
|
527
|
+
slowSpeedThresholdMbps: 5,
|
|
528
|
+
slowSpeedChunkSizeMB: 2,
|
|
529
|
+
fileSizeThresholds: [
|
|
530
|
+
{ maxSizeMB: 200, chunkSizeMB: 50 },
|
|
531
|
+
{ maxSizeMB: 400, chunkSizeMB: 100 },
|
|
532
|
+
{ maxSizeMB: 800, chunkSizeMB: 300 },
|
|
533
|
+
{ maxSizeMB: 1000, chunkSizeMB: 500 },
|
|
534
|
+
{ maxSizeMB: Infinity, chunkSizeMB: 700 }
|
|
535
|
+
]
|
|
536
|
+
}
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
---
|
|
540
|
+
|
|
541
|
+
### UploadEndpoints
|
|
542
|
+
|
|
543
|
+
```typescript
|
|
544
|
+
interface UploadEndpoints {
|
|
545
|
+
init: string | URL; // Upload session initialization endpoint
|
|
546
|
+
upload: string | URL; // Chunk upload endpoint
|
|
547
|
+
finalize: string | URL; // Upload finalization endpoint
|
|
548
|
+
}
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
---
|
|
552
|
+
|
|
553
|
+
### UploadResumeCacheInterface
|
|
554
|
+
|
|
555
|
+
Your cache implementation **must** implement this interface.
|
|
556
|
+
The library does **not** provide a default implementation to avoid
|
|
557
|
+
unnecessary dependencies (localStorage, IndexedDB, Redis, filesystem, etc.).
|
|
558
|
+
|
|
559
|
+
```typescript
|
|
560
|
+
interface UploadResumeCacheInterface {
|
|
561
|
+
getItem(key: string): Promise<ResumeData | null>;
|
|
562
|
+
setItem(key: string, value: ResumeData): Promise<void>;
|
|
563
|
+
removeItem(key: string): Promise<void>;
|
|
564
|
+
}
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
#### Example Implementations
|
|
568
|
+
|
|
569
|
+
##### Browser — localStorage
|
|
570
|
+
|
|
571
|
+
```typescript
|
|
572
|
+
class LocalStorageCache implements UploadResumeCacheInterface {
|
|
573
|
+
async getItem(key: string): Promise<ResumeData | null> {
|
|
574
|
+
const item = localStorage.getItem(key);
|
|
575
|
+
return item ? JSON.parse(item) : null;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
async setItem(key: string, value: ResumeData): Promise<void> {
|
|
579
|
+
localStorage.setItem(key, JSON.stringify(value));
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
async removeItem(key: string): Promise<void> {
|
|
583
|
+
localStorage.removeItem(key);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
##### Browser — IndexedDB
|
|
589
|
+
|
|
590
|
+
```typescript
|
|
591
|
+
class IndexedDBCache implements UploadResumeCacheInterface {
|
|
592
|
+
private db: IDBDatabase | null = null;
|
|
593
|
+
|
|
594
|
+
private async openDB(): Promise<IDBDatabase> {
|
|
595
|
+
return new Promise((resolve, reject) => {
|
|
596
|
+
const request = indexedDB.open('file-uploader-cache', 1);
|
|
597
|
+
request.onupgradeneeded = () => {
|
|
598
|
+
request.result.createObjectStore('uploads');
|
|
599
|
+
};
|
|
600
|
+
request.onsuccess = () => resolve(request.result);
|
|
601
|
+
request.onerror = () => reject(request.error);
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
async getItem(key: string): Promise<ResumeData | null> {
|
|
606
|
+
const db = await this.openDB();
|
|
607
|
+
return new Promise((resolve, reject) => {
|
|
608
|
+
const tx = db.transaction('uploads', 'readonly');
|
|
609
|
+
const request = tx.objectStore('uploads').get(key);
|
|
610
|
+
request.onsuccess = () => resolve(request.result ?? null);
|
|
611
|
+
request.onerror = () => reject(request.error);
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
async setItem(key: string, value: ResumeData): Promise<void> {
|
|
616
|
+
const db = await this.openDB();
|
|
617
|
+
return new Promise((resolve, reject) => {
|
|
618
|
+
const tx = db.transaction('uploads', 'readwrite');
|
|
619
|
+
tx.objectStore('uploads').put(value, key);
|
|
620
|
+
tx.oncomplete = () => resolve();
|
|
621
|
+
tx.onerror = () => reject(tx.error);
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
async removeItem(key: string): Promise<void> {
|
|
626
|
+
const db = await this.openDB();
|
|
627
|
+
return new Promise((resolve, reject) => {
|
|
628
|
+
const tx = db.transaction('uploads', 'readwrite');
|
|
629
|
+
tx.objectStore('uploads').delete(key);
|
|
630
|
+
tx.oncomplete = () => resolve();
|
|
631
|
+
tx.onerror = () => reject(tx.error);
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
##### Node.js — Redis
|
|
638
|
+
|
|
639
|
+
```typescript
|
|
640
|
+
import { createClient } from 'redis';
|
|
641
|
+
|
|
642
|
+
class RedisCache implements UploadResumeCacheInterface {
|
|
643
|
+
private client = createClient({ url: process.env.REDIS_URL });
|
|
644
|
+
|
|
645
|
+
constructor() {
|
|
646
|
+
this.client.connect();
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
async getItem(key: string): Promise<ResumeData | null> {
|
|
650
|
+
const item = await this.client.get(key);
|
|
651
|
+
return item ? JSON.parse(item) : null;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
async setItem(key: string, value: ResumeData): Promise<void> {
|
|
655
|
+
// TTL: 24 hours
|
|
656
|
+
await this.client.set(key, JSON.stringify(value), { EX: 86400 });
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
async removeItem(key: string): Promise<void> {
|
|
660
|
+
await this.client.del(key);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
##### Node.js — Filesystem
|
|
666
|
+
|
|
667
|
+
```typescript
|
|
668
|
+
import fs from 'fs/promises';
|
|
669
|
+
import path from 'path';
|
|
670
|
+
|
|
671
|
+
class FileSystemCache implements UploadResumeCacheInterface {
|
|
672
|
+
constructor(private readonly cacheDir: string = '/tmp/upload-cache') {
|
|
673
|
+
fs.mkdir(cacheDir, { recursive: true }).catch(() => {});
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
private filePath(key: string): string {
|
|
677
|
+
return path.join(this.cacheDir, `${key}.json`);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
async getItem(key: string): Promise<ResumeData | null> {
|
|
681
|
+
try {
|
|
682
|
+
const content = await fs.readFile(this.filePath(key), 'utf-8');
|
|
683
|
+
return JSON.parse(content);
|
|
684
|
+
} catch {
|
|
685
|
+
return null;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
async setItem(key: string, value: ResumeData): Promise<void> {
|
|
690
|
+
await fs.writeFile(this.filePath(key), JSON.stringify(value), 'utf-8');
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
async removeItem(key: string): Promise<void> {
|
|
694
|
+
await fs.unlink(this.filePath(key)).catch(() => {});
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
## Events System
|
|
700
|
+
|
|
701
|
+
### Event Architecture
|
|
702
|
+
|
|
703
|
+
The library uses a **two-level event architecture**:
|
|
704
|
+
|
|
705
|
+
```
|
|
706
|
+
YOUR APPLICATION
|
|
707
|
+
└── listens to Upload Events (public)
|
|
708
|
+
└── dispatched by ChunkedFileUploader
|
|
709
|
+
└── which internally handles HTTP events (private)
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
You **only need to interact with Upload Events** via `HttpFileUploaderEvents.*`.
|
|
713
|
+
HTTP-level events are handled internally by the library's subscribers.
|
|
714
|
+
|
|
715
|
+
### All Available Events
|
|
716
|
+
|
|
717
|
+
#### Initialize Events
|
|
718
|
+
|
|
719
|
+
| Constant | Value | Dispatched When |
|
|
720
|
+
|----------|-------|-----------------|
|
|
721
|
+
| `INITIALIZE_UPLOAD` | `"initializeUpload"` | Upload session is about to start (internal) |
|
|
722
|
+
| `INITIALIZE_UPLOAD_STARTED` | `"initializeUploadStarted"` | Init HTTP request is sent |
|
|
723
|
+
| `INITIALIZE_UPLOAD_SUCCESS` | `"initializeUploadSuccess"` | Server confirmed the session |
|
|
724
|
+
| `INITIALIZE_UPLOAD_FAILURE` | `"initializeUploadFailure"` | Init request failed |
|
|
725
|
+
|
|
726
|
+
#### Chunk Upload Events
|
|
727
|
+
|
|
728
|
+
| Constant | Value | Dispatched When |
|
|
729
|
+
|----------|-------|-----------------|
|
|
730
|
+
| `MEDIA_CHUNK_UPLOAD_STARTED` | `"mediaChunkUploadStarted"` | A chunk is about to be uploaded |
|
|
731
|
+
| `MEDIA_CHUNK_UPLOAD_SUCCESS` | `"mediaChunkUploadSuccess"` | A chunk was uploaded successfully |
|
|
732
|
+
| `MEDIA_CHUNK_UPLOAD_FAILED` | `"mediaChunkUploadFailed"` | A chunk upload attempt failed |
|
|
733
|
+
| `MEDIA_CHUNK_UPLOAD_HTTP_ERROR_RESPONSE` | `"mediaChunkUploadHttpErrorResponse"` | Server returned 4xx/5xx for a chunk |
|
|
734
|
+
| `MEDIA_CHUNK_UPLOAD_STATUS` | `"mediaChunkUploadStatus"` | Chunk status update |
|
|
735
|
+
| `MEDIA_CHUNK_UPLOAD_MAXRETRY_EXPIRE` | `"mediaChunkUploadMaxRetryExpire"` | All retry attempts exhausted |
|
|
736
|
+
| `MEDIA_CHUNK_UPLOAD_RESUME` | `"mediaChunkUploadResume"` | Upload resumed from saved state |
|
|
737
|
+
|
|
738
|
+
#### Upload State Events
|
|
739
|
+
|
|
740
|
+
| Constant | Value | Dispatched When |
|
|
741
|
+
|----------|-------|-----------------|
|
|
742
|
+
| `UPLOAD_PAUSED` | `"uploadPaused"` | Upload was paused |
|
|
743
|
+
| `UPLOAD_RESUMED` | `"uploadResumed"` | Upload was resumed after pause |
|
|
744
|
+
| `UPLOAD_CANCELLED` | `"uploadCancelled"` | Upload was cancelled |
|
|
745
|
+
| `UPLOAD_STATE_CHANGED` | `"uploadStateChanged"` | Any state transition occurs |
|
|
746
|
+
|
|
747
|
+
#### Completion Events
|
|
748
|
+
|
|
749
|
+
| Constant | Value | Dispatched When |
|
|
750
|
+
|----------|-------|-----------------|
|
|
751
|
+
| `FINALIZE_UPLOAD` | `"finalizeUpload"` | Finalize request is about to be sent (internal) |
|
|
752
|
+
| `FINALIZE_UPLOAD_FAILURE` | `"finalizeUploadFailure"` | Finalize request failed |
|
|
753
|
+
| `DOWNLOAD_MEDIA_COMPLETE` | `"downloadMediaComplete"` | Upload fully completed |
|
|
754
|
+
| `DOWNLOAD_MEDIA_FAILURE` | `"downloadMediaFailure"` | Upload failed completely |
|
|
755
|
+
| `DOWNLOAD_MEDIA_RESUME` | `"downloadMediaResume"` | Upload resumed from cache |
|
|
756
|
+
|
|
757
|
+
#### Metadata Events
|
|
758
|
+
|
|
759
|
+
| Constant | Value | Dispatched When |
|
|
760
|
+
|----------|-------|-----------------|
|
|
761
|
+
| `MEDIA_METADATA_SAVE_SUCCESS` | `"mediaMetadataSaveSuccess"` | Media metadata was saved |
|
|
762
|
+
|
|
763
|
+
---
|
|
764
|
+
|
|
765
|
+
### Event Classes Reference
|
|
766
|
+
|
|
767
|
+
#### `UploadProgressEvent`
|
|
768
|
+
|
|
769
|
+
Dispatched on `MEDIA_CHUNK_UPLOAD_SUCCESS`.
|
|
770
|
+
|
|
771
|
+
```typescript
|
|
772
|
+
dispatcher.addListener(
|
|
773
|
+
HttpFileUploaderEvents.MEDIA_CHUNK_UPLOAD_SUCCESS,
|
|
774
|
+
(event: UploadProgressEvent) => {
|
|
775
|
+
console.log(event.percentage); // 0-100
|
|
776
|
+
console.log(event.uploadedChunks); // number of chunks uploaded
|
|
777
|
+
console.log(event.totalChunks); // total number of chunks
|
|
778
|
+
console.log(event.uploadedBytes); // bytes uploaded so far
|
|
779
|
+
console.log(event.totalBytes); // total file size in bytes
|
|
780
|
+
console.log(event.currentChunk); // current chunk index
|
|
781
|
+
console.log(event.speed); // bytes per second (optional)
|
|
782
|
+
console.log(event.estimatedTimeRemaining); // seconds remaining (optional)
|
|
783
|
+
console.log(event.elapsed); // seconds elapsed
|
|
784
|
+
console.log(event.responseData); // raw server response data
|
|
785
|
+
console.log(event.httpStatus); // HTTP status code
|
|
786
|
+
}
|
|
787
|
+
);
|
|
788
|
+
```
|
|
789
|
+
|
|
790
|
+
---
|
|
791
|
+
|
|
792
|
+
#### `UploadStateChangedEvent`
|
|
793
|
+
|
|
794
|
+
Dispatched on `UPLOAD_STATE_CHANGED`.
|
|
795
|
+
|
|
796
|
+
```typescript
|
|
797
|
+
dispatcher.addListener(
|
|
798
|
+
HttpFileUploaderEvents.UPLOAD_STATE_CHANGED,
|
|
799
|
+
(event: UploadStateChangedEvent) => {
|
|
800
|
+
console.log(event.oldState); // Previous UploadState
|
|
801
|
+
console.log(event.newState); // New UploadState
|
|
802
|
+
console.log(event.changedAt); // Timestamp (Date.now())
|
|
803
|
+
}
|
|
804
|
+
);
|
|
805
|
+
```
|
|
806
|
+
|
|
807
|
+
---
|
|
808
|
+
|
|
809
|
+
#### `UploadMediaCompleteEvent`
|
|
810
|
+
|
|
811
|
+
Dispatched on `DOWNLOAD_MEDIA_COMPLETE`.
|
|
812
|
+
|
|
813
|
+
```typescript
|
|
814
|
+
dispatcher.addListener(
|
|
815
|
+
HttpFileUploaderEvents.DOWNLOAD_MEDIA_COMPLETE,
|
|
816
|
+
(event: UploadMediaCompleteEvent) => {
|
|
817
|
+
console.log(event.success); // boolean
|
|
818
|
+
console.log(event.mediaId); // string | number
|
|
819
|
+
console.log(event.totalBytes); // number
|
|
820
|
+
console.log(event.totalChunks); // number
|
|
821
|
+
console.log(event.operationDuration); // seconds
|
|
822
|
+
console.log(event.averageSpeed); // bytes/second
|
|
823
|
+
console.log(event.finalizeUploadHttpResponse); // server response
|
|
824
|
+
}
|
|
825
|
+
);
|
|
826
|
+
```
|
|
827
|
+
|
|
828
|
+
---
|
|
829
|
+
|
|
830
|
+
#### `UploadCancelledEvent`
|
|
831
|
+
|
|
832
|
+
Dispatched on `UPLOAD_CANCELLED`.
|
|
833
|
+
|
|
834
|
+
```typescript
|
|
835
|
+
dispatcher.addListener(
|
|
836
|
+
HttpFileUploaderEvents.UPLOAD_CANCELLED,
|
|
837
|
+
(event: UploadCancelledEvent) => {
|
|
838
|
+
console.log(event.mediaName); // file name
|
|
839
|
+
console.log(event.totalChunks); // number
|
|
840
|
+
console.log(event.uploadedBytes); // number
|
|
841
|
+
console.log(event.percentage); // number
|
|
842
|
+
console.log(event.currentChunkIndex); // number
|
|
843
|
+
console.log(event.message); // string
|
|
844
|
+
console.log(event.cancelledAt); // timestamp
|
|
845
|
+
}
|
|
846
|
+
);
|
|
847
|
+
```
|
|
848
|
+
|
|
849
|
+
---
|
|
850
|
+
|
|
851
|
+
#### `UploadPausedEvent`
|
|
852
|
+
|
|
853
|
+
Dispatched on `UPLOAD_PAUSED`.
|
|
854
|
+
|
|
855
|
+
```typescript
|
|
856
|
+
dispatcher.addListener(
|
|
857
|
+
HttpFileUploaderEvents.UPLOAD_PAUSED,
|
|
858
|
+
(event: UploadPausedEvent) => {
|
|
859
|
+
console.log(event.mediaName); // file name
|
|
860
|
+
console.log(event.totalChunks); // number
|
|
861
|
+
console.log(event.uploadedBytes); // number
|
|
862
|
+
console.log(event.percentage); // number
|
|
863
|
+
console.log(event.pausedAt); // timestamp
|
|
864
|
+
}
|
|
865
|
+
);
|
|
866
|
+
```
|
|
867
|
+
|
|
868
|
+
---
|
|
869
|
+
|
|
870
|
+
#### `UploadResumedEvent`
|
|
871
|
+
|
|
872
|
+
Dispatched on `UPLOAD_RESUMED`.
|
|
873
|
+
|
|
874
|
+
```typescript
|
|
875
|
+
dispatcher.addListener(
|
|
876
|
+
HttpFileUploaderEvents.UPLOAD_RESUMED,
|
|
877
|
+
(event: UploadResumedEvent) => {
|
|
878
|
+
console.log(event.mediaName); // file name
|
|
879
|
+
console.log(event.totalChunks); // number
|
|
880
|
+
console.log(event.uploadedBytes); // number
|
|
881
|
+
console.log(event.percentage); // number
|
|
882
|
+
console.log(event.resumedAt); // timestamp
|
|
883
|
+
}
|
|
884
|
+
);
|
|
885
|
+
```
|
|
886
|
+
|
|
887
|
+
---
|
|
888
|
+
|
|
889
|
+
#### `ChunkUploadHttpErrorResponseEvent`
|
|
890
|
+
|
|
891
|
+
Dispatched on `MEDIA_CHUNK_UPLOAD_HTTP_ERROR_RESPONSE`.
|
|
892
|
+
|
|
893
|
+
```typescript
|
|
894
|
+
dispatcher.addListener(
|
|
895
|
+
HttpFileUploaderEvents.MEDIA_CHUNK_UPLOAD_HTTP_ERROR_RESPONSE,
|
|
896
|
+
(event: ChunkUploadHttpErrorResponseEvent) => {
|
|
897
|
+
console.log(event.statusResponse); // 4xx or 5xx status code
|
|
898
|
+
console.log(event.errorPayload); // server error body
|
|
899
|
+
console.log(event.urlEndpoint); // endpoint URL
|
|
900
|
+
console.log(event.chunkIndex); // which chunk failed
|
|
901
|
+
console.log(event.chunkInfo); // full ChunkInfo object
|
|
902
|
+
}
|
|
903
|
+
);
|
|
904
|
+
```
|
|
905
|
+
|
|
906
|
+
---
|
|
907
|
+
|
|
908
|
+
#### `InitializeUploadStartedEvent`
|
|
909
|
+
|
|
910
|
+
Dispatched on `INITIALIZE_UPLOAD_STARTED`.
|
|
911
|
+
|
|
912
|
+
```typescript
|
|
913
|
+
dispatcher.addListener(
|
|
914
|
+
HttpFileUploaderEvents.INITIALIZE_UPLOAD_STARTED,
|
|
915
|
+
(event: InitializeUploadStartedEvent) => {
|
|
916
|
+
console.log(event.fileName); // string
|
|
917
|
+
console.log(event.fileSize); // number (bytes)
|
|
918
|
+
console.log(event.fileHash); // SHA-256 hex string
|
|
919
|
+
}
|
|
920
|
+
);
|
|
921
|
+
```
|
|
922
|
+
|
|
923
|
+
---
|
|
924
|
+
|
|
925
|
+
#### `InitializeUploadSuccessEvent`
|
|
926
|
+
|
|
927
|
+
Dispatched on `INITIALIZE_UPLOAD_SUCCESS`.
|
|
928
|
+
|
|
929
|
+
```typescript
|
|
930
|
+
dispatcher.addListener(
|
|
931
|
+
HttpFileUploaderEvents.INITIALIZE_UPLOAD_SUCCESS,
|
|
932
|
+
(event: InitializeUploadSuccessEvent) => {
|
|
933
|
+
console.log(event.status); // HTTP status code
|
|
934
|
+
console.log(event.sessionId); // server-assigned session/media ID
|
|
935
|
+
console.log(event.responseData); // full server response
|
|
936
|
+
}
|
|
937
|
+
);
|
|
938
|
+
```
|
|
939
|
+
|
|
940
|
+
---
|
|
941
|
+
|
|
942
|
+
#### `InitializeUploadFailureEvent`
|
|
943
|
+
|
|
944
|
+
Dispatched on `INITIALIZE_UPLOAD_FAILURE`.
|
|
945
|
+
|
|
946
|
+
```typescript
|
|
947
|
+
dispatcher.addListener(
|
|
948
|
+
HttpFileUploaderEvents.INITIALIZE_UPLOAD_FAILURE,
|
|
949
|
+
(event: InitializeUploadFailureEvent) => {
|
|
950
|
+
console.log(event.error); // Error instance
|
|
951
|
+
console.log(event.status); // HTTP status (optional)
|
|
952
|
+
console.log(event.errorData); // raw error data (optional)
|
|
953
|
+
console.log(event.isNetworkError); // boolean (optional)
|
|
954
|
+
}
|
|
955
|
+
);
|
|
956
|
+
```
|
|
957
|
+
|
|
958
|
+
---
|
|
959
|
+
|
|
960
|
+
#### `ResumeUploadEvent`
|
|
961
|
+
|
|
962
|
+
Dispatched on `MEDIA_CHUNK_UPLOAD_RESUME`.
|
|
963
|
+
|
|
964
|
+
```typescript
|
|
965
|
+
dispatcher.addListener(
|
|
966
|
+
HttpFileUploaderEvents.MEDIA_CHUNK_UPLOAD_RESUME,
|
|
967
|
+
(event: ResumeUploadEvent) => {
|
|
968
|
+
console.log(event.mediaId); // string | number
|
|
969
|
+
console.log(event.fileName); // string
|
|
970
|
+
console.log(event.uploadedChunks); // number
|
|
971
|
+
console.log(event.lastChunkIndex); // number
|
|
972
|
+
console.log(event.lastBytePosition); // number
|
|
973
|
+
console.log(event.chunkSize); // number
|
|
974
|
+
console.log(event.fileSize); // number
|
|
975
|
+
console.log(event.message); // info message
|
|
976
|
+
}
|
|
977
|
+
);
|
|
978
|
+
```
|
|
979
|
+
|
|
980
|
+
---
|
|
981
|
+
|
|
982
|
+
# Subscribers — Upload Lifecycle Handlers
|
|
983
|
+
|
|
984
|
+
The library ships with two built-in **EventSubscribers** that handle the critical
|
|
985
|
+
HTTP communication phases of the upload lifecycle:
|
|
986
|
+
|
|
987
|
+
| Subscriber | Listens To | Responsibility |
|
|
988
|
+
|------------|-----------|----------------|
|
|
989
|
+
| `InitializeUploadSubscriber` | `HttpFileUploaderEvents.INITIALIZE_UPLOAD` | Opens an upload session with the server |
|
|
990
|
+
| `FinalizeUploadSubscriber` | `HttpFileUploaderEvents.FINALIZE_UPLOAD` | Assembles chunks and closes the upload session |
|
|
991
|
+
|
|
992
|
+
> These subscribers follow the **Symfony EventSubscriber pattern** via
|
|
993
|
+
> `@wlindabla/event_dispatcher`. They must be **registered manually**
|
|
994
|
+
> on your event dispatcher before calling `.upload()`.
|
|
995
|
+
|
|
996
|
+
---
|
|
997
|
+
|
|
998
|
+
## ⚠️ Important — You Must Register the Subscribers
|
|
999
|
+
|
|
1000
|
+
Unlike regular event listeners, subscribers are **not auto-registered**.
|
|
1001
|
+
You are responsible for adding them to your dispatcher instance.
|
|
1002
|
+
**If you forget to register them, the upload will fail silently.**
|
|
1003
|
+
|
|
1004
|
+
---
|
|
1005
|
+
|
|
1006
|
+
## Registration
|
|
1007
|
+
|
|
1008
|
+
### Browser Environment
|
|
1009
|
+
|
|
1010
|
+
```typescript
|
|
1011
|
+
import {
|
|
1012
|
+
InitializeUploadSubscriber,
|
|
1013
|
+
FinalizeUploadSubscriber
|
|
1014
|
+
} from '@wlindabla/file_uploader';
|
|
1015
|
+
|
|
1016
|
+
import { BrowserEventDispatcher } from '@wlindabla/event_dispatcher';
|
|
1017
|
+
|
|
1018
|
+
// BrowserEventDispatcher accepts: document, window, or new EventTarget()
|
|
1019
|
+
const dispatcher = new BrowserEventDispatcher(document);
|
|
1020
|
+
// or: new BrowserEventDispatcher(window)
|
|
1021
|
+
// or: new BrowserEventDispatcher(new EventTarget())
|
|
1022
|
+
|
|
1023
|
+
// ✅ Register both subscribers BEFORE calling .upload()
|
|
1024
|
+
dispatcher.addSubscriber(new InitializeUploadSubscriber(dispatcher));
|
|
1025
|
+
dispatcher.addSubscriber(new FinalizeUploadSubscriber(dispatcher));
|
|
1026
|
+
```
|
|
1027
|
+
|
|
1028
|
+
### Node.js Environment
|
|
1029
|
+
|
|
1030
|
+
```typescript
|
|
1031
|
+
import {
|
|
1032
|
+
InitializeUploadSubscriber,
|
|
1033
|
+
FinalizeUploadSubscriber
|
|
1034
|
+
} from '@wlindabla/file_uploader';
|
|
1035
|
+
|
|
1036
|
+
import { NodeEventDispatcher } from '@wlindabla/event_dispatcher';
|
|
1037
|
+
|
|
1038
|
+
const dispatcher = new NodeEventDispatcher();
|
|
1039
|
+
|
|
1040
|
+
// ✅ Register both subscribers BEFORE calling .upload()
|
|
1041
|
+
dispatcher.addSubscriber(new InitializeUploadSubscriber(dispatcher));
|
|
1042
|
+
dispatcher.addSubscriber(new FinalizeUploadSubscriber(dispatcher));
|
|
1043
|
+
```
|
|
1044
|
+
|
|
1045
|
+
> **Note:** Both subscribers receive the **same dispatcher instance** as their
|
|
1046
|
+
> constructor argument. They use it internally to dispatch their own
|
|
1047
|
+
> success/failure events back to your application.
|
|
1048
|
+
|
|
1049
|
+
---
|
|
1050
|
+
|
|
1051
|
+
## Complete Setup Example
|
|
1052
|
+
|
|
1053
|
+
### Browser
|
|
1054
|
+
|
|
1055
|
+
```typescript
|
|
1056
|
+
import {
|
|
1057
|
+
ChunkedFileUploader,
|
|
1058
|
+
InitializeUploadSubscriber,
|
|
1059
|
+
FinalizeUploadSubscriber,
|
|
1060
|
+
HttpFileUploaderEvents,
|
|
1061
|
+
InitializeUploadSuccessEvent,
|
|
1062
|
+
InitializeUploadFailureEvent,
|
|
1063
|
+
FinalizeUploadFailureEvent,
|
|
1064
|
+
UploadMediaCompleteEvent
|
|
1065
|
+
} from '@wlindabla/file_uploader';
|
|
1066
|
+
|
|
1067
|
+
import { BrowserEventDispatcher } from '@wlindabla/event_dispatcher';
|
|
1068
|
+
|
|
1069
|
+
// 1. Create the dispatcher
|
|
1070
|
+
const dispatcher = new BrowserEventDispatcher(document);
|
|
1071
|
+
|
|
1072
|
+
// 2. Register the subscribers ← MANDATORY
|
|
1073
|
+
dispatcher.addSubscriber(new InitializeUploadSubscriber(dispatcher));
|
|
1074
|
+
dispatcher.addSubscriber(new FinalizeUploadSubscriber(dispatcher));
|
|
1075
|
+
|
|
1076
|
+
// 3. (Optional) Listen to events emitted BY the subscribers
|
|
1077
|
+
dispatcher.addListener(
|
|
1078
|
+
HttpFileUploaderEvents.INITIALIZE_UPLOAD_STARTED,
|
|
1079
|
+
() => console.log('⏳ Initializing upload session...')
|
|
1080
|
+
);
|
|
1081
|
+
|
|
1082
|
+
dispatcher.addListener(
|
|
1083
|
+
HttpFileUploaderEvents.INITIALIZE_UPLOAD_SUCCESS,
|
|
1084
|
+
(event: InitializeUploadSuccessEvent) => {
|
|
1085
|
+
console.log('✅ Session created. Media ID:', event.sessionId);
|
|
1086
|
+
}
|
|
1087
|
+
);
|
|
1088
|
+
|
|
1089
|
+
dispatcher.addListener(
|
|
1090
|
+
HttpFileUploaderEvents.INITIALIZE_UPLOAD_FAILURE,
|
|
1091
|
+
(event: InitializeUploadFailureEvent) => {
|
|
1092
|
+
console.error('❌ Init failed:', event.error.message);
|
|
1093
|
+
}
|
|
1094
|
+
);
|
|
1095
|
+
|
|
1096
|
+
dispatcher.addListener(
|
|
1097
|
+
HttpFileUploaderEvents.FINALIZE_UPLOAD_FAILURE,
|
|
1098
|
+
(event: FinalizeUploadFailureEvent) => {
|
|
1099
|
+
console.error('❌ Finalize failed:', event.error.message);
|
|
1100
|
+
}
|
|
1101
|
+
);
|
|
1102
|
+
|
|
1103
|
+
dispatcher.addListener(
|
|
1104
|
+
HttpFileUploaderEvents.DOWNLOAD_MEDIA_COMPLETE,
|
|
1105
|
+
(event: UploadMediaCompleteEvent) => {
|
|
1106
|
+
console.log('🎉 Upload complete! Media ID:', event.mediaId);
|
|
1107
|
+
}
|
|
1108
|
+
);
|
|
1109
|
+
|
|
1110
|
+
// 4. Create and use the uploader
|
|
1111
|
+
const uploader = new ChunkedFileUploader(dispatcher, cache, options);
|
|
1112
|
+
|
|
1113
|
+
await uploader
|
|
1114
|
+
.withFile(file)
|
|
1115
|
+
.withEndpoints(endpoints)
|
|
1116
|
+
.upload();
|
|
1117
|
+
```
|
|
1118
|
+
|
|
1119
|
+
---
|
|
1120
|
+
|
|
1121
|
+
### Node.js
|
|
1122
|
+
|
|
1123
|
+
```typescript
|
|
1124
|
+
import {
|
|
1125
|
+
ChunkedFileUploader,
|
|
1126
|
+
InitializeUploadSubscriber,
|
|
1127
|
+
FinalizeUploadSubscriber,
|
|
1128
|
+
HttpFileUploaderEvents,
|
|
1129
|
+
InitializeUploadSuccessEvent,
|
|
1130
|
+
FinalizeUploadFailureEvent
|
|
1131
|
+
} from '@wlindabla/file_uploader';
|
|
1132
|
+
|
|
1133
|
+
import { NodeEventDispatcher } from '@wlindabla/event_dispatcher';
|
|
1134
|
+
|
|
1135
|
+
// 1. Create the dispatcher
|
|
1136
|
+
const dispatcher = new NodeEventDispatcher();
|
|
1137
|
+
|
|
1138
|
+
// 2. Register the subscribers ← MANDATORY
|
|
1139
|
+
dispatcher.addSubscriber(new InitializeUploadSubscriber(dispatcher));
|
|
1140
|
+
dispatcher.addSubscriber(new FinalizeUploadSubscriber(dispatcher));
|
|
1141
|
+
|
|
1142
|
+
// 3. (Optional) Listen to events emitted BY the subscribers
|
|
1143
|
+
dispatcher.addListener(
|
|
1144
|
+
HttpFileUploaderEvents.INITIALIZE_UPLOAD_SUCCESS,
|
|
1145
|
+
(event: InitializeUploadSuccessEvent) => {
|
|
1146
|
+
console.log('Session ID:', event.sessionId);
|
|
1147
|
+
}
|
|
1148
|
+
);
|
|
1149
|
+
|
|
1150
|
+
dispatcher.addListener(
|
|
1151
|
+
HttpFileUploaderEvents.FINALIZE_UPLOAD_FAILURE,
|
|
1152
|
+
(event: FinalizeUploadFailureEvent) => {
|
|
1153
|
+
console.error('Finalize error:', event.error.message);
|
|
1154
|
+
}
|
|
1155
|
+
);
|
|
1156
|
+
|
|
1157
|
+
// 4. Create and use the uploader
|
|
1158
|
+
const uploader = new ChunkedFileUploader(dispatcher, cache, options);
|
|
1159
|
+
|
|
1160
|
+
await uploader
|
|
1161
|
+
.withFile(file)
|
|
1162
|
+
.withEndpoints(endpoints)
|
|
1163
|
+
.upload();
|
|
1164
|
+
```
|
|
1165
|
+
|
|
1166
|
+
---
|
|
1167
|
+
|
|
1168
|
+
## InitializeUploadSubscriber
|
|
1169
|
+
|
|
1170
|
+
### What It Does Internally
|
|
1171
|
+
|
|
1172
|
+
```
|
|
1173
|
+
INITIALIZE_UPLOAD event received
|
|
1174
|
+
↓
|
|
1175
|
+
Dispatch INITIALIZE_UPLOAD_STARTED
|
|
1176
|
+
↓
|
|
1177
|
+
POST → endpoints.init
|
|
1178
|
+
↓
|
|
1179
|
+
┌───┴────────────────────┐
|
|
1180
|
+
│ response.failed? │
|
|
1181
|
+
│ YES → dispatch │
|
|
1182
|
+
│ INITIALIZE_UPLOAD_ │
|
|
1183
|
+
│ FAILURE + throw │
|
|
1184
|
+
└───┬────────────────────┘
|
|
1185
|
+
↓
|
|
1186
|
+
Validate responseData structure
|
|
1187
|
+
↓
|
|
1188
|
+
Extract sessionId from response
|
|
1189
|
+
(mediaId | mediaIdFromServer
|
|
1190
|
+
| sessionId | uploadId)
|
|
1191
|
+
↓
|
|
1192
|
+
event.setMediaId(sessionId)
|
|
1193
|
+
↓
|
|
1194
|
+
Dispatch INITIALIZE_UPLOAD_SUCCESS
|
|
1195
|
+
```
|
|
1196
|
+
|
|
1197
|
+
### Events It Dispatches
|
|
1198
|
+
|
|
1199
|
+
| Event | When |
|
|
1200
|
+
|-------|------|
|
|
1201
|
+
| `INITIALIZE_UPLOAD_STARTED` | Before the HTTP request is sent |
|
|
1202
|
+
| `INITIALIZE_UPLOAD_SUCCESS` | Server returned a valid session ID |
|
|
1203
|
+
| `INITIALIZE_UPLOAD_FAILURE` | HTTP error, network error, or invalid response |
|
|
1204
|
+
|
|
1205
|
+
### Retry Behavior
|
|
1206
|
+
|
|
1207
|
+
The subscriber uses `safeFetch` internally with:
|
|
1208
|
+
|
|
1209
|
+
```
|
|
1210
|
+
retryCount: 3
|
|
1211
|
+
retryOnStatusCode: true ← Retries on 5xx errors
|
|
1212
|
+
timeout: 45000 ← 45 seconds
|
|
1213
|
+
```
|
|
1214
|
+
|
|
1215
|
+
> 4xx errors (client errors) are **not retried** — they dispatch
|
|
1216
|
+
> `INITIALIZE_UPLOAD_FAILURE` immediately.
|
|
1217
|
+
|
|
1218
|
+
### Error Cases Handled
|
|
1219
|
+
|
|
1220
|
+
| Scenario | Behavior |
|
|
1221
|
+
|----------|----------|
|
|
1222
|
+
| HTTP 4xx from server | Dispatch `INITIALIZE_UPLOAD_FAILURE` + throw |
|
|
1223
|
+
| HTTP 5xx from server | Retry up to 3 times, then dispatch `INITIALIZE_UPLOAD_FAILURE` + throw |
|
|
1224
|
+
| Network error / Timeout | Dispatch `INITIALIZE_UPLOAD_FAILURE` + return silently |
|
|
1225
|
+
| Invalid response structure | Throw `InitializeUploadFailureException` |
|
|
1226
|
+
| Missing session ID in response | Throw `InitializeUploadFailureException` |
|
|
1227
|
+
|
|
1228
|
+
---
|
|
1229
|
+
|
|
1230
|
+
## FinalizeUploadSubscriber
|
|
1231
|
+
|
|
1232
|
+
### What It Does Internally
|
|
1233
|
+
|
|
1234
|
+
```
|
|
1235
|
+
FINALIZE_UPLOAD event received
|
|
1236
|
+
↓
|
|
1237
|
+
POST → endpoints.finalize
|
|
1238
|
+
body: { mediaId, mediaHash }
|
|
1239
|
+
↓
|
|
1240
|
+
┌───┴──────────────────┐
|
|
1241
|
+
│ HttpFetchError? │
|
|
1242
|
+
│ YES → dispatch │
|
|
1243
|
+
│ FINALIZE_UPLOAD_ │
|
|
1244
|
+
│ FAILURE + return │
|
|
1245
|
+
└───┬──────────────────┘
|
|
1246
|
+
↓
|
|
1247
|
+
event.setResponse(response)
|
|
1248
|
+
(ChunkedFileUploader reads it
|
|
1249
|
+
to build the UploadResult)
|
|
1250
|
+
```
|
|
1251
|
+
|
|
1252
|
+
### Events It Dispatches
|
|
1253
|
+
|
|
1254
|
+
| Event | When |
|
|
1255
|
+
|-------|------|
|
|
1256
|
+
| `FINALIZE_UPLOAD_FAILURE` | Network error or timeout during finalize |
|
|
1257
|
+
|
|
1258
|
+
### Error Cases Handled
|
|
1259
|
+
|
|
1260
|
+
| Scenario | Behavior |
|
|
1261
|
+
|----------|----------|
|
|
1262
|
+
| Network error / Timeout | Dispatch `FINALIZE_UPLOAD_FAILURE` + return silently |
|
|
1263
|
+
| Any other error | Re-thrown to `ChunkedFileUploader` |
|
|
1264
|
+
|
|
1265
|
+
---
|
|
1266
|
+
|
|
1267
|
+
## Subscriber Priority
|
|
1268
|
+
|
|
1269
|
+
Both subscribers are registered with **priority 100**, which means they execute
|
|
1270
|
+
**before** any listeners you add manually at the default priority (0).
|
|
1271
|
+
|
|
1272
|
+
```typescript
|
|
1273
|
+
// This returns:
|
|
1274
|
+
getSubscribedEvents() {
|
|
1275
|
+
return {
|
|
1276
|
+
[HttpFileUploaderEvents.INITIALIZE_UPLOAD]: {
|
|
1277
|
+
listener: 'onInitializeUpload',
|
|
1278
|
+
priority: 100 // ← Executes first
|
|
1279
|
+
}
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
```
|
|
1283
|
+
|
|
1284
|
+
> If you add your own listener on `INITIALIZE_UPLOAD` or `FINALIZE_UPLOAD`,
|
|
1285
|
+
> it will run **after** the subscriber has already handled the event,
|
|
1286
|
+
> unless you set a priority higher than 100.
|
|
1287
|
+
|
|
1288
|
+
---
|
|
1289
|
+
|
|
1290
|
+
## Common Mistakes
|
|
1291
|
+
|
|
1292
|
+
### ❌ Forgetting to register subscribers
|
|
1293
|
+
|
|
1294
|
+
```typescript
|
|
1295
|
+
const dispatcher = new BrowserEventDispatcher(document);
|
|
1296
|
+
|
|
1297
|
+
// ❌ Missing addSubscriber() calls!
|
|
1298
|
+
|
|
1299
|
+
const uploader = new ChunkedFileUploader(dispatcher, cache, options);
|
|
1300
|
+
await uploader.upload();
|
|
1301
|
+
// → Upload will hang or fail: no one handles INITIALIZE_UPLOAD
|
|
1302
|
+
```
|
|
1303
|
+
|
|
1304
|
+
### ❌ Registering subscribers AFTER calling upload()
|
|
1305
|
+
|
|
1306
|
+
```typescript
|
|
1307
|
+
const dispatcher = new BrowserEventDispatcher(document);
|
|
1308
|
+
|
|
1309
|
+
uploader.upload(); // ← upload starts immediately
|
|
1310
|
+
|
|
1311
|
+
// ❌ Too late! INITIALIZE_UPLOAD was already dispatched
|
|
1312
|
+
dispatcher.addSubscriber(new InitializeUploadSubscriber(dispatcher));
|
|
1313
|
+
```
|
|
1314
|
+
|
|
1315
|
+
### ✅ Correct order
|
|
1316
|
+
|
|
1317
|
+
```typescript
|
|
1318
|
+
const dispatcher = new BrowserEventDispatcher(document);
|
|
1319
|
+
|
|
1320
|
+
// ✅ 1. Register subscribers first
|
|
1321
|
+
dispatcher.addSubscriber(new InitializeUploadSubscriber(dispatcher));
|
|
1322
|
+
dispatcher.addSubscriber(new FinalizeUploadSubscriber(dispatcher));
|
|
1323
|
+
|
|
1324
|
+
// ✅ 2. Add your own listeners
|
|
1325
|
+
dispatcher.addListener(HttpFileUploaderEvents.DOWNLOAD_MEDIA_COMPLETE, handler);
|
|
1326
|
+
|
|
1327
|
+
// ✅ 3. Then upload
|
|
1328
|
+
await uploader.withFile(file).withEndpoints(endpoints).upload();
|
|
1329
|
+
```
|
|
1330
|
+
|
|
1331
|
+
## Subscribers
|
|
1332
|
+
|
|
1333
|
+
The library uses two internal `EventSubscriberInterface` implementations.
|
|
1334
|
+
You do **not** need to instantiate them manually — `ChunkedFileUploader` does it
|
|
1335
|
+
automatically. However, you can **extend** them if needed.
|
|
1336
|
+
|
|
1337
|
+
### InitializeUploadSubscriber
|
|
1338
|
+
|
|
1339
|
+
Handles the `INITIALIZE_UPLOAD` event internally.
|
|
1340
|
+
|
|
1341
|
+
**What it does:**
|
|
1342
|
+
1. Sends a POST request to `endpoints.init` using `safeFetch`
|
|
1343
|
+
2. Validates the server response
|
|
1344
|
+
3. Extracts the `mediaId`/`sessionId`/`uploadId` from the response
|
|
1345
|
+
4. Dispatches `INITIALIZE_UPLOAD_STARTED`, `INITIALIZE_UPLOAD_SUCCESS`,
|
|
1346
|
+
or `INITIALIZE_UPLOAD_FAILURE`
|
|
1347
|
+
|
|
1348
|
+
**Expected server response** (any of these keys is accepted):
|
|
1349
|
+
|
|
1350
|
+
```json
|
|
1351
|
+
{
|
|
1352
|
+
"mediaId": "abc-123",
|
|
1353
|
+
"message": "Upload session created"
|
|
1354
|
+
}
|
|
1355
|
+
```
|
|
1356
|
+
|
|
1357
|
+
or
|
|
1358
|
+
|
|
1359
|
+
```json
|
|
1360
|
+
{ "sessionId": "abc-123" }
|
|
1361
|
+
```
|
|
1362
|
+
|
|
1363
|
+
or
|
|
1364
|
+
|
|
1365
|
+
```json
|
|
1366
|
+
{ "uploadId": "abc-123" }
|
|
1367
|
+
```
|
|
1368
|
+
|
|
1369
|
+
or
|
|
1370
|
+
|
|
1371
|
+
```json
|
|
1372
|
+
{ "mediaIdFromServer": "abc-123" }
|
|
1373
|
+
```
|
|
1374
|
+
|
|
1375
|
+
---
|
|
1376
|
+
|
|
1377
|
+
### FinalizeUploadSubscriber
|
|
1378
|
+
|
|
1379
|
+
Handles the `FINALIZE_UPLOAD` event internally.
|
|
1380
|
+
|
|
1381
|
+
**What it does:**
|
|
1382
|
+
1. Sends a POST request to `endpoints.finalize` using `safeFetch`
|
|
1383
|
+
2. Passes `{ mediaId, mediaHash }` in the request body
|
|
1384
|
+
3. Dispatches `FINALIZE_UPLOAD_FAILURE` on error
|
|
1385
|
+
|
|
1386
|
+
**Request body sent to finalize endpoint:**
|
|
1387
|
+
|
|
1388
|
+
```json
|
|
1389
|
+
{
|
|
1390
|
+
"mediaId": "abc-123",
|
|
1391
|
+
"mediaHash": "sha256hexstring..."
|
|
1392
|
+
}
|
|
1393
|
+
```
|
|
1394
|
+
|
|
1395
|
+
---
|
|
1396
|
+
|
|
1397
|
+
## Types Reference
|
|
1398
|
+
|
|
1399
|
+
### `UploadState` (enum)
|
|
1400
|
+
|
|
1401
|
+
```typescript
|
|
1402
|
+
enum UploadState {
|
|
1403
|
+
IDLE = 'idle',
|
|
1404
|
+
INITIALIZING = 'initializing',
|
|
1405
|
+
UPLOADING = 'uploading',
|
|
1406
|
+
PAUSED = 'paused',
|
|
1407
|
+
CANCELLED = 'cancelled',
|
|
1408
|
+
FINALIZING = 'finalizing',
|
|
1409
|
+
COMPLETED = 'completed',
|
|
1410
|
+
FAILED = 'failed'
|
|
1411
|
+
}
|
|
1412
|
+
```
|
|
1413
|
+
|
|
1414
|
+
---
|
|
1415
|
+
|
|
1416
|
+
### `ChunkInfo`
|
|
1417
|
+
|
|
1418
|
+
```typescript
|
|
1419
|
+
interface ChunkInfo {
|
|
1420
|
+
index: number; // Zero-based chunk index
|
|
1421
|
+
start: number; // Start byte position in file
|
|
1422
|
+
end: number; // End byte position in file
|
|
1423
|
+
size: number; // Chunk size in bytes
|
|
1424
|
+
attempt: number; // Current attempt number
|
|
1425
|
+
status: UploadStatus; // 'pending' | 'uploading' | 'success' | 'error'
|
|
1426
|
+
}
|
|
1427
|
+
```
|
|
1428
|
+
|
|
1429
|
+
---
|
|
1430
|
+
|
|
1431
|
+
### `UploadProgress`
|
|
1432
|
+
|
|
1433
|
+
```typescript
|
|
1434
|
+
interface UploadProgress {
|
|
1435
|
+
uploadedChunks: number;
|
|
1436
|
+
totalChunks: number;
|
|
1437
|
+
uploadedBytes: number;
|
|
1438
|
+
totalBytes: number;
|
|
1439
|
+
percentage: number; // 0-100
|
|
1440
|
+
currentChunk: number;
|
|
1441
|
+
speed?: number; // bytes per second
|
|
1442
|
+
estimatedTimeRemaining?: number | null; // seconds
|
|
1443
|
+
elapsed: number; // seconds elapsed
|
|
1444
|
+
}
|
|
1445
|
+
```
|
|
1446
|
+
|
|
1447
|
+
---
|
|
1448
|
+
|
|
1449
|
+
### `ResumeData`
|
|
1450
|
+
|
|
1451
|
+
```typescript
|
|
1452
|
+
interface ResumeData {
|
|
1453
|
+
fileId: string;
|
|
1454
|
+
fileName: string;
|
|
1455
|
+
fileSize: number;
|
|
1456
|
+
uploadedChunks: number;
|
|
1457
|
+
lastChunkIndex: number;
|
|
1458
|
+
lastBytePosition: number;
|
|
1459
|
+
chunkSize: number;
|
|
1460
|
+
concurrency: number;
|
|
1461
|
+
}
|
|
1462
|
+
```
|
|
1463
|
+
|
|
1464
|
+
---
|
|
1465
|
+
|
|
1466
|
+
### `UploadResult`
|
|
1467
|
+
|
|
1468
|
+
```typescript
|
|
1469
|
+
interface UploadResult {
|
|
1470
|
+
success: boolean;
|
|
1471
|
+
fileId?: string;
|
|
1472
|
+
totalChunks: number;
|
|
1473
|
+
totalBytes: number;
|
|
1474
|
+
duration: number; // seconds
|
|
1475
|
+
averageSpeed: number; // bytes per second
|
|
1476
|
+
finalizeUploadResponse?: FetchResponseInterface | null;
|
|
1477
|
+
statusResponse?: number;
|
|
1478
|
+
}
|
|
1479
|
+
```
|
|
1480
|
+
|
|
1481
|
+
---
|
|
1482
|
+
|
|
1483
|
+
### `ChunkError`
|
|
1484
|
+
|
|
1485
|
+
```typescript
|
|
1486
|
+
interface ChunkError {
|
|
1487
|
+
chunk: ChunkInfo;
|
|
1488
|
+
error: Error;
|
|
1489
|
+
attempt: number;
|
|
1490
|
+
willRetry: boolean;
|
|
1491
|
+
}
|
|
1492
|
+
```
|
|
1493
|
+
|
|
1494
|
+
---
|
|
1495
|
+
|
|
1496
|
+
### `InitializeUploadResponse`
|
|
1497
|
+
|
|
1498
|
+
Your server's init endpoint must return an object with **at least one** of these keys:
|
|
1499
|
+
|
|
1500
|
+
```typescript
|
|
1501
|
+
interface InitializeUploadResponse {
|
|
1502
|
+
mediaId?: string | number;
|
|
1503
|
+
mediaIdFromServer?: string | number;
|
|
1504
|
+
sessionId?: string | number;
|
|
1505
|
+
uploadId?: string | number;
|
|
1506
|
+
message?: string;
|
|
1507
|
+
[key: string]: any; // Additional fields are allowed
|
|
1508
|
+
}
|
|
1509
|
+
```
|
|
1510
|
+
|
|
1511
|
+
---
|
|
1512
|
+
|
|
1513
|
+
## Exceptions
|
|
1514
|
+
|
|
1515
|
+
### `InitializeUploadFailureException`
|
|
1516
|
+
|
|
1517
|
+
Thrown when the server init request fails or returns an invalid response.
|
|
1518
|
+
|
|
1519
|
+
```typescript
|
|
1520
|
+
try {
|
|
1521
|
+
await uploader.upload();
|
|
1522
|
+
} catch (error) {
|
|
1523
|
+
if (error instanceof InitializeUploadFailureException) {
|
|
1524
|
+
console.log(error.message); // Error description
|
|
1525
|
+
console.log(error.responseData); // Raw server response
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
```
|
|
1529
|
+
|
|
1530
|
+
---
|
|
1531
|
+
|
|
1532
|
+
### `FileUploadChunkError`
|
|
1533
|
+
|
|
1534
|
+
Thrown when a chunk fails after all retry attempts.
|
|
1535
|
+
|
|
1536
|
+
```typescript
|
|
1537
|
+
try {
|
|
1538
|
+
await uploader.upload();
|
|
1539
|
+
} catch (error) {
|
|
1540
|
+
if (error instanceof FileUploadChunkError) {
|
|
1541
|
+
console.log(error.message); // "Failed to upload chunk X after N attempts"
|
|
1542
|
+
console.log(error.chunkIndex); // Which chunk failed (0-based)
|
|
1543
|
+
console.log(error.attemptNumber); // How many attempts were made
|
|
1544
|
+
console.log(error.willRetry); // false (max retries exhausted)
|
|
1545
|
+
console.log(error.underlyingError); // Original error
|
|
1546
|
+
console.log(error.chunk); // Full ChunkInfo object
|
|
1547
|
+
console.log(error.toJSON()); // JSON representation
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
```
|
|
1551
|
+
|
|
1552
|
+
---
|
|
1553
|
+
|
|
1554
|
+
### `ChunkUploadHttpErrorException`
|
|
1555
|
+
|
|
1556
|
+
Thrown internally when the server returns a 4xx/5xx response for a chunk.
|
|
1557
|
+
You can catch it via the `MEDIA_CHUNK_UPLOAD_HTTP_ERROR_RESPONSE` event.
|
|
1558
|
+
|
|
1559
|
+
```typescript
|
|
1560
|
+
dispatcher.addListener(
|
|
1561
|
+
HttpFileUploaderEvents.MEDIA_CHUNK_UPLOAD_HTTP_ERROR_RESPONSE,
|
|
1562
|
+
(event: ChunkUploadHttpErrorResponseEvent) => {
|
|
1563
|
+
console.log(event.statusResponse); // e.g. 413 (Payload Too Large)
|
|
1564
|
+
console.log(event.errorPayload); // server error body
|
|
1565
|
+
}
|
|
1566
|
+
);
|
|
1567
|
+
```
|
|
1568
|
+
|
|
1569
|
+
---
|
|
1570
|
+
|
|
1571
|
+
### `UploadCancelledException`
|
|
1572
|
+
|
|
1573
|
+
Thrown when an upload is cancelled via `.cancel()`.
|
|
1574
|
+
|
|
1575
|
+
```typescript
|
|
1576
|
+
try {
|
|
1577
|
+
await uploader.upload();
|
|
1578
|
+
} catch (error) {
|
|
1579
|
+
if (error instanceof UploadCancelledException) {
|
|
1580
|
+
console.log(error.chunkIndex); // Which chunk was being uploaded
|
|
1581
|
+
console.log(error.totalChunks); // Total number of chunks
|
|
1582
|
+
console.log(error.uploadedBytes); // How many bytes were uploaded
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
```
|
|
1586
|
+
|
|
1587
|
+
---
|
|
1588
|
+
|
|
1589
|
+
## Advanced Usage
|
|
1590
|
+
|
|
1591
|
+
### Concurrency Control
|
|
1592
|
+
|
|
1593
|
+
Control how many chunks are uploaded in parallel.
|
|
1594
|
+
|
|
1595
|
+
```typescript
|
|
1596
|
+
// Conservative: 1 chunk at a time (slow connections, mobile)
|
|
1597
|
+
new ChunkedFileUploader(dispatcher, cache, { concurrency: 1 });
|
|
1598
|
+
|
|
1599
|
+
// Balanced: 3 chunks in parallel (default, recommended)
|
|
1600
|
+
new ChunkedFileUploader(dispatcher, cache, { concurrency: 3 });
|
|
1601
|
+
|
|
1602
|
+
// Aggressive: 5 chunks in parallel (fast connections)
|
|
1603
|
+
new ChunkedFileUploader(dispatcher, cache, { concurrency: 5 });
|
|
1604
|
+
```
|
|
1605
|
+
|
|
1606
|
+
**Concurrency visual:**
|
|
1607
|
+
|
|
1608
|
+
```
|
|
1609
|
+
concurrency = 3
|
|
1610
|
+
|
|
1611
|
+
Time → → → → → →
|
|
1612
|
+
Chunk 0: [████████]
|
|
1613
|
+
Chunk 1: [████████] ← 3 chunks in parallel
|
|
1614
|
+
Chunk 2: [████████]
|
|
1615
|
+
↓ batch complete
|
|
1616
|
+
Chunk 3: [████████]
|
|
1617
|
+
Chunk 4: [████████] ← next batch
|
|
1618
|
+
Chunk 5: [████████]
|
|
1619
|
+
```
|
|
1620
|
+
|
|
1621
|
+
> **Note:** Values higher than 5 are not recommended as they may trigger
|
|
1622
|
+
> server-side rate limiting or browser connection limits.
|
|
1623
|
+
|
|
1624
|
+
---
|
|
1625
|
+
|
|
1626
|
+
### Pause and Resume
|
|
1627
|
+
|
|
1628
|
+
```typescript
|
|
1629
|
+
const uploader = new ChunkedFileUploader(dispatcher, cache, options);
|
|
1630
|
+
|
|
1631
|
+
// Start upload
|
|
1632
|
+
uploader.withFile(file).withEndpoints(endpoints);
|
|
1633
|
+
uploader.upload(); // Don't await here if you want to control it
|
|
1634
|
+
|
|
1635
|
+
// Pause after 2 seconds
|
|
1636
|
+
setTimeout(() => {
|
|
1637
|
+
uploader.pause();
|
|
1638
|
+
console.log('Upload paused. State:', uploader.getState()); // 'paused'
|
|
1639
|
+
}, 2000);
|
|
1640
|
+
|
|
1641
|
+
// Resume after 5 seconds
|
|
1642
|
+
setTimeout(() => {
|
|
1643
|
+
uploader.resume();
|
|
1644
|
+
console.log('Upload resumed. State:', uploader.getState()); // 'uploading'
|
|
1645
|
+
}, 5000);
|
|
1646
|
+
```
|
|
1647
|
+
|
|
1648
|
+
---
|
|
1649
|
+
|
|
1650
|
+
### Cancel Upload
|
|
1651
|
+
|
|
1652
|
+
```typescript
|
|
1653
|
+
const uploader = new ChunkedFileUploader(dispatcher, cache, options);
|
|
1654
|
+
|
|
1655
|
+
// Listen for cancellation
|
|
1656
|
+
dispatcher.addListener(
|
|
1657
|
+
HttpFileUploaderEvents.UPLOAD_CANCELLED,
|
|
1658
|
+
(event: UploadCancelledEvent) => {
|
|
1659
|
+
console.log(`Upload of "${event.mediaName}" was cancelled`);
|
|
1660
|
+
console.log(`${event.percentage}% was completed`);
|
|
1661
|
+
}
|
|
1662
|
+
);
|
|
1663
|
+
|
|
1664
|
+
// Start upload (fire and forget)
|
|
1665
|
+
uploader.withFile(file).withEndpoints(endpoints).upload().catch(() => {});
|
|
1666
|
+
|
|
1667
|
+
// Cancel after 3 seconds
|
|
1668
|
+
setTimeout(() => {
|
|
1669
|
+
uploader.cancel();
|
|
1670
|
+
}, 3000);
|
|
1671
|
+
```
|
|
1672
|
+
|
|
1673
|
+
---
|
|
1674
|
+
|
|
1675
|
+
### Resumable Upload
|
|
1676
|
+
|
|
1677
|
+
Enable upload resumption after page refresh or network failure.
|
|
1678
|
+
|
|
1679
|
+
```typescript
|
|
1680
|
+
// Step 1: Enable auto-save during upload
|
|
1681
|
+
const uploader = new ChunkedFileUploader(
|
|
1682
|
+
dispatcher,
|
|
1683
|
+
new IndexedDBCache(),
|
|
1684
|
+
{
|
|
1685
|
+
autoSave: true, // ← Save progress after each chunk
|
|
1686
|
+
concurrency: 3
|
|
1687
|
+
}
|
|
1688
|
+
);
|
|
1689
|
+
|
|
1690
|
+
// Step 2: On page load, check for saved progress
|
|
1691
|
+
async function startOrResumeUpload(file: File) {
|
|
1692
|
+
const resumeData = await uploader.loadResumeData(file.name);
|
|
1693
|
+
|
|
1694
|
+
uploader.withFile(file).withEndpoints(endpoints);
|
|
1695
|
+
|
|
1696
|
+
if (resumeData) {
|
|
1697
|
+
const confirmed = confirm(
|
|
1698
|
+
`Resume upload from ${resumeData.uploadedChunks} chunks? ` +
|
|
1699
|
+
`(${Math.round(resumeData.lastBytePosition / 1024 / 1024)}MB already uploaded)`
|
|
1700
|
+
);
|
|
1701
|
+
|
|
1702
|
+
if (confirmed) {
|
|
1703
|
+
// Resume from where we left off
|
|
1704
|
+
await uploader.resumeUpload(resumeData);
|
|
1705
|
+
return;
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
// Start fresh
|
|
1710
|
+
await uploader.upload();
|
|
1711
|
+
}
|
|
1712
|
+
```
|
|
1713
|
+
|
|
1714
|
+
---
|
|
1715
|
+
|
|
1716
|
+
### Custom Logger
|
|
1717
|
+
|
|
1718
|
+
```typescript
|
|
1719
|
+
import { LoggerInterface } from '@wlindabla/file_uploader';
|
|
1720
|
+
|
|
1721
|
+
// Example: Send logs to a monitoring service
|
|
1722
|
+
class MonitoringLogger implements LoggerInterface {
|
|
1723
|
+
info(message: string, ...args: any[]) {
|
|
1724
|
+
myMonitoringService.track('info', message, args);
|
|
1725
|
+
}
|
|
1726
|
+
warn(message: string, ...args: any[]) {
|
|
1727
|
+
myMonitoringService.track('warn', message, args);
|
|
1728
|
+
}
|
|
1729
|
+
error(message: string, ...args: any[]) {
|
|
1730
|
+
myMonitoringService.track('error', message, args);
|
|
1731
|
+
alertOnCallTeam(message);
|
|
1732
|
+
}
|
|
1733
|
+
debug(message: string, ...args: any[]) {
|
|
1734
|
+
if (process.env.DEBUG) {
|
|
1735
|
+
console.debug('[FileUploader]', message, ...args);
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
const uploader = new ChunkedFileUploader(
|
|
1741
|
+
dispatcher,
|
|
1742
|
+
cache,
|
|
1743
|
+
options,
|
|
1744
|
+
new MonitoringLogger()
|
|
1745
|
+
);
|
|
1746
|
+
```
|
|
1747
|
+
|
|
1748
|
+
---
|
|
1749
|
+
|
|
1750
|
+
### Custom Cache Implementation
|
|
1751
|
+
|
|
1752
|
+
Implement `UploadResumeCacheInterface` with any storage backend you prefer.
|
|
1753
|
+
See [UploadResumeCacheInterface](#uploadresumecacheinterface) for ready-to-use examples.
|
|
1754
|
+
|
|
1755
|
+
---
|
|
1756
|
+
|
|
1757
|
+
## Environment-Specific Examples
|
|
1758
|
+
|
|
1759
|
+
### Browser Example
|
|
1760
|
+
|
|
1761
|
+
Complete browser example with progress bar and UI controls.
|
|
1762
|
+
|
|
1763
|
+
```typescript
|
|
1764
|
+
import {
|
|
1765
|
+
ChunkedFileUploader,
|
|
1766
|
+
HttpFileUploaderEvents,
|
|
1767
|
+
UploadProgressEvent,
|
|
1768
|
+
UploadStateChangedEvent,
|
|
1769
|
+
UploadMediaCompleteEvent,
|
|
1770
|
+
UploadCancelledEvent,
|
|
1771
|
+
ConsoleLogger
|
|
1772
|
+
} from '@wlindabla/file_uploader';
|
|
1773
|
+
|
|
1774
|
+
import { BrowserEventDispatcher } from '@wlindabla/event_dispatcher';
|
|
1775
|
+
|
|
1776
|
+
// Cache
|
|
1777
|
+
class LocalStorageCache {
|
|
1778
|
+
async getItem(key: string) {
|
|
1779
|
+
const v = localStorage.getItem(key);
|
|
1780
|
+
return v ? JSON.parse(v) : null;
|
|
1781
|
+
}
|
|
1782
|
+
async setItem(key: string, value: any) {
|
|
1783
|
+
localStorage.setItem(key, JSON.stringify(value));
|
|
1784
|
+
}
|
|
1785
|
+
async removeItem(key: string) {
|
|
1786
|
+
localStorage.removeItem(key);
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
// Setup
|
|
1791
|
+
const dispatcher = new BrowserEventDispatcher();
|
|
1792
|
+
|
|
1793
|
+
// Progress bar
|
|
1794
|
+
dispatcher.addListener(
|
|
1795
|
+
HttpFileUploaderEvents.MEDIA_CHUNK_UPLOAD_SUCCESS,
|
|
1796
|
+
(event: UploadProgressEvent) => {
|
|
1797
|
+
const bar = document.getElementById('progress-bar') as HTMLElement;
|
|
1798
|
+
const label = document.getElementById('progress-label') as HTMLElement;
|
|
1799
|
+
const speed = document.getElementById('speed') as HTMLElement;
|
|
1800
|
+
|
|
1801
|
+
bar.style.width = `${event.percentage}%`;
|
|
1802
|
+
label.textContent = `${event.percentage}%`;
|
|
1803
|
+
|
|
1804
|
+
if (event.speed) {
|
|
1805
|
+
const mbps = (event.speed / 1024 / 1024).toFixed(2);
|
|
1806
|
+
speed.textContent = `${mbps} MB/s`;
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
if (event.estimatedTimeRemaining) {
|
|
1810
|
+
const eta = document.getElementById('eta') as HTMLElement;
|
|
1811
|
+
eta.textContent = `${Math.ceil(event.estimatedTimeRemaining)}s remaining`;
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
);
|
|
1815
|
+
|
|
1816
|
+
// State changes
|
|
1817
|
+
dispatcher.addListener(
|
|
1818
|
+
HttpFileUploaderEvents.UPLOAD_STATE_CHANGED,
|
|
1819
|
+
(event: UploadStateChangedEvent) => {
|
|
1820
|
+
const status = document.getElementById('status') as HTMLElement;
|
|
1821
|
+
status.textContent = event.newState.toUpperCase();
|
|
1822
|
+
}
|
|
1823
|
+
);
|
|
1824
|
+
|
|
1825
|
+
// Completion
|
|
1826
|
+
dispatcher.addListener(
|
|
1827
|
+
HttpFileUploaderEvents.DOWNLOAD_MEDIA_COMPLETE,
|
|
1828
|
+
(event: UploadMediaCompleteEvent) => {
|
|
1829
|
+
const duration = event.operationDuration.toFixed(1);
|
|
1830
|
+
alert(`✅ Upload complete in ${duration}s! Media ID: ${event.mediaId}`);
|
|
1831
|
+
}
|
|
1832
|
+
);
|
|
1833
|
+
|
|
1834
|
+
// Error
|
|
1835
|
+
dispatcher.addListener(
|
|
1836
|
+
HttpFileUploaderEvents.DOWNLOAD_MEDIA_FAILURE,
|
|
1837
|
+
(error: Error) => {
|
|
1838
|
+
alert(`❌ Upload failed: ${error.message}`);
|
|
1839
|
+
}
|
|
1840
|
+
);
|
|
1841
|
+
|
|
1842
|
+
// Uploader instance
|
|
1843
|
+
const uploader = new ChunkedFileUploader(
|
|
1844
|
+
dispatcher,
|
|
1845
|
+
new LocalStorageCache(),
|
|
1846
|
+
{
|
|
1847
|
+
concurrency: 3,
|
|
1848
|
+
maxRetries: 3,
|
|
1849
|
+
autoSave: true,
|
|
1850
|
+
timeout: 60000
|
|
1851
|
+
},
|
|
1852
|
+
new ConsoleLogger()
|
|
1853
|
+
);
|
|
1854
|
+
|
|
1855
|
+
// Wire up DOM
|
|
1856
|
+
document.getElementById('upload-btn')?.addEventListener('click', async () => {
|
|
1857
|
+
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
|
1858
|
+
const file = fileInput.files?.[0];
|
|
1859
|
+
|
|
1860
|
+
if (!file) return alert('Please select a file');
|
|
1861
|
+
|
|
1862
|
+
uploader
|
|
1863
|
+
.withFile(file)
|
|
1864
|
+
.withEndpoints({
|
|
1865
|
+
init: '/api/upload/init',
|
|
1866
|
+
upload: '/api/upload/chunk',
|
|
1867
|
+
finalize: '/api/upload/finalize'
|
|
1868
|
+
});
|
|
1869
|
+
|
|
1870
|
+
try {
|
|
1871
|
+
await uploader.upload();
|
|
1872
|
+
} catch (error) {
|
|
1873
|
+
console.error('Upload failed:', error);
|
|
1874
|
+
}
|
|
1875
|
+
});
|
|
1876
|
+
|
|
1877
|
+
document.getElementById('pause-btn')?.addEventListener('click', () => uploader.pause());
|
|
1878
|
+
document.getElementById('resume-btn')?.addEventListener('click', () => uploader.resume());
|
|
1879
|
+
document.getElementById('cancel-btn')?.addEventListener('click', () => uploader.cancel());
|
|
1880
|
+
```
|
|
1881
|
+
|
|
1882
|
+
---
|
|
1883
|
+
|
|
1884
|
+
### Node.js Example
|
|
1885
|
+
|
|
1886
|
+
Server-to-server file transfer.
|
|
1887
|
+
|
|
1888
|
+
```typescript
|
|
1889
|
+
import {
|
|
1890
|
+
ChunkedFileUploader,
|
|
1891
|
+
HttpFileUploaderEvents,
|
|
1892
|
+
UploadProgressEvent,
|
|
1893
|
+
UploadMediaCompleteEvent,
|
|
1894
|
+
ConsoleLogger
|
|
1895
|
+
} from '@wlindabla/file_uploader';
|
|
1896
|
+
|
|
1897
|
+
import { NodeEventDispatcher } from '@wlindabla/event_dispatcher';
|
|
1898
|
+
import fs from 'fs';
|
|
1899
|
+
|
|
1900
|
+
// Node.js File-like object
|
|
1901
|
+
function createFileFromPath(filePath: string): File {
|
|
1902
|
+
const buffer = fs.readFileSync(filePath);
|
|
1903
|
+
const blob = new Blob([buffer]);
|
|
1904
|
+
return new File([blob], path.basename(filePath));
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
// Setup
|
|
1908
|
+
const dispatcher = new NodeEventDispatcher();
|
|
1909
|
+
|
|
1910
|
+
// Track progress
|
|
1911
|
+
dispatcher.addListener(
|
|
1912
|
+
HttpFileUploaderEvents.MEDIA_CHUNK_UPLOAD_SUCCESS,
|
|
1913
|
+
(event: UploadProgressEvent) => {
|
|
1914
|
+
process.stdout.write(`\r⬆️ ${event.percentage}% uploaded...`);
|
|
1915
|
+
}
|
|
1916
|
+
);
|
|
1917
|
+
|
|
1918
|
+
dispatcher.addListener(
|
|
1919
|
+
HttpFileUploaderEvents.DOWNLOAD_MEDIA_COMPLETE,
|
|
1920
|
+
(event: UploadMediaCompleteEvent) => {
|
|
1921
|
+
console.log(`\n✅ Done! Media ID: ${event.mediaId}`);
|
|
1922
|
+
console.log(` Duration: ${event.operationDuration.toFixed(1)}s`);
|
|
1923
|
+
console.log(` Average speed: ${(event.averageSpeed / 1024 / 1024).toFixed(2)} MB/s`);
|
|
1924
|
+
}
|
|
1925
|
+
);
|
|
1926
|
+
|
|
1927
|
+
// File system cache for Node.js
|
|
1928
|
+
class FileSystemCache {
|
|
1929
|
+
async getItem(key: string) {
|
|
1930
|
+
try {
|
|
1931
|
+
const data = fs.readFileSync(`/tmp/${key}.json`, 'utf-8');
|
|
1932
|
+
return JSON.parse(data);
|
|
1933
|
+
} catch { return null; }
|
|
1934
|
+
}
|
|
1935
|
+
async setItem(key: string, value: any) {
|
|
1936
|
+
fs.writeFileSync(`/tmp/${key}.json`, JSON.stringify(value));
|
|
1937
|
+
}
|
|
1938
|
+
async removeItem(key: string) {
|
|
1939
|
+
try { fs.unlinkSync(`/tmp/${key}.json`); } catch {}
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
// Upload
|
|
1944
|
+
async function uploadFile(filePath: string) {
|
|
1945
|
+
const file = createFileFromPath(filePath);
|
|
1946
|
+
|
|
1947
|
+
const uploader = new ChunkedFileUploader(
|
|
1948
|
+
dispatcher,
|
|
1949
|
+
new FileSystemCache(),
|
|
1950
|
+
{
|
|
1951
|
+
concurrency: 5,
|
|
1952
|
+
maxRetries: 3,
|
|
1953
|
+
autoSave: true,
|
|
1954
|
+
headers: {
|
|
1955
|
+
'Authorization': `Bearer ${process.env.API_TOKEN}`,
|
|
1956
|
+
'X-Server-ID': process.env.SERVER_ID ?? 'unknown'
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
);
|
|
1960
|
+
|
|
1961
|
+
await uploader
|
|
1962
|
+
.withFile(file)
|
|
1963
|
+
.withEndpoints({
|
|
1964
|
+
init: 'https://api.example.com/upload/init',
|
|
1965
|
+
upload: 'https://api.example.com/upload/chunk',
|
|
1966
|
+
finalize: 'https://api.example.com/upload/finalize'
|
|
1967
|
+
})
|
|
1968
|
+
.upload();
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
uploadFile('/path/to/large-video.mp4').catch(console.error);
|
|
1972
|
+
```
|
|
1973
|
+
|
|
1974
|
+
---
|
|
1975
|
+
|
|
1976
|
+
## Server-Side Integration Guide
|
|
1977
|
+
|
|
1978
|
+
### Init Endpoint
|
|
1979
|
+
|
|
1980
|
+
**Request received:**
|
|
1981
|
+
|
|
1982
|
+
```json
|
|
1983
|
+
{
|
|
1984
|
+
"fileName": "video.mp4",
|
|
1985
|
+
"fileSize": 1073741824,
|
|
1986
|
+
"fileType": "video/mp4",
|
|
1987
|
+
"fileHash": "a3f5b9c...",
|
|
1988
|
+
"metadata": { "userId": "42", "albumId": "5" }
|
|
1989
|
+
}
|
|
1990
|
+
```
|
|
1991
|
+
|
|
1992
|
+
**Expected response (HTTP 200/201):**
|
|
1993
|
+
|
|
1994
|
+
```json
|
|
1995
|
+
{
|
|
1996
|
+
"mediaId": "upload-session-abc-123",
|
|
1997
|
+
"message": "Upload session created"
|
|
1998
|
+
}
|
|
1999
|
+
```
|
|
2000
|
+
|
|
2001
|
+
---
|
|
2002
|
+
|
|
2003
|
+
### Chunk Upload Endpoint
|
|
2004
|
+
|
|
2005
|
+
**FormData fields received per chunk:**
|
|
2006
|
+
|
|
2007
|
+
| Field | Type | Description |
|
|
2008
|
+
|-------|------|-------------|
|
|
2009
|
+
| `chunk` | `Blob` | The binary chunk data |
|
|
2010
|
+
| `chunkIndex` | `string` | Zero-based chunk index |
|
|
2011
|
+
| `chunkSize` | `string` | Chunk size in bytes |
|
|
2012
|
+
| `totalChunks` | `string` | Total number of chunks |
|
|
2013
|
+
| `fileName` | `string` | Original file name |
|
|
2014
|
+
| `fileSize` | `string` | Total file size in bytes |
|
|
2015
|
+
| `fileHash` | `string` | SHA-256 hash of the file |
|
|
2016
|
+
| `mediaId` | `string` | Session ID from init |
|
|
2017
|
+
|
|
2018
|
+
**Expected response (HTTP 200):**
|
|
2019
|
+
|
|
2020
|
+
```json
|
|
2021
|
+
{
|
|
2022
|
+
"chunkIndex": 5,
|
|
2023
|
+
"received": true,
|
|
2024
|
+
"message": "Chunk received"
|
|
2025
|
+
}
|
|
2026
|
+
```
|
|
2027
|
+
|
|
2028
|
+
---
|
|
2029
|
+
|
|
2030
|
+
### Finalize Endpoint
|
|
2031
|
+
|
|
2032
|
+
**Request received:**
|
|
2033
|
+
|
|
2034
|
+
```json
|
|
2035
|
+
{
|
|
2036
|
+
"mediaId": "upload-session-abc-123",
|
|
2037
|
+
"mediaHash": "a3f5b9c..."
|
|
2038
|
+
}
|
|
2039
|
+
```
|
|
2040
|
+
|
|
2041
|
+
**Expected response (HTTP 200):**
|
|
2042
|
+
|
|
2043
|
+
```json
|
|
2044
|
+
{
|
|
2045
|
+
"success": true,
|
|
2046
|
+
"fileUrl": "https://cdn.example.com/videos/video.mp4",
|
|
2047
|
+
"mediaId": "upload-session-abc-123"
|
|
2048
|
+
}
|
|
2049
|
+
```
|
|
2050
|
+
|
|
2051
|
+
---
|
|
2052
|
+
|
|
2053
|
+
## Error Handling
|
|
2054
|
+
|
|
2055
|
+
### Complete Error Handling Example
|
|
2056
|
+
|
|
2057
|
+
```typescript
|
|
2058
|
+
import {
|
|
2059
|
+
ChunkedFileUploader,
|
|
2060
|
+
HttpFileUploaderEvents,
|
|
2061
|
+
InitializeUploadFailureEvent,
|
|
2062
|
+
FileUploadChunkError,
|
|
2063
|
+
InitializeUploadFailureException,
|
|
2064
|
+
UploadCancelledException
|
|
2065
|
+
} from '@wlindabla/file_uploader';
|
|
2066
|
+
|
|
2067
|
+
// Listen to granular error events
|
|
2068
|
+
dispatcher.addListener(
|
|
2069
|
+
HttpFileUploaderEvents.INITIALIZE_UPLOAD_FAILURE,
|
|
2070
|
+
(event: InitializeUploadFailureEvent) => {
|
|
2071
|
+
console.error('Init failed:', event.error.message);
|
|
2072
|
+
if (event.status === 401) {
|
|
2073
|
+
// Redirect to login
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
);
|
|
2077
|
+
|
|
2078
|
+
dispatcher.addListener(
|
|
2079
|
+
HttpFileUploaderEvents.MEDIA_CHUNK_UPLOAD_HTTP_ERROR_RESPONSE,
|
|
2080
|
+
(event: ChunkUploadHttpErrorResponseEvent) => {
|
|
2081
|
+
console.warn(
|
|
2082
|
+
`Chunk ${event.chunkIndex} got HTTP ${event.statusResponse}`,
|
|
2083
|
+
event.errorPayload
|
|
2084
|
+
);
|
|
2085
|
+
}
|
|
2086
|
+
);
|
|
2087
|
+
|
|
2088
|
+
dispatcher.addListener(
|
|
2089
|
+
HttpFileUploaderEvents.MEDIA_CHUNK_UPLOAD_MAXRETRY_EXPIRE,
|
|
2090
|
+
(error: FileUploadChunkError) => {
|
|
2091
|
+
console.error(
|
|
2092
|
+
`Chunk ${error.chunkIndex} permanently failed after ${error.attemptNumber} attempts`
|
|
2093
|
+
);
|
|
2094
|
+
}
|
|
2095
|
+
);
|
|
2096
|
+
|
|
2097
|
+
dispatcher.addListener(
|
|
2098
|
+
HttpFileUploaderEvents.DOWNLOAD_MEDIA_FAILURE,
|
|
2099
|
+
(error: Error) => {
|
|
2100
|
+
console.error('Upload completely failed:', error.message);
|
|
2101
|
+
}
|
|
2102
|
+
);
|
|
2103
|
+
|
|
2104
|
+
// Handle thrown exceptions
|
|
2105
|
+
try {
|
|
2106
|
+
await uploader.upload();
|
|
2107
|
+
} catch (error) {
|
|
2108
|
+
if (error instanceof UploadCancelledException) {
|
|
2109
|
+
console.log('User cancelled the upload');
|
|
2110
|
+
|
|
2111
|
+
} else if (error instanceof InitializeUploadFailureException) {
|
|
2112
|
+
console.error('Could not start upload session:', error.message);
|
|
2113
|
+
|
|
2114
|
+
} else if (error instanceof FileUploadChunkError) {
|
|
2115
|
+
console.error(`Chunk ${error.chunkIndex} failed:`, error.message);
|
|
2116
|
+
console.error('Underlying cause:', error.underlyingError.message);
|
|
2117
|
+
|
|
2118
|
+
} else {
|
|
2119
|
+
console.error('Unexpected error:', error);
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
```
|
|
2123
|
+
|
|
2124
|
+
---
|
|
2125
|
+
|
|
2126
|
+
## Contributing
|
|
2127
|
+
|
|
2128
|
+
Contributions are welcome! Please follow these steps:
|
|
2129
|
+
|
|
2130
|
+
1. Fork the repository: [github.com/Agbokoudjo/file_uploader](https://github.com/Agbokoudjo/file_uploader)
|
|
2131
|
+
2. Create your feature branch: `git checkout -b feature/my-feature`
|
|
2132
|
+
3. Commit your changes: `git commit -m 'feat: add my feature'`
|
|
2133
|
+
4. Push to the branch: `git push origin feature/my-feature`
|
|
2134
|
+
5. Open a Pull Request
|
|
2135
|
+
|
|
2136
|
+
### Development Setup
|
|
2137
|
+
|
|
2138
|
+
```bash
|
|
2139
|
+
git clone https://github.com/Agbokoudjo/file_uploader.git
|
|
2140
|
+
cd file_uploader
|
|
2141
|
+
yarn install
|
|
2142
|
+
yarn build
|
|
2143
|
+
yarn test
|
|
2144
|
+
```
|
|
2145
|
+
|
|
2146
|
+
### Running Tests
|
|
2147
|
+
|
|
2148
|
+
```bash
|
|
2149
|
+
yarn test # Run all tests
|
|
2150
|
+
yarn test:watch # Watch mode
|
|
2151
|
+
yarn test:coverage # With coverage report
|
|
2152
|
+
yarn test:ui # Vitest UI
|
|
2153
|
+
```
|
|
2154
|
+
|
|
2155
|
+
---
|
|
2156
|
+
|
|
2157
|
+
## License
|
|
2158
|
+
|
|
2159
|
+
MIT © [AGBOKOUDJO Franck — INTERNATIONALES WEB APPS & SERVICES](https://github.com/Agbokoudjo)
|
|
2160
|
+
|
|
2161
|
+
---
|
|
2162
|
+
|
|
2163
|
+
## Support
|
|
2164
|
+
|
|
2165
|
+
- 📧 Email: [internationaleswebservices@gmail.com](mailto:internationaleswebservices@gmail.com)
|
|
2166
|
+
- 🐛 Issues: [GitHub Issues](https://github.com/Agbokoudjo/file_uploader/issues)
|
|
2167
|
+
- 💼 LinkedIn: [INTERNATIONALES WEB APPS & SERVICES](https://www.linkedin.com/in/internationales-web-apps-services-120520193/)
|
|
2168
|
+
- 📞 Phone: +229 0167 25 18 86
|
|
2169
|
+
|
|
2170
|
+
---
|
|
2171
|
+
|
|
2172
|
+
**Built with ❤️ by AGBOKOUDJO Franck — INTERNATIONALES WEB APPS & SERVICES**
|