@umituz/react-native-ai-fal-provider 3.2.39 → 3.2.40
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 +2 -1
- package/src/global.d.ts +10 -0
- package/src/index.ts +146 -12
- package/src/infrastructure/services/fal-provider-subscription.ts +9 -7
- package/src/infrastructure/services/fal-provider.ts +1 -1
- package/src/infrastructure/services/request-store.ts +10 -2
- package/src/infrastructure/utils/fal-error-handler.util.ts +36 -8
- package/src/infrastructure/utils/fal-storage.util.ts +19 -5
- package/src/infrastructure/utils/helpers/calculation-helpers.util.ts +168 -0
- package/src/infrastructure/utils/helpers/error-helpers.util.ts +0 -21
- package/src/infrastructure/utils/helpers/index.ts +2 -1
- package/src/infrastructure/utils/input-preprocessor.util.ts +1 -3
- package/src/infrastructure/utils/pricing/fal-pricing.util.ts +19 -1
- package/src/infrastructure/utils/type-guards/index.ts +0 -2
- package/src/infrastructure/utils/type-guards/validation-guards.util.ts +4 -1
- package/src/init/createAiProviderInitModule.ts +3 -1
- package/src/init/initializeFalProvider.ts +1 -1
- package/src/exports/domain.ts +0 -51
- package/src/exports/infrastructure.ts +0 -76
- package/src/exports/presentation.ts +0 -14
- package/src/infrastructure/services/index.ts +0 -13
- package/src/infrastructure/utils/index.ts +0 -55
- package/src/infrastructure/utils/parsers/index.ts +0 -6
- package/src/infrastructure/utils/parsers/json-parsers.util.ts +0 -73
- package/src/infrastructure/utils/parsers/object-transformers.util.ts +0 -53
- package/src/presentation/hooks/index.ts +0 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-fal-provider",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.40",
|
|
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",
|
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
"expo-clipboard": "^8.0.8",
|
|
69
69
|
"expo-crypto": "^15.0.8",
|
|
70
70
|
"expo-device": "^8.0.10",
|
|
71
|
+
"expo-document-picker": "~14.0.8",
|
|
71
72
|
"expo-file-system": "^19.0.21",
|
|
72
73
|
"expo-font": "^14.0.10",
|
|
73
74
|
"expo-haptics": "^15.0.8",
|
package/src/global.d.ts
ADDED
package/src/index.ts
CHANGED
|
@@ -1,22 +1,156 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @umituz/react-native-ai-fal-provider
|
|
3
|
-
* FAL AI provider for React Native -
|
|
3
|
+
* FAL AI provider for React Native - Public API exports
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
//
|
|
7
|
-
export
|
|
6
|
+
// ─── Core Provider ───────────────────────────────────────────────────────────
|
|
7
|
+
export { FalProvider, falProvider } from "./infrastructure/services/fal-provider";
|
|
8
|
+
export type { FalProvider as FalProviderType } from "./infrastructure/services/fal-provider";
|
|
9
|
+
export { NSFWContentError } from "./infrastructure/services/nsfw-content-error";
|
|
8
10
|
|
|
9
|
-
//
|
|
10
|
-
export
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
// ─── React Hook ───────────────────────────────────────────────────────────────
|
|
12
|
+
export { useFalGeneration } from "./presentation/hooks/use-fal-generation";
|
|
13
|
+
export type {
|
|
14
|
+
UseFalGenerationOptions,
|
|
15
|
+
UseFalGenerationResult,
|
|
16
|
+
} from "./presentation/hooks/use-fal-generation";
|
|
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";
|
|
14
24
|
|
|
15
|
-
//
|
|
25
|
+
// ─── Initialization ───────────────────────────────────────────────────────────
|
|
26
|
+
export { initializeFalProvider } from "./init/initializeFalProvider";
|
|
16
27
|
export {
|
|
17
28
|
createAiProviderInitModule,
|
|
18
29
|
type AiProviderInitModuleConfig,
|
|
19
|
-
} from
|
|
30
|
+
} from "./init/createAiProviderInitModule";
|
|
31
|
+
|
|
32
|
+
// ─── Error Handling ───────────────────────────────────────────────────────────
|
|
33
|
+
export {
|
|
34
|
+
mapFalError,
|
|
35
|
+
parseFalError,
|
|
36
|
+
isFalErrorRetryable,
|
|
37
|
+
} from "./infrastructure/utils/fal-error-handler.util";
|
|
38
|
+
export { FalErrorType } from "./domain/entities/error.types";
|
|
39
|
+
export type {
|
|
40
|
+
FalErrorCategory,
|
|
41
|
+
FalErrorInfo,
|
|
42
|
+
FalErrorMessages,
|
|
43
|
+
} from "./domain/entities/error.types";
|
|
20
44
|
|
|
21
|
-
//
|
|
22
|
-
export {
|
|
45
|
+
// ─── Utilities (public API) ────────────────────────────────────────────────────
|
|
46
|
+
export {
|
|
47
|
+
getErrorMessage,
|
|
48
|
+
getErrorMessageOr,
|
|
49
|
+
formatErrorMessage,
|
|
50
|
+
} from "./infrastructure/utils/helpers/error-helpers.util";
|
|
51
|
+
|
|
52
|
+
export {
|
|
53
|
+
IMAGE_URL_FIELDS,
|
|
54
|
+
isImageField,
|
|
55
|
+
} from "./infrastructure/utils/constants/image-fields.constants";
|
|
56
|
+
export type { ImageUrlField } from "./infrastructure/utils/constants/image-fields.constants";
|
|
57
|
+
|
|
58
|
+
export {
|
|
59
|
+
isDataUri,
|
|
60
|
+
isBase64DataUri,
|
|
61
|
+
extractMimeType,
|
|
62
|
+
extractBase64Content,
|
|
63
|
+
} from "./infrastructure/utils/validators/data-uri-validator.util";
|
|
64
|
+
|
|
65
|
+
export {
|
|
66
|
+
isEmptyString,
|
|
67
|
+
isNonEmptyString,
|
|
68
|
+
isString,
|
|
69
|
+
} from "./infrastructure/utils/validators/string-validator.util";
|
|
70
|
+
|
|
71
|
+
export {
|
|
72
|
+
isFalModelType,
|
|
73
|
+
isModelType,
|
|
74
|
+
isFalErrorType,
|
|
75
|
+
isValidBase64Image,
|
|
76
|
+
isValidApiKey,
|
|
77
|
+
isValidModelId,
|
|
78
|
+
isValidPrompt,
|
|
79
|
+
isValidTimeout,
|
|
80
|
+
isValidRetryCount,
|
|
81
|
+
} from "./infrastructure/utils/type-guards";
|
|
82
|
+
|
|
83
|
+
export {
|
|
84
|
+
formatImageDataUri,
|
|
85
|
+
extractBase64,
|
|
86
|
+
getDataUriExtension,
|
|
87
|
+
} from "./infrastructure/utils/image-helpers.util";
|
|
88
|
+
|
|
89
|
+
export {
|
|
90
|
+
uploadToFalStorage,
|
|
91
|
+
uploadMultipleToFalStorage,
|
|
92
|
+
} from "./infrastructure/utils/fal-storage.util";
|
|
93
|
+
|
|
94
|
+
export {
|
|
95
|
+
buildErrorMessage,
|
|
96
|
+
isDefined,
|
|
97
|
+
removeNullish,
|
|
98
|
+
generateUniqueId,
|
|
99
|
+
sleep,
|
|
100
|
+
} from "./infrastructure/utils/helpers";
|
|
101
|
+
|
|
102
|
+
export { preprocessInput } from "./infrastructure/utils/input-preprocessor.util";
|
|
103
|
+
|
|
104
|
+
export {
|
|
105
|
+
calculateVideoCredits,
|
|
106
|
+
calculateImageCredits,
|
|
107
|
+
calculateCreditsFromConfig,
|
|
108
|
+
} from "./infrastructure/utils/pricing/fal-pricing.util";
|
|
109
|
+
export type { GenerationResolution } from "./infrastructure/utils/pricing/fal-pricing.util";
|
|
110
|
+
|
|
111
|
+
// ─── Types (public API) ───────────────────────────────────────────────────────
|
|
112
|
+
export type {
|
|
113
|
+
FalConfig,
|
|
114
|
+
FalModel,
|
|
115
|
+
FalModelType,
|
|
116
|
+
FalModelPricing,
|
|
117
|
+
FalJobInput,
|
|
118
|
+
FalJobResult,
|
|
119
|
+
FalLogEntry,
|
|
120
|
+
FalQueueStatus,
|
|
121
|
+
FalSubscribeOptions,
|
|
122
|
+
} from "./domain/entities/fal.types";
|
|
123
|
+
export type { FalModelConfig } from "./domain/types/fal-model-config.types";
|
|
124
|
+
export type {
|
|
125
|
+
UpscaleOptions,
|
|
126
|
+
PhotoRestoreOptions,
|
|
127
|
+
FaceSwapOptions,
|
|
128
|
+
ImageToImagePromptConfig,
|
|
129
|
+
RemoveBackgroundOptions,
|
|
130
|
+
RemoveObjectOptions,
|
|
131
|
+
ReplaceBackgroundOptions,
|
|
132
|
+
VideoFromImageOptions,
|
|
133
|
+
TextToVideoOptions,
|
|
134
|
+
TextToVoiceOptions,
|
|
135
|
+
ImageFeatureType,
|
|
136
|
+
VideoFeatureType,
|
|
137
|
+
AIProviderConfig,
|
|
138
|
+
AIJobStatusType,
|
|
139
|
+
AILogEntry,
|
|
140
|
+
JobSubmission,
|
|
141
|
+
JobStatus,
|
|
142
|
+
ProviderProgressInfo,
|
|
143
|
+
SubscribeOptions,
|
|
144
|
+
RunOptions,
|
|
145
|
+
ProviderCapabilities,
|
|
146
|
+
ImageFeatureInputData,
|
|
147
|
+
VideoFeatureInputData,
|
|
148
|
+
IAIProvider,
|
|
149
|
+
} from "./domain/types";
|
|
150
|
+
|
|
151
|
+
// ─── Request Store Management ─────────────────────────────────────────────────
|
|
152
|
+
export {
|
|
153
|
+
cleanupRequestStore,
|
|
154
|
+
stopAutomaticCleanup,
|
|
155
|
+
} from "./infrastructure/services/request-store";
|
|
156
|
+
export type { ActiveRequest } from "./infrastructure/services/request-store";
|
|
@@ -90,14 +90,15 @@ function formatFalError(error: unknown): string {
|
|
|
90
90
|
if (error.status === 402) {
|
|
91
91
|
return "Insufficient credits. Please check your billing.";
|
|
92
92
|
}
|
|
93
|
-
return error.message
|
|
93
|
+
return error.message ?? `API error (${error.status})`;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
if (error instanceof Error) {
|
|
97
97
|
return error.message;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
// Handle null/undefined/other types safely
|
|
101
|
+
return error != null ? String(error) : "Unknown error";
|
|
101
102
|
}
|
|
102
103
|
|
|
103
104
|
// ─── Retry Logic ────────────────────────────────────────────────────────────
|
|
@@ -113,8 +114,8 @@ function isRetryableSubscribeError(error: unknown): boolean {
|
|
|
113
114
|
// Never retry NSFW
|
|
114
115
|
if (error instanceof NSFWContentError) return false;
|
|
115
116
|
|
|
116
|
-
// Never retry user cancellation
|
|
117
|
-
if (error instanceof Error && error.message
|
|
117
|
+
// Never retry user cancellation - check Error instance first
|
|
118
|
+
if (error instanceof Error && error.message?.includes("cancelled by user")) return false;
|
|
118
119
|
|
|
119
120
|
// ApiError — check status code
|
|
120
121
|
if (error instanceof ApiError) {
|
|
@@ -200,15 +201,16 @@ async function singleSubscribeAttempt<T = unknown>(
|
|
|
200
201
|
];
|
|
201
202
|
|
|
202
203
|
if (signal) {
|
|
204
|
+
// Check for aborted state BEFORE adding listener to prevent race condition
|
|
205
|
+
if (signal.aborted) {
|
|
206
|
+
throw new Error("Request cancelled by user");
|
|
207
|
+
}
|
|
203
208
|
const abortPromise = new Promise<never>((_, reject) => {
|
|
204
209
|
abortHandler = () => reject(new Error("Request cancelled by user"));
|
|
205
210
|
signal.addEventListener("abort", abortHandler, { once: true });
|
|
206
211
|
listenerAdded = true;
|
|
207
212
|
});
|
|
208
213
|
promises.push(abortPromise);
|
|
209
|
-
if (signal.aborted) {
|
|
210
|
-
throw new Error("Request cancelled by user");
|
|
211
|
-
}
|
|
212
214
|
}
|
|
213
215
|
|
|
214
216
|
const rawResult = await Promise.race(promises);
|
|
@@ -11,7 +11,7 @@ import type {
|
|
|
11
11
|
} from "../../domain/types";
|
|
12
12
|
import { DEFAULT_FAL_CONFIG, FAL_CAPABILITIES } from "./fal-provider.constants";
|
|
13
13
|
import { handleFalSubscription, handleFalRun } from "./fal-provider-subscription";
|
|
14
|
-
import { preprocessInput } from "../utils";
|
|
14
|
+
import { preprocessInput } from "../utils/input-preprocessor.util";
|
|
15
15
|
import { getErrorMessage } from "../utils/helpers/error-helpers.util";
|
|
16
16
|
import { generationLogCollector } from "../utils/log-collector";
|
|
17
17
|
import type { LogEntry } from "../utils/log-collector";
|
|
@@ -56,7 +56,15 @@ function sortKeys(obj: unknown): unknown {
|
|
|
56
56
|
* 64-bit combined hash space makes accidental collisions extremely unlikely.
|
|
57
57
|
*/
|
|
58
58
|
export function createRequestKey(model: string, input: Record<string, unknown>): string {
|
|
59
|
-
|
|
59
|
+
let inputStr: string;
|
|
60
|
+
try {
|
|
61
|
+
inputStr = JSON.stringify(sortKeys(input));
|
|
62
|
+
} catch (error) {
|
|
63
|
+
// Handle circular references or non-serializable values
|
|
64
|
+
// Fallback to a simple string representation that won't throw
|
|
65
|
+
const simpleKeys = Object.keys(input).sort().join(",");
|
|
66
|
+
inputStr = `${simpleKeys}:${Object.keys(input).length}`;
|
|
67
|
+
}
|
|
60
68
|
|
|
61
69
|
// FNV-1a hash
|
|
62
70
|
let h1 = 0x811c9dc5;
|
|
@@ -85,7 +93,7 @@ export function getExistingRequest<T>(key: string): ActiveRequest<T> | undefined
|
|
|
85
93
|
export function storeRequest<T>(key: string, request: ActiveRequest<T>): void {
|
|
86
94
|
getRequestStore().set(key, {
|
|
87
95
|
...request,
|
|
88
|
-
createdAt: request.createdAt
|
|
96
|
+
createdAt: request.createdAt, // createdAt is required, no fallback needed
|
|
89
97
|
});
|
|
90
98
|
ensureCleanupRunning();
|
|
91
99
|
}
|
|
@@ -61,7 +61,7 @@ function categorizeError(error: unknown): FalErrorCategory {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
// 2. Standard Error - match message patterns
|
|
64
|
-
const message = (error instanceof Error ? error.message : String(error)).toLowerCase();
|
|
64
|
+
const message = (error instanceof Error ? error.message : error != null ? String(error) : "unknown").toLowerCase();
|
|
65
65
|
|
|
66
66
|
for (const { type, patterns } of MESSAGE_PATTERNS) {
|
|
67
67
|
if (patterns.some((p) => message.includes(p))) {
|
|
@@ -81,8 +81,11 @@ function extractMessage(error: unknown): string {
|
|
|
81
81
|
// ValidationError - extract field-level messages
|
|
82
82
|
if (error instanceof ValidationError) {
|
|
83
83
|
const fieldErrors = error.fieldErrors;
|
|
84
|
-
if (fieldErrors.length > 0) {
|
|
85
|
-
|
|
84
|
+
if (Array.isArray(fieldErrors) && fieldErrors.length > 0) {
|
|
85
|
+
// Safely extract messages from field errors with validation
|
|
86
|
+
const messages = fieldErrors
|
|
87
|
+
.map((e) => (e && typeof e === 'object' && 'msg' in e && typeof e.msg === 'string' ? e.msg : null))
|
|
88
|
+
.filter((msg): msg is string => msg !== null);
|
|
86
89
|
if (messages.length > 0) return messages.join("; ");
|
|
87
90
|
}
|
|
88
91
|
return error.message;
|
|
@@ -90,10 +93,35 @@ function extractMessage(error: unknown): string {
|
|
|
90
93
|
|
|
91
94
|
// ApiError - extract from body or message
|
|
92
95
|
if (error instanceof ApiError) {
|
|
93
|
-
// body may contain detail array
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
96
|
+
// body may contain detail array - validate structure before access
|
|
97
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
98
|
+
const body = error.body;
|
|
99
|
+
|
|
100
|
+
// Type guard for detail array structure
|
|
101
|
+
interface DetailItem {
|
|
102
|
+
msg?: string;
|
|
103
|
+
}
|
|
104
|
+
interface BodyWithDetail {
|
|
105
|
+
detail?: DetailItem[];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const validatedBody = body as BodyWithDetail | undefined;
|
|
109
|
+
|
|
110
|
+
if (
|
|
111
|
+
validatedBody &&
|
|
112
|
+
validatedBody.detail &&
|
|
113
|
+
Array.isArray(validatedBody.detail) &&
|
|
114
|
+
validatedBody.detail.length > 0
|
|
115
|
+
) {
|
|
116
|
+
const firstItem = validatedBody.detail[0];
|
|
117
|
+
if (
|
|
118
|
+
firstItem &&
|
|
119
|
+
typeof firstItem === "object" &&
|
|
120
|
+
typeof firstItem.msg === "string" &&
|
|
121
|
+
firstItem.msg
|
|
122
|
+
) {
|
|
123
|
+
return firstItem.msg;
|
|
124
|
+
}
|
|
97
125
|
}
|
|
98
126
|
return error.message;
|
|
99
127
|
}
|
|
@@ -103,7 +131,7 @@ function extractMessage(error: unknown): string {
|
|
|
103
131
|
return error.message;
|
|
104
132
|
}
|
|
105
133
|
|
|
106
|
-
return String(error);
|
|
134
|
+
return error != null ? String(error) : "Unknown error";
|
|
107
135
|
}
|
|
108
136
|
|
|
109
137
|
/**
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
deleteTempFile,
|
|
11
11
|
} from "@umituz/react-native-design-system/filesystem";
|
|
12
12
|
import { getErrorMessage } from './helpers/error-helpers.util';
|
|
13
|
+
import { getElapsedTime, getActualSizeKB } from './helpers';
|
|
13
14
|
import { generationLogCollector } from './log-collector';
|
|
14
15
|
import { UPLOAD_CONFIG } from '../services/fal-provider.constants';
|
|
15
16
|
|
|
@@ -72,13 +73,14 @@ async function withRetry<T>(
|
|
|
72
73
|
*/
|
|
73
74
|
export async function uploadToFalStorage(base64: string, sessionId: string): Promise<string> {
|
|
74
75
|
const startTime = Date.now();
|
|
75
|
-
const
|
|
76
|
-
const actualSizeKB = Math.round(sizeKB * 0.75); // base64 inflates ~33%
|
|
76
|
+
const actualSizeKB = getActualSizeKB(base64);
|
|
77
77
|
generationLogCollector.log(sessionId, TAG, `Starting upload (~${actualSizeKB}KB actual)`);
|
|
78
78
|
|
|
79
79
|
const tempUri = await base64ToTempFile(base64);
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
// base64ToTempFile returns a string, so this check is for defensive programming
|
|
82
|
+
// in case the implementation changes in the future
|
|
83
|
+
if (!tempUri || typeof tempUri !== 'string') {
|
|
82
84
|
throw new Error("Failed to create temporary file from base64 data");
|
|
83
85
|
}
|
|
84
86
|
|
|
@@ -86,6 +88,12 @@ export async function uploadToFalStorage(base64: string, sessionId: string): Pro
|
|
|
86
88
|
const url = await withRetry(
|
|
87
89
|
async () => {
|
|
88
90
|
const response = await fetch(tempUri);
|
|
91
|
+
|
|
92
|
+
// Validate response before processing blob
|
|
93
|
+
if (!response.ok) {
|
|
94
|
+
throw new Error(`Failed to fetch temp file: HTTP ${response.status} ${response.statusText}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
89
97
|
const blob = await response.blob();
|
|
90
98
|
generationLogCollector.log(sessionId, TAG, `Blob created (${blob.size} bytes), uploading to FAL CDN...`);
|
|
91
99
|
return withTimeout(
|
|
@@ -98,11 +106,11 @@ export async function uploadToFalStorage(base64: string, sessionId: string): Pro
|
|
|
98
106
|
'upload',
|
|
99
107
|
);
|
|
100
108
|
|
|
101
|
-
const elapsed =
|
|
109
|
+
const elapsed = getElapsedTime(startTime);
|
|
102
110
|
generationLogCollector.log(sessionId, TAG, `Upload complete in ${elapsed}ms`, { url, actualSizeKB, elapsed });
|
|
103
111
|
return url;
|
|
104
112
|
} catch (error) {
|
|
105
|
-
const elapsed =
|
|
113
|
+
const elapsed = getElapsedTime(startTime);
|
|
106
114
|
generationLogCollector.error(sessionId, TAG, `Upload FAILED after ${elapsed}ms: ${getErrorMessage(error)}`, { actualSizeKB, elapsed });
|
|
107
115
|
throw error;
|
|
108
116
|
} finally {
|
|
@@ -125,6 +133,12 @@ export async function uploadLocalFileToFalStorage(fileUri: string, sessionId: st
|
|
|
125
133
|
const url = await withRetry(
|
|
126
134
|
async () => {
|
|
127
135
|
const response = await fetch(fileUri);
|
|
136
|
+
|
|
137
|
+
// Validate response before processing blob
|
|
138
|
+
if (!response.ok) {
|
|
139
|
+
throw new Error(`Failed to fetch local file: HTTP ${response.status} ${response.statusText}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
128
142
|
const blob = await response.blob();
|
|
129
143
|
generationLogCollector.log(sessionId, TAG, `Local file blob (${blob.size} bytes), uploading...`);
|
|
130
144
|
return withTimeout(
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calculation Helper Utilities
|
|
3
|
+
* Centralized calculation functions for consistent metrics across the codebase
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Calculate elapsed time in milliseconds from a start timestamp
|
|
8
|
+
*
|
|
9
|
+
* @param startTime - Start timestamp in milliseconds (from Date.now())
|
|
10
|
+
* @returns Elapsed time in milliseconds
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* const start = Date.now();
|
|
15
|
+
* // ... do work ...
|
|
16
|
+
* const elapsed = getElapsedTime(start); // e.g., 1234
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export function getElapsedTime(startTime: number): number {
|
|
20
|
+
return Date.now() - startTime;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Format elapsed time in milliseconds to human-readable string
|
|
25
|
+
*
|
|
26
|
+
* @param elapsedMs - Elapsed time in milliseconds
|
|
27
|
+
* @returns Formatted string (e.g., "1234ms", "1.2s")
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```typescript
|
|
31
|
+
* formatDuration(1234); // "1234ms"
|
|
32
|
+
* formatDuration(1200); // "1.2s"
|
|
33
|
+
* formatDuration(1500); // "1.5s"
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export function formatDuration(elapsedMs: number): string {
|
|
37
|
+
if (elapsedMs < 1000) {
|
|
38
|
+
return `${Math.round(elapsedMs)}ms`;
|
|
39
|
+
}
|
|
40
|
+
return `${(elapsedMs / 1000).toFixed(1)}s`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Convert bytes to kilobytes (KB)
|
|
45
|
+
*
|
|
46
|
+
* @param bytes - Size in bytes
|
|
47
|
+
* @returns Size in kilobytes (rounded)
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```typescript
|
|
51
|
+
* bytesToKB(5000); // 5
|
|
52
|
+
* bytesToKB(1536); // 2
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export function bytesToKB(bytes: number): number {
|
|
56
|
+
return Math.round(bytes / 1024);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Calculate actual file size from base64 string
|
|
61
|
+
* Base64 encoding inflates size by ~33%, so actual size is ~75% of base64 length
|
|
62
|
+
*
|
|
63
|
+
* @param base64String - Base64 encoded string
|
|
64
|
+
* @returns Actual size in kilobytes (rounded)
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```typescript
|
|
68
|
+
* getActualSizeKB("data:image/png;base64,iVBOR..."); // ~3KB for 4KB base64
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
export function getActualSizeKB(base64String: string): number {
|
|
72
|
+
const base64SizeKB = Math.round(base64String.length / 1024);
|
|
73
|
+
// Base64 inflates by ~33%, so actual size is ~75%
|
|
74
|
+
return Math.round(base64SizeKB * 0.75);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Calculate base64 size in KB (before inflation adjustment)
|
|
79
|
+
*
|
|
80
|
+
* @param base64String - Base64 encoded string
|
|
81
|
+
* @returns Size in kilobytes (rounded)
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```typescript
|
|
85
|
+
* getBase64SizeKB("data:image/png;base64,iVBOR..."); // 4
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export function getBase64SizeKB(base64String: string): number {
|
|
89
|
+
return Math.round(base64String.length / 1024);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Calculate success rate as percentage
|
|
94
|
+
*
|
|
95
|
+
* @param successCount - Number of successful operations
|
|
96
|
+
* @param totalCount - Total number of operations
|
|
97
|
+
* @returns Success rate as percentage (0-100)
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```typescript
|
|
101
|
+
* getSuccessRate(8, 10); // 80
|
|
102
|
+
* getSuccessRate(5, 5); // 100
|
|
103
|
+
* getSuccessRate(0, 10); // 0
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
106
|
+
export function getSuccessRate(successCount: number, totalCount: number): number {
|
|
107
|
+
if (totalCount === 0) return 0;
|
|
108
|
+
return Math.round((successCount / totalCount) * 100);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Format bytes to human-readable size
|
|
113
|
+
*
|
|
114
|
+
* @param bytes - Size in bytes
|
|
115
|
+
* @returns Formatted string (e.g., "1.5KB", "2.3MB")
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* ```typescript
|
|
119
|
+
* formatBytes(1536); // "1.5KB"
|
|
120
|
+
* formatBytes(2048000); // "2.0MB"
|
|
121
|
+
* formatBytes(500); // "500B"
|
|
122
|
+
* ```
|
|
123
|
+
*/
|
|
124
|
+
export function formatBytes(bytes: number): string {
|
|
125
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
126
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
127
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Calculate retry count suffix for logging
|
|
132
|
+
*
|
|
133
|
+
* @param attempt - Current attempt number (0-indexed)
|
|
134
|
+
* @param totalAttempts - Total number of attempts
|
|
135
|
+
* @returns Formatted suffix string
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* ```typescript
|
|
139
|
+
* getRetrySuffix(1, 2); // " (succeeded on retry 1)"
|
|
140
|
+
* getRetrySuffix(0, 1); // ""
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
143
|
+
export function getRetrySuffix(attempt: number, _totalAttempts: number): string {
|
|
144
|
+
if (attempt > 0) {
|
|
145
|
+
return ` (succeeded on retry ${attempt})`;
|
|
146
|
+
}
|
|
147
|
+
return "";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Calculate failure info string for logging
|
|
152
|
+
*
|
|
153
|
+
* @param attempt - Current attempt number (0-indexed)
|
|
154
|
+
* @param totalAttempts - Total number of attempts
|
|
155
|
+
* @returns Formatted failure info string
|
|
156
|
+
*
|
|
157
|
+
* @example
|
|
158
|
+
* ```typescript
|
|
159
|
+
* getFailureInfo(1, 2); // " after 2 attempts"
|
|
160
|
+
* getFailureInfo(0, 1); // ""
|
|
161
|
+
* ```
|
|
162
|
+
*/
|
|
163
|
+
export function getFailureInfo(attempt: number, _totalAttempts: number): string {
|
|
164
|
+
if (attempt > 0) {
|
|
165
|
+
return ` after ${attempt + 1} attempts`;
|
|
166
|
+
}
|
|
167
|
+
return "";
|
|
168
|
+
}
|
|
@@ -63,24 +63,3 @@ export function formatErrorMessage(error: unknown, context: string): string {
|
|
|
63
63
|
return `${context}: ${getErrorMessage(error)}`;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
/**
|
|
67
|
-
* Extract error name (for Error instances)
|
|
68
|
-
* Returns undefined for non-Error types
|
|
69
|
-
*
|
|
70
|
-
* @param error - The error to extract name from
|
|
71
|
-
* @returns The error name or undefined
|
|
72
|
-
*/
|
|
73
|
-
export function getErrorName(error: unknown): string | undefined {
|
|
74
|
-
return error instanceof Error ? error.name : undefined;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Extract error stack trace
|
|
79
|
-
* Returns undefined for non-Error types
|
|
80
|
-
*
|
|
81
|
-
* @param error - The error to extract stack from
|
|
82
|
-
* @returns The error stack trace or undefined
|
|
83
|
-
*/
|
|
84
|
-
export function getErrorStack(error: unknown): string | undefined {
|
|
85
|
-
return error instanceof Error ? error.stack : undefined;
|
|
86
|
-
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Helper Utilities - Centralized Exports
|
|
3
|
-
* Re-exports all helper utilities from submodules
|
|
4
3
|
*/
|
|
5
4
|
|
|
6
5
|
export * from './timing-helpers.util';
|
|
7
6
|
export * from './object-helpers.util';
|
|
7
|
+
export * from './calculation-helpers.util';
|
|
8
|
+
export * from './error-helpers.util';
|
|
@@ -173,9 +173,7 @@ export async function preprocessInput(
|
|
|
173
173
|
|
|
174
174
|
if (failedUploads.length > 0) {
|
|
175
175
|
const successCount = individualUploadResults.length - failedUploads.length;
|
|
176
|
-
const errorMessages = failedUploads.map((r) =>
|
|
177
|
-
r.status === 'rejected' ? getErrorMessage(r.reason) : 'Unknown error'
|
|
178
|
-
);
|
|
176
|
+
const errorMessages = failedUploads.map((r) => getErrorMessage(r.reason));
|
|
179
177
|
generationLogCollector.error(sessionId, TAG, `Individual uploads: ${successCount}/${individualUploadResults.length} succeeded`, { errors: errorMessages });
|
|
180
178
|
throw new Error(classifyUploadError(errorMessages[0]));
|
|
181
179
|
}
|
|
@@ -47,7 +47,25 @@ export function calculateCreditsFromConfig(
|
|
|
47
47
|
duration: number,
|
|
48
48
|
resolution: string,
|
|
49
49
|
): number {
|
|
50
|
-
|
|
50
|
+
// Validate config structure before accessing nested properties
|
|
51
|
+
if (
|
|
52
|
+
!config ||
|
|
53
|
+
typeof config !== "object" ||
|
|
54
|
+
!config.pricing ||
|
|
55
|
+
typeof config.pricing !== "object" ||
|
|
56
|
+
!config.pricing.costPerSecond ||
|
|
57
|
+
typeof config.pricing.costPerSecond !== "object"
|
|
58
|
+
) {
|
|
59
|
+
throw new Error("Invalid VideoModelConfig: pricing structure is missing or invalid");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const costPerSecondMap = config.pricing.costPerSecond;
|
|
63
|
+
const costPerSec = costPerSecondMap[resolution] ?? 0;
|
|
64
|
+
|
|
65
|
+
if (typeof costPerSec !== "number" || costPerSec < 0) {
|
|
66
|
+
throw new Error(`Invalid cost per second for resolution "${resolution}": must be a non-negative number`);
|
|
67
|
+
}
|
|
68
|
+
|
|
51
69
|
const cost = costPerSec * duration;
|
|
52
70
|
return Math.max(1, Math.ceil((cost * MARKUP) / CREDIT_PRICE));
|
|
53
71
|
}
|
|
@@ -22,7 +22,10 @@ export function isValidBase64Image(value: unknown): boolean {
|
|
|
22
22
|
|
|
23
23
|
// Check data URI prefix - use direct check instead of type guard to avoid type narrowing issues
|
|
24
24
|
if (value.startsWith("data:image/")) {
|
|
25
|
-
const
|
|
25
|
+
const parts = value.split("base64,");
|
|
26
|
+
// Ensure split produced at least 2 parts and the second part exists
|
|
27
|
+
if (parts.length < 2) return false;
|
|
28
|
+
const base64Part = parts[1];
|
|
26
29
|
if (!base64Part) return false;
|
|
27
30
|
return base64Part.length >= MIN_BASE64_IMAGE_LENGTH;
|
|
28
31
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { providerRegistry } from '@umituz/react-native-ai-generation-content';
|
|
7
|
-
import { falProvider } from '../infrastructure/services';
|
|
7
|
+
import { falProvider } from '../infrastructure/services/fal-provider';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* InitModule interface (from @umituz/react-native-design-system)
|
|
@@ -79,6 +79,8 @@ export function createAiProviderInitModule(
|
|
|
79
79
|
|
|
80
80
|
return Promise.resolve(true);
|
|
81
81
|
} catch (error) {
|
|
82
|
+
// Use console.error for init module failures (logging may not be initialized yet)
|
|
83
|
+
// This is a critical startup error, so we want it visible
|
|
82
84
|
console.error('[AiProviderInitModule] Initialization failed:', error);
|
|
83
85
|
throw error;
|
|
84
86
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { providerRegistry } from '@umituz/react-native-ai-generation-content';
|
|
7
|
-
import { falProvider } from '../infrastructure/services';
|
|
7
|
+
import { falProvider } from '../infrastructure/services/fal-provider';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Initializes FAL provider and registers it with providerRegistry in one call.
|
package/src/exports/domain.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Domain Layer Exports
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
export type {
|
|
6
|
-
FalConfig,
|
|
7
|
-
FalModel,
|
|
8
|
-
FalModelType,
|
|
9
|
-
FalModelPricing,
|
|
10
|
-
FalJobInput,
|
|
11
|
-
FalJobResult,
|
|
12
|
-
FalLogEntry,
|
|
13
|
-
FalQueueStatus,
|
|
14
|
-
FalSubscribeOptions,
|
|
15
|
-
} from "../domain/entities/fal.types";
|
|
16
|
-
|
|
17
|
-
export { FalErrorType } from "../domain/entities/error.types";
|
|
18
|
-
export type {
|
|
19
|
-
FalErrorCategory,
|
|
20
|
-
FalErrorInfo,
|
|
21
|
-
FalErrorMessages,
|
|
22
|
-
} from "../domain/entities/error.types";
|
|
23
|
-
|
|
24
|
-
export type { FalModelConfig } from "../domain/types/fal-model-config.types";
|
|
25
|
-
|
|
26
|
-
export type {
|
|
27
|
-
UpscaleOptions,
|
|
28
|
-
PhotoRestoreOptions,
|
|
29
|
-
FaceSwapOptions,
|
|
30
|
-
ImageToImagePromptConfig,
|
|
31
|
-
RemoveBackgroundOptions,
|
|
32
|
-
RemoveObjectOptions,
|
|
33
|
-
ReplaceBackgroundOptions,
|
|
34
|
-
VideoFromImageOptions,
|
|
35
|
-
TextToVideoOptions,
|
|
36
|
-
TextToVoiceOptions,
|
|
37
|
-
ImageFeatureType,
|
|
38
|
-
VideoFeatureType,
|
|
39
|
-
AIProviderConfig,
|
|
40
|
-
AIJobStatusType,
|
|
41
|
-
AILogEntry,
|
|
42
|
-
JobSubmission,
|
|
43
|
-
JobStatus,
|
|
44
|
-
ProviderProgressInfo,
|
|
45
|
-
SubscribeOptions,
|
|
46
|
-
RunOptions,
|
|
47
|
-
ProviderCapabilities,
|
|
48
|
-
ImageFeatureInputData,
|
|
49
|
-
VideoFeatureInputData,
|
|
50
|
-
IAIProvider,
|
|
51
|
-
} from "../domain/types";
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Infrastructure Layer Exports
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
export {
|
|
6
|
-
FalProvider,
|
|
7
|
-
falProvider,
|
|
8
|
-
NSFWContentError,
|
|
9
|
-
cleanupRequestStore,
|
|
10
|
-
stopAutomaticCleanup,
|
|
11
|
-
} from "../infrastructure/services";
|
|
12
|
-
export type { FalProviderType, ActiveRequest } from "../infrastructure/services";
|
|
13
|
-
|
|
14
|
-
export {
|
|
15
|
-
mapFalError,
|
|
16
|
-
parseFalError,
|
|
17
|
-
isFalErrorRetryable,
|
|
18
|
-
} from "../infrastructure/utils";
|
|
19
|
-
|
|
20
|
-
export {
|
|
21
|
-
getErrorMessage,
|
|
22
|
-
getErrorMessageOr,
|
|
23
|
-
formatErrorMessage,
|
|
24
|
-
} from "../infrastructure/utils/helpers/error-helpers.util";
|
|
25
|
-
|
|
26
|
-
export {
|
|
27
|
-
IMAGE_URL_FIELDS,
|
|
28
|
-
isImageField,
|
|
29
|
-
} from "../infrastructure/utils/constants/image-fields.constants";
|
|
30
|
-
export type {
|
|
31
|
-
ImageUrlField,
|
|
32
|
-
} from "../infrastructure/utils/constants/image-fields.constants";
|
|
33
|
-
|
|
34
|
-
export {
|
|
35
|
-
isDataUri,
|
|
36
|
-
isBase64DataUri,
|
|
37
|
-
extractMimeType,
|
|
38
|
-
extractBase64Content,
|
|
39
|
-
} from "../infrastructure/utils/validators/data-uri-validator.util";
|
|
40
|
-
export {
|
|
41
|
-
isEmptyString,
|
|
42
|
-
isNonEmptyString,
|
|
43
|
-
isString,
|
|
44
|
-
} from "../infrastructure/utils/validators/string-validator.util";
|
|
45
|
-
|
|
46
|
-
export {
|
|
47
|
-
isFalModelType,
|
|
48
|
-
isModelType,
|
|
49
|
-
isFalErrorType,
|
|
50
|
-
isValidBase64Image,
|
|
51
|
-
isValidApiKey,
|
|
52
|
-
isValidModelId,
|
|
53
|
-
isValidPrompt,
|
|
54
|
-
isValidTimeout,
|
|
55
|
-
isValidRetryCount,
|
|
56
|
-
} from "../infrastructure/utils";
|
|
57
|
-
|
|
58
|
-
export {
|
|
59
|
-
formatImageDataUri,
|
|
60
|
-
extractBase64,
|
|
61
|
-
getDataUriExtension,
|
|
62
|
-
uploadToFalStorage,
|
|
63
|
-
uploadMultipleToFalStorage,
|
|
64
|
-
buildErrorMessage,
|
|
65
|
-
isDefined,
|
|
66
|
-
removeNullish,
|
|
67
|
-
generateUniqueId,
|
|
68
|
-
sleep,
|
|
69
|
-
} from "../infrastructure/utils";
|
|
70
|
-
|
|
71
|
-
export {
|
|
72
|
-
calculateVideoCredits,
|
|
73
|
-
calculateImageCredits,
|
|
74
|
-
calculateCreditsFromConfig,
|
|
75
|
-
} from "../infrastructure/utils";
|
|
76
|
-
export type { GenerationResolution } from "../infrastructure/utils";
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Presentation Layer Exports
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
export { useFalGeneration } from "../presentation/hooks";
|
|
6
|
-
export type { UseFalGenerationOptions, UseFalGenerationResult } from "../presentation/hooks";
|
|
7
|
-
|
|
8
|
-
export {
|
|
9
|
-
FalGenerationStateManager,
|
|
10
|
-
} from "../infrastructure/utils/fal-generation-state-manager.util";
|
|
11
|
-
export type {
|
|
12
|
-
GenerationState,
|
|
13
|
-
GenerationStateOptions,
|
|
14
|
-
} from "../infrastructure/utils/fal-generation-state-manager.util";
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Services Index
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
export { FalProvider, falProvider } from "./fal-provider";
|
|
6
|
-
export type { FalProvider as FalProviderType } from "./fal-provider";
|
|
7
|
-
export { NSFWContentError } from "./nsfw-content-error";
|
|
8
|
-
|
|
9
|
-
export {
|
|
10
|
-
cleanupRequestStore,
|
|
11
|
-
stopAutomaticCleanup,
|
|
12
|
-
} from "./request-store";
|
|
13
|
-
export type { ActiveRequest } from "./request-store";
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Utils Index
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
export {
|
|
6
|
-
mapFalError,
|
|
7
|
-
parseFalError,
|
|
8
|
-
isFalErrorRetryable,
|
|
9
|
-
} from "./fal-error-handler.util";
|
|
10
|
-
|
|
11
|
-
export {
|
|
12
|
-
isFalModelType,
|
|
13
|
-
isModelType,
|
|
14
|
-
isFalErrorType,
|
|
15
|
-
isValidBase64Image,
|
|
16
|
-
isValidApiKey,
|
|
17
|
-
isValidModelId,
|
|
18
|
-
isValidPrompt,
|
|
19
|
-
isValidTimeout,
|
|
20
|
-
isValidRetryCount,
|
|
21
|
-
} from "./type-guards";
|
|
22
|
-
|
|
23
|
-
export {
|
|
24
|
-
formatImageDataUri,
|
|
25
|
-
extractBase64,
|
|
26
|
-
getDataUriExtension,
|
|
27
|
-
} from "./image-helpers.util";
|
|
28
|
-
|
|
29
|
-
export {
|
|
30
|
-
uploadToFalStorage,
|
|
31
|
-
uploadMultipleToFalStorage,
|
|
32
|
-
} from "./fal-storage.util";
|
|
33
|
-
|
|
34
|
-
export {
|
|
35
|
-
buildErrorMessage,
|
|
36
|
-
isDefined,
|
|
37
|
-
removeNullish,
|
|
38
|
-
generateUniqueId,
|
|
39
|
-
sleep,
|
|
40
|
-
} from "./helpers";
|
|
41
|
-
|
|
42
|
-
export { preprocessInput } from "./input-preprocessor.util";
|
|
43
|
-
|
|
44
|
-
export { generationLogCollector } from "./log-collector";
|
|
45
|
-
export type { LogEntry } from "./log-collector";
|
|
46
|
-
|
|
47
|
-
export { FalGenerationStateManager } from "./fal-generation-state-manager.util";
|
|
48
|
-
export type { GenerationState } from "./fal-generation-state-manager.util";
|
|
49
|
-
|
|
50
|
-
export {
|
|
51
|
-
calculateVideoCredits,
|
|
52
|
-
calculateImageCredits,
|
|
53
|
-
calculateCreditsFromConfig,
|
|
54
|
-
} from "./pricing/fal-pricing.util";
|
|
55
|
-
export type { GenerationResolution } from "./pricing/fal-pricing.util";
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* JSON Parser Utilities
|
|
3
|
-
* Safe JSON parsing and validation operations
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { getErrorMessage } from '../helpers/error-helpers.util';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Safely parse JSON with fallback
|
|
10
|
-
*/
|
|
11
|
-
export function safeJsonParse<T = unknown>(
|
|
12
|
-
data: string,
|
|
13
|
-
fallback: T
|
|
14
|
-
): T {
|
|
15
|
-
try {
|
|
16
|
-
return JSON.parse(data) as T;
|
|
17
|
-
} catch (error) {
|
|
18
|
-
console.warn(
|
|
19
|
-
'[json-parsers] Failed to parse JSON, using fallback:',
|
|
20
|
-
getErrorMessage(error),
|
|
21
|
-
{ dataPreview: data.substring(0, 100) }
|
|
22
|
-
);
|
|
23
|
-
return fallback;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Safely parse JSON with null fallback
|
|
29
|
-
*/
|
|
30
|
-
export function safeJsonParseOrNull<T = unknown>(data: string): T | null {
|
|
31
|
-
try {
|
|
32
|
-
return JSON.parse(data) as T;
|
|
33
|
-
} catch (error) {
|
|
34
|
-
console.warn(
|
|
35
|
-
'[json-parsers] Failed to parse JSON, returning null:',
|
|
36
|
-
getErrorMessage(error),
|
|
37
|
-
{ dataPreview: data.substring(0, 100) }
|
|
38
|
-
);
|
|
39
|
-
return null;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Safely stringify object with fallback
|
|
45
|
-
*/
|
|
46
|
-
export function safeJsonStringify(
|
|
47
|
-
data: unknown,
|
|
48
|
-
fallback: string
|
|
49
|
-
): string {
|
|
50
|
-
try {
|
|
51
|
-
return JSON.stringify(data);
|
|
52
|
-
} catch (error) {
|
|
53
|
-
console.warn(
|
|
54
|
-
'[json-parsers] Failed to stringify object, using fallback:',
|
|
55
|
-
getErrorMessage(error),
|
|
56
|
-
{ dataType: typeof data }
|
|
57
|
-
);
|
|
58
|
-
return fallback;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Check if string is valid JSON
|
|
64
|
-
*/
|
|
65
|
-
export function isValidJson(data: string): boolean {
|
|
66
|
-
try {
|
|
67
|
-
JSON.parse(data);
|
|
68
|
-
return true;
|
|
69
|
-
} catch {
|
|
70
|
-
// Don't log here - this is expected to fail for validation checks
|
|
71
|
-
return false;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Object Transformer Utilities
|
|
3
|
-
* Clone, merge, pick, and omit operations
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Deep clone object using JSON serialization
|
|
8
|
-
* Throws on failure (circular references, non-serializable values)
|
|
9
|
-
* No silent fallback - caller must handle errors explicitly
|
|
10
|
-
*/
|
|
11
|
-
export function deepClone<T>(data: T): T {
|
|
12
|
-
const serialized = JSON.stringify(data);
|
|
13
|
-
return JSON.parse(serialized) as T;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Merge objects with later objects overriding earlier ones
|
|
18
|
-
*/
|
|
19
|
-
export function mergeObjects<T extends Record<string, unknown>>(
|
|
20
|
-
...objects: Partial<T>[]
|
|
21
|
-
): T {
|
|
22
|
-
return Object.assign({}, ...objects) as T;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Pick specified properties from object
|
|
27
|
-
*/
|
|
28
|
-
export function pickProperties<T extends Record<string, unknown>, K extends keyof T>(
|
|
29
|
-
obj: T,
|
|
30
|
-
keys: readonly K[]
|
|
31
|
-
): Pick<T, K> {
|
|
32
|
-
const result = {} as Pick<T, K>;
|
|
33
|
-
for (const key of keys) {
|
|
34
|
-
if (key in obj) {
|
|
35
|
-
result[key] = obj[key];
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
return result;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Omit specified properties from object
|
|
43
|
-
*/
|
|
44
|
-
export function omitProperties<T extends Record<string, unknown>, K extends keyof T>(
|
|
45
|
-
obj: T,
|
|
46
|
-
keys: readonly K[]
|
|
47
|
-
): Omit<T, K> {
|
|
48
|
-
const result = { ...obj };
|
|
49
|
-
for (const key of keys) {
|
|
50
|
-
delete result[key];
|
|
51
|
-
}
|
|
52
|
-
return result as Omit<T, K>;
|
|
53
|
-
}
|