@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.
@@ -0,0 +1,60 @@
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
+ * Retry configuration interface
9
+ */
10
+ export interface RetryConfig {
11
+ enabled: boolean;
12
+ maxAttempts: number;
13
+ baseDelay: number; // Initial delay in ms
14
+ maxDelay: number; // Maximum delay in ms
15
+ backoffMultiplier: number; // Exponential backoff multiplier
16
+ jitter: boolean; // Add randomization to prevent thundering herd
17
+ retryableStatusCodes: number[]; // Which HTTP status codes to retry
18
+ retryableErrors: string[]; // Which error types to retry
19
+ }
20
+
21
+ /**
22
+ * Preset retry configurations
23
+ */
24
+ export type RetryPreset = 'aggressive' | 'balanced' | 'conservative' | 'none';
25
+
26
+ /**
27
+ * Retry event callbacks for monitoring
28
+ */
29
+ export interface RetryEventCallbacks {
30
+ /** Called before each retry attempt */
31
+ onRetry?: (attempt: number, error: Error, delay: number) => void;
32
+
33
+ /** Called when all retry attempts are exhausted */
34
+ onRetryExhausted?: (operation: string, finalError: Error) => void;
35
+ }
36
+
37
+ /**
38
+ * Retry-related error types
39
+ */
40
+ export class RetryError extends Error {
41
+ constructor(
42
+ message: string,
43
+ public attempts: number,
44
+ public lastError: Error
45
+ ) {
46
+ super(message);
47
+ this.name = 'RetryError';
48
+ }
49
+ }
50
+
51
+ export class MaxRetriesExceededError extends RetryError {
52
+ constructor(attempts: number, lastError: Error) {
53
+ super(\`Maximum retry attempts (\${attempts}) exceeded\`, attempts, lastError);
54
+ this.name = 'MaxRetriesExceededError';
55
+ }
56
+ }
57
+ `}
58
+ </File>
59
+ );
60
+ };
@@ -12,14 +12,22 @@ module.exports = function ({ asyncapi, params }) {
12
12
  <File name="http.ts">
13
13
  {`import { Transport, TransportConfig, RequestOptions, MessageEnvelope } from '../types';
14
14
  import { TransportError, ConnectionError, TimeoutError } from '../errors';
15
+ import { generateAuthHeaders, generateAuthQueryParams, hasAuthCredentials, AuthError, UnauthorizedError, AuthCredentials } from '../auth';
16
+ import { createRetryManager, getRetryConfig } from '../retry';
15
17
 
16
18
  export class HttpTransport implements Transport {
17
19
  private config: TransportConfig;
18
20
  private baseUrl: string;
21
+ private retryManager: any;
19
22
 
20
23
  constructor(config: TransportConfig) {
21
24
  this.config = config;
22
25
  this.baseUrl = config.url;
26
+
27
+ // Initialize retry manager if retry config is provided
28
+ if (config.retry) {
29
+ this.retryManager = createRetryManager(config.retry, config.retryCallbacks);
30
+ }
23
31
  }
24
32
 
25
33
  async connect(): Promise<void> {
@@ -33,46 +41,114 @@ export class HttpTransport implements Transport {
33
41
  }
34
42
 
35
43
  async send(channel: string, envelope: MessageEnvelope, options?: RequestOptions): Promise<any> {
36
- const url = \`\${this.baseUrl}/\${channel}\`;
37
- const timeout = options?.timeout || 30000;
44
+ // Determine retry configuration (options override config)
45
+ const retryConfig = options?.retry ? getRetryConfig(options.retry) :
46
+ this.config.retry ? getRetryConfig(this.config.retry) : null;
47
+
48
+ const retryManager = retryConfig ? createRetryManager(retryConfig, this.config.retryCallbacks) : null;
49
+
50
+ const sendOperation = async () => {
51
+ return this.performHttpRequest(channel, envelope, options);
52
+ };
53
+
54
+ // Execute with retry if configured
55
+ if (retryManager) {
56
+ return retryManager.executeWithRetry(sendOperation, \`\${envelope.operation} on \${channel}\`);
57
+ } else {
58
+ return sendOperation();
59
+ }
60
+ }
61
+
62
+ private async performHttpRequest(channel: string, envelope: MessageEnvelope, options?: RequestOptions): Promise<any> {
63
+ let url = \`\${this.baseUrl}/\${channel}\`;
64
+ const timeout = options?.timeout || this.config.timeout || 30000;
65
+
66
+ // Generate auth headers if credentials are available
67
+ const authHeaders = this.config.auth ? generateAuthHeaders(this.config.auth) : {};
38
68
 
39
69
  // Prepare the complete envelope for HTTP transport
40
70
  const messageEnvelope: MessageEnvelope = {
41
71
  ...envelope,
42
72
  channel: envelope.channel || channel,
43
73
  timestamp: envelope.timestamp || new Date().toISOString(),
44
- id: options?.correlationId || envelope.id
74
+ id: options?.correlationId || envelope.id || this.generateCorrelationId(),
75
+ headers: {
76
+ ...envelope.headers,
77
+ ...authHeaders
78
+ }
79
+ };
80
+
81
+ // Prepare headers
82
+ const headers: Record<string, string> = {
83
+ 'Content-Type': 'application/json',
84
+ 'X-Operation': envelope.operation,
85
+ 'X-Correlation-ID': messageEnvelope.id || '',
86
+ ...this.config.headers
45
87
  };
46
88
 
89
+ // Add auth headers if auth is configured
90
+ if (this.config.auth && hasAuthCredentials(this.config.auth)) {
91
+ try {
92
+ const authHeaders = generateAuthHeaders(this.config.auth);
93
+ Object.assign(headers, authHeaders);
94
+ } catch (error) {
95
+ const errorMessage = error instanceof Error ? error.message : String(error);
96
+ throw new AuthError(\`Failed to generate auth headers: \${errorMessage}\`);
97
+ }
98
+ }
99
+
100
+ // Add auth query parameters if needed
101
+ if (this.config.auth?.apikey?.location === 'query') {
102
+ const authParams = generateAuthQueryParams(this.config.auth);
103
+ const urlObj = new URL(url);
104
+ Object.entries(authParams).forEach(([key, value]) => {
105
+ urlObj.searchParams.set(key, value);
106
+ });
107
+ url = urlObj.toString();
108
+ }
109
+
47
110
  const controller = new AbortController();
48
111
  const timeoutId = setTimeout(() => controller.abort(), timeout);
49
112
 
50
113
  try {
51
114
  const response = await fetch(url, {
52
115
  method: 'POST',
53
- headers: {
54
- 'Content-Type': 'application/json',
55
- 'X-Operation': envelope.operation,
56
- 'X-Correlation-ID': messageEnvelope.id || '',
57
- ...this.config.headers
58
- },
116
+ headers,
59
117
  body: JSON.stringify(messageEnvelope),
60
118
  signal: controller.signal
61
119
  });
62
120
 
63
121
  clearTimeout(timeoutId);
64
122
 
123
+ // Handle auth errors specifically
124
+ if (response.status === 401) {
125
+ // Call auth error callback if available
126
+ if (this.config.authCallbacks?.onAuthError) {
127
+ const shouldRetry = await this.config.authCallbacks.onAuthError();
128
+ if (shouldRetry) {
129
+ // Retry the request with potentially updated auth
130
+ return this.performHttpRequest(channel, envelope, options);
131
+ }
132
+ }
133
+ throw new UnauthorizedError('Authentication failed');
134
+ }
135
+
65
136
  if (!response.ok) {
137
+ // Create error with status for retry logic
138
+ const error = new TransportError(\`HTTP request failed: \${response.status} \${response.statusText}\`);
139
+ (error as any).status = response.status;
140
+
66
141
  // Try to parse error response as envelope
67
142
  try {
68
143
  const errorEnvelope: MessageEnvelope = await response.json();
69
144
  if (errorEnvelope.error) {
70
- throw new TransportError(\`\${errorEnvelope.error.code}: \${errorEnvelope.error.message}\`);
145
+ error.message = \`\${errorEnvelope.error.code}: \${errorEnvelope.error.message}\`;
71
146
  }
72
147
  } catch {
73
- // Fall back to HTTP status error
74
- throw new TransportError(\`HTTP request failed: \${response.status} \${response.statusText}\`);
148
+ // Use the default error message
75
149
  }
150
+
151
+ throw error;
76
152
  }
77
153
 
78
154
  // Parse response as envelope
@@ -88,15 +164,34 @@ export class HttpTransport implements Transport {
88
164
  clearTimeout(timeoutId);
89
165
 
90
166
  if (error.name === 'AbortError') {
91
- throw new TimeoutError(\`Request timeout after \${timeout}ms\`);
167
+ const timeoutError = new TimeoutError(\`Request timeout after \${timeout}ms\`);
168
+ (timeoutError as any).code = 'TIMEOUT';
169
+ throw timeoutError;
92
170
  }
93
171
 
94
- // Re-throw TransportError as-is
95
- if (error instanceof TransportError) {
172
+ // Add error codes for retry logic
173
+ if (error instanceof TransportError || error instanceof AuthError) {
96
174
  throw error;
97
175
  }
98
176
 
99
- throw new TransportError(\`HTTP request failed: \${error.message}\`);
177
+ // Network errors
178
+ const networkError = new TransportError(\`HTTP request failed: \${error.message}\`);
179
+ (networkError as any).code = 'NETWORK_ERROR';
180
+ throw networkError;
181
+ }
182
+ }
183
+
184
+ private generateCorrelationId(): string {
185
+ return \`http-\${Date.now()}-\${Math.random().toString(36).substr(2, 9)}\`;
186
+ }
187
+
188
+ /**
189
+ * Update authentication configuration
190
+ * @param auth New authentication configuration
191
+ */
192
+ updateAuth(auth: AuthCredentials): void {
193
+ if (this.config) {
194
+ this.config.auth = auth;
100
195
  }
101
196
  }
102
197
 
@@ -119,4 +214,4 @@ export class HttpTransport implements Transport {
119
214
  }`}
120
215
  </File>
121
216
  );
