@uploadista/react-native-core 0.0.17-beta.8 → 0.0.17
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 +7 -7
- package/package.json +4 -4
- package/src/hooks/index.ts +10 -0
- package/src/hooks/use-flow.ts +576 -0
package/dist/index.d.mts
CHANGED
|
@@ -4,7 +4,7 @@ import * as _uploadista_client_core3 from "@uploadista/client-core";
|
|
|
4
4
|
import { Base64Service, ConnectionMetrics, ConnectionPoolConfig, ConnectionPoolConfig as ConnectionPoolConfig$1, DetailedConnectionMetrics, FileReaderService, FlowManager, FlowManagerCallbacks, FlowUploadOptions, FlowUploadState, FlowUploadStatus, HttpClient, IdGenerationService, ServiceContainer, ServiceContainer as ServiceContainer$1, StorageService, UploadState, UploadStatus, UploadistaClientOptions, UploadistaEvent } from "@uploadista/client-core";
|
|
5
5
|
import { TypedOutput } from "@uploadista/core/flow";
|
|
6
6
|
import { UploadFile } from "@uploadista/core/types";
|
|
7
|
-
import * as
|
|
7
|
+
import * as react_jsx_runtime1 from "react/jsx-runtime";
|
|
8
8
|
import * as _uploadista_core0 from "@uploadista/core";
|
|
9
9
|
|
|
10
10
|
//#region src/types/types.d.ts
|
|
@@ -306,7 +306,7 @@ declare function CameraUploadButton({
|
|
|
306
306
|
onError,
|
|
307
307
|
onCancel,
|
|
308
308
|
showProgress
|
|
309
|
-
}: CameraUploadButtonProps):
|
|
309
|
+
}: CameraUploadButtonProps): react_jsx_runtime1.JSX.Element;
|
|
310
310
|
//#endregion
|
|
311
311
|
//#region src/components/FileUploadButton.d.ts
|
|
312
312
|
interface FileUploadButtonProps {
|
|
@@ -337,7 +337,7 @@ declare function FileUploadButton({
|
|
|
337
337
|
onError,
|
|
338
338
|
onCancel,
|
|
339
339
|
showProgress
|
|
340
|
-
}: FileUploadButtonProps):
|
|
340
|
+
}: FileUploadButtonProps): react_jsx_runtime1.JSX.Element;
|
|
341
341
|
//#endregion
|
|
342
342
|
//#region src/components/GalleryUploadButton.d.ts
|
|
343
343
|
interface GalleryUploadButtonProps {
|
|
@@ -368,7 +368,7 @@ declare function GalleryUploadButton({
|
|
|
368
368
|
onError,
|
|
369
369
|
onCancel,
|
|
370
370
|
showProgress
|
|
371
|
-
}: GalleryUploadButtonProps):
|
|
371
|
+
}: GalleryUploadButtonProps): react_jsx_runtime1.JSX.Element;
|
|
372
372
|
//#endregion
|
|
373
373
|
//#region src/components/UploadList.d.ts
|
|
374
374
|
interface UploadListProps {
|
|
@@ -390,7 +390,7 @@ declare function UploadList({
|
|
|
390
390
|
onRemove,
|
|
391
391
|
onItemPress,
|
|
392
392
|
showRemoveButton
|
|
393
|
-
}: UploadListProps):
|
|
393
|
+
}: UploadListProps): react_jsx_runtime1.JSX.Element;
|
|
394
394
|
//#endregion
|
|
395
395
|
//#region src/components/UploadProgress.d.ts
|
|
396
396
|
interface UploadProgressProps {
|
|
@@ -405,7 +405,7 @@ interface UploadProgressProps {
|
|
|
405
405
|
declare function UploadProgress({
|
|
406
406
|
state,
|
|
407
407
|
label
|
|
408
|
-
}: UploadProgressProps):
|
|
408
|
+
}: UploadProgressProps): react_jsx_runtime1.JSX.Element;
|
|
409
409
|
//#endregion
|
|
410
410
|
//#region src/contexts/flow-manager-context.d.ts
|
|
411
411
|
/**
|
|
@@ -455,7 +455,7 @@ interface FlowManagerProviderProps {
|
|
|
455
455
|
*/
|
|
456
456
|
declare function FlowManagerProvider({
|
|
457
457
|
children
|
|
458
|
-
}: FlowManagerProviderProps):
|
|
458
|
+
}: FlowManagerProviderProps): react_jsx_runtime1.JSX.Element;
|
|
459
459
|
/**
|
|
460
460
|
* Hook to access the FlowManager context.
|
|
461
461
|
* Must be used within a FlowManagerProvider.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uploadista/react-native-core",
|
|
3
|
-
"version": "0.0.17
|
|
3
|
+
"version": "0.0.17",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Core React Native client for Uploadista",
|
|
6
6
|
"license": "MIT",
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"uuid": "^13.0.0",
|
|
17
17
|
"js-base64": "^3.7.7",
|
|
18
|
-
"@uploadista/
|
|
19
|
-
"@uploadista/core": "0.0.17
|
|
18
|
+
"@uploadista/core": "0.0.17",
|
|
19
|
+
"@uploadista/client-core": "0.0.17"
|
|
20
20
|
},
|
|
21
21
|
"peerDependencies": {
|
|
22
22
|
"react": ">=16.8.0",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@types/react": ">=18.0.0",
|
|
33
33
|
"tsdown": "0.16.6",
|
|
34
|
-
"@uploadista/typescript-config": "0.0.17
|
|
34
|
+
"@uploadista/typescript-config": "0.0.17"
|
|
35
35
|
},
|
|
36
36
|
"scripts": {
|
|
37
37
|
"build": "tsdown",
|
package/src/hooks/index.ts
CHANGED
|
@@ -5,6 +5,16 @@ export {
|
|
|
5
5
|
} from "./uploadista-context";
|
|
6
6
|
export { useCameraUpload } from "./use-camera-upload";
|
|
7
7
|
export { useFileUpload } from "./use-file-upload";
|
|
8
|
+
// Flow hooks
|
|
9
|
+
export type {
|
|
10
|
+
FlowInputMetadata,
|
|
11
|
+
FlowUploadState,
|
|
12
|
+
FlowUploadStatus,
|
|
13
|
+
InputExecutionState,
|
|
14
|
+
UseFlowOptions,
|
|
15
|
+
UseFlowReturn,
|
|
16
|
+
} from "./use-flow";
|
|
17
|
+
export { useFlow } from "./use-flow";
|
|
8
18
|
export { useFlowUpload } from "./use-flow-upload";
|
|
9
19
|
export { useGalleryUpload } from "./use-gallery-upload";
|
|
10
20
|
// Multi-upload hooks
|
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
FlowManager,
|
|
3
|
+
FlowUploadState,
|
|
4
|
+
FlowUploadStatus,
|
|
5
|
+
InputExecutionState,
|
|
6
|
+
} from "@uploadista/client-core";
|
|
7
|
+
import type { TypedOutput } from "@uploadista/core/flow";
|
|
8
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
9
|
+
import { useFlowManagerContext } from "../contexts/flow-manager-context";
|
|
10
|
+
import type { FilePickResult } from "../types";
|
|
11
|
+
import { createBlobFromBuffer } from "../types/platform-types";
|
|
12
|
+
import { useUploadistaContext } from "./use-uploadista-context";
|
|
13
|
+
|
|
14
|
+
// Re-export types from core for convenience
|
|
15
|
+
export type { FlowUploadState, FlowUploadStatus, InputExecutionState };
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Input metadata discovered from the flow
|
|
19
|
+
*/
|
|
20
|
+
export interface FlowInputMetadata {
|
|
21
|
+
/** Input node ID */
|
|
22
|
+
nodeId: string;
|
|
23
|
+
/** Human-readable node name */
|
|
24
|
+
nodeName: string;
|
|
25
|
+
/** Node description explaining what input is needed */
|
|
26
|
+
nodeDescription: string;
|
|
27
|
+
/** Input node type */
|
|
28
|
+
nodeTypeId: string;
|
|
29
|
+
/** Whether this input is required */
|
|
30
|
+
required: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Options for the useFlow hook
|
|
35
|
+
*/
|
|
36
|
+
export interface UseFlowOptions {
|
|
37
|
+
/** Flow ID to execute */
|
|
38
|
+
flowId: string;
|
|
39
|
+
/** Storage ID for the upload */
|
|
40
|
+
storageId: string;
|
|
41
|
+
/** Output node ID for the flow */
|
|
42
|
+
outputNodeId?: string;
|
|
43
|
+
/** Metadata to pass to flow */
|
|
44
|
+
metadata?: Record<string, unknown>;
|
|
45
|
+
/** Called when upload succeeds (receives typed outputs from all output nodes) */
|
|
46
|
+
onSuccess?: (outputs: TypedOutput[]) => void;
|
|
47
|
+
/** Called when the flow completes successfully (receives full flow outputs) */
|
|
48
|
+
onFlowComplete?: (outputs: TypedOutput[]) => void;
|
|
49
|
+
/** Called when upload fails */
|
|
50
|
+
onError?: (error: Error) => void;
|
|
51
|
+
/** Called when upload progress updates */
|
|
52
|
+
onProgress?: (
|
|
53
|
+
progress: number,
|
|
54
|
+
bytesUploaded: number,
|
|
55
|
+
totalBytes: number | null,
|
|
56
|
+
) => void;
|
|
57
|
+
/** Called when a chunk completes */
|
|
58
|
+
onChunkComplete?: (
|
|
59
|
+
chunkSize: number,
|
|
60
|
+
bytesAccepted: number,
|
|
61
|
+
bytesTotal: number | null,
|
|
62
|
+
) => void;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Return value from the useFlow hook with upload control methods and state.
|
|
67
|
+
*
|
|
68
|
+
* @property state - Complete flow upload state with progress and outputs
|
|
69
|
+
* @property inputMetadata - Metadata about discovered input nodes (null until discovered)
|
|
70
|
+
* @property inputStates - Per-input execution state for multi-input flows
|
|
71
|
+
* @property inputs - Current input values set via setInput()
|
|
72
|
+
* @property setInput - Set an input value for a specific node (for progressive provision)
|
|
73
|
+
* @property execute - Execute the flow with current inputs (auto-detects types)
|
|
74
|
+
* @property upload - Convenience method for single-file upload (same as execute with one file input)
|
|
75
|
+
* @property abort - Cancel the current upload and flow execution
|
|
76
|
+
* @property reset - Reset state to idle (clears all data)
|
|
77
|
+
* @property retry - Retry the last failed upload
|
|
78
|
+
* @property isActive - True when upload or processing is active
|
|
79
|
+
* @property isUploadingFile - True only during file upload phase
|
|
80
|
+
* @property isProcessing - True only during flow processing phase
|
|
81
|
+
* @property isDiscoveringInputs - True while discovering flow inputs
|
|
82
|
+
* @property canRetry - True if a retry is possible
|
|
83
|
+
*/
|
|
84
|
+
export interface UseFlowReturn {
|
|
85
|
+
/**
|
|
86
|
+
* Current upload state
|
|
87
|
+
*/
|
|
88
|
+
state: FlowUploadState;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Discovered input nodes metadata (null until discovery completes)
|
|
92
|
+
*/
|
|
93
|
+
inputMetadata: FlowInputMetadata[] | null;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Per-input execution state for multi-input flows
|
|
97
|
+
*/
|
|
98
|
+
inputStates: ReadonlyMap<string, InputExecutionState>;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Current inputs set via setInput()
|
|
102
|
+
*/
|
|
103
|
+
inputs: Record<string, unknown>;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Set an input value for a specific node.
|
|
107
|
+
* For progressive input provision before calling execute().
|
|
108
|
+
*
|
|
109
|
+
* @param nodeId - The input node ID
|
|
110
|
+
* @param value - The input value (FilePickResult, URL string, or structured data)
|
|
111
|
+
*/
|
|
112
|
+
setInput: (nodeId: string, value: FilePickResult | string | unknown) => void;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Execute the flow with current inputs.
|
|
116
|
+
* Automatically detects input types and routes appropriately.
|
|
117
|
+
* For single input, uses standard upload path.
|
|
118
|
+
* For multiple inputs, requires multiInputUploadFn.
|
|
119
|
+
*/
|
|
120
|
+
execute: () => Promise<void>;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Upload a single file through the flow (convenience method).
|
|
124
|
+
* Equivalent to setInput(firstNodeId, file) + execute().
|
|
125
|
+
*
|
|
126
|
+
* @param file - FilePickResult from a picker
|
|
127
|
+
*/
|
|
128
|
+
upload: (file: FilePickResult) => Promise<void>;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Abort the current upload
|
|
132
|
+
*/
|
|
133
|
+
abort: () => void;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Reset the upload state and clear all inputs
|
|
137
|
+
*/
|
|
138
|
+
reset: () => void;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Retry the last failed upload
|
|
142
|
+
*/
|
|
143
|
+
retry: () => void;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Whether an upload or flow execution is in progress (uploading OR processing)
|
|
147
|
+
*/
|
|
148
|
+
isActive: boolean;
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Whether the file is currently being uploaded (chunks being sent)
|
|
152
|
+
*/
|
|
153
|
+
isUploadingFile: boolean;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Whether the flow is currently processing (after upload completes)
|
|
157
|
+
*/
|
|
158
|
+
isProcessing: boolean;
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Whether the hook is discovering flow inputs
|
|
162
|
+
*/
|
|
163
|
+
isDiscoveringInputs: boolean;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Whether a retry is possible (after error or abort with stored inputs)
|
|
167
|
+
*/
|
|
168
|
+
canRetry: boolean;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const initialState: FlowUploadState = {
|
|
172
|
+
status: "idle",
|
|
173
|
+
progress: 0,
|
|
174
|
+
bytesUploaded: 0,
|
|
175
|
+
totalBytes: null,
|
|
176
|
+
error: null,
|
|
177
|
+
jobId: null,
|
|
178
|
+
flowStarted: false,
|
|
179
|
+
currentNodeName: null,
|
|
180
|
+
currentNodeType: null,
|
|
181
|
+
flowOutputs: null,
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* React Native hook for executing flows with single or multiple inputs.
|
|
186
|
+
* Automatically discovers input nodes and detects input types (File, URL, structured data).
|
|
187
|
+
* Supports progressive input provision via setInput() and execute().
|
|
188
|
+
*
|
|
189
|
+
* This is the unified flow hook that replaces useFlowUpload for advanced use cases.
|
|
190
|
+
* It provides:
|
|
191
|
+
* - Auto-discovery of flow input nodes
|
|
192
|
+
* - Automatic input type detection (FilePickResult -> upload, string -> URL, object -> data)
|
|
193
|
+
* - Progressive input provision via setInput()
|
|
194
|
+
* - Multi-input support with parallel coordination
|
|
195
|
+
* - Per-input state tracking
|
|
196
|
+
*
|
|
197
|
+
* Must be used within FlowManagerProvider (which must be within UploadistaProvider).
|
|
198
|
+
* Flow events are automatically routed by the provider to the appropriate manager.
|
|
199
|
+
*
|
|
200
|
+
* @param options - Flow upload configuration including flow ID and event handlers
|
|
201
|
+
* @returns Flow upload state and control methods
|
|
202
|
+
*
|
|
203
|
+
* @example
|
|
204
|
+
* ```tsx
|
|
205
|
+
* function MyComponent() {
|
|
206
|
+
* const flow = useFlow({
|
|
207
|
+
* flowId: 'image-processing-flow',
|
|
208
|
+
* storageId: 'my-storage',
|
|
209
|
+
* onSuccess: (outputs) => console.log('Flow complete:', outputs),
|
|
210
|
+
* onError: (error) => console.error('Flow failed:', error),
|
|
211
|
+
* });
|
|
212
|
+
*
|
|
213
|
+
* const handlePickFile = async () => {
|
|
214
|
+
* const file = await fileSystemProvider.pickDocument();
|
|
215
|
+
* if (file) {
|
|
216
|
+
* await flow.upload(file);
|
|
217
|
+
* }
|
|
218
|
+
* };
|
|
219
|
+
*
|
|
220
|
+
* return (
|
|
221
|
+
* <View>
|
|
222
|
+
* <Button title="Pick File" onPress={handlePickFile} />
|
|
223
|
+
* {flow.isActive && <Text>Progress: {flow.state.progress}%</Text>}
|
|
224
|
+
* {flow.inputMetadata && (
|
|
225
|
+
* <Text>Found {flow.inputMetadata.length} input nodes</Text>
|
|
226
|
+
* )}
|
|
227
|
+
* <Button title="Abort" onPress={flow.abort} disabled={!flow.isActive} />
|
|
228
|
+
* </View>
|
|
229
|
+
* );
|
|
230
|
+
* }
|
|
231
|
+
* ```
|
|
232
|
+
*
|
|
233
|
+
* @example
|
|
234
|
+
* ```tsx
|
|
235
|
+
* // Multi-input flow
|
|
236
|
+
* function MultiInputComponent() {
|
|
237
|
+
* const flow = useFlow({
|
|
238
|
+
* flowId: 'multi-source-flow',
|
|
239
|
+
* storageId: 'my-storage',
|
|
240
|
+
* });
|
|
241
|
+
*
|
|
242
|
+
* const handlePickPrimary = async () => {
|
|
243
|
+
* const file = await fileSystemProvider.pickDocument();
|
|
244
|
+
* if (file.status === 'success') {
|
|
245
|
+
* flow.setInput('primary-input', file);
|
|
246
|
+
* }
|
|
247
|
+
* };
|
|
248
|
+
*
|
|
249
|
+
* const handleSetUrl = (url: string) => {
|
|
250
|
+
* flow.setInput('url-input', url);
|
|
251
|
+
* };
|
|
252
|
+
*
|
|
253
|
+
* return (
|
|
254
|
+
* <View>
|
|
255
|
+
* <Button title="Pick Primary" onPress={handlePickPrimary} />
|
|
256
|
+
* <TextInput onChangeText={handleSetUrl} placeholder="Enter URL" />
|
|
257
|
+
* <Button title="Execute" onPress={flow.execute} />
|
|
258
|
+
* </View>
|
|
259
|
+
* );
|
|
260
|
+
* }
|
|
261
|
+
* ```
|
|
262
|
+
*
|
|
263
|
+
* @see {@link useFlowUpload} for a simpler file-only upload hook
|
|
264
|
+
*/
|
|
265
|
+
export function useFlow(options: UseFlowOptions): UseFlowReturn {
|
|
266
|
+
const { client, fileSystemProvider } = useUploadistaContext();
|
|
267
|
+
const { getManager, releaseManager } = useFlowManagerContext();
|
|
268
|
+
const [state, setState] = useState<FlowUploadState>(initialState);
|
|
269
|
+
const [inputMetadata, setInputMetadata] = useState<
|
|
270
|
+
FlowInputMetadata[] | null
|
|
271
|
+
>(null);
|
|
272
|
+
const [isDiscoveringInputs, setIsDiscoveringInputs] = useState(false);
|
|
273
|
+
const [inputs, setInputs] = useState<Record<string, unknown>>({});
|
|
274
|
+
const [inputStates, setInputStates] = useState<
|
|
275
|
+
ReadonlyMap<string, InputExecutionState>
|
|
276
|
+
>(new Map());
|
|
277
|
+
const managerRef = useRef<FlowManager<unknown> | null>(null);
|
|
278
|
+
const lastInputsRef = useRef<Record<string, unknown> | null>(null);
|
|
279
|
+
|
|
280
|
+
// Store callbacks in refs so they can be updated without recreating the manager
|
|
281
|
+
const callbacksRef = useRef(options);
|
|
282
|
+
|
|
283
|
+
// Update refs on every render to capture latest callbacks
|
|
284
|
+
useEffect(() => {
|
|
285
|
+
callbacksRef.current = options;
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// Auto-discover flow inputs on mount
|
|
289
|
+
useEffect(() => {
|
|
290
|
+
const discoverInputs = async () => {
|
|
291
|
+
setIsDiscoveringInputs(true);
|
|
292
|
+
try {
|
|
293
|
+
const { flow } = await client.getFlow(options.flowId);
|
|
294
|
+
|
|
295
|
+
// Find all input nodes
|
|
296
|
+
const inputNodes = flow.nodes.filter((node) => node.type === "input");
|
|
297
|
+
|
|
298
|
+
const metadata: FlowInputMetadata[] = inputNodes.map((node) => ({
|
|
299
|
+
nodeId: node.id,
|
|
300
|
+
nodeName: node.name,
|
|
301
|
+
nodeDescription: node.description,
|
|
302
|
+
nodeTypeId: node.nodeTypeId,
|
|
303
|
+
required: true,
|
|
304
|
+
}));
|
|
305
|
+
|
|
306
|
+
setInputMetadata(metadata);
|
|
307
|
+
} catch (error) {
|
|
308
|
+
console.error("Failed to discover flow inputs:", error);
|
|
309
|
+
} finally {
|
|
310
|
+
setIsDiscoveringInputs(false);
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
discoverInputs();
|
|
315
|
+
}, [client, options.flowId]);
|
|
316
|
+
|
|
317
|
+
// Get or create manager from context when component mounts
|
|
318
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: we don't want to recreate the manager on every render
|
|
319
|
+
useEffect(() => {
|
|
320
|
+
const flowId = options.flowId;
|
|
321
|
+
|
|
322
|
+
// Create stable callback wrappers that call the latest callbacks via refs
|
|
323
|
+
const stableCallbacks = {
|
|
324
|
+
onStateChange: (newState: FlowUploadState) => {
|
|
325
|
+
setState(newState);
|
|
326
|
+
},
|
|
327
|
+
onProgress: (
|
|
328
|
+
_uploadId: string,
|
|
329
|
+
bytesUploaded: number,
|
|
330
|
+
totalBytes: number | null,
|
|
331
|
+
) => {
|
|
332
|
+
if (callbacksRef.current.onProgress) {
|
|
333
|
+
const progress = totalBytes
|
|
334
|
+
? Math.round((bytesUploaded / totalBytes) * 100)
|
|
335
|
+
: 0;
|
|
336
|
+
callbacksRef.current.onProgress(progress, bytesUploaded, totalBytes);
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
onChunkComplete: (
|
|
340
|
+
chunkSize: number,
|
|
341
|
+
bytesAccepted: number,
|
|
342
|
+
bytesTotal: number | null,
|
|
343
|
+
) => {
|
|
344
|
+
callbacksRef.current.onChunkComplete?.(
|
|
345
|
+
chunkSize,
|
|
346
|
+
bytesAccepted,
|
|
347
|
+
bytesTotal,
|
|
348
|
+
);
|
|
349
|
+
},
|
|
350
|
+
onFlowComplete: (outputs: TypedOutput[]) => {
|
|
351
|
+
callbacksRef.current.onFlowComplete?.(outputs);
|
|
352
|
+
},
|
|
353
|
+
onSuccess: (outputs: TypedOutput[]) => {
|
|
354
|
+
callbacksRef.current.onSuccess?.(outputs);
|
|
355
|
+
},
|
|
356
|
+
onError: (error: Error) => {
|
|
357
|
+
callbacksRef.current.onError?.(error);
|
|
358
|
+
},
|
|
359
|
+
onAbort: () => {
|
|
360
|
+
// onAbort not exposed in public API
|
|
361
|
+
},
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
// Get manager from context (creates if doesn't exist, increments ref count)
|
|
365
|
+
managerRef.current = getManager(flowId, stableCallbacks, {
|
|
366
|
+
flowConfig: {
|
|
367
|
+
flowId: options.flowId,
|
|
368
|
+
storageId: options.storageId,
|
|
369
|
+
outputNodeId: options.outputNodeId,
|
|
370
|
+
metadata: options.metadata as Record<string, string> | undefined,
|
|
371
|
+
},
|
|
372
|
+
onChunkComplete: options.onChunkComplete,
|
|
373
|
+
onSuccess: options.onSuccess,
|
|
374
|
+
onError: options.onError,
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// Set up interval to poll input states for multi-input flows
|
|
378
|
+
const pollInterval = setInterval(() => {
|
|
379
|
+
if (managerRef.current) {
|
|
380
|
+
const states = managerRef.current.getInputStates();
|
|
381
|
+
if (states.size > 0) {
|
|
382
|
+
setInputStates(new Map(states));
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}, 100); // Poll every 100ms
|
|
386
|
+
|
|
387
|
+
// Release manager when component unmounts or flowId changes
|
|
388
|
+
return () => {
|
|
389
|
+
clearInterval(pollInterval);
|
|
390
|
+
releaseManager(flowId);
|
|
391
|
+
managerRef.current = null;
|
|
392
|
+
};
|
|
393
|
+
}, [
|
|
394
|
+
options.flowId,
|
|
395
|
+
options.storageId,
|
|
396
|
+
options.outputNodeId,
|
|
397
|
+
getManager,
|
|
398
|
+
releaseManager,
|
|
399
|
+
]);
|
|
400
|
+
|
|
401
|
+
// Set an input value
|
|
402
|
+
const setInput = useCallback(
|
|
403
|
+
(nodeId: string, value: FilePickResult | string | unknown) => {
|
|
404
|
+
setInputs((prev) => ({ ...prev, [nodeId]: value }));
|
|
405
|
+
},
|
|
406
|
+
[],
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
// Helper to convert FilePickResult to Blob
|
|
410
|
+
const filePickToBlob = useCallback(
|
|
411
|
+
async (file: FilePickResult): Promise<Blob | null> => {
|
|
412
|
+
if (file.status === "cancelled") {
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
if (file.status === "error") {
|
|
416
|
+
throw file.error;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const fileContent = await fileSystemProvider.readFile(file.data.uri);
|
|
420
|
+
return createBlobFromBuffer(fileContent, {
|
|
421
|
+
type: file.data.mimeType || "application/octet-stream",
|
|
422
|
+
});
|
|
423
|
+
},
|
|
424
|
+
[fileSystemProvider],
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
// Execute flow with current inputs
|
|
428
|
+
const execute = useCallback(async () => {
|
|
429
|
+
if (!managerRef.current) {
|
|
430
|
+
throw new Error("FlowManager not initialized");
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (Object.keys(inputs).length === 0) {
|
|
434
|
+
throw new Error(
|
|
435
|
+
"No inputs provided. Use setInput() to provide inputs before calling execute()",
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Store inputs for retry
|
|
440
|
+
lastInputsRef.current = { ...inputs };
|
|
441
|
+
|
|
442
|
+
// Convert FilePickResults to Blobs
|
|
443
|
+
const processedInputs: Record<string, unknown> = {};
|
|
444
|
+
|
|
445
|
+
for (const [nodeId, value] of Object.entries(inputs)) {
|
|
446
|
+
// Check if value is a FilePickResult
|
|
447
|
+
if (
|
|
448
|
+
value &&
|
|
449
|
+
typeof value === "object" &&
|
|
450
|
+
"status" in value &&
|
|
451
|
+
(value.status === "success" ||
|
|
452
|
+
value.status === "cancelled" ||
|
|
453
|
+
value.status === "error")
|
|
454
|
+
) {
|
|
455
|
+
const blob = await filePickToBlob(value as FilePickResult);
|
|
456
|
+
if (blob) {
|
|
457
|
+
processedInputs[nodeId] = blob;
|
|
458
|
+
}
|
|
459
|
+
// If blob is null (cancelled), skip this input
|
|
460
|
+
} else {
|
|
461
|
+
// Pass through strings (URLs) and other values as-is
|
|
462
|
+
processedInputs[nodeId] = value;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (Object.keys(processedInputs).length === 0) {
|
|
467
|
+
throw new Error(
|
|
468
|
+
"No valid inputs after processing. All files may have been cancelled.",
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
await managerRef.current.executeFlow(processedInputs);
|
|
473
|
+
}, [inputs, filePickToBlob]);
|
|
474
|
+
|
|
475
|
+
// Convenience method for single file upload
|
|
476
|
+
const upload = useCallback(
|
|
477
|
+
async (file: FilePickResult) => {
|
|
478
|
+
// Handle cancelled picker
|
|
479
|
+
if (file.status === "cancelled") {
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Handle picker error
|
|
484
|
+
if (file.status === "error") {
|
|
485
|
+
options.onError?.(file.error);
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if (!managerRef.current) {
|
|
490
|
+
throw new Error("FlowManager not initialized");
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Store for retry
|
|
494
|
+
if (inputMetadata && inputMetadata.length > 0) {
|
|
495
|
+
const firstInputNode = inputMetadata[0];
|
|
496
|
+
if (firstInputNode) {
|
|
497
|
+
lastInputsRef.current = { [firstInputNode.nodeId]: file };
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
try {
|
|
502
|
+
const fileContent = await fileSystemProvider.readFile(file.data.uri);
|
|
503
|
+
const blob = createBlobFromBuffer(fileContent, {
|
|
504
|
+
type: file.data.mimeType || "application/octet-stream",
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
// If we have input metadata, use the first input node
|
|
508
|
+
if (inputMetadata && inputMetadata.length > 0) {
|
|
509
|
+
const firstInputNode = inputMetadata[0];
|
|
510
|
+
if (!firstInputNode) {
|
|
511
|
+
throw new Error("No input nodes found");
|
|
512
|
+
}
|
|
513
|
+
setInputs({ [firstInputNode.nodeId]: file });
|
|
514
|
+
await managerRef.current.executeFlow({
|
|
515
|
+
[firstInputNode.nodeId]: blob,
|
|
516
|
+
});
|
|
517
|
+
} else {
|
|
518
|
+
// Fall back to direct upload (manager will handle discovery)
|
|
519
|
+
await managerRef.current.upload(blob);
|
|
520
|
+
}
|
|
521
|
+
} catch (error) {
|
|
522
|
+
options.onError?.(error as Error);
|
|
523
|
+
}
|
|
524
|
+
},
|
|
525
|
+
[inputMetadata, fileSystemProvider, options],
|
|
526
|
+
);
|
|
527
|
+
|
|
528
|
+
const abort = useCallback(() => {
|
|
529
|
+
managerRef.current?.abort();
|
|
530
|
+
}, []);
|
|
531
|
+
|
|
532
|
+
const reset = useCallback(() => {
|
|
533
|
+
managerRef.current?.reset();
|
|
534
|
+
setInputs({});
|
|
535
|
+
setInputStates(new Map());
|
|
536
|
+
lastInputsRef.current = null;
|
|
537
|
+
}, []);
|
|
538
|
+
|
|
539
|
+
const retry = useCallback(() => {
|
|
540
|
+
if (
|
|
541
|
+
lastInputsRef.current &&
|
|
542
|
+
(state.status === "error" || state.status === "aborted")
|
|
543
|
+
) {
|
|
544
|
+
// Restore inputs and re-execute
|
|
545
|
+
setInputs(lastInputsRef.current);
|
|
546
|
+
execute();
|
|
547
|
+
}
|
|
548
|
+
}, [execute, state.status]);
|
|
549
|
+
|
|
550
|
+
// Derive computed values from state (reactive to state changes)
|
|
551
|
+
const isActive =
|
|
552
|
+
state.status === "uploading" || state.status === "processing";
|
|
553
|
+
const isUploadingFile = state.status === "uploading";
|
|
554
|
+
const isProcessing = state.status === "processing";
|
|
555
|
+
const canRetry =
|
|
556
|
+
(state.status === "error" || state.status === "aborted") &&
|
|
557
|
+
lastInputsRef.current !== null;
|
|
558
|
+
|
|
559
|
+
return {
|
|
560
|
+
state,
|
|
561
|
+
inputMetadata,
|
|
562
|
+
inputStates,
|
|
563
|
+
inputs,
|
|
564
|
+
setInput,
|
|
565
|
+
execute,
|
|
566
|
+
upload,
|
|
567
|
+
abort,
|
|
568
|
+
reset,
|
|
569
|
+
retry,
|
|
570
|
+
isActive,
|
|
571
|
+
isUploadingFile,
|
|
572
|
+
isProcessing,
|
|
573
|
+
isDiscoveringInputs,
|
|
574
|
+
canRetry,
|
|
575
|
+
};
|
|
576
|
+
}
|