@umituz/react-native-ai-fal-provider 2.1.4 → 2.1.6

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 (39) hide show
  1. package/package.json +1 -1
  2. package/src/exports/infrastructure.ts +2 -11
  3. package/src/index.ts +1 -1
  4. package/src/infrastructure/services/fal-provider-subscription.ts +1 -1
  5. package/src/infrastructure/services/fal-provider.ts +1 -2
  6. package/src/infrastructure/services/index.ts +0 -16
  7. package/src/infrastructure/services/request-store.ts +3 -3
  8. package/src/infrastructure/utils/collections/array-sorters.util.ts +34 -0
  9. package/src/infrastructure/utils/collections/index.ts +0 -1
  10. package/src/infrastructure/utils/cost-tracker.ts +4 -2
  11. package/src/infrastructure/utils/date-format.util.ts +13 -47
  12. package/src/infrastructure/utils/fal-error-handler.util.ts +1 -1
  13. package/src/infrastructure/utils/fal-storage.util.ts +40 -1
  14. package/src/infrastructure/utils/helpers/index.ts +0 -1
  15. package/src/infrastructure/utils/helpers/timing-helpers.util.ts +1 -79
  16. package/src/infrastructure/utils/image-helpers.util.ts +9 -5
  17. package/src/infrastructure/utils/index.ts +20 -63
  18. package/src/infrastructure/utils/input-preprocessor.util.ts +47 -4
  19. package/src/infrastructure/utils/input-validator.util.ts +1 -1
  20. package/src/infrastructure/utils/job-metadata/job-metadata-format.util.ts +26 -1
  21. package/src/infrastructure/utils/job-metadata/job-metadata-queries.util.ts +2 -2
  22. package/src/infrastructure/utils/job-storage/job-storage-crud.util.ts +1 -1
  23. package/src/infrastructure/utils/job-storage/job-storage-queries.util.ts +1 -1
  24. package/src/infrastructure/utils/number-format.util.ts +27 -20
  25. package/src/infrastructure/utils/parsers/index.ts +0 -1
  26. package/src/infrastructure/utils/string-format.util.ts +0 -59
  27. package/src/presentation/hooks/use-fal-generation.ts +1 -1
  28. package/src/infrastructure/utils/collection-filters.util.ts +0 -9
  29. package/src/infrastructure/utils/collections/array-reducers.util.ts +0 -67
  30. package/src/infrastructure/utils/data-parsers.util.ts +0 -9
  31. package/src/infrastructure/utils/error-mapper.ts +0 -24
  32. package/src/infrastructure/utils/formatting.util.ts +0 -31
  33. package/src/infrastructure/utils/general-helpers.util.ts +0 -9
  34. package/src/infrastructure/utils/helpers/function-helpers.util.ts +0 -25
  35. package/src/infrastructure/utils/input-builders.util.ts +0 -6
  36. package/src/infrastructure/utils/parsers/number-helpers.util.ts +0 -19
  37. package/src/infrastructure/utils/type-guards.util.ts +0 -9
  38. package/src/infrastructure/validators/index.ts +0 -6
  39. package/src/init/index.ts +0 -10
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-ai-fal-provider",
3
- "version": "2.1.4",
3
+ "version": "2.1.6",
4
4
  "description": "FAL AI provider for React Native - implements IAIProvider interface for unified AI generation",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -7,8 +7,6 @@ export {
7
7
  falProvider,
8
8
  falModelsService,
9
9
  NSFWContentError,
10
- cancelCurrentFalRequest,
11
- hasRunningFalRequest,
12
10
  cleanupRequestStore,
13
11
  stopAutomaticCleanup,
14
12
  } from "../infrastructure/services";
