@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.
- package/package.json +1 -1
- package/src/index.ts +11 -104
- package/src/infrastructure/interceptors/BaseInterceptor.ts +78 -0
- package/src/infrastructure/interceptors/RequestInterceptors.ts +6 -62
- package/src/infrastructure/interceptors/ResponseInterceptors.ts +6 -61
- package/src/infrastructure/interceptors/index.ts +7 -13
- package/src/infrastructure/services/base-gemini.service.ts +82 -0
- package/src/infrastructure/services/gemini-streaming.service.ts +31 -61
- package/src/infrastructure/services/gemini-text-generation.service.ts +11 -33
- package/src/infrastructure/services/index.ts +7 -4
- package/src/infrastructure/utils/async/index.ts +1 -10
- package/src/infrastructure/utils/index.ts +43 -26
- package/src/infrastructure/utils/stream-processor.util.ts +156 -0
- package/src/infrastructure/utils/validation-composer.util.ts +160 -0
- package/src/infrastructure/utils/validation.util.ts +9 -68
- package/src/presentation/hooks/index.ts +6 -1
- package/src/presentation/hooks/use-gemini.ts +21 -72
- package/src/presentation/hooks/use-operation-manager.ts +88 -0
- package/src/providers/ConfigBuilder.ts +121 -0
- package/src/providers/ProviderFactory.ts +37 -49
- package/src/providers/index.ts +4 -13
- package/src/infrastructure/utils/async/debounce.util.ts +0 -100
- package/src/infrastructure/utils/async/memoize.util.ts +0 -55
- package/src/infrastructure/utils/env.util.ts +0 -175
- package/src/infrastructure/utils/performance.util.ts +0 -139
- package/src/infrastructure/utils/rate-limiter.util.ts +0 -86
- package/src/infrastructure/utils/retry.util.ts +0 -158
- package/src/providers/ProviderConfig.ts +0 -36
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
import { useState, useCallback, useRef, useMemo, useEffect } from "react";
|
|
1
|
+
import { useState, useCallback, useMemo } from "react";
|
|
3
2
|
import type { GeminiGenerationConfig } from "../../domain/entities";
|
|
4
3
|
import { DEFAULT_MODELS } from "../../domain/entities";
|
|
5
4
|
import { geminiTextGenerationService, geminiStructuredTextService } from "../../infrastructure/services";
|
|
6
5
|
import { executeWithState, type AsyncStateSetters } from "../../infrastructure/utils/async";
|
|
7
6
|
import { parseJsonResponse } from "../../infrastructure/utils/json-parser.util";
|
|
7
|
+
import { useOperationManager } from "./use-operation-manager";
|
|
8
8
|
|
|
9
9
|
export interface UseGeminiOptions {
|
|
10
10
|
model?: string;
|
|
@@ -28,8 +28,8 @@ export function useGemini(options: UseGeminiOptions = {}): UseGeminiReturn {
|
|
|
28
28
|
const [jsonResult, setJsonResult] = useState<unknown>(null);
|
|
29
29
|
const [isGenerating, setIsGenerating] = useState(false);
|
|
30
30
|
const [error, setError] = useState<string | null>(null);
|
|
31
|
-
|
|
32
|
-
const
|
|
31
|
+
|
|
32
|
+
const { executeOperation, abort } = useOperationManager();
|
|
33
33
|
|
|
34
34
|
const setters: AsyncStateSetters<string, unknown> = useMemo(
|
|
35
35
|
() => ({
|
|
@@ -50,63 +50,37 @@ export function useGemini(options: UseGeminiOptions = {}): UseGeminiReturn {
|
|
|
50
50
|
|
|
51
51
|
const generate = useCallback(
|
|
52
52
|
async (prompt: string) => {
|
|
53
|
-
|
|
54
|
-
abortControllerRef.current.abort();
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const controller = new AbortController();
|
|
58
|
-
abortControllerRef.current = controller;
|
|
59
|
-
const currentOpId = ++operationIdRef.current;
|
|
60
|
-
|
|
61
|
-
try {
|
|
53
|
+
await executeOperation(async (signal, _operationId) => {
|
|
62
54
|
await executeWithState(
|
|
63
55
|
setters,
|
|
64
56
|
callbacks,
|
|
65
57
|
async () => {
|
|
66
|
-
if (currentOpId !== operationIdRef.current) {
|
|
67
|
-
controller.abort();
|
|
68
|
-
throw new Error("Operation cancelled by newer request");
|
|
69
|
-
}
|
|
70
58
|
return geminiTextGenerationService.generateText(
|
|
71
59
|
model,
|
|
72
60
|
prompt,
|
|
73
61
|
options.generationConfig,
|
|
74
|
-
|
|
62
|
+
signal
|
|
75
63
|
);
|
|
76
64
|
},
|
|
77
65
|
(text: string) => {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
options.onSuccess?.(text);
|
|
81
|
-
}
|
|
66
|
+
setResult(text);
|
|
67
|
+
options.onSuccess?.(text);
|
|
82
68
|
}
|
|
83
69
|
);
|
|
84
|
-
}
|
|
85
|
-
if (currentOpId === operationIdRef.current) {
|
|
86
|
-
abortControllerRef.current = null;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
70
|
+
});
|
|
89
71
|
},
|
|
90
|
-
[model, options.generationConfig, setters, callbacks, options.onSuccess]
|
|
72
|
+
[model, options.generationConfig, setters, callbacks, options.onSuccess, executeOperation]
|
|
91
73
|
);
|
|
92
74
|
|
|
93
75
|
const generateJSON = useCallback(
|
|
94
76
|
async <T>(prompt: string, schema?: Record<string, unknown>): Promise<T | null> => {
|
|
95
|
-
|
|
96
|
-
abortControllerRef.current.abort();
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const controller = new AbortController();
|
|
100
|
-
abortControllerRef.current = controller;
|
|
101
|
-
const currentOpId = ++operationIdRef.current;
|
|
102
|
-
|
|
103
|
-
try {
|
|
104
|
-
// Create separate setters for JSON generation with proper types
|
|
77
|
+
return executeOperation(async (signal, _operationId) => {
|
|
105
78
|
const jsonSetters: AsyncStateSetters<unknown, unknown> = {
|
|
106
79
|
setIsLoading: setIsGenerating,
|
|
107
80
|
setError,
|
|
108
81
|
setResult: setJsonResult,
|
|
109
|
-
setSecondaryResult: (value) =>
|
|
82
|
+
setSecondaryResult: (value) =>
|
|
83
|
+
setResult(typeof value === "string" ? value : JSON.stringify(value)),
|
|
110
84
|
};
|
|
111
85
|
|
|
112
86
|
const jsonCallbacks = {
|
|
@@ -120,18 +94,13 @@ export function useGemini(options: UseGeminiOptions = {}): UseGeminiReturn {
|
|
|
120
94
|
jsonSetters,
|
|
121
95
|
jsonCallbacks,
|
|
122
96
|
async () => {
|
|
123
|
-
if (currentOpId !== operationIdRef.current) {
|
|
124
|
-
controller.abort();
|
|
125
|
-
throw new Error("Operation cancelled by newer request");
|
|
126
|
-
}
|
|
127
|
-
|
|
128
97
|
if (schema) {
|
|
129
98
|
return geminiStructuredTextService.generateStructuredText<T>(
|
|
130
99
|
model,
|
|
131
100
|
prompt,
|
|
132
101
|
schema,
|
|
133
102
|
options.generationConfig,
|
|
134
|
-
|
|
103
|
+
signal
|
|
135
104
|
);
|
|
136
105
|
}
|
|
137
106
|
|
|
@@ -139,50 +108,30 @@ export function useGemini(options: UseGeminiOptions = {}): UseGeminiReturn {
|
|
|
139
108
|
model,
|
|
140
109
|
prompt,
|
|
141
110
|
{ ...options.generationConfig, responseMimeType: "application/json" },
|
|
142
|
-
|
|
111
|
+
signal
|
|
143
112
|
);
|
|
144
113
|
|
|
145
114
|
return parseJsonResponse<T>(text);
|
|
146
115
|
},
|
|
147
116
|
(parsed: unknown) => {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
setResult(JSON.stringify(parsed, null, 2));
|
|
151
|
-
}
|
|
117
|
+
setJsonResult(parsed);
|
|
118
|
+
setResult(JSON.stringify(parsed, null, 2));
|
|
152
119
|
}
|
|
153
120
|
);
|
|
154
121
|
|
|
155
122
|
return operationResult as T | null;
|
|
156
|
-
}
|
|
157
|
-
if (currentOpId === operationIdRef.current) {
|
|
158
|
-
abortControllerRef.current = null;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
123
|
+
});
|
|
161
124
|
},
|
|
162
|
-
[model, options.generationConfig, callbacks, options.onSuccess]
|
|
125
|
+
[model, options.generationConfig, callbacks, options.onSuccess, executeOperation]
|
|
163
126
|
);
|
|
164
127
|
|
|
165
128
|
const reset = useCallback(() => {
|
|
166
|
-
|
|
167
|
-
abortControllerRef.current.abort();
|
|
168
|
-
abortControllerRef.current = null;
|
|
169
|
-
}
|
|
170
|
-
operationIdRef.current++;
|
|
171
|
-
|
|
129
|
+
abort();
|
|
172
130
|
setResult(null);
|
|
173
131
|
setJsonResult(null);
|
|
174
132
|
setIsGenerating(false);
|
|
175
133
|
setError(null);
|
|
176
|
-
}, []);
|
|
177
|
-
|
|
178
|
-
useEffect(() => {
|
|
179
|
-
return () => {
|
|
180
|
-
if (abortControllerRef.current) {
|
|
181
|
-
abortControllerRef.current.abort();
|
|
182
|
-
}
|
|
183
|
-
operationIdRef.current++;
|
|
184
|
-
};
|
|
185
|
-
}, []);
|
|
134
|
+
}, [abort]);
|
|
186
135
|
|
|
187
136
|
return { generate, generateJSON, result, jsonResult, isGenerating, error, reset };
|
|
188
137
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Operation Manager Hook
|
|
3
|
+
* Reusable abort controller and operation ID management
|
|
4
|
+
* Eliminates code duplication in hooks
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useRef, useCallback, useEffect } from "react";
|
|
8
|
+
|
|
9
|
+
export interface OperationManager {
|
|
10
|
+
/**
|
|
11
|
+
* Execute an operation with abort support and operation ID tracking
|
|
12
|
+
*/
|
|
13
|
+
executeOperation: <T>(
|
|
14
|
+
operation: (signal: AbortSignal, operationId: number) => Promise<T>
|
|
15
|
+
) => Promise<T>;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Abort current operation
|
|
19
|
+
*/
|
|
20
|
+
abort: () => void;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check if current operation is active
|
|
24
|
+
*/
|
|
25
|
+
isOperationActive: (operationId: number) => boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Hook for managing operations with abort control
|
|
30
|
+
*/
|
|
31
|
+
export function useOperationManager(): OperationManager {
|
|
32
|
+
const abortControllerRef = useRef<AbortController | null>(null);
|
|
33
|
+
const operationIdRef = useRef(0);
|
|
34
|
+
|
|
35
|
+
const abort = useCallback(() => {
|
|
36
|
+
if (abortControllerRef.current) {
|
|
37
|
+
abortControllerRef.current.abort();
|
|
38
|
+
abortControllerRef.current = null;
|
|
39
|
+
}
|
|
40
|
+
operationIdRef.current++;
|
|
41
|
+
}, []);
|
|
42
|
+
|
|
43
|
+
const isOperationActive = useCallback((operationId: number): boolean => {
|
|
44
|
+
return operationId === operationIdRef.current;
|
|
45
|
+
}, []);
|
|
46
|
+
|
|
47
|
+
const executeOperation = useCallback(
|
|
48
|
+
async <T>(
|
|
49
|
+
operation: (signal: AbortSignal, operationId: number) => Promise<T>
|
|
50
|
+
): Promise<T> => {
|
|
51
|
+
// Abort any existing operation
|
|
52
|
+
if (abortControllerRef.current) {
|
|
53
|
+
abortControllerRef.current.abort();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Create new controller and increment operation ID
|
|
57
|
+
const controller = new AbortController();
|
|
58
|
+
abortControllerRef.current = controller;
|
|
59
|
+
const currentOpId = ++operationIdRef.current;
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
return await operation(controller.signal, currentOpId);
|
|
63
|
+
} finally {
|
|
64
|
+
// Clean up only if this is still the current operation
|
|
65
|
+
if (currentOpId === operationIdRef.current) {
|
|
66
|
+
abortControllerRef.current = null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
[]
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// Cleanup on unmount
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
return () => {
|
|
76
|
+
if (abortControllerRef.current) {
|
|
77
|
+
abortControllerRef.current.abort();
|
|
78
|
+
}
|
|
79
|
+
operationIdRef.current++;
|
|
80
|
+
};
|
|
81
|
+
}, []);
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
executeOperation,
|
|
85
|
+
abort,
|
|
86
|
+
isOperationActive,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config Builder Pattern
|
|
3
|
+
* Fluent API for building provider configuration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { DEFAULT_MODELS } from "../domain/entities";
|
|
7
|
+
import type { GeminiConfig } from "../domain/entities";
|
|
8
|
+
|
|
9
|
+
export interface ProviderConfig {
|
|
10
|
+
apiKey: string;
|
|
11
|
+
textModel: string;
|
|
12
|
+
timeout: number;
|
|
13
|
+
strategy?: "cost" | "quality";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Builder for constructing provider configuration
|
|
18
|
+
* Provides a fluent API with validation and defaults
|
|
19
|
+
*/
|
|
20
|
+
export class ConfigBuilder {
|
|
21
|
+
private config: Partial<ProviderConfig> = {
|
|
22
|
+
textModel: DEFAULT_MODELS.TEXT,
|
|
23
|
+
timeout: 30000,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Set API key (required)
|
|
28
|
+
*/
|
|
29
|
+
withApiKey(apiKey: string): this {
|
|
30
|
+
if (!apiKey || typeof apiKey !== "string" || apiKey.trim().length === 0) {
|
|
31
|
+
throw new Error("API key must be a non-empty string");
|
|
32
|
+
}
|
|
33
|
+
this.config.apiKey = apiKey.trim();
|
|
34
|
+
return this;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Set text model
|
|
39
|
+
*/
|
|
40
|
+
withTextModel(model: string): this {
|
|
41
|
+
if (!model || !model.startsWith("gemini-")) {
|
|
42
|
+
throw new Error("Invalid model name");
|
|
43
|
+
}
|
|
44
|
+
this.config.textModel = model;
|
|
45
|
+
return this;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Set request timeout (ms)
|
|
50
|
+
*/
|
|
51
|
+
withTimeout(timeout: number): this {
|
|
52
|
+
if (timeout <= 0 || timeout > 300000) {
|
|
53
|
+
throw new Error("Timeout must be between 1ms and 300000ms (5 minutes)");
|
|
54
|
+
}
|
|
55
|
+
this.config.timeout = timeout;
|
|
56
|
+
return this;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Set strategy (applies preset timeout)
|
|
61
|
+
*/
|
|
62
|
+
withStrategy(strategy: "cost" | "quality"): this {
|
|
63
|
+
this.config.strategy = strategy;
|
|
64
|
+
|
|
65
|
+
// Apply strategy-based defaults
|
|
66
|
+
if (strategy === "quality") {
|
|
67
|
+
this.config.timeout = 60000;
|
|
68
|
+
} else {
|
|
69
|
+
this.config.timeout = 30000;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return this;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Build final configuration
|
|
77
|
+
*/
|
|
78
|
+
build(): ProviderConfig {
|
|
79
|
+
if (!this.config.apiKey) {
|
|
80
|
+
throw new Error("API key is required. Call withApiKey() before build()");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
apiKey: this.config.apiKey,
|
|
85
|
+
textModel: this.config.textModel!,
|
|
86
|
+
timeout: this.config.timeout!,
|
|
87
|
+
strategy: this.config.strategy,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Convert to GeminiConfig format
|
|
93
|
+
*/
|
|
94
|
+
toGeminiConfig(): GeminiConfig {
|
|
95
|
+
const config = this.build();
|
|
96
|
+
return {
|
|
97
|
+
apiKey: config.apiKey,
|
|
98
|
+
textModel: config.textModel,
|
|
99
|
+
defaultTimeoutMs: config.timeout,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Create a new builder instance
|
|
105
|
+
*/
|
|
106
|
+
static create(): ConfigBuilder {
|
|
107
|
+
return new ConfigBuilder();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Create from existing config (for updates)
|
|
112
|
+
*/
|
|
113
|
+
static from(config: Partial<ProviderConfig>): ConfigBuilder {
|
|
114
|
+
const builder = new ConfigBuilder();
|
|
115
|
+
if (config.apiKey) builder.withApiKey(config.apiKey);
|
|
116
|
+
if (config.textModel) builder.withTextModel(config.textModel);
|
|
117
|
+
if (config.timeout) builder.withTimeout(config.timeout);
|
|
118
|
+
if (config.strategy) builder.withStrategy(config.strategy);
|
|
119
|
+
return builder;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -1,49 +1,52 @@
|
|
|
1
|
-
|
|
2
1
|
import { geminiClientCoreService } from "../infrastructure/services/gemini-client-core.service";
|
|
3
|
-
import type
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
} from "./
|
|
8
|
-
import { resolveProviderConfig } from "./ProviderConfig";
|
|
2
|
+
import { ConfigBuilder, type ProviderConfig } from "./ConfigBuilder";
|
|
3
|
+
|
|
4
|
+
// Re-export for public API
|
|
5
|
+
export { ConfigBuilder } from "./ConfigBuilder";
|
|
6
|
+
export type { ProviderConfig } from "./ConfigBuilder";
|
|
9
7
|
|
|
10
|
-
export interface ProviderFactoryOptions
|
|
11
|
-
|
|
8
|
+
export interface ProviderFactoryOptions {
|
|
9
|
+
apiKey: string;
|
|
10
|
+
timeout?: number;
|
|
11
|
+
textModel?: string;
|
|
12
12
|
strategy?: "cost" | "quality";
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
class ProviderFactory {
|
|
16
|
-
private currentConfig:
|
|
17
|
-
private
|
|
16
|
+
private currentConfig: ProviderConfig | null = null;
|
|
17
|
+
private builder: ConfigBuilder | null = null;
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Initialize provider with configuration
|
|
21
21
|
*/
|
|
22
22
|
initialize(options: ProviderFactoryOptions): void {
|
|
23
|
-
|
|
23
|
+
// Build configuration using builder pattern
|
|
24
|
+
this.builder = ConfigBuilder.create()
|
|
25
|
+
.withApiKey(options.apiKey);
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
config.timeout = 60000; // Longer timeout for quality
|
|
27
|
+
if (options.strategy) {
|
|
28
|
+
this.builder.withStrategy(options.strategy);
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
if (options.textModel) {
|
|
32
|
+
this.builder.withTextModel(options.textModel);
|
|
33
|
+
}
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
defaultTimeoutMs: config.timeout,
|
|
37
|
-
textModel: config.textModel,
|
|
38
|
-
};
|
|
35
|
+
if (options.timeout) {
|
|
36
|
+
this.builder.withTimeout(options.timeout);
|
|
37
|
+
}
|
|
39
38
|
|
|
39
|
+
this.currentConfig = this.builder.build();
|
|
40
|
+
|
|
41
|
+
// Initialize Gemini client
|
|
42
|
+
const geminiConfig = this.builder.toGeminiConfig();
|
|
40
43
|
geminiClientCoreService.initialize(geminiConfig);
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
/**
|
|
44
|
-
* Get current
|
|
47
|
+
* Get current configuration
|
|
45
48
|
*/
|
|
46
|
-
getConfig():
|
|
49
|
+
getConfig(): ProviderConfig | null {
|
|
47
50
|
return this.currentConfig;
|
|
48
51
|
}
|
|
49
52
|
|
|
@@ -55,38 +58,23 @@ class ProviderFactory {
|
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
/**
|
|
58
|
-
* Update configuration
|
|
59
|
-
*
|
|
61
|
+
* Update configuration
|
|
62
|
+
* API key changes require re-initialization
|
|
60
63
|
*/
|
|
61
|
-
updateConfig(updates: Partial<
|
|
62
|
-
if (!this.currentConfig
|
|
64
|
+
updateConfig(updates: Partial<ProviderFactoryOptions>): void {
|
|
65
|
+
if (!this.currentConfig) {
|
|
63
66
|
throw new Error("Provider not initialized. Call initialize() first.");
|
|
64
67
|
}
|
|
65
68
|
|
|
66
|
-
// If API key
|
|
69
|
+
// If API key changes, re-initialize
|
|
67
70
|
if (updates.apiKey && updates.apiKey !== this.currentConfig.apiKey) {
|
|
68
|
-
|
|
69
|
-
apiKey: updates.apiKey,
|
|
70
|
-
preferences: updates.preferences || this.currentOptions.preferences,
|
|
71
|
-
strategy: this.currentOptions.strategy,
|
|
72
|
-
};
|
|
73
|
-
this.initialize(newInput);
|
|
71
|
+
this.initialize({ ...this.currentConfig, ...updates });
|
|
74
72
|
return;
|
|
75
73
|
}
|
|
76
74
|
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
...updates.preferences,
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
this.currentOptions.preferences = mergedPreferences;
|
|
84
|
-
|
|
85
|
-
this.currentConfig = {
|
|
86
|
-
apiKey: this.currentConfig.apiKey,
|
|
87
|
-
textModel: this.currentConfig.textModel,
|
|
88
|
-
timeout: mergedPreferences.timeout ?? this.currentConfig.timeout,
|
|
89
|
-
};
|
|
75
|
+
// Otherwise, update using builder
|
|
76
|
+
this.builder = ConfigBuilder.from({ ...this.currentConfig, ...updates });
|
|
77
|
+
this.currentConfig = this.builder.build();
|
|
90
78
|
}
|
|
91
79
|
}
|
|
92
80
|
|
package/src/providers/index.ts
CHANGED
|
@@ -1,18 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Provider Configuration & Factory
|
|
3
|
-
* Centralized configuration system for AI provider setup
|
|
2
|
+
* Provider Configuration & Factory - Public API
|
|
4
3
|
*/
|
|
5
4
|
|
|
6
|
-
export {
|
|
7
|
-
|
|
8
|
-
export type {
|
|
9
|
-
ProviderPreferences,
|
|
10
|
-
ProviderConfigInput,
|
|
11
|
-
ResolvedProviderConfig,
|
|
12
|
-
} from "./ProviderConfig";
|
|
5
|
+
export { ConfigBuilder } from "./ConfigBuilder";
|
|
6
|
+
export type { ProviderConfig } from "./ConfigBuilder";
|
|
13
7
|
|
|
14
8
|
export { providerFactory } from "./ProviderFactory";
|
|
15
|
-
|
|
16
|
-
export type {
|
|
17
|
-
ProviderFactoryOptions,
|
|
18
|
-
} from "./ProviderFactory";
|
|
9
|
+
export type { ProviderFactoryOptions } from "./ProviderFactory";
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Debounced Async Utilities
|
|
3
|
-
* Creates debounced versions of async functions with proper cancellation
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Create a debounced async function with state management
|
|
8
|
-
* Fixed to prevent race conditions when cancelled
|
|
9
|
-
*
|
|
10
|
-
* @example
|
|
11
|
-
* ```ts
|
|
12
|
-
* const debouncedFetch = createDebouncedAsync(fetchData, 300);
|
|
13
|
-
* const result = await debouncedFetch.execute(id);
|
|
14
|
-
* ```
|
|
15
|
-
*/
|
|
16
|
-
export function createDebouncedAsync<T, Args extends unknown[]>(
|
|
17
|
-
fn: (...args: Args) => Promise<T>,
|
|
18
|
-
delay: number,
|
|
19
|
-
): {
|
|
20
|
-
execute: (...args: Args) => Promise<T | null>;
|
|
21
|
-
cancel: () => void;
|
|
22
|
-
flush: () => void;
|
|
23
|
-
} {
|
|
24
|
-
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
25
|
-
let currentResolve: ((result: T | null) => void) | null = null;
|
|
26
|
-
let isCancelled = false;
|
|
27
|
-
|
|
28
|
-
const execute = (...args: Args): Promise<T | null> => {
|
|
29
|
-
return new Promise((resolve) => {
|
|
30
|
-
// Cancel any pending operation
|
|
31
|
-
if (timeout) {
|
|
32
|
-
clearTimeout(timeout);
|
|
33
|
-
// Mark previous operation as cancelled
|
|
34
|
-
isCancelled = true;
|
|
35
|
-
// Resolve previous promise with null
|
|
36
|
-
if (currentResolve) {
|
|
37
|
-
currentResolve(null);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Store new resolver and reset cancelled flag
|
|
42
|
-
currentResolve = resolve;
|
|
43
|
-
isCancelled = false;
|
|
44
|
-
|
|
45
|
-
timeout = setTimeout(() => {
|
|
46
|
-
// Capture current resolve and reset it
|
|
47
|
-
const resolveFn = currentResolve;
|
|
48
|
-
currentResolve = null;
|
|
49
|
-
|
|
50
|
-
// Check if this operation was cancelled before executing
|
|
51
|
-
if (isCancelled) {
|
|
52
|
-
resolveFn?.(null);
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
fn(...args)
|
|
57
|
-
.then((result) => {
|
|
58
|
-
// Only resolve if not cancelled
|
|
59
|
-
if (!isCancelled && resolveFn) {
|
|
60
|
-
resolveFn(result);
|
|
61
|
-
}
|
|
62
|
-
})
|
|
63
|
-
.catch(() => {
|
|
64
|
-
// Only resolve with null if not cancelled
|
|
65
|
-
if (!isCancelled && resolveFn) {
|
|
66
|
-
resolveFn(null);
|
|
67
|
-
}
|
|
68
|
-
})
|
|
69
|
-
.finally(() => {
|
|
70
|
-
timeout = undefined;
|
|
71
|
-
});
|
|
72
|
-
}, delay);
|
|
73
|
-
});
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
const cancel = (): void => {
|
|
77
|
-
if (timeout) {
|
|
78
|
-
clearTimeout(timeout);
|
|
79
|
-
timeout = undefined;
|
|
80
|
-
}
|
|
81
|
-
// Mark as cancelled and resolve pending promise
|
|
82
|
-
isCancelled = true;
|
|
83
|
-
if (currentResolve) {
|
|
84
|
-
currentResolve(null);
|
|
85
|
-
currentResolve = null;
|
|
86
|
-
}
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
const flush = (): void => {
|
|
90
|
-
if (timeout) {
|
|
91
|
-
clearTimeout(timeout);
|
|
92
|
-
// Don't resolve - just cancel
|
|
93
|
-
timeout = undefined;
|
|
94
|
-
}
|
|
95
|
-
isCancelled = true;
|
|
96
|
-
currentResolve = null;
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
return { execute, cancel, flush };
|
|
100
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Memoized Async Utilities
|
|
3
|
-
* Creates memoized versions of async functions with TTL-based caching
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Create a memoized async function with cache TTL
|
|
8
|
-
*
|
|
9
|
-
* @example
|
|
10
|
-
* ```ts
|
|
11
|
-
* const memoizedFetch = createMemoizedAsync(fetchData, 5000);
|
|
12
|
-
* const result1 = await memoizedFetch.execute(id); // Fetches
|
|
13
|
-
* const result2 = await memoizedFetch.execute(id); // Uses cache
|
|
14
|
-
* memoizedFetch.invalidate(id); // Clears cache for this id
|
|
15
|
-
* ```
|
|
16
|
-
*/
|
|
17
|
-
export function createMemoizedAsync<T, Args extends unknown[]>(
|
|
18
|
-
fn: (...args: Args) => Promise<T>,
|
|
19
|
-
ttl: number = 5000,
|
|
20
|
-
): {
|
|
21
|
-
execute: (...args: Args) => Promise<T>;
|
|
22
|
-
invalidate: (...args: Args) => void;
|
|
23
|
-
clear: () => void;
|
|
24
|
-
} {
|
|
25
|
-
const cache = new Map<string, { data: T; timestamp: number }>();
|
|
26
|
-
|
|
27
|
-
const generateKey = (args: Args): string => {
|
|
28
|
-
return JSON.stringify(args);
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
const execute = async (...args: Args): Promise<T> => {
|
|
32
|
-
const key = generateKey(args);
|
|
33
|
-
const cached = cache.get(key);
|
|
34
|
-
|
|
35
|
-
if (cached && Date.now() - cached.timestamp < ttl) {
|
|
36
|
-
return cached.data;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const result = await fn(...args);
|
|
40
|
-
cache.set(key, { data: result, timestamp: Date.now() });
|
|
41
|
-
|
|
42
|
-
return result;
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
const invalidate = (...args: Args): void => {
|
|
46
|
-
const key = generateKey(args);
|
|
47
|
-
cache.delete(key);
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
const clear = (): void => {
|
|
51
|
-
cache.clear();
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
return { execute, invalidate, clear };
|
|
55
|
-
}
|