@umituz/react-native-ai-pruna-provider 1.0.8 → 1.0.9
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/domain/entities/error.types.ts +0 -21
- package/src/exports/domain.ts +0 -2
- package/src/infrastructure/services/pruna-api-client.ts +8 -3
- package/src/infrastructure/services/pruna-provider-subscription.ts +8 -3
- package/src/infrastructure/services/pruna-queue-operations.ts +7 -5
- package/src/infrastructure/utils/log-collector.ts +9 -0
- package/src/infrastructure/utils/type-guards/index.ts +1 -1
- package/src/init/createAiProviderInitModule.ts +11 -1
- package/src/presentation/hooks/use-pruna-generation.ts +27 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-pruna-provider",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9",
|
|
4
4
|
"description": "Pruna AI provider for React Native - implements IAIProvider interface for unified AI generation",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -19,12 +19,6 @@ export enum PrunaErrorType {
|
|
|
19
19
|
UNKNOWN = "unknown",
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
export interface PrunaErrorCategory {
|
|
23
|
-
readonly type: PrunaErrorType;
|
|
24
|
-
readonly messageKey: string;
|
|
25
|
-
readonly retryable: boolean;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
22
|
export interface PrunaErrorInfo {
|
|
29
23
|
readonly type: PrunaErrorType;
|
|
30
24
|
readonly messageKey: string;
|
|
@@ -35,18 +29,3 @@ export interface PrunaErrorInfo {
|
|
|
35
29
|
readonly statusCode?: number;
|
|
36
30
|
}
|
|
37
31
|
|
|
38
|
-
export interface PrunaErrorMessages {
|
|
39
|
-
network?: string;
|
|
40
|
-
timeout?: string;
|
|
41
|
-
api_error?: string;
|
|
42
|
-
validation?: string;
|
|
43
|
-
content_policy?: string;
|
|
44
|
-
rate_limit?: string;
|
|
45
|
-
authentication?: string;
|
|
46
|
-
quota_exceeded?: string;
|
|
47
|
-
model_not_found?: string;
|
|
48
|
-
file_upload?: string;
|
|
49
|
-
polling_timeout?: string;
|
|
50
|
-
invalid_image?: string;
|
|
51
|
-
unknown?: string;
|
|
52
|
-
}
|
package/src/exports/domain.ts
CHANGED
|
@@ -29,6 +29,11 @@ export async function uploadFileToStorage(
|
|
|
29
29
|
apiKey: string,
|
|
30
30
|
sessionId: string,
|
|
31
31
|
): Promise<string> {
|
|
32
|
+
// Guard: empty or whitespace-only input
|
|
33
|
+
if (!base64Data || !base64Data.trim()) {
|
|
34
|
+
throw new Error("File data is empty. Provide a base64 string or URL.");
|
|
35
|
+
}
|
|
36
|
+
|
|
32
37
|
// Already a URL — return as-is
|
|
33
38
|
if (base64Data.startsWith('http')) {
|
|
34
39
|
generationLogCollector.log(sessionId, TAG, 'File already a URL, skipping upload');
|
|
@@ -82,9 +87,6 @@ export async function uploadFileToStorage(
|
|
|
82
87
|
return fileUrl;
|
|
83
88
|
}
|
|
84
89
|
|
|
85
|
-
/** @deprecated Use uploadFileToStorage instead */
|
|
86
|
-
export const uploadImageToFiles = uploadFileToStorage;
|
|
87
|
-
|
|
88
90
|
/**
|
|
89
91
|
* Strip base64 data URI prefix, returning raw base64 string.
|
|
90
92
|
* If input is already a URL, returns it unchanged.
|
|
@@ -104,6 +106,7 @@ export async function submitPrediction(
|
|
|
104
106
|
input: Record<string, unknown>,
|
|
105
107
|
apiKey: string,
|
|
106
108
|
sessionId: string,
|
|
109
|
+
signal?: AbortSignal,
|
|
107
110
|
): Promise<PrunaPredictionResponse> {
|
|
108
111
|
generationLogCollector.log(sessionId, TAG, `Submitting prediction for model: ${model}`, {
|
|
109
112
|
inputKeys: Object.keys(input),
|
|
@@ -120,6 +123,7 @@ export async function submitPrediction(
|
|
|
120
123
|
'Content-Type': 'application/json',
|
|
121
124
|
},
|
|
122
125
|
body: JSON.stringify({ input }),
|
|
126
|
+
signal,
|
|
123
127
|
});
|
|
124
128
|
|
|
125
129
|
if (!response.ok) {
|
|
@@ -176,6 +180,7 @@ export async function pollForResult(
|
|
|
176
180
|
try {
|
|
177
181
|
const statusRes = await fetch(fullPollUrl, {
|
|
178
182
|
headers: { 'apikey': apiKey },
|
|
183
|
+
signal,
|
|
179
184
|
});
|
|
180
185
|
|
|
181
186
|
if (!statusRes.ok) {
|
|
@@ -64,7 +64,7 @@ async function singleSubscribeAttempt<T = unknown>(
|
|
|
64
64
|
// Notify progress: IN_PROGRESS
|
|
65
65
|
options?.onProgress?.({ progress: -1, status: "IN_PROGRESS" });
|
|
66
66
|
|
|
67
|
-
const response = await submitPrediction(model, modelInput, apiKey, sessionId);
|
|
67
|
+
const response = await submitPrediction(model, modelInput, apiKey, sessionId, signal);
|
|
68
68
|
let uri = extractUri(response);
|
|
69
69
|
|
|
70
70
|
// If no immediate result, poll for async result
|
|
@@ -118,6 +118,10 @@ async function singleSubscribeAttempt<T = unknown>(
|
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
// Prevent unhandled rejection if predictionPromise loses the race
|
|
122
|
+
// (timeout or abort wins → prediction may reject later with no handler)
|
|
123
|
+
predictionPromise.catch(() => {});
|
|
124
|
+
|
|
121
125
|
const resultUrl = await Promise.race(promises) as string;
|
|
122
126
|
const requestId = `pruna_${model}_${Date.now()}`;
|
|
123
127
|
|
|
@@ -205,7 +209,8 @@ export async function handlePrunaSubscription<T = unknown>(
|
|
|
205
209
|
}
|
|
206
210
|
}
|
|
207
211
|
|
|
208
|
-
|
|
212
|
+
// Unreachable: loop always returns or throws. TypeScript safety net.
|
|
213
|
+
throw lastError instanceof Error ? lastError : new Error("Subscription failed after all retry attempts.");
|
|
209
214
|
}
|
|
210
215
|
|
|
211
216
|
/**
|
|
@@ -226,7 +231,7 @@ export async function handlePrunaRun<T = unknown>(
|
|
|
226
231
|
|
|
227
232
|
try {
|
|
228
233
|
const modelInput = await buildModelInput(model, input, apiKey, sessionId);
|
|
229
|
-
const response = await submitPrediction(model, modelInput, apiKey, sessionId);
|
|
234
|
+
const response = await submitPrediction(model, modelInput, apiKey, sessionId, options?.signal);
|
|
230
235
|
|
|
231
236
|
let uri = extractUri(response);
|
|
232
237
|
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
import type { PrunaModelId } from "../../domain/entities/pruna.types";
|
|
7
7
|
import type { JobSubmission, JobStatus } from "../../domain/types";
|
|
8
|
-
import { submitPrediction, extractUri } from "./pruna-api-client";
|
|
8
|
+
import { submitPrediction, extractUri, resolveUri } from "./pruna-api-client";
|
|
9
|
+
import { PRUNA_BASE_URL } from "./pruna-provider.constants";
|
|
9
10
|
import { buildModelInput } from "./pruna-input-builder";
|
|
10
11
|
import { generationLogCollector } from "../utils/log-collector";
|
|
11
12
|
|
|
@@ -62,7 +63,7 @@ export async function getJobStatus(
|
|
|
62
63
|
statusUrl: string,
|
|
63
64
|
apiKey: string,
|
|
64
65
|
): Promise<JobStatus> {
|
|
65
|
-
const fullUrl = statusUrl.startsWith('http') ? statusUrl :
|
|
66
|
+
const fullUrl = statusUrl.startsWith('http') ? statusUrl : `${PRUNA_BASE_URL}${statusUrl}`;
|
|
66
67
|
|
|
67
68
|
const response = await fetch(fullUrl, {
|
|
68
69
|
headers: { 'apikey': apiKey },
|
|
@@ -83,9 +84,11 @@ export async function getJobStatus(
|
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
if (typedData.status === 'failed') {
|
|
87
|
+
const errorMessage = typedData.error || "Generation failed during processing.";
|
|
86
88
|
return {
|
|
87
89
|
status: "FAILED",
|
|
88
90
|
requestId: statusUrl,
|
|
91
|
+
logs: [{ message: errorMessage, level: "error" }],
|
|
89
92
|
};
|
|
90
93
|
}
|
|
91
94
|
|
|
@@ -104,7 +107,7 @@ export async function getJobResult<T = unknown>(
|
|
|
104
107
|
statusUrl: string,
|
|
105
108
|
apiKey: string,
|
|
106
109
|
): Promise<T> {
|
|
107
|
-
const fullUrl = statusUrl.startsWith('http') ? statusUrl :
|
|
110
|
+
const fullUrl = statusUrl.startsWith('http') ? statusUrl : `${PRUNA_BASE_URL}${statusUrl}`;
|
|
108
111
|
|
|
109
112
|
const response = await fetch(fullUrl, {
|
|
110
113
|
headers: { 'apikey': apiKey },
|
|
@@ -126,6 +129,5 @@ export async function getJobResult<T = unknown>(
|
|
|
126
129
|
throw new Error("Result not ready or extraction failed.");
|
|
127
130
|
}
|
|
128
131
|
|
|
129
|
-
|
|
130
|
-
return { url: resolvedUri } as T;
|
|
132
|
+
return { url: resolveUri(uri) } as T;
|
|
131
133
|
}
|
|
@@ -25,10 +25,19 @@ interface Session {
|
|
|
25
25
|
|
|
26
26
|
let sessionCounter = 0;
|
|
27
27
|
|
|
28
|
+
/** Max concurrent sessions before auto-evicting oldest */
|
|
29
|
+
const MAX_SESSIONS = 50;
|
|
30
|
+
|
|
28
31
|
class GenerationLogCollector {
|
|
29
32
|
private sessions = new Map<string, Session>();
|
|
30
33
|
|
|
31
34
|
startSession(): string {
|
|
35
|
+
// Evict oldest sessions if limit exceeded
|
|
36
|
+
if (this.sessions.size >= MAX_SESSIONS) {
|
|
37
|
+
const oldestKey = this.sessions.keys().next().value;
|
|
38
|
+
if (oldestKey) this.sessions.delete(oldestKey);
|
|
39
|
+
}
|
|
40
|
+
|
|
32
41
|
const id = `pruna_session_${++sessionCounter}_${Date.now()}`;
|
|
33
42
|
this.sessions.set(id, { startTime: Date.now(), entries: [] });
|
|
34
43
|
return id;
|
|
@@ -27,5 +27,5 @@ export function isValidPrompt(value: unknown): value is string {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export function isValidTimeout(value: unknown): value is number {
|
|
30
|
-
return typeof value === 'number' && Number.isInteger(value) && value >= 1 && value <=
|
|
30
|
+
return typeof value === 'number' && Number.isInteger(value) && value >= 1 && value <= 3600000;
|
|
31
31
|
}
|
|
@@ -35,6 +35,13 @@ export interface AiProviderInitModuleConfig {
|
|
|
35
35
|
*/
|
|
36
36
|
dependsOn?: string[];
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Whether to set Pruna as the active provider after initialization.
|
|
40
|
+
* When false, registers the provider but doesn't make it active.
|
|
41
|
+
* @default true
|
|
42
|
+
*/
|
|
43
|
+
setAsActive?: boolean;
|
|
44
|
+
|
|
38
45
|
/**
|
|
39
46
|
* Optional callback called after provider is initialized
|
|
40
47
|
*/
|
|
@@ -51,6 +58,7 @@ export function createAiProviderInitModule(
|
|
|
51
58
|
getApiKey,
|
|
52
59
|
critical = false,
|
|
53
60
|
dependsOn = ['firebase'],
|
|
61
|
+
setAsActive = true,
|
|
54
62
|
onInitialized,
|
|
55
63
|
} = config;
|
|
56
64
|
|
|
@@ -71,7 +79,9 @@ export function createAiProviderInitModule(
|
|
|
71
79
|
if (!providerRegistry.hasProvider(prunaProvider.providerId)) {
|
|
72
80
|
providerRegistry.register(prunaProvider);
|
|
73
81
|
}
|
|
74
|
-
|
|
82
|
+
if (setAsActive) {
|
|
83
|
+
providerRegistry.setActiveProvider(prunaProvider.providerId);
|
|
84
|
+
}
|
|
75
85
|
|
|
76
86
|
if (onInitialized) {
|
|
77
87
|
onInitialized();
|
|
@@ -49,8 +49,10 @@ export function usePrunaGeneration<T = unknown>(
|
|
|
49
49
|
const [error, setError] = useState<PrunaErrorInfo | null>(null);
|
|
50
50
|
const [isLoading, setIsLoading] = useState(false);
|
|
51
51
|
const [isCancelling, setIsCancelling] = useState(false);
|
|
52
|
+
const [requestId, setRequestId] = useState<string | null>(null);
|
|
52
53
|
|
|
53
54
|
const stateManagerRef = useRef<PrunaGenerationStateManager<T> | null>(null);
|
|
55
|
+
const abortControllerRef = useRef<AbortController | null>(null);
|
|
54
56
|
const optionsRef = useRef(options);
|
|
55
57
|
|
|
56
58
|
useEffect(() => {
|
|
@@ -73,12 +75,10 @@ export function usePrunaGeneration<T = unknown>(
|
|
|
73
75
|
stateManagerRef.current = null;
|
|
74
76
|
}
|
|
75
77
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
console.warn('[usePrunaGeneration] Error cancelling request on unmount:', error);
|
|
81
|
-
}
|
|
78
|
+
// Cancel only this hook's active request on unmount
|
|
79
|
+
if (abortControllerRef.current) {
|
|
80
|
+
abortControllerRef.current.abort();
|
|
81
|
+
abortControllerRef.current = null;
|
|
82
82
|
}
|
|
83
83
|
};
|
|
84
84
|
}, []);
|
|
@@ -88,10 +88,18 @@ export function usePrunaGeneration<T = unknown>(
|
|
|
88
88
|
const stateManager = stateManagerRef.current;
|
|
89
89
|
if (!stateManager || !stateManager.checkMounted()) return null;
|
|
90
90
|
|
|
91
|
+
// Cancel any previous in-flight request from this hook
|
|
92
|
+
if (abortControllerRef.current) {
|
|
93
|
+
abortControllerRef.current.abort();
|
|
94
|
+
}
|
|
95
|
+
const controller = new AbortController();
|
|
96
|
+
abortControllerRef.current = controller;
|
|
97
|
+
|
|
91
98
|
stateManager.setLastRequest(model, input);
|
|
92
99
|
setIsLoading(true);
|
|
93
100
|
setError(null);
|
|
94
101
|
setData(null);
|
|
102
|
+
setRequestId(null);
|
|
95
103
|
stateManager.setCurrentRequestId(null);
|
|
96
104
|
setIsCancelling(false);
|
|
97
105
|
|
|
@@ -104,6 +112,11 @@ export function usePrunaGeneration<T = unknown>(
|
|
|
104
112
|
stateManager.getCurrentRequestId()
|
|
105
113
|
);
|
|
106
114
|
stateManager.handleQueueUpdate(prunaStatus);
|
|
115
|
+
|
|
116
|
+
// Update reactive requestId from queue status
|
|
117
|
+
if (status.requestId) {
|
|
118
|
+
setRequestId(status.requestId);
|
|
119
|
+
}
|
|
107
120
|
},
|
|
108
121
|
});
|
|
109
122
|
|
|
@@ -121,6 +134,10 @@ export function usePrunaGeneration<T = unknown>(
|
|
|
121
134
|
setIsLoading(false);
|
|
122
135
|
setIsCancelling(false);
|
|
123
136
|
}
|
|
137
|
+
// Clean up controller reference
|
|
138
|
+
if (abortControllerRef.current === controller) {
|
|
139
|
+
abortControllerRef.current = null;
|
|
140
|
+
}
|
|
124
141
|
}
|
|
125
142
|
},
|
|
126
143
|
[]
|
|
@@ -137,9 +154,10 @@ export function usePrunaGeneration<T = unknown>(
|
|
|
137
154
|
}, [generate]);
|
|
138
155
|
|
|
139
156
|
const cancel = useCallback(() => {
|
|
140
|
-
if (
|
|
157
|
+
if (abortControllerRef.current) {
|
|
141
158
|
setIsCancelling(true);
|
|
142
|
-
|
|
159
|
+
abortControllerRef.current.abort();
|
|
160
|
+
abortControllerRef.current = null;
|
|
143
161
|
}
|
|
144
162
|
}, []);
|
|
145
163
|
|
|
@@ -149,11 +167,10 @@ export function usePrunaGeneration<T = unknown>(
|
|
|
149
167
|
setError(null);
|
|
150
168
|
setIsLoading(false);
|
|
151
169
|
setIsCancelling(false);
|
|
170
|
+
setRequestId(null);
|
|
152
171
|
stateManagerRef.current?.clearLastRequest();
|
|
153
172
|
}, [cancel]);
|
|
154
173
|
|
|
155
|
-
const requestId = stateManagerRef.current?.getCurrentRequestId() ?? null;
|
|
156
|
-
|
|
157
174
|
return {
|
|
158
175
|
data,
|
|
159
176
|
error,
|