UNPKG

delta-sync

Version:

A lightweight framework for bi-directional database synchronization with automatic version tracking and conflict resolution.

446 lines (445 loc) 16 kB
// core/SyncEngine.ts import { SyncStatus, TOMBSTONE_STORE } from './types'; import { getViewDiff } from './SyncView'; import { syncFromDiff, applyChangesToAdapter } from './sync'; import { createDefaultOptions } from './option'; import { clearOldTombstones } from './clear'; import packageJson from '../package.json'; export class SyncEngine { constructor(localAdapter, storesToSync, options = {}) { this.syncStatus = SyncStatus.OFFLINE; this.isInitialized = false; this.storesToSync = []; // 待同步的数据库更改 this.pendingChanges = { put: new Map(), delete: new Map(), }; this.storesToSync = storesToSync; this.localAdapter = localAdapter; this.options = createDefaultOptions(options); console.log(`Powered by Delta Sync ${packageJson.version}`); } async initialize() { clearOldTombstones(this.localAdapter); if (this.isInitialized) return; try { if (this.options.autoSync?.enabled) { this.enableAutoSync(this.options.autoSync.pullInterval); } this.isInitialized = true; } catch (error) { console.error('Failed to initialize sync engine:', error); throw error; } } async ensureInitialized() { if (!this.isInitialized) { await this.initialize(); } } async setCloudAdapter(cloudAdapter) { this.updateStatus(SyncStatus.IDLE); this.cloudAdapter = cloudAdapter; } // 数据操作方法 async save(storeName, data) { await this.ensureInitialized(); try { const items = Array.isArray(data) ? data : [data]; const newVersion = Date.now(); const itemsWithVersion = items.map(item => ({ ...item, _ver: newVersion })); const results = await this.localAdapter.putBulk(storeName, itemsWithVersion); this.cacheDataChanges(storeName, results, 'put'); this.handleDataChange(); return results; } catch (error) { console.error('Save operation failed:', error); throw error; } } // 删除数据 async delete(storeName, ids) { await this.ensureInitialized(); try { const idsToDelete = Array.isArray(ids) ? ids : [ids]; await this.localAdapter.deleteBulk(storeName, idsToDelete); const currentVersion = Date.now(); // 把墓碑存到墓碑表 const itemsWithDeleteMark = idsToDelete.map(id => ({ id, store: storeName, // 记录原始store _ver: currentVersion, deleted: true })); await this.localAdapter.putBulk(TOMBSTONE_STORE, itemsWithDeleteMark); this.cacheDataChanges(storeName, idsToDelete.map(id => ({ id, _ver: Date.now() })), 'delete'); this.handleDataChange(); } catch (error) { console.error('Delete operation failed:', error); throw error; } } // 记录本地的即时变更以便及时同步 cacheDataChanges(storeName, items, operation) { const targetMap = operation === 'put' ? this.pendingChanges.put : this.pendingChanges.delete; if (operation === 'put') { const putItems = items; const changes = putItems.map(item => ({ id: item.id, data: item, _ver: item._ver || Date.now() })); targetMap.set(storeName, changes); } else { const deleteItems = items; const changes = deleteItems.map(item => ({ id: item.id, _ver: item._ver })); targetMap.set(storeName, changes); } } // 拉取最新的云端数据,允许指定版本号 async pull(stores = this.storesToSync, since) { if (this.syncStatus !== SyncStatus.IDLE) { console.warn('[DeltySync] Sync is busy, skip pull'); return; } console.log('[DeltySync] Pulling data from cloud...'); if (!this.cloudAdapter) { this.updateStatus(SyncStatus.OFFLINE); return; } if (!await this.checkPullAvailable()) { this.updateStatus(SyncStatus.ERROR); return; } try { this.updateStatus(SyncStatus.CHECKING); const diff = await getViewDiff(this.localAdapter, this.cloudAdapter, stores, since); this.updateStatus(SyncStatus.DOWNLOADING); await syncFromDiff(this.cloudAdapter, this.localAdapter, diff, { batchSize: this.options.batchSize || 100, onProgress: this.options.onSyncProgress, onChangesApplied: this.options.onChangePulled, onVersionUpdated: (version) => { this.options.onVersionUpdate && this.options.onVersionUpdate(version); } }); this.clearPendingChanges(); this.updateStatus(SyncStatus.IDLE); return; } catch (error) { this.updateStatus(SyncStatus.ERROR); console.error('[DeltySync] Pull failed:', error); return; } } async push(stores = this.storesToSync, since) { if (this.syncStatus !== SyncStatus.IDLE) { console.warn('[DeltySync] Sync is busy, skip pull'); return; } console.log('[DeltySync] Pushing data to cloud...'); if (!this.cloudAdapter) { this.updateStatus(SyncStatus.OFFLINE); return; } if (!await this.checkPushAvailable()) { this.updateStatus(SyncStatus.ERROR); return; } try { this.updateStatus(SyncStatus.CHECKING); const diff = await getViewDiff(this.cloudAdapter, this.localAdapter, stores, since); this.updateStatus(SyncStatus.UPLOADING); await syncFromDiff(this.localAdapter, this.cloudAdapter, diff, { batchSize: this.options.batchSize || 100, onProgress: this.options.onSyncProgress, onChangesApplied: this.options.onChangePushed, }); this.clearPendingChanges(); this.updateStatus(SyncStatus.IDLE); return; } catch (error) { this.updateStatus(SyncStatus.ERROR); console.error('[DeltySync] Push failed:', error); return; } } // 全量同步 async fullSync() { if (!this.cloudAdapter) { this.updateStatus(SyncStatus.OFFLINE); return; } try { await this.pull(this.storesToSync); await this.push(this.storesToSync); } catch (error) { console.error('[DeltySync] Sync failed:', error); return; } } enableAutoSync(pullInterval) { if (this.pullTimer) { clearInterval(this.pullTimer); } const currentAutoSync = this.options.autoSync || {}; this.options.autoSync = { ...currentAutoSync, enabled: true, pullInterval: pullInterval || currentAutoSync.pullInterval || 30000000 }; this.options.autoSync.pullInterval; this.pullTimer = setInterval(() => { this.executePullTask(); }, this.options.autoSync.pullInterval); } disableAutoSync() { if (this.pullTimer) { clearInterval(this.pullTimer); this.pullTimer = undefined; } if (this.pushDebounceTimer) { clearTimeout(this.pushDebounceTimer); this.pushDebounceTimer = undefined; } const currentAutoSync = this.options.autoSync || {}; this.options.autoSync = { ...currentAutoSync, enabled: false }; } async executePullTask() { if (this.syncStatus !== SyncStatus.IDLE) { return; } if (!this.cloudAdapter) { this.updateStatus(SyncStatus.OFFLINE); return; } try { await this.pull(this.storesToSync); } catch (error) { console.error('[DeltySync] Pull task failed:', error); } } clearPendingChanges() { this.pendingChanges.put.clear(); this.pendingChanges.delete.clear(); } // 即时上传本地的最新改动 async instantPush() { console.log('[DeltySync] Instant syncing data to cloud...'); if (!this.cloudAdapter) { this.updateStatus(SyncStatus.OFFLINE); return; } if (!this.hasPendingChanges()) { return; } if (!await this.checkPushAvailable()) { this.updateStatus(SyncStatus.ERROR); return; } try { this.updateStatus(SyncStatus.UPLOADING); if (this.hasPendingChanges()) { await applyChangesToAdapter(this.cloudAdapter, this.pendingChanges); this.options.onChangePushed && this.options.onChangePushed(this.pendingChanges); } this.clearPendingChanges(); this.updateStatus(SyncStatus.IDLE); return; } catch (error) { this.updateStatus(SyncStatus.ERROR); console.error('[DeltySync] Instant sync failed:', error); return; } } // callback handleDataChange() { if (!this.options.autoSync?.enabled) { return; } if (!this.cloudAdapter) { return; } if (this.pushDebounceTimer) { clearTimeout(this.pushDebounceTimer); } this.pushDebounceTimer = setTimeout(async () => { try { if (this.canTriggerSync()) { await this.instantPush(); } else { console.warn('[DeltySync] Instant sync skipped caused by checking process'); } } catch (error) { console.error('[DeltySync] Scheduled instant sync failed:', error); } }, this.options.autoSync?.pushDebounce || 10000); } // 更新同步设置 updateSyncOptions(options) { this.options = createDefaultOptions({ ...this.options, ...options }); if (options.autoSync) { if (options.autoSync.enabled) { this.enableAutoSync(options.autoSync.pullInterval); } else { this.disableAutoSync(); } } return this.options; } async clearCloudStores(stores) { if (!this.cloudAdapter) { throw new Error('Cloud adapter not set'); } try { this.updateStatus(SyncStatus.OPERATING); const cloudAdapter = await this.cloudAdapter; const storesToClear = Array.isArray(stores) ? stores : [stores]; const clearPromises = storesToClear.map(async (store) => { try { const result = await cloudAdapter.clearStore(store); if (!result) { console.warn(`Failed to clear store: ${store}`); } return { store, success: result }; } catch (error) { console.error(`Error clearing store ${store}:`, error); return { store, success: false }; } }); const results = await Promise.all(clearPromises); const failures = results.filter(r => !r.success); if (failures.length > 0) { throw new Error(`Failed to clear stores: ${failures.map(f => f.store).join(', ')}`); } this.updateStatus(SyncStatus.IDLE); } catch (error) { this.updateStatus(SyncStatus.ERROR); throw error; } } async clearLocalStores(stores) { try { this.updateStatus(SyncStatus.OPERATING); const storesToClear = Array.isArray(stores) ? stores : [stores]; const invalidStores = storesToClear.filter(store => !this.storesToSync.includes(store)); if (invalidStores.length > 0) { throw new Error(`Invalid stores: ${invalidStores.join(', ')}`); } const clearPromises = storesToClear.map(async (store) => { try { const result = await this.localAdapter.clearStore(store); if (!result) { console.warn(`Failed to clear store: ${store}`); } return { store, success: result }; } catch (error) { console.error(`Error clearing store ${store}:`, error); return { store, success: false }; } }); const results = await Promise.all(clearPromises); const failures = results.filter(r => !r.success); if (failures.length > 0) { throw new Error(`Failed to clear stores: ${failures.map(f => f.store).join(', ')}`); } this.updateStatus(SyncStatus.IDLE); } catch (error) { this.updateStatus(SyncStatus.ERROR); throw error; } } // 检查当前是否可以触发同步 canTriggerSync() { const isAutoSyncEnabled = this.options.autoSync?.enabled === true; const canSync = Boolean(isAutoSyncEnabled && this.cloudAdapter !== undefined && ![SyncStatus.ERROR, SyncStatus.UPLOADING, SyncStatus.DOWNLOADING].includes(this.syncStatus)); return canSync; } hasPendingChanges() { return this.pendingChanges.put.size > 0 || this.pendingChanges.delete.size > 0; } async checkPullAvailable() { if (this.options.onPullAvailableCheck) { try { const result = await Promise.resolve(this.options.onPullAvailableCheck()); if (!result) { return false; } } catch (error) { console.error('[DeltySync] Pull availability check failed:', error); return false; } } return true; } async checkPushAvailable() { if (this.options.onPushAvailableCheck) { try { const result = await Promise.resolve(this.options.onPushAvailableCheck()); if (result === false) { this.updateStatus(SyncStatus.REJECTED); return false; } return true; } catch (error) { console.error('[DeltySync] Push availability check failed:', error); throw new Error(error instanceof Error ? error.message : 'Push availability check failed'); } } return true; } // private methods updateStatus(status) { this.syncStatus = status; if (this.options.onStatusUpdate) { try { this.options.onStatusUpdate(status); } catch (error) { console.error('Status update callback error:', error); } } } // dispose dispose() { this.disableAutoSync(); this.clearPendingChanges(); this.syncStatus = SyncStatus.OFFLINE; this.isInitialized = false; this.cloudAdapter = undefined; } }