From af5d5e4087270ef137758aeb9877ded0faf30926 Mon Sep 17 00:00:00 2001 From: npub1jmc9dt2lyvzu3h0kxlwxt5zg4fxp9476awyxw6gwxn72g6cw7exqs64whm <96f056ad5f2305c8ddf637dc65d048aa4c12d7daeb8867690e34fca46b0ef64c@sprout-oss.stage.blox.sqprod.co> Date: Wed, 8 Jul 2026 10:23:49 -0400 Subject: [PATCH] perf(desktop): batch workflows overview into one relay query (#h fanout) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit L6 finding: WorkflowsView's allWorkflowsQuery issued one get_channel_workflows relay POST per member channel (Promise.all fanout) — N round-trips scaling with channel count on the Workflows overview. A nostr #h filter matches ANY listed value, so one query with all channel ids returns the identical set. Added get_channels_workflows(channel_ids) which queries once; each WorkflowWire already carries its own channel_id (from the event h tag), so WorkflowsView groups results client-side via a channelId->name map. Neither the per-channel nor the batched command sets a limit, so batching does not change result completeness (review-bar #3: no shared-limit truncation). e2eBridge gains a matching get_channels_workflows mock so overview e2e still resolves. Single-channel get_channel_workflows stays for useChannelWorkflowsQuery. Co-authored-by: Tyler Longwell Signed-off-by: Tyler Longwell --- desktop/src-tauri/src/commands/workflows.rs | 30 +++++++++++++++++++ desktop/src-tauri/src/lib.rs | 1 + .../features/workflows/ui/WorkflowsView.tsx | 24 +++++++++------ desktop/src/shared/api/tauriWorkflows.ts | 16 ++++++++++ desktop/src/testing/e2eBridge.ts | 11 +++++++ 5 files changed, 73 insertions(+), 9 deletions(-) diff --git a/desktop/src-tauri/src/commands/workflows.rs b/desktop/src-tauri/src/commands/workflows.rs index 6dfeb6b51..1d5f309fb 100644 --- a/desktop/src-tauri/src/commands/workflows.rs +++ b/desktop/src-tauri/src/commands/workflows.rs @@ -66,6 +66,36 @@ pub async fn get_channel_workflows( Ok(events.iter().map(workflow_from_event).collect()) } +/// Fetch workflows across many channels in a single relay round-trip. +/// +/// The Workflows overview screen previously issued one `get_channel_workflows` +/// query per member channel (`Promise.all` fanout in `WorkflowsView`), i.e. N +/// relay POSTs. A nostr `#h` filter matches ANY of its listed values, so one +/// query with all channel ids returns the same set. Each `WorkflowWire` carries +/// its own `channel_id` (from the event's `h` tag), so the frontend can still +/// group results by channel. Neither this nor the per-channel command sets a +/// `limit`, so batching does not change result completeness. +#[tauri::command] +pub async fn get_channels_workflows( + channel_ids: Vec, + state: State<'_, AppState>, +) -> Result, String> { + if channel_ids.is_empty() { + return Ok(Vec::new()); + } + + let events = query_relay( + &state, + &[serde_json::json!({ + "kinds": [30620], + "#h": channel_ids, + })], + ) + .await?; + + Ok(events.iter().map(workflow_from_event).collect()) +} + #[tauri::command] pub async fn get_workflow( workflow_id: String, diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 341c99d7b..9f95922f4 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -569,6 +569,7 @@ pub fn run() { parse_persona_files, export_persona_to_json, get_channel_workflows, + get_channels_workflows, get_workflow, create_workflow, update_workflow, diff --git a/desktop/src/features/workflows/ui/WorkflowsView.tsx b/desktop/src/features/workflows/ui/WorkflowsView.tsx index a628fd83a..449db3781 100644 --- a/desktop/src/features/workflows/ui/WorkflowsView.tsx +++ b/desktop/src/features/workflows/ui/WorkflowsView.tsx @@ -10,7 +10,7 @@ import { WorkflowDialog } from "@/features/workflows/ui/WorkflowDialog"; import type { Channel, Workflow } from "@/shared/api/types"; import { deleteWorkflow, - getChannelWorkflows, + getChannelsWorkflows, triggerWorkflow, } from "@/shared/api/tauriWorkflows"; import { Button } from "@/shared/ui/button"; @@ -83,15 +83,21 @@ export function WorkflowsView({ const allWorkflowsQuery = useQuery({ queryKey: allWorkflowsQueryKey(channelIdKey), queryFn: async () => { - const results: WorkflowWithChannel[] = []; - await Promise.all( - memberChannels.map(async (channel) => { - const workflows = await getChannelWorkflows(channel.id); - for (const workflow of workflows) { - results.push({ workflow, channelName: channel.name }); - } - }), + // Single batched relay query for all member channels, then group by the + // channel_id each workflow carries — replaces the per-channel fanout. + const channelNameById = new Map( + memberChannels.map((channel) => [channel.id, channel.name]), ); + const workflows = await getChannelsWorkflows(channelIds); + const results: WorkflowWithChannel[] = []; + for (const workflow of workflows) { + results.push({ + workflow, + channelName: workflow.channelId + ? (channelNameById.get(workflow.channelId) ?? "") + : "", + }); + } return results; }, enabled: memberChannels.length > 0, diff --git a/desktop/src/shared/api/tauriWorkflows.ts b/desktop/src/shared/api/tauriWorkflows.ts index be72b2c49..2fbf4be04 100644 --- a/desktop/src/shared/api/tauriWorkflows.ts +++ b/desktop/src/shared/api/tauriWorkflows.ts @@ -169,6 +169,22 @@ export async function getChannelWorkflows( return raw.map(fromRawWorkflow); } +/** + * Fetch workflows across many channels in a single relay round-trip. + * + * Replaces the per-channel `Promise.all(getChannelWorkflows)` fanout on the + * Workflows overview: the backend `#h` filter matches any listed channel, and + * each returned workflow carries its own `channelId` so callers can group. + */ +export async function getChannelsWorkflows( + channelIds: string[], +): Promise { + const raw = await invokeTauri("get_channels_workflows", { + channelIds, + }); + return raw.map(fromRawWorkflow); +} + export async function getWorkflow(workflowId: string): Promise { const raw = await invokeTauri("get_workflow", { workflowId }); return fromRawWorkflow(raw); diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts index 953b3de40..1bfba7f02 100644 --- a/desktop/src/testing/e2eBridge.ts +++ b/desktop/src/testing/e2eBridge.ts @@ -2368,6 +2368,13 @@ function handleGetChannelWorkflows(args: { channelId: string }) { return mockWorkflows.filter((w) => w.channel_id === args.channelId); } +function handleGetChannelsWorkflows(args: { channelIds: string[] }) { + const ids = new Set(args.channelIds); + return mockWorkflows.filter( + (w) => w.channel_id != null && ids.has(w.channel_id), + ); +} + function handleGetWorkflow(args: { workflowId: string }) { const workflow = mockWorkflows.find((w) => w.id === args.workflowId); if (!workflow) throw new Error(`Workflow ${args.workflowId} not found`); @@ -8806,6 +8813,10 @@ export function maybeInstallE2eTauriMocks() { return handleGetChannelWorkflows( payload as Parameters[0], ); + case "get_channels_workflows": + return handleGetChannelsWorkflows( + payload as Parameters[0], + ); case "get_workflow": return handleGetWorkflow( payload as Parameters[0],