@uploadista/react-native-core 0.0.13 → 0.0.14

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.
@@ -1,25 +1,20 @@
1
- import type { UploadistaEvent } from "@uploadista/client-core";
1
+ import type {
2
+ UploadistaUploadOptions,
3
+ UploadMetrics,
4
+ } from "@uploadista/client-core";
5
+ import {
6
+ UploadManager,
7
+ type UploadState,
8
+ type UploadStatus,
9
+ } from "@uploadista/client-core";
2
10
  import type { UploadFile } from "@uploadista/core/types";
3
- import { UploadEventType } from "@uploadista/core/types";
4
11
  import { useCallback, useEffect, useRef, useState } from "react";
5
12
  import type { FilePickResult } from "../types";
13
+ import { createBlobFromBuffer } from "../types/platform-types";
6
14
  import { useUploadistaContext } from "./use-uploadista-context";
7
15
 
8
- export type UploadStatus =
9
- | "idle"
10
- | "uploading"
11
- | "success"
12
- | "error"
13
- | "aborted";
14
-
15
- export interface UploadState {
16
- status: UploadStatus;
17
- progress: number;
18
- bytesUploaded: number;
19
- totalBytes: number | null;
20
- error: Error | null;
21
- result: UploadFile | null;
22
- }
16
+ // Re-export types from core for convenience
17
+ export type { UploadState, UploadStatus };
23
18
 