122
- }
217
+ };
@@ -13,6 +13,8 @@ module.exports = function ({ asyncapi, params }) {
13
13
  {`import { v4 as uuidv4 } from 'uuid';
14
14
  import { Transport, TransportConfig, RequestOptions, ResponseHandler, MessageEnvelope, EnvelopeCallback } from '../types';
15
15
  import { TransportError, ConnectionError, TimeoutError } from '../errors';
16
+ import { generateAuthHeaders, generateAuthQueryParams, hasAuthCredentials, AuthError, UnauthorizedError, AuthCredentials } from '../auth';
17
+ import { createRetryManager, getRetryConfig } from '../retry';
16
18
 
17
19
  // Environment-aware WebSocket implementation
18
20
  const getWebSocketImpl = (): typeof WebSocket => {
@@ -122,13 +124,20 @@ export class WebSocketTransport implements Transport {
122
124
  throw new TransportError('WebSocket is not connected');
123
125
  }
124
126
 
127
+ // Generate auth headers if credentials are available
128
+ const authHeaders = this.config.auth ? generateAuthHeaders(this.config.auth) : {};
129
+
125
130
  // Ensure envelope has required fields
126
131
  const requestId = options?.correlationId || envelope.id || uuidv4();
127
132
  const messageEnvelope: MessageEnvelope = {
128
133
  ...envelope,
129
134
  id: requestId,
130
135
  channel: envelope.channel || channel,
131
- timestamp: envelope.timestamp || new Date().toISOString()
136
+ timestamp: envelope.timestamp || new Date().toISOString(),
137
+ headers: {
138
+ ...envelope.headers,
139
+ ...authHeaders
140
+ }
132
141
  };
133
142
 
134
143
  return new Promise((resolve, reject) => {
@@ -162,11 +171,15 @@ export class WebSocketTransport implements Transport {
162
171
 
163
172
  // Send subscription message to server using envelope format
164
173
  if (this.ws && this.ws.readyState === this.ws.OPEN) {
174
+ // Generate auth headers if credentials are available
175
+ const authHeaders = this.config.auth ? generateAuthHeaders(this.config.auth) : {};
176
+
165
177
  const subscribeEnvelope: MessageEnvelope = {
166
178
  operation: 'subscribe',
167
179
  channel,
168
180
  payload: { channel },
169
- timestamp: new Date().toISOString()
181
+ timestamp: new Date().toISOString(),
182
+ headers: authHeaders
170
183
  };
171
184
  this.ws.send(JSON.stringify(subscribeEnvelope));
172
185
  }
@@ -323,7 +336,19 @@ export class WebSocketTransport implements Transport {
323
336
  }
324
337
  }
325
338
  }
339
+
340
+ /**
341
+ * Update authentication configuration
342
+ * @param auth New authentication configuration
343
+ */
344
+ updateAuth(auth: AuthCredentials): void {
345
+ if (this.config) {
346
+ this.config.auth = auth;
347
+ }
348
+ // Note: For WebSocket, auth is typically handled during connection
349
+ // If you need to update auth for an active connection, you may need to reconnect
350
+ }
326
351
  }`}
327
352
  </File>
328
353
  );
