Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion packages/ui/src/workflows/hooks/use-workflow-handler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
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";
Expand All @@ -11,6 +11,8 @@ interface UseWorkflowHandlerResult {
stopStreaming: () => void;
clearEvents: () => void;
sendEvent: (event: WorkflowEvent) => Promise<void>;
/** True when the provided handler id does not exist on the server */
notFound: boolean;
}

export function useWorkflowHandler(
Expand All @@ -24,6 +26,10 @@ export function useWorkflowHandler(
const unsubscribe = useHandlerStore((state) => state.unsubscribe);
const isSubscribed = useHandlerStore((state) => state.isSubscribed);
const clearEvents = useHandlerStore((state) => state.clearEvents);
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(() => {
Expand All @@ -46,6 +52,11 @@ export function useWorkflowHandler(
};
}, [handlerId, handler, autoStream, subscribe, unsubscribe]);

// Trigger existence verification when handlerId changes
useEffect(() => {
void checkExists(handlerId);
}, [handlerId, checkExists]);

const stopStreaming = useCallback(() => {
unsubscribe(handlerId);
}, [handlerId, unsubscribe]);
Expand All @@ -72,5 +83,6 @@ export function useWorkflowHandler(
stopStreaming,
clearEvents: clearHandlerEvents,
sendEvent,
notFound,
};
}
38 changes: 38 additions & 0 deletions packages/ui/src/workflows/store/handler-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
createHandler as createHandlerAPI,
fetchHandlerEvents,
getRunningHandlers,
getExistingHandler,
} from "./helper";
import type {
WorkflowHandlerSummary,
Expand All @@ -23,6 +24,7 @@ export interface HandlerStoreState {
// State
handlers: Record<string, WorkflowHandlerSummary>;
events: Record<string, WorkflowEvent[]>;
missingHandlers: Record<string, boolean>;

// Basic operations
clearCompleted(): void;
Expand All @@ -39,13 +41,18 @@ export interface HandlerStoreState {
subscribe(handlerId: string): void;
unsubscribe(handlerId: string): void;
isSubscribed(handlerId: string): boolean;

// Existence detection
checkExists(handlerId: string): Promise<void>;
isMissing(handlerId: string): boolean;
}

export const createHandlerStore = (client: Client) =>
create<HandlerStoreState>()((set, get) => ({
// Initial state
handlers: {},
events: {},
missingHandlers: {},

// Basic operations
clearCompleted: () =>
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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;
},
}));
44 changes: 43 additions & 1 deletion packages/ui/tests/workflows/hooks/use-workflow-handler.test.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -123,4 +123,46 @@ 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).mockReset();
(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).mockReset();
(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);
});
});
});
});
Loading