/** * @fileoverview V3 Workflows Data Layer Composable * @description Provides V3 workflows data management for Sidepanel UI * * This composable wraps the V3 RPC client and provides: * - Flow listing, running, and deletion * - Run listing and event subscription * - Trigger management * - Data mapping from V3 types to UI types */ import { onMounted, onUnmounted, ref, type Ref } from 'vue'; import type { FlowV3 } from '@/entrypoints/background/record-replay-v3/domain/flow'; import type { RunRecordV3 } from '@/entrypoints/background/record-replay-v3/domain/events'; import type { TriggerSpec } from '@/entrypoints/background/record-replay-v3/domain/triggers'; import type { FlowId, RunId } from '@/entrypoints/background/record-replay-v3/domain/ids'; import { useRRV3Rpc } from './useRRV3Rpc'; // ==================== UI Types ==================== /** Flow type for UI display (compatible with existing WorkflowsView) */ export interface FlowLite { id: string; name: string; description?: string; meta?: { domain?: string; tags?: string[]; bindings?: Array<{ kind?: string; // V3 uses 'kind' type?: string; // V2 uses 'type' value: string; }>; }; } /** Run type for UI display (compatible with existing WorkflowsView) */ export interface RunLite { id: string; flowId: string; startedAt: string; finishedAt?: string; /** * Terminal success status: true=succeeded, false=failed/canceled, undefined=in progress * UI should check `isInProgress` first to distinguish in-progress from failed */ success?: boolean; /** Whether the run is still in progress (queued/running/paused) */ isInProgress: boolean; status: RunRecordV3['status']; entries: unknown[]; } /** Trigger type for UI display */ export interface TriggerLite { id: string; type: string; // UI uses 'type', V3 uses 'kind' kind: string; // V3 uses 'kind' flowId: string; enabled?: boolean; match?: Array<{ kind: string; value: string }>; // For URL triggers [key: string]: unknown; } // ==================== Mappers ==================== /** Convert V3 FlowV3 to UI FlowLite */ function mapFlowV3ToLite(flow: FlowV3): FlowLite { return { id: flow.id, name: flow.name, description: flow.description, meta: { tags: flow.meta?.tags, bindings: flow.meta?.bindings?.map((b) => ({ kind: b.kind, type: b.kind, // For V2 compatibility value: b.value, })), }, }; } /** Convert V3 RunRecordV3 to UI RunLite */ function mapRunV3ToLite(run: RunRecordV3): RunLite { // Determine if run is in progress const inProgressStatuses = ['queued', 'running', 'paused']; const isInProgress = inProgressStatuses.includes(run.status); // Map V3 status to success boolean for terminal states only let success: boolean | undefined; if (run.status === 'succeeded') success = true; else if (run.status === 'failed' || run.status === 'canceled') success = false; // For in-progress states, success remains undefined return { id: run.id, flowId: run.flowId, startedAt: run.startedAt ? new Date(run.startedAt).toISOString() : new Date(run.createdAt).toISOString(), finishedAt: run.finishedAt ? new Date(run.finishedAt).toISOString() : undefined, success, isInProgress, status: run.status, entries: [], // V3 doesn't have entries in RunRecord, use getEvents for details }; } /** Convert V3 TriggerSpec to UI TriggerLite */ function mapTriggerV3ToLite(trigger: TriggerSpec): TriggerLite { return { ...trigger, type: trigger.kind, // Map 'kind' to 'type' for UI compatibility kind: trigger.kind, } as TriggerLite; } // ==================== Composable ==================== export interface UseWorkflowsV3Options { /** Auto-refresh interval in ms (0 = disabled) */ autoRefreshMs?: number; /** Auto-connect on mount */ autoConnect?: boolean; } export interface UseWorkflowsV3Return { // Connection state connected: Ref; loading: Ref; error: Ref; // Data flows: Ref; runs: Ref; triggers: Ref; // Actions refresh: () => Promise; refreshFlows: () => Promise; refreshRuns: () => Promise; refreshTriggers: () => Promise; runFlow: (flowId: string) => Promise<{ runId: string } | null>; deleteFlow: (flowId: string) => Promise; exportFlow: (flowId: string) => Promise; deleteTrigger: (triggerId: string) => Promise; // V3-specific getFlowById: (flowId: string) => Promise; getRunEvents: (runId: string) => Promise; } /** * V3 Workflows data layer composable */ export function useWorkflowsV3(options: UseWorkflowsV3Options = {}): UseWorkflowsV3Return { const { autoRefreshMs = 0, autoConnect = true } = options; // RPC client const rpc = useRRV3Rpc({ autoConnect }); // State const loading = ref(false); const error = ref(null); const flows = ref([]); const runs = ref([]); const triggers = ref([]); // Auto-refresh timer let refreshTimer: ReturnType | null = null; // Event subscription cleanup function let eventUnsubscribe: (() => void) | null = null; // ==================== Actions ==================== async function refreshFlows(): Promise { try { const result = (await rpc.request('rr_v3.listFlows')) as FlowV3[] | null; flows.value = (result || []).map(mapFlowV3ToLite); } catch (e) { console.warn('[useWorkflowsV3] Failed to refresh flows:', e); error.value = e instanceof Error ? e.message : String(e); } } async function refreshRuns(): Promise { try { const result = (await rpc.request('rr_v3.listRuns')) as RunRecordV3[] | null; // Sort by createdAt descending (newest first) const sorted = (result || []).slice().sort((a, b) => b.createdAt - a.createdAt); runs.value = sorted.map(mapRunV3ToLite); } catch (e) { console.warn('[useWorkflowsV3] Failed to refresh runs:', e); error.value = e instanceof Error ? e.message : String(e); } } async function refreshTriggers(): Promise { try { const result = (await rpc.request('rr_v3.listTriggers')) as TriggerSpec[] | null; triggers.value = (result || []).map(mapTriggerV3ToLite); } catch (e) { console.warn('[useWorkflowsV3] Failed to refresh triggers:', e); error.value = e instanceof Error ? e.message : String(e); } } async function refresh(): Promise { loading.value = true; error.value = null; try { await Promise.all([refreshFlows(), refreshRuns(), refreshTriggers()]); } finally { loading.value = false; } } async function runFlow(flowId: string): Promise<{ runId: string } | null> { try { const result = (await rpc.request('rr_v3.enqueueRun', { flowId: flowId as FlowId, })) as { runId: RunId; position: number } | null; // Refresh runs to show the new run void refreshRuns(); return result ? { runId: result.runId } : null; } catch (e) { console.warn('[useWorkflowsV3] Failed to run flow:', e); error.value = e instanceof Error ? e.message : String(e); return null; } } async function deleteFlow(flowId: string): Promise { try { await rpc.request('rr_v3.deleteFlow', { flowId: flowId as FlowId }); // Refresh flows after deletion void refreshFlows(); return true; } catch (e) { console.warn('[useWorkflowsV3] Failed to delete flow:', e); error.value = e instanceof Error ? e.message : String(e); return false; } } async function exportFlow(flowId: string): Promise { try { const result = (await rpc.request('rr_v3.getFlow', { flowId: flowId as FlowId, })) as FlowV3 | null; return result; } catch (e) { console.warn('[useWorkflowsV3] Failed to export flow:', e); error.value = e instanceof Error ? e.message : String(e); return null; } } async function deleteTrigger(triggerId: string): Promise { try { await rpc.request('rr_v3.deleteTrigger', { triggerId }); // Refresh triggers after deletion void refreshTriggers(); return true; } catch (e) { console.warn('[useWorkflowsV3] Failed to delete trigger:', e); error.value = e instanceof Error ? e.message : String(e); return false; } } async function getFlowById(flowId: string): Promise { try { return (await rpc.request('rr_v3.getFlow', { flowId: flowId as FlowId, })) as FlowV3 | null; } catch (e) { console.warn('[useWorkflowsV3] Failed to get flow:', e); return null; } } async function getRunEvents(runId: string): Promise { try { return (await rpc.request('rr_v3.getEvents', { runId: runId as RunId, })) as unknown[]; } catch (e) { console.warn('[useWorkflowsV3] Failed to get run events:', e); return []; } } // ==================== Lifecycle ==================== onMounted(async () => { if (autoConnect) { await rpc.ensureConnected(); await refresh(); } // Setup auto-refresh if (autoRefreshMs > 0) { refreshTimer = setInterval(() => { void refresh(); }, autoRefreshMs); } // Subscribe to all run events for real-time updates void rpc.subscribe(null); eventUnsubscribe = rpc.onEvent((event) => { // Refresh runs when run status changes const runStatusEvents = [ 'run.queued', 'run.started', 'run.succeeded', 'run.failed', 'run.canceled', 'run.paused', 'run.resumed', 'run.recovered', ]; if (runStatusEvents.includes(event.type)) { void refreshRuns(); } }); }); onUnmounted(() => { // Cleanup auto-refresh timer if (refreshTimer) { clearInterval(refreshTimer); refreshTimer = null; } // Cleanup event subscription if (eventUnsubscribe) { eventUnsubscribe(); eventUnsubscribe = null; } // Unsubscribe from run events void rpc.unsubscribe(null); }); return { connected: rpc.connected, loading, error, flows, runs, triggers, refresh, refreshFlows, refreshRuns, refreshTriggers, runFlow, deleteFlow, exportFlow, deleteTrigger, getFlowById, getRunEvents, }; }