/** * SyncEngine - Background synchronization service * * Handles: * - Full sync at startup and periodically * - Incremental sync every 30 seconds * - Schema-driven sync for all object types */ import { logger } from './logger.js'; import { normalizedCacheStore as cacheStore } from './normalizedCacheStore.js'; import { jiraAssetsClient, JiraObjectNotFoundError } from './jiraAssetsClient.js'; import { OBJECT_TYPES, getObjectTypesBySyncPriority } from '../generated/jira-schema.js'; import type { CMDBObject, CMDBObjectTypeName } from '../generated/jira-types.js'; import { schemaDiscoveryService } from './schemaDiscoveryService.js'; import type { ObjectEntry } from '../domain/jiraAssetsPayload.js'; // ============================================================================= // Types // ============================================================================= export interface SyncStats { objectType: string; objectsProcessed: number; relationsExtracted: number; duration: number; } export interface SyncResult { success: boolean; stats: SyncStats[]; totalObjects: number; totalRelations: number; duration: number; error?: string; } export interface SyncEngineStatus { isRunning: boolean; isSyncing: boolean; lastFullSync: string | null; lastIncrementalSync: string | null; nextIncrementalSync: string | null; incrementalInterval: number; } // ============================================================================= // Configuration // ============================================================================= const DEFAULT_INCREMENTAL_INTERVAL = 30_000; // 30 seconds const DEFAULT_BATCH_SIZE = 50; // ============================================================================= // Sync Engine Implementation // ============================================================================= class SyncEngine { private isRunning: boolean = false; private isSyncing: boolean = false; // For full/incremental syncs private syncingTypes: Set = new Set(); // Track which types are being synced private incrementalTimer: NodeJS.Timeout | null = null; private incrementalInterval: number; private batchSize: number; private lastIncrementalSync: Date | null = null; private lastConfigCheck: number = 0; // Track last config check time to avoid log spam constructor() { this.incrementalInterval = parseInt( process.env.SYNC_INCREMENTAL_INTERVAL_MS || String(DEFAULT_INCREMENTAL_INTERVAL), 10 ); this.batchSize = parseInt( process.env.JIRA_API_BATCH_SIZE || String(DEFAULT_BATCH_SIZE), 10 ); } // ========================================================================== // Lifecycle // ========================================================================== /** * Initialize the sync engine * Performs initial sync if cache is cold, then starts incremental sync * Note: Sync engine uses service account token from .env (JIRA_SERVICE_ACCOUNT_TOKEN) * for all read operations. Write operations require user PAT from profile settings. */ async initialize(): Promise { if (this.isRunning) { logger.warn('SyncEngine: Already running'); return; } logger.info('SyncEngine: Initializing...'); logger.info('SyncEngine: Sync uses service account token (JIRA_SERVICE_ACCOUNT_TOKEN) from .env'); this.isRunning = true; // Check if configuration is complete before starting scheduler const { schemaConfigurationService } = await import('./schemaConfigurationService.js'); const isConfigured = await schemaConfigurationService.isConfigurationComplete(); // Start incremental sync scheduler if token is available AND configuration is complete if (jiraAssetsClient.hasToken()) { if (isConfigured) { this.startIncrementalSyncScheduler(); logger.info('SyncEngine: Incremental sync scheduler started (configuration complete)'); } else { logger.info('SyncEngine: Incremental sync scheduler NOT started - schema configuration not complete. Please configure object types in settings first.'); // Start scheduler but it will check configuration on each run // This allows scheduler to start automatically when configuration is completed later this.startIncrementalSyncScheduler(); logger.info('SyncEngine: Incremental sync scheduler started (will check configuration on each run)'); } } else { logger.info('SyncEngine: Service account token not configured, incremental sync disabled'); } logger.info('SyncEngine: Initialized (using service account token for sync operations)'); } /** * Stop the sync engine */ stop(): void { logger.info('SyncEngine: Stopping...'); this.isRunning = false; if (this.incrementalTimer) { clearInterval(this.incrementalTimer); this.incrementalTimer = null; } logger.info('SyncEngine: Stopped'); } /** * Check if a timestamp is stale */ private isStale(timestamp: string, maxAgeMs: number): boolean { const age = Date.now() - new Date(timestamp).getTime(); return age > maxAgeMs; } // ========================================================================== // Full Sync // ========================================================================== /** * Perform a full sync of all object types * Uses service account token from .env (JIRA_SERVICE_ACCOUNT_TOKEN) */ async fullSync(): Promise { // Check if service account token is configured (sync uses service account token) if (!jiraAssetsClient.hasToken()) { logger.warn('SyncEngine: Jira service account token not configured, cannot perform sync'); return { success: false, stats: [], totalObjects: 0, totalRelations: 0, duration: 0, error: 'Jira service account token (JIRA_SERVICE_ACCOUNT_TOKEN) not configured in .env. Please configure it to enable sync operations.', }; } if (this.isSyncing) { logger.warn('SyncEngine: Sync already in progress'); return { success: false, stats: [], totalObjects: 0, totalRelations: 0, duration: 0, error: 'Sync already in progress', }; } this.isSyncing = true; const startTime = Date.now(); const stats: SyncStats[] = []; let totalObjects = 0; let totalRelations = 0; logger.info('SyncEngine: Starting full sync...'); try { // Check if configuration is complete const { schemaConfigurationService } = await import('./schemaConfigurationService.js'); const isConfigured = await schemaConfigurationService.isConfigurationComplete(); if (!isConfigured) { throw new Error('Schema configuration not complete. Please configure at least one object type to be synced in the settings page.'); } // Get enabled object types from configuration logger.info('SyncEngine: Fetching enabled object types from configuration...'); const enabledTypes = await schemaConfigurationService.getEnabledObjectTypes(); logger.info(`SyncEngine: Found ${enabledTypes.length} enabled object types to sync`); if (enabledTypes.length === 0) { throw new Error('No object types enabled for syncing. Please enable at least one object type in the settings page.'); } // Schema discovery will happen automatically when needed (e.g., for relation extraction) // It's no longer required upfront - the user has already configured which object types to sync logger.info('SyncEngine: Starting object sync for configured object types...'); // Sync each enabled object type for (const enabledType of enabledTypes) { try { const typeStat = await this.syncConfiguredObjectType(enabledType); stats.push(typeStat); totalObjects += typeStat.objectsProcessed; totalRelations += typeStat.relationsExtracted; } catch (error) { logger.error(`SyncEngine: Failed to sync ${enabledType.displayName}`, error); stats.push({ objectType: enabledType.displayName, objectsProcessed: 0, relationsExtracted: 0, duration: 0, }); } } // Update sync metadata const now = new Date().toISOString(); await cacheStore.setSyncMetadata('lastFullSync', now); await cacheStore.setSyncMetadata('lastIncrementalSync', now); this.lastIncrementalSync = new Date(); const duration = Date.now() - startTime; logger.info(`SyncEngine: Full sync complete. ${totalObjects} objects, ${totalRelations} relations in ${duration}ms`); return { success: true, stats, totalObjects, totalRelations, duration, }; } catch (error) { logger.error('SyncEngine: Full sync failed', error); return { success: false, stats, totalObjects, totalRelations, duration: Date.now() - startTime, error: error instanceof Error ? error.message : 'Unknown error', }; } finally { this.isSyncing = false; } } /** * Store an object and all its nested referenced objects recursively * This method processes the entire object tree, storing all nested objects * and extracting all relations, while preventing infinite loops with circular references. * * @param entry - The object entry to store (in ObjectEntry format from API) * @param typeName - The type name of the object * @param processedIds - Set of already processed object IDs (to prevent duplicates and circular refs) * @returns Statistics about objects stored and relations extracted */ private async storeObjectTree( entry: ObjectEntry, typeName: CMDBObjectTypeName, processedIds: Set ): Promise<{ objectsStored: number; relationsExtracted: number }> { const entryId = String(entry.id); // Skip if already processed (handles circular references) if (processedIds.has(entryId)) { logger.debug(`SyncEngine: Skipping already processed object ${entry.objectKey || entryId} of type ${typeName}`); return { objectsStored: 0, relationsExtracted: 0 }; } processedIds.add(entryId); let objectsStored = 0; let relationsExtracted = 0; try { logger.debug(`SyncEngine: [Recursive] Storing object tree for ${entry.objectKey || entryId} of type ${typeName} (depth: ${processedIds.size - 1})`); // 1. Adapt and parse the object const adapted = jiraAssetsClient.adaptObjectEntryToJiraAssetsObject(entry); if (!adapted) { logger.warn(`SyncEngine: Failed to adapt object ${entry.objectKey || entryId}`); return { objectsStored: 0, relationsExtracted: 0 }; } const parsed = await jiraAssetsClient.parseObject(adapted); if (!parsed) { logger.warn(`SyncEngine: Failed to parse object ${entry.objectKey || entryId}`); return { objectsStored: 0, relationsExtracted: 0 }; } // 2. Store the object await cacheStore.upsertObject(typeName, parsed); objectsStored++; logger.debug(`SyncEngine: Stored object ${parsed.objectKey || parsed.id} of type ${typeName}`); // 3. Schema discovery must be manually triggered via API endpoints // No automatic discovery // 4. Extract and store relations for this object await cacheStore.extractAndStoreRelations(typeName, parsed); relationsExtracted++; logger.debug(`SyncEngine: Extracted relations for object ${parsed.objectKey || parsed.id}`); // 5. Recursively process nested referenced objects // Note: Lookup maps should already be initialized by getAllObjectsOfType // Use a separate Set for extraction to avoid conflicts with storage tracking const extractionProcessedIds = new Set(); const nestedRefs = jiraAssetsClient.extractNestedReferencedObjects( entry, extractionProcessedIds, // Separate Set for extraction (prevents infinite loops in traversal) 5, // max depth 0 // current depth ); if (nestedRefs.length > 0) { logger.debug(`SyncEngine: [Recursive] Found ${nestedRefs.length} nested referenced objects for ${entry.objectKey || entryId}`); // Group by type for better logging const refsByType = new Map(); for (const ref of nestedRefs) { refsByType.set(ref.typeName, (refsByType.get(ref.typeName) || 0) + 1); } const typeSummary = Array.from(refsByType.entries()) .map(([type, count]) => `${count} ${type}`) .join(', '); logger.debug(`SyncEngine: [Recursive] Nested objects by type: ${typeSummary}`); } // 6. Recursively store each nested object for (const { entry: nestedEntry, typeName: nestedTypeName } of nestedRefs) { logger.debug(`SyncEngine: [Recursive] Processing nested object ${nestedEntry.objectKey || nestedEntry.id} of type ${nestedTypeName}`); const nestedResult = await this.storeObjectTree( nestedEntry, nestedTypeName as CMDBObjectTypeName, processedIds ); objectsStored += nestedResult.objectsStored; relationsExtracted += nestedResult.relationsExtracted; } logger.debug(`SyncEngine: [Recursive] Completed storing object tree for ${entry.objectKey || entryId}: ${objectsStored} objects, ${relationsExtracted} relations`); return { objectsStored, relationsExtracted }; } catch (error) { logger.error(`SyncEngine: Failed to store object tree for ${entry.objectKey || entryId}`, error); return { objectsStored, relationsExtracted }; } } /** * Sync a configured object type (from schema configuration) */ private async syncConfiguredObjectType(enabledType: { schemaId: string; objectTypeId: number; objectTypeName: string; displayName: string; }): Promise { const startTime = Date.now(); let objectsProcessed = 0; let relationsExtracted = 0; try { logger.info(`SyncEngine: Syncing ${enabledType.displayName} (${enabledType.objectTypeName}) from schema ${enabledType.schemaId}...`); // Fetch all objects from Jira using the configured schema and object type // This returns raw entries for recursive processing (includeAttributesDeep=2 provides nested data) const { objects: jiraObjects, rawEntries } = await jiraAssetsClient.getAllObjectsOfType( enabledType.displayName, // Use display name for Jira API this.batchSize, enabledType.schemaId ); logger.info(`SyncEngine: Fetched ${jiraObjects.length} ${enabledType.displayName} objects from Jira (schema: ${enabledType.schemaId})`); // Schema discovery must be manually triggered via API endpoints // No automatic discovery // Use objectTypeName for cache storage (PascalCase) const typeName = enabledType.objectTypeName as CMDBObjectTypeName; // Process each main object recursively using storeObjectTree // This will store the object and all its nested referenced objects const processedIds = new Set(); // Track processed objects to prevent duplicates and circular refs const failedObjects: Array<{ id: string; key: string; label: string; reason: string }> = []; if (rawEntries && rawEntries.length > 0) { logger.info(`SyncEngine: Processing ${rawEntries.length} ${enabledType.displayName} objects recursively...`); for (const rawEntry of rawEntries) { try { const result = await this.storeObjectTree(rawEntry, typeName, processedIds); objectsProcessed += result.objectsStored; relationsExtracted += result.relationsExtracted; } catch (error) { const entryId = String(rawEntry.id); failedObjects.push({ id: entryId, key: rawEntry.objectKey || 'unknown', label: rawEntry.label || 'unknown', reason: error instanceof Error ? error.message : 'Unknown error', }); logger.warn(`SyncEngine: Failed to store object tree for ${enabledType.displayName} object: ${rawEntry.objectKey || entryId} (${rawEntry.label || 'unknown label'})`, error); } } } else { // Fallback: if rawEntries not available, use adapted objects (less efficient, no recursion) logger.warn(`SyncEngine: Raw entries not available, using fallback linear processing (no recursive nesting)`); const parsedObjects: CMDBObject[] = []; for (const jiraObj of jiraObjects) { const parsed = await jiraAssetsClient.parseObject(jiraObj); if (parsed) { parsedObjects.push(parsed); } else { failedObjects.push({ id: jiraObj.id?.toString() || 'unknown', key: jiraObj.objectKey || 'unknown', label: jiraObj.label || 'unknown', reason: 'parseObject returned null', }); logger.warn(`SyncEngine: Failed to parse ${enabledType.displayName} object: ${jiraObj.objectKey || jiraObj.id} (${jiraObj.label || 'unknown label'})`); } } if (parsedObjects.length > 0) { await cacheStore.batchUpsertObjects(typeName, parsedObjects); objectsProcessed = parsedObjects.length; // Extract relations for (const obj of parsedObjects) { await cacheStore.extractAndStoreRelations(typeName, obj); relationsExtracted++; } } } // Log parsing statistics if (failedObjects.length > 0) { logger.warn(`SyncEngine: ${failedObjects.length} ${enabledType.displayName} objects failed to process:`, failedObjects.map(o => `${o.key} (${o.label}): ${o.reason}`).join(', ')); } const duration = Date.now() - startTime; const skippedCount = jiraObjects.length - objectsProcessed; if (skippedCount > 0) { logger.warn(`SyncEngine: Synced ${objectsProcessed}/${jiraObjects.length} ${enabledType.displayName} objects in ${duration}ms (${skippedCount} skipped)`); } else { logger.debug(`SyncEngine: Synced ${objectsProcessed} ${enabledType.displayName} objects in ${duration}ms`); } return { objectType: enabledType.displayName, objectsProcessed, relationsExtracted, duration, }; } catch (error) { logger.error(`SyncEngine: Failed to sync ${enabledType.displayName}`, error); return { objectType: enabledType.displayName, objectsProcessed, relationsExtracted, duration: Date.now() - startTime, }; } } /** * Sync a single object type (legacy method, kept for backward compatibility) */ private async syncObjectType(typeName: CMDBObjectTypeName): Promise { // This method is deprecated - use syncConfiguredObjectType instead logger.warn(`SyncEngine: syncObjectType(${typeName}) is deprecated, use configured object types instead`); return { objectType: typeName, objectsProcessed: 0, relationsExtracted: 0, duration: 0, }; } // ========================================================================== // Incremental Sync // ========================================================================== /** * Start the incremental sync scheduler * The scheduler will check configuration on each run and only sync if configuration is complete */ private startIncrementalSyncScheduler(): void { if (this.incrementalTimer) { clearInterval(this.incrementalTimer); } logger.info(`SyncEngine: Starting incremental sync scheduler (every ${this.incrementalInterval}ms)`); logger.info('SyncEngine: Scheduler will only perform syncs when schema configuration is complete'); this.incrementalTimer = setInterval(() => { if (!this.isSyncing && this.isRunning) { // incrementalSync() will check if configuration is complete before syncing this.incrementalSync().catch(err => { logger.error('SyncEngine: Incremental sync failed', err); }); } }, this.incrementalInterval); } /** * Perform an incremental sync (only updated objects) * Uses service account token from .env (JIRA_SERVICE_ACCOUNT_TOKEN) * * Note: On Jira Data Center, IQL-based incremental sync is not supported. * We instead check if a periodic full sync is needed. */ async incrementalSync(): Promise<{ success: boolean; updatedCount: number }> { // Check if service account token is configured (sync uses service account token) if (!jiraAssetsClient.hasToken()) { logger.debug('SyncEngine: Jira service account token not configured, skipping incremental sync'); return { success: false, updatedCount: 0 }; } // Check if configuration is complete before attempting sync const { schemaConfigurationService } = await import('./schemaConfigurationService.js'); const isConfigured = await schemaConfigurationService.isConfigurationComplete(); if (!isConfigured) { // Don't log on every interval - only log once per minute to avoid spam const now = Date.now(); if (!this.lastConfigCheck || now - this.lastConfigCheck > 60000) { logger.debug('SyncEngine: Schema configuration not complete, skipping incremental sync. Please configure object types in settings.'); this.lastConfigCheck = now; } return { success: false, updatedCount: 0 }; } // Get enabled object types - will be used later to filter updated objects const enabledTypes = await schemaConfigurationService.getEnabledObjectTypes(); if (enabledTypes.length === 0) { logger.debug('SyncEngine: No enabled object types, skipping incremental sync'); return { success: false, updatedCount: 0 }; } if (this.isSyncing) { return { success: false, updatedCount: 0 }; } this.isSyncing = true; try { // Get the last sync time const lastSyncStr = await cacheStore.getSyncMetadata('lastIncrementalSync'); const since = lastSyncStr ? new Date(lastSyncStr) : new Date(Date.now() - 60000); // Default: last minute logger.debug(`SyncEngine: Incremental sync since ${since.toISOString()}`); // Get enabled object types to filter incremental sync const enabledTypes = await schemaConfigurationService.getEnabledObjectTypes(); const enabledTypeNames = new Set(enabledTypes.map(et => et.objectTypeName)); if (enabledTypeNames.size === 0) { logger.debug('SyncEngine: No enabled object types, skipping incremental sync'); return { success: false, updatedCount: 0 }; } // Fetch updated objects from Jira const updatedObjects = await jiraAssetsClient.getUpdatedObjectsSince(since, this.batchSize); // If no objects returned (e.g., Data Center doesn't support IQL incremental sync), // check if we should trigger a full sync instead if (updatedObjects.length === 0) { const lastFullSyncStr = await cacheStore.getSyncMetadata('lastFullSync'); if (lastFullSyncStr) { const lastFullSync = new Date(lastFullSyncStr); const fullSyncAge = Date.now() - lastFullSync.getTime(); const FULL_SYNC_INTERVAL = 15 * 60 * 1000; // 15 minutes if (fullSyncAge > FULL_SYNC_INTERVAL) { logger.info('SyncEngine: Triggering periodic full sync (incremental not available)'); // Release the lock before calling fullSync this.isSyncing = false; await this.fullSync(); return { success: true, updatedCount: 0 }; } } // Update timestamp even if no objects were synced const now = new Date(); await cacheStore.setSyncMetadata('lastIncrementalSync', now.toISOString()); this.lastIncrementalSync = now; return { success: true, updatedCount: 0 }; } // Schema discovery must be manually triggered via API endpoints // No automatic discovery let updatedCount = 0; const processedIds = new Set(); // Track processed objects for recursive sync // Filter updated objects to only process enabled object types // Use recursive processing to handle nested references for (const jiraObj of updatedObjects) { const parsed = await jiraAssetsClient.parseObject(jiraObj); if (parsed) { const typeName = parsed._objectType as CMDBObjectTypeName; // Only sync if this object type is enabled if (!enabledTypeNames.has(typeName)) { logger.debug(`SyncEngine: Skipping ${typeName} in incremental sync - not enabled`); continue; } // Get raw entry for recursive processing const objectId = parsed.id; try { const entry = await jiraAssetsClient.getObjectEntry(objectId); if (entry) { // Use recursive storeObjectTree to process object and all nested references const result = await this.storeObjectTree(entry, typeName, processedIds); if (result.objectsStored > 0) { updatedCount++; logger.debug(`SyncEngine: Incremental sync processed ${objectId}: ${result.objectsStored} objects, ${result.relationsExtracted} relations`); } } else { // Fallback to linear processing if raw entry not available await cacheStore.upsertObject(typeName, parsed); await cacheStore.extractAndStoreRelations(typeName, parsed); updatedCount++; } } catch (error) { logger.warn(`SyncEngine: Failed to get raw entry for ${objectId}, using fallback`, error); // Fallback to linear processing await cacheStore.upsertObject(typeName, parsed); await cacheStore.extractAndStoreRelations(typeName, parsed); updatedCount++; } } } // Update sync metadata const now = new Date(); await cacheStore.setSyncMetadata('lastIncrementalSync', now.toISOString()); this.lastIncrementalSync = now; if (updatedCount > 0) { logger.info(`SyncEngine: Incremental sync updated ${updatedCount} objects`); } return { success: true, updatedCount }; } catch (error) { logger.error('SyncEngine: Incremental sync failed', error); return { success: false, updatedCount: 0 }; } finally { this.isSyncing = false; } } // ========================================================================== // Manual Sync Triggers // ========================================================================== /** * Trigger a sync for a specific object type * Only syncs if the object type is enabled in configuration * Allows concurrent syncs for different types, but blocks if: * - A full sync is in progress * - An incremental sync is in progress * - This specific type is already being synced */ async syncType(typeName: CMDBObjectTypeName): Promise { // Block if a full or incremental sync is running if (this.isSyncing) { throw new Error('Full or incremental sync already in progress'); } // Block if this specific type is already being synced if (this.syncingTypes.has(typeName)) { throw new Error(`Sync already in progress for ${typeName}`); } // Check if this type is enabled in configuration const { schemaConfigurationService } = await import('./schemaConfigurationService.js'); const enabledTypes = await schemaConfigurationService.getEnabledObjectTypes(); const enabledType = enabledTypes.find(et => et.objectTypeName === typeName); if (!enabledType) { throw new Error(`Object type ${typeName} is not enabled for syncing. Please enable it in the Schema Configuration settings page.`); } this.syncingTypes.add(typeName); try { return await this.syncConfiguredObjectType(enabledType); } finally { this.syncingTypes.delete(typeName); } } /** * Force sync a single object * Only syncs if the object type is enabled in configuration * If the object was deleted from Jira, it will be removed from the local cache * Uses recursive processing to store nested referenced objects */ async syncObject(typeName: CMDBObjectTypeName, objectId: string): Promise { try { // Check if this type is enabled in configuration const { schemaConfigurationService } = await import('./schemaConfigurationService.js'); const enabledTypes = await schemaConfigurationService.getEnabledObjectTypes(); const isEnabled = enabledTypes.some(et => et.objectTypeName === typeName); if (!isEnabled) { logger.warn(`SyncEngine: Cannot sync object ${objectId} - type ${typeName} is not enabled for syncing`); return false; } // Schema discovery must be manually triggered via API endpoints // No automatic discovery // Get raw ObjectEntry for recursive processing const entry = await jiraAssetsClient.getObjectEntry(objectId); if (!entry) return false; // Use recursive storeObjectTree to process object and all nested references const processedIds = new Set(); const result = await this.storeObjectTree(entry, typeName, processedIds); if (result.objectsStored > 0) { logger.info(`SyncEngine: Synced object ${objectId} recursively: ${result.objectsStored} objects, ${result.relationsExtracted} relations`); return true; } return false; } catch (error) { // If object was deleted from Jira, remove it from our cache if (error instanceof JiraObjectNotFoundError) { const deleted = await cacheStore.deleteObject(typeName, objectId); if (deleted) { logger.info(`SyncEngine: Removed deleted object ${typeName}/${objectId} from cache`); } return false; } logger.error(`SyncEngine: Failed to sync object ${objectId}`, error); return false; } } // ========================================================================== // Status // ========================================================================== /** * Get current sync engine status */ async getStatus(): Promise { const stats = await cacheStore.getStats(); let nextIncrementalSync: string | null = null; if (this.isRunning && this.lastIncrementalSync) { const nextTime = new Date(this.lastIncrementalSync.getTime() + this.incrementalInterval); nextIncrementalSync = nextTime.toISOString(); } return { isRunning: this.isRunning, isSyncing: this.isSyncing, lastFullSync: stats.lastFullSync, lastIncrementalSync: stats.lastIncrementalSync, nextIncrementalSync, incrementalInterval: this.incrementalInterval, }; } } // Export singleton instance export const syncEngine = new SyncEngine();