@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.
package/upload-many.js ADDED
@@ -0,0 +1,204 @@
1
+ /**
2
+ * KLB Upload Many Module
3
+ *
4
+ * This module provides the uploadManyFiles function for batch uploading files.
5
+ *
6
+ * @module upload-many
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const { uploadFile } = require('./upload');
12
+
13
+ /**
14
+ * Upload multiple files with concurrency control
15
+ *
16
+ * This function uploads an array of files to the same API endpoint, with up to 3
17
+ * concurrent uploads at a time. It provides progress information for both individual
18
+ * files and overall progress.
19
+ *
20
+ * @param {string} api - API endpoint path (e.g., 'Misc/Debug:testUpload')
21
+ * @param {Array} files - Array of files to upload. Each element can be:
22
+ * - A browser File object
23
+ * - A Node.js Buffer
24
+ * - An ArrayBuffer or Uint8Array
25
+ * - A file-like object with { name, size, type, content }
26
+ * - A string (will be converted to UTF-8 bytes)
27
+ * @param {string} [method='POST'] - HTTP method for the initial API call
28
+ * @param {Object} [params={}] - Additional parameters to send with each upload.
29
+ * Note: filename, size, type, lastModified are set per-file automatically.
30
+ * @param {Object} [context=null] - Request context (uses default context if not provided)
31
+ * @param {Object} [options={}] - Upload options
32
+ * @param {Function} [options.onProgress] - Progress callback({ fileIndex, fileCount, fileProgress, totalProgress })
33
+ * - fileIndex: Current file index (0-based)
34
+ * - fileCount: Total number of files
35
+ * - fileProgress: Progress of current file (0-1)
36
+ * - totalProgress: Overall progress (0-1)
37
+ * @param {Function} [options.onFileComplete] - Called when each file completes({ fileIndex, fileCount, result })
38
+ * @param {Function} [options.onError] - Error callback(error, context). Can return a Promise
39
+ * that, if resolved, will cause the failed operation to be retried.
40
+ * Context includes { fileIndex, phase, attempt } where phase is 'file' for file-level errors,
41
+ * or 'upload'/'init'/'complete' for block-level errors (also includes blockNum for 'upload').
42
+ * @param {number} [options.concurrency=3] - Maximum concurrent uploads (1-10)
43
+ * @returns {Promise<Array>} - Resolves with array of upload results in same order as input files
44
+ *
45
+ * @example
46
+ * // Upload multiple files from a file input
47
+ * const files = document.querySelector('input[type="file"]').files;
48
+ * const results = await uploadManyFiles('Misc/Debug:testUpload', Array.from(files), 'POST', {
49
+ * image_variation: 'alias=thumb&scale_crop=200x200'
50
+ * }, null, {
51
+ * onProgress: ({ fileIndex, fileCount, totalProgress }) => {
52
+ * console.log(`File ${fileIndex + 1}/${fileCount}, Total: ${Math.round(totalProgress * 100)}%`);
53
+ * },
54
+ * onFileComplete: ({ fileIndex, fileCount, result }) => {
55
+ * console.log(`File ${fileIndex + 1}/${fileCount} complete:`, result);
56
+ * }
57
+ * });
58
+ *
59
+ * @example
60
+ * // Upload buffers with custom concurrency
61
+ * const buffers = [buffer1, buffer2, buffer3, buffer4, buffer5];
62
+ * const results = await uploadManyFiles('Misc/Debug:testUpload', buffers, 'POST', {}, null, {
63
+ * concurrency: 5,
64
+ * onProgress: ({ totalProgress }) => console.log(`${Math.round(totalProgress * 100)}%`)
65
+ * });
66
+ */
67
+ async function uploadManyFiles(api, files, method, params, context, options) {
68
+ // Handle default values
69
+ method = method || 'POST';
70
+ params = params || {};
71
+ options = options || {};
72
+
73
+ const fileCount = files.length;
74
+ if (fileCount === 0) {
75
+ return [];
76
+ }
77
+
78
+ const concurrency = Math.min(Math.max(options.concurrency || 3, 1), 10);
79
+ const { onProgress, onFileComplete, onError } = options;
80
+
81
+ // Results array in same order as input
82
+ const results = new Array(fileCount);
83
+
84
+ // Track progress for each file (0-1)
85
+ const fileProgressArray = new Array(fileCount).fill(0);
86
+
87
+ // Queue of file indices to process
88
+ let nextIndex = 0;
89
+
90
+ // Currently running uploads
91
+ const running = new Set();
92
+
93
+ // Helper to calculate and report progress
94
+ const reportProgress = (fileIndex, fileProgress) => {
95
+ fileProgressArray[fileIndex] = fileProgress;
96
+
97
+ if (onProgress) {
98
+ // Calculate total progress as average of all file progress
99
+ const totalProgress = fileProgressArray.reduce((sum, p) => sum + p, 0) / fileCount;
100
+
101
+ onProgress({
102
+ fileIndex,
103
+ fileCount,
104
+ fileProgress,
105
+ totalProgress
106
+ });
107
+ }
108
+ };
109
+
110
+ // Upload a single file and return its result (with retry support)
111
+ const uploadOne = async (fileIndex) => {
112
+ const file = files[fileIndex];
113
+ let attempt = 0;
114
+
115
+ while (true) {
116
+ attempt++;
117
+
118
+ // Create per-file options with wrapped callbacks
119
+ const fileOptions = {
120
+ onProgress: (progress) => {
121
+ reportProgress(fileIndex, progress);
122
+ }
123
+ };
124
+
125
+ // Wrap onError to include fileIndex for block-level errors
126
+ if (onError) {
127
+ fileOptions.onError = (error, ctx) => {
128
+ return onError(error, { ...ctx, fileIndex });
129
+ };
130
+ }
131
+
132
+ try {
133
+ const result = await uploadFile(api, file, method, { ...params }, context, fileOptions);
134
+
135
+ // Mark as complete
136
+ fileProgressArray[fileIndex] = 1;
137
+ results[fileIndex] = result;
138
+
139
+ if (onFileComplete) {
140
+ onFileComplete({ fileIndex, fileCount, result });
141
+ }
142
+
143
+ return result;
144
+ } catch (error) {
145
+ // Give onError a chance to retry the whole file
146
+ if (onError) {
147
+ try {
148
+ await onError(error, { fileIndex, phase: 'file', attempt });
149
+ // Reset progress for retry
150
+ fileProgressArray[fileIndex] = 0;
151
+ continue; // Retry
152
+ } catch (e) {
153
+ // onError rejected, don't retry
154
+ }
155
+ }
156
+
157
+ // Store error in results and give up
158
+ results[fileIndex] = { error };
159
+ throw error;
160
+ }
161
+ }
162
+ };
163
+
164
+ // Process files with concurrency limit
165
+ const processQueue = async () => {
166
+ const workers = [];
167
+
168
+ for (let i = 0; i < concurrency; i++) {
169
+ workers.push((async () => {
170
+ while (nextIndex < fileCount) {
171
+ const fileIndex = nextIndex++;
172
+ running.add(fileIndex);
173
+
174
+ try {
175
+ await uploadOne(fileIndex);
176
+ } catch (error) {
177
+ // Continue with next file even if one fails
178
+ // Error is already stored in results
179
+ } finally {
180
+ running.delete(fileIndex);
181
+ }
182
+ }
183
+ })());
184
+ }
185
+
186
+ await Promise.all(workers);
187
+ };
188
+
189
+ await processQueue();
190
+
191
+ // Check if any uploads failed
192
+ const errors = results.filter(r => r && r.error).map(r => r.error);
193
+ if (errors.length > 0) {
194
+ const error = new Error(`${errors.length} of ${fileCount} uploads failed`);
195
+ error.errors = errors;
196
+ error.results = results;
197
+ throw error;
198
+ }
199
+
200
+ return results;
201
+ }
202
+
203
+ // Export
204
+ module.exports.uploadManyFiles = uploadManyFiles;