From eb6812aaa8086359c48ddcbb322e427f1ae64272 Mon Sep 17 00:00:00 2001 From: Emir Karabeg Date: Mon, 17 Feb 2025 14:04:26 -0800 Subject: [PATCH] fix(dbsync): only sync with db on exit --- app/w/[id]/workflow.tsx | 5 +- app/w/components/workflows/sync-wrapper.tsx | 18 -- drizzle.config.ts | 2 +- middleware.ts | 2 +- stores/index.ts | 6 + stores/sync-manager.ts | 69 ++++++++ stores/workflow/sync/hooks.ts | 176 -------------------- 7 files changed, 78 insertions(+), 200 deletions(-) delete mode 100644 app/w/components/workflows/sync-wrapper.tsx create mode 100644 stores/sync-manager.ts delete mode 100644 stores/workflow/sync/hooks.ts diff --git a/app/w/[id]/workflow.tsx b/app/w/[id]/workflow.tsx index 5944c2ad02..224a2534e8 100644 --- a/app/w/[id]/workflow.tsx +++ b/app/w/[id]/workflow.tsx @@ -19,7 +19,6 @@ import { initializeStateLogger } from '@/stores/workflow/logger' import { useWorkflowRegistry } from '@/stores/workflow/registry/store' import { useWorkflowStore } from '@/stores/workflow/store' import { NotificationList } from '@/app/w/components/notifications/notifications' -import { WorkflowSyncWrapper } from '@/app/w/components/workflows/sync-wrapper' import { getBlock } from '../../../blocks' import { ErrorBoundary } from '../components/error-boundary/error-boundary' import { CustomEdge } from './components/custom-edge/custom-edge' @@ -379,9 +378,7 @@ export default function Workflow() { return ( - - - + ) diff --git a/app/w/components/workflows/sync-wrapper.tsx b/app/w/components/workflows/sync-wrapper.tsx deleted file mode 100644 index 0428922e52..0000000000 --- a/app/w/components/workflows/sync-wrapper.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { ReactNode } from 'react' -import { - useDebouncedWorkflowSync, - usePeriodicWorkflowSync, - useSyncOnUnload, -} from '@/stores/workflow/sync/hooks' - -interface WorkflowSyncWrapperProps { - children: ReactNode -} - -export function WorkflowSyncWrapper({ children }: WorkflowSyncWrapperProps) { - useDebouncedWorkflowSync() - usePeriodicWorkflowSync() - useSyncOnUnload() - - return <>{children} -} diff --git a/drizzle.config.ts b/drizzle.config.ts index c7ddc326a7..e3fb4bda9b 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -5,6 +5,6 @@ export default { out: './db/migrations', dialect: 'postgresql', dbCredentials: { - url: process.env.DATABASE_URL!, + url: 'postgresql://postgres:%23WaldoEmmy1@db.jchdgebatsqopodtyast.supabase.co:5432/postgres', }, } satisfies Config diff --git a/middleware.ts b/middleware.ts index b1ef96d4d8..211337f155 100644 --- a/middleware.ts +++ b/middleware.ts @@ -11,5 +11,5 @@ export async function middleware(request: NextRequest) { // TODO: Add protected routes export const config = { - matcher: ['/dashboard/:path*', '/w/:path*'], + matcher: ['/w/:path*'], } diff --git a/stores/index.ts b/stores/index.ts index 3b6a135590..085a707604 100644 --- a/stores/index.ts +++ b/stores/index.ts @@ -4,9 +4,15 @@ import { useExecutionStore } from './execution/store' import { useNotificationStore } from './notifications/store' import { useEnvironmentStore } from './settings/environment/store' import { useGeneralStore } from './settings/general/store' +import { initializeSyncManager } from './sync-manager' import { useWorkflowRegistry } from './workflow/registry/store' import { useWorkflowStore } from './workflow/store' +// Initialize sync manager when the store is first imported +if (typeof window !== 'undefined') { + initializeSyncManager() +} + // Reset all application stores to their initial state export const resetAllStores = () => { // Selectively clear localStorage items diff --git a/stores/sync-manager.ts b/stores/sync-manager.ts new file mode 100644 index 0000000000..7bc8d4b02e --- /dev/null +++ b/stores/sync-manager.ts @@ -0,0 +1,69 @@ +import { useWorkflowRegistry } from './workflow/registry/store' +import { useWorkflowStore } from './workflow/store' + +interface SyncPayload { + id: string + name: string + description?: string + state: string +} + +async function syncWorkflowToServer(payload: SyncPayload): Promise { + try { + const response = await fetch('/api/workflows/sync', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + keepalive: true, // Ensures request completes even during page unload + }) + + if (!response.ok) { + if (response.status === 401) { + window.location.href = '/login' + return false + } + throw new Error(`Sync failed: ${response.statusText}`) + } + + return true + } catch (error) { + console.error('Error syncing workflow:', error) + return false + } +} + +export function initializeSyncManager() { + if (typeof window === 'undefined') return + + const handleBeforeUnload = async (event: BeforeUnloadEvent) => { + const { activeWorkflowId, workflows } = useWorkflowRegistry.getState() + const workflowState = useWorkflowStore.getState() + + if (!activeWorkflowId || !workflows[activeWorkflowId]) { + return + } + + const activeWorkflow = workflows[activeWorkflowId] + const payload: SyncPayload = { + id: activeWorkflowId, + name: activeWorkflow.name, + description: activeWorkflow.description, + state: JSON.stringify({ + blocks: workflowState.blocks, + edges: workflowState.edges, + loops: workflowState.loops, + lastSaved: workflowState.lastSaved, + }), + } + + // Show confirmation dialog + event.preventDefault() + event.returnValue = '' + + // Attempt to sync + await syncWorkflowToServer(payload) + } + + window.addEventListener('beforeunload', handleBeforeUnload) + return () => window.removeEventListener('beforeunload', handleBeforeUnload) +} diff --git a/stores/workflow/sync/hooks.ts b/stores/workflow/sync/hooks.ts deleted file mode 100644 index 197ea12f72..0000000000 --- a/stores/workflow/sync/hooks.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { useEffect, useRef } from 'react' -import { useRouter } from 'next/navigation' -import debounce from 'lodash.debounce' -import { useNotificationStore } from '@/stores/notifications/store' -import { useWorkflowRegistry } from '../registry/store' -import { useWorkflowStore } from '../store' - -const SYNC_DEBOUNCE_MS = 2000 // 2 seconds -const PERIODIC_SYNC_MS = 30000 // 30 seconds - -interface SyncPayload { - id: string - name: string - description?: string - state: string -} - -async function syncWorkflowToServer(payload: SyncPayload): Promise { - try { - const response = await fetch('/api/workflows/sync', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload), - }) - - if (!response.ok) { - if (response.status === 401) { - // Auth error - will be handled by the middleware - window.location.href = '/login' - return false - } - throw new Error(`Sync failed: ${response.statusText}`) - } - - return true - } catch (error) { - console.error('Error syncing workflow:', error) - return false - } -} - -export function useDebouncedWorkflowSync() { - const router = useRouter() - const { addNotification } = useNotificationStore() - const workflowState = useWorkflowStore((state) => ({ - blocks: state.blocks, - edges: state.edges, - loops: state.loops, - lastSaved: state.lastSaved, - })) - const { activeWorkflowId, workflows } = useWorkflowRegistry() - - const debouncedSyncRef = useRef | null>(null) - - useEffect(() => { - if (!activeWorkflowId || !workflows[activeWorkflowId]) return - - const syncWorkflow = async () => { - const activeWorkflow = workflows[activeWorkflowId] - const payload: SyncPayload = { - id: activeWorkflowId, - name: activeWorkflow.name, - description: activeWorkflow.description, - state: JSON.stringify(workflowState), - } - - const success = await syncWorkflowToServer(payload) - if (!success) { - addNotification( - 'error', - 'Failed to save workflow changes. Please try again.', - activeWorkflowId - ) - } - } - - // Create a debounced version of syncWorkflow - if (!debouncedSyncRef.current) { - debouncedSyncRef.current = debounce(syncWorkflow, SYNC_DEBOUNCE_MS) - } - - // Call the debounced sync - debouncedSyncRef.current() - - // Cleanup - return () => { - debouncedSyncRef.current?.cancel() - } - }, [activeWorkflowId, workflows, workflowState, addNotification]) -} - -export function usePeriodicWorkflowSync() { - const { addNotification } = useNotificationStore() - const workflowState = useWorkflowStore((state) => ({ - blocks: state.blocks, - edges: state.edges, - loops: state.loops, - lastSaved: state.lastSaved, - })) - const { activeWorkflowId, workflows } = useWorkflowRegistry() - - useEffect(() => { - if (!activeWorkflowId || !workflows[activeWorkflowId]) return - - const syncWorkflow = async () => { - const activeWorkflow = workflows[activeWorkflowId] - const payload: SyncPayload = { - id: activeWorkflowId, - name: activeWorkflow.name, - description: activeWorkflow.description, - state: JSON.stringify(workflowState), - } - - const success = await syncWorkflowToServer(payload) - if (!success) { - addNotification( - 'error', - 'Failed to auto-save workflow changes. Please save manually.', - activeWorkflowId - ) - } - } - - const intervalId = setInterval(syncWorkflow, PERIODIC_SYNC_MS) - - return () => clearInterval(intervalId) - }, [activeWorkflowId, workflows, workflowState, addNotification]) -} - -export function useSyncOnUnload() { - const { addNotification } = useNotificationStore() - const workflowState = useWorkflowStore((state) => ({ - blocks: state.blocks, - edges: state.edges, - loops: state.loops, - lastSaved: state.lastSaved, - })) - const { activeWorkflowId, workflows } = useWorkflowRegistry() - - useEffect(() => { - if (!activeWorkflowId || !workflows[activeWorkflowId]) return - - const handleBeforeUnload = async (event: BeforeUnloadEvent) => { - const activeWorkflow = workflows[activeWorkflowId] - const payload: SyncPayload = { - id: activeWorkflowId, - name: activeWorkflow.name, - description: activeWorkflow.description, - state: JSON.stringify(workflowState), - } - - // Use the keepalive option to try to complete the request even during unload - const response = await fetch('/api/workflows/sync', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload), - keepalive: true, - }) - - if (!response.ok) { - addNotification( - 'error', - 'Failed to save workflow changes before closing.', - activeWorkflowId - ) - } - - // Show a confirmation dialog - event.preventDefault() - event.returnValue = '' - } - - window.addEventListener('beforeunload', handleBeforeUnload) - return () => window.removeEventListener('beforeunload', handleBeforeUnload) - }, [activeWorkflowId, workflows, workflowState, addNotification]) -}