feat: updated the bru async parsing logic (#3898)

This commit is contained in:
lohit
2025-01-28 21:26:46 +05:30
committed by GitHub
parent a06a339d0c
commit 98f3a524dc
18 changed files with 262 additions and 90 deletions

View File

@@ -28,26 +28,28 @@ const RequestNotLoaded = ({ collection, item }) => {
<div>{item?.pathname}</div>
</div>
</div>
<div className='flex flex-col gap-6 w-fit justify-start'>
<div className='flex flex-col'>
<button className={`submit btn btn-sm btn-secondary w-fit h-fit flex flex-row gap-2 ${item?.loading? 'opacity-50 cursor-blocked': ''}`} onClick={handleLoadRequestSync}>
{item?.loading ? `Loading Request` : `Load Request`}
{item?.loading ? <IconLoader2 className="animate-spin" size={18} strokeWidth={1.5} /> : null}
</button>
<small className='text-muted mt-1'>
May cause the app to freeze temporarily while it runs.
</small>
{!item?.error ?
<div className='flex flex-col gap-6 w-fit justify-start'>
<div className='flex flex-col'>
<button className={`submit btn btn-sm btn-secondary w-fit h-fit flex flex-row gap-2 ${item?.loading? 'opacity-50 cursor-blocked': ''}`} onClick={handleLoadRequestSync}>
{item?.loading ? `Loading Request` : `Load Request`}
{item?.loading ? <IconLoader2 className="animate-spin" size={18} strokeWidth={1.5} /> : null}
</button>
<small className='text-muted mt-1'>
May cause the app to freeze temporarily while it runs.
</small>
</div>
<div className='flex flex-col'>
<button className={`submit btn btn-sm btn-secondary w-fit h-fit flex flex-row gap-2 ${item?.loading? 'opacity-50 cursor-blocked': ''}`} onClick={handleLoadRequest}>
{item?.loading ? `Loading Request` : `Load Request in Background`}
{item?.loading ? <IconLoader2 className="animate-spin" size={18} strokeWidth={1.5} /> : null}
</button>
<small className='text-muted mt-1'>
Runs in background.
</small>
</div>
</div>
<div className='flex flex-col'>
<button className={`submit btn btn-sm btn-secondary w-fit h-fit flex flex-row gap-2 ${item?.loading? 'opacity-50 cursor-blocked': ''}`} onClick={handleLoadRequest}>
{item?.loading ? `Loading Request` : `Load Request in Background`}
{item?.loading ? <IconLoader2 className="animate-spin" size={18} strokeWidth={1.5} /> : null}
</button>
<small className='text-muted mt-1'>
Runs in background.
</small>
</div>
</div>
: null}
</div>
</>
}

View File

@@ -9,6 +9,7 @@ import { IconRefresh, IconCircleCheck, IconCircleX, IconCheck, IconX, IconRun }
import slash from 'utils/common/slash';
import ResponsePane from './ResponsePane';
import StyledWrapper from './StyledWrapper';
import { areItemsLoading } from 'utils/collections/index';
const getRelativePath = (fullPath, pathname) => {
// convert to unix style path
@@ -106,6 +107,8 @@ export default function RunnerResults({ collection }) {
return (item.status !== 'error' && item.testStatus === 'fail') || item.assertionStatus === 'fail';
});
let isCollectionLoading = areItemsLoading(collection);
if (!items || !items.length) {
return (
<StyledWrapper className="px-4 pb-4">
@@ -116,7 +119,7 @@ export default function RunnerResults({ collection }) {
<div className="mt-6">
You have <span className="font-medium">{totalRequestsInCollection}</span> requests in this collection.
</div>
{isCollectionLoading ? <div className='my-1 danger'>Requests in this collection are still loading.</div> : null}
<div className="mt-6">
<label>Delay (in ms)</label>
<input

View File

@@ -4,6 +4,9 @@ const Wrapper = styled.div`
.partial {
color: ${(props) => props.theme.colors.text.yellow};
}
.error {
color: ${(props) => props.theme.colors.text.danger};
}
`;
export default Wrapper;

View File

@@ -1,8 +1,12 @@
import RequestMethod from "../RequestMethod";
import { IconLoader2, IconAlertTriangle } from '@tabler/icons';
import { IconLoader2, IconAlertTriangle, IconAlertCircle } from '@tabler/icons';
import StyledWrapper from "./StyledWrapper";
const CollectionItemIcon = ({ item }) => {
if (item?.error) {
return <StyledWrapper><IconAlertCircle className="w-fit mr-2 error" size={18} strokeWidth={1.5} /></StyledWrapper>;
}
if (item?.loading) {
return <IconLoader2 className="animate-spin w-fit mr-2" size={18} strokeWidth={1.5} />;
}

View File

@@ -4,6 +4,9 @@ const Wrapper = styled.div`
.bruno-modal-content {
padding-bottom: 1rem;
}
.warning {
color: ${(props) => props.theme.colors.text.danger};
}
`;
export default Wrapper;

View File

@@ -7,6 +7,7 @@ import { addTab } from 'providers/ReduxStore/slices/tabs';
import { runCollectionFolder } from 'providers/ReduxStore/slices/collections/actions';
import { flattenItems } from 'utils/collections';
import StyledWrapper from './StyledWrapper';
import { areItemsLoading } from 'utils/collections/index';
const RunCollectionItem = ({ collection, item, onClose }) => {
const dispatch = useDispatch();
@@ -32,6 +33,8 @@ const RunCollectionItem = ({ collection, item, onClose }) => {
const flattenedItems = flattenItems(item ? item.items : collection.items);
const recursiveRunLength = getRequestsCount(flattenedItems);
const isFolderLoading = areItemsLoading(item);
return (
<StyledWrapper>
<Modal size="md" title="Collection Runner" hideFooter={true} handleCancel={onClose}>
@@ -44,13 +47,12 @@ const RunCollectionItem = ({ collection, item, onClose }) => {
<span className="ml-1 text-xs">({runLength} requests)</span>
</div>
<div className="mb-8">This will only run the requests in this folder.</div>
<div className="mb-1">
<span className="font-medium">Recursive Run</span>
<span className="ml-1 text-xs">({recursiveRunLength} requests)</span>
</div>
<div className="mb-8">This will run all the requests in this folder and all its subfolders.</div>
<div className={isFolderLoading ? "mb-2" : "mb-8"}>This will run all the requests in this folder and all its subfolders.</div>
{isFolderLoading ? <div className='mb-8 warning'>Requests in this folder are still loading.</div> : null}
<div className="flex justify-end bruno-modal-footer">
<span className="mr-3">
<button type="button" onClick={onClose} className="btn btn-md btn-close">

View File

@@ -1196,7 +1196,6 @@ export const hydrateCollectionWithUiStateSnapshot = (payload) => (dispatch, getS
export const loadRequest = ({ collectionUid, pathname }) => (dispatch, getState) => {
return new Promise(async (resolve, reject) => {
const { ipcRenderer } = window;
ipcRenderer.invoke('renderer:load-request-init', { collectionUid, pathname }).then(resolve).catch(reject);
ipcRenderer.invoke('renderer:load-request', { collectionUid, pathname }).then(resolve).catch(reject);
});
};
@@ -1204,7 +1203,6 @@ export const loadRequest = ({ collectionUid, pathname }) => (dispatch, getState)
export const loadRequestSync = ({ collectionUid, pathname }) => (dispatch, getState) => {
return new Promise(async (resolve, reject) => {
const { ipcRenderer } = window;
ipcRenderer.invoke('renderer:load-request-init', { collectionUid, pathname }).then(resolve).catch(reject);
ipcRenderer.invoke('renderer:load-request-sync', { collectionUid, pathname }).then(resolve).catch(reject);
});
};

View File

@@ -1617,6 +1617,7 @@ export const collectionsSlice = createSlice({
currentItem.partial = file.partial;
currentItem.loading = file.loading;
currentItem.size = file.size;
currentItem.error = file.error;
} else {
currentSubItems.push({
uid: file.data.uid,
@@ -1629,7 +1630,8 @@ export const collectionsSlice = createSlice({
draft: null,
partial: file.partial,
loading: file.loading,
size: file.size
size: file.size,
error: file.error
});
}
}

View File

@@ -3,7 +3,7 @@ const fs = require('fs');
const path = require('path');
const chokidar = require('chokidar');
const { hasBruExtension, isWSLPath, normalizeAndResolvePath, normalizeWslPath, sizeInMB } = require('../utils/filesystem');
const { bruToEnvJson, bruToJson, collectionBruToJson, bruToJsonViaWorker, collectionBruToJsonViaWorker } = require('../bru');
const { bruToEnvJson, bruToJson, bruToJsonViaWorker ,collectionBruToJson } = require('../bru');
const { dotenvToJson } = require('@usebruno/lang');
const { uuid } = require('../utils/common');
@@ -259,7 +259,7 @@ const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread
// If worker thread is not used, we can directly parse the file
if (!useWorkerThread) {
try {
file.data = bruToJson(bruContent);
file.data = await bruToJson(bruContent);
file.partial = false;
file.loading = false;
file.size = sizeInMB(fileStats?.size);
@@ -278,15 +278,22 @@ const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread
name: path.basename(pathname),
type: 'http-request'
};
const metaJson = await bruToJson(getBruFileMeta(bruContent), true);
file.data = metaJson;
file.partial = true;
file.loading = false;
file.size = sizeInMB(fileStats?.size);
hydrateRequestWithUuid(file.data, pathname);
win.webContents.send('main:collection-tree-updated', 'addFile', file);
// If the file is smaller than the max file size, we can parse the file
// and send the full file info to the UI
if (fileStats.size < MAX_FILE_SIZE) {
file.data = metaJson;
file.partial = false;
file.loading = true;
hydrateRequestWithUuid(file.data, pathname);
win.webContents.send('main:collection-tree-updated', 'addFile', file);
file.data = await bruToJsonViaWorker(bruContent);
file.partial = false;
file.loading = false;
@@ -298,6 +305,9 @@ const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread
name: path.basename(pathname),
type: 'http-request'
};
file.error = {
message: error?.message
};
file.partial = true;
file.loading = false;
file.size = sizeInMB(fileStats?.size);

View File

@@ -1,6 +1,7 @@
const _ = require('lodash');
const {
bruToJsonV2,
jsonToBruV2,
bruToEnvJsonV2,
envJsonToBruV2,
collectionBruToJson: _collectionBruToJson,
@@ -40,16 +41,6 @@ const collectionBruToJson = async (data, parsed = false) => {
}
};
const collectionBruToJsonViaWorker = async (bru) => {
try {
const json = await bruParserWorker?.collectionBruToJson(bru);
return collectionBruToJson(json);
} catch (error) {
return Promise.reject(error);
}
};
const jsonToCollectionBru = async (json, isFolder) => {
try {
const collectionBruJson = {
@@ -165,7 +156,6 @@ const bruToJson = (data, parsed = false) => {
const bruToJsonViaWorker = async (data) => {
try {
const json = await bruParserWorker?.bruToJson(data);
return bruToJson(json, true);
} catch (e) {
return Promise.reject(e);
@@ -218,10 +208,52 @@ const jsonToBru = async (json) => {
docs: _.get(json, 'request.docs', '')
};
const bru = jsonToBruV2(bruJson);
return bru;
};
const jsonToBruViaWorker = async (json) => {
let type = _.get(json, 'type');
if (type === 'http-request') {
type = 'http';
} else if (type === 'graphql-request') {
type = 'graphql';
} else {
type = 'http';
}
const sequence = _.get(json, 'seq');
const bruJson = {
meta: {
name: _.get(json, 'name'),
type: type,
seq: !isNaN(sequence) ? Number(sequence) : 1
},
http: {
method: _.lowerCase(_.get(json, 'request.method')),
url: _.get(json, 'request.url'),
auth: _.get(json, 'request.auth.mode', 'none'),
body: _.get(json, 'request.body.mode', 'none')
},
params: _.get(json, 'request.params', []),
headers: _.get(json, 'request.headers', []),
auth: _.get(json, 'request.auth', {}),
body: _.get(json, 'request.body', {}),
script: _.get(json, 'request.script', {}),
vars: {
req: _.get(json, 'request.vars.req', []),
res: _.get(json, 'request.vars.res', [])
},
assertions: _.get(json, 'request.assertions', []),
tests: _.get(json, 'request.tests', ''),
docs: _.get(json, 'request.docs', '')
};
const bru = await bruParserWorker?.jsonToBru(bruJson)
return bru;
};
module.exports = {
bruToJson,
bruToJsonViaWorker,
@@ -229,6 +261,6 @@ module.exports = {
bruToEnvJson,
envJsonToBru,
collectionBruToJson,
collectionBruToJsonViaWorker,
jsonToCollectionBru
jsonToCollectionBru,
jsonToBruViaWorker
};

View File

@@ -49,8 +49,8 @@ class BruParserWorker {
return this.enqueueTask({ data, scriptFile: `bru-to-json` });
}
async collectionBruToJson(data) {
return this.enqueueTask({ data, scriptFile: `collection-bru-to-json` });
async jsonToBru(data) {
return this.enqueueTask({ data, scriptFile: `json-to-bru` });
}
}

View File

@@ -10,4 +10,5 @@ try {
}
catch(error) {
console.error(error);
parentPort.postMessage({ error: error?.message });
}

View File

@@ -1,13 +0,0 @@
const { workerData, parentPort } = require('worker_threads');
const {
collectionBruToJson,
} = require('@usebruno/lang');
try {
const bru = workerData;
const json = collectionBruToJson(bru);
parentPort.postMessage(json);
}
catch(error) {
console.error(error);
}

View File

@@ -0,0 +1,13 @@
const { workerData, parentPort } = require('worker_threads');
const {
jsonToBruV2,
} = require('@usebruno/lang');
try {
const json = workerData;
const bru = jsonToBruV2(json);
parentPort.postMessage(bru);
}
catch(error) {
console.error(error);
parentPort.postMessage({ error: error?.message });
}

View File

@@ -4,7 +4,7 @@ const fsExtra = require('fs-extra');
const os = require('os');
const path = require('path');
const { ipcMain, shell, dialog, app } = require('electron');
const { envJsonToBru, bruToJson, jsonToBru, jsonToCollectionBru, bruToJsonViaWorker } = require('../bru');
const { envJsonToBru, bruToJson, jsonToBruViaWorker, jsonToCollectionBru, bruToJsonViaWorker } = require('../bru');
const {
isValidPathname,
@@ -226,7 +226,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
if (!isValidFilename(request.name)) {
throw new Error(`path: ${request.name}.bru is not a valid filename`);
}
const content = await jsonToBru(request);
const content = await jsonToBruViaWorker(request);
await writeFile(pathname, content);
} catch (error) {
return Promise.reject(error);
@@ -240,7 +240,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
throw new Error(`path: ${pathname} does not exist`);
}
const content = await jsonToBru(request);
const content = await jsonToBruViaWorker(request);
await writeFile(pathname, content);
} catch (error) {
return Promise.reject(error);
@@ -258,7 +258,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
throw new Error(`path: ${pathname} does not exist`);
}
const content = await jsonToBru(request);
const content = await jsonToBruViaWorker(request);
await writeFile(pathname, content);
}
} catch (error) {
@@ -425,11 +425,11 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
// update name in file and save new copy, then delete old copy
const data = await fs.promises.readFile(oldPath, 'utf8'); // Use async read
const jsonData = await bruToJson(data);
const jsonData = await bruToJsonViaWorker(data);
jsonData.name = newName;
moveRequestUid(oldPath, newPath);
const content = await jsonToBru(jsonData);
const content = await jsonToBruViaWorker(jsonData);
await fs.promises.unlink(oldPath);
await writeFile(newPath, content);
@@ -531,7 +531,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
const parseCollectionItems = (items = [], currentPath) => {
items.forEach(async (item) => {
if (['http-request', 'graphql-request'].includes(item.type)) {
const content = await jsonToBru(item);
const content = await jsonToBruViaWorker(item);
const filePath = path.join(currentPath, `${item.name}.bru`);
fs.writeFileSync(filePath, content);
}
@@ -626,7 +626,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
const parseCollectionItems = (items = [], currentPath) => {
items.forEach(async (item) => {
if (['http-request', 'graphql-request'].includes(item.type)) {
const content = await jsonToBru(item);
const content = await jsonToBruViaWorker(item);
const filePath = path.join(currentPath, `${item.name}.bru`);
fs.writeFileSync(filePath, content);
}
@@ -672,11 +672,11 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
try {
for await (let item of itemsToResequence) {
const bru = fs.readFileSync(item.pathname, 'utf8');
const jsonData = await bruToJson(bru);
const jsonData = await bruToJsonViaWorker(bru);
if (jsonData.seq !== item.seq) {
jsonData.seq = item.seq;
const content = await jsonToBru(jsonData);
const content = await jsonToBruViaWorker(jsonData);
await writeFile(item.pathname, content);
}
}
@@ -792,7 +792,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
}
});
ipcMain.handle('renderer:load-request-init', async (event, { collectionUid, pathname }) => {
ipcMain.handle('renderer:load-request', async (event, { collectionUid, pathname }) => {
let fileStats;
try {
fileStats = fs.statSync(pathname);
@@ -812,26 +812,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
file.size = sizeInMB(fileStats?.size);
hydrateRequestWithUuid(file.data, pathname);
mainWindow.webContents.send('main:collection-tree-updated', 'addFile', file);
}
} catch (error) {
return Promise.reject(error);
}
});
ipcMain.handle('renderer:load-request', async (event, { collectionUid, pathname }) => {
let fileStats;
try {
fileStats = fs.statSync(pathname);
if (hasBruExtension(pathname)) {
const file = {
meta: {
collectionUid,
pathname,
name: path.basename(pathname)
}
};
let bruContent = fs.readFileSync(pathname, 'utf8');
file.data = await bruToJson(bruContent);
file.data = await bruToJsonViaWorker(bruContent);
file.partial = false;
file.loading = true;
file.size = sizeInMB(fileStats?.size);
@@ -873,6 +854,13 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
}
};
let bruContent = fs.readFileSync(pathname, 'utf8');
const metaJson = await bruToJson(getBruFileMeta(bruContent), true);
file.data = metaJson;
file.loading = true;
file.partial = true;
file.size = sizeInMB(fileStats?.size);
hydrateRequestWithUuid(file.data, pathname);
mainWindow.webContents.send('main:collection-tree-updated', 'addFile', file);
file.data = bruToJson(bruContent);
file.partial = false;
file.loading = true;

View File

@@ -210,7 +210,7 @@ const getTreePathFromCollectionToItem = (collection, _item) => {
const getBruFileMeta = (data) => {
try {
const metaRegex = /meta\s*{\s*([\s\S]*?)\s*}/;
const match = data.match(metaRegex);
const match = data?.match?.(metaRegex);
if (match) {
const metaContent = match[1].trim();
const lines = metaContent.replace(/\r\n/g, '\n').split('\n');

View File

@@ -39,6 +39,9 @@ class WorkerQueue {
return new Promise((resolve, reject) => {
const worker = new Worker(scriptPath, { workerData: data });
worker.on('message', (data) => {
if (data?.error) {
reject(new Error(data?.error));
}
resolve(data);
worker.terminate();
});

View File

@@ -0,0 +1,121 @@
const { getBruFileMeta } = require("../../src/utils/collection");
describe('getBruFileMeta', () => {
test('parses valid meta block correctly', () => {
const data = `meta {
name: 0.2_mb
type: http
seq: 1
}`;
const result = getBruFileMeta(data);
expect(result).toEqual({
meta: {
name: '0.2_mb',
type: 'http',
seq: 1,
},
});
});
test('returns undefined for missing meta block', () => {
const data = `someOtherBlock {
key: value
}`;
const result = getBruFileMeta(data);
expect(result).toBeUndefined();
});
test('handles empty meta block gracefully', () => {
const data = `meta {}`;
const result = getBruFileMeta(data);
expect(result).toEqual({ meta: {} });
});
test('ignores invalid lines in meta block', () => {
const data = `meta {
name: 0.2_mb
invalidLine
seq: 1
}`;
const result = getBruFileMeta(data);
expect(result).toEqual({
meta: {
name: '0.2_mb',
seq: 1,
},
});
});
test('handles unexpected input gracefully', () => {
const data = null;
const result = getBruFileMeta(data);
expect(result).toBeUndefined();
});
test('handles missing colon gracefully', () => {
const data = `meta {
name 0.2_mb
seq: 1
}`;
const result = getBruFileMeta(data);
expect(result).toEqual({
meta: {
seq: 1,
},
});
});
test('parses numeric values correctly', () => {
const data = `meta {
numValue: 1234
floatValue: 12.34
strValue: some_text
}`;
const result = getBruFileMeta(data);
expect(result).toEqual({
meta: {
numValue: 1234,
floatValue: 12.34,
strValue: 'some_text',
},
});
});
test('handles syntax error in meta block 1', () => {
const data = `meta
name: 0.2_mb
type: http
seq: 1
}`;
const result = getBruFileMeta(data);
expect(result).toBeUndefined();
});
test('handles syntax error in meta block 2', () => {
const data = `meta {
name: 0.2_mb
type: http
seq: 1
`;
const result = getBruFileMeta(data);
expect(result).toBeUndefined();
});
});