@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.
- package/.turbo/turbo-check.log +89 -0
- package/FLOW_UPLOAD.md +307 -0
- package/LICENSE +21 -0
- package/README.md +318 -0
- package/package.json +35 -0
- package/src/components/flow-upload-list.tsx +614 -0
- package/src/components/flow-upload-zone.tsx +441 -0
- package/src/components/upload-list.tsx +626 -0
- package/src/components/upload-zone.tsx +545 -0
- package/src/components/uploadista-provider.tsx +190 -0
- package/src/hooks/use-drag-drop.ts +404 -0
- package/src/hooks/use-flow-upload.ts +568 -0
- package/src/hooks/use-multi-flow-upload.ts +477 -0
- package/src/hooks/use-multi-upload.ts +691 -0
- package/src/hooks/use-upload-metrics.ts +585 -0
- package/src/hooks/use-upload.ts +411 -0
- package/src/hooks/use-uploadista-client.ts +145 -0
- package/src/index.ts +87 -0
- package/tsconfig.json +11 -0
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
FlowUploadOptions,
|
|
3
|
+
UploadistaEvent,
|
|
4
|
+
} from "@uploadista/client-browser";
|
|
5
|
+
import { EventType, type FlowEvent } from "@uploadista/core/flow";
|
|
6
|
+
import type { UploadFile } from "@uploadista/core/types";
|
|
7
|
+
import { UploadEventType } from "@uploadista/core/types";
|
|
8
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
9
|
+
import { useUploadistaContext } from "../components/uploadista-provider";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Type guard to check if an event is a flow event
|
|
13
|
+
*/
|
|
14
|
+
function isFlowEvent(event: UploadistaEvent): event is FlowEvent {
|
|
15
|
+
// FlowEvent has eventType, not type
|
|
16
|
+
const flowEvent = event as FlowEvent;
|
|
17
|
+
return (
|
|
18
|
+
flowEvent.eventType === EventType.FlowStart ||
|
|
19
|
+
flowEvent.eventType === EventType.FlowEnd ||
|
|
20
|
+
flowEvent.eventType === EventType.FlowError ||
|
|
21
|
+
flowEvent.eventType === EventType.NodeStart ||
|
|
22
|
+
flowEvent.eventType === EventType.NodeEnd ||
|
|
23
|
+
flowEvent.eventType === EventType.NodePause ||
|
|
24
|
+
flowEvent.eventType === EventType.NodeResume ||
|
|
25
|
+
flowEvent.eventType === EventType.NodeError
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Possible states for a flow upload lifecycle.
|
|
31
|
+
* Flow uploads progress through: idle → uploading → processing → success/error/aborted
|
|
32
|
+
*/
|
|
33
|
+
export type FlowUploadStatus =
|
|
34
|
+
| "idle"
|
|
35
|
+
| "uploading"
|
|
36
|
+
| "processing"
|
|
37
|
+
| "success"
|
|
38
|
+
| "error"
|
|
39
|
+
| "aborted";
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Complete state information for a flow upload operation.
|
|
43
|
+
* Tracks both the upload phase (file transfer) and processing phase (flow execution).
|
|
44
|
+
*
|
|
45
|
+
* @template TOutput - Type of the final output from the flow (defaults to UploadFile)
|
|
46
|
+
*
|
|
47
|
+
* @property status - Current upload status (idle, uploading, processing, success, error, aborted)
|
|
48
|
+
* @property progress - Upload progress percentage (0-100)
|
|
49
|
+
* @property bytesUploaded - Number of bytes successfully uploaded
|
|
50
|
+
* @property totalBytes - Total file size in bytes (null if unknown)
|
|
51
|
+
* @property error - Error object if upload or processing failed
|
|
52
|
+
* @property result - Final output from the flow (available when status is "success")
|
|
53
|
+
* @property jobId - Unique identifier for the flow execution job
|
|
54
|
+
* @property flowStarted - Whether the flow processing has started
|
|
55
|
+
* @property currentNodeName - Name of the currently executing flow node
|
|
56
|
+
* @property currentNodeType - Type of the currently executing flow node
|
|
57
|
+
* @property flowOutputs - Complete outputs from all output nodes in the flow
|
|
58
|
+
*/
|
|
59
|
+
export interface FlowUploadState<TOutput = UploadFile> {
|
|
60
|
+
status: FlowUploadStatus;
|
|
61
|
+
progress: number;
|
|
62
|
+
bytesUploaded: number;
|
|
63
|
+
totalBytes: number | null;
|
|
64
|
+
error: Error | null;
|
|
65
|
+
result: TOutput | null;
|
|
66
|
+
jobId: string | null;
|
|
67
|
+
// Flow execution tracking
|
|
68
|
+
flowStarted: boolean;
|
|
69
|
+
currentNodeName: string | null;
|
|
70
|
+
currentNodeType: string | null;
|
|
71
|
+
// Full flow outputs (all output nodes)
|
|
72
|
+
flowOutputs: Record<string, unknown> | null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Return value from the useFlowUpload hook with upload control methods and state.
|
|
77
|
+
*
|
|
78
|
+
* @template TOutput - Type of the final output from the flow (defaults to UploadFile)
|
|
79
|
+
*
|
|
80
|
+
* @property state - Complete flow upload state with progress and outputs
|
|
81
|
+
* @property upload - Function to initiate file upload through the flow
|
|
82
|
+
* @property abort - Cancel the current upload and flow execution
|
|
83
|
+
* @property reset - Reset state to idle (clears all data)
|
|
84
|
+
* @property isUploading - True when upload or processing is active
|
|
85
|
+
* @property isUploadingFile - True only during file upload phase
|
|
86
|
+
* @property isProcessing - True only during flow processing phase
|
|
87
|
+
*/
|
|
88
|
+
export interface UseFlowUploadReturn<TOutput = UploadFile> {
|
|
89
|
+
/**
|
|
90
|
+
* Current upload state
|
|
91
|
+
*/
|
|
92
|
+
state: FlowUploadState<TOutput>;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Upload a file through the flow
|
|
96
|
+
*/
|
|
97
|
+
upload: (file: File | Blob) => Promise<void>;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Abort the current upload
|
|
101
|
+
*/
|
|
102
|
+
abort: () => void;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Reset the upload state
|
|
106
|
+
*/
|
|
107
|
+
reset: () => void;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Whether an upload or flow execution is in progress (uploading OR processing)
|
|
111
|
+
*/
|
|
112
|
+
isUploading: boolean;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Whether the file is currently being uploaded (chunks being sent)
|
|
116
|
+
*/
|
|
117
|
+
isUploadingFile: boolean;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Whether the flow is currently processing (after upload completes)
|
|
121
|
+
*/
|
|
122
|
+
isProcessing: boolean;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const initialState: FlowUploadState = {
|
|
126
|
+
status: "idle",
|
|
127
|
+
progress: 0,
|
|
128
|
+
bytesUploaded: 0,
|
|
129
|
+
totalBytes: null,
|
|
130
|
+
error: null,
|
|
131
|
+
result: null,
|
|
132
|
+
jobId: null,
|
|
133
|
+
flowStarted: false,
|
|
134
|
+
currentNodeName: null,
|
|
135
|
+
currentNodeType: null,
|
|
136
|
+
flowOutputs: null,
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* React hook for uploading files through a flow with automatic flow execution.
|
|
141
|
+
* Handles both the file upload phase and the flow processing phase, providing
|
|
142
|
+
* real-time progress updates and flow node execution tracking.
|
|
143
|
+
*
|
|
144
|
+
* The flow engine processes the uploaded file through a DAG of nodes, which can
|
|
145
|
+
* perform operations like image optimization, storage saving, webhooks, etc.
|
|
146
|
+
*
|
|
147
|
+
* Must be used within an UploadistaProvider. Flow events (node start/end, flow complete)
|
|
148
|
+
* are automatically subscribed through the provider context.
|
|
149
|
+
*
|
|
150
|
+
* @template TOutput - Type of the final result from the flow (defaults to UploadFile)
|
|
151
|
+
* @param options - Flow upload configuration including flow ID and event handlers
|
|
152
|
+
* @returns Flow upload state and control methods
|
|
153
|
+
*
|
|
154
|
+
* @example
|
|
155
|
+
* ```tsx
|
|
156
|
+
* // Basic flow upload with progress tracking
|
|
157
|
+
* function ImageUploader() {
|
|
158
|
+
* const flowUpload = useFlowUpload({
|
|
159
|
+
* flowConfig: {
|
|
160
|
+
* flowId: "image-optimization-flow",
|
|
161
|
+
* storageId: "s3-images",
|
|
162
|
+
* outputNodeId: "optimized-output", // Optional: specify which output to use
|
|
163
|
+
* },
|
|
164
|
+
* onSuccess: (result) => {
|
|
165
|
+
* console.log("Image optimized and saved:", result);
|
|
166
|
+
* },
|
|
167
|
+
* onFlowComplete: (outputs) => {
|
|
168
|
+
* console.log("All flow outputs:", outputs);
|
|
169
|
+
* // outputs might include: { thumbnail: {...}, optimized: {...}, original: {...} }
|
|
170
|
+
* },
|
|
171
|
+
* onError: (error) => {
|
|
172
|
+
* console.error("Upload or processing failed:", error);
|
|
173
|
+
* },
|
|
174
|
+
* });
|
|
175
|
+
*
|
|
176
|
+
* return (
|
|
177
|
+
* <div>
|
|
178
|
+
* <input
|
|
179
|
+
* type="file"
|
|
180
|
+
* accept="image/*"
|
|
181
|
+
* onChange={(e) => {
|
|
182
|
+
* const file = e.target.files?.[0];
|
|
183
|
+
* if (file) flowUpload.upload(file);
|
|
184
|
+
* }}
|
|
185
|
+
* />
|
|
186
|
+
*
|
|
187
|
+
* {flowUpload.isUploadingFile && (
|
|
188
|
+
* <div>Uploading... {flowUpload.state.progress}%</div>
|
|
189
|
+
* )}
|
|
190
|
+
*
|
|
191
|
+
* {flowUpload.isProcessing && (
|
|
192
|
+
* <div>
|
|
193
|
+
* Processing...
|
|
194
|
+
* {flowUpload.state.currentNodeName && (
|
|
195
|
+
* <span>Current step: {flowUpload.state.currentNodeName}</span>
|
|
196
|
+
* )}
|
|
197
|
+
* </div>
|
|
198
|
+
* )}
|
|
199
|
+
*
|
|
200
|
+
* {flowUpload.state.status === "success" && (
|
|
201
|
+
* <div>
|
|
202
|
+
* <p>Upload complete!</p>
|
|
203
|
+
* {flowUpload.state.result && (
|
|
204
|
+
* <img src={flowUpload.state.result.url} alt="Uploaded" />
|
|
205
|
+
* )}
|
|
206
|
+
* </div>
|
|
207
|
+
* )}
|
|
208
|
+
*
|
|
209
|
+
* {flowUpload.state.status === "error" && (
|
|
210
|
+
* <div>
|
|
211
|
+
* <p>Error: {flowUpload.state.error?.message}</p>
|
|
212
|
+
* <button onClick={flowUpload.reset}>Try Again</button>
|
|
213
|
+
* </div>
|
|
214
|
+
* )}
|
|
215
|
+
*
|
|
216
|
+
* {flowUpload.isUploading && (
|
|
217
|
+
* <button onClick={flowUpload.abort}>Cancel</button>
|
|
218
|
+
* )}
|
|
219
|
+
* </div>
|
|
220
|
+
* );
|
|
221
|
+
* }
|
|
222
|
+
* ```
|
|
223
|
+
*
|
|
224
|
+
* @see {@link useMultiFlowUpload} for uploading multiple files through a flow
|
|
225
|
+
* @see {@link useUpload} for simple uploads without flow processing
|
|
226
|
+
*/
|
|
227
|
+
export function useFlowUpload<TOutput = UploadFile>(
|
|
228
|
+
options: FlowUploadOptions<TOutput>,
|
|
229
|
+
): UseFlowUploadReturn<TOutput> {
|
|
230
|
+
// Get client and event subscription from context
|
|
231
|
+
const client = useUploadistaContext();
|
|
232
|
+
const [state, setState] = useState<FlowUploadState<TOutput>>(
|
|
233
|
+
initialState as FlowUploadState<TOutput>,
|
|
234
|
+
);
|
|
235
|
+
const abortRef = useRef<(() => void) | null>(null);
|
|
236
|
+
const onSuccessRef = useRef(options.onSuccess);
|
|
237
|
+
const onErrorRef = useRef(options.onError);
|
|
238
|
+
const onFlowCompleteRef = useRef(options.onFlowComplete);
|
|
239
|
+
const outputNodeIdRef = useRef(options.flowConfig.outputNodeId);
|
|
240
|
+
|
|
241
|
+
// Update refs when callbacks change
|
|
242
|
+
useEffect(() => {
|
|
243
|
+
onSuccessRef.current = options.onSuccess;
|
|
244
|
+
onErrorRef.current = options.onError;
|
|
245
|
+
onFlowCompleteRef.current = options.onFlowComplete;
|
|
246
|
+
outputNodeIdRef.current = options.flowConfig.outputNodeId;
|
|
247
|
+
}, [
|
|
248
|
+
options.onSuccess,
|
|
249
|
+
options.onError,
|
|
250
|
+
options.onFlowComplete,
|
|
251
|
+
options.flowConfig.outputNodeId,
|
|
252
|
+
]);
|
|
253
|
+
|
|
254
|
+
// Store jobId in ref for event handling
|
|
255
|
+
const jobIdRef = useRef<string | null>(null);
|
|
256
|
+
|
|
257
|
+
useEffect(() => {
|
|
258
|
+
jobIdRef.current = state.jobId;
|
|
259
|
+
}, [state.jobId]);
|
|
260
|
+
|
|
261
|
+
// Create stable event handler
|
|
262
|
+
const handleFlowEvent = useCallback((event: FlowEvent) => {
|
|
263
|
+
console.log(
|
|
264
|
+
"[useFlowUpload] Received event:",
|
|
265
|
+
event,
|
|
266
|
+
"Current jobId:",
|
|
267
|
+
jobIdRef.current,
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
// Only handle events for the current job
|
|
271
|
+
if (!jobIdRef.current || event.jobId !== jobIdRef.current) {
|
|
272
|
+
console.log("[useFlowUpload] Ignoring event - jobId mismatch");
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
console.log("[useFlowUpload] Processing event type:", event.eventType);
|
|
277
|
+
|
|
278
|
+
switch (event.eventType) {
|
|
279
|
+
case EventType.FlowStart:
|
|
280
|
+
console.log("[useFlowUpload] Flow started");
|
|
281
|
+
setState((prev) => ({
|
|
282
|
+
...prev,
|
|
283
|
+
flowStarted: true,
|
|
284
|
+
status: "processing",
|
|
285
|
+
}));
|
|
286
|
+
break;
|
|
287
|
+
|
|
288
|
+
case EventType.NodeStart:
|
|
289
|
+
console.log("[useFlowUpload] Node started:", event.nodeName);
|
|
290
|
+
setState((prev) => ({
|
|
291
|
+
...prev,
|
|
292
|
+
status: "processing",
|
|
293
|
+
currentNodeName: event.nodeName,
|
|
294
|
+
currentNodeType: event.nodeType,
|
|
295
|
+
}));
|
|
296
|
+
break;
|
|
297
|
+
|
|
298
|
+
case EventType.NodePause:
|
|
299
|
+
console.log(
|
|
300
|
+
"[useFlowUpload] Node paused (waiting for upload):",
|
|
301
|
+
event.nodeName,
|
|
302
|
+
);
|
|
303
|
+
// When input node pauses, it's waiting for upload - switch to uploading state
|
|
304
|
+
setState((prev) => ({
|
|
305
|
+
...prev,
|
|
306
|
+
status: "uploading",
|
|
307
|
+
currentNodeName: event.nodeName,
|
|
308
|
+
// NodePause doesn't have nodeType, keep previous value
|
|
309
|
+
}));
|
|
310
|
+
break;
|
|
311
|
+
|
|
312
|
+
case EventType.NodeResume:
|
|
313
|
+
console.log(
|
|
314
|
+
"[useFlowUpload] Node resumed (upload complete):",
|
|
315
|
+
event.nodeName,
|
|
316
|
+
);
|
|
317
|
+
// When node resumes, upload is complete - switch to processing state
|
|
318
|
+
setState((prev) => ({
|
|
319
|
+
...prev,
|
|
320
|
+
status: "processing",
|
|
321
|
+
currentNodeName: event.nodeName,
|
|
322
|
+
currentNodeType: event.nodeType,
|
|
323
|
+
}));
|
|
324
|
+
break;
|
|
325
|
+
|
|
326
|
+
case EventType.NodeEnd:
|
|
327
|
+
console.log("[useFlowUpload] Node ended:", event.nodeName);
|
|
328
|
+
setState((prev) => ({
|
|
329
|
+
...prev,
|
|
330
|
+
status: prev.status === "uploading" ? "processing" : prev.status,
|
|
331
|
+
currentNodeName: null,
|
|
332
|
+
currentNodeType: null,
|
|
333
|
+
}));
|
|
334
|
+
break;
|
|
335
|
+
|
|
336
|
+
case EventType.FlowEnd:
|
|
337
|
+
console.log("[useFlowUpload] Flow ended, processing outputs");
|
|
338
|
+
setState((prev) => {
|
|
339
|
+
// Get flow outputs from the event result
|
|
340
|
+
const flowOutputs = (event.result as Record<string, unknown>) || null;
|
|
341
|
+
|
|
342
|
+
console.log("[useFlowUpload] Flow outputs:", flowOutputs);
|
|
343
|
+
|
|
344
|
+
// Call onFlowComplete with full outputs
|
|
345
|
+
if (flowOutputs && onFlowCompleteRef.current) {
|
|
346
|
+
console.log(
|
|
347
|
+
"[useFlowUpload] Calling onFlowComplete with outputs:",
|
|
348
|
+
flowOutputs,
|
|
349
|
+
);
|
|
350
|
+
onFlowCompleteRef.current(flowOutputs);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Extract single output for onSuccess callback
|
|
354
|
+
let extractedOutput: TOutput | null = null;
|
|
355
|
+
if (flowOutputs) {
|
|
356
|
+
if (
|
|
357
|
+
outputNodeIdRef.current &&
|
|
358
|
+
outputNodeIdRef.current in flowOutputs
|
|
359
|
+
) {
|
|
360
|
+
// Use specified output node
|
|
361
|
+
extractedOutput = flowOutputs[outputNodeIdRef.current] as TOutput;
|
|
362
|
+
console.log(
|
|
363
|
+
"[useFlowUpload] Extracted output from specified node:",
|
|
364
|
+
outputNodeIdRef.current,
|
|
365
|
+
);
|
|
366
|
+
} else {
|
|
367
|
+
// Use first output node
|
|
368
|
+
const firstOutputValue = Object.values(flowOutputs)[0];
|
|
369
|
+
extractedOutput = firstOutputValue as TOutput;
|
|
370
|
+
console.log("[useFlowUpload] Extracted output from first node");
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Call onSuccess with extracted output
|
|
375
|
+
if (extractedOutput && onSuccessRef.current) {
|
|
376
|
+
console.log(
|
|
377
|
+
"[useFlowUpload] Calling onSuccess with result:",
|
|
378
|
+
extractedOutput,
|
|
379
|
+
);
|
|
380
|
+
onSuccessRef.current(extractedOutput);
|
|
381
|
+
} else if (!extractedOutput && onSuccessRef.current) {
|
|
382
|
+
console.warn("[useFlowUpload] No result available for onSuccess");
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return {
|
|
386
|
+
...prev,
|
|
387
|
+
status: "success",
|
|
388
|
+
currentNodeName: null,
|
|
389
|
+
currentNodeType: null,
|
|
390
|
+
result: extractedOutput,
|
|
391
|
+
flowOutputs,
|
|
392
|
+
};
|
|
393
|
+
});
|
|
394
|
+
break;
|
|
395
|
+
|
|
396
|
+
case EventType.FlowError:
|
|
397
|
+
console.log("[useFlowUpload] Flow error:", event.error);
|
|
398
|
+
setState((prev) => ({
|
|
399
|
+
...prev,
|
|
400
|
+
status: "error",
|
|
401
|
+
error: new Error(event.error),
|
|
402
|
+
}));
|
|
403
|
+
onErrorRef.current?.(new Error(event.error));
|
|
404
|
+
break;
|
|
405
|
+
|
|
406
|
+
case EventType.NodeError:
|
|
407
|
+
console.log("[useFlowUpload] Node error:", event.error);
|
|
408
|
+
setState((prev) => ({
|
|
409
|
+
...prev,
|
|
410
|
+
status: "error",
|
|
411
|
+
error: new Error(event.error),
|
|
412
|
+
}));
|
|
413
|
+
onErrorRef.current?.(new Error(event.error));
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
}, []);
|
|
417
|
+
|
|
418
|
+
// Automatically subscribe to flow events and upload events from context
|
|
419
|
+
useEffect(() => {
|
|
420
|
+
console.log("[useFlowUpload] Subscribing to events from context");
|
|
421
|
+
const unsubscribe = client.subscribeToEvents((event: UploadistaEvent) => {
|
|
422
|
+
// Handle flow events
|
|
423
|
+
if (isFlowEvent(event)) {
|
|
424
|
+
handleFlowEvent(event);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Handle upload progress events for this job's upload
|
|
429
|
+
const uploadEvent = event as {
|
|
430
|
+
type: string;
|
|
431
|
+
data?: { id: string; progress: number; total: number };
|
|
432
|
+
flow?: { jobId: string };
|
|
433
|
+
};
|
|
434
|
+
if (
|
|
435
|
+
uploadEvent.type === UploadEventType.UPLOAD_PROGRESS &&
|
|
436
|
+
uploadEvent.flow?.jobId === jobIdRef.current &&
|
|
437
|
+
uploadEvent.data
|
|
438
|
+
) {
|
|
439
|
+
const { progress: bytesUploaded, total: totalBytes } = uploadEvent.data;
|
|
440
|
+
const progress = totalBytes
|
|
441
|
+
? Math.round((bytesUploaded / totalBytes) * 100)
|
|
442
|
+
: 0;
|
|
443
|
+
|
|
444
|
+
console.log("[useFlowUpload] Upload progress event:", {
|
|
445
|
+
progress,
|
|
446
|
+
bytesUploaded,
|
|
447
|
+
totalBytes,
|
|
448
|
+
jobId: uploadEvent.flow.jobId,
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
setState((prev) => ({
|
|
452
|
+
...prev,
|
|
453
|
+
progress,
|
|
454
|
+
bytesUploaded,
|
|
455
|
+
totalBytes,
|
|
456
|
+
}));
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
return unsubscribe;
|
|
461
|
+
}, [client, handleFlowEvent]);
|
|
462
|
+
|
|
463
|
+
const abort = useCallback(() => {
|
|
464
|
+
if (abortRef.current) {
|
|
465
|
+
abortRef.current();
|
|
466
|
+
abortRef.current = null;
|
|
467
|
+
|
|
468
|
+
setState((prev) => ({
|
|
469
|
+
...prev,
|
|
470
|
+
status: "aborted",
|
|
471
|
+
}));
|
|
472
|
+
|
|
473
|
+
options.onAbort?.();
|
|
474
|
+
}
|
|
475
|
+
}, [options]);
|
|
476
|
+
|
|
477
|
+
const upload = useCallback(
|
|
478
|
+
async (file: File | Blob) => {
|
|
479
|
+
jobIdRef.current = null;
|
|
480
|
+
|
|
481
|
+
setState({
|
|
482
|
+
...initialState,
|
|
483
|
+
status: "uploading",
|
|
484
|
+
totalBytes: file.size,
|
|
485
|
+
} as FlowUploadState<TOutput>);
|
|
486
|
+
|
|
487
|
+
try {
|
|
488
|
+
const { abort: _abortFn } = await client.client.uploadWithFlow(
|
|
489
|
+
file,
|
|
490
|
+
options.flowConfig,
|
|
491
|
+
{
|
|
492
|
+
onJobStart: (jobId: string) => {
|
|
493
|
+
jobIdRef.current = jobId;
|
|
494
|
+
setState((prev) => ({ ...prev, jobId }));
|
|
495
|
+
},
|
|
496
|
+
onProgress: (
|
|
497
|
+
_uploadId: string,
|
|
498
|
+
bytesUploaded: number,
|
|
499
|
+
totalBytes: number | null,
|
|
500
|
+
) => {
|
|
501
|
+
const progress = totalBytes
|
|
502
|
+
? Math.round((bytesUploaded / totalBytes) * 100)
|
|
503
|
+
: 0;
|
|
504
|
+
|
|
505
|
+
setState((prev) => ({
|
|
506
|
+
...prev,
|
|
507
|
+
progress,
|
|
508
|
+
bytesUploaded,
|
|
509
|
+
totalBytes,
|
|
510
|
+
}));
|
|
511
|
+
|
|
512
|
+
options.onProgress?.(progress, bytesUploaded, totalBytes);
|
|
513
|
+
},
|
|
514
|
+
onChunkComplete: options.onChunkComplete,
|
|
515
|
+
onSuccess: (_result: UploadFile) => {
|
|
516
|
+
// Upload phase is complete, now waiting for flow execution
|
|
517
|
+
// Note: we don't store the upload result as our final result
|
|
518
|
+
// The final result will come from the FlowEnd event
|
|
519
|
+
// Status transition from "uploading" to "processing" is handled by NodeResume event
|
|
520
|
+
setState((prev) => ({
|
|
521
|
+
...prev,
|
|
522
|
+
progress: 100,
|
|
523
|
+
}));
|
|
524
|
+
// Don't call onSuccess here - wait for FlowEnd event
|
|
525
|
+
},
|
|
526
|
+
onError: (error: Error) => {
|
|
527
|
+
setState((prev) => ({
|
|
528
|
+
...prev,
|
|
529
|
+
status: "error",
|
|
530
|
+
error,
|
|
531
|
+
}));
|
|
532
|
+
|
|
533
|
+
options.onError?.(error);
|
|
534
|
+
},
|
|
535
|
+
onShouldRetry: options.onShouldRetry,
|
|
536
|
+
},
|
|
537
|
+
);
|
|
538
|
+
|
|
539
|
+
abortRef.current = _abortFn;
|
|
540
|
+
} catch (error) {
|
|
541
|
+
setState((prev) => ({
|
|
542
|
+
...prev,
|
|
543
|
+
status: "error",
|
|
544
|
+
error: error as Error,
|
|
545
|
+
}));
|
|
546
|
+
|
|
547
|
+
options.onError?.(error as Error);
|
|
548
|
+
}
|
|
549
|
+
},
|
|
550
|
+
[client, options],
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
const reset = useCallback(() => {
|
|
554
|
+
setState(initialState as FlowUploadState<TOutput>);
|
|
555
|
+
abortRef.current = null;
|
|
556
|
+
jobIdRef.current = null;
|
|
557
|
+
}, []);
|
|
558
|
+
|
|
559
|
+
return {
|
|
560
|
+
state,
|
|
561
|
+
upload,
|
|
562
|
+
abort,
|
|
563
|
+
reset,
|
|
564
|
+
isUploading: state.status === "uploading" || state.status === "processing",
|
|
565
|
+
isUploadingFile: state.status === "uploading",
|
|
566
|
+
isProcessing: state.status === "processing",
|
|
567
|
+
};
|
|
568
|
+
}
|