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:
+2
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user