ubersearch 0.0.0-development
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/LICENSE +21 -0
- package/README.md +374 -0
- package/package.json +76 -0
- package/src/app/index.ts +30 -0
- package/src/bootstrap/container.ts +157 -0
- package/src/cli.ts +380 -0
- package/src/config/defineConfig.ts +176 -0
- package/src/config/load.ts +368 -0
- package/src/config/types.ts +86 -0
- package/src/config/validation.ts +148 -0
- package/src/core/cache.ts +74 -0
- package/src/core/container.ts +268 -0
- package/src/core/credits/CreditManager.ts +158 -0
- package/src/core/credits/CreditStateProvider.ts +151 -0
- package/src/core/credits/FileCreditStateProvider.ts +137 -0
- package/src/core/credits/index.ts +3 -0
- package/src/core/docker/dockerComposeHelper.ts +177 -0
- package/src/core/docker/dockerLifecycleManager.ts +361 -0
- package/src/core/docker/index.ts +8 -0
- package/src/core/logger.ts +146 -0
- package/src/core/orchestrator.ts +103 -0
- package/src/core/paths.ts +157 -0
- package/src/core/provider/ILifecycleProvider.ts +120 -0
- package/src/core/provider/ProviderFactory.ts +120 -0
- package/src/core/provider.ts +61 -0
- package/src/core/serviceKeys.ts +45 -0
- package/src/core/strategy/AllProvidersStrategy.ts +245 -0
- package/src/core/strategy/FirstSuccessStrategy.ts +98 -0
- package/src/core/strategy/ISearchStrategy.ts +94 -0
- package/src/core/strategy/StrategyFactory.ts +204 -0
- package/src/core/strategy/index.ts +9 -0
- package/src/core/strategy/types.ts +56 -0
- package/src/core/types.ts +58 -0
- package/src/index.ts +1 -0
- package/src/plugin/PluginRegistry.ts +336 -0
- package/src/plugin/builtin.ts +130 -0
- package/src/plugin/index.ts +33 -0
- package/src/plugin/types.ts +212 -0
- package/src/providers/BaseProvider.ts +49 -0
- package/src/providers/brave.ts +66 -0
- package/src/providers/constants.ts +13 -0
- package/src/providers/helpers/index.ts +24 -0
- package/src/providers/helpers/lifecycleHelpers.ts +110 -0
- package/src/providers/helpers/resultMappers.ts +168 -0
- package/src/providers/index.ts +6 -0
- package/src/providers/linkup.ts +114 -0
- package/src/providers/retry.ts +95 -0
- package/src/providers/searchxng.ts +163 -0
- package/src/providers/tavily.ts +73 -0
- package/src/providers/types/index.ts +185 -0
- package/src/providers/utils.ts +182 -0
- package/src/tool/allSearchTool.ts +110 -0
- package/src/tool/interface.ts +71 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AllProvidersStrategy - Queries all available providers and combines their results
|
|
3
|
+
*
|
|
4
|
+
* This strategy executes search queries against all configured providers,
|
|
5
|
+
* collecting results from all successful providers. Supports both sequential
|
|
6
|
+
* and parallel execution modes. Failed providers are logged but don't stop
|
|
7
|
+
* the overall search.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createLogger } from "../logger";
|
|
11
|
+
import type { SearchProvider } from "../provider";
|
|
12
|
+
import type { EngineId, SearchResponse, SearchResultItem } from "../types";
|
|
13
|
+
import { SearchError } from "../types";
|
|
14
|
+
import type {
|
|
15
|
+
EngineAttempt,
|
|
16
|
+
ISearchStrategy,
|
|
17
|
+
AllSearchOptions,
|
|
18
|
+
StrategyContext,
|
|
19
|
+
StrategyResult,
|
|
20
|
+
} from "./ISearchStrategy";
|
|
21
|
+
|
|
22
|
+
const log = createLogger("AllProviders");
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Result from a single engine search attempt
|
|
26
|
+
*/
|
|
27
|
+
interface EngineSearchResult {
|
|
28
|
+
engineId: EngineId;
|
|
29
|
+
response?: SearchResponse;
|
|
30
|
+
error?: Error;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Strategy implementation that queries all providers and combines results
|
|
35
|
+
*
|
|
36
|
+
* Supports two execution modes:
|
|
37
|
+
* - Sequential (default): Providers are queried one at a time
|
|
38
|
+
* - Parallel: All providers are queried simultaneously using Promise.allSettled
|
|
39
|
+
*/
|
|
40
|
+
export class AllProvidersStrategy implements ISearchStrategy {
|
|
41
|
+
/**
|
|
42
|
+
* Execute search against all providers and combine results
|
|
43
|
+
*
|
|
44
|
+
* @param query - The search query string
|
|
45
|
+
* @param engineIds - Ordered list of engine IDs to search
|
|
46
|
+
* @param options - Search options (limit, includeRaw, parallel, etc.)
|
|
47
|
+
* @param context - Strategy context with registry and credits
|
|
48
|
+
* @returns Combined results from all successful providers with attempt metadata
|
|
49
|
+
*/
|
|
50
|
+
async execute(
|
|
51
|
+
query: string,
|
|
52
|
+
engineIds: EngineId[],
|
|
53
|
+
options: AllSearchOptions,
|
|
54
|
+
context: StrategyContext,
|
|
55
|
+
): Promise<StrategyResult> {
|
|
56
|
+
// Choose execution mode based on options
|
|
57
|
+
if (options.parallel) {
|
|
58
|
+
return this.executeParallel(query, engineIds, options, context);
|
|
59
|
+
}
|
|
60
|
+
return this.executeSequential(query, engineIds, options, context);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Execute searches sequentially (original behavior)
|
|
65
|
+
*/
|
|
66
|
+
private async executeSequential(
|
|
67
|
+
query: string,
|
|
68
|
+
engineIds: EngineId[],
|
|
69
|
+
options: AllSearchOptions,
|
|
70
|
+
context: StrategyContext,
|
|
71
|
+
): Promise<StrategyResult> {
|
|
72
|
+
const results: SearchResultItem[] = [];
|
|
73
|
+
const attempts: EngineAttempt[] = [];
|
|
74
|
+
|
|
75
|
+
// Query each engine sequentially
|
|
76
|
+
for (const engineId of engineIds) {
|
|
77
|
+
const provider = context.providerRegistry.get(engineId);
|
|
78
|
+
if (!provider) {
|
|
79
|
+
attempts.push({ engineId, success: false, reason: "no_provider" });
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Check credit availability
|
|
84
|
+
if (!context.creditManager.hasSufficientCredits(engineId)) {
|
|
85
|
+
attempts.push({ engineId, success: false, reason: "out_of_credit" });
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
// Execute search
|
|
91
|
+
const response = await provider.search({
|
|
92
|
+
query,
|
|
93
|
+
limit: options.limit,
|
|
94
|
+
includeRaw: options.includeRaw,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Deduct credits
|
|
98
|
+
if (!context.creditManager.charge(engineId)) {
|
|
99
|
+
// This shouldn't happen because we checked above, but handle gracefully
|
|
100
|
+
attempts.push({ engineId, success: false, reason: "out_of_credit" });
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Record success
|
|
105
|
+
attempts.push({ engineId, success: true });
|
|
106
|
+
|
|
107
|
+
// Add results, applying limit if specified
|
|
108
|
+
if (options.limit !== undefined) {
|
|
109
|
+
results.push(...response.items.slice(0, options.limit - results.length));
|
|
110
|
+
} else {
|
|
111
|
+
results.push(...response.items);
|
|
112
|
+
}
|
|
113
|
+
} catch (error) {
|
|
114
|
+
// Record failure
|
|
115
|
+
if (error instanceof SearchError) {
|
|
116
|
+
attempts.push({ engineId, success: false, reason: error.reason });
|
|
117
|
+
} else {
|
|
118
|
+
attempts.push({ engineId, success: false, reason: "unknown" });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Log warning but continue with other providers
|
|
122
|
+
log.warn(
|
|
123
|
+
`Search failed for ${engineId}: ${error instanceof Error ? error.message : String(error)}`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Apply final limit if specified
|
|
129
|
+
if (options.limit !== undefined && results.length > options.limit) {
|
|
130
|
+
results.splice(options.limit);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return { results, attempts };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Execute searches in parallel using Promise.allSettled
|
|
138
|
+
*
|
|
139
|
+
* This provides faster execution when querying multiple providers,
|
|
140
|
+
* as all requests are made simultaneously.
|
|
141
|
+
*/
|
|
142
|
+
private async executeParallel(
|
|
143
|
+
query: string,
|
|
144
|
+
engineIds: EngineId[],
|
|
145
|
+
options: AllSearchOptions,
|
|
146
|
+
context: StrategyContext,
|
|
147
|
+
): Promise<StrategyResult> {
|
|
148
|
+
const results: SearchResultItem[] = [];
|
|
149
|
+
const attempts: EngineAttempt[] = [];
|
|
150
|
+
|
|
151
|
+
// Filter engines that are available and have credits
|
|
152
|
+
const eligibleEngines: { engineId: EngineId; provider: SearchProvider }[] = [];
|
|
153
|
+
const ineligibleAttempts: EngineAttempt[] = [];
|
|
154
|
+
|
|
155
|
+
for (const engineId of engineIds) {
|
|
156
|
+
const provider = context.providerRegistry.get(engineId);
|
|
157
|
+
if (!provider) {
|
|
158
|
+
ineligibleAttempts.push({ engineId, success: false, reason: "no_provider" });
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!context.creditManager.hasSufficientCredits(engineId)) {
|
|
163
|
+
ineligibleAttempts.push({ engineId, success: false, reason: "out_of_credit" });
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
eligibleEngines.push({ engineId, provider });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Execute all eligible searches in parallel
|
|
171
|
+
const searchPromises = eligibleEngines.map(
|
|
172
|
+
async ({ engineId, provider }): Promise<EngineSearchResult> => {
|
|
173
|
+
try {
|
|
174
|
+
const response = await provider.search({
|
|
175
|
+
query,
|
|
176
|
+
limit: options.limit,
|
|
177
|
+
includeRaw: options.includeRaw,
|
|
178
|
+
});
|
|
179
|
+
return { engineId, response };
|
|
180
|
+
} catch (error) {
|
|
181
|
+
return { engineId, error: error instanceof Error ? error : new Error(String(error)) };
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
// Wait for all searches to complete
|
|
187
|
+
const searchResults = await Promise.allSettled(searchPromises);
|
|
188
|
+
|
|
189
|
+
// Process results in original order (maintain engine priority)
|
|
190
|
+
for (let i = 0; i < searchResults.length; i++) {
|
|
191
|
+
const settledResult = searchResults[i];
|
|
192
|
+
const { engineId } = eligibleEngines[i];
|
|
193
|
+
|
|
194
|
+
if (settledResult.status === "rejected") {
|
|
195
|
+
// Promise itself was rejected (shouldn't happen with our try/catch, but handle it)
|
|
196
|
+
attempts.push({ engineId, success: false, reason: "unknown" });
|
|
197
|
+
log.warn(`Search failed for ${engineId}: Promise rejected`);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const { response, error } = settledResult.value;
|
|
202
|
+
|
|
203
|
+
if (error) {
|
|
204
|
+
// Search threw an error
|
|
205
|
+
if (error instanceof SearchError) {
|
|
206
|
+
attempts.push({ engineId, success: false, reason: error.reason });
|
|
207
|
+
} else {
|
|
208
|
+
attempts.push({ engineId, success: false, reason: "unknown" });
|
|
209
|
+
}
|
|
210
|
+
log.warn(`Search failed for ${engineId}: ${error.message}`);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (response) {
|
|
215
|
+
// Deduct credits
|
|
216
|
+
if (!context.creditManager.charge(engineId)) {
|
|
217
|
+
attempts.push({ engineId, success: false, reason: "out_of_credit" });
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Record success
|
|
222
|
+
attempts.push({ engineId, success: true });
|
|
223
|
+
|
|
224
|
+
// Add results
|
|
225
|
+
results.push(...response.items);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Add ineligible attempts at the end (maintaining order within their category)
|
|
230
|
+
attempts.push(...ineligibleAttempts);
|
|
231
|
+
|
|
232
|
+
// Sort attempts to match original engine order
|
|
233
|
+
const engineOrder = new Map(engineIds.map((id, idx) => [id, idx]));
|
|
234
|
+
attempts.sort(
|
|
235
|
+
(a, b) => (engineOrder.get(a.engineId) ?? 0) - (engineOrder.get(b.engineId) ?? 0),
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
// Apply final limit if specified
|
|
239
|
+
if (options.limit !== undefined && results.length > options.limit) {
|
|
240
|
+
results.splice(options.limit);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return { results, attempts };
|
|
244
|
+
}
|
|
245
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* First Success Strategy - stops after first successful provider
|
|
3
|
+
*
|
|
4
|
+
* This strategy tries search providers in order and returns immediately
|
|
5
|
+
* with results from the first successful provider. It skips providers
|
|
6
|
+
* with insufficient credits and stops execution after the first success.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { createLogger } from "../logger";
|
|
10
|
+
import type { EngineId } from "../types";
|
|
11
|
+
import { SearchError } from "../types";
|
|
12
|
+
import type {
|
|
13
|
+
EngineAttempt,
|
|
14
|
+
ISearchStrategy,
|
|
15
|
+
AllSearchOptions,
|
|
16
|
+
StrategyContext,
|
|
17
|
+
StrategyResult,
|
|
18
|
+
} from "./ISearchStrategy";
|
|
19
|
+
|
|
20
|
+
const log = createLogger("FirstSuccess");
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* First Success Strategy implementation
|
|
24
|
+
*
|
|
25
|
+
* Tries engines in order until first success, then returns immediately.
|
|
26
|
+
* Records all attempts up to and including the first successful one.
|
|
27
|
+
* Skips providers with insufficient credits.
|
|
28
|
+
*/
|
|
29
|
+
export class FirstSuccessStrategy implements ISearchStrategy {
|
|
30
|
+
/**
|
|
31
|
+
* Execute search with first-success strategy
|
|
32
|
+
*
|
|
33
|
+
* @param query - The search query string
|
|
34
|
+
* @param engineIds - Ordered list of engine IDs to try
|
|
35
|
+
* @param options - Search options (limit, includeRaw, etc.)
|
|
36
|
+
* @param context - Strategy context with registry and credits
|
|
37
|
+
* @returns Promise resolving to strategy result with first successful results
|
|
38
|
+
*/
|
|
39
|
+
async execute(
|
|
40
|
+
query: string,
|
|
41
|
+
engineIds: EngineId[],
|
|
42
|
+
options: AllSearchOptions,
|
|
43
|
+
context: StrategyContext,
|
|
44
|
+
): Promise<StrategyResult> {
|
|
45
|
+
const attempts: EngineAttempt[] = [];
|
|
46
|
+
|
|
47
|
+
// Try each engine in order until one succeeds
|
|
48
|
+
for (const engineId of engineIds) {
|
|
49
|
+
const provider = context.providerRegistry.get(engineId);
|
|
50
|
+
if (!provider) {
|
|
51
|
+
attempts.push({ engineId, success: false, reason: "no_provider" });
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Check credit availability
|
|
56
|
+
if (!context.creditManager.hasSufficientCredits(engineId)) {
|
|
57
|
+
attempts.push({ engineId, success: false, reason: "out_of_credit" });
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
// Execute search
|
|
63
|
+
const response = await provider.search({
|
|
64
|
+
query,
|
|
65
|
+
limit: options.limit,
|
|
66
|
+
includeRaw: options.includeRaw,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Deduct credits
|
|
70
|
+
if (!context.creditManager.charge(engineId)) {
|
|
71
|
+
attempts.push({ engineId, success: false, reason: "out_of_credit" });
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Record success
|
|
76
|
+
attempts.push({ engineId, success: true });
|
|
77
|
+
|
|
78
|
+
// Return immediately with results from this provider
|
|
79
|
+
return { results: response.items, attempts };
|
|
80
|
+
} catch (error) {
|
|
81
|
+
// Record failure and continue to next provider
|
|
82
|
+
if (error instanceof SearchError) {
|
|
83
|
+
attempts.push({ engineId, success: false, reason: error.reason });
|
|
84
|
+
} else {
|
|
85
|
+
attempts.push({ engineId, success: false, reason: "unknown" });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Log warning and continue
|
|
89
|
+
log.warn(
|
|
90
|
+
`Search failed for ${engineId}: ${error instanceof Error ? error.message : String(error)}`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// No provider succeeded
|
|
96
|
+
return { results: [], attempts };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search Strategy Interface
|
|
3
|
+
*
|
|
4
|
+
* Defines the contract for all search strategies, enabling the Strategy pattern
|
|
5
|
+
* for different search execution approaches (all engines, first success, etc.)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { CreditManager } from "../credits";
|
|
9
|
+
import type { ProviderRegistry } from "../provider";
|
|
10
|
+
import type { EngineId, SearchResultItem } from "../types";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Context object providing dependencies to search strategies
|
|
14
|
+
*/
|
|
15
|
+
export interface StrategyContext {
|
|
16
|
+
/** Registry of available search providers */
|
|
17
|
+
providerRegistry: ProviderRegistry;
|
|
18
|
+
|
|
19
|
+
/** Credit manager for tracking usage across providers */
|
|
20
|
+
creditManager: CreditManager;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Options for configuring search strategy behavior
|
|
25
|
+
*/
|
|
26
|
+
export interface AllSearchOptions {
|
|
27
|
+
/** Override the default engine order */
|
|
28
|
+
engineOrderOverride?: EngineId[];
|
|
29
|
+
|
|
30
|
+
/** Maximum results per provider */
|
|
31
|
+
limit?: number;
|
|
32
|
+
|
|
33
|
+
/** Include raw provider responses */
|
|
34
|
+
includeRaw?: boolean;
|
|
35
|
+
|
|
36
|
+
/** Search strategy */
|
|
37
|
+
strategy?: "all" | "first-success";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Execute searches in parallel (only applies to 'all' strategy)
|
|
41
|
+
* When true, all providers are queried simultaneously using Promise.allSettled
|
|
42
|
+
* When false (default), providers are queried sequentially
|
|
43
|
+
*/
|
|
44
|
+
parallel?: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Metadata about a single engine attempt
|
|
49
|
+
*/
|
|
50
|
+
export interface EngineAttempt {
|
|
51
|
+
/** Engine ID that was attempted */
|
|
52
|
+
engineId: EngineId;
|
|
53
|
+
|
|
54
|
+
/** Whether the attempt succeeded */
|
|
55
|
+
success: boolean;
|
|
56
|
+
|
|
57
|
+
/** Reason for failure (if success=false) */
|
|
58
|
+
reason?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Result from executing a search strategy
|
|
63
|
+
*/
|
|
64
|
+
export interface StrategyResult {
|
|
65
|
+
/** Combined results from all successful providers */
|
|
66
|
+
results: SearchResultItem[];
|
|
67
|
+
|
|
68
|
+
/** Metadata about which engines were tried and their outcomes */
|
|
69
|
+
attempts: EngineAttempt[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Interface for implementing different search execution strategies
|
|
74
|
+
*
|
|
75
|
+
* This enables the Strategy pattern, allowing different approaches to
|
|
76
|
+
* coordinate multiple search providers while maintaining a consistent interface.
|
|
77
|
+
*/
|
|
78
|
+
export interface ISearchStrategy {
|
|
79
|
+
/**
|
|
80
|
+
* Execute the search strategy with the given parameters
|
|
81
|
+
*
|
|
82
|
+
* @param query - The search query string
|
|
83
|
+
* @param engineIds - Ordered list of engine IDs to attempt
|
|
84
|
+
* @param options - Strategy configuration options
|
|
85
|
+
* @param context - Dependencies and services needed for execution
|
|
86
|
+
* @returns Promise resolving to strategy execution results
|
|
87
|
+
*/
|
|
88
|
+
execute(
|
|
89
|
+
query: string,
|
|
90
|
+
engineIds: EngineId[],
|
|
91
|
+
options: AllSearchOptions,
|
|
92
|
+
context: StrategyContext,
|
|
93
|
+
): Promise<StrategyResult>;
|
|
94
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { AllProvidersStrategy } from "./AllProvidersStrategy";
|
|
2
|
+
import { FirstSuccessStrategy } from "./FirstSuccessStrategy";
|
|
3
|
+
import type { ISearchStrategy } from "./ISearchStrategy";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Configuration options for search strategies
|
|
7
|
+
* @interface StrategyOptions
|
|
8
|
+
*/
|
|
9
|
+
export interface StrategyOptions {
|
|
10
|
+
/**
|
|
11
|
+
* Timeout in milliseconds for individual provider requests
|
|
12
|
+
* @default 30000
|
|
13
|
+
*/
|
|
14
|
+
timeout?: number;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Maximum number of concurrent requests
|
|
18
|
+
* @default 5
|
|
19
|
+
*/
|
|
20
|
+
maxConcurrent?: number;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Retry configuration
|
|
24
|
+
*/
|
|
25
|
+
retry?: {
|
|
26
|
+
/**
|
|
27
|
+
* Number of retry attempts
|
|
28
|
+
* @default 2
|
|
29
|
+
*/
|
|
30
|
+
attempts?: number;
|
|
31
|
+
/**
|
|
32
|
+
* Delay between retries in milliseconds
|
|
33
|
+
* @default 1000
|
|
34
|
+
*/
|
|
35
|
+
delay?: number;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Additional strategy-specific options
|
|
40
|
+
*/
|
|
41
|
+
[key: string]: unknown;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Maps strategy names to their implementing classes
|
|
46
|
+
* @interface StrategyMap
|
|
47
|
+
*/
|
|
48
|
+
interface StrategyMap {
|
|
49
|
+
[strategyName: string]: new (options?: StrategyOptions) => ISearchStrategy;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Factory for creating search strategy instances.
|
|
54
|
+
* Implements the Factory pattern to enable Open/Closed Principle -
|
|
55
|
+
* new strategies can be added without modifying existing code.
|
|
56
|
+
*
|
|
57
|
+
* @class StrategyFactory
|
|
58
|
+
* @example
|
|
59
|
+
* ```typescript
|
|
60
|
+
* // Create an all-providers strategy
|
|
61
|
+
* const allStrategy = StrategyFactory.createStrategy('all', {
|
|
62
|
+
* timeout: 30000,
|
|
63
|
+
* maxConcurrent: 3
|
|
64
|
+
* });
|
|
65
|
+
*
|
|
66
|
+
* // Create a first-success strategy
|
|
67
|
+
* const firstSuccessStrategy = StrategyFactory.createStrategy('first-success', {
|
|
68
|
+
* timeout: 15000
|
|
69
|
+
* });
|
|
70
|
+
*
|
|
71
|
+
* // Register a custom strategy
|
|
72
|
+
* StrategyFactory.registerStrategy('custom', CustomStrategy);
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
let strategies: StrategyMap = {
|
|
76
|
+
all: AllProvidersStrategy,
|
|
77
|
+
"first-success": FirstSuccessStrategy,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Creates a search strategy instance based on the provided name and options.
|
|
82
|
+
*
|
|
83
|
+
* @param {string} strategyName - The name of the strategy to create
|
|
84
|
+
* @param {StrategyOptions} [options] - Optional configuration for the strategy
|
|
85
|
+
* @returns {ISearchStrategy} An instance of the requested strategy
|
|
86
|
+
* @throws {Error} If the strategy name is not recognized
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```typescript
|
|
90
|
+
* const strategy = StrategyFactory.createStrategy('all', {
|
|
91
|
+
* timeout: 20000,
|
|
92
|
+
* maxConcurrent: 5,
|
|
93
|
+
* retry: { attempts: 3, delay: 500 }
|
|
94
|
+
* });
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
function createStrategy(strategyName: string, options?: StrategyOptions): ISearchStrategy {
|
|
98
|
+
const StrategyClass = strategies[strategyName];
|
|
99
|
+
|
|
100
|
+
if (!StrategyClass) {
|
|
101
|
+
const availableStrategies = Object.keys(strategies).join(", ");
|
|
102
|
+
throw new Error(
|
|
103
|
+
`Unknown strategy: "${strategyName}". Available strategies: [${availableStrategies}]`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return new StrategyClass(options);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Registers a new strategy in the factory.
|
|
112
|
+
* Enables the Open/Closed Principle by allowing new strategies to be added
|
|
113
|
+
* without modifying the factory code itself.
|
|
114
|
+
*
|
|
115
|
+
* @param {string} name - The name to register the strategy under
|
|
116
|
+
* @param strategyClass - The strategy class constructor
|
|
117
|
+
* @throws {Error} If a strategy with the same name already exists
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```typescript
|
|
121
|
+
* class WeightedStrategy implements ISearchStrategy {
|
|
122
|
+
* constructor(private options?: StrategyOptions) {}
|
|
123
|
+
*
|
|
124
|
+
* async search(
|
|
125
|
+
* query: string,
|
|
126
|
+
* providers: ISearchProvider[],
|
|
127
|
+
* options?: SearchOptions
|
|
128
|
+
* ): Promise<SearchResult[]> {
|
|
129
|
+
* // Implementation
|
|
130
|
+
* }
|
|
131
|
+
* }
|
|
132
|
+
*
|
|
133
|
+
* StrategyFactory.registerStrategy('weighted', WeightedStrategy);
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
function registerStrategy(
|
|
137
|
+
name: string,
|
|
138
|
+
strategyClass: new (options?: StrategyOptions) => ISearchStrategy,
|
|
139
|
+
): void {
|
|
140
|
+
if (strategies[name]) {
|
|
141
|
+
throw new Error(`Strategy "${name}" is already registered`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
strategies[name] = strategyClass;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Gets a list of all available strategy names.
|
|
149
|
+
*
|
|
150
|
+
* @returns {string[]} Array of available strategy names
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* ```typescript
|
|
154
|
+
* const availableStrategies = StrategyFactory.getAvailableStrategies();
|
|
155
|
+
* console.log(availableStrategies); // ['all', 'first-success', 'custom']
|
|
156
|
+
* ```
|
|
157
|
+
*/
|
|
158
|
+
function getAvailableStrategies(): string[] {
|
|
159
|
+
return Object.keys(strategies);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Checks if a strategy is registered.
|
|
164
|
+
*
|
|
165
|
+
* @param {string} strategyName - The name of the strategy to check
|
|
166
|
+
* @returns {boolean} True if the strategy is registered, false otherwise
|
|
167
|
+
*
|
|
168
|
+
* @example
|
|
169
|
+
* ```typescript
|
|
170
|
+
* if (StrategyFactory.hasStrategy('all')) {
|
|
171
|
+
* console.log('All providers strategy is available');
|
|
172
|
+
* }
|
|
173
|
+
* ```
|
|
174
|
+
*/
|
|
175
|
+
function hasStrategy(strategyName: string): boolean {
|
|
176
|
+
return strategyName in strategies;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Resets the factory to its default state.
|
|
181
|
+
* This is primarily useful for testing to ensure test isolation.
|
|
182
|
+
*
|
|
183
|
+
* @example
|
|
184
|
+
* ```typescript
|
|
185
|
+
* // In test setup/teardown
|
|
186
|
+
* afterEach(() => {
|
|
187
|
+
* StrategyFactory.reset();
|
|
188
|
+
* });
|
|
189
|
+
* ```
|
|
190
|
+
*/
|
|
191
|
+
function reset(): void {
|
|
192
|
+
strategies = {
|
|
193
|
+
all: AllProvidersStrategy,
|
|
194
|
+
"first-success": FirstSuccessStrategy,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export const StrategyFactory = {
|
|
199
|
+
createStrategy,
|
|
200
|
+
registerStrategy,
|
|
201
|
+
getAvailableStrategies,
|
|
202
|
+
hasStrategy,
|
|
203
|
+
reset,
|
|
204
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strategy interface and types for search execution patterns
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { CreditManager } from "../credits";
|
|
6
|
+
import type { ProviderRegistry } from "../provider";
|
|
7
|
+
import type { EngineId, SearchQuery, SearchResultItem } from "../types";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Context passed to search strategies containing dependencies
|
|
11
|
+
*/
|
|
12
|
+
export interface StrategyContext {
|
|
13
|
+
/** Provider registry for accessing search providers */
|
|
14
|
+
registry: ProviderRegistry;
|
|
15
|
+
/** Credit manager for checking and charging credits */
|
|
16
|
+
credits: CreditManager;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Result from a search strategy execution
|
|
21
|
+
*/
|
|
22
|
+
export interface StrategyResult {
|
|
23
|
+
/** Search results from successful providers */
|
|
24
|
+
results: SearchResultItem[];
|
|
25
|
+
/** Metadata about engine attempts */
|
|
26
|
+
attempts: EngineAttempt[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Engine attempt metadata
|
|
31
|
+
*/
|
|
32
|
+
export interface EngineAttempt {
|
|
33
|
+
engineId: EngineId;
|
|
34
|
+
success: boolean;
|
|
35
|
+
reason?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Interface for implementing different search execution strategies
|
|
40
|
+
*/
|
|
41
|
+
export interface ISearchStrategy {
|
|
42
|
+
/**
|
|
43
|
+
* Execute search using this strategy
|
|
44
|
+
* @param query - The search query
|
|
45
|
+
* @param engineIds - Ordered list of engine IDs to try
|
|
46
|
+
* @param options - Search options (limit, includeRaw, etc.)
|
|
47
|
+
* @param context - Strategy context with dependencies
|
|
48
|
+
* @returns Promise resolving to strategy result
|
|
49
|
+
*/
|
|
50
|
+
execute(
|
|
51
|
+
query: string,
|
|
52
|
+
engineIds: EngineId[],
|
|
53
|
+
options: SearchQuery,
|
|
54
|
+
context: StrategyContext,
|
|
55
|
+
): Promise<StrategyResult>;
|
|
56
|
+
}
|