@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.
- package/package.json +1 -1
- package/src/exports/infrastructure.ts +2 -11
- package/src/index.ts +1 -1
- package/src/infrastructure/services/fal-provider-subscription.ts +1 -1
- package/src/infrastructure/services/fal-provider.ts +1 -2
- package/src/infrastructure/services/index.ts +0 -16
- package/src/infrastructure/services/request-store.ts +3 -3
- package/src/infrastructure/utils/collections/array-sorters.util.ts +34 -0
- package/src/infrastructure/utils/collections/index.ts +0 -1
- package/src/infrastructure/utils/cost-tracker.ts +4 -2
- package/src/infrastructure/utils/date-format.util.ts +13 -47
- package/src/infrastructure/utils/fal-error-handler.util.ts +1 -1
- package/src/infrastructure/utils/fal-storage.util.ts +40 -1
- package/src/infrastructure/utils/helpers/index.ts +0 -1
- package/src/infrastructure/utils/helpers/timing-helpers.util.ts +1 -79
- package/src/infrastructure/utils/image-helpers.util.ts +9 -5
- package/src/infrastructure/utils/index.ts +20 -63
- package/src/infrastructure/utils/input-preprocessor.util.ts +47 -4
- package/src/infrastructure/utils/input-validator.util.ts +1 -1
- package/src/infrastructure/utils/job-metadata/job-metadata-format.util.ts +26 -1
- package/src/infrastructure/utils/job-metadata/job-metadata-queries.util.ts +2 -2
- package/src/infrastructure/utils/job-storage/job-storage-crud.util.ts +1 -1
- package/src/infrastructure/utils/job-storage/job-storage-queries.util.ts +1 -1
- package/src/infrastructure/utils/number-format.util.ts +27 -20
- package/src/infrastructure/utils/parsers/index.ts +0 -1
- package/src/infrastructure/utils/string-format.util.ts +0 -59
- package/src/presentation/hooks/use-fal-generation.ts +1 -1
- package/src/infrastructure/utils/collection-filters.util.ts +0 -9
- package/src/infrastructure/utils/collections/array-reducers.util.ts +0 -67
- package/src/infrastructure/utils/data-parsers.util.ts +0 -9
- package/src/infrastructure/utils/error-mapper.ts +0 -24
- package/src/infrastructure/utils/formatting.util.ts +0 -31
- package/src/infrastructure/utils/general-helpers.util.ts +0 -9
- package/src/infrastructure/utils/helpers/function-helpers.util.ts +0 -25
- package/src/infrastructure/utils/input-builders.util.ts +0 -6
- package/src/infrastructure/utils/parsers/number-helpers.util.ts +0 -19
- package/src/infrastructure/utils/type-guards.util.ts +0 -9
- package/src/infrastructure/validators/index.ts +0 -6
- package/src/init/index.ts +0 -10
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
@@ -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-
|
|
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
|
|
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
|
}
|
|
@@ -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 "./
|
|
12
|
+
import { filterByProperty, filterByTimeRange } from "./collections";
|
|
13
13
|
|
|
14
|
-
|
|
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
|
-
*
|
|
7
|
+
* Validate that a date is valid
|
|
8
8
|
*/
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
14
|
+
* Format date to locale string
|
|
15
|
+
* @throws {Error} if date is invalid
|
|
20
16
|
*/
|
|
21
|
-
export function
|
|
17
|
+
export function formatDate(date: Date | string, locale: string = "en-US"): string {
|
|
22
18
|
const dateObj = typeof date === "string" ? new Date(date) : date;
|
|
23
|
-
|
|
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 "./
|
|
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
|
-
|
|
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
|
}
|
|
@@ -1,89 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Timing Helper Utilities
|
|
3
|
-
*
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
30
|
+
} from "./fal-error-handler.util";
|
|
44
31
|
|
|
45
|
-
export {
|
|
46
|
-
|
|
47
|
-
|
|
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 "./
|
|
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
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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 {
|
|
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 {
|
|
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
|
|
94
|
-
//
|
|
95
|
-
const
|
|
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.
|
|
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;
|
|
@@ -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
|
-
|
|
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 "../
|
|
9
|
-
import { safeJsonParseOrNull, validateObjectStructure } from "../
|
|
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 "../
|
|
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 "../
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
@@ -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-
|
|
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,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';
|
package/src/init/index.ts
DELETED