- 收口 page aggregate 读取、本地状态与命令客户端\n- 接入 phase7 document ai sidecar 与前端编排入口\n- 更新 architecture 与 design 状态迁移
145 lines
4.1 KiB
TypeScript
145 lines
4.1 KiB
TypeScript
import type { PageBodyPersistedMeta } from "@/lib/documents/page-command-client";
|
|
import type { PageAggregateProjection } from "@/lib/documents/page-aggregate";
|
|
import type { PageSubtreeProjection } from "@/lib/documents/page-subtree";
|
|
import type { PageOptionsState } from "@/types/page-options";
|
|
import type { Json } from "@/types/supabase";
|
|
|
|
export type PageAggregateClientState = {
|
|
options: PageOptionsState;
|
|
content: unknown;
|
|
serverContentSnapshot: unknown;
|
|
serverPageSubtreeSnapshot: PageSubtreeProjection | null;
|
|
serverPageSubtreeTitle: string;
|
|
contentRevision: number | null;
|
|
conflictDetectionKey: string | null;
|
|
};
|
|
|
|
export type PageAggregateClientStateAction =
|
|
| {
|
|
type: "hydrate_from_page";
|
|
page: PageAggregateProjection;
|
|
}
|
|
| {
|
|
type: "patch_page_options";
|
|
patch: Partial<PageOptionsState>;
|
|
}
|
|
| {
|
|
type: "apply_local_content_snapshot";
|
|
content: unknown;
|
|
}
|
|
| {
|
|
type: "apply_persisted_body_meta";
|
|
meta: PageBodyPersistedMeta;
|
|
}
|
|
| {
|
|
type: "update_server_page_subtree_title";
|
|
title: string;
|
|
};
|
|
|
|
function normalizePageTitle(title: string | null | undefined): string {
|
|
const normalized = String(title ?? "").trim();
|
|
return normalized || "无标题";
|
|
}
|
|
|
|
function resolveServerPageSubtreeTitle(page: PageAggregateProjection): string {
|
|
const subtreeTitle = page.tree.pageSubtree?.rootNode.metadata.title;
|
|
if (typeof subtreeTitle === "string" && subtreeTitle.trim()) {
|
|
return subtreeTitle.trim();
|
|
}
|
|
return normalizePageTitle(page.head.title);
|
|
}
|
|
|
|
export function createPageAggregateClientState(
|
|
page: PageAggregateProjection,
|
|
): PageAggregateClientState {
|
|
return {
|
|
options: page.layout.pageOptions,
|
|
content: page.body.content,
|
|
serverContentSnapshot: page.body.content,
|
|
serverPageSubtreeSnapshot: page.tree.pageSubtree,
|
|
serverPageSubtreeTitle: resolveServerPageSubtreeTitle(page),
|
|
contentRevision: page.body.revision,
|
|
conflictDetectionKey: page.body.conflictDetectionKey,
|
|
};
|
|
}
|
|
|
|
export function pageAggregateClientStateReducer(
|
|
state: PageAggregateClientState,
|
|
action: PageAggregateClientStateAction,
|
|
): PageAggregateClientState {
|
|
switch (action.type) {
|
|
case "hydrate_from_page":
|
|
return createPageAggregateClientState(action.page);
|
|
case "patch_page_options":
|
|
return {
|
|
...state,
|
|
options: {
|
|
...state.options,
|
|
...action.patch,
|
|
},
|
|
};
|
|
case "apply_local_content_snapshot":
|
|
return {
|
|
...state,
|
|
content: action.content,
|
|
};
|
|
case "apply_persisted_body_meta":
|
|
return {
|
|
...state,
|
|
contentRevision: action.meta.revision,
|
|
conflictDetectionKey: action.meta.conflictDetectionKey,
|
|
};
|
|
case "update_server_page_subtree_title":
|
|
return {
|
|
...state,
|
|
serverPageSubtreeTitle: normalizePageTitle(action.title),
|
|
};
|
|
default:
|
|
return state;
|
|
}
|
|
}
|
|
|
|
export function selectPageAggregateClientPageSubtree(
|
|
state: PageAggregateClientState,
|
|
pageTitle: string,
|
|
): PageSubtreeProjection | null {
|
|
const hasServerPageSubtree = Boolean(state.serverPageSubtreeSnapshot);
|
|
const titleUnchanged = normalizePageTitle(pageTitle) === state.serverPageSubtreeTitle;
|
|
const contentUnchanged = state.content === state.serverContentSnapshot;
|
|
|
|
if (hasServerPageSubtree && titleUnchanged && contentUnchanged) {
|
|
return state.serverPageSubtreeSnapshot;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function selectPageAggregateClientAiSnapshot(
|
|
state: PageAggregateClientState,
|
|
input: {
|
|
workspaceId: string | null;
|
|
pageTitle: string;
|
|
},
|
|
): {
|
|
blocks: Json | null;
|
|
pageSubtree: PageSubtreeProjection | null;
|
|
persistedMeta: {
|
|
workspaceId: string | null;
|
|
revision: number | null;
|
|
conflictDetectionKey: string | null;
|
|
};
|
|
pageOptions: PageOptionsState;
|
|
} {
|
|
const blocks = state.content as Json | null;
|
|
|
|
return {
|
|
blocks,
|
|
pageSubtree: selectPageAggregateClientPageSubtree(state, input.pageTitle),
|
|
persistedMeta: {
|
|
workspaceId: input.workspaceId,
|
|
revision: state.contentRevision,
|
|
conflictDetectionKey: state.conflictDetectionKey,
|
|
},
|
|
pageOptions: state.options,
|
|
};
|
|
}
|