在多设备同步过程中,需要处理两类主要事件:本地文件变更 和 云端文件变更。事件处理在同步算法中起着核心作用——后续的同步操作(如上传、下载、删除、重命名、移动和合并)都基于事件处理的结果。
在 Obsidian 中,同步涉及 4 种类型的事件:
- 创建文件或文件夹。
- 重命名文件或文件夹。
- 删除文件或文件夹。
- 修改文件。
以上是同步过程中关注的四种事件类型,每种都对应 Obsidian 中相关的用户操作。
事件类型
使用 vault.on() 方法监听文件变更事件:
vault.on('create', () => {}) // 新建文件/文件夹
vault.on('rename', () => {}) // 重命名(移动)
vault.on('delete', () => {}) // 删除文件/文件夹
vault.on('modify', () => {}) // 修改文件代码级监控
此外,还可以通过使用
cross等工具进行方法修补 (method patching) 来实现。这种方法尚未探索——以下所有内容均基于vault.on触发的事件进行描述。
了解如何监听事件后,让我们检查这四种事件类型的特征,以实施适当的处理。正确识别每个事件的唯一性对于设计有效的解决方案至关重要。
事件特征
1. 创建事件
- 触发方式:当用户创建新文件或文件夹时。
- 特征:由于用户一次只能创建一个文件/文件夹,因此一个创建操作正好对应一个创建事件。
2. 重命名事件
- 触发方式:(1) 点击重命名文件/文件夹;(2) 将文件/文件夹移动到另一个位置。
- 特征:重命名或移动文件夹时,文件夹本身及其所有子项都会触发重命名事件。事件顺序遵循 广度优先遍历——首先是文件夹本身,然后是其直接子项,依此类推。
3. 删除事件
- 触发方式:当用户删除文件或文件夹时。
- 特征:删除文件夹时,文件夹及其所有子项都会触发删除事件。然而,与重命名事件不同,没有明显的顺序——事件基本上是批量同时触发的。
4. 修改事件
- 触发方式:当用户修改文件内容时。
- 特征:仅当文件内容更改时触发;文件夹永远不会触发此事件。
收集与预处理
创建和修改事件不需要额外的预处理,因为每个操作映射到一个事件。然而,重命名或删除文件夹带来了特殊的挑战。例如:如果您删除了包含子文件夹 A-1 的文件夹 A,假设删除事件的触发顺序为:
- 删除文件夹
A - 删除文件夹
A-1
直接在云端执行这些事件会导致:
- 删除云端文件夹
A - 尝试删除云端文件夹
A-1(该文件夹已不存在),导致无效操作。
删除或移动大文件夹会产生大量无效操作。因此,在向云端发送请求之前,必须进行预处理。
此外,事件收集也存在问题。使用上面的删除示例,我们需要收集“删除 A”和“删除 A-1”两个事件,过滤掉冗余操作,最后向云端发送单个“删除 A”请求。但是,我们无法假设事件触发的顺序或间隔。
解决方案:
- 对于删除事件:在典型场景中,删除事件会批量发生,间隔仅为几毫秒。因此,可以使用
debounce(防抖) 方法延迟处理并在指定窗口内收集事件。 - 对于重命名事件:由于事件遵循特定顺序,我们可以检查父文件/文件夹是否已在处理中。如果是,则可以忽略子项的事件。
这里我们得到了处理本地文件事件的基本工作流:
flowchart LR A[事件生成] --> 收集 --> 过滤 --> D[更多...]
事件收集和过滤的代码示例:
debounce(() => {
// 事件预处理:fileEventTemp 将原始事件存储为队列
this.fileEventTemp.sort((a, b) => {
if (a.timestamp !== b.timestamp) {
return a.timestamp - b.timestamp;
} else {
return a.targetPath.length - b.targetPath.length;
}
});
// historyEvents 存储预处理后的事件
const historyEvents: FileEvent[] = [];
let nextEvent = this.fileEventTemp.shift();
while (nextEvent) {
if (historyEvents.some(e => nextEvent?.targetPath.startsWith(e.targetPath))) {
logger.info(`Ignore event: ${nextEvent}`);
} else {
historyEvents.push(nextEvent);
}
nextEvent = this.fileEventTemp.shift();
}
}, 1000, true)(); // 延迟 1 秒以收集过去一秒内的事件到 fileEventTemp压缩事件 (Compact Events)
考虑这种情况:文件 A 重命名为 B,然后再次重命名为 C。这会生成两个重命名事件:
- 重命名
A→B - 重命名
B→C
在预处理期间,这两个事件都被收集。实际上,这两个事件可以合并为一个:
3. 重命名 A → C
Sync Vault 支持重命名事件的链式聚合,以减少云端请求的数量。
本地事件预处理完成后,将触发实际的 Synchronization Flow (同步流程)。