feat(tree): complete rust family runtime checklist

- add tree shell runtime artifact contracts and page/filetree/picker runtime reducers
- sink tree.subtree.move write operation through Rust and formalize command event plans
- harden file tree search projection contract and route thin-proxy boundaries
- record completed harness tasks and move design docs into process/done
This commit is contained in:
lix-2026
2026-04-27 10:27:15 +08:00
parent e564dfde02
commit 4ab36a9386
30 changed files with 2502 additions and 432 deletions
+42 -11
View File
@@ -6,6 +6,7 @@ import { nowIso } from "./_utils/time";
import { buildParentById, collectSubtree, isAncestorOf } from "./_utils/documentTree";
import {
assertDocumentMoveOrderPlanMatches,
assertDocumentMoveWriteOperationMatches,
buildDocumentMoveOrderPlanFromDocuments,
} from "./_utils/documentMoveOrder";
import { enqueueIngestDocumentJob } from "./_utils/ingestJobs";
@@ -1400,6 +1401,7 @@ export const move = mutation({
parentId: v.union(v.string(), v.null()),
sortOrder: v.number(),
normalizedMove: v.optional(v.any()),
treeWriteOperation: v.optional(v.any()),
},
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
@@ -1436,23 +1438,52 @@ export const move = mutation({
}
}
if (args.normalizedMove != null) {
assertDocumentMoveOrderPlanMatches(
args.normalizedMove,
buildDocumentMoveOrderPlanFromDocuments({
documents: canonicalWorkspaceDocs,
documentId: doc.id,
parentId: toParentId,
sortOrder: args.sortOrder,
}),
);
}
const currentMoveOrderPlan = buildDocumentMoveOrderPlanFromDocuments({
documents: canonicalWorkspaceDocs,
documentId: doc.id,
parentId: toParentId,
sortOrder: args.sortOrder,
});
const rustMoveOrderPlan =
args.treeWriteOperation != null
? assertDocumentMoveWriteOperationMatches(args.treeWriteOperation, currentMoveOrderPlan)
: args.normalizedMove != null
? assertDocumentMoveOrderPlanMatches(args.normalizedMove, currentMoveOrderPlan)
: null;
// 说明:仅更新当前节点的 sort_order 会导致兄弟节点出现重复 sort_order
// 前端会按 sort_order + created_at 排序,结果常常表现为“拖拽无效/拖完又回弹”。
// 这里统一对目标父节点(以及跨父移动时的原父节点)做重新编号,保证 sort_order 唯一且连续。
const ts = nowIso();
if (rustMoveOrderPlan != null) {
const documentsById = new Map(canonicalWorkspaceDocs.map((item) => [item.id, item]));
for (const item of rustMoveOrderPlan.patches) {
const target = documentsById.get(item.documentId);
if (!target) {
throw new Error("Rust move plan 与 Convex 当前排序状态不一致");
}
const patch: Record<string, unknown> = {};
if ((target.parent_id ?? null) !== item.parentId) patch.parent_id = item.parentId;
if ((target.sort_order ?? null) !== item.sortOrder) patch.sort_order = item.sortOrder;
// Rust plan 决定被移动节点;Convex 只负责把同一结果持久化。
if (item.moved) patch.updated_at = ts;
if (Object.keys(patch).length > 0) {
await ctx.db.patch(target._id, patch);
}
}
return {
ok: true,
parent_id: rustMoveOrderPlan.toParentId,
sort_order: rustMoveOrderPlan.normalizedSortOrder,
workspace_id: doc.workspace_id,
updated_at: ts,
};
}
const compareDocOrder = (a: any, b: any) => {
const orderA = typeof a.sort_order === "number" ? a.sort_order : Number.MAX_SAFE_INTEGER;
const orderB = typeof b.sort_order === "number" ? b.sort_order : Number.MAX_SAFE_INTEGER;