@uploadista/react-native-core 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,363 @@
1
+ import type { UploadFile } from "@uploadista/core/types";
2
+ import { useCallback, useRef, useState } from "react";
3
+ import type { FilePickResult, UseMultiUploadOptions } from "../types";
4
+ import { useUploadistaContext } from "./use-uploadista-context";
5
+
6
+ export interface UploadItemState {
7
+ id: string;
8
+ file: FilePickResult;
9
+ status: "idle" | "uploading" | "success" | "error" | "aborted";
10
+ progress: number;
11
+ bytesUploaded: number;
12
+ totalBytes: number;
13
+ error: Error | null;
14
+ result: UploadFile | null;
15
+ }
16
+
17
+ export interface MultiUploadState {
18
+ items: UploadItemState[];
19
+ totalProgress: number;
20
+ totalUploaded: number;
21
+ totalBytes: number;
22
+ activeCount: number;
23
+ completedCount: number;
24
+ failedCount: number;
25
+ }
26
+
27
+ const initialState: MultiUploadState = {
28
+ items: [],
29
+ totalProgress: 0,
30
+ totalUploaded: 0,
31
+ totalBytes: 0,
32
+ activeCount: 0,
33
+ completedCount: 0,
34
+ failedCount: 0,
35
+ };
36
+
37
+ /**
38
+ * Hook for managing multiple concurrent file uploads with progress tracking.
39
+ * Each file is uploaded independently using the core upload client.
40
+ *
41
+ * Must be used within an UploadistaProvider.
42
+ *
43
+ * @param options - Multi-upload configuration options
44
+ * @returns Multi-upload state and control methods
45
+ *
46
+ * @example
47
+ * ```tsx
48
+ * function MyComponent() {
49
+ * const multiUpload = useMultiUpload({
50
+ * maxConcurrent: 3,
51
+ * onSuccess: (result) => console.log('File uploaded:', result),
52
+ * onError: (error) => console.error('Upload failed:', error),
53
+ * });
54
+ *
55
+ * const handlePickFiles = async () => {
56
+ * const files = await fileSystemProvider.pickImage({ allowMultiple: true });
57
+ * multiUpload.addFiles(files);
58
+ * await multiUpload.startUploads();
59
+ * };
60
+ *
61
+ * return (
62
+ * <View>
63
+ * <Button title="Pick Files" onPress={handlePickFiles} />
64
+ * <Text>Progress: {multiUpload.state.totalProgress}%</Text>
65
+ * <Text>Active: {multiUpload.state.activeCount}</Text>
66
+ * <Text>Completed: {multiUpload.state.completedCount}/{multiUpload.state.items.length}</Text>
67
+ * </View>
68
+ * );
69
+ * }
70
+ * ```
71
+ */
72
+ export function useMultiUpload(options: UseMultiUploadOptions = {}) {
73
+ const { client, fileSystemProvider } = useUploadistaContext();
74
+ const [state, setState] = useState<MultiUploadState>(initialState);
75
+ const abortControllersRef = useRef<Map<string, { abort: () => void }>>(
76
+ new Map(),
77
+ );
78
+ const nextIdRef = useRef(0);
79
+
80
+ const generateId = useCallback(() => {
81
+ return `upload-${Date.now()}-${nextIdRef.current++}`;
82
+ }, []);
83
+
84
+ const updateAggregateStats = useCallback((items: UploadItemState[]) => {
85
+ const totalBytes = items.reduce((sum, item) => sum + item.totalBytes, 0);
86
+ const totalUploaded = items.reduce(
87
+ (sum, item) => sum + item.bytesUploaded,
88
+ 0,
89
+ );
90
+ const totalProgress =
91
+ totalBytes > 0 ? Math.round((totalUploaded / totalBytes) * 100) : 0;
92
+ const activeCount = items.filter(
93
+ (item) => item.status === "uploading",
94
+ ).length;
95
+ const completedCount = items.filter(
96
+ (item) => item.status === "success",
97
+ ).length;
98
+ const failedCount = items.filter((item) => item.status === "error").length;
99
+
100
+ setState((prev) => ({
101
+ ...prev,
102
+ items,
103
+ totalProgress,
104
+ totalUploaded,
105
+ totalBytes,
106
+ activeCount,
107
+ completedCount,
108
+ failedCount,
109
+ }));
110
+ }, []);
111
+
112
+ const addFiles = useCallback(
113
+ (files: FilePickResult[]) => {
114
+ const newItems: UploadItemState[] = files.map((file) => ({
115
+ id: generateId(),
116
+ file,
117
+ status: "idle" as const,
118
+ progress: 0,
119
+ bytesUploaded: 0,
120
+ totalBytes: file.size,
121
+ error: null,
122
+ result: null,
123
+ }));
124
+
125
+ setState((prev) => {
126
+ const updatedItems = [...prev.items, ...newItems];
127
+ const totalBytes = updatedItems.reduce(
128
+ (sum, item) => sum + item.totalBytes,
129
+ 0,
130
+ );
131
+ return {
132
+ ...prev,
133
+ items: updatedItems,
134
+ totalBytes,
135
+ };
136
+ });
137
+
138
+ return newItems.map((item) => item.id);
139
+ },
140
+ [generateId],
141
+ );
142
+
143
+ const uploadSingleItem = useCallback(
144
+ async (item: UploadItemState) => {
145
+ try {
146
+ // Update status to uploading
147
+ setState((prev) => {
148
+ const updatedItems = prev.items.map((i) =>
149
+ i.id === item.id ? { ...i, status: "uploading" as const } : i,
150
+ );
151
+ updateAggregateStats(updatedItems);
152
+ return prev;
153
+ });
154
+
155
+ // Read file content
156
+ const fileContent = await fileSystemProvider.readFile(item.file.uri);
157
+
158
+ // Create a Blob from the file content
159
+ // Convert ArrayBuffer to Uint8Array for better compatibility
160
+ const data =
161
+ fileContent instanceof ArrayBuffer
162
+ ? new Uint8Array(fileContent)
163
+ : fileContent;
164
+ // Note: Using any cast here because React Native Blob accepts BufferSource
165
+ // but TypeScript's lib.dom.d.ts Blob type doesn't include it
166
+ // biome-ignore lint/suspicious/noExplicitAny: React Native Blob accepts BufferSource
167
+ const blob = new Blob([data as any], {
168
+ type: item.file.mimeType || "application/octet-stream",
169
+ // biome-ignore lint/suspicious/noExplicitAny: BlobPropertyBag type differs by platform
170
+ } as any);
171
+
172
+ // use the Blob (for React Native)
173
+ const uploadInput = blob;
174
+
175
+ // Start upload using the client
176
+ const uploadPromise = client.upload(uploadInput, {
177
+ metadata: options.metadata,
178
+
179
+ onProgress: (
180
+ _uploadId: string,
181
+ bytesUploaded: number,
182
+ totalBytes: number | null,
183
+ ) => {
184
+ const progress = totalBytes
185
+ ? Math.round((bytesUploaded / totalBytes) * 100)
186
+ : 0;
187
+
188
+ setState((prev) => {
189
+ const updatedItems = prev.items.map((i) =>
190
+ i.id === item.id
191
+ ? {
192
+ ...i,
193
+ progress,
194
+ bytesUploaded,
195
+ totalBytes: totalBytes || i.totalBytes,
196
+ }
197
+ : i,
198
+ );
199
+ updateAggregateStats(updatedItems);
200
+ return prev;
201
+ });
202
+ },
203
+
204
+ onSuccess: (result: UploadFile) => {
205
+ setState((prev) => {
206
+ const updatedItems = prev.items.map((i) =>
207
+ i.id === item.id
208
+ ? {
209
+ ...i,
210
+ status: "success" as const,
211
+ progress: 100,
212
+ result,
213
+ bytesUploaded: result.size || i.totalBytes,
214
+ }
215
+ : i,
216
+ );
217
+ updateAggregateStats(updatedItems);
218
+ return prev;
219
+ });
220
+
221
+ options.onSuccess?.(result);
222
+ abortControllersRef.current.delete(item.id);
223
+ },
224
+
225
+ onError: (error: Error) => {
226
+ setState((prev) => {
227
+ const updatedItems = prev.items.map((i) =>
228
+ i.id === item.id
229
+ ? { ...i, status: "error" as const, error }
230
+ : i,
231
+ );
232
+ updateAggregateStats(updatedItems);
233
+ return prev;
234
+ });
235
+
236
+ options.onError?.(error);
237
+ abortControllersRef.current.delete(item.id);
238
+ },
239
+ });
240
+
241
+ // Store abort controller
242
+ const controller = await uploadPromise;
243
+ abortControllersRef.current.set(item.id, controller);
244
+ } catch (error) {
245
+ setState((prev) => {
246
+ const updatedItems = prev.items.map((i) =>
247
+ i.id === item.id
248
+ ? {
249
+ ...i,
250
+ status: "error" as const,
251
+ error: error as Error,
252
+ }
253
+ : i,
254
+ );
255
+ updateAggregateStats(updatedItems);
256
+ return prev;
257
+ });
258
+
259
+ options.onError?.(error as Error);
260
+ abortControllersRef.current.delete(item.id);
261
+ }
262
+ },
263
+ [client, fileSystemProvider, options, updateAggregateStats],
264
+ );
265
+
266
+ const startUploads = useCallback(async () => {
267
+ const maxConcurrent = options.maxConcurrent || 3;
268
+ const itemsToUpload = state.items.filter((item) => item.status === "idle");
269
+
270
+ // Process items in batches
271
+ for (let i = 0; i < itemsToUpload.length; i += maxConcurrent) {
272
+ const batch = itemsToUpload.slice(i, i + maxConcurrent);
273
+ await Promise.all(batch.map((item) => uploadSingleItem(item)));
274
+ }
275
+ }, [state.items, options.maxConcurrent, uploadSingleItem]);
276
+
277
+ const removeItem = useCallback(
278
+ (id: string) => {
279
+ const controller = abortControllersRef.current.get(id);
280
+ if (controller) {
281
+ controller.abort();
282
+ abortControllersRef.current.delete(id);
283
+ }
284
+
285
+ setState((prev) => {
286
+ const updatedItems = prev.items.filter((item) => item.id !== id);
287
+ updateAggregateStats(updatedItems);
288
+ return prev;
289
+ });
290
+ },
291
+ [updateAggregateStats],
292
+ );
293
+
294
+ const abortItem = useCallback(
295
+ (id: string) => {
296
+ const controller = abortControllersRef.current.get(id);
297
+ if (controller) {
298
+ controller.abort();
299
+ abortControllersRef.current.delete(id);
300
+ }
301
+
302
+ setState((prev) => {
303
+ const updatedItems = prev.items.map((item) =>
304
+ item.id === id ? { ...item, status: "aborted" as const } : item,
305
+ );
306
+ updateAggregateStats(updatedItems);
307
+ return prev;
308
+ });
309
+ },
310
+ [updateAggregateStats],
311
+ );
312
+
313
+ const clear = useCallback(() => {
314
+ // Abort all active uploads
315
+ abortControllersRef.current.forEach((controller) => {
316
+ controller.abort();
317
+ });
318
+ abortControllersRef.current.clear();
319
+
320
+ setState(initialState);
321
+ }, []);
322
+
323
+ const retryItem = useCallback(
324
+ async (id: string) => {
325
+ const item = state.items.find((i) => i.id === id);
326
+ if (item && (item.status === "error" || item.status === "aborted")) {
327
+ // Reset item status to idle
328
+ setState((prev) => {
329
+ const updatedItems = prev.items.map((i) =>
330
+ i.id === id
331
+ ? {
332
+ ...i,
333
+ status: "idle" as const,
334
+ progress: 0,
335
+ bytesUploaded: 0,
336
+ error: null,
337
+ }
338
+ : i,
339
+ );
340
+ updateAggregateStats(updatedItems);
341
+ return prev;
342
+ });
343
+
344
+ // Upload it
345
+ const resetItem = state.items.find((i) => i.id === id);
346
+ if (resetItem) {
347
+ await uploadSingleItem(resetItem);
348
+ }
349
+ }
350
+ },
351
+ [state.items, uploadSingleItem, updateAggregateStats],
352
+ );
353
+
354
+ return {
355
+ state,
356
+ addFiles,
357
+ startUploads,
358
+ removeItem,
359
+ abortItem,
360
+ retryItem,
361
+ clear,
362
+ };
363
+ }
@@ -0,0 +1,82 @@
1
+ import { useCallback, useRef, useState } from "react";
2
+ import type { UploadMetrics } from "../types";
3
+
4
+ /**
5
+ * Hook for tracking upload performance metrics
6
+ * @returns Metrics object and methods to track uploads
7
+ */
8
+ export function useUploadMetrics() {
9
+ const startTimeRef = useRef<number | null>(null);
10
+ const startBytesRef = useRef<number>(0);
11
+ const peakSpeedRef = useRef<number>(0);
12
+
13
+ const [metrics, setMetrics] = useState<UploadMetrics>({
14
+ totalBytes: 0,
15
+ durationMs: 0,
16
+ avgSpeed: 0,
17
+ peakSpeed: 0,
18
+ retries: 0,
19
+ });
20
+
21
+ // Start tracking
22
+ const start = useCallback(() => {
23
+ startTimeRef.current = Date.now();
24
+ startBytesRef.current = 0;
25
+ peakSpeedRef.current = 0;
26
+ }, []);
27
+
28
+ // Update metrics based on current progress
29
+ const update = useCallback(
30
+ (uploadedBytes: number, _totalBytes: number, currentRetries = 0) => {
31
+ if (!startTimeRef.current) {
32
+ return;
33
+ }
34
+
35
+ const now = Date.now();
36
+ const durationMs = now - startTimeRef.current;
37
+ const speed = durationMs > 0 ? (uploadedBytes / durationMs) * 1000 : 0;
38
+
39
+ if (speed > peakSpeedRef.current) {
40
+ peakSpeedRef.current = speed;
41
+ }
42
+
43
+ setMetrics({
44
+ totalBytes: uploadedBytes,
45
+ durationMs,
46
+ avgSpeed: durationMs > 0 ? (uploadedBytes / durationMs) * 1000 : 0,
47
+ peakSpeed: peakSpeedRef.current,
48
+ retries: currentRetries,
49
+ });
50
+ },
51
+ [],
52
+ );
53
+
54
+ // End tracking and return final metrics
55
+ const end = useCallback(() => {
56
+ const finalMetrics = metrics;
57
+ startTimeRef.current = null;
58
+ return finalMetrics;
59
+ }, [metrics]);
60
+
61
+ // Reset metrics
62
+ const reset = useCallback(() => {
63
+ startTimeRef.current = null;
64
+ startBytesRef.current = 0;
65
+ peakSpeedRef.current = 0;
66
+ setMetrics({
67
+ totalBytes: 0,
68
+ durationMs: 0,
69
+ avgSpeed: 0,
70
+ peakSpeed: 0,
71
+ retries: 0,
72
+ });
73
+ }, []);
74
+
75
+ return {
76
+ metrics,
77
+ start,
78
+ update,
79
+ end,
80
+ reset,
81
+ };
82
+ }