@umituz/react-native-ai-gemini-provider 2.1.5 → 2.1.7

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 (28) hide show
  1. package/package.json +1 -1
  2. package/src/index.ts +11 -104
  3. package/src/infrastructure/interceptors/BaseInterceptor.ts +78 -0
  4. package/src/infrastructure/interceptors/RequestInterceptors.ts +6 -62
  5. package/src/infrastructure/interceptors/ResponseInterceptors.ts +6 -61
  6. package/src/infrastructure/interceptors/index.ts +7 -13
  7. package/src/infrastructure/services/base-gemini.service.ts +82 -0
  8. package/src/infrastructure/services/gemini-streaming.service.ts +31 -61
  9. package/src/infrastructure/services/gemini-text-generation.service.ts +11 -33
  10. package/src/infrastructure/services/index.ts +7 -4
  11. package/src/infrastructure/utils/async/index.ts +1 -10
  12. package/src/infrastructure/utils/index.ts +43 -26
  13. package/src/infrastructure/utils/stream-processor.util.ts +156 -0
  14. package/src/infrastructure/utils/validation-composer.util.ts +160 -0
  15. package/src/infrastructure/utils/validation.util.ts +9 -68
  16. package/src/presentation/hooks/index.ts +6 -1
  17. package/src/presentation/hooks/use-gemini.ts +21 -72
  18. package/src/presentation/hooks/use-operation-manager.ts +88 -0
  19. package/src/providers/ConfigBuilder.ts +121 -0
  20. package/src/providers/ProviderFactory.ts +37 -49
  21. package/src/providers/index.ts +4 -13
  22. package/src/infrastructure/utils/async/debounce.util.ts +0 -100
  23. package/src/infrastructure/utils/async/memoize.util.ts +0 -55
  24. package/src/infrastructure/utils/env.util.ts +0 -175
  25. package/src/infrastructure/utils/performance.util.ts +0 -139
  26. package/src/infrastructure/utils/rate-limiter.util.ts +0 -86
  27. package/src/infrastructure/utils/retry.util.ts +0 -158
  28. package/src/providers/ProviderConfig.ts +0 -36
@@ -1,16 +1,14 @@
1
-
2
- import { geminiClientCoreService } from "./gemini-client-core.service";
1
+ import { BaseGeminiService } from "./base-gemini.service";
3
2
  import { extractTextFromResponse } from "../utils/gemini-data-transformer.util";
4
- import { toSdkContent, transformResponse, createTextContent } from "../utils/content-mapper.util";
3
+ import { transformResponse, createTextContent } from "../utils/content-mapper.util";
5
4
  import { validatePrompt } from "../utils/validation.util";
6
- import { createGeminiError } from "../utils/error-mapper.util";
7
5
  import type {
8
6
  GeminiContent,
9
7
  GeminiGenerationConfig,
10
8
  GeminiResponse,
11
9
  } from "../../domain/entities";
12
10
 
