@ioka-technologies/asyncapi-ts-client-template 0.0.12 → 0.0.18
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/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ioka-technologies/asyncapi-ts-client-template",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.18",
|
|
4
4
|
"description": "TypeScript AsyncAPI client generator template compatible with rust-asyncapi patterns",
|
|
5
5
|
"main": "template/index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"test": "npm run test:generate",
|
|
8
|
-
"test:generate": "asyncapi generate fromTemplate examples/auth-retry/asyncapi.yaml . -o test-output-auth-retry --force-write
|
|
9
|
-
"test:realworld": "asyncapi generate fromTemplate ../examples/realworld/asyncapi.yaml . -o test-output-realworld --force-write
|
|
10
|
-
"test:simple": "asyncapi generate fromTemplate ../examples/simple/asyncapi.yaml . -o test-output-simple --force-write
|
|
8
|
+
"test:generate": "asyncapi generate fromTemplate examples/auth-retry/asyncapi.yaml . -o test-output-auth-retry --force-write && cd test-output-auth-retry && npm install && npm run build",
|
|
9
|
+
"test:realworld": "asyncapi generate fromTemplate ../examples/realworld/asyncapi.yaml . -o test-output-realworld --force-write",
|
|
10
|
+
"test:simple": "asyncapi generate fromTemplate ../examples/simple/asyncapi.yaml . -o test-output-simple --force-write",
|
|
11
11
|
"clean": "rm -rf test-output-*"
|
|
12
12
|
},
|
|
13
13
|
"keywords": [
|
|
@@ -60,11 +60,6 @@
|
|
|
60
60
|
"default": "Apache-2.0",
|
|
61
61
|
"required": false
|
|
62
62
|
},
|
|
63
|
-
"enableAuth": {
|
|
64
|
-
"description": "Enable authentication middleware",
|
|
65
|
-
"default": true,
|
|
66
|
-
"required": false
|
|
67
|
-
},
|
|
68
63
|
"transports": {
|
|
69
64
|
"description": "Comma-separated list of transports to include",
|
|
70
65
|
"default": "websocket,http",
|
|
@@ -2,6 +2,99 @@
|
|
|
2
2
|
import { File } from '@asyncapi/generator-react-sdk';
|
|
3
3
|
import { operationRequiresAuth, extractOperationSecurityMap } from '../helpers/security.js';
|
|
4
4
|
|
|
5
|
+
// Helper functions for dynamic channels
|
|
6
|
+
function isDynamicChannel(address) {
|
|
7
|
+
if (!address || typeof address !== 'string') return false;
|
|
8
|
+
return /\{[^}]+\}/.test(address);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function extractChannelVariables(address) {
|
|
12
|
+
if (!address || typeof address !== 'string') return [];
|
|
13
|
+
const matches = address.match(/\{([^}]+)\}/g);
|
|
14
|
+
if (!matches) return [];
|
|
15
|
+
return matches.map(match => match.slice(1, -1)); // Remove { and }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getChannelParameters(channel) {
|
|
19
|
+
try {
|
|
20
|
+
const parameters = [];
|
|
21
|
+
|
|
22
|
+
// Try to get parameters from the channel
|
|
23
|
+
let channelParams = null;
|
|
24
|
+
if (channel.parameters && typeof channel.parameters === 'function') {
|
|
25
|
+
channelParams = channel.parameters();
|
|
26
|
+
} else if (channel.parameters) {
|
|
27
|
+
channelParams = channel.parameters;
|
|
28
|
+
} else if (channel._json && channel._json.parameters) {
|
|
29
|
+
channelParams = channel._json.parameters;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (channelParams) {
|
|
33
|
+
// Handle different parameter formats
|
|
34
|
+
if (typeof channelParams === 'object') {
|
|
35
|
+
for (const [paramName, paramDef] of Object.entries(channelParams)) {
|
|
36
|
+
// Skip internal AsyncAPI parser properties
|
|
37
|
+
if (paramName.startsWith('_') || paramName === 'collections' || paramName === 'meta') {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let description = 'Channel parameter';
|
|
42
|
+
|
|
43
|
+
if (paramDef && typeof paramDef === 'object') {
|
|
44
|
+
if (typeof paramDef.description === 'string') {
|
|
45
|
+
description = paramDef.description;
|
|
46
|
+
} else if (typeof paramDef.description === 'function') {
|
|
47
|
+
try {
|
|
48
|
+
description = paramDef.description();
|
|
49
|
+
} catch (e) {
|
|
50
|
+
description = 'Channel parameter';
|
|
51
|
+
}
|
|
52
|
+
} else if (paramDef._json && paramDef._json.description) {
|
|
53
|
+
description = paramDef._json.description;
|
|
54
|
+
}
|
|
55
|
+
} else if (typeof paramDef === 'string') {
|
|
56
|
+
description = paramDef;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Create a valid TypeScript identifier
|
|
60
|
+
let tsName = paramName.replace(/[^a-zA-Z0-9]/g, '_');
|
|
61
|
+
if (/^[0-9]/.test(tsName)) {
|
|
62
|
+
tsName = 'param_' + tsName;
|
|
63
|
+
}
|
|
64
|
+
if (!tsName || tsName === '_') {
|
|
65
|
+
tsName = 'param';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
parameters.push({
|
|
69
|
+
name: paramName,
|
|
70
|
+
description: description,
|
|
71
|
+
tsName: tsName,
|
|
72
|
+
tsType: 'string' // For now, assume all parameters are strings
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return parameters;
|
|
79
|
+
} catch (e) {
|
|
80
|
+
console.warn('Error extracting channel parameters:', e.message);
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function resolveChannelAddress(address, variables) {
|
|
86
|
+
if (!address || typeof address !== 'string') return address;
|
|
87
|
+
if (!variables || typeof variables !== 'object') return address;
|
|
88
|
+
|
|
89
|
+
let resolved = address;
|
|
90
|
+
for (const [varName, varValue] of Object.entries(variables)) {
|
|
91
|
+
const placeholder = `{${varName}}`;
|
|
92
|
+
resolved = resolved.replace(new RegExp(placeholder.replace(/[{}]/g, '\\$&'), 'g'), varValue);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return resolved;
|
|
96
|
+
}
|
|
97
|
+
|
|
5
98
|
function generateClient(asyncapi, clientName) {
|
|
6
99
|
// Method name sanitization function
|
|
7
100
|
function sanitizeMethodName(operationId) {
|
|
@@ -95,6 +188,83 @@ export class ${clientName} {
|
|
|
95
188
|
// Generated operation methods
|
|
96
189
|
`;
|
|
97
190
|
|
|
191
|
+
// Extract channels and their operations for dynamic channel support
|
|
192
|
+
const channelServices = [];
|
|
193
|
+
if (asyncapi.channels) {
|
|
194
|
+
const channels = asyncapi.channels();
|
|
195
|
+
if (channels) {
|
|
196
|
+
for (const [channelKey, channel] of Object.entries(channels)) {
|
|
197
|
+
try {
|
|
198
|
+
const channelName = channel.id ? channel.id() : channelKey;
|
|
199
|
+
let channelAddress = null;
|
|
200
|
+
if (channel.address && typeof channel.address === 'function') {
|
|
201
|
+
channelAddress = channel.address();
|
|
202
|
+
} else if (channel.address) {
|
|
203
|
+
channelAddress = channel.address;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (channelAddress) {
|
|
207
|
+
const isDynamic = isDynamicChannel(channelAddress);
|
|
208
|
+
const parameters = isDynamic ? getChannelParameters(channel) : [];
|
|
209
|
+
|
|
210
|
+
channelServices.push({
|
|
211
|
+
channelName,
|
|
212
|
+
channelAddress,
|
|
213
|
+
isDynamic,
|
|
214
|
+
parameters,
|
|
215
|
+
channel
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
} catch (e) {
|
|
219
|
+
console.warn(`Error processing channel ${channelKey}:`, e.message);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// We'll generate channel services after processing operations
|
|
226
|
+
|
|
227
|
+
// Generate client accessor methods for dynamic channels
|
|
228
|
+
const dynamicChannelServices = channelServices.filter(cs => cs.isDynamic);
|
|
229
|
+
if (dynamicChannelServices.length > 0) {
|
|
230
|
+
dynamicChannelServices.forEach(channelService => {
|
|
231
|
+
const serviceName = `${channelService.channelName.charAt(0).toUpperCase() + channelService.channelName.slice(1)}Service`;
|
|
232
|
+
|
|
233
|
+
// Extract variables from the channel address to get the actual parameter names
|
|
234
|
+
const variables = extractChannelVariables(channelService.channelAddress);
|
|
235
|
+
const actualParams = variables.map(varName => {
|
|
236
|
+
const existingParam = channelService.parameters.find(p => p.name === varName);
|
|
237
|
+
return {
|
|
238
|
+
name: varName,
|
|
239
|
+
tsName: varName.replace(/[^a-zA-Z0-9]/g, '_'),
|
|
240
|
+
tsType: 'string',
|
|
241
|
+
description: existingParam?.description || `${varName} parameter`
|
|
242
|
+
};
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const paramInterface = actualParams.map(p => `${p.tsName}: ${p.tsType}`).join(', ');
|
|
246
|
+
const methodName = channelService.channelName.replace(/[^a-zA-Z0-9]/g, '');
|
|
247
|
+
|
|
248
|
+
content += `
|
|
249
|
+
/**
|
|
250
|
+
* Access ${channelService.channelName} channel operations with parameters
|
|
251
|
+
*
|
|
252
|
+
* Returns a service instance configured for the specific channel parameters.
|
|
253
|
+
*
|
|
254
|
+
* @param ${actualParams.map(p => `${p.tsName} ${p.description}`).join('\n * @param ')}
|
|
255
|
+
* @returns ${serviceName} instance for the resolved channel
|
|
256
|
+
*
|
|
257
|
+
* @example
|
|
258
|
+
* const service = client.${methodName}(${actualParams.map(p => `'${p.name.replace('_', '-')}'`).join(', ')});
|
|
259
|
+
* await service.someOperation(payload);
|
|
260
|
+
*/
|
|
261
|
+
${methodName}(${paramInterface}): ${serviceName} {
|
|
262
|
+
return new ${serviceName}(this.transport, ${actualParams.map(p => p.tsName).join(', ')});
|
|
263
|
+
}
|
|
264
|
+
`;
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
98
268
|
// Generate methods for each operation (AsyncAPI v3.0.0)
|
|
99
269
|
if (asyncapi.operations) {
|
|
100
270
|
const operations = asyncapi.operations();
|
|
@@ -238,6 +408,12 @@ export class ${clientName} {
|
|
|
238
408
|
}
|
|
239
409
|
|
|
240
410
|
if (channelAddress) {
|
|
411
|
+
// Skip operations for dynamic channels - they'll be handled by channel services
|
|
412
|
+
const channelService = channelServices.find(cs => cs.channelAddress === channelAddress);
|
|
413
|
+
if (channelService && channelService.isDynamic) {
|
|
414
|
+
return; // Skip this operation for the main client
|
|
415
|
+
}
|
|
416
|
+
|
|
241
417
|
if (action === 'send') {
|
|
242
418
|
// Check if this is a request/response pattern
|
|
243
419
|
let hasReply = false;
|
|
@@ -336,6 +512,27 @@ export class ${clientName} {
|
|
|
336
512
|
await this.transport.send('${channelAddress}', envelope, options);
|
|
337
513
|
}
|
|
338
514
|
`;
|
|
515
|
+
|
|
516
|
+
// Also generate a subscription method for notification-type operations
|
|
517
|
+
// (send operations without replies are typically notifications/events)
|
|
518
|
+
const subscribeMethodName = `on${methodName.charAt(0).toUpperCase() + methodName.slice(1)}`;
|
|
519
|
+
content += `
|
|
520
|
+
/**
|
|
521
|
+
* ${subscribeMethodName} - Subscribe to ${operationId} notifications
|
|
522
|
+
* Original operation: ${operationId}
|
|
523
|
+
* Channel: ${channelAddress}
|
|
524
|
+
* @param callback Function to call when a notification is received
|
|
525
|
+
* @returns Unsubscribe function to stop listening for notifications
|
|
526
|
+
*/
|
|
527
|
+
${subscribeMethodName}(callback: (payload: ${payloadType}) => void): () => void {
|
|
528
|
+
return this.transport.subscribe('${channelAddress}', '${operationId}', (envelope: MessageEnvelope) => {
|
|
529
|
+
// Filter by operation to ensure we only handle messages for this specific operation
|
|
530
|
+
if (envelope.operation === '${operationId}') {
|
|
531
|
+
callback(envelope.payload);
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
`;
|
|
339
536
|
}
|
|
340
537
|
} else if (action === 'receive') {
|
|
341
538
|
// Sanitize the method name
|
|
@@ -352,7 +549,7 @@ export class ${clientName} {
|
|
|
352
549
|
* @returns Unsubscribe function to stop listening for messages
|
|
353
550
|
*/
|
|
354
551
|
${methodName}(callback: (payload: ${payloadType}) => void): () => void {
|
|
355
|
-
return this.transport.subscribe('${channelAddress}', (envelope: MessageEnvelope) => {
|
|
552
|
+
return this.transport.subscribe('${channelAddress}', '${operationId}', (envelope: MessageEnvelope) => {
|
|
356
553
|
// Filter by operation to ensure we only handle messages for this specific operation
|
|
357
554
|
if (envelope.operation === '${operationId}') {
|
|
358
555
|
callback(envelope.payload);
|
|
@@ -371,7 +568,238 @@ export class ${clientName} {
|
|
|
371
568
|
|
|
372
569
|
content += `
|
|
373
570
|
}
|
|
571
|
+
`;
|
|
572
|
+
|
|
573
|
+
// Generate channel service classes for dynamic channels
|
|
574
|
+
if (dynamicChannelServices.length > 0) {
|
|
575
|
+
dynamicChannelServices.forEach(channelService => {
|
|
576
|
+
const serviceName = `${channelService.channelName.charAt(0).toUpperCase() + channelService.channelName.slice(1)}Service`;
|
|
577
|
+
|
|
578
|
+
// Extract variables from the channel address to get the actual parameter names
|
|
579
|
+
const variables = extractChannelVariables(channelService.channelAddress);
|
|
580
|
+
const actualParams = variables.map(varName => {
|
|
581
|
+
const existingParam = channelService.parameters.find(p => p.name === varName);
|
|
582
|
+
return {
|
|
583
|
+
name: varName,
|
|
584
|
+
tsName: varName.replace(/[^a-zA-Z0-9]/g, '_'),
|
|
585
|
+
tsType: 'string',
|
|
586
|
+
description: existingParam?.description || `${varName} parameter`
|
|
587
|
+
};
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
const paramInterface = actualParams.map(p => `${p.tsName}: ${p.tsType}`).join(', ');
|
|
591
|
+
|
|
592
|
+
content += `
|
|
593
|
+
/**
|
|
594
|
+
* ${channelService.channelName} channel service with resolved parameters
|
|
595
|
+
*
|
|
596
|
+
* This service provides access to operations on the ${channelService.channelName} channel
|
|
597
|
+
* with resolved channel parameters for dynamic routing.
|
|
598
|
+
*/
|
|
599
|
+
export class ${serviceName} {
|
|
600
|
+
private transport: Transport;
|
|
601
|
+
private resolvedChannel: string;
|
|
602
|
+
|
|
603
|
+
constructor(transport: Transport, ${paramInterface}) {
|
|
604
|
+
this.transport = transport;
|
|
605
|
+
this.resolvedChannel = '${channelService.channelAddress}'${actualParams.map(p => `
|
|
606
|
+
.replace('{${p.name}}', ${p.tsName})`).join('')};
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Get the resolved channel address for this service
|
|
611
|
+
*/
|
|
612
|
+
getChannel(): string {
|
|
613
|
+
return this.resolvedChannel;
|
|
614
|
+
}
|
|
615
|
+
`;
|
|
616
|
+
|
|
617
|
+
// Add operations for this dynamic channel
|
|
618
|
+
if (asyncapi.operations) {
|
|
619
|
+
const operations = asyncapi.operations();
|
|
620
|
+
if (operations) {
|
|
621
|
+
const operationArray = operations.all ? operations.all() : Object.values(operations);
|
|
622
|
+
operationArray.forEach((operation) => {
|
|
623
|
+
let operationId = null;
|
|
624
|
+
if (operation._meta && operation._meta.id) {
|
|
625
|
+
operationId = operation._meta.id;
|
|
626
|
+
} else if (operation.id && typeof operation.id === 'function') {
|
|
627
|
+
operationId = operation.id();
|
|
628
|
+
} else if (operation.id) {
|
|
629
|
+
operationId = operation.id;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
if (!operationId) return;
|
|
633
|
+
|
|
634
|
+
try {
|
|
635
|
+
// Check if this operation belongs to this dynamic channel
|
|
636
|
+
let channelAddress = null;
|
|
637
|
+
const embeddedChannel = operation._json && operation._json.channel;
|
|
638
|
+
|
|
639
|
+
if (embeddedChannel) {
|
|
640
|
+
const embeddedChannelId = embeddedChannel['x-parser-unique-object-id'];
|
|
641
|
+
if (embeddedChannelId) {
|
|
642
|
+
const channels = asyncapi.channels();
|
|
643
|
+
for (const [channelKey, channel] of Object.entries(channels || {})) {
|
|
644
|
+
if (channel.id && channel.id() === embeddedChannelId) {
|
|
645
|
+
if (channel.address && typeof channel.address === 'function') {
|
|
646
|
+
channelAddress = channel.address();
|
|
647
|
+
} else if (channel.address) {
|
|
648
|
+
channelAddress = channel.address;
|
|
649
|
+
}
|
|
650
|
+
break;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
if (channelAddress === channelService.channelAddress) {
|
|
657
|
+
// This operation belongs to this dynamic channel
|
|
658
|
+
let action = 'send';
|
|
659
|
+
if (operation.action && typeof operation.action === 'function') {
|
|
660
|
+
action = operation.action();
|
|
661
|
+
} else if (operation.action) {
|
|
662
|
+
action = operation.action;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Get message types
|
|
666
|
+
let messageTypes = [];
|
|
667
|
+
if (operation.messages && typeof operation.messages === 'function') {
|
|
668
|
+
const messages = operation.messages();
|
|
669
|
+
if (messages && typeof messages.all === 'function') {
|
|
670
|
+
const messageArray = messages.all();
|
|
671
|
+
messageTypes = messageArray.map(msg => {
|
|
672
|
+
if (msg.name && typeof msg.name === 'function') {
|
|
673
|
+
return msg.name();
|
|
674
|
+
} else if (msg.name) {
|
|
675
|
+
return msg.name;
|
|
676
|
+
}
|
|
677
|
+
return null;
|
|
678
|
+
}).filter(Boolean);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const methodName = sanitizeMethodName(operationId);
|
|
683
|
+
|
|
684
|
+
if (action === 'send') {
|
|
685
|
+
// Check for reply
|
|
686
|
+
let hasReply = false;
|
|
687
|
+
let replyMessageTypes = [];
|
|
688
|
+
|
|
689
|
+
if (operation.reply && typeof operation.reply === 'function') {
|
|
690
|
+
const reply = operation.reply();
|
|
691
|
+
if (reply) {
|
|
692
|
+
hasReply = true;
|
|
693
|
+
if (reply.messages && typeof reply.messages === 'function') {
|
|
694
|
+
const replyMessages = reply.messages();
|
|
695
|
+
if (replyMessages && typeof replyMessages.all === 'function') {
|
|
696
|
+
const messageArray = replyMessages.all();
|
|
697
|
+
replyMessageTypes = messageArray.map(msg => {
|
|
698
|
+
if (msg.name && typeof msg.name === 'function') {
|
|
699
|
+
return msg.name();
|
|
700
|
+
} else if (msg.name) {
|
|
701
|
+
return msg.name;
|
|
702
|
+
}
|
|
703
|
+
return null;
|
|
704
|
+
}).filter(Boolean);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
if (hasReply) {
|
|
711
|
+
const requestPayloadType = messageTypes.length > 0 ? `Models.${messageTypes[0]}Payload` : 'any';
|
|
712
|
+
const responseType = replyMessageTypes.length > 0 ? `Models.${replyMessageTypes[0]}Payload` : 'any';
|
|
713
|
+
|
|
714
|
+
content += `
|
|
715
|
+
/**
|
|
716
|
+
* ${methodName} - Request/Response operation
|
|
717
|
+
* Original operation: ${operationId}
|
|
718
|
+
* @param payload Request payload
|
|
719
|
+
* @param options Request options
|
|
720
|
+
* @returns Promise that resolves with the response
|
|
721
|
+
*/
|
|
722
|
+
async ${methodName}(payload: ${requestPayloadType}, options?: RequestOptions): Promise<${responseType}> {
|
|
723
|
+
const envelope: MessageEnvelope = {
|
|
724
|
+
operation: '${operationId}',
|
|
725
|
+
payload,
|
|
726
|
+
channel: this.resolvedChannel
|
|
727
|
+
};
|
|
728
|
+
return this.transport.send(this.resolvedChannel, envelope, options);
|
|
729
|
+
}
|
|
730
|
+
`;
|
|
731
|
+
} else {
|
|
732
|
+
const payloadType = messageTypes.length > 0 ? `Models.${messageTypes[0]}Payload` : 'any';
|
|
733
|
+
content += `
|
|
734
|
+
/**
|
|
735
|
+
* ${methodName} - Send operation (fire and forget)
|
|
736
|
+
* Original operation: ${operationId}
|
|
737
|
+
*/
|
|
738
|
+
async ${methodName}(payload: ${payloadType}, options?: RequestOptions): Promise<void> {
|
|
739
|
+
const envelope: MessageEnvelope = {
|
|
740
|
+
operation: '${operationId}',
|
|
741
|
+
payload,
|
|
742
|
+
channel: this.resolvedChannel
|
|
743
|
+
};
|
|
744
|
+
await this.transport.send(this.resolvedChannel, envelope, options);
|
|
745
|
+
}
|
|
746
|
+
`;
|
|
747
|
+
|
|
748
|
+
// Also generate a subscription method for notification-type operations
|
|
749
|
+
// (send operations without replies are typically notifications/events)
|
|
750
|
+
const subscribeMethodName = `on${methodName.charAt(0).toUpperCase() + methodName.slice(1)}`;
|
|
751
|
+
content += `
|
|
752
|
+
/**
|
|
753
|
+
* ${subscribeMethodName} - Subscribe to ${operationId} notifications
|
|
754
|
+
* Original operation: ${operationId}
|
|
755
|
+
* @param callback Function to call when a notification is received
|
|
756
|
+
* @returns Unsubscribe function to stop listening for notifications
|
|
757
|
+
*/
|
|
758
|
+
${subscribeMethodName}(callback: (payload: ${payloadType}) => void): () => void {
|
|
759
|
+
return this.transport.subscribe(this.resolvedChannel, '${operationId}', (envelope: MessageEnvelope) => {
|
|
760
|
+
// Filter by operation to ensure we only handle messages for this specific operation
|
|
761
|
+
if (envelope.operation === '${operationId}') {
|
|
762
|
+
callback(envelope.payload);
|
|
763
|
+
}
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
`;
|
|
767
|
+
}
|
|
768
|
+
} else if (action === 'receive') {
|
|
769
|
+
// Generate receive method (event listener setup) for dynamic channels
|
|
770
|
+
const payloadType = messageTypes.length > 0 ? `Models.${messageTypes[0]}Payload` : 'any';
|
|
771
|
+
content += `
|
|
772
|
+
/**
|
|
773
|
+
* ${methodName} - Receive operation
|
|
774
|
+
* Original operation: ${operationId}
|
|
775
|
+
* @param callback Function to call when a message is received
|
|
776
|
+
* @returns Unsubscribe function to stop listening for messages
|
|
777
|
+
*/
|
|
778
|
+
${methodName}(callback: (payload: ${payloadType}) => void): () => void {
|
|
779
|
+
return this.transport.subscribe(this.resolvedChannel, '${operationId}', (envelope: MessageEnvelope) => {
|
|
780
|
+
// Filter by operation to ensure we only handle messages for this specific operation
|
|
781
|
+
if (envelope.operation === '${operationId}') {
|
|
782
|
+
callback(envelope.payload);
|
|
783
|
+
}
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
`;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
} catch (error) {
|
|
790
|
+
// Skip operations that can't be processed
|
|
791
|
+
}
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
content += `
|
|
797
|
+
}
|
|
798
|
+
`;
|
|
799
|
+
});
|
|
800
|
+
}
|
|
374
801
|
|
|
802
|
+
content += `
|
|
375
803
|
export default ${clientName};
|
|
376
804
|
`;
|
|
377
805
|
|
|
@@ -13,7 +13,7 @@ function generateModels(asyncapi) {
|
|
|
13
13
|
.replace(/_+/g, '_')
|
|
14
14
|
.replace(/^_+|_+$/g, '');
|
|
15
15
|
if (/^[0-9]/.test(identifier)) {
|
|
16
|
-
identifier = '
|
|
16
|
+
identifier = 'item_' + identifier;
|
|
17
17
|
}
|
|
18
18
|
if (!identifier) {
|
|
19
19
|
identifier = 'unknown';
|
|
@@ -21,6 +21,7 @@ function generateModels(asyncapi) {
|
|
|
21
21
|
return identifier;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
|
|
24
25
|
function toTypeScriptTypeName(str) {
|
|
25
26
|
if (!str) return 'Unknown';
|
|
26
27
|
// Handle camelCase and PascalCase properly
|
|
@@ -195,10 +195,10 @@ export class HttpTransport implements Transport {
|
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
-
subscribe(channel: string, callback: (envelope: MessageEnvelope) => void): () => void {
|
|
198
|
+
subscribe(channel: string, operation: string, callback: (envelope: MessageEnvelope) => void): () => void {
|
|
199
199
|
// HTTP transport doesn't support real-time subscriptions
|
|
200
200
|
// This is a placeholder implementation that logs a warning
|
|
201
|
-
console.warn(\`HTTP transport does not support subscriptions.
|
|
201
|
+
console.warn(\`HTTP transport does not support subscriptions. Operation '\${operation}' on channel '\${channel}' subscription ignored.\`);
|
|
202
202
|
console.warn('Consider using WebSocket transport for real-time message subscriptions.');
|
|
203
203
|
|
|
204
204
|
// Return a no-op unsubscribe function
|
|
@@ -52,6 +52,7 @@ export class WebSocketTransport implements Transport {
|
|
|
52
52
|
private responseHandlers: Map<string, ResponseHandler> = new Map();
|
|
53
53
|
private subscriptions: Map<string, Set<EnvelopeCallback>> = new Map();
|
|
54
54
|
private operationSubscriptions: Map<string, Set<(payload: any) => void>> = new Map();
|
|
55
|
+
private channelOperations: Map<string, string> = new Map(); // Track operation for each channel
|
|
55
56
|
private reconnectAttempts = 0;
|
|
56
57
|
private maxReconnectAttempts = 5;
|
|
57
58
|
private reconnectDelay = 1000;
|
|
@@ -162,22 +163,44 @@ export class WebSocketTransport implements Transport {
|
|
|
162
163
|
});
|
|
163
164
|
}
|
|
164
165
|
|
|
165
|
-
subscribe(channel: string, callback: (envelope: MessageEnvelope) => void): () => void {
|
|
166
|
+
subscribe(channel: string, operation: string, callback: (envelope: MessageEnvelope) => void): () => void {
|
|
167
|
+
// When an operation is provided, use operation-based routing to extract the payload
|
|
168
|
+
if (operation) {
|
|
169
|
+
// Create a wrapper that extracts the payload from the envelope
|
|
170
|
+
const payloadCallback = (payload: any) => {
|
|
171
|
+
// Reconstruct the envelope for backward compatibility
|
|
172
|
+
const envelope: MessageEnvelope = {
|
|
173
|
+
operation: operation,
|
|
174
|
+
channel: channel,
|
|
175
|
+
payload: payload,
|
|
176
|
+
timestamp: new Date().toISOString()
|
|
177
|
+
};
|
|
178
|
+
callback(envelope);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// Use operation-based subscription which correctly extracts the payload
|
|
182
|
+
return this.subscribeToOperation(channel, operation, payloadCallback);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Fallback to channel-based subscription for backward compatibility
|
|
166
186
|
if (!this.subscriptions.has(channel)) {
|
|
167
187
|
this.subscriptions.set(channel, new Set());
|
|
168
188
|
}
|
|
169
189
|
|
|
170
190
|
this.subscriptions.get(channel)!.add(callback);
|
|
171
191
|
|
|
192
|
+
// Track the operation for this channel for reconnection purposes
|
|
193
|
+
this.channelOperations.set(channel, operation);
|
|
194
|
+
|
|
172
195
|
// Send subscription message to server using envelope format
|
|
173
196
|
if (this.ws && this.ws.readyState === this.ws.OPEN) {
|
|
174
197
|
// Generate auth headers if credentials are available
|
|
175
198
|
const authHeaders = this.config.auth ? generateAuthHeaders(this.config.auth) : {};
|
|
176
199
|
|
|
177
200
|
const subscribeEnvelope: MessageEnvelope = {
|
|
178
|
-
operation:
|
|
201
|
+
operation: operation, // ✅ Use the actual operation name
|
|
179
202
|
channel,
|
|
180
|
-
payload: { channel },
|
|
203
|
+
payload: { channel, operation },
|
|
181
204
|
timestamp: new Date().toISOString(),
|
|
182
205
|
headers: authHeaders
|
|
183
206
|
};
|
|
@@ -200,11 +223,13 @@ export class WebSocketTransport implements Transport {
|
|
|
200
223
|
channelSubscriptions.delete(callback);
|
|
201
224
|
if (channelSubscriptions.size === 0) {
|
|
202
225
|
this.subscriptions.delete(channel);
|
|
226
|
+
this.channelOperations.delete(channel); // Clean up operation tracking
|
|
203
227
|
this.sendUnsubscribeMessage(channel);
|
|
204
228
|
}
|
|
205
229
|
} else {
|
|
206
230
|
// Unsubscribe all callbacks for this channel
|
|
207
231
|
this.subscriptions.delete(channel);
|
|
232
|
+
this.channelOperations.delete(channel); // Clean up operation tracking
|
|
208
233
|
this.sendUnsubscribeMessage(channel);
|
|
209
234
|
}
|
|
210
235
|
}
|
|
@@ -222,11 +247,28 @@ export class WebSocketTransport implements Transport {
|
|
|
222
247
|
|
|
223
248
|
this.operationSubscriptions.get(operationKey)!.add(callback);
|
|
224
249
|
|
|
225
|
-
// Subscribe to the channel if not already subscribed
|
|
250
|
+
// Subscribe to the channel if not already subscribed (use direct channel subscription to avoid circular call)
|
|
226
251
|
if (!this.subscriptions.has(channel)) {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
252
|
+
// Direct channel subscription without going through the subscribe method
|
|
253
|
+
this.subscriptions.set(channel, new Set());
|
|
254
|
+
|
|
255
|
+
// Track the operation for this channel for reconnection purposes
|
|
256
|
+
this.channelOperations.set(channel, operation);
|
|
257
|
+
|
|
258
|
+
// Send subscription message to server using envelope format
|
|
259
|
+
if (this.ws && this.ws.readyState === this.ws.OPEN) {
|
|
260
|
+
// Generate auth headers if credentials are available
|
|
261
|
+
const authHeaders = this.config.auth ? generateAuthHeaders(this.config.auth) : {};
|
|
262
|
+
|
|
263
|
+
const subscribeEnvelope: MessageEnvelope = {
|
|
264
|
+
operation: operation,
|
|
265
|
+
channel,
|
|
266
|
+
payload: { channel, operation },
|
|
267
|
+
timestamp: new Date().toISOString(),
|
|
268
|
+
headers: authHeaders
|
|
269
|
+
};
|
|
270
|
+
this.ws.send(JSON.stringify(subscribeEnvelope));
|
|
271
|
+
}
|
|
230
272
|
}
|
|
231
273
|
|
|
232
274
|
// Return unsubscribe function
|
|
@@ -275,21 +317,22 @@ export class WebSocketTransport implements Transport {
|
|
|
275
317
|
try {
|
|
276
318
|
const envelope: MessageEnvelope = JSON.parse(data);
|
|
277
319
|
|
|
278
|
-
//
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
320
|
+
// Check if this is actually a response to a pending request
|
|
321
|
+
const isResponse = envelope.id && this.responseHandlers.has(envelope.id);
|
|
322
|
+
|
|
323
|
+
if (isResponse) {
|
|
324
|
+
// Handle as response message
|
|
325
|
+
const handler = this.responseHandlers.get(envelope.id!)!;
|
|
326
|
+
this.responseHandlers.delete(envelope.id!);
|
|
327
|
+
if (envelope.error) {
|
|
328
|
+
handler.reject(new TransportError(\`\${envelope.error.code}: \${envelope.error.message}\`));
|
|
329
|
+
} else {
|
|
330
|
+
handler.resolve(envelope.payload);
|
|
288
331
|
}
|
|
289
332
|
return;
|
|
290
333
|
}
|
|
291
334
|
|
|
292
|
-
// Handle subscription
|
|
335
|
+
// Handle as subscription message (even if it has an ID)
|
|
293
336
|
if (envelope.channel) {
|
|
294
337
|
const channelSubscriptions = this.subscriptions.get(envelope.channel);
|
|
295
338
|
if (channelSubscriptions) {
|
|
@@ -326,13 +369,21 @@ export class WebSocketTransport implements Transport {
|
|
|
326
369
|
|
|
327
370
|
private resubscribeAll(): void {
|
|
328
371
|
if (this.ws && this.ws.readyState === this.ws.OPEN) {
|
|
372
|
+
// Generate auth headers if credentials are available
|
|
373
|
+
const authHeaders = this.config.auth ? generateAuthHeaders(this.config.auth) : {};
|
|
374
|
+
|
|
329
375
|
for (const channel of this.subscriptions.keys()) {
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
376
|
+
const operation = this.channelOperations.get(channel);
|
|
377
|
+
if (operation) {
|
|
378
|
+
const subscribeEnvelope: MessageEnvelope = {
|
|
379
|
+
operation: operation,
|
|
380
|
+
channel,
|
|
381
|
+
payload: { channel, operation },
|
|
382
|
+
timestamp: new Date().toISOString(),
|
|
383
|
+
headers: authHeaders
|
|
384
|
+
};
|
|
385
|
+
this.ws.send(JSON.stringify(subscribeEnvelope));
|
|
386
|
+
}
|
|
336
387
|
}
|
|
337
388
|
}
|
|
338
389
|
}
|
|
@@ -30,7 +30,7 @@ export interface Transport {
|
|
|
30
30
|
connect(): Promise<void>;
|
|
31
31
|
disconnect(): Promise<void>;
|
|
32
32
|
send(channel: string, envelope: MessageEnvelope, options?: RequestOptions): Promise<any>;
|
|
33
|
-
subscribe(channel: string, callback: (envelope: MessageEnvelope) => void): () => void;
|
|
33
|
+
subscribe(channel: string, operation: string, callback: (envelope: MessageEnvelope) => void): () => void;
|
|
34
34
|
unsubscribe(channel: string, callback?: (envelope: MessageEnvelope) => void): void;
|
|
35
35
|
}
|
|
36
36
|
|