@umituz/react-native-ai-fal-provider 1.0.58 → 1.0.60
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/index.ts +21 -0
- package/src/infrastructure/services/fal-provider.ts +9 -4
- package/src/infrastructure/services/fal-status-mapper.ts +18 -10
- package/src/infrastructure/utils/helpers.util.ts +151 -0
- package/src/infrastructure/utils/index.ts +47 -0
- package/src/infrastructure/utils/job-metadata.util.ts +176 -0
- package/src/infrastructure/utils/type-guards.util.ts +114 -0
- package/src/presentation/hooks/use-fal-generation.ts +8 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-fal-provider",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.60",
|
|
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",
|
package/src/index.ts
CHANGED
|
@@ -36,6 +36,27 @@ export {
|
|
|
36
36
|
buildReplaceBackgroundInput, buildHDTouchUpInput,
|
|
37
37
|
} from "./infrastructure/utils";
|
|
38
38
|
|
|
39
|
+
export {
|
|
40
|
+
isFalModelType, isModelType, isFalErrorType,
|
|
41
|
+
isValidBase64Image, isValidApiKey, isValidModelId, isValidPrompt,
|
|
42
|
+
isValidTimeout, isValidRetryCount,
|
|
43
|
+
} from "./infrastructure/utils";
|
|
44
|
+
|
|
45
|
+
export {
|
|
46
|
+
formatImageDataUri, extractBase64, getDataUriExtension, isImageDataUri,
|
|
47
|
+
calculateTimeoutWithJitter, formatCreditCost, truncatePrompt, sanitizePrompt,
|
|
48
|
+
buildErrorMessage, isDefined, removeNullish, debounce, throttle,
|
|
49
|
+
} from "./infrastructure/utils";
|
|
50
|
+
|
|
51
|
+
export {
|
|
52
|
+
createJobMetadata, updateJobMetadata, isJobCompleted, isJobRunning,
|
|
53
|
+
isJobStale, getJobDuration, formatJobDuration, calculateJobProgress,
|
|
54
|
+
serializeJobMetadata, deserializeJobMetadata, filterValidJobs,
|
|
55
|
+
sortJobsByCreation, getActiveJobs, getCompletedJobs,
|
|
56
|
+
} from "./infrastructure/utils";
|
|
57
|
+
|
|
58
|
+
export type { FalJobMetadata } from "./infrastructure/utils";
|
|
59
|
+
|
|
39
60
|
export type {
|
|
40
61
|
UpscaleOptions, PhotoRestoreOptions, FaceSwapOptions, ImageToImagePromptConfig,
|
|
41
62
|
RemoveBackgroundOptions, RemoveObjectOptions, ReplaceBackgroundOptions,
|
|
@@ -106,6 +106,7 @@ export class FalProvider implements IAIProvider {
|
|
|
106
106
|
this.validateInitialization();
|
|
107
107
|
const timeoutMs = options?.timeoutMs ?? this.config?.defaultTimeoutMs ?? DEFAULT_FAL_CONFIG.defaultTimeoutMs;
|
|
108
108
|
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
109
|
+
let currentRequestId: string | null = null;
|
|
109
110
|
|
|
110
111
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
111
112
|
console.log("[FalProvider] Subscribe started:", { model, timeoutMs });
|
|
@@ -119,12 +120,16 @@ export class FalProvider implements IAIProvider {
|
|
|
119
120
|
input,
|
|
120
121
|
logs: false,
|
|
121
122
|
pollInterval: DEFAULT_FAL_CONFIG.pollInterval,
|
|
122
|
-
onQueueUpdate: (update: { status: string; logs?: unknown[] }) => {
|
|
123
|
-
|
|
123
|
+
onQueueUpdate: (update: { status: string; logs?: unknown[]; request_id?: string }) => {
|
|
124
|
+
currentRequestId = update.request_id ?? null;
|
|
125
|
+
const jobStatus = mapFalStatusToJobStatus({
|
|
126
|
+
...update as unknown as FalQueueStatus,
|
|
127
|
+
requestId: currentRequestId ?? "",
|
|
128
|
+
});
|
|
124
129
|
if (jobStatus.status !== lastStatus) {
|
|
125
130
|
lastStatus = jobStatus.status;
|
|
126
131
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
127
|
-
console.log("[FalProvider] Status:", jobStatus.status);
|
|
132
|
+
console.log("[FalProvider] Status:", jobStatus.status, "RequestId:", currentRequestId);
|
|
128
133
|
}
|
|
129
134
|
}
|
|
130
135
|
options?.onQueueUpdate?.(jobStatus);
|
|
@@ -136,7 +141,7 @@ export class FalProvider implements IAIProvider {
|
|
|
136
141
|
]);
|
|
137
142
|
|
|
138
143
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
139
|
-
console.log("[FalProvider] Subscribe completed:", { model });
|
|
144
|
+
console.log("[FalProvider] Subscribe completed:", { model, requestId: currentRequestId });
|
|
140
145
|
}
|
|
141
146
|
|
|
142
147
|
validateNSFWContent(result as Record<string, unknown>);
|
|
@@ -6,21 +6,29 @@
|
|
|
6
6
|
import type { JobStatus, AIJobStatusType } from "@umituz/react-native-ai-generation-content";
|
|
7
7
|
import type { FalQueueStatus, FalLogEntry } from "../../domain/entities/fal.types";
|
|
8
8
|
|
|
9
|
-
const STATUS_MAP
|
|
10
|
-
IN_QUEUE: "IN_QUEUE",
|
|
11
|
-
IN_PROGRESS: "IN_PROGRESS",
|
|
12
|
-
COMPLETED: "COMPLETED",
|
|
13
|
-
FAILED: "FAILED",
|
|
14
|
-
}
|
|
9
|
+
const STATUS_MAP = {
|
|
10
|
+
IN_QUEUE: "IN_QUEUE" as const,
|
|
11
|
+
IN_PROGRESS: "IN_PROGRESS" as const,
|
|
12
|
+
COMPLETED: "COMPLETED" as const,
|
|
13
|
+
FAILED: "FAILED" as const,
|
|
14
|
+
} as const satisfies Record<string, AIJobStatusType>;
|
|
15
15
|
|
|
16
|
+
const DEFAULT_STATUS: AIJobStatusType = "IN_PROGRESS";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Map FAL queue status to standardized job status
|
|
20
|
+
* Provides safe defaults for missing or invalid values
|
|
21
|
+
*/
|
|
16
22
|
export function mapFalStatusToJobStatus(status: FalQueueStatus): JobStatus {
|
|
23
|
+
const mappedStatus = STATUS_MAP[status.status] ?? DEFAULT_STATUS;
|
|
24
|
+
|
|
17
25
|
return {
|
|
18
|
-
status:
|
|
26
|
+
status: mappedStatus,
|
|
19
27
|
logs: status.logs?.map((log: FalLogEntry) => ({
|
|
20
28
|
message: log.message,
|
|
21
29
|
level: log.level ?? "info",
|
|
22
|
-
timestamp: log.timestamp,
|
|
23
|
-
})),
|
|
24
|
-
queuePosition: status.queuePosition,
|
|
30
|
+
timestamp: log.timestamp ?? new Date().toISOString(),
|
|
31
|
+
})) ?? [],
|
|
32
|
+
queuePosition: status.queuePosition ?? undefined,
|
|
25
33
|
};
|
|
26
34
|
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helper Utilities
|
|
3
|
+
* Common helper functions for FAL operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Format image as data URI if not already formatted
|
|
8
|
+
*/
|
|
9
|
+
export function formatImageDataUri(base64: string): string {
|
|
10
|
+
if (base64.startsWith("data:")) {
|
|
11
|
+
return base64;
|
|
12
|
+
}
|
|
13
|
+
return `data:image/jpeg;base64,${base64}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Extract base64 from data URI
|
|
18
|
+
*/
|
|
19
|
+
export function extractBase64(dataUri: string): string {
|
|
20
|
+
if (!dataUri.startsWith("data:")) {
|
|
21
|
+
return dataUri;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const parts = dataUri.split(",");
|
|
25
|
+
return parts.length > 1 ? parts[1] : dataUri;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get file extension from data URI
|
|
30
|
+
*/
|
|
31
|
+
export function getDataUriExtension(dataUri: string): string | null {
|
|
32
|
+
const match = dataUri.match(/^data:image\/(\w+);base64/);
|
|
33
|
+
return match ? match[1] : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Check if data URI is an image
|
|
38
|
+
*/
|
|
39
|
+
export function isImageDataUri(value: string): boolean {
|
|
40
|
+
return value.startsWith("data:image/");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Calculate timeout with jitter to avoid thundering herd
|
|
45
|
+
*/
|
|
46
|
+
export function calculateTimeoutWithJitter(
|
|
47
|
+
baseTimeout: number,
|
|
48
|
+
jitterPercent: number = 0.1
|
|
49
|
+
): number {
|
|
50
|
+
const jitter = baseTimeout * jitterPercent;
|
|
51
|
+
const randomJitter = Math.random() * jitter - jitter / 2;
|
|
52
|
+
return Math.max(1000, baseTimeout + randomJitter);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Format credit cost for display
|
|
57
|
+
*/
|
|
58
|
+
export function formatCreditCost(cost: number): string {
|
|
59
|
+
if (cost % 1 === 0) {
|
|
60
|
+
return cost.toString();
|
|
61
|
+
}
|
|
62
|
+
return cost.toFixed(2);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Truncate prompt to maximum length
|
|
67
|
+
*/
|
|
68
|
+
export function truncatePrompt(prompt: string, maxLength: number = 5000): string {
|
|
69
|
+
if (prompt.length <= maxLength) {
|
|
70
|
+
return prompt;
|
|
71
|
+
}
|
|
72
|
+
return prompt.slice(0, maxLength - 3) + "...";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Sanitize prompt by removing excessive whitespace
|
|
77
|
+
*/
|
|
78
|
+
export function sanitizePrompt(prompt: string): string {
|
|
79
|
+
return prompt.trim().replace(/\s+/g, " ");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Build error message with context
|
|
84
|
+
*/
|
|
85
|
+
export function buildErrorMessage(
|
|
86
|
+
type: string,
|
|
87
|
+
context: Record<string, unknown>
|
|
88
|
+
): string {
|
|
89
|
+
const contextStr = Object.entries(context)
|
|
90
|
+
.map(([key, value]) => `${key}=${JSON.stringify(value)}`)
|
|
91
|
+
.join(", ");
|
|
92
|
+
return `${type}${contextStr ? ` (${contextStr})` : ""}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Check if value is defined (not null or undefined)
|
|
97
|
+
*/
|
|
98
|
+
export function isDefined<T>(value: T | null | undefined): value is T {
|
|
99
|
+
return value !== null && value !== undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Filter out null and undefined values from object
|
|
104
|
+
*/
|
|
105
|
+
export function removeNullish<T extends Record<string, unknown>>(
|
|
106
|
+
obj: T
|
|
107
|
+
): Partial<T> {
|
|
108
|
+
return Object.fromEntries(
|
|
109
|
+
Object.entries(obj).filter(([_, value]) => isDefined(value))
|
|
110
|
+
) as Partial<T>;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Debounce function (for rate limiting)
|
|
115
|
+
*/
|
|
116
|
+
export function debounce<T extends (...args: never[]) => unknown>(
|
|
117
|
+
func: T,
|
|
118
|
+
wait: number
|
|
119
|
+
): (...args: Parameters<T>) => void {
|
|
120
|
+
let timeout: ReturnType<typeof setTimeout> | null = null;
|
|
121
|
+
|
|
122
|
+
return function executedFunction(...args: Parameters<T>) {
|
|
123
|
+
const later = () => {
|
|
124
|
+
timeout = null;
|
|
125
|
+
func(...args);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
if (timeout) {
|
|
129
|
+
clearTimeout(timeout);
|
|
130
|
+
}
|
|
131
|
+
timeout = setTimeout(later, wait);
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Simple throttle function
|
|
137
|
+
*/
|
|
138
|
+
export function throttle<T extends (...args: never[]) => unknown>(
|
|
139
|
+
func: T,
|
|
140
|
+
limit: number
|
|
141
|
+
): (...args: Parameters<T>) => void {
|
|
142
|
+
let inThrottle = false;
|
|
143
|
+
|
|
144
|
+
return function executedFunction(...args: Parameters<T>) {
|
|
145
|
+
if (!inThrottle) {
|
|
146
|
+
func(...args);
|
|
147
|
+
inThrottle = true;
|
|
148
|
+
setTimeout(() => (inThrottle = false), limit);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
}
|
|
@@ -18,3 +18,50 @@ export {
|
|
|
18
18
|
buildReplaceBackgroundInput,
|
|
19
19
|
buildHDTouchUpInput,
|
|
20
20
|
} from "./input-builders.util";
|
|
21
|
+
|
|
22
|
+
export {
|
|
23
|
+
isFalModelType,
|
|
24
|
+
isModelType,
|
|
25
|
+
isFalErrorType,
|
|
26
|
+
isValidBase64Image,
|
|
27
|
+
isValidApiKey,
|
|
28
|
+
isValidModelId,
|
|
29
|
+
isValidPrompt,
|
|
30
|
+
isValidTimeout,
|
|
31
|
+
isValidRetryCount,
|
|
32
|
+
} from "./type-guards.util";
|
|
33
|
+
|
|
34
|
+
export {
|
|
35
|
+
formatImageDataUri,
|
|
36
|
+
extractBase64,
|
|
37
|
+
getDataUriExtension,
|
|
38
|
+
isImageDataUri,
|
|
39
|
+
calculateTimeoutWithJitter,
|
|
40
|
+
formatCreditCost,
|
|
41
|
+
truncatePrompt,
|
|
42
|
+
sanitizePrompt,
|
|
43
|
+
buildErrorMessage,
|
|
44
|
+
isDefined,
|
|
45
|
+
removeNullish,
|
|
46
|
+
debounce,
|
|
47
|
+
throttle,
|
|
48
|
+
} from "./helpers.util";
|
|
49
|
+
|
|
50
|
+
export {
|
|
51
|
+
createJobMetadata,
|
|
52
|
+
updateJobMetadata,
|
|
53
|
+
isJobCompleted,
|
|
54
|
+
isJobRunning,
|
|
55
|
+
isJobStale,
|
|
56
|
+
getJobDuration,
|
|
57
|
+
formatJobDuration,
|
|
58
|
+
calculateJobProgress,
|
|
59
|
+
serializeJobMetadata,
|
|
60
|
+
deserializeJobMetadata,
|
|
61
|
+
filterValidJobs,
|
|
62
|
+
sortJobsByCreation,
|
|
63
|
+
getActiveJobs,
|
|
64
|
+
getCompletedJobs,
|
|
65
|
+
} from "./job-metadata.util";
|
|
66
|
+
|
|
67
|
+
export type { FalJobMetadata } from "./job-metadata.util";
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Job Metadata Utilities
|
|
3
|
+
* Helper functions for job metadata and management
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { JobStatus } from "@umituz/react-native-ai-generation-content";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Job metadata for tracking and persistence
|
|
10
|
+
*/
|
|
11
|
+
export interface FalJobMetadata {
|
|
12
|
+
readonly requestId: string;
|
|
13
|
+
readonly model: string;
|
|
14
|
+
readonly status: JobStatus["status"];
|
|
15
|
+
readonly createdAt: string;
|
|
16
|
+
readonly updatedAt: string;
|
|
17
|
+
readonly completedAt?: string;
|
|
18
|
+
readonly timeout?: number;
|
|
19
|
+
readonly error?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Create job metadata
|
|
24
|
+
*/
|
|
25
|
+
export function createJobMetadata(
|
|
26
|
+
requestId: string,
|
|
27
|
+
model: string,
|
|
28
|
+
timeout?: number
|
|
29
|
+
): FalJobMetadata {
|
|
30
|
+
const now = new Date().toISOString();
|
|
31
|
+
return {
|
|
32
|
+
requestId,
|
|
33
|
+
model,
|
|
34
|
+
status: "IN_QUEUE",
|
|
35
|
+
createdAt: now,
|
|
36
|
+
updatedAt: now,
|
|
37
|
+
timeout,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Update job metadata status
|
|
43
|
+
*/
|
|
44
|
+
export function updateJobMetadata(
|
|
45
|
+
metadata: FalJobMetadata,
|
|
46
|
+
status: JobStatus["status"],
|
|
47
|
+
error?: string
|
|
48
|
+
): FalJobMetadata {
|
|
49
|
+
return {
|
|
50
|
+
...metadata,
|
|
51
|
+
status,
|
|
52
|
+
updatedAt: new Date().toISOString(),
|
|
53
|
+
...(status === "COMPLETED" || status === "FAILED" ? { completedAt: new Date().toISOString() } : {}),
|
|
54
|
+
...(error ? { error } : {}),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Check if job is completed (success or failure)
|
|
60
|
+
*/
|
|
61
|
+
export function isJobCompleted(metadata: FalJobMetadata): boolean {
|
|
62
|
+
return metadata.status === "COMPLETED" || metadata.status === "FAILED";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Check if job is running
|
|
67
|
+
*/
|
|
68
|
+
export function isJobRunning(metadata: FalJobMetadata): boolean {
|
|
69
|
+
return metadata.status === "IN_QUEUE" || metadata.status === "IN_PROGRESS";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Check if job is stale (older than specified minutes)
|
|
74
|
+
*/
|
|
75
|
+
export function isJobStale(metadata: FalJobMetadata, maxAgeMinutes: number = 60): boolean {
|
|
76
|
+
const age = Date.now() - new Date(metadata.createdAt).getTime();
|
|
77
|
+
const maxAgeMs = maxAgeMinutes * 60 * 1000;
|
|
78
|
+
return age > maxAgeMs;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get job duration in milliseconds
|
|
83
|
+
*/
|
|
84
|
+
export function getJobDuration(metadata: FalJobMetadata): number | null {
|
|
85
|
+
if (!metadata.completedAt) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
return new Date(metadata.completedAt).getTime() - new Date(metadata.createdAt).getTime();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Format job duration for display
|
|
93
|
+
*/
|
|
94
|
+
export function formatJobDuration(metadata: FalJobMetadata): string {
|
|
95
|
+
const duration = getJobDuration(metadata);
|
|
96
|
+
if (!duration) {
|
|
97
|
+
return "In progress";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const seconds = Math.floor(duration / 1000);
|
|
101
|
+
if (seconds < 60) {
|
|
102
|
+
return `${seconds}s`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const minutes = Math.floor(seconds / 60);
|
|
106
|
+
const remainingSeconds = seconds % 60;
|
|
107
|
+
return remainingSeconds > 0 ? `${minutes}m ${remainingSeconds}s` : `${minutes}m`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Calculate job progress percentage
|
|
112
|
+
*/
|
|
113
|
+
export function calculateJobProgress(metadata: FalJobMetadata): number {
|
|
114
|
+
switch (metadata.status) {
|
|
115
|
+
case "IN_QUEUE":
|
|
116
|
+
return 10;
|
|
117
|
+
case "IN_PROGRESS":
|
|
118
|
+
return 50;
|
|
119
|
+
case "COMPLETED":
|
|
120
|
+
return 100;
|
|
121
|
+
case "FAILED":
|
|
122
|
+
return 0;
|
|
123
|
+
default:
|
|
124
|
+
return 0;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Serialize job metadata for storage
|
|
130
|
+
*/
|
|
131
|
+
export function serializeJobMetadata(metadata: FalJobMetadata): string {
|
|
132
|
+
return JSON.stringify(metadata);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Deserialize job metadata from storage
|
|
137
|
+
*/
|
|
138
|
+
export function deserializeJobMetadata(data: string): FalJobMetadata | null {
|
|
139
|
+
try {
|
|
140
|
+
return JSON.parse(data) as FalJobMetadata;
|
|
141
|
+
} catch {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Filter valid job metadata from array
|
|
148
|
+
*/
|
|
149
|
+
export function filterValidJobs(jobs: FalJobMetadata[]): FalJobMetadata[] {
|
|
150
|
+
return jobs.filter((job) => !isJobStale(job));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Sort jobs by creation time (newest first)
|
|
155
|
+
*/
|
|
156
|
+
export function sortJobsByCreation(jobs: FalJobMetadata[]): FalJobMetadata[] {
|
|
157
|
+
return [...jobs].sort((a, b) => {
|
|
158
|
+
const timeA = new Date(a.createdAt).getTime();
|
|
159
|
+
const timeB = new Date(b.createdAt).getTime();
|
|
160
|
+
return timeB - timeA;
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Get active jobs (not completed and not stale)
|
|
166
|
+
*/
|
|
167
|
+
export function getActiveJobs(jobs: FalJobMetadata[]): FalJobMetadata[] {
|
|
168
|
+
return jobs.filter((job) => isJobRunning(job) && !isJobStale(job));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Get completed jobs
|
|
173
|
+
*/
|
|
174
|
+
export function getCompletedJobs(jobs: FalJobMetadata[]): FalJobMetadata[] {
|
|
175
|
+
return jobs.filter((job) => isJobCompleted(job));
|
|
176
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type Guards and Validation Utilities
|
|
3
|
+
* Runtime type checking and validation helpers
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { FalModelType } from "../../domain/entities/fal.types";
|
|
7
|
+
import type { ModelType } from "../../domain/types/model-selection.types";
|
|
8
|
+
import { FalErrorType } from "../../domain/entities/error.types";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Check if a string is a valid FalModelType
|
|
12
|
+
*/
|
|
13
|
+
export function isFalModelType(value: unknown): value is FalModelType {
|
|
14
|
+
const validTypes: ReadonlyArray<FalModelType> = [
|
|
15
|
+
"text-to-image",
|
|
16
|
+
"text-to-video",
|
|
17
|
+
"text-to-voice",
|
|
18
|
+
"image-to-video",
|
|
19
|
+
"image-to-image",
|
|
20
|
+
"text-to-text",
|
|
21
|
+
];
|
|
22
|
+
return typeof value === "string" && validTypes.includes(value as FalModelType);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Check if a string is a valid ModelType
|
|
27
|
+
*/
|
|
28
|
+
export function isModelType(value: unknown): value is ModelType {
|
|
29
|
+
const validTypes: ReadonlyArray<ModelType> = [
|
|
30
|
+
"text-to-image",
|
|
31
|
+
"text-to-video",
|
|
32
|
+
"image-to-video",
|
|
33
|
+
"text-to-voice",
|
|
34
|
+
];
|
|
35
|
+
return typeof value === "string" && validTypes.includes(value as ModelType);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Check if error is a FalErrorType
|
|
40
|
+
*/
|
|
41
|
+
export function isFalErrorType(value: unknown): value is FalErrorType {
|
|
42
|
+
const validTypes: ReadonlyArray<FalErrorType> = [
|
|
43
|
+
FalErrorType.NETWORK,
|
|
44
|
+
FalErrorType.TIMEOUT,
|
|
45
|
+
FalErrorType.API_ERROR,
|
|
46
|
+
FalErrorType.VALIDATION,
|
|
47
|
+
FalErrorType.CONTENT_POLICY,
|
|
48
|
+
FalErrorType.RATE_LIMIT,
|
|
49
|
+
FalErrorType.AUTHENTICATION,
|
|
50
|
+
FalErrorType.QUOTA_EXCEEDED,
|
|
51
|
+
FalErrorType.MODEL_NOT_FOUND,
|
|
52
|
+
FalErrorType.UNKNOWN,
|
|
53
|
+
];
|
|
54
|
+
return typeof value === "string" && validTypes.includes(value as FalErrorType);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Validate base64 image string
|
|
59
|
+
*/
|
|
60
|
+
export function isValidBase64Image(value: unknown): boolean {
|
|
61
|
+
if (typeof value !== "string") {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Check data URI prefix
|
|
66
|
+
if (value.startsWith("data:image/")) {
|
|
67
|
+
return value.includes("base64,");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Check if it's a valid base64 string
|
|
71
|
+
const base64Pattern = /^[A-Za-z0-9+/]+=*$/;
|
|
72
|
+
return base64Pattern.test(value) && value.length > 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Validate API key format
|
|
77
|
+
*/
|
|
78
|
+
export function isValidApiKey(value: unknown): boolean {
|
|
79
|
+
return typeof value === "string" && value.length > 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Validate model ID format
|
|
84
|
+
*/
|
|
85
|
+
export function isValidModelId(value: unknown): boolean {
|
|
86
|
+
if (typeof value !== "string") {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// FAL model IDs typically follow the pattern: "owner/model-name" or "owner/model/version"
|
|
91
|
+
const modelIdPattern = /^[a-z0-9-]+\/[a-z0-9-]+(\/[a-z0-9.]+)?$/;
|
|
92
|
+
return modelIdPattern.test(value);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Validate prompt string
|
|
97
|
+
*/
|
|
98
|
+
export function isValidPrompt(value: unknown): boolean {
|
|
99
|
+
return typeof value === "string" && value.trim().length > 0 && value.length <= 5000;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Validate timeout value
|
|
104
|
+
*/
|
|
105
|
+
export function isValidTimeout(value: unknown): boolean {
|
|
106
|
+
return typeof value === "number" && value > 0 && value <= 600000; // Max 10 minutes
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Validate retry count
|
|
111
|
+
*/
|
|
112
|
+
export function isValidRetryCount(value: unknown): boolean {
|
|
113
|
+
return typeof value === "number" && value >= 0 && value <= 10;
|
|
114
|
+
}
|
|
@@ -20,6 +20,7 @@ export interface UseFalGenerationResult<T> {
|
|
|
20
20
|
error: FalErrorInfo | null;
|
|
21
21
|
isLoading: boolean;
|
|
22
22
|
isRetryable: boolean;
|
|
23
|
+
requestId: string | null;
|
|
23
24
|
generate: (modelEndpoint: string, input: FalJobInput) => Promise<T | null>;
|
|
24
25
|
retry: () => Promise<T | null>;
|
|
25
26
|
reset: () => void;
|
|
@@ -33,6 +34,7 @@ export function useFalGeneration<T = unknown>(
|
|
|
33
34
|
const [isLoading, setIsLoading] = useState(false);
|
|
34
35
|
|
|
35
36
|
const lastRequestRef = useRef<{ endpoint: string; input: FalJobInput } | null>(null);
|
|
37
|
+
const currentRequestIdRef = useRef<string | null>(null);
|
|
36
38
|
|
|
37
39
|
const generate = useCallback(
|
|
38
40
|
async (modelEndpoint: string, input: FalJobInput): Promise<T | null> => {
|
|
@@ -40,15 +42,18 @@ export function useFalGeneration<T = unknown>(
|
|
|
40
42
|
setIsLoading(true);
|
|
41
43
|
setError(null);
|
|
42
44
|
setData(null);
|
|
45
|
+
currentRequestIdRef.current = null;
|
|
43
46
|
|
|
44
47
|
try {
|
|
45
48
|
const result = await falProvider.subscribe<T>(modelEndpoint, input, {
|
|
46
49
|
timeoutMs: options?.timeoutMs,
|
|
47
50
|
onQueueUpdate: (status) => {
|
|
51
|
+
// Note: requestId is tracked internally by falProvider subscribe
|
|
52
|
+
// and exposed via the requestId ref, not from status object
|
|
48
53
|
// Map JobStatus to FalQueueStatus for backward compatibility
|
|
49
54
|
options?.onProgress?.({
|
|
50
55
|
status: status.status,
|
|
51
|
-
requestId: "",
|
|
56
|
+
requestId: currentRequestIdRef.current ?? "",
|
|
52
57
|
logs: status.logs?.map((log: FalLogEntry) => ({
|
|
53
58
|
message: log.message,
|
|
54
59
|
level: log.level,
|
|
@@ -84,6 +89,7 @@ export function useFalGeneration<T = unknown>(
|
|
|
84
89
|
setError(null);
|
|
85
90
|
setIsLoading(false);
|
|
86
91
|
lastRequestRef.current = null;
|
|
92
|
+
currentRequestIdRef.current = null;
|
|
87
93
|
}, []);
|
|
88
94
|
|
|
89
95
|
return {
|
|
@@ -91,6 +97,7 @@ export function useFalGeneration<T = unknown>(
|
|
|
91
97
|
error,
|
|
92
98
|
isLoading,
|
|
93
99
|
isRetryable: error ? isFalErrorRetryable(error.originalError) : false,
|
|
100
|
+
requestId: currentRequestIdRef.current,
|
|
94
101
|
generate,
|
|
95
102
|
retry,
|
|
96
103
|
reset,
|