@umituz/react-native-ai-generation-content 1.65.6 → 1.65.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-ai-generation-content",
3
- "version": "1.65.6",
3
+ "version": "1.65.7",
4
4
  "description": "Provider-agnostic AI generation orchestration for React Native with result preview components",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -43,7 +43,8 @@ interface QueuedExecutionParams<TInput, TResult> {
43
43
  input: TInput;
44
44
  executor: JobExecutorConfig<TInput, TResult>;
45
45
  updateJob: (params: { id: string; updates: Partial<BackgroundJob<TInput, TResult>> }) => void;
46
- removeJob: (id: string) => void;
46
+ updateJobAsync: (params: { id: string; updates: Partial<BackgroundJob<TInput, TResult>> }) => Promise<{ id: string; updates: Partial<BackgroundJob<TInput, TResult>> }>;
47
+ removeJobAsync: (id: string) => Promise<string>;
47
48
  getJob: (id: string) => BackgroundJob<TInput, TResult> | undefined;
48
49
  activeJobsRef: React.MutableRefObject<Set<string>>;
49
50
  onJobComplete?: (job: BackgroundJob<TInput, TResult>) => void;
@@ -59,7 +60,8 @@ export const executeQueuedJob = async <TInput, TResult>(
59
60
  input,
60
61
  executor,
61
62
  updateJob,
62
- removeJob,
63
+ updateJobAsync,
64
+ removeJobAsync,
63
65
  getJob,
64
66
  activeJobsRef,
65
67
  onJobComplete,
@@ -68,13 +70,17 @@ export const executeQueuedJob = async <TInput, TResult>(
68
70
  } = params;
69
71
 
70
72
  try {
71
- updateJob({ id: jobId, updates: { status: "processing", progress: 10 } });
73
+ // Critical status update - await to ensure state consistency
74
+ await updateJobAsync({ id: jobId, updates: { status: "processing", progress: 10 } });
72
75
 
73
76
  const result = await executor.execute(input, (p) => {
77
+ // Progress updates use non-async version for performance
78
+ // Progress updates are frequent and eventual consistency is acceptable
74
79
  updateJob({ id: jobId, updates: { progress: p } });
75
80
  });
76
81
 
77
- updateJob({
82
+ // Critical status update - await to ensure state consistency
83
+ await updateJobAsync({
78
84
  id: jobId,
79
85
  updates: { status: "completed", progress: 100, result, completedAt: new Date() },
80
86
  });
@@ -85,11 +91,13 @@ export const executeQueuedJob = async <TInput, TResult>(
85
91
  onJobComplete?.(completedJob);
86
92
  }
87
93
 
88
- removeJob(jobId);
94
+ // Await removal to ensure cleanup happens before checking activeJobs
95
+ await removeJobAsync(jobId);
89
96
  } catch (error) {
90
97
  const errorMsg = error instanceof Error ? error.message : String(error);
91
98
 
92
- updateJob({ id: jobId, updates: { status: "failed", error: errorMsg, progress: 0 } });
99
+ // Critical error status update - await for consistency
100
+ await updateJobAsync({ id: jobId, updates: { status: "failed", error: errorMsg, progress: 0 } });
93
101
 
94
102
  const failedJob = getJob(jobId);
95
103
  if (failedJob) {
@@ -97,7 +105,10 @@ export const executeQueuedJob = async <TInput, TResult>(
97
105
  onJobError?.(failedJob);
98
106
  }
99
107
  } finally {
108
+ // Use atomic Set operation to prevent race conditions
100
109
  activeJobsRef.current.delete(jobId);
110
+
111
+ // Check size after deletion to prevent multiple onAllComplete calls
101
112
  if (activeJobsRef.current.size === 0) {
102
113
  onAllComplete?.();
103
114
  }
@@ -21,7 +21,7 @@ export function useBackgroundGeneration<TInput = unknown, TResult = unknown>(
21
21
  const [isProcessing, setIsProcessing] = useState(false);
22
22
  const [progress, setProgress] = useState(0);
23
23
 
24
- const { jobs, addJobAsync, updateJob, removeJob, getJob } = usePendingJobs<
24
+ const { jobs, addJobAsync, updateJob, updateJobAsync, removeJob, removeJobAsync, getJob } = usePendingJobs<
25
25
  TInput,
26
26
  TResult
27
27
  >({
@@ -43,14 +43,15 @@ export function useBackgroundGeneration<TInput = unknown, TResult = unknown>(
43
43
  input,
44
44
  executor,
45
45
  updateJob,
46
- removeJob,
46
+ updateJobAsync,
47
+ removeJobAsync,
47
48
  getJob,
48
49
  activeJobsRef,
49
50
  onJobComplete,
50
51
  onJobError,
51
52
  onAllComplete,
52
53
  }),
53
- [executor, onJobComplete, onJobError, onAllComplete, updateJob, removeJob, getJob],
54
+ [executor, onJobComplete, onJobError, onAllComplete, updateJob, updateJobAsync, removeJobAsync, getJob],
54
55
  );
55
56
 
56
57
  const startJob = useCallback(
@@ -22,7 +22,9 @@ export interface UsePendingJobsReturn<TInput = unknown, TResult = unknown> {
22
22
  readonly addJob: (input: AddJobInput<TInput>) => void;
23
23
  readonly addJobAsync: (input: AddJobInput<TInput>) => Promise<BackgroundJob<TInput, TResult>>;
24
24
  readonly updateJob: (input: UpdateJobInput) => void;
25
+ readonly updateJobAsync: (input: UpdateJobInput) => Promise<{ id: string; updates: Partial<BackgroundJob<TInput, TResult>> }>;
25
26
  readonly removeJob: (id: string) => void;
27
+ readonly removeJobAsync: (id: string) => Promise<string>;
26
28
  readonly clearCompleted: () => void;
27
29
  readonly clearFailed: () => void;
28
30
  readonly getJob: (id: string) => BackgroundJob<TInput, TResult> | undefined;
@@ -116,7 +118,9 @@ export function usePendingJobs<TInput = unknown, TResult = unknown>(
116
118
  addJob: addJobMutation.mutate,
117
119
  addJobAsync: addJobMutation.mutateAsync,
118
120
  updateJob: updateJobMutation.mutate,
121
+ updateJobAsync: updateJobMutation.mutateAsync,
119
122
  removeJob: removeJobMutation.mutate,
123
+ removeJobAsync: removeJobMutation.mutateAsync,
120
124
  clearCompleted: clearCompletedMutation.mutate,
121
125
  clearFailed: clearFailedMutation.mutate,
122
126
  getJob,
@@ -95,8 +95,20 @@ export class CreationsFetcher {
95
95
  ): UnsubscribeFunction {
96
96
  const userCollection = this.pathResolver.getUserCollection(userId);
97
97
  if (!userCollection) {
98
+ const error = new Error(`[CreationsFetcher] Cannot subscribe: Invalid user collection for userId: ${userId}`);
99
+ if (__DEV__) {
100
+ console.error(error.message);
101
+ }
102
+ // Return empty array immediately
98
103
  onData([]);
99
- return () => {};
104
+ // Report error to callback
105
+ onError?.(error);
106
+ // Return no-op unsubscribe function (no listener was created)
107
+ return () => {
108
+ if (__DEV__) {
109
+ console.log("[CreationsFetcher] No-op unsubscribe called (no listener was created)");
110
+ }
111
+ };
100
112
  }
101
113
 
102
114
  // Optimized query with server-side filtering
@@ -25,12 +25,9 @@ interface UseFlowReturn extends FlowState, FlowActions {
25
25
  getPartnerName: (partnerId: string) => string;
26
26
  }
27
27
 
28
- let flowStoreInstance: FlowStoreType | null = null;
29
-
30
28
  export const useFlow = (config: UseFlowConfig): UseFlowReturn => {
31
29
  const storeRef = useRef<FlowStoreType | null>(null);
32
30
  const prevConfigRef = useRef<{ initialStepIndex?: number; initialStepId?: string; stepsCount: number } | undefined>(undefined);
33
- const isResettingRef = useRef(false);
34
31
 
35
32
  // Detect config changes (initialStepIndex, initialStepId, or steps changed)
36
33
  const configChanged =
@@ -39,27 +36,19 @@ export const useFlow = (config: UseFlowConfig): UseFlowReturn => {
39
36
  prevConfigRef.current.initialStepId !== config.initialStepId ||
40
37
  prevConfigRef.current.stepsCount !== config.steps.length);
41
38
 
42
- // If config changed, reset and recreate store (with guard against multiple resets)
43
- if (configChanged && !isResettingRef.current) {
44
- isResettingRef.current = true;
45
- if (flowStoreInstance) {
46
- flowStoreInstance.getState().reset();
47
- }
48
- flowStoreInstance = null;
39
+ // If config changed, reset and recreate store (per-component instance)
40
+ if (configChanged && storeRef.current) {
41
+ storeRef.current.getState().reset();
49
42
  storeRef.current = null;
50
- isResettingRef.current = false;
51
43
  }
52
44
 
53
- // Initialize store if needed
54
- if (!storeRef.current && !isResettingRef.current) {
55
- if (!flowStoreInstance) {
56
- flowStoreInstance = createFlowStore({
57
- steps: config.steps,
58
- initialStepId: config.initialStepId,
59
- initialStepIndex: config.initialStepIndex,
60
- });
61
- }
62
- storeRef.current = flowStoreInstance;
45
+ // Initialize store if needed (per-component instance)
46
+ if (!storeRef.current) {
47
+ storeRef.current = createFlowStore({
48
+ steps: config.steps,
49
+ initialStepId: config.initialStepId,
50
+ initialStepIndex: config.initialStepIndex,
51
+ });
63
52
  }
64
53
 
65
54
  // Store current config for next render comparison
@@ -115,9 +104,8 @@ export const useFlow = (config: UseFlowConfig): UseFlowReturn => {
115
104
  };
116
105
  };
117
106
 
107
+ // Note: resetFlowStore is no longer needed as each component instance maintains its own store
108
+ // If you need to reset flow state, use the reset() action from the useFlow hook
118
109
  export const resetFlowStore = () => {
119
- if (flowStoreInstance) {
120
- flowStoreInstance.getState().reset();
121
- }
122
- flowStoreInstance = null;
110
+ console.warn('resetFlowStore is deprecated. Each component now maintains its own flow store instance.');
123
111
  };
package/src/index.ts CHANGED
@@ -7,3 +7,6 @@ export * from "./exports/domain";
7
7
  export * from "./exports/infrastructure";
8
8
  export * from "./exports/presentation";
9
9
  export * from "./exports/features";
10
+
11
+ // Creations Domain
12
+ export * from "./domains/creations";
@@ -37,27 +37,40 @@ export function useGeneration<T = unknown>(
37
37
  const [isGenerating, setIsGenerating] = useState(false);
38
38
  const [error, setError] = useState<string | null>(null);
39
39
 
40
+ const abortControllerRef = useRef<AbortController | null>(null);
40
41
  const abortRef = useRef(false);
41
42
 
42
43
  // Abort on unmount to prevent state updates after unmount
43
44
  useEffect(() => {
44
45
  return () => {
45
46
  abortRef.current = true;
47
+ abortControllerRef.current?.abort();
46
48
  };
47
49
  }, []);
48
50
 
49
- const handleProgress = useCallback(
50
- (prog: GenerationProgress) => {
51
- if (abortRef.current) return;
52
- setProgress(prog);
53
- options.onProgress?.(prog);
54
- },
55
- [options],
56
- );
51
+ // Stabilize callbacks to prevent unnecessary re-renders
52
+ const onSuccessRef = useRef(options.onSuccess);
53
+ const onErrorRef = useRef(options.onError);
54
+ const onProgressRef = useRef(options.onProgress);
55
+
56
+ useEffect(() => {
57
+ onSuccessRef.current = options.onSuccess;
58
+ onErrorRef.current = options.onError;
59
+ onProgressRef.current = options.onProgress;
60
+ }, [options.onSuccess, options.onError, options.onProgress]);
61
+
62
+ const handleProgress = useCallback((prog: GenerationProgress) => {
63
+ if (abortRef.current) return;
64
+ setProgress(prog);
65
+ onProgressRef.current?.(prog);
66
+ }, []);
57
67
 
58
68
  const generate = useCallback(
59
69
  async (input: Record<string, unknown>, userId?: string) => {
70
+ // Create new AbortController for this generation
71
+ abortControllerRef.current = new AbortController();
60
72
  abortRef.current = false;
73
+
61
74
  setIsGenerating(true);
62
75
  setError(null);
63
76
  setResult(null);
@@ -70,38 +83,42 @@ export function useGeneration<T = unknown>(
70
83
  userId,
71
84
  capability: options.capability,
72
85
  onProgress: handleProgress,
86
+ signal: abortControllerRef.current.signal,
73
87
  };
74
88
 
75
89
  const genResult = await generationOrchestrator.generate<T>(request);
76
90
 
77
- if (abortRef.current) return;
91
+ if (abortRef.current || abortControllerRef.current.signal.aborted) return;
78
92
 
79
93
  setResult(genResult);
80
94
 
81
95
  if (genResult.success) {
82
- options.onSuccess?.(genResult);
96
+ onSuccessRef.current?.(genResult);
83
97
  } else if (genResult.error) {
84
98
  setError(genResult.error);
85
- options.onError?.(genResult.error);
99
+ onErrorRef.current?.(genResult.error);
86
100
  }
87
101
  } catch (err) {
88
- if (abortRef.current) return;
102
+ if (abortRef.current || abortControllerRef.current?.signal.aborted) return;
89
103
 
90
104
  const errorMessage =
91
105
  err instanceof Error ? err.message : "error.unknown";
92
106
  setError(errorMessage);
93
- options.onError?.(errorMessage);
107
+ onErrorRef.current?.(errorMessage);
94
108
  } finally {
95
- if (!abortRef.current) {
109
+ if (!abortRef.current && !abortControllerRef.current?.signal.aborted) {
96
110
  setIsGenerating(false);
97
111
  }
112
+ abortControllerRef.current = null;
98
113
  }
99
114
  },
100
- [options, handleProgress],
115
+ [options.model, options.capability, handleProgress],
101
116
  );
102
117
 
103
118
  const reset = useCallback(() => {
104
119
  abortRef.current = true;
120
+ abortControllerRef.current?.abort();
121
+ abortControllerRef.current = null;
105
122
  setResult(null);
106
123
  setProgress(null);
107
124
  setIsGenerating(false);