@umituz/react-native-ai-groq-provider 1.0.13 → 1.0.15

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-groq-provider",
3
- "version": "1.0.13",
3
+ "version": "1.0.15",
4
4
  "description": "Groq text generation provider for React Native applications",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./dist/index.d.ts",
package/src/index.ts CHANGED
@@ -107,6 +107,22 @@ export {
107
107
  type AsyncCallbacks,
108
108
  } from "./infrastructure/utils/async";
109
109
 
110
+ export {
111
+ generateRandomId,
112
+ generateSessionId,
113
+ calculateMaxMessages,
114
+ calculateExponentialBackoff,
115
+ clamp,
116
+ calculatePercentage,
117
+ calculateSafeBufferSize,
118
+ estimateTokens,
119
+ isWithinSafeLimit,
120
+ calculateRetryDelayWithJitter,
121
+ calculateRequestTimeout,
122
+ calculateTransferRate,
123
+ calculateAverage,
124
+ } from "./infrastructure/utils/calculation.util";
125
+
110
126
  export {
111
127
  telemetry,
112
128
  useTelemetry,
@@ -11,6 +11,7 @@ import type {
11
11
  import { groqHttpClient } from "./GroqClient";
12
12
  import { DEFAULT_MODELS } from "../../domain/entities";
13
13
  import { GroqError, GroqErrorType } from "../../domain/entities/error.types";
14
+ import { generateSessionId, calculateMaxMessages } from "../../infrastructure/utils/calculation.util";
14
15
 
15
16
  /**
16
17
  * Chat session state
@@ -59,7 +60,7 @@ export type ChatHistoryMessage = {
59
60
  */
60
61
  export function createChatSession(config: GroqChatConfig = {}): ChatSession {
61
62
  return {
62
- id: `groq-chat-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
63
+ id: generateSessionId("groq-chat"),
63
64
  model: config.model || DEFAULT_MODELS.TEXT,
64
65
  systemInstruction: config.systemInstruction,
65
66
  messages: config.history ? [...config.history] : [],
@@ -74,13 +75,31 @@ export function createChatSession(config: GroqChatConfig = {}): ChatSession {
74
75
  */
75
76
  class ChatSessionService {
76
77
  private sessions = new Map<string, ChatSession>();
78
+ private readonly MAX_SESSIONS = 100; // Prevent unlimited memory growth
79
+ private readonly SESSION_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
77
80
 
78
81
  /**
79
82
  * Create a new chat session
80
83
  */
81
84
  create(config: GroqChatConfig = {}): ChatSession {
85
+ // Auto-cleanup old sessions before creating new one
86
+ this.cleanupOldSessions();
87
+
82
88
  const session = createChatSession(config);
83
89
  this.sessions.set(session.id, session);
90
+
91
+ // Enforce session limit
92
+ if (this.sessions.size > this.MAX_SESSIONS) {
93
+ // Remove oldest sessions
94
+ const sortedSessions = Array.from(this.sessions.entries())
95
+ .sort(([, a], [, b]) => a.createdAt.getTime() - b.createdAt.getTime());
96
+
97
+ const toRemove = sortedSessions.slice(0, this.sessions.size - this.MAX_SESSIONS);
98
+ for (const [id] of toRemove) {
99
+ this.sessions.delete(id);
100
+ }
101
+ }
102
+
84
103
  return session;
85
104
  }
86
105
 
@@ -98,6 +117,39 @@ class ChatSessionService {
98
117
  return this.sessions.delete(sessionId);
99
118
  }
100
119
 
120
+ /**
121
+ * Cleanup old sessions automatically
122
+ */
123
+ private cleanupOldSessions(): void {
124
+ const now = Date.now();
125
+ const expiredIds: string[] = [];
126
+
127
+ for (const [id, session] of this.sessions.entries()) {
128
+ const age = now - session.updatedAt.getTime();
129
+ if (age > this.SESSION_TTL_MS) {
130
+ expiredIds.push(id);
131
+ }
132
+ }
133
+
134
+ for (const id of expiredIds) {
135
+ this.sessions.delete(id);
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Get active session count
141
+ */
142
+ getActiveCount(): number {
143
+ return this.sessions.size;
144
+ }
145
+
146
+ /**
147
+ * Clear all sessions
148
+ */
149
+ clearAll(): void {
150
+ this.sessions.clear();
151
+ }
152
+
101
153
  /**
102
154
  * Send a message in a chat session
103
155
  */
@@ -247,9 +299,8 @@ export function trimChatHistory(
247
299
  messages: GroqMessage[],
248
300
  maxTokens: number = 4000
249
301
  ): GroqMessage[] {
250
- // Simple heuristic: assume average of 4 tokens per message
251
- // For production, use a proper token counter
252
- const maxMessages = Math.floor(maxTokens / 100);
302
+ // Calculate max messages using utility function
303
+ const maxMessages = calculateMaxMessages(maxTokens);
253
304
 
254
305
  if (messages.length <= maxMessages) {
255
306
  return messages;
@@ -10,6 +10,7 @@ import type {
10
10
  GroqChatChunk,
11
11
  } from "../../domain/entities";
12
12
  import { GroqError, GroqErrorType, mapHttpStatusToErrorType } from "../../domain/entities/error.types";
13
+ import { calculateSafeBufferSize } from "../../infrastructure/utils/calculation.util";
13
14
 
14
15
  const DEFAULT_BASE_URL = "https://api.groq.com/openai/v1";
15
16
  const DEFAULT_TIMEOUT = 60000; // 60 seconds
@@ -295,6 +296,7 @@ class GroqHttpClient {
295
296
  const reader = response.body.getReader();
296
297
  const decoder = new TextDecoder();
297
298
  let buffer = "";
299
+ const MAX_BUFFER_SIZE = 1024 * 1024; // 1MB max buffer to prevent memory issues
298
300
 
299
301
  while (true) {
300
302
  const { done, value } = await reader.read();
@@ -302,6 +304,13 @@ class GroqHttpClient {
302
304
  if (done) break;
303
305
 
304
306
  buffer += decoder.decode(value, { stream: true });
307
+
308
+ // Prevent unlimited buffer growth using utility function
309
+ const safeSize = calculateSafeBufferSize(buffer.length, MAX_BUFFER_SIZE);
310
+ if (safeSize < buffer.length) {
311
+ buffer = buffer.slice(-safeSize);
312
+ }
313
+
305
314
  const lines = buffer.split("\n");
306
315
  buffer = lines.pop() || "";
307
316
 
@@ -315,7 +324,9 @@ class GroqHttpClient {
315
324
  const chunk = JSON.parse(jsonStr) as GroqChatChunk;
316
325
  yield chunk;
317
326
  } catch (error) {
318
- console.error("Failed to parse SSE chunk:", error);
327
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
328
+ console.error("Failed to parse SSE chunk:", error);
329
+ }
319
330
  }
320
331
  }
321
332
  }
@@ -50,15 +50,18 @@ export async function* streaming(
50
50
  top_p: options.generationConfig?.topP,
51
51
  };
52
52
 
53
+ let fullContent = "";
54
+
53
55
  try {
54
56
  for await (const chunk of groqHttpClient.chatCompletionStream(request)) {
55
57
  const content = chunk.choices[0]?.delta?.content;
56
58
  if (content) {
59
+ fullContent += content;
57
60
  options.callbacks?.onChunk?.(content);
58
61
  yield content;
59
62
  }
60
63
  }
61
- options.callbacks?.onComplete?.(await collectStreamContent(request));
64
+ options.callbacks?.onComplete?.(fullContent);
62
65
  } catch (error) {
63
66
  options.callbacks?.onError?.(error as Error);
64
67
  throw error;
@@ -82,33 +85,20 @@ export async function* streamingChat(
82
85
  top_p: options.generationConfig?.topP,
83
86
  };
84
87
 
88
+ let fullContent = "";
89
+
85
90
  try {
86
91
  for await (const chunk of groqHttpClient.chatCompletionStream(request)) {
87
92
  const content = chunk.choices[0]?.delta?.content;
88
93
  if (content) {
94
+ fullContent += content;
89
95
  options.callbacks?.onChunk?.(content);
90
96
  yield content;
91
97
  }
92
98
  }
93
- options.callbacks?.onComplete?.(await collectStreamContent(request));
99
+ options.callbacks?.onComplete?.(fullContent);
94
100
  } catch (error) {
95
101
  options.callbacks?.onError?.(error as Error);
96
102
  throw error;
97
103
  }
98
104
  }
99
-
100
- /**
101
- * Collect full content from streaming (for onComplete callback)
102
- */
103
- async function collectStreamContent(request: GroqChatRequest): Promise<string> {
104
- let fullContent = "";
105
-
106
- for await (const chunk of groqHttpClient.chatCompletionStream(request)) {
107
- const content = chunk.choices[0]?.delta?.content;
108
- if (content) {
109
- fullContent += content;
110
- }
111
- }
112
-
113
- return fullContent;
114
- }
@@ -11,6 +11,7 @@ import type {
11
11
  import { groqHttpClient } from "./GroqClient";
12
12
  import { DEFAULT_MODELS } from "../../domain/entities";
13
13
  import { GroqError, GroqErrorType } from "../../domain/entities/error.types";
14
+ import { cleanJsonResponse } from "../../infrastructure/utils/content-mapper.util";
14
15
 
15
16
  export interface StructuredTextOptions<T> {
16
17
  model?: string;
@@ -111,17 +112,7 @@ export async function structuredText<T = Record<string, unknown>>(
111
112
  });
112
113
  }
113
114
 
114
- // Clean up the response: remove markdown code blocks if present
115
- content = content.trim();
116
- if (content.startsWith("```json")) {
117
- content = content.slice(7);
118
- } else if (content.startsWith("```")) {
119
- content = content.slice(3);
120
- }
121
- if (content.endsWith("```")) {
122
- content = content.slice(0, -3);
123
- }
124
- content = content.trim();
115
+ content = cleanJsonResponse(content);
125
116
 
126
117
  if (typeof __DEV__ !== "undefined" && __DEV__) {
127
118
  console.log("[Groq] Attempting JSON parse...");
@@ -232,17 +223,7 @@ export async function structuredChat<T = Record<string, unknown>>(
232
223
  );
233
224
  }
234
225
 
235
- // Clean up the response: remove markdown code blocks if present
236
- content = content.trim();
237
- if (content.startsWith("```json")) {
238
- content = content.slice(7);
239
- } else if (content.startsWith("```")) {
240
- content = content.slice(3);
241
- }
242
- if (content.endsWith("```")) {
243
- content = content.slice(0, -3);
244
- }
245
- content = content.trim();
226
+ content = cleanJsonResponse(content);
246
227
 
247
228
  const totalDuration = Date.now() - startTime;
248
229
  if (typeof __DEV__ !== "undefined" && __DEV__) {
@@ -3,6 +3,8 @@
3
3
  * Simple telemetry tracking for Groq operations
4
4
  */
5
5
 
6
+ import { useMemo } from "react";
7
+
6
8
  type TelemetryEvent = {
7
9
  name: string;
8
10
  timestamp: number;
@@ -12,6 +14,7 @@ type TelemetryEvent = {
12
14
  class Telemetry {
13
15
  private events: TelemetryEvent[] = [];
14
16
  private enabled = __DEV__;
17
+ private readonly MAX_EVENTS = 1000; // Prevent unlimited memory growth
15
18
 
16
19
  /**
17
20
  * Log a telemetry event
@@ -27,23 +30,28 @@ class Telemetry {
27
30
 
28
31
  this.events.push(event);
29
32
 
33
+ // Auto-cleanup old events to prevent memory leak
34
+ if (this.events.length > this.MAX_EVENTS) {
35
+ this.events.splice(0, this.events.length - this.MAX_EVENTS);
36
+ }
37
+
30
38
  if (__DEV__) {
31
39
  console.log(`[Groq Telemetry] ${name}`, data);
32
40
  }
33
41
  }
34
42
 
35
43
  /**
36
- * Get all events
44
+ * Get all events (returns readonly reference for performance)
37
45
  */
38
- getEvents(): TelemetryEvent[] {
39
- return [...this.events];
46
+ getEvents(): ReadonlyArray<TelemetryEvent> {
47
+ return this.events;
40
48
  }
41
49
 
42
50
  /**
43
51
  * Clear all events
44
52
  */
45
53
  clear(): void {
46
- this.events = [];
54
+ this.events.length = 0; // More efficient than reassignment
47
55
  }
48
56
 
49
57
  /**
@@ -51,6 +59,10 @@ class Telemetry {
51
59
  */
52
60
  setEnabled(enabled: boolean): void {
53
61
  this.enabled = enabled;
62
+ // Disable cleanup when disabled
63
+ if (!enabled) {
64
+ this.clear();
65
+ }
54
66
  }
55
67
 
56
68
  /**
@@ -59,6 +71,13 @@ class Telemetry {
59
71
  isEnabled(): boolean {
60
72
  return this.enabled;
61
73
  }
74
+
75
+ /**
76
+ * Get event count (lightweight check)
77
+ */
78
+ getEventCount(): number {
79
+ return this.events.length;
80
+ }
62
81
  }
63
82
 
64
83
  /**
@@ -68,12 +87,17 @@ export const telemetry = new Telemetry();
68
87
 
69
88
  /**
70
89
  * Hook to use telemetry in components
90
+ * Optimized with useMemo to prevent unnecessary re-renders
71
91
  */
72
92
  export function useTelemetry() {
73
- return {
74
- log: telemetry.log.bind(telemetry),
75
- getEvents: telemetry.getEvents.bind(telemetry),
76
- clear: telemetry.clear.bind(telemetry),
77
- isEnabled: telemetry.isEnabled.bind(telemetry),
78
- };
93
+ return useMemo(
94
+ () => ({
95
+ log: telemetry.log.bind(telemetry),
96
+ getEvents: telemetry.getEvents.bind(telemetry),
97
+ clear: telemetry.clear.bind(telemetry),
98
+ isEnabled: telemetry.isEnabled.bind(telemetry),
99
+ getEventCount: telemetry.getEventCount.bind(telemetry),
100
+ }),
101
+ []
102
+ );
79
103
  }
@@ -2,6 +2,11 @@
2
2
  * Async State Utilities
3
3
  */
4
4
 
5
+ import {
6
+ calculateExponentialBackoff,
7
+ clamp,
8
+ } from "../calculation.util";
9
+
5
10
  /**
6
11
  * State setters for async operations
7
12
  */
@@ -50,24 +55,53 @@ export async function executeWithState<T>(
50
55
  }
51
56
 
52
57
  /**
53
- * Execute async function with retry logic
58
+ * Execute async function with retry logic and exponential backoff
59
+ * @param asyncFn - Function to execute
60
+ * @param maxRetries - Maximum number of retry attempts (default: 3)
61
+ * @param delayMs - Initial delay in milliseconds (default: 1000)
62
+ * @param signal - Optional AbortSignal to cancel retries
54
63
  */
55
64
  export async function executeWithRetry<T>(
56
65
  asyncFn: () => Promise<T>,
57
66
  maxRetries: number = 3,
58
- delayMs: number = 1000
67
+ delayMs: number = 1000,
68
+ signal?: AbortSignal
59
69
  ): Promise<T> {
70
+ // Validate inputs
71
+ if (maxRetries < 1) {
72
+ throw new Error("maxRetries must be at least 1");
73
+ }
74
+ if (delayMs < 0) {
75
+ throw new Error("delayMs must be non-negative");
76
+ }
77
+
60
78
  let lastError: Error | null = null;
79
+ const MAX_DELAY_MS = 30000; // Cap at 30 seconds
61
80
 
62
81
  for (let attempt = 0; attempt < maxRetries; attempt++) {
82
+ // Check if aborted
83
+ if (signal?.aborted) {
84
+ throw new Error("Retry operation was aborted");
85
+ }
86
+
63
87
  try {
64
88
  return await asyncFn();
65
89
  } catch (error) {
66
90
  lastError = error as Error;
67
91
 
68
92
  if (attempt < maxRetries - 1) {
69
- // Wait before retrying with exponential backoff
70
- await new Promise((resolve) => setTimeout(resolve, delayMs * Math.pow(2, attempt)));
93
+ // Calculate exponential backoff delay using utility
94
+ const delay = calculateExponentialBackoff(delayMs, attempt);
95
+ const cappedDelay = clamp(delay, 0, MAX_DELAY_MS);
96
+
97
+ await new Promise<void>((resolve, reject) => {
98
+ const timeoutId = setTimeout(() => resolve(), cappedDelay);
99
+
100
+ signal?.addEventListener("abort", () => {
101
+ clearTimeout(timeoutId);
102
+ reject(new Error("Retry operation was aborted during delay"));
103
+ }, { once: true });
104
+ });
71
105
  }
72
106
  }
73
107
  }
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Calculation Utilities
3
+ * Common calculation and utility functions for numeric operations
4
+ */
5
+
6
+ /**
7
+ * Generate a random unique identifier string
8
+ * @param length - Length of the random string (default: 9)
9
+ * @returns Random string in base-36
10
+ */
11
+ export function generateRandomId(length: number = 9): string {
12
+ return Math.random().toString(36).substring(2, 2 + length);
13
+ }
14
+
15
+ /**
16
+ * Generate a unique chat session ID
17
+ * @param prefix - Optional prefix for the ID (default: "groq-chat")
18
+ * @returns Unique session identifier
19
+ */
20
+ export function generateSessionId(prefix: string = "groq-chat"): string {
21
+ return `${prefix}-${Date.now()}-${generateRandomId(9)}`;
22
+ }
23
+
24
+ /**
25
+ * Calculate maximum number of messages based on token limit
26
+ * Uses a heuristic of approximately 100 tokens per message
27
+ * @param maxTokens - Maximum allowed tokens
28
+ * @param tokensPerMessage - Estimated tokens per message (default: 100)
29
+ * @returns Maximum number of messages
30
+ */
31
+ export function calculateMaxMessages(
32
+ maxTokens: number,
33
+ tokensPerMessage: number = 100
34
+ ): number {
35
+ if (maxTokens <= 0) {
36
+ return 0;
37
+ }
38
+ return Math.floor(maxTokens / tokensPerMessage);
39
+ }
40
+
41
+ /**
42
+ * Calculate exponential backoff delay
43
+ * @param baseDelay - Initial delay in milliseconds
44
+ * @param attempt - Current attempt number (0-indexed)
45
+ * @returns Delay in milliseconds
46
+ */
47
+ export function calculateExponentialBackoff(baseDelay: number, attempt: number): number {
48
+ if (baseDelay < 0 || attempt < 0) {
49
+ return 0;
50
+ }
51
+ return baseDelay * Math.pow(2, attempt);
52
+ }
53
+
54
+ /**
55
+ * Clamp a value between min and max
56
+ * @param value - Value to clamp
57
+ * @param min - Minimum allowed value
58
+ * @param max - Maximum allowed value
59
+ * @returns Clamped value
60
+ */
61
+ export function clamp(value: number, min: number, max: number): number {
62
+ return Math.min(Math.max(value, min), max);
63
+ }
64
+
65
+ /**
66
+ * Calculate percentage with optional decimal places
67
+ * @param value - Current value
68
+ * @param total - Total value
69
+ * @param decimals - Number of decimal places (default: 2)
70
+ * @returns Percentage value
71
+ */
72
+ export function calculatePercentage(
73
+ value: number,
74
+ total: number,
75
+ decimals: number = 2
76
+ ): number {
77
+ if (total === 0) {
78
+ return 0;
79
+ }
80
+ return Number(((value / total) * 100).toFixed(decimals));
81
+ }
82
+
83
+ /**
84
+ * Calculate buffer size limit for streaming
85
+ * Ensures buffer doesn't grow beyond reasonable limits
86
+ * @param currentSize - Current buffer size
87
+ * @param maxSize - Maximum allowed buffer size
88
+ * @returns Safe buffer size
89
+ */
90
+ export function calculateSafeBufferSize(currentSize: number, maxSize: number): number {
91
+ if (currentSize > maxSize) {
92
+ return Math.floor(maxSize / 2);
93
+ }
94
+ return currentSize;
95
+ }
96
+
97
+ /**
98
+ * Calculate token estimate from text
99
+ * Rough approximation: ~4 characters per token
100
+ * @param text - Text to estimate tokens for
101
+ * @returns Estimated token count
102
+ */
103
+ export function estimateTokens(text: string): number {
104
+ if (!text) {
105
+ return 0;
106
+ }
107
+ return Math.ceil(text.length / 4);
108
+ }
109
+
110
+ /**
111
+ * Calculate if message count is within safe limits
112
+ * @param messageCount - Current message count
113
+ * @param maxMessages - Maximum allowed messages
114
+ * @returns Whether within safe limits
115
+ */
116
+ export function isWithinSafeLimit(messageCount: number, maxMessages: number): boolean {
117
+ return messageCount >= 0 && messageCount <= maxMessages;
118
+ }
119
+
120
+ /**
121
+ * Calculate retry delay with jitter
122
+ * Adds random jitter to prevent thundering herd
123
+ * @param baseDelay - Base delay in milliseconds
124
+ * @param attempt - Current attempt number
125
+ * @param jitterFactor - Jitter factor (0-1, default: 0.1)
126
+ * @returns Delay with jitter applied
127
+ */
128
+ export function calculateRetryDelayWithJitter(
129
+ baseDelay: number,
130
+ attempt: number,
131
+ jitterFactor: number = 0.1
132
+ ): number {
133
+ const exponentialDelay = calculateExponentialBackoff(baseDelay, attempt);
134
+ const jitter = exponentialDelay * jitterFactor * (Math.random() * 2 - 1);
135
+ return Math.max(0, exponentialDelay + jitter);
136
+ }
137
+
138
+ /**
139
+ * Calculate timeout for network requests
140
+ * Based on exponential backoff with a maximum cap
141
+ * @param attempt - Current attempt number
142
+ * @param baseTimeout - Base timeout in milliseconds (default: 5000)
143
+ * @param maxTimeout - Maximum timeout in milliseconds (default: 30000)
144
+ * @returns Timeout in milliseconds
145
+ */
146
+ export function calculateRequestTimeout(
147
+ attempt: number,
148
+ baseTimeout: number = 5000,
149
+ maxTimeout: number = 30000
150
+ ): number {
151
+ const timeout = calculateExponentialBackoff(baseTimeout, attempt);
152
+ return Math.min(timeout, maxTimeout);
153
+ }
154
+
155
+ /**
156
+ * Calculate data transfer rate
157
+ * @param bytes - Number of bytes transferred
158
+ * @param milliseconds - Time taken in milliseconds
159
+ * @returns Transfer rate in KB/s
160
+ */
161
+ export function calculateTransferRate(bytes: number, milliseconds: number): number {
162
+ if (milliseconds === 0) {
163
+ return 0;
164
+ }
165
+ const seconds = milliseconds / 1000;
166
+ const kilobytes = bytes / 1024;
167
+ return Number((kilobytes / seconds).toFixed(2));
168
+ }
169
+
170
+ /**
171
+ * Calculate average from array of numbers
172
+ * @param values - Array of numbers
173
+ * @returns Average value or 0 if array is empty
174
+ */
175
+ export function calculateAverage(values: number[]): number {
176
+ if (values.length === 0) {
177
+ return 0;
178
+ }
179
+ const sum = values.reduce((acc, val) => acc + val, 0);
180
+ return sum / values.length;
181
+ }
@@ -84,3 +84,19 @@ export function formatMessagesForDisplay(messages: GroqMessage[]): string {
84
84
  })
85
85
  .join("\n\n---\n\n");
86
86
  }
87
+
88
+ /**
89
+ * Clean JSON response by removing markdown code blocks
90
+ */
91
+ export function cleanJsonResponse(content: string): string {
92
+ let cleaned = content.trim();
93
+ if (cleaned.startsWith("```json")) {
94
+ cleaned = cleaned.slice(7);
95
+ } else if (cleaned.startsWith("```")) {
96
+ cleaned = cleaned.slice(3);
97
+ }
98
+ if (cleaned.endsWith("```")) {
99
+ cleaned = cleaned.slice(0, -3);
100
+ }
101
+ return cleaned.trim();
102
+ }
@@ -4,4 +4,5 @@
4
4
 
5
5
  export * from "./content-mapper.util";
6
6
  export * from "./error-mapper.util";
7
+ export * from "./calculation.util";
7
8
  export * from "./async";
@@ -3,7 +3,7 @@
3
3
  * Main React hook for Groq text generation
4
4
  */
5
5
 
6
- import { useState, useCallback, useRef } from "react";
6
+ import { useState, useCallback, useRef, useMemo } from "react";
7
7
  import type { GroqGenerationConfig } from "../../domain/entities";
8
8
  import { textGeneration } from "../../infrastructure/services/TextGeneration";
9
9
  import { structuredText } from "../../infrastructure/services/StructuredText";
@@ -52,6 +52,7 @@ export interface UseGroqReturn {
52
52
 
53
53
  /**
54
54
  * Hook for Groq text generation
55
+ * Optimized to prevent unnecessary re-renders and memory leaks
55
56
  */
56
57
  export function useGroq(options: UseGroqOptions = {}): UseGroqReturn {
57
58
  const [isLoading, setIsLoading] = useState(false);
@@ -59,6 +60,17 @@ export function useGroq(options: UseGroqOptions = {}): UseGroqReturn {
59
60
  const [result, setResult] = useState<string | null>(null);
60
61
  const abortControllerRef = useRef<AbortController | null>(null);
61
62
 
63
+ // Memoize options to prevent unnecessary callback recreations
64
+ const stableOptions = useMemo(() => options, [
65
+ options.model,
66
+ options.generationConfig?.temperature,
67
+ options.generationConfig?.maxTokens,
68
+ options.generationConfig?.topP,
69
+ options.onStart,
70
+ options.onSuccess,
71
+ options.onError,
72
+ ]);
73
+
62
74
  const generate = useCallback(
63
75
  async (prompt: string, config?: GroqGenerationConfig): Promise<string> => {
64
76
  // Cancel any ongoing request
@@ -72,23 +84,23 @@ export function useGroq(options: UseGroqOptions = {}): UseGroqReturn {
72
84
  setResult(null);
73
85
 
74
86
  telemetry.log("groq_generate_start", { prompt: prompt.substring(0, 100) });
75
- options.onStart?.();
87
+ stableOptions.onStart?.();
76
88
 
77
89
  try {
78
90
  const response = await textGeneration(prompt, {
79
- model: options.model,
80
- generationConfig: { ...options.generationConfig, ...config },
91
+ model: stableOptions.model,
92
+ generationConfig: { ...stableOptions.generationConfig, ...config },
81
93
  });
82
94
 
83
95
  setResult(response);
84
- options.onSuccess?.(response);
96
+ stableOptions.onSuccess?.(response);
85
97
  telemetry.log("groq_generate_success", { responseLength: response.length });
86
98
 
87
99
  return response;
88
100
  } catch (err) {
89
101
  const errorMessage = getUserFriendlyError(err);
90
102
  setError(errorMessage);
91
- options.onError?.(errorMessage);
103
+ stableOptions.onError?.(errorMessage);
92
104
  telemetry.log("groq_generate_error", { error: errorMessage });
93
105
  throw err;
94
106
  } finally {
@@ -96,7 +108,7 @@ export function useGroq(options: UseGroqOptions = {}): UseGroqReturn {
96
108
  abortControllerRef.current = null;
97
109
  }
98
110
  },
99
- [options]
111
+ [stableOptions]
100
112
  );
101
113
 
102
114
  const generateJSON = useCallback(
@@ -115,24 +127,24 @@ export function useGroq(options: UseGroqOptions = {}): UseGroqReturn {
115
127
  setResult(null);
116
128
 
117
129
  telemetry.log("groq_generate_json_start", { prompt: prompt.substring(0, 100) });
118
- options.onStart?.();
130
+ stableOptions.onStart?.();
119
131
 
120
132
  try {
121
133
  const response = await structuredText<T>(prompt, {
122
- model: options.model,
123
- generationConfig: { ...options.generationConfig, ...config },
134
+ model: stableOptions.model,
135
+ generationConfig: { ...stableOptions.generationConfig, ...config },
124
136
  schema: config?.schema,
125
137
  });
126
138
 
127
139
  setResult(JSON.stringify(response, null, 2));
128
- options.onSuccess?.(JSON.stringify(response, null, 2));
140
+ stableOptions.onSuccess?.(JSON.stringify(response, null, 2));
129
141
  telemetry.log("groq_generate_json_success");
130
142
 
131
143
  return response;
132
144
  } catch (err) {
133
145
  const errorMessage = getUserFriendlyError(err);
134
146
  setError(errorMessage);
135
- options.onError?.(errorMessage);
147
+ stableOptions.onError?.(errorMessage);
136
148
  telemetry.log("groq_generate_json_error", { error: errorMessage });
137
149
  throw err;
138
150
  } finally {
@@ -140,7 +152,7 @@ export function useGroq(options: UseGroqOptions = {}): UseGroqReturn {
140
152
  abortControllerRef.current = null;
141
153
  }
142
154
  },
143
- [options]
155
+ [stableOptions]
144
156
  );
145
157
 
146
158
  const stream = useCallback(
@@ -162,12 +174,12 @@ export function useGroq(options: UseGroqOptions = {}): UseGroqReturn {
162
174
  let fullContent = "";
163
175
 
164
176
  telemetry.log("groq_stream_start", { prompt: prompt.substring(0, 100) });
165
- options.onStart?.();
177
+ stableOptions.onStart?.();
166
178
 
167
179
  try {
168
180
  for await (const streamingResult of streaming(prompt, {
169
- model: options.model,
170
- generationConfig: { ...options.generationConfig, ...config },
181
+ model: stableOptions.model,
182
+ generationConfig: { ...stableOptions.generationConfig, ...config },
171
183
  callbacks: {
172
184
  onChunk: (c) => {
173
185
  fullContent += c;
@@ -180,12 +192,12 @@ export function useGroq(options: UseGroqOptions = {}): UseGroqReturn {
180
192
  }
181
193
 
182
194
  setResult(fullContent);
183
- options.onSuccess?.(fullContent);
195
+ stableOptions.onSuccess?.(fullContent);
184
196
  telemetry.log("groq_stream_success", { contentLength: fullContent.length });
185
197
  } catch (err) {
186
198
  const errorMessage = getUserFriendlyError(err);
187
199
  setError(errorMessage);
188
- options.onError?.(errorMessage);
200
+ stableOptions.onError?.(errorMessage);
189
201
  telemetry.log("groq_stream_error", { error: errorMessage });
190
202
  throw err;
191
203
  } finally {
@@ -193,7 +205,7 @@ export function useGroq(options: UseGroqOptions = {}): UseGroqReturn {
193
205
  abortControllerRef.current = null;
194
206
  }
195
207
  },
196
- [options]
208
+ [stableOptions]
197
209
  );
198
210
 
199
211
  const reset = useCallback(() => {
@@ -3,7 +3,7 @@
3
3
  * Manages async operations with loading, error, and success states
4
4
  */
5
5
 
6
- import { useState, useCallback, useRef } from "react";
6
+ import { useState, useCallback, useRef, useMemo } from "react";
7
7
  import { getUserFriendlyError } from "../../infrastructure/utils/error-mapper.util";
8
8
  import { telemetry } from "../../infrastructure/telemetry";
9
9
 
@@ -26,14 +26,23 @@ export interface UseOperationManagerOptions<T> {
26
26
 
27
27
  /**
28
28
  * Hook for managing async operations
29
+ * Optimized to prevent unnecessary re-renders
29
30
  */
30
31
  export function useOperationManager<T = unknown>(
31
32
  options: UseOperationManagerOptions<T> = {}
32
33
  ) {
34
+ // Memoize options to prevent unnecessary callback recreations
35
+ const stableOptions = useMemo(() => options, [
36
+ options.initialData,
37
+ options.onStart,
38
+ options.onSuccess,
39
+ options.onError,
40
+ ]);
41
+
33
42
  const [state, setState] = useState<OperationState<T>>({
34
43
  isLoading: false,
35
44
  error: null,
36
- data: options.initialData ?? null,
45
+ data: stableOptions.initialData ?? null,
37
46
  });
38
47
 
39
48
  const abortControllerRef = useRef<AbortController | null>(null);
@@ -56,27 +65,27 @@ export function useOperationManager<T = unknown>(
56
65
  setState((prev) => ({ ...prev, isLoading: true, error: null }));
57
66
 
58
67
  telemetry.log(`${operationName}_start`);
59
- options.onStart?.();
68
+ stableOptions.onStart?.();
60
69
 
61
70
  try {
62
71
  const result = await asyncFn(abortControllerRef.current.signal);
63
72
 
64
73
  setState((prev) => ({ ...prev, isLoading: false, data: result as unknown as T }));
65
- options.onSuccess?.(result as unknown as T);
74
+ stableOptions.onSuccess?.(result as unknown as T);
66
75
  telemetry.log(`${operationName}_success`);
67
76
 
68
77
  return result;
69
78
  } catch (error) {
70
79
  const errorMessage = getUserFriendlyError(error);
71
80
  setState((prev) => ({ ...prev, isLoading: false, error: errorMessage }));
72
- options.onError?.(errorMessage);
81
+ stableOptions.onError?.(errorMessage);
73
82
  telemetry.log(`${operationName}_error`, { error: errorMessage });
74
83
  throw error;
75
84
  } finally {
76
85
  abortControllerRef.current = null;
77
86
  }
78
87
  },
79
- [options]
88
+ [stableOptions]
80
89
  );
81
90
 
82
91
  const reset = useCallback(() => {
@@ -88,9 +97,9 @@ export function useOperationManager<T = unknown>(
88
97
  ...prev,
89
98
  isLoading: false,
90
99
  error: null,
91
- data: options.initialData ?? null,
100
+ data: stableOptions.initialData ?? null,
92
101
  }));
93
- }, [options.initialData]);
102
+ }, [stableOptions.initialData]);
94
103
 
95
104
  const clearError = useCallback(() => {
96
105
  setState((prev) => ({ ...prev, error: null }));