feat: 完成 rust cutover phase 8 收口
This commit is contained in:
@@ -0,0 +1,333 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { api } from "@/lib/convex/api";
|
||||
import { getAuthedConvexClient } from "@/lib/convex/route";
|
||||
import {
|
||||
buildDocumentBridgeContext,
|
||||
buildDocumentCommandEnvelope,
|
||||
buildDocumentQueryEnvelope,
|
||||
documentBridgeErrorResponse,
|
||||
type BridgeContext,
|
||||
} from "@/lib/documents/bridge";
|
||||
import {
|
||||
recordBridgeCommandArtifacts,
|
||||
recordBridgeCommandFailureArtifacts,
|
||||
} from "@/lib/documents/bridge-log";
|
||||
import {
|
||||
executeRustBridgeMutationTransport,
|
||||
executeRustBridgeQueryTransport,
|
||||
resolveRustBridgeCommandPlan,
|
||||
resolveRustBridgeQueryPlan,
|
||||
} from "@/lib/documents/rust-runtime";
|
||||
import { composeContentWithBlocks, extractBlocksFromContent } from "@/lib/document-content";
|
||||
|
||||
type BlockLike = {
|
||||
id: string;
|
||||
type?: string;
|
||||
props?: Record<string, unknown>;
|
||||
content?: unknown;
|
||||
children?: BlockLike[];
|
||||
};
|
||||
|
||||
function cloneBlock(block: BlockLike): BlockLike {
|
||||
return {
|
||||
...block,
|
||||
props: block.props ? { ...block.props } : undefined,
|
||||
content: Array.isArray(block.content) ? [...block.content] : block.content,
|
||||
children: Array.isArray(block.children) ? block.children.map((item) => cloneBlock(item)) : block.children,
|
||||
};
|
||||
}
|
||||
|
||||
function findBlockInTree(
|
||||
blocks: BlockLike[],
|
||||
blockId: string,
|
||||
): { block: BlockLike; parent: BlockLike | null; index: number } | null {
|
||||
const stack: Array<{ list: BlockLike[]; parent: BlockLike | null }> = [{ list: blocks, parent: null }];
|
||||
while (stack.length > 0) {
|
||||
const current = stack.pop();
|
||||
if (!current) continue;
|
||||
for (let i = 0; i < current.list.length; i += 1) {
|
||||
const block = current.list[i]!;
|
||||
if (block.id === blockId) {
|
||||
return { block, parent: current.parent, index: i };
|
||||
}
|
||||
if (Array.isArray(block.children) && block.children.length > 0) {
|
||||
stack.push({ list: block.children, parent: block });
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function removeBlockSubtree(blocks: BlockLike[], blockId: string) {
|
||||
const nextTop = blocks.map((item) => cloneBlock(item));
|
||||
const hit = findBlockInTree(nextTop, blockId);
|
||||
if (!hit) {
|
||||
return { removed: null as BlockLike | null, nextBlocks: nextTop };
|
||||
}
|
||||
if (hit.parent) {
|
||||
const nextChildren = Array.isArray(hit.parent.children) ? hit.parent.children.map((item) => cloneBlock(item)) : [];
|
||||
const removed = nextChildren.splice(hit.index, 1)[0] ?? null;
|
||||
hit.parent.children = nextChildren;
|
||||
return { removed, nextBlocks: nextTop };
|
||||
}
|
||||
const removed = nextTop.splice(hit.index, 1)[0] ?? null;
|
||||
return { removed, nextBlocks: nextTop };
|
||||
}
|
||||
|
||||
function replaceBlockInTree(blocks: BlockLike[], blockId: string, nextBlock: unknown) {
|
||||
const nextTop = blocks.map((item) => cloneBlock(item));
|
||||
const hit = findBlockInTree(nextTop, blockId);
|
||||
if (!hit || !nextBlock || typeof nextBlock !== "object" || Array.isArray(nextBlock)) {
|
||||
return { ok: false, nextBlocks: nextTop };
|
||||
}
|
||||
const normalized = cloneBlock({ ...(nextBlock as BlockLike), id: blockId });
|
||||
if (hit.parent) {
|
||||
const nextChildren = Array.isArray(hit.parent.children) ? hit.parent.children.map((item) => cloneBlock(item)) : [];
|
||||
nextChildren[hit.index] = normalized;
|
||||
hit.parent.children = nextChildren;
|
||||
return { ok: true, nextBlocks: nextTop };
|
||||
}
|
||||
nextTop[hit.index] = normalized;
|
||||
return { ok: true, nextBlocks: nextTop };
|
||||
}
|
||||
|
||||
function buildReferenceBlock(sourceDocumentId: string, blockId: string): BlockLike {
|
||||
return {
|
||||
id: randomUUID(),
|
||||
type: "blockReference",
|
||||
props: {
|
||||
sourceDocumentId,
|
||||
targetBlockId: blockId,
|
||||
display: "embed",
|
||||
},
|
||||
content: [],
|
||||
children: [],
|
||||
};
|
||||
}
|
||||
|
||||
async function buildBridgeContext(request: Request, workspaceId: string | null): Promise<BridgeContext> {
|
||||
return await buildDocumentBridgeContext({ request, workspaceId });
|
||||
}
|
||||
|
||||
export async function executeBlockGetBridgeQuery(input: {
|
||||
request: Request;
|
||||
sourceDocumentId: string;
|
||||
blockId: string;
|
||||
}) {
|
||||
const context = await buildBridgeContext(input.request, null);
|
||||
const envelope = buildDocumentQueryEnvelope({
|
||||
name: "blocks.get",
|
||||
payload: {
|
||||
sourceDocumentId: input.sourceDocumentId,
|
||||
blockId: input.blockId,
|
||||
},
|
||||
});
|
||||
const plan = await resolveRustBridgeQueryPlan({ context, envelope });
|
||||
const { client } = await getAuthedConvexClient();
|
||||
const doc = await executeRustBridgeQueryTransport<{ content?: unknown } | null>({
|
||||
client,
|
||||
plan,
|
||||
});
|
||||
if (!doc) {
|
||||
throw new Error("页面不存在或无权限");
|
||||
}
|
||||
const blocks = extractBlocksFromContent(doc.content) as BlockLike[];
|
||||
const hit = findBlockInTree(blocks, input.blockId);
|
||||
if (!hit) {
|
||||
throw new Error("块不存在或无权限");
|
||||
}
|
||||
return {
|
||||
requestId: context.requestId,
|
||||
traceId: context.traceId,
|
||||
queryName: envelope.name,
|
||||
result: { block: hit.block },
|
||||
};
|
||||
}
|
||||
|
||||
export async function executeBlockPatchBridgeCommand(input: {
|
||||
request: Request;
|
||||
sourceDocumentId: string;
|
||||
workspaceId: string | null;
|
||||
blockId: string;
|
||||
nextBlock: unknown;
|
||||
}) {
|
||||
const context = await buildBridgeContext(input.request, input.workspaceId);
|
||||
const envelope = buildDocumentCommandEnvelope({
|
||||
name: "blocks.patch",
|
||||
payload: {
|
||||
documentId: input.sourceDocumentId,
|
||||
workspaceId: input.workspaceId,
|
||||
blockId: input.blockId,
|
||||
nextBlock: input.nextBlock,
|
||||
},
|
||||
context,
|
||||
target: {
|
||||
workspaceId: input.workspaceId,
|
||||
pageId: input.sourceDocumentId,
|
||||
blockId: input.blockId,
|
||||
},
|
||||
});
|
||||
const plan = await resolveRustBridgeCommandPlan({ context, envelope });
|
||||
const { client } = await getAuthedConvexClient();
|
||||
const doc = await client.query(api.documents.getContent, { id: input.sourceDocumentId });
|
||||
if (!doc) {
|
||||
throw new Error("页面不存在或无权限");
|
||||
}
|
||||
const blocks = extractBlocksFromContent(doc.content) as BlockLike[];
|
||||
const replaced = replaceBlockInTree(blocks, input.blockId, input.nextBlock);
|
||||
if (!replaced.ok) {
|
||||
throw new Error("块不存在或无权限");
|
||||
}
|
||||
try {
|
||||
await executeRustBridgeMutationTransport({
|
||||
client,
|
||||
plan,
|
||||
});
|
||||
await client.mutation(api.documents.updateContent, {
|
||||
id: input.sourceDocumentId,
|
||||
content: composeContentWithBlocks(doc.content, replaced.nextBlocks as never),
|
||||
});
|
||||
await recordBridgeCommandArtifacts({ context, envelope });
|
||||
} catch (error) {
|
||||
await recordBridgeCommandFailureArtifacts({
|
||||
client,
|
||||
context,
|
||||
envelope,
|
||||
error,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
return {
|
||||
requestId: context.requestId,
|
||||
traceId: context.traceId,
|
||||
commandId: envelope.commandId,
|
||||
commandName: envelope.name,
|
||||
result: { ok: true as const },
|
||||
};
|
||||
}
|
||||
|
||||
export async function executeBlockMoveBridgeCommand(input: {
|
||||
request: Request;
|
||||
sourceDocumentId: string;
|
||||
targetDocumentId: string;
|
||||
blockId: string;
|
||||
}) {
|
||||
const { client } = await getAuthedConvexClient();
|
||||
const source = await client.query(api.documents.getContent, { id: input.sourceDocumentId });
|
||||
if (!source) throw new Error("源页面不存在或无权限");
|
||||
const target = await client.query(api.documents.getContent, { id: input.targetDocumentId });
|
||||
if (!target) throw new Error("目标页面不存在或无权限");
|
||||
const sourceBlocks = extractBlocksFromContent(source.content) as BlockLike[];
|
||||
const removedRes = removeBlockSubtree(sourceBlocks, input.blockId);
|
||||
if (!removedRes.removed) throw new Error("源块不存在或无权限");
|
||||
const targetBlocks = extractBlocksFromContent(target.content) as BlockLike[];
|
||||
const nextSourceContent = composeContentWithBlocks(source.content, removedRes.nextBlocks as never);
|
||||
const nextTargetContent = composeContentWithBlocks(target.content, [...targetBlocks, removedRes.removed] as never);
|
||||
const context = await buildBridgeContext(input.request, null);
|
||||
const envelope = buildDocumentCommandEnvelope({
|
||||
name: "blocks.move",
|
||||
payload: {
|
||||
sourceDocumentId: input.sourceDocumentId,
|
||||
targetDocumentId: input.targetDocumentId,
|
||||
blockId: input.blockId,
|
||||
},
|
||||
context,
|
||||
target: {
|
||||
pageId: input.targetDocumentId,
|
||||
blockId: input.blockId,
|
||||
},
|
||||
});
|
||||
const plan = await resolveRustBridgeCommandPlan({ context, envelope });
|
||||
try {
|
||||
await executeRustBridgeMutationTransport({ client, plan });
|
||||
await client.mutation(api.documents.updateContent, { id: input.sourceDocumentId, content: nextSourceContent });
|
||||
await client.mutation(api.documents.updateContent, { id: input.targetDocumentId, content: nextTargetContent });
|
||||
await recordBridgeCommandArtifacts({ context, envelope });
|
||||
} catch (error) {
|
||||
await recordBridgeCommandFailureArtifacts({
|
||||
client,
|
||||
context,
|
||||
envelope,
|
||||
error,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
return {
|
||||
requestId: context.requestId,
|
||||
traceId: context.traceId,
|
||||
commandId: envelope.commandId,
|
||||
commandName: envelope.name,
|
||||
result: { ok: true as const },
|
||||
};
|
||||
}
|
||||
|
||||
export async function executeBlockEmbedBridgeCommand(input: {
|
||||
request: Request;
|
||||
sourceDocumentId: string;
|
||||
targetDocumentId: string;
|
||||
blockId: string;
|
||||
}) {
|
||||
const { client } = await getAuthedConvexClient();
|
||||
const source = await client.query(api.documents.getContent, { id: input.sourceDocumentId });
|
||||
if (!source) throw new Error("源页面不存在或无权限");
|
||||
const target = await client.query(api.documents.getContent, { id: input.targetDocumentId });
|
||||
if (!target) throw new Error("目标页面不存在或无权限");
|
||||
const targetMeta = await client.query(api.documents.getMeta, { id: input.targetDocumentId });
|
||||
const sourceBlocks = extractBlocksFromContent(source.content) as BlockLike[];
|
||||
const hit = findBlockInTree(sourceBlocks, input.blockId);
|
||||
if (!hit) throw new Error("源块不存在或无权限");
|
||||
const targetBlocks = extractBlocksFromContent(target.content) as BlockLike[];
|
||||
const anchorId = (targetMeta as { embed_default_block_id?: string | null } | null)?.embed_default_block_id ?? null;
|
||||
const anchorIndex =
|
||||
typeof anchorId === "string" && anchorId.trim()
|
||||
? targetBlocks.findIndex((block) => String(block.id ?? "") === anchorId)
|
||||
: -1;
|
||||
const insertIndex = anchorIndex >= 0 ? anchorIndex + 1 : targetBlocks.length;
|
||||
const nextTargetBlocks = [
|
||||
...targetBlocks.slice(0, insertIndex),
|
||||
buildReferenceBlock(input.sourceDocumentId, input.blockId),
|
||||
...targetBlocks.slice(insertIndex),
|
||||
];
|
||||
const context = await buildBridgeContext(input.request, null);
|
||||
const envelope = buildDocumentCommandEnvelope({
|
||||
name: "blocks.embed",
|
||||
payload: {
|
||||
sourceDocumentId: input.sourceDocumentId,
|
||||
targetDocumentId: input.targetDocumentId,
|
||||
blockId: input.blockId,
|
||||
},
|
||||
context,
|
||||
target: {
|
||||
pageId: input.targetDocumentId,
|
||||
blockId: input.blockId,
|
||||
},
|
||||
});
|
||||
const plan = await resolveRustBridgeCommandPlan({ context, envelope });
|
||||
try {
|
||||
await executeRustBridgeMutationTransport({ client, plan });
|
||||
await client.mutation(api.documents.updateContent, {
|
||||
id: input.targetDocumentId,
|
||||
content: composeContentWithBlocks(target.content, nextTargetBlocks as never),
|
||||
});
|
||||
await recordBridgeCommandArtifacts({ context, envelope });
|
||||
} catch (error) {
|
||||
await recordBridgeCommandFailureArtifacts({
|
||||
client,
|
||||
context,
|
||||
envelope,
|
||||
error,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
return {
|
||||
requestId: context.requestId,
|
||||
traceId: context.traceId,
|
||||
commandId: envelope.commandId,
|
||||
commandName: envelope.name,
|
||||
result: { ok: true as const },
|
||||
};
|
||||
}
|
||||
|
||||
export function handleBlockBridgeError(error: unknown) {
|
||||
return documentBridgeErrorResponse(error);
|
||||
}
|
||||
Reference in New Issue
Block a user