尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Super Productivity SECTION 冲突重放契约:准入判定、状态投影与原子恢复的完整实现解析

Super Productivity SECTION 冲突重放契约:准入判定、状态投影与原子恢复的完整实现解析 Super Productivity SECTION 冲突重放契约准入判定、状态投影与原子恢复的完整实现解析【免费下载链接】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同步管线中一个窄而关键的正确性契约——SECTION Conflict ReplaySECTION 冲突重放。当服务端以并发为由拒绝本地 SECTION 操作移动、移除、重排时通用实体级 LWWLast-Write-Wins快照会丢失跨 Section 与 Project/Tag 工作上下文的顺序语义导致任务同时出现在两个容器、从两边消失或各端排序不一致。本文从契约原文出发结合 section-conflict-commutativity.util.ts 与 superseded-operation-resolver.service.ts 的源码实现完整拆解重放的准入契约、基于状态的四种投影结果、单事务原子恢复、已发布客户端兼容策略以及单元测试与 E2E 收敛验证帮助你理解并安全修改 SECTION 冲突恢复行为。该契约由以下可执行文件共同拥有本文所有结论均可回到这些文件验证src/app/op-log/sync/section-conflict-commutativity.util.tssrc/app/op-log/sync/superseded-operation-resolver.service.tssrc/app/op-log/sync/superseded-operation-resolver.service.spec.tse2e/tests/sync/supersync-section-convergence.spec.ts为什么通用实体 LWW 对 SECTION 不够Super Productivity 的同步模型以持久化 NgRx action 为单一客户端同步管线持久化 action 同时更新运行时投影并被捕获为可持久化操作operation服务端用向量时钟vector clock检测因果顺序与并发编辑相关整体模型可参考 README.md 与 sync-architecture.html。当服务端判定本地操作的向量时钟被其他客户端主导时本地操作会被拒绝为“已被取代”superseded。对大多数实体SupersededOperationResolverService 的通用路径会取实体当前状态、合并向量时钟并生成新的 LWW Update 操作来保留本地修改详见该服务类头部的文档注释。但SECTION 操作不是普通实体快照能表达的SECTION_ADD_TASK/SECTION_REMOVE_TASK/SECTION_UPDATE_ORDER编码的是“一个 Section 与其 Project 或 Tag 工作上下文之间的有序关系”若把被拒绝的移动、移除或重排折叠成一个单实体快照snapshotreducer 语义就会丢失任务可能同时留在两个容器、从两边都消失或者各客户端收敛出不同的排序。因此该契约允许 resolver 对被拒绝的 SECTION 意图执行“重放”replay而不是折叠成通用实体快照。但这是刻意收窄的例外绝不是任意被拒绝 action 都可以重放的许可——这正是下面“准入契约”存在的意义。准入契约哪些 SECTION 操作允许重放候选 action 家族只有三个 action 家族是候选与源码 superseded-operation-resolver.service.ts 中CAUSALLY_REPLAYABLE_SECTION_ACTIONS集合完全对应Action语义SECTION_UPDATE_ORDER重排某工作上下文内 Section 的显示顺序SECTION_ADD_TASK把任务放入某个 Section含跨 Section 移动SECTION_REMOVE_TASK从某个 Section 移除任务并携带工作上下文锚点五个必须同时满足的条件重放只有在以下条件全部成立时才被准入被拒绝的操作已存在实体 frontierexistingClock存在并且与保留操作的时钟关系是CONCURRENT并发而非因果主导恰好一条保留操作命中该操作影响的实体/时钟 frontier源码中通过retainedByEntityClock索引查找多于一条立即回退见 superseded-operation-resolver.service.ts该保留行是一条已应用、已同步、非拒绝的远程操作source remote、syncedAt已定义、applicationStatus applied、rejectedAt与reducerRejectedAt均未设置见 superseded-operation-resolver.service.tsareCommutingSectionOperations()精确识别出这一对操作是可交换commuting的操作元数据与被拒绝时用于决策的 action payload 完全一致——即在重放前会把被拒绝操作与保留操作精确比对后再重放。被识别为可交换的交叉crossings只有两种源码areCommutingSectionOperations()刻意只识别两类交叉绝不泛化同一任务从移动源 Section 的“移动 × 移除”即SECTION_ADD_TASK跨 Section 移动sourceSectionId removal.sectionId且taskId相同与SECTION_REMOVE_TASK的配对section-conflict-commutativity.util.ts一个 section-order 更新 × 一个触及该有序 Section 的 placement/removalgetSectionOrder()与getSectionPlacement()/getSectionRemoval()配对只要 placement/removal 的实体 ID 命中 order 的sectionIds即认为可交换section-conflict-commutativity.util.ts。契约同时给出明确红线证据缺失、模糊、畸形或不可交换时一律保留通用 LWW 回退绝不因为两个 action 在某个 fixture 里看似无害就扩大识别范围Never broaden recognition merely because two actions appear harmless in one fixture.。单元测试中也专门覆盖了“保留的 SECTION 操作不可交换时保持 LWW 回退”的场景见 superseded-operation-resolver.service.spec.ts 与第 1962 行的参数化用例。基于状态的投影四种判定结果准入通过后被接受的意图要投影到一个稳定且全部由可持久化操作表达的 NgRx 快照上。快照的稳定性保证_getStableSectionReplaySnapshot()的实现体现了稳定性契约先用getPhantomChangeRisk()来自 phantom-change-guard.util.ts做幻影变更检查有风险直接抛错拒绝投影再同步读取getStateSnapshotForOperationLog()快照。关键点在于幻影变更检查与快照读取之间没有任何await。操作日志锁把后续用户 action 挡在恢复事务之后因此快照读取时不会夹进新的未持久化变更。projectSectionReplayAgainstState()的四种结果projectSectionReplayAgainstState()依据被拒绝操作的类别order / placement / removal分别投影返回四种结果之一结果含义处理replay用当前排序与锚点创建替代操作生成新的 SECTION 替代操作必要时附带工作上下文状态补偿work-context-state需要精确的 Project/Tag 状态补偿来保持工作上下文排序生成完整的工作上下文 LWW 快照操作superseded当前持久化状态已使该意图过时直接拒绝过时的前驱不创建替代blocked该转变无法安全表达附reason保留通用 LWW 回退具体投影逻辑要点与源码对应Section order用当前快照中属于该 context 的 Section id 列表重建 order 操作若该 context 下已无 Section 则返回supersededsection-conflict-commutativity.util.tsPlacementSECTION_ADD_TASK先检查源/目标 Section 是否属于同一工作上下文跨上下文直接blocked任务在目标 Section 中锚点存在则replayplacement任务已不在任何 Section 时检查工作上下文实体中任务的排序必要时生成removal 替代 工作上下文状态补偿section-conflict-commutativity.util.tsRemovalSECTION_REMOVE_TASK校验 Section 与声明的工作上下文匹配、任务排序无歧义后按当前锚点重建 removal并附上工作上下文状态补偿section-conflict-commutativity.util.ts。替换排序的三种作用域替代操作排序由SectionReplayOrderscopeposition控制作用域只有三类section-order:contextId—— Section 顺序section-tasks:contextType:contextId:sectionId—— Section 内任务顺序work-context-tasks:contextType:contextId—— 工作上下文任务顺序见 section-conflict-commutativity.util.ts 的getReplayScope。最终这些替代操作按scope字典序 →position→originalIndex排序后批量追加superseded-operation-resolver.service.ts保证不同 scope 间顺序稳定可复现。合并递增时钟客户端绝不提前剪枝所有替代/补偿操作都使用mergeAndIncrementClocks()生成的合并递增时钟它同时主导dominate被拒绝操作的 frontier 与保留操作的 frontier。源码中有一条醒目的注释说明为什么客户端不能剪枝Dont prune here — the server prunes AFTER conflict detection (before storage). Client-side pruning would drop entity clock IDs when the merged clock exceeds MAX_VECTOR_CLOCK_SIZE, causing the comparison to return CONCURRENT instead of GREATER_THAN → infinite rejection loop.见 superseded-operation-resolver.service.ts删除类操作的处理路径见第 465-466 行同样的说明。即客户端合并后必须原样保留完整时钟交给服务端做冲突检测由服务端在检测之后、存储之前剪枝否则会陷入“反复被拒、无限循环”的拒绝死循环。时钟机制的背景可参考 vector-clocks.md。原子性单事务完成替换与拒绝resolver 在同一个操作日志事务内完成两件事追加所有替代/补偿操作source 为local拒绝它们的过时前驱。对应源码为一次appendMixedSourceBatchSkipDuplicates()调用通过{ rejectOpIds: opsToReject }参数原子完成superseded-operation-resolver.service.ts。契约强调一次崩溃绝不能只暴露恢复的一半。A crash must not expose only one half of the recovery.单元测试中的expectAtomicRejection辅助函数正是为此而写断言最新一次appendMixedSourceBatchSkipDuplicates调用携带了全部rejectOpIds且没有走旧的逐条markRejected路径superseded-operation-resolver.service.spec.ts。整个解析过程还包裹在LOCK_NAMES.OPERATION_LOG锁内superseded-operation-resolver.service.ts防止冲突恢复期间用户操作写入带已取代时钟的操作造成数据损坏。恢复完成后通过syncConflictBanner.maybeShowSummaryBanner()SPAP-15基于 journal 驱动的汇总横幅向用户呈现结果。已发布客户端兼容v18.4.0–v18.4.3 窗口SECTION 重放还承担一项向后兼容责任。处于v18.4.0–v18.4.3兼容窗口内的客户端能理解schema-4 的 SECTION removal操作但会忽略后来新增的工作上下文锚点字段workContextAfterTaskId等。因此一次语义上的 removal 在需要时必须配对一份完整的 Project/Tag LWW 替换complete Project/Tag LWW replacement让旧客户端现有 reducer 也能应用并收敛任务排序。这正是投影结果中work-context-state与stateCompensation的来源——源码中createStateCompensation()的注释明确写了这一兼容理由section-conflict-commutativity.util.ts。契约同时给出两条硬性约束不要用 schema bump 代替这个补偿机制Do not use a schema bump as a substitute for this compensation任何对替代 payload 的改动都必须对照已发布客户端舰队规则核查即 operation-log-architecture.md 的 Bump Policy。后者之所以重要是因为该 Bump Policy 是规范性的版本号提升只能围栏“提升之后”发布的接收端保护不了已发布的旧客户端。截至文档所述时间2026-07v17.0.0–v18.14.0 客户端对 schema 5 以内的操作不迁移直接应用而 v18.14.0 之后的新接收端会硬阻断新 schema 并冻结游标——因此任何替换 payload 变更都必须以“旧客户端可优雅降级”为前提而不是指望一次 bump 兜底。验证单元测试与真实客户端收敛 E2E聚焦单元测试套件运行被取代操作解析器的聚焦单测套件npm run test:file src/app/op-log/sync/superseded-operation-resolver.service.spec.ts该套件共 2765 行覆盖的关键断言包括SECTION placement 处理的四种组合跨 Section 移动、带工作上下文重排的 removal、section order、以及“移动被 section order 挡在后面”的交叉superseded-operation-resolver.service.spec.ts可交换时生成SECTION_REMOVE_TASK/SECTION_ADD_TASK替代操作不可交换时保持 LWW 回退第 1145、1187、1243、1493 行等无关保留的非 SECTION 操作不会触发安全重放第 1517 行参数化的畸形 SECTION 操作一律走回退第 1962 行起。真实客户端收敛 E2E通过定时 SuperSync E2E 工作流运行真实客户端收敛场景本地具备专用服务端环境时也可直接运行npm run e2e:file e2e/tests/sync/supersync-section-convergence.spec.ts -- --retries0该 E2Ee2e/tests/sync/supersync-section-convergence.spec.ts构造了并发移动、移除、重排与依赖放置同时发生的最坏场景三个模拟客户端 A、B、C 接入同一 SuperSync 账户createSimulatedClientsetupSuperSync种子状态Left/Right/Third 三个 TODAY 上下文的 SectionMoving 任务在 Left、Right 锚点在 Right、三个 Placement 任务在主列表客户端 A 并发发起Moving 从 Left 移到 Right[Section] Add Task to Section带sourceSectionId、三个 Placement 放入 Third客户端 B 并发发起从 Left 移除 Moving[Section] Remove Task from Section以及 TODAY 的[Section] Update Section OrderRight/Third/Left交替syncAndWait后断言A、B 两端快照完全一致movingSectionMemberships: 1、movingTodayOccurrences: 1且任务 DOM 中 Moving 各只出现一次两端page.reload()重启后再断言收敛快照不变最后新加入客户端 C同步两次后同样收敛到同一快照。也就是说E2E 必须持续证明并发移动、移除、重排和依赖放置在多个真实客户端之间收敛且重启后依然保持The E2E must continue to prove concurrent move, removal, reorder, and dependent placements converge and survive restart。小结SECTION Conflict Replay 是 Super Productivity 同步正确性体系中最能体现“窄例外”工程原则的契约它只为两类可交换交叉开放语义重放用“五条件准入 状态投影四结果 单事务原子恢复”取代通用实体 LWW同时通过工作上下文状态补偿照顾 v18.4.0–v18.4.3 旧客户端并明确禁止用 schema bump 替代补偿。若你需要修改 SECTION 冲突恢复行为请以本文所述四个可执行文件为准绳先改 section-conflict-commutativity.util.ts 的识别与投影逻辑再在 superseded-operation-resolver.service.spec.ts 中补充单测覆盖最后用 supersync-section-convergence.spec.ts 证明真实多端收敛与重启保持任何对替代 payload 的改动都需对照 Bump Policy 检查已发布客户端兼容性。【免费下载链接】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),仅供参考
返回列表