@umituz/react-native-ai-pruna-provider 1.0.0
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/README.md +447 -0
- package/package.json +58 -0
- package/src/domain/entities/error.types.ts +52 -0
- package/src/domain/entities/pruna.types.ts +112 -0
- package/src/domain/types/index.ts +21 -0
- package/src/exports/domain.ts +46 -0
- package/src/exports/infrastructure.ts +47 -0
- package/src/exports/presentation.ts +14 -0
- package/src/index.ts +27 -0
- package/src/infrastructure/services/index.ts +12 -0
- package/src/infrastructure/services/pruna-api-client.ts +243 -0
- package/src/infrastructure/services/pruna-input-builder.ts +127 -0
- package/src/infrastructure/services/pruna-provider-subscription.ts +262 -0
- package/src/infrastructure/services/pruna-provider.constants.ts +83 -0
- package/src/infrastructure/services/pruna-provider.ts +211 -0
- package/src/infrastructure/services/pruna-queue-operations.ts +131 -0
- package/src/infrastructure/services/request-store.ts +148 -0
- package/src/infrastructure/utils/helpers/index.ts +25 -0
- package/src/infrastructure/utils/index.ts +30 -0
- package/src/infrastructure/utils/log-collector.ts +96 -0
- package/src/infrastructure/utils/pruna-error-handler.util.ts +119 -0
- package/src/infrastructure/utils/pruna-generation-state-manager.util.ts +98 -0
- package/src/infrastructure/utils/type-guards/index.ts +31 -0
- package/src/init/createAiProviderInitModule.ts +87 -0
- package/src/init/initializePrunaProvider.ts +35 -0
- package/src/presentation/hooks/index.ts +6 -0
- package/src/presentation/hooks/use-pruna-generation.ts +169 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request Store - Promise Deduplication with globalThis
|
|
3
|
+
* Survives hot reloads for React Native development
|
|
4
|
+
*
|
|
5
|
+
* React Native is single-threaded - no lock mechanism needed.
|
|
6
|
+
* Direct Map operations are atomic in JS event loop.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface ActiveRequest<T = unknown> {
|
|
10
|
+
promise: Promise<T>;
|
|
11
|
+
abortController: AbortController;
|
|
12
|
+
createdAt: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const STORE_KEY = "__PRUNA_PROVIDER_REQUESTS__";
|
|
16
|
+
const TIMER_KEY = "__PRUNA_PROVIDER_CLEANUP_TIMER__";
|
|
17
|
+
type RequestStore = Map<string, ActiveRequest>;
|
|
18
|
+
|
|
19
|
+
const CLEANUP_INTERVAL = 60000;
|
|
20
|
+
const MAX_REQUEST_AGE = 300000;
|
|
21
|
+
|
|
22
|
+
function getCleanupTimer(): ReturnType<typeof setInterval> | null {
|
|
23
|
+
const globalObj = globalThis as Record<string, unknown>;
|
|
24
|
+
return (globalObj[TIMER_KEY] as ReturnType<typeof setInterval>) ?? null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function setCleanupTimer(timer: ReturnType<typeof setInterval> | null): void {
|
|
28
|
+
const globalObj = globalThis as Record<string, unknown>;
|
|
29
|
+
globalObj[TIMER_KEY] = timer;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getRequestStore(): RequestStore {
|
|
33
|
+
const globalObj = globalThis as Record<string, unknown>;
|
|
34
|
+
if (!globalObj[STORE_KEY]) {
|
|
35
|
+
globalObj[STORE_KEY] = new Map();
|
|
36
|
+
}
|
|
37
|
+
return globalObj[STORE_KEY] as RequestStore;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function sortKeys(obj: unknown): unknown {
|
|
41
|
+
if (obj === null || typeof obj !== "object") return obj;
|
|
42
|
+
if (Array.isArray(obj)) return obj.map(sortKeys);
|
|
43
|
+
const sorted: Record<string, unknown> = {};
|
|
44
|
+
for (const key of Object.keys(obj as Record<string, unknown>).sort()) {
|
|
45
|
+
sorted[key] = sortKeys((obj as Record<string, unknown>)[key]);
|
|
46
|
+
}
|
|
47
|
+
return sorted;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function createRequestKey(model: string, input: Record<string, unknown>): string {
|
|
51
|
+
const inputStr = JSON.stringify(sortKeys(input));
|
|
52
|
+
let hash = 0;
|
|
53
|
+
for (let i = 0; i < inputStr.length; i++) {
|
|
54
|
+
const char = inputStr.charCodeAt(i);
|
|
55
|
+
hash = ((hash << 5) - hash + char) | 0;
|
|
56
|
+
}
|
|
57
|
+
return `${model}:${hash.toString(36)}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function getExistingRequest<T>(key: string): ActiveRequest<T> | undefined {
|
|
61
|
+
return getRequestStore().get(key) as ActiveRequest<T> | undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function storeRequest<T>(key: string, request: ActiveRequest<T>): void {
|
|
65
|
+
getRequestStore().set(key, {
|
|
66
|
+
...request,
|
|
67
|
+
createdAt: request.createdAt ?? Date.now(),
|
|
68
|
+
});
|
|
69
|
+
ensureCleanupRunning();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function removeRequest(key: string): void {
|
|
73
|
+
getRequestStore().delete(key);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function cancelRequest(key: string): void {
|
|
77
|
+
const store = getRequestStore();
|
|
78
|
+
const req = store.get(key);
|
|
79
|
+
if (req) {
|
|
80
|
+
req.abortController.abort();
|
|
81
|
+
store.delete(key);
|
|
82
|
+
if (store.size === 0) stopCleanupTimer();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function cancelAllRequests(): void {
|
|
87
|
+
const store = getRequestStore();
|
|
88
|
+
store.forEach((req) => {
|
|
89
|
+
req.abortController.abort();
|
|
90
|
+
});
|
|
91
|
+
store.clear();
|
|
92
|
+
stopCleanupTimer();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function hasActiveRequests(): boolean {
|
|
96
|
+
return getRequestStore().size > 0;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function cleanupRequestStore(maxAge: number = MAX_REQUEST_AGE): number {
|
|
100
|
+
const store = getRequestStore();
|
|
101
|
+
const now = Date.now();
|
|
102
|
+
let cleanedCount = 0;
|
|
103
|
+
|
|
104
|
+
for (const [key, request] of store.entries()) {
|
|
105
|
+
if (now - request.createdAt > maxAge) {
|
|
106
|
+
request.abortController.abort();
|
|
107
|
+
store.delete(key);
|
|
108
|
+
cleanedCount++;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (store.size === 0) {
|
|
113
|
+
stopCleanupTimer();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return cleanedCount;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function ensureCleanupRunning(): void {
|
|
120
|
+
if (getCleanupTimer()) return;
|
|
121
|
+
|
|
122
|
+
const timer = setInterval(() => {
|
|
123
|
+
cleanupRequestStore(MAX_REQUEST_AGE);
|
|
124
|
+
}, CLEANUP_INTERVAL);
|
|
125
|
+
|
|
126
|
+
setCleanupTimer(timer);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function stopCleanupTimer(): void {
|
|
130
|
+
const timer = getCleanupTimer();
|
|
131
|
+
if (timer) {
|
|
132
|
+
clearInterval(timer);
|
|
133
|
+
setCleanupTimer(null);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function stopAutomaticCleanup(): void {
|
|
138
|
+
stopCleanupTimer();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Clear any leftover timer on module load (hot reload safety)
|
|
142
|
+
if (typeof globalThis !== "undefined") {
|
|
143
|
+
const existingTimer = getCleanupTimer();
|
|
144
|
+
if (existingTimer) {
|
|
145
|
+
clearInterval(existingTimer);
|
|
146
|
+
setCleanupTimer(null);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers Index
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export function isDefined<T>(value: T | null | undefined): value is T {
|
|
6
|
+
return value !== null && value !== undefined;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function removeNullish<T extends Record<string, unknown>>(obj: T): Partial<T> {
|
|
10
|
+
const result: Partial<T> = {};
|
|
11
|
+
for (const key of Object.keys(obj) as (keyof T)[]) {
|
|
12
|
+
if (obj[key] !== null && obj[key] !== undefined) {
|
|
13
|
+
result[key] = obj[key];
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function generateUniqueId(prefix = 'pruna'): string {
|
|
20
|
+
return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function sleep(ms: number): Promise<void> {
|
|
24
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
25
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utils Index
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
mapPrunaError,
|
|
7
|
+
isPrunaErrorRetryable,
|
|
8
|
+
getErrorMessage,
|
|
9
|
+
getErrorMessageOr,
|
|
10
|
+
formatErrorMessage,
|
|
11
|
+
} from "./pruna-error-handler.util";
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
isPrunaModelId,
|
|
15
|
+
isPrunaErrorType,
|
|
16
|
+
isValidApiKey,
|
|
17
|
+
isValidModelId,
|
|
18
|
+
isValidPrompt,
|
|
19
|
+
isValidTimeout,
|
|
20
|
+
} from "./type-guards";
|
|
21
|
+
|
|
22
|
+
export {
|
|
23
|
+
isDefined,
|
|
24
|
+
removeNullish,
|
|
25
|
+
generateUniqueId,
|
|
26
|
+
sleep,
|
|
27
|
+
} from "./helpers";
|
|
28
|
+
|
|
29
|
+
export { generationLogCollector } from "./log-collector";
|
|
30
|
+
export type { LogEntry } from "./log-collector";
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generation Log Collector
|
|
3
|
+
* Session-scoped log collection — each generation gets its own isolated session.
|
|
4
|
+
* Supports concurrent generations without data corruption.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* const sessionId = collector.startSession();
|
|
8
|
+
* collector.log(sessionId, 'tag', 'message');
|
|
9
|
+
* const entries = collector.endSession(sessionId);
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export interface LogEntry {
|
|
13
|
+
readonly timestamp: number;
|
|
14
|
+
readonly elapsed: number;
|
|
15
|
+
readonly level: 'info' | 'warn' | 'error';
|
|
16
|
+
readonly tag: string;
|
|
17
|
+
readonly message: string;
|
|
18
|
+
readonly data?: Record<string, unknown>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface Session {
|
|
22
|
+
readonly startTime: number;
|
|
23
|
+
entries: LogEntry[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let sessionCounter = 0;
|
|
27
|
+
|
|
28
|
+
class GenerationLogCollector {
|
|
29
|
+
private sessions = new Map<string, Session>();
|
|
30
|
+
|
|
31
|
+
startSession(): string {
|
|
32
|
+
const id = `pruna_session_${++sessionCounter}_${Date.now()}`;
|
|
33
|
+
this.sessions.set(id, { startTime: Date.now(), entries: [] });
|
|
34
|
+
return id;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
log(sessionId: string, tag: string, message: string, data?: Record<string, unknown>): void {
|
|
38
|
+
this.addEntry(sessionId, 'info', tag, message, data);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
warn(sessionId: string, tag: string, message: string, data?: Record<string, unknown>): void {
|
|
42
|
+
this.addEntry(sessionId, 'warn', tag, message, data);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
error(sessionId: string, tag: string, message: string, data?: Record<string, unknown>): void {
|
|
46
|
+
this.addEntry(sessionId, 'error', tag, message, data);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
getEntries(sessionId: string): LogEntry[] {
|
|
50
|
+
return [...(this.sessions.get(sessionId)?.entries ?? [])];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
endSession(sessionId: string): LogEntry[] {
|
|
54
|
+
const session = this.sessions.get(sessionId);
|
|
55
|
+
if (!session) return [];
|
|
56
|
+
const entries = [...session.entries];
|
|
57
|
+
this.sessions.delete(sessionId);
|
|
58
|
+
return entries;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private addEntry(
|
|
62
|
+
sessionId: string,
|
|
63
|
+
level: LogEntry['level'],
|
|
64
|
+
tag: string,
|
|
65
|
+
message: string,
|
|
66
|
+
data?: Record<string, unknown>,
|
|
67
|
+
): void {
|
|
68
|
+
const session = this.sessions.get(sessionId);
|
|
69
|
+
if (!session) {
|
|
70
|
+
this.consoleOutput(level, tag, message, data);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const now = Date.now();
|
|
75
|
+
session.entries.push({
|
|
76
|
+
timestamp: now,
|
|
77
|
+
elapsed: now - session.startTime,
|
|
78
|
+
level,
|
|
79
|
+
tag,
|
|
80
|
+
message,
|
|
81
|
+
...(data && { data }),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
this.consoleOutput(level, tag, message, data);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private consoleOutput(level: LogEntry['level'], tag: string, message: string, data?: Record<string, unknown>): void {
|
|
88
|
+
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
89
|
+
const fn = level === 'error' ? console.error : level === 'warn' ? console.warn : console.log;
|
|
90
|
+
fn(`[${tag}] ${message}`, data ?? '');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Module-level singleton — safe for concurrent sessions via session IDs */
|
|
96
|
+
export const generationLogCollector = new GenerationLogCollector();
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pruna Error Handler
|
|
3
|
+
* Maps raw errors to typed PrunaErrorInfo with retryable classification
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { PrunaErrorType } from "../../domain/entities/error.types";
|
|
7
|
+
import type { PrunaErrorInfo } from "../../domain/entities/error.types";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Map an error to a typed PrunaErrorInfo
|
|
11
|
+
*/
|
|
12
|
+
export function mapPrunaError(error: unknown): PrunaErrorInfo {
|
|
13
|
+
const originalError = getErrorMessage(error);
|
|
14
|
+
const originalErrorName = error instanceof Error ? error.name : undefined;
|
|
15
|
+
const stack = error instanceof Error ? error.stack : undefined;
|
|
16
|
+
const statusCode = (error as Error & { statusCode?: number }).statusCode;
|
|
17
|
+
|
|
18
|
+
// HTTP status code mapping
|
|
19
|
+
if (statusCode !== undefined) {
|
|
20
|
+
return mapStatusCode(statusCode, originalError, originalErrorName, stack);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Message pattern matching
|
|
24
|
+
const msg = originalError.toLowerCase();
|
|
25
|
+
|
|
26
|
+
if (msg.includes("network") || msg.includes("fetch") || msg.includes("econnrefused") || msg.includes("enotfound")) {
|
|
27
|
+
return buildErrorInfo(PrunaErrorType.NETWORK, "error.pruna.network", true, originalError, originalErrorName, stack);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (msg.includes("timeout") || msg.includes("timed out") || msg.includes("polling attempts reached")) {
|
|
31
|
+
return buildErrorInfo(PrunaErrorType.POLLING_TIMEOUT, "error.pruna.polling_timeout", true, originalError, originalErrorName, stack);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (msg.includes("file upload")) {
|
|
35
|
+
return buildErrorInfo(PrunaErrorType.FILE_UPLOAD, "error.pruna.file_upload", true, originalError, originalErrorName, stack);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (msg.includes("invalid image") || msg.includes("invalid character")) {
|
|
39
|
+
return buildErrorInfo(PrunaErrorType.INVALID_IMAGE, "error.pruna.invalid_image", false, originalError, originalErrorName, stack);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (msg.includes("cancelled by user")) {
|
|
43
|
+
return buildErrorInfo(PrunaErrorType.UNKNOWN, "error.pruna.cancelled", false, originalError, originalErrorName, stack);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (msg.includes("prompt is required") || msg.includes("image is required")) {
|
|
47
|
+
return buildErrorInfo(PrunaErrorType.VALIDATION, "error.pruna.validation", false, originalError, originalErrorName, stack);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return buildErrorInfo(PrunaErrorType.UNKNOWN, "error.pruna.unknown", false, originalError, originalErrorName, stack);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function mapStatusCode(
|
|
54
|
+
statusCode: number,
|
|
55
|
+
originalError: string,
|
|
56
|
+
originalErrorName: string | undefined,
|
|
57
|
+
stack: string | undefined,
|
|
58
|
+
): PrunaErrorInfo {
|
|
59
|
+
if (statusCode === 400 || statusCode === 422) {
|
|
60
|
+
return buildErrorInfo(PrunaErrorType.VALIDATION, "error.pruna.validation", false, originalError, originalErrorName, stack, statusCode);
|
|
61
|
+
}
|
|
62
|
+
if (statusCode === 401 || statusCode === 403) {
|
|
63
|
+
return buildErrorInfo(PrunaErrorType.AUTHENTICATION, "error.pruna.authentication", false, originalError, originalErrorName, stack, statusCode);
|
|
64
|
+
}
|
|
65
|
+
if (statusCode === 402) {
|
|
66
|
+
return buildErrorInfo(PrunaErrorType.QUOTA_EXCEEDED, "error.pruna.quota_exceeded", false, originalError, originalErrorName, stack, statusCode);
|
|
67
|
+
}
|
|
68
|
+
if (statusCode === 404) {
|
|
69
|
+
return buildErrorInfo(PrunaErrorType.MODEL_NOT_FOUND, "error.pruna.model_not_found", false, originalError, originalErrorName, stack, statusCode);
|
|
70
|
+
}
|
|
71
|
+
if (statusCode === 429) {
|
|
72
|
+
return buildErrorInfo(PrunaErrorType.RATE_LIMIT, "error.pruna.rate_limit", true, originalError, originalErrorName, stack, statusCode);
|
|
73
|
+
}
|
|
74
|
+
if (statusCode >= 500 && statusCode <= 504) {
|
|
75
|
+
return buildErrorInfo(PrunaErrorType.API_ERROR, "error.pruna.api_error", true, originalError, originalErrorName, stack, statusCode);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return buildErrorInfo(PrunaErrorType.UNKNOWN, "error.pruna.unknown", false, originalError, originalErrorName, stack, statusCode);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function buildErrorInfo(
|
|
82
|
+
type: PrunaErrorType,
|
|
83
|
+
messageKey: string,
|
|
84
|
+
retryable: boolean,
|
|
85
|
+
originalError: string,
|
|
86
|
+
originalErrorName: string | undefined,
|
|
87
|
+
stack: string | undefined,
|
|
88
|
+
statusCode?: number,
|
|
89
|
+
): PrunaErrorInfo {
|
|
90
|
+
return {
|
|
91
|
+
type,
|
|
92
|
+
messageKey,
|
|
93
|
+
retryable,
|
|
94
|
+
originalError,
|
|
95
|
+
...(originalErrorName && { originalErrorName }),
|
|
96
|
+
...(stack && { stack }),
|
|
97
|
+
...(statusCode !== undefined && { statusCode }),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function isPrunaErrorRetryable(error: PrunaErrorInfo): boolean {
|
|
102
|
+
return error.retryable;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function getErrorMessage(error: unknown): string {
|
|
106
|
+
if (error instanceof Error) return error.message;
|
|
107
|
+
if (typeof error === 'string') return error;
|
|
108
|
+
return String(error);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function getErrorMessageOr(error: unknown, fallback: string): string {
|
|
112
|
+
const msg = getErrorMessage(error);
|
|
113
|
+
return msg || fallback;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function formatErrorMessage(error: unknown, context: string): string {
|
|
117
|
+
const msg = getErrorMessage(error);
|
|
118
|
+
return `[${context}] ${msg}`;
|
|
119
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pruna Generation State Manager
|
|
3
|
+
* Manages state and refs for Pruna generation operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { PrunaJobInput, PrunaLogEntry, PrunaQueueStatus } from "../../domain/entities/pruna.types";
|
|
7
|
+
|
|
8
|
+
export interface GenerationState<T> {
|
|
9
|
+
data: T | null;
|
|
10
|
+
error: Error | null;
|
|
11
|
+
isLoading: boolean;
|
|
12
|
+
isCancelling: boolean;
|
|
13
|
+
requestId: string | null;
|
|
14
|
+
lastRequest: { endpoint: string; input: PrunaJobInput } | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface GenerationStateOptions<T> {
|
|
18
|
+
onQueueUpdate?: (status: PrunaQueueStatus) => void;
|
|
19
|
+
onProgress?: (status: PrunaQueueStatus) => void;
|
|
20
|
+
onError?: (error: Error) => void;
|
|
21
|
+
onResult?: (result: T) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class PrunaGenerationStateManager<T> {
|
|
25
|
+
private isMounted = true;
|
|
26
|
+
private currentRequestId: string | null = null;
|
|
27
|
+
private lastRequest: { endpoint: string; input: PrunaJobInput } | null = null;
|
|
28
|
+
private lastNotifiedStatus: string | null = null;
|
|
29
|
+
|
|
30
|
+
constructor(
|
|
31
|
+
private options?: GenerationStateOptions<T>
|
|
32
|
+
) {}
|
|
33
|
+
|
|
34
|
+
setIsMounted(mounted: boolean): void {
|
|
35
|
+
this.isMounted = mounted;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
checkMounted(): boolean {
|
|
39
|
+
return this.isMounted;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
setCurrentRequestId(requestId: string | null): void {
|
|
43
|
+
this.currentRequestId = requestId;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
getCurrentRequestId(): string | null {
|
|
47
|
+
return this.currentRequestId;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
setLastRequest(endpoint: string, input: PrunaJobInput): void {
|
|
51
|
+
this.lastRequest = { endpoint, input };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
getLastRequest(): { endpoint: string; input: PrunaJobInput } | null {
|
|
55
|
+
return this.lastRequest;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
clearLastRequest(): void {
|
|
59
|
+
this.lastRequest = null;
|
|
60
|
+
this.currentRequestId = null;
|
|
61
|
+
this.lastNotifiedStatus = null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
handleQueueUpdate(status: PrunaQueueStatus): void {
|
|
65
|
+
if (!this.isMounted) return;
|
|
66
|
+
|
|
67
|
+
if (status.requestId) {
|
|
68
|
+
this.currentRequestId = status.requestId;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const normalizedStatus: PrunaQueueStatus = {
|
|
72
|
+
status: status.status,
|
|
73
|
+
requestId: status.requestId ?? this.currentRequestId ?? "",
|
|
74
|
+
logs: status.logs?.map((log: PrunaLogEntry) => ({
|
|
75
|
+
message: log.message,
|
|
76
|
+
level: log.level,
|
|
77
|
+
timestamp: log.timestamp,
|
|
78
|
+
})),
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const statusKey = `${normalizedStatus.status}-${normalizedStatus.requestId}`;
|
|
82
|
+
if (this.lastNotifiedStatus !== statusKey) {
|
|
83
|
+
this.lastNotifiedStatus = statusKey;
|
|
84
|
+
this.options?.onQueueUpdate?.(normalizedStatus);
|
|
85
|
+
this.options?.onProgress?.(normalizedStatus);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
handleResult(result: T): void {
|
|
90
|
+
if (!this.isMounted) return;
|
|
91
|
+
this.options?.onResult?.(result);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
handleError(error: Error): void {
|
|
95
|
+
if (!this.isMounted) return;
|
|
96
|
+
this.options?.onError?.(error);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type Guards Index
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { PrunaModelId } from "../../../domain/entities/pruna.types";
|
|
6
|
+
import { PrunaErrorType } from "../../../domain/entities/error.types";
|
|
7
|
+
import { VALID_PRUNA_MODELS } from "../../services/pruna-provider.constants";
|
|
8
|
+
|
|
9
|
+
export function isPrunaModelId(value: unknown): value is PrunaModelId {
|
|
10
|
+
return typeof value === 'string' && VALID_PRUNA_MODELS.includes(value as PrunaModelId);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function isPrunaErrorType(value: unknown): value is PrunaErrorType {
|
|
14
|
+
return typeof value === 'string' && Object.values(PrunaErrorType).includes(value as PrunaErrorType);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function isValidApiKey(value: unknown): value is string {
|
|
18
|
+
return typeof value === 'string' && value.length > 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function isValidModelId(value: unknown): value is string {
|
|
22
|
+
return typeof value === 'string' && value.length >= 3;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function isValidPrompt(value: unknown): value is string {
|
|
26
|
+
return typeof value === 'string' && value.length > 0 && value.length <= 5000;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function isValidTimeout(value: unknown): value is number {
|
|
30
|
+
return typeof value === 'number' && Number.isInteger(value) && value >= 1 && value <= 600000;
|
|
31
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Provider Init Module Factory
|
|
3
|
+
* Creates a ready-to-use InitModule for app initialization
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { providerRegistry } from '@umituz/react-native-ai-generation-content';
|
|
7
|
+
import { prunaProvider } from '../infrastructure/services';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* InitModule interface (from @umituz/react-native-design-system)
|
|
11
|
+
*/
|
|
12
|
+
interface InitModule {
|
|
13
|
+
name: string;
|
|
14
|
+
init: () => Promise<boolean>;
|
|
15
|
+
critical?: boolean;
|
|
16
|
+
dependsOn?: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface AiProviderInitModuleConfig {
|
|
20
|
+
/**
|
|
21
|
+
* Pruna AI API key getter function
|
|
22
|
+
* Returns the API key or undefined if not available
|
|
23
|
+
*/
|
|
24
|
+
getApiKey: () => string | undefined;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Whether this module is critical for app startup
|
|
28
|
+
* @default false
|
|
29
|
+
*/
|
|
30
|
+
critical?: boolean;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Module dependencies
|
|
34
|
+
* @default ["firebase"]
|
|
35
|
+
*/
|
|
36
|
+
dependsOn?: string[];
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Optional callback called after provider is initialized
|
|
40
|
+
*/
|
|
41
|
+
onInitialized?: () => void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Creates a Pruna AI Provider initialization module for use with createAppInitializer
|
|
46
|
+
*/
|
|
47
|
+
export function createAiProviderInitModule(
|
|
48
|
+
config: AiProviderInitModuleConfig
|
|
49
|
+
): InitModule {
|
|
50
|
+
const {
|
|
51
|
+
getApiKey,
|
|
52
|
+
critical = false,
|
|
53
|
+
dependsOn = ['firebase'],
|
|
54
|
+
onInitialized,
|
|
55
|
+
} = config;
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
name: 'aiProviders',
|
|
59
|
+
critical,
|
|
60
|
+
dependsOn,
|
|
61
|
+
init: () => {
|
|
62
|
+
try {
|
|
63
|
+
const apiKey = getApiKey();
|
|
64
|
+
|
|
65
|
+
if (!apiKey) {
|
|
66
|
+
return Promise.resolve(false);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
prunaProvider.initialize({ apiKey });
|
|
70
|
+
|
|
71
|
+
if (!providerRegistry.hasProvider(prunaProvider.providerId)) {
|
|
72
|
+
providerRegistry.register(prunaProvider);
|
|
73
|
+
}
|
|
74
|
+
providerRegistry.setActiveProvider(prunaProvider.providerId);
|
|
75
|
+
|
|
76
|
+
if (onInitialized) {
|
|
77
|
+
onInitialized();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return Promise.resolve(true);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.error('[AiProviderInitModule] Pruna initialization failed:', error);
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Direct Pruna Provider Initialization
|
|
3
|
+
* Synchronous initialization for simple app startup
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { providerRegistry } from '@umituz/react-native-ai-generation-content';
|
|
7
|
+
import { prunaProvider } from '../infrastructure/services';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Initializes Pruna provider and registers it with providerRegistry in one call.
|
|
11
|
+
* Use this for simple synchronous registration at app startup.
|
|
12
|
+
*/
|
|
13
|
+
export function initializePrunaProvider(config: {
|
|
14
|
+
apiKey: string | undefined;
|
|
15
|
+
}): boolean {
|
|
16
|
+
try {
|
|
17
|
+
const { apiKey } = config;
|
|
18
|
+
|
|
19
|
+
if (!apiKey) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
prunaProvider.initialize({ apiKey });
|
|
24
|
+
|
|
25
|
+
if (!providerRegistry.hasProvider(prunaProvider.providerId)) {
|
|
26
|
+
providerRegistry.register(prunaProvider);
|
|
27
|
+
}
|
|
28
|
+
providerRegistry.setActiveProvider(prunaProvider.providerId);
|
|
29
|
+
|
|
30
|
+
return true;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.error('[initializePrunaProvider] Initialization failed:', error);
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|