diff --git a/packages/bruno-app/src/components/ResponsePane/WsResponsePane/WSMessagesList/index.js b/packages/bruno-app/src/components/ResponsePane/WsResponsePane/WSMessagesList/index.js index c515c9ae61..5729caeec3 100644 --- a/packages/bruno-app/src/components/ResponsePane/WsResponsePane/WSMessagesList/index.js +++ b/packages/bruno-app/src/components/ResponsePane/WsResponsePane/WSMessagesList/index.js @@ -6,7 +6,6 @@ import CodeEditor from 'components/CodeEditor/index'; import { useTheme } from 'providers/Theme'; import { useState } from 'react'; import { useSelector } from 'react-redux'; -import _ from 'lodash'; import { useRef } from 'react'; import { useEffect } from 'react'; @@ -183,12 +182,16 @@ const WSMessagesList = ({ order = -1, messages = [] }) => { if (!messages.length) { return
No messages yet.
; } - const ordered = order === -1 ? messages : messages.slice().reverse(); + + // sort based on order, seq was newly added and might be missing in some cases and when missing, + // the timestamp will be used instead + const ordered = messages.toSorted((x, y) => ((x.seq ?? x.timestamp) - (y.seq ?? y.timestamp)) * order); + return ( {ordered.map((msg, idx, src) => { const inFocus = order === -1 ? src.length - 1 === idx : idx === 0; - return ; + return ; })} ); diff --git a/packages/bruno-app/src/components/ResponsePane/index.js b/packages/bruno-app/src/components/ResponsePane/index.js index 7256660453..26346e7b74 100644 --- a/packages/bruno-app/src/components/ResponsePane/index.js +++ b/packages/bruno-app/src/components/ResponsePane/index.js @@ -225,7 +225,7 @@ const ResponsePane = ({ item, collection }) => { onClick={() => setShowScriptErrorCard(true)} /> )} - {focusedTab?.responsePaneTab === 'response' && item?.response ? ( + {focusedTab?.responsePaneTab === 'response' && item?.response && !(item.response?.stream ?? false) ? ( <> {/* Result View Tabs (Visualizations + Response Format) */}
diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js index f741b957bb..3665661924 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -383,7 +383,7 @@ export const collectionsSlice = createSlice({ } }, requestCancelled: (state, action) => { - const { itemUid, collectionUid } = action.payload; + const { itemUid, collectionUid, seq, timestamp } = action.payload; const collection = findCollectionByUid(state.collections, collectionUid); if (collection) { @@ -394,7 +394,7 @@ export const collectionsSlice = createSlice({ const startTimestamp = item.requestSent.timestamp; item.response.duration = startTimestamp ? Date.now() - startTimestamp : item.response.duration; - item.response.data = [{ type: 'info', timestamp: Date.now(), message: 'Connection Closed' }].concat(item.response.data); + item.response.data = [{ type: 'info', timestamp: Date.now(), seq: seq, message: 'Connection Closed' }].concat(item.response.data); } else { item.response = null; item.requestUid = null; @@ -3118,7 +3118,7 @@ export const collectionsSlice = createSlice({ } }, streamDataReceived: (state, action) => { - const { itemUid, collectionUid, data } = action.payload; + const { itemUid, collectionUid, seq, timestamp, data } = action.payload; const collection = findCollectionByUid(state.collections, collectionUid); if (collection) { @@ -3127,12 +3127,16 @@ export const collectionsSlice = createSlice({ item.response.data ||= []; item.response.data = [{ type: 'incoming', + seq, message: data.data, messageHexdump: hexdump(data.data), - timestamp: Date.now() + timestamp: timestamp || Date.now() }].concat(item.response.data); } - item.response.dataBuffer = Buffer.concat([Buffer.from(item.response.dataBuffer), Buffer.from(data.dataBuffer)]); + if (item.response.dataBuffer && item.response.dataBuffer.length && data.dataBuffer) { + item.response.dataBuffer = Buffer.concat([Buffer.from(item.response.dataBuffer), Buffer.from(data.dataBuffer)]); + } + item.response.size = data.data?.length + (item.response.size || 0); } }, @@ -3255,7 +3259,8 @@ export const collectionsSlice = createSlice({ updatedResponse.responses.push({ message: eventData.message, type: eventData.type, - timestamp: eventData.timestamp + timestamp: eventData.timestamp, + seq: eventData.seq }); break; @@ -3271,7 +3276,8 @@ export const collectionsSlice = createSlice({ updatedResponse.responses.push({ message: `Connected to ${eventData.url}`, type: 'info', - timestamp: eventData.timestamp + timestamp: eventData.timestamp, + seq: eventData.seq }); break; @@ -3287,7 +3293,8 @@ export const collectionsSlice = createSlice({ updatedResponse.responses.push({ type: code !== 1000 ? 'info' : 'error', message: reason.trim().length ? ['Closed:', reason.trim()].join(' ') : 'Closed', - timestamp + timestamp: eventData.timestamp, + seq: eventData.seq }); break; @@ -3302,7 +3309,8 @@ export const collectionsSlice = createSlice({ updatedResponse.responses.push({ type: 'error', message: errorDetails || 'WebSocket error occurred', - timestamp + timestamp: eventData.timestamp, + seq: eventData.seq }); break; diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index d8a58bc720..30a356f3a6 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -1003,6 +1003,7 @@ const registerNetworkIpc = (mainWindow) => { // handler for sending http request ipcMain.handle('send-http-request', async (event, item, collection, environment, runtimeVariables) => { + let seq = 0; const collectionUid = collection.uid; const envVars = getEnvVars(environment); const processEnvVars = getProcessEnvVars(collectionUid); @@ -1012,16 +1013,29 @@ const registerNetworkIpc = (mainWindow) => { response.stream = { running: response.status >= 200 && response.status < 300 }; stream.on('data', (newData) => { + seq += 1; + const parsed = parseDataFromResponse({ data: newData, headers: {} }); - mainWindow.webContents.send('main:http-stream-new-data', { collectionUid, itemUid: item.uid, data: parsed }); + + mainWindow.webContents.send('main:http-stream-new-data', { + collectionUid, + itemUid: item.uid, + seq, + timestamp: Date.now(), + data: parsed + }); }); stream.on('close', () => { - if (!cancelTokens[response.cancelTokenUid]) { - return; - } + if (!cancelTokens[response.cancelTokenUid]) return; + + mainWindow.webContents.send('main:http-stream-end', { + collectionUid, + itemUid: item.uid, + seq: seq + 1, + timestamp: Date.now() + }); - mainWindow.webContents.send('main:http-stream-end', { collectionUid, itemUid: item.uid }); deleteCancelToken(response.cancelTokenUid); }); } diff --git a/packages/bruno-requests/src/ws/ws-client.js b/packages/bruno-requests/src/ws/ws-client.js index 838e939b46..def4c06e23 100644 --- a/packages/bruno-requests/src/ws/ws-client.js +++ b/packages/bruno-requests/src/ws/ws-client.js @@ -49,6 +49,36 @@ const normalizeMessageByFormat = (message, format) => { } }; +const createSequencer = () => { + const seq = {}; + + const nextSeq = (requestId, collectionId) => { + seq[requestId] ||= {}; + seq[requestId][collectionId] ||= 0; + return ++seq[requestId][collectionId]; + }; + + /** + * @param {string} requestId + * @param {string} [collectionId] + */ + const clean = (requestId, collectionId = undefined) => { + if (collectionId) { + delete seq[requestId][collectionId]; + } + if (!Object.keys(seq[requestId]).length) { + delete seq[requestId]; + } + }; + + return { + next: nextSeq, + clean + }; +}; + +const seq = createSequencer(); + class WsClient { messageQueues = {}; activeConnections = new Map(); @@ -181,6 +211,7 @@ class WsClient { message: payload, messageHexdump: hexdump(payload), type: 'outgoing', + seq: seq.next(requestId, collectionUid), timestamp: Date.now() }); } @@ -204,6 +235,7 @@ class WsClient { if (connectionMeta?.connection) { connectionMeta.connection.close(code, reason); this.#removeConnection(requestId); + seq.clean(requestId); } } @@ -283,7 +315,8 @@ class WsClient { this.eventCallback('main:ws:open', requestId, collectionUid, { timestamp: Date.now(), - url: ws.url + url: ws.url, + seq: seq.next(requestId, collectionUid) }); }); @@ -294,7 +327,8 @@ class WsClient { message: `Redirected to ${url}`, type: 'info', timestamp: Date.now(), - headers: headers + headers: headers, + seq: seq.next(requestId, collectionUid) }); }); @@ -302,6 +336,7 @@ class WsClient { this.eventCallback('main:ws:upgrade', requestId, collectionUid, { type: 'info', timestamp: Date.now(), + seq: seq.next(requestId, collectionUid), headers: { ...response.headers } }); }); @@ -313,6 +348,7 @@ class WsClient { message, messageHexdump: hexdump(Buffer.from(data)), type: 'incoming', + seq: seq.next(requestId, collectionUid), timestamp: Date.now() }); } catch (error) { @@ -321,6 +357,7 @@ class WsClient { message: data.toString(), messageHexdump: hexdump(data), type: 'incoming', + seq: seq.next(requestId, collectionUid), timestamp: Date.now() }); } @@ -330,14 +367,17 @@ class WsClient { this.eventCallback('main:ws:close', requestId, collectionUid, { code, reason: Buffer.from(reason).toString(), + seq: seq.next(requestId, collectionUid), timestamp: Date.now() }); + seq.clean(requestId, collectionUid); this.#removeConnection(requestId); }); ws.on('error', (error) => { this.eventCallback('main:ws:error', requestId, collectionUid, { error: error.message, + seq: seq.next(requestId, collectionUid), timestamp: Date.now() }); }); @@ -356,6 +396,7 @@ class WsClient { this.eventCallback('main:ws:connections-changed', { type: 'added', requestId, + seq: seq.next(requestId, collectionUid), activeConnectionIds: this.getActiveConnectionIds() }); }