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

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,122 @@
1
+ /* eslint-disable no-unused-vars */
2
+ import { File } from '@asyncapi/generator-react-sdk';
3
+
4
+ module.exports = function ({ asyncapi, params }) {
5
+ const transports = (params.transports || 'websocket,http').split(',').map(t => t.trim());
6
+
7
+ if (!transports.includes('http')) {
8
+ return null;
9
+ }
10
+
11
+ return (
12
+ <File name="http.ts">
13
+ {`import { Transport, TransportConfig, RequestOptions, MessageEnvelope } from '../types';
14
+ import { TransportError, ConnectionError, TimeoutError } from '../errors';
15
+
16
+ export class HttpTransport implements Transport {
17
+ private config: TransportConfig;
18
+ private baseUrl: string;
19
+
20
+ constructor(config: TransportConfig) {
21
+ this.config = config;
22
+ this.baseUrl = config.url;
23
+ }
24
+
25
+ async connect(): Promise<void> {
26
+ // HTTP doesn't require explicit connection
27
+ return Promise.resolve();
28
+ }
29
+
30
+ async disconnect(): Promise<void> {
31
+ // HTTP doesn't require explicit disconnection
32
+ return Promise.resolve();
33
+ }
34
+
35
+ async send(channel: string, envelope: MessageEnvelope, options?: RequestOptions): Promise<any> {
36
+ const url = \`\${this.baseUrl}/\${channel}\`;
37
+ const timeout = options?.timeout || 30000;
38
+
39
+ // Prepare the complete envelope for HTTP transport
40
+ const messageEnvelope: MessageEnvelope = {
41
+ ...envelope,
42
+ channel: envelope.channel || channel,
43
+ timestamp: envelope.timestamp || new Date().toISOString(),
44
+ id: options?.correlationId || envelope.id
45
+ };
46
+
47
+ const controller = new AbortController();
48
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
49
+
50
+ try {
51
+ const response = await fetch(url, {
52
+ 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
+ },
59
+ body: JSON.stringify(messageEnvelope),
60
+ signal: controller.signal
61
+ });
62
+
63
+ clearTimeout(timeoutId);
64
+
65
+ if (!response.ok) {
66
+ // Try to parse error response as envelope
67
+ try {
68
+ const errorEnvelope: MessageEnvelope = await response.json();
69
+ if (errorEnvelope.error) {
70
+ throw new TransportError(\`\${errorEnvelope.error.code}: \${errorEnvelope.error.message}\`);
71
+ }
72
+ } catch {
73
+ // Fall back to HTTP status error
74
+ throw new TransportError(\`HTTP request failed: \${response.status} \${response.statusText}\`);
75
+ }
76
+ }
77
+
78
+ // Parse response as envelope
79
+ const responseEnvelope: MessageEnvelope = await response.json();
80
+
81
+ // Check for envelope-level errors
82
+ if (responseEnvelope.error) {
83
+ throw new TransportError(\`\${responseEnvelope.error.code}: \${responseEnvelope.error.message}\`);
84
+ }
85
+
86
+ return responseEnvelope.payload;
87
+ } catch (error: any) {
88
+ clearTimeout(timeoutId);
89
+
90
+ if (error.name === 'AbortError') {
91
+ throw new TimeoutError(\`Request timeout after \${timeout}ms\`);
92
+ }
93
+
94
+ // Re-throw TransportError as-is
95
+ if (error instanceof TransportError) {
96
+ throw error;
97
+ }
98
+
99
+ throw new TransportError(\`HTTP request failed: \${error.message}\`);
100
+ }
101
+ }
102
+
103
+ subscribe(channel: string, callback: (envelope: MessageEnvelope) => void): () => void {
104
+ // HTTP transport doesn't support real-time subscriptions
105
+ // This is a placeholder implementation that logs a warning
106
+ console.warn(\`HTTP transport does not support subscriptions. Channel '\${channel}' subscription ignored.\`);
107
+ console.warn('Consider using WebSocket transport for real-time message subscriptions.');
108
+
109
+ // Return a no-op unsubscribe function
110
+ return () => {
111
+ // No-op for HTTP transport
112
+ };
113
+ }
114
+
115
+ unsubscribe(channel: string, callback?: (envelope: MessageEnvelope) => void): void {
116
+ // HTTP transport doesn't support subscriptions, so nothing to unsubscribe
117
+ console.warn(\`HTTP transport does not support subscriptions. Channel '\${channel}' unsubscribe ignored.\`);
118
+ }
119
+ }`}
120
+ </File>
121
+ );
122
+ }
@@ -0,0 +1,329 @@
1
+ /* eslint-disable no-unused-vars */
2
+ import { File } from '@asyncapi/generator-react-sdk';
3
+
4
+ module.exports = function ({ asyncapi, params }) {
5
+ const transports = (params.transports || 'websocket,http').split(',').map(t => t.trim());
6
+
7
+ if (!transports.includes('websocket')) {
8
+ return null;
9
+ }
10
+
11
+ return (
12
+ <File name="websocket.ts">
13
+ {`import { v4 as uuidv4 } from 'uuid';
14
+ import { Transport, TransportConfig, RequestOptions, ResponseHandler, MessageEnvelope, EnvelopeCallback } from '../types';
15
+ import { TransportError, ConnectionError, TimeoutError } from '../errors';
16
+
17
+ // Environment-aware WebSocket implementation
18
+ const getWebSocketImpl = (): typeof WebSocket => {
19
+ if (typeof window !== 'undefined' && window.WebSocket) {
20
+ // Browser environment - use native WebSocket
21
+ return window.WebSocket;
22
+ } else if (typeof global !== 'undefined') {
23
+ // Node.js environment - try to require ws
24
+ try {
25
+ return require('ws');
26
+ } catch (error) {
27
+ throw new Error('WebSocket implementation not available. In Node.js, please install the "ws" package: npm install ws');
28
+ }
29
+ } else {
30
+ throw new Error('WebSocket implementation not available in this environment');
31
+ }
32
+ };
33
+
34
+ // Type definition that works for both browser and Node.js WebSocket
35
+ type WebSocketLike = {
36
+ readonly readyState: number;
37
+ readonly OPEN: number;
38
+ send(data: string): void;
39
+ close(): void;
40
+ addEventListener?(type: string, listener: (event: any) => void): void;
41
+ removeEventListener?(type: string, listener: (event: any) => void): void;
42
+ on?(event: string, listener: (...args: any[]) => void): void;
43
+ off?(event: string, listener: (...args: any[]) => void): void;
44
+ };
45
+
46
+ export class WebSocketTransport implements Transport {
47
+ private ws: WebSocketLike | null = null;
48
+ private WebSocketImpl: typeof WebSocket;
49
+ private config: TransportConfig;
50
+ private responseHandlers: Map<string, ResponseHandler> = new Map();
51
+ private subscriptions: Map<string, Set<EnvelopeCallback>> = new Map();
52
+ private operationSubscriptions: Map<string, Set<(payload: any) => void>> = new Map();
53
+ private reconnectAttempts = 0;
54
+ private maxReconnectAttempts = 5;
55
+ private reconnectDelay = 1000;
56
+
57
+ constructor(config: TransportConfig) {
58
+ this.config = config;
59
+ this.WebSocketImpl = getWebSocketImpl();
60
+ }
61
+
62
+ async connect(): Promise<void> {
63
+ return new Promise((resolve, reject) => {
64
+ try {
65
+ this.ws = new this.WebSocketImpl(this.config.url) as WebSocketLike;
66
+
67
+ // Handle both browser and Node.js WebSocket APIs
68
+ if (typeof window !== 'undefined' && this.ws.addEventListener) {
69
+ // Browser WebSocket API
70
+ this.ws.addEventListener('open', () => {
71
+ this.reconnectAttempts = 0;
72
+ resolve();
73
+ });
74
+
75
+ this.ws.addEventListener('message', (event: MessageEvent) => {
76
+ this.handleMessage(event.data);
77
+ });
78
+
79
+ this.ws.addEventListener('close', () => {
80
+ this.handleDisconnect();
81
+ });
82
+
83
+ this.ws.addEventListener('error', (event: Event) => {
84
+ reject(new ConnectionError(\`WebSocket connection failed: \${event.type}\`));
85
+ });
86
+ } else if (this.ws.on) {
87
+ // Node.js ws library API
88
+ this.ws.on('open', () => {
89
+ this.reconnectAttempts = 0;
90
+ resolve();
91
+ });
92
+
93
+ this.ws.on('message', (data: any) => {
94
+ this.handleMessage(data.toString());
95
+ });
96
+
97
+ this.ws.on('close', () => {
98
+ this.handleDisconnect();
99
+ });
100
+
101
+ this.ws.on('error', (error: any) => {
102
+ reject(new ConnectionError(\`WebSocket connection failed: \${error.message}\`));
103
+ });
104
+ } else {
105
+ reject(new ConnectionError('WebSocket implementation does not support required event handling'));
106
+ }
107
+ } catch (error: any) {
108
+ reject(new ConnectionError(\`Failed to create WebSocket: \${error.message}\`));
109
+ }
110
+ });
111
+ }
112
+
113
+ async disconnect(): Promise<void> {
114
+ if (this.ws) {
115
+ this.ws.close();
116
+ this.ws = null;
117
+ }
118
+ }
119
+
120
+ async send(channel: string, envelope: MessageEnvelope, options?: RequestOptions): Promise<any> {
121
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN) {
122
+ throw new TransportError('WebSocket is not connected');
123
+ }
124
+
125
+ // Ensure envelope has required fields
126
+ const requestId = options?.correlationId || envelope.id || uuidv4();
127
+ const messageEnvelope: MessageEnvelope = {
128
+ ...envelope,
129
+ id: requestId,
130
+ channel: envelope.channel || channel,
131
+ timestamp: envelope.timestamp || new Date().toISOString()
132
+ };
133
+
134
+ return new Promise((resolve, reject) => {
135
+ const timeout = options?.timeout || 30000;
136
+ const timeoutId = setTimeout(() => {
137
+ this.responseHandlers.delete(requestId);
138
+ reject(new TimeoutError(\`Request timeout after \${timeout}ms\`));
139
+ }, timeout);
140
+
141
+ this.responseHandlers.set(requestId, {
142
+ resolve: (data) => {
143
+ clearTimeout(timeoutId);
144
+ resolve(data);
145
+ },
146
+ reject: (error) => {
147
+ clearTimeout(timeoutId);
148
+ reject(error);
149
+ }
150
+ });
151
+
152
+ this.ws!.send(JSON.stringify(messageEnvelope));
153
+ });
154
+ }
155
+
156
+ subscribe(channel: string, callback: (envelope: MessageEnvelope) => void): () => void {
157
+ if (!this.subscriptions.has(channel)) {
158
+ this.subscriptions.set(channel, new Set());
159
+ }
160
+
161
+ this.subscriptions.get(channel)!.add(callback);
162
+
163
+ // Send subscription message to server using envelope format
164
+ if (this.ws && this.ws.readyState === this.ws.OPEN) {
165
+ const subscribeEnvelope: MessageEnvelope = {
166
+ operation: 'subscribe',
167
+ channel,
168
+ payload: { channel },
169
+ timestamp: new Date().toISOString()
170
+ };
171
+ this.ws.send(JSON.stringify(subscribeEnvelope));
172
+ }
173
+
174
+ // Return unsubscribe function
175
+ return () => {
176
+ this.unsubscribe(channel, callback);
177
+ };
178
+ }
179
+
180
+ unsubscribe(channel: string, callback?: (envelope: MessageEnvelope) => void): void {
181
+ const channelSubscriptions = this.subscriptions.get(channel);
182
+ if (!channelSubscriptions) {
183
+ return;
184
+ }
185
+
186
+ if (callback) {
187
+ channelSubscriptions.delete(callback);
188
+ if (channelSubscriptions.size === 0) {
189
+ this.subscriptions.delete(channel);
190
+ this.sendUnsubscribeMessage(channel);
191
+ }
192
+ } else {
193
+ // Unsubscribe all callbacks for this channel
194
+ this.subscriptions.delete(channel);
195
+ this.sendUnsubscribeMessage(channel);
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Subscribe to a specific operation on a channel
201
+ * This provides operation-based filtering on the client side
202
+ */
203
+ subscribeToOperation(channel: string, operation: string, callback: (payload: any) => void): () => void {
204
+ const operationKey = \`\${channel}:\${operation}\`;
205
+
206
+ if (!this.operationSubscriptions.has(operationKey)) {
207
+ this.operationSubscriptions.set(operationKey, new Set());
208
+ }
209
+
210
+ this.operationSubscriptions.get(operationKey)!.add(callback);
211
+
212
+ // Subscribe to the channel if not already subscribed
213
+ if (!this.subscriptions.has(channel)) {
214
+ this.subscribe(channel, (envelope: MessageEnvelope) => {
215
+ this.handleOperationMessage(envelope);
216
+ });
217
+ }
218
+
219
+ // Return unsubscribe function
220
+ return () => {
221
+ const opCallbacks = this.operationSubscriptions.get(operationKey);
222
+ if (opCallbacks) {
223
+ opCallbacks.delete(callback);
224
+ if (opCallbacks.size === 0) {
225
+ this.operationSubscriptions.delete(operationKey);
226
+ }
227
+ }
228
+ };
229
+ }
230
+
231
+ private handleOperationMessage(envelope: MessageEnvelope): void {
232
+ if (!envelope.operation || !envelope.channel) {
233
+ return;
234
+ }
235
+
236
+ const operationKey = \`\${envelope.channel}:\${envelope.operation}\`;
237
+ const operationCallbacks = this.operationSubscriptions.get(operationKey);
238
+
239
+ if (operationCallbacks) {
240
+ operationCallbacks.forEach(callback => {
241
+ try {
242
+ callback(envelope.payload);
243
+ } catch (error) {
244
+ console.error(\`Error in operation callback for \${envelope.operation}:\`, error);
245
+ }
246
+ });
247
+ }
248
+ }
249
+
250
+ private sendUnsubscribeMessage(channel: string): void {
251
+ if (this.ws && this.ws.readyState === this.ws.OPEN) {
252
+ const unsubscribeMessage = {
253
+ type: 'unsubscribe',
254
+ channel,
255
+ timestamp: new Date().toISOString()
256
+ };
257
+ this.ws.send(JSON.stringify(unsubscribeMessage));
258
+ }
259
+ }
260
+
261
+ private handleMessage(data: string): void {
262
+ try {
263
+ const envelope: MessageEnvelope = JSON.parse(data);
264
+
265
+ // Handle response messages (with ID for request/response correlation)
266
+ if (envelope.id) {
267
+ const handler = this.responseHandlers.get(envelope.id);
268
+ if (handler) {
269
+ this.responseHandlers.delete(envelope.id);
270
+ if (envelope.error) {
271
+ handler.reject(new TransportError(\`\${envelope.error.code}: \${envelope.error.message}\`));
272
+ } else {
273
+ handler.resolve(envelope.payload);
274
+ }
275
+ }
276
+ return;
277
+ }
278
+
279
+ // Handle subscription messages (broadcast messages without correlation ID)
280
+ if (envelope.channel) {
281
+ const channelSubscriptions = this.subscriptions.get(envelope.channel);
282
+ if (channelSubscriptions) {
283
+ channelSubscriptions.forEach(callback => {
284
+ try {
285
+ callback(envelope);
286
+ } catch (error) {
287
+ console.error(\`Error in subscription callback for channel \${envelope.channel}:\`, error);
288
+ }
289
+ });
290
+ }
291
+
292
+ // Also handle operation-based subscriptions
293
+ this.handleOperationMessage(envelope);
294
+ }
295
+ } catch (error) {
296
+ console.error('Failed to parse WebSocket message:', error);
297
+ }
298
+ }
299
+
300
+ private handleDisconnect(): void {
301
+ if (this.reconnectAttempts < this.maxReconnectAttempts) {
302
+ this.reconnectAttempts++;
303
+ setTimeout(() => {
304
+ this.connect().then(() => {
305
+ // Re-subscribe to all channels after reconnection
306
+ this.resubscribeAll();
307
+ }).catch(() => {
308
+ // Failed to reconnect
309
+ });
310
+ }, this.reconnectDelay * this.reconnectAttempts);
311
+ }
312
+ }
313
+
314
+ private resubscribeAll(): void {
315
+ if (this.ws && this.ws.readyState === this.ws.OPEN) {
316
+ for (const channel of this.subscriptions.keys()) {
317
+ const subscribeMessage = {
318
+ type: 'subscribe',
319
+ channel,
320
+ timestamp: new Date().toISOString()
321
+ };
322
+ this.ws.send(JSON.stringify(subscribeMessage));
323
+ }
324
+ }
325
+ }
326
+ }`}
327
+ </File>
328
+ );
329
+ }
@@ -0,0 +1,75 @@
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
+ * Standard message envelope for all AsyncAPI communications
9
+ */
10
+ export interface MessageEnvelope {
11
+ operation: string; // AsyncAPI operation ID
12
+ id?: string; // Correlation ID for request/response
13
+ channel?: string; // Optional channel context
14
+ payload: any; // Message payload
15
+ timestamp?: string; // Optional ISO 8601 timestamp
16
+ error?: { // Error information
17
+ code: string;
18
+ message: string;
19
+ };
20
+ }
21
+
22
+ /**
23
+ * Transport interface for sending and receiving messages
24
+ */
25
+ export interface Transport {
26
+ connect(): Promise<void>;
27
+ disconnect(): Promise<void>;
28
+ send(channel: string, envelope: MessageEnvelope, options?: RequestOptions): Promise<any>;
29
+ subscribe(channel: string, callback: (envelope: MessageEnvelope) => void): () => void;
30
+ unsubscribe(channel: string, callback?: (envelope: MessageEnvelope) => void): void;
31
+ }
32
+
33
+ /**
34
+ * Configuration for transport implementations
35
+ */
36
+ export interface TransportConfig {
37
+ type: 'websocket' | 'http';
38
+ url: string;
39
+ headers?: Record<string, string>;
40
+ timeout?: number;
41
+ }
42
+
43
+ /**
44
+ * Options for individual requests
45
+ */
46
+ export interface RequestOptions {
47
+ timeout?: number;
48
+ correlationId?: string; // Override correlation ID
49
+ }
50
+
51
+ /**
52
+ * Internal response handler for request/response patterns
53
+ */
54
+ export interface ResponseHandler {
55
+ resolve: (data: any) => void;
56
+ reject: (error: Error) => void;
57
+ }
58
+
59
+ /**
60
+ * Callback function for message subscriptions
61
+ */
62
+ export type MessageCallback = (payload: any) => void;
63
+
64
+ /**
65
+ * Function to unsubscribe from a channel
66
+ */
67
+ export type UnsubscribeFunction = () => void;
68
+
69
+ /**
70
+ * Envelope callback for transport-level message handling
71
+ */
72
+ export type EnvelopeCallback = (envelope: MessageEnvelope) => void;`}
73
+ </File>
74
+ );
75
+ }