@umituz/react-native-ai-groq-provider 1.0.24 → 1.0.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/application/use-cases/chat-session.usecase.ts +62 -14
- package/src/application/use-cases/streaming.usecase.ts +13 -7
- package/src/application/use-cases/structured-generation.usecase.ts +27 -10
- package/src/application/use-cases/text-generation.usecase.ts +4 -3
- package/src/domain/entities/error.types.ts +17 -2
- package/src/index.ts +24 -66
- package/src/infrastructure/http/groq-http-client.ts +68 -12
- package/src/infrastructure/http/streaming-client.ts +139 -87
- package/src/infrastructure/telemetry/TelemetryHooks.ts +39 -19
- package/src/infrastructure/utils/calculation.util.ts +59 -63
- package/src/infrastructure/utils/content-mapper.util.ts +1 -1
- package/src/presentation/hooks/use-groq.hook.ts +58 -41
- package/src/providers/ConfigBuilder.ts +2 -73
- package/src/providers/ProviderFactory.ts +7 -62
- package/src/shared/request-builder.ts +29 -10
- package/src/shared/response-handler.ts +93 -0
- package/src/types/react-native-global.d.ts +12 -0
- package/src/application/use-cases/index.ts +0 -19
- package/src/domain/entities/index.ts +0 -7
- package/src/infrastructure/http/index.ts +0 -7
- package/src/infrastructure/telemetry/index.ts +0 -5
- package/src/infrastructure/utils/async/index.ts +0 -6
- package/src/infrastructure/utils/index.ts +0 -8
- package/src/presentation/hooks/index.ts +0 -7
- package/src/presentation/hooks/useGroq.ts +0 -235
- package/src/presentation/hooks/useOperationManager.ts +0 -119
- package/src/presentation/index.ts +0 -6
- package/src/providers/index.ts +0 -16
- package/src/shared/index.ts +0 -16
|
@@ -1,125 +1,177 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Streaming Client
|
|
3
3
|
* Handles SSE streaming from Groq API
|
|
4
|
+
* Optimized for performance and memory efficiency
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
|
-
import type { GroqChatRequest, GroqChatChunk } from "../../domain/entities";
|
|
7
|
+
import type { GroqChatRequest, GroqChatChunk } from "../../domain/entities/groq.types";
|
|
7
8
|
import { GroqError, GroqErrorType, mapHttpStatusToErrorType } from "../../domain/entities/error.types";
|
|
8
9
|
import { logger } from "../../shared/logger";
|
|
9
|
-
import { calculateSafeBufferSize } from "../../utils/calculation.util";
|
|
10
10
|
|
|
11
11
|
const DEFAULT_TIMEOUT = 60000;
|
|
12
|
+
const MAX_BUFFER_SIZE = 1024 * 1024; // 1MB
|
|
13
|
+
const MAX_INCOMPLETE_CHUNKS = 10;
|
|
12
14
|
|
|
13
15
|
export async function* streamChatCompletion(
|
|
14
16
|
request: GroqChatRequest,
|
|
15
17
|
config: { apiKey: string; baseUrl: string; timeoutMs?: number }
|
|
16
18
|
): AsyncGenerator<GroqChatChunk> {
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
26
|
-
|
|
27
|
-
try {
|
|
28
|
-
const response = await fetch(url, {
|
|
29
|
-
method: "POST",
|
|
30
|
-
headers: {
|
|
31
|
-
"Content-Type": "application/json",
|
|
32
|
-
"Authorization": `Bearer ${config.apiKey}`,
|
|
33
|
-
},
|
|
34
|
-
body: JSON.stringify({ ...request, stream: true }),
|
|
35
|
-
signal: controller.signal,
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
clearTimeout(timeoutId);
|
|
39
|
-
|
|
40
|
-
if (!response.ok) {
|
|
41
|
-
await handleErrorResponse(response);
|
|
42
|
-
}
|
|
19
|
+
const client = new GroqStreamingClient();
|
|
20
|
+
yield* client.stream(request, config);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
class GroqStreamingClient {
|
|
24
|
+
private normalizeBaseUrl(baseUrl: string): string {
|
|
25
|
+
return baseUrl.replace(/\/+$/, "");
|
|
26
|
+
}
|
|
43
27
|
|
|
44
|
-
|
|
45
|
-
|
|
28
|
+
private validateTimeout(timeout?: number): number {
|
|
29
|
+
if (timeout === undefined || timeout === null || timeout <= 0) {
|
|
30
|
+
return DEFAULT_TIMEOUT;
|
|
46
31
|
}
|
|
32
|
+
return Math.min(timeout, 300000);
|
|
33
|
+
}
|
|
47
34
|
|
|
48
|
-
|
|
35
|
+
async* stream(
|
|
36
|
+
request: GroqChatRequest,
|
|
37
|
+
config: { apiKey: string; baseUrl: string; timeoutMs?: number }
|
|
38
|
+
): AsyncGenerator<GroqChatChunk> {
|
|
39
|
+
const baseUrl = this.normalizeBaseUrl(config.baseUrl);
|
|
40
|
+
const url = `${baseUrl}/chat/completions`;
|
|
41
|
+
const timeout = this.validateTimeout(config.timeoutMs);
|
|
42
|
+
|
|
43
|
+
logger.debug("StreamingClient", "Starting stream", { model: request.model });
|
|
44
|
+
|
|
45
|
+
const controller = new AbortController();
|
|
46
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const response = await fetch(url, {
|
|
50
|
+
method: "POST",
|
|
51
|
+
headers: {
|
|
52
|
+
"Content-Type": "application/json",
|
|
53
|
+
"Authorization": `Bearer ${config.apiKey}`,
|
|
54
|
+
},
|
|
55
|
+
body: JSON.stringify({ ...request, stream: true }),
|
|
56
|
+
signal: controller.signal,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
clearTimeout(timeoutId);
|
|
60
|
+
|
|
61
|
+
if (!response.ok) {
|
|
62
|
+
await this.handleErrorResponse(response);
|
|
63
|
+
}
|
|
49
64
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
65
|
+
if (!response.body) {
|
|
66
|
+
throw new GroqError(GroqErrorType.NETWORK_ERROR, "Response body is empty");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
yield* this.parseSSE(response.body);
|
|
54
70
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
71
|
+
} catch (error) {
|
|
72
|
+
throw this.handleRequestError(error);
|
|
73
|
+
} finally {
|
|
74
|
+
clearTimeout(timeoutId);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
60
77
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
78
|
+
private async* parseSSE(body: ReadableStream<Uint8Array>): AsyncGenerator<GroqChatChunk> {
|
|
79
|
+
const reader = body.getReader();
|
|
80
|
+
const decoder = new TextDecoder();
|
|
81
|
+
const chunks: string[] = []; // Array for efficient string building
|
|
82
|
+
let buffer = "";
|
|
83
|
+
let consecutiveErrors = 0;
|
|
65
84
|
|
|
66
|
-
|
|
85
|
+
try {
|
|
86
|
+
while (true) {
|
|
87
|
+
const { done, value } = await reader.read();
|
|
88
|
+
if (done) break;
|
|
67
89
|
|
|
68
|
-
|
|
69
|
-
if (safeSize < buffer.length) {
|
|
70
|
-
buffer = buffer.slice(-safeSize);
|
|
71
|
-
}
|
|
90
|
+
chunks.push(decoder.decode(value, { stream: true }));
|
|
72
91
|
|
|
73
|
-
|
|
74
|
-
|
|
92
|
+
// Join all chunks at once - more efficient than +=
|
|
93
|
+
buffer = chunks.join("");
|
|
94
|
+
chunks.length = 0; // Clear array
|
|
75
95
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
96
|
+
// Trim buffer if necessary
|
|
97
|
+
if (buffer.length > MAX_BUFFER_SIZE) {
|
|
98
|
+
buffer = buffer.slice(-Math.floor(MAX_BUFFER_SIZE / 2));
|
|
99
|
+
logger.warn("StreamingClient", "Buffer trimmed");
|
|
100
|
+
}
|
|
79
101
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
102
|
+
const lines = buffer.split("\n");
|
|
103
|
+
buffer = lines.pop() || "";
|
|
104
|
+
|
|
105
|
+
for (const line of lines) {
|
|
106
|
+
const trimmed = line.trim();
|
|
107
|
+
if (!trimmed || trimmed === "data: [DONE]") continue;
|
|
108
|
+
|
|
109
|
+
if (trimmed.startsWith("data: ")) {
|
|
110
|
+
try {
|
|
111
|
+
const jsonStr = trimmed.slice(6);
|
|
112
|
+
const chunk = JSON.parse(jsonStr) as GroqChatChunk;
|
|
113
|
+
consecutiveErrors = 0;
|
|
114
|
+
yield chunk;
|
|
115
|
+
} catch (error) {
|
|
116
|
+
consecutiveErrors++;
|
|
117
|
+
logger.error("StreamingClient", "Parse error", {
|
|
118
|
+
error: error instanceof Error ? error.message : String(error),
|
|
119
|
+
consecutiveErrors,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
if (consecutiveErrors >= MAX_INCOMPLETE_CHUNKS) {
|
|
123
|
+
throw new GroqError(
|
|
124
|
+
GroqErrorType.SERVER_ERROR,
|
|
125
|
+
`Stream corrupted: ${consecutiveErrors} parse failures`
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
86
129
|
}
|
|
87
130
|
}
|
|
88
131
|
}
|
|
132
|
+
} finally {
|
|
133
|
+
reader.releaseLock();
|
|
89
134
|
}
|
|
90
|
-
} finally {
|
|
91
|
-
reader.releaseLock();
|
|
92
135
|
}
|
|
93
|
-
}
|
|
94
136
|
|
|
95
|
-
async
|
|
96
|
-
|
|
97
|
-
|
|
137
|
+
private async handleErrorResponse(response: Response): Promise<never> {
|
|
138
|
+
let errorMessage = `HTTP ${response.status}`;
|
|
139
|
+
const errorType = mapHttpStatusToErrorType(response.status);
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const text = await response.text();
|
|
143
|
+
if (text) {
|
|
144
|
+
try {
|
|
145
|
+
const errorData = JSON.parse(text) as { error?: { message?: string } };
|
|
146
|
+
if (errorData.error?.message) {
|
|
147
|
+
errorMessage = errorData.error.message;
|
|
148
|
+
}
|
|
149
|
+
} catch {
|
|
150
|
+
errorMessage = text.substring(0, 500);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
} catch {
|
|
154
|
+
// Use default message
|
|
155
|
+
}
|
|
98
156
|
|
|
99
|
-
|
|
100
|
-
const errorData = (await response.json()) as { error?: { message?: string } };
|
|
101
|
-
if (errorData.error?.message) errorMessage = errorData.error.message;
|
|
102
|
-
} catch {
|
|
103
|
-
// Use default
|
|
157
|
+
throw new GroqError(errorType, errorMessage);
|
|
104
158
|
}
|
|
105
159
|
|
|
106
|
-
|
|
107
|
-
|
|
160
|
+
private handleRequestError(error: unknown): GroqError {
|
|
161
|
+
if (error instanceof GroqError) return error;
|
|
108
162
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (error.message.includes("network")) {
|
|
117
|
-
return new GroqError(GroqErrorType.NETWORK_ERROR, "Network error", error);
|
|
163
|
+
if (error instanceof Error) {
|
|
164
|
+
if (error.name === "AbortError") {
|
|
165
|
+
return new GroqError(GroqErrorType.ABORT_ERROR, "Request timeout", error);
|
|
166
|
+
}
|
|
167
|
+
if (error.name === "TypeError" && error.message.includes("fetch")) {
|
|
168
|
+
return new GroqError(GroqErrorType.NETWORK_ERROR, "Network error", error);
|
|
169
|
+
}
|
|
118
170
|
}
|
|
119
|
-
}
|
|
120
171
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
172
|
+
return new GroqError(
|
|
173
|
+
GroqErrorType.UNKNOWN_ERROR,
|
|
174
|
+
error instanceof Error ? error.message : "Unknown error"
|
|
175
|
+
);
|
|
176
|
+
}
|
|
125
177
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Telemetry Hooks
|
|
3
3
|
* Simple telemetry tracking for Groq operations
|
|
4
|
+
* Optimized with O(1) circular buffer
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
import { useMemo } from "react";
|
|
@@ -13,11 +14,17 @@ type TelemetryEvent = {
|
|
|
13
14
|
|
|
14
15
|
class Telemetry {
|
|
15
16
|
private events: TelemetryEvent[] = [];
|
|
16
|
-
private enabled
|
|
17
|
-
private readonly MAX_EVENTS = 1000;
|
|
17
|
+
private enabled: boolean;
|
|
18
|
+
private readonly MAX_EVENTS = 1000;
|
|
19
|
+
private head = 0; // Write position
|
|
20
|
+
private count = 0; // Actual number of events
|
|
21
|
+
|
|
22
|
+
constructor() {
|
|
23
|
+
this.enabled = typeof __DEV__ !== "undefined" && __DEV__;
|
|
24
|
+
}
|
|
18
25
|
|
|
19
26
|
/**
|
|
20
|
-
* Log a telemetry event
|
|
27
|
+
* Log a telemetry event - O(1) operation
|
|
21
28
|
*/
|
|
22
29
|
log(name: string, data?: Record<string, unknown>): void {
|
|
23
30
|
if (!this.enabled) return;
|
|
@@ -28,30 +35,44 @@ class Telemetry {
|
|
|
28
35
|
data,
|
|
29
36
|
};
|
|
30
37
|
|
|
31
|
-
|
|
38
|
+
// Circular buffer: O(1) write
|
|
39
|
+
this.events[this.head] = event;
|
|
40
|
+
this.head = (this.head + 1) % this.MAX_EVENTS;
|
|
32
41
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
this.events.splice(0, this.events.length - this.MAX_EVENTS);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (__DEV__) {
|
|
39
|
-
console.log(`[Groq Telemetry] ${name}`, data);
|
|
42
|
+
if (this.count < this.MAX_EVENTS) {
|
|
43
|
+
this.count++;
|
|
40
44
|
}
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
/**
|
|
44
|
-
* Get all events
|
|
48
|
+
* Get all events in chronological order
|
|
49
|
+
* Returns frozen array to prevent external mutations
|
|
45
50
|
*/
|
|
46
51
|
getEvents(): ReadonlyArray<TelemetryEvent> {
|
|
47
|
-
|
|
52
|
+
if (this.count === 0) {
|
|
53
|
+
return Object.freeze([]);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// O(n) but only when called, not on every log
|
|
57
|
+
if (this.count < this.MAX_EVENTS) {
|
|
58
|
+
// Not wrapped yet, just return slice
|
|
59
|
+
return Object.freeze(this.events.slice(0, this.count));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Wrapped around - need to reorder
|
|
63
|
+
const result: TelemetryEvent[] = new Array(this.count);
|
|
64
|
+
for (let i = 0; i < this.count; i++) {
|
|
65
|
+
result[i] = this.events[(this.head + i) % this.MAX_EVENTS];
|
|
66
|
+
}
|
|
67
|
+
return Object.freeze(result);
|
|
48
68
|
}
|
|
49
69
|
|
|
50
70
|
/**
|
|
51
|
-
* Clear all events
|
|
71
|
+
* Clear all events - O(1)
|
|
52
72
|
*/
|
|
53
73
|
clear(): void {
|
|
54
|
-
this.
|
|
74
|
+
this.head = 0;
|
|
75
|
+
this.count = 0;
|
|
55
76
|
}
|
|
56
77
|
|
|
57
78
|
/**
|
|
@@ -59,7 +80,6 @@ class Telemetry {
|
|
|
59
80
|
*/
|
|
60
81
|
setEnabled(enabled: boolean): void {
|
|
61
82
|
this.enabled = enabled;
|
|
62
|
-
// Disable cleanup when disabled
|
|
63
83
|
if (!enabled) {
|
|
64
84
|
this.clear();
|
|
65
85
|
}
|
|
@@ -73,10 +93,10 @@ class Telemetry {
|
|
|
73
93
|
}
|
|
74
94
|
|
|
75
95
|
/**
|
|
76
|
-
* Get event count (
|
|
96
|
+
* Get event count - O(1)
|
|
77
97
|
*/
|
|
78
98
|
getEventCount(): number {
|
|
79
|
-
return this.
|
|
99
|
+
return this.count;
|
|
80
100
|
}
|
|
81
101
|
}
|
|
82
102
|
|
|
@@ -87,7 +107,7 @@ export const telemetry = new Telemetry();
|
|
|
87
107
|
|
|
88
108
|
/**
|
|
89
109
|
* Hook to use telemetry in components
|
|
90
|
-
*
|
|
110
|
+
* Memoized to prevent unnecessary re-renders
|
|
91
111
|
*/
|
|
92
112
|
export function useTelemetry() {
|
|
93
113
|
return useMemo(
|
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Calculation Utilities
|
|
3
3
|
* Common calculation and utility functions for numeric operations
|
|
4
|
+
* Optimized for performance
|
|
4
5
|
*/
|
|
5
6
|
|
|
7
|
+
const MAX_RANDOM_ID_LENGTH = 11;
|
|
8
|
+
|
|
6
9
|
/**
|
|
7
10
|
* Generate a random unique identifier string
|
|
8
|
-
*
|
|
9
|
-
* @returns Random string in base-36
|
|
11
|
+
* Uses optimized single-pass operations
|
|
10
12
|
*/
|
|
11
13
|
export function generateRandomId(length: number = 9): string {
|
|
12
|
-
|
|
14
|
+
const safeLength = Math.min(Math.max(1, Math.floor(length)), MAX_RANDOM_ID_LENGTH);
|
|
15
|
+
const randomStr = Math.random().toString(36).substring(2, 2 + safeLength);
|
|
16
|
+
return randomStr;
|
|
13
17
|
}
|
|
14
18
|
|
|
15
19
|
/**
|
|
16
20
|
* Generate a unique chat session ID
|
|
17
|
-
* @param prefix - Optional prefix for the ID (default: "groq-chat")
|
|
18
|
-
* @returns Unique session identifier
|
|
19
21
|
*/
|
|
20
22
|
export function generateSessionId(prefix: string = "groq-chat"): string {
|
|
21
23
|
return `${prefix}-${Date.now()}-${generateRandomId(9)}`;
|
|
@@ -23,16 +25,13 @@ export function generateSessionId(prefix: string = "groq-chat"): string {
|
|
|
23
25
|
|
|
24
26
|
/**
|
|
25
27
|
* 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
28
|
*/
|
|
31
29
|
export function calculateMaxMessages(
|
|
32
30
|
maxTokens: number,
|
|
33
31
|
tokensPerMessage: number = 100
|
|
34
32
|
): number {
|
|
35
|
-
if (maxTokens <= 0
|
|
33
|
+
if (!Number.isFinite(maxTokens) || maxTokens <= 0 ||
|
|
34
|
+
!Number.isFinite(tokensPerMessage) || tokensPerMessage <= 0) {
|
|
36
35
|
return 0;
|
|
37
36
|
}
|
|
38
37
|
return Math.floor(maxTokens / tokensPerMessage);
|
|
@@ -40,12 +39,10 @@ export function calculateMaxMessages(
|
|
|
40
39
|
|
|
41
40
|
/**
|
|
42
41
|
* 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
42
|
*/
|
|
47
43
|
export function calculateExponentialBackoff(baseDelay: number, attempt: number): number {
|
|
48
|
-
if (baseDelay
|
|
44
|
+
if (!Number.isFinite(baseDelay) || baseDelay < 0 ||
|
|
45
|
+
!Number.isFinite(attempt) || attempt < 0) {
|
|
49
46
|
return 0;
|
|
50
47
|
}
|
|
51
48
|
return baseDelay * Math.pow(2, attempt);
|
|
@@ -53,41 +50,37 @@ export function calculateExponentialBackoff(baseDelay: number, attempt: number):
|
|
|
53
50
|
|
|
54
51
|
/**
|
|
55
52
|
* 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
53
|
*/
|
|
61
54
|
export function clamp(value: number, min: number, max: number): number {
|
|
55
|
+
if (!Number.isFinite(value)) return min;
|
|
56
|
+
if (!Number.isFinite(min)) return max;
|
|
57
|
+
if (!Number.isFinite(max)) return value;
|
|
62
58
|
return Math.min(Math.max(value, min), max);
|
|
63
59
|
}
|
|
64
60
|
|
|
65
61
|
/**
|
|
66
62
|
* Calculate percentage with optional decimal places
|
|
67
|
-
*
|
|
68
|
-
* @param total - Total value
|
|
69
|
-
* @param decimals - Number of decimal places (default: 2)
|
|
70
|
-
* @returns Percentage value
|
|
63
|
+
* Optimized: avoids string conversion
|
|
71
64
|
*/
|
|
72
65
|
export function calculatePercentage(
|
|
73
66
|
value: number,
|
|
74
67
|
total: number,
|
|
75
68
|
decimals: number = 2
|
|
76
69
|
): number {
|
|
77
|
-
if (total === 0) {
|
|
70
|
+
if (!Number.isFinite(value) || !Number.isFinite(total) || total === 0) {
|
|
78
71
|
return 0;
|
|
79
72
|
}
|
|
80
|
-
|
|
73
|
+
const multiplier = Math.pow(10, Math.max(0, Math.min(20, Math.floor(decimals))));
|
|
74
|
+
return Math.round((value / total) * 100 * multiplier) / multiplier;
|
|
81
75
|
}
|
|
82
76
|
|
|
83
77
|
/**
|
|
84
78
|
* 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
79
|
*/
|
|
90
80
|
export function calculateSafeBufferSize(currentSize: number, maxSize: number): number {
|
|
81
|
+
if (!Number.isFinite(currentSize) || !Number.isFinite(maxSize) || maxSize <= 0) {
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
91
84
|
if (currentSize > maxSize) {
|
|
92
85
|
return Math.floor(maxSize / 2);
|
|
93
86
|
}
|
|
@@ -96,86 +89,89 @@ export function calculateSafeBufferSize(currentSize: number, maxSize: number): n
|
|
|
96
89
|
|
|
97
90
|
/**
|
|
98
91
|
* 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
92
|
*/
|
|
103
93
|
export function estimateTokens(text: string): number {
|
|
104
|
-
if (!text)
|
|
105
|
-
return 0;
|
|
106
|
-
}
|
|
94
|
+
if (!text) return 0;
|
|
107
95
|
return Math.ceil(text.length / 4);
|
|
108
96
|
}
|
|
109
97
|
|
|
110
98
|
/**
|
|
111
99
|
* 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
100
|
*/
|
|
116
101
|
export function isWithinSafeLimit(messageCount: number, maxMessages: number): boolean {
|
|
117
|
-
return messageCount
|
|
102
|
+
return Number.isFinite(messageCount) &&
|
|
103
|
+
Number.isFinite(maxMessages) &&
|
|
104
|
+
messageCount >= 0 &&
|
|
105
|
+
messageCount <= maxMessages;
|
|
118
106
|
}
|
|
119
107
|
|
|
120
108
|
/**
|
|
121
109
|
* 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
110
|
*/
|
|
128
111
|
export function calculateRetryDelayWithJitter(
|
|
129
112
|
baseDelay: number,
|
|
130
113
|
attempt: number,
|
|
131
114
|
jitterFactor: number = 0.1
|
|
132
115
|
): number {
|
|
116
|
+
if (!Number.isFinite(baseDelay) || baseDelay < 0 ||
|
|
117
|
+
!Number.isFinite(attempt) || attempt < 0) {
|
|
118
|
+
return 0;
|
|
119
|
+
}
|
|
120
|
+
const safeJitterFactor = clamp(jitterFactor, 0, 1);
|
|
133
121
|
const exponentialDelay = calculateExponentialBackoff(baseDelay, attempt);
|
|
134
|
-
const jitter = exponentialDelay *
|
|
122
|
+
const jitter = exponentialDelay * safeJitterFactor * (Math.random() * 2 - 1);
|
|
135
123
|
return Math.max(0, exponentialDelay + jitter);
|
|
136
124
|
}
|
|
137
125
|
|
|
138
126
|
/**
|
|
139
127
|
* 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
128
|
*/
|
|
146
129
|
export function calculateRequestTimeout(
|
|
147
130
|
attempt: number,
|
|
148
131
|
baseTimeout: number = 5000,
|
|
149
132
|
maxTimeout: number = 30000
|
|
150
133
|
): number {
|
|
151
|
-
|
|
152
|
-
|
|
134
|
+
if (!Number.isFinite(attempt) || attempt < 0) {
|
|
135
|
+
return baseTimeout;
|
|
136
|
+
}
|
|
137
|
+
const safeBaseTimeout = Math.max(0, baseTimeout);
|
|
138
|
+
const safeMaxTimeout = Math.max(safeBaseTimeout, maxTimeout);
|
|
139
|
+
const timeout = calculateExponentialBackoff(safeBaseTimeout, attempt);
|
|
140
|
+
return Math.min(timeout, safeMaxTimeout);
|
|
153
141
|
}
|
|
154
142
|
|
|
155
143
|
/**
|
|
156
144
|
* 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
145
|
*/
|
|
161
146
|
export function calculateTransferRate(bytes: number, milliseconds: number): number {
|
|
162
|
-
if (
|
|
147
|
+
if (!Number.isFinite(bytes) || bytes < 0 ||
|
|
148
|
+
!Number.isFinite(milliseconds) || milliseconds <= 0) {
|
|
163
149
|
return 0;
|
|
164
150
|
}
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
return Number((kilobytes / seconds).toFixed(2));
|
|
151
|
+
const kilobytesPerSecond = (bytes / 1024) * (1000 / milliseconds);
|
|
152
|
+
return Math.round(kilobytesPerSecond * 100) / 100;
|
|
168
153
|
}
|
|
169
154
|
|
|
170
155
|
/**
|
|
171
156
|
* Calculate average from array of numbers
|
|
172
|
-
*
|
|
173
|
-
* @returns Average value or 0 if array is empty
|
|
157
|
+
* Optimized: Single pass with inline validation
|
|
174
158
|
*/
|
|
175
159
|
export function calculateAverage(values: number[]): number {
|
|
176
|
-
if (values.length === 0) {
|
|
160
|
+
if (!Array.isArray(values) || values.length === 0) {
|
|
177
161
|
return 0;
|
|
178
162
|
}
|
|
179
|
-
|
|
180
|
-
|
|
163
|
+
|
|
164
|
+
let sum = 0;
|
|
165
|
+
let count = 0;
|
|
166
|
+
|
|
167
|
+
// Single pass: validate and sum
|
|
168
|
+
for (let i = 0; i < values.length; i++) {
|
|
169
|
+
const v = values[i];
|
|
170
|
+
if (Number.isFinite(v)) {
|
|
171
|
+
sum += v;
|
|
172
|
+
count++;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return count === 0 ? 0 : sum / count;
|
|
181
177
|
}
|