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
+2
View File
@@ -10,6 +10,7 @@
import type * as _utils_attachmentExtract from "../_utils/attachmentExtract.js";
import type * as _utils_auth from "../_utils/auth.js";
import type * as _utils_documentMoveOrder from "../_utils/documentMoveOrder.js";
import type * as _utils_documentRecord from "../_utils/documentRecord.js";
import type * as _utils_documentTree from "../_utils/documentTree.js";
import type * as _utils_id from "../_utils/id.js";
@@ -55,6 +56,7 @@ import type {
declare const fullApi: ApiFromModules<{
"_utils/attachmentExtract": typeof _utils_attachmentExtract;
"_utils/auth": typeof _utils_auth;
"_utils/documentMoveOrder": typeof _utils_documentMoveOrder;
"_utils/documentRecord": typeof _utils_documentRecord;
"_utils/documentTree": typeof _utils_documentTree;
"_utils/id": typeof _utils_id;
@@ -21,6 +21,14 @@ export type DocumentMoveOrderPlan = {
patches: DocumentMoveOrderPatch[];
};
export type DocumentMoveWriteOperation = DocumentMoveOrderPlan & {
family: "tree";
schema: "mnote.tree.write_operation";
schemaVersion: 1;
operation: "tree.subtree.move.write";
workspaceId: string;
};
function normalizeParentId(value: string | null | undefined): string | null {
const trimmed = typeof value === "string" ? value.trim() : "";
return trimmed.length > 0 ? trimmed : null;
@@ -121,7 +129,7 @@ export function buildDocumentMoveOrderPlanFromDocuments(input: {
};
}
function normalizePlan(value: unknown): DocumentMoveOrderPlan | null {
export function normalizeDocumentMoveOrderPlan(value: unknown): DocumentMoveOrderPlan | null {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return null;
}
@@ -152,8 +160,37 @@ function normalizePlan(value: unknown): DocumentMoveOrderPlan | null {
};
}
export function assertDocumentMoveOrderPlanMatches(expected: unknown, actual: DocumentMoveOrderPlan) {
const normalizedExpected = normalizePlan(expected);
export function normalizeDocumentMoveWriteOperation(value: unknown): DocumentMoveWriteOperation | null {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return null;
}
const record = value as Partial<DocumentMoveWriteOperation>;
if (
record.family !== "tree" ||
record.schema !== "mnote.tree.write_operation" ||
record.schemaVersion !== 1 ||
record.operation !== "tree.subtree.move.write" ||
typeof record.workspaceId !== "string" ||
!record.workspaceId.trim()
) {
return null;
}
const normalizedPlan = normalizeDocumentMoveOrderPlan(value);
if (!normalizedPlan) {
return null;
}
return {
family: "tree",
schema: "mnote.tree.write_operation",
schemaVersion: 1,
operation: "tree.subtree.move.write",
workspaceId: record.workspaceId.trim(),
...normalizedPlan,
};
}
export function assertDocumentMoveOrderPlanMatches(expected: unknown, actual: DocumentMoveOrderPlan): DocumentMoveOrderPlan {
const normalizedExpected = normalizeDocumentMoveOrderPlan(expected);
if (!normalizedExpected) {
throw new Error("Rust move plan 与 Convex 当前排序状态不一致");
}
@@ -161,4 +198,21 @@ export function assertDocumentMoveOrderPlanMatches(expected: unknown, actual: Do
if (JSON.stringify(normalizedExpected) !== JSON.stringify(actual)) {
throw new Error("Rust move plan 与 Convex 当前排序状态不一致");
}
return normalizedExpected;
}
export function assertDocumentMoveWriteOperationMatches(expected: unknown, actual: DocumentMoveOrderPlan): DocumentMoveOrderPlan {
const normalizedExpected = normalizeDocumentMoveWriteOperation(expected);
if (!normalizedExpected) {
throw new Error("Rust move write operation 与 Convex 当前排序状态不一致");
}
const { family: _family, schema: _schema, schemaVersion: _schemaVersion, operation: _operation, workspaceId: _workspaceId, ...expectedPlan } =
normalizedExpected;
if (JSON.stringify(expectedPlan) !== JSON.stringify(actual)) {
throw new Error("Rust move write operation 与 Convex 当前排序状态不一致");
}
return expectedPlan;
}
+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;