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.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +374 -0
  3. package/package.json +76 -0
  4. package/src/app/index.ts +30 -0
  5. package/src/bootstrap/container.ts +157 -0
  6. package/src/cli.ts +380 -0
  7. package/src/config/defineConfig.ts +176 -0
  8. package/src/config/load.ts +368 -0
  9. package/src/config/types.ts +86 -0
  10. package/src/config/validation.ts +148 -0
  11. package/src/core/cache.ts +74 -0
  12. package/src/core/container.ts +268 -0
  13. package/src/core/credits/CreditManager.ts +158 -0
  14. package/src/core/credits/CreditStateProvider.ts +151 -0
  15. package/src/core/credits/FileCreditStateProvider.ts +137 -0
  16. package/src/core/credits/index.ts +3 -0
  17. package/src/core/docker/dockerComposeHelper.ts +177 -0
  18. package/src/core/docker/dockerLifecycleManager.ts +361 -0
  19. package/src/core/docker/index.ts +8 -0
  20. package/src/core/logger.ts +146 -0
  21. package/src/core/orchestrator.ts +103 -0
  22. package/src/core/paths.ts +157 -0
  23. package/src/core/provider/ILifecycleProvider.ts +120 -0
  24. package/src/core/provider/ProviderFactory.ts +120 -0
  25. package/src/core/provider.ts +61 -0
  26. package/src/core/serviceKeys.ts +45 -0
  27. package/src/core/strategy/AllProvidersStrategy.ts +245 -0
  28. package/src/core/strategy/FirstSuccessStrategy.ts +98 -0
  29. package/src/core/strategy/ISearchStrategy.ts +94 -0
  30. package/src/core/strategy/StrategyFactory.ts +204 -0
  31. package/src/core/strategy/index.ts +9 -0
  32. package/src/core/strategy/types.ts +56 -0
  33. package/src/core/types.ts +58 -0
  34. package/src/index.ts +1 -0
  35. package/src/plugin/PluginRegistry.ts +336 -0
  36. package/src/plugin/builtin.ts +130 -0
  37. package/src/plugin/index.ts +33 -0
  38. package/src/plugin/types.ts +212 -0
  39. package/src/providers/BaseProvider.ts +49 -0
  40. package/src/providers/brave.ts +66 -0
  41. package/src/providers/constants.ts +13 -0
  42. package/src/providers/helpers/index.ts +24 -0
  43. package/src/providers/helpers/lifecycleHelpers.ts +110 -0
  44. package/src/providers/helpers/resultMappers.ts +168 -0
  45. package/src/providers/index.ts +6 -0
  46. package/src/providers/linkup.ts +114 -0
  47. package/src/providers/retry.ts +95 -0
  48. package/src/providers/searchxng.ts +163 -0
  49. package/src/providers/tavily.ts +73 -0
  50. package/src/providers/types/index.ts +185 -0
  51. package/src/providers/utils.ts +182 -0
  52. package/src/tool/allSearchTool.ts +110 -0
  53. package/src/tool/interface.ts +71 -0
