From 6c69fff27da4dcccaa2be4decc27edd562a2dc0c Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Fri, 2 Feb 2024 13:32:06 -0800 Subject: [PATCH] A bit better handling of responses --- src-tauri/src/main.rs | 80 +++++++++++---------- src-web/components/GrpcConnectionLayout.tsx | 60 +++++++++------- src-web/components/RequestPane.tsx | 2 - src-web/components/UrlBar.tsx | 2 +- src-web/components/core/SplitLayout.tsx | 60 +++++++++------- src-web/hooks/useGrpc.ts | 68 ++++++++++++------ src-web/hooks/useKeyValue.ts | 16 +++-- 7 files changed, 168 insertions(+), 120 deletions(-) diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 966e0e51..15d7d518 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -189,25 +189,29 @@ async fn cmd_grpc_bidi_streaming( } } }; - let event_handler = app_handle.listen_global("grpc_message_in", cb); + let event_handler = + app_handle.listen_global(format!("grpc_client_msg_{}", conn_id).as_str(), cb); - let app_handle2 = app_handle.clone(); - let grpc_listen = async move { - loop { - match stream.next().await { - Some(Ok(item)) => { - let item = serde_json::to_string_pretty(&item).unwrap(); - app_handle2 - .emit_all("grpc_message", item) - .expect("Failed to emit"); - } - Some(Err(e)) => { - error!("gRPC stream error: {:?}", e); - // TODO: Handle error - } - None => { - info!("gRPC stream closed by sender"); - break; + let grpc_listen = { + let app_handle = app_handle.clone(); + let conn_id = conn_id.clone(); + async move { + loop { + match stream.next().await { + Some(Ok(item)) => { + let item = serde_json::to_string_pretty(&item).unwrap(); + app_handle + .emit_all(format!("grpc_server_msg_{}", &conn_id).as_str(), item) + .expect("Failed to emit"); + } + Some(Err(e)) => { + error!("gRPC stream error: {:?}", e); + // TODO: Handle error + } + None => { + info!("gRPC stream closed by sender"); + break; + } } } } @@ -283,25 +287,29 @@ async fn cmd_grpc_server_streaming( } } }; - let event_handler = app_handle.listen_global("grpc_message_in", cb); + let event_handler = + app_handle.listen_global(format!("grpc_client_msg_{}", conn_id).as_str(), cb); - let app_handle2 = app_handle.clone(); - let grpc_listen = async move { - loop { - match stream.next().await { - Some(Ok(item)) => { - let item = serde_json::to_string_pretty(&item).unwrap(); - app_handle2 - .emit_all("grpc_message", item) - .expect("Failed to emit"); - } - Some(Err(e)) => { - error!("gRPC stream error: {:?}", e); - // TODO: Handle error - } - None => { - info!("gRPC stream closed by sender"); - break; + let grpc_listen = { + let app_handle = app_handle.clone(); + let conn_id = conn_id.clone(); + async move { + loop { + match stream.next().await { + Some(Ok(item)) => { + let item = serde_json::to_string_pretty(&item).unwrap(); + app_handle + .emit_all(format!("grpc_server_msg_{}", &conn_id).as_str(), item) + .expect("Failed to emit"); + } + Some(Err(e)) => { + error!("gRPC stream error: {:?}", e); + // TODO: Handle error + } + None => { + info!("gRPC stream closed by sender"); + break; + } } } } diff --git a/src-web/components/GrpcConnectionLayout.tsx b/src-web/components/GrpcConnectionLayout.tsx index 72af6e18..b8e881ca 100644 --- a/src-web/components/GrpcConnectionLayout.tsx +++ b/src-web/components/GrpcConnectionLayout.tsx @@ -3,6 +3,7 @@ import classNames from 'classnames'; import { format } from 'date-fns'; import type { CSSProperties, FormEvent } from 'react'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useActiveRequestId } from '../hooks/useActiveRequestId'; import { useAlert } from '../hooks/useAlert'; import type { GrpcMessage } from '../hooks/useGrpc'; import { useGrpc } from '../hooks/useGrpc'; @@ -26,26 +27,31 @@ interface Props { } export function GrpcConnectionLayout({ style }: Props) { - const url = useKeyValue({ namespace: 'debug', key: 'grpc_url', defaultValue: '' }); + const activeRequestId = useActiveRequestId(); + const url = useKeyValue({ + namespace: 'debug', + key: ['grpc_url', activeRequestId ?? ''], + defaultValue: '', + }); const alert = useAlert(); const service = useKeyValue({ namespace: 'debug', - key: 'grpc_service', + key: ['grpc_service', activeRequestId ?? ''], defaultValue: null, }); const method = useKeyValue({ namespace: 'debug', - key: 'grpc_method', + key: ['grpc_method', activeRequestId ?? ''], defaultValue: null, }); const message = useKeyValue({ namespace: 'debug', - key: 'grpc_message', + key: ['grpc_message', activeRequestId ?? ''], defaultValue: '', }); const [activeMessage, setActiveMessage] = useState(null); const [resp, setResp] = useState(''); - const grpc = useGrpc(url.value ?? null); + const grpc = useGrpc(url.value ?? null, activeRequestId); const activeMethod = useMemo(() => { if (grpc.schema == null) return null; @@ -105,6 +111,7 @@ export function GrpcConnectionLayout({ style }: Props) { ); useEffect(() => { + console.log('GrpcConnectionLayout'); if (grpc.schema == null) return; const s = grpc.schema.find((s) => s.name === service.value); if (s == null) { @@ -167,11 +174,10 @@ export function GrpcConnectionLayout({ style }: Props) { )} > {!service.isLoading && !method.isLoading && ( {grpc.unary.error} - ) : grpc.messages.length > 0 ? ( + ) : (grpc.messages.value ?? []).length > 0 ? ( (
- {...grpc.messages.map((m, i) => ( + {...(grpc.messages.value ?? []).map((m, i) => ( { if (m === activeMessage) setActiveMessage(null); @@ -292,25 +298,29 @@ export function GrpcConnectionLayout({ style }: Props) { : 'info' } /> -
{m.message}
-
- {format(m.time, 'HH:mm:ss')} +
{m.message}
+
+ {format(m.timestamp, 'HH:mm:ss')}
))}
)} - rightSlot={() => - activeMessage && ( -
-
- -
-
- -
-
- ) + rightSlot={ + !activeMessage + ? null + : () => ( +
+
+ +
+
+ +
+
+ ) } /> ) : resp ? ( diff --git a/src-web/components/RequestPane.tsx b/src-web/components/RequestPane.tsx index ae899af4..196d1dd9 100644 --- a/src-web/components/RequestPane.tsx +++ b/src-web/components/RequestPane.tsx @@ -209,8 +209,6 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN {activeRequest && ( <> & { +type Props = Pick & { className?: string; method: HttpRequest['method'] | null; placeholder: string; diff --git a/src-web/components/core/SplitLayout.tsx b/src-web/components/core/SplitLayout.tsx index 9a8d2cc3..a073bfdf 100644 --- a/src-web/components/core/SplitLayout.tsx +++ b/src-web/components/core/SplitLayout.tsx @@ -17,7 +17,7 @@ interface SlotProps { interface Props { name: string; leftSlot: (props: SlotProps) => ReactNode; - rightSlot: (props: SlotProps) => ReactNode; + rightSlot: null | ((props: SlotProps) => ReactNode); style?: CSSProperties; className?: string; defaultRatio?: number; @@ -48,33 +48,37 @@ export function SplitLayout({ `${name}_height::${useActiveWorkspaceId()}`, ); const width = widthRaw ?? defaultRatio; - const height = heightRaw ?? defaultRatio; + let height = heightRaw ?? defaultRatio; const [isResizing, setIsResizing] = useState(false); const moveState = useRef<{ move: (e: MouseEvent) => void; up: (e: MouseEvent) => void } | null>( null, ); + if (!rightSlot) { + height = 0; + minHeightPx = 0; + } + useResizeObserver(containerRef.current, ({ contentRect }) => { setVertical(contentRect.width < STACK_VERTICAL_WIDTH); }); - const styles = useMemo( - () => ({ + const styles = useMemo(() => { + return { ...style, gridTemplate: vertical ? ` - ' ${areaL.gridArea}' minmax(0,${1 - height}fr) - ' ${areaD.gridArea}' 0 - ' ${areaR.gridArea}' minmax(${minHeightPx}px,${height}fr) - / 1fr - ` + ' ${areaL.gridArea}' minmax(0,${1 - height}fr) + ' ${areaD.gridArea}' 0 + ' ${areaR.gridArea}' minmax(${minHeightPx}px,${height}fr) + / 1fr + ` : ` - ' ${areaL.gridArea} ${areaD.gridArea} ${areaR.gridArea}' minmax(0,1fr) - / ${1 - width}fr 0 ${width}fr - `, - }), - [vertical, width, height, style], - ); + ' ${areaL.gridArea} ${areaD.gridArea} ${areaR.gridArea}' minmax(0,1fr) + / ${1 - width}fr 0 ${width}fr + `, + }; + }, [style, vertical, height, minHeightPx, width]); const unsub = () => { if (moveState.current !== null) { @@ -142,17 +146,21 @@ export function SplitLayout({ return (
{leftSlot({ style: areaL, orientation: vertical ? 'vertical' : 'horizontal' })} - - {rightSlot({ style: areaR, orientation: vertical ? 'vertical' : 'horizontal' })} + {rightSlot && ( + <> + + {rightSlot({ style: areaR, orientation: vertical ? 'vertical' : 'horizontal' })} + + )}
); } diff --git a/src-web/hooks/useGrpc.ts b/src-web/hooks/useGrpc.ts index 8bde7391..0ac6e5d4 100644 --- a/src-web/hooks/useGrpc.ts +++ b/src-web/hooks/useGrpc.ts @@ -1,8 +1,9 @@ import { useMutation, useQuery } from '@tanstack/react-query'; import { invoke } from '@tauri-apps/api'; -import { emit } from '@tauri-apps/api/event'; -import { useState } from 'react'; -import { useListenToTauriEvent } from './useListenToTauriEvent'; +import type { UnlistenFn } from '@tauri-apps/api/event'; +import { emit, listen } from '@tauri-apps/api/event'; +import { useEffect, useRef, useState } from 'react'; +import { useKeyValue } from './useKeyValue'; interface ReflectResponseService { name: string; @@ -11,24 +12,23 @@ interface ReflectResponseService { export interface GrpcMessage { message: string; - time: Date; + timestamp: string; type: 'server' | 'client' | 'info'; } -export function useGrpc(url: string | null) { - const [messages, setMessages] = useState([]); +export function useGrpc(url: string | null, requestId: string | null) { + const messages = useKeyValue({ + namespace: 'debug', + key: ['grpc_msgs', requestId ?? 'n/a'], + defaultValue: [], + }); const [activeConnectionId, setActiveConnectionId] = useState(null); + const unlisten = useRef(null); - useListenToTauriEvent( - 'grpc_message', - (event) => { - setMessages((prev) => [ - ...prev, - { message: event.payload, time: new Date(), type: 'server' }, - ]); - }, - [setMessages], - ); + useEffect(() => { + setActiveConnectionId(null); + unlisten.current?.(); + }, [requestId]); const unary = useMutation({ mutationKey: ['grpc_unary', url], @@ -51,8 +51,12 @@ export function useGrpc(url: string | null) { mutationKey: ['grpc_server_streaming', url], mutationFn: async ({ service, method, message }) => { if (url === null) throw new Error('No URL provided'); - setMessages([ - { type: 'client', message: JSON.stringify(JSON.parse(message)), time: new Date() }, + await messages.set([ + { + type: 'client', + message: JSON.stringify(JSON.parse(message)), + timestamp: new Date().toISOString(), + }, ]); const id: string = await invoke('cmd_grpc_server_streaming', { endpoint: url, @@ -60,6 +64,12 @@ export function useGrpc(url: string | null) { method, message, }); + unlisten.current = await listen(`grpc_server_msg_${id}`, async (event) => { + await messages.set((prev) => [ + ...prev, + { message: event.payload as string, timestamp: new Date().toISOString(), type: 'server' }, + ]); + }); setActiveConnectionId(id); }, }); @@ -78,16 +88,27 @@ export function useGrpc(url: string | null) { method, message, }); - setMessages([{ type: 'info', message: `Started connection ${id}`, time: new Date() }]); + messages.set([ + { type: 'info', message: `Started connection ${id}`, timestamp: new Date().toISOString() }, + ]); setActiveConnectionId(id); + unlisten.current = await listen(`grpc_server_msg_${id}`, (event) => { + messages.set((prev) => [ + ...prev, + { message: event.payload as string, timestamp: new Date().toISOString(), type: 'server' }, + ]); + }); }, }); const send = useMutation({ mutationKey: ['grpc_send', url], mutationFn: async ({ message }: { message: string }) => { - await emit('grpc_message_in', { Message: message }); - setMessages((m) => [...m, { type: 'client', message, time: new Date() }]); + if (activeConnectionId == null) throw new Error('No active connection'); + await messages.set((m) => { + return [...m, { type: 'client', message, timestamp: new Date().toISOString() }]; + }); + await emit(`grpc_client_msg_${activeConnectionId}`, { Message: message }); }, }); @@ -95,10 +116,11 @@ export function useGrpc(url: string | null) { mutationKey: ['grpc_cancel', url], mutationFn: async () => { setActiveConnectionId(null); + unlisten.current?.(); await emit('grpc_message_in', 'Cancel'); - setMessages((m) => [ + await messages.set((m) => [ ...m, - { type: 'info', message: 'Cancelled by client', time: new Date() }, + { type: 'info', message: 'Cancelled by client', timestamp: new Date().toISOString() }, ]); }, }); diff --git a/src-web/hooks/useKeyValue.ts b/src-web/hooks/useKeyValue.ts index 3a2b0d56..aaa5561b 100644 --- a/src-web/hooks/useKeyValue.ts +++ b/src-web/hooks/useKeyValue.ts @@ -37,19 +37,21 @@ export function useKeyValue({ }); const set = useCallback( - (value: ((v: T) => T) | T) => { + async (value: ((v: T) => T) | T) => { if (typeof value === 'function') { - getKeyValue({ namespace, key, fallback: defaultValue }).then((kv) => { - mutate.mutate(value(kv)); + await getKeyValue({ namespace, key, fallback: defaultValue }).then((kv) => { + const newV = value(kv); + if (newV === kv) return; + return mutate.mutateAsync(newV); }); - } else { - mutate.mutate(value); + } else if (value !== query.data) { + await mutate.mutateAsync(value); } }, - [defaultValue, key, mutate, namespace], + [defaultValue, key, mutate, namespace, query.data], ); - const reset = useCallback(() => mutate.mutate(defaultValue), [mutate, defaultValue]); + const reset = useCallback(async () => mutate.mutateAsync(defaultValue), [mutate, defaultValue]); return useMemo( () => ({