task-threads.ts2.8 KBView on GitHub
/**
 * Which email threads a task can put in front of you — the preload targets.
 *
 * Opening a task lands on one of exactly two things, and both of them are a `mail.get` away:
 *
 *   - an email task with a produced draft opens THAT DRAFT'S THREAD (see `openTask`), and
 *   - everything else opens the ticket, which renders the thread the task is ABOUT
 *     (`sourceThreadId`) inline via ThreadDataSync.
 *
 * So this is the list of thread bodies that decide whether a task opens instantly or with a
 * spinner, and it is what the hover prefetch and the post-load sweep both feed to `mail.get` —
 * the same treatment `/inbox` gives the thread list (see `usePrefetchThread` / `useThreads`).
 *
 * The output axis is read `taskOutput` FIRST and `taskActionData` second: `taskOutput` is
 * authoritative (see aop-schema.ts), but ~12k rows still carry only the legacy pair, and
 * `openTask` still routes on `taskActionData`, so a task that has one and not the other must
 * still preload. Order matters — the output thread is what a click opens, so it goes first and
 * gets the sweep's budget before the source thread does.
 */

import { isClientOnlyThreadId } from '@/modules/drafting/store/draftSlice';

/** The minimum a task has to expose for its threads to be found. */
export interface TaskWithThreads {
  /** The authoritative output axis. An email output carries the draft's thread. */
  taskOutput?: { kind?: string | null; threadId?: string | null } | null;
  /** The legacy output pair, still written and still what `openTask` routes on. */
  taskActionData?: { channel?: string | null; threadId?: string | null } | null;
  /** The thread the task is ABOUT — rendered inside the ticket. */
  sourceThreadId?: string | null;
}

/**
 * A thread id worth spending a `mail.get` on: present, and backed by a real provider thread.
 * A client-only compose id (`draftSessionId-…`) has no thread behind it — fetching one 400s,
 * which is why ThreadDataSync refuses them too.
 */
function isFetchableThreadId(threadId: string | null | undefined): threadId is string {
  return !!threadId && !isClientOnlyThreadId(threadId);
}

/**
 * Every fetchable thread this task can open, most-likely-to-be-opened first, deduped.
 * Empty for a task that produced nothing and names no thread — a reminder has no body to preload.
 */
export function taskThreadIds(task: TaskWithThreads | null | undefined): string[] {
  if (!task) return [];

  const candidates: (string | null | undefined)[] = [
    task.taskOutput?.kind === 'email' ? task.taskOutput.threadId : undefined,
    task.taskActionData?.channel === 'email' ? task.taskActionData.threadId : undefined,
    task.sourceThreadId,
  ];

  const ids: string[] = [];
  for (const id of candidates) {
    if (!isFetchableThreadId(id) || ids.includes(id)) continue;
    ids.push(id);
  }
  return ids;
}