@ioka-technologies/asyncapi-ts-client-template 0.0.8 → 0.0.11
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 +113 -289
- 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/index.js +113 -122
- 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
- package/USAGE.md +0 -586
|
@@ -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
|
+
};
|
|
@@ -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
|
+
};
|