const frames = await collectFrames(
      streamTreeFrames({
        workspaceId: "ws_1",
        pollMs: 1,
        maxPolls: 1,
        loadOverview,
        loadSnapshot,
        sleep: vi.fn().mockResolvedValue(undefined),
      }),
    );

    expect(frames).toHaveLength(2);
    expect(frames[1]).toMatchObject({
      event: "delta",
      payload: {
        kind: "delta",
        data: streamDelta,
      },
    });
    expect(loadSnapshot).toHaveBeenCalledTimes(1);
  });

  it("command log 与 domain event 同时推进且 delta 不一致时，应回退 resync 避免漏发", async () => {
    const loadOverview = vi
      .fn()
      .mockResolvedValueOnce(
        buildOverview([{ id: "clog_1", created_at: "2026-04-24T00:00:01Z" }]),
      )
      .mockResolvedValueOnce({
        command_logs: [
          {
            id: "clog_2",
            created_at: "2026-04-24T00:00:03Z",
            command_name: "tree.node.rename",
            payload: {
              streamDelta: {
                op: "upsert_document",
                document: { id: "page_2", title: "命令标题" },
              },
            },
          },
          { id: "clog_1", created_at: "2026-04-24T00:00:01Z" },
        ],
        domain_events: [
          {
            event_id: "evt_2",
            created_at: "2026-04-24T00:00:02Z",
            payload: {
              streamDelta: {
                op: "remove_document",
                documentId: "page_3",
              },
            },
          },
        ],
        has_more: false,
        next_cursor: null,
        generated_at: "2026-04-24T00:00:03Z",
      });


payload: {
              streamDelta: {
                op: "remove_document",
                documentId: "page_3",
              },
            },
          },
        ],
        has_more: false,
        next_cursor: null,
        generated_at: "2026-04-24T00:00:03Z",
      });
    const loadSnapshot = vi
      .fn()
      .mockResolvedValueOnce({
        requestId: "req_1",
        traceId: "trace_1",
        data: { activeWorkspaceId: "ws_1", documents: [{ id: "page_1" }] },
        snapshot: { dataset: { active_workspace_id: "ws_1", documents: [{ id: "page_1" }] }, tree: { items: [] } },
      })
      .mockResolvedValueOnce({
        requestId: "req_2",
        traceId: "trace_2",
        data: { activeWorkspaceId: "ws_1", documents: [{ id: "page_2" }] },
        snapshot: { dataset: { active_workspace_id: "ws_1", documents: [{ id: "page_2" }] }, tree: { items: [] } },
      });

    const frames = await collectFrames(
      streamTreeFrames({
        workspaceId: "ws_1",
        pollMs: 1,
        maxPolls: 1,
        loadOverview,
        loadSnapshot,
        sleep: vi.fn().mockResolvedValue(undefined),
      }),
    );

    expect(frames).toHaveLength(2);
    expect(frames[1]).toMatchObject({
      event: "resync",
      payload: {
        kind: "resync",
      },
    });
    expect(loadSnapshot).toHaveBeenCalledTimes(2);
  });

  it("单条新命令缺少可稳定解释的 streamDelta 时，应回退 resync", async () => {
    const loadOverview = vi
      .fn()
      .mockResolvedValueOnce(
        buildOverview([{ id: "clog_1", created_at: "2026-04-24T00:00:01Z" }]),
      )
      .mockResolvedValueOnce(
        buildOverview([
          {
            id: "clog_2",
            created_at: "2026-04-24T00:00:02Z",
            command_name: "tree.subtree.move",


it("单条新命令缺少可稳定解释的 streamDelta 时，应回退 resync", async () => {
    const loadOverview = vi
      .fn()
      .mockResolvedValueOnce(
        buildOverview([{ id: "clog_1", created_at: "2026-04-24T00:00:01Z" }]),
      )
      .mockResolvedValueOnce(
        buildOverview([
          {
            id: "clog_2",
            created_at: "2026-04-24T00:00:02Z",
            command_name: "tree.subtree.move",
            payload: {
              documentId: "page_2",
            },
          },
          { id: "clog_1", created_at: "2026-04-24T00:00:01Z" },
        ]),
      );
    const loadSnapshot = vi
      .fn()
      .mockResolvedValueOnce({
        requestId: "req_1",
        traceId: "trace_1",
        data: { activeWorkspaceId: "ws_1", documents: [{ id: "page_1" }] },
        snapshot: { dataset: { active_workspace_id: "ws_1", documents: [{ id: "page_1" }] }, tree: { items: [] } },
      })
      .mockResolvedValueOnce({
        requestId: "req_2",
        traceId: "trace_2",
        data: { activeWorkspaceId: "ws_1", documents: [{ id: "page_1" }, { id: "page_2" }] },
        snapshot: {
          dataset: { active_workspace_id: "ws_1", documents: [{ id: "page_1" }, { id: "page_2" }] },
          tree: { items: [] },
        },
      });

    const frames = await collectFrames(
      streamTreeFrames({
        workspaceId: "ws_1",
        pollMs: 1,
        maxPolls: 1,
        loadOverview,
        loadSnapshot,
        sleep: vi.fn().mockResolvedValue(undefined),
      }),
    );

    expect(frames).toHaveLength(2);
    expect(frames[1]).toMatchObject({
      event: "resync",
      payload: {
        kind: "resync",
      },
    });
    expect(loadSnapshot).toHaveBeenCalledTimes(2);
  });

  it("正文保存这类无树结构影响的命令应降级为 noop delta，而不是触发 resync", async () => {
    const loadOverview = vi


expect(frames).toHaveLength(2);
    expect(frames[1]).toMatchObject({
      event: "resync",
      payload: {
        kind: "resync",
      },
    });
    expect(loadSnapshot).toHaveBeenCalledTimes(2);
  });

  it("正文保存这类无树结构影响的命令应降级为 noop delta，而不是触发 resync", async () => {
    const loadOverview = vi
      .fn()
      .mockResolvedValueOnce(
        buildOverview([{ id: "clog_1", created_at: "2026-04-24T00:00:01Z" }]),
      )
      .mockResolvedValueOnce(
        buildOverview([
          {
            id: "clog_2",
            created_at: "2026-04-24T00:00:02Z",
            command_name: "page.body.save",
            payload: {
              documentId: "page_1",
            },
          },
          { id: "clog_1", created_at: "2026-04-24T00:00:01Z" },
        ]),
      );
    const loadSnapshot = vi.fn().mockResolvedValue({
      requestId: "req_1",
      traceId: "trace_1",
      data: { activeWorkspaceId: "ws_1", documents: [{ id: "page_1" }] },
      snapshot: { dataset: { active_workspace_id: "ws_1", documents: [{ id: "page_1" }] }, tree: { items: [] } },
    });

    const frames = await collectFrames(
      streamTreeFrames({
        workspaceId: "ws_1",
        pollMs: 1,
        maxPolls: 1,
        loadOverview,
        loadSnapshot,
        sleep: vi.fn().mockResolvedValue(undefined),
      }),
    );

    expect(frames).toHaveLength(2);
    expect(frames[1]).toMatchObject({
      event: "delta",
      payload: {
        kind: "delta",
        data: {
          op: "noop",
        },
      },
    });
    expect(loadSnapshot).toHaveBeenCalledTimes(1);
  });


expect(frames[1]).toMatchObject({
      event: "delta",
      payload: {
        kind: "delta",
        data: {
          op: "noop",
        },
      },
    });
    expect(loadSnapshot).toHaveBeenCalledTimes(1);
  });

  it("move 这类附带 move_document 的新命令应直接发 delta，而不是触发 resync", async () => {
    const sidebarSnapshot = {
      activeWorkspaceId: "ws_1",
      workspaces: [],
      documents: [
        {
          id: "page_1",
          workspace_id: "ws_1",
          title: "页面 1",
          parent_id: null,
          sort_order: 0,
          access_scope: "private",
          is_starred: false,
          is_template: false,
          created_at: "2026-04-24T00:00:00Z",
          updated_at: "2026-04-24T00:01:00Z",
        },
      ],
      kernelSidebarProjection: {
        projectionId: "kernel_projection:sidebar_tree:workspace_root",
        projection: "sidebar_tree",
        rootNodeId: null,
        items: [],
        edges: [],
      },
      kernelFileTreeProjection: {
        projectionId: "kernel_projection:file_tree:workspace_root",
        projection: "file_tree",
        rootNodeId: null,
        items: [],
        edges: [],
      },
      kernelSidebarTree: [],
      trashedDocuments: [],
      mediaAssets: [],
      mindmapDocs: [],
      mindmapAssets: [],
      mindmapAssetChildren: {},
      tableAssets: [],
    };
    const loadOverview = vi
      .fn()
      .mockResolvedValueOnce(
        buildOverview([{ id: "clog_1", created_at: "2026-04-24T00:00:01Z" }]),
      )
      .mockResolvedValueOnce(
        buildOverview([
          {


mindmapAssets: [],
      mindmapAssetChildren: {},
      tableAssets: [],
    };
    const loadOverview = vi
      .fn()
      .mockResolvedValueOnce(
        buildOverview([{ id: "clog_1", created_at: "2026-04-24T00:00:01Z" }]),
      )
      .mockResolvedValueOnce(
        buildOverview([
          {
            id: "clog_2",
            created_at: "2026-04-24T00:00:02Z",
            command_name: "tree.subtree.move",
            payload: {
              documentId: "page_1",
              streamDelta: {
                op: "move_document",
                documentId: "page_1",
                parentId: null,
                sortOrder: 0,
                updatedAt: "2026-04-24T00:01:00Z",
              },
            },
          },
          { id: "clog_1", created_at: "2026-04-24T00:00:01Z" },
        ]),
      );
    const loadSnapshot = vi.fn().mockResolvedValue({
      requestId: "req_1",
      traceId: "trace_1",
      data: { activeWorkspaceId: "ws_1", documents: [] },
      snapshot: { dataset: { active_workspace_id: "ws_1", documents: [] }, tree: { items: [] } },
    });

    const frames = await collectFrames(
      streamTreeFrames({
        workspaceId: "ws_1",
        pollMs: 1,
        maxPolls: 1,
        loadOverview,
        loadSnapshot,
        sleep: vi.fn().mockResolvedValue(undefined),
      }),
    );

    expect(frames).toHaveLength(2);
    expect(frames[1]).toMatchObject({
      event: "delta",
      payload: {
        kind: "delta",
        data: {
          op: "move_document",
          documentId: "page_1",
          parentId: null,
          sortOrder: 0,
          updatedAt: "2026-04-24T00:01:00Z",
        },
      },


expect(frames[1]).toMatchObject({
      event: "delta",
      payload: {
        kind: "delta",
        data: {
          op: "move_document",
          documentId: "page_1",
          parentId: null,
          sortOrder: 0,
          updatedAt: "2026-04-24T00:01:00Z",
        },
      },
    });
    expect(loadSnapshot).toHaveBeenCalledTimes(1);
  });

  it("copy 这类附带 upsert_documents 的新命令应保留批量文档字段", async () => {
    const loadOverview = vi
      .fn()
      .mockResolvedValueOnce(
        buildOverview([{ id: "clog_1", created_at: "2026-04-24T00:00:01Z" }]),
      )
      .mockResolvedValueOnce(
        buildOverview([
          {
            id: "clog_2",
            created_at: "2026-04-24T00:00:02Z",
            command_name: "tree.subtree.copy",
            payload: {
              streamDelta: {
                op: "upsert_documents",
                upsertDocuments: [
                  {
                    id: "copy_1",
                    workspace_id: "ws_1",
                    title: "Copy",
                    parent_id: null,
                    sort_order: 2,
                    is_starred: false,
                    access_scope: "private",
                    is_template: false,
                    created_at: "2026-04-24T00:00:02Z",
                    updated_at: "2026-04-24T00:00:02Z",
                  },
                ],
              },
            },
          },
          { id: "clog_1", created_at: "2026-04-24T00:00:01Z" },
        ]),
      );
    const loadSnapshot = vi.fn().mockResolvedValue({
      requestId: "req_1",
      traceId: "trace_1",
      data: { activeWorkspaceId: "ws_1", documents: [] },
      snapshot: { dataset: { active_workspace_id: "ws_1", documents: [] }, tree: { items: [] } },
    });

    const frames = await collectFrames(
      streamTreeFrames({


{ id: "clog_1", created_at: "2026-04-24T00:00:01Z" },
        ]),
      );
    const loadSnapshot = vi.fn().mockResolvedValue({
      requestId: "req_1",
      traceId: "trace_1",
      data: { activeWorkspaceId: "ws_1", documents: [] },
      snapshot: { dataset: { active_workspace_id: "ws_1", documents: [] }, tree: { items: [] } },
    });

    const frames = await collectFrames(
      streamTreeFrames({
        workspaceId: "ws_1",
        pollMs: 1,
        maxPolls: 1,
        loadOverview,
        loadSnapshot,
        sleep: vi.fn().mockResolvedValue(undefined),
      }),
    );

    expect(frames[1]).toMatchObject({
      event: "delta",
      payload: {
        kind: "delta",
        data: {
          op: "upsert_documents",
          upsertDocuments: [
            expect.objectContaining({
              id: "copy_1",
              workspace_id: "ws_1",
            }),
          ],
        },
      },
    });
    expect(loadSnapshot).toHaveBeenCalledTimes(1);
  });

  it("轮询期间没有新 cursor 时，不应额外发 resync", async () => {
    const loadOverview = vi
      .fn()
      .mockResolvedValue(buildOverview([{ id: "clog_1", created_at: "2026-04-24T00:00:01Z" }]));

    const frames = await collectFrames(
      streamTreeFrames({
        workspaceId: "ws_1",
        pollMs: 1,
        maxPolls: 1,
        loadOverview,
        loadSnapshot: vi.fn().mockResolvedValue({
          requestId: "req_1",
          traceId: "trace_1",
          data: { activeWorkspaceId: "ws_1", documents: [] },
          snapshot: { dataset: { active_workspace_id: "ws_1", documents: [] }, tree: { items: [] } },
        }),
        sleep: vi.fn().mockResolvedValue(undefined),
      }),
    );


maxPolls: 1,
        loadOverview,
        loadSnapshot: vi.fn().mockResolvedValue({
          requestId: "req_1",
          traceId: "trace_1",
          data: { activeWorkspaceId: "ws_1", documents: [] },
          snapshot: { dataset: { active_workspace_id: "ws_1", documents: [] }, tree: { items: [] } },
        }),
        sleep: vi.fn().mockResolvedValue(undefined),
      }),
    );

    expect(frames).toHaveLength(1);
    expect(frames[0]).toMatchObject({ event: "snapshot" });
  });
});


import type { TreeStreamDeltaEvent } from "./tree-delta";

export type TreeStreamScope = "workspace" | "subtree";
export type TreeStreamProjection = "sidebar_tree" | "page_tree";
export type TreeStreamEventName = "snapshot" | "delta" | "resync";

export interface TreeStreamCommandLogCursorRow {
  id?: string | null;
  command_id?: string | null;
  commandId?: string | null;
  created_at?: string | null;
  command_name?: string | null;
  commandName?: string | null;
  payload?: unknown;
}

export interface TreeStreamOverview {
  command_logs?: TreeStreamCommandLogCursorRow[] | null;
  domain_events?: unknown[] | null;
  next_cursor?: string | null;
  has_more?: boolean | null;
  generated_at?: string | null;
}

export interface TreeStreamSnapshotPayload {
  requestId: string;
  traceId: string;
  data: unknown;
  snapshot: unknown;
}

export interface TreeStreamEnvelope {
  kind: TreeStreamEventName;
  stream: TreeStreamScope;
  workspaceId: string;
  rootNodeId: string | null;
  cursor: string | null;
  projection: TreeStreamProjection;
  requestId: string;
  traceId: string;
  data: unknown;
  snapshot: unknown;
  overview: TreeStreamOverview;
}

export interface TreeStreamFrame {
  event: TreeStreamEventName;
  payload: TreeStreamEnvelope;
}

export interface StreamTreeFramesInput {
  workspaceId: string;
  rootNodeId?: string | null;
  initialCursor?: string | null;
  pollMs?: number;
  maxPolls?: number | null;
  loadOverview: () => Promise<TreeStreamOverview>;
  loadSnapshot: () => Promise<TreeStreamSnapshotPayload>;
  sleep?: (ms: number) => Promise<void>;
}


}

export interface StreamTreeFramesInput {
  workspaceId: string;
  rootNodeId?: string | null;
  initialCursor?: string | null;
  pollMs?: number;
  maxPolls?: number | null;
  loadOverview: () => Promise<TreeStreamOverview>;
  loadSnapshot: () => Promise<TreeStreamSnapshotPayload>;
  sleep?: (ms: number) => Promise<void>;
}

type DecodedTreeStreamCursor = {
  createdAt: string;
  id: string;
};

type TreeStreamDomainEventCursorRow = {
  id?: string | null;
  event_id?: string | null;
  command_id?: string | null;
  commandId?: string | null;
  created_at?: string | null;
  createdAt?: string | null;
  payload?: unknown;
};

const TREE_STREAM_NOOP_COMMANDS = new Set([
  "page.body.save",
  "page.layout.updateOptions",
  "documents.stats.update",
  "blocks.patch",
  "blocks.move",
  "blocks.embed",
]);

function normalizeNodeId(value: string | null | undefined) {
  const normalized = typeof value === "string" ? value.trim() : "";
  return normalized || null;
}

export function resolveTreeStreamContract(input: {
  rootNodeId?: string | null;
}): {
  stream: TreeStreamScope;
  projection: TreeStreamProjection;
  rootNodeId: string | null;
} {
  const rootNodeId = normalizeNodeId(input.rootNodeId);
  if (rootNodeId) {
    return {
      stream: "subtree",
      projection: "page_tree",
      rootNodeId,
    };
  }
  return {
    stream: "workspace",
    projection: "sidebar_tree",


} {
  const rootNodeId = normalizeNodeId(input.rootNodeId);
  if (rootNodeId) {
    return {
      stream: "subtree",
      projection: "page_tree",
      rootNodeId,
    };
  }
  return {
    stream: "workspace",
    projection: "sidebar_tree",
    rootNodeId: null,
  };
}

export function encodeTreeStreamCursor(row: TreeStreamCommandLogCursorRow | null | undefined) {
  const id = typeof row?.id === "string" ? row.id.trim() : "";
  const createdAt = typeof row?.created_at === "string" ? row.created_at.trim() : "";
  if (!id || !createdAt) {
    return null;
  }
  return JSON.stringify({
    createdAt,
    id,
  });
}

function encodeTreeStreamDomainEventCursor(
  row: TreeStreamDomainEventCursorRow | null | undefined,
) {
  const rawId =
    typeof row?.event_id === "string"
      ? row.event_id.trim()
      : typeof row?.id === "string"
        ? row.id.trim()
        : "";
  const createdAt =
    typeof row?.created_at === "string"
      ? row.created_at.trim()
      : typeof row?.createdAt === "string"
        ? row.createdAt.trim()
        : "";
  if (!rawId || !createdAt) {
    return null;
  }
  return JSON.stringify({
    createdAt,
    id: `domain_event:${rawId}`,
  });
}

function decodeTreeStreamCursor(raw: string | null | undefined): DecodedTreeStreamCursor | null {
  if (!raw) {
    return null;
  }
  try {
    const parsed = JSON.parse(raw) as {
      createdAt?: string | null;
      id?: string | null;


id: `domain_event:${rawId}`,
  });
}

function decodeTreeStreamCursor(raw: string | null | undefined): DecodedTreeStreamCursor | null {
  if (!raw) {
    return null;
  }
  try {
    const parsed = JSON.parse(raw) as {
      createdAt?: string | null;
      id?: string | null;
    };
    const createdAt = typeof parsed.createdAt === "string" ? parsed.createdAt.trim() : "";
    const id = typeof parsed.id === "string" ? parsed.id.trim() : "";
    return createdAt && id ? { createdAt, id } : null;
  } catch {
    return null;
  }
}

export function resolveOverviewCursor(
  overview: TreeStreamOverview,
  fallback?: string | null,
) {
  const commandRows = Array.isArray(overview.command_logs) ? overview.command_logs : [];
  const eventRows = Array.isArray(overview.domain_events)
    ? (overview.domain_events as TreeStreamDomainEventCursorRow[])
    : [];
  const commandCursor = encodeTreeStreamCursor(commandRows[0] ?? null);
  const domainEventCursor = encodeTreeStreamDomainEventCursor(eventRows[0] ?? null);

  if (!commandCursor) {
    return domainEventCursor ?? fallback ?? null;
  }
  if (!domainEventCursor) {
    return commandCursor ?? fallback ?? null;
  }

  const decodedCommandCursor = decodeTreeStreamCursor(commandCursor);
  const decodedDomainEventCursor = decodeTreeStreamCursor(domainEventCursor);
  if (!decodedCommandCursor) {
    return domainEventCursor;
  }
  if (!decodedDomainEventCursor) {
    return commandCursor;
  }

  return decodedDomainEventCursor.createdAt > decodedCommandCursor.createdAt
    ? domainEventCursor
    : commandCursor;
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}

function readCommandPayloadDelta(row: TreeStreamCommandLogCursorRow): TreeStreamDeltaEvent | null {
  const commandName =
    typeof row.command_name === "string"


return decodedDomainEventCursor.createdAt > decodedCommandCursor.createdAt
    ? domainEventCursor
    : commandCursor;
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}

function readCommandPayloadDelta(row: TreeStreamCommandLogCursorRow): TreeStreamDeltaEvent | null {
  const commandName =
    typeof row.command_name === "string"
      ? row.command_name.trim()
      : typeof row.commandName === "string"
        ? row.commandName.trim()
        : "";
  if (commandName && TREE_STREAM_NOOP_COMMANDS.has(commandName)) {
    return {
      op: "noop",
    };
  }
  if (!isRecord(row.payload) || !("streamDelta" in row.payload)) {
    return null;
  }
  const candidate = row.payload.streamDelta;
  if (!isRecord(candidate) || typeof candidate.op !== "string") {
    return null;
  }

  return {
    op: candidate.op as TreeStreamDeltaEvent["op"],
    node: isRecord(candidate.node) ? (candidate.node as TreeStreamDeltaEvent["node"]) : null,
    document: isRecord(candidate.document) ? (candidate.document as TreeStreamDeltaEvent["document"]) : null,
    documentId: typeof candidate.documentId === "string" ? candidate.documentId : null,
    parentId: typeof candidate.parentId === "string" ? candidate.parentId : candidate.parentId === null ? null : undefined,
    sortOrder:
      typeof candidate.sortOrder === "number" && Number.isFinite(candidate.sortOrder)
        ? candidate.sortOrder
        : undefined,
    updatedAt: typeof candidate.updatedAt === "string" ? candidate.updatedAt : null,
    upsertDocuments: Array.isArray(candidate.upsertDocuments)
      ? (candidate.upsertDocuments as TreeStreamDeltaEvent["upsertDocuments"])
      : null,
    upsertAssets: Array.isArray(candidate.upsertAssets)
      ? (candidate.upsertAssets as TreeStreamDeltaEvent["upsertAssets"])
      : null,
    documents: Array.isArray(candidate.documents) ? (candidate.documents as TreeStreamDeltaEvent["documents"]) : null,
    sidebar: isRecord(candidate.sidebar) ? (candidate.sidebar as TreeStreamDeltaEvent["sidebar"]) : null,
  };
}

function readStreamDeltaCandidate(candidate: unknown): TreeStreamDeltaEvent | null {
  if (!isRecord(candidate) || typeof candidate.op !== "string") {
    return null;
  }

  return {
    op: candidate.op as TreeStreamDeltaEvent["op"],
    node: isRecord(candidate.node) ? (candidate.node as TreeStreamDeltaEvent["node"]) : null,
    document: isRecord(candidate.document) ? (candidate.document as TreeStreamDeltaEvent["document"]) : null,


};
}

function readStreamDeltaCandidate(candidate: unknown): TreeStreamDeltaEvent | null {
  if (!isRecord(candidate) || typeof candidate.op !== "string") {
    return null;
  }

  return {
    op: candidate.op as TreeStreamDeltaEvent["op"],
    node: isRecord(candidate.node) ? (candidate.node as TreeStreamDeltaEvent["node"]) : null,
    document: isRecord(candidate.document) ? (candidate.document as TreeStreamDeltaEvent["document"]) : null,
    documentId: typeof candidate.documentId === "string" ? candidate.documentId : null,
    parentId: typeof candidate.parentId === "string" ? candidate.parentId : candidate.parentId === null ? null : undefined,
    sortOrder:
      typeof candidate.sortOrder === "number" && Number.isFinite(candidate.sortOrder)
        ? candidate.sortOrder
        : undefined,
    updatedAt: typeof candidate.updatedAt === "string" ? candidate.updatedAt : null,
    upsertDocuments: Array.isArray(candidate.upsertDocuments)
      ? (candidate.upsertDocuments as TreeStreamDeltaEvent["upsertDocuments"])
      : null,
    upsertAssets: Array.isArray(candidate.upsertAssets)
      ? (candidate.upsertAssets as TreeStreamDeltaEvent["upsertAssets"])
      : null,
    documents: Array.isArray(candidate.documents) ? (candidate.documents as TreeStreamDeltaEvent["documents"]) : null,
    sidebar: isRecord(candidate.sidebar) ? (candidate.sidebar as TreeStreamDeltaEvent["sidebar"]) : null,
  };
}

function readDomainEventPayloadDelta(row: TreeStreamDomainEventCursorRow): TreeStreamDeltaEvent | null {
  if (!isRecord(row.payload)) {
    return null;
  }
  return readStreamDeltaCandidate(row.payload.streamDelta ?? row.payload.stream_delta);
}

function readCommandRowCommandId(row: TreeStreamCommandLogCursorRow): string | null {
  const raw =
    typeof row.command_id === "string"
      ? row.command_id
      : typeof row.commandId === "string"
        ? row.commandId
        : typeof row.id === "string"
          ? row.id
          : "";
  const trimmed = raw.trim();
  return trimmed || null;
}

function readDomainEventCommandId(row: TreeStreamDomainEventCursorRow): string | null {
  const raw =
    typeof row.command_id === "string"
      ? row.command_id
      : typeof row.commandId === "string"
        ? row.commandId
        : "";
  const trimmed = raw.trim();
  return trimmed || null;
}


}

function readDomainEventCommandId(row: TreeStreamDomainEventCursorRow): string | null {
  const raw =
    typeof row.command_id === "string"
      ? row.command_id
      : typeof row.commandId === "string"
        ? row.commandId
        : "";
  const trimmed = raw.trim();
  return trimmed || null;
}

function resolveMatchingCommandDomainEventDelta(input: {
  commandRows: TreeStreamCommandLogCursorRow[];
  domainEventRows: TreeStreamDomainEventCursorRow[];
  commandDrifted: boolean;
  domainEventDrifted: boolean;
}): TreeStreamDeltaEvent | null {
  if (
    input.commandDrifted ||
    input.commandRows.length !== 1 ||
    input.domainEventRows.length !== 1
  ) {
    return null;
  }

  const commandId = readCommandRowCommandId(input.commandRows[0] ?? {});
  const eventCommandId = readDomainEventCommandId(input.domainEventRows[0] ?? {});
  if (!commandId || commandId !== eventCommandId) {
    return null;
  }

  const commandDelta = readCommandPayloadDelta(input.commandRows[0] ?? {});
  const eventDelta = readDomainEventPayloadDelta(input.domainEventRows[0] ?? {});
  if (!commandDelta || !eventDelta) {
    return null;
  }

  return JSON.stringify(commandDelta) === JSON.stringify(eventDelta) ? eventDelta : null;
}

function collectNewCommandLogs(input: {
  rows: TreeStreamCommandLogCursorRow[];
  previousCursor: string | null;
}) {
  const previousCursor = decodeTreeStreamCursor(input.previousCursor);
  if (!previousCursor) {
    return {
      rows: input.rows,
      drifted: false,
    };
  }

  const previousIndex = input.rows.findIndex((row) => {
    const id = typeof row.id === "string" ? row.id.trim() : "";
    const createdAt = typeof row.created_at === "string" ? row.created_at.trim() : "";
    return id === previousCursor.id && createdAt === previousCursor.createdAt;
  });


return {
      rows: input.rows,
      drifted: false,
    };
  }

  const previousIndex = input.rows.findIndex((row) => {
    const id = typeof row.id === "string" ? row.id.trim() : "";
    const createdAt = typeof row.created_at === "string" ? row.created_at.trim() : "";
    return id === previousCursor.id && createdAt === previousCursor.createdAt;
  });

  if (previousIndex >= 0) {
    return {
      rows: input.rows.slice(0, previousIndex),
      drifted: false,
    };
  }

  const newerRows = input.rows.filter((row) => {
    const createdAt = typeof row.created_at === "string" ? row.created_at.trim() : "";
    return createdAt > previousCursor.createdAt;
  });
  if (newerRows.length < input.rows.length) {
    return {
      rows: newerRows,
      drifted: false,
    };
  }

  return {
    rows: input.rows,
    drifted: input.rows.length > 0,
  };
}

function collectNewDomainEvents(input: {
  rows: TreeStreamDomainEventCursorRow[];
  previousCursor: string | null;
}) {
  const previousCursor = decodeTreeStreamCursor(input.previousCursor);
  if (!previousCursor) {
    return {
      rows: input.rows,
      drifted: false,
    };
  }

  const previousId = previousCursor.id.startsWith("domain_event:")
    ? previousCursor.id.slice("domain_event:".length)
    : previousCursor.id;
  const previousIndex = input.rows.findIndex((row) => {
    const id =
      typeof row.event_id === "string"
        ? row.event_id.trim()
        : typeof row.id === "string"
          ? row.id.trim()
          : "";
    const createdAt =
      typeof row.created_at === "string"


const previousId = previousCursor.id.startsWith("domain_event:")
    ? previousCursor.id.slice("domain_event:".length)
    : previousCursor.id;
  const previousIndex = input.rows.findIndex((row) => {
    const id =
      typeof row.event_id === "string"
        ? row.event_id.trim()
        : typeof row.id === "string"
          ? row.id.trim()
          : "";
    const createdAt =
      typeof row.created_at === "string"
        ? row.created_at.trim()
        : typeof row.createdAt === "string"
          ? row.createdAt.trim()
          : "";
    return id === previousId && createdAt === previousCursor.createdAt;
  });

  if (previousIndex >= 0) {
    return {
      rows: input.rows.slice(0, previousIndex),
      drifted: false,
    };
  }

  const newerRows = input.rows.filter((row) => {
    const createdAt =
      typeof row.created_at === "string"
        ? row.created_at.trim()
        : typeof row.createdAt === "string"
          ? row.createdAt.trim()
          : "";
    return createdAt > previousCursor.createdAt;
  });
  if (newerRows.length < input.rows.length) {
    return {
      rows: newerRows,
      drifted: false,
    };
  }

  return {
    rows: input.rows,
    drifted: input.rows.length > 0,
  };
}

function buildTreeStreamEnvelope(input: {
  kind: TreeStreamEventName;
  workspaceId: string;
  rootNodeId: string | null;
  projection: TreeStreamProjection;
  cursor: string | null;
  overview: TreeStreamOverview;
  snapshot: TreeStreamSnapshotPayload;
}): TreeStreamEnvelope {
  return {
    kind: input.kind,
    stream: input.rootNodeId ? "subtree" : "workspace",


function buildTreeStreamEnvelope(input: {
  kind: TreeStreamEventName;
  workspaceId: string;
  rootNodeId: string | null;
  projection: TreeStreamProjection;
  cursor: string | null;
  overview: TreeStreamOverview;
  snapshot: TreeStreamSnapshotPayload;
}): TreeStreamEnvelope {
  return {
    kind: input.kind,
    stream: input.rootNodeId ? "subtree" : "workspace",
    workspaceId: input.workspaceId,
    rootNodeId: input.rootNodeId,
    cursor: input.cursor,
    projection: input.projection,
    requestId: input.snapshot.requestId,
    traceId: input.snapshot.traceId,
    data: input.snapshot.data,
    snapshot: input.snapshot.snapshot,
    overview: input.overview,
  };
}

function buildTreeStreamDeltaEnvelope(input: {
  workspaceId: string;
  rootNodeId: string | null;
  projection: TreeStreamProjection;
  cursor: string | null;
  overview: TreeStreamOverview;
  snapshot: TreeStreamSnapshotPayload;
  delta: TreeStreamDeltaEvent;
}): TreeStreamEnvelope {
  return {
    kind: "delta",
    stream: input.rootNodeId ? "subtree" : "workspace",
    workspaceId: input.workspaceId,
    rootNodeId: input.rootNodeId,
    cursor: input.cursor,
    projection: input.projection,
    requestId: input.snapshot.requestId,
    traceId: input.snapshot.traceId,
    data: input.delta,
    snapshot: null,
    overview: input.overview,
  };
}

async function defaultSleep(ms: number) {
  await new Promise((resolve) => setTimeout(resolve, ms));
}

export async function* streamTreeFrames(
  input: StreamTreeFramesInput,
): AsyncGenerator<TreeStreamFrame> {
  const contract = resolveTreeStreamContract({
    rootNodeId: input.rootNodeId,
  });
  const pollMs = Math.max(250, Math.floor(input.pollMs ?? 2000));
  const maxPolls =


async function defaultSleep(ms: number) {
  await new Promise((resolve) => setTimeout(resolve, ms));
}

export async function* streamTreeFrames(
  input: StreamTreeFramesInput,
): AsyncGenerator<TreeStreamFrame> {
  const contract = resolveTreeStreamContract({
    rootNodeId: input.rootNodeId,
  });
  const pollMs = Math.max(250, Math.floor(input.pollMs ?? 2000));
  const maxPolls =
    typeof input.maxPolls === "number" && Number.isFinite(input.maxPolls)
      ? Math.max(0, Math.floor(input.maxPolls))
      : null;
  const sleep = input.sleep ?? defaultSleep;

  let snapshot = await input.loadSnapshot();
  let overview = await input.loadOverview();
  let cursor = resolveOverviewCursor(overview, input.initialCursor ?? null);

  yield {
    event: "snapshot",
    payload: buildTreeStreamEnvelope({
      kind: "snapshot",
      workspaceId: input.workspaceId,
      rootNodeId: contract.rootNodeId,
      projection: contract.projection,
      cursor,
      overview,
      snapshot,
    }),
  };

  let polls = 0;
  while (maxPolls === null || polls < maxPolls) {
    polls += 1;
    await sleep(pollMs);

    overview = await input.loadOverview();
    const nextCursor = resolveOverviewCursor(overview, cursor);
    if (nextCursor === cursor) {
      continue;
    }

    const rows = Array.isArray(overview.command_logs) ? overview.command_logs : [];
    const newRows = collectNewCommandLogs({
      rows,
      previousCursor: cursor,
    });
    const eventRows = Array.isArray(overview.domain_events)
      ? (overview.domain_events as TreeStreamDomainEventCursorRow[])
      : [];
    const newEventRows = collectNewDomainEvents({
      rows: eventRows,
      previousCursor: cursor,
    });
    const hasNewCommandRows = newRows.rows.length > 0;
    const hasNewDomainEventRows = newEventRows.rows.length > 0;
    if (hasNewCommandRows && hasNewDomainEventRows) {


previousCursor: cursor,
    });
    const eventRows = Array.isArray(overview.domain_events)
      ? (overview.domain_events as TreeStreamDomainEventCursorRow[])
      : [];
    const newEventRows = collectNewDomainEvents({
      rows: eventRows,
      previousCursor: cursor,
    });
    const hasNewCommandRows = newRows.rows.length > 0;
    const hasNewDomainEventRows = newEventRows.rows.length > 0;
    if (hasNewCommandRows && hasNewDomainEventRows) {
      const delta = resolveMatchingCommandDomainEventDelta({
        commandRows: newRows.rows,
        domainEventRows: newEventRows.rows,
        commandDrifted: newRows.drifted,
        domainEventDrifted: newEventRows.drifted,
      });
      if (delta) {
        cursor = nextCursor;
        yield {
          event: "delta",
          payload: buildTreeStreamDeltaEnvelope({
            workspaceId: input.workspaceId,
            rootNodeId: contract.rootNodeId,
            projection: contract.projection,
            cursor,
            overview,
            snapshot,
            delta,
          }),
        };
        continue;
      }

      snapshot = await input.loadSnapshot();
      cursor = nextCursor;
      yield {
        event: "resync",
        payload: buildTreeStreamEnvelope({
          kind: "resync",
          workspaceId: input.workspaceId,
          rootNodeId: contract.rootNodeId,
          projection: contract.projection,
          cursor,
          overview,
          snapshot,
        }),
      };
      continue;
    }

    if (!newRows.drifted && newRows.rows.length === 1) {
      const delta = readCommandPayloadDelta(newRows.rows[0] ?? {});
      if (delta) {
        cursor = nextCursor;
        yield {
          event: "delta",
          payload: buildTreeStreamDeltaEnvelope({
            workspaceId: input.workspaceId,


};
      continue;
    }

    if (!newRows.drifted && newRows.rows.length === 1) {
      const delta = readCommandPayloadDelta(newRows.rows[0] ?? {});
      if (delta) {
        cursor = nextCursor;
        yield {
          event: "delta",
          payload: buildTreeStreamDeltaEnvelope({
            workspaceId: input.workspaceId,
            rootNodeId: contract.rootNodeId,
            projection: contract.projection,
            cursor,
            overview,
            snapshot,
            delta,
          }),
        };
        continue;
      }
    }

    if (!newEventRows.drifted && newRows.rows.length === 0 && newEventRows.rows.length === 1) {
      const delta = readDomainEventPayloadDelta(newEventRows.rows[0] ?? {});
      if (delta) {
        cursor = nextCursor;
        yield {
          event: "delta",
          payload: buildTreeStreamDeltaEnvelope({
            workspaceId: input.workspaceId,
            rootNodeId: contract.rootNodeId,
            projection: contract.projection,
            cursor,
            overview,
            snapshot,
            delta,
          }),
        };
        continue;
      }
    }

    snapshot = await input.loadSnapshot();
    cursor = nextCursor;

    yield {
      event: "resync",
      payload: buildTreeStreamEnvelope({
        kind: "resync",
        workspaceId: input.workspaceId,
        rootNodeId: contract.rootNodeId,
        projection: contract.projection,
        cursor,
        overview,
        snapshot,
      }),
    };
  }


event: "resync",
      payload: buildTreeStreamEnvelope({
        kind: "resync",
        workspaceId: input.workspaceId,
        rootNodeId: contract.rootNodeId,
        projection: contract.projection,
        cursor,
        overview,
        snapshot,
      }),
    };
  }
}


import { describe, expect, it } from "vitest";
import type { SidebarInitialData } from "@/components/sidebar/types";
import {
  applyTreeStreamDelta,
  applyTreeStreamDeltaToProjectionState,
} from "./tree-delta";

const baseSidebarData: SidebarInitialData = {
  activeWorkspaceId: "ws_1",
  workspaces: [],
  documents: [
    {
      id: "root",
      workspace_id: "ws_1",
      title: "Root",
      parent_id: null,
      sort_order: 0,
      is_starred: false,
      access_scope: "private",
      is_template: false,
      created_at: "2026-04-18T00:00:00Z",
      updated_at: null,
    },
    {
      id: "child",
      workspace_id: "ws_1",
      title: "Child",
      parent_id: "root",
      sort_order: 1,
      is_starred: false,
      access_scope: "private",
      is_template: false,
      created_at: "2026-04-18T00:00:00Z",
      updated_at: null,
    },
  ],
  kernelFileTreeProjection: {
    projectionId: "kernel_projection:file_tree:root",
    projection: "file_tree",
    rootNodeId: null,
    items: [
      {
        rowId: "doc:root",
        rowKind: "document",
        nodeId: "root",
        parentNodeId: null,
        nodeType: "page",
        projectionKind: "file_tree",
        title: "Root",
        depth: 0,
        position: 0,
        childCount: 2,
        expandable: true,
        expandedByDefault: true,
        capabilities: ["expand", "open", "select"],
        resourceMeta: {
          resourceKind: "document",
          documentId: "root",
          workspaceId: "ws_1",
          iconHint: "page",


title: "Root",
        depth: 0,
        position: 0,
        childCount: 2,
        expandable: true,
        expandedByDefault: true,
        capabilities: ["expand", "open", "select"],
        resourceMeta: {
          resourceKind: "document",
          documentId: "root",
          workspaceId: "ws_1",
          iconHint: "page",
        },
        iconHint: "page",
      },
      {
        rowId: "index:root",
        rowKind: "index",
        nodeId: "index:root",
        parentNodeId: "root",
        nodeType: "index",
        projectionKind: "file_tree",
        title: "index.md",
        depth: 1,
        position: 0,
        childCount: 0,
        expandable: false,
        expandedByDefault: false,
        capabilities: ["open", "select"],
        resourceMeta: {
          resourceKind: "index",
          documentId: "root",
          workspaceId: "ws_1",
          iconHint: "index",
        },
        iconHint: "index",
      },
    ],
    edges: [],
  },
  kernelSidebarProjection: {
    projectionId: "kernel_projection:sidebar_tree:workspace_root",
    projection: "sidebar_tree",
    rootNodeId: null,
    items: [
      {
        nodeId: "root",
        parentNodeId: null,
        nodeType: "page",
        projectionKind: "sidebar_tree",
        title: "Root",
        depth: 0,
        position: 0,
        childCount: 1,
        expandable: true,
        expandedByDefault: true,
        capabilities: [
          "expand",
          "open",
          "drag",


nodeType: "page",
        projectionKind: "sidebar_tree",
        title: "Root",
        depth: 0,
        position: 0,
        childCount: 1,
        expandable: true,
        expandedByDefault: true,
        capabilities: [
          "expand",
          "open",
          "drag",
          "drop",
          "select",
          "create-child",
          "rename",
          "archive",
          "restore",
          "context-menu",
          "reorder",
        ],
        resourceMeta: {
          resourceKind: "document",
          documentId: "root",
          workspaceId: "ws_1",
          iconHint: "page",
        },
        iconHint: "page",
      },
      {
        nodeId: "child",
        parentNodeId: "root",
        nodeType: "page",
        projectionKind: "sidebar_tree",
        title: "Child",
        depth: 1,
        position: 1,
        childCount: 0,
        expandable: false,
        expandedByDefault: true,
        capabilities: [
          "open",
          "drag",
          "drop",
          "select",
          "create-child",
          "rename",
          "archive",
          "restore",
          "context-menu",
          "reorder",
        ],
        resourceMeta: {
          resourceKind: "document",
          documentId: "child",
          workspaceId: "ws_1",
          iconHint: "page",
        },
        iconHint: "page",
      },


"restore",
          "context-menu",
          "reorder",
        ],
        resourceMeta: {
          resourceKind: "document",
          documentId: "child",
          workspaceId: "ws_1",
          iconHint: "page",
        },
        iconHint: "page",
      },
    ],
    edges: [
      {
        id: "edge:root:child:parent_of",
        edgeType: "parent_of",
        workspaceId: "ws_1",
        fromNodeId: "root",
        toNodeId: "child",
      },
    ],
  },
  kernelSidebarTree: [],
  trashedDocuments: [],
  trashedMediaAssets: [],
  trashedMindmapAssets: [],
  trashedTableAssets: [],
  tableAssets: [],
  mindmapDocs: [],
  mindmapAssets: [],
  mindmapAssetChildren: {},
  mediaAssets: [],
};

const fileTreeProjectionBase: SidebarInitialData = {
  ...baseSidebarData,
  mediaAssets: [
    {
      id: "asset_pdf",
      workspace_id: "ws_1",
      document_id: "root",
      asset_type: "file",
      file_url: "/manual.pdf",
      thumbnail_url: null,
      bucket: null,
      storage_path: "documents/root/manual.pdf",
      file_name: "manual.pdf",
      file_size: 1024,
      mime_type: "application/pdf",
      ocr_payload: undefined,
      ocr_strategy: null,
      ocr_text: null,
      ocr_status: null,
      signed_url: null,
      created_at: "2026-04-18T00:00:00Z",
      updated_at: "2026-04-18T00:00:00Z",
    },
    {
      id: "asset_book",


file_size: 1024,
      mime_type: "application/pdf",
      ocr_payload: undefined,
      ocr_strategy: null,
      ocr_text: null,
      ocr_status: null,
      signed_url: null,
      created_at: "2026-04-18T00:00:00Z",
      updated_at: "2026-04-18T00:00:00Z",
    },
    {
      id: "asset_book",
      workspace_id: "ws_1",
      document_id: "root",
      asset_type: "file",
      file_url: "/novel.epub",
      thumbnail_url: null,
      bucket: null,
      storage_path: "documents/root/novel.epub",
      file_name: "novel.epub",
      file_size: 2048,
      mime_type: "application/epub+zip",
      ocr_payload: undefined,
      ocr_strategy: null,
      ocr_text: null,
      ocr_status: null,
      signed_url: null,
      created_at: "2026-04-18T00:00:00Z",
      updated_at: "2026-04-18T00:00:00Z",
    },
    {
      id: "asset_mindmap_child",
      workspace_id: "ws_1",
      document_id: "root",
      asset_type: "image",
      file_url: "/mindmap/concept.png",
      thumbnail_url: null,
      bucket: null,
      storage_path: "documents/root/mindmaps/asset_mindmap/concept.png",
      file_name: "concept.png",
      file_size: 512,
      mime_type: "image/png",
      ocr_payload: undefined,
      ocr_strategy: null,
      ocr_text: null,
      ocr_status: null,
      signed_url: null,
      created_at: "2026-04-18T00:00:00Z",
      updated_at: "2026-04-18T00:00:00Z",
    },
  ],
  tableAssets: [
    {
      id: "asset_table",
      workspace_id: "ws_1",
      document_id: "root",
      asset_type: "luckysheet",
      file_url: null,
      thumbnail_url: null,
      bucket: null,


updated_at: "2026-04-18T00:00:00Z",
    },
  ],
  tableAssets: [
    {
      id: "asset_table",
      workspace_id: "ws_1",
      document_id: "root",
      asset_type: "luckysheet",
      file_url: null,
      thumbnail_url: null,
      bucket: null,
      storage_path: null,
      file_name: "budget.luckysheet",
      file_size: null,
      mime_type: "application/json",
      ocr_payload: undefined,
      ocr_strategy: null,
      ocr_text: null,
      ocr_status: null,
      signed_url: null,
      created_at: "2026-04-18T00:00:00Z",
      updated_at: "2026-04-18T00:00:00Z",
    },
  ],
  mindmapAssets: [
    {
      id: "asset_mindmap",
      workspace_id: "ws_1",
      document_id: "root",
      asset_type: "mindmap",
      file_url: null,
      thumbnail_url: null,
      bucket: null,
      storage_path: null,
      file_name: "mindmap.json",
      file_size: null,
      mime_type: "application/json",
      ocr_payload: undefined,
      ocr_strategy: null,
      ocr_text: null,
      ocr_status: null,
      signed_url: null,
      created_at: "2026-04-18T00:00:00Z",
      updated_at: "2026-04-18T00:00:00Z",
    },
  ],
  mindmapAssetChildren: {
    asset_mindmap: ["asset_mindmap_child"],
  },
};

describe("tree-stream/tree-delta", () => {
  it("支持 upsert_document 重建 sidebar projection", () => {
    const next = applyTreeStreamDelta(baseSidebarData, {
      op: "upsert_document",
      document: {
        id: "leaf",
        workspace_id: "ws_1",
        title: "Leaf",


asset_mindmap: ["asset_mindmap_child"],
  },
};

describe("tree-stream/tree-delta", () => {
  it("支持 upsert_document 重建 sidebar projection", () => {
    const next = applyTreeStreamDelta(baseSidebarData, {
      op: "upsert_document",
      document: {
        id: "leaf",
        workspace_id: "ws_1",
        title: "Leaf",
        parent_id: "child",
        sort_order: 0,
        is_starred: false,
        access_scope: "private",
        is_template: false,
        created_at: "2026-04-18T00:00:00Z",
        updated_at: null,
      },
    });

    expect(next.documents.map((item) => item.id)).toEqual(["root", "child", "leaf"]);
    expect(next.kernelSidebarProjection.items.map((item) => item.nodeId)).toEqual([
      "root",
      "child",
      "leaf",
    ]);
  });

  it("支持 remove_document 级联移除子树", () => {
    const next = applyTreeStreamDelta(baseSidebarData, {
      op: "remove_document",
      documentId: "root",
    });

    expect(next.documents).toEqual([]);
    expect(next.kernelSidebarProjection.items).toEqual([]);
  });

  it("支持对已存在文档做局部 upsert patch，而不丢失原有排序字段", () => {
    const next = applyTreeStreamDelta(baseSidebarData, {
      op: "upsert_document",
      document: {
        id: "child",
        title: "Child Renamed",
        updated_at: "2026-04-18T00:05:00Z",
      },
    });

    expect(next.documents).toHaveLength(2);
    expect(next.documents.find((item) => item.id === "child")).toMatchObject({
      id: "child",
      title: "Child Renamed",
      parent_id: "root",
      sort_order: 1,
      workspace_id: "ws_1",
    });
  });


});

    expect(next.documents).toHaveLength(2);
    expect(next.documents.find((item) => item.id === "child")).toMatchObject({
      id: "child",
      title: "Child Renamed",
      parent_id: "root",
      sort_order: 1,
      workspace_id: "ws_1",
    });
  });

  it("支持 upsert_documents 批量新增复制出的子树", () => {
    const next = applyTreeStreamDelta(baseSidebarData, {
      op: "upsert_documents",
      upsertDocuments: [
        {
          id: "copy_root",
          workspace_id: "ws_1",
          title: "Copy Root",
          parent_id: null,
          sort_order: 2,
          is_starred: false,
          access_scope: "private",
          is_template: false,
          created_at: "2026-04-18T00:10:00Z",
          updated_at: "2026-04-18T00:10:00Z",
        },
        {
          id: "copy_child",
          workspace_id: "ws_1",
          title: "Copy Child",
          parent_id: "copy_root",
          sort_order: 0,
          is_starred: false,
          access_scope: "private",
          is_template: false,
          created_at: "2026-04-18T00:10:00Z",
          updated_at: "2026-04-18T00:10:00Z",
        },
      ],
    });

    expect(next.documents.map((item) => item.id)).toEqual([
      "root",
      "child",
      "copy_root",
      "copy_child",
    ]);
    expect(next.kernelSidebarProjection.items.map((item) => item.nodeId)).toEqual([
      "root",
      "child",
      "copy_root",
      "copy_child",
    ]);
    expect(
      next.kernelSidebarProjection.items.find((item) => item.nodeId === "copy_child"),
    ).toMatchObject({
      parentNodeId: "copy_root",
      depth: 1,


]);
    expect(next.kernelSidebarProjection.items.map((item) => item.nodeId)).toEqual([
      "root",
      "child",
      "copy_root",
      "copy_child",
    ]);
    expect(
      next.kernelSidebarProjection.items.find((item) => item.nodeId === "copy_child"),
    ).toMatchObject({
      parentNodeId: "copy_root",
      depth: 1,
    });
  });

  it("支持 move_document 细粒度更新父节点与排序字段", () => {
    const next = applyTreeStreamDelta(baseSidebarData, {
      op: "move_document",
      documentId: "child",
      parentId: null,
      sortOrder: 0,
      updatedAt: "2026-04-18T00:10:00Z",
    });

    expect(next.documents.find((item) => item.id === "child")).toMatchObject({
      id: "child",
      parent_id: null,
      sort_order: 0,
      updated_at: "2026-04-18T00:10:00Z",
    });
    expect(next.kernelSidebarProjection.items.map((item) => item.nodeId)).toEqual([
      "root",
      "child",
    ]);
    expect(
      next.kernelSidebarProjection.items.find((item) => item.nodeId === "child"),
    ).toMatchObject({
      parentNodeId: null,
      depth: 0,
    });
  });

  it("支持 replace_sidebar 直接切换到 resync snapshot", () => {
    const next = applyTreeStreamDelta(baseSidebarData, {
      op: "replace_sidebar",
      sidebar: {
        activeWorkspaceId: "ws_1",
        workspaces: [],
        documents: [],
        kernelSidebarProjection: {
          projectionId: "kernel_projection:sidebar_tree:workspace_root",
          projection: "sidebar_tree",
          rootNodeId: null,
          items: [],
          edges: [],
        },
        kernelFileTreeProjection: {
          projectionId: "kernel_projection:file_tree:root",
          projection: "file_tree",
          rootNodeId: null,


documents: [],
        kernelSidebarProjection: {
          projectionId: "kernel_projection:sidebar_tree:workspace_root",
          projection: "sidebar_tree",
          rootNodeId: null,
          items: [],
          edges: [],
        },
        kernelFileTreeProjection: {
          projectionId: "kernel_projection:file_tree:root",
          projection: "file_tree",
          rootNodeId: null,
          items: [],
          edges: [],
        },
        kernelSidebarTree: [],
        trashedDocuments: [],
        trashedMediaAssets: [],
        trashedMindmapAssets: [],
        trashedTableAssets: [],
        tableAssets: [],
        mindmapDocs: [],
        mindmapAssets: [],
        mindmapAssetChildren: {},
        mediaAssets: [],
      },
    });

    expect(next.documents).toEqual([]);
    expect(next.kernelSidebarProjection.items).toEqual([]);
  });

  it("支持 noop delta 仅推进 cursor，不修改当前 sidebar snapshot", () => {
    const next = applyTreeStreamDelta(baseSidebarData, {
      op: "noop",
    });

    expect(next).toEqual(baseSidebarData);
  });

  it("支持 resync_required delta 仅推进 cursor，等待后续 snapshot/resync", () => {
    const next = applyTreeStreamDelta(baseSidebarData, {
      op: "resync_required",
    });

    expect(next).toEqual(baseSidebarData);
  });

  it("为 page_tree 定义统一 delta 应用边界，并可稳定派生页面行", () => {
    const next = applyTreeStreamDeltaToProjectionState({
      projection: "page_tree",
      base: baseSidebarData,
      event: {
        op: "upsert_document",
        document: {
          id: "leaf",
          workspace_id: "ws_1",
          title: "Leaf",
          parent_id: "child",
          sort_order: 0,


it("为 page_tree 定义统一 delta 应用边界，并可稳定派生页面行", () => {
    const next = applyTreeStreamDeltaToProjectionState({
      projection: "page_tree",
      base: baseSidebarData,
      event: {
        op: "upsert_document",
        document: {
          id: "leaf",
          workspace_id: "ws_1",
          title: "Leaf",
          parent_id: "child",
          sort_order: 0,
          is_starred: false,
          access_scope: "private",
          is_template: false,
          created_at: "2026-04-18T00:00:00Z",
          updated_at: null,
        },
      },
    });

    expect(next.projection).toBe("page_tree");
    expect(next.documents.map((item) => item.id)).toEqual(["root", "child", "leaf"]);
    expect(next.pageTreeItems.map((item) => item.nodeId)).toEqual(["root", "child", "leaf"]);
    expect(next.pageTreeItems.find((item) => item.nodeId === "leaf")).toMatchObject({
      parentNodeId: "child",
      depth: 2,
      title: "Leaf",
    });
  });

  it("为 file_tree 定义统一 delta 应用边界，并保留 doc/index/asset-folder/asset 行语义", () => {
    const next = applyTreeStreamDeltaToProjectionState({
      projection: "file_tree",
      base: fileTreeProjectionBase,
      event: {
        op: "replace_documents",
        documents: [...fileTreeProjectionBase.documents],
      },
    });

    expect(next.projection).toBe("file_tree");
    expect(next.fileTreeItems.map((item) => item.rowKind)).toEqual(
      expect.arrayContaining(["document", "index", "asset_folder", "asset"]),
    );
    expect(
      next.fileTreeItems.find((item) => item.resourceMeta.assetId === "asset_mindmap"),
    ).toMatchObject({
      rowKind: "asset_folder",
      iconHint: "mindmap",
      resourceMeta: expect.objectContaining({
        resourceKind: "mindmap",
        assetKind: "mindmap",
      }),
    });
    expect(
      next.fileTreeItems.find((item) => item.resourceMeta.assetId === "asset_pdf"),
    ).toMatchObject({
      rowKind: "asset",
      iconHint: "pdf",


rowKind: "asset_folder",
      iconHint: "mindmap",
      resourceMeta: expect.objectContaining({
        resourceKind: "mindmap",
        assetKind: "mindmap",
      }),
    });
    expect(
      next.fileTreeItems.find((item) => item.resourceMeta.assetId === "asset_pdf"),
    ).toMatchObject({
      rowKind: "asset",
      iconHint: "pdf",
      resourceMeta: expect.objectContaining({
        resourceKind: "pdf",
        assetKind: "pdf",
      }),
    });
    expect(
      next.fileTreeItems.find((item) => item.resourceMeta.assetId === "asset_book"),
    ).toMatchObject({
      rowKind: "asset",
      iconHint: "book",
      resourceMeta: expect.objectContaining({
        resourceKind: "book",
        assetKind: "book",
      }),
    });
    expect(
      next.fileTreeItems.find((item) => item.resourceMeta.assetId === "asset_table"),
    ).toMatchObject({
      rowKind: "asset",
      iconHint: "table",
      resourceMeta: expect.objectContaining({
        resourceKind: "table",
        assetKind: "table",
      }),
    });
  });

  it("file_tree fixture 覆盖搜索 hardening 需要的 index、asset-folder、mindmap child、book 与 pdf 资源类型", () => {
    const next = applyTreeStreamDeltaToProjectionState({
      projection: "file_tree",
      base: fileTreeProjectionBase,
      event: {
        op: "replace_documents",
        documents: [...fileTreeProjectionBase.documents],
      },
    });
    const rowById = new Map(next.fileTreeItems.map((item) => [item.rowId, item]));

    expect(rowById.get("index:root")).toMatchObject({
      rowKind: "index",
      title: "index.md",
      resourceMeta: expect.objectContaining({
        resourceKind: "index",
        documentId: "root",
      }),
    });
    expect(rowById.get("asset-folder:asset_mindmap")).toMatchObject({
      rowKind: "asset_folder",


const rowById = new Map(next.fileTreeItems.map((item) => [item.rowId, item]));

    expect(rowById.get("index:root")).toMatchObject({
      rowKind: "index",
      title: "index.md",
      resourceMeta: expect.objectContaining({
        resourceKind: "index",
        documentId: "root",
      }),
    });
    expect(rowById.get("asset-folder:asset_mindmap")).toMatchObject({
      rowKind: "asset_folder",
      title: "mindmap",
      resourceMeta: expect.objectContaining({
        resourceKind: "mindmap",
        assetKind: "mindmap",
      }),
    });
    expect(rowById.get("asset:asset_mindmap_child")).toMatchObject({
      rowKind: "asset",
      parentNodeId: "asset-folder:asset_mindmap",
      resourceMeta: expect.objectContaining({
        resourceKind: "asset",
        assetKind: "image",
      }),
    });
    expect(rowById.get("asset:asset_book")).toMatchObject({
      rowKind: "asset",
      iconHint: "book",
      resourceMeta: expect.objectContaining({
        resourceKind: "book",
        assetKind: "book",
      }),
    });
    expect(rowById.get("asset:asset_pdf")).toMatchObject({
      rowKind: "asset",
      iconHint: "pdf",
      resourceMeta: expect.objectContaining({
        resourceKind: "pdf",
        assetKind: "pdf",
      }),
    });
  });

  it("支持 upsert_assets 更新资源归属并重建 file_tree projection", () => {
    const next = applyTreeStreamDeltaToProjectionState({
      projection: "file_tree",
      base: fileTreeProjectionBase,
      event: {
        op: "upsert_assets",
        upsertAssets: [
          {
            id: "asset_pdf",
            workspace_id: "ws_1",
            document_id: "child",
            asset_type: "file",
            file_url: "/manual.pdf",
            thumbnail_url: "/manual.pdf",
            bucket: null,
            storage_path: "documents/child/manual.pdf",


event: {
        op: "upsert_assets",
        upsertAssets: [
          {
            id: "asset_pdf",
            workspace_id: "ws_1",
            document_id: "child",
            asset_type: "file",
            file_url: "/manual.pdf",
            thumbnail_url: "/manual.pdf",
            bucket: null,
            storage_path: "documents/child/manual.pdf",
            file_name: "manual.pdf",
            file_size: 1024,
            mime_type: "application/pdf",
            ocr_text: null,
            ocr_status: null,
            created_at: "2026-04-18T00:00:00Z",
            updated_at: "2026-04-26T00:00:00Z",
          },
        ],
      },
    });

    expect(next.sidebar.mediaAssets?.find((asset) => asset.id === "asset_pdf")).toMatchObject({
      document_id: "child",
      updated_at: "2026-04-26T00:00:00Z",
    });
    expect(
      next.fileTreeItems.find((item) => item.resourceMeta.assetId === "asset_pdf"),
    ).toMatchObject({
      parentNodeId: "child",
      resourceMeta: expect.objectContaining({
        documentId: "child",
        resourceKind: "pdf",
      }),
    });
  });
});


import type { SidebarInitialData } from "@/components/sidebar/types";
import { buildKernelFileTreeProjection } from "@/lib/kernel-file-tree";
import type { SidebarDatasetListQueryResult } from "@/lib/sidebar-data";
import {
  buildSidebarDatasetListQueryResult,
  mapSidebarDatasetListQueryResultToInitialData,
} from "@/lib/sidebar-data";
import type { DocumentRecord } from "@/lib/documents";
import {
  buildSidebarTreeFromKernelProjection,
  type KernelSidebarProjectionItem,
} from "@/lib/kernel-sidebar";
import type { KernelFileTreeProjectionItem } from "@/lib/kernel-file-tree";
import {
  buildPageTreeProjectionItems,
  type PageTreeProjectionItem,
} from "@/lib/tree-projection";
import type { WorkspaceSummary } from "@/lib/workspaces";
import type { MediaAsset } from "@/types/media";

export type TreeStreamDocumentPatch =
  Partial<DocumentRecord> & Pick<DocumentRecord, "id">;

export type TreeStreamDeltaOp =
  | "noop"
  | "resync_required"
  | "upsert_document"
  | "upsert_documents"
  | "upsert_assets"
  | "remove_document"
  | "move_document"
  | "replace_documents"
  | "replace_sidebar";

export type TreeStreamDeltaEvent = {
  op: TreeStreamDeltaOp;
  node?: TreeStreamDocumentPatch | null;
  document?: TreeStreamDocumentPatch | null;
  documentId?: string | null;
  parentId?: string | null;
  sortOrder?: number | null;
  updatedAt?: string | null;
  upsertDocuments?: TreeStreamDocumentPatch[] | null;
  upsertAssets?: MediaAsset[] | null;
  documents?: DocumentRecord[] | null;
  sidebar?: SidebarDatasetListQueryResult | SidebarInitialData | null;
};

export type TreeRendererProjection = "sidebar_tree" | "page_tree" | "file_tree";

export type TreeRendererDeltaState = {
  projection: TreeRendererProjection;
  sidebar: SidebarInitialData;
  documents: DocumentRecord[];
  sidebarItems: KernelSidebarProjectionItem[];
  pageTreeItems: PageTreeProjectionItem[];
  fileTreeItems: KernelFileTreeProjectionItem[];
};

function cloneSidebarData(data: SidebarInitialData): SidebarInitialData {


export type TreeRendererProjection = "sidebar_tree" | "page_tree" | "file_tree";

export type TreeRendererDeltaState = {
  projection: TreeRendererProjection;
  sidebar: SidebarInitialData;
  documents: DocumentRecord[];
  sidebarItems: KernelSidebarProjectionItem[];
  pageTreeItems: PageTreeProjectionItem[];
  fileTreeItems: KernelFileTreeProjectionItem[];
};

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) => ({


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

  if (
    (input.base.mediaAssets?.length ?? 0) > 0 ||
    (input.base.mindmapAssets?.length ?? 0) > 0 ||
    (input.base.tableAssets?.length ?? 0) > 0 ||
    Object.keys(input.base.mindmapAssetChildren ?? {}).length > 0
  ) {
    // 说明：stream delta 只替换 documents 时，仍要保留已有资源树语义；
    // 否则 mindmap 子附件会在 resync 前退化成普通 asset。
    const nextFileTreeProjection = buildKernelFileTreeProjection({
      documents: input.documents,
      mediaAssets: input.base.mediaAssets,
      mindmapAssets: input.base.mindmapAssets,
      tableAssets: input.base.tableAssets,
      mindmapAssetChildren: input.base.mindmapAssetChildren,
    });
    queryResult.kernel_file_tree_projection = nextFileTreeProjection;
    queryResult.kernelFileTreeProjection = nextFileTreeProjection;
    queryResult.mindmap_asset_children = { ...(input.base.mindmapAssetChildren ?? {}) };
  }

  return mapSidebarDatasetListQueryResultToInitialData(queryResult);
}

function normalizeUpsertDocument(event: TreeStreamDeltaEvent): TreeStreamDocumentPatch | null {
  const candidate = event.node ?? event.document ?? null;
  if (!candidate || typeof candidate !== "object") {
    return null;
  }
  return typeof candidate.id === "string" && candidate.id.trim()
    ? ({ ...candidate, id: candidate.id.trim() } as TreeStreamDocumentPatch)
    : null;
}

function normalizeDocumentId(event: TreeStreamDeltaEvent): string | null {
  const candidate = typeof event.documentId === "string" ? event.documentId.trim() : "";


function normalizeUpsertDocument(event: TreeStreamDeltaEvent): TreeStreamDocumentPatch | null {
  const candidate = event.node ?? event.document ?? null;
  if (!candidate || typeof candidate !== "object") {
    return null;
  }
  return typeof candidate.id === "string" && candidate.id.trim()
    ? ({ ...candidate, id: candidate.id.trim() } as TreeStreamDocumentPatch)
    : null;
}

function normalizeDocumentId(event: TreeStreamDeltaEvent): string | null {
  const candidate = typeof event.documentId === "string" ? event.documentId.trim() : "";
  return candidate || null;
}

function isCompleteDocumentRecord(value: TreeStreamDocumentPatch): value is DocumentRecord {
  return (
    typeof value.workspace_id === "string" &&
    typeof value.access_scope === "string" &&
    typeof value.is_template === "boolean" &&
    typeof value.created_at === "string" &&
    "parent_id" in value &&
    "sort_order" in value &&
    "is_starred" in value &&
    "updated_at" in value
  );
}

function isCompleteMediaAsset(value: unknown): value is MediaAsset {
  if (!value || typeof value !== "object") {
    return false;
  }
  const asset = value as MediaAsset;
  return (
    typeof asset.id === "string" &&
    typeof asset.workspace_id === "string" &&
    typeof asset.document_id === "string" &&
    typeof asset.asset_type === "string" &&
    typeof asset.created_at === "string" &&
    typeof asset.updated_at === "string" &&
    "file_name" in value &&
    "file_url" in value &&
    "thumbnail_url" in value &&
    "file_size" in value &&
    "mime_type" in value
  );
}

function buildSidebarFromAssets(input: {
  base: SidebarInitialData;
  mediaAssets: MediaAsset[];
}): SidebarInitialData {
  const next = cloneSidebarData(input.base);
  next.mediaAssets = [...input.mediaAssets];
  next.kernelFileTreeProjection = buildKernelFileTreeProjection({
    documents: next.documents,
    mediaAssets: next.mediaAssets,
    mindmapAssets: next.mindmapAssets,
    tableAssets: next.tableAssets,
    mindmapAssetChildren: next.mindmapAssetChildren,


function buildSidebarFromAssets(input: {
  base: SidebarInitialData;
  mediaAssets: MediaAsset[];
}): SidebarInitialData {
  const next = cloneSidebarData(input.base);
  next.mediaAssets = [...input.mediaAssets];
  next.kernelFileTreeProjection = buildKernelFileTreeProjection({
    documents: next.documents,
    mediaAssets: next.mediaAssets,
    mindmapAssets: next.mindmapAssets,
    tableAssets: next.tableAssets,
    mindmapAssetChildren: next.mindmapAssetChildren,
  });
  return next;
}

export function deriveTreeRendererDeltaState(input: {
  projection: TreeRendererProjection;
  sidebar: SidebarInitialData;
}): TreeRendererDeltaState {
  const sidebar = input.sidebar;
  const pageTreeSource =
    sidebar.kernelSidebarTree.length > 0
      ? sidebar.kernelSidebarTree
      : buildSidebarTreeFromKernelProjection({
          records: sidebar.documents,
          projection: sidebar.kernelSidebarProjection,
        });

  return {
    projection: input.projection,
    sidebar,
    documents: [...sidebar.documents],
    sidebarItems: [...sidebar.kernelSidebarProjection.items],
    pageTreeItems: buildPageTreeProjectionItems(pageTreeSource),
    fileTreeItems: [...sidebar.kernelFileTreeProjection.items],
  };
}

export function applyTreeStreamDelta(
  base: SidebarInitialData,
  event: TreeStreamDeltaEvent,
): SidebarInitialData {
  if (event.op === "noop") {
    return base;
  }

  if (event.op === "resync_required") {
    return base;
  }

  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({


return base;
  }

  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 documentPatch = normalizeUpsertDocument(event);
    if (!documentPatch) {
      return base;
    }
    const nextDocuments = [...base.documents];
    const existingIndex = nextDocuments.findIndex((item) => item.id === documentPatch.id);
    if (existingIndex >= 0) {
      nextDocuments[existingIndex] = {
        ...nextDocuments[existingIndex],
        ...documentPatch,
      };
    } else {
      if (!isCompleteDocumentRecord(documentPatch)) {
        return base;
      }
      nextDocuments.push(documentPatch);
    }
    return buildSidebarFromDocuments({
      base,
      documents: nextDocuments,
    });
  }

  if (event.op === "upsert_documents") {
    const patches = Array.isArray(event.upsertDocuments) ? event.upsertDocuments : [];
    if (patches.length === 0) {
      return base;
    }
    let changed = false;
    const nextDocuments = [...base.documents];
    for (const rawPatch of patches) {
      if (!rawPatch || typeof rawPatch.id !== "string" || !rawPatch.id.trim()) {
        continue;
      }
      const documentPatch = {
        ...rawPatch,
        id: rawPatch.id.trim(),
      } as TreeStreamDocumentPatch;
      const existingIndex = nextDocuments.findIndex((item) => item.id === documentPatch.id);
      if (existingIndex >= 0) {
        nextDocuments[existingIndex] = {
          ...nextDocuments[existingIndex],


for (const rawPatch of patches) {
      if (!rawPatch || typeof rawPatch.id !== "string" || !rawPatch.id.trim()) {
        continue;
      }
      const documentPatch = {
        ...rawPatch,
        id: rawPatch.id.trim(),
      } as TreeStreamDocumentPatch;
      const existingIndex = nextDocuments.findIndex((item) => item.id === documentPatch.id);
      if (existingIndex >= 0) {
        nextDocuments[existingIndex] = {
          ...nextDocuments[existingIndex],
          ...documentPatch,
        };
        changed = true;
        continue;
      }
      if (!isCompleteDocumentRecord(documentPatch)) {
        continue;
      }
      nextDocuments.push(documentPatch);
      changed = true;
    }
    if (!changed) {
      return base;
    }
    return buildSidebarFromDocuments({
      base,
      documents: nextDocuments,
    });
  }

  if (event.op === "remove_document") {
    const documentId = normalizeDocumentId(event);
    if (!documentId) {
      return base;
    }
    const removedIds = new Set<string>([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)),
    });
  }

  if (event.op === "move_document") {
    const documentId = normalizeDocumentId(event);
    if (!documentId) {
      return base;
    }
    const existingIndex = base.documents.findIndex((item) => item.id === documentId);


return buildSidebarFromDocuments({
      base,
      documents: base.documents.filter((item) => !removedIds.has(item.id)),
    });
  }

  if (event.op === "move_document") {
    const documentId = normalizeDocumentId(event);
    if (!documentId) {
      return base;
    }
    const existingIndex = base.documents.findIndex((item) => item.id === documentId);
    if (existingIndex < 0) {
      return base;
    }
    const nextDocuments = [...base.documents];
    const existing = nextDocuments[existingIndex]!;
    nextDocuments[existingIndex] = {
      ...existing,
      parent_id: "parentId" in event ? (event.parentId ?? null) : existing.parent_id,
      sort_order:
        typeof event.sortOrder === "number" && Number.isFinite(event.sortOrder)
          ? event.sortOrder
          : existing.sort_order,
      updated_at:
        typeof event.updatedAt === "string" && event.updatedAt.trim()
          ? event.updatedAt.trim()
          : existing.updated_at,
    };
    return buildSidebarFromDocuments({
      base,
      documents: nextDocuments,
    });
  }

  if (event.op === "upsert_assets") {
    const assets = Array.isArray(event.upsertAssets) ? event.upsertAssets : [];
    if (assets.length === 0) {
      return base;
    }
    let changed = false;
    const nextAssets = [...(base.mediaAssets ?? [])];
    for (const asset of assets) {
      if (!isCompleteMediaAsset(asset)) {
        continue;
      }
      const existingIndex = nextAssets.findIndex((item) => item.id === asset.id);
      if (existingIndex >= 0) {
        nextAssets[existingIndex] = {
          ...nextAssets[existingIndex],
          ...asset,
        };
      } else {
        nextAssets.push(asset);
      }
      changed = true;
    }
    if (!changed) {
      return base;
    }


nextAssets[existingIndex] = {
          ...nextAssets[existingIndex],
          ...asset,
        };
      } else {
        nextAssets.push(asset);
      }
      changed = true;
    }
    if (!changed) {
      return base;
    }
    return buildSidebarFromAssets({
      base,
      mediaAssets: nextAssets,
    });
  }

  return base;
}

export function applyTreeStreamDeltaToProjectionState(input: {
  projection: TreeRendererProjection;
  base: SidebarInitialData;
  event: TreeStreamDeltaEvent;
}): TreeRendererDeltaState {
  return deriveTreeRendererDeltaState({
    projection: input.projection,
    sidebar: applyTreeStreamDelta(input.base, input.event),
  });
}


import { afterEach, describe, expect, it, vi } from "vitest";
import {
  buildWorkspaceTreeStreamUrl,
  normalizeTreeStreamSnapshot,
  parseTreeStreamMessage,
} from "./protocol";

describe("tree-stream/protocol", () => {
  afterEach(() => {
    vi.unstubAllGlobals();
  });

  it("构造 workspace sidebar stream url", () => {
    vi.stubGlobal("window", {
      ...window,
      location: {
        ...window.location,
        origin: "http://127.0.0.1:3000",
      },
    });
    expect(
      buildWorkspaceTreeStreamUrl(" ws_1 ", "evt_9"),
    ).toBe(
      "http://127.0.0.1:3000/api/tree/events?workspaceId=ws_1&cursor=evt_9",
    );
  });

  it("固定走同源 stream route", () => {
    vi.stubGlobal("window", {
      ...window,
      location: {
        ...window.location,
        origin: "http://127.0.0.1:3000",
      },
    });
    expect(
      buildWorkspaceTreeStreamUrl("ws_1", null),
    ).toBe("http://127.0.0.1:3000/api/tree/events?workspaceId=ws_1");
  });

  it("解析 snapshot / delta / resync 协议消息", () => {
    expect(
      parseTreeStreamMessage({
        eventType: "snapshot",
        rawData: JSON.stringify({
          stream: "workspace",
          workspaceId: "ws_1",
          cursor: "evt_1",
          projection: "sidebar_tree",
          data: {
            active_workspace_id: "ws_1",
            workspaces: [],
            documents: [],
            kernel_file_tree_projection: {
              projectionId: "kernel_projection:file_tree:root",
              projection: "file_tree",
              rootNodeId: null,
              items: [],
              edges: [],
            },


projection: "sidebar_tree",
          data: {
            active_workspace_id: "ws_1",
            workspaces: [],
            documents: [],
            kernel_file_tree_projection: {
              projectionId: "kernel_projection:file_tree:root",
              projection: "file_tree",
              rootNodeId: null,
              items: [],
              edges: [],
            },
            kernel_sidebar_projection: {
              projectionId: "kernel_projection:sidebar_tree:workspace_root",
              projection: "sidebar_tree",
              rootNodeId: null,
              items: [],
              edges: [],
            },
            trashed_documents: [],
            media_assets: [],
            trashed_media_assets: [],
            mindmap_assets: [],
            trashed_mindmap_assets: [],
            table_assets: [],
            trashed_table_assets: [],
            mindmap_docs: [],
            mindmap_asset_children: {},
          },
        }),
      }),
    ).toMatchObject({
      kind: "snapshot",
      stream: "workspace",
      workspaceId: "ws_1",
      cursor: "evt_1",
      projection: "sidebar_tree",
    });

    expect(
      parseTreeStreamMessage({
        eventType: "heartbeat",
        rawData: JSON.stringify({
          kind: "heartbeat",
          workspaceId: "ws_1",
          cursor: "evt_3",
        }),
      }),
    ).toMatchObject({
      kind: "heartbeat",
      workspaceId: "ws_1",
      cursor: "evt_3",
    });

    expect(
      parseTreeStreamMessage({
        eventType: "delta",
        rawData: JSON.stringify({
          kind: "delta",
          workspace_id: "ws_1",


).toMatchObject({
      kind: "heartbeat",
      workspaceId: "ws_1",
      cursor: "evt_3",
    });

    expect(
      parseTreeStreamMessage({
        eventType: "delta",
        rawData: JSON.stringify({
          kind: "delta",
          workspace_id: "ws_1",
          cursor: "evt_2",
          payload: { op: "remove_document", documentId: "doc_1" },
        }),
      }),
    ).toMatchObject({
      kind: "delta",
      workspaceId: "ws_1",
      cursor: "evt_2",
      data: { op: "remove_document", documentId: "doc_1" },
    });
  });

  it("把 sidebar.dataset.list snapshot 归一化成 SidebarInitialData", () => {
    const snapshot = normalizeTreeStreamSnapshot({
      active_workspace_id: "ws_1",
      workspaces: [],
      documents: [],
      kernel_file_tree_projection: {
        projectionId: "kernel_projection:file_tree:root",
        projection: "file_tree",
        rootNodeId: null,
        items: [],
        edges: [],
      },
      kernel_sidebar_projection: {
        projectionId: "kernel_projection:sidebar_tree:workspace_root",
        projection: "sidebar_tree",
        rootNodeId: null,
        items: [],
        edges: [],
      },
      trashed_documents: [],
      media_assets: [],
      trashed_media_assets: [],
      mindmap_assets: [],
      trashed_mindmap_assets: [],
      table_assets: [],
      trashed_table_assets: [],
      mindmap_docs: [],
      mindmap_asset_children: {},
    });

    expect(snapshot).toMatchObject({
      activeWorkspaceId: "ws_1",
      kernelFileTreeProjection: {
        projection: "file_tree",
      },
      kernelSidebarProjection: {


table_assets: [],
      trashed_table_assets: [],
      mindmap_docs: [],
      mindmap_asset_children: {},
    });

    expect(snapshot).toMatchObject({
      activeWorkspaceId: "ws_1",
      kernelFileTreeProjection: {
        projection: "file_tree",
      },
      kernelSidebarProjection: {
        projection: "sidebar_tree",
      },
    });
  });
});


import { act, useEffect } from "react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createRoot, type Root } from "react-dom/client";
import { useSidebarTreeStream } from "./use-sidebar-tree-stream";
import type { SidebarInitialData } from "@/components/sidebar/types";

type MockEventListener = (event: MessageEvent<string>) => void;

class MockEventSource {
  static instances: MockEventSource[] = [];

  readonly url: string;
  readonly withCredentials: boolean;
  readonly listeners = new Map<string, Set<MockEventListener>>();
  onmessage: MockEventListener | null = null;
  onerror: (() => void) | null = null;
  closed = false;

  constructor(url: string, options?: EventSourceInit) {
    this.url = url;
    this.withCredentials = Boolean(options?.withCredentials);
    MockEventSource.instances.push(this);
  }

  addEventListener(type: string, listener: EventListener) {
    const typedListener = listener as unknown as MockEventListener;
    const next = this.listeners.get(type) ?? new Set<MockEventListener>();
    next.add(typedListener);
    this.listeners.set(type, next);
  }

  removeEventListener(type: string, listener: EventListener) {
    const typedListener = listener as unknown as MockEventListener;
    this.listeners.get(type)?.delete(typedListener);
  }

  close() {
    this.closed = true;
  }

  emit(type: string, payload: unknown) {
    const event = {
      type,
      data: JSON.stringify(payload),
    } as MessageEvent<string>;
    this.listeners.get(type)?.forEach((listener) => listener(event));
    if (type === "message" && this.onmessage) {
      this.onmessage(event);
    }
  }
}

function flush() {
  return new Promise((resolve) => {
    setTimeout(resolve, 0);
  });
}

function buildInitialData(): SidebarInitialData {
  return {


}
  }
}

function flush() {
  return new Promise((resolve) => {
    setTimeout(resolve, 0);
  });
}

function buildInitialData(): SidebarInitialData {
  return {
    activeWorkspaceId: "ws_1",
    workspaces: [],
    documents: [],
    kernelSidebarProjection: {
      projectionId: "kernel_projection:sidebar_tree:workspace_root",
      projection: "sidebar_tree",
      rootNodeId: null,
      items: [],
      edges: [],
    },
    kernelSidebarTree: [],
    kernelFileTreeProjection: {
      projectionId: "kernel_projection:file_tree:workspace_root",
      projection: "file_tree",
      rootNodeId: null,
      items: [],
      edges: [],
    },
    trashedDocuments: [],
    trashedMediaAssets: [],
    trashedMindmapAssets: [],
    trashedTableAssets: [],
    tableAssets: [],
    mindmapDocs: [],
    mindmapAssets: [],
    mindmapAssetChildren: {},
    mediaAssets: [],
  };
}

function buildSnapshotEnvelope(title = "工作区首页") {
  return {
    stream: "workspace",
    workspaceId: "ws_1",
    cursor: "evt_2",
    projection: "sidebar_tree",
    data: {
      active_workspace_id: "ws_1",
      workspaces: [],
      documents: [
        {
          id: "page_root",
          workspace_id: "ws_1",
          title,
          parent_id: null,
          sort_order: 0,
          is_starred: false,
          access_scope: "private",


data: {
      active_workspace_id: "ws_1",
      workspaces: [],
      documents: [
        {
          id: "page_root",
          workspace_id: "ws_1",
          title,
          parent_id: null,
          sort_order: 0,
          is_starred: false,
          access_scope: "private",
          is_template: false,
          created_at: "2026-04-24T00:00:00Z",
          updated_at: "2026-04-24T00:00:00Z",
        },
      ],
      kernel_file_tree_projection: {
        projectionId: "kernel_projection:file_tree:root",
        projection: "file_tree",
        rootNodeId: null,
        items: [],
        edges: [],
      },
      kernel_sidebar_projection: {
        projectionId: "kernel_projection:sidebar_tree:workspace_root",
        projection: "sidebar_tree",
        rootNodeId: null,
        items: [],
        edges: [],
      },
      trashed_documents: [],
      media_assets: [],
      trashed_media_assets: [],
      mindmap_assets: [],
      trashed_mindmap_assets: [],
      table_assets: [],
      trashed_table_assets: [],
      mindmap_docs: [],
      mindmap_asset_children: {},
    },
  };
}

function Harness({ onState }: { onState: (state: ReturnType<typeof useSidebarTreeStream>) => void }) {
  const state = useSidebarTreeStream(buildInitialData());

  useEffect(() => {
    onState(state);
  }, [onState, state]);

  return null;
}

describe("useSidebarTreeStream", () => {
  let container: HTMLDivElement;
  let root: Root;
  const onState = vi.fn();

  beforeEach(() => {


onState(state);
  }, [onState, state]);

  return null;
}

describe("useSidebarTreeStream", () => {
  let container: HTMLDivElement;
  let root: Root;
  const onState = vi.fn();

  beforeEach(() => {
    (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT =
      true;
    vi.stubGlobal("window", {
      ...window,
      location: {
        ...window.location,
        origin: "http://127.0.0.1:3000",
      },
    });
    MockEventSource.instances = [];
    vi.stubGlobal("EventSource", MockEventSource as unknown as typeof EventSource);
    container = document.createElement("div");
    document.body.appendChild(container);
    root = createRoot(container);
    onState.mockClear();
  });

  afterEach(() => {
    act(() => {
      root.unmount();
    });
    container.remove();
  });

  it("收到带 cursor 的 snapshot 后不应重建 EventSource", async () => {
    await act(async () => {
      root.render(<Harness onState={onState} />);
      await flush();
      await flush();
    });

    expect(MockEventSource.instances).toHaveLength(1);

    await act(async () => {
      MockEventSource.instances[0]?.emit("snapshot", {
        stream: "workspace",
        workspaceId: "ws_1",
        cursor: "evt_2",
        projection: "sidebar_tree",
        data: {
          active_workspace_id: "ws_1",
          workspaces: [],
          documents: [],
          kernel_sidebar_projection: {
            projectionId: "kernel_projection:sidebar_tree:workspace_root",
            projection: "sidebar_tree",
            rootNodeId: null,
            items: [],


workspaceId: "ws_1",
        cursor: "evt_2",
        projection: "sidebar_tree",
        data: {
          active_workspace_id: "ws_1",
          workspaces: [],
          documents: [],
          kernel_sidebar_projection: {
            projectionId: "kernel_projection:sidebar_tree:workspace_root",
            projection: "sidebar_tree",
            rootNodeId: null,
            items: [],
            edges: [],
          },
          trashed_documents: [],
          media_assets: [],
          trashed_media_assets: [],
          mindmap_assets: [],
          trashed_mindmap_assets: [],
          table_assets: [],
          trashed_table_assets: [],
          mindmap_docs: [],
          mindmap_asset_children: {},
        },
      });
      await flush();
      await flush();
    });

    expect(MockEventSource.instances).toHaveLength(1);
    expect(onState).toHaveBeenLastCalledWith(
      expect.objectContaining({
        cursor: "evt_2",
        status: "live",
      }),
    );
  });

  it("首帧前连接报错时应进入 fallback", async () => {
    await act(async () => {
      root.render(<Harness onState={onState} />);
      await flush();
      await flush();
    });

    expect(MockEventSource.instances).toHaveLength(1);

    await act(async () => {
      MockEventSource.instances[0]?.onerror?.();
      await flush();
      await flush();
    });

    expect(onState).toHaveBeenLastCalledWith(
      expect.objectContaining({
        data: null,
        status: "fallback",
        cursor: null,
        error: expect.any(Error),
      }),


MockEventSource.instances[0]?.onerror?.();
      await flush();
      await flush();
    });

    expect(onState).toHaveBeenLastCalledWith(
      expect.objectContaining({
        data: null,
        status: "fallback",
        cursor: null,
        error: expect.any(Error),
      }),
    );
    expect(MockEventSource.instances[0]?.closed).toBe(true);
  });

  it("resync_required delta 只推进 cursor，后续 resync 才替换数据", async () => {
    await act(async () => {
      root.render(<Harness onState={onState} />);
      await flush();
      await flush();
    });

    expect(MockEventSource.instances).toHaveLength(1);

    await act(async () => {
      MockEventSource.instances[0]?.emit("snapshot", buildSnapshotEnvelope("旧标题"));
      await flush();
      await flush();
    });

    expect(onState).toHaveBeenLastCalledWith(
      expect.objectContaining({
        status: "live",
        cursor: "evt_2",
        data: expect.objectContaining({
          documents: [expect.objectContaining({ title: "旧标题" })],
        }),
      }),
    );

    await act(async () => {
      MockEventSource.instances[0]?.emit("delta", {
        stream: "workspace",
        workspaceId: "ws_1",
        cursor: "evt_3",
        projection: "sidebar_tree",
        data: {
          op: "resync_required",
          reason: "documents_empty_trash",
        },
      });
      await flush();
      await flush();
    });

    expect(onState).toHaveBeenLastCalledWith(
      expect.objectContaining({
        status: "live",
        cursor: "evt_3",


op: "resync_required",
          reason: "documents_empty_trash",
        },
      });
      await flush();
      await flush();
    });

    expect(onState).toHaveBeenLastCalledWith(
      expect.objectContaining({
        status: "live",
        cursor: "evt_3",
        data: expect.objectContaining({
          documents: [expect.objectContaining({ title: "旧标题" })],
        }),
      }),
    );

    await act(async () => {
      MockEventSource.instances[0]?.emit("resync", buildSnapshotEnvelope("新标题"));
      await flush();
      await flush();
    });

    expect(onState).toHaveBeenLastCalledWith(
      expect.objectContaining({
        status: "live",
        cursor: "evt_2",
        data: expect.objectContaining({
          documents: [expect.objectContaining({ title: "新标题" })],
        }),
      }),
    );
  });

  it("收到 snapshot 后连接中断也应切到 fallback，并保留最近一次 stream 数据", async () => {
    await act(async () => {
      root.render(<Harness onState={onState} />);
      await flush();
      await flush();
    });

    expect(MockEventSource.instances).toHaveLength(1);

    await act(async () => {
      MockEventSource.instances[0]?.emit("snapshot", buildSnapshotEnvelope("来自 stream 的标题"));
      await flush();
      await flush();
    });

    expect(onState).toHaveBeenLastCalledWith(
      expect.objectContaining({
        status: "live",
        cursor: "evt_2",
        data: expect.objectContaining({
          documents: [expect.objectContaining({ title: "来自 stream 的标题" })],
        }),
      }),
    );


});

    expect(onState).toHaveBeenLastCalledWith(
      expect.objectContaining({
        status: "live",
        cursor: "evt_2",
        data: expect.objectContaining({
          documents: [expect.objectContaining({ title: "来自 stream 的标题" })],
        }),
      }),
    );

    await act(async () => {
      MockEventSource.instances[0]?.onerror?.();
      await flush();
      await flush();
    });

    expect(onState).toHaveBeenLastCalledWith(
      expect.objectContaining({
        status: "fallback",
        cursor: "evt_2",
        error: expect.any(Error),
        data: expect.objectContaining({
          documents: [expect.objectContaining({ title: "来自 stream 的标题" })],
        }),
      }),
    );
    expect(MockEventSource.instances[0]?.closed).toBe(true);
  });
});


"use client";

import { useEffect, useMemo, useRef, useState } from "react";
import type { SidebarInitialData } from "@/components/sidebar/types";
import {
  buildWorkspaceTreeStreamUrl,
  normalizeTreeStreamSnapshot,
  parseTreeStreamMessage,
} from "@/lib/tree-stream/protocol";
import { applyTreeStreamDelta, type TreeStreamDeltaEvent } from "@/lib/tree-stream/tree-delta";

export interface SidebarTreeStreamState {
  data: SidebarInitialData | null;
  status: "idle" | "connecting" | "live" | "fallback";
  cursor: string | null;
  error: Error | null;
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}

function normalizeDeltaEvent(input: unknown): TreeStreamDeltaEvent | null {
  if (!isRecord(input) || typeof input.op !== "string") {
    return null;
  }
  return {
    op: input.op as TreeStreamDeltaEvent["op"],
    node: isRecord(input.node) ? (input.node as TreeStreamDeltaEvent["node"]) : null,
    document: isRecord(input.document) ? (input.document as TreeStreamDeltaEvent["document"]) : null,
    documentId: typeof input.documentId === "string" ? input.documentId : null,
    documents: Array.isArray(input.documents) ? (input.documents as TreeStreamDeltaEvent["documents"]) : null,
    sidebar: isRecord(input.sidebar) ? (input.sidebar as TreeStreamDeltaEvent["sidebar"]) : null,
  };
}

export function useSidebarTreeStream(initialData: SidebarInitialData): SidebarTreeStreamState {
  const workspaceId = initialData.activeWorkspaceId;
  const streamEnabled = Boolean(workspaceId);
  const cursorRef = useRef<string | null>(null);

  const [state, setState] = useState<SidebarTreeStreamState>({
    data: null,
    status: streamEnabled ? "connecting" : "idle",
    cursor: null,
    error: null,
  });
  const eventSourceRef = useRef<EventSource | null>(null);

  useEffect(() => {
    cursorRef.current = state.cursor;
  }, [state.cursor]);

  useEffect(() => {
    if (!streamEnabled) {
      cursorRef.current = null;
      setState({
        data: null,
        status: "idle",
        cursor: null,


useEffect(() => {
    cursorRef.current = state.cursor;
  }, [state.cursor]);

  useEffect(() => {
    if (!streamEnabled) {
      cursorRef.current = null;
      setState({
        data: null,
        status: "idle",
        cursor: null,
        error: null,
      });
      return undefined;
    }

    let cancelled = false;
    let eventSource: EventSource | null = null;

    const handleMessage = (event: MessageEvent<string>) => {
      const envelope = parseTreeStreamMessage({
        rawData: event.data,
        eventType: event.type,
      });
      if (!envelope) {
        return;
      }

      setState((previous) => {
        const nextCursor = envelope.cursor ?? previous.cursor;

        if (envelope.kind === "snapshot" || envelope.kind === "resync") {
          const snapshot = normalizeTreeStreamSnapshot(envelope.data);
          if (!snapshot) {
            return {
              ...previous,
              cursor: nextCursor,
            };
          }
          return {
            data: snapshot,
            status: "live",
            cursor: nextCursor,
            error: null,
          };
        }

        if (envelope.kind === "delta" && previous.data) {
          const deltaEvent = normalizeDeltaEvent(envelope.data);
          if (!deltaEvent) {
            return {
              ...previous,
              cursor: nextCursor,
            };
          }
          return {
            data: applyTreeStreamDelta(previous.data, deltaEvent),
            status: "live",
            cursor: nextCursor,


if (envelope.kind === "delta" && previous.data) {
          const deltaEvent = normalizeDeltaEvent(envelope.data);
          if (!deltaEvent) {
            return {
              ...previous,
              cursor: nextCursor,
            };
          }
          return {
            data: applyTreeStreamDelta(previous.data, deltaEvent),
            status: "live",
            cursor: nextCursor,
            error: null,
          };
        }

        return {
          ...previous,
          cursor: nextCursor,
        };
      });
    };

    const handleError = () => {
      setState((previous) => ({
        ...previous,
        status: "fallback",
        error: previous.error ?? new Error("tree stream 连接失败"),
      }));
      eventSource?.close();
      if (eventSourceRef.current === eventSource) {
        eventSourceRef.current = null;
      }
    };

    if (cancelled) {
      return undefined;
    }

    const url = buildWorkspaceTreeStreamUrl(workspaceId, cursorRef.current);
    eventSource = new EventSource(url, { withCredentials: true });
    eventSourceRef.current = eventSource;
    eventSource.addEventListener("snapshot", handleMessage as EventListener);
    eventSource.addEventListener("delta", handleMessage as EventListener);
    eventSource.addEventListener("resync", handleMessage as EventListener);
    eventSource.onmessage = handleMessage;
    eventSource.onerror = handleError;

    return () => {
      cancelled = true;
      eventSource?.removeEventListener("snapshot", handleMessage as EventListener);
      eventSource?.removeEventListener("delta", handleMessage as EventListener);
      eventSource?.removeEventListener("resync", handleMessage as EventListener);
      eventSource?.close();
      if (eventSourceRef.current === eventSource) {
        eventSourceRef.current = null;
      }
    };
  }, [streamEnabled, workspaceId]);


return () => {
      cancelled = true;
      eventSource?.removeEventListener("snapshot", handleMessage as EventListener);
      eventSource?.removeEventListener("delta", handleMessage as EventListener);
      eventSource?.removeEventListener("resync", handleMessage as EventListener);
      eventSource?.close();
      if (eventSourceRef.current === eventSource) {
        eventSourceRef.current = null;
      }
    };
  }, [streamEnabled, workspaceId]);

  return state;
}