From d19c67373bad91490cf0787fbbc9bd1f21cd43cb Mon Sep 17 00:00:00 2001 From: Varshitha Besthavemula Date: Wed, 1 Jul 2026 18:12:43 +0530 Subject: [PATCH 1/4] feat: add query Parameters support to javascript websocket client --- .../src/components/QueryParamsVariables.js | 14 ++--- .../QueryParamsVariables.test.js.snap | 32 ++++++------ .../javascript/components/ClientClass.js | 4 +- .../javascript/components/Constructor.js | 51 +++++++++++++------ .../javascript/components/InitSignature.js | 24 +++++++++ .../components/QueryParamsArgumentsDocs.js | 18 +++++++ .../javascript/template/client.js.js | 13 +++-- .../test/components/InitSignature.test.js | 47 +++++++++++++++++ .../QueryParamsArgumentsDocs.test.js | 35 +++++++++++++ .../__snapshots__/InitSignature.test.js.snap | 11 ++++ .../QueryParamsArgumentsDocs.test.js.snap | 9 ++++ .../integration.test.js.javascript.snap | 3 -- 12 files changed, 214 insertions(+), 47 deletions(-) create mode 100644 packages/templates/clients/websocket/javascript/components/InitSignature.js create mode 100644 packages/templates/clients/websocket/javascript/components/QueryParamsArgumentsDocs.js create mode 100644 packages/templates/clients/websocket/javascript/test/components/InitSignature.test.js create mode 100644 packages/templates/clients/websocket/javascript/test/components/QueryParamsArgumentsDocs.test.js create mode 100644 packages/templates/clients/websocket/javascript/test/components/__snapshots__/InitSignature.test.js.snap create mode 100644 packages/templates/clients/websocket/javascript/test/components/__snapshots__/QueryParamsArgumentsDocs.test.js.snap diff --git a/packages/components/src/components/QueryParamsVariables.js b/packages/components/src/components/QueryParamsVariables.js index 59bab9f230..b296b28a90 100644 --- a/packages/components/src/components/QueryParamsVariables.js +++ b/packages/components/src/components/QueryParamsVariables.js @@ -68,20 +68,20 @@ const queryParamLogicConfig = { const paramName = param[0]; return { variableDefinition: { - text: `const ${paramName} = ${paramName} || process.env.${paramName.toUpperCase()};`, - indent: 8, + text: `const _${paramName} = ${paramName} || process.env.${paramName.toUpperCase()};`, + indent: 0, }, ifCondition: { - text: `if (${paramName}) {`, - indent: 8, + text: `if (_${paramName}) {`, + indent: 0, }, assignment: { - text: `params["${paramName}"] = ${paramName};`, - indent: 10, + text: `params["${paramName}"] = _${paramName};`, + indent: 2, }, closing: { text: '}', - indent: 8, + indent: 0, newLines: 1, }, }; diff --git a/packages/components/test/components/__snapshots__/QueryParamsVariables.test.js.snap b/packages/components/test/components/__snapshots__/QueryParamsVariables.test.js.snap index 10d7398f5a..21ec809dee 100644 --- a/packages/components/test/components/__snapshots__/QueryParamsVariables.test.js.snap +++ b/packages/components/test/components/__snapshots__/QueryParamsVariables.test.js.snap @@ -28,22 +28,22 @@ exports[`Testing of QueryParamsVariables component renders java quarkus query pa `; exports[`Testing of QueryParamsVariables component renders js query params correctly with query parameters 1`] = ` -"const heartbeat = heartbeat || process.env.HEARTBEAT; - if (heartbeat) { - params[\\"heartbeat\\"] = heartbeat; - } - const top_of_book = top_of_book || process.env.TOP_OF_BOOK; - if (top_of_book) { - params[\\"top_of_book\\"] = top_of_book; - } - const bids = bids || process.env.BIDS; - if (bids) { - params[\\"bids\\"] = bids; - } - const offers = offers || process.env.OFFERS; - if (offers) { - params[\\"offers\\"] = offers; - }" +"const _heartbeat = heartbeat || process.env.HEARTBEAT; +if (_heartbeat) { + params[\\"heartbeat\\"] = _heartbeat; +} +const _top_of_book = top_of_book || process.env.TOP_OF_BOOK; +if (_top_of_book) { + params[\\"top_of_book\\"] = _top_of_book; +} +const _bids = bids || process.env.BIDS; +if (_bids) { + params[\\"bids\\"] = _bids; +} +const _offers = offers || process.env.OFFERS; +if (_offers) { + params[\\"offers\\"] = _offers; +}" `; exports[`Testing of QueryParamsVariables component renders python nothing when queryParams is null 1`] = `""`; diff --git a/packages/templates/clients/websocket/javascript/components/ClientClass.js b/packages/templates/clients/websocket/javascript/components/ClientClass.js index d35f0d8b6e..6e412f172c 100644 --- a/packages/templates/clients/websocket/javascript/components/ClientClass.js +++ b/packages/templates/clients/websocket/javascript/components/ClientClass.js @@ -5,13 +5,13 @@ import { ModuleExport } from './ModuleExport'; import { CompileOperationSchemas } from './CompileOperationSchemas'; import { RegisterOutgoingProcessor } from './RegisterOutgoingProcessor'; -export function ClientClass({ clientName, serverUrl, title, sendOperations }) { +export function ClientClass({ clientName, serverUrl, queryParams, title, sendOperations }) { return ( {`class ${clientName} {`} - + operation.id()); const sendOperationsArray = JSON.stringify(sendOperationsId); + const queryParamsArray = queryParams && Array.from(queryParams.entries()); return ( - { - `/* + {`/* * Constructor to initialize the WebSocket client * @param {string} url - The WebSocket server URL. Use it if the server URL is different from the default one taken from the AsyncAPI document. - * @param {boolean} [throwSendErrors=true] - Controls how the instance send methods react to a send failure. +`} + + {` * @param {boolean} [throwSendErrors=true] - Controls how the instance send methods react to a send failure. * When true (default) the error is re-thrown after the registered error handlers run, so the caller can * handle each failure. When false the error is suppressed after the handlers run, which keeps a * high-throughput producer loop going. */ -constructor(url, throwSendErrors = true) { - this.url = url || '${serverUrl}'; - this.websocket = null; - this.messageHandlers = []; - this.errorHandlers = []; - this.outgoingProcessors = []; - this.compiledSchemas = {}; - this.schemasCompiled = false; - this.sendOperationsId = ${sendOperationsArray}; - this.throwSendErrors = throwSendErrors; // Re-throw send failures after handlers run +`} + + + {`this.url = url || '${serverUrl}'; +`} + {queryParamsArray && queryParamsArray.length > 0 && ( + + {`const params = {}; +`} + + {`const queryString = querystring.stringify(params); +if (queryString) { + this.url += '?' + queryString; } -` - } +`} + + )} + {`this.websocket = null; +this.messageHandlers = []; +this.errorHandlers = []; +this.outgoingProcessors = []; +this.compiledSchemas = {}; +this.schemasCompiled = false; +this.sendOperationsId = ${sendOperationsArray}; +this.throwSendErrors = throwSendErrors; // Re-throw send failures after handlers run`} + + {'}'} ); } diff --git a/packages/templates/clients/websocket/javascript/components/InitSignature.js b/packages/templates/clients/websocket/javascript/components/InitSignature.js new file mode 100644 index 0000000000..f5e99ddc2d --- /dev/null +++ b/packages/templates/clients/websocket/javascript/components/InitSignature.js @@ -0,0 +1,24 @@ +import { Text } from '@asyncapi/generator-react-sdk'; + +export function InitSignature({ queryParams }) { + if (!queryParams || queryParams.length === 0) { + return ( + + {'constructor(url, throwSendErrors = true) {'} + + ); + } + + const queryParamsArguments = queryParams.map((param) => { + const paramName = param[0]; + const paramDefaultValue = param[1]; + const defaultValue = paramDefaultValue ? ` = "${paramDefaultValue}"` : ''; + return `${paramName}${defaultValue}`; + }).join(', '); + + return ( + + {`constructor(url, ${queryParamsArguments}, throwSendErrors = true) {`} + + ); +} diff --git a/packages/templates/clients/websocket/javascript/components/QueryParamsArgumentsDocs.js b/packages/templates/clients/websocket/javascript/components/QueryParamsArgumentsDocs.js new file mode 100644 index 0000000000..8a350d4d12 --- /dev/null +++ b/packages/templates/clients/websocket/javascript/components/QueryParamsArgumentsDocs.js @@ -0,0 +1,18 @@ +import { Text } from '@asyncapi/generator-react-sdk'; + +export function QueryParamsArgumentsDocs({ queryParams }) { + if (!queryParams || queryParams.length === 0) { + return null; + } + + return queryParams.map((param) => { + const paramName = param[0]; + const firstLine = `* @param {string} ${paramName} - `; + const secondLine = `If provided (or if ${paramName.toUpperCase()} environment variable is set), added as ?${paramName}=… to URL`; + return ( + + {`${firstLine}${secondLine}\n`} + + ); + }); +} diff --git a/packages/templates/clients/websocket/javascript/template/client.js.js b/packages/templates/clients/websocket/javascript/template/client.js.js index 207fb33dc7..6352aab0e6 100644 --- a/packages/templates/clients/websocket/javascript/template/client.js.js +++ b/packages/templates/clients/websocket/javascript/template/client.js.js @@ -1,5 +1,5 @@ import { File } from '@asyncapi/generator-react-sdk'; -import { getClientName, getServerUrl, getServer, getInfo, getTitle } from '@asyncapi/generator-helpers'; +import { getClientName, getServerUrl, getServer, getInfo, getTitle, getQueryParams } from '@asyncapi/generator-helpers'; import { FileHeaderInfo, DependencyProvider } from '@asyncapi/generator-components'; import { ClientClass } from '../components/ClientClass'; @@ -11,6 +11,13 @@ export default function ({ asyncapi, params }) { const serverUrl = getServerUrl(server); const sendOperations = asyncapi.operations().filterBySend(); const asyncapiFilepath = `${params.asyncapiFileDir}/asyncapi.yaml`; + const queryParams = getQueryParams(asyncapi.channels()); + + const dependencies = ['const path = require(\'path\');', `const asyncapiFilepath = path.resolve(__dirname, '${asyncapiFilepath}');`]; + if (queryParams) { + dependencies.push('const querystring = require(\'querystring\');'); + } + return ( - + ); } diff --git a/packages/templates/clients/websocket/javascript/test/components/InitSignature.test.js b/packages/templates/clients/websocket/javascript/test/components/InitSignature.test.js new file mode 100644 index 0000000000..d20b04d360 --- /dev/null +++ b/packages/templates/clients/websocket/javascript/test/components/InitSignature.test.js @@ -0,0 +1,47 @@ +import path from 'path'; +import { render } from '@asyncapi/generator-react-sdk'; +import { Parser, fromFile } from '@asyncapi/parser'; +import { getQueryParams } from '@asyncapi/generator-helpers'; +import { InitSignature } from '../../components/InitSignature.js'; + +const parser = new Parser(); +const asyncapiFilePath = path.resolve(__dirname, '../../../test/__fixtures__/asyncapi-websocket-components.yml'); + +describe('InitSignature component (integration with AsyncAPI document)', () => { + let parsedAsyncAPIDocument; + + beforeAll(async () => { + const parseResult = await fromFile(parser, asyncapiFilePath).parse(); + parsedAsyncAPIDocument = parseResult.document; + }); + + test('renders with no query parameters', () => { + const result = render(); + expect(result.trim()).toMatchSnapshot(); + }); + + test('renders with empty query parameters array', () => { + const result = render(); + expect(result.trim()).toMatchSnapshot(); + }); + + test('renders with single query parameter with default value false', () => { + const queryParamsWithFalseDefault = [['heartbeat', 'false']]; + const result = render(); + expect(result.trim()).toMatchSnapshot(); + }); + + test('renders with single query parameter with default value true', () => { + const queryParamsWithTrueDefault = [['bids', 'true']]; + const result = render(); + expect(result.trim()).toMatchSnapshot(); + }); + + test('renders with multiple query parameters with mixed default values', () => { + const channels = parsedAsyncAPIDocument.channels(); + const queryParams = getQueryParams(channels); + const queryParamsArray = queryParams && Array.from(queryParams.entries()); + const result = render(); + expect(result.trim()).toMatchSnapshot(); + }); +}); diff --git a/packages/templates/clients/websocket/javascript/test/components/QueryParamsArgumentsDocs.test.js b/packages/templates/clients/websocket/javascript/test/components/QueryParamsArgumentsDocs.test.js new file mode 100644 index 0000000000..47d5fec0b2 --- /dev/null +++ b/packages/templates/clients/websocket/javascript/test/components/QueryParamsArgumentsDocs.test.js @@ -0,0 +1,35 @@ +import path from 'path'; +import { render } from '@asyncapi/generator-react-sdk'; +import { Parser, fromFile } from '@asyncapi/parser'; +import { getQueryParams } from '@asyncapi/generator-helpers'; +import { QueryParamsArgumentsDocs } from '../../components/QueryParamsArgumentsDocs.js'; + +const parser = new Parser(); +const asyncapiFilePath = path.resolve(__dirname, '../../../test/__fixtures__/asyncapi-websocket-components.yml'); + +describe('QueryParamsArgumentsDocs component (integration with AsyncAPI document)', () => { + let parsedAsyncAPIDocument; + + beforeAll(async () => { + const parseResult = await fromFile(parser, asyncapiFilePath).parse(); + parsedAsyncAPIDocument = parseResult.document; + }); + + test('renders with query parameters', () => { + const channels = parsedAsyncAPIDocument.channels(); + const queryParams = getQueryParams(channels); + const queryParamsArray = queryParams && Array.from(queryParams.entries()); + const result = render(); + expect(result.trim()).toMatchSnapshot(); + }); + + test('renders empty with empty array', () => { + const result = render(); + expect(result).toBe(''); + }); + + test('renders empty with null', () => { + const result = render(); + expect(result).toBe(''); + }); +}); diff --git a/packages/templates/clients/websocket/javascript/test/components/__snapshots__/InitSignature.test.js.snap b/packages/templates/clients/websocket/javascript/test/components/__snapshots__/InitSignature.test.js.snap new file mode 100644 index 0000000000..7fb7f2ab07 --- /dev/null +++ b/packages/templates/clients/websocket/javascript/test/components/__snapshots__/InitSignature.test.js.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`InitSignature component (integration with AsyncAPI document) renders with empty query parameters array 1`] = `"constructor(url, throwSendErrors = true) {"`; + +exports[`InitSignature component (integration with AsyncAPI document) renders with multiple query parameters with mixed default values 1`] = `"constructor(url, heartbeat = \\"false\\", bids = \\"true\\", sessionId, throwSendErrors = true) {"`; + +exports[`InitSignature component (integration with AsyncAPI document) renders with no query parameters 1`] = `"constructor(url, throwSendErrors = true) {"`; + +exports[`InitSignature component (integration with AsyncAPI document) renders with single query parameter with default value false 1`] = `"constructor(url, heartbeat = \\"false\\", throwSendErrors = true) {"`; + +exports[`InitSignature component (integration with AsyncAPI document) renders with single query parameter with default value true 1`] = `"constructor(url, bids = \\"true\\", throwSendErrors = true) {"`; diff --git a/packages/templates/clients/websocket/javascript/test/components/__snapshots__/QueryParamsArgumentsDocs.test.js.snap b/packages/templates/clients/websocket/javascript/test/components/__snapshots__/QueryParamsArgumentsDocs.test.js.snap new file mode 100644 index 0000000000..7650e5d4d7 --- /dev/null +++ b/packages/templates/clients/websocket/javascript/test/components/__snapshots__/QueryParamsArgumentsDocs.test.js.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`QueryParamsArgumentsDocs component (integration with AsyncAPI document) renders with query parameters 1`] = ` +"* @param {string} heartbeat - If provided (or if HEARTBEAT environment variable is set), added as ?heartbeat=… to URL + + * @param {string} bids - If provided (or if BIDS environment variable is set), added as ?bids=… to URL + + * @param {string} sessionId - If provided (or if SESSIONID environment variable is set), added as ?sessionId=… to URL" +`; diff --git a/packages/templates/clients/websocket/test/integration-test/__snapshots__/integration.test.js.javascript.snap b/packages/templates/clients/websocket/test/integration-test/__snapshots__/integration.test.js.javascript.snap index c58c779e7a..0353483fb1 100644 --- a/packages/templates/clients/websocket/test/integration-test/__snapshots__/integration.test.js.javascript.snap +++ b/packages/templates/clients/websocket/test/integration-test/__snapshots__/integration.test.js.javascript.snap @@ -212,7 +212,6 @@ class HoppscotchClient { this.sendOperationsId = [\\"sendEchoMessage\\"]; this.throwSendErrors = throwSendErrors; // Re-throw send failures after handlers run } - // Method to establish a WebSocket connection connect() { return new Promise((resolve, reject) => { @@ -622,7 +621,6 @@ class HoppscotchEchoWebSocketClient { this.sendOperationsId = [\\"sendEchoMessage\\"]; this.throwSendErrors = throwSendErrors; // Re-throw send failures after handlers run } - // Method to establish a WebSocket connection connect() { return new Promise((resolve, reject) => { @@ -1002,7 +1000,6 @@ class PostmanEchoWebSocketClientClient { this.sendOperationsId = [\\"sendEchoMessage\\"]; this.throwSendErrors = throwSendErrors; // Re-throw send failures after handlers run } - // Method to establish a WebSocket connection connect() { return new Promise((resolve, reject) => { From 50cdcef4a01ea0822a96c1122cdee1f2a79ed2b3 Mon Sep 17 00:00:00 2001 From: Varshitha Besthavemula Date: Thu, 2 Jul 2026 00:04:37 +0530 Subject: [PATCH 2/4] add jsdocs for new components in js websocket and improve fromatting of params --- .../javascript/components/InitSignature.js | 23 ++++++++++-- .../components/QueryParamsArgumentsDocs.js | 15 ++++++-- .../javascript/components/getSafeJsName.js | 20 +++++++++++ .../test/components/InitSignature.test.js | 29 +++++++++++++++ .../__snapshots__/InitSignature.test.js.snap | 16 +++++++-- .../test/components/getSafeJsName.test.js | 35 +++++++++++++++++++ 6 files changed, 131 insertions(+), 7 deletions(-) create mode 100644 packages/templates/clients/websocket/javascript/components/getSafeJsName.js create mode 100644 packages/templates/clients/websocket/javascript/test/components/getSafeJsName.test.js diff --git a/packages/templates/clients/websocket/javascript/components/InitSignature.js b/packages/templates/clients/websocket/javascript/components/InitSignature.js index f5e99ddc2d..04a17aec13 100644 --- a/packages/templates/clients/websocket/javascript/components/InitSignature.js +++ b/packages/templates/clients/websocket/javascript/components/InitSignature.js @@ -1,5 +1,14 @@ import { Text } from '@asyncapi/generator-react-sdk'; +import { getSafeJSName } from './getSafeJsName'; +/** + * Renders the constructor signature for the generated WebSocket client. + * Injects any query parameters into the signature with their default values (if provided in the AsyncAPI document). + * + * @param {Object} props - The component props. + * @param {Array>} [props.queryParams] - Array of query parameters from the AsyncAPI document, where each item is a tuple `[name, defaultValue]`. + * @returns {React.Element} The rendered React SDK Text element containing the constructor signature. + */ export function InitSignature({ queryParams }) { if (!queryParams || queryParams.length === 0) { return ( @@ -10,9 +19,19 @@ export function InitSignature({ queryParams }) { } const queryParamsArguments = queryParams.map((param) => { - const paramName = param[0]; + const paramName = getSafeJSName(param[0]); const paramDefaultValue = param[1]; - const defaultValue = paramDefaultValue ? ` = "${paramDefaultValue}"` : ''; + let defaultValue = ''; + if (paramDefaultValue !== undefined && paramDefaultValue !== null && paramDefaultValue !== '') { + const isBoolean = paramDefaultValue === 'true' || paramDefaultValue === 'false' || typeof paramDefaultValue === 'boolean'; + const isNumber = typeof paramDefaultValue === 'number' || (!isNaN(parseFloat(paramDefaultValue)) && isFinite(paramDefaultValue)); + + if (isBoolean || isNumber) { + defaultValue = ` = ${paramDefaultValue}`; + } else { + defaultValue = ` = "${paramDefaultValue}"`; + } + } return `${paramName}${defaultValue}`; }).join(', '); diff --git a/packages/templates/clients/websocket/javascript/components/QueryParamsArgumentsDocs.js b/packages/templates/clients/websocket/javascript/components/QueryParamsArgumentsDocs.js index 8a350d4d12..750fb5cc4c 100644 --- a/packages/templates/clients/websocket/javascript/components/QueryParamsArgumentsDocs.js +++ b/packages/templates/clients/websocket/javascript/components/QueryParamsArgumentsDocs.js @@ -1,14 +1,25 @@ import { Text } from '@asyncapi/generator-react-sdk'; +import { getSafeJSName } from './getSafeJsName'; +/** + * Generates the JSDoc `@param` documentation blocks for each query parameter + * injected into the constructor signature. + * + * @param {Object} props - The component props. + * @param {Array>} [props.queryParams] - Array of query parameters from the AsyncAPI document, where each item is a tuple `[name, defaultValue]`. + * @returns {React.Element[] | null} An array of rendered React SDK Text elements containing the parameter documentation, or null if no query parameters exist. + */ export function QueryParamsArgumentsDocs({ queryParams }) { if (!queryParams || queryParams.length === 0) { return null; } return queryParams.map((param) => { - const paramName = param[0]; + const originalParamName = param[0]; + const paramName = getSafeJSName(originalParamName); + const envVarName = originalParamName.toUpperCase().replace(/[^A-Z0-9_]/g, '_'); const firstLine = `* @param {string} ${paramName} - `; - const secondLine = `If provided (or if ${paramName.toUpperCase()} environment variable is set), added as ?${paramName}=… to URL`; + const secondLine = `If provided (or if ${envVarName} environment variable is set), added as ?${originalParamName}=… to URL`; return ( {`${firstLine}${secondLine}\n`} diff --git a/packages/templates/clients/websocket/javascript/components/getSafeJsName.js b/packages/templates/clients/websocket/javascript/components/getSafeJsName.js new file mode 100644 index 0000000000..c32e0c593f --- /dev/null +++ b/packages/templates/clients/websocket/javascript/components/getSafeJsName.js @@ -0,0 +1,20 @@ +import { toCamelCase } from '@asyncapi/generator-helpers'; + +/** + * Converts a given string into a safe JavaScript identifier. + * It camelCases the string, replaces invalid characters, ensures it does not start with a digit, + * and avoids collisions with reserved words (including specific constructor parameters). + * + * @param {string} name - The original string (e.g., a query parameter name). + * @returns {string} A safe JavaScript identifier. + */ +export function getSafeJSName(name) { + let safe = toCamelCase(name); + safe = safe.replace(/[^a-zA-Z0-9_]/g, '_'); + if ((/^[0-9]/).test(safe)) { + safe = `_${safe}`; + } + const reserved = ['url', 'throwSendErrors', 'params', 'queryString', 'class', 'const', 'let', 'var', 'if', 'else', 'return', 'this', 'true', 'false', 'null', 'undefined']; + if (reserved.includes(safe)) safe = `_${ safe}`; + return safe; +} \ No newline at end of file diff --git a/packages/templates/clients/websocket/javascript/test/components/InitSignature.test.js b/packages/templates/clients/websocket/javascript/test/components/InitSignature.test.js index d20b04d360..b56ac2d172 100644 --- a/packages/templates/clients/websocket/javascript/test/components/InitSignature.test.js +++ b/packages/templates/clients/websocket/javascript/test/components/InitSignature.test.js @@ -44,4 +44,33 @@ describe('InitSignature component (integration with AsyncAPI document)', () => { const result = render(); expect(result.trim()).toMatchSnapshot(); }); + test('renders with single query parameter without default value', () => { + const queryParamsWithoutDefault = [['token', undefined]]; + const result = render(); + expect(result.trim()).toMatchSnapshot(); + }); + + test('renders with single query parameter with default value as primitive boolean', () => { + const queryParams = [['bids', false]]; + const result = render(); + expect(result.trim()).toMatchSnapshot(); + }); + + test('renders with single query parameter with default value as primitive number', () => { + const queryParams = [['count', 42]]; + const result = render(); + expect(result.trim()).toMatchSnapshot(); + }); + + test('renders with single query parameter with default value as stringified number', () => { + const queryParams = [['count', '42']]; + const result = render(); + expect(result.trim()).toMatchSnapshot(); + }); + + test('renders with single query parameter with explicit empty string default', () => { + const queryParams = [['token', '']]; + const result = render(); + expect(result.trim()).toMatchSnapshot(); + }); }); diff --git a/packages/templates/clients/websocket/javascript/test/components/__snapshots__/InitSignature.test.js.snap b/packages/templates/clients/websocket/javascript/test/components/__snapshots__/InitSignature.test.js.snap index 7fb7f2ab07..3422ce1d70 100644 --- a/packages/templates/clients/websocket/javascript/test/components/__snapshots__/InitSignature.test.js.snap +++ b/packages/templates/clients/websocket/javascript/test/components/__snapshots__/InitSignature.test.js.snap @@ -2,10 +2,20 @@ exports[`InitSignature component (integration with AsyncAPI document) renders with empty query parameters array 1`] = `"constructor(url, throwSendErrors = true) {"`; -exports[`InitSignature component (integration with AsyncAPI document) renders with multiple query parameters with mixed default values 1`] = `"constructor(url, heartbeat = \\"false\\", bids = \\"true\\", sessionId, throwSendErrors = true) {"`; +exports[`InitSignature component (integration with AsyncAPI document) renders with multiple query parameters with mixed default values 1`] = `"constructor(url, heartbeat = false, bids = true, sessionId, throwSendErrors = true) {"`; exports[`InitSignature component (integration with AsyncAPI document) renders with no query parameters 1`] = `"constructor(url, throwSendErrors = true) {"`; -exports[`InitSignature component (integration with AsyncAPI document) renders with single query parameter with default value false 1`] = `"constructor(url, heartbeat = \\"false\\", throwSendErrors = true) {"`; +exports[`InitSignature component (integration with AsyncAPI document) renders with single query parameter with default value as primitive boolean 1`] = `"constructor(url, bids = false, throwSendErrors = true) {"`; -exports[`InitSignature component (integration with AsyncAPI document) renders with single query parameter with default value true 1`] = `"constructor(url, bids = \\"true\\", throwSendErrors = true) {"`; +exports[`InitSignature component (integration with AsyncAPI document) renders with single query parameter with default value as primitive number 1`] = `"constructor(url, count = 42, throwSendErrors = true) {"`; + +exports[`InitSignature component (integration with AsyncAPI document) renders with single query parameter with default value as stringified number 1`] = `"constructor(url, count = 42, throwSendErrors = true) {"`; + +exports[`InitSignature component (integration with AsyncAPI document) renders with single query parameter with default value false 1`] = `"constructor(url, heartbeat = false, throwSendErrors = true) {"`; + +exports[`InitSignature component (integration with AsyncAPI document) renders with single query parameter with default value true 1`] = `"constructor(url, bids = true, throwSendErrors = true) {"`; + +exports[`InitSignature component (integration with AsyncAPI document) renders with single query parameter with explicit empty string default 1`] = `"constructor(url, token, throwSendErrors = true) {"`; + +exports[`InitSignature component (integration with AsyncAPI document) renders with single query parameter without default value 1`] = `"constructor(url, token, throwSendErrors = true) {"`; diff --git a/packages/templates/clients/websocket/javascript/test/components/getSafeJsName.test.js b/packages/templates/clients/websocket/javascript/test/components/getSafeJsName.test.js new file mode 100644 index 0000000000..760fdebe14 --- /dev/null +++ b/packages/templates/clients/websocket/javascript/test/components/getSafeJsName.test.js @@ -0,0 +1,35 @@ +import { getSafeJSName } from '../../components/getSafeJsName.js'; + +describe('getSafeJSName', () => { + test('converts standard names to camelCase', () => { + expect(getSafeJSName('my-param')).toBe('myParam'); + expect(getSafeJSName('some_other_param')).toBe('someOtherParam'); + }); + + test('prefixes names starting with a digit with an underscore', () => { + expect(getSafeJSName('1st')).toBe('_1st'); + expect(getSafeJSName('2nd-param')).toBe('_2ndParam'); + }); + + test('replaces invalid javascript identifier characters with an underscore', () => { + // toCamelCase usually handles most punctuation by removing it and camelCasing, + // but if any invalid characters slip through (e.g. non-ASCII or specific symbols that toCamelCase doesn't touch), + // they should be replaced. + // For testing, we ensure that the resulting string only contains valid characters. + const result = getSafeJSName('my@param!'); + expect(result).toMatch(/^[a-zA-Z_$][a-zA-Z0-9_$]*$/); + }); + + test('prefixes reserved words to avoid collisions', () => { + expect(getSafeJSName('url')).toBe('_url'); + expect(getSafeJSName('throwSendErrors')).toBe('_throwSendErrors'); + expect(getSafeJSName('class')).toBe('_class'); + expect(getSafeJSName('return')).toBe('_return'); + expect(getSafeJSName('true')).toBe('_true'); + }); + + test('does not prefix non-reserved words', () => { + expect(getSafeJSName('urlValue')).toBe('urlValue'); + expect(getSafeJSName('classic')).toBe('classic'); + }); +}); From aafe46250c969c3e5bbe2b7a68a251872cf90516 Mon Sep 17 00:00:00 2001 From: Varshitha Besthavemula Date: Thu, 2 Jul 2026 00:12:24 +0530 Subject: [PATCH 3/4] enhance getSafeName.js in js ws --- .../javascript/components/InitSignature.js | 3 ++- .../components/QueryParamsArgumentsDocs.js | 3 ++- .../javascript/components/getSafeJsName.js | 13 +++++++++++-- .../test/components/getSafeJsName.test.js | 8 ++++++++ 4 files changed, 23 insertions(+), 4 deletions(-) diff --git a/packages/templates/clients/websocket/javascript/components/InitSignature.js b/packages/templates/clients/websocket/javascript/components/InitSignature.js index 04a17aec13..2be75b9994 100644 --- a/packages/templates/clients/websocket/javascript/components/InitSignature.js +++ b/packages/templates/clients/websocket/javascript/components/InitSignature.js @@ -18,8 +18,9 @@ export function InitSignature({ queryParams }) { ); } + const usedNames = new Set(); const queryParamsArguments = queryParams.map((param) => { - const paramName = getSafeJSName(param[0]); + const paramName = getSafeJSName(param[0], usedNames); const paramDefaultValue = param[1]; let defaultValue = ''; if (paramDefaultValue !== undefined && paramDefaultValue !== null && paramDefaultValue !== '') { diff --git a/packages/templates/clients/websocket/javascript/components/QueryParamsArgumentsDocs.js b/packages/templates/clients/websocket/javascript/components/QueryParamsArgumentsDocs.js index 750fb5cc4c..c260da0754 100644 --- a/packages/templates/clients/websocket/javascript/components/QueryParamsArgumentsDocs.js +++ b/packages/templates/clients/websocket/javascript/components/QueryParamsArgumentsDocs.js @@ -14,9 +14,10 @@ export function QueryParamsArgumentsDocs({ queryParams }) { return null; } + const usedNames = new Set(); return queryParams.map((param) => { const originalParamName = param[0]; - const paramName = getSafeJSName(originalParamName); + const paramName = getSafeJSName(originalParamName, usedNames); const envVarName = originalParamName.toUpperCase().replace(/[^A-Z0-9_]/g, '_'); const firstLine = `* @param {string} ${paramName} - `; const secondLine = `If provided (or if ${envVarName} environment variable is set), added as ?${originalParamName}=… to URL`; diff --git a/packages/templates/clients/websocket/javascript/components/getSafeJsName.js b/packages/templates/clients/websocket/javascript/components/getSafeJsName.js index c32e0c593f..9b9aafc464 100644 --- a/packages/templates/clients/websocket/javascript/components/getSafeJsName.js +++ b/packages/templates/clients/websocket/javascript/components/getSafeJsName.js @@ -6,9 +6,10 @@ import { toCamelCase } from '@asyncapi/generator-helpers'; * and avoids collisions with reserved words (including specific constructor parameters). * * @param {string} name - The original string (e.g., a query parameter name). + * @param {Set} [usedNames=new Set()] - A set of already used names to prevent collisions. * @returns {string} A safe JavaScript identifier. */ -export function getSafeJSName(name) { +export function getSafeJSName(name, usedNames = new Set()) { let safe = toCamelCase(name); safe = safe.replace(/[^a-zA-Z0-9_]/g, '_'); if ((/^[0-9]/).test(safe)) { @@ -16,5 +17,13 @@ export function getSafeJSName(name) { } const reserved = ['url', 'throwSendErrors', 'params', 'queryString', 'class', 'const', 'let', 'var', 'if', 'else', 'return', 'this', 'true', 'false', 'null', 'undefined']; if (reserved.includes(safe)) safe = `_${ safe}`; - return safe; + + let candidate = safe; + let suffix = 1; + while (usedNames.has(candidate)) { + candidate = `${safe}_${suffix++}`; + } + usedNames.add(candidate); + + return candidate; } \ No newline at end of file diff --git a/packages/templates/clients/websocket/javascript/test/components/getSafeJsName.test.js b/packages/templates/clients/websocket/javascript/test/components/getSafeJsName.test.js index 760fdebe14..7d5fbe95bc 100644 --- a/packages/templates/clients/websocket/javascript/test/components/getSafeJsName.test.js +++ b/packages/templates/clients/websocket/javascript/test/components/getSafeJsName.test.js @@ -20,6 +20,14 @@ describe('getSafeJSName', () => { expect(result).toMatch(/^[a-zA-Z_$][a-zA-Z0-9_$]*$/); }); + test('generates unique names for collisions when given a shared Set', () => { + const usedNames = new Set(); + expect(getSafeJSName('my-param', usedNames)).toBe('myParam'); + expect(getSafeJSName('my_param', usedNames)).toBe('myParam_1'); + expect(getSafeJSName('myParam', usedNames)).toBe('myParam_2'); + expect(getSafeJSName('my-param', usedNames)).toBe('myParam_3'); + }); + test('prefixes reserved words to avoid collisions', () => { expect(getSafeJSName('url')).toBe('_url'); expect(getSafeJSName('throwSendErrors')).toBe('_throwSendErrors'); From f790e68c06551252546707e3823b8d53a110851a Mon Sep 17 00:00:00 2001 From: Varshitha Besthavemula Date: Tue, 11 Aug 2026 19:48:06 +0530 Subject: [PATCH 4/4] add basic slack support to js ws --- .../src/components/QueryParamsVariables.js | 9 +- .../QueryParamsVariables.test.js.snap | 2 +- .../javascript/components/getSafeJsName.js | 2 +- .../websocket/javascript/example-slack.js | 27 + .../integration.test.js.javascript.snap | 1236 +++++++++++++++++ .../test/integration-test/integration.test.js | 1 + 6 files changed, 1271 insertions(+), 6 deletions(-) create mode 100644 packages/templates/clients/websocket/javascript/example-slack.js diff --git a/packages/components/src/components/QueryParamsVariables.js b/packages/components/src/components/QueryParamsVariables.js index b296b28a90..01635d7742 100644 --- a/packages/components/src/components/QueryParamsVariables.js +++ b/packages/components/src/components/QueryParamsVariables.js @@ -65,18 +65,19 @@ const queryParamLogicConfig = { }, }, javascript: (param) => { - const paramName = param[0]; + const rawName = param[0]; + const paramName = toCamelCase(rawName); return { variableDefinition: { - text: `const _${paramName} = ${paramName} || process.env.${paramName.toUpperCase()};`, + text: `const _${rawName} = ${paramName} || process.env.${rawName.toUpperCase()};`, indent: 0, }, ifCondition: { - text: `if (_${paramName}) {`, + text: `if (_${rawName}) {`, indent: 0, }, assignment: { - text: `params["${paramName}"] = _${paramName};`, + text: `params["${rawName}"] = _${rawName};`, indent: 2, }, closing: { diff --git a/packages/components/test/components/__snapshots__/QueryParamsVariables.test.js.snap b/packages/components/test/components/__snapshots__/QueryParamsVariables.test.js.snap index 21ec809dee..103b87f4d4 100644 --- a/packages/components/test/components/__snapshots__/QueryParamsVariables.test.js.snap +++ b/packages/components/test/components/__snapshots__/QueryParamsVariables.test.js.snap @@ -32,7 +32,7 @@ exports[`Testing of QueryParamsVariables component renders js query params corre if (_heartbeat) { params[\\"heartbeat\\"] = _heartbeat; } -const _top_of_book = top_of_book || process.env.TOP_OF_BOOK; +const _top_of_book = topOfBook || process.env.TOP_OF_BOOK; if (_top_of_book) { params[\\"top_of_book\\"] = _top_of_book; } diff --git a/packages/templates/clients/websocket/javascript/components/getSafeJsName.js b/packages/templates/clients/websocket/javascript/components/getSafeJsName.js index 9b9aafc464..052020e136 100644 --- a/packages/templates/clients/websocket/javascript/components/getSafeJsName.js +++ b/packages/templates/clients/websocket/javascript/components/getSafeJsName.js @@ -16,7 +16,7 @@ export function getSafeJSName(name, usedNames = new Set()) { safe = `_${safe}`; } const reserved = ['url', 'throwSendErrors', 'params', 'queryString', 'class', 'const', 'let', 'var', 'if', 'else', 'return', 'this', 'true', 'false', 'null', 'undefined']; - if (reserved.includes(safe)) safe = `_${ safe}`; + if (reserved.includes(safe)) safe = `_${safe}`; let candidate = safe; let suffix = 1; diff --git a/packages/templates/clients/websocket/javascript/example-slack.js b/packages/templates/clients/websocket/javascript/example-slack.js new file mode 100644 index 0000000000..51a79ce495 --- /dev/null +++ b/packages/templates/clients/websocket/javascript/example-slack.js @@ -0,0 +1,27 @@ +const WSClient = require('./test/temp/snapshotTestResult/client_slack/client.js'); +// Example usage +const wsClient = new WSClient(); + +// Example of how custom message handler that operates on incoming messages can look like +function myHandler(message) { + console.log('===================='); + console.log('\x1b[94mIncoming event from Slack\x1b[0m:', message); + console.log('===================='); +} + +async function main() { + wsClient.registerMessageHandler(myHandler); + + try { + await wsClient.connect(); + + // Keep the process alive to receive Slack events + // The connection will stay open until you terminate the process + console.log('Listening for Slack events... Press Ctrl+C to exit.'); + await new Promise(() => {}); + } catch (error) { + console.error('Failed to connect to WebSocket:', error.message); + } +} + +main(); diff --git a/packages/templates/clients/websocket/test/integration-test/__snapshots__/integration.test.js.javascript.snap b/packages/templates/clients/websocket/test/integration-test/__snapshots__/integration.test.js.javascript.snap index 0353483fb1..824c20ae4a 100644 --- a/packages/templates/clients/websocket/test/integration-test/__snapshots__/integration.test.js.javascript.snap +++ b/packages/templates/clients/websocket/test/integration-test/__snapshots__/integration.test.js.javascript.snap @@ -1,5 +1,1241 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`WebSocket Clients Integration Tests JavaScript Client Additional tests for JavaScript client generate client for slack: README.md 1`] = ` +"# Slack Websocket API Client + +## Overview + +AsyncAPI document that represent the API of the application that is a client of the official Slack Websocket API To start using the Slack Websocket API, you need to create a Slack app and install it in your workspace. Make sure that the app is approved by the admin of the workspace. +1. Create app in Slack: https://api.slack.com/apps. Do it by selecting option to create from scratch. Provide a name and select the workspace where the app will be used. +1. Enable Socket Mode: Go to the \\"Socket Mode\\" section and enable the API. Follow all steps and save generated token for later. By default you are not yet subscribed to any events. +1. Subscribe to events: Go to the \\"Event Subscriptions\\" section and enable the API. Subscribe to the bot events you want to receive. For example, you can subscribe to the \`reaction_added\` event. Remember to save changes. +1. Install the app: Go to the \\"Install App\\" section and install the app in your workspace. After that make sure admin approved it for the workspace. +1. Generate WebSocket URL: Make a REST call to the endpoint that will generate the WebSocket URL. Use the \`apps.connections.open\` path from Slack's API. The response will contain a \`url\` field with the WebSocket URL. Use this URL to connect to the WebSocket API. + \`\`\` + curl --location --request POST 'https://slack.com/api/apps.connections.open' \\\\ + --header 'Authorization: Bearer USE_TOKEN_GENERATED_DURING_SOCKET_MODE_ENABLEMENT' \\\\ + \`\`\` + +1. Generated URL is the WebSocket URL you need to use to connect to the Slack WebSocket API. The URL will look like this: + \`\`\` + wss://wss-primary.slack.com/link?app_id=YOUR_APP_ID&ticket=YOUR_TICKET + + +- **Version:** 1.0.0 +- **Server URL:** wss://wss-primary.slack.com/link + + +## Installation + +Install dependencies: + +\`\`\`bash +npm install +\`\`\` + + +## Usage + +\`\`\`javascript +const SlackWebsocketAPIClient = require('./client'); + +// throwSendErrors defaults to true: a failed send re-throws after the registered +// error handlers run, so you can react to each failure. Pass +// new SlackWebsocketAPIClient(undefined, false) to keep a high-throughput producer loop +// running and rely on registered error handlers instead. +const wsClient = new SlackWebsocketAPIClient(); + +async function main() { + try { + await wsClient.connect(); + // use wsClient to send/receive messages + await wsClient.close(); + } catch (error) { + console.error('Failed to connect or send:', error); + } +} + +main(); +\`\`\` + + +## API + +### \`connect()\` +Establishes a WebSocket connection. + +### \`registerMessageHandler(handlerFunction)\` +Registers a callback for incoming messages. + +### \`registerErrorHandler(handlerFunction)\` +Registers a callback for connection errors. + +### \`close()\` +Closes the WebSocket connection. + + +### Available Operations + +#### \`onHelloMessage(payload)\` +One time operation. You receive one message once you connect to the API + + +**Example (JavaScript):** +\`\`\`javascript +client.onHelloMessage({ + \\"type\\": \\"hello\\", + \\"num_connections\\": 1, + \\"debug_info\\": { + \\"host\\": \\"applink-8\\", + \\"build_number\\": 105, + \\"approximate_connection_time\\": 18060 + }, + \\"connection_info\\": { + \\"app_id\\": \\"A08NKKBFGBD\\" + } +}) +\`\`\` + + +**Example (Python):** +\`\`\`python +client.on_hello_message({ + \\"type\\": \\"hello\\", + \\"num_connections\\": 1, + \\"debug_info\\": { + \\"host\\": \\"applink-8\\", + \\"build_number\\": 105, + \\"approximate_connection_time\\": 18060 + }, + \\"connection_info\\": { + \\"app_id\\": \\"A08NKKBFGBD\\" + } +}) +\`\`\` + + + + +#### \`onEvent(payload)\` + + + +**Example (JavaScript):** +\`\`\`javascript +client.onEvent({ + \\"envelope_id\\": \\"c044c358-d17e-4f6d-94aa-3ddadb82ef59\\", + \\"payload\\": { + \\"token\\": \\"el8A2aGi0yIJ2so0ENvWcAfX\\", + \\"team_id\\": \\"T34F2JRQU\\", + \\"context_team_id\\": \\"T34F2JRQU\\", + \\"context_enterprise_id\\": null, + \\"api_app_id\\": \\"A08NKKBFGBD\\", + \\"event\\": { + \\"subtype\\": \\"channel_join\\", + \\"user\\": \\"U08NR6C69R8\\", + \\"text\\": \\"<@U08NR6C69R8> has joined the channel\\", + \\"inviter\\": \\"UD698Q5LM\\", + \\"type\\": \\"message\\", + \\"ts\\": \\"1744903248.779009\\", + \\"channel\\": \\"C072JMTJ85Q\\", + \\"event_ts\\": \\"1744903248.779009\\", + \\"channel_type\\": \\"channel\\" + }, + \\"type\\": \\"event_callback\\", + \\"event_id\\": \\"Ev08NDP302UE\\", + \\"event_time\\": 1744903248, + \\"authorizations\\": [ + { + \\"enterprise_id\\": null, + \\"team_id\\": \\"T34F2JRQU\\", + \\"user_id\\": \\"U08NR6C69R8\\", + \\"is_bot\\": true, + \\"is_enterprise_install\\": false + } + ], + \\"is_ext_shared_channel\\": false, + \\"event_context\\": \\"4-eyJldCI6Im1lc3NhZ2UiLCJ0aWQiOiJUMzRGMkpSUVUiLCJhaWQiOiJBMDhOS0tCRkdCRCIsImNpZCI6IkMwNzJKTVRKODVRIn0\\" + }, + \\"type\\": \\"events_api\\", + \\"accepts_response_payload\\": false, + \\"retry_attempt\\": 0, + \\"retry_reason\\": \\"\\" +}) +\`\`\` + + +**Example (Python):** +\`\`\`python +client.on_event({ + \\"envelope_id\\": \\"c044c358-d17e-4f6d-94aa-3ddadb82ef59\\", + \\"payload\\": { + \\"token\\": \\"el8A2aGi0yIJ2so0ENvWcAfX\\", + \\"team_id\\": \\"T34F2JRQU\\", + \\"context_team_id\\": \\"T34F2JRQU\\", + \\"context_enterprise_id\\": null, + \\"api_app_id\\": \\"A08NKKBFGBD\\", + \\"event\\": { + \\"subtype\\": \\"channel_join\\", + \\"user\\": \\"U08NR6C69R8\\", + \\"text\\": \\"<@U08NR6C69R8> has joined the channel\\", + \\"inviter\\": \\"UD698Q5LM\\", + \\"type\\": \\"message\\", + \\"ts\\": \\"1744903248.779009\\", + \\"channel\\": \\"C072JMTJ85Q\\", + \\"event_ts\\": \\"1744903248.779009\\", + \\"channel_type\\": \\"channel\\" + }, + \\"type\\": \\"event_callback\\", + \\"event_id\\": \\"Ev08NDP302UE\\", + \\"event_time\\": 1744903248, + \\"authorizations\\": [ + { + \\"enterprise_id\\": null, + \\"team_id\\": \\"T34F2JRQU\\", + \\"user_id\\": \\"U08NR6C69R8\\", + \\"is_bot\\": true, + \\"is_enterprise_install\\": false + } + ], + \\"is_ext_shared_channel\\": false, + \\"event_context\\": \\"4-eyJldCI6Im1lc3NhZ2UiLCJ0aWQiOiJUMzRGMkpSUVUiLCJhaWQiOiJBMDhOS0tCRkdCRCIsImNpZCI6IkMwNzJKTVRKODVRIn0\\" + }, + \\"type\\": \\"events_api\\", + \\"accepts_response_payload\\": false, + \\"retry_attempt\\": 0, + \\"retry_reason\\": \\"\\" +}) +\`\`\` + + +**Example (JavaScript):** +\`\`\`javascript +client.onEvent({ + \\"envelope_id\\": \\"6d0b4caf-95d3-410c-b85d-ce592e622aea\\", + \\"payload\\": { + \\"token\\": \\"el8A2aGi0yIJ2so0ENvWcAfX\\", + \\"team_id\\": \\"T34F2JRQU\\", + \\"context_team_id\\": \\"T34F2JRQU\\", + \\"context_enterprise_id\\": null, + \\"api_app_id\\": \\"A08NKKBFGBD\\", + \\"event\\": { + \\"type\\": \\"reaction_added\\", + \\"user\\": \\"UD698Q5LM\\", + \\"reaction\\": \\"beers\\", + \\"item\\": { + \\"type\\": \\"message\\", + \\"channel\\": \\"C072JMTJ85Q\\", + \\"ts\\": \\"1744903248.779009\\" + }, + \\"event_ts\\": \\"1744903279.021400\\" + }, + \\"type\\": \\"event_callback\\", + \\"event_id\\": \\"Ev08NRBU37QB\\", + \\"event_time\\": 1744903279, + \\"authorizations\\": [ + { + \\"enterprise_id\\": null, + \\"team_id\\": \\"T34F2JRQU\\", + \\"user_id\\": \\"U08NR6C69R8\\", + \\"is_bot\\": true, + \\"is_enterprise_install\\": false + } + ], + \\"is_ext_shared_channel\\": false, + \\"event_context\\": \\"4-eyJldCI6InJlYWN0aW9uX2FkZGVkIiwidGlkIjoiVDM0RjJKUlFVIiwiYWlkIjoiQTA4TktLQkZHQkQiLCJjaWQiOiJDMDcySk1USjg1USJ9\\" + }, + \\"type\\": \\"events_api\\", + \\"accepts_response_payload\\": false, + \\"retry_attempt\\": 3, + \\"retry_reason\\": \\"timeout\\" +}) +\`\`\` + + +**Example (Python):** +\`\`\`python +client.on_event({ + \\"envelope_id\\": \\"6d0b4caf-95d3-410c-b85d-ce592e622aea\\", + \\"payload\\": { + \\"token\\": \\"el8A2aGi0yIJ2so0ENvWcAfX\\", + \\"team_id\\": \\"T34F2JRQU\\", + \\"context_team_id\\": \\"T34F2JRQU\\", + \\"context_enterprise_id\\": null, + \\"api_app_id\\": \\"A08NKKBFGBD\\", + \\"event\\": { + \\"type\\": \\"reaction_added\\", + \\"user\\": \\"UD698Q5LM\\", + \\"reaction\\": \\"beers\\", + \\"item\\": { + \\"type\\": \\"message\\", + \\"channel\\": \\"C072JMTJ85Q\\", + \\"ts\\": \\"1744903248.779009\\" + }, + \\"event_ts\\": \\"1744903279.021400\\" + }, + \\"type\\": \\"event_callback\\", + \\"event_id\\": \\"Ev08NRBU37QB\\", + \\"event_time\\": 1744903279, + \\"authorizations\\": [ + { + \\"enterprise_id\\": null, + \\"team_id\\": \\"T34F2JRQU\\", + \\"user_id\\": \\"U08NR6C69R8\\", + \\"is_bot\\": true, + \\"is_enterprise_install\\": false + } + ], + \\"is_ext_shared_channel\\": false, + \\"event_context\\": \\"4-eyJldCI6InJlYWN0aW9uX2FkZGVkIiwidGlkIjoiVDM0RjJKUlFVIiwiYWlkIjoiQTA4TktLQkZHQkQiLCJjaWQiOiJDMDcySk1USjg1USJ9\\" + }, + \\"type\\": \\"events_api\\", + \\"accepts_response_payload\\": false, + \\"retry_attempt\\": 3, + \\"retry_reason\\": \\"timeout\\" +}) +\`\`\` + + +**Example (JavaScript):** +\`\`\`javascript +client.onEvent({ + \\"envelope_id\\": \\"aefc571f-0d8f-4f2a-a5e7-6acc7f986816\\", + \\"payload\\": { + \\"token\\": \\"el8A2aGi0yIJ2so0ENvWcAfX\\", + \\"team_id\\": \\"T34F2JRQU\\", + \\"context_team_id\\": \\"T34F2JRQU\\", + \\"context_enterprise_id\\": null, + \\"api_app_id\\": \\"A08NKKBFGBD\\", + \\"event\\": { + \\"type\\": \\"message\\", + \\"subtype\\": \\"message_deleted\\", + \\"previous_message\\": { + \\"user\\": \\"U08KSJS9Z4N\\", + \\"type\\": \\"message\\", + \\"ts\\": \\"1744904504.505559\\", + \\"client_msg_id\\": \\"c6d9111e-f9df-4b52-ba17-7bbb04255243\\", + \\"text\\": \\"Hi can we connect on linkedin\\", + \\"team\\": \\"T34F2JRQU\\", + \\"blocks\\": [ + { + \\"type\\": \\"rich_text\\", + \\"block_id\\": \\"gCBVb\\", + \\"elements\\": [ + { + \\"type\\": \\"rich_text_section\\", + \\"elements\\": [ + { + \\"type\\": \\"text\\", + \\"text\\": \\"Hi can we connect on linkedin\\" + } + ] + } + ] + } + ] + }, + \\"channel\\": \\"C072JMTJ85Q\\", + \\"hidden\\": true, + \\"deleted_ts\\": \\"1744904504.505559\\", + \\"event_ts\\": \\"1744904513.021500\\", + \\"ts\\": \\"1744904513.021500\\", + \\"channel_type\\": \\"channel\\" + }, + \\"type\\": \\"event_callback\\", + \\"event_id\\": \\"Ev08N76U6JHM\\", + \\"event_time\\": 1744904513, + \\"authorizations\\": [ + { + \\"enterprise_id\\": null, + \\"team_id\\": \\"T34F2JRQU\\", + \\"user_id\\": \\"U08NR6C69R8\\", + \\"is_bot\\": true, + \\"is_enterprise_install\\": false + } + ], + \\"is_ext_shared_channel\\": false, + \\"event_context\\": \\"4-eyJldCI6Im1lc3NhZ2UiLCJ0aWQiOiJUMzRGMkpSUVUiLCJhaWQiOiJBMDhOS0tCRkdCRCIsImNpZCI6IkMwNzJKTVRKODVRIn0\\" + }, + \\"type\\": \\"events_api\\", + \\"accepts_response_payload\\": false, + \\"retry_attempt\\": 3, + \\"retry_reason\\": \\"timeout\\" +}) +\`\`\` + + +**Example (Python):** +\`\`\`python +client.on_event({ + \\"envelope_id\\": \\"aefc571f-0d8f-4f2a-a5e7-6acc7f986816\\", + \\"payload\\": { + \\"token\\": \\"el8A2aGi0yIJ2so0ENvWcAfX\\", + \\"team_id\\": \\"T34F2JRQU\\", + \\"context_team_id\\": \\"T34F2JRQU\\", + \\"context_enterprise_id\\": null, + \\"api_app_id\\": \\"A08NKKBFGBD\\", + \\"event\\": { + \\"type\\": \\"message\\", + \\"subtype\\": \\"message_deleted\\", + \\"previous_message\\": { + \\"user\\": \\"U08KSJS9Z4N\\", + \\"type\\": \\"message\\", + \\"ts\\": \\"1744904504.505559\\", + \\"client_msg_id\\": \\"c6d9111e-f9df-4b52-ba17-7bbb04255243\\", + \\"text\\": \\"Hi can we connect on linkedin\\", + \\"team\\": \\"T34F2JRQU\\", + \\"blocks\\": [ + { + \\"type\\": \\"rich_text\\", + \\"block_id\\": \\"gCBVb\\", + \\"elements\\": [ + { + \\"type\\": \\"rich_text_section\\", + \\"elements\\": [ + { + \\"type\\": \\"text\\", + \\"text\\": \\"Hi can we connect on linkedin\\" + } + ] + } + ] + } + ] + }, + \\"channel\\": \\"C072JMTJ85Q\\", + \\"hidden\\": true, + \\"deleted_ts\\": \\"1744904504.505559\\", + \\"event_ts\\": \\"1744904513.021500\\", + \\"ts\\": \\"1744904513.021500\\", + \\"channel_type\\": \\"channel\\" + }, + \\"type\\": \\"event_callback\\", + \\"event_id\\": \\"Ev08N76U6JHM\\", + \\"event_time\\": 1744904513, + \\"authorizations\\": [ + { + \\"enterprise_id\\": null, + \\"team_id\\": \\"T34F2JRQU\\", + \\"user_id\\": \\"U08NR6C69R8\\", + \\"is_bot\\": true, + \\"is_enterprise_install\\": false + } + ], + \\"is_ext_shared_channel\\": false, + \\"event_context\\": \\"4-eyJldCI6Im1lc3NhZ2UiLCJ0aWQiOiJUMzRGMkpSUVUiLCJhaWQiOiJBMDhOS0tCRkdCRCIsImNpZCI6IkMwNzJKTVRKODVRIn0\\" + }, + \\"type\\": \\"events_api\\", + \\"accepts_response_payload\\": false, + \\"retry_attempt\\": 3, + \\"retry_reason\\": \\"timeout\\" +}) +\`\`\` + + + + +#### \`onDisconnectMessage(payload)\` + + + +**Example (JavaScript):** +\`\`\`javascript +client.onDisconnectMessage({ + \\"type\\": \\"disconnect\\", + \\"reason\\": \\"warning\\", + \\"debug_info\\": { + \\"host\\": \\"applink-0\\" + } +}) +\`\`\` + + +**Example (Python):** +\`\`\`python +client.on_disconnect_message({ + \\"type\\": \\"disconnect\\", + \\"reason\\": \\"warning\\", + \\"debug_info\\": { + \\"host\\": \\"applink-0\\" + } +}) +\`\`\` + + +**Example (JavaScript):** +\`\`\`javascript +client.onDisconnectMessage({ + \\"type\\": \\"disconnect\\", + \\"reason\\": \\"refresh_requested\\", + \\"debug_info\\": { + \\"host\\": \\"applink-0\\" + } +}) +\`\`\` + + +**Example (Python):** +\`\`\`python +client.on_disconnect_message({ + \\"type\\": \\"disconnect\\", + \\"reason\\": \\"refresh_requested\\", + \\"debug_info\\": { + \\"host\\": \\"applink-0\\" + } +}) +\`\`\` + + + + +" +`; + +exports[`WebSocket Clients Integration Tests JavaScript Client Additional tests for JavaScript client generate client for slack: asyncapi.yaml 1`] = ` +"asyncapi: 3.0.0 +info: + title: Slack Websocket API Client + version: 1.0.0 + externalDocs: + description: Slack Websocket API + url: 'https://api.slack.com/apis/socket-mode' + description: > + AsyncAPI document that represent the API of the application that is a client + of the official Slack Websocket API To start using the Slack Websocket API, + you need to create a Slack app and install it in your workspace. Make sure + that the app is approved by the admin of the workspace. + + 1. Create app in Slack: https://api.slack.com/apps. Do it by selecting + option to create from scratch. Provide a name and select the workspace where + the app will be used. + + 1. Enable Socket Mode: Go to the \\"Socket Mode\\" section and enable the API. + Follow all steps and save generated token for later. By default you are not + yet subscribed to any events. + + 1. Subscribe to events: Go to the \\"Event Subscriptions\\" section and enable + the API. Subscribe to the bot events you want to receive. For example, you + can subscribe to the \`reaction_added\` event. Remember to save changes. + + 1. Install the app: Go to the \\"Install App\\" section and install the app in + your workspace. After that make sure admin approved it for the workspace. + + 1. Generate WebSocket URL: Make a REST call to the endpoint that will + generate the WebSocket URL. Use the \`apps.connections.open\` path from + Slack's API. The response will contain a \`url\` field with the WebSocket URL. + Use this URL to connect to the WebSocket API. + \`\`\` + curl --location --request POST 'https://slack.com/api/apps.connections.open' \\\\ + --header 'Authorization: Bearer USE_TOKEN_GENERATED_DURING_SOCKET_MODE_ENABLEMENT' \\\\ + \`\`\` + + 1. Generated URL is the WebSocket URL you need to use to connect to the + Slack WebSocket API. The URL will look like this: + \`\`\` + wss://wss-primary.slack.com/link?app_id=YOUR_APP_ID&ticket=YOUR_TICKET +servers: + production: + host: wss-primary.slack.com + pathname: /link + protocol: wss + description: Slack's server in Socket Mode for real-time communication + security: + - type: http + scheme: bearer + bearerFormat: OAuth2 + description: | + First you need to obtain a token and connection URL by making a HTTP request to \`https://slack.com/api/apps.connections.open\`. In response you receive WebSocket connection link that includes query parameters containing authorization information. Each time you connect with the WebSocket API, you need to generate a new connection link. +channels: + root: + address: / + messages: + hello: + $ref: '#/components/messages/hello' + event: + $ref: '#/components/messages/event' + acknowledge: + $ref: '#/components/messages/acknowledge' + disconnect: + $ref: '#/components/messages/disconnect' + bindings: + ws: + query: + title: connectionQueryParams + type: object + description: >- + Tokens are produced in the WebSocket URL generated from the + [apps.connections.open](https://api.slack.com/methods/apps.connections.open) + method from Slack's API + properties: + ticket: + type: string + description: Temporary token generated when connection is initiated + app_id: + type: string + description: Unique identifier assigned to the Slack app +operations: + onHelloMessage: + summary: One time operation. You receive one message once you connect to the API + action: receive + channel: + $ref: '#/channels/root' + messages: + - $ref: '#/channels/root/messages/hello' + onEvent: + action: receive + channel: + $ref: '#/channels/root' + messages: + - $ref: '#/channels/root/messages/event' + reply: + messages: + - $ref: '#/channels/root/messages/acknowledge' + channel: + $ref: '#/channels/root' + onDisconnectMessage: + action: receive + channel: + $ref: '#/channels/root' + messages: + - $ref: '#/channels/root/messages/disconnect' +components: + messages: + event: + summary: Event message representing different event types + payload: + $ref: '#/components/schemas/event' + examples: + - summary: New member joined channel + payload: + envelope_id: c044c358-d17e-4f6d-94aa-3ddadb82ef59 + payload: + token: el8A2aGi0yIJ2so0ENvWcAfX + team_id: T34F2JRQU + context_team_id: T34F2JRQU + context_enterprise_id: null + api_app_id: A08NKKBFGBD + event: + subtype: channel_join + user: U08NR6C69R8 + text: <@U08NR6C69R8> has joined the channel + inviter: UD698Q5LM + type: message + ts: '1744903248.779009' + channel: C072JMTJ85Q + event_ts: '1744903248.779009' + channel_type: channel + type: event_callback + event_id: Ev08NDP302UE + event_time: 1744903248 + authorizations: + - enterprise_id: null + team_id: T34F2JRQU + user_id: U08NR6C69R8 + is_bot: true + is_enterprise_install: false + is_ext_shared_channel: false + event_context: >- + 4-eyJldCI6Im1lc3NhZ2UiLCJ0aWQiOiJUMzRGMkpSUVUiLCJhaWQiOiJBMDhOS0tCRkdCRCIsImNpZCI6IkMwNzJKTVRKODVRIn0 + type: events_api + accepts_response_payload: false + retry_attempt: 0 + retry_reason: '' + - summary: Reaction added + payload: + envelope_id: 6d0b4caf-95d3-410c-b85d-ce592e622aea + payload: + token: el8A2aGi0yIJ2so0ENvWcAfX + team_id: T34F2JRQU + context_team_id: T34F2JRQU + context_enterprise_id: null + api_app_id: A08NKKBFGBD + event: + type: reaction_added + user: UD698Q5LM + reaction: beers + item: + type: message + channel: C072JMTJ85Q + ts: '1744903248.779009' + event_ts: '1744903279.021400' + type: event_callback + event_id: Ev08NRBU37QB + event_time: 1744903279 + authorizations: + - enterprise_id: null + team_id: T34F2JRQU + user_id: U08NR6C69R8 + is_bot: true + is_enterprise_install: false + is_ext_shared_channel: false + event_context: >- + 4-eyJldCI6InJlYWN0aW9uX2FkZGVkIiwidGlkIjoiVDM0RjJKUlFVIiwiYWlkIjoiQTA4TktLQkZHQkQiLCJjaWQiOiJDMDcySk1USjg1USJ9 + type: events_api + accepts_response_payload: false + retry_attempt: 3 + retry_reason: timeout + - summary: Message deleted + payload: + envelope_id: aefc571f-0d8f-4f2a-a5e7-6acc7f986816 + payload: + token: el8A2aGi0yIJ2so0ENvWcAfX + team_id: T34F2JRQU + context_team_id: T34F2JRQU + context_enterprise_id: null + api_app_id: A08NKKBFGBD + event: + type: message + subtype: message_deleted + previous_message: + user: U08KSJS9Z4N + type: message + ts: '1744904504.505559' + client_msg_id: c6d9111e-f9df-4b52-ba17-7bbb04255243 + text: Hi can we connect on linkedin + team: T34F2JRQU + blocks: + - type: rich_text + block_id: gCBVb + elements: + - type: rich_text_section + elements: + - type: text + text: Hi can we connect on linkedin + channel: C072JMTJ85Q + hidden: true + deleted_ts: '1744904504.505559' + event_ts: '1744904513.021500' + ts: '1744904513.021500' + channel_type: channel + type: event_callback + event_id: Ev08N76U6JHM + event_time: 1744904513 + authorizations: + - enterprise_id: null + team_id: T34F2JRQU + user_id: U08NR6C69R8 + is_bot: true + is_enterprise_install: false + is_ext_shared_channel: false + event_context: >- + 4-eyJldCI6Im1lc3NhZ2UiLCJ0aWQiOiJUMzRGMkpSUVUiLCJhaWQiOiJBMDhOS0tCRkdCRCIsImNpZCI6IkMwNzJKTVRKODVRIn0 + type: events_api + accepts_response_payload: false + retry_attempt: 3 + retry_reason: timeout + hello: + summary: Message triggered when a successful WebSocket connection is established + description: > + One time message that helps undestand connection was successful. Use the + \`approximate_connection_time\` (in seconds) to estimate how long the + connection will persist until Slack refreshes it. + payload: + $ref: '#/components/schemas/hello' + examples: + - payload: + type: hello + num_connections: 1 + debug_info: + host: applink-8 + build_number: 105 + approximate_connection_time: 18060 + connection_info: + app_id: A08NKKBFGBD + acknowledge: + summary: Acknowledgement response sent to Server + payload: + $ref: '#/components/schemas/acknowledge' + examples: + - payload: + envelope_id: bb491378-bb91-48d2-b082-777a7e4e9663 + disconnect: + summary: >- + Message you receive to warn you the connection with the API will be + terminated + payload: + $ref: '#/components/schemas/disconnect' + examples: + - payload: + type: disconnect + reason: warning + debug_info: + host: applink-0 + - payload: + type: disconnect + reason: refresh_requested + debug_info: + host: applink-0 + schemas: + hello: + type: object + discriminator: type + properties: + type: + type: string + const: hello + description: A hello string confirming WebSocket connection + connection_info: + title: connectionInfo + type: object + properties: + app_id: + type: string + num_connections: + type: integer + debug_info: + $ref: '#/components/schemas/debugInfo' + + event: + type: object + discriminator: type + additionalProperties: false + properties: + envelope_id: + type: string + description: Unique ID assigned to payload + payload: + title: eventPayload + type: object + description: >- + Payload of the event with some differences per event type inside + event object + additionalProperties: false + properties: + token: + type: string + description: >- + The shared-private callback token that authenticates this + callback to the application as having come from Slack. Match + this against what you were given when the subscription was + created. If it does not match, do not process the event and + discard it. Example: JhjZd2rVax7ZwH7jRYyWjbDl + team_id: + type: string + description: >- + The unique identifier for the workspace/team where this event + occurred. + example: T34F2JRQU + context_team_id: + type: string + description: >- + The unique identifier for the workspace/team where this event + occurred. + example: T461EG9ZZ + context_enterprise_id: + type: + - string + - 'null' + description: >- + The unique identifier for the enterprise where this event + occurred. + example: E1234567890 + api_app_id: + type: string + description: >- + The unique identifier for the application this event is intended + for. Your application's ID can be found in the URL of the your + application console. If your Request URL manages multiple + applications, use this field along with the token field to + validate and route incoming requests. + example: A4ZFV49KK + event: + title: eventData + type: object + oneOf: + - $ref: '#/components/schemas/channelJoin' + - $ref: '#/components/schemas/reactionAdded' + - $ref: '#/components/schemas/messageDeleted' + properties: + type: + type: string + description: >- + The specific name of the event described by its adjacent + fields. This field is included with every inner event type. + example: reaction_added + event_ts: + type: string + description: >- + The timestamp of the event. The combination of + event_ts,team_id, user_id, or channel_id is intended to be + unique. This field is included with every inner event type. + example: '1469470591.759709' + type: + type: string + description: >- + This reflects the type of callback you are receiving. Typically, + that is event_callback. You may encounter url_verification + during the configuration process. The event field \\"inner event\\" + will also contain a type field indicating which event type lurks + within + event_id: + type: string + description: >- + A unique identifier for this specific event, globally unique + across all workspaces. + event_time: + type: integer + description: >- + The epoch timestamp in seconds indicating when this event was + dispatched. + event_context: + type: string + description: >- + An identifier for this specific event. This field can be used + with the apps.event.authorizations.list method to obtain a full + list of installations of your app for which this event is + visible. + authorizations: + type: array + items: + title: authorization + type: object + properties: + enterprise_id: + type: + - string + - 'null' + description: >- + The unique identifier for the enterprise where this event + occurred. + team_id: + type: string + description: >- + The unique identifier for the workspace/team where this + event occurred. + user_id: + type: string + description: >- + The unique identifier for the user who triggered this + event. + is_bot: + type: boolean + description: Whether the user is a bot or not + is_enterprise_install: + type: boolean + description: Whether the app is installed in an enterprise or not + is_ext_shared_channel: + type: boolean + description: Whether the event is from an external shared channel or not + type: + type: string + const: events_api + accepts_response_payload: + type: boolean + retry_attempt: + description: >- + How many times the event was retried and no acknowledgement was + received by the server. + type: integer + retry_reason: + type: string + description: Reason for retrying the event + acknowledge: + type: object + properties: + envelope_id: + type: string + description: Unique ID of acknowledged payload + payload: + type: object + description: Optional payload of event + disconnect: + type: object + discriminator: type + properties: + type: + type: string + const: disconnect + reason: + title: disconnectReasonEnum + type: string + enum: + - link_disabled + - warning + - refresh_requested + debug_info: + $ref: '#/components/schemas/debugInfo' + channelJoin: + additionalProperties: false + properties: + type: + const: message + subtype: + type: string + const: channel_join + inviter: + type: string + description: The user ID of the user that invited the new member to the channel + example: U123456789 + text: + type: string + description: The text of the message that was sent to the channel + example: <@U2147483828|cal> has joined the channel + user: + type: string + description: >- + The user ID belonging to the user that incited this action. Not + included in all events as not all events are controlled by users. + example: UD698Q5LM + ts: + type: string + description: Timestamp information of original message + channel_type: + type: string + description: Type of channel where the event occurred + example: channel + reactionAdded: + additionalProperties: false + properties: + type: + const: reaction_added + reaction: + type: string + description: The only reaction that you need is a heart emoji + example: beers + user: + type: string + description: >- + The user ID belonging to the user that incited this action. Not + included in all events as not all events are controlled by users. + example: UD698Q5LM + item: + type: object + properties: + channel: + type: string + description: Channel information of original message + ts: + type: string + description: Timestamp information of original message + messageDeleted: + additionalProperties: false + properties: + type: + type: string + const: message + subtype: + const: message_deleted + deleted_ts: + type: string + description: Gives the timestamp of the message that was deleted. + channel: + type: string + description: Information in which channel the message was deleted + ts: + type: string + description: Timestamp information of original message + channel_type: + type: string + description: Type of channel where the event occurred + example: channel + previous_message: + type: object + properties: + user: + type: string + description: >- + The user ID belonging to the user that incited this action. Not + included in all events as not all events are controlled by + users. + example: UD698Q5LM + ts: + type: string + description: Timestamp information of original message + example: 1744904504.505559 + client_msg_id: + type: string + description: Client message ID of original message + example: c6d9111e-f9df-4b52-ba17-7bbb04255243 + text: + type: string + description: Text of original message + example: Hi can we connect on linkedin + team: + type: string + description: Team ID of original message + example: T34F2JRQU + blocks: + type: array + items: + type: object + properties: + type: + type: string + description: Type of block in the message + example: rich_text + block_id: + type: string + description: Block ID of the block in the message + example: gCBVb + elements: + type: array + items: + type: object + properties: + type: + type: string + description: Type of element in the block + example: text + text: + type: string + description: Text of the element in the block + example: Hi can we connect on linkedin + debugInfo: + type: object + properties: + host: + type: string + started: + type: string + build_number: + type: integer + approximate_connection_time: + type: integer" +`; + +exports[`WebSocket Clients Integration Tests JavaScript Client Additional tests for JavaScript client generate client for slack: client.js 1`] = ` +"////////////////////////////////////////////////// +// +// Slack Websocket API Client - 1.0.0 +// Protocol: wss +// Host: wss-primary.slack.com +// Path: /link +// +////////////////////////////////////////////////// + +const WebSocket = require('ws'); +const { compileSchemasByOperationId, validateMessage } = require('@asyncapi/keeper'); +const path = require('path'); +const asyncapiFilepath = path.resolve(__dirname, './asyncapi.yaml'); +const querystring = require('querystring'); +class SlackWebsocketAPIClient { + + /* + * Constructor to initialize the WebSocket client + * @param {string} url - The WebSocket server URL. Use it if the server URL is different from the default one taken from the AsyncAPI document. + * @param {string} ticket - If provided (or if TICKET environment variable is set), added as ?ticket=… to URL + + * @param {string} appId - If provided (or if APP_ID environment variable is set), added as ?app_id=… to URL + + * @param {boolean} [throwSendErrors=true] - Controls how the instance send methods react to a send failure. + * When true (default) the error is re-thrown after the registered error handlers run, so the caller can + * handle each failure. When false the error is suppressed after the handlers run, which keeps a + * high-throughput producer loop going. + */ + constructor(url, ticket, appId, throwSendErrors = true) { + this.url = url || 'wss://wss-primary.slack.com/link'; + const params = {}; + const _ticket = ticket || process.env.TICKET; + if (_ticket) { + params[\\"ticket\\"] = _ticket; + } + const _app_id = appId || process.env.APP_ID; + if (_app_id) { + params[\\"app_id\\"] = _app_id; + } + const queryString = querystring.stringify(params); + if (queryString) { + this.url += '?' + queryString; + } + + this.websocket = null; + this.messageHandlers = []; + this.errorHandlers = []; + this.outgoingProcessors = []; + this.compiledSchemas = {}; + this.schemasCompiled = false; + this.sendOperationsId = []; + this.throwSendErrors = throwSendErrors; // Re-throw send failures after handlers run + } + // Method to establish a WebSocket connection + connect() { + return new Promise((resolve, reject) => { + this.websocket = new WebSocket(this.url); + // On successful connection + this.websocket.onopen = () => { + console.log('Connected to Slack Websocket API Client server'); + resolve(); + }; + + // On receiving a message + this.websocket.onmessage = (event) => { + if (this.messageHandlers.length > 0) { + // Call custom message handlers + this.messageHandlers.forEach(handler => { + if (typeof handler === 'function') { + this.handleMessage(event.data, handler); + } + }); + } else { + // Default message logging + console.log('Message received:', event.data); + } + }; + + // On error first call custom error handlers, then default error behavior + this.websocket.onerror = (error) => { + if (this.errorHandlers.length > 0) { + // Call custom error handlers + this.errorHandlers.forEach(handler => handler(error)); + } else { + // Default error behavior + console.error('WebSocket Error:', error); + } + reject(error); + }; + + // On connection close + this.websocket.onclose = () => { + console.log('Disconnected from Slack Websocket API Client server'); + }; + + }); + } + + // Method to register custom message handlers + registerMessageHandler(handler) { + if (typeof handler === 'function') { + this.messageHandlers.push(handler); + } else { + console.warn('Message handler must be a function'); + } + } + + // Method to register custom error handlers + registerErrorHandler(handler) { + if (typeof handler === 'function') { + this.errorHandlers.push(handler); + } else { + console.warn('Error handler must be a function'); + } + } + + registerOutgoingProcessor(processor) { + if (typeof processor === 'function') { + this.outgoingProcessors.push(processor); + } else { + console.warn('Outgoing processor must be a function'); + } + } + + // Method to handle message with callback + handleMessage(message, cb) { + if (cb) cb(message); + } + + // Method to close the WebSocket connection + close() { + if (this.websocket) { + this.websocket.close(); + console.log('WebSocket connection closed.'); + } + } +} +module.exports = SlackWebsocketAPIClient; + +" +`; + exports[`WebSocket Clients Integration Tests JavaScript Client Common Integration tests for JavaScript client generation generate simple client for hoppscotch echo with custom client name: README.md 1`] = ` "# Hoppscotch Echo WebSocket Client diff --git a/packages/templates/clients/websocket/test/integration-test/integration.test.js b/packages/templates/clients/websocket/test/integration-test/integration.test.js index 0e2eea3e0e..f852aba2ad 100644 --- a/packages/templates/clients/websocket/test/integration-test/integration.test.js +++ b/packages/templates/clients/websocket/test/integration-test/integration.test.js @@ -91,6 +91,7 @@ describe('WebSocket Clients Integration Tests', () => { describe('JavaScript Client', () => { const config = languageConfig.javascript; runCommonTests('JavaScript', config); + runCommonSlackTests('JavaScript', config); describe('Additional tests for JavaScript client', () => { it('generate simple client for hoppscotch echo without clientFileName param', async () => {