docs(riir): M13 final — node-granular journal (diff-produced), snapshots, persistent undo

This commit is contained in:
2026-08-16 01:19:41 +08:00
parent 78d8073c89
commit 530bc762dc
+93 -83
View File
@@ -6,119 +6,129 @@
>
> 约束(用户已定):数据库同时支持 PostgreSQL 与 SQLitesea-orm);
> **所有改动实时写数据库,无保存按钮**;数据库为默认后端;工程可在
> 数据库与 .ove / .otio / .fcpxml 之间导入导出
> 数据库与 .ove / .otio / .fcpxml 之间导入导出**撤销历史持久化**
> (无限撤销)。
## 0. 形态决策
## 0. 形态决策(定稿)
- **默认后端 = 数据库**。本地默认 SQLite`oakdb+sqlite:///…/library.db`
用户级单一库文件);`oakdb+pg://…` 连接串接 PostgreSQL(团队/服务器场景)。
.ove 降级为**导入/导出格式**(交换与备份),不再是日常存储。
- **写穿(write-through),没有保存动作**。每一条 undoable 命令
redo 成功即触发持久化;undo/redo/jump 同样落库(历史即数据)。
- **单一序列化事实**:落库 payload 与 .ove 同源(oaknode serializer
全特性 XML,S1 已完成)。数据库不另建节点级关系表——节点级查询
不是需求,保持 schema 小而稳
- 接口边界:app/facade 只认识 oakstorage 的会话 APIRust 直调;
facade `oakengine_storage_*` 仅当 app 需要 C ABI 时按需新增,只增不改)。
- **按聚合粒度数据库**,不做全关系型。库只管四件事:工程元信息、
KV 设置、加载加速快照、节点粒度的命令日志。领域语义(时间线结构、
连接、关键帧、效果链)全部留在节点 XML 里——**工程状态 = 节点图 +
settings,没有第三种东西**,因此节点粒度是封闭全集。
- **journal 由 diff 产生,不由命令申报**。每条命令 redo 后把工程在
内存里重新序列化(S1 的 save 按节点吐 XML),与上一版逐节点比对,
变化/新增/删除的节点各落一行。零命令侵入:现有与未来的命令类型
(含调整图层)自动覆盖,正确性与序列化器共用同一事实源
- **journal 同时就是持久化撤销历史**:回放 = 快照 + 按 seq 顺序把
identity 为 X 的节点整个替换成 new_xml;撤销到任意点 = 逆序回写
old_xml。old/new_xml 都是整节点 XML,定位靠 node_identity 主键,
不碰 XML 内部。
- **快照只加速加载**:默认每 600s(`Storage/SnapshotIntervalSec`
可配)脏状态下落一份全量 XML;定期清理只留最近 3 份。快照损坏也
能从空工程 + 全 journal 重建。
- **崩溃恢复**journal 同步写 ⇒ kill -9 最坏丢 0 条命令。
- 默认后端 = 数据库(本地 SQLite 单库文件;PG 用连接串)。.ove 降级
为导入/导出格式。
## 1. SchemaPG/SQLite 同构,sea-orm migration 管理)
## 1. SchemaSQLite / PG 同构,sea-orm migration 管理)
```
```sql
projects(
id BIGSERIAL/INTEGER PK,
uuid TEXT UNIQUE NOT NULL, -- 工程 uuid(序列化器同值)
name TEXT NOT NULL,
created_at TIMESTAMP NOT NULL,
modified_at TIMESTAMP NOT NULL,
schema_ver INTEGER NOT NULL -- payload 格式版本(=serializer CURRENT_VERSION
id INTEGER PRIMARY KEY, -- PG: BIGSERIAL
uuid TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
schema_ver INTEGER NOT NULL, -- serializer CURRENT_VERSION
created_at TIMESTAMP NOT NULL,
modified_at TIMESTAMP NOT NULL,
command_seq BIGINT NOT NULL DEFAULT 0 -- 当前头的命令序号
)
snapshots(
project_id BIGINT PK REFERENCES projects(id) ON DELETE CASCADE,
payload TEXT NOT NULL, -- 全特性 XMLgzip 预留:PG BYTEA / SQLite BLOB 备选)
command_seq BIGINT NOT NULL, -- 该快照对应的命令序号
written_at TIMESTAMP NOT NULL
settings(
project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
key TEXT NOT NULL,
value TEXT NOT NULL,
PRIMARY KEY (project_id, key)
)
journal(
project_id BIGINT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
seq BIGINT NOT NULL, -- 单调命令序号(每工程)
kind TEXT NOT NULL, -- 'redo' | 'undo' | 'jump'
payload TEXT NOT NULL, -- 该命令后的全量 XML(见 §2 取舍
written_at TIMESTAMP NOT NULL,
PRIMARY KEY (project_id, seq)
snapshots( -- 周期全量快照(定期清理)
project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
command_seq BIGINT NOT NULL,
payload TEXT NOT NULL, -- 全特性 XML(与 .ove 同源
written_at TIMESTAMP NOT NULL,
PRIMARY KEY (project_id, command_seq)
)
project_meta( -- 项目管理器用的派生数据,冗余可重建
project_id BIGINT PK REFERENCES projects(id) ON DELETE CASCADE,
duration_ms BIGINT, track_count INT, clip_count INT, footage_count INT
journal( -- 命令日志:每命令每受影响节点一行
project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
seq BIGINT NOT NULL,
node_identity BIGINT NOT NULL, -- 0 = settings 伪节点
kind TEXT NOT NULL, -- 'redo'|'undo'|'jump'|'group'|'import'
old_xml TEXT, -- 命令前像(undo 回放;新增为 NULL)
new_xml TEXT, -- 命令后像(redo 回放;删除为 NULL)
at TIMESTAMP NOT NULL,
PRIMARY KEY (project_id, seq, node_identity)
)
```
- 全部经 sea-orm `DeriveEntityModel` + migrationSQLite 开 WAL。
- 连接配置走 oakcommon config`Storage/Backend``Storage/SqlitePath`
`Storage/PgUrl`),偏好设置面板可改。
- settings 变更以 `node_identity = 0` 的伪节点行入 journalpayload 为
变更键的 KV 片段。
- 项目管理器统计(轨道/片段/素材数)打开工程时从节点图派生,
不落库。
- journal 保留窗口:`Storage/JournalRetentionDays`(默认 0 = 全保留;
行均 KB 级,全保留量级可接受)。窗口外撤销历史失效(文档注明)。
## 2. 写穿策略(核心取舍)
## 2. 运转方式
候选 A:每条命令写全量 payload——语义最简单、恢复即最新行,
代价是每次编辑 O(工程大小)。候选 B:命令级增量日志——写入最小,
但要求每条 undo 命令可序列化(当前不具备,工作量大且易错)
- **写穿**redo/undo/jump 成功后一个事务:diff 出的受影响节点各行
+ `command_seq + 1` + `modified_at`。组命令在 `group_end` 提交一次;
`group_abort` 不写
- **快照**:后台线程,latest-wins,不卡 UI;写后清理旧快照(留 3 份)。
- **加载**:最新快照(无则空工程)+ 其后 journal 按 seq 重放 new_xml
→ 拼装 `<project>` 交给 serializer::load。
- **导入**.ove/.otio/.fcpxml → 库):解析进内存工程 → 一个事务写
`kind='import'` 的全节点 journal 行(seq=1)。**导出**反向走内存
序列化,不落库。
- **退出**:无保存;退出前等快照线程 flush(latest-wins 队列排空)。
**定案:A 的改良型——同步小写 + 异步大写**
## 3. 写穿接入点
1. redo/undo/jump 成功后,**同步**写 journal 一行(kind + 全量 payload
全量保证任意 seq 点可恢复,不依赖命令可序列化)
2. snapshots 由后台写线程 latest-wins 合并(每 ~2s 或有新写入即落),
journal 按 N=500 条或快照成功后截断
3. 打开工程 = snapshots.payload + 其后的 journal 末行(二者同源,
取 command_seq 较大者)。
4. 崩溃恢复:journal 永远完整 ⇒ 最坏损失 0 条命令(同步写)。
5. 大工程(>10MB payload)同步写仍可能卡 UI——届时把 journal 写入
也挪到写线程但保留"未落盘即禁止退出"闸(退出前 flush)。v1 先
全同步测性能,超 50ms 的写记入日志备查。
## 3. 实时性的接入点
不侵入 oaknode/oaktimeline:在 **facade 的 undo 推送路径**挂钩
`oakengine_undo_push` / `undo_group_end` / `undo` / `redo` / `jump`
成功后)通知当前工程会话 → 写穿。新建/导入工程时建立
(project ↔ storage session) 绑定;关闭工程解绑。app 不再出现
"保存/另存为脏标记"语义——状态栏的修改标记改为"已写入/写入中"。
facade 的 undo 推送路径挂钩(`oakengine_undo_push` / `undo_group_end`
/ `undo` / `redo` / `jump` 成功后)→ 当前工程会话执行 §2 写穿
新建/导入建立 (project ↔ storage session) 绑定;关闭解绑。状态栏
脏标记改为"已写入/写入中"
## 4. 项目管理器窗口(app
达芬奇式启动窗 + 菜单 文件→项目管理器:
- 列表:name、modified_at、时长/轨道数(project_meta、双击打开。
- 新建(命名即建库行并打开)、重命名、复制、删除(确认对话框)。
- 导入.ove / .otio / .fcpxml 新库行(走既有后端解析后转库)
- 导出:选中工程 → .ove / .otio / .fcpxml(经 serializer/oakotio)。
- 启动行为:有库则显示管理器;`--project <path|oakdb-uri>` 或双击
进入主界面。gpui 实现,数据源走 oakstorage 会话 APIRealEngine
加 project-store 面)。
- 列表:name、modified_at、时长/轨道/片段/素材统计、双击打开。
- 新建 / 重命名 / 复制 / 删除(确认对话框)。
- 导入 .ove/.otio/.fcpxml 新库行;选中工程导出为 .ove/.otio/.fcpxml
- 数据源走 oakstorage 会话 APIRust 直调;需要 C ABI 时 facade 只增)。
## 5. 分期与判据
| 期 | 内容 | 完成判据(可命令验证) |
|----|------|------------------------|
| D1 | sea-orm schema + SQLite 后端(load/save/list/delete/duplicate/rename/exportjournal+snapshot 读写 | `cargo test -p oakstorage`SQLite 库 round-trip 全特性字段比对、journal 截断/恢复测试、meta 派生正确 |
| D2 | 写穿挂钩(facade undo 路径)+ 异步写线程 + 退出 flush | 编辑工程→进程直接 kill -9重开恢复到最后一条命令(集成测试) |
| D3 | PG 后端(同 schema+ 连接配置 | `OAK_TEST_PG_URL` 存在时 PG 测试全绿;不存在自动 ignoreSQLite 测试不受 PG 影响 |
| D4 | 项目管理器窗口 + 导入导出 + 启动接线 | app 测试:建/删/复制/重命名/导入 .ove/导出 .ove;两张截图(中英)入 docs |
| D5 | .ove 手动保存语义退役(菜单改"导入/导出",脏标记改写入状态)+ M10/M12 文档更新 | 全量测试绿docs 更新 |
| 期 | 内容 | 完成判据 |
|----|------|----------|
| D1 | sea-orm schema + SQLite 后端(save/load/快照/重放/截断/list/delete/duplicate/rename/export | `cargo test -p oakstorage`全特性 round-trip 字段比对、快照+journal 重放、撤销到任意点、截断、管理 API、导入导出 |
| D2 | diff 写穿挂钩(facade undo 路径)+ 快照线程 + 退出 flush | 编辑工程 → kill -9重开恢复到最后一条命令;撤销历史跨会话可用(集成测试) |
| D3 | PG 后端(同 schema+ 连接配置 | `OAK_TEST_PG_URL` 存在时 PG 测试全绿,否则自动 ignore |
| D4 | 项目管理器窗口 + 导入导出 + 启动接线 | app 测试:建/删/复制/重命名/导入/导出;中英截图入 docs |
| D5 | .ove 手动保存语义退役(菜单改导入/导出,脏标记改写入状态)+ 文档更新 | 全量测试绿 |
依赖:D1→D2→D3 顺序D4 可在 D1 后并行(窗口用 SQLiteD5 最后。
依赖:D1→D2→D3;D4 可在 D1 后并行;D5 最后。
## 6. 风险与对策
| 风险 | 对策 |
|------|------|
| 全量 payload 同步写大工程慢 | §2.5 的写线程化退路;journal 截断阈值可配 |
| sea-orm async 同步 facade 的阻抗 | 后端内嵌私有 current_thread runtimeCargo.toml 已预留 tokio rt |
| 大工程每命令一次内存序列化 | 微秒-低毫秒级;超阈值记日志,必要时增量序列化优化 |
| sea-orm async 同步 facade | 后端内嵌私有 current_thread runtime |
| PG 测试依赖外部服务 | env 门控 ignoreCI 先只跑 SQLite |
| 写穿与 undo 语义错位(组命令) | group_end 才算一条 journalabort 不写 |
| 旧 .ove 用户数据 | 首次启动提示导入;.ove 读写后端永久保留 |
| 多实例同库并发 | v1 单写者假设 + `PRAGMA busy_timeout`/PG 行锁;多写者留待 M14 |
| 多实例同库并发 | v1 单写者(busy_timeout/PG 行锁);多写者 M14 |
| 快照线程与退出竞态 | 退出前 flush 闸门 |
## 7. 不在本期
- 节点级关系表与库内查询、多写者协作、云同步、payload 压缩/差分、
命令级增量序列化
- 全关系型节点表、命令级语义序列化、多写者协作、云同步、payload
压缩、库内节点级查询