Files
mnote/wolai-frontend/src/components/editor/page-aggregate-client-state.ts
T
lix-2026 41e958769e feat: land page aggregate and phase7 document ai mainline
- 收口 page aggregate 读取、本地状态与命令客户端\n- 接入 phase7 document ai sidecar 与前端编排入口\n- 更新 architecture 与 design 状态迁移
2026-04-23 07:38:34 +08:00

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,
};
}