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