@@ -0,0 +1,268 @@
1
+ /**
2
+ * Dependency Injection Container
3
+ *
4
+ * Central service locator that manages service registration and resolution.
5
+ * Implements factory pattern with singleton and transient lifetimes.
6
+ *
7
+ * @module core/container
8
+ */
9
+
10
+ /**
11
+ * Service identifier - can be string or Symbol for unique identification
12
+ */
13
+ export type ServiceIdentifier<_T = unknown> = string | symbol;
14
+
15
+ /**
16
+ * Container binding configuration
17
+ */
18
+ export interface ContainerBinding<T> {
19
+ /** Factory function that creates service instance */
20
+ factory: (container: Container) => T;
21
+ /** Whether service should be singleton (cached) */
22
+ singleton: boolean;
23
+ /** Cached instance for singleton services */
24
+ cached?: T;
25
+ }
26
+
27
+ /**
28
+ * Dependency Injection Container
29
+ *
30
+ * Manages service registration and resolution with support for:
31
+ * - Singleton and transient lifetimes
32
+ * - Circular dependency detection
33
+ * - Type-safe resolution
34
+ * - Factory pattern for lazy instantiation
35
+ *
36
+ * @example
37
+ * ```typescript
38
+ * const container = new Container();
39
+ *
40
+ * // Register singleton service
41
+ * container.singleton('config', () => loadConfig());
42
+ *
43
+ * // Register transient service
44
+ * container.bind('strategy', (c) => new SearchStrategy(c.get('config')));
45
+ *
46
+ * // Resolve service
47
+ * const config = container.get<Config>('config');
48
+ * ```
49
+ */
50
+ export class Container {
51
+ /** Internal storage for service bindings */
52
+ private bindings = new Map<ServiceIdentifier, ContainerBinding<unknown>>();
53
+
54
+ /** Stack to detect circular dependencies during resolution */
55
+ private resolutionStack = new Set<ServiceIdentifier>();
56
+
57
+ /**
58
+ * Register a transient service (new instance each time)
59
+ *
60
+ * @param id - Service identifier
61
+ * @param factory - Factory function that creates the service
62
+ * @template T - Service type
63
+ *
64
+ * @example
65
+ * ```typescript
66
+ * container.bind('searchStrategy', (c) => new AllProvidersStrategy());
67
+ * ```
68
+ */
69
+ bind<T>(id: ServiceIdentifier<T>, factory: (container: Container) => T): void {
70
+ this.bindings.set(id, {
71
+ factory,
72
+ singleton: false,
73
+ });
74
+ }
75
+
76
+ /**
77
+ * Register a singleton service (cached instance)
78
+ *
79
+ * @param id - Service identifier
80
+ * @param factory - Factory function that creates the service
81
+ * @template T - Service type
82
+ *
83
+ * @example
84
+ * ```typescript
85
+ * container.singleton('config', () => loadConfiguration());
86
+ * container.singleton('creditManager', (c) => new CreditManager(
87
+ * c.get('engines'),
88
+ * c.get('creditProvider')
89
+ * ));
90
+ * ```
91
+ */
92
+ singleton<T>(id: ServiceIdentifier<T>, factory: (container: Container) => T): void {
93
+ this.bindings.set(id, {
94
+ factory,
95
+ singleton: true,
96
+ });
97
+ }
98
+
99
+ /**
100
+ * Resolve a service instance
101
+ *
102
+ * @param id - Service identifier
103
+ * @returns Service instance
104
+ * @template T - Service type
105
+ * @throws {Error} If service is not registered or circular dependency detected
106
+ *
107
+ * @example
108
+ * ```typescript
109
+ * const config = container.get<Config>('config');
110
+ * const manager = container.get<CreditManager>('creditManager');
111
+ * ```
112
+ */
113
+ get<T>(id: ServiceIdentifier<T>): T {
114
+ // Check for circular dependency
115
+ if (this.resolutionStack.has(id)) {
116
+ const chain = [...this.resolutionStack, id].map(String).join(" -> ");
117
+ throw new Error(`Circular dependency detected: ${chain}`);
118
+ }
119
+
120
+ const binding = this.bindings.get(id);
121
+ if (!binding) {
122
+ const registered = Array.from(this.bindings.keys()).map(String);
123
+ throw new Error(
124
+ `No binding found for '${String(id)}'. Registered services: [${registered.join(", ")}]`,
125
+ );
126
+ }
127
+
128
+ // Handle singleton caching
129
+ if (binding.singleton && binding.cached !== undefined) {
130
+ return binding.cached;
131
+ }
132
+
133
+ // Track resolution for circular dependency detection
134
+ this.resolutionStack.add(id);
135
+
136
+ try {
137
+ // Create instance using factory
138
+ const instance = binding.factory(this);
139
+
140
+ // Cache singleton instances
141
+ if (binding.singleton) {
142
+ binding.cached = instance;
143
+ }
144
+
145
+ return instance;
146
+ } catch (error) {
147
+ // Enhance error message with context
148
+ if (error instanceof Error) {
149
+ throw new Error(`Failed to resolve service '${String(id)}': ${error.message}`);
150
+ }
151
+ throw error;
152
+ } finally {
153
+ // Clean up resolution stack
154
+ this.resolutionStack.delete(id);
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Check if a service is registered
160
+ *
161
+ * @param id - Service identifier
162
+ * @returns true if service is registered
163
+ *
164
+ * @example
165
+ * ```typescript
166
+ * if (container.has('optionalService')) {
167
+ * const service = container.get('optionalService');
168
+ * }
169
+ * ```
170
+ */
171
+ has(id: ServiceIdentifier): boolean {
172
+ return this.bindings.has(id);
173
+ }
174
+
175
+ /**
176
+ * Remove a service binding
177
+ *
178
+ * @param id - Service identifier to remove
179
+ * @returns true if binding was removed, false if it didn't exist
180
+ *
181
+ * @example
182
+ * ```typescript
183
+ * container.unbind('oldService');
184
+ * ```
185
+ */
186
+ unbind(id: ServiceIdentifier): boolean {
187
+ return this.bindings.delete(id);
188
+ }
189
+
190
+ /**
191
+ * Clear all service bindings
192
+ * Useful for testing or resetting container state
193
+ *
194
+ * @example
195
+ * ```typescript
196
+ * container.reset();
197
+ * // Container is now empty
198
+ * ```
199
+ */
200
+ reset(): void {
201
+ this.bindings.clear();
202
+ this.resolutionStack.clear();
203
+ }
204
+
205
+ /**
206
+ * Get list of all registered service identifiers
207
+ *
208
+ * @returns Array of service identifiers
209
+ *
210
+ * @example
211
+ * ```typescript
212
+ * const services = container.getRegisteredServices();
213
+ * console.log('Available services:', services);
214
+ * ```
215
+ */
216
+ getRegisteredServices(): ServiceIdentifier[] {
217
+ return Array.from(this.bindings.keys());
218
+ }
219
+
220
+ /**
221
+ * Get service information including lifetime and factory details
222
+ * Useful for debugging and introspection
223
+ *
224
+ * @param id - Service identifier
225
+ * @returns Service information or undefined if not found
226
+ *
227
+ * @example
228
+ * ```typescript
229
+ * const info = container.getServiceInfo('config');
230
+ * console.log('Service lifetime:', info?.singleton ? 'singleton' : 'transient');
231
+ * ```
232
+ */
233
+ getServiceInfo(id: ServiceIdentifier):
234
+ | {
235
+ singleton: boolean;
236
+ cached: boolean;
237
+ factory: (container: Container) => unknown;
238
+ }
239
+ | undefined {
240
+ const binding = this.bindings.get(id);
241
+ if (!binding) {
242
+ return undefined;
243
+ }
244
+
245
+ return {
246
+ singleton: binding.singleton,
247
+ cached: binding.cached !== undefined,
248
+ factory: binding.factory,
249
+ };
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Global container instance for convenience
255
+ * Use this for application-wide dependency injection
256
+ *
257
+ * @example
258
+ * ```typescript
259
+ * import { container } from './core/container';
260
+ *
261
+ * // Register services globally
262
+ * container.singleton('config', () => loadConfig());
263
+ *
264
+ * // Use in any module
265
+ * const config = container.get<Config>('config');
266
+ * ```
267
+ */
268
+ export const container = new Container();
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Credit management system for tracking usage across providers
3
+ * Refactored to use dependency injection - no file I/O dependencies
4
+ */
5
+
6
+ import type { EngineConfig } from "../../config/types";
7
+ import type { EngineId } from "../types";
8
+ import type { CreditState, CreditStateProvider } from "./CreditStateProvider";
9
+
10
+ export interface CreditSnapshot {
11
+ engineId: EngineId;
12
+ quota: number;
13
+ used: number;
14
+ remaining: number;
15
+ isExhausted: boolean;
16
+ }
17
+
18
+ export class CreditManager {
19
+ private engines: Map<EngineId, EngineConfig>;
20
+ private state: CreditState = {};
21
+
22
+ constructor(
23
+ engines: EngineConfig[],
24
+ private stateProvider: CreditStateProvider,
25
+ ) {
26
+ this.engines = new Map(engines.map((e) => [e.id, e]));
27
+ }
28
+
29
+ /**
30
+ * Initialize the credit manager by loading state from provider
31
+ * Must be called after construction
32
+ */
33
+ async initialize(): Promise<void> {
34
+ this.state = await this.stateProvider.loadState();
35
+ await this.resetIfNeeded();
36
+ }
37
+
38
+ /**
39
+ * Check if we need to reset monthly usage (first day of month)
40
+ */
41
+ private async resetIfNeeded(): Promise<void> {
42
+ const now = new Date();
43
+ const currentMonth = now.toISOString().slice(0, 7); // YYYY-MM
44
+
45
+ for (const [engineId, _config] of this.engines) {
46
+ const record = this.state[engineId];
47
+
48
+ if (!record) {
49
+ this.state[engineId] = {
50
+ used: 0,
51
+ lastReset: now.toISOString(),
52
+ };
53
+ continue;
54
+ }
55
+
56
+ const lastResetMonth = record.lastReset.slice(0, 7);
57
+ if (lastResetMonth !== currentMonth) {
58
+ // New month - reset counter
59
+ record.used = 0;
60
+ record.lastReset = now.toISOString();
61
+ }
62
+ }
63
+
64
+ await this.stateProvider.saveState(this.state);
65
+ }
66
+
67
+ /**
68
+ * Deduct credits for a search (synchronous version for immediate checks)
69
+ * @returns true if successful, false if exhausted
70
+ * @note This does NOT persist the state - call saveState() separately
71
+ */
72
+ charge(engineId: EngineId): boolean {
73
+ const config = this.engines.get(engineId);
74
+ if (!config) {
75
+ throw new Error(`Unknown engine: ${engineId}`);
76
+ }
77
+
78
+ const record = this.state[engineId];
79
+ if (!record) {
80
+ throw new Error(`No credit record for engine: ${engineId}`);
81
+ }
82
+
83
+ if (record.used + config.creditCostPerSearch > config.monthlyQuota) {
84
+ return false; // Exhausted
85
+ }
86
+
87
+ record.used += config.creditCostPerSearch;
88
+ return true;
89
+ }
90
+
91
+ /**
92
+ * Deduct credits for a search and persist state
93
+ * @returns true if successful, false if exhausted
94
+ */
95
+ async chargeAndSave(engineId: EngineId): Promise<boolean> {
96
+ const result = this.charge(engineId);
97
+ if (result) {
98
+ await this.stateProvider.saveState(this.state);
99
+ }
100
+ return result;
101
+ }
102
+
103
+ /**
104
+ * Check if engine has sufficient credits
105
+ */
106
+ hasSufficientCredits(engineId: EngineId): boolean {
107
+ const config = this.engines.get(engineId);
108
+ if (!config) {
109
+ return false;
110
+ }
111
+
112
+ const record = this.state[engineId];
113
+ if (!record) {
114
+ return true; // No usage yet
115
+ }
116
+
117
+ return record.used + config.creditCostPerSearch <= config.monthlyQuota;
118
+ }
119
+
120
+ /**
121
+ * Get credit snapshot for an engine
122
+ */
123
+ getSnapshot(engineId: EngineId): CreditSnapshot {
124
+ const config = this.engines.get(engineId);
125
+ if (!config) {
126
+ throw new Error(`Unknown engine: ${engineId}`);
127
+ }
128
+
129
+ const record = this.state[engineId] ?? {
130
+ used: 0,
131
+ lastReset: new Date().toISOString(),
132
+ };
133
+
134
+ const remaining = Math.max(0, config.monthlyQuota - record.used);
135
+
136
+ return {
137
+ engineId,
138
+ quota: config.monthlyQuota,
139
+ used: record.used,
140
+ remaining,
141
+ isExhausted: remaining < config.creditCostPerSearch,
142
+ };
143
+ }
144
+
145
+ /**
146
+ * Save current state to persistence layer
147
+ */
148
+ async saveState(): Promise<void> {
149
+ await this.stateProvider.saveState(this.state);
150
+ }
151
+
152
+ /**
153
+ * Get snapshots for all engines
154
+ */
155
+ listSnapshots(): CreditSnapshot[] {
156
+ return Array.from(this.engines.keys()).map((id) => this.getSnapshot(id));
157
+ }
158
+ }
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Credit state persistence abstraction layer
3
+ *
4
+ * This module defines interfaces for credit state persistence operations,
5
+ * separating persistence concerns from business logic. Enables unit testing
6
+ * without file I/O and supports multiple persistence implementations.
7
+ *
8
+ * @module credits/CreditStateProvider
9
+ */
10
+
11
+ import type { EngineId } from "../types";
12
+
13
+ /**
14
+ * Represents the persistent state of credit usage for search engines.
15
+ * Maps engine IDs to their usage tracking data.
16
+ *
17
+ * @interface CreditState
18
+ */
19
+ export interface CreditState {
20
+ /**
21
+ * Maps engine identifiers to their credit usage information.
22
+ * Each entry tracks the total credits used and the last reset timestamp.
23
+ */
24
+ [engineId: EngineId]: {
25
+ /**
26
+ * Total number of credits used by this engine in the current billing period.
27
+ */
28
+ used: number;
29
+
30
+ /**
31
+ * ISO 8601 date string indicating when the credits were last reset.
32
+ * Used to determine when monthly quotas should be refreshed.
33
+ */
34
+ lastReset: string;
35
+ };
36
+ }
37
+
38
+ /**
39
+ * Abstraction for credit state persistence operations.
40
+ *
41
+ * This interface defines the contract for loading, saving, and checking
42
+ * the existence of credit state data. Implementations can use various
43
+ * persistence mechanisms (file system, memory, database, etc.) while
44
+ * maintaining the same API for the business logic layer.
45
+ *
46
+ * All methods are asynchronous to support both synchronous and
47
+ * asynchronous persistence backends without blocking the main thread.
48
+ *
49
+ * @interface CreditStateProvider
50
+ *
51
+ * @example
52
+ * ```typescript
53
+ * // File-based implementation
54
+ * class FileCreditStateProvider implements CreditStateProvider {
55
+ * async loadState(): Promise<CreditState> {
56
+ * const data = await fs.readFile(this.filePath, 'utf8');
57
+ * return JSON.parse(data);
58
+ * }
59
+ *
60
+ * async saveState(state: CreditState): Promise<void> {
61
+ * await fs.writeFile(this.filePath, JSON.stringify(state));
62
+ * }
63
+ *
64
+ * async stateExists(): Promise<boolean> {
65
+ * return fs.exists(this.filePath);
66
+ * }
67
+ * }
68
+ *
69
+ * // Memory-based implementation for testing
70
+ * class MemoryCreditStateProvider implements CreditStateProvider {
71
+ * private state: CreditState = {};
72
+ *
73
+ * async loadState(): Promise<CreditState> {
74
+ * return { ...this.state };
75
+ * }
76
+ *
77
+ * async saveState(state: CreditState): Promise<void> {
78
+ * this.state = { ...state };
79
+ * }
80
+ *
81
+ * async stateExists(): Promise<boolean> {
82
+ * return Object.keys(this.state).length > 0;
83
+ * }
84
+ * }
85
+ * ```
86
+ */
87
+ export interface CreditStateProvider {
88
+ /**
89
+ * Load the current credit state from persistence.
90
+ *
91
+ * @returns Promise resolving to the current credit state.
92
+ * Returns empty object `{}` if no state exists.
93
+ *
94
+ * @throws May throw errors related to persistence layer failures
95
+ * (file system errors, network issues, etc.).
96
+ *
97
+ * @example
98
+ * ```typescript
99
+ * const provider = new FileCreditStateProvider();
100
+ * const state = await provider.loadState();
101
+ * console.log(`Loaded state for ${Object.keys(state).length} engines`);
102
+ * ```
103
+ */
104
+ loadState(): Promise<CreditState>;
105
+
106
+ /**
107
+ * Save the credit state to persistence.
108
+ *
109
+ * @param state - The credit state to persist.
110
+ * Should be a complete snapshot of current usage.
111
+ *
112
+ * @returns Promise that resolves when the state has been successfully saved.
113
+ *
114
+ * @throws May throw errors related to persistence layer failures
115
+ * (file system errors, network issues, permission errors, etc.).
116
+ *
117
+ * @example
118
+ * ```typescript
119
+ * const newState: CreditState = {
120
+ * 'google': { used: 50, lastReset: '2024-01-15T10:30:00.000Z' },
121
+ * 'bing': { used: 25, lastReset: '2024-01-15T10:30:00.000Z' }
122
+ * };
123
+ * await provider.saveState(newState);
124
+ * ```
125
+ */
126
+ saveState(state: CreditState): Promise<void>;
127
+
128
+ /**
129
+ * Check if credit state exists in persistence.
130
+ *
131
+ * Used to determine whether to load existing state or initialize
132
+ * with default values.
133
+ *
134
+ * @returns Promise resolving to true if state exists in persistence,
135
+ * false otherwise.
136
+ *
137
+ * @throws May throw errors related to persistence layer failures.
138
+ *
139
+ * @example
140
+ * ```typescript
141
+ * if (await provider.stateExists()) {
142
+ * const state = await provider.loadState();
143
+ * // Process existing state
144
+ * } else {
145
+ * // Initialize with default state
146
+ * await provider.saveState({});
147
+ * }
148
+ * ```
149
+ */
150
+ stateExists(): Promise<boolean>;
151
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * File-based implementation of CreditStateProvider
3
+ * Handles file I/O operations for credit state persistence using Bun's async APIs
4
+ */
5
+
6
+ import { homedir } from "node:os";
7
+ import { join } from "node:path";
8
+ import { createLogger } from "../logger";
9
+ import type { CreditState, CreditStateProvider } from "./CreditStateProvider";
10
+
11
+ const log = createLogger("CreditState");
12
+
13
+ /**
14
+ * File-based persistence implementation of CreditStateProvider.
15
+ * Manages credit state storage using the local filesystem with async I/O.
16
+ *
17
+ * @remarks
18
+ * This provider stores credit state as JSON in the following locations:
19
+ * - Default: ~/.local/state/allsearch/credits.json
20
+ * - With XDG_STATE_HOME: $XDG_STATE_HOME/allsearch/credits.json
21
+ *
22
+ * Uses Bun's async file APIs for non-blocking I/O operations.
23
+ * The provider handles directory creation automatically and never throws errors,
24
+ * instead logging warnings and returning sensible defaults.
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * const provider = new FileCreditStateProvider();
29
+ * const state = await provider.loadState();
30
+ * await provider.saveState(newState);
31
+ * ```
32
+ */
33
+ export class FileCreditStateProvider implements CreditStateProvider {
34
+ private readonly statePath: string;
35
+
36
+ /**
37
+ * Creates a new FileCreditStateProvider instance.
38
+ *
39
+ * @param statePath - Optional custom path for the state file.
40
+ * If not provided, uses the default location based on
41
+ * XDG_STATE_HOME or ~/.local/state/allsearch/credits.json
42
+ */
43
+ constructor(statePath?: string) {
44
+ this.statePath = statePath ?? this.getDefaultStatePath();
45
+ }
46
+
47
+ /**
48
+ * Gets the default state file path following XDG Base Directory Specification.
49
+ *
50
+ * @returns The resolved state file path
51
+ * @private
52
+ */
53
+ private getDefaultStatePath(): string {
54
+ const base = process.env.XDG_STATE_HOME ?? join(homedir(), ".local", "state");
55
+ return join(base, "allsearch", "credits.json");
56
+ }
57
+
58
+ /**
59
+ * Load credit state from the file system using async I/O.
60
+ *
61
+ * @returns The loaded credit state, or an empty object if the file doesn't exist
62
+ * or cannot be parsed
63
+ *
64
+ * @remarks
65
+ * This method never throws. If the file doesn't exist, an empty state is returned.
66
+ * If the file exists but cannot be parsed, a warning is logged and an empty state
67
+ * is returned. Uses Bun.file() for non-blocking file reads.
68
+ */
69
+ async loadState(): Promise<CreditState> {
70
+ const file = Bun.file(this.statePath);
71
+
72
+ // Check if file exists
73
+ if (!(await file.exists())) {
74
+ return {};
75
+ }
76
+
77
+ try {
78
+ const raw = await file.text();
79
+ return JSON.parse(raw) as CreditState;
80
+ } catch (error) {
81
+ log.warn(`Failed to load credit state from ${this.statePath}:`, error);
82
+ return {};
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Save credit state to the file system using async I/O.
88
+ *
89
+ * @param state - The credit state to save
90
+ *
91
+ * @remarks
92
+ * This method never throws. If the directory doesn't exist, it will be created
93
+ * recursively. If saving fails, a warning is logged but no error is thrown.
94
+ * Uses Bun.write() for non-blocking file writes.
95
+ */
96
+ async saveState(state: CreditState): Promise<void> {
97
+ try {
98
+ // Ensure parent directory exists
99
+ const { dirname } = await import("node:path");
100
+ const { mkdir } = await import("node:fs/promises");
101
+ const dir = dirname(this.statePath);
102
+
103
+ try {
104
+ await mkdir(dir, { recursive: true });
105
+ } catch (mkdirError) {
106
+ // Ignore EEXIST errors
107
+ if ((mkdirError as NodeJS.ErrnoException).code !== "EEXIST") {
108
+ throw mkdirError;
109
+ }
110
+ }
111
+
112
+ // Write file using Bun's async API
113
+ await Bun.write(this.statePath, JSON.stringify(state, null, 2));
114
+ } catch (error) {
115
+ log.warn(`Failed to save credit state to ${this.statePath}:`, error);
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Check if the state file exists in the file system.
121
+ *
122
+ * @returns true if the state file exists, false otherwise
123
+ */
124
+ async stateExists(): Promise<boolean> {
125
+ const file = Bun.file(this.statePath);
126
+ return await file.exists();
127
+ }
128
+
129
+ /**
130
+ * Get the path where state is stored.
131
+ *
132
+ * @returns The absolute path to the state file
133
+ */
134
+ getStatePath(): string {
135
+ return this.statePath;
136
+ }
137
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./CreditManager";
2
+ export * from "./CreditStateProvider";
3
+ export * from "./FileCreditStateProvider";