@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 +1 -1
- package/src/domains/background/infrastructure/executors/backgroundJobExecutor.ts +17 -6
- package/src/domains/background/presentation/hooks/use-background-generation.ts +4 -3
- package/src/domains/background/presentation/hooks/use-pending-jobs.ts +4 -0
- package/src/domains/creations/infrastructure/repositories/CreationsFetcher.ts +13 -1
- package/src/domains/generation/infrastructure/flow/useFlow.ts +13 -25
- package/src/index.ts +3 -0
- package/src/presentation/hooks/use-generation.ts +32 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-generation-content",
|
|
3
|
-
"version": "1.65.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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 (
|
|
43
|
-
if (configChanged &&
|
|
44
|
-
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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
|
@@ -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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
96
|
+
onSuccessRef.current?.(genResult);
|
|
83
97
|
} else if (genResult.error) {
|
|
84
98
|
setError(genResult.error);
|
|
85
|
-
|
|
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
|
-
|
|
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);
|