Improvement/sync (#101)

* fix(dbsync): resolved workflow disappearing error

* fix(dbsync): new session w/ no local storage correct loading
This commit is contained in:
Emir Karabeg
2025-03-17 01:20:43 -07:00
committed by GitHub
parent 44609b49cd
commit 21105363e4
6 changed files with 122 additions and 9 deletions

View File

@@ -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()

View File

@@ -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

View File

@@ -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 }
}

View File

@@ -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()}

View File

@@ -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)
}

View File

@@ -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 (