在多设备同步过程中,需要处理两大类事件:一是本地的文件变化,二是云端的文件变化。事件的处理在同步算法中扮演了核心的角色,后续的同步动作,比如上传、下载、删除、重命名、移动、合并等都基于对事件的处理结果。
在Obsidian中,同步涉及到4类事件,分别为
- 创建文件或文件夹。
- 重命名文件或文件夹。
- 删除文件或文件夹。
- 修改文件。
以上是同步过程中关心的四类事件,这四类事件都对应了Obsidian用户的相关操作。
监听4类事件
文件的变化事件通过 vault.on() 方法进行监听:
vault.on('create', () => {}) // 新增文件
vault.on('rename', () => {}) // 重命名(移动)
vault.on('delete', () => {}) // 删除文件
vault.on('modify', () => {}) // 修改文件
✏️ 除此之外,还可以通过cross对相关方法进行插桩进行代码层级上的监听。暂未对该方法进行探究,以下内容都是基于
vault.on触发的事件进行描述的。
知道了怎么监听之后,再来看一下这四类事件的特点,然后再进行合理地处理。正确认识到问题的特殊性,才能做出相应合理的措施。
Obsidian文件事件的特点
- create事件。
- 触发:用户新建文件或者文件夹的时候。
- 特点:由于用户在一个时刻只能create一个文件或者文件夹,因此一个新建操作对应一个create事件。
- rename事件。
- 触发:(1)点击重命名文件或文件夹;(2)移动文件或者文件夹到另一个位置。
- 特点:在针对文件夹重命名或者移动的时候,会触发针对文件夹及其子项的rename事件,事件的顺序为先文件夹自身,再子项,符合广度优先的特征。
- delete事件:
- 触发:用户删除文件或文件夹。
- 特点:在删除文件夹的时候,会触发针对文件夹及其子项的delete事件,但是事件的顺序和rename不一样,没有明显的顺序,基本上是批量一起触发的。
- modify事件。
- 触发:用户修改了文件。
- 特点:仅在文件内容变动的时候触发,因此文件夹是不会触发这种事件的。
事件收集和预处理
create事件和modify事件由于存在单操作对应单个事件的特点,因此不需要额外的预处理。但是,在重命名文件夹和删除文件夹的时候,情况就不一样了。举个例子,比如删除文件夹A,里面有文件夹A-1,假设先触发了删除A,再触发了删除A-1。假如直接基于这两个事件触发云盘操作:
- 删除云盘文件夹A。
- 删除云盘文件夹A-1,此时由于A-1已经不存在,那么这是一次无效的删除操作。
✏️ 在删除或者移动一个大文件夹时,会产生大量的无效操作,因此需要在真正向云端发送请求前进行预处理。
除此之外,事件的收集也变成了一个问题,还是上面的删除事件为例,我们希望同时收集事件删除A和事件删除A-1,然后再过滤冗余操作,最后向云服务发送删除A请求。但是事件触发的时间先后以及时间间隔我们无法假定。
- 针对delete事件。在一般场景下,删除事件批量发生,事件间隔在几个毫秒左右。因此通过
debounce方法可以达到延迟触发的效果。 - 针对rename事件。由于事件有先后顺序,因此可以通过当前事件对应的文件路径来判断时候已经有parent文件在处理,如果存在parent,那么可以忽略当前事件。
综上,我们得到了一个处理本地文件事件的基本流程:
事件触发 ➡ 事件收集 ➡ 事件过滤 ➡ 后续操作
事件收集和过滤操作可以用下面的示例代码表示:
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秒,收集最近1秒的事件到fileEventTemp数组中
事件压缩(聚合)
考虑这样的场景,文件A重命名为了文件B,然后又重命名成了文件C,这就产生了两个rename事件:
- rename A -> B
- rename B -> C
在预处理中,会收集到这两个事件,事实上这两个事件可以合并成一个事件 - rename A -> C
Sync Vault中支持rename事件的链式聚合,可以减少云端请求数量。
本地事件预处理完毕后,就触发真正的 同步流程 了。