import type { SidebarInitialData } from "@/components/sidebar/types"; import type { SidebarDatasetListQueryResult } from "@/lib/sidebar-data"; import { buildSidebarDatasetListQueryResult, mapSidebarDatasetListQueryResultToInitialData, } from "@/lib/sidebar-data"; import type { DocumentRecord } from "@/lib/documents"; import type { WorkspaceSummary } from "@/lib/workspaces"; import type { MediaAsset } from "@/types/media"; export type TreeStreamDeltaOp = | "upsert_document" | "remove_document" | "replace_documents" | "replace_sidebar"; export type TreeStreamDeltaEvent = { op: TreeStreamDeltaOp; node?: DocumentRecord | null; document?: DocumentRecord | null; documentId?: string | null; documents?: DocumentRecord[] | null; sidebar?: SidebarDatasetListQueryResult | SidebarInitialData | null; }; function cloneSidebarData(data: SidebarInitialData): SidebarInitialData { return { ...data, workspaces: [...data.workspaces], documents: [...data.documents], kernelFileTreeProjection: { ...data.kernelFileTreeProjection, items: [...data.kernelFileTreeProjection.items], edges: [...data.kernelFileTreeProjection.edges], }, kernelSidebarProjection: { ...data.kernelSidebarProjection, items: [...data.kernelSidebarProjection.items], edges: [...data.kernelSidebarProjection.edges], }, kernelSidebarTree: [...data.kernelSidebarTree], trashedDocuments: [...data.trashedDocuments], trashedMediaAssets: [...(data.trashedMediaAssets ?? [])], trashedMindmapAssets: [...(data.trashedMindmapAssets ?? [])], trashedTableAssets: [...(data.trashedTableAssets ?? [])], mindmapDocs: [...(data.mindmapDocs ?? [])], mindmapAssets: [...(data.mindmapAssets ?? [])], mindmapAssetChildren: { ...(data.mindmapAssetChildren ?? {}) }, tableAssets: [...(data.tableAssets ?? [])], mediaAssets: [...(data.mediaAssets ?? [])], }; } function buildSidebarFromDocuments(input: { base: SidebarInitialData; documents: DocumentRecord[]; }): SidebarInitialData { const queryResult = buildSidebarDatasetListQueryResult({ activeWorkspaceId: input.base.activeWorkspaceId, workspaces: input.base.workspaces as WorkspaceSummary[], documents: input.documents, trashedDocuments: input.base.trashedDocuments, mindmaps: (input.base.mindmapAssets ?? []).map((asset) => ({ mindmap_id: asset.id, workspace_id: asset.workspace_id, document_id: asset.document_id, created_at: asset.created_at, updated_at: asset.updated_at, deleted_at: asset.deleted_at ?? null, deleted_by: asset.deleted_by ?? null, })), mediaAssets: input.base.mediaAssets as MediaAsset[] | null, trashedMediaAssets: input.base.trashedMediaAssets as MediaAsset[] | null, tables: (input.base.tableAssets ?? []).map((asset) => ({ id: asset.id, workspace_id: asset.workspace_id, document_id: asset.document_id, title: asset.file_name ?? null, created_at: asset.created_at, updated_at: asset.updated_at, deleted_at: asset.deleted_at ?? null, deleted_by: asset.deleted_by ?? null, purged_at: asset.purged_at ?? null, is_archived: Boolean(asset.deleted_at), })), }); return mapSidebarDatasetListQueryResultToInitialData(queryResult); } function normalizeUpsertDocument(event: TreeStreamDeltaEvent): DocumentRecord | null { const candidate = event.node ?? event.document ?? null; return candidate && typeof candidate === "object" ? candidate : null; } function normalizeDocumentId(event: TreeStreamDeltaEvent): string | null { const candidate = typeof event.documentId === "string" ? event.documentId.trim() : ""; return candidate || null; } export function applyTreeStreamDelta( base: SidebarInitialData, event: TreeStreamDeltaEvent, ): SidebarInitialData { if (event.op === "replace_sidebar" && event.sidebar) { if ("activeWorkspaceId" in event.sidebar) { return cloneSidebarData(event.sidebar as SidebarInitialData); } return mapSidebarDatasetListQueryResultToInitialData(event.sidebar as SidebarDatasetListQueryResult); } if (event.op === "replace_documents" && Array.isArray(event.documents)) { return buildSidebarFromDocuments({ base, documents: [...event.documents], }); } if (event.op === "upsert_document") { const nextDocument = normalizeUpsertDocument(event); if (!nextDocument) { return base; } const nextDocuments = [...base.documents]; const existingIndex = nextDocuments.findIndex((item) => item.id === nextDocument.id); if (existingIndex >= 0) { nextDocuments[existingIndex] = nextDocument; } else { nextDocuments.push(nextDocument); } return buildSidebarFromDocuments({ base, documents: nextDocuments, }); } if (event.op === "remove_document") { const documentId = normalizeDocumentId(event); if (!documentId) { return base; } const removedIds = new Set([documentId]); let changed = true; while (changed) { changed = false; for (const document of base.documents) { if (document.parent_id && removedIds.has(document.parent_id) && !removedIds.has(document.id)) { removedIds.add(document.id); changed = true; } } } return buildSidebarFromDocuments({ base, documents: base.documents.filter((item) => !removedIds.has(item.id)), }); } return base; }