@umituz/react-native-ai-generation-content 1.87.0 → 1.87.2

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.
Files changed (30) hide show
  1. package/package.json +1 -1
  2. package/src/domains/background/infrastructure/executors/backgroundJobExecutor.ts +25 -2
  3. package/src/domains/background/infrastructure/services/job-poller.service.ts +11 -0
  4. package/src/domains/creations/infrastructure/repositories/CreationsQuery.ts +9 -1
  5. package/src/domains/creations/infrastructure/repositories/creation-create.operations.ts +10 -10
  6. package/src/domains/image-to-video/index.ts +2 -2
  7. package/src/domains/image-to-video/presentation/components/index.ts +2 -2
  8. package/src/domains/text-to-image/infrastructure-exports.ts +2 -2
  9. package/src/domains/text-to-image/presentation/components/index.ts +4 -4
  10. package/src/domains/text-to-video/index.ts +2 -2
  11. package/src/infrastructure/services/image-feature-executor.service.ts +2 -2
  12. package/src/infrastructure/services/video-feature-executor.service.ts +2 -2
  13. package/src/infrastructure/utils/feature-utils.ts +7 -2
  14. package/src/presentation/components/buttons/index.ts +2 -1
  15. package/src/presentation/components/image-picker/index.ts +2 -1
  16. package/src/presentation/hooks/ai-feature-callbacks-execution.hooks.ts +3 -1
  17. package/src/presentation/hooks/generation/moderation-handler.ts +9 -5
  18. package/src/presentation/hooks/generation/orchestrator.ts +48 -7
  19. package/src/presentation/hooks/generation/repositorySingleton.ts +17 -0
  20. package/src/presentation/hooks/generation/useAudioGenerationExecutor.ts +4 -7
  21. package/src/presentation/hooks/generation/useImageGenerationExecutor.ts +3 -6
  22. package/src/presentation/hooks/generation/useImagePicker.ts +1 -1
  23. package/src/shared/hooks/factories/createFormStateHook.ts +0 -4
  24. package/src/shared/utils/credit.ts +69 -0
  25. package/src/shared/utils/date/index.ts +3 -7
  26. package/src/shared/utils/date/timestamp.ts +71 -0
  27. package/src/shared/utils/progress.ts +123 -0
  28. package/src/domains/text-to-image/infrastructure/index.ts +0 -1
  29. package/src/domains/text-to-video/infrastructure/index.ts +0 -1
  30. package/src/presentation/components/prompts/index.ts +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-ai-generation-content",
3
- "version": "1.87.0",
3
+ "version": "1.87.2",
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",
@@ -89,6 +89,14 @@ export const executeQueuedJob = async <TInput, TResult>(
89
89
  if (completedJob) {
90
90
  await executor.onComplete?.(completedJob);
91
91
  onJobComplete?.(completedJob);
92
+ } else {
93
+ // Job reference lost - log for debugging
94
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
95
+ console.warn("[BackgroundJobExecutor] Completed job not found in cache:", {
96
+ jobId,
97
+ timestamp: new Date().toISOString(),
98
+ });
99
+ }
92
100
  }
93
101
 
94
102
  // Await removal to ensure cleanup happens before checking activeJobs
@@ -103,13 +111,28 @@ export const executeQueuedJob = async <TInput, TResult>(
103
111
  if (failedJob) {
104
112
  await executor.onError?.(failedJob, error instanceof Error ? error : new Error(errorMsg));
105
113
  onJobError?.(failedJob);
114
+ } else {
115
+ // Job reference lost - log for debugging
116
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
117
+ console.warn("[BackgroundJobExecutor] Failed job not found in cache:", {
118
+ jobId,
119
+ error: errorMsg,
120
+ timestamp: new Date().toISOString(),
121
+ });
122
+ }
106
123
  }
107
124
 
108
125
  // Remove failed job from cache to prevent accumulation
109
126
  try {
110
127
  await removeJobAsync(jobId);
111
- } catch {
112
- // Best effort cleanup
128
+ } catch (removeError) {
129
+ // Log cleanup failure but don't throw - state is already consistent
130
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
131
+ console.error("[BackgroundJobExecutor] Failed to remove job after error:", {
132
+ jobId,
133
+ error: removeError instanceof Error ? removeError.message : String(removeError),
134
+ });
135
+ }
113
136
  }
