diff --git a/crates/yaak-http/src/transaction.rs b/crates/yaak-http/src/transaction.rs index 9c997edc..1a3eb5af 100644 --- a/crates/yaak-http/src/transaction.rs +++ b/crates/yaak-http/src/transaction.rs @@ -342,7 +342,8 @@ mod tests { #[tokio::test] async fn test_transaction_single_redirect() { - let redirect_headers = vec![("Location".to_string(), "https://example.com/new".to_string())]; + let redirect_headers = + vec![("Location".to_string(), "https://example.com/new".to_string())]; let responses = vec![ MockResponse { status: 302, headers: redirect_headers, body: vec![] }, @@ -373,7 +374,8 @@ mod tests { #[tokio::test] async fn test_transaction_max_redirects_exceeded() { - let redirect_headers = vec![("Location".to_string(), "https://example.com/loop".to_string())]; + let redirect_headers = + vec![("Location".to_string(), "https://example.com/loop".to_string())]; // Create more redirects than allowed let responses: Vec = (0..12) @@ -525,7 +527,8 @@ mod tests { _request: SendableHttpRequest, _event_tx: mpsc::Sender, ) -> Result { - let headers = vec![("set-cookie".to_string(), "session=xyz789; Path=/".to_string())]; + let headers = + vec![("set-cookie".to_string(), "session=xyz789; Path=/".to_string())]; let body_stream: Pin> = Box::pin(std::io::Cursor::new(vec![])); @@ -584,7 +587,10 @@ mod tests { let headers = vec![ ("set-cookie".to_string(), "session=abc123; Path=/".to_string()), ("set-cookie".to_string(), "user_id=42; Path=/".to_string()), - ("set-cookie".to_string(), "preferences=dark; Path=/; Max-Age=86400".to_string()), + ( + "set-cookie".to_string(), + "preferences=dark; Path=/; Max-Age=86400".to_string(), + ), ]; let body_stream: Pin> = diff --git a/crates/yaak-models/guest-js/store.ts b/crates/yaak-models/guest-js/store.ts index 836ba9d1..2ac19a25 100644 --- a/crates/yaak-models/guest-js/store.ts +++ b/crates/yaak-models/guest-js/store.ts @@ -206,6 +206,22 @@ export function replaceModelsInStore< }); } +export function mergeModelsInStore< + M extends AnyModel['model'], + T extends Extract, +>(model: M, models: T[]) { + mustStore().set(modelStoreDataAtom, (prev: ModelStoreData) => { + const existingModels = { ...prev[model] } as Record; + for (const m of models) { + existingModels[m.id] = m; + } + return { + ...prev, + [model]: existingModels, + }; + }); +} + function shouldIgnoreModel({ model, updateSource }: ModelPayload) { // Never ignore updates from non-user sources if (updateSource.type !== 'window') { diff --git a/src-web/hooks/useHttpResponseEvents.ts b/src-web/hooks/useHttpResponseEvents.ts index 6e0dff79..582bff11 100644 --- a/src-web/hooks/useHttpResponseEvents.ts +++ b/src-web/hooks/useHttpResponseEvents.ts @@ -1,6 +1,10 @@ import { invoke } from '@tauri-apps/api/core'; import type { HttpResponse, HttpResponseEvent } from '@yaakapp-internal/models'; -import { httpResponseEventsAtom, replaceModelsInStore } from '@yaakapp-internal/models'; +import { + httpResponseEventsAtom, + mergeModelsInStore, + replaceModelsInStore, +} from '@yaakapp-internal/models'; import { useAtomValue } from 'jotai'; import { useEffect, useMemo } from 'react'; @@ -13,8 +17,10 @@ export function useHttpResponseEvents(response: HttpResponse | null) { return; } + // Use merge instead of replace to preserve events that came in via model_write + // while we were fetching from the database invoke('cmd_get_http_response_events', { responseId: response.id }).then( - (events) => replaceModelsInStore('http_response_event', events), + (events) => mergeModelsInStore('http_response_event', events), ); }, [response?.id]); diff --git a/src-web/hooks/usePinnedGrpcConnection.ts b/src-web/hooks/usePinnedGrpcConnection.ts index f000b6e5..bd88a69a 100644 --- a/src-web/hooks/usePinnedGrpcConnection.ts +++ b/src-web/hooks/usePinnedGrpcConnection.ts @@ -3,6 +3,7 @@ import type { GrpcConnection, GrpcEvent } from '@yaakapp-internal/models'; import { grpcConnectionsAtom, grpcEventsAtom, + mergeModelsInStore, replaceModelsInStore, } from '@yaakapp-internal/models'; import { atom, useAtomValue } from 'jotai'; @@ -67,8 +68,10 @@ export function useGrpcEvents(connectionId: string | null) { return; } + // Use merge instead of replace to preserve events that came in via model_write + // while we were fetching from the database invoke('models_grpc_events', { connectionId }).then((events) => { - replaceModelsInStore('grpc_event', events); + mergeModelsInStore('grpc_event', events); }); }, [connectionId]); diff --git a/src-web/hooks/usePinnedWebsocketConnection.ts b/src-web/hooks/usePinnedWebsocketConnection.ts index ee44cc68..d36ed6d5 100644 --- a/src-web/hooks/usePinnedWebsocketConnection.ts +++ b/src-web/hooks/usePinnedWebsocketConnection.ts @@ -1,6 +1,7 @@ import { invoke } from '@tauri-apps/api/core'; import type { WebsocketConnection, WebsocketEvent } from '@yaakapp-internal/models'; import { + mergeModelsInStore, replaceModelsInStore, websocketConnectionsAtom, websocketEventsAtom, @@ -54,8 +55,10 @@ export function useWebsocketEvents(connectionId: string | null) { return; } + // Use merge instead of replace to preserve events that came in via model_write + // while we were fetching from the database invoke('models_websocket_events', { connectionId }).then( - (events) => replaceModelsInStore('websocket_event', events), + (events) => mergeModelsInStore('websocket_event', events), ); }, [connectionId]); diff --git a/src-web/lib/model_util.test.ts b/src-web/lib/model_util.test.ts index 4cf179ab..dc7a6c88 100644 --- a/src-web/lib/model_util.test.ts +++ b/src-web/lib/model_util.test.ts @@ -11,7 +11,9 @@ function makeEvent( id: 'test', model: 'http_response_event', responseId: 'resp', - createdAt: Date.now(), + workspaceId: 'ws', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), event: { type, name, value } as HttpResponseEvent['event'], }; }