13
- class GeminiTextGenerationService {
11
+ class GeminiTextGenerationService extends BaseGeminiService {
14
12
  /**
15
13
  * Generate content (text, with optional images)
16
14
  *
@@ -23,24 +21,15 @@ class GeminiTextGenerationService {
23
21
  generationConfig?: GeminiGenerationConfig,
24
22
  signal?: AbortSignal,
25
23
  ): Promise<GeminiResponse> {
26
- // Validate input
27
- if (!contents || contents.length === 0) {
28
- throw new Error("Contents array cannot be empty");
29
- }
30
-
31
- // Check for early abort
32
- if (signal?.aborted) {
33
- throw new Error("Request was aborted");
34
- }
35
-
36
24
  try {
37
- const genModel = geminiClientCoreService.getModel(model);
38
- const sdkContents = toSdkContent(contents);
39
-
40
- const requestOptions = {
41
- contents: sdkContents as Parameters<typeof genModel.generateContent>[0] extends { contents: infer C } ? C : never,
25
+ const { genModel, sdkContents } = this.validateAndPrepare({
26
+ model,
27
+ contents,
42
28
  generationConfig,
43
- };
29
+ signal,
30
+ });
31
+
32
+ const requestOptions = this.createRequestOptions(sdkContents, generationConfig);
44
33
 
45
34
  const result = signal
46
35
  ? await genModel.generateContent(requestOptions, { signal })
@@ -54,18 +43,7 @@ class GeminiTextGenerationService {
54
43
 
55
44
  return transformResponse(response);
56
45
  } catch (error) {
57
- // Re-throw as GeminiError if it's an API error
58
- if (error instanceof Error && error.name === "GeminiError") {
59
- throw error;
60
- }
61
-
62
- // Check for abort error
63
- if (error instanceof Error && error.name === "AbortError") {
64
- throw new Error("Request was aborted");
65
- }
66
-
67
- // Wrap other errors
68
- throw createGeminiError(error);
46
+ return this.handleError(error, "Request was aborted");
69
47
  }
70
48
  }
71
49
 
@@ -1,15 +1,18 @@
1
1
  /**
2
- * Infrastructure Services
3
- * Text-only Gemini services
2
+ * Infrastructure Services - Internal Use Only
4
3
  */
5
4
 
6
- // Core services
5
+ // Internal base classes
6
+ export { BaseGeminiService } from "./base-gemini.service";
7
+ export type { BaseRequestOptions } from "./base-gemini.service";
8
+
9
+ // Internal services
7
10
  export { geminiClientCoreService } from "./gemini-client-core.service";
8
11
  export { geminiTextGenerationService } from "./gemini-text-generation.service";
9
12
  export { geminiStructuredTextService } from "./gemini-structured-text.service";
10
13
  export { geminiStreamingService } from "./gemini-streaming.service";
11
14
 
12
- // Provider
15
+ // Main Provider - Public API
13
16
  export {
14
17
  geminiProviderService,
15
18
  GeminiProvider,
@@ -1,6 +1,5 @@
1
1
  /**
2
- * Async Utilities
3
- * Re-exports all async utility functions
2
+ * Async State Management - Internal Use Only
4
3
  */
5
4
 
6
5
  export {
@@ -9,11 +8,3 @@ export {
9
8
  type AsyncStateSetters,
10
9
  type AsyncStateConfig,
11
10
  } from "./execute-state.util";
12
-
13
- export {
14
- createDebouncedAsync,
15
- } from "./debounce.util";
16
-
17
- export {
18
- createMemoizedAsync,
19
- } from "./memoize.util";
@@ -1,30 +1,47 @@
1
- // Error handling
2
- export { mapGeminiError, isGeminiErrorRetryable, categorizeGeminiError, createGeminiError } from "./error-mapper.util";
1
+ /**
2
+ * Utility Functions - Internal Use Only
3
+ * These are internal implementation details and should not be used directly by consumers
4
+ */
3
5
 
4
- // Data transformation
5
- export { extractTextFromResponse } from "./gemini-data-transformer.util";
6
- export { cleanJsonText, parseJsonResponse, safeParseJson, extractJsonFromText } from "./json-parser.util";
7
- export { toSdkContent, createTextContent, transformCandidate, transformResponse, extractTextFromParts } from "./content-mapper.util";
8
-
9
- // Performance
10
- export { measureAsync, measureSync, debounce, throttle, PerformanceTimer } from "./performance.util";
11
- export type { PerformanceMetrics } from "./performance.util";
12
-
13
- // Rate limiting
14
- export { RateLimiter } from "./rate-limiter.util";
15
- export type { RateLimiterOptions } from "./rate-limiter.util";
6
+ // Error handling (internal)
7
+ export {
8
+ mapGeminiError,
9
+ isGeminiErrorRetryable,
10
+ categorizeGeminiError,
11
+ createGeminiError
12
+ } from "./error-mapper.util";
16
13
 
17
- // Retry logic
18
- export { retryWithBackoff, retryIf, retryWithFixedDelay, shouldRetryNetworkError, createRetryPredicate } from "./retry.util";
19
- export type { RetryOptions, RetryResult } from "./retry.util";
20
-
21
- // Validation
22
- export { validateModelName, validateApiKey, validateSchema, validatePrompt, validateTimeout, isValidObject, validateRequiredFields } from "./validation.util";
14
+ // Data transformation (internal)
15
+ export { extractTextFromResponse } from "./gemini-data-transformer.util";
16
+ export {
17
+ cleanJsonText,
18
+ parseJsonResponse,
19
+ safeParseJson,
20
+ extractJsonFromText
21
+ } from "./json-parser.util";
22
+ export {
23
+ toSdkContent,
24
+ createTextContent,
25
+ transformCandidate,
26
+ transformResponse,
27
+ extractTextFromParts
28
+ } from "./content-mapper.util";
23
29
 
24
- // Environment
25
- export { getRequiredEnv, getOptionalEnv, getEnvNumber, getEnvBoolean, loadGeminiEnv, getApiKeyFromEnv, isDevelopment, isDebugEnabled, validateEnv, getGeminiConfigFromEnv } from "./env.util";
26
- export type { EnvConfig } from "./env.util";
30
+ // Validation (internal)
31
+ export {
32
+ validateModelName,
33
+ validateApiKey,
34
+ validateSchema,
35
+ validatePrompt,
36
+ validateTimeout,
37
+ isValidObject,
38
+ validateRequiredFields
39
+ } from "./validation.util";
27
40
 
28
- // Async state management
29
- export { executeWithState, createDebouncedAsync, createMemoizedAsync } from "./async";
30
- export type { AsyncStateCallbacks, AsyncStateSetters, AsyncStateConfig } from "./async";
41
+ // Async state management (internal)
42
+ export {
43
+ executeWithState,
44
+ type AsyncStateCallbacks,
45
+ type AsyncStateSetters,
46
+ type AsyncStateConfig
47
+ } from "./async";
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Stream Processing Utilities
3
+ * Reusable stream handling logic
4
+ */
5
+
6
+ export interface StreamChunk {
7
+ text: () => string;
8
+ }
9
+
10
+ export type ChunkCallback = (text: string) => void;
11
+ export type ErrorLogger = (error: unknown, context?: string) => void;
12
+
13
+ /**
14
+ * Process async stream with chunk callback
15
+ */
16
+ export async function processStream(
17
+ stream: AsyncIterable<StreamChunk>,
18
+ onChunk: ChunkCallback,
19
+ onError?: ErrorLogger
20
+ ): Promise<string> {
21
+ let fullText = "";
22
+
23
+ for await (const chunk of stream) {
24
+ try {
25
+ const chunkText = chunk.text();
26
+ if (chunkText) {
27
+ fullText += chunkText;
28
+ safeCallChunk(onChunk, chunkText, onError);
29
+ }
30
+ } catch (chunkError) {
31
+ logError(onError, chunkError, "stream-chunk");
32
+ }
33
+ }
34
+
35
+ return fullText;
36
+ }
37
+
38
+ /**
39
+ * Safely call chunk callback without breaking the stream
40
+ */
41
+ function safeCallChunk(
42
+ callback: ChunkCallback,
43
+ text: string,
44
+ onError?: ErrorLogger
45
+ ): void {
46
+ try {
47
+ callback(text);
48
+ } catch (callbackError) {
49
+ logError(onError, callbackError, "stream-callback");
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Log error without breaking the stream
55
+ */
56
+ function logError(
57
+ onError: ErrorLogger | undefined,
58
+ error: unknown,
59
+ context?: string
60
+ ): void {
61
+ if (onError) {
62
+ try {
63
+ onError(error, context);
64
+ } catch {
65
+ // Silently ignore logger errors
66
+ }
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Create a buffered stream processor
72
+ * Accumulates chunks until a condition is met
73
+ */
74
+ export class BufferedStreamProcessor {
75
+ private buffer = "";
76
+ private fullText = "";
77
+
78
+ constructor(
79
+ private onFlush: (text: string) => void,
80
+ private flushCondition: (buffer: string) => boolean = () => false
81
+ ) {}
82
+
83
+ /**
84
+ * Process a single chunk
85
+ */
86
+ processChunk(chunk: StreamChunk): void {
87
+ try {
88
+ const chunkText = chunk.text();
89
+ if (!chunkText) return;
90
+
91
+ this.buffer += chunkText;
92
+ this.fullText += chunkText;
93
+
94
+ if (this.flushCondition(this.buffer)) {
95
+ this.flush();
96
+ }
97
+ } catch (error) {
98
+ // Ignore chunk errors
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Flush buffer to callback
104
+ */
105
+ flush(): void {
106
+ if (this.buffer) {
107
+ try {
108
+ this.onFlush(this.buffer);
109
+ this.buffer = "";
110
+ } catch {
111
+ // Ignore callback errors
112
+ }
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Get accumulated full text
118
+ */
119
+ getFullText(): string {
120
+ return this.fullText;
121
+ }
122
+
123
+ /**
124
+ * Process entire stream
125
+ */
126
+ async processStream(stream: AsyncIterable<StreamChunk>): Promise<string> {
127
+ for await (const chunk of stream) {
128
+ this.processChunk(chunk);
129
+ }
130
+
131
+ // Flush remaining buffer
132
+ this.flush();
133
+
134
+ return this.fullText;
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Common flush conditions
140
+ */
141
+ export const flushConditions = {
142
+ /**
143
+ * Flush on newline
144
+ */
145
+ onNewline: (buffer: string): boolean => buffer.includes("\n"),
146
+
147
+ /**
148
+ * Flush when buffer reaches size
149
+ */
150
+ onSize: (size: number) => (buffer: string): boolean => buffer.length >= size,
151
+
152
+ /**
153
+ * Flush on pattern match
154
+ */
155
+ onPattern: (pattern: RegExp) => (buffer: string): boolean => pattern.test(buffer),
156
+ };
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Validation Composers
3
+ * Composable validation rules for clean, reusable validation
4
+ */
5
+
6
+ export type ValidationRule<T = unknown> = (value: T) => string | null;
7
+
8
+ /**
9
+ * Compose multiple validation rules into one
10
+ */
11
+ export function compose<T>(...rules: ValidationRule<T>[]): ValidationRule<T> {
12
+ return (value: T): string | null => {
13
+ for (const rule of rules) {
14
+ const error = rule(value);
15
+ if (error) return error;
16
+ }
17
+ return null;
18
+ };
19
+ }
20
+
21
+ /**
22
+ * Validate that value is not empty
23
+ */
24
+ export function required(fieldName: string = "Field"): ValidationRule<string> {
25
+ return (value: string): string | null => {
26
+ if (!value || typeof value !== "string" || value.trim().length === 0) {
27
+ return `${fieldName} is required`;
28
+ }
29
+ return null;
30
+ };
31
+ }
32
+
33
+ /**
34
+ * Validate minimum length
35
+ */
36
+ export function minLength(min: number, fieldName: string = "Field"): ValidationRule<string> {
37
+ return (value: string): string | null => {
38
+ if (value.trim().length < min) {
39
+ return `${fieldName} must be at least ${min} characters`;
40
+ }
41
+ return null;
42
+ };
43
+ }
44
+
45
+ /**
46
+ * Validate maximum length
47
+ */
48
+ export function maxLength(max: number, fieldName: string = "Field"): ValidationRule<string> {
49
+ return (value: string): string | null => {
50
+ if (value.length > max) {
51
+ return `${fieldName} must be at most ${max} characters`;
52
+ }
53
+ return null;
54
+ };
55
+ }
56
+
57
+ /**
58
+ * Validate string starts with prefix
59
+ */
60
+ export function startsWith(prefix: string, fieldName: string = "Field"): ValidationRule<string> {
61
+ return (value: string): string | null => {
62
+ if (!value.startsWith(prefix)) {
63
+ return `${fieldName} must start with "${prefix}"`;
64
+ }
65
+ return null;
66
+ };
67
+ }
68
+
69
+ /**
70
+ * Validate number is in range
71
+ */
72
+ export function inRange(min: number, max: number, fieldName: string = "Value"): ValidationRule<number> {
73
+ return (value: number): string | null => {
74
+ if (typeof value !== "number" || value < min || value > max) {
75
+ return `${fieldName} must be between ${min} and ${max}`;
76
+ }
77
+ return null;
78
+ };
79
+ }
80
+
81
+ /**
82
+ * Validate object has required properties
83
+ */
84
+ export function hasProperties(...props: string[]): ValidationRule<Record<string, unknown>> {
85
+ return (value: Record<string, unknown>): string | null => {
86
+ const missing = props.filter((prop) => !(prop in value) || value[prop] === undefined);
87
+ if (missing.length > 0) {
88
+ return `Missing required properties: ${missing.join(", ")}`;
89
+ }
90
+ return null;
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Validate object structure (for schemas)
96
+ */
97
+ export function isValidSchema(): ValidationRule<Record<string, unknown>> {
98
+ return compose(
99
+ (schema): string | null => {
100
+ if (!schema || typeof schema !== "object") {
101
+ return "Schema must be a non-empty object";
102
+ }
103
+ return null;
104
+ },
105
+ (schema): string | null => {
106
+ if (Object.keys(schema).length === 0) {
107
+ return "Schema must contain at least one property";
108
+ }
109
+ return null;
110
+ },
111
+ hasProperties("type"),
112
+ (schema): string | null => {
113
+ const schemaType = schema.type;
114
+ if (schemaType !== "object" && schemaType !== "array") {
115
+ return `Schema type must be "object" or "array", got "${String(schemaType)}"`;
116
+ }
117
+ return null;
118
+ },
119
+ (schema): string | null => {
120
+ if (schema.type === "object" && !("properties" in schema)) {
121
+ return 'Object schema must have a "properties" field';
122
+ }
123
+ return null;
124
+ }
125
+ );
126
+ }
127
+
128
+ /**
129
+ * Helper to validate and throw on error
130
+ */
131
+ export function validateOrThrow<T>(value: T, rule: ValidationRule<T>): void {
132
+ const error = rule(value);
133
+ if (error) {
134
+ throw new Error(error);
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Pre-built composite validators
140
+ */
141
+ export const validators = {
142
+ apiKey: compose(
143
+ required("API key"),
144
+ minLength(10, "API key")
145
+ ),
146
+
147
+ modelName: compose(
148
+ required("Model name"),
149
+ startsWith("gemini-", "Model name")
150
+ ),
151
+
152
+ prompt: compose(
153
+ required("Prompt"),
154
+ minLength(3, "Prompt")
155
+ ),
156
+
157
+ timeout: inRange(1, 300000, "Timeout"),
158
+
159
+ schema: isValidSchema(),
160
+ };
@@ -1,32 +1,17 @@
1
1
  /**
2
- * Validation Utilities
3
- * Common validation logic for models, configurations, and inputs
2
+ * Validation Utilities (Legacy)
3
+ * Maintained for backward compatibility
4
+ * New code should use validation-composer.util.ts
4
5
  */
5
6
 
6
- /** Maximum timeout value (5 minutes) */
7
- const MAX_TIMEOUT_MS = 300000;
8
-
9
- /** Minimum prompt length */
10
- const MIN_PROMPT_LENGTH = 3;
7
+ import { validateOrThrow, validators } from "./validation-composer.util";
11
8
 
12
9
  /**
13
10
  * Validate model name format
14
11
  * @throws Error if model name is invalid
15
12
  */
16
13
  export function validateModelName(modelName: string): void {
17
- if (!modelName || typeof modelName !== "string" || modelName.trim().length === 0) {
18
- const displayName: string = modelName === null ? "null" : typeof modelName;
19
- throw new Error(
20
- `Invalid model name: "${displayName}". Model name must be a non-empty string.`
21
- );
22
- }
23
-
24
- // Check for valid model format (starts with gemini-)
25
- if (!modelName.startsWith("gemini-")) {
26
- throw new Error(
27
- `Invalid model name: "${modelName}". Gemini models should start with "gemini-".`
28
- );
29
- }
14
+ validateOrThrow(modelName, validators.modelName);
30
15
  }
31
16
 
32
17
  /**
@@ -34,16 +19,7 @@ export function validateModelName(modelName: string): void {
34
19
  * @throws Error if API key is invalid
35
20
  */
36
21
  export function validateApiKey(apiKey: string): void {
37
- if (!apiKey || typeof apiKey !== "string" || apiKey.trim().length === 0) {
38
- throw new Error("API key must be a non-empty string");
39
- }
40
-
41
- // Gemini API keys typically start with "AIza"
42
- // This is a soft validation - actual validation happens on the API side
43
- const trimmedKey = apiKey.trim();
44
- if (trimmedKey.length < 10) {
45
- throw new Error("API key appears to be invalid (too short)");
46
- }
22
+ validateOrThrow(apiKey, validators.apiKey);
47
23
  }
48
24
 
49
25
  /**
@@ -51,30 +27,7 @@ export function validateApiKey(apiKey: string): void {
51
27
  * @throws Error if schema is invalid
52
28
  */
53
29
  export function validateSchema(schema: Record<string, unknown>): void {
54
- if (!schema || typeof schema !== "object") {
55
- throw new Error("Schema must be a non-empty object");
56
- }
57
-
58
- if (Object.keys(schema).length === 0) {
59
- throw new Error("Schema must contain at least one property");
60
- }
61
-
62
- // Basic structure validation - schema should have type
63
- if (!("type" in schema)) {
64
- throw new Error('Schema must have a "type" property (e.g., "object")');
65
- }
66
-
67
- const schemaType = schema.type;
68
-
69
- if (schemaType !== "object" && schemaType !== "array") {
70
- const typeStr = String(schemaType);
71
- throw new Error(`Schema type must be "object" or "array", got "${typeStr}"`);
72
- }
73
-
74
- // If type is object, should have properties
75
- if (schemaType === "object" && !("properties" in schema)) {
76
- throw new Error('Object schema must have a "properties" field');
77
- }
30
+ validateOrThrow(schema, validators.schema);
78
31
  }
79
32
 
80
33
  /**
@@ -82,13 +35,7 @@ export function validateSchema(schema: Record<string, unknown>): void {
82
35
  * @throws Error if prompt is invalid
83
36
  */
84
37
  export function validatePrompt(prompt: string): void {
85
- if (!prompt || typeof prompt !== "string" || prompt.trim().length === 0) {
86
- throw new Error("Prompt must be a non-empty string");
87
- }
88
-
89
- if (prompt.trim().length < MIN_PROMPT_LENGTH) {
90
- throw new Error(`Prompt is too short (minimum ${MIN_PROMPT_LENGTH} characters)`);
91
- }
38
+ validateOrThrow(prompt, validators.prompt);
92
39
  }
93
40
 
94
41
  /**
@@ -96,13 +43,7 @@ export function validatePrompt(prompt: string): void {
96
43
  * @throws Error if timeout is invalid
97
44
  */
98
45
  export function validateTimeout(timeout: number): void {
99
- if (typeof timeout !== "number" || timeout <= 0) {
100
- throw new Error("Timeout must be a positive number");
101
- }
102
-
103
- if (timeout > MAX_TIMEOUT_MS) {
104
- throw new Error(`Timeout cannot exceed ${MAX_TIMEOUT_MS}ms (5 minutes)`);
105
- }
46
+ validateOrThrow(timeout, validators.timeout);
106
47
  }
107
48
 
108
49
  /**
@@ -1,6 +1,11 @@
1
1
  /**
2
- * Presentation Hooks
2
+ * React Hooks - Public API
3
3
  */
4
4
 
5
+ // Internal - not exported from main index
6
+ export { useOperationManager } from "./use-operation-manager";
7
+ export type { OperationManager } from "./use-operation-manager";
8
+
9
+ // Public API
5
10
  export { useGemini } from "./use-gemini";
6
11
  export type { UseGeminiOptions, UseGeminiReturn } from "./use-gemini";