24
19
  export interface UseUploadOptions {
25
20
  /**
@@ -41,7 +36,7 @@ export interface UseUploadOptions {
41
36
  * Called when upload progress updates
42
37
  */
43
38
  onProgress?: (
44
- progress: number,
39
+ uploadId: string,
45
40
  bytesUploaded: number,
46
41
  totalBytes: number | null,
47
42
  ) => void;
@@ -111,6 +106,11 @@ export interface UseUploadReturn {
111
106
  * Whether the upload can be retried
112
107
  */
113
108
  canRetry: boolean;
109
+
110
+ /**
111
+ * Upload metrics and performance insights from the client
112
+ */
113
+ metrics: UploadMetrics;
114
114
  }
115
115
 
116
116
  const initialState: UploadState = {
@@ -123,7 +123,7 @@ const initialState: UploadState = {
123
123
  };
124
124
 
125
125
  /**
126
- * React hook for managing individual file uploads with full state management.
126
+ * React Native hook for managing individual file uploads with full state management.
127
127
  * Provides upload progress tracking, error handling, abort functionality, and retry logic.
128
128
  *
129
129
  * Must be used within an UploadistaProvider.
@@ -140,16 +140,14 @@ const initialState: UploadState = {
140
140
  * onProgress: (progress) => console.log('Progress:', progress + '%'),
141
141
  * });
142
142
  *
143
- * const handlePickFile = async () => {
144
- * const file = await fileSystemProvider.pickDocument();
145
- * if (file) {
146
- * await upload.upload(file);
147
- * }
143
+ * const handleFilePick = async () => {
144
+ * const file = await pickFile();
145
+ * if (file) await upload.upload(file);
148
146
  * };
149
147
  *
150
148
  * return (
151
149
  * <View>
152
- * <Button title="Pick File" onPress={handlePickFile} />
150
+ * <Button title="Pick File" onPress={handleFilePick} />
153
151
  * {upload.isUploading && <Text>Progress: {upload.state.progress}%</Text>}
154
152
  * {upload.state.error && <Text>Error: {upload.state.error.message}</Text>}
155
153
  * {upload.canRetry && <Button title="Retry" onPress={upload.retry} />}
@@ -160,226 +158,92 @@ const initialState: UploadState = {
160
158
  * ```
161
159
  */
162
160
  export function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
163
- const { client, fileSystemProvider, subscribeToEvents } =
164
- useUploadistaContext();
161
+ const { client, fileSystemProvider } = useUploadistaContext();
165
162
  const [state, setState] = useState<UploadState>(initialState);
166
- const abortControllerRef = useRef<{ abort: () => void } | null>(null);
163
+ const managerRef = useRef<UploadManager | null>(null);
167
164
  const lastFileRef = useRef<FilePickResult | null>(null);
168
- const currentUploadIdRef = useRef<string | null>(null);
169
-
170
- const updateState = useCallback((update: Partial<UploadState>) => {
171
- setState((prev) => ({ ...prev, ...update }));
172
- }, []);
173
-
174
- const reset = useCallback(() => {
175
- if (abortControllerRef.current) {
176
- abortControllerRef.current.abort();
177
- abortControllerRef.current = null;
178
- }
179
- setState(initialState);
180
- lastFileRef.current = null;
181
- currentUploadIdRef.current = null;
182
- }, []);
183
-
184
- const abort = useCallback(() => {
185
- if (abortControllerRef.current) {
186
- abortControllerRef.current.abort();
187
- abortControllerRef.current = null;
188
- }
189
-
190
- updateState({
191
- status: "aborted",
192
- });
193
-
194
- options.onAbort?.();
195
- }, [options, updateState]);
196
-
197
- const upload = useCallback(
198
- async (file: FilePickResult) => {
199
- // Handle cancelled picker
200
- if (file.status === "cancelled") {
201
- return;
202
- }
203
-
204
- // Handle picker error
205
- if (file.status === "error") {
206
- updateState({
207
- status: "error",
208
- error: file.error,
209
- });
210
- options.onError?.(file.error);
211
- return;
212
- }
213
-
214
- // Reset any previous state
215
- setState({
216
- ...initialState,
217
- status: "uploading",
218
- totalBytes: file.data.size,
219
- });
220
165
 
221
- lastFileRef.current = file;
166
+ // Create UploadManager instance
167
+ useEffect(() => {
168
+ // Create upload function that handles React Native file reading
169
+ const uploadFn = async (input: unknown, opts: UploadistaUploadOptions) => {
170
+ const file = input as FilePickResult;
222
171
 
223
- try {
224
- // Read file content
172
+ if (file.status === "success") {
173
+ // Read file content from React Native file system
225
174
  const fileContent = await fileSystemProvider.readFile(file.data.uri);
226
175
 
227
- // Create a Blob from the file content
228
- // Convert ArrayBuffer to Uint8Array for better compatibility
229
- const data =
230
- fileContent instanceof ArrayBuffer
231
- ? new Uint8Array(fileContent)
232
- : fileContent;
233
- // Note: Using any cast here because React Native Blob accepts BufferSource
234
- // but TypeScript's lib.dom.d.ts Blob type doesn't include it
235
- // biome-ignore lint/suspicious/noExplicitAny: React Native Blob accepts BufferSource
236
- const blob = new Blob([data as any], {
176
+ // Create a Blob from the file content using platform-aware utility
177
+ const blob = createBlobFromBuffer(fileContent, {
237
178
  type: file.data.mimeType || "application/octet-stream",
238
- // biome-ignore lint/suspicious/noExplicitAny: BlobPropertyBag type differs by platform
239
- } as any);
240
-
241
- // use the Blob (for React Native)
242
- const uploadInput = blob;
243
-
244
- // Start the upload using the client
245
- const uploadPromise = client.upload(uploadInput, {
246
- metadata: options.metadata,
247
- uploadLengthDeferred: options.uploadLengthDeferred,
248
- uploadSize: options.uploadSize,
249
-
250
- onStart: ({ uploadId }) => {
251
- currentUploadIdRef.current = uploadId;
252
- },
253
-
254
- onProgress: (
255
- _uploadId: string,
256
- bytesUploaded: number,
257
- totalBytes: number | null,
258
- ) => {
259
- const progress = totalBytes
260
- ? Math.round((bytesUploaded / totalBytes) * 100)
261
- : 0;
262
-
263
- updateState({
264
- progress,
265
- bytesUploaded,
266
- totalBytes,
267
- });
268
-
269
- options.onProgress?.(progress, bytesUploaded, totalBytes);
270
- },
271
-
272
- onChunkComplete: (
273
- chunkSize: number,
274
- bytesAccepted: number,
275
- bytesTotal: number | null,
276
- ) => {
277
- options.onChunkComplete?.(chunkSize, bytesAccepted, bytesTotal);
278
- },
279
-
280
- onSuccess: (result: UploadFile) => {
281
- updateState({
282
- status: "success",
283
- result,
284
- progress: 100,
285
- bytesUploaded: result.size || 0,
286
- totalBytes: result.size || null,
287
- });
288
-
289
- options.onSuccess?.(result);
290
- abortControllerRef.current = null;
291
- },
292
-
293
- onError: (error: Error) => {
294
- updateState({
295
- status: "error",
296
- error,
297
- });
298
-
299
- options.onError?.(error);
300
- abortControllerRef.current = null;
301
- },
302
-
303
- onShouldRetry: options.onShouldRetry,
304
- });
305
-
306
- // Handle the promise to get the abort controller
307
- const controller = await uploadPromise;
308
- abortControllerRef.current = controller;
309
- } catch (error) {
310
- updateState({
311
- status: "error",
312
- error: error as Error,
313
179
  });
314
180
 
315
- options.onError?.(error as Error);
316
- abortControllerRef.current = null;
181
+ // Upload the Blob
182
+ return client.upload(blob, opts);
317
183
  }
318
- },
319
- [client, fileSystemProvider, options, updateState],
320
- );
321
184
 
322
- const retry = useCallback(() => {
323
- if (
324
- lastFileRef.current &&
325
- (state.status === "error" || state.status === "aborted")
326
- ) {
327
- upload(lastFileRef.current);
328
- }
329
- }, [upload, state.status]);
185
+ return Promise.resolve({ abort: () => {} });
186
+ };
187
+
188
+ managerRef.current = new UploadManager(
189
+ uploadFn,
190
+ {
191
+ onStateChange: setState,
192
+ onProgress: options.onProgress,
193
+ onChunkComplete: options.onChunkComplete,
194
+ onSuccess: options.onSuccess,
195
+ onError: options.onError,
196
+ onAbort: options.onAbort,
197
+ },
198
+ {
199
+ metadata: options.metadata,
200
+ uploadLengthDeferred: options.uploadLengthDeferred,
201
+ uploadSize: options.uploadSize,
202
+ onShouldRetry: options.onShouldRetry,
203
+ },
204
+ );
205
+
206
+ return () => {
207
+ managerRef.current?.cleanup();
208
+ };
209
+ }, [client, fileSystemProvider, options]);
210
+
211
+ // Upload function - stores file reference for retry
212
+ const upload = useCallback(async (file: FilePickResult) => {
213
+ lastFileRef.current = file;
214
+ await managerRef.current?.upload(file);
215
+ }, []);
330
216
 
331
- // Subscribe to events from context (WebSocket events)
332
- useEffect(() => {
333
- const unsubscribe = subscribeToEvents((event: UploadistaEvent) => {
334
- // Handle upload progress events
335
- const uploadEvent = event as {
336
- type: string;
337
- data?: { id: string; progress: number; total: number };
338
- };
339
-
340
- if (
341
- uploadEvent.type === UploadEventType.UPLOAD_PROGRESS &&
342
- uploadEvent.data
343
- ) {
344
- const {
345
- id: uploadId,
346
- progress: bytesUploaded,
347
- total: totalBytes,
348
- } = uploadEvent.data;
349
-
350
- if (uploadId !== currentUploadIdRef.current) {
351
- return;
352
- }
353
-
354
- // Update state for this upload
355
- const progress = totalBytes
356
- ? Math.round((bytesUploaded / totalBytes) * 100)
357
- : 0;
358
-
359
- setState((prev) => {
360
- // Only update if we're currently uploading
361
- if (prev.status === "uploading") {
362
- return {
363
- ...prev,
364
- progress,
365
- bytesUploaded,
366
- totalBytes,
367
- };
368
- }
369
- return prev;
370
- });
217
+ // Abort function
218
+ const abort = useCallback(() => {
219
+ managerRef.current?.abort();
220
+ }, []);
371
221
 
372
- options.onProgress?.(progress, bytesUploaded, totalBytes);
373
- }
374
- });
222
+ // Reset function
223
+ const reset = useCallback(() => {
224
+ managerRef.current?.reset();
225
+ lastFileRef.current = null;
226
+ }, []);
375
227
 
376
- return unsubscribe;
377
- }, [subscribeToEvents, options]);
228
+ // Retry function
229
+ const retry = useCallback(() => {
230
+ if (lastFileRef.current && managerRef.current?.canRetry()) {
231
+ managerRef.current.retry();
232
+ }
233
+ }, []);
378
234
 
235
+ // Derive computed values from state
379
236
  const isUploading = state.status === "uploading";
380
- const canRetry =
381
- (state.status === "error" || state.status === "aborted") &&
382
- lastFileRef.current !== null;
237
+ const canRetry = managerRef.current?.canRetry() ?? false;
238
+
239
+ // Create metrics object that delegates to the upload client
240
+ const metrics: UploadMetrics = {
241
+ getInsights: () => client.getChunkingInsights(),
242
+ exportMetrics: () => client.exportMetrics(),
243
+ getNetworkMetrics: () => client.getNetworkMetrics(),
244
+ getNetworkCondition: () => client.getNetworkCondition(),
245
+ resetMetrics: () => client.resetMetrics(),
246
+ };
383
247
 
384
248
  return {
385
249
  state,
@@ -389,5 +253,6 @@ export function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
389
253
  retry,
390
254
  isUploading,
391
255
  canRetry,
256
+ metrics,
392
257
  };
393
258
  }
@@ -1,2 +1,3 @@
1
+ export * from "./platform-types";
1
2
  export * from "./types";
2
3
  export * from "./upload-input";
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Platform-specific type definitions for React Native
3
+ *
4
+ * React Native's Blob implementation differs from the browser's Blob API.
5
+ * This file provides proper type definitions and guards for platform-specific behavior.
6
+ */
7
+
8
+ /**
9
+ * BufferSource represents data that can be passed to Blob constructor
10
+ * Includes both ArrayBuffer and typed arrays (Uint8Array, etc.)
11
+ */
12
+ export type BufferSource = ArrayBuffer | ArrayBufferView;
13
+
14
+ /**
15
+ * React Native Blob constructor options
16
+ * Extends standard BlobPropertyBag with platform-specific properties
17
+ */
18
+ export interface ReactNativeBlobOptions {
19
+ /** MIME type of the blob */
20
+ type?: string;
21
+ /** Platform-specific: file path for optimization (React Native only) */
22
+ path?: string;
23
+ }
24
+
25
+ /**
26
+ * React Native Blob constructor type
27
+ * Unlike browser Blob, accepts BufferSource in the parts array
28
+ */
29
+ export interface ReactNativeBlobConstructor {
30
+ new (
31
+ parts?: Array<BufferSource | Blob | string>,
32
+ options?: ReactNativeBlobOptions,
33
+ ): Blob;
34
+ prototype: Blob;
35
+ }
36
+
37
+ /**
38
+ * Type guard to check if a value is ArrayBuffer
39
+ */
40
+ export function isArrayBuffer(value: unknown): value is ArrayBuffer {
41
+ return value instanceof ArrayBuffer;
42
+ }
43
+
44
+ /**
45
+ * Type guard to check if a value is ArrayBufferView (typed array)
46
+ */
47
+ export function isArrayBufferView(value: unknown): value is ArrayBufferView {
48
+ return (
49
+ value !== null &&
50
+ typeof value === "object" &&
51
+ "buffer" in value &&
52
+ value.buffer instanceof ArrayBuffer
53
+ );
54
+ }
55
+
56
+ /**
57
+ * Type guard to check if a value is BufferSource
58
+ */
59
+ export function isBufferSource(value: unknown): value is BufferSource {
60
+ return isArrayBuffer(value) || isArrayBufferView(value);
61
+ }
62
+
63
+ /**
64
+ * Type guard to check if we're in React Native environment
65
+ * (checks for global.navigator.product === 'ReactNative')
66
+ */
67
+ export function isReactNativeEnvironment(): boolean {
68
+ return (
69
+ typeof global !== "undefined" &&
70
+ typeof global.navigator !== "undefined" &&
71
+ global.navigator.product === "ReactNative"
72
+ );
73
+ }
74
+
75
+ /**
76
+ * Create a Blob from BufferSource with proper typing for React Native
77
+ *
78
+ * This function handles the platform differences between browser and React Native Blob APIs.
79
+ * React Native's Blob constructor accepts BufferSource directly, while browser Blob requires
80
+ * conversion to Uint8Array first in some cases.
81
+ *
82
+ * @param data - ArrayBuffer or typed array to convert to Blob
83
+ * @param options - Blob options including MIME type
84
+ * @returns Platform-appropriate Blob instance
85
+ *
86
+ * @example
87
+ * ```typescript
88
+ * const arrayBuffer = await fileSystemProvider.readFile(uri);
89
+ * const blob = createBlobFromBuffer(arrayBuffer, {
90
+ * type: 'image/jpeg'
91
+ * });
92
+ * ```
93
+ */
94
+ export function createBlobFromBuffer(
95
+ data: BufferSource,
96
+ options?: ReactNativeBlobOptions,
97
+ ): Blob {
98
+ // Convert ArrayBuffer to Uint8Array for consistent handling
99
+ const uint8Array = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
100
+
101
+ // In React Native, Blob constructor accepts BufferSource
102
+ // Cast to ReactNativeBlobConstructor to use the correct signature
103
+ const BlobConstructor = Blob as unknown as ReactNativeBlobConstructor;
104
+ return new BlobConstructor([uint8Array], options);
105
+ }