@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.
- package/dist/index.d.mts +26 -45
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -5
- package/src/components/UploadList.tsx +1 -1
- package/src/hooks/use-flow-upload.ts +153 -142
- package/src/hooks/use-gallery-upload.ts +0 -2
- package/src/hooks/use-upload.ts +94 -229
- package/src/types/index.ts +1 -0
- package/src/types/platform-types.ts +105 -0
package/src/hooks/use-upload.ts
CHANGED
|
@@ -1,25 +1,20 @@
|
|
|
1
|
-
import type {
|
|
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
|
|
9
|
-
|
|
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
|
-
|
|
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
|
|
144
|
-
* const file = await
|
|
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={
|
|
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
|
|
164
|
-
useUploadistaContext();
|
|
161
|
+
const { client, fileSystemProvider } = useUploadistaContext();
|
|
165
162
|
const [state, setState] = useState<UploadState>(initialState);
|
|
166
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
316
|
-
|
|
181
|
+
// Upload the Blob
|
|
182
|
+
return client.upload(blob, opts);
|
|
317
183
|
}
|
|
318
|
-
},
|
|
319
|
-
[client, fileSystemProvider, options, updateState],
|
|
320
|
-
);
|
|
321
184
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
-
//
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
222
|
+
// Reset function
|
|
223
|
+
const reset = useCallback(() => {
|
|
224
|
+
managerRef.current?.reset();
|
|
225
|
+
lastFileRef.current = null;
|
|
226
|
+
}, []);
|
|
375
227
|
|
|
376
|
-
|
|
377
|
-
|
|
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
|
-
|
|
382
|
-
|
|
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
|
}
|
package/src/types/index.ts
CHANGED
|
@@ -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
|
+
}
|