mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-10 23:48:09 -05:00
Improvement/sync (#101)
* fix(dbsync): resolved workflow disappearing error * fix(dbsync): new session w/ no local storage correct loading
This commit is contained in:
@@ -72,6 +72,24 @@ export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const { workflows: clientWorkflows } = SyncPayloadSchema.parse(body)
|
||||
|
||||
// CRITICAL SAFEGUARD: Prevent wiping out existing workflows
|
||||
// If client is sending empty workflows object, first check if user has existing workflows
|
||||
if (Object.keys(clientWorkflows).length === 0) {
|
||||
const existingWorkflows = await db
|
||||
.select()
|
||||
.from(workflow)
|
||||
.where(eq(workflow.userId, session.user.id))
|
||||
|
||||
// If user has existing workflows, but client sends empty, reject the sync
|
||||
if (existingWorkflows.length > 0) {
|
||||
logger.warn(`[${requestId}] Prevented data loss: Client attempted to sync empty workflows while DB has ${existingWorkflows.length} workflows`)
|
||||
return NextResponse.json({
|
||||
error: 'Sync rejected to prevent data loss',
|
||||
message: 'Client sent empty workflows, but user has existing workflows in database'
|
||||
}, { status: 409 })
|
||||
}
|
||||
}
|
||||
|
||||
// Get all workflows for the user from the database
|
||||
const dbWorkflows = await db
|
||||
.select()
|
||||
|
||||
@@ -40,13 +40,20 @@ async function initializeApplication(): Promise<void> {
|
||||
// Load environment variables directly from DB
|
||||
await useEnvironmentStore.getState().loadEnvironmentVariables()
|
||||
|
||||
// Set a flag in sessionStorage to detect new login sessions
|
||||
// This helps identify fresh logins in private browsers
|
||||
const isNewLoginSession = !sessionStorage.getItem('app_initialized')
|
||||
sessionStorage.setItem('app_initialized', 'true')
|
||||
|
||||
// Initialize sync system for other stores
|
||||
await initializeSyncManagers()
|
||||
|
||||
// After DB sync, check if we need to load from localStorage
|
||||
// This is a fallback in case DB sync failed or there's no data in DB
|
||||
const registryState = useWorkflowRegistry.getState()
|
||||
if (Object.keys(registryState.workflows).length === 0) {
|
||||
const hasDbWorkflows = Object.keys(registryState.workflows).length > 0
|
||||
|
||||
if (!hasDbWorkflows) {
|
||||
// No workflows loaded from DB, try localStorage as fallback
|
||||
const workflows = loadRegistry()
|
||||
if (workflows && Object.keys(workflows).length > 0) {
|
||||
@@ -57,6 +64,12 @@ async function initializeApplication(): Promise<void> {
|
||||
if (activeWorkflowId) {
|
||||
initializeWorkflowState(activeWorkflowId)
|
||||
}
|
||||
} else if (isNewLoginSession) {
|
||||
// Critical safeguard: For new login sessions with no DB workflows
|
||||
// and no localStorage, we disable sync temporarily to prevent data loss
|
||||
logger.info('New login session with no workflows - preventing initial sync')
|
||||
const syncManagers = getSyncManagers()
|
||||
syncManagers.forEach(manager => manager.stopIntervalSync())
|
||||
}
|
||||
} else {
|
||||
logger.info('Using workflows loaded from DB, ignoring localStorage')
|
||||
@@ -264,6 +277,15 @@ export async function reinitializeAfterLogin(): Promise<void> {
|
||||
if (typeof window === 'undefined') return
|
||||
|
||||
try {
|
||||
// Reset sync managers to prevent any active syncs during reinitialization
|
||||
resetSyncManagers()
|
||||
|
||||
// Clean existing state to avoid stale data
|
||||
resetAllStores()
|
||||
|
||||
// Mark as a new login session
|
||||
sessionStorage.removeItem('app_initialized')
|
||||
|
||||
// Reset initialization flags to force a fresh load
|
||||
isInitializing = false
|
||||
|
||||
|
||||
@@ -14,10 +14,31 @@ const logger = createLogger('Workflows Sync')
|
||||
|
||||
// Flag to prevent immediate sync back to DB after loading from DB
|
||||
let isLoadingFromDB = false
|
||||
let loadingFromDBToken: string | null = null
|
||||
let loadingFromDBStartTime = 0
|
||||
const LOADING_TIMEOUT = 3000 // 3 seconds maximum loading time
|
||||
|
||||
// Track workflows that had scheduling enabled in previous syncs
|
||||
const scheduledWorkflows = new Set<string>()
|
||||
|
||||
/**
|
||||
* Checks if the system is currently in the process of loading data from the database
|
||||
* Includes safety timeout to prevent permanent blocking of syncs
|
||||
* @returns true if loading is active, false otherwise
|
||||
*/
|
||||
export function isActivelyLoadingFromDB(): boolean {
|
||||
if (!loadingFromDBToken) return false
|
||||
|
||||
// Safety check: ensure loading doesn't block syncs indefinitely
|
||||
const elapsedTime = Date.now() - loadingFromDBStartTime
|
||||
if (elapsedTime > LOADING_TIMEOUT) {
|
||||
loadingFromDBToken = null
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a workflow has scheduling enabled
|
||||
* @param blocks The workflow blocks
|
||||
@@ -87,6 +108,8 @@ export async function fetchWorkflowsFromDB(): Promise<void> {
|
||||
try {
|
||||
// Set flag to prevent sync back to DB during loading
|
||||
isLoadingFromDB = true
|
||||
loadingFromDBToken = 'loading'
|
||||
loadingFromDBStartTime = Date.now()
|
||||
|
||||
// Call the API endpoint to get workflows from DB
|
||||
const response = await fetch(API_ENDPOINTS.WORKFLOW, {
|
||||
@@ -214,7 +237,21 @@ export async function fetchWorkflowsFromDB(): Promise<void> {
|
||||
// Reset the flag after a short delay to allow state to settle
|
||||
setTimeout(() => {
|
||||
isLoadingFromDB = false
|
||||
}, 500)
|
||||
loadingFromDBToken = null
|
||||
|
||||
// Verify if registry has workflows as a final check
|
||||
const registryWorkflows = useWorkflowRegistry.getState().workflows
|
||||
const workflowCount = Object.keys(registryWorkflows).length
|
||||
logger.info(`DB loading complete. Workflows in registry: ${workflowCount}`)
|
||||
|
||||
// Trigger one final sync to ensure consistency
|
||||
if (workflowCount > 0) {
|
||||
// Small delay for state to fully settle before allowing syncs
|
||||
setTimeout(() => {
|
||||
workflowSync.sync()
|
||||
}, 500)
|
||||
}
|
||||
}, 1000) // Increased to 1 second for more reliable state settling
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,7 +262,7 @@ export const workflowSync = createSingletonSyncManager('workflow-sync', () => ({
|
||||
if (typeof window === 'undefined') return {}
|
||||
|
||||
// Skip sync if we're currently loading from DB to prevent overwriting DB data
|
||||
if (isLoadingFromDB) {
|
||||
if (isActivelyLoadingFromDB()) {
|
||||
logger.info('Skipping workflow sync while loading from DB')
|
||||
return { skipSync: true }
|
||||
}
|
||||
@@ -235,6 +272,13 @@ export const workflowSync = createSingletonSyncManager('workflow-sync', () => ({
|
||||
|
||||
// Skip sync if there are no workflows to sync
|
||||
if (Object.keys(workflowsData).length === 0) {
|
||||
// Safety check: if registry has workflows but we're sending empty data, something is wrong
|
||||
const registryWorkflows = useWorkflowRegistry.getState().workflows
|
||||
if (Object.keys(registryWorkflows).length > 0) {
|
||||
logger.warn('Potential data loss prevented: Registry has workflows but sync payload is empty')
|
||||
return { skipSync: true }
|
||||
}
|
||||
|
||||
logger.info('Skipping workflow sync - no workflows to sync')
|
||||
return { skipSync: true }
|
||||
}
|
||||
|
||||
@@ -467,7 +467,7 @@ export function ControlBar() {
|
||||
<Button
|
||||
className={cn(
|
||||
// Base styles
|
||||
'gap-2 font-medium',
|
||||
'gap-2 ml-1 font-medium',
|
||||
// Brand color with hover states
|
||||
'bg-[#7F2FFF] hover:bg-[#7028E6]',
|
||||
// Hover effect with brand color
|
||||
@@ -497,7 +497,7 @@ export function ControlBar() {
|
||||
<div className="flex-1" />
|
||||
|
||||
{/* Right Section - Actions */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
{renderDeleteButton()}
|
||||
{renderHistoryDropdown()}
|
||||
{renderNotificationsDropdown()}
|
||||
|
||||
@@ -75,7 +75,7 @@ function WorkflowContent() {
|
||||
useEffect(() => {
|
||||
if (!isInitialized) return
|
||||
|
||||
const validateAndNavigate = () => {
|
||||
const validateAndNavigate = async () => {
|
||||
const workflowIds = Object.keys(workflows)
|
||||
const currentId = params.id as string
|
||||
|
||||
@@ -91,6 +91,21 @@ function WorkflowContent() {
|
||||
return
|
||||
}
|
||||
|
||||
// Import the isActivelyLoadingFromDB function to check sync status
|
||||
const { isActivelyLoadingFromDB } = await import('@/stores/workflows/sync')
|
||||
|
||||
// Wait for any active DB loading to complete before switching workflows
|
||||
if (isActivelyLoadingFromDB()) {
|
||||
logger.info('Waiting for DB loading to complete before switching workflow')
|
||||
const checkInterval = setInterval(() => {
|
||||
if (!isActivelyLoadingFromDB()) {
|
||||
clearInterval(checkInterval)
|
||||
setActiveWorkflow(currentId)
|
||||
}
|
||||
}, 100)
|
||||
return
|
||||
}
|
||||
|
||||
setActiveWorkflow(currentId)
|
||||
}
|
||||
|
||||
|
||||
@@ -37,9 +37,23 @@ export function Sidebar() {
|
||||
})
|
||||
}, [workflows])
|
||||
|
||||
const handleCreateWorkflow = () => {
|
||||
const id = createWorkflow()
|
||||
router.push(`/w/${id}`)
|
||||
// Create workflow
|
||||
const handleCreateWorkflow = async () => {
|
||||
try {
|
||||
// Import the isActivelyLoadingFromDB function to check sync status
|
||||
const { isActivelyLoadingFromDB } = await import('@/stores/workflows/sync')
|
||||
|
||||
// Prevent creating workflows during active DB operations
|
||||
if (isActivelyLoadingFromDB()) {
|
||||
console.log('Please wait, syncing in progress...')
|
||||
return
|
||||
}
|
||||
|
||||
const id = createWorkflow()
|
||||
router.push(`/w/${id}`)
|
||||
} catch (error) {
|
||||
console.error('Error creating workflow:', error)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user