feat: continue tree rust family cutover
- add rust renderer/state-family scaffolds and inline compat host thinning for page tree, file tree, and picker - route tree/filetree preflight, file projection, resource artifact, and stream delta contracts through rust plans - preserve canonical move-order validation, file-tree search projection, and related frontend/runtime regression coverage
This commit is contained in:
@@ -13,7 +13,6 @@ import {
|
||||
documentBridgeErrorResponse,
|
||||
} from "@/lib/documents/bridge";
|
||||
import {
|
||||
recordBridgeCommandArtifacts,
|
||||
recordBridgeCommandFailureArtifacts,
|
||||
} from "@/lib/documents/bridge-log";
|
||||
import {
|
||||
@@ -24,6 +23,7 @@ import { buildDocumentSavePayload } from "@/lib/documents/save-contract";
|
||||
import { loadSidebarDataFromConvex } from "@/lib/server/sidebar-data";
|
||||
import {
|
||||
executeRustBridgeMutationTransport,
|
||||
recordRustBridgeCommandArtifacts,
|
||||
resolveRustBridgeCommandPlan,
|
||||
} from "@/lib/documents/rust-runtime";
|
||||
|
||||
@@ -57,6 +57,26 @@ type TreeCommandPayload = {
|
||||
items?: TreeCopyItem[] | null;
|
||||
};
|
||||
|
||||
type TreeDeltaDocument = {
|
||||
id: string;
|
||||
workspace_id: string;
|
||||
title: string | null;
|
||||
parent_id: string | null;
|
||||
sort_order: number | null;
|
||||
is_starred: boolean | null;
|
||||
access_scope: "private" | "shared" | "public";
|
||||
is_template: boolean;
|
||||
created_at: string;
|
||||
updated_at: string | null;
|
||||
};
|
||||
|
||||
type TreeMutationResult<TResult> = {
|
||||
context: Awaited<ReturnType<typeof buildDocumentBridgeContext>>;
|
||||
envelope: ReturnType<typeof buildDocumentCommandEnvelope>;
|
||||
plan: Awaited<ReturnType<typeof resolveRustBridgeCommandPlan>>;
|
||||
result: TResult;
|
||||
};
|
||||
|
||||
function trimOrNull(value: unknown) {
|
||||
if (typeof value !== "string") return null;
|
||||
const trimmed = value.trim();
|
||||
@@ -76,37 +96,23 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function attachStreamDelta(commandPayload: unknown, streamDelta?: Record<string, unknown> | null) {
|
||||
if (!streamDelta) {
|
||||
return commandPayload;
|
||||
}
|
||||
if (isRecord(commandPayload)) {
|
||||
return {
|
||||
...commandPayload,
|
||||
streamDelta,
|
||||
};
|
||||
}
|
||||
return {
|
||||
payload: commandPayload,
|
||||
streamDelta,
|
||||
};
|
||||
}
|
||||
|
||||
async function recordTreeCommandSuccess(args: {
|
||||
context: Awaited<ReturnType<typeof buildDocumentBridgeContext>>;
|
||||
envelope: ReturnType<typeof buildDocumentCommandEnvelope>;
|
||||
client: Awaited<ReturnType<typeof getAuthedConvexClient>>["client"];
|
||||
commandPayload?: unknown;
|
||||
plan: Awaited<ReturnType<typeof resolveRustBridgeCommandPlan>>;
|
||||
result: unknown;
|
||||
}) {
|
||||
try {
|
||||
await recordBridgeCommandArtifacts({
|
||||
await recordRustBridgeCommandArtifacts({
|
||||
context: args.context,
|
||||
envelope: args.envelope,
|
||||
client: args.client,
|
||||
commandPayload: args.commandPayload,
|
||||
plan: args.plan,
|
||||
result: args.result,
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn("[tree.commands] bridge success artifacts skipped:", error);
|
||||
console.warn("[tree.commands] Rust bridge success artifacts skipped:", error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,24 +142,6 @@ async function loadTreeCommandSidebarSnapshot(args: {
|
||||
}
|
||||
}
|
||||
|
||||
function buildTreeCommandSnapshotDelta(
|
||||
sidebarSnapshot: unknown,
|
||||
): Record<string, unknown> | null {
|
||||
if (!isRecord(sidebarSnapshot)) {
|
||||
return null;
|
||||
}
|
||||
if (Array.isArray(sidebarSnapshot.documents)) {
|
||||
return {
|
||||
op: "replace_documents",
|
||||
documents: sidebarSnapshot.documents,
|
||||
};
|
||||
}
|
||||
return {
|
||||
op: "replace_sidebar",
|
||||
sidebar: sidebarSnapshot,
|
||||
};
|
||||
}
|
||||
|
||||
function buildTreeMovePreflightDataFromSidebarSnapshot(
|
||||
sidebarSnapshot: unknown,
|
||||
): Record<string, unknown> | null {
|
||||
@@ -203,6 +191,7 @@ async function resolveTreeMutationResult<TResult>(args: {
|
||||
return {
|
||||
context,
|
||||
envelope,
|
||||
plan,
|
||||
result,
|
||||
};
|
||||
} catch (error) {
|
||||
@@ -275,8 +264,9 @@ async function handleCreate(request: Request, payload: TreeCommandPayload) {
|
||||
|
||||
const documentId = trimOrNull(payload.documentId) ?? randomUUID();
|
||||
const title = normalizeTitle(payload.title);
|
||||
const { context, envelope, result } = await resolveTreeMutationResult<{
|
||||
const mutation = await resolveTreeMutationResult<{
|
||||
id: string;
|
||||
document?: TreeDeltaDocument | null;
|
||||
title: string | null;
|
||||
parent_id: string | null;
|
||||
sort_order: number | null;
|
||||
@@ -300,27 +290,15 @@ async function handleCreate(request: Request, payload: TreeCommandPayload) {
|
||||
pageId: documentId,
|
||||
client,
|
||||
});
|
||||
const { context, envelope, result } = mutation;
|
||||
|
||||
await ensureDocumentScaffold(result.id, result.title ?? title);
|
||||
await recordTreeCommandSuccess({
|
||||
context,
|
||||
envelope,
|
||||
client,
|
||||
commandPayload: attachStreamDelta(envelope.payload, {
|
||||
op: "upsert_document",
|
||||
document: {
|
||||
id: result.id,
|
||||
workspace_id: result.workspace_id,
|
||||
title: result.title ?? title,
|
||||
parent_id: result.parent_id ?? parentId,
|
||||
sort_order: result.sort_order ?? 0,
|
||||
access_scope: result.access_scope,
|
||||
is_starred: false,
|
||||
is_template: result.is_template,
|
||||
created_at: result.created_at,
|
||||
updated_at: result.updated_at,
|
||||
},
|
||||
}),
|
||||
plan: mutation.plan,
|
||||
result,
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
@@ -356,7 +334,7 @@ async function handleMove(request: Request, payload: TreeCommandPayload) {
|
||||
workspaceId,
|
||||
});
|
||||
const movePreflightData = buildTreeMovePreflightDataFromSidebarSnapshot(sidebarSnapshot);
|
||||
const { context, envelope, result } = await resolveTreeMutationResult<{
|
||||
const mutation = await resolveTreeMutationResult<{
|
||||
ok?: boolean;
|
||||
parent_id?: string | null;
|
||||
sort_order?: number | null;
|
||||
@@ -375,19 +353,13 @@ async function handleMove(request: Request, payload: TreeCommandPayload) {
|
||||
pageId: documentId,
|
||||
client,
|
||||
});
|
||||
const postMutationSidebarSnapshot = await loadTreeCommandSidebarSnapshot({
|
||||
client,
|
||||
auth,
|
||||
workspaceId,
|
||||
});
|
||||
const { context, envelope, result } = mutation;
|
||||
await recordTreeCommandSuccess({
|
||||
context,
|
||||
envelope,
|
||||
client,
|
||||
commandPayload: attachStreamDelta(
|
||||
envelope.payload,
|
||||
buildTreeCommandSnapshotDelta(postMutationSidebarSnapshot),
|
||||
),
|
||||
plan: mutation.plan,
|
||||
result,
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
@@ -426,8 +398,9 @@ async function handleRename(request: Request, payload: TreeCommandPayload) {
|
||||
|
||||
const workspaceId = trimOrNull(payload.workspaceId) ?? trimOrNull(sourceDoc.workspace_id);
|
||||
const title = assertTitle(payload.title ?? null);
|
||||
const { context, envelope, result } = await resolveTreeMutationResult<{
|
||||
const mutation = await resolveTreeMutationResult<{
|
||||
ok?: boolean;
|
||||
document?: TreeDeltaDocument | null;
|
||||
updated_at?: string | null;
|
||||
}>({
|
||||
request,
|
||||
@@ -441,18 +414,13 @@ async function handleRename(request: Request, payload: TreeCommandPayload) {
|
||||
pageId: documentId,
|
||||
client,
|
||||
});
|
||||
const { context, envelope, result } = mutation;
|
||||
await recordTreeCommandSuccess({
|
||||
context,
|
||||
envelope,
|
||||
client,
|
||||
commandPayload: attachStreamDelta(envelope.payload, {
|
||||
op: "upsert_document",
|
||||
document: {
|
||||
id: documentId,
|
||||
title,
|
||||
updated_at: result?.updated_at ?? null,
|
||||
},
|
||||
}),
|
||||
plan: mutation.plan,
|
||||
result,
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
@@ -478,8 +446,9 @@ async function handleArchive(request: Request, payload: TreeCommandPayload) {
|
||||
}
|
||||
|
||||
const workspaceId = trimOrNull(payload.workspaceId) ?? trimOrNull(sourceDoc.workspace_id);
|
||||
const { context, envelope, result } = await resolveTreeMutationResult<{
|
||||
const mutation = await resolveTreeMutationResult<{
|
||||
ok?: boolean;
|
||||
document?: TreeDeltaDocument | null;
|
||||
updated_at?: string | null;
|
||||
}>({
|
||||
request,
|
||||
@@ -492,14 +461,13 @@ async function handleArchive(request: Request, payload: TreeCommandPayload) {
|
||||
pageId: documentId,
|
||||
client,
|
||||
});
|
||||
const { context, envelope, result } = mutation;
|
||||
await recordTreeCommandSuccess({
|
||||
context,
|
||||
envelope,
|
||||
client,
|
||||
commandPayload: attachStreamDelta(envelope.payload, {
|
||||
op: "remove_document",
|
||||
documentId,
|
||||
}),
|
||||
plan: mutation.plan,
|
||||
result,
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
@@ -516,7 +484,7 @@ async function handleArchive(request: Request, payload: TreeCommandPayload) {
|
||||
}
|
||||
|
||||
async function handleRestore(request: Request, payload: TreeCommandPayload) {
|
||||
const { auth, client } = await getAuthedConvexClient();
|
||||
const { client } = await getAuthedConvexClient();
|
||||
const documentId = assertDocumentId(payload.documentId ?? null);
|
||||
const sourceDoc = await client.query(api.documents.getMeta, { id: documentId });
|
||||
if (!sourceDoc) {
|
||||
@@ -524,8 +492,9 @@ async function handleRestore(request: Request, payload: TreeCommandPayload) {
|
||||
}
|
||||
|
||||
const workspaceId = trimOrNull(payload.workspaceId) ?? trimOrNull(sourceDoc.workspace_id);
|
||||
const { context, envelope, result } = await resolveTreeMutationResult<{
|
||||
const mutation = await resolveTreeMutationResult<{
|
||||
ok?: boolean;
|
||||
document?: TreeDeltaDocument | null;
|
||||
updated_at?: string | null;
|
||||
}>({
|
||||
request,
|
||||
@@ -538,19 +507,13 @@ async function handleRestore(request: Request, payload: TreeCommandPayload) {
|
||||
pageId: documentId,
|
||||
client,
|
||||
});
|
||||
const sidebarSnapshot = await loadTreeCommandSidebarSnapshot({
|
||||
client,
|
||||
auth,
|
||||
workspaceId,
|
||||
});
|
||||
const { context, envelope, result } = mutation;
|
||||
await recordTreeCommandSuccess({
|
||||
context,
|
||||
envelope,
|
||||
client,
|
||||
commandPayload: attachStreamDelta(
|
||||
envelope.payload,
|
||||
buildTreeCommandSnapshotDelta(sidebarSnapshot),
|
||||
),
|
||||
plan: mutation.plan,
|
||||
result,
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
@@ -575,7 +538,7 @@ async function handlePurge(request: Request, payload: TreeCommandPayload) {
|
||||
}
|
||||
|
||||
const workspaceId = trimOrNull(payload.workspaceId) ?? trimOrNull(sourceDoc.workspace_id);
|
||||
const { context, envelope, result } = await resolveTreeMutationResult<{
|
||||
const mutation = await resolveTreeMutationResult<{
|
||||
ok?: boolean;
|
||||
purged?: boolean;
|
||||
purged_at?: string | null;
|
||||
@@ -589,14 +552,13 @@ async function handlePurge(request: Request, payload: TreeCommandPayload) {
|
||||
pageId: documentId,
|
||||
client,
|
||||
});
|
||||
const { context, envelope, result } = mutation;
|
||||
await recordTreeCommandSuccess({
|
||||
context,
|
||||
envelope,
|
||||
client,
|
||||
commandPayload: attachStreamDelta(envelope.payload, {
|
||||
op: "remove_document",
|
||||
documentId,
|
||||
}),
|
||||
plan: mutation.plan,
|
||||
result,
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
@@ -659,7 +621,7 @@ async function handleEmbed(request: Request, payload: TreeCommandPayload) {
|
||||
trimOrNull(sourceDoc.workspace_id) ??
|
||||
trimOrNull((targetMeta as { workspace_id?: string | null } | null)?.workspace_id);
|
||||
|
||||
const { context, envelope, result } = await resolveTreeMutationResult<{
|
||||
const mutation = await resolveTreeMutationResult<{
|
||||
revision?: number | null;
|
||||
conflict_detection_key?: string | null;
|
||||
}>({
|
||||
@@ -688,13 +650,13 @@ async function handleEmbed(request: Request, payload: TreeCommandPayload) {
|
||||
pageId: targetId,
|
||||
client,
|
||||
});
|
||||
const { context, envelope, result } = mutation;
|
||||
await recordTreeCommandSuccess({
|
||||
context,
|
||||
envelope,
|
||||
client,
|
||||
commandPayload: attachStreamDelta(envelope.payload, {
|
||||
op: "noop",
|
||||
}),
|
||||
plan: mutation.plan,
|
||||
result,
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
@@ -712,7 +674,7 @@ async function handleEmbed(request: Request, payload: TreeCommandPayload) {
|
||||
}
|
||||
|
||||
async function handleCopy(request: Request, payload: TreeCommandPayload) {
|
||||
const { auth, client } = await getAuthedConvexClient();
|
||||
const { client } = await getAuthedConvexClient();
|
||||
const normalizedItems = (payload.items ?? [])
|
||||
.filter((item) => item?.documentId)
|
||||
.map((item) => ({
|
||||
@@ -747,11 +709,12 @@ async function handleCopy(request: Request, payload: TreeCommandPayload) {
|
||||
return NextResponse.json({ error: "缺少目标工作空间" }, { status: 400 });
|
||||
}
|
||||
|
||||
const { context, envelope, result } = await resolveTreeMutationResult<{
|
||||
const mutation = await resolveTreeMutationResult<{
|
||||
items: Array<{
|
||||
oldId: string;
|
||||
newId: string;
|
||||
title?: string | null;
|
||||
document?: TreeDeltaDocument | null;
|
||||
}>;
|
||||
}>({
|
||||
request,
|
||||
@@ -765,6 +728,7 @@ async function handleCopy(request: Request, payload: TreeCommandPayload) {
|
||||
pageId: targetParentId,
|
||||
client,
|
||||
});
|
||||
const { context, envelope, result } = mutation;
|
||||
|
||||
await Promise.all(
|
||||
(result.items ?? []).map(async (item) => {
|
||||
@@ -772,19 +736,12 @@ async function handleCopy(request: Request, payload: TreeCommandPayload) {
|
||||
await copyMindmapFilesIfExists(item.oldId, item.newId);
|
||||
}),
|
||||
);
|
||||
const sidebarSnapshot = await loadTreeCommandSidebarSnapshot({
|
||||
client,
|
||||
auth,
|
||||
workspaceId,
|
||||
});
|
||||
await recordTreeCommandSuccess({
|
||||
context,
|
||||
envelope,
|
||||
client,
|
||||
commandPayload: attachStreamDelta(
|
||||
envelope.payload,
|
||||
buildTreeCommandSnapshotDelta(sidebarSnapshot),
|
||||
),
|
||||
plan: mutation.plan,
|
||||
result,
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
|
||||
Reference in New Issue
Block a user