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