@umituz/react-native-ai-fal-provider 1.0.0
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/LICENSE +21 -0
- package/README.md +106 -0
- package/package.json +53 -0
- package/src/domain/entities/error.types.ts +44 -0
- package/src/domain/entities/fal.types.ts +64 -0
- package/src/index.ts +67 -0
- package/src/infrastructure/services/fal-client.service.ts +146 -0
- package/src/infrastructure/services/index.ts +6 -0
- package/src/infrastructure/utils/error-categorizer.ts +131 -0
- package/src/infrastructure/utils/error-mapper.ts +68 -0
- package/src/infrastructure/utils/index.ts +7 -0
- package/src/presentation/hooks/index.ts +10 -0
- package/src/presentation/hooks/use-fal-generation.ts +86 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Umit UZ
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# @umituz/react-native-ai-fal-provider
|
|
2
|
+
|
|
3
|
+
FAL AI provider service for React Native applications. Provides client wrapper, error handling, and React hooks.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @umituz/react-native-ai-fal-provider @fal-ai/client
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Initialize the Client
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { falClientService } from "@umituz/react-native-ai-fal-provider";
|
|
17
|
+
|
|
18
|
+
falClientService.initialize({
|
|
19
|
+
apiKey: "YOUR_FAL_API_KEY",
|
|
20
|
+
maxRetries: 3,
|
|
21
|
+
defaultTimeoutMs: 300000,
|
|
22
|
+
});
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Using the Hook
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { useFalGeneration } from "@umituz/react-native-ai-fal-provider";
|
|
29
|
+
|
|
30
|
+
function MyComponent() {
|
|
31
|
+
const { data, error, isLoading, generate, retry } = useFalGeneration({
|
|
32
|
+
timeoutMs: 120000,
|
|
33
|
+
onProgress: (status) => console.log("Progress:", status),
|
|
34
|
+
onError: (error) => console.log("Error:", error),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const handleGenerate = async () => {
|
|
38
|
+
await generate("fal-ai/flux/dev", {
|
|
39
|
+
prompt: "A beautiful sunset",
|
|
40
|
+
image_size: "landscape_16_9",
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
// Your UI
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Direct Service Usage
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
import { falClientService } from "@umituz/react-native-ai-fal-provider";
|
|
54
|
+
|
|
55
|
+
const result = await falClientService.subscribe("fal-ai/flux/dev", {
|
|
56
|
+
prompt: "A beautiful sunset",
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Error Handling
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
import { mapFalError, isFalErrorRetryable } from "@umituz/react-native-ai-fal-provider";
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
await falClientService.run("fal-ai/flux/dev", { prompt: "test" });
|
|
67
|
+
} catch (error) {
|
|
68
|
+
const errorInfo = mapFalError(error);
|
|
69
|
+
console.log("Error type:", errorInfo.type);
|
|
70
|
+
console.log("Message key:", errorInfo.messageKey);
|
|
71
|
+
console.log("Retryable:", errorInfo.retryable);
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## API
|
|
76
|
+
|
|
77
|
+
### falClientService
|
|
78
|
+
|
|
79
|
+
- `initialize(config)` - Initialize the client with API key
|
|
80
|
+
- `subscribe(endpoint, input, options)` - Subscribe to generation job
|
|
81
|
+
- `run(endpoint, input)` - Run a generation job
|
|
82
|
+
- `submitJob(endpoint, input)` - Submit a job to queue
|
|
83
|
+
- `getJobStatus(endpoint, requestId)` - Get job status
|
|
84
|
+
- `getJobResult(endpoint, requestId)` - Get job result
|
|
85
|
+
- `isInitialized()` - Check if client is initialized
|
|
86
|
+
- `reset()` - Reset the client
|
|
87
|
+
|
|
88
|
+
### useFalGeneration Hook
|
|
89
|
+
|
|
90
|
+
- `data` - Generation result
|
|
91
|
+
- `error` - Error info if failed
|
|
92
|
+
- `isLoading` - Loading state
|
|
93
|
+
- `isRetryable` - Whether error is retryable
|
|
94
|
+
- `generate(endpoint, input)` - Start generation
|
|
95
|
+
- `retry()` - Retry last generation
|
|
96
|
+
- `reset()` - Reset state
|
|
97
|
+
|
|
98
|
+
### Error Utilities
|
|
99
|
+
|
|
100
|
+
- `mapFalError(error)` - Map error to FalErrorInfo
|
|
101
|
+
- `isFalErrorRetryable(error)` - Check if error is retryable
|
|
102
|
+
- `categorizeFalError(error)` - Get error category
|
|
103
|
+
|
|
104
|
+
## License
|
|
105
|
+
|
|
106
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@umituz/react-native-ai-fal-provider",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "FAL AI provider service for React Native applications - client wrapper, error handling, and utilities",
|
|
5
|
+
"main": "./src/index.ts",
|
|
6
|
+
"types": "./src/index.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"typecheck": "tsc --noEmit",
|
|
9
|
+
"lint": "eslint src --ext .ts,.tsx --max-warnings 0",
|
|
10
|
+
"version:minor": "npm version minor -m 'chore: release v%s'",
|
|
11
|
+
"version:major": "npm version major -m 'chore: release v%s'"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"react-native",
|
|
15
|
+
"fal-ai",
|
|
16
|
+
"ai",
|
|
17
|
+
"text-to-image",
|
|
18
|
+
"text-to-video",
|
|
19
|
+
"image-to-video",
|
|
20
|
+
"ai-generation"
|
|
21
|
+
],
|
|
22
|
+
"author": "Umit UZ <umit@umituz.com>",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/umituz/react-native-ai-fal-provider"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"@fal-ai/client": ">=1.0.0",
|
|
30
|
+
"react": ">=18.2.0",
|
|
31
|
+
"react-native": ">=0.76.0"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@fal-ai/client": "^1.2.1"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/react": "^18.3.0",
|
|
38
|
+
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
39
|
+
"@typescript-eslint/parser": "^8.0.0",
|
|
40
|
+
"eslint": "^8.57.0",
|
|
41
|
+
"react": "^18.3.0",
|
|
42
|
+
"react-native": "^0.76.0",
|
|
43
|
+
"typescript": "^5.6.0"
|
|
44
|
+
},
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public"
|
|
47
|
+
},
|
|
48
|
+
"files": [
|
|
49
|
+
"src",
|
|
50
|
+
"README.md",
|
|
51
|
+
"LICENSE"
|
|
52
|
+
]
|
|
53
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FAL Error Types
|
|
3
|
+
* Error handling type definitions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export enum FalErrorType {
|
|
7
|
+
NETWORK = "network",
|
|
8
|
+
TIMEOUT = "timeout",
|
|
9
|
+
API_ERROR = "api_error",
|
|
10
|
+
VALIDATION = "validation",
|
|
11
|
+
CONTENT_POLICY = "content_policy",
|
|
12
|
+
RATE_LIMIT = "rate_limit",
|
|
13
|
+
AUTHENTICATION = "authentication",
|
|
14
|
+
QUOTA_EXCEEDED = "quota_exceeded",
|
|
15
|
+
MODEL_NOT_FOUND = "model_not_found",
|
|
16
|
+
UNKNOWN = "unknown",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface FalErrorCategory {
|
|
20
|
+
type: FalErrorType;
|
|
21
|
+
messageKey: string;
|
|
22
|
+
retryable: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface FalErrorInfo {
|
|
26
|
+
type: FalErrorType;
|
|
27
|
+
messageKey: string;
|
|
28
|
+
retryable: boolean;
|
|
29
|
+
originalError: string;
|
|
30
|
+
statusCode?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface FalErrorMessages {
|
|
34
|
+
network?: string;
|
|
35
|
+
timeout?: string;
|
|
36
|
+
api_error?: string;
|
|
37
|
+
validation?: string;
|
|
38
|
+
content_policy?: string;
|
|
39
|
+
rate_limit?: string;
|
|
40
|
+
authentication?: string;
|
|
41
|
+
quota_exceeded?: string;
|
|
42
|
+
model_not_found?: string;
|
|
43
|
+
unknown?: string;
|
|
44
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FAL AI Types
|
|
3
|
+
* Core type definitions for FAL AI integration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface FalConfig {
|
|
7
|
+
apiKey: string;
|
|
8
|
+
baseUrl?: string;
|
|
9
|
+
maxRetries?: number;
|
|
10
|
+
baseDelay?: number;
|
|
11
|
+
maxDelay?: number;
|
|
12
|
+
defaultTimeoutMs?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface FalModel {
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
endpoint: string;
|
|
20
|
+
type: FalModelType;
|
|
21
|
+
pricing?: FalModelPricing;
|
|
22
|
+
enabled: boolean;
|
|
23
|
+
order?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type FalModelType =
|
|
27
|
+
| "text-to-image"
|
|
28
|
+
| "text-to-video"
|
|
29
|
+
| "text-to-voice"
|
|
30
|
+
| "image-to-video"
|
|
31
|
+
| "image-to-image";
|
|
32
|
+
|
|
33
|
+
export interface FalModelPricing {
|
|
34
|
+
creditsPerGeneration: number;
|
|
35
|
+
currency?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface FalJobInput {
|
|
39
|
+
[key: string]: unknown;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface FalJobResult<T = unknown> {
|
|
43
|
+
requestId: string;
|
|
44
|
+
data: T;
|
|
45
|
+
logs?: FalLogEntry[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface FalLogEntry {
|
|
49
|
+
message: string;
|
|
50
|
+
timestamp?: string;
|
|
51
|
+
level?: "info" | "warn" | "error";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface FalQueueStatus {
|
|
55
|
+
status: "IN_QUEUE" | "IN_PROGRESS" | "COMPLETED" | "FAILED";
|
|
56
|
+
requestId: string;
|
|
57
|
+
logs?: FalLogEntry[];
|
|
58
|
+
queuePosition?: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface FalSubscribeOptions {
|
|
62
|
+
onQueueUpdate?: (update: FalQueueStatus) => void;
|
|
63
|
+
timeoutMs?: number;
|
|
64
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @umituz/react-native-ai-fal-provider
|
|
3
|
+
* FAL AI provider service for React Native applications
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* import {
|
|
7
|
+
* falClientService,
|
|
8
|
+
* useFalGeneration,
|
|
9
|
+
* mapFalError
|
|
10
|
+
* } from '@umituz/react-native-ai-fal-provider';
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// =============================================================================
|
|
14
|
+
// DOMAIN LAYER - Types
|
|
15
|
+
// =============================================================================
|
|
16
|
+
|
|
17
|
+
export type {
|
|
18
|
+
FalConfig,
|
|
19
|
+
FalModel,
|
|
20
|
+
FalModelType,
|
|
21
|
+
FalModelPricing,
|
|
22
|
+
FalJobInput,
|
|
23
|
+
FalJobResult,
|
|
24
|
+
FalLogEntry,
|
|
25
|
+
FalQueueStatus,
|
|
26
|
+
FalSubscribeOptions,
|
|
27
|
+
} from "./domain/entities/fal.types";
|
|
28
|
+
|
|
29
|
+
export {
|
|
30
|
+
FalErrorType,
|
|
31
|
+
} from "./domain/entities/error.types";
|
|
32
|
+
|
|
33
|
+
export type {
|
|
34
|
+
FalErrorCategory,
|
|
35
|
+
FalErrorInfo,
|
|
36
|
+
FalErrorMessages,
|
|
37
|
+
} from "./domain/entities/error.types";
|
|
38
|
+
|
|
39
|
+
// =============================================================================
|
|
40
|
+
// INFRASTRUCTURE LAYER - Services
|
|
41
|
+
// =============================================================================
|
|
42
|
+
|
|
43
|
+
export { falClientService } from "./infrastructure/services";
|
|
44
|
+
|
|
45
|
+
// =============================================================================
|
|
46
|
+
// INFRASTRUCTURE LAYER - Utils
|
|
47
|
+
// =============================================================================
|
|
48
|
+
|
|
49
|
+
export {
|
|
50
|
+
categorizeFalError,
|
|
51
|
+
falErrorMapper,
|
|
52
|
+
mapFalError,
|
|
53
|
+
isFalErrorRetryable,
|
|
54
|
+
} from "./infrastructure/utils";
|
|
55
|
+
|
|
56
|
+
// =============================================================================
|
|
57
|
+
// PRESENTATION LAYER - Hooks
|
|
58
|
+
// =============================================================================
|
|
59
|
+
|
|
60
|
+
export {
|
|
61
|
+
useFalGeneration,
|
|
62
|
+
} from "./presentation/hooks";
|
|
63
|
+
|
|
64
|
+
export type {
|
|
65
|
+
UseFalGenerationOptions,
|
|
66
|
+
UseFalGenerationResult,
|
|
67
|
+
} from "./presentation/hooks";
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FAL Client Service
|
|
3
|
+
* Wrapper for FAL AI client operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { fal } from "@fal-ai/client";
|
|
7
|
+
import type {
|
|
8
|
+
FalConfig,
|
|
9
|
+
FalJobInput,
|
|
10
|
+
FalSubscribeOptions,
|
|
11
|
+
} from "../../domain/entities/fal.types";
|
|
12
|
+
|
|
13
|
+
declare const __DEV__: boolean;
|
|
14
|
+
|
|
15
|
+
const DEFAULT_CONFIG: Partial<FalConfig> = {
|
|
16
|
+
maxRetries: 3,
|
|
17
|
+
baseDelay: 1000,
|
|
18
|
+
maxDelay: 10000,
|
|
19
|
+
defaultTimeoutMs: 300000,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
class FalClientService {
|
|
23
|
+
private apiKey: string | null = null;
|
|
24
|
+
private config: FalConfig | null = null;
|
|
25
|
+
private initialized = false;
|
|
26
|
+
|
|
27
|
+
initialize(config: FalConfig): void {
|
|
28
|
+
this.apiKey = config.apiKey;
|
|
29
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
30
|
+
|
|
31
|
+
fal.config({
|
|
32
|
+
credentials: config.apiKey,
|
|
33
|
+
retry: {
|
|
34
|
+
maxRetries: this.config.maxRetries ?? 3,
|
|
35
|
+
baseDelay: this.config.baseDelay ?? 1000,
|
|
36
|
+
maxDelay: this.config.maxDelay ?? 10000,
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
this.initialized = true;
|
|
41
|
+
|
|
42
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
43
|
+
// eslint-disable-next-line no-console
|
|
44
|
+
console.log("[FAL] Client initialized");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
isInitialized(): boolean {
|
|
49
|
+
return this.initialized;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
getConfig(): FalConfig | null {
|
|
53
|
+
return this.config;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private validateInitialization(): void {
|
|
57
|
+
if (!this.apiKey || !this.initialized) {
|
|
58
|
+
throw new Error("FAL client not initialized. Call initialize() first.");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async submitJob(modelEndpoint: string, input: FalJobInput) {
|
|
63
|
+
this.validateInitialization();
|
|
64
|
+
return fal.queue.submit(modelEndpoint, { input });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async getJobStatus(modelEndpoint: string, requestId: string) {
|
|
68
|
+
this.validateInitialization();
|
|
69
|
+
return fal.queue.status(modelEndpoint, { requestId, logs: true });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async getJobResult(modelEndpoint: string, requestId: string) {
|
|
73
|
+
this.validateInitialization();
|
|
74
|
+
return fal.queue.result(modelEndpoint, { requestId });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async subscribe<T = unknown>(
|
|
78
|
+
modelEndpoint: string,
|
|
79
|
+
input: FalJobInput,
|
|
80
|
+
options?: FalSubscribeOptions
|
|
81
|
+
): Promise<T> {
|
|
82
|
+
this.validateInitialization();
|
|
83
|
+
|
|
84
|
+
const timeoutMs = options?.timeoutMs ?? this.config?.defaultTimeoutMs ?? 300000;
|
|
85
|
+
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
86
|
+
|
|
87
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
88
|
+
// eslint-disable-next-line no-console
|
|
89
|
+
console.log("[FAL] Subscribe started:", { modelEndpoint, timeoutMs });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const result = await Promise.race([
|
|
94
|
+
fal.subscribe(modelEndpoint, {
|
|
95
|
+
input,
|
|
96
|
+
onQueueUpdate: (update) => {
|
|
97
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
98
|
+
// eslint-disable-next-line no-console
|
|
99
|
+
console.log("[FAL] Queue update:", { status: update.status });
|
|
100
|
+
}
|
|
101
|
+
options?.onQueueUpdate?.(update as never);
|
|
102
|
+
},
|
|
103
|
+
}),
|
|
104
|
+
new Promise<never>((_, reject) => {
|
|
105
|
+
timeoutId = setTimeout(
|
|
106
|
+
() => reject(new Error("FAL subscription timeout")),
|
|
107
|
+
timeoutMs
|
|
108
|
+
);
|
|
109
|
+
}),
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
113
|
+
// eslint-disable-next-line no-console
|
|
114
|
+
console.log("[FAL] Subscribe completed:", { modelEndpoint });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return result as T;
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
120
|
+
// eslint-disable-next-line no-console
|
|
121
|
+
console.error("[FAL] Subscribe error:", {
|
|
122
|
+
modelEndpoint,
|
|
123
|
+
error: error instanceof Error ? error.message : error,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
throw error;
|
|
127
|
+
} finally {
|
|
128
|
+
if (timeoutId) {
|
|
129
|
+
clearTimeout(timeoutId);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async run<T = unknown>(modelEndpoint: string, input: FalJobInput): Promise<T> {
|
|
135
|
+
this.validateInitialization();
|
|
136
|
+
return fal.run(modelEndpoint, { input }) as Promise<T>;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
reset(): void {
|
|
140
|
+
this.apiKey = null;
|
|
141
|
+
this.config = null;
|
|
142
|
+
this.initialized = false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export const falClientService = new FalClientService();
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FAL Error Categorizer
|
|
3
|
+
* Classifies FAL AI errors for handling
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { FalErrorType, type FalErrorCategory } from "../../domain/entities/error.types";
|
|
7
|
+
|
|
8
|
+
const NETWORK_PATTERNS = [
|
|
9
|
+
"network",
|
|
10
|
+
"fetch",
|
|
11
|
+
"connection",
|
|
12
|
+
"econnrefused",
|
|
13
|
+
"enotfound",
|
|
14
|
+
"timeout",
|
|
15
|
+
"etimedout",
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const VALIDATION_PATTERNS = [
|
|
19
|
+
"validation",
|
|
20
|
+
"invalid",
|
|
21
|
+
"unprocessable",
|
|
22
|
+
"422",
|
|
23
|
+
"bad request",
|
|
24
|
+
"400",
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const CONTENT_POLICY_PATTERNS = [
|
|
28
|
+
"content_policy",
|
|
29
|
+
"content policy",
|
|
30
|
+
"policy violation",
|
|
31
|
+
"not allowed",
|
|
32
|
+
"nsfw",
|
|
33
|
+
"inappropriate",
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const RATE_LIMIT_PATTERNS = ["rate limit", "too many requests", "429", "quota"];
|
|
37
|
+
|
|
38
|
+
const AUTH_PATTERNS = [
|
|
39
|
+
"unauthorized",
|
|
40
|
+
"401",
|
|
41
|
+
"forbidden",
|
|
42
|
+
"403",
|
|
43
|
+
"api key",
|
|
44
|
+
"authentication",
|
|
45
|
+
"invalid credentials",
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
const QUOTA_PATTERNS = [
|
|
49
|
+
"quota exceeded",
|
|
50
|
+
"insufficient credits",
|
|
51
|
+
"billing",
|
|
52
|
+
"payment required",
|
|
53
|
+
"402",
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
const MODEL_NOT_FOUND_PATTERNS = [
|
|
57
|
+
"model not found",
|
|
58
|
+
"endpoint not found",
|
|
59
|
+
"404",
|
|
60
|
+
"not found",
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
class FalErrorClassifier {
|
|
64
|
+
categorize(error: unknown): FalErrorCategory {
|
|
65
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
66
|
+
const errorString = errorMessage.toLowerCase();
|
|
67
|
+
const errorName = error instanceof Error ? error.constructor.name : "";
|
|
68
|
+
|
|
69
|
+
if (this.matchesPatterns(errorString, NETWORK_PATTERNS)) {
|
|
70
|
+
return { type: FalErrorType.NETWORK, messageKey: "network", retryable: true };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (errorString.includes("timeout") || errorString.includes("timed out")) {
|
|
74
|
+
return { type: FalErrorType.TIMEOUT, messageKey: "timeout", retryable: true };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (this.matchesPatterns(errorString, VALIDATION_PATTERNS)) {
|
|
78
|
+
return { type: FalErrorType.VALIDATION, messageKey: "validation", retryable: false };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (this.matchesPatterns(errorString, CONTENT_POLICY_PATTERNS)) {
|
|
82
|
+
return { type: FalErrorType.CONTENT_POLICY, messageKey: "content_policy", retryable: false };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (this.matchesPatterns(errorString, RATE_LIMIT_PATTERNS)) {
|
|
86
|
+
return { type: FalErrorType.RATE_LIMIT, messageKey: "rate_limit", retryable: true };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (this.matchesPatterns(errorString, AUTH_PATTERNS)) {
|
|
90
|
+
return { type: FalErrorType.AUTHENTICATION, messageKey: "authentication", retryable: false };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (this.matchesPatterns(errorString, QUOTA_PATTERNS)) {
|
|
94
|
+
return { type: FalErrorType.QUOTA_EXCEEDED, messageKey: "quota_exceeded", retryable: false };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (this.matchesPatterns(errorString, MODEL_NOT_FOUND_PATTERNS)) {
|
|
98
|
+
return { type: FalErrorType.MODEL_NOT_FOUND, messageKey: "model_not_found", retryable: false };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (this.isApiError(errorString, errorName)) {
|
|
102
|
+
const retryable = !this.isInternalServerError(errorString);
|
|
103
|
+
return { type: FalErrorType.API_ERROR, messageKey: "api_error", retryable };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return { type: FalErrorType.UNKNOWN, messageKey: "unknown", retryable: false };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private matchesPatterns(errorString: string, patterns: string[]): boolean {
|
|
110
|
+
return patterns.some((pattern) => errorString.includes(pattern));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private isApiError(errorString: string, errorName: string): boolean {
|
|
114
|
+
return (
|
|
115
|
+
errorString.includes("api error") ||
|
|
116
|
+
errorName.toLowerCase() === "apierror" ||
|
|
117
|
+
["502", "503", "504"].some((code) => errorString.includes(code)) ||
|
|
118
|
+
this.isInternalServerError(errorString)
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private isInternalServerError(errorString: string): boolean {
|
|
123
|
+
return errorString.includes("internal server error") || errorString.includes("500");
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const classifier = new FalErrorClassifier();
|
|
128
|
+
|
|
129
|
+
export const categorizeFalError = (error: unknown): FalErrorCategory => {
|
|
130
|
+
return classifier.categorize(error);
|
|
131
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FAL Error Mapper
|
|
3
|
+
* Maps FAL errors to user-friendly information
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { FalErrorInfo, FalErrorMessages } from "../../domain/entities/error.types";
|
|
7
|
+
import { categorizeFalError } from "./error-categorizer";
|
|
8
|
+
|
|
9
|
+
const DEFAULT_MESSAGE_PREFIX = "fal.errors";
|
|
10
|
+
|
|
11
|
+
class FalErrorMapper {
|
|
12
|
+
private messagePrefix = DEFAULT_MESSAGE_PREFIX;
|
|
13
|
+
private customMessages?: FalErrorMessages;
|
|
14
|
+
|
|
15
|
+
setMessagePrefix(prefix: string): void {
|
|
16
|
+
this.messagePrefix = prefix;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
setCustomMessages(messages: FalErrorMessages): void {
|
|
20
|
+
this.customMessages = messages;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
mapToErrorInfo(error: unknown): FalErrorInfo {
|
|
24
|
+
const category = categorizeFalError(error);
|
|
25
|
+
const originalError = error instanceof Error ? error.message : String(error);
|
|
26
|
+
|
|
27
|
+
const messageKey = this.customMessages?.[category.messageKey as keyof FalErrorMessages]
|
|
28
|
+
? category.messageKey
|
|
29
|
+
: `${this.messagePrefix}.${category.messageKey}`;
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
type: category.type,
|
|
33
|
+
messageKey,
|
|
34
|
+
retryable: category.retryable,
|
|
35
|
+
originalError,
|
|
36
|
+
statusCode: this.extractStatusCode(originalError),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
isRetryable(error: unknown): boolean {
|
|
41
|
+
const category = categorizeFalError(error);
|
|
42
|
+
return category.retryable;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
getErrorType(error: unknown) {
|
|
46
|
+
return categorizeFalError(error).type;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private extractStatusCode(errorString: string): number | undefined {
|
|
50
|
+
const codes = ["400", "401", "402", "403", "404", "422", "429", "500", "502", "503", "504"];
|
|
51
|
+
for (const code of codes) {
|
|
52
|
+
if (errorString.includes(code)) {
|
|
53
|
+
return parseInt(code, 10);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const falErrorMapper = new FalErrorMapper();
|
|
61
|
+
|
|
62
|
+
export const mapFalError = (error: unknown): FalErrorInfo => {
|
|
63
|
+
return falErrorMapper.mapToErrorInfo(error);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const isFalErrorRetryable = (error: unknown): boolean => {
|
|
67
|
+
return falErrorMapper.isRetryable(error);
|
|
68
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useFalGeneration Hook
|
|
3
|
+
* React hook for FAL AI generation operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState, useCallback, useRef } from "react";
|
|
7
|
+
import { falClientService } from "../../infrastructure/services/fal-client.service";
|
|
8
|
+
import { mapFalError, isFalErrorRetryable } from "../../infrastructure/utils/error-mapper";
|
|
9
|
+
import type { FalJobInput, FalQueueStatus } from "../../domain/entities/fal.types";
|
|
10
|
+
import type { FalErrorInfo } from "../../domain/entities/error.types";
|
|
11
|
+
|
|
12
|
+
export interface UseFalGenerationOptions {
|
|
13
|
+
timeoutMs?: number;
|
|
14
|
+
onProgress?: (status: FalQueueStatus) => void;
|
|
15
|
+
onError?: (error: FalErrorInfo) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface UseFalGenerationResult<T> {
|
|
19
|
+
data: T | null;
|
|
20
|
+
error: FalErrorInfo | null;
|
|
21
|
+
isLoading: boolean;
|
|
22
|
+
isRetryable: boolean;
|
|
23
|
+
generate: (modelEndpoint: string, input: FalJobInput) => Promise<T | null>;
|
|
24
|
+
retry: () => Promise<T | null>;
|
|
25
|
+
reset: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function useFalGeneration<T = unknown>(
|
|
29
|
+
options?: UseFalGenerationOptions
|
|
30
|
+
): UseFalGenerationResult<T> {
|
|
31
|
+
const [data, setData] = useState<T | null>(null);
|
|
32
|
+
const [error, setError] = useState<FalErrorInfo | null>(null);
|
|
33
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
34
|
+
|
|
35
|
+
const lastRequestRef = useRef<{ endpoint: string; input: FalJobInput } | null>(null);
|
|
36
|
+
|
|
37
|
+
const generate = useCallback(
|
|
38
|
+
async (modelEndpoint: string, input: FalJobInput): Promise<T | null> => {
|
|
39
|
+
lastRequestRef.current = { endpoint: modelEndpoint, input };
|
|
40
|
+
setIsLoading(true);
|
|
41
|
+
setError(null);
|
|
42
|
+
setData(null);
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const result = await falClientService.subscribe<T>(modelEndpoint, input, {
|
|
46
|
+
timeoutMs: options?.timeoutMs,
|
|
47
|
+
onQueueUpdate: options?.onProgress,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
setData(result);
|
|
51
|
+
return result;
|
|
52
|
+
} catch (err) {
|
|
53
|
+
const errorInfo = mapFalError(err);
|
|
54
|
+
setError(errorInfo);
|
|
55
|
+
options?.onError?.(errorInfo);
|
|
56
|
+
return null;
|
|
57
|
+
} finally {
|
|
58
|
+
setIsLoading(false);
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
[options]
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const retry = useCallback(async (): Promise<T | null> => {
|
|
65
|
+
if (!lastRequestRef.current) return null;
|
|
66
|
+
const { endpoint, input } = lastRequestRef.current;
|
|
67
|
+
return generate(endpoint, input);
|
|
68
|
+
}, [generate]);
|
|
69
|
+
|
|
70
|
+
const reset = useCallback(() => {
|
|
71
|
+
setData(null);
|
|
72
|
+
setError(null);
|
|
73
|
+
setIsLoading(false);
|
|
74
|
+
lastRequestRef.current = null;
|
|
75
|
+
}, []);
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
data,
|
|
79
|
+
error,
|
|
80
|
+
isLoading,
|
|
81
|
+
isRetryable: error ? isFalErrorRetryable(error.originalError) : false,
|
|
82
|
+
generate,
|
|
83
|
+
retry,
|
|
84
|
+
reset,
|
|
85
|
+
};
|
|
86
|
+
}
|