@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.
Files changed (110) hide show
  1. package/README.md +54 -28
  2. package/dist/cjs/cache/index.d.ts +198 -0
  3. package/dist/cjs/cache/index.js +318 -0
  4. package/dist/cjs/cache/index.js.map +1 -0
  5. package/dist/cjs/core/index.d.ts +271 -0
  6. package/dist/cjs/core/index.js +762 -0
  7. package/dist/cjs/core/index.js.map +1 -0
  8. package/dist/cjs/events/chunk/index.d.ts +27 -0
  9. package/dist/cjs/events/chunk/index.js +70 -0
  10. package/dist/cjs/events/chunk/index.js.map +1 -0
  11. package/dist/cjs/events/complete/index.d.ts +64 -0
  12. package/dist/cjs/events/complete/index.js +152 -0
  13. package/dist/cjs/events/complete/index.js.map +1 -0
  14. package/dist/cjs/events/index.d.ts +95 -0
  15. package/dist/cjs/events/index.js +85 -0
  16. package/dist/cjs/events/index.js.map +1 -0
  17. package/dist/cjs/events/initialize/index.d.ts +45 -0
  18. package/dist/cjs/events/initialize/index.js +105 -0
  19. package/dist/cjs/events/initialize/index.js.map +1 -0
  20. package/dist/cjs/events/state/index.d.ts +67 -0
  21. package/dist/cjs/events/state/index.js +145 -0
  22. package/dist/cjs/events/state/index.js.map +1 -0
  23. package/dist/cjs/exceptions/index.d.ts +84 -0
  24. package/dist/{exceptions → cjs/exceptions}/index.js +38 -18
  25. package/dist/cjs/exceptions/index.js.map +1 -0
  26. package/dist/cjs/index.d.ts +14 -0
  27. package/dist/cjs/index.js +33 -0
  28. package/dist/cjs/index.js.map +1 -0
  29. package/dist/cjs/subscribers/index.d.ts +34 -0
  30. package/dist/cjs/subscribers/index.js +187 -0
  31. package/dist/cjs/subscribers/index.js.map +1 -0
  32. package/dist/cjs/types/index.d.ts +110 -0
  33. package/dist/cjs/types/index.js +53 -0
  34. package/dist/cjs/types/index.js.map +1 -0
  35. package/dist/cjs/utils/index.d.ts +72 -0
  36. package/dist/cjs/utils/index.js +208 -0
  37. package/dist/cjs/utils/index.js.map +1 -0
  38. package/dist/esm/cache/index.d.mts +198 -0
  39. package/dist/esm/cache/index.js +4 -0
  40. package/dist/{index.js.map → esm/cache/index.js.map} +1 -1
  41. package/dist/{events/initialize/index.js → esm/chunk-3JTTZCSQ.js} +25 -19
  42. package/dist/esm/chunk-3JTTZCSQ.js.map +1 -0
  43. package/dist/{events/state/index.js → esm/chunk-6225YMFE.js} +38 -20
  44. package/dist/esm/chunk-6225YMFE.js.map +1 -0
  45. package/dist/esm/chunk-7QVYU63E.js +6 -0
  46. package/dist/esm/chunk-7QVYU63E.js.map +1 -0
  47. package/dist/{events/complete/index.js → esm/chunk-BNMI7DW3.js} +25 -19
  48. package/dist/esm/chunk-BNMI7DW3.js.map +1 -0
  49. package/dist/{subscribers/index.js → esm/chunk-HYNJBWW5.js} +36 -34
  50. package/dist/esm/chunk-HYNJBWW5.js.map +1 -0
  51. package/dist/{events/index.js → esm/chunk-JDL3U4OX.js} +7 -37
  52. package/dist/esm/chunk-JDL3U4OX.js.map +1 -0
  53. package/dist/{events/chunk/index.js → esm/chunk-LD2DWZRJ.js} +14 -11
  54. package/dist/esm/chunk-LD2DWZRJ.js.map +1 -0
  55. package/dist/{utils/index.js → esm/chunk-MFYC4PBP.js} +15 -22
  56. package/dist/esm/chunk-MFYC4PBP.js.map +1 -0
  57. package/dist/esm/chunk-NXYS73I4.js +125 -0
  58. package/dist/esm/chunk-NXYS73I4.js.map +1 -0
  59. package/dist/{cache/index.js → esm/chunk-PFALORWQ.js} +10 -11
  60. package/dist/esm/chunk-PFALORWQ.js.map +1 -0
  61. package/dist/{core/index.js → esm/chunk-TONVXBLH.js} +239 -231
  62. package/dist/esm/chunk-TONVXBLH.js.map +1 -0
  63. package/dist/{types/index.js → esm/chunk-X757PBC5.js} +5 -7
  64. package/dist/esm/chunk-X757PBC5.js.map +1 -0
  65. package/dist/esm/core/index.d.mts +271 -0
  66. package/dist/esm/core/index.js +12 -0
  67. package/dist/esm/core/index.js.map +1 -0
  68. package/dist/esm/events/chunk/index.d.mts +27 -0
  69. package/dist/esm/events/chunk/index.js +4 -0
  70. package/dist/esm/events/chunk/index.js.map +1 -0
  71. package/dist/esm/events/complete/index.d.mts +64 -0
  72. package/dist/esm/events/complete/index.js +4 -0
  73. package/dist/esm/events/complete/index.js.map +1 -0
  74. package/dist/esm/events/index.d.mts +95 -0
  75. package/dist/esm/events/index.js +8 -0
  76. package/dist/esm/events/index.js.map +1 -0
  77. package/dist/esm/events/initialize/index.d.mts +45 -0
  78. package/dist/esm/events/initialize/index.js +4 -0
  79. package/dist/esm/events/initialize/index.js.map +1 -0
  80. package/dist/esm/events/state/index.d.mts +67 -0
  81. package/dist/esm/events/state/index.js +4 -0
  82. package/dist/esm/events/state/index.js.map +1 -0
  83. package/dist/esm/exceptions/index.d.mts +84 -0
  84. package/dist/esm/exceptions/index.js +4 -0
  85. package/dist/esm/exceptions/index.js.map +1 -0
  86. package/dist/esm/index.d.mts +14 -0
  87. package/dist/esm/index.js +14 -0
  88. package/dist/esm/index.js.map +1 -0
  89. package/dist/esm/subscribers/index.d.mts +34 -0
  90. package/dist/esm/subscribers/index.js +10 -0
  91. package/dist/esm/subscribers/index.js.map +1 -0
  92. package/dist/esm/types/index.d.mts +110 -0
  93. package/dist/esm/types/index.js +4 -0
  94. package/dist/esm/types/index.js.map +1 -0
  95. package/dist/esm/utils/index.d.mts +72 -0
  96. package/dist/esm/utils/index.js +5 -0
  97. package/dist/esm/utils/index.js.map +1 -0
  98. package/package.json +165 -14
  99. package/dist/cache/index.js.map +0 -1
  100. package/dist/core/index.js.map +0 -1
  101. package/dist/events/chunk/index.js.map +0 -1
  102. package/dist/events/complete/index.js.map +0 -1
  103. package/dist/events/index.js.map +0 -1
  104. package/dist/events/initialize/index.js.map +0 -1
  105. package/dist/events/state/index.js.map +0 -1
  106. package/dist/exceptions/index.js.map +0 -1
  107. package/dist/index.js +0 -49
  108. package/dist/subscribers/index.js.map +0 -1
  109. package/dist/types/index.js.map +0 -1
  110. package/dist/utils/index.js.map +0 -1
