wraptc 1.0.2 → 1.0.4

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 (71) hide show
  1. package/bin/wraptc +4 -4
  2. package/package.json +2 -2
  3. package/src/cli/__tests__/cli.test.ts +337 -0
  4. package/src/cli/index.ts +149 -0
  5. package/src/core/__tests__/fixtures/configs/project-config.json +14 -0
  6. package/src/core/__tests__/fixtures/configs/system-config.json +14 -0
  7. package/src/core/__tests__/fixtures/configs/user-config.json +15 -0
  8. package/src/core/__tests__/integration/integration.test.ts +241 -0
  9. package/src/core/__tests__/integration/mock-coder-adapter.test.ts +243 -0
  10. package/src/core/__tests__/test-utils.ts +136 -0
  11. package/src/core/__tests__/unit/adapters/runner.test.ts +302 -0
  12. package/src/core/__tests__/unit/basic-test.test.ts +44 -0
  13. package/src/core/__tests__/unit/basic.test.ts +12 -0
  14. package/src/core/__tests__/unit/config.test.ts +244 -0
  15. package/src/core/__tests__/unit/error-patterns.test.ts +181 -0
  16. package/src/core/__tests__/unit/memory-monitor.test.ts +354 -0
  17. package/src/core/__tests__/unit/plugin/registry.test.ts +356 -0
  18. package/src/core/__tests__/unit/providers/codex.test.ts +173 -0
  19. package/src/core/__tests__/unit/providers/configurable.test.ts +429 -0
  20. package/src/core/__tests__/unit/providers/gemini.test.ts +251 -0
  21. package/src/core/__tests__/unit/providers/opencode.test.ts +258 -0
  22. package/src/core/__tests__/unit/providers/qwen-code.test.ts +195 -0
  23. package/src/core/__tests__/unit/providers/simple-codex.test.ts +18 -0
  24. package/src/core/__tests__/unit/router.test.ts +967 -0
  25. package/src/core/__tests__/unit/state.test.ts +1079 -0
  26. package/src/core/__tests__/unit/unified/capabilities.test.ts +186 -0
  27. package/src/core/__tests__/unit/wrap-terminalcoder.test.ts +32 -0
  28. package/src/core/adapters/builtin/codex.ts +35 -0
  29. package/src/core/adapters/builtin/gemini.ts +34 -0
  30. package/src/core/adapters/builtin/index.ts +31 -0
  31. package/src/core/adapters/builtin/mock-coder.ts +148 -0
  32. package/src/core/adapters/builtin/qwen.ts +34 -0
  33. package/src/core/adapters/define.ts +48 -0
  34. package/src/core/adapters/index.ts +43 -0
  35. package/src/core/adapters/loader.ts +143 -0
  36. package/src/core/adapters/provider-bridge.ts +190 -0
  37. package/src/core/adapters/runner.ts +437 -0
  38. package/src/core/adapters/types.ts +172 -0
  39. package/src/core/config.ts +290 -0
  40. package/src/core/define-provider.ts +212 -0
  41. package/src/core/error-patterns.ts +147 -0
  42. package/src/core/index.ts +130 -0
  43. package/src/core/memory-monitor.ts +171 -0
  44. package/src/core/plugin/builtin.ts +87 -0
  45. package/src/core/plugin/index.ts +34 -0
  46. package/src/core/plugin/registry.ts +350 -0
  47. package/src/core/plugin/types.ts +209 -0
  48. package/src/core/provider-factory.ts +397 -0
  49. package/src/core/provider-loader.ts +171 -0
  50. package/src/core/providers/codex.ts +56 -0
  51. package/src/core/providers/configurable.ts +637 -0
  52. package/src/core/providers/custom.ts +261 -0
  53. package/src/core/providers/gemini.ts +41 -0
  54. package/src/core/providers/index.ts +383 -0
  55. package/src/core/providers/opencode.ts +168 -0
  56. package/src/core/providers/qwen-code.ts +41 -0
  57. package/src/core/router.ts +370 -0
  58. package/src/core/state.ts +258 -0
  59. package/src/core/types.ts +206 -0
  60. package/src/core/unified/capabilities.ts +184 -0
  61. package/src/core/unified/errors.ts +141 -0
  62. package/src/core/unified/index.ts +29 -0
  63. package/src/core/unified/output.ts +189 -0
  64. package/src/core/wrap-terminalcoder.ts +245 -0
  65. package/src/mcp/__tests__/server.test.ts +295 -0
  66. package/src/mcp/server.ts +284 -0
  67. package/src/test-fixtures/mock-coder.sh +194 -0
  68. package/dist/cli/index.js +0 -16501
  69. package/dist/core/index.js +0 -7531
  70. package/dist/mcp/server.js +0 -14568
  71. package/dist/wraptc-1.0.2.tgz +0 -0
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Enhanced Error Types
3
+ *
4
+ * Provides structured error handling with categorization and suggestions.
5
+ */
6
+
7
+ import type { ProviderErrorContext, ProviderErrorKind } from "../types";
8
+
9
+ /**
10
+ * Enhanced provider error interface
11
+ */
12
+ export interface IProviderError extends Error {
13
+ readonly kind: ProviderErrorKind;
14
+ readonly provider: string;
15
+ readonly context: ProviderErrorContext;
16
+ readonly isRetryable: boolean;
17
+ readonly isUserError: boolean;
18
+ readonly suggestedAction?: string;
19
+ }
20
+
21
+ /**
22
+ * Enhanced error class with helpful properties
23
+ */
24
+ export class ProviderError extends Error implements IProviderError {
25
+ readonly kind: ProviderErrorKind;
26
+ readonly provider: string;
27
+ readonly context: ProviderErrorContext;
28
+
29
+ constructor(
30
+ message: string,
31
+ kind: ProviderErrorKind,
32
+ provider: string,
33
+ context: ProviderErrorContext = {},
34
+ ) {
35
+ super(message);
36
+ this.name = "ProviderError";
37
+ this.kind = kind;
38
+ this.provider = provider;
39
+ this.context = context;
40
+ }
41
+
42
+ /**
43
+ * Whether this error is safe to retry
44
+ */
45
+ get isRetryable(): boolean {
46
+ return ["TRANSIENT", "RATE_LIMIT", "TIMEOUT"].includes(this.kind);
47
+ }
48
+
49
+ /**
50
+ * Whether this error is likely due to user input
51
+ */
52
+ get isUserError(): boolean {
53
+ return ["BAD_REQUEST", "UNAUTHORIZED", "CONTEXT_LENGTH", "CONTENT_FILTER"].includes(this.kind);
54
+ }
55
+
56
+ /**
57
+ * Suggested action to resolve the error
58
+ */
59
+ get suggestedAction(): string | undefined {
60
+ const actions: Partial<Record<ProviderErrorKind, string>> = {
61
+ UNAUTHORIZED: "Check your API key configuration",
62
+ RATE_LIMIT: "Wait and retry, or switch to a different provider",
63
+ OUT_OF_CREDITS: "Check your quota or upgrade your plan",
64
+ CONTEXT_LENGTH: "Reduce the input size or use a different model",
65
+ CONTENT_FILTER: "Modify your prompt to comply with content policies",
66
+ TIMEOUT: "Try again with a shorter prompt or increase timeout",
67
+ BAD_REQUEST: "Check the request format and parameters",
68
+ NOT_FOUND: "Verify the model or endpoint exists",
69
+ FORBIDDEN: "Check your permissions for this operation",
70
+ INTERNAL: "The provider is experiencing issues, try again later",
71
+ TRANSIENT: "This is a temporary error, retry the request",
72
+ };
73
+ return actions[this.kind];
74
+ }
75
+
76
+ /**
77
+ * Create from an adapter error
78
+ */
79
+ static fromAdapterError(
80
+ err: Error & { kind?: ProviderErrorKind; adapterId?: string; context?: ProviderErrorContext },
81
+ ): ProviderError {
82
+ return new ProviderError(
83
+ err.message,
84
+ err.kind ?? "UNKNOWN",
85
+ err.adapterId ?? "unknown",
86
+ err.context ?? {},
87
+ );
88
+ }
89
+
90
+ /**
91
+ * Create from a generic error
92
+ */
93
+ static fromError(
94
+ err: Error,
95
+ provider: string,
96
+ kind: ProviderErrorKind = "UNKNOWN",
97
+ ): ProviderError {
98
+ return new ProviderError(err.message, kind, provider, {});
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Error classification helper
104
+ */
105
+ export function classifyErrorMessage(message: string): ProviderErrorKind {
106
+ const lower = message.toLowerCase();
107
+
108
+ // Check patterns in priority order
109
+ if (lower.includes("rate limit") || lower.includes("429") || lower.includes("too many")) {
110
+ return "RATE_LIMIT";
111
+ }
112
+ if (lower.includes("quota") || lower.includes("credits") || lower.includes("exceeded")) {
113
+ return "OUT_OF_CREDITS";
114
+ }
115
+ if (lower.includes("unauthorized") || lower.includes("401") || lower.includes("api key")) {
116
+ return "UNAUTHORIZED";
117
+ }
118
+ if (lower.includes("forbidden") || lower.includes("403")) {
119
+ return "FORBIDDEN";
120
+ }
121
+ if (lower.includes("not found") || lower.includes("404")) {
122
+ return "NOT_FOUND";
123
+ }
124
+ if (lower.includes("timeout") || lower.includes("timed out")) {
125
+ return "TIMEOUT";
126
+ }
127
+ if (lower.includes("context length") || lower.includes("too long")) {
128
+ return "CONTEXT_LENGTH";
129
+ }
130
+ if (lower.includes("content filter") || lower.includes("blocked")) {
131
+ return "CONTENT_FILTER";
132
+ }
133
+ if (lower.includes("500") || lower.includes("internal error")) {
134
+ return "INTERNAL";
135
+ }
136
+ if (lower.includes("bad request") || lower.includes("400")) {
137
+ return "BAD_REQUEST";
138
+ }
139
+
140
+ return "TRANSIENT";
141
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Unified Interface Exports
3
+ */
4
+
5
+ // Capabilities
6
+ export {
7
+ Capability,
8
+ type CapabilityType,
9
+ type ICapabilities,
10
+ ProviderCapabilities,
11
+ } from "./capabilities";
12
+
13
+ // Output
14
+ export {
15
+ type IOutput,
16
+ type IOutputMeta,
17
+ type UnifiedStreamChunk,
18
+ type IProviderError as IOutputError,
19
+ type NormalizerConfig,
20
+ DEFAULT_NORMALIZER_CONFIG,
21
+ OutputNormalizer,
22
+ } from "./output";
23
+
24
+ // Errors
25
+ export {
26
+ type IProviderError,
27
+ ProviderError,
28
+ classifyErrorMessage,
29
+ } from "./errors";
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Unified Output Types
3
+ *
4
+ * Provides a consistent output format across all providers.
5
+ */
6
+
7
+ import type { TokenUsage } from "../types";
8
+
9
+ /**
10
+ * Normalized output format - all providers produce this
11
+ */
12
+ export interface IOutput {
13
+ /** The text response */
14
+ readonly text: string;
15
+
16
+ /** Token usage (if available) */
17
+ readonly usage?: TokenUsage;
18
+
19
+ /** Response metadata */
20
+ readonly meta: IOutputMeta;
21
+
22
+ /** Original raw output (for debugging) */
23
+ readonly raw?: unknown;
24
+ }
25
+
26
+ /**
27
+ * Output metadata
28
+ */
29
+ export interface IOutputMeta {
30
+ readonly provider: string;
31
+ readonly model?: string;
32
+ readonly elapsedMs: number;
33
+ readonly finishReason?: "stop" | "length" | "content_filter" | "error";
34
+ readonly streamingMode?: "none" | "text" | "jsonl";
35
+ }
36
+
37
+ /**
38
+ * Streaming output chunk types (unified)
39
+ */
40
+ export type UnifiedStreamChunk =
41
+ | { type: "start"; provider: string; model?: string; requestId: string }
42
+ | { type: "text"; content: string }
43
+ | { type: "json"; data: unknown }
44
+ | { type: "usage"; usage: TokenUsage }
45
+ | { type: "complete"; output: IOutput }
46
+ | { type: "error"; error: IProviderError };
47
+
48
+ /**
49
+ * Provider error interface
50
+ */
51
+ export interface IProviderError {
52
+ readonly message: string;
53
+ readonly kind: string;
54
+ readonly provider: string;
55
+ readonly isRetryable: boolean;
56
+ }
57
+
58
+ /**
59
+ * Output normalizer configuration
60
+ */
61
+ export interface NormalizerConfig {
62
+ /** Fields to check for text in JSON output */
63
+ jsonTextFields: string[];
64
+ /** Field containing usage info */
65
+ jsonUsageField?: string;
66
+ /** Fields to check for text in JSONL streaming */
67
+ jsonlTextFields: string[];
68
+ }
69
+
70
+ /**
71
+ * Default normalizer configuration
72
+ */
73
+ export const DEFAULT_NORMALIZER_CONFIG: NormalizerConfig = {
74
+ jsonTextFields: ["text", "response", "output", "content", "result", "message"],
75
+ jsonUsageField: "usage",
76
+ jsonlTextFields: ["text", "delta", "content"],
77
+ };
78
+
79
+ /**
80
+ * Output normalizer - converts raw output to unified format
81
+ */
82
+ export class OutputNormalizer {
83
+ private config: NormalizerConfig;
84
+
85
+ constructor(config: NormalizerConfig = DEFAULT_NORMALIZER_CONFIG) {
86
+ this.config = config;
87
+ }
88
+
89
+ /**
90
+ * Normalize raw text output
91
+ */
92
+ normalizeText(stdout: string, meta: Partial<IOutputMeta>): IOutput {
93
+ return {
94
+ text: stdout.trim(),
95
+ meta: this.buildMeta(meta),
96
+ raw: stdout,
97
+ };
98
+ }
99
+
100
+ /**
101
+ * Normalize JSON output
102
+ */
103
+ normalizeJson(stdout: string, meta: Partial<IOutputMeta>): IOutput {
104
+ try {
105
+ const parsed = JSON.parse(stdout);
106
+ const text = this.extractTextField(parsed);
107
+ const usage = this.extractUsage(parsed);
108
+
109
+ return {
110
+ text,
111
+ usage,
112
+ meta: this.buildMeta(meta),
113
+ raw: parsed,
114
+ };
115
+ } catch {
116
+ // Fall back to text normalization if JSON parsing fails
117
+ return this.normalizeText(stdout, meta);
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Normalize JSONL streaming output
123
+ */
124
+ *normalizeJsonl(lines: string[]): Generator<{ text?: string; usage?: TokenUsage }> {
125
+ for (const line of lines) {
126
+ if (!line.trim()) continue;
127
+
128
+ try {
129
+ const parsed = JSON.parse(line);
130
+ const text = this.extractTextField(parsed);
131
+ const usage = this.extractUsage(parsed);
132
+
133
+ if (text) yield { text };
134
+ if (usage) yield { usage };
135
+ } catch {
136
+ // Non-JSON line, emit as text
137
+ yield { text: line };
138
+ }
139
+ }
140
+ }
141
+
142
+ private extractTextField(obj: unknown): string {
143
+ if (typeof obj === "string") return obj;
144
+ if (typeof obj !== "object" || obj === null) return String(obj);
145
+
146
+ for (const field of this.config.jsonTextFields) {
147
+ const value = this.getNestedField(obj, field);
148
+ if (typeof value === "string") return value;
149
+ }
150
+
151
+ return JSON.stringify(obj);
152
+ }
153
+
154
+ private extractUsage(obj: unknown): TokenUsage | undefined {
155
+ if (typeof obj !== "object" || obj === null) return undefined;
156
+
157
+ const usageField = this.config.jsonUsageField;
158
+ if (usageField) {
159
+ const usage = this.getNestedField(obj, usageField);
160
+ if (usage && typeof usage === "object") {
161
+ return usage as TokenUsage;
162
+ }
163
+ }
164
+
165
+ return undefined;
166
+ }
167
+
168
+ private getNestedField(obj: unknown, path: string): unknown {
169
+ const parts = path.split(".");
170
+ let current = obj;
171
+
172
+ for (const part of parts) {
173
+ if (current === null || current === undefined) return undefined;
174
+ current = (current as Record<string, unknown>)[part];
175
+ }
176
+
177
+ return current;
178
+ }
179
+
180
+ private buildMeta(partial: Partial<IOutputMeta>): IOutputMeta {
181
+ return {
182
+ provider: partial.provider || "unknown",
183
+ model: partial.model,
184
+ elapsedMs: partial.elapsedMs || 0,
185
+ finishReason: partial.finishReason,
186
+ streamingMode: partial.streamingMode,
187
+ };
188
+ }
189
+ }
@@ -0,0 +1,245 @@
1
+ import { ConfigLoader } from "./config";
2
+ import { ProviderFactory, RequestDeduplicator } from "./provider-factory";
3
+ import { CodexProvider } from "./providers/codex";
4
+ import { CustomProvider } from "./providers/custom";
5
+ import { GeminiProvider } from "./providers/gemini";
6
+ import type { Provider } from "./providers/index";
7
+ import { OpenCodeProvider } from "./providers/opencode";
8
+ import { QwenCodeProvider } from "./providers/qwen-code";
9
+ import { Router } from "./router";
10
+ import { StateManager } from "./state";
11
+ import type { CodingEvent, CodingRequest, CodingResponse, Config } from "./types";
12
+
13
+ export interface WrapTerminalCoderConfig {
14
+ configPath?: string;
15
+ /** Preload providers at startup (default: false for lazy loading) */
16
+ preloadProviders?: boolean;
17
+ /** Enable request deduplication (default: true) */
18
+ deduplicateRequests?: boolean;
19
+ }
20
+
21
+ export class WrapTerminalCoder {
22
+ private configLoader: ConfigLoader;
23
+ private config: Config;
24
+ private stateManager: StateManager;
25
+ private router: Router;
26
+ private providerFactory: ProviderFactory;
27
+ private deduplicator: RequestDeduplicator | null;
28
+
29
+ private constructor(
30
+ configLoader: ConfigLoader,
31
+ config: Config,
32
+ stateManager: StateManager,
33
+ router: Router,
34
+ providerFactory: ProviderFactory,
35
+ deduplicator: RequestDeduplicator | null,
36
+ ) {
37
+ this.configLoader = configLoader;
38
+ this.config = config;
39
+ this.stateManager = stateManager;
40
+ this.router = router;
41
+ this.providerFactory = providerFactory;
42
+ this.deduplicator = deduplicator;
43
+ }
44
+
45
+ static async create(config: WrapTerminalCoderConfig = {}): Promise<WrapTerminalCoder> {
46
+ // Load configuration
47
+ const configLoader = new ConfigLoader({
48
+ projectConfigPath: config.configPath,
49
+ });
50
+ const loadedConfig = await configLoader.loadConfig();
51
+
52
+ // Initialize state manager
53
+ const stateManager = new StateManager();
54
+ await stateManager.initialize();
55
+
56
+ // Create provider factory with lazy loading
57
+ const providerFactory = new ProviderFactory(loadedConfig);
58
+
59
+ // Register known provider constructors
60
+ providerFactory.registerProvider("gemini", (id, cfg) => new GeminiProvider(cfg));
61
+ providerFactory.registerProvider("qwen-code", (id, cfg) => new QwenCodeProvider(cfg));
62
+ providerFactory.registerProvider("codex", (id, cfg) => new CodexProvider(cfg));
63
+ providerFactory.registerProvider("opencode", (id, cfg) => new OpenCodeProvider(cfg));
64
+ providerFactory.registerCustomProvider((id, cfg) => new CustomProvider(id, cfg));
65
+
66
+ // Create request deduplicator
67
+ const deduplicator = config.deduplicateRequests !== false ? new RequestDeduplicator() : null;
68
+
69
+ // Optionally preload providers (legacy behavior)
70
+ let providers: Map<string, Provider>;
71
+ if (config.preloadProviders) {
72
+ const providerIds = Object.keys(loadedConfig.providers);
73
+ await providerFactory.preloadProviders(providerIds);
74
+ providers = providerFactory.getCachedProviders();
75
+ } else {
76
+ // Create a lazy-loading proxy map for the router
77
+ providers = new LazyProviderMap(providerFactory, Object.keys(loadedConfig.providers));
78
+ }
79
+
80
+ // Create router
81
+ const router = new Router(providers, {
82
+ config: loadedConfig,
83
+ stateManager,
84
+ });
85
+
86
+ return new WrapTerminalCoder(
87
+ configLoader,
88
+ loadedConfig,
89
+ stateManager,
90
+ router,
91
+ providerFactory,
92
+ deduplicator,
93
+ );
94
+ }
95
+
96
+ async route(request: CodingRequest): Promise<CodingResponse> {
97
+ return await this.router.route(request);
98
+ }
99
+
100
+ async *routeStream(request: CodingRequest): AsyncGenerator<CodingResponse> {
101
+ yield* this.router.routeStream(request);
102
+ }
103
+
104
+ async getProviderInfo(): Promise<any[]> {
105
+ const providerInfo = [];
106
+ const providerIds = Object.keys(this.config.providers);
107
+
108
+ for (const id of providerIds) {
109
+ const state = await this.stateManager.getProviderState(id);
110
+ const isAvailable = await this.providerFactory.isProviderAvailable(id);
111
+ providerInfo.push({
112
+ id,
113
+ displayName: this.getProviderDisplayName(id),
114
+ requestsToday: state.requestsToday,
115
+ outOfCreditsUntil: state.outOfCreditsUntil,
116
+ available: isAvailable,
117
+ });
118
+ }
119
+
120
+ return providerInfo;
121
+ }
122
+
123
+ /**
124
+ * Get available provider IDs (those with valid binaries)
125
+ */
126
+ async getAvailableProviders(): Promise<string[]> {
127
+ return this.providerFactory.getAvailableProviderIds();
128
+ }
129
+
130
+ getRouter(): Router {
131
+ return this.router;
132
+ }
133
+
134
+ getStateManager(): StateManager {
135
+ return this.stateManager;
136
+ }
137
+
138
+ getConfig(): Config {
139
+ return this.config;
140
+ }
141
+
142
+ getProviderFactory(): ProviderFactory {
143
+ return this.providerFactory;
144
+ }
145
+
146
+ private getProviderDisplayName(id: string): string {
147
+ const displayNames: Record<string, string> = {
148
+ gemini: "Gemini CLI",
149
+ "qwen-code": "Qwen Code CLI",
150
+ codex: "Codex CLI",
151
+ opencode: "OpenCode Agent",
152
+ };
153
+ return displayNames[id] || id;
154
+ }
155
+ }
156
+
157
+ /**
158
+ * LazyProviderMap - A Map-like object that lazily loads providers
159
+ *
160
+ * This allows the Router to work with providers without loading them all upfront.
161
+ * Providers are loaded on first access via get().
162
+ */
163
+ class LazyProviderMap implements Map<string, Provider> {
164
+ private factory: ProviderFactory;
165
+ private knownIds: Set<string>;
166
+ private cache: Map<string, Provider> = new Map();
167
+
168
+ constructor(factory: ProviderFactory, providerIds: string[]) {
169
+ this.factory = factory;
170
+ this.knownIds = new Set(providerIds);
171
+ }
172
+
173
+ get(key: string): Provider | undefined {
174
+ // Return from cache if available
175
+ if (this.cache.has(key)) {
176
+ return this.cache.get(key);
177
+ }
178
+
179
+ // For lazy loading, we need to handle async loading synchronously
180
+ // The Router will need to be updated to handle async provider loading
181
+ // For now, return undefined and let the Router handle missing providers
182
+ return undefined;
183
+ }
184
+
185
+ has(key: string): boolean {
186
+ return this.knownIds.has(key) || this.cache.has(key);
187
+ }
188
+
189
+ set(key: string, value: Provider): this {
190
+ this.cache.set(key, value);
191
+ this.knownIds.add(key);
192
+ return this;
193
+ }
194
+
195
+ delete(key: string): boolean {
196
+ this.knownIds.delete(key);
197
+ return this.cache.delete(key);
198
+ }
199
+
200
+ clear(): void {
201
+ this.knownIds.clear();
202
+ this.cache.clear();
203
+ }
204
+
205
+ get size(): number {
206
+ return this.knownIds.size;
207
+ }
208
+
209
+ keys(): IterableIterator<string> {
210
+ return this.knownIds.values();
211
+ }
212
+
213
+ values(): IterableIterator<Provider> {
214
+ return this.cache.values();
215
+ }
216
+
217
+ entries(): IterableIterator<[string, Provider]> {
218
+ return this.cache.entries();
219
+ }
220
+
221
+ forEach(callbackfn: (value: Provider, key: string, map: Map<string, Provider>) => void): void {
222
+ this.cache.forEach(callbackfn);
223
+ }
224
+
225
+ [Symbol.iterator](): IterableIterator<[string, Provider]> {
226
+ return this.cache[Symbol.iterator]();
227
+ }
228
+
229
+ [Symbol.toStringTag] = "LazyProviderMap";
230
+
231
+ /**
232
+ * Async method to get a provider (loads if not cached)
233
+ */
234
+ async getAsync(key: string): Promise<Provider | null> {
235
+ if (this.cache.has(key)) {
236
+ return this.cache.get(key)!;
237
+ }
238
+
239
+ const provider = await this.factory.getProvider(key);
240
+ if (provider) {
241
+ this.cache.set(key, provider);
242
+ }
243
+ return provider;
244
+ }
245
+ }