delta-sync
Version:
A lightweight framework for bi-directional database synchronization with automatic version tracking and conflict resolution.
272 lines (271 loc) • 11.1 kB
JavaScript
// core/LocalCoordinator.ts
// 本地的协调层,包装数据库适配器同时提供自动化的变更记录
export class LocalCoordinator {
constructor(localAdapter, encryptionConfig) {
this.LOCAL_CHANGES_STORE = 'local_data_changes'; // 独立的变更表,记录所有数据修改
this.ATTACHMENT_CHANGES_STORE = 'local_attachment_changes'; // 新增附件变更表
this.META_STORE = 'local_meta'; // 改为更通用的meta存储表
this.VERSION_KEY = 'sync_version'; // 修改key名使其更明确
this.localAdapter = localAdapter;
this.encryptionConfig = encryptionConfig;
}
// 写入数据并自动跟踪变更
async putBulk(storeName, items, skipTracking = false) {
const version = Date.now();
const updatedItems = items.map(item => ({
...item,
_version: version,
}));
const result = await this.localAdapter.putBulk(storeName, updatedItems);
if (!skipTracking) {
for (const item of updatedItems) {
await this.trackDataChange(storeName, item, 'put');
}
}
return result;
}
// 删除数据并且自动写入变更
async deleteBulk(storeName, ids, skipTracking = false) {
const items = await this.localAdapter.readBulk(storeName, ids);
for (const item of items) {
if (item._attachments) {
const attachmentIds = item._attachments
.filter(att => att.id && !att.missingAt)
.map(att => att.id);
if (attachmentIds.length > 0) {
await this.localAdapter.deleteFiles(attachmentIds);
}
}
}
await this.localAdapter.deleteBulk(storeName, ids);
if (!skipTracking) {
const version = Date.now(); // 使用统一的当前时间戳
const changes = items.map(item => ({
id: item.id,
_version: version // 使用新的时间戳
}));
await Promise.all(changes.map(change => this.trackDataChange(storeName, change, 'delete')));
}
}
async attachFile(modelId, storeName, file, filename, mimeType, metadata = {}) {
// 首先获取原始模型
const items = await this.localAdapter.readBulk(storeName, [modelId]);
if (items.length === 0) {
throw new Error(`无法找到ID为 ${modelId} 的模型`);
}
const fileId = `attachment_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`;
const fileItem = {
fileId: fileId,
content: file
};
// 保存文件到存储
const [savedAttachment] = await this.localAdapter.saveFiles([fileItem]);
const attachment = {
id: savedAttachment.id,
filename: filename,
mimeType: mimeType, // 明确保留为必需参数
size: savedAttachment.size,
createdAt: Date.now(),
updatedAt: Date.now(),
metadata: metadata || {}
};
const model = items[0];
// 添加附件到模型
if (!model._attachments) {
model._attachments = [];
}
model._attachments.push(attachment);
const targetStoreName = model._store || storeName;
await this.putBulk(targetStoreName, [model]);
await this.trackAttachmentChange(savedAttachment.id, model._version || 0, 'put');
return attachment;
}
// 从模型中移除附件
async detachFile(storeName, modelId, attachmentId) {
// 1. 首先获取原始模型
const items = await this.localAdapter.readBulk(storeName, [modelId]);
if (items.length === 0) {
throw new Error(`Cannot find model with ID ${modelId} in store ${storeName}`);
}
const model = items[0];
if (!model._attachments) {
throw new Error(`Model ${modelId} has no attachments`);
}
// 2. 查找附件
const index = model._attachments.findIndex(att => att.id === attachmentId);
if (index === -1) {
throw new Error(`Attachment ${attachmentId} not found in model ${modelId}`);
}
try {
// 3. 删除物理文件
await this.localAdapter.deleteFiles([attachmentId]);
// 4. 创建模型的副本并更新附件列表
const updatedModel = {
...model,
_attachments: [...model._attachments]
};
updatedModel._attachments.splice(index, 1);
// 5. 保存更新后的模型,这将触发变更记录
const [savedModel] = await this.putBulk(storeName, [updatedModel]);
await this.trackAttachmentChange(attachmentId, savedModel._version || 0, 'delete');
return savedModel;
}
catch (error) {
console.error(`Error detaching file ${attachmentId} from model ${modelId}:`, error);
throw error;
}
}
// 记录附件变更
async trackAttachmentChange(attachmentId, // 附件ID
version, // 版本号
type) {
const attachmentChange = {
id: attachmentId,
_version: version,
type: type,
};
await this.localAdapter.putBulk(this.ATTACHMENT_CHANGES_STORE, [attachmentChange]);
console.log(`记录附件变更:attachmentId=${attachmentId}, ` +
`version=${version}, ` +
`type=${type}`);
}
// 记录数据变更
async trackDataChange(storeName, data, operationType) {
const changeRecord = {
id: data.id,
_store: storeName,
_version: data._version || Date.now(), // 使用数字时间戳
_operation: operationType,
data: operationType === 'put' ? data : undefined,
};
await this.localAdapter.putBulk(this.LOCAL_CHANGES_STORE, [changeRecord]);
console.log(`记录待同步变更:
- 存储: ${storeName}
- ID: ${changeRecord.id}
- 操作: ${operationType}
- 时间: ${changeRecord._version}` // 日志中展示可读格式
);
}
// 从云端同步数据
async applyDataChange(changes) {
console.log('待应用的更改:', changes);
const changesByStore = new Map();
for (const change of changes) {
if (!changesByStore.has(change._store)) {
changesByStore.set(change._store, { puts: [], deletes: [] });
}
const storeChanges = changesByStore.get(change._store);
if (change._operation === 'put' && change.data) {
storeChanges.puts.push(change.data);
}
else if (change._operation === 'delete') {
storeChanges.deletes.push(change.id);
}
}
// 应用数据变更
for (const [storeName, storeChanges] of changesByStore.entries()) {
console.log(`Writing to store ${storeName}:`, {
puts: storeChanges.puts,
deletes: storeChanges.deletes
});
if (storeChanges.puts.length > 0) {
await this.localAdapter.putBulk(storeName, storeChanges.puts);
}
if (storeChanges.deletes.length > 0) {
await this.localAdapter.deleteBulk(storeName, storeChanges.deletes);
}
}
}
async applyAttachmentChanges(changes) {
try {
// 按照版本号排序
const sortedChanges = [...changes].sort((a, b) => (a._version || 0) - (b._version || 0));
// 记录成功处理的变更,以便后续更新状态
const processedChanges = [];
for (const change of sortedChanges) {
try {
// 标记为已同步
const markedChange = {
...change,
};
// 保存变更记录到本地附件变更表
await this.localAdapter.putBulk(this.ATTACHMENT_CHANGES_STORE, [markedChange]);
processedChanges.push(markedChange);
console.log(`应用附件变更:attachmentId=${change.id}, ` +
`version=${change._version}, ` +
`type=${change.type}`);
}
catch (error) {
console.error(`处理附件变更失败: attachmentId=${change.id}`, error);
}
}
}
catch (error) {
console.error('应用附件变更时发生错误:', error);
throw error;
}
}
// 获取待同步的变更记录
async getPendingChanges(since, limit = 100) {
try {
const result = await this.localAdapter.readByVersion(this.LOCAL_CHANGES_STORE, {
since: since,
limit,
order: 'asc'
});
console.log(`获取自 ${since} 依赖的待同步变更: ${result.items.length} 条`);
return result.items;
}
catch (error) {
console.error('获取待同步变更失败:', error);
return [];
}
}
// 获取待同步的附件变更
async getPendingAttachmentChanges(since, limit = 100) {
const result = await this.localAdapter.readByVersion(this.ATTACHMENT_CHANGES_STORE, {
since: since,
limit: limit,
order: 'asc'
});
return result.items.sort((a, b) => (a._version || 0) - (b._version || 0));
}
async updateCurrentVersion(timestamp) {
try {
if (!Number.isInteger(timestamp) || timestamp < 0) {
throw new Error("时间戳必须是一个有效的正整数");
}
await this.localAdapter.putBulk(this.META_STORE, [{
id: this.VERSION_KEY,
value: timestamp
}]);
console.log(`成功更新同步时间戳: ${timestamp}`);
}
catch (error) {
console.error("更新同步时间戳失败:", error);
throw new Error(`更新同步时间戳失败: ${error instanceof Error ? error.message : String(error)}`);
}
}
// 获取当前版本号(Todo优化查询方式)
async getCurrentVersion() {
try {
const result = await this.localAdapter.readBulk(this.META_STORE, [this.VERSION_KEY]);
if (result.length > 0) {
return result[0].value;
}
// 如果没有记录,初始化为0
const initialState = {
id: this.VERSION_KEY,
_version: 0,
_store: this.META_STORE,
value: 0
};
await this.localAdapter.putBulk(this.META_STORE, [initialState]);
return 0;
}
catch (error) {
console.warn("读取同步时间戳失败:", error);
return 0;
}
}
}