mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
feat(dbsync): added workflow sync wrapper to workflow, combine debounce, period, and unload syncing to sync local state store to workflow pg table
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { z } from 'zod'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { db } from '@/db'
|
||||
import { workflow } from '@/db/schema'
|
||||
|
||||
// Define the schema for incoming data
|
||||
const WorkflowSyncSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string().optional(),
|
||||
state: z.string(), // JSON stringified workflow state
|
||||
})
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
// Get the authenticated user
|
||||
const session = await getSession()
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
// Parse and validate the request body
|
||||
const body = await request.json()
|
||||
const { id, name, description, state } = WorkflowSyncSchema.parse(body)
|
||||
|
||||
// Get the current timestamp
|
||||
const now = new Date()
|
||||
|
||||
// Upsert the workflow
|
||||
await db
|
||||
.insert(workflow)
|
||||
.values({
|
||||
id,
|
||||
userId: session.user.id,
|
||||
name,
|
||||
description,
|
||||
state,
|
||||
lastSynced: now,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: [workflow.id],
|
||||
set: {
|
||||
name,
|
||||
description,
|
||||
state,
|
||||
lastSynced: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
where: eq(workflow.userId, session.user.id), // Only update if the workflow belongs to the user
|
||||
})
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
console.error('Workflow sync error:', error)
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
return NextResponse.json({ error: 'Sync failed' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ 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'
|
||||
@@ -378,7 +379,9 @@ export default function Workflow() {
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<ErrorBoundary>
|
||||
<WorkflowContent />
|
||||
<WorkflowSyncWrapper>
|
||||
<WorkflowContent />
|
||||
</WorkflowSyncWrapper>
|
||||
</ErrorBoundary>
|
||||
</ReactFlowProvider>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
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}</>
|
||||
}
|
||||
Generated
+23
@@ -21,12 +21,14 @@
|
||||
"@radix-ui/react-switch": "^1.1.2",
|
||||
"@radix-ui/react-tabs": "^1.1.2",
|
||||
"@radix-ui/react-tooltip": "^1.1.6",
|
||||
"@types/lodash.debounce": "^4.0.9",
|
||||
"better-auth": "^1.1.18",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.0.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"drizzle-orm": "^0.39.3",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lucide-react": "^0.469.0",
|
||||
"next": "15.1.3",
|
||||
"openai": "^4.83.0",
|
||||
@@ -4512,6 +4514,21 @@
|
||||
"pretty-format": "^29.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/lodash": {
|
||||
"version": "4.17.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.15.tgz",
|
||||
"integrity": "sha512-w/P33JFeySuhN6JLkysYUK2gEmy9kHHFN7E8ro0tkfmlDOgxBDzWEZ/J8cWA+fHqFevpswDTFZnDx+R9lbL6xw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/lodash.debounce": {
|
||||
"version": "4.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash.debounce/-/lodash.debounce-4.0.9.tgz",
|
||||
"integrity": "sha512-Ma5JcgTREwpLRwMM+XwBR7DaWe96nC38uCBDFKZWbNKD+osjVzdpnUSwBcqCptrp16sSOLBAUb50Car5I0TCsQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.17.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.11.tgz",
|
||||
@@ -8513,6 +8530,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.debounce": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
|
||||
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.memoize": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
|
||||
|
||||
@@ -29,12 +29,14 @@
|
||||
"@radix-ui/react-switch": "^1.1.2",
|
||||
"@radix-ui/react-tabs": "^1.1.2",
|
||||
"@radix-ui/react-tooltip": "^1.1.6",
|
||||
"@types/lodash.debounce": "^4.0.9",
|
||||
"better-auth": "^1.1.18",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.0.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"drizzle-orm": "^0.39.3",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lucide-react": "^0.469.0",
|
||||
"next": "15.1.3",
|
||||
"openai": "^4.83.0",
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
import { useCallback, useEffect, useRef } from 'react'
|
||||
import debounce from 'lodash.debounce'
|
||||
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) {
|
||||
throw new Error(`Sync failed: ${response.statusText}`)
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Error syncing workflow:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function useDebouncedWorkflowSync() {
|
||||
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),
|
||||
}
|
||||
|
||||
await syncWorkflowToServer(payload)
|
||||
}
|
||||
|
||||
// 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])
|
||||
}
|
||||
|
||||
export function usePeriodicWorkflowSync() {
|
||||
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),
|
||||
}
|
||||
|
||||
await syncWorkflowToServer(payload)
|
||||
}
|
||||
|
||||
const intervalId = setInterval(syncWorkflow, PERIODIC_SYNC_MS)
|
||||
|
||||
return () => clearInterval(intervalId)
|
||||
}, [activeWorkflowId, workflows, workflowState])
|
||||
}
|
||||
|
||||
export function useSyncOnUnload() {
|
||||
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
|
||||
await fetch('/api/workflows/sync', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
keepalive: true,
|
||||
})
|
||||
|
||||
// Show a confirmation dialog
|
||||
event.preventDefault()
|
||||
event.returnValue = ''
|
||||
}
|
||||
|
||||
window.addEventListener('beforeunload', handleBeforeUnload)
|
||||
return () => window.removeEventListener('beforeunload', handleBeforeUnload)
|
||||
}, [activeWorkflowId, workflows, workflowState])
|
||||
}
|
||||
Reference in New Issue
Block a user