From 21105363e465e132eec0466f56b93de112f95773 Mon Sep 17 00:00:00 2001 From: Emir Karabeg <78010029+emir-karabeg@users.noreply.github.com> Date: Mon, 17 Mar 2025 01:20:43 -0700 Subject: [PATCH] Improvement/sync (#101) * fix(dbsync): resolved workflow disappearing error * fix(dbsync): new session w/ no local storage correct loading --- sim/app/api/db/workflow/route.ts | 18 +++++++ sim/app/stores/index.ts | 24 +++++++++- sim/app/stores/workflows/sync.ts | 48 ++++++++++++++++++- .../components/control-bar/control-bar.tsx | 4 +- sim/app/w/[id]/workflow.tsx | 17 ++++++- sim/app/w/components/sidebar/sidebar.tsx | 20 ++++++-- 6 files changed, 122 insertions(+), 9 deletions(-) diff --git a/sim/app/api/db/workflow/route.ts b/sim/app/api/db/workflow/route.ts index f47f1f2a74..89cc79ee79 100644 --- a/sim/app/api/db/workflow/route.ts +++ b/sim/app/api/db/workflow/route.ts @@ -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() diff --git a/sim/app/stores/index.ts b/sim/app/stores/index.ts index ac4aaebeb2..eea74cca3b 100644 --- a/sim/app/stores/index.ts +++ b/sim/app/stores/index.ts @@ -40,13 +40,20 @@ async function initializeApplication(): Promise { // 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 { 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 { 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 diff --git a/sim/app/stores/workflows/sync.ts b/sim/app/stores/workflows/sync.ts index 2afd87a3a2..399b8ba6fa 100644 --- a/sim/app/stores/workflows/sync.ts +++ b/sim/app/stores/workflows/sync.ts @@ -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() +/** + * 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 { 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 { // 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 } } diff --git a/sim/app/w/[id]/components/control-bar/control-bar.tsx b/sim/app/w/[id]/components/control-bar/control-bar.tsx index 52d691b788..f75d05938f 100644 --- a/sim/app/w/[id]/components/control-bar/control-bar.tsx +++ b/sim/app/w/[id]/components/control-bar/control-bar.tsx @@ -467,7 +467,7 @@ export function ControlBar() {