mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-29 15:44:13 +00:00
* feat: add variable interpolation support for WebSocket requests
- Add WebSocket body interpolation in interpolateVars function
- Interpolate URL, headers, and all messages in request.body.ws array with full variable context
- Refactor sendWsRequest to use main process preparation (removes duplication)
- Add mode property to wsRequest object for proper request type detection
- Ensure consistent variable precedence matching HTTP/gRPC requests
- Centralize all interpolation logic in main process via prepareWsRequest
* Add Playwright tests for WebSocket variable interpolation
- Add tests for URL interpolation (wss://echo.{{url}}.org)
- Add tests for message content interpolation ({"test": "{{data}}"})
- Update test fixtures to use wss://echo.websocket.org echo server
- Add WEBSOCKET_FLOWS.md documentation
- Refactor queueWsMessage to handle variable interpolation in main process
* removed ws flow documentation
* chore: updated the network/index.js file to reduce merge conflicts by moving around code
* fix: added collection and item to WsQueryUrl Editor to fix available variable highlight
* chore: remove unnecessary whitespace in WebSocket event handlers
* feat: add automatic WebSocket reconnection on URL variable changes
- Detect changes to interpolated URL (including variable changes)
- Automatically disconnect and reconnect when interpolated URL changes
- Add debouncing (400ms) to prevent excessive reconnections
- Track previous interpolated URL to avoid unnecessary reconnects
- Store interpolated URL when connection becomes active
- Improve error handling and cleanup
* chore: removing diff
* Add WebSocket connection status IPC method
- Add connectionStatus() method to WsClient that returns detailed status
('disconnected', 'connecting', 'connected') instead of boolean
- Add renderer:ws:connection-status IPC handler in electron layer
- Add getWsConnectionStatus() utility function in network utils
- Provides more granular connection state information for UI components
* refactor: improve WebSocket connection status tracking in WsQueryUrl
- Replace boolean isConnectionActive with connectionStatus state ('disconnected', 'connecting', 'connected')
- Add useWsConnectionStatus hook to poll connection status every 2 seconds
- Refactor connection handlers: handleConnect, handleDisconnect, handleReconnect
- Update to use getWsConnectionStatus instead of isWsConnectionActive for more granular status
- Improve reconnect logic to handle URL variable interpolation changes
- Add proper connection status indicators in UI (connecting state with pulse animation)
* fix: improve WebSocket URL handling and request initialization
- Fix WebSocket URL state management by reading directly from item instead of local state
- Add handleUrlChange function to properly dispatch URL changes
- Fix interpolated URL change detection logic in useEffect
- Initialize params array for new WebSocket requests to prevent undefined errors
- Ensure params array is initialized when URL changes in draft/request
- Remove console.log statements and unused imports
- Update persistence test replacement URL to avoid port conflicts
These changes ensure WebSocket requests properly handle URL changes and
maintain consistent state between draft and saved requests.
* feat: refactor WebSocket connection status handling
---------
Co-authored-by: Sid <siddharth@usebruno.com>
360 lines
12 KiB
JavaScript
360 lines
12 KiB
JavaScript
import cloneDeep from 'lodash/cloneDeep';
|
|
import { resolvePath } from 'utils/filesystem';
|
|
|
|
export const sendNetworkRequest = async (item, collection, environment, runtimeVariables) => {
|
|
return new Promise((resolve, reject) => {
|
|
if (['http-request', 'graphql-request'].includes(item.type)) {
|
|
sendHttpRequest(item, collection, environment, runtimeVariables)
|
|
.then((response) => {
|
|
// if there is an error, we return the response object as is
|
|
if (response?.error) {
|
|
resolve(response)
|
|
}
|
|
|
|
resolve({
|
|
state: 'success',
|
|
data: response.data,
|
|
// Note that the Buffer is encoded as a base64 string, because Buffers / TypedArrays are not allowed in the redux store
|
|
dataBuffer: response.dataBuffer,
|
|
headers: response.headers,
|
|
size: response.size,
|
|
status: response.status,
|
|
statusText: response.statusText,
|
|
duration: response.duration,
|
|
timeline: response.timeline,
|
|
stream: response.stream
|
|
});
|
|
})
|
|
.catch((err) => reject(err));
|
|
}
|
|
});
|
|
};
|
|
|
|
export const sendGrpcRequest = async (item, collection, environment, runtimeVariables) => {
|
|
return new Promise((resolve, reject) => {
|
|
startGrpcRequest(item, collection, environment, runtimeVariables)
|
|
.then((initialState) => {
|
|
// Return an initial state object to update the UI
|
|
// The real response data will be handled by event listeners
|
|
resolve({
|
|
...initialState,
|
|
timeline: []
|
|
});
|
|
})
|
|
.catch((err) => reject(err));
|
|
});
|
|
}
|
|
|
|
|
|
|
|
const sendHttpRequest = async (item, collection, environment, runtimeVariables) => {
|
|
return new Promise((resolve, reject) => {
|
|
const { ipcRenderer } = window;
|
|
|
|
ipcRenderer
|
|
.invoke('send-http-request', item, collection, environment, runtimeVariables)
|
|
.then(resolve)
|
|
.catch(reject);
|
|
});
|
|
};
|
|
|
|
export const sendCollectionOauth2Request = async (collection, environment, runtimeVariables) => {
|
|
return new Promise((resolve, reject) => {
|
|
const { ipcRenderer } = window;
|
|
resolve({});
|
|
});
|
|
};
|
|
|
|
export const fetchGqlSchema = async (endpoint, environment, request, collection) => {
|
|
return new Promise((resolve, reject) => {
|
|
const { ipcRenderer } = window;
|
|
|
|
ipcRenderer.invoke('fetch-gql-schema', endpoint, environment, request, collection).then(resolve).catch(reject);
|
|
});
|
|
};
|
|
|
|
export const cancelNetworkRequest = async (cancelTokenUid) => {
|
|
return new Promise((resolve, reject) => {
|
|
const { ipcRenderer } = window;
|
|
|
|
ipcRenderer.invoke('cancel-http-request', cancelTokenUid).then(resolve).catch(reject);
|
|
});
|
|
};
|
|
|
|
export const startGrpcRequest = async (item, collection, environment, runtimeVariables) => {
|
|
return new Promise((resolve, reject) => {
|
|
const { ipcRenderer } = window;
|
|
const request = item.draft ? item.draft : item;
|
|
|
|
ipcRenderer.invoke('grpc:start-connection', {
|
|
request,
|
|
collection,
|
|
environment,
|
|
runtimeVariables
|
|
})
|
|
.then(() => {
|
|
resolve();
|
|
})
|
|
.catch(err => {
|
|
reject(err);
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Sends a message to an existing gRPC stream
|
|
* @param {string} requestId - The request ID to send a message to
|
|
* @param {Object} message - The message to send
|
|
* @returns {Promise<Object>} - The result of the send operation
|
|
*/
|
|
export const sendGrpcMessage = async (item, collectionUid, message) => {
|
|
return new Promise((resolve, reject) => {
|
|
const { ipcRenderer } = window;
|
|
ipcRenderer.invoke('grpc:send-message', item.uid, collectionUid, message)
|
|
.then(resolve)
|
|
.catch(reject);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Cancels a running gRPC request
|
|
* @param {string} requestId - The request ID to cancel
|
|
* @returns {Promise<Object>} - The result of the cancel operation
|
|
*/
|
|
export const cancelGrpcRequest = async (requestId) => {
|
|
return new Promise((resolve, reject) => {
|
|
const { ipcRenderer } = window;
|
|
ipcRenderer.invoke('grpc:cancel', requestId)
|
|
.then(resolve)
|
|
.catch(reject);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Ends a gRPC streaming request (client-streaming or bidirectional)
|
|
* @param {string} requestId - The request ID to end
|
|
* @returns {Promise<Object>} - The result of the end operation
|
|
*/
|
|
export const endGrpcStream = async (requestId) => {
|
|
return new Promise((resolve, reject) => {
|
|
const { ipcRenderer } = window;
|
|
ipcRenderer.invoke('grpc:end', requestId)
|
|
.then(resolve)
|
|
.catch(reject);
|
|
});
|
|
};
|
|
|
|
export const loadGrpcMethodsFromProtoFile = async (filePath, collection = null) => {
|
|
return new Promise(async (resolve, reject) => {
|
|
const { ipcRenderer } = window;
|
|
|
|
// Extract import paths from collection's gRPC config if available
|
|
let importPaths = [];
|
|
|
|
if (collection) {
|
|
const config = cloneDeep(collection.brunoConfig);
|
|
|
|
if (config.protobuf && config.protobuf.importPaths) {
|
|
// Use Promise.all to wait for all resolvePath calls to complete
|
|
const enabledImportPaths = config.protobuf.importPaths.filter((importPath) => importPath.enabled);
|
|
importPaths = await Promise.all(enabledImportPaths.map((importPath) => {
|
|
return resolvePath(importPath.path, collection.pathname);
|
|
}));
|
|
}
|
|
}
|
|
|
|
ipcRenderer.invoke('grpc:load-methods-proto', { filePath, includeDirs: importPaths }).then(resolve).catch(reject);
|
|
});
|
|
};
|
|
|
|
export const cancelGrpcConnection = async (connectionId) => {
|
|
return new Promise((resolve, reject) => {
|
|
const { ipcRenderer } = window;
|
|
ipcRenderer.invoke('grpc:cancel-request', { requestId: connectionId }).then(resolve).catch(reject);
|
|
});
|
|
};
|
|
|
|
export const endGrpcConnection = async (connectionId) => {
|
|
return new Promise((resolve, reject) => {
|
|
const { ipcRenderer } = window;
|
|
ipcRenderer.invoke('grpc:end-request', { requestId: connectionId }).then(resolve).catch(reject);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Check if a gRPC connection is active
|
|
* @param {string} connectionId - The connection ID to check
|
|
* @returns {Promise<boolean>} - Whether the connection is active
|
|
*/
|
|
export const isGrpcConnectionActive = async (connectionId) => {
|
|
return new Promise((resolve, reject) => {
|
|
const { ipcRenderer } = window;
|
|
ipcRenderer.invoke('grpc:is-connection-active', connectionId)
|
|
.then(response => {
|
|
if (response.success) {
|
|
resolve(response.isActive);
|
|
} else {
|
|
// If there was an error, assume the connection is not active
|
|
console.error('Error checking connection status:', response.error);
|
|
resolve(false);
|
|
}
|
|
})
|
|
.catch(err => {
|
|
console.error('Failed to check connection status:', err);
|
|
// On error, assume the connection is not active
|
|
resolve(false);
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Generates a sample gRPC message for a method
|
|
* @param {string} methodPath - The full gRPC method path
|
|
* @param {string|null} existingMessage - Optional existing message JSON string to use as a template
|
|
* @param {Object} options - Additional options for message generation
|
|
* @returns {Promise<Object>} The generated sample message or error
|
|
*/
|
|
export const generateGrpcSampleMessage = async (methodPath, existingMessage = null, options = {}) => {
|
|
return new Promise((resolve, reject) => {
|
|
const { ipcRenderer } = window;
|
|
|
|
ipcRenderer.invoke('grpc:generate-sample-message', {
|
|
methodPath,
|
|
existingMessage,
|
|
options
|
|
})
|
|
.then(resolve)
|
|
.catch(reject);
|
|
});
|
|
};
|
|
|
|
export const connectWS = async (item, collection, environment, runtimeVariables, options) => {
|
|
return new Promise((resolve, reject) => {
|
|
startWsConnection(item, collection, environment, runtimeVariables, options)
|
|
.then((initialState) => {
|
|
// Return an initial state object to update the UI
|
|
// The real response data will be handled by event listeners
|
|
resolve({
|
|
...initialState,
|
|
timeline: []
|
|
});
|
|
})
|
|
.catch((err) => reject(err));
|
|
});
|
|
};
|
|
|
|
export const sendWsRequest = async (item, collection, environment, runtimeVariables) => {
|
|
const ensureConnection = async () => {
|
|
const connectionStatus = await isWsConnectionActive(item.uid);
|
|
if (!connectionStatus.isActive) {
|
|
await connectWS(item, collection, environment, runtimeVariables, { connectOnly: true });
|
|
}
|
|
};
|
|
|
|
await ensureConnection();
|
|
|
|
// Use queueWsMessage helper to queue all messages with proper variable interpolation
|
|
const result = await queueWsMessage(item, collection, environment, runtimeVariables, null);
|
|
|
|
if (result.success) {
|
|
return {};
|
|
} else {
|
|
throw new Error(result.error || 'Failed to queue messages');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Queues a message to an existing WebSocket connection with variable interpolation
|
|
* @param {Object} item - The request item
|
|
* @param {Object} collection - The collection object
|
|
* @param {Object} environment - The environment variables
|
|
* @param {Object} runtimeVariables - The runtime variables
|
|
* @param {string} messageContent - The message content to queue (or null to queue all messages)
|
|
* @returns {Promise<Object>} - The result of the queue operation
|
|
*/
|
|
export const queueWsMessage = async (item, collection, environment, runtimeVariables, messageContent) => {
|
|
return new Promise((resolve, reject) => {
|
|
const { ipcRenderer } = window;
|
|
ipcRenderer.invoke('renderer:ws:queue-message', {
|
|
item,
|
|
collection,
|
|
environment,
|
|
runtimeVariables,
|
|
messageContent
|
|
}).then(resolve).catch(reject);
|
|
});
|
|
};
|
|
|
|
export const startWsConnection = async (item, collection, environment, runtimeVariables, options) => {
|
|
return new Promise((resolve, reject) => {
|
|
const { ipcRenderer } = window;
|
|
const request = item.draft ? item.draft : item;
|
|
const settings = item.draft ? item.draft.settings : item.settings;
|
|
|
|
ipcRenderer
|
|
.invoke('renderer:ws:start-connection', {
|
|
request,
|
|
collection,
|
|
environment,
|
|
runtimeVariables,
|
|
settings,
|
|
options
|
|
})
|
|
.then(() => {
|
|
resolve();
|
|
})
|
|
.catch((err) => {
|
|
reject(err);
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Sends a message to an existing WebSocket connection
|
|
* @param {string} requestId - The request ID to send a message to
|
|
* @param {Object} message - The message to send
|
|
* @returns {Promise<Object>} - The result of the send operation
|
|
*/
|
|
export const sendWsMessage = async (item, collectionUid, message) => {
|
|
return new Promise((resolve, reject) => {
|
|
const { ipcRenderer } = window;
|
|
ipcRenderer.invoke('renderer:ws:send-message', item.uid, collectionUid, message).then(resolve).catch(reject);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Closes a WebSocket connection
|
|
* @param {string} requestId - The request ID to close
|
|
* @returns {Promise<Object>} - The result of the close operation
|
|
*/
|
|
export const closeWsConnection = async (requestId) => {
|
|
return new Promise((resolve, reject) => {
|
|
const { ipcRenderer } = window;
|
|
ipcRenderer.invoke('renderer:ws:close-connection', requestId).then(resolve).catch(reject);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Checks if a WebSocket connection is active
|
|
* @param {string} requestId - The request ID to check
|
|
* @returns {Promise<boolean>} - Whether the connection is active
|
|
*/
|
|
export const isWsConnectionActive = async (requestId) => {
|
|
return new Promise((resolve, reject) => {
|
|
const { ipcRenderer } = window;
|
|
ipcRenderer.invoke('renderer:ws:is-connection-active', requestId).then(resolve).catch(reject);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Get the connection status of a WebSocket connection
|
|
* @param {string} requestId - The request ID to get the connection status of
|
|
* @returns {Promise<Object>} - The result of the get operation
|
|
*/
|
|
export const getWsConnectionStatus = async (requestId) => {
|
|
return new Promise((resolve, reject) => {
|
|
const { ipcRenderer } = window;
|
|
ipcRenderer.invoke('renderer:ws:connection-status', requestId).then(resolve).catch(reject);
|
|
});
|
|
};
|