@uploadista/react 0.0.3
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/.turbo/turbo-check.log +89 -0
- package/FLOW_UPLOAD.md +307 -0
- package/LICENSE +21 -0
- package/README.md +318 -0
- package/package.json +35 -0
- package/src/components/flow-upload-list.tsx +614 -0
- package/src/components/flow-upload-zone.tsx +441 -0
- package/src/components/upload-list.tsx +626 -0
- package/src/components/upload-zone.tsx +545 -0
- package/src/components/uploadista-provider.tsx +190 -0
- package/src/hooks/use-drag-drop.ts +404 -0
- package/src/hooks/use-flow-upload.ts +568 -0
- package/src/hooks/use-multi-flow-upload.ts +477 -0
- package/src/hooks/use-multi-upload.ts +691 -0
- package/src/hooks/use-upload-metrics.ts +585 -0
- package/src/hooks/use-upload.ts +411 -0
- package/src/hooks/use-uploadista-client.ts +145 -0
- package/src/index.ts +87 -0
- package/tsconfig.json +11 -0
|
@@ -0,0 +1,691 @@
|
|
|
1
|
+
import type { BrowserUploadInput } from "@uploadista/client-browser";
|
|
2
|
+
import type { UploadFile } from "@uploadista/core/types";
|
|
3
|
+
import { useCallback, useRef, useState } from "react";
|
|
4
|
+
import { useUploadistaContext } from "../components/uploadista-provider";
|
|
5
|
+
import type {
|
|
6
|
+
UploadMetrics,
|
|
7
|
+
UploadState,
|
|
8
|
+
UploadStatus,
|
|
9
|
+
UseUploadOptions,
|
|
10
|
+
} from "./use-upload";
|
|
11
|
+
|
|
12
|
+
export interface UploadItem {
|
|
13
|
+
id: string;
|
|
14
|
+
file: BrowserUploadInput;
|
|
15
|
+
state: UploadState;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface MultiUploadOptions
|
|
19
|
+
extends Omit<UseUploadOptions, "onSuccess" | "onError" | "onProgress"> {
|
|
20
|
+
/**
|
|
21
|
+
* Maximum number of concurrent uploads
|
|
22
|
+
*/
|
|
23
|
+
maxConcurrent?: number;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Called when an individual file upload starts
|
|
27
|
+
*/
|
|
28
|
+
onUploadStart?: (item: UploadItem) => void;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Called when an individual file upload progresses
|
|
32
|
+
*/
|
|
33
|
+
onUploadProgress?: (
|
|
34
|
+
item: UploadItem,
|
|
35
|
+
progress: number,
|
|
36
|
+
bytesUploaded: number,
|
|
37
|
+
totalBytes: number | null,
|
|
38
|
+
) => void;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Called when an individual file upload succeeds
|
|
42
|
+
*/
|
|
43
|
+
onUploadSuccess?: (item: UploadItem, result: UploadFile) => void;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Called when an individual file upload fails
|
|
47
|
+
*/
|
|
48
|
+
onUploadError?: (item: UploadItem, error: Error) => void;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Called when all uploads complete (successfully or with errors)
|
|
52
|
+
*/
|
|
53
|
+
onComplete?: (results: {
|
|
54
|
+
successful: UploadItem[];
|
|
55
|
+
failed: UploadItem[];
|
|
56
|
+
total: number;
|
|
57
|
+
}) => void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface MultiUploadState {
|
|
61
|
+
/**
|
|
62
|
+
* Total number of uploads
|
|
63
|
+
*/
|
|
64
|
+
total: number;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Number of completed uploads (successful + failed)
|
|
68
|
+
*/
|
|
69
|
+
completed: number;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Number of successful uploads
|
|
73
|
+
*/
|
|
74
|
+
successful: number;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Number of failed uploads
|
|
78
|
+
*/
|
|
79
|
+
failed: number;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Number of currently uploading files
|
|
83
|
+
*/
|
|
84
|
+
uploading: number;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Overall progress as a percentage (0-100)
|
|
88
|
+
*/
|
|
89
|
+
progress: number;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Total bytes uploaded across all files
|
|
93
|
+
*/
|
|
94
|
+
totalBytesUploaded: number;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Total bytes to upload across all files
|
|
98
|
+
*/
|
|
99
|
+
totalBytes: number;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Whether any uploads are currently active
|
|
103
|
+
*/
|
|
104
|
+
isUploading: boolean;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Whether all uploads have completed
|
|
108
|
+
*/
|
|
109
|
+
isComplete: boolean;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface UseMultiUploadReturn {
|
|
113
|
+
/**
|
|
114
|
+
* Current multi-upload state
|
|
115
|
+
*/
|
|
116
|
+
state: MultiUploadState;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Array of all upload items
|
|
120
|
+
*/
|
|
121
|
+
items: UploadItem[];
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Add files to the upload queue
|
|
125
|
+
*/
|
|
126
|
+
addFiles: (files: BrowserUploadInput[]) => void;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Remove an item from the queue (only if not currently uploading)
|
|
130
|
+
*/
|
|
131
|
+
removeItem: (id: string) => void;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Remove a file from the queue (alias for removeItem)
|
|
135
|
+
*/
|
|
136
|
+
removeFile: (id: string) => void;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Start all pending uploads
|
|
140
|
+
*/
|
|
141
|
+
startAll: () => void;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Abort a specific upload by ID
|
|
145
|
+
*/
|
|
146
|
+
abortUpload: (id: string) => void;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Abort all active uploads
|
|
150
|
+
*/
|
|
151
|
+
abortAll: () => void;
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Retry a specific failed upload by ID
|
|
155
|
+
*/
|
|
156
|
+
retryUpload: (id: string) => void;
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Retry all failed uploads
|
|
160
|
+
*/
|
|
161
|
+
retryFailed: () => void;
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Clear all completed uploads (successful and failed)
|
|
165
|
+
*/
|
|
166
|
+
clearCompleted: () => void;
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Clear all items
|
|
170
|
+
*/
|
|
171
|
+
clearAll: () => void;
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Get items by status
|
|
175
|
+
*/
|
|
176
|
+
getItemsByStatus: (status: UploadStatus) => UploadItem[];
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Aggregated upload metrics and performance insights from the client
|
|
180
|
+
*/
|
|
181
|
+
metrics: UploadMetrics;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* React hook for managing multiple file uploads with queue management,
|
|
186
|
+
* concurrent upload limits, and batch operations.
|
|
187
|
+
*
|
|
188
|
+
* Must be used within an UploadistaProvider.
|
|
189
|
+
*
|
|
190
|
+
* @param options - Multi-upload configuration and event handlers
|
|
191
|
+
* @returns Multi-upload state and control methods
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
194
|
+
* ```tsx
|
|
195
|
+
* function MyComponent() {
|
|
196
|
+
* const multiUpload = useMultiUpload({
|
|
197
|
+
* maxConcurrent: 3,
|
|
198
|
+
* onUploadSuccess: (item, result) => {
|
|
199
|
+
* console.log(`${item.file.name} uploaded successfully`);
|
|
200
|
+
* },
|
|
201
|
+
* onComplete: (results) => {
|
|
202
|
+
* console.log(`Upload batch complete: ${results.successful.length}/${results.total} successful`);
|
|
203
|
+
* },
|
|
204
|
+
* });
|
|
205
|
+
*
|
|
206
|
+
* return (
|
|
207
|
+
* <div>
|
|
208
|
+
* <input
|
|
209
|
+
* type="file"
|
|
210
|
+
* multiple
|
|
211
|
+
* onChange={(e) => {
|
|
212
|
+
* if (e.target.files) {
|
|
213
|
+
* multiUpload.addFiles(Array.from(e.target.files));
|
|
214
|
+
* }
|
|
215
|
+
* }}
|
|
216
|
+
* />
|
|
217
|
+
*
|
|
218
|
+
* <div>Progress: {multiUpload.state.progress}%</div>
|
|
219
|
+
* <div>
|
|
220
|
+
* {multiUpload.state.uploading} uploading, {multiUpload.state.successful} successful,
|
|
221
|
+
* {multiUpload.state.failed} failed
|
|
222
|
+
* </div>
|
|
223
|
+
*
|
|
224
|
+
* <button onClick={multiUpload.startAll} disabled={multiUpload.state.isUploading}>
|
|
225
|
+
* Start All
|
|
226
|
+
* </button>
|
|
227
|
+
* <button onClick={multiUpload.abortAll} disabled={!multiUpload.state.isUploading}>
|
|
228
|
+
* Abort All
|
|
229
|
+
* </button>
|
|
230
|
+
* <button onClick={multiUpload.retryFailed} disabled={multiUpload.state.failed === 0}>
|
|
231
|
+
* Retry Failed
|
|
232
|
+
* </button>
|
|
233
|
+
*
|
|
234
|
+
* {multiUpload.items.map((item) => (
|
|
235
|
+
* <div key={item.id}>
|
|
236
|
+
* {item.file.name}: {item.state.status} ({item.state.progress}%)
|
|
237
|
+
* </div>
|
|
238
|
+
* ))}
|
|
239
|
+
* </div>
|
|
240
|
+
* );
|
|
241
|
+
* }
|
|
242
|
+
* ```
|
|
243
|
+
*/
|
|
244
|
+
|
|
245
|
+
export function useMultiUpload(
|
|
246
|
+
options: MultiUploadOptions = {},
|
|
247
|
+
): UseMultiUploadReturn {
|
|
248
|
+
const uploadClient = useUploadistaContext();
|
|
249
|
+
const { maxConcurrent = 3 } = options;
|
|
250
|
+
const [items, setItems] = useState<UploadItem[]>([]);
|
|
251
|
+
const itemsRef = useRef<UploadItem[]>([]);
|
|
252
|
+
const nextIdRef = useRef(0);
|
|
253
|
+
const activeUploadsRef = useRef(new Set<string>());
|
|
254
|
+
|
|
255
|
+
// Store abort controllers for each upload
|
|
256
|
+
const abortControllersRef = useRef<Map<string, { abort: () => void }>>(
|
|
257
|
+
new Map(),
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
// Keep ref in sync with state (also updated synchronously in setItems callbacks)
|
|
261
|
+
itemsRef.current = items;
|
|
262
|
+
|
|
263
|
+
// Generate a unique ID for each upload item
|
|
264
|
+
const generateId = useCallback(() => {
|
|
265
|
+
return `upload-${Date.now()}-${nextIdRef.current++}`;
|
|
266
|
+
}, []);
|
|
267
|
+
|
|
268
|
+
// State update callback for individual uploads
|
|
269
|
+
const onStateUpdate = useCallback(
|
|
270
|
+
(id: string, state: Partial<UploadState>) => {
|
|
271
|
+
setItems((prev) => {
|
|
272
|
+
const updated = prev.map((item) =>
|
|
273
|
+
item.id === id
|
|
274
|
+
? { ...item, state: { ...item.state, ...state } }
|
|
275
|
+
: item,
|
|
276
|
+
);
|
|
277
|
+
itemsRef.current = updated;
|
|
278
|
+
return updated;
|
|
279
|
+
});
|
|
280
|
+
},
|
|
281
|
+
[],
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
// Check if all uploads are complete and trigger completion callback
|
|
285
|
+
const checkForCompletion = useCallback(() => {
|
|
286
|
+
const currentItems = itemsRef.current;
|
|
287
|
+
const allComplete = currentItems.every((item) =>
|
|
288
|
+
["success", "error", "aborted"].includes(item.state.status),
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
if (allComplete && currentItems.length > 0) {
|
|
292
|
+
const successful = currentItems.filter(
|
|
293
|
+
(item) => item.state.status === "success",
|
|
294
|
+
);
|
|
295
|
+
const failed = currentItems.filter((item) =>
|
|
296
|
+
["error", "aborted"].includes(item.state.status),
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
options.onComplete?.({
|
|
300
|
+
successful,
|
|
301
|
+
failed,
|
|
302
|
+
total: currentItems.length,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}, [options]);
|
|
306
|
+
|
|
307
|
+
// Start the next available upload if we have capacity
|
|
308
|
+
const startNextUpload = useCallback(() => {
|
|
309
|
+
if (activeUploadsRef.current.size >= maxConcurrent) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const currentItems = itemsRef.current;
|
|
314
|
+
const nextItem = currentItems.find(
|
|
315
|
+
(item) =>
|
|
316
|
+
item.state.status === "idle" && !activeUploadsRef.current.has(item.id),
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
if (!nextItem) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Perform upload inline to avoid circular dependency
|
|
324
|
+
const performUploadInline = async () => {
|
|
325
|
+
activeUploadsRef.current.add(nextItem.id);
|
|
326
|
+
options.onUploadStart?.(nextItem);
|
|
327
|
+
|
|
328
|
+
// Update state to uploading
|
|
329
|
+
onStateUpdate(nextItem.id, { status: "uploading" });
|
|
330
|
+
|
|
331
|
+
try {
|
|
332
|
+
const controller = await uploadClient.client.upload(nextItem.file, {
|
|
333
|
+
metadata: options.metadata,
|
|
334
|
+
uploadLengthDeferred: options.uploadLengthDeferred,
|
|
335
|
+
uploadSize: options.uploadSize,
|
|
336
|
+
|
|
337
|
+
onProgress: (
|
|
338
|
+
_uploadId: string,
|
|
339
|
+
bytesUploaded: number,
|
|
340
|
+
totalBytes: number | null,
|
|
341
|
+
) => {
|
|
342
|
+
const progress = totalBytes
|
|
343
|
+
? Math.round((bytesUploaded / totalBytes) * 100)
|
|
344
|
+
: 0;
|
|
345
|
+
|
|
346
|
+
onStateUpdate(nextItem.id, {
|
|
347
|
+
progress,
|
|
348
|
+
bytesUploaded,
|
|
349
|
+
totalBytes,
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
options.onUploadProgress?.(
|
|
353
|
+
nextItem,
|
|
354
|
+
progress,
|
|
355
|
+
bytesUploaded,
|
|
356
|
+
totalBytes,
|
|
357
|
+
);
|
|
358
|
+
},
|
|
359
|
+
|
|
360
|
+
onChunkComplete: () => {
|
|
361
|
+
// Optional: could expose this as an option
|
|
362
|
+
},
|
|
363
|
+
|
|
364
|
+
onSuccess: (result: UploadFile) => {
|
|
365
|
+
onStateUpdate(nextItem.id, {
|
|
366
|
+
status: "success",
|
|
367
|
+
result,
|
|
368
|
+
progress: 100,
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
const updatedItem = {
|
|
372
|
+
...nextItem,
|
|
373
|
+
state: { ...nextItem.state, status: "success" as const, result },
|
|
374
|
+
};
|
|
375
|
+
options.onUploadSuccess?.(updatedItem, result);
|
|
376
|
+
|
|
377
|
+
// Mark complete and start next
|
|
378
|
+
activeUploadsRef.current.delete(nextItem.id);
|
|
379
|
+
abortControllersRef.current.delete(nextItem.id);
|
|
380
|
+
startNextUpload();
|
|
381
|
+
checkForCompletion();
|
|
382
|
+
},
|
|
383
|
+
|
|
384
|
+
onError: (error: Error) => {
|
|
385
|
+
onStateUpdate(nextItem.id, {
|
|
386
|
+
status: "error",
|
|
387
|
+
error,
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
const updatedItem = {
|
|
391
|
+
...nextItem,
|
|
392
|
+
state: { ...nextItem.state, status: "error" as const, error },
|
|
393
|
+
};
|
|
394
|
+
options.onUploadError?.(updatedItem, error);
|
|
395
|
+
|
|
396
|
+
// Mark complete and start next
|
|
397
|
+
activeUploadsRef.current.delete(nextItem.id);
|
|
398
|
+
abortControllersRef.current.delete(nextItem.id);
|
|
399
|
+
startNextUpload();
|
|
400
|
+
checkForCompletion();
|
|
401
|
+
},
|
|
402
|
+
|
|
403
|
+
onShouldRetry: options.onShouldRetry,
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// Store abort controller
|
|
407
|
+
abortControllersRef.current.set(nextItem.id, controller);
|
|
408
|
+
} catch (error) {
|
|
409
|
+
onStateUpdate(nextItem.id, {
|
|
410
|
+
status: "error",
|
|
411
|
+
error: error as Error,
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
const updatedItem = {
|
|
415
|
+
...nextItem,
|
|
416
|
+
state: {
|
|
417
|
+
...nextItem.state,
|
|
418
|
+
status: "error" as const,
|
|
419
|
+
error: error as Error,
|
|
420
|
+
},
|
|
421
|
+
};
|
|
422
|
+
options.onUploadError?.(updatedItem, error as Error);
|
|
423
|
+
|
|
424
|
+
// Mark complete and start next
|
|
425
|
+
activeUploadsRef.current.delete(nextItem.id);
|
|
426
|
+
abortControllersRef.current.delete(nextItem.id);
|
|
427
|
+
startNextUpload();
|
|
428
|
+
checkForCompletion();
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
performUploadInline();
|
|
433
|
+
}, [maxConcurrent, uploadClient, options, onStateUpdate, checkForCompletion]);
|
|
434
|
+
|
|
435
|
+
// Calculate overall state
|
|
436
|
+
const state: MultiUploadState = {
|
|
437
|
+
total: items.length,
|
|
438
|
+
completed: items.filter((item) =>
|
|
439
|
+
["success", "error", "aborted"].includes(item.state.status),
|
|
440
|
+
).length,
|
|
441
|
+
successful: items.filter((item) => item.state.status === "success").length,
|
|
442
|
+
failed: items.filter((item) =>
|
|
443
|
+
["error", "aborted"].includes(item.state.status),
|
|
444
|
+
).length,
|
|
445
|
+
uploading: items.filter((item) => item.state.status === "uploading").length,
|
|
446
|
+
progress:
|
|
447
|
+
items.length > 0
|
|
448
|
+
? Math.round(
|
|
449
|
+
items.reduce((sum, item) => sum + item.state.progress, 0) /
|
|
450
|
+
items.length,
|
|
451
|
+
)
|
|
452
|
+
: 0,
|
|
453
|
+
totalBytesUploaded: items.reduce(
|
|
454
|
+
(sum, item) => sum + item.state.bytesUploaded,
|
|
455
|
+
0,
|
|
456
|
+
),
|
|
457
|
+
totalBytes: items.reduce(
|
|
458
|
+
(sum, item) => sum + (item.state.totalBytes || 0),
|
|
459
|
+
0,
|
|
460
|
+
),
|
|
461
|
+
isUploading: items.some((item) => item.state.status === "uploading"),
|
|
462
|
+
isComplete:
|
|
463
|
+
items.length > 0 &&
|
|
464
|
+
items.every((item) =>
|
|
465
|
+
["success", "error", "aborted"].includes(item.state.status),
|
|
466
|
+
),
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
const addFiles = useCallback(
|
|
470
|
+
(files: BrowserUploadInput[]) => {
|
|
471
|
+
const newItems: UploadItem[] = files.map((file) => {
|
|
472
|
+
const id = generateId();
|
|
473
|
+
return {
|
|
474
|
+
id,
|
|
475
|
+
file,
|
|
476
|
+
state: {
|
|
477
|
+
status: "idle",
|
|
478
|
+
progress: 0,
|
|
479
|
+
bytesUploaded: 0,
|
|
480
|
+
totalBytes: file instanceof File ? file.size : null,
|
|
481
|
+
error: null,
|
|
482
|
+
result: null,
|
|
483
|
+
},
|
|
484
|
+
};
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
console.log("addFiles: Adding", newItems.length, "files");
|
|
488
|
+
|
|
489
|
+
// Update ref synchronously BEFORE setItems
|
|
490
|
+
const updated = [...itemsRef.current, ...newItems];
|
|
491
|
+
itemsRef.current = updated;
|
|
492
|
+
console.log(
|
|
493
|
+
"addFiles: Updated itemsRef.current to",
|
|
494
|
+
updated.length,
|
|
495
|
+
"items",
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
setItems(updated);
|
|
499
|
+
},
|
|
500
|
+
[generateId],
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
const removeItem = useCallback((id: string) => {
|
|
504
|
+
const currentItems = itemsRef.current;
|
|
505
|
+
const item = currentItems.find((i) => i.id === id);
|
|
506
|
+
if (item && item.state.status === "uploading") {
|
|
507
|
+
// Abort before removing
|
|
508
|
+
const controller = abortControllersRef.current.get(id);
|
|
509
|
+
if (controller) {
|
|
510
|
+
controller.abort();
|
|
511
|
+
abortControllersRef.current.delete(id);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
setItems((prev) => {
|
|
516
|
+
const updated = prev.filter((item) => item.id !== id);
|
|
517
|
+
itemsRef.current = updated;
|
|
518
|
+
return updated;
|
|
519
|
+
});
|
|
520
|
+
activeUploadsRef.current.delete(id);
|
|
521
|
+
}, []);
|
|
522
|
+
|
|
523
|
+
const abortUpload = useCallback(
|
|
524
|
+
(id: string) => {
|
|
525
|
+
const currentItems = itemsRef.current;
|
|
526
|
+
const item = currentItems.find((i) => i.id === id);
|
|
527
|
+
if (item && item.state.status === "uploading") {
|
|
528
|
+
const controller = abortControllersRef.current.get(id);
|
|
529
|
+
if (controller) {
|
|
530
|
+
controller.abort();
|
|
531
|
+
abortControllersRef.current.delete(id);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
activeUploadsRef.current.delete(id);
|
|
535
|
+
|
|
536
|
+
setItems((prev) => {
|
|
537
|
+
const updated = prev.map((i) =>
|
|
538
|
+
i.id === id
|
|
539
|
+
? { ...i, state: { ...i.state, status: "aborted" as const } }
|
|
540
|
+
: i,
|
|
541
|
+
);
|
|
542
|
+
itemsRef.current = updated;
|
|
543
|
+
return updated;
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
// Try to start next upload in queue
|
|
547
|
+
startNextUpload();
|
|
548
|
+
}
|
|
549
|
+
},
|
|
550
|
+
[startNextUpload],
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
const retryUpload = useCallback(
|
|
554
|
+
(id: string) => {
|
|
555
|
+
const currentItems = itemsRef.current;
|
|
556
|
+
const item = currentItems.find((i) => i.id === id);
|
|
557
|
+
if (item && ["error", "aborted"].includes(item.state.status)) {
|
|
558
|
+
setItems((prev) => {
|
|
559
|
+
const updated = prev.map((i) =>
|
|
560
|
+
i.id === id
|
|
561
|
+
? {
|
|
562
|
+
...i,
|
|
563
|
+
state: { ...i.state, status: "idle" as const, error: null },
|
|
564
|
+
}
|
|
565
|
+
: i,
|
|
566
|
+
);
|
|
567
|
+
itemsRef.current = updated;
|
|
568
|
+
return updated;
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
// Auto-start the upload
|
|
572
|
+
setTimeout(() => startNextUpload(), 0);
|
|
573
|
+
}
|
|
574
|
+
},
|
|
575
|
+
[startNextUpload],
|
|
576
|
+
);
|
|
577
|
+
|
|
578
|
+
const startAll = useCallback(() => {
|
|
579
|
+
const currentItems = itemsRef.current;
|
|
580
|
+
console.log("Starting all uploads", currentItems);
|
|
581
|
+
// Start as many uploads as we can up to the concurrent limit
|
|
582
|
+
const idleItems = currentItems.filter(
|
|
583
|
+
(item) => item.state.status === "idle",
|
|
584
|
+
);
|
|
585
|
+
const slotsAvailable = maxConcurrent - activeUploadsRef.current.size;
|
|
586
|
+
const itemsToStart = idleItems.slice(0, slotsAvailable);
|
|
587
|
+
|
|
588
|
+
for (const item of itemsToStart) {
|
|
589
|
+
console.log("Starting next upload", item);
|
|
590
|
+
startNextUpload();
|
|
591
|
+
}
|
|
592
|
+
}, [maxConcurrent, startNextUpload]);
|
|
593
|
+
|
|
594
|
+
const abortAll = useCallback(() => {
|
|
595
|
+
const currentItems = itemsRef.current;
|
|
596
|
+
currentItems
|
|
597
|
+
.filter((item) => item.state.status === "uploading")
|
|
598
|
+
.forEach((item) => {
|
|
599
|
+
const controller = abortControllersRef.current.get(item.id);
|
|
600
|
+
if (controller) {
|
|
601
|
+
controller.abort();
|
|
602
|
+
abortControllersRef.current.delete(item.id);
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
activeUploadsRef.current.clear();
|
|
607
|
+
|
|
608
|
+
// Update all uploading items to aborted status
|
|
609
|
+
setItems((prev) => {
|
|
610
|
+
const updated = prev.map((item) =>
|
|
611
|
+
item.state.status === "uploading"
|
|
612
|
+
? { ...item, state: { ...item.state, status: "aborted" as const } }
|
|
613
|
+
: item,
|
|
614
|
+
);
|
|
615
|
+
itemsRef.current = updated;
|
|
616
|
+
return updated;
|
|
617
|
+
});
|
|
618
|
+
}, []);
|
|
619
|
+
|
|
620
|
+
const retryFailed = useCallback(() => {
|
|
621
|
+
const currentItems = itemsRef.current;
|
|
622
|
+
const failedItems = currentItems.filter((item) =>
|
|
623
|
+
["error", "aborted"].includes(item.state.status),
|
|
624
|
+
);
|
|
625
|
+
|
|
626
|
+
if (failedItems.length > 0) {
|
|
627
|
+
setItems((prev) => {
|
|
628
|
+
const updated = prev.map((item) =>
|
|
629
|
+
failedItems.some((f) => f.id === item.id)
|
|
630
|
+
? {
|
|
631
|
+
...item,
|
|
632
|
+
state: { ...item.state, status: "idle" as const, error: null },
|
|
633
|
+
}
|
|
634
|
+
: item,
|
|
635
|
+
);
|
|
636
|
+
itemsRef.current = updated;
|
|
637
|
+
return updated;
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
// Auto-start uploads if we have capacity
|
|
641
|
+
setTimeout(startAll, 0);
|
|
642
|
+
}
|
|
643
|
+
}, [startAll]);
|
|
644
|
+
|
|
645
|
+
const clearCompleted = useCallback(() => {
|
|
646
|
+
setItems((prev) => {
|
|
647
|
+
const updated = prev.filter(
|
|
648
|
+
(item) => !["success", "error", "aborted"].includes(item.state.status),
|
|
649
|
+
);
|
|
650
|
+
itemsRef.current = updated;
|
|
651
|
+
return updated;
|
|
652
|
+
});
|
|
653
|
+
}, []);
|
|
654
|
+
|
|
655
|
+
const clearAll = useCallback(() => {
|
|
656
|
+
abortAll();
|
|
657
|
+
setItems([]);
|
|
658
|
+
itemsRef.current = [];
|
|
659
|
+
activeUploadsRef.current.clear();
|
|
660
|
+
}, [abortAll]);
|
|
661
|
+
|
|
662
|
+
const getItemsByStatus = useCallback((status: UploadStatus) => {
|
|
663
|
+
return itemsRef.current.filter((item) => item.state.status === status);
|
|
664
|
+
}, []);
|
|
665
|
+
|
|
666
|
+
// Create aggregated metrics object that delegates to the upload client
|
|
667
|
+
const metrics: UploadMetrics = {
|
|
668
|
+
getInsights: () => uploadClient.client.getChunkingInsights(),
|
|
669
|
+
exportMetrics: () => uploadClient.client.exportMetrics(),
|
|
670
|
+
getNetworkMetrics: () => uploadClient.client.getNetworkMetrics(),
|
|
671
|
+
getNetworkCondition: () => uploadClient.client.getNetworkCondition(),
|
|
672
|
+
resetMetrics: () => uploadClient.client.resetMetrics(),
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
return {
|
|
676
|
+
state,
|
|
677
|
+
items,
|
|
678
|
+
addFiles,
|
|
679
|
+
removeItem,
|
|
680
|
+
removeFile: removeItem, // Alias for consistency with MultiUploadExample
|
|
681
|
+
startAll,
|
|
682
|
+
abortUpload,
|
|
683
|
+
abortAll,
|
|
684
|
+
retryUpload,
|
|
685
|
+
retryFailed,
|
|
686
|
+
clearCompleted,
|
|
687
|
+
clearAll,
|
|
688
|
+
getItemsByStatus,
|
|
689
|
+
metrics,
|
|
690
|
+
};
|
|
691
|
+
}
|