114
137
  } finally {
115
138
  // Use atomic Set operation to prevent race conditions
@@ -31,7 +31,11 @@ function withAbortSignal<T>(
31
31
  return;
32
32
  }
33
33
 
34
+ let isResolved = false;
34
35
  const abortHandler = () => {
36
+ if (isResolved) return;
37
+ isResolved = true;
38
+ if (timeoutId) clearTimeout(timeoutId);
35
39
  reject(new Error("Operation aborted"));
36
40
  };
37
41
 
@@ -41,17 +45,24 @@ function withAbortSignal<T>(
41
45
  let timeoutId: ReturnType<typeof setTimeout> | undefined;
42
46
  if (timeoutMs) {
43
47
  timeoutId = setTimeout(() => {
48
+ if (isResolved) return;
49
+ isResolved = true;
50
+ signal?.removeEventListener("abort", abortHandler);
44
51
  reject(new Error(`Operation timeout after ${timeoutMs}ms`));
45
52
  }, timeoutMs);
46
53
  }
47
54
 
48
55
  promise
49
56
  .then((result) => {
57
+ if (isResolved) return;
58
+ isResolved = true;
50
59
  signal?.removeEventListener("abort", abortHandler);
51
60
  if (timeoutId) clearTimeout(timeoutId);
52
61
  resolve(result);
53
62
  })
54
63
  .catch((error) => {
64
+ if (isResolved) return;
65
+ isResolved = true;
55
66
  signal?.removeEventListener("abort", abortHandler);
56
67
  if (timeoutId) clearTimeout(timeoutId);
57
68
  reject(error);
@@ -19,7 +19,15 @@ export class CreationsQuery {
19
19
 
20
20
  /**
21
21
  * Get all creations for a user
22
- * Optimized query: Server-side filtering for non-deleted items
22
+ *
23
+ * NOTE: Client-side filtering for deletedAt is used here for simplicity.
24
+ * For better performance with large datasets, consider:
25
+ * 1. Adding a where clause: where(CREATION_FIELDS.DELETED_AT, "==", null)
26
+ * 2. Creating a composite index on (deletedAt, createdAt)
27
+ *
28
+ * Current implementation fetches all documents including soft-deleted ones,
29
+ * which is acceptable for small-to-medium datasets but may impact performance
30
+ * at scale.
23
31
  */
24
32
  async getAll(userId: string): Promise<Creation[]> {
25
33
  const userCollection = this.pathResolver.getUserCollection(userId);
@@ -19,16 +19,16 @@ export async function createCreation(
19
19
  uri: creation.uri,
20
20
  createdAt: creation.createdAt,
21
21
  deletedAt: null,
22
- metadata: creation.metadata || {},
23
- isShared: creation.isShared || false,
24
- isFavorite: creation.isFavorite || false,
25
- ...(creation.status !== undefined && { status: creation.status }),
26
- ...(creation.output !== undefined && { output: creation.output }),
27
- ...(creation.prompt !== undefined && { prompt: creation.prompt }),
28
- ...(creation.provider !== undefined && { provider: creation.provider }),
29
- ...(creation.requestId !== undefined && { requestId: creation.requestId }),
30
- ...(creation.model !== undefined && { model: creation.model }),
31
- ...(creation.startedAt !== undefined && { startedAt: creation.startedAt }),
22
+ metadata: creation.metadata ?? {},
23
+ isShared: creation.isShared ?? false,
24
+ isFavorite: creation.isFavorite ?? false,
25
+ status: creation.status ?? null,
26
+ output: creation.output ?? null,
27
+ prompt: creation.prompt ?? null,
28
+ provider: creation.provider ?? null,
29
+ requestId: creation.requestId ?? null,
30
+ model: creation.model ?? null,
31
+ startedAt: creation.startedAt ?? null,
32
32
  };
33
33
 
34
34
  try {
@@ -48,8 +48,8 @@ export type {
48
48
  // INFRASTRUCTURE LAYER
49
49
  // =============================================================================
50
50
 
51
- export { executeImageToVideo, hasImageToVideoSupport } from "./infrastructure";
52
- export type { ExecuteImageToVideoOptions } from "./infrastructure";
51
+ export { executeImageToVideo, hasImageToVideoSupport } from "./infrastructure/services";
52
+ export type { ExecuteImageToVideoOptions } from "./infrastructure/services";
53
53
 
54
54
  // =============================================================================
55
55
  // PRESENTATION LAYER - Hooks
@@ -18,5 +18,5 @@ export type {
18
18
  } from "./ImageSelectionGrid";
19
19
 
20
20
  // Action Components
21
- export { GenerateButton as ImageToVideoGenerateButton } from "../../../../presentation/components/buttons";
22
- export type { GenerateButtonProps as ImageToVideoGenerateButtonProps } from "../../../../presentation/components/buttons";
21
+ export { GenerateButton as ImageToVideoGenerateButton } from "../../../../presentation/components/buttons/GenerateButton";
22
+ export type { GenerateButtonProps as ImageToVideoGenerateButtonProps } from "../../../../presentation/components/buttons/GenerateButton";
@@ -2,5 +2,5 @@
2
2
  * Text-to-Image Infrastructure Layer Exports
3
3
  */
4
4
 
5
- export { executeTextToImage, hasTextToImageSupport } from "./infrastructure";
6
- export type { ExecuteTextToImageOptions } from "./infrastructure";
5
+ export { executeTextToImage, hasTextToImageSupport } from "./infrastructure/services";
6
+ export type { ExecuteTextToImageOptions } from "./infrastructure/services";
@@ -7,8 +7,8 @@
7
7
  export { PromptInput as TextToImagePromptInput } from "../../../../presentation/components/PromptInput";
8
8
  export type { PromptInputProps as TextToImagePromptInputProps } from "../../../../presentation/components/PromptInput";
9
9
 
10
- export { ExamplePrompts as TextToImageExamplePrompts } from "../../../../presentation/components/prompts";
11
- export type { ExamplePromptsProps as TextToImageExamplePromptsProps } from "../../../../presentation/components/prompts";
10
+ export { ExamplePrompts as TextToImageExamplePrompts } from "../../../../presentation/components/prompts/ExamplePrompts";
11
+ export type { ExamplePromptsProps as TextToImageExamplePromptsProps } from "../../../../presentation/components/prompts/ExamplePrompts";
12
12
 
13
13
  // Selector Components
14
14
  export { StyleSelector as TextToImageStyleSelector } from "../../../../presentation/components/selectors";
@@ -22,8 +22,8 @@ export { GridSelector as TextToImageOutputFormatSelector } from "../../../../pre
22
22
  export { GridSelector as TextToImageNumImagesSelector } from "../../../../presentation/components/selectors";
23
23
 
24
24
  // Action Components
25
- export { GenerateButton as TextToImageGenerateButton } from "../../../../presentation/components/buttons";
26
- export type { GenerateButtonProps as TextToImageGenerateButtonProps } from "../../../../presentation/components/buttons";
25
+ export { GenerateButton as TextToImageGenerateButton } from "../../../../presentation/components/buttons/GenerateButton";
26
+ export type { GenerateButtonProps as TextToImageGenerateButtonProps } from "../../../../presentation/components/buttons/GenerateButton";
27
27
 
28
28
  // Sheet Components
29
29
  export { SettingsSheet as TextToImageSettingsSheet } from "../../../../presentation/components/modals/SettingsSheet";
@@ -41,8 +41,8 @@ export type {
41
41
  export { INITIAL_FORM_STATE, INITIAL_GENERATION_STATE } from "./domain";
42
42
 
43
43
  // Infrastructure Services
44
- export { executeTextToVideo, hasTextToVideoSupport } from "./infrastructure";
45
- export type { ExecuteTextToVideoOptions } from "./infrastructure";
44
+ export { executeTextToVideo, hasTextToVideoSupport } from "./infrastructure/services";
45
+ export type { ExecuteTextToVideoOptions } from "./infrastructure/services";
46
46
 
47
47
  // Presentation Hooks
48
48
  export {
@@ -35,7 +35,7 @@ export interface ImageFeatureRequest {
35
35
  export async function executeImageFeature(
36
36
  featureType: ImageFeatureType,
37
37
  request: ImageFeatureRequest,
38
- options?: ExecuteImageFeatureOptions,
38
+ options: ExecuteImageFeatureOptions = {},
39
39
  ): Promise<ImageFeatureResult> {
40
40
  const validation = validateProvider(`Image:${featureType}`);
41
41
  if (!validation.success) {
@@ -43,7 +43,7 @@ export async function executeImageFeature(
43
43
  }
44
44
 
45
45
  const { provider } = validation;
46
- const { extractResult, onProgress } = options ?? {};
46
+ const { extractResult, onProgress } = options;
47
47
  const model = provider.getImageFeatureModel(featureType);
48
48
 
49
49
  try {
@@ -18,7 +18,7 @@ import type { ExecuteVideoFeatureOptions, VideoFeatureResult, VideoFeatureReques
18
18
  export async function executeVideoFeature(
19
19
  featureType: VideoFeatureType,
20
20
  request: VideoFeatureRequest,
21
- options?: ExecuteVideoFeatureOptions,
21
+ options: ExecuteVideoFeatureOptions = {},
22
22
  ): Promise<VideoFeatureResult> {
23
23
  const validation = validateProvider(`VideoExecutor:${featureType}`);
24
24
  if (!validation.success) {
@@ -26,7 +26,7 @@ export async function executeVideoFeature(
26
26
  }
27
27
 
28
28
  const { provider } = validation;
29
- const { extractResult, onStatusChange } = options ?? {};
29
+ const { extractResult, onStatusChange } = options;
30
30
  const model = provider.getVideoFeatureModel(featureType);
31
31
 
32
32
  try {
@@ -87,15 +87,20 @@ async function checkCreditGuard(cost: number, featureName: string): Promise<bool
87
87
 
88
88
  if (!authService.isAuthenticated()) {
89
89
  if (__DEV__) {
90
-
91
90
  console.log(`[${featureName}] Auth required`);
92
91
  }
93
92
  try {
94
93
  authService.requireAuth();
94
+ // If requireAuth succeeds (e.g., user was already logged in), check auth status again
95
+ if (!authService.isAuthenticated()) {
96
+ // requireAuth succeeded but user is still not authenticated
97
+ // This can happen if requireAuth just navigated without throwing
98
+ return false;
99
+ }
95
100
  } catch {
101
+ // requireAuth threw an error (e.g., user cancelled login, navigation failed)
96
102
  return false;
97
103
  }
98
- return false;
99
104
  }
100
105
 
101
106
  const hasCredits = await creditService.checkCredits(cost);
@@ -1 +1,2 @@
1
- export { GenerateButton, type GenerateButtonProps } from "./GenerateButton";
1
+ export { GenerateButton } from "./GenerateButton";
2
+ export type { GenerateButtonProps } from "./GenerateButton";
@@ -1 +1,2 @@
1
- export { DualImagePicker, type DualImagePickerProps } from "./DualImagePicker";
1
+ export { DualImagePicker } from "./DualImagePicker";
2
+ export type { DualImagePickerProps } from "./DualImagePicker";
@@ -43,7 +43,9 @@ export function useExecutionCallback<TRequest = unknown, TResult = unknown>(
43
43
  }
44
44
 
45
45
  if (result.success) {
46
- return { success: true, imageUrls: result.imageUrls ?? [] };
46
+ // imageUrls is required when result.success is true
47
+ // executor must provide it
48
+ return { success: true, imageUrls: result.imageUrls! };
47
49
  }
48
50
  return { success: false, error: result.error ?? "Unknown error" };
49
51
  } catch (error) {
@@ -49,12 +49,16 @@ export async function handleModeration<TInput, TResult>(
49
49
  moderation.onShowWarning(
50
50
  moderationResult.warnings,
51
51
  () => {
52
+ // Cancel callback - reset state
52
53
  isGeneratingRef.current = false;
53
54
  if (isMountedRef.current) resetState();
54
55
  },
55
- () => {
56
- // Return the promise to allow proper error handling chain
57
- return executeGeneration(input).catch(async (err) => {
56
+ async () => {
57
+ // Proceed callback - execute generation with full error handling
58
+ try {
59
+ const result = await executeGeneration(input);
60
+ return result;
61
+ } catch (err) {
58
62
  const error = parseError(err);
59
63
  if (isMountedRef.current) {
60
64
  setState({ status: "error", isGenerating: false, result: null, error });
@@ -63,9 +67,9 @@ export async function handleModeration<TInput, TResult>(
63
67
  await onError?.(error);
64
68
  handleLifecycleComplete("error", undefined, error);
65
69
  throw error; // Re-throw to allow caller to handle
66
- }).finally(() => {
70
+ } finally {
67
71
  isGeneratingRef.current = false;
68
- });
72
+ }
69
73
  },
70
74
  );
71
75
  return undefined;
@@ -16,8 +16,12 @@ import type {
16
16
  UseGenerationOrchestratorReturn,
17
17
  } from "./types";
18
18
 
19
-
20
- const INITIAL_STATE = { status: "idle" as const, isGenerating: false, result: null, error: null };
19
+ const getInitialState = <T>(): GenerationState<T> => ({
20
+ status: "idle",
21
+ isGenerating: false,
22
+ result: null,
23
+ error: null,
24
+ });
21
25
 
22
26
  export const useGenerationOrchestrator = <TInput, TResult>(
23
27
  strategy: GenerationStrategy<TInput, TResult>,
@@ -25,7 +29,7 @@ export const useGenerationOrchestrator = <TInput, TResult>(
25
29
  ): UseGenerationOrchestratorReturn<TInput, TResult> => {
26
30
  const { userId, alertMessages, onSuccess, onError, moderation, lifecycle } = config;
27
31
 
28
- const [state, setState] = useState<GenerationState<TResult>>(INITIAL_STATE);
32
+ const [state, setState] = useState<GenerationState<TResult>>(getInitialState<TResult>());
29
33
  const isGeneratingRef = useRef(false);
30
34
  const isMountedRef = useRef(true);
31
35
  const abortControllerRef = useRef<AbortController | null>(null);
@@ -83,17 +87,42 @@ export const useGenerationOrchestrator = <TInput, TResult>(
83
87
  console.log("[Orchestrator] executeGeneration() called");
84
88
  }
85
89
 
90
+ // Check abort signal before starting
91
+ if (abortControllerRef.current?.signal.aborted) {
92
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
93
+ console.log("[Orchestrator] Aborted before generation started");
94
+ }
95
+ throw new Error("Generation aborted");
96
+ }
97
+
86
98
  setState((prev) => ({ ...prev, status: "generating" }));
87
99
  if (typeof __DEV__ !== "undefined" && __DEV__) {
88
100
  console.log("[Orchestrator] State: generating - calling strategy.execute()");
89
101
  }
90
102
 
91
103
  const result = await strategy.execute(input);
104
+
105
+ // Check abort signal after execution
106
+ if (abortControllerRef.current?.signal.aborted) {
107
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
108
+ console.log("[Orchestrator] Aborted after generation completed");
109
+ }
110
+ throw new Error("Generation aborted");
111
+ }
112
+
92
113
  if (typeof __DEV__ !== "undefined" && __DEV__) {
93
114
  console.log("[Orchestrator] strategy.execute() completed");
94
115
  }
95
116
 
96
117
  if (strategy.save && userId) {
118
+ // Check abort signal before save
119
+ if (abortControllerRef.current?.signal.aborted) {
120
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
121
+ console.log("[Orchestrator] Aborted before save");
122
+ }
123
+ throw new Error("Generation aborted");
124
+ }
125
+
97
126
  if (isMountedRef.current) setState((prev) => ({ ...prev, status: "saving" }));
98
127
  if (typeof __DEV__ !== "undefined" && __DEV__) {
99
128
  console.log("[Orchestrator] Saving result to Firestore");
@@ -107,10 +136,22 @@ export const useGenerationOrchestrator = <TInput, TResult>(
107
136
  if (typeof __DEV__ !== "undefined" && __DEV__) {
108
137
  console.log("[Orchestrator] ERROR: Save failed:", saveErr);
109
138
  }
139
+ // Update state to error on save failure
140
+ if (isMountedRef.current) {
141
+ setState((prev) => ({ ...prev, status: "error", isGenerating: false }));
142
+ }
110
143
  throw createGenerationError("save", alertMessages.saveFailed, saveErr instanceof Error ? saveErr : undefined);
111
144
  }
112
145
  }
113
146
 
147
+ // Final abort check before success
148
+ if (abortControllerRef.current?.signal.aborted) {
149
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
150
+ console.log("[Orchestrator] Aborted before success callback");
151
+ }
152
+ throw new Error("Generation aborted");
153
+ }
154
+
114
155
  if (isMountedRef.current) setState({ status: "success", isGenerating: false, result, error: null });
115
156
  if (typeof __DEV__ !== "undefined" && __DEV__) {
116
157
  console.log("[Orchestrator] ✅ Generation SUCCESS");
@@ -126,7 +167,7 @@ export const useGenerationOrchestrator = <TInput, TResult>(
126
167
  );
127
168
 
128
169
  const generate = useCallback(
129
- async (input: TInput) => {
170
+ async (input: TInput): Promise<TResult | void> => {
130
171
  if (typeof __DEV__ !== "undefined" && __DEV__) {
131
172
  console.log("[Orchestrator] ========================================");
132
173
  console.log("[Orchestrator] generate() called with input:", JSON.stringify(input).substring(0, 200));
@@ -145,7 +186,7 @@ export const useGenerationOrchestrator = <TInput, TResult>(
145
186
  abortControllerRef.current = new AbortController();
146
187
  isGeneratingRef.current = true;
147
188
  let moderationPending = false;
148
- setState({ ...INITIAL_STATE, status: "checking", isGenerating: true });
189
+ setState({ ...getInitialState<TResult>(), status: "checking", isGenerating: true });
149
190
 
150
191
  if (typeof __DEV__ !== "undefined" && __DEV__) {
151
192
  console.log("[Orchestrator] State set to 'checking', isGenerating: true");
@@ -183,7 +224,7 @@ export const useGenerationOrchestrator = <TInput, TResult>(
183
224
  isMountedRef,
184
225
  isGeneratingRef,
185
226
  setState: (s) => setState(s as GenerationState<TResult>),
186
- resetState: () => setState(INITIAL_STATE),
227
+ resetState: () => setState(getInitialState<TResult>()),
187
228
  executeGeneration,
188
229
  showError,
189
230
  onError,
@@ -227,7 +268,7 @@ export const useGenerationOrchestrator = <TInput, TResult>(
227
268
  const reset = useCallback(() => {
228
269
  abortControllerRef.current?.abort();
229
270
  abortControllerRef.current = null;
230
- setState(INITIAL_STATE);
271
+ setState(getInitialState<TResult>());
231
272
  isGeneratingRef.current = false;
232
273
  }, []);
233
274
 
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Repository Singleton
3
+ * Prevents multiple repository instances across hooks
4
+ */
5
+
6
+ import { createCreationsRepository } from "../../../domains/creations/infrastructure/adapters";
7
+
8
+ const REPO_COLLECTION = "creations";
9
+
10
+ let repositoryInstance: ReturnType<typeof createCreationsRepository> | null = null;
11
+
12
+ export function getCreationsRepository() {
13
+ if (!repositoryInstance) {
14
+ repositoryInstance = createCreationsRepository(REPO_COLLECTION);
15
+ }
16
+ return repositoryInstance;
17
+ }
@@ -9,10 +9,10 @@
9
9
  * - buildMetadata: creation metadata
10
10
  */
11
11
 
12
- import { useState, useCallback, useMemo } from "react";
12
+ import { useState, useCallback } from "react";
13
13
  import { useGenerationServices } from "../../../infrastructure/providers/generation-services.provider";
14
14
  import { resolveProvider } from "../../../infrastructure/services/provider-resolver";
15
- import { createCreationsRepository } from "../../../domains/creations/infrastructure/adapters";
15
+ import { getCreationsRepository } from "./repositorySingleton";
16
16
  import type { GenerationTarget } from "./useImageGenerationExecutor";
17
17
  import { handleCreditRefund, logGenerationError, generateCreationId } from "./generation-execution.utils";
18
18
 
@@ -60,10 +60,7 @@ export function useAudioGenerationExecutor<P>(
60
60
  ): AudioGenerationExecutorReturn<P> {
61
61
  const { userId, deductCredits, refundCredits, onGenerationSuccess } =
62
62
  useGenerationServices();
63
- const repository = useMemo(
64
- () => createCreationsRepository("creations"),
65
- [],
66
- );
63
+ const repository = getCreationsRepository();
67
64
  const [error, setError] = useState<string | null>(null);
68
65
  const [isLoading, setIsLoading] = useState(false);
69
66
 
@@ -106,7 +103,7 @@ export function useAudioGenerationExecutor<P>(
106
103
  output: { audioUrl },
107
104
  model: input.target.model,
108
105
  prompt: input.prompt,
109
- metadata: config.buildMetadata?.(params) ?? {},
106
+ metadata: config.buildMetadata?.(params),
110
107
  });
111
108
 
112
109
  if (config.trackRating && onGenerationSuccess) {
@@ -11,10 +11,10 @@
11
11
  * Auth and credit services come from GenerationServicesProvider context.
12
12
  */
13
13
 
14
- import { useState, useCallback, useMemo } from "react";
14
+ import { useState, useCallback } from "react";
15
15
  import { useGenerationServices } from "../../../infrastructure/providers/generation-services.provider";
16
16
  import { resolveProvider } from "../../../infrastructure/services/provider-resolver";
17
- import { createCreationsRepository } from "../../../domains/creations/infrastructure/adapters";
17
+ import { getCreationsRepository } from "./repositorySingleton";
18
18
  import { preprocessImageInputs } from "../../../infrastructure/utils/image-input-preprocessor.util";
19
19
  import { resolveType, handleCreditRefund, logGenerationError, generateCreationId } from "./generation-execution.utils";
20
20
 
@@ -66,10 +66,7 @@ export function useImageGenerationExecutor<P>(
66
66
  ): ImageGenerationExecutorReturn<P> {
67
67
  const { userId, deductCredits, refundCredits, onGenerationSuccess } =
68
68
  useGenerationServices();
69
- const repository = useMemo(
70
- () => createCreationsRepository("creations"),
71
- [],
72
- );
69
+ const repository = getCreationsRepository();
73
70
  const [error, setError] = useState<string | null>(null);
74
71
  const [isLoading, setIsLoading] = useState(false);
75
72
 
@@ -26,7 +26,7 @@ export interface UseImagePickerReturn {
26
26
  reset(): void;
27
27
  }
28
28
 
29
- export const useImagePicker = (options: UseImagePickerOptions = {}): UseImagePickerReturn => {
29
+ export const useImagePicker = (options: UseImagePickerOptions): UseImagePickerReturn => {
30
30
  const { aspect = [1, 1], onError } = options;
31
31
 
32
32
  const [uri, setUri] = useState<string | null>(null);
@@ -113,7 +113,3 @@ export function createFormStateHook<
113
113
  return { state, actions };
114
114
  };
115
115
  }
116
-
117
- // Note: createFormStateHookWithIndividualState removed due to type complexity
118
- // and lack of usage. Use createFormStateHook instead which provides the same
119
- // functionality with better type safety.
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Credit Calculation Utilities
3
+ * Provides consistent credit calculation operations
4
+ */
5
+
6
+ /**
7
+ * Validates if a value is a valid credit amount
8
+ * @param credits - Credit value to validate
9
+ * @returns True if valid (non-negative number)
10
+ */
11
+ export function isValidCreditAmount(credits: number): boolean {
12
+ return typeof credits === "number" && !isNaN(credits) && credits >= 0 && isFinite(credits);
13
+ }
14
+
15
+ /**
16
+ * Calculates total credits from multiple items
17
+ * @param items - Array of credit costs
18
+ * @returns Total credits
19
+ */
20
+ export function sumCredits(...items: number[]): number {
21
+ return items.reduce((sum, credits) => {
22
+ if (!isValidCreditAmount(credits)) return sum;
23
+ return sum + credits;
24
+ }, 0);
25
+ }
26
+
27
+ /**
28
+ * Calculates credits per unit from total cost
29
+ * @param totalCost - Total cost in base currency
30
+ * @param units - Number of units
31
+ * @returns Cost per unit
32
+ */
33
+ export function calculateCostPerUnit(totalCost: number, units: number): number {
34
+ if (units <= 0) return 0;
35
+ return totalCost / units;
36
+ }
37
+
38
+ /**
39
+ * Calculates total cost from per-unit cost
40
+ * @param costPerUnit - Cost per single unit
41
+ * @param units - Number of units
42
+ * @returns Total cost
43
+ */
44
+ export function calculateTotalCost(costPerUnit: number, units: number): number {
45
+ if (units <= 0) return 0;
46
+ return costPerUnit * units;
47
+ }
48
+
49
+ /**
50
+ * Formats credits for display
51
+ * @param credits - Credit amount
52
+ * @returns Formatted string
53
+ */
54
+ export function formatCredits(credits: number): string {
55
+ if (!isValidCreditAmount(credits)) return "0";
56
+ return Math.floor(credits).toString();
57
+ }
58
+
59
+ /**
60
+ * Parses credits from a string value
61
+ * @param value - String value to parse
62
+ * @param defaultValue - Default value if parsing fails (default: 0)
63
+ * @returns Parsed credits
64
+ */
65
+ export function parseCredits(value: string, defaultValue: number = 0): number {
66
+ const parsed = parseFloat(value);
67
+ if (isNaN(parsed)) return defaultValue;
68
+ return Math.max(0, parsed);
69
+ }
@@ -1,11 +1,7 @@
1
1
  /**
2
2
  * Date Utilities
3
- * Centralized date handling utilities
3
+ * Date normalization and timestamp operations
4
4
  */
5
5
 
6
- export {
7
- normalizeDateToTimestamp,
8
- normalizeToDate,
9
- isValidDate,
10
- compareDates,
11
- } from "./normalization";
6
+ export * from "./normalization";
7
+ export * from "./timestamp";
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Timestamp Utilities
3
+ * Provides consistent timestamp operations across the application
4
+ */
5
+
6
+ /**
7
+ * Gets the current timestamp in milliseconds
8
+ * @returns Current timestamp in milliseconds
9
+ */
10
+ export function getCurrentTimestamp(): number {
11
+ return Date.now();
12
+ }
13
+
14
+ /**
15
+ * Calculates elapsed time in milliseconds
16
+ * @param startTime - Start timestamp in milliseconds
17
+ * @param endTime - End timestamp in milliseconds (defaults to current time)
18
+ * @returns Elapsed time in milliseconds
19
+ */
20
+ export function getElapsedMs(startTime: number, endTime?: number): number {
21
+ const end = endTime ?? Date.now();
22
+ return end - startTime;
23
+ }
24
+
25
+ /**
26
+ * Formats elapsed time in milliseconds to a human-readable string
27
+ * @param elapsedMs - Elapsed time in milliseconds
28
+ * @returns Formatted string (e.g., "1.5s", "500ms", "2m 30s")
29
+ */
30
+ export function formatElapsedMs(elapsedMs: number): string {
31
+ if (elapsedMs < 1000) {
32
+ return `${elapsedMs}ms`;
33
+ }
34
+ if (elapsedMs < 60000) {
35
+ return `${(elapsedMs / 1000).toFixed(1)}s`;
36
+ }
37
+ const minutes = Math.floor(elapsedMs / 60000);
38
+ const seconds = Math.floor((elapsedMs % 60000) / 1000);
39
+ return `${minutes}m ${seconds}s`;
40
+ }
41
+
42
+ /**
43
+ * Creates a unique timestamp-based ID
44
+ * @param prefix - Optional prefix for the ID
45
+ * @returns Unique ID with timestamp
46
+ */
47
+ export function createTimestampId(prefix: string = ""): string {
48
+ const timestamp = Date.now();
49
+ const random = Math.random().toString(36).slice(2, 9);
50
+ return prefix ? `${prefix}-${timestamp}-${random}` : `${timestamp}-${random}`;
51
+ }
52
+
53
+ /**
54
+ * Converts a timestamp to ISO string
55
+ * @param timestamp - Timestamp in milliseconds
56
+ * @returns ISO 8601 formatted string
57
+ */
58
+ export function timestampToISOString(timestamp: number): string {
59
+ return new Date(timestamp).toISOString();
60
+ }
61
+
62
+ /**
63
+ * Checks if a timestamp is recent (within the specified milliseconds)
64
+ * @param timestamp - Timestamp to check
65
+ * @param recentMs - Threshold for "recent" in milliseconds (default: 5000ms)
66
+ * @returns True if timestamp is within the threshold
67
+ */
68
+ export function isTimestampRecent(timestamp: number, recentMs: number = 5000): boolean {
69
+ const elapsed = Date.now() - timestamp;
70
+ return elapsed >= 0 && elapsed <= recentMs;
71
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Progress Utilities
3
+ * Provides consistent progress calculations and mappings
4
+ */
5
+
6
+ /**
7
+ * Clamps a value between 0 and 100
8
+ * @param value - Value to clamp
9
+ * @returns Clamped value between 0 and 100
10
+ */
11
+ export function clampProgress(value: number): number {
12
+ return Math.max(0, Math.min(100, value));
13
+ }
14
+
15
+ /**
16
+ * Maps a value from one range to another (0-100)
17
+ * @param value - Value to map
18
+ * @param min - Minimum of input range
19
+ * @param max - Maximum of input range
20
+ * @returns Mapped value in 0-100 range
21
+ */
22
+ export function mapToProgress(value: number, min: number, max: number): number {
23
+ if (max === min) return 0;
24
+ const normalized = (value - min) / (max - min);
25
+ return clampProgress(normalized * 100);
26
+ }
27
+
28
+ /**
29
+ * Maps a value from one range to another (0-100) with optional clamping
30
+ * @param value - Value to map
31
+ * @param min - Minimum of input range
32
+ * @param max - Maximum of input range
33
+ * @param clamp - Whether to clamp the result (default: true)
34
+ * @returns Mapped value in 0-100 range
35
+ */
36
+ export function mapRangeToProgress(
37
+ value: number,
38
+ min: number,
39
+ max: number,
40
+ clamp: boolean = true
41
+ ): number {
42
+ if (max === min) return 0;
43
+ const normalized = (value - min) / (max - min);
44
+ const result = normalized * 100;
45
+ return clamp ? clampProgress(result) : result;
46
+ }
47
+
48
+ /**
49
+ * Calculates progress percentage from current and total
50
+ * @param current - Current value
51
+ * @param total - Total value
52
+ * @returns Progress percentage (0-100)
53
+ */
54
+ export function calculateProgress(current: number, total: number): number {
55
+ if (total <= 0) return 0;
56
+ if (current >= total) return 100;
57
+ return clampProgress((current / total) * 100);
58
+ }
59
+
60
+ /**
61
+ * Interpolates between two values based on progress (0-100)
62
+ * @param start - Start value
63
+ * @param end - End value
64
+ * @param progress - Progress (0-100)
65
+ * @returns Interpolated value
66
+ */
67
+ export function interpolateProgress(start: number, end: number, progress: number): number {
68
+ const clampedProgress = clampProgress(progress);
69
+ const factor = clampedProgress / 100;
70
+ return start + (end - start) * factor;
71
+ }
72
+
73
+ /**
74
+ * Converts linear progress to eased progress (smooth animation)
75
+ * @param progress - Linear progress (0-100)
76
+ * @param easing - Easing function (default: ease-out cubic)
77
+ * @returns Eased progress (0-100)
78
+ */
79
+ export function easeProgress(
80
+ progress: number,
81
+ easing: "linear" | "easeIn" | "easeOut" | "easeInOut" = "easeOut"
82
+ ): number {
83
+ const clamped = clampProgress(progress);
84
+ const normalized = clamped / 100;
85
+
86
+ let eased: number;
87
+ switch (easing) {
88
+ case "linear":
89
+ eased = normalized;
90
+ break;
91
+ case "easeIn":
92
+ eased = normalized * normalized;
93
+ break;
94
+ case "easeOut":
95
+ eased = 1 - Math.pow(1 - normalized, 2);
96
+ break;
97
+ case "easeInOut":
98
+ eased = normalized < 0.5
99
+ ? 2 * normalized * normalized
100
+ : 1 - Math.pow(-2 * normalized + 2, 2) / 2;
101
+ break;
102
+ }
103
+
104
+ return eased * 100;
105
+ }
106
+
107
+ /**
108
+ * Checks if progress is complete (100)
109
+ * @param progress - Progress value to check
110
+ * @returns True if progress is 100
111
+ */
112
+ export function isProgressComplete(progress: number): boolean {
113
+ return progress >= 100;
114
+ }
115
+
116
+ /**
117
+ * Checks if progress has started (> 0)
118
+ * @param progress - Progress value to check
119
+ * @returns True if progress is greater than 0
120
+ */
121
+ export function hasProgressStarted(progress: number): boolean {
122
+ return progress > 0;
123
+ }
@@ -1 +0,0 @@
1
- export * from "./services";
@@ -1 +0,0 @@
1
- export * from "./services";
@@ -1 +0,0 @@
1
- export { ExamplePrompts, type ExamplePromptsProps } from "./ExamplePrompts";