@umituz/react-native-ai-fal-provider 2.1.3 → 2.1.5

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.
Files changed (33) hide show
  1. package/package.json +1 -1
  2. package/src/domain/constants/default-models.constants.ts +13 -4
  3. package/src/domain/types/model-selection.types.ts +9 -5
  4. package/src/exports/infrastructure.ts +2 -9
  5. package/src/infrastructure/services/fal-models.service.ts +8 -7
  6. package/src/infrastructure/services/fal-provider.ts +9 -8
  7. package/src/infrastructure/services/fal-queue-operations.ts +8 -1
  8. package/src/infrastructure/services/nsfw-content-error.ts +1 -1
  9. package/src/infrastructure/services/request-store.ts +78 -22
  10. package/src/infrastructure/utils/collections/array-filters.util.ts +12 -1
  11. package/src/infrastructure/utils/collections/array-sorters.util.ts +34 -0
  12. package/src/infrastructure/utils/collections/index.ts +0 -1
  13. package/src/infrastructure/utils/cost-tracker.ts +3 -1
  14. package/src/infrastructure/utils/cost-tracking-executor.util.ts +14 -3
  15. package/src/infrastructure/utils/date-format.util.ts +13 -47
  16. package/src/infrastructure/utils/fal-storage.util.ts +40 -1
  17. package/src/infrastructure/utils/formatting.util.ts +3 -21
  18. package/src/infrastructure/utils/helpers/index.ts +0 -1
  19. package/src/infrastructure/utils/helpers/timing-helpers.util.ts +1 -79
  20. package/src/infrastructure/utils/image-helpers.util.ts +9 -5
  21. package/src/infrastructure/utils/index.ts +11 -50
  22. package/src/infrastructure/utils/input-preprocessor.util.ts +53 -6
  23. package/src/infrastructure/utils/input-validator.util.ts +71 -28
  24. package/src/infrastructure/utils/job-metadata/job-metadata-format.util.ts +26 -1
  25. package/src/infrastructure/utils/number-format.util.ts +27 -20
  26. package/src/infrastructure/utils/parsers/index.ts +0 -1
  27. package/src/infrastructure/utils/parsers/json-parsers.util.ts +19 -3
  28. package/src/infrastructure/utils/string-format.util.ts +0 -59
  29. package/src/infrastructure/utils/type-guards/validation-guards.util.ts +12 -0
  30. package/src/presentation/hooks/use-models.ts +12 -4
  31. package/src/infrastructure/utils/collections/array-reducers.util.ts +0 -67
  32. package/src/infrastructure/utils/helpers/function-helpers.util.ts +0 -25
  33. package/src/infrastructure/utils/parsers/number-helpers.util.ts +0 -19
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-ai-fal-provider",
3
- "version": "2.1.3",
3
+ "version": "2.1.5",
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",
@@ -4,7 +4,6 @@
4
4
  */
5
5
 
6
6
  import type { FalModelType } from "../entities/fal.types";
7
- import type { ModelType } from "../types/model-selection.types";
8
7
  import { DEFAULT_TEXT_TO_IMAGE_MODELS } from "./models/text-to-image.models";
9
8
  import { DEFAULT_TEXT_TO_VOICE_MODELS } from "./models/text-to-voice.models";
10
9
  import { DEFAULT_TEXT_TO_VIDEO_MODELS } from "./models/text-to-video.models";
