@umituz/react-native-ai-fal-provider 2.0.29 → 2.0.30
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/domain/constants/default-models.constants.ts +1 -6
- package/src/exports/infrastructure.ts +3 -1
- package/src/exports/presentation.ts +9 -0
- package/src/infrastructure/services/fal-models.service.ts +88 -1
- package/src/infrastructure/services/fal-provider.ts +24 -4
- package/src/infrastructure/services/index.ts +14 -1
- package/src/infrastructure/services/request-store.ts +53 -6
- package/src/infrastructure/utils/cost-tracker.ts +12 -4
- package/src/infrastructure/utils/date-format.util.ts +64 -0
- package/src/infrastructure/utils/error-categorizer.ts +5 -43
- package/src/infrastructure/utils/error-mapper.ts +10 -69
- package/src/infrastructure/utils/fal-error-handler.util.ts +153 -0
- package/src/infrastructure/utils/fal-generation-state-manager.util.ts +93 -0
- package/src/infrastructure/utils/fal-storage.util.ts +9 -9
- package/src/infrastructure/utils/formatting.util.ts +28 -205
- package/src/infrastructure/utils/index.ts +7 -0
- package/src/infrastructure/utils/input-preprocessor.util.ts +16 -14
- package/src/infrastructure/utils/number-format.util.ts +79 -0
- package/src/infrastructure/utils/string-format.util.ts +73 -0
- package/src/presentation/hooks/use-fal-generation.ts +60 -38
- package/src/presentation/hooks/use-models.ts +11 -27
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-fal-provider",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.30",
|
|
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",
|
|
@@ -80,14 +80,9 @@ export function getDefaultModelsByType(type: FalModelType): FalModelConfig[] {
|
|
|
80
80
|
return DEFAULT_IMAGE_TO_VIDEO_MODELS;
|
|
81
81
|
case "text-to-text":
|
|
82
82
|
return DEFAULT_TEXT_TO_TEXT_MODELS;
|
|
83
|
-
case "image-to-image":
|
|
84
|
-
// eslint-disable-next-line no-console
|
|
85
|
-
console.warn('Model type "image-to-image" not supported yet');
|
|
83
|
+
case "image-to-image":
|
|
86
84
|
return [];
|
|
87
|
-
}
|
|
88
85
|
default: {
|
|
89
|
-
// eslint-disable-next-line no-console
|
|
90
|
-
console.warn('Unknown model type:', type);
|
|
91
86
|
return [];
|
|
92
87
|
}
|
|
93
88
|
}
|
|
@@ -9,8 +9,10 @@ export {
|
|
|
9
9
|
NSFWContentError,
|
|
10
10
|
cancelCurrentFalRequest,
|
|
11
11
|
hasRunningFalRequest,
|
|
12
|
+
cleanupRequestStore,
|
|
13
|
+
stopAutomaticCleanup,
|
|
12
14
|
} from "../infrastructure/services";
|
|
13
|
-
export type { FalProviderType } from "../infrastructure/services";
|
|
15
|
+
export type { FalProviderType, ActiveRequest } from "../infrastructure/services";
|
|
14
16
|
|
|
15
17
|
export {
|
|
16
18
|
categorizeFalError,
|
|
@@ -12,3 +12,12 @@ export type {
|
|
|
12
12
|
UseFalGenerationResult,
|
|
13
13
|
UseModelsProps,
|
|
14
14
|
} from "../presentation/hooks";
|
|
15
|
+
|
|
16
|
+
// Export state manager for advanced use cases
|
|
17
|
+
export {
|
|
18
|
+
FalGenerationStateManager,
|
|
19
|
+
} from "../infrastructure/utils/fal-generation-state-manager.util";
|
|
20
|
+
export type {
|
|
21
|
+
GenerationState,
|
|
22
|
+
GenerationStateOptions,
|
|
23
|
+
} from "../infrastructure/utils/fal-generation-state-manager.util";
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* FAL Models Service -
|
|
2
|
+
* FAL Models Service - Model retrieval and selection logic
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { FalModelType } from "../../domain/entities/fal.types";
|
|
6
|
+
import type { ModelType, ModelSelectionConfig } from "../../domain/types/model-selection.types";
|
|
7
|
+
import { DEFAULT_CREDIT_COSTS, DEFAULT_MODEL_IDS } from "../../domain/constants/default-models.constants";
|
|
6
8
|
import {
|
|
7
9
|
type FalModelConfig,
|
|
8
10
|
getDefaultModelsByType,
|
|
@@ -12,6 +14,16 @@ import {
|
|
|
12
14
|
|
|
13
15
|
export type { FalModelConfig };
|
|
14
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Model selection result
|
|
19
|
+
*/
|
|
20
|
+
export interface ModelSelectionResult {
|
|
21
|
+
models: FalModelConfig[];
|
|
22
|
+
selectedModel: FalModelConfig | null;
|
|
23
|
+
defaultCreditCost: number;
|
|
24
|
+
defaultModelId: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
15
27
|
function sortModels(models: FalModelConfig[]): FalModelConfig[] {
|
|
16
28
|
return [...models].sort((a, b) => {
|
|
17
29
|
if (a.order !== b.order) {
|
|
@@ -38,12 +50,87 @@ export function getModelPricing(modelId: string): { freeUserCost: number; premiu
|
|
|
38
50
|
return model?.pricing ?? null;
|
|
39
51
|
}
|
|
40
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Get credit cost for a model
|
|
55
|
+
* Returns the model's free user cost if available, otherwise returns the default cost for the type
|
|
56
|
+
*/
|
|
57
|
+
export function getModelCreditCost(modelId: string, modelType: ModelType): number {
|
|
58
|
+
const pricing = getModelPricing(modelId);
|
|
59
|
+
if (pricing?.freeUserCost) {
|
|
60
|
+
return pricing.freeUserCost;
|
|
61
|
+
}
|
|
62
|
+
return DEFAULT_CREDIT_COSTS[modelType];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get default credit cost for a model type
|
|
67
|
+
*/
|
|
68
|
+
export function getDefaultCreditCost(modelType: ModelType): number {
|
|
69
|
+
return DEFAULT_CREDIT_COSTS[modelType];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get default model ID for a model type
|
|
74
|
+
*/
|
|
75
|
+
export function getDefaultModelId(modelType: ModelType): string {
|
|
76
|
+
return DEFAULT_MODEL_IDS[modelType];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Select initial model based on configuration
|
|
81
|
+
* Returns the model matching initialModelId, or the default model, or the first model
|
|
82
|
+
*/
|
|
83
|
+
export function selectInitialModel(
|
|
84
|
+
models: FalModelConfig[],
|
|
85
|
+
config: ModelSelectionConfig | undefined,
|
|
86
|
+
modelType: ModelType
|
|
87
|
+
): FalModelConfig | null {
|
|
88
|
+
if (models.length === 0) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const defaultModelId = getDefaultModelId(modelType);
|
|
93
|
+
const targetId = config?.initialModelId ?? defaultModelId;
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
models.find((m) => m.id === targetId) ??
|
|
97
|
+
models.find((m) => m.isDefault) ??
|
|
98
|
+
models[0]
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get model selection data for a model type
|
|
104
|
+
* Returns models, selected model, and default configuration
|
|
105
|
+
*/
|
|
106
|
+
export function getModelSelectionData(
|
|
107
|
+
modelType: ModelType,
|
|
108
|
+
config?: ModelSelectionConfig
|
|
109
|
+
): ModelSelectionResult {
|
|
110
|
+
const models = getModels(modelType as FalModelType);
|
|
111
|
+
const defaultCreditCost = config?.defaultCreditCost ?? getDefaultCreditCost(modelType);
|
|
112
|
+
const defaultModelId = config?.defaultModelId ?? getDefaultModelId(modelType);
|
|
113
|
+
const selectedModel = selectInitialModel(models, config, modelType);
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
models,
|
|
117
|
+
selectedModel,
|
|
118
|
+
defaultCreditCost,
|
|
119
|
+
defaultModelId,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
41
123
|
// Singleton service export
|
|
42
124
|
export const falModelsService = {
|
|
43
125
|
getModels,
|
|
44
126
|
getDefaultModel,
|
|
45
127
|
findById: findModelById,
|
|
46
128
|
getModelPricing,
|
|
129
|
+
getModelCreditCost,
|
|
130
|
+
getDefaultCreditCost,
|
|
131
|
+
getDefaultModelId,
|
|
132
|
+
selectInitialModel,
|
|
133
|
+
getModelSelectionData,
|
|
47
134
|
getTextToImageModels: () => getModels("text-to-image"),
|
|
48
135
|
getTextToVoiceModels: () => getModels("text-to-voice"),
|
|
49
136
|
getTextToVideoModels: () => getModels("text-to-video"),
|
|
@@ -126,16 +126,35 @@ export class FalProvider implements IAIProvider {
|
|
|
126
126
|
const abortController = new AbortController();
|
|
127
127
|
const tracker = this.costTracker;
|
|
128
128
|
|
|
129
|
-
|
|
129
|
+
// Store promise immediately BEFORE creating it to prevent race condition
|
|
130
|
+
// Multiple simultaneous calls with same params will get the same promise
|
|
131
|
+
let resolvePromise: (value: T) => void;
|
|
132
|
+
let rejectPromise: (error: unknown) => void;
|
|
133
|
+
const promise = new Promise<T>((resolve, reject) => {
|
|
134
|
+
resolvePromise = resolve;
|
|
135
|
+
rejectPromise = reject;
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
storeRequest(key, { promise, abortController, createdAt: Date.now() });
|
|
139
|
+
|
|
140
|
+
// Execute the actual operation and resolve/reject the stored promise
|
|
141
|
+
executeWithCostTracking({
|
|
130
142
|
tracker,
|
|
131
143
|
model,
|
|
132
144
|
operation: "subscribe",
|
|
133
145
|
execute: () => handleFalSubscription<T>(model, processedInput, options, abortController.signal),
|
|
134
146
|
getRequestId: (res) => res.requestId ?? undefined,
|
|
135
|
-
})
|
|
147
|
+
})
|
|
148
|
+
.then((res) => {
|
|
149
|
+
resolvePromise!(res.result);
|
|
150
|
+
return res.result;
|
|
151
|
+
})
|
|
152
|
+
.catch((error) => {
|
|
153
|
+
rejectPromise!(error);
|
|
154
|
+
throw error;
|
|
155
|
+
})
|
|
156
|
+
.finally(() => removeRequest(key));
|
|
136
157
|
|
|
137
|
-
// Store promise immediately to prevent race condition
|
|
138
|
-
storeRequest(key, { promise, abortController, createdAt: Date.now() });
|
|
139
158
|
return promise;
|
|
140
159
|
}
|
|
141
160
|
|
|
@@ -161,6 +180,7 @@ export class FalProvider implements IAIProvider {
|
|
|
161
180
|
this.cancelCurrentRequest();
|
|
162
181
|
this.apiKey = null;
|
|
163
182
|
this.initialized = false;
|
|
183
|
+
this.costTracker = null;
|
|
164
184
|
}
|
|
165
185
|
|
|
166
186
|
cancelCurrentRequest(): void {
|
|
@@ -6,9 +6,22 @@ import { falProvider } from "./fal-provider";
|
|
|
6
6
|
|
|
7
7
|
export { FalProvider, falProvider } from "./fal-provider";
|
|
8
8
|
export type { FalProvider as FalProviderType } from "./fal-provider";
|
|
9
|
-
export { falModelsService, type FalModelConfig } from "./fal-models.service";
|
|
9
|
+
export { falModelsService, type FalModelConfig, type ModelSelectionResult } from "./fal-models.service";
|
|
10
10
|
export { NSFWContentError } from "./nsfw-content-error";
|
|
11
11
|
|
|
12
|
+
// Request store exports for advanced use cases
|
|
13
|
+
export {
|
|
14
|
+
createRequestKey,
|
|
15
|
+
getExistingRequest,
|
|
16
|
+
storeRequest,
|
|
17
|
+
removeRequest,
|
|
18
|
+
cancelAllRequests,
|
|
19
|
+
hasActiveRequests,
|
|
20
|
+
cleanupRequestStore,
|
|
21
|
+
stopAutomaticCleanup,
|
|
22
|
+
} from "./request-store";
|
|
23
|
+
export type { ActiveRequest } from "./request-store";
|
|
24
|
+
|
|
12
25
|
/**
|
|
13
26
|
* Cancel the current running FAL request
|
|
14
27
|
*/
|
|
@@ -12,6 +12,10 @@ export interface ActiveRequest<T = unknown> {
|
|
|
12
12
|
const STORE_KEY = "__FAL_PROVIDER_REQUESTS__";
|
|
13
13
|
type RequestStore = Map<string, ActiveRequest>;
|
|
14
14
|
|
|
15
|
+
let cleanupTimer: ReturnType<typeof setInterval> | null = null;
|
|
16
|
+
const CLEANUP_INTERVAL = 60000; // 1 minute
|
|
17
|
+
const MAX_REQUEST_AGE = 300000; // 5 minutes
|
|
18
|
+
|
|
15
19
|
export function getRequestStore(): RequestStore {
|
|
16
20
|
if (!(globalThis as Record<string, unknown>)[STORE_KEY]) {
|
|
17
21
|
(globalThis as Record<string, unknown>)[STORE_KEY] = new Map();
|
|
@@ -46,10 +50,20 @@ export function storeRequest<T>(key: string, request: ActiveRequest<T>): void {
|
|
|
46
50
|
createdAt: request.createdAt ?? Date.now(),
|
|
47
51
|
};
|
|
48
52
|
getRequestStore().set(key, requestWithTimestamp);
|
|
53
|
+
|
|
54
|
+
// Start automatic cleanup if not already running
|
|
55
|
+
startAutomaticCleanup();
|
|
49
56
|
}
|
|
50
57
|
|
|
51
58
|
export function removeRequest(key: string): void {
|
|
52
|
-
getRequestStore()
|
|
59
|
+
const store = getRequestStore();
|
|
60
|
+
store.delete(key);
|
|
61
|
+
|
|
62
|
+
// Stop cleanup timer if store is empty
|
|
63
|
+
if (store.size === 0 && cleanupTimer) {
|
|
64
|
+
clearInterval(cleanupTimer);
|
|
65
|
+
cleanupTimer = null;
|
|
66
|
+
}
|
|
53
67
|
}
|
|
54
68
|
|
|
55
69
|
export function cancelAllRequests(): void {
|
|
@@ -58,6 +72,12 @@ export function cancelAllRequests(): void {
|
|
|
58
72
|
req.abortController.abort();
|
|
59
73
|
});
|
|
60
74
|
store.clear();
|
|
75
|
+
|
|
76
|
+
// Stop cleanup timer
|
|
77
|
+
if (cleanupTimer) {
|
|
78
|
+
clearInterval(cleanupTimer);
|
|
79
|
+
cleanupTimer = null;
|
|
80
|
+
}
|
|
61
81
|
}
|
|
62
82
|
|
|
63
83
|
export function hasActiveRequests(): boolean {
|
|
@@ -71,7 +91,7 @@ export function hasActiveRequests(): boolean {
|
|
|
71
91
|
* @param maxAge - Maximum age in milliseconds (default: 5 minutes)
|
|
72
92
|
* @returns Number of requests cleaned up
|
|
73
93
|
*/
|
|
74
|
-
export function cleanupRequestStore(maxAge: number =
|
|
94
|
+
export function cleanupRequestStore(maxAge: number = MAX_REQUEST_AGE): number {
|
|
75
95
|
const store = getRequestStore();
|
|
76
96
|
const now = Date.now();
|
|
77
97
|
let cleanedCount = 0;
|
|
@@ -98,11 +118,38 @@ export function cleanupRequestStore(maxAge: number = 300000): number {
|
|
|
98
118
|
}
|
|
99
119
|
}
|
|
100
120
|
|
|
101
|
-
//
|
|
102
|
-
if (store.size
|
|
103
|
-
|
|
104
|
-
|
|
121
|
+
// Stop cleanup timer if store is empty
|
|
122
|
+
if (store.size === 0 && cleanupTimer) {
|
|
123
|
+
clearInterval(cleanupTimer);
|
|
124
|
+
cleanupTimer = null;
|
|
105
125
|
}
|
|
106
126
|
|
|
107
127
|
return cleanedCount;
|
|
108
128
|
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Start automatic cleanup of stale requests
|
|
132
|
+
* Runs periodically to prevent memory leaks
|
|
133
|
+
*/
|
|
134
|
+
function startAutomaticCleanup(): void {
|
|
135
|
+
if (cleanupTimer) {
|
|
136
|
+
return; // Already running
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
cleanupTimer = setInterval(() => {
|
|
140
|
+
const cleanedCount = cleanupRequestStore(MAX_REQUEST_AGE);
|
|
141
|
+
if (cleanedCount > 0) {
|
|
142
|
+
// Cleanup was performed
|
|
143
|
+
}
|
|
144
|
+
}, CLEANUP_INTERVAL);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Stop automatic cleanup (typically on app shutdown)
|
|
149
|
+
*/
|
|
150
|
+
export function stopAutomaticCleanup(): void {
|
|
151
|
+
if (cleanupTimer) {
|
|
152
|
+
clearInterval(cleanupTimer);
|
|
153
|
+
cleanupTimer = null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -72,10 +72,18 @@ export class CostTracker {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
startOperation(modelId: string, operation: string): string {
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
75
|
+
// Generate unique operation ID
|
|
76
|
+
let uniqueId: string;
|
|
77
|
+
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
78
|
+
uniqueId = crypto.randomUUID();
|
|
79
|
+
} else {
|
|
80
|
+
// Fallback: Use timestamp with random component and counter
|
|
81
|
+
// Format: timestamp-randomCounter-operationHash
|
|
82
|
+
const timestamp = Date.now().toString(36);
|
|
83
|
+
const random = Math.random().toString(36).substring(2, 11);
|
|
84
|
+
const operationHash = operation.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0).toString(36);
|
|
85
|
+
uniqueId = `${timestamp}-${random}-${operationHash}`;
|
|
86
|
+
}
|
|
79
87
|
|
|
80
88
|
const estimatedCost = this.calculateEstimatedCost(modelId);
|
|
81
89
|
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Date Formatting Utilities
|
|
3
|
+
* Functions for formatting dates and times
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Format date to locale string
|
|
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
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Format date and time to locale string
|
|
20
|
+
*/
|
|
21
|
+
export function formatDateTime(date: Date | string, locale: string = "en-US"): string {
|
|
22
|
+
const dateObj = typeof date === "string" ? new Date(date) : date;
|
|
23
|
+
return dateObj.toLocaleString(locale, {
|
|
24
|
+
year: "numeric",
|
|
25
|
+
month: "short",
|
|
26
|
+
day: "numeric",
|
|
27
|
+
hour: "2-digit",
|
|
28
|
+
minute: "2-digit",
|
|
29
|
+
});
|
|
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
|
+
}
|
|
@@ -1,47 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* FAL Error Categorizer - Classifies FAL AI errors
|
|
3
|
+
*
|
|
4
|
+
* This module re-exports error categorization functions from the unified
|
|
5
|
+
* fal-error-handler.util module for backward compatibility.
|
|
3
6
|
*/
|
|
4
7
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const PATTERNS: Record<FalErrorType, string[]> = {
|
|
8
|
-
[FalErrorType.NETWORK]: ["network", "fetch", "connection", "econnrefused", "enotfound", "etimedout"],
|
|
9
|
-
[FalErrorType.TIMEOUT]: ["timeout", "timed out"],
|
|
10
|
-
[FalErrorType.IMAGE_TOO_SMALL]: ["image_too_small", "image dimensions are too small", "minimum dimensions"],
|
|
11
|
-
[FalErrorType.VALIDATION]: ["validation", "invalid", "unprocessable", "422", "bad request", "400"],
|
|
12
|
-
[FalErrorType.CONTENT_POLICY]: ["content_policy", "content policy", "policy violation", "nsfw", "inappropriate"],
|
|
13
|
-
[FalErrorType.RATE_LIMIT]: ["rate limit", "too many requests", "429"],
|
|
14
|
-
[FalErrorType.AUTHENTICATION]: ["unauthorized", "401", "forbidden", "403", "api key", "authentication"],
|
|
15
|
-
[FalErrorType.QUOTA_EXCEEDED]: ["quota exceeded", "insufficient credits", "billing", "payment required", "402"],
|
|
16
|
-
[FalErrorType.MODEL_NOT_FOUND]: ["model not found", "endpoint not found", "404", "not found"],
|
|
17
|
-
[FalErrorType.API_ERROR]: ["api error", "502", "503", "504", "500", "internal server error"],
|
|
18
|
-
[FalErrorType.UNKNOWN]: [],
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
const RETRYABLE_TYPES = new Set([
|
|
22
|
-
FalErrorType.NETWORK,
|
|
23
|
-
FalErrorType.TIMEOUT,
|
|
24
|
-
FalErrorType.RATE_LIMIT,
|
|
25
|
-
]);
|
|
26
|
-
|
|
27
|
-
function matchesPatterns(errorString: string, patterns: string[]): boolean {
|
|
28
|
-
return patterns.some((pattern) => errorString.includes(pattern));
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function categorizeFalError(error: unknown): FalErrorCategory {
|
|
32
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
33
|
-
const errorString = message.toLowerCase();
|
|
34
|
-
|
|
35
|
-
for (const [type, patterns] of Object.entries(PATTERNS)) {
|
|
36
|
-
if (patterns.length > 0 && matchesPatterns(errorString, patterns)) {
|
|
37
|
-
const errorType = type as FalErrorType;
|
|
38
|
-
return {
|
|
39
|
-
type: errorType,
|
|
40
|
-
messageKey: errorType,
|
|
41
|
-
retryable: RETRYABLE_TYPES.has(errorType),
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return { type: FalErrorType.UNKNOWN, messageKey: "unknown", retryable: false };
|
|
47
|
-
}
|
|
8
|
+
export { categorizeFalError } from "./fal-error-handler.util";
|
|
9
|
+
export type { FalErrorCategory } from "../../domain/entities/error.types";
|
|
@@ -1,73 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* FAL Error Mapper - Maps errors to user-friendly info
|
|
3
|
+
*
|
|
4
|
+
* This module re-exports error handling functions from the unified
|
|
5
|
+
* fal-error-handler.util module for backward compatibility.
|
|
3
6
|
*/
|
|
4
7
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
msg?: string;
|
|
13
|
-
type?: string;
|
|
14
|
-
loc?: string[];
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
interface FalApiError {
|
|
18
|
-
body?: { detail?: FalApiErrorDetail[] } | string;
|
|
19
|
-
message?: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function extractStatusCode(errorString: string): number | undefined {
|
|
23
|
-
const code = STATUS_CODES.find((c) => errorString.includes(c));
|
|
24
|
-
return code ? parseInt(code, 10) : undefined;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Parse FAL API error and extract user-friendly message
|
|
29
|
-
*/
|
|
30
|
-
export function parseFalError(error: unknown): string {
|
|
31
|
-
const fallback = error instanceof Error ? error.message : String(error);
|
|
32
|
-
|
|
33
|
-
const falError = error as FalApiError;
|
|
34
|
-
if (!falError?.body) return fallback;
|
|
35
|
-
|
|
36
|
-
const body = typeof falError.body === "string"
|
|
37
|
-
? safeJsonParseOrNull<{ detail?: FalApiErrorDetail[] }>(falError.body)
|
|
38
|
-
: falError.body;
|
|
39
|
-
|
|
40
|
-
const detail = body?.detail?.[0];
|
|
41
|
-
return detail?.msg ?? falError.message ?? fallback;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function mapFalError(error: unknown): FalErrorInfo {
|
|
45
|
-
const category = categorizeFalError(error);
|
|
46
|
-
|
|
47
|
-
// Preserve full error information including stack trace
|
|
48
|
-
if (error instanceof Error) {
|
|
49
|
-
return {
|
|
50
|
-
type: category.type,
|
|
51
|
-
messageKey: `fal.errors.${category.messageKey}`,
|
|
52
|
-
retryable: category.retryable,
|
|
53
|
-
originalError: error.message,
|
|
54
|
-
originalErrorName: error.name,
|
|
55
|
-
stack: error.stack,
|
|
56
|
-
statusCode: extractStatusCode(error.message),
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const errorString = String(error);
|
|
61
|
-
|
|
62
|
-
return {
|
|
63
|
-
type: category.type,
|
|
64
|
-
messageKey: `fal.errors.${category.messageKey}`,
|
|
65
|
-
retryable: category.retryable,
|
|
66
|
-
originalError: errorString,
|
|
67
|
-
statusCode: extractStatusCode(errorString),
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function isFalErrorRetryable(error: unknown): boolean {
|
|
72
|
-
return categorizeFalError(error).retryable;
|
|
73
|
-
}
|
|
8
|
+
export {
|
|
9
|
+
mapFalError,
|
|
10
|
+
parseFalError,
|
|
11
|
+
isFalErrorRetryable,
|
|
12
|
+
categorizeFalError,
|
|
13
|
+
extractStatusCode,
|
|
14
|
+
} from "./fal-error-handler.util";
|