@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
|
@@ -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 {
|
|
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
|
|
7
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
90
|
+
const context = useUploadistaContext();
|
|
91
|
+
const { client, fileSystemProvider } = context;
|
|
75
92
|
const [state, setState] = useState<FlowUploadState>(initialState);
|
|
76
|
-
const
|
|
93
|
+
const managerRef = useRef<FlowManager<Blob, UploadFile> | null>(null);
|
|
77
94
|
const lastFileRef = useRef<FilePickResult | null>(null);
|
|
78
95
|
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
//
|
|
135
|
-
const
|
|
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
|
-
[
|
|
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
|
}, [
|