@@ -34,22 +33,28 @@ export interface FalModelConfig {
34
33
 
35
34
  /**
36
35
  * Default credit costs for each model type
36
+ * Supports all FalModelType values
37
37
  */
38
- export const DEFAULT_CREDIT_COSTS: Record<ModelType, number> = {
38
+ export const DEFAULT_CREDIT_COSTS: Record<FalModelType, number> = {
39
39
  "text-to-image": 2,
40
40
  "text-to-video": 20,
41
41
  "image-to-video": 20,
42
42
  "text-to-voice": 3,
43
+ "image-to-image": 2,
44
+ "text-to-text": 1,
43
45
  } as const;
44
46
 
45
47
  /**
46
48
  * Default model IDs for each model type
49
+ * Supports all FalModelType values
47
50
  */
48
- export const DEFAULT_MODEL_IDS: Record<ModelType, string> = {
51
+ export const DEFAULT_MODEL_IDS: Record<FalModelType, string> = {
49
52
  "text-to-image": "fal-ai/flux/schnell",
50
53
  "text-to-video": "fal-ai/minimax-video",
51
54
  "image-to-video": "fal-ai/kling-video/v1.5/pro/image-to-video",
52
55
  "text-to-voice": "fal-ai/playai/tts/v3",
56
+ "image-to-image": "fal-ai/flux/schnell",
57
+ "text-to-text": "fal-ai/flux/schnell",
53
58
  } as const;
54
59
 
55
60
  /**
@@ -90,10 +95,14 @@ export function getDefaultModelsByType(type: FalModelType): FalModelConfig[] {
90
95
 
91
96
  /**
92
97
  * Get default model for a type
98
+ * Returns the model marked as default, or the first model, or undefined if no models exist
93
99
  */
94
100
  export function getDefaultModel(type: FalModelType): FalModelConfig | undefined {
95
101
  const models = getDefaultModelsByType(type);
96
- return models.find((m) => m.isDefault) || models[0];
102
+ if (models.length === 0) {
103
+ return undefined;
104
+ }
105
+ return models.find((m) => m.isDefault) ?? models[0];
97
106
  }
98
107
 
99
108
  /**
@@ -4,12 +4,16 @@
4
4
  */
5
5
 
6
6
  import type { FalModelConfig } from "../constants/default-models.constants";
7
+ import type { FalModelType } from "../entities/fal.types";
7
8
 
8
- export type ModelType =
9
- | "text-to-image"
10
- | "text-to-video"
11
- | "image-to-video"
12
- | "text-to-voice";
9
+ /**
10
+ * Public API model types (subset of FalModelType)
11
+ * UI components support these core model types
12
+ */
13
+ export type ModelType = Extract<
14
+ FalModelType,
15
+ "text-to-image" | "text-to-video" | "image-to-video" | "text-to-voice"
16
+ >;
13
17
 
14
18
  /**
15
19
  * Configuration for model selection behavior
@@ -45,7 +45,6 @@ export {
45
45
  uploadToFalStorage,
46
46
  uploadMultipleToFalStorage,
47
47
  formatNumber,
48
- formatCurrency,
49
48
  formatBytes,
50
49
  formatDuration,
51
50
  truncateText,
@@ -55,10 +54,7 @@ export {
55
54
  isDefined,
56
55
  removeNullish,
57
56
  generateUniqueId,
58
- debounce,
59
- throttle,
60
57
  sleep,
61
- retry,
62
58
  } from "../infrastructure/utils";
63
59
 
64
60
  export {
@@ -89,8 +85,5 @@ export {
89
85
  updateJobStatus,
90
86
  } from "../infrastructure/utils";
91
87
 
92
- export type {
93
- FalJobMetadata,
94
- IJobStorage,
95
- InMemoryJobStorage,
96
- } from "../infrastructure/utils";
88
+ export type { FalJobMetadata, IJobStorage } from "../infrastructure/utils";
89
+ export { InMemoryJobStorage } from "../infrastructure/utils";
@@ -55,28 +55,28 @@ export function getModelPricing(modelId: string): { freeUserCost: number; premiu
55
55
  * Returns the model's free user cost if available, otherwise returns the default cost for the type
56
56
  * NOTE: Use ?? instead of || to handle 0 values correctly (free models)
57
57
  */
58
- export function getModelCreditCost(modelId: string, modelType: ModelType): number {
58
+ export function getModelCreditCost(modelId: string, modelType: FalModelType): number {
59
59
  const pricing = getModelPricing(modelId);
60
60
  // CRITICAL: Use !== undefined instead of truthy check
61
61
  // because freeUserCost can be 0 for free models!
62
62
  if (pricing && pricing.freeUserCost !== undefined) {
63
63
  return pricing.freeUserCost;
64
64
  }
65
- return DEFAULT_CREDIT_COSTS[modelType];
65
+ return DEFAULT_CREDIT_COSTS[modelType] ?? 0;
66
66
  }
67
67
 
68
68
  /**
69
69
  * Get default credit cost for a model type
70
70
  */
71
- export function getDefaultCreditCost(modelType: ModelType): number {
72
- return DEFAULT_CREDIT_COSTS[modelType];
71
+ export function getDefaultCreditCost(modelType: FalModelType): number {
72
+ return DEFAULT_CREDIT_COSTS[modelType] ?? 0;
73
73
  }
74
74
 
75
75
  /**
76
76
  * Get default model ID for a model type
77
77
  */
78
- export function getDefaultModelId(modelType: ModelType): string {
79
- return DEFAULT_MODEL_IDS[modelType];
78
+ export function getDefaultModelId(modelType: FalModelType): string {
79
+ return DEFAULT_MODEL_IDS[modelType] ?? "";
80
80
  }
81
81
 
82
82
  /**
@@ -110,7 +110,8 @@ export function getModelSelectionData(
110
110
  modelType: ModelType,
111
111
  config?: ModelSelectionConfig
112
112
  ): ModelSelectionResult {
113
- const models = getModels(modelType as FalModelType);
113
+ // ModelType is now a subset of FalModelType, so this cast is safe
114
+ const models = getModels(modelType);
114
115
  const defaultCreditCost = config?.defaultCreditCost ?? getDefaultCreditCost(modelType);
115
116
  const defaultModelId = config?.defaultModelId ?? getDefaultModelId(modelType);
116
117
  const selectedModel = selectInitialModel(models, config, modelType);
@@ -126,18 +126,21 @@ export class FalProvider implements IAIProvider {
126
126
  const abortController = new AbortController();
127
127
  const tracker = this.costTracker;
128
128
 
129
- // Store promise immediately BEFORE creating it to prevent race condition
130
- // Multiple simultaneous calls with same params will get the same promise
131
- let resolvePromise: (value: T) => void;
132
- let rejectPromise: (error: unknown) => void;
129
+ // Create promise with resolvers using definite assignment
130
+ // This prevents race conditions and ensures type safety
131
+ let resolvePromise!: (value: T) => void;
132
+ let rejectPromise!: (error: unknown) => void;
133
133
  const promise = new Promise<T>((resolve, reject) => {
134
134
  resolvePromise = resolve;
135
135
  rejectPromise = reject;
136
136
  });
137
137
 
138
+ // Store promise immediately to enable request deduplication
139
+ // Multiple simultaneous calls with same params will get the same promise
138
140
  storeRequest(key, { promise, abortController, createdAt: Date.now() });
139
141
 
140
142
  // Execute the actual operation and resolve/reject the stored promise
143
+ // Note: This promise chain is not awaited - it runs independently
141
144
  executeWithCostTracking({
142
145
  tracker,
143
146
  model,
@@ -146,12 +149,10 @@ export class FalProvider implements IAIProvider {
146
149
  getRequestId: (res) => res.requestId ?? undefined,
147
150
  })
148
151
  .then((res) => {
149
- resolvePromise!(res.result);
150
- return res.result;
152
+ resolvePromise(res.result);
151
153
  })
152
154
  .catch((error) => {
153
- rejectPromise!(error);
154
- throw error;
155
+ rejectPromise(error);
155
156
  })
156
157
  .finally(() => {
157
158
  try {
@@ -61,7 +61,14 @@ export async function getJobResult<T = unknown>(model: string, requestId: string
61
61
 
62
62
  if (!result || typeof result !== 'object') {
63
63
  throw new Error(
64
- `Invalid FAL queue result for model ${model}, requestId ${requestId}`
64
+ `Invalid FAL queue result for model ${model}, requestId ${requestId}: Result is not an object`
65
+ );
66
+ }
67
+
68
+ // Type guard: ensure result.data exists before casting
69
+ if (!('data' in result)) {
70
+ throw new Error(
71
+ `Invalid FAL queue result for model ${model}, requestId ${requestId}: Missing 'data' property`
65
72
  );
66
73
  }
67
74
 
@@ -12,7 +12,7 @@ export class NSFWContentError extends Error {
12
12
 
13
13
  constructor(message?: string) {
14
14
  super(
15
- message ||
15
+ message ??
16
16
  "The generated content was flagged as inappropriate. Please try a different prompt."
17
17
  );
18
18
  this.name = "NSFWContentError";
@@ -11,12 +11,28 @@ export interface ActiveRequest<T = unknown> {
11
11
 
12
12
  const STORE_KEY = "__FAL_PROVIDER_REQUESTS__";
13
13
  const LOCK_KEY = "__FAL_PROVIDER_REQUESTS_LOCK__";
14
+ const TIMER_KEY = "__FAL_PROVIDER_CLEANUP_TIMER__";
14
15
  type RequestStore = Map<string, ActiveRequest>;
15
16
 
16
- let cleanupTimer: ReturnType<typeof setInterval> | null = null;
17
17
  const CLEANUP_INTERVAL = 60000; // 1 minute
18
18
  const MAX_REQUEST_AGE = 300000; // 5 minutes
19
19
 
20
+ /**
21
+ * Get cleanup timer from globalThis to survive hot reloads
22
+ */
23
+ function getCleanupTimer(): ReturnType<typeof setInterval> | null {
24
+ const globalObj = globalThis as Record<string, unknown>;
25
+ return (globalObj[TIMER_KEY] as ReturnType<typeof setInterval>) ?? null;
26
+ }
27
+
28
+ /**
29
+ * Set cleanup timer in globalThis to survive hot reloads
30
+ */
31
+ function setCleanupTimer(timer: ReturnType<typeof setInterval> | null): void {
32
+ const globalObj = globalThis as Record<string, unknown>;
33
+ globalObj[TIMER_KEY] = timer;
34
+ }
35
+
20
36
  /**
21
37
  * Simple lock mechanism to prevent concurrent access issues
22
38
  * NOTE: This is not a true mutex but provides basic protection for React Native
@@ -65,13 +81,25 @@ export function getExistingRequest<T>(key: string): ActiveRequest<T> | undefined
65
81
  }
66
82
 
67
83
  export function storeRequest<T>(key: string, request: ActiveRequest<T>): void {
68
- // Acquire lock for thread-safe operation
69
- const maxRetries = 3;
84
+ // Acquire lock for consistent operation
85
+ // React Native is single-threaded, but this prevents re-entrancy issues
86
+ const maxRetries = 10;
70
87
  let retries = 0;
71
88
 
89
+ // Spin-wait loop (synchronous)
90
+ // Note: Does NOT yield to event loop - tight loop
91
+ // In practice, this rarely loops due to single-threaded nature of React Native
72
92
  while (!acquireLock() && retries < maxRetries) {
73
93
  retries++;
74
- // Brief spin wait (not ideal, but works for React Native single-threaded model)
94
+ }
95
+
96
+ if (retries >= maxRetries) {
97
+ // Lock acquisition failed - this shouldn't happen in normal operation
98
+ // Log warning but proceed anyway since RN is single-threaded
99
+ console.warn(
100
+ `[request-store] Failed to acquire lock after ${maxRetries} attempts for key: ${key}. ` +
101
+ 'Proceeding anyway (safe in single-threaded environment)'
102
+ );
75
103
  }
76
104
 
77
105
  try {
@@ -84,6 +112,8 @@ export function storeRequest<T>(key: string, request: ActiveRequest<T>): void {
84
112
  // Start automatic cleanup if not already running
85
113
  startAutomaticCleanup();
86
114
  } finally {
115
+ // Always release lock, even if we didn't successfully acquire it
116
+ // to prevent deadlocks
87
117
  releaseLock();
88
118
  }
89
119
  }
@@ -93,9 +123,12 @@ export function removeRequest(key: string): void {
93
123
  store.delete(key);
94
124
 
95
125
  // Stop cleanup timer if store is empty
96
- if (store.size === 0 && cleanupTimer) {
97
- clearInterval(cleanupTimer);
98
- cleanupTimer = null;
126
+ if (store.size === 0) {
127
+ const timer = getCleanupTimer();
128
+ if (timer) {
129
+ clearInterval(timer);
130
+ setCleanupTimer(null);
131
+ }
99
132
  }
100
133
  }
101
134
 
@@ -107,9 +140,10 @@ export function cancelAllRequests(): void {
107
140
  store.clear();
108
141
 
109
142
  // Stop cleanup timer
110
- if (cleanupTimer) {
111
- clearInterval(cleanupTimer);
112
- cleanupTimer = null;
143
+ const timer = getCleanupTimer();
144
+ if (timer) {
145
+ clearInterval(timer);
146
+ setCleanupTimer(null);
113
147
  }
114
148
  }
115
149
 
@@ -152,9 +186,12 @@ export function cleanupRequestStore(maxAge: number = MAX_REQUEST_AGE): number {
152
186
  }
153
187
 
154
188
  // Stop cleanup timer if store is empty
155
- if (store.size === 0 && cleanupTimer) {
156
- clearInterval(cleanupTimer);
157
- cleanupTimer = null;
189
+ if (store.size === 0) {
190
+ const timer = getCleanupTimer();
191
+ if (timer) {
192
+ clearInterval(timer);
193
+ setCleanupTimer(null);
194
+ }
158
195
  }
159
196
 
160
197
  return cleanedCount;
@@ -163,34 +200,53 @@ export function cleanupRequestStore(maxAge: number = MAX_REQUEST_AGE): number {
163
200
  /**
164
201
  * Start automatic cleanup of stale requests
165
202
  * Runs periodically to prevent memory leaks
203
+ * Uses globalThis to survive hot reloads in React Native
166
204
  */
167
205
  function startAutomaticCleanup(): void {
168
- if (cleanupTimer) {
206
+ const existingTimer = getCleanupTimer();
207
+ if (existingTimer) {
169
208
  return; // Already running
170
209
  }
171
210
 
172
- cleanupTimer = setInterval(() => {
211
+ const timer = setInterval(() => {
173
212
  const cleanedCount = cleanupRequestStore(MAX_REQUEST_AGE);
174
213
  const store = getRequestStore();
175
214
 
176
215
  // Stop timer if no more requests in store (prevents indefinite timer)
177
- if (store.size === 0 && cleanupTimer) {
178
- clearInterval(cleanupTimer);
179
- cleanupTimer = null;
216
+ if (store.size === 0) {
217
+ const currentTimer = getCleanupTimer();
218
+ if (currentTimer) {
219
+ clearInterval(currentTimer);
220
+ setCleanupTimer(null);
221
+ }
180
222
  }
181
223
 
182
224
  if (cleanedCount > 0) {
183
225
  console.log(`[request-store] Cleaned up ${cleanedCount} stale request(s)`);
184
226
  }
185
227
  }, CLEANUP_INTERVAL);
228
+
229
+ setCleanupTimer(timer);
186
230
  }
187
231
 
188
232
  /**
189
- * Stop automatic cleanup (typically on app shutdown)
233
+ * Stop automatic cleanup (typically on app shutdown or hot reload)
234
+ * Clears the global timer to prevent memory leaks
190
235
  */
191
236
  export function stopAutomaticCleanup(): void {
192
- if (cleanupTimer) {
193
- clearInterval(cleanupTimer);
194
- cleanupTimer = null;
237
+ const timer = getCleanupTimer();
238
+ if (timer) {
239
+ clearInterval(timer);
240
+ setCleanupTimer(null);
241
+ }
242
+ }
243
+
244
+ // Clean up any existing timer on module load to prevent leaks during hot reload
245
+ // This ensures old timers are cleared when the module is reloaded in development
246
+ if (typeof globalThis !== "undefined") {
247
+ const existingTimer = getCleanupTimer();
248
+ if (existingTimer) {
249
+ clearInterval(existingTimer);
250
+ setCleanupTimer(null);
195
251
  }
196
252
  }
@@ -26,6 +26,7 @@ export function filterByPredicate<T>(
26
26
 
27
27
  /**
28
28
  * Filter array by time range (timestamp property)
29
+ * Validates that the timestamp property is actually a number before comparison
29
30
  */
30
31
  export function filterByTimeRange<T>(
31
32
  items: readonly T[],
@@ -34,7 +35,17 @@ export function filterByTimeRange<T>(
34
35
  endTime: number
35
36
  ): T[] {
36
37
  return items.filter((item) => {
37
- const timestamp = item[timestampProperty] as unknown as number;
38
+ const timestamp = item[timestampProperty];
39
+
40
+ // Type guard: ensure timestamp is actually a number
41
+ if (typeof timestamp !== 'number') {
42
+ console.warn(
43
+ `[array-filters] Skipping item with non-numeric timestamp property '${String(timestampProperty)}':`,
44
+ typeof timestamp
45
+ );
46
+ return false;
47
+ }
48
+
38
49
  return timestamp >= startTime && timestamp <= endTime;
39
50
  });
40
51
  }
@@ -5,6 +5,7 @@
5
5
 
6
6
  /**
7
7
  * Sort array by date property (descending - newest first)
8
+ * Invalid dates are sorted to the end
8
9
  */
9
10
  export function sortByDateDescending<T>(
10
11
  items: readonly T[],
@@ -13,12 +14,19 @@ export function sortByDateDescending<T>(
13
14
  return [...items].sort((a, b) => {
14
15
  const timeA = new Date(a[dateProperty] as unknown as string).getTime();
15
16
  const timeB = new Date(b[dateProperty] as unknown as string).getTime();
17
+
18
+ // Handle invalid dates - NaN should sort to end
19
+ if (isNaN(timeA) && isNaN(timeB)) return 0;
20
+ if (isNaN(timeA)) return 1; // a goes to end
21
+ if (isNaN(timeB)) return -1; // b goes to end
22
+
16
23
  return timeB - timeA;
17
24
  });
18
25
  }
19
26
 
20
27
  /**
21
28
  * Sort array by date property (ascending - oldest first)
29
+ * Invalid dates are sorted to the end
22
30
  */
23
31
  export function sortByDateAscending<T>(
24
32
  items: readonly T[],
@@ -27,12 +35,19 @@ export function sortByDateAscending<T>(
27
35
  return [...items].sort((a, b) => {
28
36
  const timeA = new Date(a[dateProperty] as unknown as string).getTime();
29
37
  const timeB = new Date(b[dateProperty] as unknown as string).getTime();
38
+
39
+ // Handle invalid dates - NaN should sort to end
40
+ if (isNaN(timeA) && isNaN(timeB)) return 0;
41
+ if (isNaN(timeA)) return 1; // a goes to end
42
+ if (isNaN(timeB)) return -1; // b goes to end
43
+
30
44
  return timeA - timeB;
31
45
  });
32
46
  }
33
47
 
34
48
  /**
35
49
  * Sort array by number property (descending)
50
+ * NaN and Infinity values are sorted to the end
36
51
  */
37
52
  export function sortByNumberDescending<T>(
38
53
  items: readonly T[],
@@ -41,12 +56,22 @@ export function sortByNumberDescending<T>(
41
56
  return [...items].sort((a, b) => {
42
57
  const numA = a[numberProperty] as unknown as number;
43
58
  const numB = b[numberProperty] as unknown as number;
59
+
60
+ // Handle NaN and Infinity
61
+ const isAValid = isFinite(numA);
62
+ const isBValid = isFinite(numB);
63
+
64
+ if (!isAValid && !isBValid) return 0;
65
+ if (!isAValid) return 1; // a goes to end
66
+ if (!isBValid) return -1; // b goes to end
67
+
44
68
  return numB - numA;
45
69
  });
46
70
  }
47
71
 
48
72
  /**
49
73
  * Sort array by number property (ascending)
74
+ * NaN and Infinity values are sorted to the end
50
75
  */
51
76
  export function sortByNumberAscending<T>(
52
77
  items: readonly T[],
@@ -55,6 +80,15 @@ export function sortByNumberAscending<T>(
55
80
  return [...items].sort((a, b) => {
56
81
  const numA = a[numberProperty] as unknown as number;
57
82
  const numB = b[numberProperty] as unknown as number;
83
+
84
+ // Handle NaN and Infinity
85
+ const isAValid = isFinite(numA);
86
+ const isBValid = isFinite(numB);
87
+
88
+ if (!isAValid && !isBValid) return 0;
89
+ if (!isAValid) return 1; // a goes to end
90
+ if (!isBValid) return -1; // b goes to end
91
+
58
92
  return numA - numB;
59
93
  });
60
94
  }
@@ -5,4 +5,3 @@
5
5
 
6
6
  export * from './array-filters.util';
7
7
  export * from './array-sorters.util';
8
- export * from './array-reducers.util';
@@ -11,7 +11,9 @@ import type {
11
11
  import { findModelById } from "../../domain/constants/default-models.constants";
12
12
  import { filterByProperty, filterByTimeRange } from "./collection-filters.util";
13
13
 
14
- interface CostSummary {
14
+ export type { GenerationCost } from "../../domain/entities/cost-tracking.types";
15
+
16
+ export interface CostSummary {
15
17
  totalEstimatedCost: number;
16
18
  totalActualCost: number;
17
19
  currency: string;
@@ -36,15 +36,26 @@ export async function executeWithCostTracking<T>(
36
36
  tracker.completeOperation(operationId, model, operation, requestId);
37
37
  } catch (costError) {
38
38
  // Cost tracking failure shouldn't break the operation
39
- // Log for debugging but don't throw
39
+ // Log for debugging and audit trail
40
+ console.error(
41
+ `[cost-tracking] Failed to complete cost tracking for ${operation} on ${model}:`,
42
+ costError instanceof Error ? costError.message : String(costError),
43
+ { operationId, model, operation }
44
+ );
40
45
  }
41
46
 
42
47
  return result;
43
48
  } catch (error) {
44
49
  try {
45
50
  tracker.failOperation(operationId);
46
- } catch {
47
- // Cost tracking cleanup failure on error path - ignore
51
+ } catch (failError) {
52
+ // Cost tracking cleanup failure on error path
53
+ // Log for debugging and audit trail
54
+ console.error(
55
+ `[cost-tracking] Failed to mark operation as failed for ${operation} on ${model}:`,
56
+ failError instanceof Error ? failError.message : String(failError),
57
+ { operationId, model, operation }
58
+ );
48
59
  }
49
60
  throw error;
50
61
  }
@@ -4,61 +4,27 @@
4
4
  */
5
5
 
6
6
  /**
7
- * Format date to locale string
7
+ * Validate that a date is valid
8
8
  */
9
- export function formatDate(date: Date | string, locale: string = "en-US"): string {
10
- const dateObj = typeof date === "string" ? new Date(date) : date;
11
- return dateObj.toLocaleDateString(locale, {
12
- year: "numeric",
13
- month: "short",
14
- day: "numeric",
15
- });
9
+ function isValidDate(date: Date): boolean {
10
+ return date instanceof Date && !isNaN(date.getTime());
16
11
  }
17
12
 
18
13
  /**
19
- * Format date and time to locale string
14
+ * Format date to locale string
15
+ * @throws {Error} if date is invalid
20
16
  */
21
- export function formatDateTime(date: Date | string, locale: string = "en-US"): string {
17
+ export function formatDate(date: Date | string, locale: string = "en-US"): string {
22
18
  const dateObj = typeof date === "string" ? new Date(date) : date;
23
- return dateObj.toLocaleString(locale, {
19
+
20
+ if (!isValidDate(dateObj)) {
21
+ const dateStr = typeof date === "string" ? date : date.toISOString();
22
+ throw new Error(`Invalid date: ${dateStr}`);
23
+ }
24
+
25
+ return dateObj.toLocaleDateString(locale, {
24
26
  year: "numeric",
25
27
  month: "short",
26
28
  day: "numeric",
27
- hour: "2-digit",
28
- minute: "2-digit",
29
29
  });
30
30
  }
31
-
32
- /**
33
- * Format relative time (e.g., "2 hours ago")
34
- */
35
- export function formatRelativeTime(date: Date | string, locale: string = "en-US"): string {
36
- const dateObj = typeof date === "string" ? new Date(date) : date;
37
- const now = new Date();
38
- const diffMs = now.getTime() - dateObj.getTime();
39
- const diffSec = Math.floor(diffMs / 1000);
40
- const diffMin = Math.floor(diffSec / 60);
41
- const diffHour = Math.floor(diffMin / 60);
42
- const diffDay = Math.floor(diffHour / 24);
43
-
44
- const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
45
-
46
- if (diffSec < 60) {
47
- return rtf.format(-diffSec, "second");
48
- }
49
- if (diffMin < 60) {
50
- return rtf.format(-diffMin, "minute");
51
- }
52
- if (diffHour < 24) {
53
- return rtf.format(-diffHour, "hour");
54
- }
55
- if (diffDay < 30) {
56
- return rtf.format(-diffDay, "day");
57
- }
58
- if (diffDay < 365) {
59
- const months = Math.floor(diffDay / 30);
60
- return rtf.format(-months, "month");
61
- }
62
- const years = Math.floor(diffDay / 365);
63
- return rtf.format(-years, "year");
64
- }