@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-ai-pruna-provider",
3
- "version": "1.0.8",
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
- }
@@ -23,9 +23,7 @@ export type {
23
23
 
24
24
  export { PrunaErrorType } from "../domain/entities/error.types";
25
25
  export type {
26
- PrunaErrorCategory,
27
26
  PrunaErrorInfo,
28
- PrunaErrorMessages,
29
27
  } from "../domain/entities/error.types";
30
28
 
31
29
  export type {
@@ -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
- throw lastError;
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 : `https://api.pruna.ai${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 : `https://api.pruna.ai${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
- const resolvedUri = uri.startsWith('/') ? `https://api.pruna.ai${uri}` : uri;
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 <= 600000;
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
- providerRegistry.setActiveProvider(prunaProvider.providerId);
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
- if (prunaProvider.hasRunningRequest()) {
77
- try {
78
- prunaProvider.cancelCurrentRequest();
79
- } catch (error) {
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 (prunaProvider.hasRunningRequest()) {
157
+ if (abortControllerRef.current) {
141
158
  setIsCancelling(true);
142
- prunaProvider.cancelCurrentRequest();
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,