delta-sync
Version:
A lightweight framework for bi-directional database synchronization with automatic version tracking and conflict resolution.
154 lines (153 loc) • 5.91 kB
JavaScript
// @/core/delta-sync/core/sync.ts
import { TOMBSTONE_STORE } from './types';
// 根据视图差异,从源数据库下载数据到目标数据库
export const syncFromDiff = async (sourceAdapter, targetAdapter, viewDiff, options = {}) => {
const { batchSize = 100, onProgress, onChangesApplied, onVersionUpdated } = options;
let updated = 0;
let deleted = 0;
let errors = 0;
const totalItems = viewDiff.toDownload.length + viewDiff.toDelete.length;
let processedItems = 0;
const updateProgress = (increment = 1) => {
processedItems += increment;
if (onProgress) {
onProgress({
processed: processedItems,
total: totalItems
});
}
};
// 记录所有处理的数据版本号
const allVersions = [];
for (let i = 0; i < viewDiff.toDownload.length; i += batchSize) {
const batch = viewDiff.toDownload.slice(i, i + batchSize);
try {
const changeSet = await extractChangesFromAdapter(sourceAdapter, batch);
await applyChangesToAdapter(targetAdapter, changeSet);
for (const changes of changeSet.put.values()) {
for (const change of changes) {
allVersions.push(change._ver);
}
}
for (const changes of changeSet.delete.values()) {
for (const change of changes) {
allVersions.push(change._ver);
}
}
onChangesApplied?.(changeSet);
updated += batch.length;
updateProgress(batch.length);
}
catch (error) {
errors++;
console.error('Data download failed:', error);
updateProgress(batch.length);
}
}
if (viewDiff.toDelete.length > 0) {
try {
const changeSet = await extractChangesFromAdapter(sourceAdapter, viewDiff.toDelete);
await applyChangesToAdapter(targetAdapter, changeSet);
for (const changes of changeSet.delete.values()) {
for (const change of changes) {
allVersions.push(change._ver);
}
}
onChangesApplied?.(changeSet);
deleted += viewDiff.toDelete.length;
updateProgress(viewDiff.toDelete.length);
}
catch (error) {
errors++;
console.error('Data deletion failed:', error);
updateProgress(viewDiff.toDelete.length);
}
}
if (allVersions.length > 0 && onVersionUpdated) {
const maxVersion = Math.max(...allVersions);
onVersionUpdated(maxVersion);
}
};
// 根据视图读取完整的数据
export const extractChangesFromAdapter = async (adapter, items) => {
const deleteMap = new Map();
const putMap = new Map();
if (!Array.isArray(items)) {
console.error('[extractChangesFromAdapter] Received invalid items:', items);
return { delete: deleteMap, put: putMap };
}
// 直接按 store 分组,不需要过滤
const storeGroups = new Map();
for (const item of items) {
if (!item || typeof item !== 'object' || !item.store || !item.id) {
console.warn('[extractChangesFromAdapter] Skipping invalid item:', item);
continue;
}
if (!storeGroups.has(item.store)) {
storeGroups.set(item.store, []);
}
storeGroups.get(item.store).push(item);
}
for (const [store, storeItems] of storeGroups) {
const deletedItems = storeItems.filter(item => item.deleted);
const updateItems = storeItems.filter(item => !item.deleted);
if (deletedItems.length > 0) {
deleteMap.set(store, deletedItems.map(item => ({
id: item.id,
_ver: item._ver
})));
}
if (updateItems.length > 0) {
try {
const data = await adapter.readBulk(store, updateItems.map(item => item.id));
if (!data || !Array.isArray(data)) {
console.error(`[extractChangesFromAdapter] readBulk returned invalid data for store ${store}:`, data);
continue;
}
const changes = data
.filter(item => item && typeof item === 'object' && item.id)
.map(item => ({
id: item.id,
data: item,
_ver: updateItems.find(i => i.id === item.id)?._ver || Date.now()
}));
if (changes.length > 0) {
putMap.set(store, changes);
}
}
catch (error) {
console.error(`[extractChangesFromAdapter] Failed to read data from store ${store}:`, error);
continue;
}
}
}
return {
delete: deleteMap,
put: putMap,
};
};
// 写入完整的变更到对应的适配器
export const applyChangesToAdapter = async (adapter, changeSet) => {
try {
// 应用数据修改
for (const [store, changes] of changeSet.put) {
await adapter.deleteBulk(TOMBSTONE_STORE, changes.map(c => c.id)); //防止墓碑中存在数据
await adapter.putBulk(store, changes.map(c => c.data));
}
// 应用删除操作
for (const [store, changes] of changeSet.delete) {
await adapter.deleteBulk(store, changes.map(c => c.id));
const tombstones = changes.map(change => ({
id: change.id,
store: store,
_ver: change._ver,
deleted: true
}));
await adapter.putBulk(TOMBSTONE_STORE, tombstones);
}
}
catch (error) {
console.error('Failed to apply changes:', error);
throw error;
}
};