fix(dbsync): only sync with db on exit

This commit is contained in:
Emir Karabeg
2025-02-17 14:04:26 -08:00
parent f62a0d261e
commit eb6812aaa8
7 changed files with 78 additions and 200 deletions
+1 -4
View File
@@ -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 (
<ReactFlowProvider>
<ErrorBoundary>
<WorkflowSyncWrapper>
<WorkflowContent />
</WorkflowSyncWrapper>
<WorkflowContent />
</ErrorBoundary>
</ReactFlowProvider>
)
@@ -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}</>
}
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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*'],
}
+6
View File
@@ -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
+69
View File
@@ -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<boolean> {
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)
}
-176
View File
@@ -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<boolean> {
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<ReturnType<typeof debounce> | 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])
}