@karpeleslab/klbfw 0.2.19 → 0.2.21

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.
@@ -0,0 +1,821 @@
1
+ /**
2
+ * KLB Upload Legacy Module
3
+ *
4
+ * This module provides the deprecated upload object for backwards compatibility.
5
+ * New code should use uploadFile() or uploadManyFiles() instead.
6
+ *
7
+ * @module upload-legacy
8
+ * @deprecated Use uploadFile() or uploadManyFiles() instead
9
+ */
10
+
11
+ 'use strict';
12
+
13
+ const rest = require('./rest');
14
+ const fwWrapper = require('./fw-wrapper');
15
+ const { env, utils, awsReq } = require('./upload-internal');
16
+
17
+ /**
18
+ * Upload module (IIFE pattern)
19
+ * @deprecated Use uploadFile() or uploadManyFiles() instead
20
+ * @returns {Object} Upload interface
21
+ */
22
+ module.exports.upload = (function () {
23
+ /**
24
+ * Upload state
25
+ */
26
+ const state = {
27
+ queue: [], // Queued uploads
28
+ failed: [], // Failed uploads
29
+ running: {}, // Currently processing uploads
30
+ nextId: 0, // Next upload ID
31
+ lastInput: null // Last created file input element (browser only)
32
+ };
33
+
34
+ // Public API object
35
+ const upload = {};
36
+
37
+ /**
38
+ * Helper Functions
39
+ */
40
+
41
+ /**
42
+ * Notify progress to listeners
43
+ * Calls onprogress callback and dispatches events
44
+ */
45
+ function sendProgress() {
46
+ const status = upload.getStatus();
47
+
48
+ // Call the onprogress callback if defined
49
+ if (typeof upload.onprogress === "function") {
50
+ upload.onprogress(status);
51
+ }
52
+
53
+ // Dispatch event for listeners
54
+ utils.dispatchEvent("upload:progress", status);
55
+ }
56
+
57
+ /**
58
+ * Handle upload failure
59
+ * @param {Object} up - Upload object
60
+ * @param {*} error - Error data
61
+ */
62
+ function handleFailure(up, error) {
63
+ // Skip if upload is no longer running
64
+ if (!(up.up_id in state.running)) return;
65
+
66
+ // Check if already in failed list
67
+ for (const failedItem of state.failed) {
68
+ if (failedItem.up_id === up.up_id) {
69
+ return; // Already recorded as failed
70
+ }
71
+ }
72
+
73
+ // Record failure
74
+ up.failure = error;
75
+ state.failed.push(up);
76
+ delete state.running[up.up_id];
77
+
78
+ // Reject the promise so callers know the upload failed
79
+ if (up.reject) {
80
+ up.reject(error);
81
+ }
82
+
83
+ // Continue processing queue
84
+ upload.run();
85
+
86
+ // Notify progress
87
+ sendProgress();
88
+
89
+ // Dispatch failure event
90
+ utils.dispatchEvent("upload:failed", {
91
+ item: up,
92
+ res: error
93
+ });
94
+ }
95
+
96
+ /**
97
+ * Process a pending upload
98
+ * Initiates the upload process with the server
99
+ * @param {Object} up - Upload object
100
+ */
101
+ function processUpload(up) {
102
+ // Mark as processing
103
+ up.status = "pending-wip";
104
+
105
+ // Prepare parameters
106
+ const params = up.params || {};
107
+
108
+ // Set file metadata
109
+ params.filename = up.file.name;
110
+ params.size = up.file.size;
111
+ params.lastModified = up.file.lastModified / 1000;
112
+ params.type = up.file.type || "application/octet-stream";
113
+
114
+ // Initialize upload with the server
115
+ rest.rest(up.path, "POST", params, up.context)
116
+ .then(function(response) {
117
+ // Method 1: AWS signed multipart upload
118
+ if (response.data.Cloud_Aws_Bucket_Upload__) {
119
+ return handleAwsMultipartUpload(up, response.data);
120
+ }
121
+
122
+ // Method 2: Direct PUT upload
123
+ if (response.data.PUT) {
124
+ return handlePutUpload(up, response.data);
125
+ }
126
+
127
+ // Invalid response format
128
+ delete state.running[up.up_id];
129
+ state.failed.push(up);
130
+ up.reject(new Error('Invalid upload response format'));
131
+ })
132
+ .catch(error => handleFailure(up, error));
133
+ }
134
+
135
+ /**
136
+ * Set up AWS multipart upload
137
+ * @param {Object} up - Upload object
138
+ * @param {Object} data - Server response data
139
+ */
140
+ function handleAwsMultipartUpload(up, data) {
141
+ // Store upload info
142
+ up.info = data;
143
+
144
+ // Initialize multipart upload
145
+ return awsReq(
146
+ up.info,
147
+ "POST",
148
+ "uploads=",
149
+ "",
150
+ {"Content-Type": up.file.type || "application/octet-stream", "X-Amz-Acl": "private"},
151
+ up.context
152
+ )
153
+ .then(response => response.text())
154
+ .then(str => utils.parseXML(str))
155
+ .then(dom => dom.querySelector('UploadId').innerHTML)
156
+ .then(uploadId => {
157
+ up.uploadId = uploadId;
158
+
159
+ // Calculate optimal block size
160
+ const fileSize = up.file.size;
161
+
162
+ // Target ~10k parts, but minimum 5MB per AWS requirements
163
+ let blockSize = Math.ceil(fileSize / 10000);
164
+ if (blockSize < 5242880) blockSize = 5242880;
165
+
166
+ // Set up upload parameters
167
+ up.method = 'aws';
168
+ up.bsize = blockSize;
169
+ up.blocks = Math.ceil(fileSize / blockSize);
170
+ up.b = {};
171
+ up.status = 'uploading';
172
+
173
+ // Continue upload process
174
+ upload.run();
175
+ })
176
+ .catch(error => handleFailure(up, error));
177
+ }
178
+
179
+ /**
180
+ * Set up direct PUT upload
181
+ * @param {Object} up - Upload object
182
+ * @param {Object} data - Server response data
183
+ */
184
+ function handlePutUpload(up, data) {
185
+ // Store upload info
186
+ up.info = data;
187
+
188
+ // Calculate block size (if multipart PUT is supported)
189
+ const fileSize = up.file.size;
190
+ let blockSize = fileSize; // Default: single block
191
+
192
+ if (data.Blocksize) {
193
+ // Server supports multipart upload
194
+ blockSize = data.Blocksize;
195
+ }
196
+
197
+ // Set up upload parameters
198
+ up.method = 'put';
199
+ up.bsize = blockSize;
200
+ up.blocks = Math.ceil(fileSize / blockSize);
201
+ up.b = {};
202
+ up.status = 'uploading';
203
+
204
+ // Continue upload process
205
+ upload.run();
206
+ }
207
+
208
+ /**
209
+ * Upload a single part of a file
210
+ * Handles both AWS multipart and direct PUT methods
211
+ * @param {Object} up - Upload object
212
+ * @param {number} partNumber - Part number (0-based)
213
+ */
214
+ function uploadPart(up, partNumber) {
215
+ // Mark part as pending
216
+ up.b[partNumber] = "pending";
217
+
218
+ // Calculate byte range for this part
219
+ const startByte = partNumber * up.bsize;
220
+ const endByte = Math.min(startByte + up.bsize, up.file.size);
221
+
222
+ // Read file slice as ArrayBuffer
223
+ utils.readAsArrayBuffer(up.file, {
224
+ start: startByte,
225
+ end: endByte
226
+ }, (arrayBuffer, error) => {
227
+ if (error) {
228
+ handleFailure(up, error);
229
+ return;
230
+ }
231
+
232
+ // Choose upload method based on protocol
233
+ if (up.method === 'aws') {
234
+ uploadAwsPart(up, partNumber, arrayBuffer);
235
+ } else if (up.method === 'put') {
236
+ uploadPutPart(up, partNumber, startByte, arrayBuffer);
237
+ } else {
238
+ handleFailure(up, new Error(`Unknown upload method: ${up.method}`));
239
+ }
240
+ });
241
+ }
242
+
243
+ /**
244
+ * Upload a part using AWS multipart upload
245
+ * @param {Object} up - Upload object
246
+ * @param {number} partNumber - Part number (0-based)
247
+ * @param {ArrayBuffer} data - Part data
248
+ */
249
+ function uploadAwsPart(up, partNumber, data) {
250
+ // AWS part numbers are 1-based
251
+ const awsPartNumber = partNumber + 1;
252
+
253
+ awsReq(
254
+ up.info,
255
+ "PUT",
256
+ `partNumber=${awsPartNumber}&uploadId=${up.uploadId}`,
257
+ data,
258
+ null,
259
+ up.context
260
+ )
261
+ .then(response => {
262
+ // Verify the response is successful
263
+ if (!response.ok) {
264
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
265
+ }
266
+ // Store ETag for this part (needed for completion)
267
+ const etag = response.headers.get("ETag");
268
+ // Read response body to ensure request completed
269
+ return response.text().then(() => etag);
270
+ })
271
+ .then(etag => {
272
+ up.b[partNumber] = etag;
273
+
274
+ // Update progress and continue processing
275
+ sendProgress();
276
+ upload.run();
277
+ })
278
+ .catch(error => handleFailure(up, error));
279
+ }
280
+
281
+ /**
282
+ * Upload a part using direct PUT
283
+ * @param {Object} up - Upload object
284
+ * @param {number} partNumber - Part number (0-based)
285
+ * @param {number} startByte - Starting byte position
286
+ * @param {ArrayBuffer} data - Part data
287
+ */
288
+ function uploadPutPart(up, partNumber, startByte, data) {
289
+ // Set up headers
290
+ const headers = {
291
+ "Content-Type": up.file.type || "application/octet-stream"
292
+ };
293
+
294
+ // Add Content-Range header for multipart PUT
295
+ if (up.blocks > 1) {
296
+ const endByte = startByte + data.byteLength - 1; // inclusive
297
+ headers["Content-Range"] = `bytes ${startByte}-${endByte}/*`;
298
+ }
299
+
300
+ // Perform the PUT request
301
+ utils.fetch(up.info.PUT, {
302
+ method: "PUT",
303
+ body: data,
304
+ headers: headers,
305
+ })
306
+ .then(response => {
307
+ // Verify the response is successful
308
+ if (!response.ok) {
309
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
310
+ }
311
+ // Read response body to ensure request completed
312
+ return response.text();
313
+ })
314
+ .then(() => {
315
+ // Mark part as done
316
+ up.b[partNumber] = "done";
317
+
318
+ // Update progress and continue processing
319
+ sendProgress();
320
+ upload.run();
321
+ })
322
+ .catch(error => handleFailure(up, error));
323
+ }
324
+
325
+ /**
326
+ * Process an upload in progress
327
+ * Manages uploading parts and completing the upload
328
+ * @param {Object} up - Upload object
329
+ */
330
+ function processActiveUpload(up) {
331
+ // Skip if paused or canceled
332
+ if (up.paused || up.canceled) return;
333
+
334
+ // Track upload progress
335
+ let pendingParts = 0;
336
+ let completedParts = 0;
337
+
338
+ // Process each part
339
+ for (let i = 0; i < up.blocks; i++) {
340
+ if (up.b[i] === undefined) {
341
+ // Part not started yet
342
+ if (up.paused) break; // Don't start new parts when paused
343
+
344
+ // Start uploading this part
345
+ uploadPart(up, i);
346
+ pendingParts++;
347
+ } else if (up.b[i] !== "pending") {
348
+ // Part completed
349
+ completedParts++;
350
+ continue;
351
+ } else {
352
+ // Part in progress
353
+ pendingParts++;
354
+ }
355
+
356
+ // Limit concurrent uploads
357
+ if (pendingParts >= 3) break;
358
+ }
359
+
360
+ // Update upload progress
361
+ up.done = completedParts;
362
+
363
+ // Check if all parts are complete
364
+ if (pendingParts === 0 && completedParts === up.blocks) {
365
+ // All parts complete, finalize the upload
366
+ up.status = "validating";
367
+
368
+ if (up.method === 'aws') {
369
+ completeAwsUpload(up);
370
+ } else if (up.method === 'put') {
371
+ completePutUpload(up);
372
+ }
373
+ }
374
+ }
375
+
376
+ /**
377
+ * Complete AWS multipart upload
378
+ * @param {Object} up - Upload object
379
+ */
380
+ function completeAwsUpload(up) {
381
+ // Create completion XML
382
+ // See: https://docs.aws.amazon.com/AmazonS3/latest/API/mpUploadComplete.html
383
+ let xml = "<CompleteMultipartUpload>";
384
+
385
+ for (let i = 0; i < up.blocks; i++) {
386
+ // AWS part numbers are 1-based
387
+ xml += `<Part><PartNumber>${i + 1}</PartNumber><ETag>${up.b[i]}</ETag></Part>`;
388
+ }
389
+
390
+ xml += "</CompleteMultipartUpload>";
391
+
392
+ // Send completion request
393
+ awsReq(up.info, "POST", `uploadId=${up.uploadId}`, xml, null, up.context)
394
+ .then(response => response.text())
395
+ .then(() => {
396
+ // Call server-side completion handler
397
+ return rest.rest(
398
+ `Cloud/Aws/Bucket/Upload/${up.info.Cloud_Aws_Bucket_Upload__}:handleComplete`,
399
+ "POST",
400
+ {},
401
+ up.context
402
+ );
403
+ })
404
+ .then(response => {
405
+ // Mark upload as complete
406
+ up.status = "complete";
407
+ up.final = response.data;
408
+
409
+ // Notify listeners
410
+ sendProgress();
411
+
412
+ // Remove from running uploads
413
+ delete state.running[up.up_id];
414
+
415
+ // Resolve the upload promise
416
+ up.resolve(up);
417
+
418
+ // Continue processing queue
419
+ upload.run();
420
+ })
421
+ .catch(error => handleFailure(up, error));
422
+ }
423
+
424
+ /**
425
+ * Complete direct PUT upload
426
+ * @param {Object} up - Upload object
427
+ */
428
+ function completePutUpload(up) {
429
+ // Call completion endpoint
430
+ rest.rest(up.info.Complete, "POST", {}, up.context)
431
+ .then(response => {
432
+ // Mark upload as complete
433
+ up.status = "complete";
434
+ up.final = response.data;
435
+
436
+ // Notify listeners
437
+ sendProgress();
438
+
439
+ // Remove from running uploads
440
+ delete state.running[up.up_id];
441
+
442
+ // Resolve the upload promise
443
+ up.resolve(up);
444
+
445
+ // Continue processing queue
446
+ upload.run();
447
+ })
448
+ .catch(error => handleFailure(up, error));
449
+ }
450
+
451
+ /**
452
+ * Fill the upload queue with new upload tasks
453
+ * Takes items from the queue and adds them to running uploads
454
+ */
455
+ function fillUploadQueue() {
456
+ // Skip if we're already running the maximum number of uploads
457
+ if (Object.keys(state.running).length >= 3) return;
458
+
459
+ // Maximum of 3 concurrent uploads
460
+ while (Object.keys(state.running).length < 3 && state.queue.length > 0) {
461
+ // Get next upload from queue
462
+ const upload = state.queue.shift();
463
+
464
+ // Add to running uploads
465
+ state.running[upload.up_id] = upload;
466
+ }
467
+
468
+ // Notify progress
469
+ sendProgress();
470
+ }
471
+
472
+ /**
473
+ * Get current upload status
474
+ * @returns {Object} Status object with queued, running and failed uploads
475
+ */
476
+ upload.getStatus = function() {
477
+ return {
478
+ queue: state.queue,
479
+ running: Object.keys(state.running).map(id => state.running[id]),
480
+ failed: state.failed
481
+ };
482
+ };
483
+
484
+ /**
485
+ * Resume all failed uploads
486
+ * Moves failed uploads back to the queue
487
+ */
488
+ upload.resume = function() {
489
+ // Move all failed uploads back to the queue
490
+ while (state.failed.length > 0) {
491
+ state.queue.push(state.failed.shift());
492
+ }
493
+
494
+ // Restart upload process
495
+ upload.run();
496
+ };
497
+
498
+ /**
499
+ * Initialize uploads in different environments
500
+ *
501
+ * @param {string} path - API path to upload to
502
+ * @param {Object} params - Upload parameters
503
+ * @param {Function} notify - Notification callback
504
+ * @returns {Function} - Function to start uploads
505
+ */
506
+ upload.init = function(path, params, notify) {
507
+ params = params || {};
508
+
509
+ if (env.isBrowser) {
510
+ // Browser implementation
511
+ if (state.lastInput !== null) {
512
+ state.lastInput.parentNode.removeChild(state.lastInput);
513
+ state.lastInput = null;
514
+ }
515
+
516
+ const input = document.createElement("input");
517
+ input.type = "file";
518
+ input.style.display = "none";
519
+ if (!params.single) {
520
+ input.multiple = "multiple";
521
+ }
522
+
523
+ document.getElementsByTagName('body')[0].appendChild(input);
524
+ state.lastInput = input;
525
+
526
+ const promise = new Promise(function(resolve, reject) {
527
+ input.onchange = function() {
528
+ if (this.files.length === 0) {
529
+ return resolve();
530
+ }
531
+
532
+ let count = this.files.length;
533
+ if (notify) notify({status: 'init', count: count});
534
+
535
+ for (let i = 0; i < this.files.length; i++) {
536
+ upload.append(path, this.files[i], params, fwWrapper.getContext())
537
+ .then(function(obj) {
538
+ count -= 1;
539
+ if (notify) notify(obj);
540
+ if (count === 0) resolve();
541
+ });
542
+ }
543
+ upload.run();
544
+ };
545
+ });
546
+
547
+ input.click();
548
+ return promise;
549
+ } else {
550
+ // Non-browser environment
551
+ return function(files) {
552
+ // Allow array, single file object, or file content buffer
553
+ if (!Array.isArray(files)) {
554
+ if (files instanceof ArrayBuffer ||
555
+ (files.buffer instanceof ArrayBuffer) ||
556
+ (typeof Buffer !== 'undefined' && files instanceof Buffer)) {
557
+ // If it's a buffer/ArrayBuffer, create a file-like object
558
+ files = [{
559
+ name: params.filename || 'file.bin',
560
+ size: files.byteLength || files.length,
561
+ type: params.type || 'application/octet-stream',
562
+ lastModified: Date.now(),
563
+ content: files
564
+ }];
565
+ } else {
566
+ // Single file object
567
+ files = [files];
568
+ }
569
+ }
570
+
571
+ return new Promise(function(resolve, reject) {
572
+ const count = files.length;
573
+ if (count === 0) {
574
+ return resolve();
575
+ }
576
+
577
+ if (notify) notify({status: 'init', count: count});
578
+
579
+ let remainingCount = count;
580
+
581
+ files.forEach(file => {
582
+ try {
583
+ // Ensure file has required properties
584
+ if (!file.name) file.name = 'file.bin';
585
+ if (!file.type) file.type = 'application/octet-stream';
586
+ if (!file.lastModified) file.lastModified = Date.now();
587
+
588
+ // Add slice method if not present
589
+ if (!file.slice && file.content) {
590
+ file.slice = function(start, end) {
591
+ return {
592
+ content: this.content.slice(start, end || this.size)
593
+ };
594
+ };
595
+ }
596
+
597
+ upload.append(path, file, params, fwWrapper.getContext())
598
+ .then(function(obj) {
599
+ remainingCount -= 1;
600
+ if (notify) notify(obj);
601
+ if (remainingCount === 0) resolve();
602
+ })
603
+ .catch(function(err) {
604
+ remainingCount -= 1;
605
+ console.error('Error uploading file:', err);
606
+ if (remainingCount === 0) resolve();
607
+ });
608
+ } catch (err) {
609
+ remainingCount -= 1;
610
+ console.error('Error processing file:', err);
611
+ if (remainingCount === 0) resolve();
612
+ }
613
+ });
614
+
615
+ upload.run();
616
+ });
617
+ };
618
+ }
619
+ };
620
+
621
+ /**
622
+ * Add a file to the upload queue
623
+ * @param {string} path - API path to upload to
624
+ * @param {File|Object} file - File to upload
625
+ * @param {Object} params - Upload parameters
626
+ * @param {Object} context - Request context
627
+ * @returns {Promise} - Upload promise
628
+ */
629
+ upload.append = function(path, file, params, context) {
630
+ return new Promise((resolve, reject) => {
631
+ // Process parameters
632
+ params = params || {};
633
+ context = context || fwWrapper.getContext();
634
+
635
+ // Create an upload object
636
+ const uploadObject = {
637
+ path: path,
638
+ file: file,
639
+ resolve: resolve,
640
+ reject: reject,
641
+ status: "pending",
642
+ paused: false,
643
+ up_id: state.nextId++,
644
+ params: params,
645
+ context: { ...context } // Create a copy to avoid modification
646
+ };
647
+
648
+ // Add to queue
649
+ state.queue.push(uploadObject);
650
+ });
651
+ };
652
+
653
+ /**
654
+ * Cancel an upload in progress or in queue
655
+ * @param {number} uploadId - Upload ID to cancel
656
+ */
657
+ upload.cancelItem = function(uploadId) {
658
+ // Check running uploads
659
+ if (state.running[uploadId]) {
660
+ // Mark running upload as canceled
661
+ state.running[uploadId].canceled = true;
662
+ } else {
663
+ // Check queued uploads
664
+ for (let i = 0; i < state.queue.length; i++) {
665
+ if (state.queue[i].up_id === uploadId) {
666
+ state.queue[i].canceled = true;
667
+ break;
668
+ }
669
+ }
670
+ }
671
+
672
+ // Update progress
673
+ sendProgress();
674
+ };
675
+
676
+ /**
677
+ * Delete an upload from queue or failed list
678
+ * Only canceled uploads can be removed from running list
679
+ * @param {number} uploadId - Upload ID to delete
680
+ */
681
+ upload.deleteItem = function(uploadId) {
682
+ // Check running uploads
683
+ if (state.running[uploadId]) {
684
+ // Only delete if canceled
685
+ if (state.running[uploadId].canceled) {
686
+ delete state.running[uploadId];
687
+ }
688
+ } else {
689
+ // Check queue
690
+ for (let i = 0; i < state.queue.length; i++) {
691
+ if (state.queue[i].up_id === uploadId) {
692
+ state.queue.splice(i, 1);
693
+ break;
694
+ }
695
+ }
696
+
697
+ // Check failed uploads
698
+ for (let i = 0; i < state.failed.length; i++) {
699
+ if (state.failed[i].up_id === uploadId) {
700
+ state.failed.splice(i, 1);
701
+ break;
702
+ }
703
+ }
704
+ }
705
+
706
+ // Update progress
707
+ sendProgress();
708
+ };
709
+
710
+ /**
711
+ * Pause an active upload
712
+ * @param {number} uploadId - Upload ID to pause
713
+ */
714
+ upload.pauseItem = function(uploadId) {
715
+ // Find upload in running list
716
+ const upload = state.running[uploadId];
717
+
718
+ // Only pause if active
719
+ if (upload && upload.status === "uploading") {
720
+ upload.paused = true;
721
+ }
722
+
723
+ // Update progress
724
+ sendProgress();
725
+ };
726
+
727
+ /**
728
+ * Resume a paused upload
729
+ * @param {number} uploadId - Upload ID to resume
730
+ */
731
+ upload.resumeItem = function(uploadId) {
732
+ // Find upload in running list
733
+ const upload = state.running[uploadId];
734
+
735
+ // Only resume if paused
736
+ if (upload && upload.paused) {
737
+ upload.paused = false;
738
+ processActiveUpload(upload);
739
+ }
740
+
741
+ // Update progress
742
+ sendProgress();
743
+ };
744
+
745
+ /**
746
+ * Retry a failed upload
747
+ * @param {number} uploadId - Upload ID to retry
748
+ */
749
+ upload.retryItem = function(uploadId) {
750
+ // Find upload in failed list
751
+ let failedUpload = null;
752
+ let failedIndex = -1;
753
+
754
+ for (let i = 0; i < state.failed.length; i++) {
755
+ if (state.failed[i].up_id === uploadId) {
756
+ failedUpload = state.failed[i];
757
+ failedIndex = i;
758
+ break;
759
+ }
760
+ }
761
+
762
+ // Skip if not found
763
+ if (!failedUpload) return;
764
+
765
+ // Check if already in queue
766
+ for (let i = 0; i < state.queue.length; i++) {
767
+ if (state.queue[i].up_id === uploadId) {
768
+ return; // Already in queue
769
+ }
770
+ }
771
+
772
+ // Reset failure data
773
+ failedUpload.failure = {};
774
+
775
+ // Reset pending parts
776
+ for (let i = 0; i < failedUpload.blocks; i++) {
777
+ if (failedUpload.b[i] === "pending") {
778
+ failedUpload.b[i] = undefined;
779
+ }
780
+ }
781
+
782
+ // Move from failed to queue
783
+ state.failed.splice(failedIndex, 1);
784
+ state.queue.push(failedUpload);
785
+
786
+ // Restart upload
787
+ upload.run();
788
+
789
+ // Dispatch retry event
790
+ utils.dispatchEvent("upload:retry", { item: failedUpload });
791
+
792
+ // Update progress
793
+ sendProgress();
794
+ };
795
+
796
+ /**
797
+ * Start or continue the upload process
798
+ * Processes queued uploads and continues running uploads
799
+ */
800
+ upload.run = function() {
801
+ // Fill queue with new uploads
802
+ fillUploadQueue();
803
+
804
+ // Process running uploads
805
+ for (const uploadId in state.running) {
806
+ const upload = state.running[uploadId];
807
+
808
+ // Process based on status
809
+ switch (upload.status) {
810
+ case "pending":
811
+ processUpload(upload);
812
+ break;
813
+ case "uploading":
814
+ processActiveUpload(upload);
815
+ break;
816
+ }
817
+ }
818
+ };
819
+
820
+ return upload;
821
+ }());