Super Productivity 同步栈包边界:@sp/sync-core、@sp/sync-providers 与 App 层的依赖治理实战
【免费下载链接】super-productivitySuper Productivity is an advanced todo list app with integrated Timeboxing and time tracking capabilities. It also comes with integrations for Jira, GitLab, GitHub and Open Project.项目地址: https://gitcode.com/GitHub_Trending/su/super-productivity
Super Productivity 的操作日志同步栈(Operation Log Sync Stack)被拆分为框架无关的核心包@sp/sync-core、内置提供方包@sp/sync-providers以及负责 Angular/NgRx 域装配的src/app应用层。本文基于仓库内的边界契约文档 package-boundaries.md,结合各包的package.json导出配置、ESLint 边界规则和源码 barrel 文件,完整讲解这套"依赖方向—所有权—公共导出—隐私边界"的四段式包治理方案,帮助你在改动同步相关代码时判断代码应落在哪个包、允许引入哪些依赖、以及如何在提交前完成边界验证。
一、设计目标:框架无关与域装配分离
边界文档开宗明义:这套包拆分的目标是让可复用的同步逻辑保持框架无关(framework-agnostic),而把 Super Productivity 的域装配留在应用内。这个目标在包描述中可以直接得到印证——packages/sync-core/package.json 将自身描述为 "Framework-agnostic core types and utilities for Super Productivity sync",packages/sync-providers/package.json 则是 "Framework-agnostic sync provider contracts for Super Productivity sync"。
两者共同点:均声明"sideEffects": false以利于 tree-shaking,均使用tsup构建、vitest测试,且测试脚本都包含一个独立的类型检查步骤(test:typecheck运行tsc --noEmit -p tsconfig.spec.json)。@sp/sync-providers还把@sp/sync-core声明为peerDependencies(生产依赖中仅hash-wasm),从依赖元数据层面明确了"providers 构建于 core 之上"的关系。
这套边界并非孤立存在。它隶属于更大的同步文档体系 docs/sync-and-op-log/README.md,其中package-boundaries.md被定位为Contract(契约类)文档,与向量时钟契约 vector-clocks.md、贡献者同步模型 contributor-sync-model.md 等并列,约束的是"代码应该放在哪里"这一结构性问题。
二、依赖方向:单向链与五条硬规则
2.1 允许的依赖方向
文档给出的允许方向是一个单向链:
src/app -> @sp/sync-providers -> @sp/sync-core src/app -> @sp/sync-core packages/super-sync-server -> @sp/sync-core -> @sp/shared-schema即:应用层可以同时依赖两个包;@sp/sync-providers只依赖@sp/sync-core;自托管服务端 packages/super-sync-server 依赖@sp/sync-core(复用向量时钟算法)和@sp/shared-schema(HTTP 契约类型与校验 schema)。
2.2 逐条规则
边界文档列出了五条必须遵守的规则:
@sp/sync-core不得导入Angular、NgRx、src/app、@sp/shared-schema,或任何提供方特定代码。它必须保持域无关。@sp/sync-providers只能导入@sp/sync-core的公共导出,禁止 deep-import@sp/sync-core/*、Angular、NgRx、src/app或@sp/shared-schema。- 应用层(
src/app)可以同时导入两个包,并负责:Angular 依赖注入、NgRx、Electron/Capacitor 桥接、配置 UI、OAuth 路由,以及 Super Productivity 特有的模型装配。 packages/shared-schema拥有应用与服务端共享的 schema 契约与校验器,它不依赖@sp/sync-core,也不应成为 core/providers 的依赖。packages/super-sync-server同时依赖@sp/shared-schema(HTTP 契约类型与校验 schema)和@sp/sync-core(向量时钟算法)。
2.3 ESLint 如何强制这些规则
这些规则不只是文档约定,仓库根部的 eslint.config.js 中对packages/sync-core/**/*.ts和packages/sync-providers/**/*.ts设置了专门的import/no-restricted-paths/ 受限模块 override(约 L156–L240),错误信息直接复述了文档措辞,例如:
@sp/sync-core must stay domain-agnostic; shared-schema is SP-specific.@sp/sync-core must not import Angular, NgRx, app code, or SP-specific schema packages.@sp/sync-core must not use dynamic imports; they bypass package-boundary checks.(动态import()会绕过静态边界检查,因此被一并禁止)@sp/sync-providers must use only public @sp/sync-core exports and must not import Angular, NgRx, app code, or SP-specific schema packages.
providers 的受限模式同时覆盖了@sp/sync-core/src、@sp/sync-core/深路径以及**/sync-core/*形式的路径变体,确保无论是 npm 风格导入还是相对路径兜底都无法绕过。
三、所有权边界:四个包各自"拥有什么"
3.1@sp/sync-core:可复用的同步引擎原语
core 包拥有通用同步引擎的全部纯逻辑。对照其唯一公共入口 packages/sync-core/src/index.ts,文档所列所有权清单与实际导出完全对应:
- 通用操作与 apply 类型:
OpType、isMultiEntityPayload、isLwwUpdatePayload、extractActionPayload、Operation、OperationLogEntry等(来自operation.types); - 向量时钟比较、合并与裁剪算法:
compareVectorClocks、mergeVectorClocks、limitVectorClockSize、MAX_VECTOR_CLOCK_SIZE——barrel 注释明确称其为 "single source of truth for client/server parity",即客户端与服务端共用的单一事实来源; - 纯函数式的冲突、导入过滤、上/下载规划、重放、压缩、前缀助手:
classifyOpAgainstSyncImport、createSyncFilePrefixHelpers、compressWithGzip/decompressGzipFromString、replayOperationBatch、applyRemoteOperations、planUploadLastServerSeqUpdate、planSnapshotHydration等; - 结构化的实体注册表契约:
EntityRegistry、EntityConfig类型以及getEntityConfig、isAdapterEntity、isMapEntity等判定函数(entity-registry.types); - 面向应用的端口契约与隐私感知的
SyncLogger接口:ActionDispatchPort、ConflictUiPort、OperationApplyPort等 9 个端口类型(ports),以及NOOP_SYNC_LOGGER、toSyncLogError(sync-logger)。
core 的依赖面极窄:生产依赖只有@noble/ciphers和hash-wasm(服务于 Argon2id KDF + AES-GCM 加密原语),没有任何 UI 框架依赖——这正是"框架无关"承诺的直接体现。
3.2@sp/sync-providers:内置提供方实现与提供方无关契约
providers 包拥有各内置提供方的实现及跨提供方共享契约:
- 提供方类:Dropbox、OneDrive、WebDAV(含 Nextcloud 变体)、SuperSync、LocalFile,对应源码目录 packages/sync-providers/src/file-based/ 下的
dropbox/、onedrive/、webdav/、local-file/与 packages/sync-providers/src/super-sync/; - 基于文件的同步信封类型与提供方响应契约(
file-based-sync-data.ts等); - 提供方拥有的文件信封常量:如
sync-data.json文件名与 file-sync 版本键; - 端口:credential、file-adapter、platform-info、web-fetch、native-HTTP、storage、response-validator;
- 提供方共享的错误类、PKCE 助手、重试助手与安全日志元数据助手。
文档还给出了一条"放置准则":跨提供方但非引擎通用的工具放@sp/sync-providers,而非 core。现有例子包括提供方共享错误类、PKCE、重试判定谓词(retryable-upload-error.ts)、native-HTTP 重试(native-http-retry.ts)和安全日志元数据助手(log/error-meta.ts)。
3.3src/app:宿主特定的配置与编排
应用层拥有宿主特定的配置与编排逻辑,包括:
ActionType、ENTITY_TYPES、SyncProviderId、提供方列表,以及REMOTE_FILE_CONTENT_PREFIX、PRIVATE_CFG_PREFIX等存储前缀;- 从 feature reducers/selectors 构建实体注册表;
- 包装后的完整状态载荷形状、导入原因、修复载荷,以及对照
@sp/shared-schema的校验; - Angular 服务、NgRx dispatch/replay 转换、本地动作过滤、hydration 窗口、归档副作用、提供方工厂、OAuth 回调、配置对话框与平台桥接实现。
3.4packages/shared-schema与packages/super-sync-server
packages/shared-schema拥有应用与服务端共享的 Super Productivity schema 契约与校验器。文档特别强调:在这个边界内它应该保持 SP 耦合(SP-coupled),绝不应成为@sp/sync-core或@sp/sync-providers的依赖。而packages/super-sync-server作为第二宿主,同时消费@sp/shared-schema(HTTP 契约)与@sp/sync-core(向量时钟算法)——core 的框架无关性正是让它能同时服务 Web 客户端与 Node 服务端两个宿主的根本原因。
四、公共导出:只从 barrel 导入
4.1 基本原则
文档要求:包的使用方只能从包 barrel 导入:
import { compareVectorClocks } from '@sp/sync-core'; import { Dropbox, PROVIDER_ID_DROPBOX } from '@sp/sync-providers/dropbox';严禁导入@sp/sync-core/src/*、@sp/sync-providers/src/*或dist/*等包内部路径。若宿主确实需要一个符号,应当刻意地将其提升到包 barrel,并确认该符号不属于 app 所有物。
4.2 providers 的子路径 barrel 清单
文档指出:@sp/sync-providers的根 barrel 已被移除,使用方必须从聚焦的子路径 barrel 导入。对照 packages/sync-providers/package.json 的exports字段,实际可用的子路径与文档清单一致:
| 子路径 barrel | 导出的提供方/能力 |
|---|---|
@sp/sync-providers/dropbox | Dropbox 提供方类 |
@sp/sync-providers/onedrive | OneDrive 提供方类 |
@sp/sync-providers/webdav | WebDAV/Nextcloud 提供方 |
@sp/sync-providers/super-sync | SuperSync(有序操作 API)提供方 |
@sp/sync-providers/local-file | LocalFile 提供方 |
@sp/sync-providers/http | native-HTTP 重试、可重试上传错误 |
@sp/sync-providers/errors | 提供方共享错误类 |
@sp/sync-providers/file-based | 文件信封类型与常量 |
@sp/sync-providers/pkce | PKCE 助手 |
@sp/sync-providers/platform | 平台信息、web-fetch 工厂 |
@sp/sync-providers/provider-types | 提供方无关契约类型 |
@sp/sync-providers/credential-store | 凭据存储端口 |
@sp/sync-providers/log | 隐私边界安全日志助手 |
文档还划定了两条补充线:其一,提供方类、提供方拥有的字符串常量和隐私边界日志助手从这里导出,但应用层枚举(如SyncProviderId)不在其中——它们是 app 所有物;其二,内部助手(如 WebDAV 的 API/adapter 类)保持不导出,除非出现第二个宿主真正需要它们。这实际上是在用导出面控制包的 API 表面积增长。
4.3 弃用兼容导出的处理
文档说明@sp/sync-core仍然导出弃用的完整状态操作兼容默认值,以及宿主自定义的OpType.SyncImport/BackupImport/Repair字符串,供现有消费方使用;新的可复用宿主应通过createFullStateOpTypeHelpers()提供自己的完整状态操作字符串,而不是依赖内置默认值。在 packages/sync-core/src/index.ts(L80–L87)中可以看到对应设计:createFullStateOpTypeHelpers与createLwwUpdateActionTypeHelpers都是由宿主传入实体类型列表/操作字符串的工厂,注释明确写着 "the lib stays domain-agnostic"——core 通过"宿主注入域常量"的方式保持通用,这正是前文所有权边界的代码级落实。
五、隐私边界:SyncLogger 与可导出日志
同步包的日志必须使用SyncLogger且只携带安全的结构化元数据。实现见 packages/sync-core/src/sync-logger.ts,其文件头注释与文档逐句对应:日志历史可能被宿主应用导出(exportable),因此通过该端口传递的元数据必须限于 ID、计数、动作字符串、实体类型、op ID 和清洗后的错误标识。
关键约束可以归纳为三档:
- 允许:ID、计数、动作字符串、实体类型、提供方 ID、错误名/错误码。
toSyncLogError()的实现印证了这一点——它只提取name与code(缺失时降级为StringError/UnknownError),从不返回message或堆栈,从 API 形状上就把自由文本挡在错误标识之外。 - 有条件允许:URL 元数据仅在调用方剥离查询串、fragment、凭据、token、原始响应体以及用户提供的路径段(文件名、邮箱、分享 ID、文件夹名)之后才可记录。文档建议优先使用粗粒度路径模板、提供方操作名、仅含 host 的值,或提供方拥有的相对路径类别,而非原始 URL 路径。
- 禁止:完整实体、操作载荷、任务标题、笔记文本、原始提供方响应、凭据、请求头、加密材料,一律不得进入可导出日志。
文档同时坦诚了执行现状:SyncLogger是隐私感知的端口形状(port shape),它本身不对任意元数据做消毒,调用点有责任传入已清洗的值;当前执行手段是代码评审加针对性测试,针对"不安全直接打日志"的 lint 规则仍是一个可能的后续项。在那之前,新增的可迁移/提供方代码应一律使用SyncLogger,测试应断言隐私敏感的 catch 路径。
六、测试如何参与边界治理
文档对测试的边界规则单独成节,要点有三:
- ESLint 包边界 override 覆盖
packages/sync-core/**与packages/sync-providers/**下的所有 TypeScript 文件,包括测试; - 测试可以通过相对路径导入自己包的内部实现(白盒覆盖所必需);
@sp/sync-providers的测试可以导入@sp/sync-core的公共导出,但不应导入 sync-core 的内部实现或其测试助手。
从实际测试布局可以印证这种分层:packages/sync-providers/tests/ 按file-based/(dropbox、local-file、webdav 子目录)、super-sync/、log/组织,与 barrel 子路径一一对应,且存在tests/helpers/credential-store.ts、tests/helpers/sync-logger.ts这类本包内自建的测试桩——后者正体现了"core 端口由宿主/测试自行注入实现"的端口模式。两个包的测试脚本(npm run test)均先跑test:typecheck再跑vitest run,配合根 package.json 中的聚合脚本packages:test(依次执行 sync-core、shared-schema、sync-providers 三包的测试),构成边界合规的第一道自动化防线。
七、跨边界移动代码前的验证清单
文档给出的标准验证流程是四条命令(均来自仓库根 package.json 中真实存在的脚本):
npm run lint npm run sync-core:build npm run sync-providers:build npm run packages:test其中sync-core:build/sync-providers:build分别在对应包目录下执行tsup构建(package.json L176–L178),packages:test聚合三个包的类型检查+单测(L175)。构建通过的意义在于:exports字段锁死了子路径面,任何"想从包内偷符号"的深路径导入都会在构建期直接失败。
文档还提供了更快的边界抽查命令,用 ripgrep 在两个包的src下扫描违禁导入模式(Angular/NgRx 框架包、@sp/shared-schema、src/app、@sp/sync-core/src深路径,含动态import()形式):
rg -n "from '\"|import\('\"" packages/sync-core/src packages/sync-providers/src该模式与 ESLint override 的受限目标互为镜像,适合作为移动代码后的即时 sanity check。
八、新增内置提供方的落地路径
文档最后给出了一条面向未来的扩展指引:新增内置提供方应遵循 Dropbox/OneDrive/WebDAV/SuperSync/LocalFile 模式——
- 把提供方拥有的协议逻辑与提供方无关契约放进
@sp/sync-providers(新增独立子目录 + 对应的子路径 barrel); - 在应用侧用薄的工厂组合 app 专有的凭据、平台桥接、校验器、OAuth 路由与 UI 配置;
- 如果某个提供方是 app 专有或插件提供的(而非内置),则直接在应用侧针对 provider 契约实现,不要为它扩大包表面。
这条规则把"导出面即 API 承诺"的原则延伸到了扩展场景:包表面只随真正的复用需求增长,一次性宿主需求一律留在应用层。
小结
package-boundaries.md 定义的这套包边界,本质上是 Super Productivity 同步栈在"单一代码库、多个宿主(Web 应用 + 自托管服务端)"现实下的一次工程治理:@sp/sync-core用极窄依赖(仅@noble/ciphers、hash-wasm)与宿主注入式工厂守住框架无关性;@sp/sync-providers用 13 个子路径 barrel 与已移除的根 barrel 守住提供方 API 表面积;src/app独享 Angular/NgRx/域模型装配;@sp/shared-schema保持 SP 耦合而不被下沉包反向依赖。整套约束由 ESLint override、exports构建期锁死、packages:test聚合测试以及可复制的rg抽查命令共同执行,而SyncLogger隐私边界则把"日志可能被用户导出"这一产品事实编码进了端口设计之中。对维护者而言,本文第二节的方向图与第七节的验证清单,就是在改动任何同步相关代码之前需要过的两道门。
【免费下载链接】super-productivitySuper Productivity is an advanced todo list app with integrated Timeboxing and time tracking capabilities. It also comes with integrations for Jira, GitLab, GitHub and Open Project.项目地址: https://gitcode.com/GitHub_Trending/su/super-productivity
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考