Files
mnote/wolai-frontend/src/lib/documents/block-command-adapter.ts
T

334 lines
11 KiB
TypeScript
Raw Normal View History

2026-04-15 20:01:12 +08:00
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);
}