From 5703571cab85405759259ca211ea449b213e938d Mon Sep 17 00:00:00 2001 From: shubh Date: Tue, 30 Dec 2025 20:10:02 +0530 Subject: [PATCH 01/13] fix: sse sequence in ipc layer --- .../WsResponsePane/WSMessagesList/index.js | 2 +- .../ReduxStore/slices/collections/index.js | 34 ++++-- .../bruno-electron/src/ipc/network/index.js | 105 ++++++++++++++++-- 3 files changed, 123 insertions(+), 18 deletions(-) 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..e42ca4e024 100644 --- a/packages/bruno-app/src/components/ResponsePane/WsResponsePane/WSMessagesList/index.js +++ b/packages/bruno-app/src/components/ResponsePane/WsResponsePane/WSMessagesList/index.js @@ -188,7 +188,7 @@ const WSMessagesList = ({ order = -1, messages = [] }) => { {ordered.map((msg, idx, src) => { const inFocus = order === -1 ? src.length - 1 === idx : idx === 0; - return ; + return ; })} ); 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..eef0504aca 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) { @@ -391,10 +391,22 @@ export const collectionsSlice = createSlice({ if (item) { if (item.response?.stream?.running) { item.response.stream.running = null; + item.response.data.push({ + type: 'info', + seq: seq, + timestamp: timestamp || Date.now(), + message: 'Connection Closed' + }); 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); + if (item.response?.data?.length) { + item.response.data.sort((a, b) => { + const sa = Number.isFinite(a.seq) ? a.seq : Number.POSITIVE_INFINITY; + const sb = Number.isFinite(b.seq) ? b.seq : Number.POSITIVE_INFINITY; + return sb - sa; + }); + } } else { item.response = null; item.requestUid = null; @@ -3118,22 +3130,28 @@ 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) { const item = findItemInCollection(collection, itemUid); if (data.data) { item.response.data ||= []; - item.response.data = [{ + const newMessage = { type: 'incoming', + seq, message: data.data, messageHexdump: hexdump(data.data), - timestamp: Date.now() - }].concat(item.response.data); + timestamp: timestamp || Date.now() + }; + + const insertIndex = item.response.data.findIndex((m) => (m.seq ?? Infinity) < seq); + if (insertIndex === -1) item.response.data.push(newMessage); + else item.response.data.splice(insertIndex, 0, newMessage); + + item.response.dataBuffer = Buffer.concat([Buffer.from(item.response.dataBuffer), Buffer.from(data.dataBuffer)]); + item.response.size = data.data?.length + (item.response.size || 0); } - item.response.dataBuffer = Buffer.concat([Buffer.from(item.response.dataBuffer), Buffer.from(data.dataBuffer)]); - item.response.size = data.data?.length + (item.response.size || 0); } }, addRequestTag: (state, action) => { diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index d8a58bc720..4b7062bdc5 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); @@ -1011,19 +1012,105 @@ const registerNetworkIpc = (mainWindow) => { const stream = response.stream; response.stream = { running: response.status >= 200 && response.status < 300 }; - stream.on('data', (newData) => { - const parsed = parseDataFromResponse({ data: newData, headers: {} }); - mainWindow.webContents.send('main:http-stream-new-data', { collectionUid, itemUid: item.uid, data: parsed }); - }); + let raw = ''; + let streamEnded = false; + // Buffer parsed SSE events here so we don't spam the renderer with IPC messages. + // Rendering each streamed event immediately can freeze the UI under high-volume streams. + let eventBuffer = []; + + // Flush a limited number of events at a fixed cadence to keep the renderer responsive. + // BATCH_SIZE controls how many events are sent per flush. + // TICK_RATE controls how often we flush (ms). + const BATCH_SIZE = 10; + const TICK_RATE = 50; + + const flush = () => { + if (!eventBuffer.length) return; + const batch = eventBuffer.splice(0, BATCH_SIZE); + for (const evt of batch) { + mainWindow.webContents.send('main:http-stream-new-data', evt); + } + }; - stream.on('close', () => { - if (!cancelTokens[response.cancelTokenUid]) { - return; + // Periodically deliver buffered events to the renderer. + // When the network stream has ended AND we've delivered everything, emit stream-end once. + const ticker = setInterval(() => { + flush(); + if (streamEnded && eventBuffer.length === 0) { + clearInterval(ticker); + mainWindow.webContents.send('main:http-stream-end', { collectionUid, itemUid: item.uid, seq: seq + 1, timestamp: Date.now() }); + deleteCancelToken(response.cancelTokenUid); } + }, TICK_RATE); + + const parseSseBlock = (block) => { + // Normalize CRLF/CR -> LF + block = block.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); + + let dataLines = []; + let id = null; + let eventType = null; + + for (const line of block.split('\n')) { + if (!line) continue; + if (line.startsWith(':')) continue; + + if (line.startsWith('data:')) dataLines.push(line.slice(5).trimStart()); + else if (line.startsWith('id:')) id = line.slice(3).trimStart(); + else if (line.startsWith('event:')) eventType = line.slice(6).trimStart(); + } + + const dataText = dataLines.join('\n'); + if (!dataText) return; + + seq += 1; + const parsed = parseDataFromResponse({ data: dataText, headers: {} }); + + eventBuffer.push({ + collectionUid, + itemUid: item.uid, + seq, + timestamp: Date.now(), + data: parsed, + event: eventType + }); + }; + + stream.on('data', (chunk) => { + raw += chunk.toString('utf8'); + + while (true) { + // Find earliest delimiter: \n\n or \r\n\r\n (we normalize per block anyway) + const a = raw.indexOf('\n\n'); + const b = raw.indexOf('\r\n\r\n'); + let cut = -1; + + if (a !== -1 && b !== -1) cut = Math.min(a, b); + else cut = a !== -1 ? a : b; + + if (cut === -1) break; + + const block = raw.slice(0, cut); + raw = raw.slice(cut); - mainWindow.webContents.send('main:http-stream-end', { collectionUid, itemUid: item.uid }); - deleteCancelToken(response.cancelTokenUid); + if (raw.startsWith('\r\n\r\n')) raw = raw.slice(4); + else if (raw.startsWith('\n\n')) raw = raw.slice(2); + + parseSseBlock(block); + } }); + + const finish = () => { + if (raw.trim()) { + parseSseBlock(raw); + } + raw = ''; + streamEnded = true; + }; + + stream.on('end', finish); + stream.on('close', finish); + // stream.on('error', finish); } return response; }); From 0ce03526a5b91eabfa836aec247050cb1d06fb91 Mon Sep 17 00:00:00 2001 From: shubh Date: Wed, 31 Dec 2025 21:53:22 +0530 Subject: [PATCH 02/13] fix: remove tick rate and flushing --- .../bruno-electron/src/ipc/network/index.js | 103 +++--------------- 1 file changed, 15 insertions(+), 88 deletions(-) diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 4b7062bdc5..30a356f3a6 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -1012,105 +1012,32 @@ const registerNetworkIpc = (mainWindow) => { const stream = response.stream; response.stream = { running: response.status >= 200 && response.status < 300 }; - let raw = ''; - let streamEnded = false; - // Buffer parsed SSE events here so we don't spam the renderer with IPC messages. - // Rendering each streamed event immediately can freeze the UI under high-volume streams. - let eventBuffer = []; - - // Flush a limited number of events at a fixed cadence to keep the renderer responsive. - // BATCH_SIZE controls how many events are sent per flush. - // TICK_RATE controls how often we flush (ms). - const BATCH_SIZE = 10; - const TICK_RATE = 50; - - const flush = () => { - if (!eventBuffer.length) return; - const batch = eventBuffer.splice(0, BATCH_SIZE); - for (const evt of batch) { - mainWindow.webContents.send('main:http-stream-new-data', evt); - } - }; - - // Periodically deliver buffered events to the renderer. - // When the network stream has ended AND we've delivered everything, emit stream-end once. - const ticker = setInterval(() => { - flush(); - if (streamEnded && eventBuffer.length === 0) { - clearInterval(ticker); - mainWindow.webContents.send('main:http-stream-end', { collectionUid, itemUid: item.uid, seq: seq + 1, timestamp: Date.now() }); - deleteCancelToken(response.cancelTokenUid); - } - }, TICK_RATE); - - const parseSseBlock = (block) => { - // Normalize CRLF/CR -> LF - block = block.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); - - let dataLines = []; - let id = null; - let eventType = null; - - for (const line of block.split('\n')) { - if (!line) continue; - if (line.startsWith(':')) continue; - - if (line.startsWith('data:')) dataLines.push(line.slice(5).trimStart()); - else if (line.startsWith('id:')) id = line.slice(3).trimStart(); - else if (line.startsWith('event:')) eventType = line.slice(6).trimStart(); - } - - const dataText = dataLines.join('\n'); - if (!dataText) return; - + stream.on('data', (newData) => { seq += 1; - const parsed = parseDataFromResponse({ data: dataText, headers: {} }); - eventBuffer.push({ + const parsed = parseDataFromResponse({ data: newData, headers: {} }); + + mainWindow.webContents.send('main:http-stream-new-data', { collectionUid, itemUid: item.uid, seq, timestamp: Date.now(), - data: parsed, - event: eventType + data: parsed }); - }; - - stream.on('data', (chunk) => { - raw += chunk.toString('utf8'); - - while (true) { - // Find earliest delimiter: \n\n or \r\n\r\n (we normalize per block anyway) - const a = raw.indexOf('\n\n'); - const b = raw.indexOf('\r\n\r\n'); - let cut = -1; - - if (a !== -1 && b !== -1) cut = Math.min(a, b); - else cut = a !== -1 ? a : b; - - if (cut === -1) break; + }); - const block = raw.slice(0, cut); - raw = raw.slice(cut); + stream.on('close', () => { + if (!cancelTokens[response.cancelTokenUid]) return; - if (raw.startsWith('\r\n\r\n')) raw = raw.slice(4); - else if (raw.startsWith('\n\n')) raw = raw.slice(2); + mainWindow.webContents.send('main:http-stream-end', { + collectionUid, + itemUid: item.uid, + seq: seq + 1, + timestamp: Date.now() + }); - parseSseBlock(block); - } + deleteCancelToken(response.cancelTokenUid); }); - - const finish = () => { - if (raw.trim()) { - parseSseBlock(raw); - } - raw = ''; - streamEnded = true; - }; - - stream.on('end', finish); - stream.on('close', finish); - // stream.on('error', finish); } return response; }); From ce5ebe66e703065e03bd36926c2476daa71c00a9 Mon Sep 17 00:00:00 2001 From: shubh Date: Wed, 31 Dec 2025 21:54:48 +0530 Subject: [PATCH 03/13] fix: added sequence logic for websockets --- .../ReduxStore/slices/collections/index.js | 37 +++++++++++-------- .../src/utils/network/ws-event-listeners.js | 23 +++++++----- 2 files changed, 36 insertions(+), 24 deletions(-) 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 eef0504aca..02f0c7ae6f 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -3243,7 +3243,7 @@ export const collectionsSlice = createSlice({ wsResponseReceived: (state, action) => { const { itemUid, collectionUid, eventType, eventData } = action.payload; const collection = findCollectionByUid(state.collections, collectionUid); - + let newResponse; if (!collection) return; const item = findItemInCollection(collection, itemUid); @@ -3264,17 +3264,19 @@ export const collectionsSlice = createSlice({ switch (eventType) { case 'message': // Add message to responses list - updatedResponse.responses = (currentResponse?.responses || []).concat(eventData); + updatedResponse.responses = [eventData].concat(currentResponse?.responses || []); break; case 'redirect': updatedResponse.requestHeaders = eventData.headers; updatedResponse.responses ||= []; - updatedResponse.responses.push({ + newResponse = { message: eventData.message, type: eventData.type, - timestamp: eventData.timestamp - }); + timestamp: eventData.timestamp, + seq: eventData.seq + }; + updatedResponse.responses = [newResponse].concat(updatedResponse.responses || []); break; case 'upgrade': @@ -3286,11 +3288,13 @@ export const collectionsSlice = createSlice({ updatedResponse.statusText = 'CONNECTED'; updatedResponse.statusCode = 0; updatedResponse.responses ||= []; - updatedResponse.responses.push({ + newResponse = { message: `Connected to ${eventData.url}`, type: 'info', - timestamp: eventData.timestamp - }); + timestamp: eventData.timestamp, + seq: eventData.seq + }; + updatedResponse.responses = [newResponse].concat(updatedResponse.responses || []); break; case 'close': @@ -3302,11 +3306,13 @@ export const collectionsSlice = createSlice({ updatedResponse.statusText = wsStatusCodes[code] || 'CLOSED'; updatedResponse.statusDescription = reason; - updatedResponse.responses.push({ + newResponse = { type: code !== 1000 ? 'info' : 'error', message: reason.trim().length ? ['Closed:', reason.trim()].join(' ') : 'Closed', - timestamp - }); + timestamp: eventData.timestamp, + seq: eventData.seq + }; + updatedResponse.responses = [newResponse].concat(updatedResponse.responses || []); break; case 'error': @@ -3317,12 +3323,13 @@ export const collectionsSlice = createSlice({ updatedResponse.statusCode = wsStatusCodes[1011]; updatedResponse.statusText = 'ERROR'; - updatedResponse.responses.push({ + newResponse = { type: 'error', message: errorDetails || 'WebSocket error occurred', - timestamp - }); - + timestamp: eventData.timestamp, + seq: eventData.seq + }; + updatedResponse.responses = [newResponse].concat(updatedResponse.responses || []); break; case 'connecting': diff --git a/packages/bruno-app/src/utils/network/ws-event-listeners.js b/packages/bruno-app/src/utils/network/ws-event-listeners.js index 4b005be584..bc96667fd4 100644 --- a/packages/bruno-app/src/utils/network/ws-event-listeners.js +++ b/packages/bruno-app/src/utils/network/ws-event-listeners.js @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { useEffect, useRef } from 'react'; import { wsResponseReceived, runWsRequestEvent } from 'providers/ReduxStore/slices/collections/index'; import { useDispatch } from 'react-redux'; import { isElectron } from 'utils/common/platform'; @@ -8,6 +8,10 @@ const useWsEventListeners = () => { const { ipcRenderer } = window; const dispatch = useDispatch(); + const seqRef = useRef(0); + const nextSeq = () => (++seqRef.current); + const resetSeq = () => { seqRef.current = 0; }; + useEffect(() => { if (!isElectron()) { return () => {}; @@ -20,7 +24,7 @@ const useWsEventListeners = () => { itemUid: requestId, collectionUid: collectionUid, requestUid: requestId, - eventData + eventData: { ...eventData, seq: nextSeq() } })); }); @@ -29,7 +33,7 @@ const useWsEventListeners = () => { itemUid: requestId, collectionUid: collectionUid, eventType: 'upgrade', - eventData: eventData + eventData: { ...eventData, seq: nextSeq() } })); }); @@ -38,7 +42,7 @@ const useWsEventListeners = () => { itemUid: requestId, collectionUid: collectionUid, eventType: 'redirect', - eventData: eventData + eventData: { ...eventData, seq: nextSeq() } })); }); @@ -48,7 +52,7 @@ const useWsEventListeners = () => { itemUid: requestId, collectionUid: collectionUid, eventType: 'message', - eventData: eventData + eventData: { ...eventData, seq: nextSeq() } })); }); @@ -58,7 +62,7 @@ const useWsEventListeners = () => { itemUid: requestId, collectionUid: collectionUid, eventType: 'open', - eventData: eventData + eventData: { ...eventData, seq: nextSeq() } })); }); @@ -68,8 +72,9 @@ const useWsEventListeners = () => { itemUid: requestId, collectionUid: collectionUid, eventType: 'close', - eventData: eventData + eventData: { ...eventData, seq: nextSeq() } })); + resetSeq(); }); // Handle WebSocket error event @@ -78,7 +83,7 @@ const useWsEventListeners = () => { itemUid: requestId, collectionUid: collectionUid, eventType: 'error', - eventData: eventData + eventData: { ...eventData, seq: nextSeq() } })); }); @@ -88,7 +93,7 @@ const useWsEventListeners = () => { itemUid: requestId, collectionUid: collectionUid, eventType: 'connecting', - eventData: eventData + eventData: { ...eventData, seq: nextSeq() } })); }); From aa9b3cfff7eb3d76562a6eeabee304829c27660a Mon Sep 17 00:00:00 2001 From: shubh Date: Wed, 31 Dec 2025 22:18:05 +0530 Subject: [PATCH 04/13] fix: added sequence logic for websockets per request based --- .../src/utils/network/ws-event-listeners.js | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/packages/bruno-app/src/utils/network/ws-event-listeners.js b/packages/bruno-app/src/utils/network/ws-event-listeners.js index bc96667fd4..8b4139fb98 100644 --- a/packages/bruno-app/src/utils/network/ws-event-listeners.js +++ b/packages/bruno-app/src/utils/network/ws-event-listeners.js @@ -8,9 +8,19 @@ const useWsEventListeners = () => { const { ipcRenderer } = window; const dispatch = useDispatch(); - const seqRef = useRef(0); - const nextSeq = () => (++seqRef.current); - const resetSeq = () => { seqRef.current = 0; }; + // requestId -> seq + const seqByRequestRef = useRef(new Map()); + + const nextSeq = (requestId) => { + const map = seqByRequestRef.current; + const next = (map.get(requestId) ?? 0) + 1; + map.set(requestId, next); + return next; + }; + + const resetSeq = (requestId) => { + seqByRequestRef.current.delete(requestId); + }; useEffect(() => { if (!isElectron()) { @@ -24,7 +34,7 @@ const useWsEventListeners = () => { itemUid: requestId, collectionUid: collectionUid, requestUid: requestId, - eventData: { ...eventData, seq: nextSeq() } + eventData: { ...eventData, seq: nextSeq(requestId) } })); }); @@ -33,7 +43,7 @@ const useWsEventListeners = () => { itemUid: requestId, collectionUid: collectionUid, eventType: 'upgrade', - eventData: { ...eventData, seq: nextSeq() } + eventData: { ...eventData, seq: nextSeq(requestId) } })); }); @@ -42,7 +52,7 @@ const useWsEventListeners = () => { itemUid: requestId, collectionUid: collectionUid, eventType: 'redirect', - eventData: { ...eventData, seq: nextSeq() } + eventData: { ...eventData, seq: nextSeq(requestId) } })); }); @@ -52,7 +62,7 @@ const useWsEventListeners = () => { itemUid: requestId, collectionUid: collectionUid, eventType: 'message', - eventData: { ...eventData, seq: nextSeq() } + eventData: { ...eventData, seq: nextSeq(requestId) } })); }); @@ -62,7 +72,7 @@ const useWsEventListeners = () => { itemUid: requestId, collectionUid: collectionUid, eventType: 'open', - eventData: { ...eventData, seq: nextSeq() } + eventData: { ...eventData, seq: nextSeq(requestId) } })); }); @@ -72,9 +82,9 @@ const useWsEventListeners = () => { itemUid: requestId, collectionUid: collectionUid, eventType: 'close', - eventData: { ...eventData, seq: nextSeq() } + eventData: { ...eventData, seq: nextSeq(requestId) } })); - resetSeq(); + resetSeq(requestId); }); // Handle WebSocket error event @@ -83,7 +93,7 @@ const useWsEventListeners = () => { itemUid: requestId, collectionUid: collectionUid, eventType: 'error', - eventData: { ...eventData, seq: nextSeq() } + eventData: { ...eventData, seq: nextSeq(requestId) } })); }); @@ -93,7 +103,7 @@ const useWsEventListeners = () => { itemUid: requestId, collectionUid: collectionUid, eventType: 'connecting', - eventData: { ...eventData, seq: nextSeq() } + eventData: { ...eventData, seq: nextSeq(requestId) } })); }); From 094f115d354945722c3116dfefd610a22dc2a606 Mon Sep 17 00:00:00 2001 From: Sid Date: Fri, 2 Jan 2026 18:51:53 +0530 Subject: [PATCH 05/13] fix: correct the order for how the messages are added. `WSMessagesList` already handles a lot of the ordering for us, don't modify the order the messages are added since redirect and connection are internal states, it changes the execution trail --- .../ReduxStore/slices/collections/index.js | 203 +++++++++--------- 1 file changed, 104 insertions(+), 99 deletions(-) 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 02f0c7ae6f..cf9226733d 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -400,13 +400,6 @@ export const collectionsSlice = createSlice({ const startTimestamp = item.requestSent.timestamp; item.response.duration = startTimestamp ? Date.now() - startTimestamp : item.response.duration; - if (item.response?.data?.length) { - item.response.data.sort((a, b) => { - const sa = Number.isFinite(a.seq) ? a.seq : Number.POSITIVE_INFINITY; - const sb = Number.isFinite(b.seq) ? b.seq : Number.POSITIVE_INFINITY; - return sb - sa; - }); - } } else { item.response = null; item.requestUid = null; @@ -432,9 +425,10 @@ export const collectionsSlice = createSlice({ } // Ensure timestamp is a number (milliseconds since epoch) - const timestamp = item?.requestSent?.timestamp instanceof Date - ? item.requestSent.timestamp.getTime() - : item?.requestSent?.timestamp || Date.now(); + const timestamp + = item?.requestSent?.timestamp instanceof Date + ? item.requestSent.timestamp.getTime() + : item?.requestSent?.timestamp || Date.now(); // Append the new timeline entry with numeric timestamp collection.timeline.push({ @@ -551,7 +545,8 @@ export const collectionsSlice = createSlice({ // Handle error status (non-zero code) if (statusCode !== 0) { updatedResponse.isError = true; - updatedResponse.error = statusDetails || `gRPC error with code ${statusCode} (${updatedResponse.statusText})`; + updatedResponse.error + = statusDetails || `gRPC error with code ${statusCode} (${updatedResponse.statusText})`; } break; @@ -964,22 +959,23 @@ export const collectionsSlice = createSlice({ item.draft = cloneDeep(item); } const existingOtherParams = item.draft.request.params?.filter((p) => p.type !== 'query') || []; - const newQueryParams = map(params, ({ uid, name = '', value = '', description = '', type = 'query', enabled = true }) => ({ - uid: uid || uuid(), - name, - value, - description, - type, - enabled - })); + const newQueryParams = map( + params, + ({ uid, name = '', value = '', description = '', type = 'query', enabled = true }) => ({ + uid: uid || uuid(), + name, + value, + description, + type, + enabled + }) + ); item.draft.request.params = [...newQueryParams, ...existingOtherParams]; // Update the request URL to reflect the new query params const parts = splitOnFirst(item.draft.request.url, '?'); - const query = stringifyQueryParams( - filter(item.draft.request.params, (p) => p.enabled && p.type === 'query') - ); + const query = stringifyQueryParams(filter(item.draft.request.params, (p) => p.enabled && p.type === 'query')); // If there are enabled query params, append them to the URL if (query && query.length) { @@ -1210,13 +1206,16 @@ export const collectionsSlice = createSlice({ if (!item.draft) { item.draft = cloneDeep(item); } - item.draft.request.headers = map(action.payload.headers, ({ uid, name = '', value = '', description = '', enabled = true }) => ({ - uid: uid || uuid(), - name, - value, - description, - enabled - })); + item.draft.request.headers = map( + action.payload.headers, + ({ uid, name = '', value = '', description = '', enabled = true }) => ({ + uid: uid || uuid(), + name, + value, + description, + enabled + }) + ); }, setCollectionHeaders: (state, action) => { const { collectionUid, headers } = action.payload; @@ -1238,13 +1237,16 @@ export const collectionsSlice = createSlice({ collection.draft.root.request = {}; } - collection.draft.root.request.headers = map(headers, ({ uid, name = '', value = '', description = '', enabled = true }) => ({ - uid: uid || uuid(), - name, - value, - description, - enabled - })); + collection.draft.root.request.headers = map( + headers, + ({ uid, name = '', value = '', description = '', enabled = true }) => ({ + uid: uid || uuid(), + name, + value, + description, + enabled + }) + ); }, setFolderHeaders: (state, action) => { const { collectionUid, folderUid, headers } = action.payload; @@ -1265,13 +1267,16 @@ export const collectionsSlice = createSlice({ if (!folder.draft.request) { folder.draft.request = {}; } - folder.draft.request.headers = map(headers, ({ uid, name = '', value = '', description = '', enabled = true }) => ({ - uid: uid || uuid(), - name, - value, - description, - enabled - })); + folder.draft.request.headers = map( + headers, + ({ uid, name = '', value = '', description = '', enabled = true }) => ({ + uid: uid || uuid(), + name, + value, + description, + enabled + }) + ); }, addFormUrlEncodedParam: (state, action) => { const collection = findCollectionByUid(state.collections, action.payload.collectionUid); @@ -1342,13 +1347,16 @@ export const collectionsSlice = createSlice({ if (!item.draft) { item.draft = cloneDeep(item); } - item.draft.request.body.formUrlEncoded = map(params, ({ uid, name = '', value = '', description = '', enabled = true }) => ({ - uid: uid || uuid(), - name, - value, - description, - enabled - })); + item.draft.request.body.formUrlEncoded = map( + params, + ({ uid, name = '', value = '', description = '', enabled = true }) => ({ + uid: uid || uuid(), + name, + value, + description, + enabled + }) + ); }, moveFormUrlEncodedParam: (state, action) => { const collection = findCollectionByUid(state.collections, action.payload.collectionUid); @@ -1443,14 +1451,17 @@ export const collectionsSlice = createSlice({ if (!item.draft) { item.draft = cloneDeep(item); } - item.draft.request.body.multipartForm = map(params, ({ uid, name = '', value = '', contentType = '', type = 'text', enabled = true }) => ({ - uid: uid || uuid(), - name, - value, - contentType, - type, - enabled - })); + item.draft.request.body.multipartForm = map( + params, + ({ uid, name = '', value = '', contentType = '', type = 'text', enabled = true }) => ({ + uid: uid || uuid(), + name, + value, + contentType, + type, + enabled + }) + ); }, moveMultipartFormParam: (state, action) => { const collection = findCollectionByUid(state.collections, action.payload.collectionUid); @@ -1531,10 +1542,7 @@ export const collectionsSlice = createSlice({ item.draft = cloneDeep(item); } - item.draft.request.body.file = filter( - item.draft.request.body.file, - (p) => p.uid !== action.payload.paramUid - ); + item.draft.request.body.file = filter(item.draft.request.body.file, (p) => p.uid !== action.payload.paramUid); if (item.draft.request.body.file.length > 0) { item.draft.request.body.file[0].selected = true; @@ -1794,13 +1802,16 @@ export const collectionsSlice = createSlice({ if (!item.draft) { item.draft = cloneDeep(item); } - item.draft.request.assertions = map(assertions, ({ uid, name = '', value = '', operator = 'eq', enabled = true }) => ({ - uid: uid || uuid(), - name, - value, - operator, - enabled - })); + item.draft.request.assertions = map( + assertions, + ({ uid, name = '', value = '', operator = 'eq', enabled = true }) => ({ + uid: uid || uuid(), + name, + value, + operator, + enabled + }) + ); }, moveAssertion: (state, action) => { const collection = findCollectionByUid(state.collections, action.payload.collectionUid); @@ -3099,8 +3110,7 @@ export const collectionsSlice = createSlice({ let collectionOauth2Credentials = cloneDeep(collection.oauth2Credentials); const filteredOauth2Credentials = filter( collectionOauth2Credentials, - (creds) => - !(creds.url === url && creds.collectionUid === collectionUid) + (creds) => !(creds.url === url && creds.collectionUid === collectionUid) ); collection.oauth2Credentials = filteredOauth2Credentials; } @@ -3111,8 +3121,7 @@ export const collectionsSlice = createSlice({ const collection = findCollectionByUid(state.collections, collectionUid); const oauth2Credential = find( collection?.oauth2Credentials || [], - (creds) => - creds.url === url && creds.collectionUid === collectionUid && creds.credentialsId === credentialsId + (creds) => creds.url === url && creds.collectionUid === collectionUid && creds.credentialsId === credentialsId ); return oauth2Credential; }, @@ -3137,19 +3146,20 @@ export const collectionsSlice = createSlice({ const item = findItemInCollection(collection, itemUid); if (data.data) { item.response.data ||= []; - const newMessage = { - type: 'incoming', - seq, - message: data.data, - messageHexdump: hexdump(data.data), - timestamp: timestamp || Date.now() - }; - - const insertIndex = item.response.data.findIndex((m) => (m.seq ?? Infinity) < seq); - if (insertIndex === -1) item.response.data.push(newMessage); - else item.response.data.splice(insertIndex, 0, newMessage); + item.response.data = [ + { + type: 'incoming', + seq, + message: data.data, + messageHexdump: hexdump(data.data), + timestamp: timestamp || Date.now() + } + ].concat(item.response.data); - item.response.dataBuffer = Buffer.concat([Buffer.from(item.response.dataBuffer), Buffer.from(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); } } @@ -3243,7 +3253,6 @@ export const collectionsSlice = createSlice({ wsResponseReceived: (state, action) => { const { itemUid, collectionUid, eventType, eventData } = action.payload; const collection = findCollectionByUid(state.collections, collectionUid); - let newResponse; if (!collection) return; const item = findItemInCollection(collection, itemUid); @@ -3264,19 +3273,18 @@ export const collectionsSlice = createSlice({ switch (eventType) { case 'message': // Add message to responses list - updatedResponse.responses = [eventData].concat(currentResponse?.responses || []); + updatedResponse.responses = (currentResponse?.responses || []).concat(eventData); break; case 'redirect': updatedResponse.requestHeaders = eventData.headers; updatedResponse.responses ||= []; - newResponse = { + updatedResponse.responses.push({ message: eventData.message, type: eventData.type, timestamp: eventData.timestamp, seq: eventData.seq - }; - updatedResponse.responses = [newResponse].concat(updatedResponse.responses || []); + }); break; case 'upgrade': @@ -3288,13 +3296,12 @@ export const collectionsSlice = createSlice({ updatedResponse.statusText = 'CONNECTED'; updatedResponse.statusCode = 0; updatedResponse.responses ||= []; - newResponse = { + updatedResponse.responses.push({ message: `Connected to ${eventData.url}`, type: 'info', timestamp: eventData.timestamp, seq: eventData.seq - }; - updatedResponse.responses = [newResponse].concat(updatedResponse.responses || []); + }); break; case 'close': @@ -3306,13 +3313,12 @@ export const collectionsSlice = createSlice({ updatedResponse.statusText = wsStatusCodes[code] || 'CLOSED'; updatedResponse.statusDescription = reason; - newResponse = { + updatedResponse.responses.push({ type: code !== 1000 ? 'info' : 'error', message: reason.trim().length ? ['Closed:', reason.trim()].join(' ') : 'Closed', timestamp: eventData.timestamp, seq: eventData.seq - }; - updatedResponse.responses = [newResponse].concat(updatedResponse.responses || []); + }); break; case 'error': @@ -3323,13 +3329,12 @@ export const collectionsSlice = createSlice({ updatedResponse.statusCode = wsStatusCodes[1011]; updatedResponse.statusText = 'ERROR'; - newResponse = { + updatedResponse.responses.push({ type: 'error', message: errorDetails || 'WebSocket error occurred', timestamp: eventData.timestamp, seq: eventData.seq - }; - updatedResponse.responses = [newResponse].concat(updatedResponse.responses || []); + }); break; case 'connecting': From 411ef8272dbf65649cd64aebfeab0a86c28bdc5e Mon Sep 17 00:00:00 2001 From: Sid Date: Fri, 2 Jan 2026 19:08:42 +0530 Subject: [PATCH 06/13] chore: reduce whitespace diffs --- .../ReduxStore/slices/collections/index.js | 181 ++++++++---------- 1 file changed, 78 insertions(+), 103 deletions(-) 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 cf9226733d..a7b21d1d98 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -391,15 +391,10 @@ export const collectionsSlice = createSlice({ if (item) { if (item.response?.stream?.running) { item.response.stream.running = null; - item.response.data.push({ - type: 'info', - seq: seq, - timestamp: timestamp || Date.now(), - message: 'Connection Closed' - }); const startTimestamp = item.requestSent.timestamp; item.response.duration = startTimestamp ? Date.now() - startTimestamp : item.response.duration; + item.response.data = [{ type: 'info', timestamp: Date.now(), seq: seq, message: 'Connection Closed' }].concat(item.response.data); } else { item.response = null; item.requestUid = null; @@ -425,10 +420,9 @@ export const collectionsSlice = createSlice({ } // Ensure timestamp is a number (milliseconds since epoch) - const timestamp - = item?.requestSent?.timestamp instanceof Date - ? item.requestSent.timestamp.getTime() - : item?.requestSent?.timestamp || Date.now(); + const timestamp = item?.requestSent?.timestamp instanceof Date + ? item.requestSent.timestamp.getTime() + : item?.requestSent?.timestamp || Date.now(); // Append the new timeline entry with numeric timestamp collection.timeline.push({ @@ -545,8 +539,7 @@ export const collectionsSlice = createSlice({ // Handle error status (non-zero code) if (statusCode !== 0) { updatedResponse.isError = true; - updatedResponse.error - = statusDetails || `gRPC error with code ${statusCode} (${updatedResponse.statusText})`; + updatedResponse.error = statusDetails || `gRPC error with code ${statusCode} (${updatedResponse.statusText})`; } break; @@ -959,23 +952,22 @@ export const collectionsSlice = createSlice({ item.draft = cloneDeep(item); } const existingOtherParams = item.draft.request.params?.filter((p) => p.type !== 'query') || []; - const newQueryParams = map( - params, - ({ uid, name = '', value = '', description = '', type = 'query', enabled = true }) => ({ - uid: uid || uuid(), - name, - value, - description, - type, - enabled - }) - ); + const newQueryParams = map(params, ({ uid, name = '', value = '', description = '', type = 'query', enabled = true }) => ({ + uid: uid || uuid(), + name, + value, + description, + type, + enabled + })); item.draft.request.params = [...newQueryParams, ...existingOtherParams]; // Update the request URL to reflect the new query params const parts = splitOnFirst(item.draft.request.url, '?'); - const query = stringifyQueryParams(filter(item.draft.request.params, (p) => p.enabled && p.type === 'query')); + const query = stringifyQueryParams( + filter(item.draft.request.params, (p) => p.enabled && p.type === 'query') + ); // If there are enabled query params, append them to the URL if (query && query.length) { @@ -1206,16 +1198,13 @@ export const collectionsSlice = createSlice({ if (!item.draft) { item.draft = cloneDeep(item); } - item.draft.request.headers = map( - action.payload.headers, - ({ uid, name = '', value = '', description = '', enabled = true }) => ({ - uid: uid || uuid(), - name, - value, - description, - enabled - }) - ); + item.draft.request.headers = map(action.payload.headers, ({ uid, name = '', value = '', description = '', enabled = true }) => ({ + uid: uid || uuid(), + name, + value, + description, + enabled + })); }, setCollectionHeaders: (state, action) => { const { collectionUid, headers } = action.payload; @@ -1237,16 +1226,13 @@ export const collectionsSlice = createSlice({ collection.draft.root.request = {}; } - collection.draft.root.request.headers = map( - headers, - ({ uid, name = '', value = '', description = '', enabled = true }) => ({ - uid: uid || uuid(), - name, - value, - description, - enabled - }) - ); + collection.draft.root.request.headers = map(headers, ({ uid, name = '', value = '', description = '', enabled = true }) => ({ + uid: uid || uuid(), + name, + value, + description, + enabled + })); }, setFolderHeaders: (state, action) => { const { collectionUid, folderUid, headers } = action.payload; @@ -1267,16 +1253,13 @@ export const collectionsSlice = createSlice({ if (!folder.draft.request) { folder.draft.request = {}; } - folder.draft.request.headers = map( - headers, - ({ uid, name = '', value = '', description = '', enabled = true }) => ({ - uid: uid || uuid(), - name, - value, - description, - enabled - }) - ); + folder.draft.request.headers = map(headers, ({ uid, name = '', value = '', description = '', enabled = true }) => ({ + uid: uid || uuid(), + name, + value, + description, + enabled + })); }, addFormUrlEncodedParam: (state, action) => { const collection = findCollectionByUid(state.collections, action.payload.collectionUid); @@ -1347,16 +1330,13 @@ export const collectionsSlice = createSlice({ if (!item.draft) { item.draft = cloneDeep(item); } - item.draft.request.body.formUrlEncoded = map( - params, - ({ uid, name = '', value = '', description = '', enabled = true }) => ({ - uid: uid || uuid(), - name, - value, - description, - enabled - }) - ); + item.draft.request.body.formUrlEncoded = map(params, ({ uid, name = '', value = '', description = '', enabled = true }) => ({ + uid: uid || uuid(), + name, + value, + description, + enabled + })); }, moveFormUrlEncodedParam: (state, action) => { const collection = findCollectionByUid(state.collections, action.payload.collectionUid); @@ -1451,17 +1431,14 @@ export const collectionsSlice = createSlice({ if (!item.draft) { item.draft = cloneDeep(item); } - item.draft.request.body.multipartForm = map( - params, - ({ uid, name = '', value = '', contentType = '', type = 'text', enabled = true }) => ({ - uid: uid || uuid(), - name, - value, - contentType, - type, - enabled - }) - ); + item.draft.request.body.multipartForm = map(params, ({ uid, name = '', value = '', contentType = '', type = 'text', enabled = true }) => ({ + uid: uid || uuid(), + name, + value, + contentType, + type, + enabled + })); }, moveMultipartFormParam: (state, action) => { const collection = findCollectionByUid(state.collections, action.payload.collectionUid); @@ -1542,7 +1519,10 @@ export const collectionsSlice = createSlice({ item.draft = cloneDeep(item); } - item.draft.request.body.file = filter(item.draft.request.body.file, (p) => p.uid !== action.payload.paramUid); + item.draft.request.body.file = filter( + item.draft.request.body.file, + (p) => p.uid !== action.payload.paramUid + ); if (item.draft.request.body.file.length > 0) { item.draft.request.body.file[0].selected = true; @@ -1802,16 +1782,13 @@ export const collectionsSlice = createSlice({ if (!item.draft) { item.draft = cloneDeep(item); } - item.draft.request.assertions = map( - assertions, - ({ uid, name = '', value = '', operator = 'eq', enabled = true }) => ({ - uid: uid || uuid(), - name, - value, - operator, - enabled - }) - ); + item.draft.request.assertions = map(assertions, ({ uid, name = '', value = '', operator = 'eq', enabled = true }) => ({ + uid: uid || uuid(), + name, + value, + operator, + enabled + })); }, moveAssertion: (state, action) => { const collection = findCollectionByUid(state.collections, action.payload.collectionUid); @@ -3110,7 +3087,8 @@ export const collectionsSlice = createSlice({ let collectionOauth2Credentials = cloneDeep(collection.oauth2Credentials); const filteredOauth2Credentials = filter( collectionOauth2Credentials, - (creds) => !(creds.url === url && creds.collectionUid === collectionUid) + (creds) => + !(creds.url === url && creds.collectionUid === collectionUid) ); collection.oauth2Credentials = filteredOauth2Credentials; } @@ -3121,7 +3099,8 @@ export const collectionsSlice = createSlice({ const collection = findCollectionByUid(state.collections, collectionUid); const oauth2Credential = find( collection?.oauth2Credentials || [], - (creds) => creds.url === url && creds.collectionUid === collectionUid && creds.credentialsId === credentialsId + (creds) => + creds.url === url && creds.collectionUid === collectionUid && creds.credentialsId === credentialsId ); return oauth2Credential; }, @@ -3146,22 +3125,16 @@ export const collectionsSlice = createSlice({ const item = findItemInCollection(collection, itemUid); if (data.data) { item.response.data ||= []; - item.response.data = [ - { - type: 'incoming', - seq, - message: data.data, - messageHexdump: hexdump(data.data), - timestamp: timestamp || Date.now() - } - ].concat(item.response.data); - - item.response.dataBuffer = Buffer.concat([ - Buffer.from(item.response.dataBuffer), - Buffer.from(data.dataBuffer) - ]); - item.response.size = data.data?.length + (item.response.size || 0); + item.response.data = [{ + type: 'incoming', + seq, + message: data.data, + messageHexdump: hexdump(data.data), + timestamp: timestamp || Date.now() + }].concat(item.response.data); } + item.response.dataBuffer = Buffer.concat([Buffer.from(item.response.dataBuffer), Buffer.from(data.dataBuffer)]); + item.response.size = data.data?.length + (item.response.size || 0); } }, addRequestTag: (state, action) => { @@ -3253,6 +3226,7 @@ export const collectionsSlice = createSlice({ wsResponseReceived: (state, action) => { const { itemUid, collectionUid, eventType, eventData } = action.payload; const collection = findCollectionByUid(state.collections, collectionUid); + if (!collection) return; const item = findItemInCollection(collection, itemUid); @@ -3335,6 +3309,7 @@ export const collectionsSlice = createSlice({ timestamp: eventData.timestamp, seq: eventData.seq }); + break; case 'connecting': From 49f09aa38fe5f8e10fbb53578f00530661db761e Mon Sep 17 00:00:00 2001 From: Sid Date: Fri, 2 Jan 2026 19:11:20 +0530 Subject: [PATCH 07/13] fix: a possible null case exception Though we always create an empty data buffer at source so shouldn't happen unless that is modified --- .../src/providers/ReduxStore/slices/collections/index.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 a7b21d1d98..3665661924 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -3133,7 +3133,10 @@ export const collectionsSlice = createSlice({ 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); } }, From 8af13e3c37f8255b5c48d21ad585e8c7ec56b012 Mon Sep 17 00:00:00 2001 From: Sid Date: Fri, 2 Jan 2026 20:02:41 +0530 Subject: [PATCH 08/13] fix: implement sequence logic for WebSocket messages --- .../WsResponsePane/WSMessagesList/index.js | 2 +- .../src/utils/network/ws-event-listeners.js | 33 +++++-------------- packages/bruno-requests/src/ws/ws-client.js | 28 ++++++++++++++-- 3 files changed, 36 insertions(+), 27 deletions(-) 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 e42ca4e024..38e771bfaf 100644 --- a/packages/bruno-app/src/components/ResponsePane/WsResponsePane/WSMessagesList/index.js +++ b/packages/bruno-app/src/components/ResponsePane/WsResponsePane/WSMessagesList/index.js @@ -183,7 +183,7 @@ const WSMessagesList = ({ order = -1, messages = [] }) => { if (!messages.length) { return
No messages yet.
; } - const ordered = order === -1 ? messages : messages.slice().reverse(); + const ordered = order === -1 ? messages.toSorted((x, y) => x.seq - y.seq) : messages.toSorted((x, y) => y.seq - x.seq); return ( {ordered.map((msg, idx, src) => { diff --git a/packages/bruno-app/src/utils/network/ws-event-listeners.js b/packages/bruno-app/src/utils/network/ws-event-listeners.js index 8b4139fb98..4b005be584 100644 --- a/packages/bruno-app/src/utils/network/ws-event-listeners.js +++ b/packages/bruno-app/src/utils/network/ws-event-listeners.js @@ -1,4 +1,4 @@ -import { useEffect, useRef } from 'react'; +import { useEffect } from 'react'; import { wsResponseReceived, runWsRequestEvent } from 'providers/ReduxStore/slices/collections/index'; import { useDispatch } from 'react-redux'; import { isElectron } from 'utils/common/platform'; @@ -8,20 +8,6 @@ const useWsEventListeners = () => { const { ipcRenderer } = window; const dispatch = useDispatch(); - // requestId -> seq - const seqByRequestRef = useRef(new Map()); - - const nextSeq = (requestId) => { - const map = seqByRequestRef.current; - const next = (map.get(requestId) ?? 0) + 1; - map.set(requestId, next); - return next; - }; - - const resetSeq = (requestId) => { - seqByRequestRef.current.delete(requestId); - }; - useEffect(() => { if (!isElectron()) { return () => {}; @@ -34,7 +20,7 @@ const useWsEventListeners = () => { itemUid: requestId, collectionUid: collectionUid, requestUid: requestId, - eventData: { ...eventData, seq: nextSeq(requestId) } + eventData })); }); @@ -43,7 +29,7 @@ const useWsEventListeners = () => { itemUid: requestId, collectionUid: collectionUid, eventType: 'upgrade', - eventData: { ...eventData, seq: nextSeq(requestId) } + eventData: eventData })); }); @@ -52,7 +38,7 @@ const useWsEventListeners = () => { itemUid: requestId, collectionUid: collectionUid, eventType: 'redirect', - eventData: { ...eventData, seq: nextSeq(requestId) } + eventData: eventData })); }); @@ -62,7 +48,7 @@ const useWsEventListeners = () => { itemUid: requestId, collectionUid: collectionUid, eventType: 'message', - eventData: { ...eventData, seq: nextSeq(requestId) } + eventData: eventData })); }); @@ -72,7 +58,7 @@ const useWsEventListeners = () => { itemUid: requestId, collectionUid: collectionUid, eventType: 'open', - eventData: { ...eventData, seq: nextSeq(requestId) } + eventData: eventData })); }); @@ -82,9 +68,8 @@ const useWsEventListeners = () => { itemUid: requestId, collectionUid: collectionUid, eventType: 'close', - eventData: { ...eventData, seq: nextSeq(requestId) } + eventData: eventData })); - resetSeq(requestId); }); // Handle WebSocket error event @@ -93,7 +78,7 @@ const useWsEventListeners = () => { itemUid: requestId, collectionUid: collectionUid, eventType: 'error', - eventData: { ...eventData, seq: nextSeq(requestId) } + eventData: eventData })); }); @@ -103,7 +88,7 @@ const useWsEventListeners = () => { itemUid: requestId, collectionUid: collectionUid, eventType: 'connecting', - eventData: { ...eventData, seq: nextSeq(requestId) } + eventData: eventData })); }); diff --git a/packages/bruno-requests/src/ws/ws-client.js b/packages/bruno-requests/src/ws/ws-client.js index 838e939b46..1ef3f9082b 100644 --- a/packages/bruno-requests/src/ws/ws-client.js +++ b/packages/bruno-requests/src/ws/ws-client.js @@ -49,7 +49,22 @@ const normalizeMessageByFormat = (message, format) => { } }; +const createSequencer = () => { + const seq = {}; + const nextSeq = (requestId, collectionId) => { + seq[requestId] ||= {}; + seq[requestId][collectionId] ||= 0; + return ++seq[requestId][collectionId]; + }; + return { + next: nextSeq + }; +}; + +const seq = createSequencer(); + class WsClient { + sequenceState = {}; messageQueues = {}; activeConnections = new Map(); connectionKeepAlive = new Map(); @@ -181,6 +196,7 @@ class WsClient { message: payload, messageHexdump: hexdump(payload), type: 'outgoing', + seq: seq.next(requestId, collectionUid), timestamp: Date.now() }); } @@ -283,7 +299,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 +311,8 @@ class WsClient { message: `Redirected to ${url}`, type: 'info', timestamp: Date.now(), - headers: headers + headers: headers, + seq: seq.next(requestId, collectionUid) }); }); @@ -302,6 +320,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 +332,7 @@ class WsClient { message, messageHexdump: hexdump(Buffer.from(data)), type: 'incoming', + seq: seq.next(requestId, collectionUid), timestamp: Date.now() }); } catch (error) { @@ -321,6 +341,7 @@ class WsClient { message: data.toString(), messageHexdump: hexdump(data), type: 'incoming', + seq: seq.next(requestId, collectionUid), timestamp: Date.now() }); } @@ -330,6 +351,7 @@ class WsClient { this.eventCallback('main:ws:close', requestId, collectionUid, { code, reason: Buffer.from(reason).toString(), + seq: seq.next(requestId, collectionUid), timestamp: Date.now() }); this.#removeConnection(requestId); @@ -338,6 +360,7 @@ class WsClient { ws.on('error', (error) => { this.eventCallback('main:ws:error', requestId, collectionUid, { error: error.message, + seq: seq.next(requestId, collectionUid), timestamp: Date.now() }); }); @@ -356,6 +379,7 @@ class WsClient { this.eventCallback('main:ws:connections-changed', { type: 'added', requestId, + seq: seq.next(requestId, collectionUid), activeConnectionIds: this.getActiveConnectionIds() }); } From e012bf6182b41253d1761694d6476b1ee3d1cd66 Mon Sep 17 00:00:00 2001 From: Sid Date: Sat, 3 Jan 2026 09:33:04 +0530 Subject: [PATCH 09/13] fix: remove unused sequenceState property from WsClient --- packages/bruno-requests/src/ws/ws-client.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/bruno-requests/src/ws/ws-client.js b/packages/bruno-requests/src/ws/ws-client.js index 1ef3f9082b..e4ea68ab65 100644 --- a/packages/bruno-requests/src/ws/ws-client.js +++ b/packages/bruno-requests/src/ws/ws-client.js @@ -64,7 +64,6 @@ const createSequencer = () => { const seq = createSequencer(); class WsClient { - sequenceState = {}; messageQueues = {}; activeConnections = new Map(); connectionKeepAlive = new Map(); From b3062a82be7aa8783e32db8c6d327e3ba6c8a0c1 Mon Sep 17 00:00:00 2001 From: Sid Date: Sat, 3 Jan 2026 10:34:52 +0530 Subject: [PATCH 10/13] fix: update message sorting logic to handle missing sequence numbers --- .../ResponsePane/WsResponsePane/WSMessagesList/index.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 38e771bfaf..832a88ead7 100644 --- a/packages/bruno-app/src/components/ResponsePane/WsResponsePane/WSMessagesList/index.js +++ b/packages/bruno-app/src/components/ResponsePane/WsResponsePane/WSMessagesList/index.js @@ -183,7 +183,11 @@ const WSMessagesList = ({ order = -1, messages = [] }) => { if (!messages.length) { return
No messages yet.
; } - const ordered = order === -1 ? messages.toSorted((x, y) => x.seq - y.seq) : messages.toSorted((x, y) => y.seq - x.seq); + + // 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) => { From 687a3090064a378a6e5d5866e31ca168fdbf3103 Mon Sep 17 00:00:00 2001 From: Sid Date: Sat, 3 Jan 2026 10:41:16 +0530 Subject: [PATCH 11/13] fix: remove unused lodash import --- .../ResponsePane/WsResponsePane/WSMessagesList/index.js | 1 - 1 file changed, 1 deletion(-) 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 832a88ead7..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'; From 88e47d66d40bede6dc8c100156a27d0497c67a5c Mon Sep 17 00:00:00 2001 From: Sid Date: Sat, 3 Jan 2026 10:57:29 +0530 Subject: [PATCH 12/13] fix: add clean method to sequencer for better sequence management --- packages/bruno-requests/src/ws/ws-client.js | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/bruno-requests/src/ws/ws-client.js b/packages/bruno-requests/src/ws/ws-client.js index e4ea68ab65..def4c06e23 100644 --- a/packages/bruno-requests/src/ws/ws-client.js +++ b/packages/bruno-requests/src/ws/ws-client.js @@ -51,13 +51,29 @@ 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 + next: nextSeq, + clean }; }; @@ -219,6 +235,7 @@ class WsClient { if (connectionMeta?.connection) { connectionMeta.connection.close(code, reason); this.#removeConnection(requestId); + seq.clean(requestId); } } @@ -353,6 +370,7 @@ class WsClient { seq: seq.next(requestId, collectionUid), timestamp: Date.now() }); + seq.clean(requestId, collectionUid); this.#removeConnection(requestId); }); From 5498ba5d2ccbeebb7c7d13d7e8f81a6ea3515fe7 Mon Sep 17 00:00:00 2001 From: Sid Date: Sat, 3 Jan 2026 11:27:45 +0530 Subject: [PATCH 13/13] fix: don't show dropdown when streaming --- packages/bruno-app/src/components/ResponsePane/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) */}