基于扁平数据结构的 Obsidian 同步设计——Halo 同步最终方案
分类、文档文件夹在本地的数据存储方式
数据存储方式必须采用扁平结构,便于更新读取,也能保持数据整洁性。
[
{
name,
priority,
displayName, // 做文件夹名称
parent, // 构建树形结构用
kind: Category/docs/ProjectVersion/DocTree // 表示 分类/文档合集/文档名称(保持Halo数据一致性的命名)/文档文件夹
}
]
文件夹使用相关的数据类型
虽然采用扁平结构数据存储,但是我们在读取文件夹数据的时候可能会有不同的查询需求,例如构建不同的属性作为 key 的 Map。为了让数据持久化保持一致性,我们不会单独存储各类 Map,然后每次更新,再去维护各个 Map,而是只更新一份数据,通过读取该数据转换成我们想要的数据类型。1
根据功能需求构建如下数据类型:
文件夹排序用 Map
pathToPriorityMap = new Map<string, number>({
path: priority
})
关键点是通过原数据构建树形结构,进而拼凑每个文件夹的 path。
分类、文档文件夹数据的更新
文件夹数据更新的核心难点就是处理差异化、冲突部分,
暴力方法——移除重建
这种方法其实符合分类、文档文件夹不断更新重构的特性。但是,其有点不符合增量更新的直觉。
增量更新
假设旧的数据为 old,新的数据为 new。则有以下分析:
- 文件夹有无检测。构建 Map<name, property>:待删除文件夹 = oldMap - newMap;新增文件夹 = newMap - oldMap;待更新文件夹 = (oldMap & newMap) & (changeHappend)
- 待更新文件夹分析。priority 的变化无需关心,直接更新即可;kind 基本不可能变化;displayName 和 parent 变化其实就代表文件夹名称和路径变化,此外,即使它们不变化,也可能因为 parent 对应元素路径变化而发生变化。所以,关键点不是简单关心数据变化,而是关心数据变化后文件夹的实际路径(包含自身文件夹名)的变化,并将旧文件夹移动到对应新路径上。
- 待更新文件夹更新目标——将对应 name 文件夹移动到新数据构建树的实际路径中即可。具体操作步骤如下:
- 基于旧数据构建目录树获取旧目录的实际路径,Map<name, path>
- 基于新数据构建目录树获取新目录的实际路径,Map<name, path>
- 对应在 “待更新” 文件夹中的 name,执行 “移动操作”
- 避免 “待删除” 的文件夹是某个无需删除文件夹的父目录,在移动完所有目录后,再执行删除操作。
文件夹更新用 Map
nameToPath = new Map<string, string>({
name: path
})
方案复盘
将文件夹的创建和数据存储分离了,将路径映射读取和扁平数据存储分离了。看似两个简单的特性,但却是我摸索了多天才发掘的最佳实践路径。
路径映射读取和扁平数据存储分离
在之前的数据存储思路中,我这两个 Map 数据也都想到了,但想法从一开始的树形结构,到后面的存储两个 Map 并同步维护,都是比较复杂的。最终,在掌握树形结构使用扁平数据存储的这一原则后,使用了 “单一存储扁平结构” 并从中读取构建多个 Map 的方式。存储数据的单一降低了维护难度,提升数据一致性以及树形结构差异化检测的难度。
文件夹的创建和数据存储分离
不再关心哪个 name 对应创建哪个文件夹,而是通过 Map 直接移动到对应路径。数据是更本,文件夹是数据驱动下的表象,只需关心驱动路径,无需关心驱动结果。
文章、文档的更新
同 增量更新 的分析。
脚注一:来源 ↩