@@ -45,7 +43,6 @@ export {
45
43
  uploadToFalStorage,
46
44
  uploadMultipleToFalStorage,
47
45
  formatNumber,
48
- formatCurrency,
49
46
  formatBytes,
50
47
  formatDuration,
51
48
  truncateText,
@@ -55,10 +52,7 @@ export {
55
52
  isDefined,
56
53
  removeNullish,
57
54
  generateUniqueId,
58
- debounce,
59
- throttle,
60
55
  sleep,
61
- retry,
62
56
  } from "../infrastructure/utils";
63
57
 
64
58
  export {
@@ -89,8 +83,5 @@ export {
89
83
  updateJobStatus,
90
84
  } from "../infrastructure/utils";
91
85
 
92
- export type {
93
- FalJobMetadata,
94
- IJobStorage,
95
- InMemoryJobStorage,
96
- } from "../infrastructure/utils";
86
+ export type { FalJobMetadata, IJobStorage } from "../infrastructure/utils";
87
+ export { InMemoryJobStorage } from "../infrastructure/utils";
package/src/index.ts CHANGED
@@ -16,4 +16,4 @@ export * from "./exports/presentation";
16
16
  export {
17
17
  createAiProviderInitModule,
18
18
  type AiProviderInitModuleConfig,
19
- } from './init';
19
+ } from './init/createAiProviderInitModule';
@@ -10,7 +10,7 @@ import { DEFAULT_FAL_CONFIG } from "./fal-provider.constants";
10
10
  import { mapFalStatusToJobStatus } from "./fal-status-mapper";
11
11
  import { validateNSFWContent } from "../validators/nsfw-validator";
12
12
  import { NSFWContentError } from "./nsfw-content-error";
13
- import { parseFalError } from "../utils/error-mapper";
13
+ import { parseFalError } from "../utils/fal-error-handler.util";
14
14
 
15
15
  /**
16
16
  * Handle FAL subscription with timeout and cancellation
@@ -140,6 +140,7 @@ export class FalProvider implements IAIProvider {
140
140
  storeRequest(key, { promise, abortController, createdAt: Date.now() });
141
141
 
142
142
  // Execute the actual operation and resolve/reject the stored promise
143
+ // Note: This promise chain is not awaited - it runs independently
143
144
  executeWithCostTracking({
144
145
  tracker,
145
146
  model,
@@ -149,11 +150,9 @@ export class FalProvider implements IAIProvider {
149
150
  })
150
151
  .then((res) => {
151
152
  resolvePromise(res.result);
152
- return res.result;
153
153
  })
154
154
  .catch((error) => {
155
155
  rejectPromise(error);
156
- throw error;
157
156
  })
158
157
  .finally(() => {
159
158
  try {
@@ -2,8 +2,6 @@
2
2
  * Services Index
3
3
  */
4
4
 
5
- import { falProvider } from "./fal-provider";
6
-
7
5
  export { FalProvider, falProvider } from "./fal-provider";
8
6
  export type { FalProvider as FalProviderType } from "./fal-provider";
9
7
  export { falModelsService, type FalModelConfig, type ModelSelectionResult } from "./fal-models.service";
@@ -21,17 +19,3 @@ export {
21
19
  stopAutomaticCleanup,
22
20
  } from "./request-store";
23
21
  export type { ActiveRequest } from "./request-store";
24
-
25
- /**
26
- * Cancel the current running FAL request
27
- */
28
- export function cancelCurrentFalRequest(): void {
29
- falProvider.cancelCurrentRequest();
30
- }
31
-
32
- /**
33
- * Check if there's a running FAL request
34
- */
35
- export function hasRunningFalRequest(): boolean {
36
- return falProvider.hasRunningRequest();
37
- }
@@ -86,11 +86,11 @@ export function storeRequest<T>(key: string, request: ActiveRequest<T>): void {
86
86
  const maxRetries = 10;
87
87
  let retries = 0;
88
88
 
89
- // Spin-wait with small delay between retries
89
+ // Spin-wait loop (synchronous)
90
+ // Note: Does NOT yield to event loop - tight loop
91
+ // In practice, this rarely loops due to single-threaded nature of React Native
90
92
  while (!acquireLock() && retries < maxRetries) {
91
93
  retries++;
92
- // Yield to event loop between retries
93
- // In practice, this rarely loops due to single-threaded nature
94
94
  }
95
95
 
96
96
  if (retries >= maxRetries) {
@@ -5,6 +5,7 @@
5
5
 
6
6
  /**
7
7
  * Sort array by date property (descending - newest first)
8
+ * Invalid dates are sorted to the end
8
9
  */
9
10
  export function sortByDateDescending<T>(
10
11
  items: readonly T[],
@@ -13,12 +14,19 @@ export function sortByDateDescending<T>(
13
14
  return [...items].sort((a, b) => {
14
15
  const timeA = new Date(a[dateProperty] as unknown as string).getTime();
15
16
  const timeB = new Date(b[dateProperty] as unknown as string).getTime();
17
+
18
+ // Handle invalid dates - NaN should sort to end
19
+ if (isNaN(timeA) && isNaN(timeB)) return 0;
20
+ if (isNaN(timeA)) return 1; // a goes to end
21
+ if (isNaN(timeB)) return -1; // b goes to end
22
+
16
23
  return timeB - timeA;
17
24
  });
18
25
  }
19
26
 
20
27
  /**
21
28
  * Sort array by date property (ascending - oldest first)
29
+ * Invalid dates are sorted to the end
22
30
  */
23
31
  export function sortByDateAscending<T>(
24
32
  items: readonly T[],
@@ -27,12 +35,19 @@ export function sortByDateAscending<T>(
27
35
  return [...items].sort((a, b) => {
28
36
  const timeA = new Date(a[dateProperty] as unknown as string).getTime();
29
37
  const timeB = new Date(b[dateProperty] as unknown as string).getTime();
38
+
39
+ // Handle invalid dates - NaN should sort to end
40
+ if (isNaN(timeA) && isNaN(timeB)) return 0;
41
+ if (isNaN(timeA)) return 1; // a goes to end
42
+ if (isNaN(timeB)) return -1; // b goes to end
43
+
30
44
  return timeA - timeB;
31
45
  });
32
46
  }
33
47
 
34
48
  /**
35
49
  * Sort array by number property (descending)
50
+ * NaN and Infinity values are sorted to the end
36
51
  */
37
52
  export function sortByNumberDescending<T>(
38
53
  items: readonly T[],
@@ -41,12 +56,22 @@ export function sortByNumberDescending<T>(
41
56
  return [...items].sort((a, b) => {
42
57
  const numA = a[numberProperty] as unknown as number;
43
58
  const numB = b[numberProperty] as unknown as number;
59
+
60
+ // Handle NaN and Infinity
61
+ const isAValid = isFinite(numA);
62
+ const isBValid = isFinite(numB);
63
+
64
+ if (!isAValid && !isBValid) return 0;
65
+ if (!isAValid) return 1; // a goes to end
66
+ if (!isBValid) return -1; // b goes to end
67
+
44
68
  return numB - numA;
45
69
  });
46
70
  }
47
71
 
48
72
  /**
49
73
  * Sort array by number property (ascending)
74
+ * NaN and Infinity values are sorted to the end
50
75
  */
51
76
  export function sortByNumberAscending<T>(
52
77
  items: readonly T[],
@@ -55,6 +80,15 @@ export function sortByNumberAscending<T>(
55
80
  return [...items].sort((a, b) => {
56
81
  const numA = a[numberProperty] as unknown as number;
57
82
  const numB = b[numberProperty] as unknown as number;
83
+
84
+ // Handle NaN and Infinity
85
+ const isAValid = isFinite(numA);
86
+ const isBValid = isFinite(numB);
87
+
88
+ if (!isAValid && !isBValid) return 0;
89
+ if (!isAValid) return 1; // a goes to end
90
+ if (!isBValid) return -1; // b goes to end
91
+
58
92
  return numA - numB;
59
93
  });
60
94
  }
@@ -5,4 +5,3 @@
5
5
 
6
6
  export * from './array-filters.util';
7
7
  export * from './array-sorters.util';
8
- export * from './array-reducers.util';
@@ -9,9 +9,11 @@ import type {
9
9
  ModelCostInfo,
10
10
  } from "../../domain/entities/cost-tracking.types";
11
11
  import { findModelById } from "../../domain/constants/default-models.constants";
12
- import { filterByProperty, filterByTimeRange } from "./collection-filters.util";
12
+ import { filterByProperty, filterByTimeRange } from "./collections";
13
13
 
14
- interface CostSummary {
14
+ export type { GenerationCost } from "../../domain/entities/cost-tracking.types";
15
+
16
+ export interface CostSummary {
15
17
  totalEstimatedCost: number;
16
18
  totalActualCost: number;
17
19
  currency: string;
@@ -4,61 +4,27 @@
4
4
  */
5
5
 
6
6
  /**
7
- * Format date to locale string
7
+ * Validate that a date is valid
8
8
  */
9
- export function formatDate(date: Date | string, locale: string = "en-US"): string {
10
- const dateObj = typeof date === "string" ? new Date(date) : date;
11
- return dateObj.toLocaleDateString(locale, {
12
- year: "numeric",
13
- month: "short",
14
- day: "numeric",
15
- });
9
+ function isValidDate(date: Date): boolean {
10
+ return date instanceof Date && !isNaN(date.getTime());
16
11
  }
17
12
 
18
13
  /**
19
- * Format date and time to locale string
14
+ * Format date to locale string
15
+ * @throws {Error} if date is invalid
20
16
  */
21
- export function formatDateTime(date: Date | string, locale: string = "en-US"): string {
17
+ export function formatDate(date: Date | string, locale: string = "en-US"): string {
22
18
  const dateObj = typeof date === "string" ? new Date(date) : date;
23
- return dateObj.toLocaleString(locale, {
19
+
20
+ if (!isValidDate(dateObj)) {
21
+ const dateStr = typeof date === "string" ? date : date.toISOString();
22
+ throw new Error(`Invalid date: ${dateStr}`);
23
+ }
24
+
25
+ return dateObj.toLocaleDateString(locale, {
24
26
  year: "numeric",
25
27
  month: "short",
26
28
  day: "numeric",
27
- hour: "2-digit",
28
- minute: "2-digit",
29
29
  });
30
30
  }
31
-
32
- /**
33
- * Format relative time (e.g., "2 hours ago")
34
- */
35
- export function formatRelativeTime(date: Date | string, locale: string = "en-US"): string {
36
- const dateObj = typeof date === "string" ? new Date(date) : date;
37
- const now = new Date();
38
- const diffMs = now.getTime() - dateObj.getTime();
39
- const diffSec = Math.floor(diffMs / 1000);
40
- const diffMin = Math.floor(diffSec / 60);
41
- const diffHour = Math.floor(diffMin / 60);
42
- const diffDay = Math.floor(diffHour / 24);
43
-
44
- const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
45
-
46
- if (diffSec < 60) {
47
- return rtf.format(-diffSec, "second");
48
- }
49
- if (diffMin < 60) {
50
- return rtf.format(-diffMin, "minute");
51
- }
52
- if (diffHour < 24) {
53
- return rtf.format(-diffHour, "hour");
54
- }
55
- if (diffDay < 30) {
56
- return rtf.format(-diffDay, "day");
57
- }
58
- if (diffDay < 365) {
59
- const months = Math.floor(diffDay / 30);
60
- return rtf.format(-months, "month");
61
- }
62
- const years = Math.floor(diffDay / 365);
63
- return rtf.format(-years, "year");
64
- }
@@ -5,7 +5,7 @@
5
5
 
6
6
  import type { FalErrorInfo, FalErrorCategory, FalErrorType } from "../../domain/entities/error.types";
7
7
  import { FalErrorType as ErrorTypeEnum } from "../../domain/entities/error.types";
8
- import { safeJsonParseOrNull } from "./data-parsers.util";
8
+ import { safeJsonParseOrNull } from "./parsers";
9
9
 
10
10
  const STATUS_CODES = ["400", "401", "402", "403", "404", "422", "429", "500", "502", "503", "504"];
11
11
 
@@ -43,9 +43,48 @@ export async function uploadToFalStorage(base64: string): Promise<string> {
43
43
 
44
44
  /**
45
45
  * Upload multiple images to FAL storage in parallel
46
+ * Uses Promise.allSettled to handle partial failures gracefully
47
+ * @throws {Error} if any upload fails, with details about all failures
48
+ * Note: Successful uploads before the first failure are NOT cleaned up automatically
49
+ * as FAL storage doesn't provide a delete API. Monitor orphaned uploads externally.
46
50
  */
47
51
  export async function uploadMultipleToFalStorage(
48
52
  images: string[],
49
53
  ): Promise<string[]> {
50
- return Promise.all(images.map(uploadToFalStorage));
54
+ const results = await Promise.allSettled(images.map(uploadToFalStorage));
55
+
56
+ const successfulUploads: string[] = [];
57
+ const failures: Array<{ index: number; error: unknown }> = [];
58
+
59
+ results.forEach((result, index) => {
60
+ if (result.status === 'fulfilled') {
61
+ successfulUploads.push(result.value);
62
+ } else {
63
+ failures.push({ index, error: result.reason });
64
+ }
65
+ });
66
+
67
+ // If any upload failed, throw detailed error
68
+ if (failures.length > 0) {
69
+ const errorMessage = failures
70
+ .map(({ index, error }) =>
71
+ `Image ${index}: ${error instanceof Error ? error.message : String(error)}`
72
+ )
73
+ .join('; ');
74
+
75
+ // Log warning about orphaned uploads
76
+ if (successfulUploads.length > 0) {
77
+ console.warn(
78
+ `[fal-storage] ${successfulUploads.length} upload(s) succeeded before failure. ` +
79
+ 'These files remain in FAL storage and may need manual cleanup:',
80
+ successfulUploads
81
+ );
82
+ }
83
+
84
+ throw new Error(
85
+ `Failed to upload ${failures.length} of ${images.length} image(s): ${errorMessage}`
86
+ );
87
+ }
88
+
89
+ return successfulUploads;
51
90
  }
@@ -5,4 +5,3 @@
5
5
 
6
6
  export * from './timing-helpers.util';
7
7
  export * from './object-helpers.util';
8
- export * from './function-helpers.util';
@@ -1,89 +1,11 @@
1
1
  /**
2
2
  * Timing Helper Utilities
3
- * Debounce, throttle, sleep, and retry operations
3
+ * Sleep utility for async delays
4
4
  */
5
5
 
6
- /**
7
- * Create debounced function
8
- */
9
- export function debounce<T extends (...args: never[]) => void>(
10
- func: T,
11
- wait: number
12
- ): (...args: Parameters<T>) => void {
13
- let timeoutId: ReturnType<typeof setTimeout> | null = null;
14
-
15
- return function debounced(...args: Parameters<T>) {
16
- if (timeoutId) {
17
- clearTimeout(timeoutId);
18
- }
19
- timeoutId = setTimeout(() => {
20
- func(...args);
21
- }, wait);
22
- };
23
- }
24
-
25
- /**
26
- * Create throttled function
27
- */
28
- export function throttle<T extends (...args: never[]) => void>(
29
- func: T,
30
- limit: number
31
- ): (...args: Parameters<T>) => void {
32
- let inThrottle = false;
33
-
34
- return function throttled(...args: Parameters<T>) {
35
- if (!inThrottle) {
36
- func(...args);
37
- inThrottle = true;
38
- setTimeout(() => {
39
- inThrottle = false;
40
- }, limit);
41
- }
42
- };
43
- }
44
-
45
6
  /**
46
7
  * Sleep for specified milliseconds
47
8
  */
48
9
  export function sleep(ms: number): Promise<void> {
49
10
  return new Promise((resolve) => setTimeout(resolve, ms));
50
11
  }
51
-
52
- /**
53
- * Retry function with exponential backoff
54
- */
55
- export async function retry<T>(
56
- func: () => Promise<T>,
57
- options: {
58
- maxRetries?: number;
59
- baseDelay?: number;
60
- maxDelay?: number;
61
- shouldRetry?: (error: unknown) => boolean;
62
- } = {}
63
- ): Promise<T> {
64
- const {
65
- maxRetries = 3,
66
- baseDelay = 1000,
67
- maxDelay = 10000,
68
- shouldRetry = () => true,
69
- } = options;
70
-
71
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
72
- try {
73
- return await func();
74
- } catch (error) {
75
- // On last attempt or non-retryable error, throw immediately
76
- if (attempt === maxRetries || !shouldRetry(error)) {
77
- throw error;
78
- }
79
-
80
- // Calculate exponential backoff delay
81
- const delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
82
- await sleep(delay);
83
- }
84
- }
85
-
86
- // This line is unreachable but required by TypeScript
87
- // The loop always returns or throws on the last iteration
88
- throw new Error('Retry loop completed without result (should not happen)');
89
- }
@@ -15,20 +15,24 @@ export function formatImageDataUri(base64: string): string {
15
15
 
16
16
  /**
17
17
  * Extract base64 from data URI
18
+ * Uses indexOf instead of split to handle edge cases where comma might appear in base64
18
19
  */
19
20
  export function extractBase64(dataUri: string): string {
20
21
  if (!dataUri.startsWith("data:")) {
21
22
  return dataUri;
22
23
  }
23
24
 
24
- const parts = dataUri.split(",");
25
- if (parts.length < 2) {
26
- throw new Error(`Invalid data URI format: ${dataUri}`);
25
+ // Find the first comma which separates header from data
26
+ const commaIndex = dataUri.indexOf(",");
27
+ if (commaIndex === -1) {
28
+ throw new Error(`Invalid data URI format (no comma separator): ${dataUri.substring(0, 50)}...`);
27
29
  }
28
30
 
29
- const base64Part = parts[1];
31
+ // Extract everything after the first comma
32
+ const base64Part = dataUri.substring(commaIndex + 1);
33
+
30
34
  if (!base64Part || base64Part.length === 0) {
31
- throw new Error(`Empty base64 data in URI: ${dataUri}`);
35
+ throw new Error(`Empty base64 data in URI: ${dataUri.substring(0, 50)}...`);
32
36
  }
33
37
 
34
38
  return base64Part;
@@ -11,11 +11,7 @@ export {
11
11
  sortByDateAscending,
12
12
  sortByNumberDescending,
13
13
  sortByNumberAscending,
14
- sumByProperty,
15
- groupByProperty,
16
- chunkArray,
17
- distinctByProperty,
18
- } from "./collection-filters.util";
14
+ } from "./collections";
19
15
 
20
16
  export {
21
17
  safeJsonParse,
@@ -23,16 +19,7 @@ export {
23
19
  safeJsonStringify,
24
20
  isValidJson,
25
21
  validateObjectStructure,
26
- validateObjectArray,
27
- parseNumber,
28
- parseBoolean,
29
- clampNumber,
30
- roundToDecimals,
31
- deepClone,
32
- mergeObjects,
33
- pickProperties,
34
- omitProperties,
35
- } from "./data-parsers.util";
22
+ } from "./parsers";
36
23
 
37
24
  export { categorizeFalError } from "./error-categorizer";
38
25
  export {
@@ -40,30 +27,16 @@ export {
40
27
  parseFalError,
41
28
  isFalErrorRetryable,
42
29
  extractStatusCode,
43
- } from "./error-mapper";
30
+ } from "./fal-error-handler.util";
44
31
 
45
- export {
46
- formatNumber,
47
- formatCurrency,
48
- formatBytes,
49
- formatDuration,
50
- formatPercentage,
51
- formatDate,
52
- formatDateTime,
53
- formatRelativeTime,
54
- truncateText,
55
- capitalize,
56
- toTitleCase,
57
- toSlug,
58
- formatList,
59
- pluralize,
60
- formatCount,
61
- } from "./formatting.util";
32
+ export { formatDate } from "./date-format.util";
33
+ export { formatNumber, formatBytes, formatDuration } from "./number-format.util";
34
+ export { truncateText } from "./string-format.util";
62
35
 
63
36
  export {
64
37
  buildSingleImageInput,
65
38
  buildDualImageInput,
66
- } from "./input-builders.util";
39
+ } from "./base-builders.util";
67
40
 
68
41
  export {
69
42
  isFalModelType,
@@ -75,7 +48,7 @@ export {
75
48
  isValidPrompt,
76
49
  isValidTimeout,
77
50
  isValidRetryCount,
78
- } from "./type-guards.util";
51
+ } from "./type-guards";
79
52
 
80
53
  export {
81
54
  formatImageDataUri,
@@ -99,15 +72,13 @@ export {
99
72
  isDefined,
100
73
  removeNullish,
101
74
  generateUniqueId,
102
- debounce,
103
- throttle,
104
75
  sleep,
105
- retry,
106
- noop,
107
- identity,
108
- constant,
109
- } from "./general-helpers.util";
76
+ } from "./helpers";
77
+
78
+ export { preprocessInput } from "./input-preprocessor.util";
79
+ export { validateInput } from "./input-validator.util";
110
80
 
81
+ export type { FalJobMetadata } from "./job-metadata";
111
82
  export {
112
83
  createJobMetadata,
113
84
  updateJobMetadata,
@@ -125,36 +96,22 @@ export {
125
96
  getCompletedJobs,
126
97
  } from "./job-metadata";
127
98
 
128
- export type { FalJobMetadata } from "./job-metadata";
129
-
99
+ export type { IJobStorage } from "./job-storage";
130
100
  export {
101
+ InMemoryJobStorage,
131
102
  saveJobMetadata,
132
103
  loadJobMetadata,
133
104
  deleteJobMetadata,
105
+ updateJobStatus,
134
106
  loadAllJobs,
135
107
  cleanupOldJobs,
136
108
  getJobsByModel,
137
109
  getJobsByStatus,
138
- updateJobStatus,
139
110
  } from "./job-storage";
140
111
 
141
- export type { IJobStorage, InMemoryJobStorage } from "./job-storage";
142
-
143
- export { CostTracker } from "./cost-tracker";
144
-
145
112
  export { executeWithCostTracking } from "./cost-tracking-executor.util";
113
+ export { CostTracker } from "./cost-tracker";
114
+ export type { CostSummary, GenerationCost } from "./cost-tracker";
146
115
 
147
- export { preprocessInput } from "./input-preprocessor.util";
148
-
149
- export {
150
- validateInput,
151
- type InputValidationError,
152
- type ValidationError,
153
- } from "./input-validator.util";
154
-
155
- // FAL generation state manager
156
- export {
157
- FalGenerationStateManager,
158
- type GenerationState,
159
- type GenerationStateOptions,
160
- } from "./fal-generation-state-manager.util";
116
+ export { FalGenerationStateManager } from "./fal-generation-state-manager.util";
117
+ export type { GenerationState } from "./fal-generation-state-manager.util";
@@ -90,18 +90,61 @@ export async function preprocessInput(
90
90
  throw new Error('image_urls array must contain at least one valid image URL');
91
91
  }
92
92
 
93
- // Wait for all uploads and build the final array
94
- // Tasks are already in correct order from the loop, no need to sort
95
- const processedUrls = await Promise.all(
93
+ // Wait for all uploads using Promise.allSettled to handle failures gracefully
94
+ // This ensures all uploads complete before reporting errors
95
+ const uploadResults = await Promise.allSettled(
96
96
  uploadTasks.map((task) => Promise.resolve(task.url))
97
97
  );
98
98
 
99
+ const processedUrls: string[] = [];
100
+ const uploadErrors: string[] = [];
101
+
102
+ uploadResults.forEach((result, index) => {
103
+ if (result.status === 'fulfilled') {
104
+ processedUrls.push(result.value);
105
+ } else {
106
+ uploadErrors.push(
107
+ `Upload ${index} failed: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`
108
+ );
109
+ }
110
+ });
111
+
112
+ // If any uploads failed, throw with details
113
+ if (uploadErrors.length > 0) {
114
+ console.warn(
115
+ `[input-preprocessor] ${processedUrls.length} of ${uploadTasks.length} uploads succeeded. ` +
116
+ 'Successful uploads remain in FAL storage.'
117
+ );
118
+ throw new Error(`Image upload failures:\n${uploadErrors.join('\n')}`);
119
+ }
120
+
99
121
  result.image_urls = processedUrls;
100
122
  }
101
123
 
102
124
  // Wait for ALL uploads to complete (both individual keys and array)
125
+ // Use Promise.allSettled to handle partial failures gracefully
103
126
  if (uploadPromises.length > 0) {
104
- await Promise.all(uploadPromises);
127
+ const individualUploadResults = await Promise.allSettled(uploadPromises);
128
+
129
+ const failedUploads = individualUploadResults.filter(
130
+ (result) => result.status === 'rejected'
131
+ );
132
+
133
+ if (failedUploads.length > 0) {
134
+ const successCount = individualUploadResults.length - failedUploads.length;
135
+ console.warn(
136
+ `[input-preprocessor] ${successCount} of ${individualUploadResults.length} individual field uploads succeeded. ` +
137
+ 'Successful uploads remain in FAL storage.'
138
+ );
139
+
140
+ const errorMessages = failedUploads.map((result) =>
141
+ result.status === 'rejected'
142
+ ? (result.reason instanceof Error ? result.reason.message : String(result.reason))
143
+ : 'Unknown error'
144
+ );
145
+
146
+ throw new Error(`Some image uploads failed:\n${errorMessages.join('\n')}`);
147
+ }
105
148
  }
106
149
 
107
150
  return result;
@@ -3,7 +3,7 @@
3
3
  * Validates input parameters before API calls
4
4
  */
5
5
 
6
- import { isValidModelId, isValidPrompt } from "./type-guards.util";
6
+ import { isValidModelId, isValidPrompt } from "./type-guards";
7
7
 
8
8
  /**
9
9
  * Detect potentially malicious content in strings
@@ -7,12 +7,37 @@ import type { FalJobMetadata } from "./job-metadata.types";
7
7
 
8
8
  /**
9
9
  * Get job duration in milliseconds
10
+ * Returns null if job not completed or if dates are invalid
10
11
  */
11
12
  export function getJobDuration(metadata: FalJobMetadata): number | null {
12
13
  if (!metadata.completedAt) {
13
14
  return null;
14
15
  }
15
- return new Date(metadata.completedAt).getTime() - new Date(metadata.createdAt).getTime();
16
+
17
+ const completedTime = new Date(metadata.completedAt).getTime();
18
+ const createdTime = new Date(metadata.createdAt).getTime();
19
+
20
+ // Validate both dates parsed correctly
21
+ if (isNaN(completedTime) || isNaN(createdTime)) {
22
+ console.warn(
23
+ '[job-metadata] Invalid date(s) in metadata:',
24
+ { completedAt: metadata.completedAt, createdAt: metadata.createdAt }
25
+ );
26
+ return null;
27
+ }
28
+
29
+ const duration = completedTime - createdTime;
30
+
31
+ // Sanity check: duration should be positive
32
+ if (duration < 0) {
33
+ console.warn(
34
+ '[job-metadata] Negative duration detected (completedAt < createdAt):',
35
+ { duration, completedAt: metadata.completedAt, createdAt: metadata.createdAt }
36
+ );
37
+ return null;
38
+ }
39
+
40
+ return duration;
16
41
  }
17
42
 
18
43
  /**
@@ -5,8 +5,8 @@
5
5
 
6
6
  import type { FalJobMetadata } from "./job-metadata.types";
7
7
  import { isJobStale, isJobRunning, isJobCompleted } from "./job-metadata-lifecycle.util";
8
- import { sortByDateDescending, filterByPredicate } from "../collection-filters.util";
9
- import { safeJsonParseOrNull, validateObjectStructure } from "../data-parsers.util";
8
+ import { sortByDateDescending, filterByPredicate } from "../collections";
9
+ import { safeJsonParseOrNull, validateObjectStructure } from "../parsers";
10
10
 
11
11
  /**
12
12
  * Serialize job metadata for storage
@@ -6,7 +6,7 @@
6
6
  import type { FalJobMetadata } from "../job-metadata";
7
7
  import { updateJobMetadata } from "../job-metadata";
8
8
  import type { IJobStorage } from "./job-storage-interface";
9
- import { safeJsonParseOrNull, safeJsonStringify } from "../data-parsers.util";
9
+ import { safeJsonParseOrNull, safeJsonStringify } from "../parsers";
10
10
 
11
11
  /**
12
12
  * Save job metadata to storage
@@ -6,7 +6,7 @@
6
6
  import type { FalJobMetadata } from "../job-metadata";
7
7
  import type { IJobStorage } from "./job-storage-interface";
8
8
  import { deleteJobMetadata } from "./job-storage-crud.util";
9
- import { safeJsonParseOrNull } from "../data-parsers.util";
9
+ import { safeJsonParseOrNull } from "../parsers";
10
10
 
11
11
  /**
12
12
  * Load all jobs from storage
@@ -19,26 +19,42 @@ export function formatNumber(value: number, decimals: number = 2): string {
19
19
  return value.toFixed(decimals);
20
20
  }
21
21
 
22
- /**
23
- * Format currency amount
24
- */
25
- export function formatCurrency(amount: number, currency: string = "USD"): string {
26
- const formatted = formatNumber(amount, 2);
27
- return `${currency} ${formatted}`;
28
- }
29
-
30
22
  /**
31
23
  * Format bytes to human-readable size
24
+ * Handles edge cases: negative bytes, NaN, Infinity, extremely large values
32
25
  */
33
26
  export function formatBytes(bytes: number, decimals: number = 2): string {
34
- if (bytes === 0) return "0 Bytes";
27
+ // Handle invalid inputs
28
+ if (!Number.isFinite(bytes) || Number.isNaN(bytes)) {
29
+ return "0 Bytes";
30
+ }
31
+
32
+ // Handle negative bytes
33
+ if (bytes < 0) {
34
+ return `-${formatBytes(-bytes, decimals)}`;
35
+ }
36
+
37
+ // Handle zero
38
+ if (bytes === 0) {
39
+ return "0 Bytes";
40
+ }
35
41
 
36
42
  const k = 1024;
37
43
  const dm = decimals < 0 ? 0 : decimals;
38
- const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB"];
44
+ const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
39
45
  const i = Math.floor(Math.log(bytes) / Math.log(k));
40
46
 
41
- return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
47
+ // Clamp index to valid range
48
+ const index = Math.min(i, sizes.length - 1);
49
+
50
+ // For extremely large values beyond our size array
51
+ if (index >= sizes.length - 1 && i > sizes.length - 1) {
52
+ const exponent = i - (sizes.length - 1);
53
+ const value = bytes / Math.pow(k, sizes.length - 1);
54
+ return `${parseFloat(value.toFixed(dm))} ${sizes[sizes.length - 1]} × ${k}^${exponent}`;
55
+ }
56
+
57
+ return `${parseFloat((bytes / Math.pow(k, index)).toFixed(dm))} ${sizes[index]}`;
42
58
  }
43
59
 
44
60
  /**
@@ -68,12 +84,3 @@ export function formatDuration(milliseconds: number): string {
68
84
  ? `${hours}h ${remainingMinutes}m`
69
85
  : `${hours}h`;
70
86
  }
71
-
72
- /**
73
- * Format percentage
74
- */
75
- export function formatPercentage(value: number, decimals: number = 1): string {
76
- if (value < 0) return "0%";
77
- if (value > 100) return "100%";
78
- return `${formatNumber(value, decimals)}%`;
79
- }
@@ -6,5 +6,4 @@
6
6
  export * from './json-parsers.util';
7
7
  export * from './object-validators.util';
8
8
  export * from './value-parsers.util';
9
- export * from './number-helpers.util';
10
9
  export * from './object-transformers.util';
@@ -12,62 +12,3 @@ export function truncateText(text: string, maxLength: number): string {
12
12
  }
13
13
  return text.slice(0, maxLength - 3) + "...";
14
14
  }
15
-
16
- /**
17
- * Capitalize first letter of string
18
- */
19
- export function capitalize(text: string): string {
20
- if (!text) return text;
21
- return text.charAt(0).toUpperCase() + text.slice(1);
22
- }
23
-
24
- /**
25
- * Convert string to title case
26
- */
27
- export function toTitleCase(text: string): string {
28
- return text
29
- .toLowerCase()
30
- .split(" ")
31
- .map((word) => capitalize(word))
32
- .join(" ");
33
- }
34
-
35
- /**
36
- * Convert string to slug
37
- */
38
- export function toSlug(text: string): string {
39
- return text
40
- .toLowerCase()
41
- .trim()
42
- .replace(/[^\w\s-]/g, "")
43
- .replace(/[\s_-]+/g, "-")
44
- .replace(/^-+|-+$/g, "");
45
- }
46
-
47
- /**
48
- * Format list of items with conjunction
49
- */
50
- export function formatList(items: readonly string[], conjunction: string = "and"): string {
51
- if (items.length === 0) return "";
52
- if (items.length === 1) return items[0] ?? "";
53
- if (items.length === 2) return items.join(` ${conjunction} `);
54
-
55
- const allButLast = items.slice(0, -1);
56
- const last = items[items.length - 1];
57
- return `${allButLast.join(", ")}, ${conjunction} ${last}`;
58
- }
59
-
60
- /**
61
- * Pluralize word based on count
62
- */
63
- export function pluralize(word: string, count: number): string {
64
- if (count === 1) return word;
65
- return `${word}s`;
66
- }
67
-
68
- /**
69
- * Format count with plural word
70
- */
71
- export function formatCount(word: string, count: number): string {
72
- return `${count} ${pluralize(word, count)}`;
73
- }
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { useState, useCallback, useRef, useEffect } from "react";
7
7
  import { falProvider } from "../../infrastructure/services/fal-provider";
8
- import { mapFalError } from "../../infrastructure/utils/error-mapper";
8
+ import { mapFalError } from "../../infrastructure/utils/fal-error-handler.util";
9
9
  import { FalGenerationStateManager } from "../../infrastructure/utils/fal-generation-state-manager.util";
10
10
  import type { FalJobInput, FalQueueStatus } from "../../domain/entities/fal.types";
11
11
  import type { FalErrorInfo } from "../../domain/entities/error.types";
@@ -1,9 +0,0 @@
1
- /**
2
- * Collection Filter Utilities
3
- * @deprecated This file is now split into smaller modules for better maintainability.
4
- * Import from './collections' submodules instead.
5
- *
6
- * This file re-exports all functions for backward compatibility.
7
- */
8
-
9
- export * from './collections';
@@ -1,67 +0,0 @@
1
- /**
2
- * Array Reducer Utilities
3
- * Reduce, group, chunk, and aggregation operations
4
- */
5
-
6
- /**
7
- * Reduce array to sum of number property
8
- */
9
- export function sumByProperty<T>(
10
- items: readonly T[],
11
- numberProperty: keyof T
12
- ): number {
13
- return items.reduce((sum, item) => {
14
- const value = item[numberProperty] as unknown as number;
15
- return sum + (typeof value === "number" ? value : 0);
16
- }, 0);
17
- }
18
-
19
- /**
20
- * Group array by property value
21
- */
22
- export function groupByProperty<T>(
23
- items: readonly T[],
24
- property: keyof T
25
- ): Map<unknown, T[]> {
26
- const groups = new Map<unknown, T[]>();
27
- for (const item of items) {
28
- const key = item[property];
29
- const existing = groups.get(key);
30
- if (existing) {
31
- existing.push(item);
32
- } else {
33
- groups.set(key, [item]);
34
- }
35
- }
36
- return groups;
37
- }
38
-
39
- /**
40
- * Chunk array into smaller arrays of specified size
41
- */
42
- export function chunkArray<T>(items: readonly T[], chunkSize: number): T[][] {
43
- const result: T[][] = [];
44
- for (let i = 0; i < items.length; i += chunkSize) {
45
- result.push([...items.slice(i, i + chunkSize)]);
46
- }
47
- return result;
48
- }
49
-
50
- /**
51
- * Get distinct values of a property from array
52
- */
53
- export function distinctByProperty<T>(
54
- items: readonly T[],
55
- property: keyof T
56
- ): unknown[] {
57
- const seen = new Set<unknown>();
58
- const result: unknown[] = [];
59
- for (const item of items) {
60
- const value = item[property];
61
- if (!seen.has(value)) {
62
- seen.add(value);
63
- result.push(value);
64
- }
65
- }
66
- return result;
67
- }
@@ -1,9 +0,0 @@
1
- /**
2
- * Data Parser Utilities
3
- * @deprecated This file is now split into smaller modules for better maintainability.
4
- * Import from './parsers' submodules instead.
5
- *
6
- * This file re-exports all functions for backward compatibility.
7
- */
8
-
9
- export * from './parsers';
@@ -1,24 +0,0 @@
1
- /**
2
- * FAL Error Mapper - Maps errors to user-friendly info
3
- *
4
- * @deprecated This module is a re-export for backward compatibility.
5
- * Import directly from './fal-error-handler.util' instead.
6
- *
7
- * This module re-exports error handling functions from the unified
8
- * fal-error-handler.util module for backward compatibility.
9
- *
10
- * @example
11
- * // Instead of:
12
- * import { mapFalError } from './error-mapper';
13
- *
14
- * // Use:
15
- * import { mapFalError } from './fal-error-handler.util';
16
- */
17
-
18
- export {
19
- mapFalError,
20
- parseFalError,
21
- isFalErrorRetryable,
22
- categorizeFalError,
23
- extractStatusCode,
24
- } from "./fal-error-handler.util";
@@ -1,31 +0,0 @@
1
- /**
2
- * Formatting Utilities
3
- * Common formatting functions for display and data presentation
4
- *
5
- * This module re-exports formatting utilities from specialized modules
6
- * for better organization and maintainability.
7
- */
8
-
9
- export {
10
- formatDate,
11
- formatDateTime,
12
- formatRelativeTime,
13
- } from "./date-format.util";
14
-
15
- export {
16
- formatNumber,
17
- formatCurrency,
18
- formatBytes,
19
- formatDuration,
20
- formatPercentage,
21
- } from "./number-format.util";
22
-
23
- export {
24
- truncateText,
25
- capitalize,
26
- toTitleCase,
27
- toSlug,
28
- formatList,
29
- pluralize,
30
- formatCount,
31
- } from "./string-format.util";
@@ -1,9 +0,0 @@
1
- /**
2
- * General Helper Utilities
3
- * @deprecated This file is now split into smaller modules for better maintainability.
4
- * Import from './helpers' submodules instead.
5
- *
6
- * This file re-exports all functions for backward compatibility.
7
- */
8
-
9
- export * from './helpers';
@@ -1,25 +0,0 @@
1
- /**
2
- * Function Helper Utilities
3
- * Common functional programming helpers
4
- */
5
-
6
- /**
7
- * No-op function
8
- */
9
- export function noop(): void {
10
- // Intentionally empty
11
- }
12
-
13
- /**
14
- * Identity function
15
- */
16
- export function identity<T>(value: T): T {
17
- return value;
18
- }
19
-
20
- /**
21
- * Constant function (returns same value regardless of input)
22
- */
23
- export function constant<T>(value: T): () => T {
24
- return () => value;
25
- }
@@ -1,6 +0,0 @@
1
- /**
2
- * FAL Input Builders - Constructs FAL API input from normalized data
3
- * Provider-agnostic: accepts prompt config as parameter, not imported
4
- */
5
-
6
- export * from "./base-builders.util";
@@ -1,19 +0,0 @@
1
- /**
2
- * Number Helper Utilities
3
- * Number manipulation and formatting
4
- */
5
-
6
- /**
7
- * Clamp number between min and max
8
- */
9
- export function clampNumber(value: number, min: number, max: number): number {
10
- return Math.min(Math.max(value, min), max);
11
- }
12
-
13
- /**
14
- * Round to decimal places
15
- */
16
- export function roundToDecimals(value: number, decimals: number): number {
17
- const multiplier = Math.pow(10, decimals);
18
- return Math.round(value * multiplier) / multiplier;
19
- }
@@ -1,9 +0,0 @@
1
- /**
2
- * Type Guards and Validation Utilities
3
- * @deprecated This file is now split into smaller modules for better maintainability.
4
- * Import from './type-guards' submodules instead.
5
- *
6
- * This file re-exports all functions for backward compatibility.
7
- */
8
-
9
- export * from './type-guards';
@@ -1,6 +0,0 @@
1
- /**
2
- * Validators Module
3
- * Exports all validator functions
4
- */
5
-
6
- export { validateNSFWContent } from "./nsfw-validator";
package/src/init/index.ts DELETED
@@ -1,10 +0,0 @@
1
- /**
2
- * AI Provider Init Module
3
- * Provides factory for creating app initialization modules
4
- */
5
-
6
- export {
7
- createAiProviderInitModule,
8
- type AiProviderInitModuleConfig,
9
- type InitModule,
10
- } from './createAiProviderInitModule';