@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,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
- const abortControllerRef = useRef<AbortController | null>(null);
32
- const operationIdRef = useRef(0);
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
- if (abortControllerRef.current) {
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
- controller.signal
62
+ signal
75
63
  );
76
64
  },
77
65
  (text: string) => {
78
- if (currentOpId === operationIdRef.current) {
79
- setResult(text);
80
- options.onSuccess?.(text);
81
- }
66
+ setResult(text);
67
+ options.onSuccess?.(text);
82
68
  }
83
69
  );
84
- } finally {
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
- if (abortControllerRef.current) {
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) => setResult(typeof value === "string" ? value : JSON.stringify(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
- controller.signal
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
- controller.signal
111
+ signal
143
112
  );
144
113
 
145
114
  return parseJsonResponse<T>(text);
146
115
  },
147
116
  (parsed: unknown) => {
148
- if (currentOpId === operationIdRef.current) {
149
- setJsonResult(parsed);
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
- } finally {
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
- if (abortControllerRef.current) {
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 { GeminiConfig } from "../domain/entities";
4
- import type {
5
- ProviderConfigInput,
6
- ResolvedProviderConfig,
7
- } from "./ProviderConfig";
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 extends ProviderConfigInput {
11
- /** Provider strategy */
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: ResolvedProviderConfig | null = null;
17
- private currentOptions: ProviderFactoryOptions | null = null;
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
- const config = resolveProviderConfig(options);
23
+ // Build configuration using builder pattern
24
+ this.builder = ConfigBuilder.create()
25
+ .withApiKey(options.apiKey);
24
26
 
25
- // Apply strategy-based adjustments
26
- if (options.strategy === "quality") {
27
- config.timeout = 60000; // Longer timeout for quality
27
+ if (options.strategy) {
28
+ this.builder.withStrategy(options.strategy);
28
29
  }
29
30
 
30
- this.currentConfig = config;
31
- this.currentOptions = options;
31
+ if (options.textModel) {
32
+ this.builder.withTextModel(options.textModel);
33
+ }
32
34
 
33
- // Initialize Gemini client with resolved config
34
- const geminiConfig: GeminiConfig = {
35
- apiKey: config.apiKey,
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 resolved configuration
47
+ * Get current configuration
45
48
  */
46
- getConfig(): ResolvedProviderConfig | null {
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 without re-initializing
59
- * Note: Changing apiKey requires full re-initialization
61
+ * Update configuration
62
+ * API key changes require re-initialization
60
63
  */
61
- updateConfig(updates: Partial<ProviderConfigInput>): void {
62
- if (!this.currentConfig || !this.currentOptions) {
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 is changing, we need to re-initialize
69
+ // If API key changes, re-initialize
67
70
  if (updates.apiKey && updates.apiKey !== this.currentConfig.apiKey) {
68
- const newInput: ProviderFactoryOptions = {
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
- // For other updates, merge with current config
78
- const mergedPreferences = {
79
- ...this.currentOptions.preferences,
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
 
@@ -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 { resolveProviderConfig } from "./ProviderConfig";
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
- }