329
- }
354
+ };
@@ -4,7 +4,10 @@ import { File } from '@asyncapi/generator-react-sdk';
4
4
  module.exports = function ({ asyncapi, params }) {
5
5
  return (
6
6
  <File name="types.ts">
7
- {`/**
7
+ {`import { AuthCredentials, AuthEventCallbacks } from './auth';
8
+ import { RetryConfig, RetryPreset, RetryEventCallbacks } from './retry';
9
+
10
+ /**
8
11
  * Standard message envelope for all AsyncAPI communications
9
12
  */
10
13
  export interface MessageEnvelope {
@@ -13,6 +16,7 @@ export interface MessageEnvelope {
13
16
  channel?: string; // Optional channel context
14
17
  payload: any; // Message payload
15
18
  timestamp?: string; // Optional ISO 8601 timestamp
19
+ headers?: Record<string, string>; // Transport-level headers (auth, routing, etc.)
16
20
  error?: { // Error information
17
21
  code: string;
18
22
  message: string;
@@ -38,6 +42,10 @@ export interface TransportConfig {
38
42
  url: string;
39
43
  headers?: Record<string, string>;
40
44
  timeout?: number;
45
+ auth?: AuthCredentials; // Authentication credentials
46
+ retry?: RetryConfig | RetryPreset; // Retry configuration
47
+ authCallbacks?: AuthEventCallbacks; // Auth event callbacks
48
+ retryCallbacks?: RetryEventCallbacks; // Retry event callbacks
41
49
  }
42
50
 
43
51
  /**
@@ -45,7 +53,8 @@ export interface TransportConfig {
45
53
  */
46
54
  export interface RequestOptions {
47
55
  timeout?: number;
48
- correlationId?: string; // Override correlation ID
56
+ correlationId?: string; // Override correlation ID
57
+ retry?: RetryConfig | RetryPreset; // Override retry config for this request
49
58
  }
50
59
 
51
60
  /**
@@ -72,4 +81,4 @@ export type UnsubscribeFunction = () => void;
72
81
  export type EnvelopeCallback = (envelope: MessageEnvelope) => void;`}
73
82
  </File>
74
83
  );
75
- }
84
+ };