@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,411 @@
1
+ import type {
2
+ BrowserUploadInput,
3
+ ChunkMetrics,
4
+ PerformanceInsights,
5
+ UploadistaEvent,
6
+ UploadSessionMetrics,
7
+ } from "@uploadista/client-browser";
8
+ import type { UploadFile } from "@uploadista/core/types";
9
+ import { UploadEventType } from "@uploadista/core/types";
10
+ import { useCallback, useEffect, useRef, useState } from "react";
11
+ import { useUploadistaContext } from "../components/uploadista-provider";
12
+
13
+ export type UploadStatus =
14
+ | "idle"
15
+ | "uploading"
16
+ | "success"
17
+ | "error"
18
+ | "aborted";
19
+
20
+ export interface UploadState {
21
+ status: UploadStatus;
22
+ progress: number;
23
+ bytesUploaded: number;
24
+ totalBytes: number | null;
25
+ error: Error | null;
26
+ result: UploadFile | null;
27
+ }
28
+
29
+ export interface UseUploadOptions {
30
+ /**
31
+ * Upload metadata to attach to the file
32
+ */
33
+ metadata?: Record<string, string>;
34
+
35
+ /**
36
+ * Whether to defer the upload size calculation
37
+ */
38
+ uploadLengthDeferred?: boolean;
39
+
40
+ /**
41
+ * Manual upload size override
42
+ */
43
+ uploadSize?: number;
44
+
45
+ /**
46
+ * Called when upload progress updates
47
+ */
48
+ onProgress?: (
49
+ progress: number,
50
+ bytesUploaded: number,
51
+ totalBytes: number | null,
52
+ ) => void;
53
+
54
+ /**
55
+ * Called when a chunk completes
56
+ */
57
+ onChunkComplete?: (
58
+ chunkSize: number,
59
+ bytesAccepted: number,
60
+ bytesTotal: number | null,
61
+ ) => void;
62
+
63
+ /**
64
+ * Called when upload succeeds
65
+ */
66
+ onSuccess?: (result: UploadFile) => void;
67
+
68
+ /**
69
+ * Called when upload fails
70
+ */
71
+ onError?: (error: Error) => void;
72
+
73
+ /**
74
+ * Called when upload is aborted
75
+ */
76
+ onAbort?: () => void;
77
+
78
+ /**
79
+ * Custom retry logic
80
+ */
81
+ onShouldRetry?: (error: Error, retryAttempt: number) => boolean;
82
+ }
83
+
84
+ export interface UploadMetrics {
85
+ /**
86
+ * Get performance insights from the upload client
87
+ */
88
+ getInsights: () => PerformanceInsights;
89
+
90
+ /**
91
+ * Export detailed metrics from the upload client
92
+ */
93
+ exportMetrics: () => {
94
+ session: Partial<UploadSessionMetrics>;
95
+ chunks: ChunkMetrics[];
96
+ insights: PerformanceInsights;
97
+ };
98
+
99
+ /**
100
+ * Get current network metrics
101
+ */
102
+ getNetworkMetrics: () => unknown;
103
+
104
+ /**
105
+ * Get current network condition
106
+ */
107
+ getNetworkCondition: () => unknown;
108
+
109
+ /**
110
+ * Reset all metrics
111
+ */
112
+ resetMetrics: () => void;
113
+ }
114
+
115
+ export interface UseUploadReturn {
116
+ /**
117
+ * Current upload state
118
+ */
119
+ state: UploadState;
120
+
121
+ /**
122
+ * Start uploading a file
123
+ */
124
+ upload: (file: BrowserUploadInput) => void;
125
+
126
+ /**
127
+ * Abort the current upload
128
+ */
129
+ abort: () => void;
130
+
131
+ /**
132
+ * Reset the upload state to idle
133
+ */
134
+ reset: () => void;
135
+
136
+ /**
137
+ * Retry the last failed upload
138
+ */
139
+ retry: () => void;
140
+
141
+ /**
142
+ * Whether an upload is currently active
143
+ */
144
+ isUploading: boolean;
145
+
146
+ /**
147
+ * Whether the upload can be retried
148
+ */
149
+ canRetry: boolean;
150
+
151
+ /**
152
+ * Upload metrics and performance insights from the client
153
+ */
154
+ metrics: UploadMetrics;
155
+ }
156
+
157
+ const initialState: UploadState = {
158
+ status: "idle",
159
+ progress: 0,
160
+ bytesUploaded: 0,
161
+ totalBytes: null,
162
+ error: null,
163
+ result: null,
164
+ };
165
+
166
+ /**
167
+ * React hook for managing individual file uploads with full state management.
168
+ * Provides upload progress tracking, error handling, abort functionality, and retry logic.
169
+ *
170
+ * Must be used within an UploadistaProvider.
171
+ *
172
+ * @param options - Upload configuration and event handlers
173
+ * @returns Upload state and control methods
174
+ *
175
+ * @example
176
+ * ```tsx
177
+ * function MyComponent() {
178
+ * const upload = useUpload({
179
+ * onSuccess: (result) => console.log('Upload complete:', result),
180
+ * onError: (error) => console.error('Upload failed:', error),
181
+ * onProgress: (progress) => console.log('Progress:', progress + '%'),
182
+ * });
183
+ *
184
+ * return (
185
+ * <div>
186
+ * <input
187
+ * type="file"
188
+ * onChange={(e) => {
189
+ * const file = e.target.files?.[0];
190
+ * if (file) upload.upload(file);
191
+ * }}
192
+ * />
193
+ * {upload.isUploading && <div>Progress: {upload.state.progress}%</div>}
194
+ * {upload.state.error && <div>Error: {upload.state.error.message}</div>}
195
+ * {upload.canRetry && <button onClick={upload.retry}>Retry</button>}
196
+ * <button onClick={upload.abort} disabled={!upload.isUploading}>Abort</button>
197
+ * </div>
198
+ * );
199
+ * }
200
+ * ```
201
+ */
202
+ export function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
203
+ const uploadClient = useUploadistaContext();
204
+ const [state, setState] = useState<UploadState>(initialState);
205
+ const abortControllerRef = useRef<{ abort: () => void } | null>(null);
206
+ const lastFileRef = useRef<BrowserUploadInput | null>(null);
207
+
208
+ const updateState = useCallback((update: Partial<UploadState>) => {
209
+ setState((prev) => ({ ...prev, ...update }));
210
+ }, []);
211
+
212
+ const reset = useCallback(() => {
213
+ if (abortControllerRef.current) {
214
+ abortControllerRef.current.abort();
215
+ abortControllerRef.current = null;
216
+ }
217
+ setState(initialState);
218
+ lastFileRef.current = null;
219
+ }, []);
220
+
221
+ const abort = useCallback(() => {
222
+ if (abortControllerRef.current) {
223
+ abortControllerRef.current.abort();
224
+ abortControllerRef.current = null;
225
+ }
226
+
227
+ updateState({
228
+ status: "aborted",
229
+ });
230
+
231
+ options.onAbort?.();
232
+ }, [options, updateState]);
233
+
234
+ const upload = useCallback(
235
+ (file: BrowserUploadInput) => {
236
+ // Reset any previous state but keep the file reference for retries
237
+ setState({
238
+ ...initialState,
239
+ status: "uploading",
240
+ totalBytes: file instanceof File ? file.size : null,
241
+ });
242
+
243
+ lastFileRef.current = file;
244
+
245
+ // Start the upload and handle the promise
246
+ const uploadPromise = uploadClient.client.upload(file, {
247
+ metadata: options.metadata,
248
+ uploadLengthDeferred: options.uploadLengthDeferred,
249
+ uploadSize: options.uploadSize,
250
+
251
+ onStart: ({ uploadId }) => {
252
+ currentUploadIdRef.current = uploadId;
253
+ },
254
+
255
+ onProgress: (
256
+ _uploadId: string,
257
+ bytesUploaded: number,
258
+ totalBytes: number | null,
259
+ ) => {
260
+ const progress = totalBytes
261
+ ? Math.round((bytesUploaded / totalBytes) * 100)
262
+ : 0;
263
+
264
+ updateState({
265
+ progress,
266
+ bytesUploaded,
267
+ totalBytes,
268
+ });
269
+
270
+ options.onProgress?.(progress, bytesUploaded, totalBytes);
271
+ },
272
+
273
+ onChunkComplete: (
274
+ chunkSize: number,
275
+ bytesAccepted: number,
276
+ bytesTotal: number | null,
277
+ ) => {
278
+ options.onChunkComplete?.(chunkSize, bytesAccepted, bytesTotal);
279
+ },
280
+
281
+ onSuccess: (result: UploadFile) => {
282
+ updateState({
283
+ status: "success",
284
+ result,
285
+ progress: 100,
286
+ bytesUploaded: result.size || 0,
287
+ totalBytes: result.size || null,
288
+ });
289
+
290
+ options.onSuccess?.(result);
291
+ abortControllerRef.current = null;
292
+ },
293
+
294
+ onError: (error: Error) => {
295
+ updateState({
296
+ status: "error",
297
+ error,
298
+ });
299
+
300
+ options.onError?.(error);
301
+ abortControllerRef.current = null;
302
+ },
303
+
304
+ onShouldRetry: options.onShouldRetry,
305
+ });
306
+
307
+ // Handle the promise to get the abort controller
308
+ uploadPromise
309
+ .then((controller) => {
310
+ abortControllerRef.current = controller;
311
+ })
312
+ .catch((error) => {
313
+ updateState({
314
+ status: "error",
315
+ error: error as Error,
316
+ });
317
+
318
+ options.onError?.(error as Error);
319
+ abortControllerRef.current = null;
320
+ });
321
+ },
322
+ [uploadClient, options, updateState],
323
+ );
324
+
325
+ const retry = useCallback(() => {
326
+ if (
327
+ lastFileRef.current &&
328
+ (state.status === "error" || state.status === "aborted")
329
+ ) {
330
+ upload(lastFileRef.current);
331
+ }
332
+ }, [upload, state.status]);
333
+
334
+ // Store current upload ID for event matching
335
+ const currentUploadIdRef = useRef<string | null>(null);
336
+
337
+ // Subscribe to events from context (WebSocket events)
338
+ useEffect(() => {
339
+ const unsubscribe = uploadClient.subscribeToEvents(
340
+ (event: UploadistaEvent) => {
341
+ // Handle upload progress events
342
+ const uploadEvent = event as {
343
+ type: string;
344
+ data?: { id: string; progress: number; total: number };
345
+ };
346
+ if (
347
+ uploadEvent.type === UploadEventType.UPLOAD_PROGRESS &&
348
+ uploadEvent.data
349
+ ) {
350
+ const {
351
+ id: uploadId,
352
+ progress: bytesUploaded,
353
+ total: totalBytes,
354
+ } = uploadEvent.data;
355
+
356
+ if (uploadId !== currentUploadIdRef.current) {
357
+ return;
358
+ }
359
+
360
+ // Update state for this upload
361
+ // Note: We update for all uploads since we don't track upload IDs in single upload mode
362
+ const progress = totalBytes
363
+ ? Math.round((bytesUploaded / totalBytes) * 100)
364
+ : 0;
365
+
366
+ setState((prev) => {
367
+ // Only update if we're currently uploading
368
+ if (prev.status === "uploading") {
369
+ return {
370
+ ...prev,
371
+ progress,
372
+ bytesUploaded,
373
+ totalBytes,
374
+ };
375
+ }
376
+ return prev;
377
+ });
378
+
379
+ options.onProgress?.(progress, bytesUploaded, totalBytes);
380
+ }
381
+ },
382
+ );
383
+
384
+ return unsubscribe;
385
+ }, [uploadClient, options]);
386
+
387
+ const isUploading = state.status === "uploading";
388
+ const canRetry =
389
+ (state.status === "error" || state.status === "aborted") &&
390
+ lastFileRef.current !== null;
391
+
392
+ // Create metrics object that delegates to the upload client
393
+ const metrics: UploadMetrics = {
394
+ getInsights: () => uploadClient.client.getChunkingInsights(),
395
+ exportMetrics: () => uploadClient.client.exportMetrics(),
396
+ getNetworkMetrics: () => uploadClient.client.getNetworkMetrics(),
397
+ getNetworkCondition: () => uploadClient.client.getNetworkCondition(),
398
+ resetMetrics: () => uploadClient.client.resetMetrics(),
399
+ };
400
+
401
+ return {
402
+ state,
403
+ upload,
404
+ abort,
405
+ reset,
406
+ retry,
407
+ isUploading,
408
+ canRetry,
409
+ metrics,
410
+ };
411
+ }
@@ -0,0 +1,145 @@
1
+ import {
2
+ createUploadistaClient,
3
+ type UploadistaClientOptions,
4
+ } from "@uploadista/client-browser";
5
+ import { useMemo, useRef } from "react";
6
+
7
+ /**
8
+ * Configuration options for the uploadista client hook.
9
+ * Extends the base client options with React-specific behavior.
10
+ *
11
+ * @property onEvent - Global event handler for all upload and flow events
12
+ * @property baseUrl - API base URL for uploads
13
+ * @property storageId - Default storage identifier
14
+ * @property chunkSize - Size of upload chunks in bytes
15
+ * @property storeFingerprintForResuming - Enable resumable uploads
16
+ * @property retryDelays - Array of retry delays in milliseconds
17
+ * @property parallelUploads - Maximum number of parallel uploads
18
+ * @property uploadStrategy - Upload strategy (sequential, parallel, adaptive)
19
+ * @property smartChunking - Enable dynamic chunk size adjustment
20
+ * @property networkMonitoring - Enable network condition monitoring
21
+ */
22
+ export interface UseUploadistaClientOptions extends UploadistaClientOptions {
23
+ /**
24
+ * Global event handler for all upload and flow events from this client
25
+ */
26
+ onEvent?: UploadistaClientOptions["onEvent"];
27
+ }
28
+
29
+ /**
30
+ * Return value from the useUploadistaClient hook.
31
+ *
32
+ * @property client - Configured uploadista client instance (stable across re-renders)
33
+ * @property config - Current client configuration options
34
+ */
35
+ export interface UseUploadistaClientReturn {
36
+ /**
37
+ * The uploadista client instance
38
+ */
39
+ client: ReturnType<typeof createUploadistaClient>;
40
+
41
+ /**
42
+ * Current configuration of the client
43
+ */
44
+ config: UseUploadistaClientOptions;
45
+ }
46
+
47
+ /**
48
+ * React hook for creating and managing an uploadista client instance.
49
+ * The client instance is memoized and stable across re-renders, only being
50
+ * recreated when configuration options change.
51
+ *
52
+ * This hook is typically used internally by UploadistaProvider, but can be
53
+ * used directly for advanced use cases requiring multiple client instances.
54
+ *
55
+ * @param options - Upload client configuration options
56
+ * @returns Object containing the stable client instance and current configuration
57
+ *
58
+ * @example
59
+ * ```tsx
60
+ * // Basic client setup
61
+ * function MyUploadComponent() {
62
+ * const { client, config } = useUploadistaClient({
63
+ * baseUrl: 'https://api.example.com',
64
+ * storageId: 'default-storage',
65
+ * chunkSize: 1024 * 1024, // 1MB chunks
66
+ * storeFingerprintForResuming: true,
67
+ * onEvent: (event) => {
68
+ * console.log('Upload event:', event);
69
+ * }
70
+ * });
71
+ *
72
+ * // Use client directly
73
+ * const handleUpload = async (file: File) => {
74
+ * await client.upload(file, {
75
+ * onSuccess: (result) => console.log('Uploaded:', result),
76
+ * onError: (error) => console.error('Failed:', error),
77
+ * });
78
+ * };
79
+ *
80
+ * return <FileUploader onUpload={handleUpload} />;
81
+ * }
82
+ *
83
+ * // Advanced: Multiple clients with different configurations
84
+ * function MultiClientComponent() {
85
+ * // Client for image uploads
86
+ * const imageClient = useUploadistaClient({
87
+ * baseUrl: 'https://images.example.com',
88
+ * storageId: 'images',
89
+ * chunkSize: 2 * 1024 * 1024, // 2MB for images
90
+ * });
91
+ *
92
+ * // Client for document uploads
93
+ * const docClient = useUploadistaClient({
94
+ * baseUrl: 'https://docs.example.com',
95
+ * storageId: 'documents',
96
+ * chunkSize: 512 * 1024, // 512KB for documents
97
+ * });
98
+ *
99
+ * return (
100
+ * <div>
101
+ * <ImageUploader client={imageClient.client} />
102
+ * <DocumentUploader client={docClient.client} />
103
+ * </div>
104
+ * );
105
+ * }
106
+ * ```
107
+ *
108
+ * @see {@link UploadistaProvider} for the recommended way to provide client context
109
+ */
110
+ export function useUploadistaClient(
111
+ options: UseUploadistaClientOptions,
112
+ ): UseUploadistaClientReturn {
113
+ // Store the options in a ref to enable stable dependency checking
114
+ const optionsRef = useRef<UseUploadistaClientOptions>(options);
115
+
116
+ // Update ref on each render but only create new client when essential deps change
117
+ optionsRef.current = options;
118
+
119
+ // Create client instance with stable identity
120
+ const client = useMemo(() => {
121
+ return createUploadistaClient({
122
+ baseUrl: options.baseUrl,
123
+ storageId: options.storageId,
124
+ uploadistaBasePath: options.uploadistaBasePath,
125
+ chunkSize: options.chunkSize,
126
+ storeFingerprintForResuming: options.storeFingerprintForResuming,
127
+ retryDelays: options.retryDelays,
128
+ parallelUploads: options.parallelUploads,
129
+ parallelChunkSize: options.parallelChunkSize,
130
+ uploadStrategy: options.uploadStrategy,
131
+ smartChunking: options.smartChunking,
132
+ networkMonitoring: options.networkMonitoring,
133
+ uploadMetrics: options.uploadMetrics,
134
+ connectionPooling: options.connectionPooling,
135
+ // logger: options.logger,
136
+ auth: options.auth,
137
+ onEvent: options.onEvent,
138
+ });
139
+ }, [options]);
140
+
141
+ return {
142
+ client,
143
+ config: options,
144
+ };
145
+ }
package/src/index.ts ADDED
@@ -0,0 +1,87 @@
1
+ // Flow Upload Hooks
2
+
3
+ export type {
4
+ FlowUploadListProps,
5
+ FlowUploadListRenderProps,
6
+ SimpleFlowUploadListItemProps,
7
+ SimpleFlowUploadListProps,
8
+ } from "./components/flow-upload-list";
9
+ export {
10
+ FlowUploadList,
11
+ SimpleFlowUploadList,
12
+ SimpleFlowUploadListItem,
13
+ } from "./components/flow-upload-list";
14
+ // Flow Upload Components
15
+ export type {
16
+ FlowUploadZoneProps,
17
+ FlowUploadZoneRenderProps,
18
+ SimpleFlowUploadZoneProps,
19
+ } from "./components/flow-upload-zone";
20
+ export {
21
+ FlowUploadZone,
22
+ SimpleFlowUploadZone,
23
+ } from "./components/flow-upload-zone";
24
+ export type {
25
+ FlowUploadState,
26
+ FlowUploadStatus,
27
+ UseFlowUploadReturn,
28
+ } from "./hooks/use-flow-upload";
29
+ export { useFlowUpload } from "./hooks/use-flow-upload";
30
+ export type { UseMultiFlowUploadReturn } from "./hooks/use-multi-flow-upload";
31
+ export { useMultiFlowUpload } from "./hooks/use-multi-flow-upload";
32
+
33
+ // Flow Hooks
34
+
35
+ // Upload Hooks
36
+ export type {
37
+ SimpleUploadListItemProps,
38
+ UploadListProps,
39
+ UploadListRenderProps,
40
+ } from "./components/upload-list";
41
+ export { SimpleUploadListItem, UploadList } from "./components/upload-list";
42
+ export type {
43
+ SimpleUploadZoneProps,
44
+ UploadZoneProps,
45
+ UploadZoneRenderProps,
46
+ } from "./components/upload-zone";
47
+ export { SimpleUploadZone, UploadZone } from "./components/upload-zone";
48
+
49
+ // Components
50
+ export {
51
+ UploadistaProvider,
52
+ useUploadistaContext,
53
+ } from "./components/uploadista-provider";
54
+ export type {
55
+ DragDropOptions,
56
+ DragDropState,
57
+ UseDragDropReturn,
58
+ } from "./hooks/use-drag-drop";
59
+ export { useDragDrop } from "./hooks/use-drag-drop";
60
+
61
+ export type {
62
+ MultiUploadOptions,
63
+ MultiUploadState,
64
+ UploadItem,
65
+ UseMultiUploadReturn,
66
+ } from "./hooks/use-multi-upload";
67
+ export { useMultiUpload } from "./hooks/use-multi-upload";
68
+ export type {
69
+ UploadState,
70
+ UploadStatus,
71
+ UseUploadOptions,
72
+ UseUploadReturn,
73
+ } from "./hooks/use-upload";
74
+ export { useUpload } from "./hooks/use-upload";
75
+ export type {
76
+ FileUploadMetrics,
77
+ UploadMetrics,
78
+ UseUploadMetricsOptions,
79
+ UseUploadMetricsReturn,
80
+ } from "./hooks/use-upload-metrics";
81
+ export { useUploadMetrics } from "./hooks/use-upload-metrics";
82
+ // Types - Hooks
83
+ export type {
84
+ UseUploadistaClientOptions,
85
+ UseUploadistaClientReturn,
86
+ } from "./hooks/use-uploadista-client";
87
+ export { useUploadistaClient } from "./hooks/use-uploadista-client";
package/tsconfig.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "@uploadista/typescript-config/react-library.json",
3
+ "compilerOptions": {
4
+ "baseUrl": "./",
5
+ "paths": {
6
+ "@/*": ["./src/*"]
7
+ },
8
+ "typeRoots": ["../../node_modules/@types"],
9
+ "types": []
10
+ }
11
+ }