UNPKG

delta-sync

Version:

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

176 lines (175 loc) 6.01 kB
import { TOMBSTONE_STORE } from './types'; export class SyncView { constructor(items = []) { this.stores = new Map(); for (const item of items) { this.addItem(item); } } // 添加单个项目 addItem(item) { if (!this.stores.has(item.store)) { this.stores.set(item.store, new Map()); } this.stores.get(item.store).set(item.id, item); } // 获取单个项目 get(store, id) { const storeMap = this.stores.get(store); return storeMap?.get(id); } // 获取整个store的Map getStoreMap(store) { return this.stores.get(store); } // 获取所有store名称 getStores() { return Array.from(this.stores.keys()); } // 获取所有项目(扁平化 getAllItems() { const allItems = []; for (const storeMap of this.stores.values()) { allItems.push(...Array.from(storeMap.values())); } return allItems; } // 添加或更新项目 put(item) { this.addItem(item); } // 删除项目 delete(store, id) { const storeMap = this.stores.get(store); if (!storeMap) return false; return storeMap.delete(id); } // 比较本地视图相对云端视图的差异,注意参数顺序。 static diffViews(local, remote) { const toDownload = []; const toDelete = []; const allStores = new Set([ ...local.getStores(), ...remote.getStores() ]); for (const store of allStores) { const localStoreMap = local.getStoreMap(store); const remoteStoreMap = remote.getStoreMap(store); if (!remoteStoreMap) { continue; } const allIds = new Set([ ...(localStoreMap?.keys() ?? []), ...(remoteStoreMap?.keys() ?? []) ]); for (const id of allIds) { const localItem = localStoreMap?.get(id); const remoteItem = remoteStoreMap.get(id); // 这里remoteStoreMap肯定存在 if (!remoteItem) { continue; // 远程不存在,不需要任何操作(上传由反向diff处理) } if (!localItem) { if (!remoteItem.deleted) { // 只有未删除的远程项目才需要下载 toDownload.push(remoteItem); } } else { if (remoteItem._ver > localItem._ver) { if (remoteItem.deleted) { toDelete.push(remoteItem); } else { toDownload.push(remoteItem); } } } } } return { toDownload, toDelete, }; } clone() { return new SyncView(this.getAllItems()); } } export const getSyncViewFromAdapter = async (adapter, specificStores, since) => { const storesToProcess = specificStores || []; const regularStores = storesToProcess.filter(store => store !== TOMBSTONE_STORE); const dataItems = []; // 读取所有常规store的数据 for (const store of regularStores) { const items = await listAllStoreItems(adapter, store, since); dataItems.push(...items.map(item => ({ id: item.id, store: store, _ver: item._ver, }))); } // 读取墓碑数据加入到store中去 const tombstones = await listAllStoreItems(adapter, TOMBSTONE_STORE, since); for (const tombstone of tombstones) { const originalStore = tombstone.store; if (regularStores.includes(originalStore)) { const existingItem = dataItems.find(item => item.store === originalStore && item.id === tombstone.id); if (existingItem) { if (tombstone._ver >= existingItem._ver) { existingItem.deleted = true; existingItem._ver = tombstone._ver; } } else { dataItems.push({ id: tombstone.id, store: tombstone.store, _ver: tombstone._ver, deleted: true }); } } } return new SyncView(dataItems); }; // 快速读取store自某个时间点之后的全部syncView export const listAllStoreItems = async (adapter, storeName, since) => { try { let allItems = []; let currentOffset = undefined; while (true) { const result = await adapter.listStoreItems(storeName, currentOffset, 100, since); if (!result || !Array.isArray(result.items)) { break; } allItems = allItems.concat(result.items); if (!result.hasMore || result.offset === undefined) { break; } currentOffset = result.offset; } return allItems; } catch (error) { console.error(`[getSyncViewFromAdapter] Failed to read all data from store ${storeName}:`, error); return []; } }; // 比较和目标协调器的差异,计算本地协调器需要做的操作 export const getViewDiff = async (localAdapter, // 本地数据目标 remoteAdapter, // 远程数据源 stores, since) => { try { const localView = await getSyncViewFromAdapter(localAdapter, stores, since); const remoteView = await getSyncViewFromAdapter(remoteAdapter, stores, since); const result = SyncView.diffViews(localView, remoteView); return result; } catch (error) { console.error('[Coordinator] Failed to calculate diff:', error); return { toDownload: [], toDelete: [] }; } };