@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.
@@ -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 { EventType, type FlowEvent } from "@uploadista/core/flow";
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 an UploadistaProvider.
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 context = useUploadistaContext();
91
- const { client, fileSystemProvider } = context;
72
+ const { getManager, releaseManager } = useFlowManagerContext();
73
+ const { fileSystemProvider } = useUploadistaContext();
92
74
  const [state, setState] = useState<FlowUploadState>(initialState);
93
- const managerRef = useRef<FlowManager<Blob, UploadFile> | null>(null);
75
+ const managerRef = useRef<FlowManager<unknown, UploadFile> | null>(null);
94
76
  const lastFileRef = useRef<FilePickResult | null>(null);
95
77
 
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
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
- 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
- };
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
- 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,
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
- managerRef.current?.cleanup();
138
+ releaseManager(flowId);
139
+ managerRef.current = null;
159
140
  };
160
- // eslint-disable-next-line react-hooks/exhaustive-deps
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,