@uploadista/react-native-core 0.0.15-beta.1 → 0.0.15-beta.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/dist/index.d.mts +76 -9
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/src/contexts/flow-manager-context.tsx +230 -0
- package/src/hooks/use-flow-upload.ts +54 -111
- package/src/index.ts +5 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import type { UploadistaEvent } from "@uploadista/client-core";
|
|
2
|
+
import {
|
|
3
|
+
FlowManager,
|
|
4
|
+
type FlowManagerCallbacks,
|
|
5
|
+
type FlowUploadOptions,
|
|
6
|
+
type FlowUploadState,
|
|
7
|
+
} from "@uploadista/client-core";
|
|
8
|
+
import { EventType, type FlowEvent } from "@uploadista/core/flow";
|
|
9
|
+
import { UploadEventType } from "@uploadista/core/types";
|
|
10
|
+
import type { ReactNode } from "react";
|
|
11
|
+
import {
|
|
12
|
+
createContext,
|
|
13
|
+
useCallback,
|
|
14
|
+
useContext,
|
|
15
|
+
useEffect,
|
|
16
|
+
useRef,
|
|
17
|
+
} from "react";
|
|
18
|
+
import { useUploadistaContext } from "../hooks/use-uploadista-context";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Type guard to check if an event is a flow event
|
|
22
|
+
*/
|
|
23
|
+
function isFlowEvent(event: UploadistaEvent): event is FlowEvent {
|
|
24
|
+
const flowEvent = event as FlowEvent;
|
|
25
|
+
return (
|
|
26
|
+
flowEvent.eventType === EventType.FlowStart ||
|
|
27
|
+
flowEvent.eventType === EventType.FlowEnd ||
|
|
28
|
+
flowEvent.eventType === EventType.FlowError ||
|
|
29
|
+
flowEvent.eventType === EventType.NodeStart ||
|
|
30
|
+
flowEvent.eventType === EventType.NodeEnd ||
|
|
31
|
+
flowEvent.eventType === EventType.NodePause ||
|
|
32
|
+
flowEvent.eventType === EventType.NodeResume ||
|
|
33
|
+
flowEvent.eventType === EventType.NodeError
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Internal manager registry entry with ref counting
|
|
39
|
+
*/
|
|
40
|
+
interface ManagerEntry<TOutput> {
|
|
41
|
+
manager: FlowManager<unknown, TOutput>;
|
|
42
|
+
refCount: number;
|
|
43
|
+
flowId: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Context value providing access to flow managers
|
|
48
|
+
*/
|
|
49
|
+
interface FlowManagerContextValue {
|
|
50
|
+
/**
|
|
51
|
+
* Get or create a flow manager for the given flow ID.
|
|
52
|
+
* Increments ref count - must call releaseManager when done.
|
|
53
|
+
*
|
|
54
|
+
* @param flowId - Unique identifier for the flow
|
|
55
|
+
* @param callbacks - Callbacks for state changes and lifecycle events
|
|
56
|
+
* @param options - Flow configuration options
|
|
57
|
+
* @returns FlowManager instance
|
|
58
|
+
*/
|
|
59
|
+
getManager: <TOutput = unknown>(
|
|
60
|
+
flowId: string,
|
|
61
|
+
callbacks: FlowManagerCallbacks<TOutput>,
|
|
62
|
+
options: FlowUploadOptions<TOutput>,
|
|
63
|
+
) => FlowManager<unknown, TOutput>;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Release a flow manager reference.
|
|
67
|
+
* Decrements ref count and cleans up when reaching zero.
|
|
68
|
+
*
|
|
69
|
+
* @param flowId - Unique identifier for the flow to release
|
|
70
|
+
*/
|
|
71
|
+
releaseManager: (flowId: string) => void;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const FlowManagerContext = createContext<FlowManagerContextValue | undefined>(
|
|
75
|
+
undefined,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Props for FlowManagerProvider
|
|
80
|
+
*/
|
|
81
|
+
interface FlowManagerProviderProps {
|
|
82
|
+
children: ReactNode;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Provider that manages FlowManager instances with ref counting and event routing.
|
|
87
|
+
* Ensures managers persist across component re-renders and are only cleaned up
|
|
88
|
+
* when all consuming components unmount.
|
|
89
|
+
*
|
|
90
|
+
* This provider should be nested inside UploadistaProvider to access the upload client
|
|
91
|
+
* and event subscription system.
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* ```tsx
|
|
95
|
+
* <UploadistaProvider baseUrl="https://api.example.com" storageId="default">
|
|
96
|
+
* <FlowManagerProvider>
|
|
97
|
+
* <App />
|
|
98
|
+
* </FlowManagerProvider>
|
|
99
|
+
* </UploadistaProvider>
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
export function FlowManagerProvider({ children }: FlowManagerProviderProps) {
|
|
103
|
+
const { client, subscribeToEvents } = useUploadistaContext();
|
|
104
|
+
const managersRef = useRef(
|
|
105
|
+
new Map<string, ManagerEntry<unknown>>(),
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// Subscribe to all events and route to appropriate managers
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
const unsubscribe = subscribeToEvents((event: UploadistaEvent) => {
|
|
111
|
+
// Route flow events to all managers (they filter by jobId internally)
|
|
112
|
+
if (isFlowEvent(event)) {
|
|
113
|
+
for (const entry of managersRef.current.values()) {
|
|
114
|
+
entry.manager.handleFlowEvent(event);
|
|
115
|
+
}
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Route upload progress events to all managers
|
|
120
|
+
if (
|
|
121
|
+
"type" in event &&
|
|
122
|
+
event.type === UploadEventType.UPLOAD_PROGRESS &&
|
|
123
|
+
"data" in event
|
|
124
|
+
) {
|
|
125
|
+
const uploadEvent = event as {
|
|
126
|
+
type: UploadEventType;
|
|
127
|
+
uploadId: string;
|
|
128
|
+
data: { progress: number; total: number | null };
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
for (const entry of managersRef.current.values()) {
|
|
132
|
+
entry.manager.handleUploadProgress(
|
|
133
|
+
uploadEvent.uploadId,
|
|
134
|
+
uploadEvent.data.progress,
|
|
135
|
+
uploadEvent.data.total,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
return unsubscribe;
|
|
142
|
+
}, [subscribeToEvents]);
|
|
143
|
+
|
|
144
|
+
const getManager = useCallback(
|
|
145
|
+
<TOutput,>(
|
|
146
|
+
flowId: string,
|
|
147
|
+
callbacks: FlowManagerCallbacks<TOutput>,
|
|
148
|
+
options: FlowUploadOptions<TOutput>,
|
|
149
|
+
): FlowManager<unknown, TOutput> => {
|
|
150
|
+
const existing = managersRef.current.get(flowId);
|
|
151
|
+
|
|
152
|
+
if (existing) {
|
|
153
|
+
// Increment ref count for existing manager
|
|
154
|
+
existing.refCount++;
|
|
155
|
+
return existing.manager as FlowManager<unknown, TOutput>;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Create new manager using client from hook scope
|
|
159
|
+
const flowUploadFn = (
|
|
160
|
+
input: unknown,
|
|
161
|
+
flowConfig: FlowUploadOptions<TOutput>["flowConfig"],
|
|
162
|
+
internalOptions: unknown,
|
|
163
|
+
) => {
|
|
164
|
+
return client.uploadWithFlow(input, flowConfig, internalOptions);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const manager = new FlowManager<unknown, TOutput>(
|
|
168
|
+
flowUploadFn,
|
|
169
|
+
callbacks,
|
|
170
|
+
options,
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
managersRef.current.set(flowId, {
|
|
174
|
+
manager: manager as FlowManager<unknown, unknown>,
|
|
175
|
+
refCount: 1,
|
|
176
|
+
flowId,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
return manager;
|
|
180
|
+
},
|
|
181
|
+
[client],
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
const releaseManager = useCallback((flowId: string) => {
|
|
185
|
+
const existing = managersRef.current.get(flowId);
|
|
186
|
+
if (!existing) return;
|
|
187
|
+
|
|
188
|
+
existing.refCount--;
|
|
189
|
+
|
|
190
|
+
// Clean up when no more refs
|
|
191
|
+
if (existing.refCount <= 0) {
|
|
192
|
+
existing.manager.cleanup();
|
|
193
|
+
managersRef.current.delete(flowId);
|
|
194
|
+
}
|
|
195
|
+
}, []);
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<FlowManagerContext.Provider value={{ getManager, releaseManager }}>
|
|
199
|
+
{children}
|
|
200
|
+
</FlowManagerContext.Provider>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Hook to access the FlowManager context.
|
|
206
|
+
* Must be used within a FlowManagerProvider.
|
|
207
|
+
*
|
|
208
|
+
* @returns FlowManager context value with getManager and releaseManager functions
|
|
209
|
+
* @throws Error if used outside of FlowManagerProvider
|
|
210
|
+
*
|
|
211
|
+
* @example
|
|
212
|
+
* ```tsx
|
|
213
|
+
* function MyComponent() {
|
|
214
|
+
* const { getManager, releaseManager } = useFlowManagerContext();
|
|
215
|
+
* // Use to create managers...
|
|
216
|
+
* }
|
|
217
|
+
* ```
|
|
218
|
+
*/
|
|
219
|
+
export function useFlowManagerContext(): FlowManagerContextValue {
|
|
220
|
+
const context = useContext(FlowManagerContext);
|
|
221
|
+
|
|
222
|
+
if (context === undefined) {
|
|
223
|
+
throw new Error(
|
|
224
|
+
"useFlowManagerContext must be used within a FlowManagerProvider. " +
|
|
225
|
+
"Make sure to wrap your component tree with <FlowManagerProvider>.",
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return context;
|
|
230
|
+
}
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
|
-
FlowManager,
|
|
2
|
+
type FlowManager,
|
|
3
3
|
type FlowUploadState,
|
|
4
4
|
type FlowUploadStatus,
|
|
5
|
-
type InternalFlowUploadOptions,
|
|
6
|
-
type UploadistaEvent,
|
|
7
5
|
} from "@uploadista/client-core";
|
|
8
|
-
import {
|
|
6
|
+
import type { TypedOutput } from "@uploadista/core/flow";
|
|
9
7
|
import type { UploadFile } from "@uploadista/core/types";
|
|
10
|
-
import { UploadEventType } from "@uploadista/core/types";
|
|
11
8
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
9
|
+
import { useFlowManagerContext } from "../contexts/flow-manager-context";
|
|
12
10
|
import type { FilePickResult, UseFlowUploadOptions } from "../types";
|
|
13
11
|
import { createBlobFromBuffer } from "../types/platform-types";
|
|
14
12
|
import { useUploadistaContext } from "./use-uploadista-context";
|
|
@@ -16,23 +14,6 @@ import { useUploadistaContext } from "./use-uploadista-context";
|
|
|
16
14
|
// Re-export types from core for convenience
|
|
17
15
|
export type { FlowUploadState, FlowUploadStatus };
|
|
18
16
|
|
|
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
|
-
);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
17
|
const initialState: FlowUploadState = {
|
|
37
18
|
status: "idle",
|
|
38
19
|
progress: 0,
|
|
@@ -51,7 +32,8 @@ const initialState: FlowUploadState = {
|
|
|
51
32
|
* Hook for uploading files through a flow pipeline with full state management.
|
|
52
33
|
* Provides upload progress tracking, flow execution monitoring, error handling, and abort functionality.
|
|
53
34
|
*
|
|
54
|
-
* Must be used within
|
|
35
|
+
* Must be used within FlowManagerProvider (which must be within UploadistaProvider).
|
|
36
|
+
* Flow events are automatically routed by the provider to the appropriate manager.
|
|
55
37
|
*
|
|
56
38
|
* @param options - Flow upload configuration
|
|
57
39
|
* @returns Flow upload state and control methods
|
|
@@ -87,60 +69,57 @@ const initialState: FlowUploadState = {
|
|
|
87
69
|
* ```
|
|
88
70
|
*/
|
|
89
71
|
export function useFlowUpload(options: UseFlowUploadOptions) {
|
|
90
|
-
const
|
|
91
|
-
const {
|
|
72
|
+
const { getManager, releaseManager } = useFlowManagerContext();
|
|
73
|
+
const { fileSystemProvider } = useUploadistaContext();
|
|
92
74
|
const [state, setState] = useState<FlowUploadState>(initialState);
|
|
93
|
-
const managerRef = useRef<FlowManager<
|
|
75
|
+
const managerRef = useRef<FlowManager<unknown, UploadFile> | null>(null);
|
|
94
76
|
const lastFileRef = useRef<FilePickResult | null>(null);
|
|
95
77
|
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
|
|
78
|
+
// Store callbacks in refs so they can be updated without recreating the manager
|
|
79
|
+
const callbacksRef = useRef(options);
|
|
80
|
+
|
|
81
|
+
// Update refs on every render to capture latest callbacks
|
|
99
82
|
useEffect(() => {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
};
|
|
83
|
+
callbacksRef.current = options;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Get or create manager from context when component mounts
|
|
87
|
+
// Manager lifecycle is now handled by FlowManagerProvider
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
const flowId = options.flowId;
|
|
90
|
+
|
|
91
|
+
// Create stable callback wrappers that call the latest callbacks via refs
|
|
92
|
+
const stableCallbacks = {
|
|
93
|
+
onStateChange: setState,
|
|
94
|
+
onProgress: (_uploadId: string, bytesUploaded: number, totalBytes: number | null) => {
|
|
95
|
+
if (callbacksRef.current.onProgress) {
|
|
96
|
+
const progress = totalBytes
|
|
97
|
+
? Math.round((bytesUploaded / totalBytes) * 100)
|
|
98
|
+
: 0;
|
|
99
|
+
callbacksRef.current.onProgress(progress, bytesUploaded, totalBytes);
|
|
100
|
+
}
|
|
129
101
|
},
|
|
130
|
-
{
|
|
131
|
-
|
|
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,
|
|
102
|
+
onChunkComplete: (chunkSize: number, bytesAccepted: number, bytesTotal: number | null) => {
|
|
103
|
+
callbacksRef.current.onChunkComplete?.(chunkSize, bytesAccepted, bytesTotal);
|
|
143
104
|
},
|
|
105
|
+
onFlowComplete: (outputs: TypedOutput[]) => {
|
|
106
|
+
callbacksRef.current.onFlowComplete?.(outputs);
|
|
107
|
+
},
|
|
108
|
+
onSuccess: (result: UploadFile) => {
|
|
109
|
+
callbacksRef.current.onSuccess?.(result);
|
|
110
|
+
},
|
|
111
|
+
onError: (error: Error) => {
|
|
112
|
+
callbacksRef.current.onError?.(error);
|
|
113
|
+
},
|
|
114
|
+
onAbort: () => {
|
|
115
|
+
callbacksRef.current.onAbort?.();
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// Get manager from context (creates if doesn't exist, increments ref count)
|
|
120
|
+
managerRef.current = getManager(
|
|
121
|
+
flowId,
|
|
122
|
+
stableCallbacks,
|
|
144
123
|
{
|
|
145
124
|
flowConfig: {
|
|
146
125
|
flowId: options.flowId,
|
|
@@ -154,48 +133,12 @@ export function useFlowUpload(options: UseFlowUploadOptions) {
|
|
|
154
133
|
},
|
|
155
134
|
);
|
|
156
135
|
|
|
136
|
+
// Release manager when component unmounts or flowId changes
|
|
157
137
|
return () => {
|
|
158
|
-
|
|
138
|
+
releaseManager(flowId);
|
|
139
|
+
managerRef.current = null;
|
|
159
140
|
};
|
|
160
|
-
|
|
161
|
-
}, [client]);
|
|
162
|
-
|
|
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
|
-
}
|
|
172
|
-
|
|
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]);
|
|
141
|
+
}, [options.flowId, options.storageId, options.outputNodeId, getManager, releaseManager]);
|
|
199
142
|
|
|
200
143
|
const upload = useCallback(
|
|
201
144
|
async (file: FilePickResult) => {
|
package/src/index.ts
CHANGED
|
@@ -40,6 +40,11 @@ export {
|
|
|
40
40
|
UploadProgress,
|
|
41
41
|
type UploadProgressProps,
|
|
42
42
|
} from "./components";
|
|
43
|
+
// Export contexts
|
|
44
|
+
export {
|
|
45
|
+
FlowManagerProvider,
|
|
46
|
+
useFlowManagerContext,
|
|
47
|
+
} from "./contexts/flow-manager-context";
|
|
43
48
|
// Export hooks
|
|
44
49
|
export {
|
|
45
50
|
UploadistaContext,
|