@ioka-technologies/asyncapi-ts-client-template 0.0.7 → 0.0.10

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.
@@ -2,7 +2,7 @@
2
2
  import { File } from '@asyncapi/generator-react-sdk';
3
3
 
4
4
  function generateModels(asyncapi) {
5
- let content = `// Generated TypeScript models from AsyncAPI specification\n\n`;
5
+ let content = '// Generated TypeScript models from AsyncAPI specification\n\n';
6
6
 
7
7
  // Helper functions for TypeScript identifier generation
8
8
  function toTypeScriptIdentifier(str) {
@@ -42,8 +42,104 @@ function generateModels(asyncapi) {
42
42
  // Extract message schemas and build channel mapping
43
43
  const components = asyncapi.components();
44
44
  const messageSchemas = [];
45
+ const componentSchemas = [];
45
46
  const messageToChannels = new Map();
46
47
  const generatedTypes = new Set();
48
+ const schemaRegistry = new Map();
49
+
50
+ // Build schema registry from components.schemas
51
+ // Try to access the raw AsyncAPI document
52
+ let rawDoc = null;
53
+ try {
54
+ if (asyncapi.json && typeof asyncapi.json === 'function') {
55
+ rawDoc = asyncapi.json();
56
+ } else if (asyncapi._json) {
57
+ rawDoc = asyncapi._json;
58
+ }
59
+ } catch (e) {
60
+ // Ignore
61
+ }
62
+
63
+ // Extract schemas from raw document if available
64
+ if (rawDoc && rawDoc.components && rawDoc.components.schemas) {
65
+ Object.entries(rawDoc.components.schemas).forEach(([name, schema]) => {
66
+ if (name && typeof name === 'string' && schema && typeof schema === 'object') {
67
+ schemaRegistry.set(name, schema);
68
+ componentSchemas.push({
69
+ name,
70
+ typeName: toTypeScriptTypeName(name),
71
+ schema: schema,
72
+ description: schema.description
73
+ });
74
+ }
75
+ });
76
+ }
77
+
78
+ // Fallback: try the components.schemas() method
79
+ if (componentSchemas.length === 0 && components && components.schemas) {
80
+ try {
81
+ const schemas = components.schemas();
82
+ if (schemas) {
83
+ // Try different ways to access schemas
84
+ let schemaEntries = [];
85
+
86
+ if (schemas instanceof Map) {
87
+ schemaEntries = Array.from(schemas.entries());
88
+ } else if (typeof schemas === 'object') {
89
+ schemaEntries = Object.entries(schemas);
90
+ } else if (schemas.all && typeof schemas.all === 'function') {
91
+ // AsyncAPI parser might have an all() method
92
+ const allSchemas = schemas.all();
93
+ if (Array.isArray(allSchemas)) {
94
+ schemaEntries = allSchemas.map(schema => {
95
+ const name = schema.uid ? schema.uid() : (schema.id ? schema.id() : null);
96
+ return [name, schema];
97
+ }).filter(([name]) => name);
98
+ }
99
+ }
100
+
101
+ schemaEntries.forEach(([name, schema]) => {
102
+ // Skip internal AsyncAPI parser objects and numeric keys
103
+ if (!name || name === 'collections' || name === '_meta' || name.startsWith('_') || /^\d+$/.test(name)) {
104
+ return;
105
+ }
106
+
107
+ let schemaData = null;
108
+ let description = null;
109
+
110
+ try {
111
+ // Handle different schema object types
112
+ if (schema && typeof schema.json === 'function') {
113
+ schemaData = schema.json();
114
+ } else if (schema && typeof schema === 'object') {
115
+ schemaData = schema;
116
+ }
117
+
118
+ if (schema && typeof schema.description === 'function') {
119
+ description = schema.description();
120
+ } else if (schema && schema.description) {
121
+ description = schema.description;
122
+ }
123
+ } catch (e) {
124
+ // Ignore schema extraction errors
125
+ console.warn(`Failed to extract schema for ${name}:`, e.message);
126
+ }
127
+
128
+ if (schemaData && typeof name === 'string' && name.length > 0) {
129
+ schemaRegistry.set(name, schemaData);
130
+ componentSchemas.push({
131
+ name,
132
+ typeName: toTypeScriptTypeName(name),
133
+ schema: schemaData,
134
+ description
135
+ });
136
+ }
137
+ });
138
+ }
139
+ } catch (e) {
140
+ console.warn('Failed to extract component schemas:', e.message);
141
+ }
142
+ }
47
143
 
48
144
  // First, build channel to message mapping
49
145
  if (asyncapi.channels) {
@@ -126,13 +222,25 @@ function generateModels(asyncapi) {
126
222
  }
127
223
 
128
224
  // Helper function to convert JSON schema to TypeScript type
129
- function jsonSchemaToTypeScriptType(schema) {
225
+ function jsonSchemaToTypeScriptType(schema, fieldName = '') {
130
226
  if (!schema) return 'any';
131
227
 
132
- // Handle $ref
228
+ // Handle $ref - resolve from schema registry
133
229
  if (schema.$ref) {
134
230
  const refName = schema.$ref.split('/').pop();
135
- return toTypeScriptTypeName(refName);
231
+ // Always return the type name for $ref, since we generate all component schemas
232
+ const typeName = toTypeScriptTypeName(refName);
233
+ return typeName;
234
+ }
235
+
236
+ // Handle resolved $ref - check for x-parser-schema-id which indicates original schema name
237
+ if (schema['x-parser-schema-id'] && typeof schema['x-parser-schema-id'] === 'string') {
238
+ const schemaId = schema['x-parser-schema-id'];
239
+ // Check if this matches a known component schema
240
+ if (schemaRegistry.has(schemaId)) {
241
+ const typeName = toTypeScriptTypeName(schemaId);
242
+ return typeName;
243
+ }
136
244
  }
137
245
 
138
246
  if (!schema.type) {
@@ -158,10 +266,19 @@ function generateModels(asyncapi) {
158
266
  case 'boolean':
159
267
  return 'boolean';
160
268
  case 'array': {
161
- const itemType = jsonSchemaToTypeScriptType(schema.items);
162
- return `${itemType}[]`;
269
+ if (schema.items) {
270
+ const itemType = jsonSchemaToTypeScriptType(schema.items, fieldName);
271
+ return `${itemType}[]`;
272
+ }
273
+ return 'any[]';
163
274
  }
164
275
  case 'object':
276
+ // For objects with properties, we should generate inline types or check if it's a known schema
277
+ if (schema.properties) {
278
+ // This is a complex object - for now return Record<string, any>
279
+ // In a more sophisticated implementation, we could generate inline types
280
+ return 'Record<string, any>';
281
+ }
165
282
  return 'Record<string, any>';
166
283
  default:
167
284
  return 'any';
@@ -175,7 +292,7 @@ function generateModels(asyncapi) {
175
292
  }
176
293
 
177
294
  const fields = Object.entries(schema.properties).map(([fieldName, fieldSchema]) => {
178
- const tsType = jsonSchemaToTypeScriptType(fieldSchema);
295
+ const tsType = jsonSchemaToTypeScriptType(fieldSchema, fieldName);
179
296
  const optional = !schema.required || !schema.required.includes(fieldName);
180
297
  const optionalMarker = optional ? '?' : '';
181
298
 
@@ -190,31 +307,54 @@ function generateModels(asyncapi) {
190
307
  return fields;
191
308
  }
192
309
 
193
- // Generate interfaces for each message
310
+ // Generate interfaces for component schemas first (so they can be referenced by messages)
311
+ componentSchemas.forEach(schema => {
312
+ const doc = schema.description ? `/** ${schema.description} */\n` : `/** ${schema.name} */\n`;
313
+
314
+ content += `${doc}export interface ${schema.typeName} {\n`;
315
+ content += generateMessageInterface(schema.schema, schema.typeName);
316
+ content += '\n}\n\n';
317
+
318
+ // Track generated types to avoid duplicates
319
+ generatedTypes.add(schema.typeName);
320
+ });
321
+
322
+ // Generate interfaces for each message (only if not already generated as component schema)
194
323
  messageSchemas.forEach(schema => {
324
+ const interfaceName = `${schema.typeName}Payload`;
325
+
326
+ // Check if this is a duplicate of a component schema
327
+ // For message payloads that match component schema names, skip the payload version
328
+ if (generatedTypes.has(schema.typeName) || generatedTypes.has(interfaceName)) {
329
+ // Skip generating the payload version if we already have the component schema
330
+ return;
331
+ }
332
+
195
333
  const doc = schema.description ? `/** ${schema.description} */\n` : `/** ${schema.name} message payload */\n`;
196
334
 
197
- content += `${doc}export interface ${schema.typeName}Payload {\n`;
335
+ content += `${doc}export interface ${interfaceName} {\n`;
198
336
  content += generateMessageInterface(schema.payload, schema.typeName);
199
- content += `\n}\n\n`;
337
+ content += '\n}\n\n';
338
+
339
+ generatedTypes.add(interfaceName);
200
340
  });
201
341
 
202
342
  // Generate a union type for all message payloads
203
343
  if (messageSchemas.length > 0) {
204
344
  const payloadTypes = messageSchemas.map(schema => `${schema.typeName}Payload`).join(' | ');
205
- content += `/** Union type for all message payloads */\n`;
345
+ content += '/** Union type for all message payloads */\n';
206
346
  content += `export type MessagePayload = ${payloadTypes};\n\n`;
207
347
 
208
348
  // Generate message type constants
209
- content += `/** Message type constants */\n`;
210
- content += `export const MessageTypes = {\n`;
349
+ content += '/** Message type constants */\n';
350
+ content += 'export const MessageTypes = {\n';
211
351
  messageSchemas.forEach(schema => {
212
352
  content += ` ${schema.typeName.toUpperCase()}: '${schema.name}',\n`;
213
353
  });
214
- content += `} as const;\n\n`;
354
+ content += '} as const;\n\n';
215
355
 
216
- content += `/** Message type union */\n`;
217
- content += `export type MessageType = typeof MessageTypes[keyof typeof MessageTypes];\n\n`;
356
+ content += '/** Message type union */\n';
357
+ content += 'export type MessageType = typeof MessageTypes[keyof typeof MessageTypes];\n\n';
218
358
  }
219
359
 
220
360
  return content;
@@ -226,4 +366,4 @@ module.exports = function ({ asyncapi, params }) {
226
366
  {generateModels(asyncapi)}
227
367
  </File>
228
368
  );
229
- }
369
+ };
@@ -0,0 +1,95 @@
1
+ /* eslint-disable no-unused-vars */
2
+ import { File } from '@asyncapi/generator-react-sdk';
3
+
4
+ module.exports = function ({ asyncapi, params }) {
5
+ return (
6
+ <File name="headers.ts">
7
+ {`import { AuthCredentials, AuthError } from './types';
8
+
9
+ /**
10
+ * Generate authentication headers based on credentials
11
+ */
12
+ export function generateAuthHeaders(auth: AuthCredentials): Record<string, string> {
13
+ const headers: Record<string, string> = {};
14
+
15
+ if (auth.jwt) {
16
+ headers['Authorization'] = \`Bearer \${auth.jwt}\`;
17
+ } else if (auth.basic) {
18
+ const credentials = btoa(\`\${auth.basic.username}:\${auth.basic.password}\`);
19
+ headers['Authorization'] = \`Basic \${credentials}\`;
20
+ } else if (auth.apikey) {
21
+ if (auth.apikey.location === 'header') {
22
+ headers[auth.apikey.name] = auth.apikey.key;
23
+ }
24
+ // Query parameters are handled in the transport layer
25
+ }
26
+
27
+ return headers;
28
+ }
29
+
30
+ /**
31
+ * Generate query parameters for API key authentication
32
+ */
33
+ export function generateAuthQueryParams(auth: AuthCredentials): Record<string, string> {
34
+ const params: Record<string, string> = {};
35
+
36
+ if (auth.apikey && auth.apikey.location === 'query') {
37
+ params[auth.apikey.name] = auth.apikey.key;
38
+ }
39
+
40
+ return params;
41
+ }
42
+
43
+ /**
44
+ * Validate auth credentials
45
+ */
46
+ export function validateAuthCredentials(auth: AuthCredentials): void {
47
+ if (auth.jwt) {
48
+ if (typeof auth.jwt !== 'string' || auth.jwt.trim() === '') {
49
+ throw new AuthError('JWT token must be a non-empty string', 'jwt');
50
+ }
51
+ }
52
+
53
+ if (auth.basic) {
54
+ if (!auth.basic.username || !auth.basic.password) {
55
+ throw new AuthError('Basic auth requires both username and password', 'basic');
56
+ }
57
+ if (typeof auth.basic.username !== 'string' || typeof auth.basic.password !== 'string') {
58
+ throw new AuthError('Basic auth username and password must be strings', 'basic');
59
+ }
60
+ }
61
+
62
+ if (auth.apikey) {
63
+ if (!auth.apikey.key || !auth.apikey.name) {
64
+ throw new AuthError('API key auth requires both key and name', 'apikey');
65
+ }
66
+ if (typeof auth.apikey.key !== 'string' || typeof auth.apikey.name !== 'string') {
67
+ throw new AuthError('API key and name must be strings', 'apikey');
68
+ }
69
+ if (!['header', 'query'].includes(auth.apikey.location)) {
70
+ throw new AuthError('API key location must be either "header" or "query"', 'apikey');
71
+ }
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Check if auth credentials are provided
77
+ */
78
+ export function hasAuthCredentials(auth?: AuthCredentials): boolean {
79
+ if (!auth) return false;
80
+ return !!(auth.jwt || auth.basic || auth.apikey);
81
+ }
82
+
83
+ /**
84
+ * Get auth type from credentials
85
+ */
86
+ export function getAuthType(auth: AuthCredentials): string | null {
87
+ if (auth.jwt) return 'jwt';
88
+ if (auth.basic) return 'basic';
89
+ if (auth.apikey) return 'apikey';
90
+ return null;
91
+ }
92
+ `}
93
+ </File>
94
+ );
95
+ };
@@ -0,0 +1,15 @@
1
+ /* eslint-disable no-unused-vars */
2
+ import { File } from '@asyncapi/generator-react-sdk';
3
+
4
+ module.exports = function ({ asyncapi, params }) {
5
+ return (
6
+ <File name="index.ts">
7
+ {`// Auth types and interfaces
8
+ export * from './types';
9
+
10
+ // Auth header utilities
11
+ export * from './headers';
12
+ `}
13
+ </File>
14
+ );
15
+ };
@@ -0,0 +1,74 @@
1
+ /* eslint-disable no-unused-vars */
2
+ import { File } from '@asyncapi/generator-react-sdk';
3
+
4
+ module.exports = function ({ asyncapi, params }) {
5
+ return (
6
+ <File name="types.ts">
7
+ {`/**
8
+ * Authentication credentials for different auth types
9
+ */
10
+ export interface AuthCredentials {
11
+ /** JWT Bearer token */
12
+ jwt?: string;
13
+
14
+ /** Basic authentication credentials */
15
+ basic?: {
16
+ username: string;
17
+ password: string;
18
+ };
19
+
20
+ /** API Key authentication */
21
+ apikey?: {
22
+ key: string;
23
+ location: 'header' | 'query';
24
+ name: string;
25
+ };
26
+ }
27
+
28
+ /**
29
+ * Auth-related error types
30
+ */
31
+ export class AuthError extends Error {
32
+ constructor(message: string, public authType?: string) {
33
+ super(message);
34
+ this.name = 'AuthError';
35
+ }
36
+ }
37
+
38
+ export class TokenExpiredError extends AuthError {
39
+ constructor(message: string = 'Token has expired') {
40
+ super(message, 'jwt');
41
+ this.name = 'TokenExpiredError';
42
+ }
43
+ }
44
+
45
+ export class UnauthorizedError extends AuthError {
46
+ constructor(message: string = 'Unauthorized access') {
47
+ super(message);
48
+ this.name = 'UnauthorizedError';
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Auth event callbacks for monitoring and token refresh
54
+ */
55
+ export interface AuthEventCallbacks {
56
+ /** Called when a 401 Unauthorized response is received */
57
+ onAuthError?: () => Promise<boolean>;
58
+
59
+ /** Called when token needs to be refreshed */
60
+ onTokenRefresh?: (oldToken: string) => Promise<string>;
61
+ }
62
+
63
+ /**
64
+ * Security requirement analysis result
65
+ */
66
+ export interface SecurityRequirement {
67
+ hasSecurityRequirements: boolean;
68
+ securitySchemes: any[];
69
+ requiresAuthentication: boolean;
70
+ }
71
+ `}
72
+ </File>
73
+ );
74
+ };
@@ -0,0 +1,18 @@
1
+ /* eslint-disable no-unused-vars */
2
+ import { File } from '@asyncapi/generator-react-sdk';
3
+
4
+ module.exports = function ({ asyncapi, params }) {
5
+ return (
6
+ <File name="index.ts">
7
+ {`// Retry types and interfaces
8
+ export * from './types';
9
+
10
+ // Retry presets
11
+ export * from './presets';
12
+
13
+ // Retry manager
14
+ export * from './manager';
15
+ `}
16
+ </File>
17
+ );
18
+ };
@@ -0,0 +1,150 @@
1
+ /* eslint-disable no-unused-vars */
2
+ import { File } from '@asyncapi/generator-react-sdk';
3
+
4
+ module.exports = function ({ asyncapi, params }) {
5
+ return (
6
+ <File name="manager.ts">
7
+ {`import { RetryConfig, RetryEventCallbacks, MaxRetriesExceededError } from './types';
8
+ import { getRetryConfig } from './presets';
9
+
10
+ /**
11
+ * Retry manager for handling exponential backoff and retry logic
12
+ */
13
+ export class RetryManager {
14
+ private config: RetryConfig;
15
+ private callbacks?: RetryEventCallbacks;
16
+
17
+ constructor(config: RetryConfig, callbacks?: RetryEventCallbacks) {
18
+ this.config = config;
19
+ this.callbacks = callbacks;
20
+ }
21
+
22
+ /**
23
+ * Execute an operation with retry logic
24
+ */
25
+ async executeWithRetry<T>(
26
+ operation: () => Promise<T>,
27
+ operationName: string
28
+ ): Promise<T> {
29
+ if (!this.config.enabled) {
30
+ return operation();
31
+ }
32
+
33
+ let lastError: Error;
34
+
35
+ for (let attempt = 1; attempt <= this.config.maxAttempts; attempt++) {
36
+ try {
37
+ return await operation();
38
+ } catch (error) {
39
+ lastError = error as Error;
40
+
41
+ // Check if error is retryable
42
+ if (!this.isRetryable(error) || attempt === this.config.maxAttempts) {
43
+ if (attempt === this.config.maxAttempts && this.callbacks?.onRetryExhausted) {
44
+ this.callbacks.onRetryExhausted(operationName, lastError);
45
+ }
46
+ throw error;
47
+ }
48
+
49
+ // Calculate delay with exponential backoff and jitter
50
+ const delay = this.calculateDelay(attempt);
51
+
52
+ // Call retry callback
53
+ if (this.callbacks?.onRetry) {
54
+ this.callbacks.onRetry(attempt, lastError, delay);
55
+ }
56
+
57
+ // Wait before retrying
58
+ await this.sleep(delay);
59
+ }
60
+ }
61
+
62
+ // This should never be reached, but TypeScript requires it
63
+ throw new MaxRetriesExceededError(this.config.maxAttempts, lastError!);
64
+ }
65
+
66
+ /**
67
+ * Check if an error is retryable based on configuration
68
+ */
69
+ private isRetryable(error: any): boolean {
70
+ // Check HTTP status codes
71
+ if (error.status && this.config.retryableStatusCodes.includes(error.status)) {
72
+ return true;
73
+ }
74
+
75
+ // Check error codes
76
+ if (error.code && this.config.retryableErrors.includes(error.code)) {
77
+ return true;
78
+ }
79
+
80
+ // Check error names
81
+ if (error.name && this.config.retryableErrors.includes(error.name)) {
82
+ return true;
83
+ }
84
+
85
+ // Check error messages for common network errors
86
+ if (error.message) {
87
+ const message = error.message.toLowerCase();
88
+ for (const retryableError of this.config.retryableErrors) {
89
+ if (message.includes(retryableError.toLowerCase())) {
90
+ return true;
91
+ }
92
+ }
93
+ }
94
+
95
+ return false;
96
+ }
97
+
98
+ /**
99
+ * Calculate delay with exponential backoff and optional jitter
100
+ */
101
+ private calculateDelay(attempt: number): number {
102
+ let delay = this.config.baseDelay * Math.pow(this.config.backoffMultiplier, attempt - 1);
103
+
104
+ // Apply maximum delay cap
105
+ delay = Math.min(delay, this.config.maxDelay);
106
+
107
+ // Add jitter to prevent thundering herd
108
+ if (this.config.jitter) {
109
+ delay = delay * (0.5 + Math.random() * 0.5);
110
+ }
111
+
112
+ return Math.floor(delay);
113
+ }
114
+
115
+ /**
116
+ * Sleep for specified milliseconds
117
+ */
118
+ private sleep(ms: number): Promise<void> {
119
+ return new Promise(resolve => setTimeout(resolve, ms));
120
+ }
121
+
122
+ /**
123
+ * Get current retry configuration
124
+ */
125
+ getConfig(): RetryConfig {
126
+ return { ...this.config };
127
+ }
128
+
129
+ /**
130
+ * Update retry configuration
131
+ */
132
+ updateConfig(config: Partial<RetryConfig>): void {
133
+ this.config = { ...this.config, ...config };
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Create a retry manager from config or preset
139
+ */
140
+ export function createRetryManager(
141
+ config: RetryConfig | string,
142
+ callbacks?: RetryEventCallbacks
143
+ ): RetryManager {
144
+ const retryConfig = getRetryConfig(config as any);
145
+ return new RetryManager(retryConfig, callbacks);
146
+ }
147
+ `}
148
+ </File>
149
+ );
150
+ };
@@ -0,0 +1,81 @@
1
+ /* eslint-disable no-unused-vars */
2
+ import { File } from '@asyncapi/generator-react-sdk';
3
+
4
+ module.exports = function ({ asyncapi, params }) {
5
+ return (
6
+ <File name="presets.ts">
7
+ {`import { RetryConfig, RetryPreset } from './types';
8
+
9
+ /**
10
+ * Predefined retry configuration presets
11
+ */
12
+ export const RETRY_PRESETS: Record<RetryPreset, RetryConfig> = {
13
+ none: {
14
+ enabled: false,
15
+ maxAttempts: 1,
16
+ baseDelay: 0,
17
+ maxDelay: 0,
18
+ backoffMultiplier: 1,
19
+ jitter: false,
20
+ retryableStatusCodes: [],
21
+ retryableErrors: []
22
+ },
23
+
24
+ conservative: {
25
+ enabled: true,
26
+ maxAttempts: 3,
27
+ baseDelay: 1000, // 1 second
28
+ maxDelay: 10000, // 10 seconds max
29
+ backoffMultiplier: 2,
30
+ jitter: true,
31
+ retryableStatusCodes: [500, 502, 503, 504],
32
+ retryableErrors: ['NETWORK_ERROR', 'TIMEOUT', 'ECONNRESET', 'ENOTFOUND']
33
+ },
34
+
35
+ balanced: {
36
+ enabled: true,
37
+ maxAttempts: 5,
38
+ baseDelay: 500, // 500ms
39
+ maxDelay: 30000, // 30 seconds max
40
+ backoffMultiplier: 2,
41
+ jitter: true,
42
+ retryableStatusCodes: [429, 500, 502, 503, 504],
43
+ retryableErrors: ['NETWORK_ERROR', 'TIMEOUT', 'CONNECTION_ERROR', 'ECONNRESET', 'ENOTFOUND', 'ETIMEDOUT']
44
+ },
45
+
46
+ aggressive: {
47
+ enabled: true,
48
+ maxAttempts: 10,
49
+ baseDelay: 100, // 100ms
50
+ maxDelay: 60000, // 1 minute max
51
+ backoffMultiplier: 1.5,
52
+ jitter: true,
53
+ retryableStatusCodes: [408, 429, 500, 502, 503, 504],
54
+ retryableErrors: ['NETWORK_ERROR', 'TIMEOUT', 'CONNECTION_ERROR', 'DNS_ERROR', 'ECONNRESET', 'ENOTFOUND', 'ETIMEDOUT', 'ECONNREFUSED']
55
+ }
56
+ };
57
+
58
+ /**
59
+ * Get retry configuration from preset or return custom config
60
+ */
61
+ export function getRetryConfig(config: RetryConfig | RetryPreset): RetryConfig {
62
+ if (typeof config === 'string') {
63
+ return RETRY_PRESETS[config];
64
+ }
65
+ return config;
66
+ }
67
+
68
+ /**
69
+ * Merge custom retry config with preset defaults
70
+ */
71
+ export function mergeRetryConfig(preset: RetryPreset, overrides: Partial<RetryConfig>): RetryConfig {
72
+ const baseConfig = RETRY_PRESETS[preset];
73
+ return {
74
+ ...baseConfig,
75
+ ...overrides
76
+ };
77
+ }
78
+ `}
79
+ </File>
80
+ );
81
+ };