151 lines · 5.5 KB · TypeScript
RawDownload
import type {  OrchestrationProjectShell,  OrchestrationV2ShellSnapshot,  OrchestrationV2ShellStreamItem,} from "@t3tools/contracts"; function upsertById<T extends { readonly id: unknown }>(  items: ReadonlyArray<T>,  item: T,): ReadonlyArray<T> {  const index = items.findIndex((candidate) => candidate.id === item.id);  if (index === -1) return [...items, item];  return items.map((candidate, candidateIndex) => (candidateIndex === index ? item : candidate));} function retainRepositoryIdentity(  previous: OrchestrationProjectShell | undefined,  next: OrchestrationProjectShell,): OrchestrationProjectShell {  if (    next.repositoryIdentity == null &&    previous?.repositoryIdentity != null &&    previous.workspaceRoot === next.workspaceRoot  ) {    return { ...next, repositoryIdentity: previous.repositoryIdentity };  }  return next;} export interface MergeShellSnapshotOptions {  /**   * Metadata-only enrichment refresh: structure and sequence never change;   * listed roots accept identity exactly (including null).   * Omit this options object for authoritative HTTP/initial WebSocket snapshots.   */  readonly resolvedRepositoryIdentityRoots: ReadonlyArray<string>;} /** * Merge an incoming full shell snapshot into prior client state. * * Authoritative snapshots (no options) replace structure and sequence even when * lower than cache, while retaining a prior non-null identity when the candidate * is still unresolved/null for the same root. * * Enrichment snapshots (options present) only patch repository identity for * matching current projects. They never replace projects, threads, archives, * or sequence, regardless of the incoming snapshot sequence. */export function mergeShellSnapshotProjects(  previous: OrchestrationV2ShellSnapshot | null | undefined,  next: OrchestrationV2ShellSnapshot,  options?: MergeShellSnapshotOptions,): OrchestrationV2ShellSnapshot {  if (previous === null || previous === undefined) {    return next;  }   const isEnrichment = options !== undefined;  const resolvedRootSet = isEnrichment ? new Set(options.resolvedRepositoryIdentityRoots) : null;   if (isEnrichment) {    const nextById = new Map(next.projects.map((project) => [project.id, project] as const));    return {      ...previous,      projects: previous.projects.map((project) => {        const candidate = nextById.get(project.id);        if (candidate === undefined || candidate.workspaceRoot !== project.workspaceRoot) {          return project;        }        if (resolvedRootSet?.has(project.workspaceRoot) === true) {          return { ...project, repositoryIdentity: candidate.repositoryIdentity };        }        if (project.repositoryIdentity == null && candidate.repositoryIdentity != null) {          return { ...project, repositoryIdentity: candidate.repositoryIdentity };        }        return project;      }),    };  }   const previousById = new Map(previous.projects.map((project) => [project.id, project] as const));  return {    ...next,    projects: next.projects.map((project) => {      const prior = previousById.get(project.id);      if (resolvedRootSet?.has(project.workspaceRoot) === true) {        return project;      }      return retainRepositoryIdentity(prior, project);    }),  };} /** Applies one committed V2 shell delta while preserving active/archive exclusivity. */export function applyShellStreamEvent(  snapshot: OrchestrationV2ShellSnapshot,  event: Exclude<    OrchestrationV2ShellStreamItem,    { readonly kind: "snapshot" } | { readonly kind: "synchronized" }  >,): OrchestrationV2ShellSnapshot {  if (event.sequence <= snapshot.snapshotSequence) return snapshot;   switch (event.kind) {    case "project.updated": {      // Enrichment is async. A project mutation can land with null      // repositoryIdentity while an earlier snapshot already resolved it.      // Keep the prior identity for the same workspace root so multi-env      // grouping does not split until a full snapshot refresh arrives.      const previous = snapshot.projects.find((project) => project.id === event.project.id);      const project = retainRepositoryIdentity(previous, event.project);      return {        ...snapshot,        projects: upsertById(snapshot.projects, project),        snapshotSequence: event.sequence,      };    }    case "project.removed":      return {        ...snapshot,        projects: snapshot.projects.filter((project) => project.id !== event.projectId),        snapshotSequence: event.sequence,      };    case "thread.updated": {      const withoutThread = (threads: OrchestrationV2ShellSnapshot["threads"]) =>        threads.filter((thread) => thread.id !== event.thread.id);      return {        ...snapshot,        threads:          event.location === "active"            ? upsertById(withoutThread(snapshot.threads), event.thread)            : withoutThread(snapshot.threads),        // The archive has its own bounded query/subscription. Older servers may        // still send archive-located deltas here; remove them from the normal        // shell instead of growing its persisted cache again.        archivedThreads: withoutThread(snapshot.archivedThreads),        snapshotSequence: event.sequence,      };    }    case "thread.removed":      return {        ...snapshot,        threads: snapshot.threads.filter((thread) => thread.id !== event.threadId),        archivedThreads: snapshot.archivedThreads.filter((thread) => thread.id !== event.threadId),        snapshotSequence: event.sequence,      };    default:      return snapshot;  }}