@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,24 +1,36 @@
1
+ import {
2
+ FlowManager,
3
+ type FlowUploadState,
4
+ type FlowUploadStatus,
5
+ type InternalFlowUploadOptions,
6
+ type UploadistaEvent,
7
+ } from "@uploadista/client-core";
8
+ import { EventType, type FlowEvent } from "@uploadista/core/flow";
1
9
  import type { UploadFile } from "@uploadista/core/types";
2
- import { useCallback, useRef, useState } from "react";
10
+ import { UploadEventType } from "@uploadista/core/types";
11
+ import { useCallback, useEffect, useRef, useState } from "react";
3
12
  import type { FilePickResult, UseFlowUploadOptions } from "../types";
13
+ import { createBlobFromBuffer } from "../types/platform-types";
4
14
  import { useUploadistaContext } from "./use-uploadista-context";
5
15
 
6
- export type FlowUploadStatus =
7
- | "idle"
8
- | "uploading"
9
- | "processing"
10
- | "success"
11
- | "error"
12
- | "aborted";
16
+ // Re-export types from core for convenience
17
+ export type { FlowUploadState, FlowUploadStatus };
13
18
 
14
- export interface FlowUploadState {
15
- status: FlowUploadStatus;
16
- progress: number;
17
- bytesUploaded: number;
18
- totalBytes: number | null;
19
- jobId: string | null;
20
- error: Error | null;
21
- result: unknown | null;
19
+ /**
20
+ * Type guard to check if an event is a flow event
21
+ */
22
+ function isFlowEvent(event: UploadistaEvent): event is FlowEvent {
23
+ const flowEvent = event as FlowEvent;
24
+ return (
25
+ flowEvent.eventType === EventType.FlowStart ||
26
+ flowEvent.eventType === EventType.FlowEnd ||
27
+ flowEvent.eventType === EventType.FlowError ||
28
+ flowEvent.eventType === EventType.NodeStart ||
29
+ flowEvent.eventType === EventType.NodeEnd ||
30
+ flowEvent.eventType === EventType.NodePause ||
31
+ flowEvent.eventType === EventType.NodeResume ||
32
+ flowEvent.eventType === EventType.NodeError
33
+ );
22
34
  }
23
35
 
24
36
  const initialState: FlowUploadState = {
@@ -26,9 +38,13 @@ const initialState: FlowUploadState = {
26
38
  progress: 0,
27
39
  bytesUploaded: 0,
28
40
  totalBytes: null,
29
- jobId: null,
30
41
  error: null,
31
42
  result: null,
43
+ jobId: null,
44
+ flowStarted: false,
45
+ currentNodeName: null,
46
+ currentNodeType: null,
47
+ flowOutputs: null,
32
48
  };
33
49
 
34
50
  /**
@@ -71,34 +87,115 @@ const initialState: FlowUploadState = {
71
87
  * ```
72
88
  */
73
89
  export function useFlowUpload(options: UseFlowUploadOptions) {
74
- const { client, fileSystemProvider } = useUploadistaContext();
90
+ const context = useUploadistaContext();
91
+ const { client, fileSystemProvider } = context;
75
92
  const [state, setState] = useState<FlowUploadState>(initialState);
76
- const abortControllerRef = useRef<{ abort: () => void } | null>(null);
93
+ const managerRef = useRef<FlowManager<Blob, UploadFile> | null>(null);
77
94
  const lastFileRef = useRef<FilePickResult | null>(null);
78
95
 
79
- const updateState = useCallback((update: Partial<FlowUploadState>) => {
80
- setState((prev) => ({ ...prev, ...update }));
81
- }, []);
96
+ // Create FlowManager instance once (only recreate if client changes)
97
+ // Note: We don't include options in deps to avoid recreating the manager on every render
98
+ // The manager will use the latest options values through closures
99
+ useEffect(() => {
100
+ managerRef.current = new FlowManager(
101
+ async (
102
+ blob: Blob,
103
+ flowConfig: {
104
+ flowId: string;
105
+ storageId: string;
106
+ outputNodeId?: string;
107
+ metadata?: Record<string, string>;
108
+ },
109
+ internalOptions: InternalFlowUploadOptions,
110
+ ) => {
111
+ const result = await client.uploadWithFlow(blob, flowConfig, {
112
+ onJobStart: internalOptions.onJobStart,
113
+ onProgress: internalOptions.onProgress,
114
+ onChunkComplete: internalOptions.onChunkComplete,
115
+ onSuccess: internalOptions.onSuccess,
116
+ onError: internalOptions.onError,
117
+ onShouldRetry: internalOptions.onShouldRetry,
118
+ });
119
+ // Return only abort and pause (ignore jobId and return value)
120
+ return {
121
+ abort: async () => {
122
+ await result.abort();
123
+ },
124
+ pause: async () => {
125
+ await result.pause();
126
+ // Ignore the FlowJob return value
127
+ },
128
+ };
129
+ },
130
+ {
131
+ onStateChange: setState,
132
+ onProgress: options.onProgress
133
+ ? (_uploadId, bytesUploaded, totalBytes) => {
134
+ const progress = totalBytes
135
+ ? Math.round((bytesUploaded / totalBytes) * 100)
136
+ : 0;
137
+ options.onProgress?.(progress, bytesUploaded, totalBytes);
138
+ }
139
+ : undefined,
140
+ onChunkComplete: options.onChunkComplete,
141
+ onSuccess: options.onSuccess,
142
+ onError: options.onError,
143
+ },
144
+ {
145
+ flowConfig: {
146
+ flowId: options.flowId,
147
+ storageId: options.storageId,
148
+ outputNodeId: options.outputNodeId,
149
+ metadata: options.metadata as Record<string, string> | undefined,
150
+ },
151
+ onChunkComplete: options.onChunkComplete,
152
+ onSuccess: options.onSuccess,
153
+ onError: options.onError,
154
+ },
155
+ );
82
156
 
83
- const reset = useCallback(() => {
84
- if (abortControllerRef.current) {
85
- abortControllerRef.current.abort();
86
- abortControllerRef.current = null;
87
- }
88
- setState(initialState);
89
- lastFileRef.current = null;
90
- }, []);
157
+ return () => {
158
+ managerRef.current?.cleanup();
159
+ };
160
+ // eslint-disable-next-line react-hooks/exhaustive-deps
161
+ }, [client]);
91
162
 
92
- const abort = useCallback(() => {
93
- if (abortControllerRef.current) {
94
- abortControllerRef.current.abort();
95
- abortControllerRef.current = null;
96
- }
163
+ // Subscribe to events and forward them to the manager
164
+ useEffect(() => {
165
+ const unsubscribe = context.subscribeToEvents(
166
+ (event: UploadistaEvent) => {
167
+ // Handle flow events
168
+ if (isFlowEvent(event)) {
169
+ managerRef.current?.handleFlowEvent(event);
170
+ return;
171
+ }
97
172
 
98
- updateState({
99
- status: "aborted",
100
- });
101
- }, [updateState]);
173
+ // Handle upload progress events for this job's upload
174
+ const uploadEvent = event as {
175
+ type: string;
176
+ data?: { id: string; progress: number; total: number };
177
+ flow?: { jobId: string };
178
+ };
179
+
180
+ if (
181
+ uploadEvent.type === UploadEventType.UPLOAD_PROGRESS &&
182
+ uploadEvent.flow?.jobId === managerRef.current?.getJobId() &&
183
+ uploadEvent.data
184
+ ) {
185
+ const { progress: bytesUploaded, total: totalBytes } =
186
+ uploadEvent.data;
187
+
188
+ managerRef.current?.handleUploadProgress(
189
+ uploadEvent.data.id,
190
+ bytesUploaded,
191
+ totalBytes,
192
+ );
193
+ }
194
+ },
195
+ );
196
+
197
+ return unsubscribe;
198
+ }, [context]);
102
199
 
103
200
  const upload = useCallback(
104
201
  async (file: FilePickResult) => {
@@ -109,127 +206,40 @@ export function useFlowUpload(options: UseFlowUploadOptions) {
109
206
 
110
207
  // Handle picker error
111
208
  if (file.status === "error") {
112
- updateState({
113
- status: "error",
114
- error: file.error,
115
- });
116
209
  options.onError?.(file.error);
117
210
  return;
118
211
  }
119
212
 
120
- // Reset any previous state
121
- setState({
122
- ...initialState,
123
- status: "uploading",
124
- totalBytes: file.data.size,
125
- });
126
-
127
213
  lastFileRef.current = file;
128
214
 
129
215
  try {
130
216
  // Read file content
131
217
  const fileContent = await fileSystemProvider.readFile(file.data.uri);
132
218
 
133
- // Create a Blob from the file content
134
- // Convert ArrayBuffer to Uint8Array for better compatibility
135
- const data =
136
- fileContent instanceof ArrayBuffer
137
- ? new Uint8Array(fileContent)
138
- : fileContent;
139
- // Note: Using any cast here because React Native Blob accepts BufferSource
140
- // but TypeScript's lib.dom.d.ts Blob type doesn't include it
141
- // biome-ignore lint/suspicious/noExplicitAny: React Native Blob accepts BufferSource
142
- const blob = new Blob([data as any], {
219
+ // Create a Blob from the file content using platform-aware utility
220
+ // Handles differences between React Native and browser Blob APIs
221
+ const blob = createBlobFromBuffer(fileContent, {
143
222
  type: file.data.mimeType || "application/octet-stream",
144
- // biome-ignore lint/suspicious/noExplicitAny: BlobPropertyBag type differs by platform
145
- } as any);
146
-
147
- // use the Blob (for React Native)
148
- const uploadInput = blob;
149
-
150
- // Start the flow upload using the client
151
- const uploadPromise = client.uploadWithFlow(
152
- uploadInput,
153
- {
154
- flowId: options.flowId,
155
- storageId: options.storageId,
156
- outputNodeId: options.outputNodeId,
157
- metadata: options.metadata as Record<string, string> | undefined,
158
- },
159
- {
160
- onJobStart: () => {
161
- updateState({
162
- status: "processing",
163
- });
164
- },
165
-
166
- onProgress: (
167
- _uploadId: string,
168
- bytesUploaded: number,
169
- totalBytes: number | null,
170
- ) => {
171
- const progress = totalBytes
172
- ? Math.round((bytesUploaded / totalBytes) * 100)
173
- : 0;
174
-
175
- updateState({
176
- progress,
177
- bytesUploaded,
178
- totalBytes,
179
- });
180
-
181
- options.onProgress?.(progress, bytesUploaded, totalBytes);
182
- },
183
-
184
- onChunkComplete: (
185
- chunkSize: number,
186
- bytesAccepted: number,
187
- bytesTotal: number | null,
188
- ) => {
189
- options.onChunkComplete?.(chunkSize, bytesAccepted, bytesTotal);
190
- },
191
-
192
- onSuccess: (result: UploadFile) => {
193
- updateState({
194
- status: "success",
195
- result,
196
- progress: 100,
197
- bytesUploaded: result.size || 0,
198
- totalBytes: result.size || null,
199
- });
200
-
201
- options.onSuccess?.(result);
202
- abortControllerRef.current = null;
203
- },
204
-
205
- onError: (error: Error) => {
206
- updateState({
207
- status: "error",
208
- error,
209
- });
210
-
211
- options.onError?.(error);
212
- abortControllerRef.current = null;
213
- },
214
- },
215
- );
216
-
217
- // Handle the promise to get the abort controller
218
- const controller = await uploadPromise;
219
- abortControllerRef.current = controller;
220
- } catch (error) {
221
- updateState({
222
- status: "error",
223
- error: error as Error,
224
223
  });
225
224
 
225
+ // Start the upload using the manager
226
+ await managerRef.current?.upload(blob);
227
+ } catch (error) {
226
228
  options.onError?.(error as Error);
227
- abortControllerRef.current = null;
228
229
  }
229
230
  },
230
- [client, fileSystemProvider, options, updateState],
231
+ [fileSystemProvider, options],
231
232
  );
232
233
 
234
+ const reset = useCallback(() => {
235
+ managerRef.current?.reset();
236
+ lastFileRef.current = null;
237
+ }, []);
238
+
239
+ const abort = useCallback(() => {
240
+ managerRef.current?.abort();
241
+ }, []);
242
+
233
243
  const retry = useCallback(() => {
234
244
  if (
235
245
  lastFileRef.current &&
@@ -239,6 +249,7 @@ export function useFlowUpload(options: UseFlowUploadOptions) {
239
249
  }
240
250
  }, [upload, state.status]);
241
251
 
252
+ // Derive computed values from state (reactive to state changes)
242
253
  const isActive =
243
254
  state.status === "uploading" || state.status === "processing";
244
255
  const canRetry =
@@ -52,9 +52,7 @@ export function useGalleryUpload(options?: UseGalleryUploadOptions) {
52
52
 
53
53
  // Success - add file and start upload
54
54
  const itemIds = uploadHook.addFiles([result]);
55
- console.log("starting uploads", itemIds);
56
55
  await uploadHook.startUploads(itemIds);
57
- console.log("uploads started", itemIds);
58
56
 
59
57
  return itemIds;
60
58
  }, [