@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.
- package/README.md +468 -122
- package/examples/auth-retry/README.md +384 -0
- package/examples/auth-retry/asyncapi.yaml +464 -0
- package/examples/auth-retry/login-flow-example.ts +375 -0
- package/package.json +3 -2
- package/template/helpers/security.js +137 -0
- package/template/src/client.ts.js +24 -2
- package/template/src/models.ts.js +157 -17
- package/template/src/runtime/auth/headers.ts.js +95 -0
- package/template/src/runtime/auth/index.ts.js +15 -0
- package/template/src/runtime/auth/types.ts.js +74 -0
- package/template/src/runtime/retry/index.ts.js +18 -0
- package/template/src/runtime/retry/manager.ts.js +150 -0
- package/template/src/runtime/retry/presets.ts.js +81 -0
- package/template/src/runtime/retry/types.ts.js +60 -0
- package/template/src/runtime/transports/http.ts.js +112 -17
- package/template/src/runtime/transports/websocket.ts.js +28 -3
- package/template/src/runtime/types.ts.js +12 -3
|
@@ -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
|
-
|
|
37
|
-
const
|
|
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
|
-
|
|
145
|
+
error.message = \`\${errorEnvelope.error.code}: \${errorEnvelope.error.message}\`;
|
|
71
146
|
}
|
|
72
147
|
} catch {
|
|
73
|
-
//
|
|
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
|
-
|
|
167
|
+
const timeoutError = new TimeoutError(\`Request timeout after \${timeout}ms\`);
|
|
168
|
+
(timeoutError as any).code = 'TIMEOUT';
|
|
169
|
+
throw timeoutError;
|
|
92
170
|
}
|
|
93
171
|
|
|
94
|
-
//
|
|
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
|
-
|
|
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;
|
|
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
|
+
};
|