1import {
2 OrchestrationV2ShellSnapshot,
3 type ThreadId,
4 ThreadPullRequestLink,
5} from "@t3tools/contracts";
6import * as Effect from "effect/Effect";
7import * as Predicate from "effect/Predicate";
8import * as Schema from "effect/Schema";
9
10export interface DeferredShellSnapshot extends OrchestrationV2ShellSnapshot {
11 readonly loadPullRequests?: Effect.Effect<
12 ReadonlyMap<ThreadId, ReadonlyArray<ThreadPullRequestLink>>
13 >;
14}
15
16// A zero sleep is a microtask yield, which on React Native runs before the next frame.
17// A 1ms sleep takes a host timer, so the rows can render before the deferred decode.
18const yieldToHost = Effect.sleep("1 millis");
19
20const decodeThreadLinks = Schema.decodeUnknownEffect(
21 Schema.UndefinedOr(Schema.toCodecJson(Schema.Array(ThreadPullRequestLink))),
22);
23const LINKS_PER_BATCH = 400;
24
25/**
26 * Removes each parsed thread row's pull request links from `snapshot` in place and returns
27 * them in row order, for `deferPullRequests` once the rows are decoded.
28 */
29export function detachPullRequests(snapshot: unknown): ReadonlyArray<unknown> {
30 if (!Predicate.isObject(snapshot)) return [];
31 const rows: unknown = snapshot.threads;
32 if (!Array.isArray(rows)) return [];
33 return rows.map((row: unknown) => {
34 if (!Predicate.isObject(row)) return undefined;
35 const links = row.pullRequests;
36 delete row.pullRequests;
37 return links;
38 });
39}
40
41export function deferPullRequests<S extends OrchestrationV2ShellSnapshot>(
42 snapshot: S,
43 rawLinks: ReadonlyArray<unknown>,
44): S & Required<Pick<DeferredShellSnapshot, "loadPullRequests">> {
45 const threadIds = snapshot.threads.map((thread) => thread.id);
46 // Schema decoding runs when called, so each thread's decode is built after a yield.
47 const loadPullRequests = Effect.gen(function* () {
48 const linksByThreadId = new Map<ThreadId, ReadonlyArray<ThreadPullRequestLink>>();
49 let batchSize = LINKS_PER_BATCH;
50 for (const [index, threadId] of threadIds.entries()) {
51 const raw = rawLinks[index];
52 if (raw === undefined) continue;
53 const size = Array.isArray(raw) ? raw.length : 1;
54 if (batchSize + size > LINKS_PER_BATCH) {
55 yield* yieldToHost;
56 batchSize = 0;
57 }
58 batchSize += size;
59 const links = yield* decodeThreadLinks(raw).pipe(
60 Effect.catch((cause) =>
61 Effect.logWarning("Discarding unreadable shell pull request links.", {
62 threadId,
63 cause: String(cause),
64 }).pipe(Effect.as(undefined)),
65 ),
66 );
67 if (links !== undefined) linksByThreadId.set(threadId, links);
68 }
69 return linksByThreadId as ReadonlyMap<ThreadId, ReadonlyArray<ThreadPullRequestLink>>;
70 });
71 return { ...snapshot, loadPullRequests };
72}
73
74const decodeShellSnapshotJson = Schema.decodeUnknownEffect(
75 Schema.toCodecJson(OrchestrationV2ShellSnapshot),
76);
77
78export const decodeShellSnapshotDeferringPullRequests = Effect.fnUntraced(function* (
79 json: unknown,
80) {
81 const rawLinks = detachPullRequests(json);
82 const snapshot = yield* decodeShellSnapshotJson(json);
83 return deferPullRequests(snapshot, rawLinks);
84});