-
-
Notifications
You must be signed in to change notification settings - Fork 393
feat: add query Parameters support to javascript websocket client #2142
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
d19c673
50cdcef
aafe462
f790e68
67be77d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,33 +1,52 @@ | ||
| import { Text } from '@asyncapi/generator-react-sdk'; | ||
| import { QueryParamsVariables } from '@asyncapi/generator-components'; | ||
| import { InitSignature } from './InitSignature'; | ||
| import { QueryParamsArgumentsDocs } from './QueryParamsArgumentsDocs'; | ||
|
|
||
| export function Constructor({ serverUrl, sendOperations }) { | ||
| export function Constructor({ serverUrl, queryParams, sendOperations }) { | ||
| const sendOperationsId = sendOperations.map((operation) => operation.id()); | ||
| const sendOperationsArray = JSON.stringify(sendOperationsId); | ||
| const queryParamsArray = queryParams && Array.from(queryParams.entries()); | ||
|
|
||
| return ( | ||
| <Text indent={2}> | ||
| { | ||
| `/* | ||
| {`/* | ||
| * 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. | ||
| `} | ||
| <QueryParamsArgumentsDocs queryParams={queryParamsArray} /> | ||
| {` * @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 | ||
| `} | ||
| <InitSignature queryParams={queryParamsArray} /> | ||
| <Text indent={2} newLines={1}> | ||
| {`this.url = url || '${serverUrl}'; | ||
| `} | ||
| {queryParamsArray && queryParamsArray.length > 0 && ( | ||
| <Text> | ||
| {`const params = {}; | ||
| `} | ||
| <QueryParamsVariables language="javascript" queryParams={queryParamsArray} /> | ||
| {`const queryString = querystring.stringify(params); | ||
| if (queryString) { | ||
| this.url += '?' + queryString; | ||
| } | ||
| ` | ||
| } | ||
| `} | ||
| </Text> | ||
| )} | ||
| {`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`} | ||
| </Text> | ||
| {'}'} | ||
| </Text> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| 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<Array<string>>} [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 ( | ||
| <Text indent={0}> | ||
| {'constructor(url, throwSendErrors = true) {'} | ||
| </Text> | ||
| ); | ||
| } | ||
|
|
||
| const usedNames = new Set(); | ||
| const queryParamsArguments = queryParams.map((param) => { | ||
| const paramName = getSafeJSName(param[0], usedNames); | ||
| const paramDefaultValue = param[1]; | ||
| 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(', '); | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| return ( | ||
| <Text indent={0}> | ||
| {`constructor(url, ${queryParamsArguments}, throwSendErrors = true) {`} | ||
| </Text> | ||
| ); | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
coderabbitai[bot] marked this conversation as resolved.
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| 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<Array<string>>} [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; | ||
| } | ||
|
|
||
| const usedNames = new Set(); | ||
| return queryParams.map((param) => { | ||
| const originalParamName = param[0]; | ||
| 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`; | ||
| return ( | ||
| <Text indent={2}> | ||
| {`${firstLine}${secondLine}\n`} | ||
| </Text> | ||
| ); | ||
| }); | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| 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). | ||
| * @param {Set<string>} [usedNames=new Set()] - A set of already used names to prevent collisions. | ||
| * @returns {string} A safe JavaScript identifier. | ||
| */ | ||
| export function getSafeJSName(name, usedNames = new Set()) { | ||
| 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}`; | ||
|
|
||
| let candidate = safe; | ||
| let suffix = 1; | ||
| while (usedNames.has(candidate)) { | ||
| candidate = `${safe}_${suffix++}`; | ||
| } | ||
| usedNames.add(candidate); | ||
|
|
||
| return candidate; | ||
| } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Firstly I don't get point of this getSafeJsName becoz we can't just change param name given in the input asyncapi file. Secondly it is not a component it should be in file https://github.com/asyncapi/generator/blob/master/packages/helpers/src/utils.js
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The main use of getSafeJsName is, if the paramName that the user has sent is, for example, Sure thing, I should have put this in the utils.js. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| const WSClient = require('./test/temp/snapshotTestResult/client_slack/client.js'); | ||
| // Example usage | ||
| const wsClient = new WSClient(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
rg -n -C 6 'new WSClient|constructor\(|query|ticket|app_id' \
packages/templates/clients/websocket/javascript \
packages/templates/clients/websocket/testRepository: asyncapi/generator Length of output: 50375 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo '--- tracked Slack-related files ---'
git ls-files packages/templates/clients/websocket | grep -Ei 'slack|fixture|snapshot' | head -80
echo '--- focused generated-client references ---'
rg -n -C 5 'class Slack|new WSClient|wss-primary\.slack\.com/link|ticket.*app_id|app_id.*ticket' \
packages/templates/clients/websocket --glob '!**/*.python.snap' --glob '!**/node_modules/**' | head -240
echo '--- constructor generation ---'
sed -n '1,90p' packages/templates/clients/websocket/javascript/components/InitSignature.js
sed -n '1,90p' packages/templates/clients/websocket/javascript/components/Constructor.js
echo '--- example ---'
cat -n packages/templates/clients/websocket/javascript/example-slack.jsRepository: asyncapi/generator Length of output: 27822 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo '--- Slack fixture ---'
cat -n packages/templates/clients/websocket/test/__fixtures__/asyncapi-slack-client.yml | sed -n '1,90p'
echo '--- generated JavaScript constructor snapshot ---'
sed -n '1106,1165p' packages/templates/clients/websocket/test/integration-test/__snapshots__/integration.test.js.javascript.snap
echo '--- JavaScript Slack documentation and example references ---'
find packages/templates/clients/websocket/javascript -maxdepth 2 -type f -print | sort
rg -n -C 8 'apps\.connections\.open|ticket|app_id|example-slack|client_slack' \
packages/templates/clients/websocket/javascript \
packages/templates/clients/websocket/test/__fixtures__/asyncapi-slack-client.yml \
packages/templates/clients/websocket/test/integration-test/__snapshots__/integration.test.js.javascript.snapRepository: asyncapi/generator Length of output: 50376 🏁 Script executed: #!/bin/bash
set -euo pipefail
python3 - <<'PY'
import re
from pathlib import Path
fixture = Path("packages/templates/clients/websocket/test/__fixtures__/asyncapi-slack-client.yml").read_text()
snapshot = Path(
"packages/templates/clients/websocket/test/integration-test/"
"__snapshots__/integration.test.js.javascript.snap"
).read_text()
fixture_section = fixture.split("properties:", 1)[1].split("operations:", 1)[0]
assert re.search(r"^\s+ticket:\s*$", fixture_section, re.MULTILINE)
assert re.search(r"^\s+app_id:\s*$", fixture_section, re.MULTILINE)
constructor = re.search(
r"constructor\(([^)]*)\) \{(?P<body>.*?)\n \}",
snapshot,
re.DOTALL,
)
assert constructor, "Slack constructor not found"
signature = constructor.group(1)
body = constructor.group("body")
assert signature == "url, ticket, appId, throwSendErrors = true"
assert "ticket = " not in signature
assert "appId = " not in signature
assert "this.url = url || 'wss://wss-primary.slack.com/link';" in body
assert "params[\\\\\"ticket\\\\\"] = _ticket;" in body
assert "params[\\\\\"app_id\\\\\"] = _app_id;" in body
# Model the constructor's zero-argument path without executing repository code.
url = "wss://wss-primary.slack.com/link"
ticket = None
app_id = None
if ticket or None:
url += "?ticket=" + ticket
if app_id or None:
url += ("&" if "?" in url else "?") + "app_id=" + app_id
print("constructor signature:", signature)
print("zero-argument URL:", url)
print("zero-argument URL has ticket:", "ticket=" in url)
print("zero-argument URL has app_id:", "app_id=" in url)
assert url == "wss://wss-primary.slack.com/link"
PYRepository: asyncapi/generator Length of output: 346 🏁 Script executed: #!/bin/bash
set -euo pipefail
python3 - <<'PY'
import re
from pathlib import Path
fixture = Path("packages/templates/clients/websocket/test/__fixtures__/asyncapi-slack-client.yml").read_text()
snapshot = Path(
"packages/templates/clients/websocket/test/integration-test/"
"__snapshots__/integration.test.js.javascript.snap"
).read_text()
fixture_section = fixture.split("properties:", 1)[1].split("operations:", 1)[0]
assert re.search(r"^\s+ticket:\s*$", fixture_section, re.MULTILINE)
assert re.search(r"^\s+app_id:\s*$", fixture_section, re.MULTILINE)
constructor = re.search(
r"constructor\(([^)]*)\) \{(?P<body>.*?)\n \}",
snapshot,
re.DOTALL,
)
assert constructor, "Slack constructor not found"
signature = constructor.group(1)
body = constructor.group("body")
assert signature == "url, ticket, appId, throwSendErrors = true"
assert "ticket = " not in signature
assert "appId = " not in signature
assert "this.url = url || 'wss://wss-primary.slack.com/link';" in body
assert "params[\\\\\"ticket\\\\\"] = _ticket;" in body
assert "params[\\\\\"app_id\\\\\"] = _app_id;" in body
url = "wss://wss-primary.slack.com/link"
ticket = None
app_id = None
if ticket or None:
url += "?ticket=" + ticket
if app_id or None:
url += ("&" if "?" in url else "?") + "app_id=" + app_id
print("constructor signature:", signature)
print("zero-argument URL:", url)
print("zero-argument URL has ticket:", "ticket=" in url)
print("zero-argument URL has app_id:", "app_id=" in url)
assert url == "wss://wss-primary.slack.com/link"
PYRepository: asyncapi/generator Length of output: 346 Pass Slack’s generated connection URL to
🤖 Prompt for AI Agents |
||
|
|
||
| // 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(); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: asyncapi/generator
Length of output: 28865
🏁 Script executed:
Repository: asyncapi/generator
Length of output: 16195
🏁 Script executed:
Repository: asyncapi/generator
Length of output: 50374
🏁 Script executed:
Repository: asyncapi/generator
Length of output: 11961
Use safe constructor names in generated query code.
QueryParamsVariablesmust use the samegetSafeJSNamemapping asInitSignature. Names such asauth-token,1st, andurlcan otherwise produce invalid or duplicate declarations. Use the safe name for all local references,process.env[JSON.stringify(...)]for environment variables, andJSON.stringify(rawName)for query keys. Add regression tests for non-identifier, reserved, and colliding names.🤖 Prompt for AI Agents