@uploadista/vue 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,502 @@
1
+ import { onUnmounted, readonly, ref } from "vue";
2
+ import { useUploadistaClient } from "./useUploadistaClient";
3
+
4
+ // Types
5
+ // biome-ignore lint/suspicious/noExplicitAny: Placeholder for detailed metrics types
6
+ type ChunkMetrics = any;
7
+ // biome-ignore lint/suspicious/noExplicitAny: Placeholder for detailed metrics types
8
+ type PerformanceInsights = any;
9
+ // biome-ignore lint/suspicious/noExplicitAny: Placeholder for detailed metrics types
10
+ type UploadSessionMetrics = any;
11
+
12
+ export interface UploadMetrics {
13
+ /**
14
+ * Total bytes uploaded across all files
15
+ */
16
+ totalBytesUploaded: number;
17
+
18
+ /**
19
+ * Total bytes to upload across all files
20
+ */
21
+ totalBytes: number;
22
+
23
+ /**
24
+ * Overall upload speed in bytes per second
25
+ */
26
+ averageSpeed: number;
27
+
28
+ /**
29
+ * Current upload speed in bytes per second
30
+ */
31
+ currentSpeed: number;
32
+
33
+ /**
34
+ * Estimated time remaining in milliseconds
35
+ */
36
+ estimatedTimeRemaining: number | null;
37
+
38
+ /**
39
+ * Total number of files being tracked
40
+ */
41
+ totalFiles: number;
42
+
43
+ /**
44
+ * Number of files completed
45
+ */
46
+ completedFiles: number;
47
+
48
+ /**
49
+ * Number of files currently uploading
50
+ */
51
+ activeUploads: number;
52
+
53
+ /**
54
+ * Overall progress as percentage (0-100)
55
+ */
56
+ progress: number;
57
+
58
+ /**
59
+ * Peak upload speed achieved
60
+ */
61
+ peakSpeed: number;
62
+
63
+ /**
64
+ * Start time of the first upload
65
+ */
66
+ startTime: number | null;
67
+
68
+ /**
69
+ * End time of the last completed upload
70
+ */
71
+ endTime: number | null;
72
+
73
+ /**
74
+ * Total duration of all uploads
75
+ */
76
+ totalDuration: number | null;
77
+
78
+ /**
79
+ * Detailed performance insights from the upload client
80
+ */
81
+ insights: PerformanceInsights;
82
+
83
+ /**
84
+ * Session metrics for completed uploads
85
+ */
86
+ sessionMetrics: Partial<UploadSessionMetrics>[];
87
+
88
+ /**
89
+ * Detailed chunk metrics from recent uploads
90
+ */
91
+ chunkMetrics: ChunkMetrics[];
92
+ }
93
+
94
+ export interface FileUploadMetrics {
95
+ id: string;
96
+ filename: string;
97
+ size: number;
98
+ bytesUploaded: number;
99
+ progress: number;
100
+ speed: number;
101
+ startTime: number;
102
+ endTime: number | null;
103
+ duration: number | null;
104
+ isComplete: boolean;
105
+ }
106
+
107
+ export interface UseUploadMetricsOptions {
108
+ /**
109
+ * Interval for calculating current speed (in milliseconds)
110
+ */
111
+ speedCalculationInterval?: number;
112
+
113
+ /**
114
+ * Number of speed samples to keep for average calculation
115
+ */
116
+ speedSampleSize?: number;
117
+
118
+ /**
119
+ * Called when metrics are updated
120
+ */
121
+ onMetricsUpdate?: (metrics: UploadMetrics) => void;
122
+
123
+ /**
124
+ * Called when a file upload starts
125
+ */
126
+ onFileStart?: (fileMetrics: FileUploadMetrics) => void;
127
+
128
+ /**
129
+ * Called when a file upload progresses
130
+ */
131
+ onFileProgress?: (fileMetrics: FileUploadMetrics) => void;
132
+
133
+ /**
134
+ * Called when a file upload completes
135
+ */
136
+ onFileComplete?: (fileMetrics: FileUploadMetrics) => void;
137
+ }
138
+
139
+ const initialMetrics: UploadMetrics = {
140
+ totalBytesUploaded: 0,
141
+ totalBytes: 0,
142
+ averageSpeed: 0,
143
+ currentSpeed: 0,
144
+ estimatedTimeRemaining: null,
145
+ totalFiles: 0,
146
+ completedFiles: 0,
147
+ activeUploads: 0,
148
+ progress: 0,
149
+ peakSpeed: 0,
150
+ startTime: null,
151
+ endTime: null,
152
+ totalDuration: null,
153
+ insights: {
154
+ overallEfficiency: 0,
155
+ chunkingEffectiveness: 0,
156
+ networkStability: 0,
157
+ recommendations: [],
158
+ optimalChunkSizeRange: { min: 256 * 1024, max: 2 * 1024 * 1024 },
159
+ },
160
+ sessionMetrics: [],
161
+ chunkMetrics: [],
162
+ };
163
+
164
+ /**
165
+ * Vue composable for tracking detailed upload metrics and performance statistics.
166
+ * Provides comprehensive monitoring of upload progress, speed, and timing data.
167
+ *
168
+ * @param options - Configuration and event handlers
169
+ * @returns Upload metrics state and control methods
170
+ *
171
+ * @example
172
+ * ```vue
173
+ * <script setup lang="ts">
174
+ * import { useUploadMetrics } from '@uploadista/vue';
175
+ *
176
+ * const uploadMetrics = useUploadMetrics({
177
+ * speedCalculationInterval: 1000, // Update speed every second
178
+ * speedSampleSize: 10, // Keep last 10 speed samples for average
179
+ * onMetricsUpdate: (metrics) => {
180
+ * console.log(`Overall progress: ${metrics.progress}%`);
181
+ * console.log(`Speed: ${(metrics.currentSpeed / 1024).toFixed(1)} KB/s`);
182
+ * console.log(`ETA: ${metrics.estimatedTimeRemaining}ms`);
183
+ * },
184
+ * onFileComplete: (fileMetrics) => {
185
+ * console.log(`${fileMetrics.filename} completed in ${fileMetrics.duration}ms`);
186
+ * },
187
+ * });
188
+ *
189
+ * // Start tracking a file
190
+ * const handleFileStart = (file: File) => {
191
+ * uploadMetrics.startFileUpload(file.name, file.name, file.size);
192
+ * };
193
+ *
194
+ * // Update progress during upload
195
+ * const handleProgress = (fileId: string, bytesUploaded: number) => {
196
+ * uploadMetrics.updateFileProgress(fileId, bytesUploaded);
197
+ * };
198
+ * </script>
199
+ *
200
+ * <template>
201
+ * <div>
202
+ * <div>Overall Progress: {{ uploadMetrics.metrics.progress }}%</div>
203
+ * <div>Speed: {{ (uploadMetrics.metrics.currentSpeed / 1024).toFixed(1) }} KB/s</div>
204
+ * <div>Files: {{ uploadMetrics.metrics.completedFiles }}/{{ uploadMetrics.metrics.totalFiles }}</div>
205
+ *
206
+ * <div v-if="uploadMetrics.metrics.estimatedTimeRemaining">
207
+ * ETA: {{ Math.round(uploadMetrics.metrics.estimatedTimeRemaining / 1000) }}s
208
+ * </div>
209
+ *
210
+ * <div v-for="file in uploadMetrics.fileMetrics" :key="file.id">
211
+ * {{ file.filename }}: {{ file.progress }}% ({{ (file.speed / 1024).toFixed(1) }} KB/s)
212
+ * </div>
213
+ * </div>
214
+ * </template>
215
+ * ```
216
+ */
217
+ export function useUploadMetrics(options: UseUploadMetricsOptions = {}) {
218
+ const {
219
+ speedCalculationInterval = 1000,
220
+ speedSampleSize = 10,
221
+ onMetricsUpdate,
222
+ onFileStart,
223
+ onFileProgress,
224
+ onFileComplete,
225
+ } = options;
226
+
227
+ const uploadClient = useUploadistaClient();
228
+
229
+ const metrics = ref<UploadMetrics>({ ...initialMetrics });
230
+ const fileMetrics = ref<FileUploadMetrics[]>([]);
231
+
232
+ const speedSamples = ref<Array<{ time: number; bytes: number }>>([]);
233
+ const lastUpdate = ref<number>(0);
234
+ const interval = ref<ReturnType<typeof setInterval> | null>(null);
235
+
236
+ const calculateSpeed = (currentTime: number, totalBytesUploaded: number) => {
237
+ const sample = { time: currentTime, bytes: totalBytesUploaded };
238
+ speedSamples.value.push(sample);
239
+
240
+ // Keep only recent samples
241
+ if (speedSamples.value.length > speedSampleSize) {
242
+ speedSamples.value = speedSamples.value.slice(-speedSampleSize);
243
+ }
244
+
245
+ // Calculate current speed (bytes per second)
246
+ let currentSpeed = 0;
247
+ if (speedSamples.value.length >= 2) {
248
+ const recent = speedSamples.value[speedSamples.value.length - 1];
249
+ const previous = speedSamples.value[speedSamples.value.length - 2];
250
+ if (recent && previous) {
251
+ const timeDiff = (recent.time - previous.time) / 1000; // Convert to seconds
252
+ const bytesDiff = recent.bytes - previous.bytes;
253
+ currentSpeed = timeDiff > 0 ? bytesDiff / timeDiff : 0;
254
+ }
255
+ }
256
+
257
+ // Calculate average speed
258
+ let averageSpeed = 0;
259
+ if (speedSamples.value.length >= 2) {
260
+ const first = speedSamples.value[0];
261
+ const last = speedSamples.value[speedSamples.value.length - 1];
262
+ if (first && last) {
263
+ const totalTime = (last.time - first.time) / 1000; // Convert to seconds
264
+ const totalBytes = last.bytes - first.bytes;
265
+ averageSpeed = totalTime > 0 ? totalBytes / totalTime : 0;
266
+ }
267
+ }
268
+
269
+ return { currentSpeed, averageSpeed };
270
+ };
271
+
272
+ const updateMetrics = () => {
273
+ const now = Date.now();
274
+
275
+ // Calculate totals from file metrics
276
+ const totalBytes = fileMetrics.value.reduce(
277
+ (sum, file) => sum + file.size,
278
+ 0,
279
+ );
280
+ const totalBytesUploaded = fileMetrics.value.reduce(
281
+ (sum, file) => sum + file.bytesUploaded,
282
+ 0,
283
+ );
284
+ const completedFiles = fileMetrics.value.filter(
285
+ (file) => file.isComplete,
286
+ ).length;
287
+ const activeUploads = fileMetrics.value.filter(
288
+ (file) => !file.isComplete && file.bytesUploaded > 0,
289
+ ).length;
290
+
291
+ // Calculate speeds
292
+ const { currentSpeed, averageSpeed } = calculateSpeed(
293
+ now,
294
+ totalBytesUploaded,
295
+ );
296
+
297
+ // Calculate progress
298
+ const progress =
299
+ totalBytes > 0 ? Math.round((totalBytesUploaded / totalBytes) * 100) : 0;
300
+
301
+ // Calculate estimated time remaining
302
+ let estimatedTimeRemaining: number | null = null;
303
+ if (currentSpeed > 0) {
304
+ const remainingBytes = totalBytes - totalBytesUploaded;
305
+ estimatedTimeRemaining = (remainingBytes / currentSpeed) * 1000; // Convert to milliseconds
306
+ }
307
+
308
+ // Find start and end times
309
+ const activeTimes = fileMetrics.value.filter((file) => file.startTime > 0);
310
+ const startTime =
311
+ activeTimes.length > 0
312
+ ? Math.min(...activeTimes.map((file) => file.startTime))
313
+ : null;
314
+
315
+ const completedTimes = fileMetrics.value.filter(
316
+ (file) => file.endTime !== null,
317
+ );
318
+ const endTime =
319
+ completedTimes.length > 0 && completedFiles === fileMetrics.value.length
320
+ ? Math.max(
321
+ ...(completedTimes
322
+ .map((file) => file.endTime)
323
+ .filter((time) => time !== null) as number[]),
324
+ )
325
+ : null;
326
+
327
+ const totalDuration = startTime && endTime ? endTime - startTime : null;
328
+
329
+ const newMetrics: UploadMetrics = {
330
+ totalBytesUploaded,
331
+ totalBytes,
332
+ averageSpeed,
333
+ currentSpeed,
334
+ estimatedTimeRemaining,
335
+ totalFiles: fileMetrics.value.length,
336
+ completedFiles,
337
+ activeUploads,
338
+ progress,
339
+ peakSpeed: Math.max(metrics.value.peakSpeed, currentSpeed),
340
+ startTime,
341
+ endTime,
342
+ totalDuration,
343
+ insights: uploadClient.client.getChunkingInsights(),
344
+ sessionMetrics: [uploadClient.client.exportMetrics().session],
345
+ chunkMetrics: uploadClient.client.exportMetrics().chunks,
346
+ };
347
+
348
+ metrics.value = newMetrics;
349
+ onMetricsUpdate?.(newMetrics);
350
+ };
351
+
352
+ // Set up periodic speed calculations
353
+ const setupSpeedCalculation = () => {
354
+ if (interval.value) {
355
+ clearInterval(interval.value);
356
+ }
357
+
358
+ interval.value = setInterval(() => {
359
+ if (
360
+ fileMetrics.value.some(
361
+ (file) => !file.isComplete && file.bytesUploaded > 0,
362
+ )
363
+ ) {
364
+ updateMetrics();
365
+ }
366
+ }, speedCalculationInterval);
367
+ };
368
+
369
+ const startFileUpload = (id: string, filename: string, size: number) => {
370
+ const now = Date.now();
371
+
372
+ const fileMetric: FileUploadMetrics = {
373
+ id,
374
+ filename,
375
+ size,
376
+ bytesUploaded: 0,
377
+ progress: 0,
378
+ speed: 0,
379
+ startTime: now,
380
+ endTime: null,
381
+ duration: null,
382
+ isComplete: false,
383
+ };
384
+
385
+ const existing = fileMetrics.value.find((file) => file.id === id);
386
+ if (existing) {
387
+ fileMetrics.value = fileMetrics.value.map((file) =>
388
+ file.id === id ? fileMetric : file,
389
+ );
390
+ } else {
391
+ fileMetrics.value = [...fileMetrics.value, fileMetric];
392
+ }
393
+
394
+ onFileStart?.(fileMetric);
395
+
396
+ // Start speed calculation if this is the first active upload
397
+ if (fileMetrics.value.filter((file) => !file.isComplete).length === 1) {
398
+ setupSpeedCalculation();
399
+ }
400
+ };
401
+
402
+ const updateFileProgress = (id: string, bytesUploaded: number) => {
403
+ const now = Date.now();
404
+
405
+ fileMetrics.value = fileMetrics.value.map((file) => {
406
+ if (file.id !== id) return file;
407
+
408
+ const timeDiff = (now - file.startTime) / 1000; // seconds
409
+ const speed = timeDiff > 0 ? bytesUploaded / timeDiff : 0;
410
+ const progress =
411
+ file.size > 0 ? Math.round((bytesUploaded / file.size) * 100) : 0;
412
+
413
+ const updatedFile = {
414
+ ...file,
415
+ bytesUploaded,
416
+ progress,
417
+ speed,
418
+ };
419
+
420
+ onFileProgress?.(updatedFile);
421
+ return updatedFile;
422
+ });
423
+
424
+ // Trigger metrics update
425
+ setTimeout(updateMetrics, 0);
426
+ };
427
+
428
+ const completeFileUpload = (id: string) => {
429
+ const now = Date.now();
430
+
431
+ fileMetrics.value = fileMetrics.value.map((file) => {
432
+ if (file.id !== id) return file;
433
+
434
+ const duration = now - file.startTime;
435
+ const speed = duration > 0 ? (file.size / duration) * 1000 : 0; // bytes per second
436
+
437
+ const completedFile = {
438
+ ...file,
439
+ bytesUploaded: file.size,
440
+ progress: 100,
441
+ speed,
442
+ endTime: now,
443
+ duration,
444
+ isComplete: true,
445
+ };
446
+
447
+ onFileComplete?.(completedFile);
448
+ return completedFile;
449
+ });
450
+
451
+ // Trigger metrics update
452
+ setTimeout(updateMetrics, 0);
453
+ };
454
+
455
+ const removeFile = (id: string) => {
456
+ fileMetrics.value = fileMetrics.value.filter((file) => file.id !== id);
457
+ setTimeout(updateMetrics, 0);
458
+ };
459
+
460
+ const reset = () => {
461
+ if (interval.value) {
462
+ clearInterval(interval.value);
463
+ interval.value = null;
464
+ }
465
+
466
+ metrics.value = { ...initialMetrics };
467
+ fileMetrics.value = [];
468
+ speedSamples.value = [];
469
+ lastUpdate.value = 0;
470
+ };
471
+
472
+ const getFileMetrics = (id: string) => {
473
+ return fileMetrics.value.find((file) => file.id === id);
474
+ };
475
+
476
+ const exportMetrics = () => {
477
+ return {
478
+ overall: metrics.value,
479
+ files: fileMetrics.value,
480
+ exportTime: Date.now(),
481
+ };
482
+ };
483
+
484
+ // Cleanup on unmount
485
+ onUnmounted(() => {
486
+ if (interval.value) {
487
+ clearInterval(interval.value);
488
+ }
489
+ });
490
+
491
+ return {
492
+ metrics: readonly(metrics),
493
+ fileMetrics: readonly(fileMetrics),
494
+ startFileUpload,
495
+ updateFileProgress,
496
+ completeFileUpload,
497
+ removeFile,
498
+ reset,
499
+ getFileMetrics,
500
+ exportMetrics,
501
+ };
502
+ }
@@ -0,0 +1,73 @@
1
+ import type { UploadistaEvent } from "@uploadista/client-browser";
2
+ import { inject, type Ref } from "vue";
3
+ import {
4
+ UPLOADISTA_CLIENT_KEY,
5
+ UPLOADISTA_EVENT_SUBSCRIBERS_KEY,
6
+ } from "./plugin";
7
+
8
+ /**
9
+ * Access the Uploadista client instance from the plugin or provider.
10
+ * Must be used within a component tree that has the Uploadista plugin or provider installed.
11
+ *
12
+ * @returns Uploadista client instance with event subscription
13
+ * @throws Error if used outside of Uploadista plugin/provider context
14
+ *
15
+ * @example
16
+ * ```vue
17
+ * <script setup lang="ts">
18
+ * import { useUploadistaClient } from '@uploadista/vue';
19
+ *
20
+ * const { client, subscribeToEvents } = useUploadistaClient();
21
+ *
22
+ * // Subscribe to all events
23
+ * const unsubscribe = subscribeToEvents((event) => {
24
+ * console.log('Upload event:', event);
25
+ * });
26
+ *
27
+ * // Clean up on unmount
28
+ * onUnmounted(() => {
29
+ * unsubscribe();
30
+ * });
31
+ * </script>
32
+ * ```
33
+ */
34
+ export function useUploadistaClient() {
35
+ const client = inject(UPLOADISTA_CLIENT_KEY);
36
+
37
+ if (!client) {
38
+ throw new Error(
39
+ "useUploadistaClient must be used within a component tree that has the Uploadista plugin or provider installed. " +
40
+ "Make sure to either use app.use(createUploadistaPlugin({ ... })) in your main app file, " +
41
+ "or wrap your component tree with <UploadistaProvider>.",
42
+ );
43
+ }
44
+
45
+ // Try to get the shared event subscribers from the provider
46
+ const eventSubscribersRef = inject<
47
+ Ref<Set<(event: UploadistaEvent) => void>> | undefined
48
+ >(UPLOADISTA_EVENT_SUBSCRIBERS_KEY);
49
+
50
+ const subscribeToEvents = (handler: (event: UploadistaEvent) => void) => {
51
+ if (!eventSubscribersRef) {
52
+ console.warn(
53
+ "subscribeToEvents called but no event subscribers provided. Events will not be dispatched. " +
54
+ "Make sure to use UploadistaProvider or createUploadistaPlugin with proper configuration.",
55
+ );
56
+ return () => {
57
+ // No-op unsubscribe if subscribers aren't available
58
+ };
59
+ }
60
+
61
+ eventSubscribersRef.value.add(handler);
62
+ return () => {
63
+ eventSubscribersRef.value.delete(handler);
64
+ };
65
+ };
66
+
67
+ return {
68
+ client,
69
+ subscribeToEvents,
70
+ };
71
+ }
72
+
73
+ export type UseUploadistaClientReturn = ReturnType<typeof useUploadistaClient>;
package/src/index.ts ADDED
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Uploadista Vue Client
3
+ *
4
+ * Vue 3 composables and components for file uploads with the Uploadista platform.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * import { createUploadistaPlugin } from '@uploadista/vue'
9
+ * import { UploadZone, FlowUploadZone } from '@uploadista/vue'
10
+ *
11
+ * // Install plugin in your Vue app
12
+ * const app = createApp(App)
13
+ * app.use(createUploadistaPlugin({
14
+ * client: uploadClient
15
+ * }))
16
+ * ```
17
+ */
18
+
19
+ // Re-export all components
20
+ export * from "./components";
21
+ // Re-export all composables
22
+ export * from "./composables";
23
+ // Re-export the plugin
24
+ export * from "./composables/plugin";
25
+
26
+ export * from "./providers";
27
+ // Re-export utilities
28
+ export * from "./utils";
@@ -0,0 +1,69 @@
1
+ <script setup lang="ts">
2
+ import {
3
+ createUploadistaClient,
4
+ type UploadistaEvent,
5
+ } from "@uploadista/client-browser";
6
+ import {
7
+ UPLOADISTA_CLIENT_KEY,
8
+ UPLOADISTA_EVENT_SUBSCRIBERS_KEY,
9
+ } from "@uploadista/vue";
10
+ import { onBeforeUnmount, provide, ref } from "vue";
11
+
12
+ const props = withDefaults(
13
+ defineProps<{
14
+ serverUrl: string;
15
+ storageId?: string;
16
+ uploadistaBasePath?: string;
17
+ chunkSize?: number;
18
+ parallelUploads?: number;
19
+ storeFingerprintForResuming?: boolean;
20
+ onEvent?: (event: UploadistaEvent) => void;
21
+ }>(),
22
+ {
23
+ storageId: "local",
24
+ uploadistaBasePath: "uploadista",
25
+ chunkSize: 1024 * 1024,
26
+ parallelUploads: 1,
27
+ storeFingerprintForResuming: true,
28
+ },
29
+ );
30
+
31
+ const emit = defineEmits<{
32
+ /**
33
+ * Emitted when the underlying client dispatches an event.
34
+ */
35
+ (e: "event", event: UploadistaEvent): void;
36
+ }>();
37
+
38
+ // Create a shared set of event subscribers
39
+ const eventSubscribers = ref(new Set<(event: UploadistaEvent) => void>());
40
+
41
+ const client = createUploadistaClient({
42
+ baseUrl: props.serverUrl,
43
+ storageId: props.storageId,
44
+ uploadistaBasePath: props.uploadistaBasePath,
45
+ chunkSize: props.chunkSize,
46
+ parallelUploads: props.parallelUploads,
47
+ storeFingerprintForResuming: props.storeFingerprintForResuming,
48
+ onEvent: (event) => {
49
+ // Dispatch to all subscribers registered via subscribeToEvents
50
+ eventSubscribers.value.forEach((subscriber) => {
51
+ subscriber(event);
52
+ });
53
+
54
+ props.onEvent?.(event);
55
+ emit("event", event);
56
+ },
57
+ });
58
+
59
+ provide(UPLOADISTA_CLIENT_KEY, client);
60
+ provide(UPLOADISTA_EVENT_SUBSCRIBERS_KEY, eventSubscribers);
61
+
62
+ onBeforeUnmount(() => {
63
+ client.closeAllWebSockets();
64
+ });
65
+ </script>
66
+
67
+ <template>
68
+ <slot />
69
+ </template>
@@ -0,0 +1 @@
1
+ export { default as UploadistaProvider } from "./UploadistaProvider.vue";