@umituz/react-native-ai-fal-provider 2.0.37 → 2.0.39
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/infrastructure/services/fal-models.service.ts +4 -1
- package/src/infrastructure/services/fal-provider-subscription.ts +17 -7
- package/src/infrastructure/services/fal-provider.ts +11 -1
- package/src/infrastructure/services/fal-queue-operations.ts +10 -0
- package/src/infrastructure/services/request-store.ts +53 -12
- package/src/infrastructure/utils/collection-filters.util.ts +5 -167
- package/src/infrastructure/utils/collections/array-filters.util.ts +52 -0
- package/src/infrastructure/utils/collections/array-reducers.util.ts +67 -0
- package/src/infrastructure/utils/collections/array-sorters.util.ts +60 -0
- package/src/infrastructure/utils/collections/index.ts +8 -0
- package/src/infrastructure/utils/cost-tracker.ts +6 -1
- package/src/infrastructure/utils/data-parsers.util.ts +5 -187
- package/src/infrastructure/utils/error-mapper.ts +10 -0
- package/src/infrastructure/utils/fal-error-handler.util.ts +21 -18
- package/src/infrastructure/utils/fal-generation-state-manager.util.ts +9 -2
- package/src/infrastructure/utils/fal-storage.util.ts +7 -2
- package/src/infrastructure/utils/general-helpers.util.ts +5 -146
- package/src/infrastructure/utils/helpers/function-helpers.util.ts +25 -0
- package/src/infrastructure/utils/helpers/index.ts +8 -0
- package/src/infrastructure/utils/helpers/object-helpers.util.ts +44 -0
- package/src/infrastructure/utils/helpers/timing-helpers.util.ts +89 -0
- package/src/infrastructure/utils/input-preprocessor.util.ts +12 -6
- package/src/infrastructure/utils/input-validator.util.ts +34 -3
- package/src/infrastructure/utils/parsers/index.ts +10 -0
- package/src/infrastructure/utils/parsers/json-parsers.util.ts +55 -0
- package/src/infrastructure/utils/parsers/number-helpers.util.ts +19 -0
- package/src/infrastructure/utils/parsers/object-transformers.util.ts +67 -0
- package/src/infrastructure/utils/parsers/object-validators.util.ts +38 -0
- package/src/infrastructure/utils/parsers/value-parsers.util.ts +45 -0
- package/src/infrastructure/utils/type-guards/constants.ts +10 -0
- package/src/infrastructure/utils/type-guards/index.ts +8 -0
- package/src/infrastructure/utils/type-guards/model-type-guards.util.ts +56 -0
- package/src/infrastructure/utils/type-guards/validation-guards.util.ts +80 -0
- package/src/infrastructure/utils/type-guards.util.ts +5 -115
- package/src/presentation/hooks/use-fal-generation.ts +15 -4
- package/src/presentation/hooks/use-models.ts +26 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-fal-provider",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.39",
|
|
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",
|
|
@@ -53,10 +53,13 @@ export function getModelPricing(modelId: string): { freeUserCost: number; premiu
|
|
|
53
53
|
/**
|
|
54
54
|
* Get credit cost for a model
|
|
55
55
|
* Returns the model's free user cost if available, otherwise returns the default cost for the type
|
|
56
|
+
* NOTE: Use ?? instead of || to handle 0 values correctly (free models)
|
|
56
57
|
*/
|
|
57
58
|
export function getModelCreditCost(modelId: string, modelType: ModelType): number {
|
|
58
59
|
const pricing = getModelPricing(modelId);
|
|
59
|
-
|
|
60
|
+
// CRITICAL: Use !== undefined instead of truthy check
|
|
61
|
+
// because freeUserCost can be 0 for free models!
|
|
62
|
+
if (pricing && pricing.freeUserCost !== undefined) {
|
|
60
63
|
return pricing.freeUserCost;
|
|
61
64
|
}
|
|
62
65
|
return DEFAULT_CREDIT_COSTS[modelType];
|
|
@@ -23,8 +23,11 @@ export async function handleFalSubscription<T = unknown>(
|
|
|
23
23
|
): Promise<{ result: T; requestId: string | null }> {
|
|
24
24
|
const timeoutMs = options?.timeoutMs ?? DEFAULT_FAL_CONFIG.defaultTimeoutMs;
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
// Validate timeout is a positive integer within reasonable bounds
|
|
27
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 3600000) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`Invalid timeout: ${timeoutMs}ms. Must be a positive integer between 1 and 3600000ms (1 hour)`
|
|
30
|
+
);
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
@@ -32,13 +35,14 @@ export async function handleFalSubscription<T = unknown>(
|
|
|
32
35
|
let abortHandler: (() => void) | null = null;
|
|
33
36
|
let listenerAdded = false;
|
|
34
37
|
|
|
35
|
-
if (signal?.aborted) {
|
|
36
|
-
throw new Error("Request cancelled by user");
|
|
37
|
-
}
|
|
38
|
-
|
|
39
38
|
let lastStatus = "";
|
|
40
39
|
|
|
41
40
|
try {
|
|
41
|
+
// Check if signal is already aborted BEFORE starting any async work
|
|
42
|
+
if (signal?.aborted) {
|
|
43
|
+
throw new Error("Request cancelled by user");
|
|
44
|
+
}
|
|
45
|
+
|
|
42
46
|
const promises: Promise<unknown>[] = [
|
|
43
47
|
fal.subscribe(model, {
|
|
44
48
|
input,
|
|
@@ -77,13 +81,19 @@ export async function handleFalSubscription<T = unknown>(
|
|
|
77
81
|
}),
|
|
78
82
|
];
|
|
79
83
|
|
|
80
|
-
|
|
84
|
+
// Set up abort listener BEFORE checking aborted state again to avoid race
|
|
85
|
+
if (signal) {
|
|
81
86
|
const abortPromise = new Promise<never>((_, reject) => {
|
|
82
87
|
abortHandler = () => {
|
|
83
88
|
reject(new Error("Request cancelled by user"));
|
|
84
89
|
};
|
|
85
90
|
signal.addEventListener("abort", abortHandler);
|
|
86
91
|
listenerAdded = true;
|
|
92
|
+
|
|
93
|
+
// Check again after adding listener to catch signals that arrived during setup
|
|
94
|
+
if (signal.aborted) {
|
|
95
|
+
abortHandler();
|
|
96
|
+
}
|
|
87
97
|
});
|
|
88
98
|
promises.push(abortPromise);
|
|
89
99
|
}
|
|
@@ -153,7 +153,17 @@ export class FalProvider implements IAIProvider {
|
|
|
153
153
|
rejectPromise!(error);
|
|
154
154
|
throw error;
|
|
155
155
|
})
|
|
156
|
-
.finally(() =>
|
|
156
|
+
.finally(() => {
|
|
157
|
+
try {
|
|
158
|
+
removeRequest(key);
|
|
159
|
+
} catch (cleanupError) {
|
|
160
|
+
// Log but don't throw - cleanup errors shouldn't affect the operation result
|
|
161
|
+
console.error(
|
|
162
|
+
`[fal-provider] Error removing request from store: ${key}`,
|
|
163
|
+
cleanupError instanceof Error ? cleanupError.message : String(cleanupError)
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
157
167
|
|
|
158
168
|
return promise;
|
|
159
169
|
}
|
|
@@ -27,6 +27,16 @@ function isValidFalQueueStatus(value: unknown): value is FalQueueStatus {
|
|
|
27
27
|
|
|
28
28
|
export async function submitJob(model: string, input: Record<string, unknown>): Promise<JobSubmission> {
|
|
29
29
|
const result = await fal.queue.submit(model, { input });
|
|
30
|
+
|
|
31
|
+
// Validate required fields from FAL API response
|
|
32
|
+
if (!result?.request_id) {
|
|
33
|
+
throw new Error(`FAL API response missing request_id for model ${model}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!result?.status_url) {
|
|
37
|
+
throw new Error(`FAL API response missing status_url for model ${model}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
30
40
|
return {
|
|
31
41
|
requestId: result.request_id,
|
|
32
42
|
statusUrl: result.status_url,
|
|
@@ -10,17 +10,37 @@ export interface ActiveRequest<T = unknown> {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
const STORE_KEY = "__FAL_PROVIDER_REQUESTS__";
|
|
13
|
+
const LOCK_KEY = "__FAL_PROVIDER_REQUESTS_LOCK__";
|
|
13
14
|
type RequestStore = Map<string, ActiveRequest>;
|
|
14
15
|
|
|
15
16
|
let cleanupTimer: ReturnType<typeof setInterval> | null = null;
|
|
16
17
|
const CLEANUP_INTERVAL = 60000; // 1 minute
|
|
17
18
|
const MAX_REQUEST_AGE = 300000; // 5 minutes
|
|
18
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Simple lock mechanism to prevent concurrent access issues
|
|
22
|
+
* NOTE: This is not a true mutex but provides basic protection for React Native
|
|
23
|
+
*/
|
|
24
|
+
function acquireLock(): boolean {
|
|
25
|
+
const globalObj = globalThis as Record<string, unknown>;
|
|
26
|
+
if (globalObj[LOCK_KEY]) {
|
|
27
|
+
return false; // Lock already held
|
|
28
|
+
}
|
|
29
|
+
globalObj[LOCK_KEY] = true;
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function releaseLock(): void {
|
|
34
|
+
const globalObj = globalThis as Record<string, unknown>;
|
|
35
|
+
globalObj[LOCK_KEY] = false;
|
|
36
|
+
}
|
|
37
|
+
|
|
19
38
|
export function getRequestStore(): RequestStore {
|
|
20
|
-
|
|
21
|
-
|
|
39
|
+
const globalObj = globalThis as Record<string, unknown>;
|
|
40
|
+
if (!globalObj[STORE_KEY]) {
|
|
41
|
+
globalObj[STORE_KEY] = new Map();
|
|
22
42
|
}
|
|
23
|
-
return
|
|
43
|
+
return globalObj[STORE_KEY] as RequestStore;
|
|
24
44
|
}
|
|
25
45
|
|
|
26
46
|
/**
|
|
@@ -45,14 +65,27 @@ export function getExistingRequest<T>(key: string): ActiveRequest<T> | undefined
|
|
|
45
65
|
}
|
|
46
66
|
|
|
47
67
|
export function storeRequest<T>(key: string, request: ActiveRequest<T>): void {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
68
|
+
// Acquire lock for thread-safe operation
|
|
69
|
+
const maxRetries = 3;
|
|
70
|
+
let retries = 0;
|
|
71
|
+
|
|
72
|
+
while (!acquireLock() && retries < maxRetries) {
|
|
73
|
+
retries++;
|
|
74
|
+
// Brief spin wait (not ideal, but works for React Native single-threaded model)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const requestWithTimestamp = {
|
|
79
|
+
...request,
|
|
80
|
+
createdAt: request.createdAt ?? Date.now(),
|
|
81
|
+
};
|
|
82
|
+
getRequestStore().set(key, requestWithTimestamp);
|
|
83
|
+
|
|
84
|
+
// Start automatic cleanup if not already running
|
|
85
|
+
startAutomaticCleanup();
|
|
86
|
+
} finally {
|
|
87
|
+
releaseLock();
|
|
88
|
+
}
|
|
56
89
|
}
|
|
57
90
|
|
|
58
91
|
export function removeRequest(key: string): void {
|
|
@@ -138,8 +171,16 @@ function startAutomaticCleanup(): void {
|
|
|
138
171
|
|
|
139
172
|
cleanupTimer = setInterval(() => {
|
|
140
173
|
const cleanedCount = cleanupRequestStore(MAX_REQUEST_AGE);
|
|
174
|
+
const store = getRequestStore();
|
|
175
|
+
|
|
176
|
+
// Stop timer if no more requests in store (prevents indefinite timer)
|
|
177
|
+
if (store.size === 0 && cleanupTimer) {
|
|
178
|
+
clearInterval(cleanupTimer);
|
|
179
|
+
cleanupTimer = null;
|
|
180
|
+
}
|
|
181
|
+
|
|
141
182
|
if (cleanedCount > 0) {
|
|
142
|
-
|
|
183
|
+
console.log(`[request-store] Cleaned up ${cleanedCount} stale request(s)`);
|
|
143
184
|
}
|
|
144
185
|
}, CLEANUP_INTERVAL);
|
|
145
186
|
}
|
|
@@ -1,171 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Collection Filter Utilities
|
|
3
|
-
*
|
|
3
|
+
* @deprecated This file is now split into smaller modules for better maintainability.
|
|
4
|
+
* Import from './collections' submodules instead.
|
|
5
|
+
*
|
|
6
|
+
* This file re-exports all functions for backward compatibility.
|
|
4
7
|
*/
|
|
5
8
|
|
|
6
|
-
|
|
7
|
-
* Filter array by property value
|
|
8
|
-
*/
|
|
9
|
-
export function filterByProperty<T>(
|
|
10
|
-
items: readonly T[],
|
|
11
|
-
property: keyof T,
|
|
12
|
-
value: unknown
|
|
13
|
-
): T[] {
|
|
14
|
-
return items.filter((item) => item[property] === value);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Filter array by predicate function
|
|
19
|
-
*/
|
|
20
|
-
export function filterByPredicate<T>(
|
|
21
|
-
items: readonly T[],
|
|
22
|
-
predicate: (item: T) => boolean
|
|
23
|
-
): T[] {
|
|
24
|
-
return items.filter(predicate);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Filter array by time range (timestamp property)
|
|
29
|
-
*/
|
|
30
|
-
export function filterByTimeRange<T>(
|
|
31
|
-
items: readonly T[],
|
|
32
|
-
timestampProperty: keyof T,
|
|
33
|
-
startTime: number,
|
|
34
|
-
endTime: number
|
|
35
|
-
): T[] {
|
|
36
|
-
return items.filter((item) => {
|
|
37
|
-
const timestamp = item[timestampProperty] as unknown as number;
|
|
38
|
-
return timestamp >= startTime && timestamp <= endTime;
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Filter array by multiple property values (OR logic)
|
|
44
|
-
*/
|
|
45
|
-
export function filterByAnyProperty<T>(
|
|
46
|
-
items: readonly T[],
|
|
47
|
-
property: keyof T,
|
|
48
|
-
values: readonly unknown[]
|
|
49
|
-
): T[] {
|
|
50
|
-
const valueSet = new Set(values);
|
|
51
|
-
return items.filter((item) => valueSet.has(item[property]));
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Sort array by date property (descending - newest first)
|
|
56
|
-
*/
|
|
57
|
-
export function sortByDateDescending<T>(
|
|
58
|
-
items: readonly T[],
|
|
59
|
-
dateProperty: keyof T
|
|
60
|
-
): T[] {
|
|
61
|
-
return [...items].sort((a, b) => {
|
|
62
|
-
const timeA = new Date(a[dateProperty] as unknown as string).getTime();
|
|
63
|
-
const timeB = new Date(b[dateProperty] as unknown as string).getTime();
|
|
64
|
-
return timeB - timeA;
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Sort array by date property (ascending - oldest first)
|
|
70
|
-
*/
|
|
71
|
-
export function sortByDateAscending<T>(
|
|
72
|
-
items: readonly T[],
|
|
73
|
-
dateProperty: keyof T
|
|
74
|
-
): T[] {
|
|
75
|
-
return [...items].sort((a, b) => {
|
|
76
|
-
const timeA = new Date(a[dateProperty] as unknown as string).getTime();
|
|
77
|
-
const timeB = new Date(b[dateProperty] as unknown as string).getTime();
|
|
78
|
-
return timeA - timeB;
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Sort array by number property (descending)
|
|
84
|
-
*/
|
|
85
|
-
export function sortByNumberDescending<T>(
|
|
86
|
-
items: readonly T[],
|
|
87
|
-
numberProperty: keyof T
|
|
88
|
-
): T[] {
|
|
89
|
-
return [...items].sort((a, b) => {
|
|
90
|
-
const numA = a[numberProperty] as unknown as number;
|
|
91
|
-
const numB = b[numberProperty] as unknown as number;
|
|
92
|
-
return numB - numA;
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Sort array by number property (ascending)
|
|
98
|
-
*/
|
|
99
|
-
export function sortByNumberAscending<T>(
|
|
100
|
-
items: readonly T[],
|
|
101
|
-
numberProperty: keyof T
|
|
102
|
-
): T[] {
|
|
103
|
-
return [...items].sort((a, b) => {
|
|
104
|
-
const numA = a[numberProperty] as unknown as number;
|
|
105
|
-
const numB = b[numberProperty] as unknown as number;
|
|
106
|
-
return numA - numB;
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Reduce array to sum of number property
|
|
112
|
-
*/
|
|
113
|
-
export function sumByProperty<T>(
|
|
114
|
-
items: readonly T[],
|
|
115
|
-
numberProperty: keyof T
|
|
116
|
-
): number {
|
|
117
|
-
return items.reduce((sum, item) => {
|
|
118
|
-
const value = item[numberProperty] as unknown as number;
|
|
119
|
-
return sum + (typeof value === "number" ? value : 0);
|
|
120
|
-
}, 0);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Group array by property value
|
|
125
|
-
*/
|
|
126
|
-
export function groupByProperty<T>(
|
|
127
|
-
items: readonly T[],
|
|
128
|
-
property: keyof T
|
|
129
|
-
): Map<unknown, T[]> {
|
|
130
|
-
const groups = new Map<unknown, T[]>();
|
|
131
|
-
for (const item of items) {
|
|
132
|
-
const key = item[property];
|
|
133
|
-
const existing = groups.get(key);
|
|
134
|
-
if (existing) {
|
|
135
|
-
existing.push(item);
|
|
136
|
-
} else {
|
|
137
|
-
groups.set(key, [item]);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
return groups;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Chunk array into smaller arrays of specified size
|
|
145
|
-
*/
|
|
146
|
-
export function chunkArray<T>(items: readonly T[], chunkSize: number): T[][] {
|
|
147
|
-
const result: T[][] = [];
|
|
148
|
-
for (let i = 0; i < items.length; i += chunkSize) {
|
|
149
|
-
result.push([...items.slice(i, i + chunkSize)]);
|
|
150
|
-
}
|
|
151
|
-
return result;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Get distinct values of a property from array
|
|
156
|
-
*/
|
|
157
|
-
export function distinctByProperty<T>(
|
|
158
|
-
items: readonly T[],
|
|
159
|
-
property: keyof T
|
|
160
|
-
): unknown[] {
|
|
161
|
-
const seen = new Set<unknown>();
|
|
162
|
-
const result: unknown[] = [];
|
|
163
|
-
for (const item of items) {
|
|
164
|
-
const value = item[property];
|
|
165
|
-
if (!seen.has(value)) {
|
|
166
|
-
seen.add(value);
|
|
167
|
-
result.push(value);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
return result;
|
|
171
|
-
}
|
|
9
|
+
export * from './collections';
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Array Filter Utilities
|
|
3
|
+
* Filter operations for arrays of objects
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Filter array by property value
|
|
8
|
+
*/
|
|
9
|
+
export function filterByProperty<T>(
|
|
10
|
+
items: readonly T[],
|
|
11
|
+
property: keyof T,
|
|
12
|
+
value: unknown
|
|
13
|
+
): T[] {
|
|
14
|
+
return items.filter((item) => item[property] === value);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Filter array by predicate function
|
|
19
|
+
*/
|
|
20
|
+
export function filterByPredicate<T>(
|
|
21
|
+
items: readonly T[],
|
|
22
|
+
predicate: (item: T) => boolean
|
|
23
|
+
): T[] {
|
|
24
|
+
return items.filter(predicate);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Filter array by time range (timestamp property)
|
|
29
|
+
*/
|
|
30
|
+
export function filterByTimeRange<T>(
|
|
31
|
+
items: readonly T[],
|
|
32
|
+
timestampProperty: keyof T,
|
|
33
|
+
startTime: number,
|
|
34
|
+
endTime: number
|
|
35
|
+
): T[] {
|
|
36
|
+
return items.filter((item) => {
|
|
37
|
+
const timestamp = item[timestampProperty] as unknown as number;
|
|
38
|
+
return timestamp >= startTime && timestamp <= endTime;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Filter array by multiple property values (OR logic)
|
|
44
|
+
*/
|
|
45
|
+
export function filterByAnyProperty<T>(
|
|
46
|
+
items: readonly T[],
|
|
47
|
+
property: keyof T,
|
|
48
|
+
values: readonly unknown[]
|
|
49
|
+
): T[] {
|
|
50
|
+
const valueSet = new Set(values);
|
|
51
|
+
return items.filter((item) => valueSet.has(item[property]));
|
|
52
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Array Reducer Utilities
|
|
3
|
+
* Reduce, group, chunk, and aggregation operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Reduce array to sum of number property
|
|
8
|
+
*/
|
|
9
|
+
export function sumByProperty<T>(
|
|
10
|
+
items: readonly T[],
|
|
11
|
+
numberProperty: keyof T
|
|
12
|
+
): number {
|
|
13
|
+
return items.reduce((sum, item) => {
|
|
14
|
+
const value = item[numberProperty] as unknown as number;
|
|
15
|
+
return sum + (typeof value === "number" ? value : 0);
|
|
16
|
+
}, 0);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Group array by property value
|
|
21
|
+
*/
|
|
22
|
+
export function groupByProperty<T>(
|
|
23
|
+
items: readonly T[],
|
|
24
|
+
property: keyof T
|
|
25
|
+
): Map<unknown, T[]> {
|
|
26
|
+
const groups = new Map<unknown, T[]>();
|
|
27
|
+
for (const item of items) {
|
|
28
|
+
const key = item[property];
|
|
29
|
+
const existing = groups.get(key);
|
|
30
|
+
if (existing) {
|
|
31
|
+
existing.push(item);
|
|
32
|
+
} else {
|
|
33
|
+
groups.set(key, [item]);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return groups;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Chunk array into smaller arrays of specified size
|
|
41
|
+
*/
|
|
42
|
+
export function chunkArray<T>(items: readonly T[], chunkSize: number): T[][] {
|
|
43
|
+
const result: T[][] = [];
|
|
44
|
+
for (let i = 0; i < items.length; i += chunkSize) {
|
|
45
|
+
result.push([...items.slice(i, i + chunkSize)]);
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get distinct values of a property from array
|
|
52
|
+
*/
|
|
53
|
+
export function distinctByProperty<T>(
|
|
54
|
+
items: readonly T[],
|
|
55
|
+
property: keyof T
|
|
56
|
+
): unknown[] {
|
|
57
|
+
const seen = new Set<unknown>();
|
|
58
|
+
const result: unknown[] = [];
|
|
59
|
+
for (const item of items) {
|
|
60
|
+
const value = item[property];
|
|
61
|
+
if (!seen.has(value)) {
|
|
62
|
+
seen.add(value);
|
|
63
|
+
result.push(value);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Array Sorter Utilities
|
|
3
|
+
* Sort operations for arrays of objects
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Sort array by date property (descending - newest first)
|
|
8
|
+
*/
|
|
9
|
+
export function sortByDateDescending<T>(
|
|
10
|
+
items: readonly T[],
|
|
11
|
+
dateProperty: keyof T
|
|
12
|
+
): T[] {
|
|
13
|
+
return [...items].sort((a, b) => {
|
|
14
|
+
const timeA = new Date(a[dateProperty] as unknown as string).getTime();
|
|
15
|
+
const timeB = new Date(b[dateProperty] as unknown as string).getTime();
|
|
16
|
+
return timeB - timeA;
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Sort array by date property (ascending - oldest first)
|
|
22
|
+
*/
|
|
23
|
+
export function sortByDateAscending<T>(
|
|
24
|
+
items: readonly T[],
|
|
25
|
+
dateProperty: keyof T
|
|
26
|
+
): T[] {
|
|
27
|
+
return [...items].sort((a, b) => {
|
|
28
|
+
const timeA = new Date(a[dateProperty] as unknown as string).getTime();
|
|
29
|
+
const timeB = new Date(b[dateProperty] as unknown as string).getTime();
|
|
30
|
+
return timeA - timeB;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Sort array by number property (descending)
|
|
36
|
+
*/
|
|
37
|
+
export function sortByNumberDescending<T>(
|
|
38
|
+
items: readonly T[],
|
|
39
|
+
numberProperty: keyof T
|
|
40
|
+
): T[] {
|
|
41
|
+
return [...items].sort((a, b) => {
|
|
42
|
+
const numA = a[numberProperty] as unknown as number;
|
|
43
|
+
const numB = b[numberProperty] as unknown as number;
|
|
44
|
+
return numB - numA;
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Sort array by number property (ascending)
|
|
50
|
+
*/
|
|
51
|
+
export function sortByNumberAscending<T>(
|
|
52
|
+
items: readonly T[],
|
|
53
|
+
numberProperty: keyof T
|
|
54
|
+
): T[] {
|
|
55
|
+
return [...items].sort((a, b) => {
|
|
56
|
+
const numA = a[numberProperty] as unknown as number;
|
|
57
|
+
const numB = b[numberProperty] as unknown as number;
|
|
58
|
+
return numA - numB;
|
|
59
|
+
});
|
|
60
|
+
}
|
|
@@ -56,9 +56,14 @@ export class CostTracker {
|
|
|
56
56
|
};
|
|
57
57
|
}
|
|
58
58
|
} catch (error) {
|
|
59
|
-
//
|
|
59
|
+
// Log error but continue with default cost info
|
|
60
|
+
console.warn(
|
|
61
|
+
`[cost-tracker] Failed to get model cost info for ${modelId}:`,
|
|
62
|
+
error instanceof Error ? error.message : String(error)
|
|
63
|
+
);
|
|
60
64
|
}
|
|
61
65
|
|
|
66
|
+
// Return default cost info (0 cost) if model not found or error occurred
|
|
62
67
|
return {
|
|
63
68
|
model: modelId,
|
|
64
69
|
costPerRequest: 0,
|