@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.
@@ -0,0 +1,585 @@
1
+ import type {
2
+ ChunkMetrics,
3
+ PerformanceInsights,
4
+ UploadSessionMetrics,
5
+ } from "@uploadista/client-core";
6
+ import React, { useCallback, useRef, useState } from "react";
7
+ import { useUploadistaContext } from "../components/uploadista-provider";
8
+
9
+ export type Timeout = ReturnType<typeof setInterval>;
10
+
11
+ export interface UploadMetrics {
12
+ /**
13
+ * Total bytes uploaded across all files
14
+ */
15
+ totalBytesUploaded: number;
16
+
17
+ /**
18
+ * Total bytes to upload across all files
19
+ */
20
+ totalBytes: number;
21
+
22
+ /**
23
+ * Overall upload speed in bytes per second
24
+ */
25
+ averageSpeed: number;
26
+
27
+ /**
28
+ * Current upload speed in bytes per second
29
+ */
30
+ currentSpeed: number;
31
+
32
+ /**
33
+ * Estimated time remaining in milliseconds
34
+ */
35
+ estimatedTimeRemaining: number | null;
36
+
37
+ /**
38
+ * Total number of files being tracked
39
+ */
40
+ totalFiles: number;
41
+
42
+ /**
43
+ * Number of files completed
44
+ */
45
+ completedFiles: number;
46
+
47
+ /**
48
+ * Number of files currently uploading
49
+ */
50
+ activeUploads: number;
51
+
52
+ /**
53
+ * Overall progress as percentage (0-100)
54
+ */
55
+ progress: number;
56
+
57
+ /**
58
+ * Peak upload speed achieved
59
+ */
60
+ peakSpeed: number;
61
+
62
+ /**
63
+ * Start time of the first upload
64
+ */
65
+ startTime: number | null;
66
+
67
+ /**
68
+ * End time of the last completed upload
69
+ */
70
+ endTime: number | null;
71
+
72
+ /**
73
+ * Total duration of all uploads
74
+ */
75
+ totalDuration: number | null;
76
+
77
+ /**
78
+ * Detailed performance insights from the upload client
79
+ */
80
+ insights: PerformanceInsights;
81
+
82
+ /**
83
+ * Session metrics for completed uploads
84
+ */
85
+ sessionMetrics: Partial<UploadSessionMetrics>[];
86
+
87
+ /**
88
+ * Detailed chunk metrics from recent uploads
89
+ */
90
+ chunkMetrics: ChunkMetrics[];
91
+ }
92
+
93
+ export interface FileUploadMetrics {
94
+ id: string;
95
+ filename: string;
96
+ size: number;
97
+ bytesUploaded: number;
98
+ progress: number;
99
+ speed: number;
100
+ startTime: number;
101
+ endTime: number | null;
102
+ duration: number | null;
103
+ isComplete: boolean;
104
+ }
105
+
106
+ export interface UseUploadMetricsOptions {
107
+ /**
108
+ * Interval for calculating current speed (in milliseconds)
109
+ */
110
+ speedCalculationInterval?: number;
111
+
112
+ /**
113
+ * Number of speed samples to keep for average calculation
114
+ */
115
+ speedSampleSize?: number;
116
+
117
+ /**
118
+ * Called when metrics are updated
119
+ */
120
+ onMetricsUpdate?: (metrics: UploadMetrics) => void;
121
+
122
+ /**
123
+ * Called when a file upload starts
124
+ */
125
+ onFileStart?: (fileMetrics: FileUploadMetrics) => void;
126
+
127
+ /**
128
+ * Called when a file upload progresses
129
+ */
130
+ onFileProgress?: (fileMetrics: FileUploadMetrics) => void;
131
+
132
+ /**
133
+ * Called when a file upload completes
134
+ */
135
+ onFileComplete?: (fileMetrics: FileUploadMetrics) => void;
136
+ }
137
+
138
+ export interface UseUploadMetricsReturn {
139
+ /**
140
+ * Current overall metrics
141
+ */
142
+ metrics: UploadMetrics;
143
+
144
+ /**
145
+ * Individual file metrics
146
+ */
147
+ fileMetrics: FileUploadMetrics[];
148
+
149
+ /**
150
+ * Start tracking a new file upload
151
+ */
152
+ startFileUpload: (id: string, filename: string, size: number) => void;
153
+
154
+ /**
155
+ * Update progress for a file upload
156
+ */
157
+ updateFileProgress: (id: string, bytesUploaded: number) => void;
158
+
159
+ /**
160
+ * Mark a file upload as complete
161
+ */
162
+ completeFileUpload: (id: string) => void;
163
+
164
+ /**
165
+ * Remove a file from tracking
166
+ */
167
+ removeFile: (id: string) => void;
168
+
169
+ /**
170
+ * Reset all metrics
171
+ */
172
+ reset: () => void;
173
+
174
+ /**
175
+ * Get metrics for a specific file
176
+ */
177
+ getFileMetrics: (id: string) => FileUploadMetrics | undefined;
178
+
179
+ /**
180
+ * Export metrics as JSON
181
+ */
182
+ exportMetrics: () => {
183
+ overall: UploadMetrics;
184
+ files: FileUploadMetrics[];
185
+ exportTime: number;
186
+ };
187
+ }
188
+
189
+ const initialMetrics: UploadMetrics = {
190
+ totalBytesUploaded: 0,
191
+ totalBytes: 0,
192
+ averageSpeed: 0,
193
+ currentSpeed: 0,
194
+ estimatedTimeRemaining: null,
195
+ totalFiles: 0,
196
+ completedFiles: 0,
197
+ activeUploads: 0,
198
+ progress: 0,
199
+ peakSpeed: 0,
200
+ startTime: null,
201
+ endTime: null,
202
+ totalDuration: null,
203
+ insights: {
204
+ overallEfficiency: 0,
205
+ chunkingEffectiveness: 0,
206
+ networkStability: 0,
207
+ recommendations: [],
208
+ optimalChunkSizeRange: { min: 256 * 1024, max: 2 * 1024 * 1024 },
209
+ },
210
+ sessionMetrics: [],
211
+ chunkMetrics: [],
212
+ };
213
+
214
+ /**
215
+ * React hook for tracking detailed upload metrics and performance statistics.
216
+ * Provides comprehensive monitoring of upload progress, speed, and timing data.
217
+ *
218
+ * @param options - Configuration and event handlers
219
+ * @returns Upload metrics state and control methods
220
+ *
221
+ * @example
222
+ * ```tsx
223
+ * const uploadMetrics = useUploadMetrics({
224
+ * speedCalculationInterval: 1000, // Update speed every second
225
+ * speedSampleSize: 10, // Keep last 10 speed samples for average
226
+ * onMetricsUpdate: (metrics) => {
227
+ * console.log(`Overall progress: ${metrics.progress}%`);
228
+ * console.log(`Speed: ${(metrics.currentSpeed / 1024).toFixed(1)} KB/s`);
229
+ * console.log(`ETA: ${metrics.estimatedTimeRemaining}ms`);
230
+ * },
231
+ * onFileComplete: (fileMetrics) => {
232
+ * console.log(`${fileMetrics.filename} completed in ${fileMetrics.duration}ms`);
233
+ * },
234
+ * });
235
+ *
236
+ * // Start tracking a file
237
+ * const handleFileStart = (file: File) => {
238
+ * uploadMetrics.startFileUpload(file.name, file.name, file.size);
239
+ * };
240
+ *
241
+ * // Update progress during upload
242
+ * const handleProgress = (fileId: string, bytesUploaded: number) => {
243
+ * uploadMetrics.updateFileProgress(fileId, bytesUploaded);
244
+ * };
245
+ *
246
+ * // Display metrics
247
+ * return (
248
+ * <div>
249
+ * <div>Overall Progress: {uploadMetrics.metrics.progress}%</div>
250
+ * <div>Speed: {(uploadMetrics.metrics.currentSpeed / 1024).toFixed(1)} KB/s</div>
251
+ * <div>Files: {uploadMetrics.metrics.completedFiles}/{uploadMetrics.metrics.totalFiles}</div>
252
+ *
253
+ * {uploadMetrics.metrics.estimatedTimeRemaining && (
254
+ * <div>ETA: {Math.round(uploadMetrics.metrics.estimatedTimeRemaining / 1000)}s</div>
255
+ * )}
256
+ *
257
+ * {uploadMetrics.fileMetrics.map((file) => (
258
+ * <div key={file.id}>
259
+ * {file.filename}: {file.progress}% ({(file.speed / 1024).toFixed(1)} KB/s)
260
+ * </div>
261
+ * ))}
262
+ * </div>
263
+ * );
264
+ * ```
265
+ */
266
+ export function useUploadMetrics(
267
+ options: UseUploadMetricsOptions = {},
268
+ ): UseUploadMetricsReturn {
269
+ const {
270
+ speedCalculationInterval = 1000,
271
+ speedSampleSize = 10,
272
+ onMetricsUpdate,
273
+ onFileStart,
274
+ onFileProgress,
275
+ onFileComplete,
276
+ } = options;
277
+
278
+ const uploadClient = useUploadistaContext();
279
+
280
+ const [metrics, setMetrics] = useState<UploadMetrics>(initialMetrics);
281
+ const [fileMetrics, setFileMetrics] = useState<FileUploadMetrics[]>([]);
282
+
283
+ const speedSamplesRef = useRef<Array<{ time: number; bytes: number }>>([]);
284
+ const lastUpdateRef = useRef<number>(0);
285
+ const intervalRef = useRef<Timeout | null>(null);
286
+
287
+ const calculateSpeed = useCallback(
288
+ (currentTime: number, totalBytesUploaded: number) => {
289
+ const sample = { time: currentTime, bytes: totalBytesUploaded };
290
+ speedSamplesRef.current.push(sample);
291
+
292
+ // Keep only recent samples
293
+ if (speedSamplesRef.current.length > speedSampleSize) {
294
+ speedSamplesRef.current = speedSamplesRef.current.slice(
295
+ -speedSampleSize,
296
+ );
297
+ }
298
+
299
+ // Calculate current speed (bytes per second)
300
+ let currentSpeed = 0;
301
+ if (speedSamplesRef.current.length >= 2) {
302
+ const recent =
303
+ speedSamplesRef.current[speedSamplesRef.current.length - 1];
304
+ const previous =
305
+ speedSamplesRef.current[speedSamplesRef.current.length - 2];
306
+ if (recent && previous) {
307
+ const timeDiff = (recent.time - previous.time) / 1000; // Convert to seconds
308
+ const bytesDiff = recent.bytes - previous.bytes;
309
+ currentSpeed = timeDiff > 0 ? bytesDiff / timeDiff : 0;
310
+ }
311
+ }
312
+
313
+ // Calculate average speed
314
+ let averageSpeed = 0;
315
+ if (speedSamplesRef.current.length >= 2) {
316
+ const first = speedSamplesRef.current[0];
317
+ const last =
318
+ speedSamplesRef.current[speedSamplesRef.current.length - 1];
319
+ if (first && last) {
320
+ const totalTime = (last.time - first.time) / 1000; // Convert to seconds
321
+ const totalBytes = last.bytes - first.bytes;
322
+ averageSpeed = totalTime > 0 ? totalBytes / totalTime : 0;
323
+ }
324
+ }
325
+
326
+ return { currentSpeed, averageSpeed };
327
+ },
328
+ [speedSampleSize],
329
+ );
330
+
331
+ const updateMetrics = useCallback(() => {
332
+ const now = Date.now();
333
+
334
+ // Calculate totals from file metrics
335
+ const totalBytes = fileMetrics.reduce((sum, file) => sum + file.size, 0);
336
+ const totalBytesUploaded = fileMetrics.reduce(
337
+ (sum, file) => sum + file.bytesUploaded,
338
+ 0,
339
+ );
340
+ const completedFiles = fileMetrics.filter((file) => file.isComplete).length;
341
+ const activeUploads = fileMetrics.filter(
342
+ (file) => !file.isComplete && file.bytesUploaded > 0,
343
+ ).length;
344
+
345
+ // Calculate speeds
346
+ const { currentSpeed, averageSpeed } = calculateSpeed(
347
+ now,
348
+ totalBytesUploaded,
349
+ );
350
+
351
+ // Calculate progress
352
+ const progress =
353
+ totalBytes > 0 ? Math.round((totalBytesUploaded / totalBytes) * 100) : 0;
354
+
355
+ // Calculate estimated time remaining
356
+ let estimatedTimeRemaining: number | null = null;
357
+ if (currentSpeed > 0) {
358
+ const remainingBytes = totalBytes - totalBytesUploaded;
359
+ estimatedTimeRemaining = (remainingBytes / currentSpeed) * 1000; // Convert to milliseconds
360
+ }
361
+
362
+ // Find start and end times
363
+ const activeTimes = fileMetrics.filter((file) => file.startTime > 0);
364
+ const startTime =
365
+ activeTimes.length > 0
366
+ ? Math.min(...activeTimes.map((file) => file.startTime))
367
+ : null;
368
+
369
+ const completedTimes = fileMetrics.filter((file) => file.endTime !== null);
370
+ const endTime =
371
+ completedTimes.length > 0 && completedFiles === fileMetrics.length
372
+ ? Math.max(
373
+ ...completedTimes
374
+ .map((file) => file.endTime)
375
+ .filter((time) => time !== null),
376
+ )
377
+ : null;
378
+
379
+ const totalDuration = startTime && endTime ? endTime - startTime : null;
380
+
381
+ const newMetrics: UploadMetrics = {
382
+ totalBytesUploaded,
383
+ totalBytes,
384
+ averageSpeed,
385
+ currentSpeed,
386
+ estimatedTimeRemaining,
387
+ totalFiles: fileMetrics.length,
388
+ completedFiles,
389
+ activeUploads,
390
+ progress,
391
+ peakSpeed: Math.max(metrics.peakSpeed, currentSpeed),
392
+ startTime,
393
+ endTime,
394
+ totalDuration,
395
+ insights: uploadClient.client.getChunkingInsights(),
396
+ sessionMetrics: [uploadClient.client.exportMetrics().session],
397
+ chunkMetrics: uploadClient.client.exportMetrics().chunks,
398
+ };
399
+
400
+ setMetrics(newMetrics);
401
+ onMetricsUpdate?.(newMetrics);
402
+ }, [
403
+ fileMetrics,
404
+ metrics.peakSpeed,
405
+ calculateSpeed,
406
+ onMetricsUpdate,
407
+ uploadClient.client,
408
+ ]);
409
+
410
+ // Set up periodic speed calculations
411
+ const setupSpeedCalculation = useCallback(() => {
412
+ if (intervalRef.current) {
413
+ clearInterval(intervalRef.current);
414
+ }
415
+
416
+ intervalRef.current = setInterval(() => {
417
+ if (
418
+ fileMetrics.some((file) => !file.isComplete && file.bytesUploaded > 0)
419
+ ) {
420
+ updateMetrics();
421
+ }
422
+ }, speedCalculationInterval);
423
+
424
+ return () => {
425
+ if (intervalRef.current) {
426
+ clearInterval(intervalRef.current);
427
+ intervalRef.current = null;
428
+ }
429
+ };
430
+ }, [speedCalculationInterval, updateMetrics, fileMetrics]);
431
+
432
+ const startFileUpload = useCallback(
433
+ (id: string, filename: string, size: number) => {
434
+ const now = Date.now();
435
+
436
+ const fileMetric: FileUploadMetrics = {
437
+ id,
438
+ filename,
439
+ size,
440
+ bytesUploaded: 0,
441
+ progress: 0,
442
+ speed: 0,
443
+ startTime: now,
444
+ endTime: null,
445
+ duration: null,
446
+ isComplete: false,
447
+ };
448
+
449
+ setFileMetrics((prev) => {
450
+ const existing = prev.find((file) => file.id === id);
451
+ if (existing) {
452
+ return prev.map((file) => (file.id === id ? fileMetric : file));
453
+ }
454
+ return [...prev, fileMetric];
455
+ });
456
+
457
+ onFileStart?.(fileMetric);
458
+
459
+ // Start speed calculation if this is the first active upload
460
+ if (fileMetrics.filter((file) => !file.isComplete).length === 0) {
461
+ setupSpeedCalculation();
462
+ }
463
+ },
464
+ [fileMetrics, onFileStart, setupSpeedCalculation],
465
+ );
466
+
467
+ const updateFileProgress = useCallback(
468
+ (id: string, bytesUploaded: number) => {
469
+ const now = Date.now();
470
+
471
+ setFileMetrics((prev) =>
472
+ prev.map((file) => {
473
+ if (file.id !== id) return file;
474
+
475
+ const timeDiff = (now - file.startTime) / 1000; // seconds
476
+ const speed = timeDiff > 0 ? bytesUploaded / timeDiff : 0;
477
+ const progress =
478
+ file.size > 0 ? Math.round((bytesUploaded / file.size) * 100) : 0;
479
+
480
+ const updatedFile = {
481
+ ...file,
482
+ bytesUploaded,
483
+ progress,
484
+ speed,
485
+ };
486
+
487
+ onFileProgress?.(updatedFile);
488
+ return updatedFile;
489
+ }),
490
+ );
491
+
492
+ // Trigger metrics update
493
+ setTimeout(updateMetrics, 0);
494
+ },
495
+ [onFileProgress, updateMetrics],
496
+ );
497
+
498
+ const completeFileUpload = useCallback(
499
+ (id: string) => {
500
+ const now = Date.now();
501
+
502
+ setFileMetrics((prev) =>
503
+ prev.map((file) => {
504
+ if (file.id !== id) return file;
505
+
506
+ const duration = now - file.startTime;
507
+ const speed = duration > 0 ? (file.size / duration) * 1000 : 0; // bytes per second
508
+
509
+ const completedFile = {
510
+ ...file,
511
+ bytesUploaded: file.size,
512
+ progress: 100,
513
+ speed,
514
+ endTime: now,
515
+ duration,
516
+ isComplete: true,
517
+ };
518
+
519
+ onFileComplete?.(completedFile);
520
+ return completedFile;
521
+ }),
522
+ );
523
+
524
+ // Trigger metrics update
525
+ setTimeout(updateMetrics, 0);
526
+ },
527
+ [onFileComplete, updateMetrics],
528
+ );
529
+
530
+ const removeFile = useCallback(
531
+ (id: string) => {
532
+ setFileMetrics((prev) => prev.filter((file) => file.id !== id));
533
+ setTimeout(updateMetrics, 0);
534
+ },
535
+ [updateMetrics],
536
+ );
537
+
538
+ const reset = useCallback(() => {
539
+ if (intervalRef.current) {
540
+ clearInterval(intervalRef.current);
541
+ intervalRef.current = null;
542
+ }
543
+
544
+ setMetrics(initialMetrics);
545
+ setFileMetrics([]);
546
+ speedSamplesRef.current = [];
547
+ lastUpdateRef.current = 0;
548
+ }, []);
549
+
550
+ const getFileMetrics = useCallback(
551
+ (id: string) => {
552
+ return fileMetrics.find((file) => file.id === id);
553
+ },
554
+ [fileMetrics],
555
+ );
556
+
557
+ const exportMetrics = useCallback(() => {
558
+ return {
559
+ overall: metrics,
560
+ files: fileMetrics,
561
+ exportTime: Date.now(),
562
+ };
563
+ }, [metrics, fileMetrics]);
564
+
565
+ // Cleanup on unmount
566
+ React.useEffect(() => {
567
+ return () => {
568
+ if (intervalRef.current) {
569
+ clearInterval(intervalRef.current);
570
+ }
571
+ };
572
+ }, []);
573
+
574
+ return {
575
+ metrics,
576
+ fileMetrics,
577
+ startFileUpload,
578
+ updateFileProgress,
579
+ completeFileUpload,
580
+ removeFile,
581
+ reset,
582
+ getFileMetrics,
583
+ exportMetrics,
584
+ };
585
+ }