From dad5a6d3c6dcffcac23363f5eefd9e2eaf1d425e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 2 Oct 2025 16:31:26 +0000 Subject: [PATCH 1/2] feat: Add notFound flag to useWorkflowHandler hook Co-authored-by: adrian --- .../workflows/hooks/use-workflow-handler.ts | 34 ++++++++++++++- .../hooks/use-workflow-handler.test.ts | 42 ++++++++++++++++++- 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/workflows/hooks/use-workflow-handler.ts b/packages/ui/src/workflows/hooks/use-workflow-handler.ts index fee326a..4538903 100644 --- a/packages/ui/src/workflows/hooks/use-workflow-handler.ts +++ b/packages/ui/src/workflows/hooks/use-workflow-handler.ts @@ -1,7 +1,7 @@ -import { useEffect, useCallback, useMemo } from "react"; +import { useEffect, useCallback, useMemo, useState } from "react"; import { useHandlerStore } from "./use-handler-store"; import type { WorkflowHandlerSummary, WorkflowEvent } from "../types"; -import { sendEventToHandler } from "../store/helper"; +import { sendEventToHandler, getExistingHandler } from "../store/helper"; import { useWorkflowsClient } from "../../lib/api-provider"; interface UseWorkflowHandlerResult { @@ -11,6 +11,8 @@ interface UseWorkflowHandlerResult { stopStreaming: () => void; clearEvents: () => void; sendEvent: (event: WorkflowEvent) => Promise; + /** True when the provided handler id does not exist on the server */ + notFound: boolean; } export function useWorkflowHandler( @@ -24,6 +26,7 @@ export function useWorkflowHandler( const unsubscribe = useHandlerStore((state) => state.unsubscribe); const isSubscribed = useHandlerStore((state) => state.isSubscribed); const clearEvents = useHandlerStore((state) => state.clearEvents); + const [notFound, setNotFound] = useState(false); // Memoize events array to avoid creating new empty arrays const events = useMemo(() => { @@ -46,6 +49,32 @@ export function useWorkflowHandler( }; }, [handlerId, handler, autoStream, subscribe, unsubscribe]); + // Verify handler existence on the server when handlerId changes + useEffect(() => { + let cancelled = false; + + // Empty id: do not attempt to fetch + if (!handlerId) { + setNotFound(false); + return; + } + + // Optimistically assume not found is false; verify with server + // If the handler is present locally, we still verify to catch expired/deleted cases + (async () => { + try { + await getExistingHandler({ client, handlerId }); + if (!cancelled) setNotFound(false); + } catch (_) { + if (!cancelled) setNotFound(true); + } + })(); + + return () => { + cancelled = true; + }; + }, [client, handlerId]); + const stopStreaming = useCallback(() => { unsubscribe(handlerId); }, [handlerId, unsubscribe]); @@ -72,5 +101,6 @@ export function useWorkflowHandler( stopStreaming, clearEvents: clearHandlerEvents, sendEvent, + notFound, }; } diff --git a/packages/ui/tests/workflows/hooks/use-workflow-handler.test.ts b/packages/ui/tests/workflows/hooks/use-workflow-handler.test.ts index 0b77382..fa54fb4 100644 --- a/packages/ui/tests/workflows/hooks/use-workflow-handler.test.ts +++ b/packages/ui/tests/workflows/hooks/use-workflow-handler.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, beforeEach, vi } from "vitest"; -import { act } from "@testing-library/react"; +import { act, waitFor } from "@testing-library/react"; import { useWorkflowHandler } from "../../../src/workflows/hooks/use-workflow-handler"; import { renderHookWithProvider } from "../../test-utils"; @@ -123,4 +123,44 @@ describe("useWorkflowTask", () => { ); }); }); + + describe("notFound flag when handler id is missing on server", () => { + it("sets notFound=true when getExistingHandler rejects", async () => { + const { getExistingHandler } = await import( + "../../../src/workflows/store/helper" + ); + + // Force rejection for this check + (getExistingHandler as unknown as vi.Mock).mockRejectedValueOnce( + new Error("not found") + ); + + const { result } = renderHookWithProvider(() => + useWorkflowHandler("missing-123") + ); + + await waitFor(() => { + expect(result.current.notFound).toBe(true); + }); + }); + + it("sets notFound=false when getExistingHandler resolves", async () => { + const { getExistingHandler } = await import( + "../../../src/workflows/store/helper" + ); + + (getExistingHandler as unknown as vi.Mock).mockResolvedValueOnce({ + handler_id: "ok-123", + status: "running", + }); + + const { result } = renderHookWithProvider(() => + useWorkflowHandler("ok-123") + ); + + await waitFor(() => { + expect(result.current.notFound).toBe(false); + }); + }); + }); }); From b53461b48c908d9d9b73321a43901d10a83302ff Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 2 Oct 2025 19:40:53 +0000 Subject: [PATCH 2/2] Refactor handler existence check to use store Co-authored-by: adrian --- .../workflows/hooks/use-workflow-handler.ts | 34 ++++------------- .../ui/src/workflows/store/handler-store.ts | 38 +++++++++++++++++++ .../hooks/use-workflow-handler.test.ts | 2 + 3 files changed, 48 insertions(+), 26 deletions(-) diff --git a/packages/ui/src/workflows/hooks/use-workflow-handler.ts b/packages/ui/src/workflows/hooks/use-workflow-handler.ts index 4538903..cb03263 100644 --- a/packages/ui/src/workflows/hooks/use-workflow-handler.ts +++ b/packages/ui/src/workflows/hooks/use-workflow-handler.ts @@ -1,7 +1,7 @@ import { useEffect, useCallback, useMemo, useState } from "react"; import { useHandlerStore } from "./use-handler-store"; import type { WorkflowHandlerSummary, WorkflowEvent } from "../types"; -import { sendEventToHandler, getExistingHandler } from "../store/helper"; +import { sendEventToHandler } from "../store/helper"; import { useWorkflowsClient } from "../../lib/api-provider"; interface UseWorkflowHandlerResult { @@ -26,7 +26,10 @@ export function useWorkflowHandler( const unsubscribe = useHandlerStore((state) => state.unsubscribe); const isSubscribed = useHandlerStore((state) => state.isSubscribed); const clearEvents = useHandlerStore((state) => state.clearEvents); - const [notFound, setNotFound] = useState(false); + const checkExists = useHandlerStore((state) => state.checkExists); + const notFound = useHandlerStore( + (state) => state.missingHandlers[handlerId] === true + ); // Memoize events array to avoid creating new empty arrays const events = useMemo(() => { @@ -49,31 +52,10 @@ export function useWorkflowHandler( }; }, [handlerId, handler, autoStream, subscribe, unsubscribe]); - // Verify handler existence on the server when handlerId changes + // Trigger existence verification when handlerId changes useEffect(() => { - let cancelled = false; - - // Empty id: do not attempt to fetch - if (!handlerId) { - setNotFound(false); - return; - } - - // Optimistically assume not found is false; verify with server - // If the handler is present locally, we still verify to catch expired/deleted cases - (async () => { - try { - await getExistingHandler({ client, handlerId }); - if (!cancelled) setNotFound(false); - } catch (_) { - if (!cancelled) setNotFound(true); - } - })(); - - return () => { - cancelled = true; - }; - }, [client, handlerId]); + void checkExists(handlerId); + }, [handlerId, checkExists]); const stopStreaming = useCallback(() => { unsubscribe(handlerId); diff --git a/packages/ui/src/workflows/store/handler-store.ts b/packages/ui/src/workflows/store/handler-store.ts index db63e75..f4f0f27 100644 --- a/packages/ui/src/workflows/store/handler-store.ts +++ b/packages/ui/src/workflows/store/handler-store.ts @@ -11,6 +11,7 @@ import { createHandler as createHandlerAPI, fetchHandlerEvents, getRunningHandlers, + getExistingHandler, } from "./helper"; import type { WorkflowHandlerSummary, @@ -23,6 +24,7 @@ export interface HandlerStoreState { // State handlers: Record; events: Record; + missingHandlers: Record; // Basic operations clearCompleted(): void; @@ -39,6 +41,10 @@ export interface HandlerStoreState { subscribe(handlerId: string): void; unsubscribe(handlerId: string): void; isSubscribed(handlerId: string): boolean; + + // Existence detection + checkExists(handlerId: string): Promise; + isMissing(handlerId: string): boolean; } export const createHandlerStore = (client: Client) => @@ -46,6 +52,7 @@ export const createHandlerStore = (client: Client) => // Initial state handlers: {}, events: {}, + missingHandlers: {}, // Basic operations clearCompleted: () => @@ -133,6 +140,8 @@ export const createHandlerStore = (client: Client) => if (!handler) { // eslint-disable-next-line no-console -- needed console.warn(`Handler ${handlerId} not found for subscription`); + // proactively check existence and mark missing if invalid + void get().checkExists(handlerId); return; } @@ -203,6 +212,8 @@ export const createHandlerStore = (client: Client) => ) { return; } + // If the stream fails to establish or errors, verify existence + void get().checkExists(handlerId); // Update handler status to error set((state) => ({ handlers: { @@ -232,4 +243,31 @@ export const createHandlerStore = (client: Client) => const streamKey = `handler:${handlerId}`; return workflowStreamingManager.isStreamActive(streamKey); }, + + // Existence detection using existing API responses + checkExists: async (handlerId: string) => { + // Avoid empty id + if (!handlerId) { + set((state) => ({ + missingHandlers: { ...state.missingHandlers, [handlerId]: false }, + })); + return; + } + + // Use helper that queries handlers to determine existence + try { + await getExistingHandler({ client, handlerId }); + set((state) => ({ + missingHandlers: { ...state.missingHandlers, [handlerId]: false }, + })); + } catch (_) { + set((state) => ({ + missingHandlers: { ...state.missingHandlers, [handlerId]: true }, + })); + } + }, + + isMissing: (handlerId: string): boolean => { + return get().missingHandlers[handlerId] === true; + }, })); diff --git a/packages/ui/tests/workflows/hooks/use-workflow-handler.test.ts b/packages/ui/tests/workflows/hooks/use-workflow-handler.test.ts index fa54fb4..de57191 100644 --- a/packages/ui/tests/workflows/hooks/use-workflow-handler.test.ts +++ b/packages/ui/tests/workflows/hooks/use-workflow-handler.test.ts @@ -131,6 +131,7 @@ describe("useWorkflowTask", () => { ); // Force rejection for this check + (getExistingHandler as unknown as vi.Mock).mockReset(); (getExistingHandler as unknown as vi.Mock).mockRejectedValueOnce( new Error("not found") ); @@ -149,6 +150,7 @@ describe("useWorkflowTask", () => { "../../../src/workflows/store/helper" ); + (getExistingHandler as unknown as vi.Mock).mockReset(); (getExistingHandler as unknown as vi.Mock).mockResolvedValueOnce({ handler_id: "ok-123", status: "running",