@wlindabla/file_uploader 1.0.0 → 2.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/README.md +51 -20
- package/dist/cjs/cache/index.d.ts +198 -0
- package/dist/cjs/cache/index.js +318 -0
- package/dist/cjs/cache/index.js.map +1 -0
- package/dist/cjs/core/index.d.ts +267 -0
- package/dist/cjs/core/index.js +753 -0
- package/dist/cjs/core/index.js.map +1 -0
- package/dist/cjs/events/chunk/index.d.ts +27 -0
- package/dist/cjs/events/chunk/index.js +70 -0
- package/dist/cjs/events/chunk/index.js.map +1 -0
- package/dist/cjs/events/complete/index.d.ts +63 -0
- package/dist/cjs/events/complete/index.js +152 -0
- package/dist/cjs/events/complete/index.js.map +1 -0
- package/dist/cjs/events/index.d.ts +94 -0
- package/dist/cjs/events/index.js +85 -0
- package/dist/cjs/events/index.js.map +1 -0
- package/dist/cjs/events/initialize/index.d.ts +45 -0
- package/dist/cjs/events/initialize/index.js +105 -0
- package/dist/cjs/events/initialize/index.js.map +1 -0
- package/dist/cjs/events/state/index.d.ts +67 -0
- package/dist/cjs/events/state/index.js +145 -0
- package/dist/cjs/events/state/index.js.map +1 -0
- package/dist/cjs/exceptions/index.d.ts +84 -0
- package/dist/{exceptions → cjs/exceptions}/index.js +38 -18
- package/dist/cjs/exceptions/index.js.map +1 -0
- package/dist/cjs/index.d.ts +13 -0
- package/dist/cjs/index.js +33 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/subscribers/index.d.ts +33 -0
- package/dist/cjs/subscribers/index.js +187 -0
- package/dist/cjs/subscribers/index.js.map +1 -0
- package/dist/cjs/types/index.d.ts +110 -0
- package/dist/cjs/types/index.js +53 -0
- package/dist/cjs/types/index.js.map +1 -0
- package/dist/cjs/utils/index.d.ts +72 -0
- package/dist/cjs/utils/index.js +208 -0
- package/dist/cjs/utils/index.js.map +1 -0
- package/dist/esm/cache/index.d.mts +198 -0
- package/dist/esm/cache/index.js +4 -0
- package/dist/{index.js.map → esm/cache/index.js.map} +1 -1
- package/dist/{subscribers/index.js → esm/chunk-332NNKOW.js} +36 -34
- package/dist/esm/chunk-332NNKOW.js.map +1 -0
- package/dist/{events/state/index.js → esm/chunk-6225YMFE.js} +38 -20
- package/dist/esm/chunk-6225YMFE.js.map +1 -0
- package/dist/{core/index.js → esm/chunk-6DIKDA6J.js} +226 -227
- package/dist/esm/chunk-6DIKDA6J.js.map +1 -0
- package/dist/esm/chunk-7QVYU63E.js +6 -0
- package/dist/esm/chunk-7QVYU63E.js.map +1 -0
- package/dist/{events/initialize/index.js → esm/chunk-DN5B6PRW.js} +25 -19
- package/dist/esm/chunk-DN5B6PRW.js.map +1 -0
- package/dist/{events/index.js → esm/chunk-JDL3U4OX.js} +7 -37
- package/dist/esm/chunk-JDL3U4OX.js.map +1 -0
- package/dist/{events/chunk/index.js → esm/chunk-LD2DWZRJ.js} +14 -11
- package/dist/esm/chunk-LD2DWZRJ.js.map +1 -0
- package/dist/{events/complete/index.js → esm/chunk-LTYMA4U4.js} +25 -19
- package/dist/{events/complete/index.js.map → esm/chunk-LTYMA4U4.js.map} +1 -1
- package/dist/{utils/index.js → esm/chunk-MFYC4PBP.js} +15 -22
- package/dist/esm/chunk-MFYC4PBP.js.map +1 -0
- package/dist/esm/chunk-NXYS73I4.js +125 -0
- package/dist/esm/chunk-NXYS73I4.js.map +1 -0
- package/dist/{cache/index.js → esm/chunk-PFALORWQ.js} +10 -11
- package/dist/esm/chunk-PFALORWQ.js.map +1 -0
- package/dist/{types/index.js → esm/chunk-X757PBC5.js} +5 -7
- package/dist/esm/chunk-X757PBC5.js.map +1 -0
- package/dist/esm/core/index.d.mts +267 -0
- package/dist/esm/core/index.js +12 -0
- package/dist/esm/core/index.js.map +1 -0
- package/dist/esm/events/chunk/index.d.mts +27 -0
- package/dist/esm/events/chunk/index.js +4 -0
- package/dist/esm/events/chunk/index.js.map +1 -0
- package/dist/esm/events/complete/index.d.mts +63 -0
- package/dist/esm/events/complete/index.js +4 -0
- package/dist/esm/events/complete/index.js.map +1 -0
- package/dist/esm/events/index.d.mts +94 -0
- package/dist/esm/events/index.js +8 -0
- package/dist/esm/events/index.js.map +1 -0
- package/dist/esm/events/initialize/index.d.mts +45 -0
- package/dist/esm/events/initialize/index.js +4 -0
- package/dist/esm/events/initialize/index.js.map +1 -0
- package/dist/esm/events/state/index.d.mts +67 -0
- package/dist/esm/events/state/index.js +4 -0
- package/dist/esm/events/state/index.js.map +1 -0
- package/dist/esm/exceptions/index.d.mts +84 -0
- package/dist/esm/exceptions/index.js +4 -0
- package/dist/esm/exceptions/index.js.map +1 -0
- package/dist/esm/index.d.mts +13 -0
- package/dist/esm/index.js +14 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/subscribers/index.d.mts +33 -0
- package/dist/esm/subscribers/index.js +10 -0
- package/dist/esm/subscribers/index.js.map +1 -0
- package/dist/esm/types/index.d.mts +110 -0
- package/dist/esm/types/index.js +4 -0
- package/dist/esm/types/index.js.map +1 -0
- package/dist/esm/utils/index.d.mts +72 -0
- package/dist/esm/utils/index.js +5 -0
- package/dist/esm/utils/index.js.map +1 -0
- package/package.json +165 -14
- package/dist/cache/index.js.map +0 -1
- package/dist/core/index.js.map +0 -1
- package/dist/events/chunk/index.js.map +0 -1
- package/dist/events/index.js.map +0 -1
- package/dist/events/initialize/index.js.map +0 -1
- package/dist/events/state/index.js.map +0 -1
- package/dist/exceptions/index.js.map +0 -1
- package/dist/index.js +0 -49
- package/dist/subscribers/index.js.map +0 -1
- package/dist/types/index.js.map +0 -1
- package/dist/utils/index.js.map +0 -1
|
@@ -1,180 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
import { ChunkUploadHttpErrorException, FileUploadChunkError } from './chunk-NXYS73I4.js';
|
|
2
|
+
import { FileUtils, createChunkFormData } from './chunk-MFYC4PBP.js';
|
|
3
|
+
import { HttpFileUploaderEvents } from './chunk-JDL3U4OX.js';
|
|
4
|
+
import { UploadChunkStartedEvent, ChunkUploadHttpErrorResponseEvent } from './chunk-LD2DWZRJ.js';
|
|
5
|
+
import { ResumeUploadEvent, FinalizeUploadEvent, UploadMediaCompleteEvent } from './chunk-LTYMA4U4.js';
|
|
6
|
+
import { InitializingUploadEvent } from './chunk-DN5B6PRW.js';
|
|
7
|
+
import { UploadCancelledEvent, UploadProgressEvent, UploadStateChangedEvent, UploadPausedEvent, UploadResumedEvent } from './chunk-6225YMFE.js';
|
|
8
|
+
import { __name } from './chunk-7QVYU63E.js';
|
|
9
|
+
import { BrowserEventDispatcher } from '@wlindabla/event_dispatcher';
|
|
10
|
+
import { HttpFetchError, safeFetch } from '@wlindabla/http_client';
|
|
11
|
+
import pLimit from 'p-limit';
|
|
2
12
|
|
|
3
|
-
var
|
|
4
|
-
|
|
5
|
-
var types = require('../types');
|
|
6
|
-
var utils = require('../utils');
|
|
7
|
-
var events = require('../events');
|
|
8
|
-
var exceptions = require('../exceptions');
|
|
9
|
-
var pLimit = require('p-limit');
|
|
10
|
-
|
|
11
|
-
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
12
|
-
|
|
13
|
-
var pLimit__default = /*#__PURE__*/_interopDefault(pLimit);
|
|
14
|
-
|
|
15
|
-
var __defProp = Object.defineProperty;
|
|
16
|
-
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
17
|
-
/**
|
|
18
|
-
* ChunkedFileUploader
|
|
19
|
-
*
|
|
20
|
-
* A production-ready, event-driven chunked file upload engine for Browser and Node.js.
|
|
21
|
-
*
|
|
22
|
-
* Designed and developed by **AGBOKOUDJO Franck** at
|
|
23
|
-
* **INTERNATIONALES WEB APPS & SERVICES**, this class provides a robust,
|
|
24
|
-
* framework-agnostic solution for uploading large files to a remote server
|
|
25
|
-
* by splitting them into smaller chunks and sending them in parallel with
|
|
26
|
-
* configurable concurrency control.
|
|
27
|
-
*
|
|
28
|
-
* ---
|
|
29
|
-
*
|
|
30
|
-
* ### How It Works
|
|
31
|
-
*
|
|
32
|
-
* The upload process follows a strict three-phase lifecycle:
|
|
33
|
-
*
|
|
34
|
-
* 1. **Initialization** — A session is opened with the server via the `init` endpoint.
|
|
35
|
-
* The file is identified by its name, size, type, and a SHA-256 hash of its
|
|
36
|
-
* first megabyte. The server returns a unique `mediaId` that identifies the session.
|
|
37
|
-
*
|
|
38
|
-
* 2. **Chunk Upload** — The file is sliced into fixed-size chunks and uploaded
|
|
39
|
-
* concurrently using `p-limit`. Each chunk carries its index, the total number
|
|
40
|
-
* of chunks, and the session `mediaId`. Failed chunks are retried automatically
|
|
41
|
-
* with exponential backoff up to `maxRetries` attempts.
|
|
42
|
-
*
|
|
43
|
-
* 3. **Finalization** — Once all chunks are successfully uploaded, the `finalize`
|
|
44
|
-
* endpoint is called to instruct the server to assemble the chunks into the
|
|
45
|
-
* final file.
|
|
46
|
-
*
|
|
47
|
-
* ---
|
|
48
|
-
*
|
|
49
|
-
* ### Event-Driven Architecture
|
|
50
|
-
*
|
|
51
|
-
* This class follows the **Symfony EventDispatcher pattern** via
|
|
52
|
-
* `@wlindabla/event_dispatcher`. Every meaningful moment in the upload
|
|
53
|
-
* lifecycle emits a typed event that your application can listen to:
|
|
54
|
-
*
|
|
55
|
-
* ```
|
|
56
|
-
* IDLE → INITIALIZING → UPLOADING → FINALIZING → COMPLETED
|
|
57
|
-
* ↓ ↓
|
|
58
|
-
* FAILED PAUSED ↔ UPLOADING
|
|
59
|
-
* ↓
|
|
60
|
-
* CANCELLED
|
|
61
|
-
* ```
|
|
62
|
-
*
|
|
63
|
-
* All event name constants are centralized in {@link HttpFileUploaderEvents}.
|
|
64
|
-
*
|
|
65
|
-
* ---
|
|
66
|
-
*
|
|
67
|
-
* ### Subscriber Registration (Required)
|
|
68
|
-
*
|
|
69
|
-
* Before calling `.upload()`, you **must** register the two built-in subscribers
|
|
70
|
-
* on your dispatcher. They handle the HTTP communication for the init and finalize phases:
|
|
71
|
-
*
|
|
72
|
-
* ```typescript
|
|
73
|
-
* // Browser
|
|
74
|
-
* const dispatcher = new BrowserEventDispatcher(document);
|
|
75
|
-
* dispatcher.addSubscriber(new InitializeUploadSubscriber(dispatcher));
|
|
76
|
-
* dispatcher.addSubscriber(new FinalizeUploadSubscriber(dispatcher));
|
|
77
|
-
*
|
|
78
|
-
* // Node.js
|
|
79
|
-
* const dispatcher = new NodeEventDispatcher();
|
|
80
|
-
* dispatcher.addSubscriber(new InitializeUploadSubscriber(dispatcher));
|
|
81
|
-
* dispatcher.addSubscriber(new FinalizeUploadSubscriber(dispatcher));
|
|
82
|
-
* ```
|
|
83
|
-
*
|
|
84
|
-
* ---
|
|
85
|
-
*
|
|
86
|
-
* ### Fluent Builder API
|
|
87
|
-
*
|
|
88
|
-
* The class exposes a fluent API to configure the upload before starting:
|
|
89
|
-
*
|
|
90
|
-
* ```typescript
|
|
91
|
-
* const uploader = new ChunkedFileUploader(dispatcher, cache, options);
|
|
92
|
-
*
|
|
93
|
-
* await uploader
|
|
94
|
-
* .withFile(file)
|
|
95
|
-
* .withEndpoints({
|
|
96
|
-
* init: 'https://api.example.com/upload/init',
|
|
97
|
-
* upload: 'https://api.example.com/upload/chunk',
|
|
98
|
-
* finalize: 'https://api.example.com/upload/finalize'
|
|
99
|
-
* })
|
|
100
|
-
* .upload();
|
|
101
|
-
* ```
|
|
102
|
-
*
|
|
103
|
-
* ---
|
|
104
|
-
*
|
|
105
|
-
* ### Resumable Uploads
|
|
106
|
-
*
|
|
107
|
-
* When `autoSave: true` is set in options, the upload progress is persisted
|
|
108
|
-
* after each successful chunk via the {@link UploadResumeCacheInterface}.
|
|
109
|
-
* A failed or interrupted upload can be resumed later:
|
|
110
|
-
*
|
|
111
|
-
* ```typescript
|
|
112
|
-
* const resumeData = await uploader.loadResumeData(file.name);
|
|
113
|
-
*
|
|
114
|
-
* if (resumeData) {
|
|
115
|
-
* await uploader
|
|
116
|
-
* .withFile(file)
|
|
117
|
-
* .withEndpoints(endpoints)
|
|
118
|
-
* .resumeUpload(resumeData);
|
|
119
|
-
* }
|
|
120
|
-
* ```
|
|
121
|
-
*
|
|
122
|
-
* ---
|
|
123
|
-
*
|
|
124
|
-
* ### Concurrency
|
|
125
|
-
*
|
|
126
|
-
* Multiple chunks can be uploaded simultaneously. The `concurrency` option
|
|
127
|
-
* controls how many parallel uploads are active at any given time.
|
|
128
|
-
* The default value is `3`, which provides a good balance between speed
|
|
129
|
-
* and server/network load:
|
|
130
|
-
*
|
|
131
|
-
* ```typescript
|
|
132
|
-
* // Upload 5 chunks in parallel
|
|
133
|
-
* new ChunkedFileUploader(dispatcher, cache, { concurrency: 5 });
|
|
134
|
-
* ```
|
|
135
|
-
*
|
|
136
|
-
* ---
|
|
137
|
-
*
|
|
138
|
-
* ### Pause, Resume and Cancel
|
|
139
|
-
*
|
|
140
|
-
* The upload can be paused, resumed, or cancelled at any time:
|
|
141
|
-
*
|
|
142
|
-
* ```typescript
|
|
143
|
-
* uploader.pause(); // Pauses after the current chunk finishes
|
|
144
|
-
* uploader.resume(); // Resumes from where it was paused
|
|
145
|
-
* uploader.cancel(); // Aborts immediately via AbortController
|
|
146
|
-
* ```
|
|
147
|
-
*
|
|
148
|
-
* ---
|
|
149
|
-
*
|
|
150
|
-
* ### Cache
|
|
151
|
-
*
|
|
152
|
-
* The library does **not** provide a built-in cache implementation to stay
|
|
153
|
-
* lightweight and environment-agnostic. You must implement
|
|
154
|
-
* {@link UploadResumeCacheInterface} with your preferred storage backend
|
|
155
|
-
* (localStorage, IndexedDB, Redis, filesystem, etc.).
|
|
156
|
-
*
|
|
157
|
-
* ---
|
|
158
|
-
*
|
|
159
|
-
* @author AGBOKOUDJO Franck <internationaleswebservices@gmail.com>
|
|
160
|
-
* @company INTERNATIONALES WEB APPS & SERVICES
|
|
161
|
-
* @phone +229 0167 25 18 86
|
|
162
|
-
* @linkedin https://www.linkedin.com/in/internationales-web-apps-services-120520193/
|
|
163
|
-
* @github https://github.com/Agbokoudjo/file_uploader
|
|
164
|
-
*
|
|
165
|
-
* @version 1.0.0
|
|
166
|
-
* @since 1.0.0
|
|
167
|
-
* @license MIT
|
|
168
|
-
*
|
|
169
|
-
* @see {@link HttpFileUploaderEvents} All event name constants
|
|
170
|
-
* @see {@link UploadResumeCacheInterface} Cache interface to implement
|
|
171
|
-
* @see {@link InitializeUploadSubscriber} Handles the init HTTP phase
|
|
172
|
-
* @see {@link FinalizeUploadSubscriber} Handles the finalize HTTP phase
|
|
173
|
-
* @see {@link UploadOptions} Full configuration reference
|
|
174
|
-
* @see {@link https://github.com/Agbokoudjo/file_uploader | GitHub Repository}
|
|
175
|
-
*/
|
|
176
|
-
class ChunkedFileUploader {
|
|
177
|
-
constructor(_uploadEventDispatcher = new event_dispatcher.BrowserEventDispatcher(), uploadResumeData, options) {
|
|
13
|
+
var ChunkedFileUploader = class {
|
|
14
|
+
constructor(_uploadEventDispatcher = new BrowserEventDispatcher(), uploadResumeData, options) {
|
|
178
15
|
this._uploadEventDispatcher = _uploadEventDispatcher;
|
|
179
16
|
this.uploadResumeData = uploadResumeData;
|
|
180
17
|
this.options = options;
|
|
@@ -184,12 +21,15 @@ class ChunkedFileUploader {
|
|
|
184
21
|
this.startTime = 0;
|
|
185
22
|
this.uploadedBytes = 0;
|
|
186
23
|
this.abortController = new AbortController();
|
|
187
|
-
this.state =
|
|
24
|
+
this.state = "idle" /* IDLE */;
|
|
188
25
|
this.totalChunks = 0;
|
|
189
26
|
this.percentage = 0;
|
|
190
27
|
this.uploadedChunks = 0;
|
|
191
28
|
this.lastUploadedChunkIndex = -1;
|
|
192
29
|
}
|
|
30
|
+
_uploadEventDispatcher;
|
|
31
|
+
uploadResumeData;
|
|
32
|
+
options;
|
|
193
33
|
static {
|
|
194
34
|
__name(this, "ChunkedFileUploader");
|
|
195
35
|
}
|
|
@@ -223,7 +63,7 @@ class ChunkedFileUploader {
|
|
|
223
63
|
* ```
|
|
224
64
|
*/
|
|
225
65
|
async upload() {
|
|
226
|
-
this.setState(
|
|
66
|
+
this.setState("initializing" /* INITIALIZING */);
|
|
227
67
|
const {
|
|
228
68
|
maxRetries = 3,
|
|
229
69
|
config,
|
|
@@ -233,11 +73,11 @@ class ChunkedFileUploader {
|
|
|
233
73
|
concurrency = 3
|
|
234
74
|
} = this.options;
|
|
235
75
|
const file = this.file;
|
|
236
|
-
const fileHash = await
|
|
76
|
+
const fileHash = await FileUtils.generateFileHash(file);
|
|
237
77
|
let fileId;
|
|
238
78
|
try {
|
|
239
|
-
const initializationEvent = this._uploadEventDispatcher.
|
|
240
|
-
new
|
|
79
|
+
const initializationEvent = await this._uploadEventDispatcher.dispatchAsync(
|
|
80
|
+
new InitializingUploadEvent(
|
|
241
81
|
{
|
|
242
82
|
fileHash,
|
|
243
83
|
fileName: this.file.name,
|
|
@@ -248,19 +88,19 @@ class ChunkedFileUploader {
|
|
|
248
88
|
headers: headerInitialzingUpload
|
|
249
89
|
}
|
|
250
90
|
),
|
|
251
|
-
|
|
91
|
+
HttpFileUploaderEvents.INITIALIZE_UPLOAD
|
|
252
92
|
);
|
|
253
93
|
fileId = initializationEvent.mediaId;
|
|
254
94
|
} catch (error) {
|
|
255
|
-
this.setState(
|
|
95
|
+
this.setState("failed" /* FAILED */);
|
|
256
96
|
throw error;
|
|
257
97
|
}
|
|
258
|
-
this.setState(
|
|
98
|
+
this.setState("uploading" /* UPLOADING */);
|
|
259
99
|
this.startTime = Date.now();
|
|
260
100
|
this.uploadedBytes = 0;
|
|
261
101
|
this.uploadedChunks = 0;
|
|
262
102
|
this.lastUploadedChunkIndex = -1;
|
|
263
|
-
const chunkSize = this.options.chunkSize ||
|
|
103
|
+
const chunkSize = this.options.chunkSize || FileUtils.calculateChunkSize(file.size, speedMbps, config);
|
|
264
104
|
this.totalChunks = Math.ceil(file.size / chunkSize);
|
|
265
105
|
try {
|
|
266
106
|
await this.uploadChunksWithConcurrency(
|
|
@@ -325,7 +165,7 @@ class ChunkedFileUploader {
|
|
|
325
165
|
*/
|
|
326
166
|
async uploadChunksWithConcurrency(file, chunkSize, fileId, fileHash, maxRetries, concurrency, startIndex = 0) {
|
|
327
167
|
try {
|
|
328
|
-
const limit =
|
|
168
|
+
const limit = pLimit(concurrency);
|
|
329
169
|
const uploadPromises = [];
|
|
330
170
|
for (let chunkIndex = startIndex; chunkIndex < this.totalChunks; chunkIndex++) {
|
|
331
171
|
const limitedUpload = limit(
|
|
@@ -362,7 +202,7 @@ class ChunkedFileUploader {
|
|
|
362
202
|
try {
|
|
363
203
|
if (this.abortController.signal.aborted) {
|
|
364
204
|
this._uploadEventDispatcher.dispatch(
|
|
365
|
-
new
|
|
205
|
+
new UploadCancelledEvent(
|
|
366
206
|
file.name,
|
|
367
207
|
this.totalChunks,
|
|
368
208
|
this.uploadedBytes,
|
|
@@ -371,7 +211,7 @@ class ChunkedFileUploader {
|
|
|
371
211
|
"Upload cancelled by user",
|
|
372
212
|
Date.now()
|
|
373
213
|
),
|
|
374
|
-
|
|
214
|
+
HttpFileUploaderEvents.UPLOAD_CANCELLED
|
|
375
215
|
);
|
|
376
216
|
return;
|
|
377
217
|
}
|
|
@@ -390,8 +230,8 @@ class ChunkedFileUploader {
|
|
|
390
230
|
status: "pending"
|
|
391
231
|
};
|
|
392
232
|
this._uploadEventDispatcher.dispatch(
|
|
393
|
-
new
|
|
394
|
-
|
|
233
|
+
new UploadChunkStartedEvent(chunkInfo),
|
|
234
|
+
HttpFileUploaderEvents.MEDIA_CHUNK_UPLOAD_STARTED
|
|
395
235
|
);
|
|
396
236
|
await this.uploadChunkWithRetry(
|
|
397
237
|
chunk,
|
|
@@ -427,15 +267,15 @@ class ChunkedFileUploader {
|
|
|
427
267
|
);
|
|
428
268
|
return;
|
|
429
269
|
} catch (error) {
|
|
430
|
-
if (error instanceof
|
|
270
|
+
if (error instanceof ChunkUploadHttpErrorException) {
|
|
431
271
|
this._uploadEventDispatcher.dispatch(
|
|
432
|
-
new
|
|
272
|
+
new ChunkUploadHttpErrorResponseEvent(
|
|
433
273
|
error.errorPayload,
|
|
434
274
|
error.statusResponse,
|
|
435
275
|
this.endPointOptions.upload,
|
|
436
276
|
chunkInfo
|
|
437
277
|
),
|
|
438
|
-
|
|
278
|
+
HttpFileUploaderEvents.MEDIA_CHUNK_UPLOAD_HTTP_ERROR_RESPONSE
|
|
439
279
|
);
|
|
440
280
|
}
|
|
441
281
|
lastError = error;
|
|
@@ -446,8 +286,8 @@ class ChunkedFileUploader {
|
|
|
446
286
|
attempt: attempt + 1,
|
|
447
287
|
willRetry: attempt < maxRetries - 1
|
|
448
288
|
};
|
|
449
|
-
if (error instanceof
|
|
450
|
-
this._uploadEventDispatcher.dispatch(chunkError,
|
|
289
|
+
if (error instanceof HttpFetchError) {
|
|
290
|
+
this._uploadEventDispatcher.dispatch(chunkError, HttpFileUploaderEvents.MEDIA_CHUNK_UPLOAD_FAILED);
|
|
451
291
|
}
|
|
452
292
|
if (attempt < maxRetries - 1) {
|
|
453
293
|
const delay = Math.pow(2, attempt) * 1e3;
|
|
@@ -457,7 +297,7 @@ class ChunkedFileUploader {
|
|
|
457
297
|
}
|
|
458
298
|
}
|
|
459
299
|
if (!success) {
|
|
460
|
-
const fileUploadChunkError = new
|
|
300
|
+
const fileUploadChunkError = new FileUploadChunkError(
|
|
461
301
|
`Failed to upload chunk ${chunkInfo.index} after ${maxRetries} attempts`,
|
|
462
302
|
{
|
|
463
303
|
chunk: chunkInfo,
|
|
@@ -468,14 +308,14 @@ class ChunkedFileUploader {
|
|
|
468
308
|
);
|
|
469
309
|
this._uploadEventDispatcher.dispatch(
|
|
470
310
|
fileUploadChunkError,
|
|
471
|
-
|
|
311
|
+
HttpFileUploaderEvents.MEDIA_CHUNK_UPLOAD_MAXRETRY_EXPIRE
|
|
472
312
|
);
|
|
473
313
|
throw fileUploadChunkError;
|
|
474
314
|
}
|
|
475
315
|
}
|
|
476
316
|
async uploadChunk(chunk, chunkInfo, mediaIdFromServer, fileHash, totalChunks) {
|
|
477
317
|
const media = this.file;
|
|
478
|
-
const chunkFormData =
|
|
318
|
+
const chunkFormData = createChunkFormData(
|
|
479
319
|
chunk,
|
|
480
320
|
{
|
|
481
321
|
chunkIndex: chunkInfo.index,
|
|
@@ -487,7 +327,7 @@ class ChunkedFileUploader {
|
|
|
487
327
|
}
|
|
488
328
|
);
|
|
489
329
|
try {
|
|
490
|
-
const response_of_server = await
|
|
330
|
+
const response_of_server = await safeFetch({
|
|
491
331
|
url: this.endPointOptions.upload,
|
|
492
332
|
headers: this.options.headers,
|
|
493
333
|
data: chunkFormData,
|
|
@@ -500,7 +340,7 @@ class ChunkedFileUploader {
|
|
|
500
340
|
});
|
|
501
341
|
const statusResponse = response_of_server.status;
|
|
502
342
|
if (response_of_server.failed) {
|
|
503
|
-
throw new
|
|
343
|
+
throw new ChunkUploadHttpErrorException(response_of_server.data, statusResponse);
|
|
504
344
|
}
|
|
505
345
|
return response_of_server;
|
|
506
346
|
} catch (error) {
|
|
@@ -535,14 +375,14 @@ class ChunkedFileUploader {
|
|
|
535
375
|
// secondes écoulées
|
|
536
376
|
};
|
|
537
377
|
this._uploadEventDispatcher.dispatch(
|
|
538
|
-
new
|
|
378
|
+
new UploadProgressEvent(
|
|
539
379
|
progress,
|
|
540
380
|
httpResponse.data,
|
|
541
381
|
httpResponse.status
|
|
542
382
|
)
|
|
543
383
|
);
|
|
544
384
|
console.info(
|
|
545
|
-
`Progress: ${this.percentage}% | Speed: ${
|
|
385
|
+
`Progress: ${this.percentage}% | Speed: ${FileUtils.formatBytes(speed)}/s | ETA: ${estimatedTimeRemaining ? FileUtils.formatDuration(estimatedTimeRemaining) : "Calculating..."}`
|
|
546
386
|
);
|
|
547
387
|
}
|
|
548
388
|
sleep(ms) {
|
|
@@ -576,51 +416,51 @@ class ChunkedFileUploader {
|
|
|
576
416
|
const oldState = this.state;
|
|
577
417
|
this.state = newState;
|
|
578
418
|
this._uploadEventDispatcher.dispatch(
|
|
579
|
-
new
|
|
419
|
+
new UploadStateChangedEvent(
|
|
580
420
|
oldState,
|
|
581
421
|
newState,
|
|
582
422
|
Date.now()
|
|
583
423
|
),
|
|
584
|
-
|
|
424
|
+
HttpFileUploaderEvents.UPLOAD_STATE_CHANGED
|
|
585
425
|
);
|
|
586
426
|
}
|
|
587
427
|
getState() {
|
|
588
428
|
return this.state;
|
|
589
429
|
}
|
|
590
430
|
handleUploadFailure(error) {
|
|
591
|
-
this.setState(
|
|
592
|
-
this._uploadEventDispatcher.dispatch(error,
|
|
431
|
+
this.setState("failed" /* FAILED */);
|
|
432
|
+
this._uploadEventDispatcher.dispatch(error, HttpFileUploaderEvents.DOWNLOAD_MEDIA_FAILURE);
|
|
593
433
|
console.error("Upload failed:", error);
|
|
594
434
|
}
|
|
595
435
|
cancel() {
|
|
596
436
|
this.abortController.abort();
|
|
597
|
-
this.setState(
|
|
437
|
+
this.setState("cancelled" /* CANCELLED */);
|
|
598
438
|
}
|
|
599
439
|
pause() {
|
|
600
440
|
this.isPaused = true;
|
|
601
|
-
this.setState(
|
|
441
|
+
this.setState("paused" /* PAUSED */);
|
|
602
442
|
this._uploadEventDispatcher.dispatch(
|
|
603
|
-
new
|
|
443
|
+
new UploadPausedEvent(
|
|
604
444
|
this.file.name,
|
|
605
445
|
this.totalChunks,
|
|
606
446
|
this.uploadedBytes,
|
|
607
447
|
this.percentage,
|
|
608
448
|
Date.now()
|
|
609
449
|
),
|
|
610
|
-
|
|
450
|
+
HttpFileUploaderEvents.UPLOAD_PAUSED
|
|
611
451
|
);
|
|
612
452
|
}
|
|
613
453
|
resume() {
|
|
614
454
|
this.isPaused = false;
|
|
615
|
-
this.setState(
|
|
455
|
+
this.setState("uploading" /* UPLOADING */);
|
|
616
456
|
this._uploadEventDispatcher.dispatch(
|
|
617
|
-
new
|
|
457
|
+
new UploadResumedEvent(
|
|
618
458
|
this.file.name,
|
|
619
459
|
this.totalChunks,
|
|
620
460
|
this.uploadedBytes,
|
|
621
461
|
this.percentage
|
|
622
462
|
),
|
|
623
|
-
|
|
463
|
+
HttpFileUploaderEvents.UPLOAD_RESUMED
|
|
624
464
|
);
|
|
625
465
|
}
|
|
626
466
|
/**
|
|
@@ -658,13 +498,13 @@ class ChunkedFileUploader {
|
|
|
658
498
|
const timeAlreadySpent = resumeData.lastBytePosition / assumedSpeed;
|
|
659
499
|
this.startTime = Date.now() - timeAlreadySpent * 1e3;
|
|
660
500
|
const { maxRetries = 3 } = this.options;
|
|
661
|
-
const fileHash = await
|
|
501
|
+
const fileHash = await FileUtils.generateFileHash(this.file);
|
|
662
502
|
const chunkSize = resumeData.chunkSize;
|
|
663
503
|
this.totalChunks = Math.ceil(this.file.size / chunkSize);
|
|
664
504
|
try {
|
|
665
505
|
this._uploadEventDispatcher.dispatch(
|
|
666
|
-
new
|
|
667
|
-
|
|
506
|
+
new ResumeUploadEvent(resumeData, __message),
|
|
507
|
+
HttpFileUploaderEvents.MEDIA_CHUNK_UPLOAD_RESUME
|
|
668
508
|
);
|
|
669
509
|
await this.uploadChunksWithConcurrency(
|
|
670
510
|
this.file,
|
|
@@ -688,16 +528,16 @@ class ChunkedFileUploader {
|
|
|
688
528
|
}
|
|
689
529
|
async finalizeUpload(mediaId, fileHash) {
|
|
690
530
|
try {
|
|
691
|
-
const finalizeUploadEvent = this._uploadEventDispatcher.
|
|
692
|
-
new
|
|
531
|
+
const finalizeUploadEvent = await this._uploadEventDispatcher.dispatchAsync(
|
|
532
|
+
new FinalizeUploadEvent(
|
|
693
533
|
this.endPointOptions.finalize,
|
|
694
534
|
mediaId,
|
|
695
535
|
fileHash,
|
|
696
536
|
this.options.headerFinalezingUpload
|
|
697
537
|
),
|
|
698
|
-
|
|
538
|
+
HttpFileUploaderEvents.FINALIZE_UPLOAD
|
|
699
539
|
);
|
|
700
|
-
this.setState(
|
|
540
|
+
this.setState("finalizing" /* FINALIZING */);
|
|
701
541
|
const duration = (Date.now() - this.startTime) / 1e3;
|
|
702
542
|
const uploadResult = {
|
|
703
543
|
success: true,
|
|
@@ -708,17 +548,176 @@ class ChunkedFileUploader {
|
|
|
708
548
|
averageSpeed: this.file.size / duration,
|
|
709
549
|
fileId: mediaId
|
|
710
550
|
};
|
|
711
|
-
this.setState(
|
|
551
|
+
this.setState("completed" /* COMPLETED */);
|
|
712
552
|
this._uploadEventDispatcher.dispatch(
|
|
713
|
-
new
|
|
714
|
-
|
|
553
|
+
new UploadMediaCompleteEvent(uploadResult),
|
|
554
|
+
HttpFileUploaderEvents.DOWNLOAD_MEDIA_COMPLETE
|
|
715
555
|
);
|
|
716
556
|
} catch (error) {
|
|
717
557
|
throw error;
|
|
718
558
|
}
|
|
719
559
|
}
|
|
720
|
-
}
|
|
560
|
+
};
|
|
561
|
+
/**
|
|
562
|
+
* ChunkedFileUploader
|
|
563
|
+
*
|
|
564
|
+
* A production-ready, event-driven chunked file upload engine for Browser and Node.js.
|
|
565
|
+
*
|
|
566
|
+
* Designed and developed by **AGBOKOUDJO Franck** at
|
|
567
|
+
* **INTERNATIONALES WEB APPS & SERVICES**, this class provides a robust,
|
|
568
|
+
* framework-agnostic solution for uploading large files to a remote server
|
|
569
|
+
* by splitting them into smaller chunks and sending them in parallel with
|
|
570
|
+
* configurable concurrency control.
|
|
571
|
+
*
|
|
572
|
+
* ---
|
|
573
|
+
*
|
|
574
|
+
* ### How It Works
|
|
575
|
+
*
|
|
576
|
+
* The upload process follows a strict three-phase lifecycle:
|
|
577
|
+
*
|
|
578
|
+
* 1. **Initialization** — A session is opened with the server via the `init` endpoint.
|
|
579
|
+
* The file is identified by its name, size, type, and a SHA-256 hash of its
|
|
580
|
+
* first megabyte. The server returns a unique `mediaId` that identifies the session.
|
|
581
|
+
*
|
|
582
|
+
* 2. **Chunk Upload** — The file is sliced into fixed-size chunks and uploaded
|
|
583
|
+
* concurrently using `p-limit`. Each chunk carries its index, the total number
|
|
584
|
+
* of chunks, and the session `mediaId`. Failed chunks are retried automatically
|
|
585
|
+
* with exponential backoff up to `maxRetries` attempts.
|
|
586
|
+
*
|
|
587
|
+
* 3. **Finalization** — Once all chunks are successfully uploaded, the `finalize`
|
|
588
|
+
* endpoint is called to instruct the server to assemble the chunks into the
|
|
589
|
+
* final file.
|
|
590
|
+
*
|
|
591
|
+
* ---
|
|
592
|
+
*
|
|
593
|
+
* ### Event-Driven Architecture
|
|
594
|
+
*
|
|
595
|
+
* This class follows the **Symfony EventDispatcher pattern** via
|
|
596
|
+
* `@wlindabla/event_dispatcher`. Every meaningful moment in the upload
|
|
597
|
+
* lifecycle emits a typed event that your application can listen to:
|
|
598
|
+
*
|
|
599
|
+
* ```
|
|
600
|
+
* IDLE → INITIALIZING → UPLOADING → FINALIZING → COMPLETED
|
|
601
|
+
* ↓ ↓
|
|
602
|
+
* FAILED PAUSED ↔ UPLOADING
|
|
603
|
+
* ↓
|
|
604
|
+
* CANCELLED
|
|
605
|
+
* ```
|
|
606
|
+
*
|
|
607
|
+
* All event name constants are centralized in {@link HttpFileUploaderEvents}.
|
|
608
|
+
*
|
|
609
|
+
* ---
|
|
610
|
+
*
|
|
611
|
+
* ### Subscriber Registration (Required)
|
|
612
|
+
*
|
|
613
|
+
* Before calling `.upload()`, you **must** register the two built-in subscribers
|
|
614
|
+
* on your dispatcher. They handle the HTTP communication for the init and finalize phases:
|
|
615
|
+
*
|
|
616
|
+
* ```typescript
|
|
617
|
+
* // Browser
|
|
618
|
+
* const dispatcher = new BrowserEventDispatcher(document);
|
|
619
|
+
* dispatcher.addSubscriber(new InitializeUploadSubscriber(dispatcher));
|
|
620
|
+
* dispatcher.addSubscriber(new FinalizeUploadSubscriber(dispatcher));
|
|
621
|
+
*
|
|
622
|
+
* // Node.js
|
|
623
|
+
* const dispatcher = new NodeEventDispatcher();
|
|
624
|
+
* dispatcher.addSubscriber(new InitializeUploadSubscriber(dispatcher));
|
|
625
|
+
* dispatcher.addSubscriber(new FinalizeUploadSubscriber(dispatcher));
|
|
626
|
+
* ```
|
|
627
|
+
*
|
|
628
|
+
* ---
|
|
629
|
+
*
|
|
630
|
+
* ### Fluent Builder API
|
|
631
|
+
*
|
|
632
|
+
* The class exposes a fluent API to configure the upload before starting:
|
|
633
|
+
*
|
|
634
|
+
* ```typescript
|
|
635
|
+
* const uploader = new ChunkedFileUploader(dispatcher, cache, options);
|
|
636
|
+
*
|
|
637
|
+
* await uploader
|
|
638
|
+
* .withFile(file)
|
|
639
|
+
* .withEndpoints({
|
|
640
|
+
* init: 'https://api.example.com/upload/init',
|
|
641
|
+
* upload: 'https://api.example.com/upload/chunk',
|
|
642
|
+
* finalize: 'https://api.example.com/upload/finalize'
|
|
643
|
+
* })
|
|
644
|
+
* .upload();
|
|
645
|
+
* ```
|
|
646
|
+
*
|
|
647
|
+
* ---
|
|
648
|
+
*
|
|
649
|
+
* ### Resumable Uploads
|
|
650
|
+
*
|
|
651
|
+
* When `autoSave: true` is set in options, the upload progress is persisted
|
|
652
|
+
* after each successful chunk via the {@link UploadResumeCacheInterface}.
|
|
653
|
+
* A failed or interrupted upload can be resumed later:
|
|
654
|
+
*
|
|
655
|
+
* ```typescript
|
|
656
|
+
* const resumeData = await uploader.loadResumeData(file.name);
|
|
657
|
+
*
|
|
658
|
+
* if (resumeData) {
|
|
659
|
+
* await uploader
|
|
660
|
+
* .withFile(file)
|
|
661
|
+
* .withEndpoints(endpoints)
|
|
662
|
+
* .resumeUpload(resumeData);
|
|
663
|
+
* }
|
|
664
|
+
* ```
|
|
665
|
+
*
|
|
666
|
+
* ---
|
|
667
|
+
*
|
|
668
|
+
* ### Concurrency
|
|
669
|
+
*
|
|
670
|
+
* Multiple chunks can be uploaded simultaneously. The `concurrency` option
|
|
671
|
+
* controls how many parallel uploads are active at any given time.
|
|
672
|
+
* The default value is `3`, which provides a good balance between speed
|
|
673
|
+
* and server/network load:
|
|
674
|
+
*
|
|
675
|
+
* ```typescript
|
|
676
|
+
* // Upload 5 chunks in parallel
|
|
677
|
+
* new ChunkedFileUploader(dispatcher, cache, { concurrency: 5 });
|
|
678
|
+
* ```
|
|
679
|
+
*
|
|
680
|
+
* ---
|
|
681
|
+
*
|
|
682
|
+
* ### Pause, Resume and Cancel
|
|
683
|
+
*
|
|
684
|
+
* The upload can be paused, resumed, or cancelled at any time:
|
|
685
|
+
*
|
|
686
|
+
* ```typescript
|
|
687
|
+
* uploader.pause(); // Pauses after the current chunk finishes
|
|
688
|
+
* uploader.resume(); // Resumes from where it was paused
|
|
689
|
+
* uploader.cancel(); // Aborts immediately via AbortController
|
|
690
|
+
* ```
|
|
691
|
+
*
|
|
692
|
+
* ---
|
|
693
|
+
*
|
|
694
|
+
* ### Cache
|
|
695
|
+
*
|
|
696
|
+
* The library does **not** provide a built-in cache implementation to stay
|
|
697
|
+
* lightweight and environment-agnostic. You must implement
|
|
698
|
+
* {@link UploadResumeCacheInterface} with your preferred storage backend
|
|
699
|
+
* (localStorage, IndexedDB, Redis, filesystem, etc.).
|
|
700
|
+
*
|
|
701
|
+
* ---
|
|
702
|
+
*
|
|
703
|
+
* @author AGBOKOUDJO Franck <internationaleswebservices@gmail.com>
|
|
704
|
+
* @company INTERNATIONALES WEB APPS & SERVICES
|
|
705
|
+
* @phone +229 0167 25 18 86
|
|
706
|
+
* @linkedin https://www.linkedin.com/in/internationales-web-apps-services-120520193/
|
|
707
|
+
* @github https://github.com/Agbokoudjo/file_uploader
|
|
708
|
+
*
|
|
709
|
+
* @version 1.0.0
|
|
710
|
+
* @since 1.0.0
|
|
711
|
+
* @license MIT
|
|
712
|
+
*
|
|
713
|
+
* @see {@link HttpFileUploaderEvents} All event name constants
|
|
714
|
+
* @see {@link UploadResumeCacheInterface} Cache interface to implement
|
|
715
|
+
* @see {@link InitializeUploadSubscriber} Handles the init HTTP phase
|
|
716
|
+
* @see {@link FinalizeUploadSubscriber} Handles the finalize HTTP phase
|
|
717
|
+
* @see {@link UploadOptions} Full configuration reference
|
|
718
|
+
* @see {@link https://github.com/Agbokoudjo/file_uploader | GitHub Repository}
|
|
719
|
+
*/
|
|
721
720
|
|
|
722
|
-
|
|
723
|
-
//# sourceMappingURL=
|
|
724
|
-
//# sourceMappingURL=
|
|
721
|
+
export { ChunkedFileUploader };
|
|
722
|
+
//# sourceMappingURL=chunk-6DIKDA6J.js.map
|
|
723
|
+
//# sourceMappingURL=chunk-6DIKDA6J.js.map
|