From 569a9454ad3cab1508d7059cfc0b5ccafea7184a Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Wed, 1 Mar 2023 09:05:00 -0800 Subject: [PATCH] Response streaming --- .../20230301163450_add-response-error.sql | 1 + src-tauri/sqlx-data.json | 358 ++++++++++-------- src-tauri/src/main.rs | 80 ++-- src-tauri/src/models.rs | 40 +- src-web/components/Editor/Editor.tsx | 1 - .../Editor/completion/completion.ts | 1 - src-web/components/Editor/extensions.ts | 38 +- src-web/components/Editor/twig/extension.ts | 51 +++ src-web/components/ResponsePane.tsx | 20 +- src-web/lib/models.ts | 1 + src-web/main.tsx | 49 ++- 11 files changed, 378 insertions(+), 262 deletions(-) create mode 100644 src-tauri/migrations/20230301163450_add-response-error.sql create mode 100644 src-web/components/Editor/twig/extension.ts diff --git a/src-tauri/migrations/20230301163450_add-response-error.sql b/src-tauri/migrations/20230301163450_add-response-error.sql new file mode 100644 index 00000000..578d876b --- /dev/null +++ b/src-tauri/migrations/20230301163450_add-response-error.sql @@ -0,0 +1 @@ +ALTER TABLE http_responses ADD COLUMN error TEXT NULL; diff --git a/src-tauri/sqlx-data.json b/src-tauri/sqlx-data.json index b30772c2..cb44cf1d 100644 --- a/src-tauri/sqlx-data.json +++ b/src-tauri/sqlx-data.json @@ -20,90 +20,6 @@ }, "query": "\n DELETE FROM http_responses\n WHERE request_id = ?\n " }, - "28675cd7ad73860417a667050694675e132b5e92cf6d3195a6eec218834e3a1d": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Text" - }, - { - "name": "workspace_id", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "request_id", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "updated_at", - "ordinal": 3, - "type_info": "Datetime" - }, - { - "name": "deleted_at", - "ordinal": 4, - "type_info": "Datetime" - }, - { - "name": "created_at", - "ordinal": 5, - "type_info": "Datetime" - }, - { - "name": "status", - "ordinal": 6, - "type_info": "Int64" - }, - { - "name": "status_reason", - "ordinal": 7, - "type_info": "Text" - }, - { - "name": "body", - "ordinal": 8, - "type_info": "Text" - }, - { - "name": "elapsed", - "ordinal": 9, - "type_info": "Int64" - }, - { - "name": "url", - "ordinal": 10, - "type_info": "Text" - }, - { - "name": "headers!: sqlx::types::Json>", - "ordinal": 11, - "type_info": "Text" - } - ], - "nullable": [ - false, - false, - false, - false, - true, - false, - false, - true, - false, - false, - false, - false - ], - "parameters": { - "Right": 1 - } - }, - "query": "\n SELECT id, workspace_id, request_id, updated_at, deleted_at, created_at, status, status_reason, body, elapsed, url,\n headers AS \"headers!: sqlx::types::Json>\"\n FROM http_responses\n WHERE request_id = ?\n ORDER BY created_at DESC\n " - }, "3d2a542964d946ff9854d053b1adf04985d97a6de27b713188505c1f99c77707": { "describe": { "columns": [ @@ -196,90 +112,6 @@ }, "query": "\n DELETE FROM http_requests\n WHERE id = ?\n " }, - "55eae4b20a2c313134579b0ea43bad4dc2dd313db6cd1654f783bac12602db8a": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Text" - }, - { - "name": "workspace_id", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "request_id", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "updated_at", - "ordinal": 3, - "type_info": "Datetime" - }, - { - "name": "deleted_at", - "ordinal": 4, - "type_info": "Datetime" - }, - { - "name": "created_at", - "ordinal": 5, - "type_info": "Datetime" - }, - { - "name": "status", - "ordinal": 6, - "type_info": "Int64" - }, - { - "name": "status_reason", - "ordinal": 7, - "type_info": "Text" - }, - { - "name": "body", - "ordinal": 8, - "type_info": "Text" - }, - { - "name": "elapsed", - "ordinal": 9, - "type_info": "Int64" - }, - { - "name": "url", - "ordinal": 10, - "type_info": "Text" - }, - { - "name": "headers!: sqlx::types::Json>", - "ordinal": 11, - "type_info": "Text" - } - ], - "nullable": [ - false, - false, - false, - false, - true, - false, - false, - true, - false, - false, - false, - false - ], - "parameters": { - "Right": 1 - } - }, - "query": "\n SELECT id, workspace_id, request_id, updated_at, deleted_at, created_at, status, status_reason, body, elapsed, url,\n headers AS \"headers!: sqlx::types::Json>\"\n FROM http_responses\n WHERE id = ?\n " - }, "7ec60cbc3c9f26e8af86a21ef6b66e564f4fa518925c92308b04f882237a244e": { "describe": { "columns": [ @@ -352,6 +184,96 @@ }, "query": "\n SELECT id, workspace_id, created_at, updated_at, deleted_at, name, url, method, body,\n headers AS \"headers!: sqlx::types::Json>\"\n FROM http_requests\n WHERE id = ?\n ORDER BY created_at DESC\n " }, + "7f623d0e8f1ddad33d356e2d159b776a2bef1a238cb9200d74eb0c5e3983df85": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "workspace_id", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "request_id", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "updated_at", + "ordinal": 3, + "type_info": "Datetime" + }, + { + "name": "deleted_at", + "ordinal": 4, + "type_info": "Datetime" + }, + { + "name": "created_at", + "ordinal": 5, + "type_info": "Datetime" + }, + { + "name": "status", + "ordinal": 6, + "type_info": "Int64" + }, + { + "name": "status_reason", + "ordinal": 7, + "type_info": "Text" + }, + { + "name": "body", + "ordinal": 8, + "type_info": "Text" + }, + { + "name": "elapsed", + "ordinal": 9, + "type_info": "Int64" + }, + { + "name": "url", + "ordinal": 10, + "type_info": "Text" + }, + { + "name": "error", + "ordinal": 11, + "type_info": "Text" + }, + { + "name": "headers!: sqlx::types::Json>", + "ordinal": 12, + "type_info": "Text" + } + ], + "nullable": [ + false, + false, + false, + false, + true, + false, + false, + true, + false, + false, + false, + true, + false + ], + "parameters": { + "Right": 1 + } + }, + "query": "\n SELECT id, workspace_id, request_id, updated_at, deleted_at,\n created_at, status, status_reason, body, elapsed, url, error,\n headers AS \"headers!: sqlx::types::Json>\"\n FROM http_responses\n WHERE request_id = ?\n ORDER BY created_at ASC\n " + }, "8069c0bd326f659faca7b45b03e5317d7339a168f4cd7776d9f84304bb7ae7ac": { "describe": { "columns": [ @@ -400,6 +322,16 @@ }, "query": "\n SELECT id, created_at, updated_at, deleted_at, name, description\n FROM workspaces\n " }, + "a83698dcf9a815b881097133edb31a34ba25e7c6c114d463c495342a85371639": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 8 + } + }, + "query": "\n UPDATE http_responses SET (elapsed, url, status, status_reason, body, error, headers, updated_at) =\n (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) WHERE id = ?;\n " + }, "e767522f92c8c49cd2e563e58737a05092daf9b1dc763bacc82a5c14d696d78e": { "describe": { "columns": [], @@ -467,5 +399,95 @@ } }, "query": "\n SELECT id, created_at, updated_at, deleted_at, name, description\n FROM workspaces\n WHERE id = ?\n " + }, + "fb2c2328678bbdcb64b79ced26f3d7a1b08d315ef6dedfe4d5ae4231c861b079": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "workspace_id", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "request_id", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "updated_at", + "ordinal": 3, + "type_info": "Datetime" + }, + { + "name": "deleted_at", + "ordinal": 4, + "type_info": "Datetime" + }, + { + "name": "created_at", + "ordinal": 5, + "type_info": "Datetime" + }, + { + "name": "status", + "ordinal": 6, + "type_info": "Int64" + }, + { + "name": "status_reason", + "ordinal": 7, + "type_info": "Text" + }, + { + "name": "body", + "ordinal": 8, + "type_info": "Text" + }, + { + "name": "elapsed", + "ordinal": 9, + "type_info": "Int64" + }, + { + "name": "url", + "ordinal": 10, + "type_info": "Text" + }, + { + "name": "error", + "ordinal": 11, + "type_info": "Text" + }, + { + "name": "headers!: sqlx::types::Json>", + "ordinal": 12, + "type_info": "Text" + } + ], + "nullable": [ + false, + false, + false, + false, + true, + false, + false, + true, + false, + false, + false, + true, + false + ], + "parameters": { + "Right": 1 + } + }, + "query": "\n SELECT id, workspace_id, request_id, updated_at, deleted_at, created_at,\n status, status_reason, body, elapsed, url, error,\n headers AS \"headers!: sqlx::types::Json>\"\n FROM http_responses\n WHERE id = ?\n " } } \ No newline at end of file diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 188af3ee..51eb1d20 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -16,12 +16,14 @@ use http::{HeaderMap, HeaderValue, Method}; use reqwest::redirect::Policy; use sqlx::migrate::Migrator; use sqlx::sqlite::SqlitePoolOptions; +use sqlx::types::Json; use sqlx::{Pool, Sqlite}; use tauri::regex::Regex; use tauri::{AppHandle, State, Wry}; use tauri::{CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, WindowEvent}; use tokio::sync::Mutex; +use crate::models::{update_response, HttpResponse}; use window_ext::WindowExt; mod models; @@ -55,11 +57,18 @@ async fn send_request( app_handle: AppHandle, db_instance: State<'_, Mutex>>, request_id: &str, -) -> Result { +) -> Result { let pool = &*db_instance.lock().await; + let req = models::get_request(request_id, pool) .await .expect("Failed to get request"); + + let mut response = models::create_response(&req.id, 0, "", 0, None, "", vec![], pool) + .await + .expect("Failed to create response"); + app_handle.emit_all("updated_response", &response).unwrap(); + let start = std::time::Instant::now(); let mut url_string = req.url.to_string(); @@ -110,7 +119,7 @@ async fn send_request( let sendable_req = match sendable_req_result { Ok(r) => r, Err(e) => { - return Err(e.to_string()); + return response_err(response, e.to_string(), app_handle, pool).await; } }; @@ -125,41 +134,44 @@ async fn send_request( match resp { Ok(v) => { - let status = v.status().as_u16() as i64; - let status_reason = v.status().canonical_reason(); - let headers = v - .headers() - .iter() - .map(|(k, v)| models::HttpResponseHeader { - name: k.as_str().to_string(), - value: v.to_str().unwrap().to_string(), - }) - .collect(); - let url = v.url().clone(); - let body = v.text().await.expect("Failed to get body"); - let elapsed = start.elapsed().as_millis() as i64; - let response = models::create_response( - &req.id, - elapsed, - url.as_str(), - status, - status_reason, - body.as_str(), - headers, - pool, - ) - .await - .expect("Failed to create response"); - - Ok(response) - } - Err(e) => { - println!("Error: {}", e); - Err(e.to_string()) + response.status = v.status().as_u16() as i64; + response.status_reason = v.status().canonical_reason().map(|s| s.to_string()); + response.headers = Json( + v.headers() + .iter() + .map(|(k, v)| models::HttpResponseHeader { + name: k.as_str().to_string(), + value: v.to_str().unwrap().to_string(), + }) + .collect(), + ); + response.url = v.url().to_string(); + response.body = v.text().await.expect("Failed to get body"); + response.elapsed = start.elapsed().as_millis() as i64; + response = update_response(response, pool) + .await + .expect("Failed to update response"); + app_handle.emit_all("updated_response", &response).unwrap(); + Ok(response.id) } + Err(e) => response_err(response, e.to_string(), app_handle, pool).await, } } +async fn response_err( + mut response: HttpResponse, + error: String, + app_handle: AppHandle, + pool: &Pool, +) -> Result { + response.error = Some(error.clone()); + response = update_response(response, pool) + .await + .expect("Failed to update response"); + app_handle.emit_all("updated_response", &response).unwrap(); + Ok(response.id) +} + #[tauri::command] async fn create_request( workspace_id: &str, @@ -175,7 +187,7 @@ async fn create_request( .expect("Failed to create request"); app_handle - .emit_all("created_request", &created_request) + .emit_all("updated_request", &created_request) .unwrap(); Ok(created_request.id) diff --git a/src-tauri/src/models.rs b/src-tauri/src/models.rs index 0085945f..2a063302 100644 --- a/src-tauri/src/models.rs +++ b/src-tauri/src/models.rs @@ -53,6 +53,7 @@ pub struct HttpResponse { pub created_at: NaiveDateTime, pub updated_at: NaiveDateTime, pub deleted_at: Option, + pub error: Option, pub url: String, pub elapsed: i64, pub status: i64, @@ -241,11 +242,37 @@ pub async fn create_response( get_response(&id, pool).await } +pub async fn update_response( + response: HttpResponse, + pool: &Pool, +) -> Result { + let headers_json = Json(response.headers); + sqlx::query!( + r#" + UPDATE http_responses SET (elapsed, url, status, status_reason, body, error, headers, updated_at) = + (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) WHERE id = ?; + "#, + response.elapsed, + response.url, + response.status, + response.status_reason, + response.body, + response.error, + headers_json, + response.id, + ) + .execute(pool) + .await + .expect("Failed to update response"); + get_response(&response.id, pool).await +} + pub async fn get_response(id: &str, pool: &Pool) -> Result { - sqlx::query_as!( + sqlx::query_as_unchecked!( HttpResponse, r#" - SELECT id, workspace_id, request_id, updated_at, deleted_at, created_at, status, status_reason, body, elapsed, url, + SELECT id, workspace_id, request_id, updated_at, deleted_at, created_at, + status, status_reason, body, elapsed, url, error, headers AS "headers!: sqlx::types::Json>" FROM http_responses WHERE id = ? @@ -263,16 +290,17 @@ pub async fn find_responses( sqlx::query_as!( HttpResponse, r#" - SELECT id, workspace_id, request_id, updated_at, deleted_at, created_at, status, status_reason, body, elapsed, url, + SELECT id, workspace_id, request_id, updated_at, deleted_at, + created_at, status, status_reason, body, elapsed, url, error, headers AS "headers!: sqlx::types::Json>" FROM http_responses WHERE request_id = ? - ORDER BY created_at DESC + ORDER BY created_at ASC "#, request_id, ) - .fetch_all(pool) - .await + .fetch_all(pool) + .await } pub async fn delete_response(id: &str, pool: &Pool) -> Result<(), sqlx::Error> { diff --git a/src-web/components/Editor/Editor.tsx b/src-web/components/Editor/Editor.tsx index 83412883..f69e4086 100644 --- a/src-web/components/Editor/Editor.tsx +++ b/src-web/components/Editor/Editor.tsx @@ -1,4 +1,3 @@ -import { autocompletion } from '@codemirror/autocomplete'; import type { Transaction, TransactionSpec } from '@codemirror/state'; import { Compartment, EditorSelection, EditorState, Prec } from '@codemirror/state'; import classnames from 'classnames'; diff --git a/src-web/components/Editor/completion/completion.ts b/src-web/components/Editor/completion/completion.ts index 3a0ff062..32c4cd1e 100644 --- a/src-web/components/Editor/completion/completion.ts +++ b/src-web/components/Editor/completion/completion.ts @@ -11,7 +11,6 @@ const variables = [ ]; export function myCompletions(context: CompletionContext) { - // console.log('COMPLETE', context); const toStartOfName = context.matchBefore(/\w*/); const toStartOfVariable = context.matchBefore(/\$\{.*/); const toMatch = toStartOfVariable ?? toStartOfName ?? null; diff --git a/src-web/components/Editor/extensions.ts b/src-web/components/Editor/extensions.ts index 4eaeb7e8..bd25dcba 100644 --- a/src-web/components/Editor/extensions.ts +++ b/src-web/components/Editor/extensions.ts @@ -9,14 +9,13 @@ import { html } from '@codemirror/lang-html'; import { javascript } from '@codemirror/lang-javascript'; import { json } from '@codemirror/lang-json'; import { xml } from '@codemirror/lang-xml'; +import type { LanguageSupport } from '@codemirror/language'; import { bracketMatching, foldGutter, foldKeymap, HighlightStyle, indentOnInput, - LanguageSupport, - LRLanguage, syntaxHighlighting, } from '@codemirror/language'; import { lintKeymap } from '@codemirror/lint'; @@ -33,12 +32,9 @@ import { lineNumbers, rectangularSelection, } from '@codemirror/view'; -import { parseMixed } from '@lezer/common'; import { tags as t } from '@lezer/highlight'; -import { myCompletions } from './completion/completion'; -import { parser as twigParser } from './twig/twig'; +import { twig } from './twig/extension'; import { url } from './url/extension'; -import { placeholders } from './widgets'; export const myHighlightStyle = HighlightStyle.define([ { @@ -102,35 +98,7 @@ export function getLanguageExtension({ return [base]; } - const mixedTwigParser = twigParser.configure({ - props: [ - // Add basic folding/indent metadata - // foldNodeProp.add({ Conditional: foldInside }), - // indentNodeProp.add({ - // Conditional: (cx) => { - // const closed = /^\s*\{% endif/.test(cx.textAfter); - // return cx.lineIndent(cx.node.from) + (closed ? 0 : cx.unit); - // }, - // }), - ], - wrap: parseMixed((node) => { - return node.type.isTop - ? { - parser: base.language.parser, - overlay: (node) => node.type.name === 'Text', - } - : null; - }), - }); - - const twigLanguage = LRLanguage.define({ parser: mixedTwigParser, languageData: {} }); - const completion = twigLanguage.data.of({ - autocomplete: myCompletions, - }); - const languageSupport = new LanguageSupport(twigLanguage, [completion]); - const completion2 = base.language.data.of({ autocomplete: myCompletions }); - const languageSupport2 = new LanguageSupport(base.language, [completion2]); - return [languageSupport, languageSupport2, placeholders, base.support]; + return twig(base); } export const baseExtensions = [ diff --git a/src-web/components/Editor/twig/extension.ts b/src-web/components/Editor/twig/extension.ts new file mode 100644 index 00000000..43cac83e --- /dev/null +++ b/src-web/components/Editor/twig/extension.ts @@ -0,0 +1,51 @@ +import { LRLanguage, LanguageSupport } from '@codemirror/language'; +import { parseMixed } from '@lezer/common'; +import { myCompletions } from '../completion/completion'; +import { placeholders } from '../widgets'; +import { parser as twigParser } from './twig'; + +export function twig(base?: LanguageSupport) { + const parser = mixedOrPlainParser(base); + const twigLanguage = LRLanguage.define({ name: 'twig', parser, languageData: {} }); + const completion = twigLanguage.data.of({ + autocomplete: myCompletions, + }); + const languageSupport = new LanguageSupport(twigLanguage, [completion]); + + if (base) { + const completion2 = base.language.data.of({ autocomplete: myCompletions }); + const languageSupport2 = new LanguageSupport(base.language, [completion2]); + return [languageSupport, languageSupport2, placeholders, base.support]; + } else { + return [languageSupport, placeholders]; + } +} + +function mixedOrPlainParser(base?: LanguageSupport) { + if (base === undefined) { + return twigParser; + } + + const mixedParser = twigParser.configure({ + props: [ + // Add basic folding/indent metadata + // foldNodeProp.add({ Conditional: foldInside }), + // indentNodeProp.add({ + // Conditional: (cx) => { + // const closed = /^\s*\{% endif/.test(cx.textAfter); + // return cx.lineIndent(cx.node.from) + (closed ? 0 : cx.unit); + // }, + // }), + ], + wrap: parseMixed((node) => { + return node.type.isTop + ? { + parser: base.language.parser, + overlay: (node) => node.type.name === 'Text', + } + : null; + }), + }); + + return mixedParser; +} diff --git a/src-web/components/ResponsePane.tsx b/src-web/components/ResponsePane.tsx index 9ace63a1..ee9bff55 100644 --- a/src-web/components/ResponsePane.tsx +++ b/src-web/components/ResponsePane.tsx @@ -1,12 +1,12 @@ -import { useDeleteAllResponses, useDeleteResponse, useResponses } from '../hooks/useResponses'; import { motion } from 'framer-motion'; -import { HStack, VStack } from './Stacks'; -import Editor from './Editor/Editor'; import { useEffect, useMemo, useState } from 'react'; -import { WindowDragRegion } from './WindowDragRegion'; +import { useDeleteAllResponses, useDeleteResponse, useResponses } from '../hooks/useResponses'; import { Dropdown } from './Dropdown'; -import { IconButton } from './IconButton'; +import Editor from './Editor/Editor'; import { Icon } from './Icon'; +import { IconButton } from './IconButton'; +import { HStack, VStack } from './Stacks'; +import { WindowDragRegion } from './WindowDragRegion'; interface Props { requestId: string; @@ -19,9 +19,10 @@ export function ResponsePane({ requestId, error }: Props) { const responses = useResponses(requestId); const response = activeResponseId ? responses.data.find((r) => r.id === activeResponseId) - : responses.data[0]; + : responses.data[responses.data.length - 1]; const deleteResponse = useDeleteResponse(response); const deleteAllResponses = useDeleteAllResponses(response?.requestId); + error = response?.error ?? error; useEffect(() => { setActiveResponseId(null); @@ -76,7 +77,9 @@ export function ResponsePane({ requestId, error }: Props) { items="center" className="italic text-gray-500 text-sm w-full h-10 mb-3 flex-shrink-0" > -
+
+ {response.updatedAt.toISOString()} +  •  {response.status} {response.statusReason && ` ${response.statusReason}`}  •  @@ -84,7 +87,6 @@ export function ResponsePane({ requestId, error }: Props) { {Math.round(response.body.length / 1000)} KB
-
{response.url}
{contentType.includes('html') && ( ) : response?.body ? ( diff --git a/src-web/lib/models.ts b/src-web/lib/models.ts index 63cd4165..b15b5105 100644 --- a/src-web/lib/models.ts +++ b/src-web/lib/models.ts @@ -28,6 +28,7 @@ export interface HttpResponse extends BaseModel { id: string; requestId: string; body: string; + error: string; status: string; elapsed: number; statusReason: string; diff --git a/src-web/main.tsx b/src-web/main.tsx index 70f1e685..dbf307bd 100644 --- a/src-web/main.tsx +++ b/src-web/main.tsx @@ -5,13 +5,15 @@ import App from './App'; import { HelmetProvider } from 'react-helmet-async'; import { MotionConfig } from 'framer-motion'; import { listen } from '@tauri-apps/api/event'; +import { responsesQueryKey } from './hooks/useResponses'; import { setTheme } from './lib/theme'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { createBrowserRouter, RouterProvider } from 'react-router-dom'; import { Layout } from './components/Layout'; import { Workspaces } from './pages/Workspaces'; import './main.css'; -import { convertDates, HttpRequest } from './lib/models'; +import type { HttpRequest, HttpResponse } from './lib/models'; +import { convertDates } from './lib/models'; import { requestsQueryKey } from './hooks/useRequest'; setTheme(); @@ -23,15 +25,24 @@ greet(); const queryClient = new QueryClient(); await listen('updated_request', ({ payload: request }: { payload: HttpRequest }) => { - queryClient.setQueryData(requestsQueryKey(request.workspaceId), (requests: HttpRequest[] = []) => - requests.map((r) => (r.id === request.id ? convertDates(request) : r)), - ); -}); - -await listen('created_request', ({ payload: request }: { payload: HttpRequest }) => { queryClient.setQueryData( requestsQueryKey(request.workspaceId), - (requests: HttpRequest[] = []) => [...requests, convertDates(request)], + (requests: HttpRequest[] = []) => { + const newRequests = []; + let found = false; + for (const r of requests) { + if (r.id === request.id) { + found = true; + newRequests.push(convertDates(request)); + } else { + newRequests.push(r); + } + } + if (!found) { + newRequests.push(convertDates(request)); + } + return newRequests; + }, ); }); @@ -41,6 +52,28 @@ await listen('deleted_request', ({ payload: request }: { payload: HttpRequest }) ); }); +await listen('updated_response', ({ payload: response }: { payload: HttpResponse }) => { + queryClient.setQueryData( + responsesQueryKey(response.requestId), + (responses: HttpResponse[] = []) => { + const newResponses = []; + let found = false; + for (const r of responses) { + if (r.id === response.id) { + found = true; + newResponses.push(convertDates(response)); + } else { + newResponses.push(r); + } + } + if (!found) { + newResponses.push(convertDates(response)); + } + return newResponses; + }, + ); +}); + const router = createBrowserRouter([ { path: '/',