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