@@ -0,0 +1,762 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+ var core_exports = {};
31
+ __export(core_exports, {
32
+ ChunkedFileUploader: () => ChunkedFileUploader
33
+ });
34
+ module.exports = __toCommonJS(core_exports);
35
+ var import_core = require("@wlindabla/http_client/core");
36
+ var import_types = require("../types");
37
+ var import_utils = require("../utils");
38
+ var import_events = require("../events");
39
+ var import_exceptions = require("../exceptions");
40
+ var import_p_limit = __toESM(require("p-limit"));
41
+ /**
42
+ * ChunkedFileUploader
43
+ *
44
+ * A production-ready, event-driven chunked file upload engine for Browser and Node.js.
45
+ *
46
+ * Designed and developed by **AGBOKOUDJO Franck** at
47
+ * **INTERNATIONALES WEB APPS & SERVICES**, this class provides a robust,
48
+ * framework-agnostic solution for uploading large files to a remote server
49
+ * by splitting them into smaller chunks and sending them in parallel with
50
+ * configurable concurrency control.
51
+ *
52
+ * ---
53
+ *
54
+ * ### How It Works
55
+ *
56
+ * The upload process follows a strict three-phase lifecycle:
57
+ *
58
+ * 1. **Initialization** — A session is opened with the server via the `init` endpoint.
59
+ * The file is identified by its name, size, type, and a SHA-256 hash of its
60
+ * first megabyte. The server returns a unique `mediaId` that identifies the session.
61
+ *
62
+ * 2. **Chunk Upload** — The file is sliced into fixed-size chunks and uploaded
63
+ * concurrently using `p-limit`. Each chunk carries its index, the total number
64
+ * of chunks, and the session `mediaId`. Failed chunks are retried automatically
65
+ * with exponential backoff up to `maxRetries` attempts.
66
+ *
67
+ * 3. **Finalization** — Once all chunks are successfully uploaded, the `finalize`
68
+ * endpoint is called to instruct the server to assemble the chunks into the
69
+ * final file.
70
+ *
71
+ * ---
72
+ *
73
+ * ### Event-Driven Architecture
74
+ *
75
+ * This class follows the **Symfony EventDispatcher pattern** via
76
+ * `@wlindabla/event_dispatcher`. Every meaningful moment in the upload
77
+ * lifecycle emits a typed event that your application can listen to:
78
+ *
79
+ * ```
80
+ * IDLE → INITIALIZING → UPLOADING → FINALIZING → COMPLETED
81
+ * ↓ ↓
82
+ * FAILED PAUSED ↔ UPLOADING
83
+ * ↓
84
+ * CANCELLED
85
+ * ```
86
+ *
87
+ * All event name constants are centralized in {@link HttpFileUploaderEvents}.
88
+ *
89
+ * ---
90
+ *
91
+ * ### Subscriber Registration (Required)
92
+ *
93
+ * Before calling `.upload()`, you **must** register the two built-in subscribers
94
+ * on your dispatcher. They handle the HTTP communication for the init and finalize phases:
95
+ *
96
+ * ```typescript
97
+ * // Browser
98
+ * const dispatcher = new BrowserEventDispatcher(document);
99
+ * dispatcher.addSubscriber(new InitializeUploadSubscriber(dispatcher));
100
+ * dispatcher.addSubscriber(new FinalizeUploadSubscriber(dispatcher));
101
+ *
102
+ * // Node.js
103
+ * const dispatcher = new NodeEventDispatcher();
104
+ * dispatcher.addSubscriber(new InitializeUploadSubscriber(dispatcher));
105
+ * dispatcher.addSubscriber(new FinalizeUploadSubscriber(dispatcher));
106
+ * ```
107
+ *
108
+ * ---
109
+ *
110
+ * ### Fluent Builder API
111
+ *
112
+ * The class exposes a fluent API to configure the upload before starting:
113
+ *
114
+ * ```typescript
115
+ * const uploader = new ChunkedFileUploader(dispatcher, cache, options);
116
+ *
117
+ * await uploader
118
+ * .withFile(file)
119
+ * .withEndpoints({
120
+ * init: 'https://api.example.com/upload/init',
121
+ * upload: 'https://api.example.com/upload/chunk',
122
+ * finalize: 'https://api.example.com/upload/finalize'
123
+ * })
124
+ * .upload();
125
+ * ```
126
+ *
127
+ * ---
128
+ *
129
+ * ### Resumable Uploads
130
+ *
131
+ * When `autoSave: true` is set in options, the upload progress is persisted
132
+ * after each successful chunk via the {@link UploadResumeCacheInterface}.
133
+ * A failed or interrupted upload can be resumed later:
134
+ *
135
+ * ```typescript
136
+ * const resumeData = await uploader.loadResumeData(file.name);
137
+ *
138
+ * if (resumeData) {
139
+ * await uploader
140
+ * .withFile(file)
141
+ * .withEndpoints(endpoints)
142
+ * .resumeUpload(resumeData);
143
+ * }
144
+ * ```
145
+ *
146
+ * ---
147
+ *
148
+ * ### Concurrency
149
+ *
150
+ * Multiple chunks can be uploaded simultaneously. The `concurrency` option
151
+ * controls how many parallel uploads are active at any given time.
152
+ * The default value is `3`, which provides a good balance between speed
153
+ * and server/network load:
154
+ *
155
+ * ```typescript
156
+ * // Upload 5 chunks in parallel
157
+ * new ChunkedFileUploader(dispatcher, cache, { concurrency: 5 });
158
+ * ```
159
+ *
160
+ * ---
161
+ *
162
+ * ### Pause, Resume and Cancel
163
+ *
164
+ * The upload can be paused, resumed, or cancelled at any time:
165
+ *
166
+ * ```typescript
167
+ * uploader.pause(); // Pauses after the current chunk finishes
168
+ * uploader.resume(); // Resumes from where it was paused
169
+ * uploader.cancel(); // Aborts immediately via AbortController
170
+ * ```
171
+ *
172
+ * ---
173
+ *
174
+ * ### Cache
175
+ *
176
+ * The library does **not** provide a built-in cache implementation to stay
177
+ * lightweight and environment-agnostic. You must implement
178
+ * {@link UploadResumeCacheInterface} with your preferred storage backend
179
+ * (localStorage, IndexedDB, Redis, filesystem, etc.).
180
+ *
181
+ * ---
182
+ *
183
+ * @author AGBOKOUDJO Franck <internationaleswebservices@gmail.com>
184
+ * @company INTERNATIONALES WEB APPS & SERVICES
185
+ * @phone +229 0167 25 18 86
186
+ * @linkedin https://www.linkedin.com/in/internationales-web-apps-services-120520193/
187
+ * @github https://github.com/Agbokoudjo/file_uploader
188
+ *
189
+ * @version 2.0.1
190
+ * @since 1.0.0
191
+ * @license MIT
192
+ *
193
+ * @see {@link HttpFileUploaderEvents} All event name constants
194
+ * @see {@link UploadResumeCacheInterface} Cache interface to implement
195
+ * @see {@link InitializeUploadSubscriber} Handles the init HTTP phase
196
+ * @see {@link FinalizeUploadSubscriber} Handles the finalize HTTP phase
197
+ * @see {@link UploadOptions} Full configuration reference
198
+ * @see {@link https://github.com/Agbokoudjo/file_uploader | GitHub Repository}
199
+ */
200
+ class ChunkedFileUploader {
201
+ /**
202
+ * @param _uploadEventDispatcher: new BrowserEventDispatcher(), //or new NodeJSEventDispatcher() if you have an environment NodeJs
203
+ * @param uploadResumeData: UploadResumeCacheInterface
204
+ * @param options: UploadOptions
205
+ */
206
+ constructor(_uploadEventDispatcher, uploadResumeData, options) {
207
+ this._uploadEventDispatcher = _uploadEventDispatcher;
208
+ this.uploadResumeData = uploadResumeData;
209
+ this.options = options;
210
+ this._file = null;
211
+ this._endpoints = null;
212
+ this.isPaused = false;
213
+ this.startTime = 0;
214
+ this.uploadedBytes = 0;
215
+ this.abortController = new AbortController();
216
+ this.state = import_types.UploadState.IDLE;
217
+ this.totalChunks = 0;
218
+ this.percentage = 0;
219
+ this.uploadedChunks = 0;
220
+ this.lastUploadedChunkIndex = -1;
221
+ }
222
+ _uploadEventDispatcher;
223
+ uploadResumeData;
224
+ options;
225
+ static {
226
+ __name(this, "ChunkedFileUploader");
227
+ }
228
+ _file;
229
+ _endpoints;
230
+ isPaused;
231
+ startTime;
232
+ uploadedBytes;
233
+ abortController;
234
+ state;
235
+ totalChunks;
236
+ percentage;
237
+ uploadedChunks;
238
+ lastUploadedChunkIndex;
239
+ /**
240
+ * Starts the chunked file upload process.
241
+ *
242
+ * @throws {Error} If file or endpoints are not set
243
+ * @throws {InitializeUploadFailureException} If server initialization fails
244
+ * @throws {FileUploadChunkError} If a chunk fails after all retries
245
+ *
246
+ * @example
247
+ * ```typescript
248
+ * const uploader = new ChunkedFileUploader(dispatcher, cache, options);
249
+ *
250
+ * uploader
251
+ * .withFile(file)
252
+ * .withEndpoints({ init, upload, finalize });
253
+ *
254
+ * await uploader.upload();
255
+ * ```
256
+ */
257
+ async upload() {
258
+ this.setState(import_types.UploadState.INITIALIZING);
259
+ const {
260
+ maxRetries = 3,
261
+ config,
262
+ speedMbps,
263
+ metadata,
264
+ headerInitialzingUpload,
265
+ concurrency = 3
266
+ } = this.options;
267
+ let file;
268
+ try {
269
+ file = this.file;
270
+ } catch (error) {
271
+ throw error;
272
+ }
273
+ const fileHash = await import_utils.FileUtils.generateFileHash(this.file);
274
+ let fileId;
275
+ try {
276
+ const initializationEvent = await this._uploadEventDispatcher.dispatchAsync(
277
+ new import_events.InitializingUploadEvent(
278
+ {
279
+ fileHash,
280
+ fileName: file.name,
281
+ fileSize: file.size,
282
+ fileType: file.type,
283
+ metadata,
284
+ endpointInit: this.endPointOptions.init,
285
+ headers: headerInitialzingUpload
286
+ }
287
+ ),
288
+ import_events.HttpFileUploaderEvents.INITIALIZE_UPLOAD
289
+ );
290
+ fileId = initializationEvent.mediaId;
291
+ } catch (error) {
292
+ this.setState(import_types.UploadState.FAILED);
293
+ throw error;
294
+ }
295
+ this.setState(import_types.UploadState.UPLOADING);
296
+ this.startTime = Date.now();
297
+ this.uploadedBytes = 0;
298
+ this.uploadedChunks = 0;
299
+ this.lastUploadedChunkIndex = -1;
300
+ const chunkSize = this.options.chunkSize || import_utils.FileUtils.calculateChunkSize(file.size, speedMbps, config);
301
+ this.totalChunks = Math.ceil(file.size / chunkSize);
302
+ try {
303
+ await this.uploadChunksWithConcurrency(
304
+ file,
305
+ chunkSize,
306
+ fileId,
307
+ fileHash,
308
+ maxRetries,
309
+ concurrency,
310
+ 0
311
+ );
312
+ await this.finalizeUpload(fileId, fileHash);
313
+ } catch (error) {
314
+ this.handleUploadFailure(error);
315
+ throw error;
316
+ }
317
+ }
318
+ withFile(file) {
319
+ if (!file) {
320
+ throw new Error("File is required");
321
+ }
322
+ if (file.size === 0) {
323
+ throw new Error("Cannot upload empty file");
324
+ }
325
+ if (!(file instanceof File) || !(file instanceof Blob)) {
326
+ throw new TypeError("Expected File or Blob instance");
327
+ }
328
+ this._file = file;
329
+ return this;
330
+ }
331
+ withEndpoints(endpoints) {
332
+ if (!endpoints.init || !endpoints.upload || !endpoints.finalize) {
333
+ throw new Error("All endpoints (init, upload, finalize) are required");
334
+ }
335
+ this._endpoints = endpoints;
336
+ return this;
337
+ }
338
+ get endPointOptions() {
339
+ if (!this._endpoints) {
340
+ throw new Error("Endpoint URL is required");
341
+ }
342
+ return this._endpoints;
343
+ }
344
+ get file() {
345
+ if (!this._file) {
346
+ throw new Error(`
347
+ This operation requires a mandatory file.Did you forget to upload the file?
348
+ Use the withFile(file: File|Blob) function of the ChunkedFileUploader ${this} class that you instantiated.
349
+ `);
350
+ }
351
+ return this._file;
352
+ }
353
+ /**
354
+ * Upload all chunks with concurrency control using p-limit
355
+ *
356
+ * @param file - File to upload
357
+ * @param chunkSize - Size of each chunk
358
+ * @param fileId - Server file ID
359
+ * @param fileHash - File hash
360
+ * @param maxRetries - Max retry attempts per chunk
361
+ * @param concurrency - Number of concurrent uploads (default: 3)
362
+ */
363
+ async uploadChunksWithConcurrency(file, chunkSize, fileId, fileHash, maxRetries, concurrency, startIndex = 0) {
364
+ try {
365
+ const limit = (0, import_p_limit.default)(concurrency);
366
+ const uploadPromises = [];
367
+ for (let chunkIndex = startIndex; chunkIndex < this.totalChunks; chunkIndex++) {
368
+ const limitedUpload = limit(
369
+ () => this.processChunk(
370
+ file,
371
+ chunkIndex,
372
+ chunkSize,
373
+ fileId,
374
+ fileHash,
375
+ maxRetries
376
+ )
377
+ );
378
+ uploadPromises.push(limitedUpload);
379
+ }
380
+ await Promise.all(uploadPromises);
381
+ } catch (error) {
382
+ throw error;
383
+ }
384
+ }
385
+ /**
386
+ * Process a single chunk: slice, upload with retry, and save progress.
387
+ *
388
+ * @param file - The file being uploaded
389
+ * @param currentChunkIndex - Index of the current chunk (0-based)
390
+ * @param chunkSize - Size of each chunk in bytes
391
+ * @param fileId - Server-provided file/session ID
392
+ * @param fileHash - SHA-256 hash of the file
393
+ * @param maxRetries - Maximum number of retry attempts
394
+ *
395
+ * @throws {UploadCancelledException} If upload is cancelled
396
+ * @throws {FileUploadChunkError} If chunk upload fails after all retries
397
+ */
398
+ async processChunk(file, currentChunkIndex, chunkSize, fileId, fileHash, maxRetries) {
399
+ try {
400
+ if (this.abortController.signal.aborted) {
401
+ this._uploadEventDispatcher.dispatch(
402
+ new import_events.UploadCancelledEvent(
403
+ file.name,
404
+ this.totalChunks,
405
+ this.uploadedBytes,
406
+ this.percentage,
407
+ currentChunkIndex,
408
+ "Upload cancelled by user",
409
+ Date.now()
410
+ ),
411
+ import_events.HttpFileUploaderEvents.UPLOAD_CANCELLED
412
+ );
413
+ return;
414
+ }
415
+ while (this.isPaused) {
416
+ await this.sleep(100);
417
+ }
418
+ const start = currentChunkIndex * chunkSize;
419
+ const end = Math.min(file.size, start + chunkSize);
420
+ const chunk = file.slice(start, end);
421
+ const chunkInfo = {
422
+ index: currentChunkIndex,
423
+ start,
424
+ end,
425
+ size: chunk.size,
426
+ attempt: 0,
427
+ status: "pending"
428
+ };
429
+ this._uploadEventDispatcher.dispatch(
430
+ new import_events.UploadChunkStartedEvent(chunkInfo),
431
+ import_events.HttpFileUploaderEvents.MEDIA_CHUNK_UPLOAD_STARTED
432
+ );
433
+ await this.uploadChunkWithRetry(
434
+ chunk,
435
+ chunkInfo,
436
+ fileId,
437
+ fileHash,
438
+ this.totalChunks,
439
+ maxRetries
440
+ );
441
+ if (this.options.autoSave) {
442
+ await this.saveResumeData(fileId, chunkSize);
443
+ }
444
+ } catch (error) {
445
+ throw error;
446
+ }
447
+ }
448
+ async uploadChunkWithRetry(chunk, chunkInfo, fileId, fileHash, totalChunks, maxRetries) {
449
+ let success = false;
450
+ let lastError = null;
451
+ for (let attempt = 0; attempt < maxRetries && !success; attempt++) {
452
+ chunkInfo.attempt = attempt + 1;
453
+ chunkInfo.status = "uploading";
454
+ try {
455
+ const response = await this.uploadChunk(chunk, chunkInfo, fileId, fileHash, totalChunks);
456
+ success = true;
457
+ chunkInfo.status = "success";
458
+ this.updateProgress(chunk.size, chunkInfo.index);
459
+ this.notifyProgress(
460
+ this.uploadedChunks,
461
+ totalChunks,
462
+ this.file.size,
463
+ response
464
+ );
465
+ return;
466
+ } catch (error) {
467
+ if (error instanceof import_exceptions.ChunkUploadHttpErrorException) {
468
+ this._uploadEventDispatcher.dispatch(
469
+ new import_events.ChunkUploadHttpErrorResponseEvent(
470
+ error.errorPayload,
471
+ error.statusResponse,
472
+ this.endPointOptions.upload,
473
+ chunkInfo
474
+ ),
475
+ import_events.HttpFileUploaderEvents.MEDIA_CHUNK_UPLOAD_HTTP_ERROR_RESPONSE
476
+ );
477
+ }
478
+ lastError = error;
479
+ chunkInfo.status = "error";
480
+ const chunkError = {
481
+ chunk: chunkInfo,
482
+ error: lastError,
483
+ attempt: attempt + 1,
484
+ willRetry: attempt < maxRetries - 1
485
+ };
486
+ if (error instanceof import_core.HttpFetchError) {
487
+ this._uploadEventDispatcher.dispatch(chunkError, import_events.HttpFileUploaderEvents.MEDIA_CHUNK_UPLOAD_FAILED);
488
+ }
489
+ if (attempt < maxRetries - 1) {
490
+ const delay = Math.pow(2, attempt) * 1e3;
491
+ await this.sleep(delay);
492
+ console.info(`Retry #${attempt + 2} in ${delay / 1e3}s...`);
493
+ }
494
+ }
495
+ }
496
+ if (!success) {
497
+ const fileUploadChunkError = new import_exceptions.FileUploadChunkError(
498
+ `Failed to upload chunk ${chunkInfo.index} after ${maxRetries} attempts`,
499
+ {
500
+ chunk: chunkInfo,
501
+ error: lastError,
502
+ attempt: maxRetries,
503
+ willRetry: false
504
+ }
505
+ );
506
+ this._uploadEventDispatcher.dispatch(
507
+ fileUploadChunkError,
508
+ import_events.HttpFileUploaderEvents.MEDIA_CHUNK_UPLOAD_MAXRETRY_EXPIRE
509
+ );
510
+ throw fileUploadChunkError;
511
+ }
512
+ }
513
+ async uploadChunk(chunk, chunkInfo, mediaIdFromServer, fileHash, totalChunks) {
514
+ const media = this.file;
515
+ const chunkFormData = (0, import_utils.createChunkFormData)(
516
+ chunk,
517
+ {
518
+ chunkIndex: chunkInfo.index,
519
+ mediaId: mediaIdFromServer,
520
+ fileSize: media.size,
521
+ fileName: media.name,
522
+ fileHash,
523
+ totalChunks
524
+ }
525
+ );
526
+ try {
527
+ const response_of_server = await (0, import_core.safeFetch)({
528
+ url: this.endPointOptions.upload,
529
+ headers: this.options.headers,
530
+ data: chunkFormData,
531
+ methodSend: "POST",
532
+ responseType: "json",
533
+ timeout: this.options.timeout ?? 6e4,
534
+ retryCount: 2,
535
+ retryOnStatusCode: false,
536
+ signal: this.createChunkAbortSignal()
537
+ });
538
+ const statusResponse = response_of_server.status;
539
+ if (response_of_server.failed) {
540
+ throw new import_exceptions.ChunkUploadHttpErrorException(response_of_server.data, statusResponse);
541
+ }
542
+ return response_of_server;
543
+ } catch (error) {
544
+ throw error;
545
+ }
546
+ }
547
+ createChunkAbortSignal() {
548
+ const chunkController = new AbortController();
549
+ this.abortController.signal.addEventListener("abort", () => {
550
+ chunkController.abort();
551
+ });
552
+ return chunkController.signal;
553
+ }
554
+ notifyProgress(uploadedChunks, totalChunks, totalBytes, httpResponse) {
555
+ const elapsed = Math.max((Date.now() - this.startTime) / 1e3, 0.1);
556
+ const speed = this.uploadedBytes / elapsed;
557
+ const remainingBytes = totalBytes - this.uploadedBytes;
558
+ const estimatedTimeRemaining = uploadedChunks < 2 ? null : remainingBytes / speed;
559
+ this.percentage = Math.round(this.uploadedBytes / totalBytes * 100);
560
+ const progress = {
561
+ uploadedChunks,
562
+ totalChunks,
563
+ uploadedBytes: this.uploadedBytes,
564
+ totalBytes,
565
+ percentage: this.percentage,
566
+ currentChunk: uploadedChunks,
567
+ speed,
568
+ // bytes/seconde
569
+ estimatedTimeRemaining,
570
+ // secondes (ou null)
571
+ elapsed
572
+ // secondes écoulées
573
+ };
574
+ this._uploadEventDispatcher.dispatch(
575
+ new import_events.UploadProgressEvent(
576
+ progress,
577
+ httpResponse.data,
578
+ httpResponse.status
579
+ )
580
+ );
581
+ console.info(
582
+ `Progress: ${this.percentage}% | Speed: ${import_utils.FileUtils.formatBytes(speed)}/s | ETA: ${estimatedTimeRemaining ? import_utils.FileUtils.formatDuration(estimatedTimeRemaining) : "Calculating..."}`
583
+ );
584
+ }
585
+ sleep(ms) {
586
+ return new Promise((resolve) => setTimeout(resolve, ms));
587
+ }
588
+ /**
589
+ * Save current upload progress to cache for resume capability
590
+ *
591
+ * @param fileId - Server-provided file/session ID
592
+ * @param chunkSize - Size of each chunk in bytes
593
+ * @returns Saved resume data
594
+ */
595
+ async saveResumeData(fileId, chunkSize) {
596
+ const data = {
597
+ fileId,
598
+ fileName: this.file.name,
599
+ fileSize: this.file.size,
600
+ uploadedChunks: this.uploadedChunks,
601
+ lastChunkIndex: this.lastUploadedChunkIndex,
602
+ lastBytePosition: this.uploadedBytes,
603
+ chunkSize,
604
+ concurrency: this.options.concurrency || 3
605
+ };
606
+ await this.uploadResumeData.setItem(`upload_${this.file.name}`, data);
607
+ console.info(
608
+ `Resume data saved: ${this.uploadedChunks}/${Math.ceil(this.file.size / chunkSize)} chunks, last chunk index: ${this.lastUploadedChunkIndex}`
609
+ );
610
+ return data;
611
+ }
612
+ setState(newState) {
613
+ const oldState = this.state;
614
+ this.state = newState;
615
+ this._uploadEventDispatcher.dispatch(
616
+ new import_events.UploadStateChangedEvent(
617
+ oldState,
618
+ newState,
619
+ Date.now()
620
+ ),
621
+ import_events.HttpFileUploaderEvents.UPLOAD_STATE_CHANGED
622
+ );
623
+ }
624
+ getState() {
625
+ return this.state;
626
+ }
627
+ handleUploadFailure(error) {
628
+ this.setState(import_types.UploadState.FAILED);
629
+ this._uploadEventDispatcher.dispatch(error, import_events.HttpFileUploaderEvents.DOWNLOAD_MEDIA_FAILURE);
630
+ console.error("Upload failed:", error);
631
+ }
632
+ cancel() {
633
+ this.abortController.abort();
634
+ this.setState(import_types.UploadState.CANCELLED);
635
+ }
636
+ pause() {
637
+ this.isPaused = true;
638
+ this.setState(import_types.UploadState.PAUSED);
639
+ this._uploadEventDispatcher.dispatch(
640
+ new import_events.UploadPausedEvent(
641
+ this.file.name,
642
+ this.totalChunks,
643
+ this.uploadedBytes,
644
+ this.percentage,
645
+ Date.now()
646
+ ),
647
+ import_events.HttpFileUploaderEvents.UPLOAD_PAUSED
648
+ );
649
+ }
650
+ resume() {
651
+ this.isPaused = false;
652
+ this.setState(import_types.UploadState.UPLOADING);
653
+ this._uploadEventDispatcher.dispatch(
654
+ new import_events.UploadResumedEvent(
655
+ this.file.name,
656
+ this.totalChunks,
657
+ this.uploadedBytes,
658
+ this.percentage
659
+ ),
660
+ import_events.HttpFileUploaderEvents.UPLOAD_RESUMED
661
+ );
662
+ }
663
+ /**
664
+ * Load previously saved resume data
665
+ *
666
+ * @param fileName - Name of the file to resume
667
+ * @returns Resume data or null if not found
668
+ */
669
+ async loadResumeData(fileName) {
670
+ try {
671
+ const data = await this.uploadResumeData.getItem(`upload_${fileName}`);
672
+ if (data) {
673
+ console.info(
674
+ `Resume data loaded: ${data.uploadedChunks} chunks uploaded, last index: ${data.lastChunkIndex}`
675
+ );
676
+ }
677
+ return data;
678
+ } catch (error) {
679
+ console.warn(`No resume data found for ${fileName}`);
680
+ return null;
681
+ }
682
+ }
683
+ /**
684
+ * Resume upload from saved state
685
+ *
686
+ * @param resumeData - Previously saved resume data
687
+ * @returns Upload result
688
+ */
689
+ async resumeUpload(resumeData) {
690
+ const __message = `Resuming upload from chunk ${resumeData.lastChunkIndex + 1} (${resumeData.uploadedChunks} chunks already uploaded)`;
691
+ this.uploadedBytes = resumeData.lastBytePosition;
692
+ this.uploadedChunks = resumeData.uploadedChunks;
693
+ this.lastUploadedChunkIndex = resumeData.lastChunkIndex;
694
+ const assumedSpeed = 5e5;
695
+ const timeAlreadySpent = resumeData.lastBytePosition / assumedSpeed;
696
+ this.startTime = Date.now() - timeAlreadySpent * 1e3;
697
+ const { maxRetries = 3 } = this.options;
698
+ const fileHash = await import_utils.FileUtils.generateFileHash(this.file);
699
+ const chunkSize = resumeData.chunkSize;
700
+ this.totalChunks = Math.ceil(this.file.size / chunkSize);
701
+ try {
702
+ this._uploadEventDispatcher.dispatch(
703
+ new import_events.ResumeUploadEvent(resumeData, __message),
704
+ import_events.HttpFileUploaderEvents.MEDIA_CHUNK_UPLOAD_RESUME
705
+ );
706
+ await this.uploadChunksWithConcurrency(
707
+ this.file,
708
+ chunkSize,
709
+ resumeData.fileId,
710
+ fileHash,
711
+ maxRetries,
712
+ resumeData.concurrency,
713
+ resumeData.lastChunkIndex + 1
714
+ );
715
+ await this.finalizeUpload(resumeData.fileId, fileHash);
716
+ } catch (error) {
717
+ this.handleUploadFailure(error);
718
+ throw error;
719
+ }
720
+ }
721
+ updateProgress(chunkSize, chunkIndex) {
722
+ this.uploadedBytes += chunkSize;
723
+ this.uploadedChunks++;
724
+ this.lastUploadedChunkIndex = Math.max(this.lastUploadedChunkIndex, chunkIndex);
725
+ }
726
+ async finalizeUpload(mediaId, fileHash) {
727
+ try {
728
+ const finalizeUploadEvent = await this._uploadEventDispatcher.dispatchAsync(
729
+ new import_events.FinalizeUploadEvent(
730
+ this.endPointOptions.finalize,
731
+ mediaId,
732
+ fileHash,
733
+ this.options.headerFinalezingUpload
734
+ ),
735
+ import_events.HttpFileUploaderEvents.FINALIZE_UPLOAD
736
+ );
737
+ this.setState(import_types.UploadState.FINALIZING);
738
+ const duration = (Date.now() - this.startTime) / 1e3;
739
+ const uploadResult = {
740
+ success: true,
741
+ finalizeUploadResponse: finalizeUploadEvent.hasResponse() ? finalizeUploadEvent.getResponse() : null,
742
+ totalChunks: this.totalChunks,
743
+ totalBytes: this.file.size,
744
+ duration,
745
+ averageSpeed: this.file.size / duration,
746
+ fileId: mediaId
747
+ };
748
+ this.setState(import_types.UploadState.COMPLETED);
749
+ this._uploadEventDispatcher.dispatch(
750
+ new import_events.UploadMediaCompleteEvent(uploadResult),
751
+ import_events.HttpFileUploaderEvents.DOWNLOAD_MEDIA_COMPLETE
752
+ );
753
+ } catch (error) {
754
+ throw error;
755
+ }
756
+ }
757
+ }
758
+ // Annotate the CommonJS export names for ESM import in node:
759
+ 0 && (module.exports = {
760
+ ChunkedFileUploader
761
+ });
762
+ //# sourceMappingURL=index.js.map