| 1 | import { |
| 2 | OrchestrationV2ShellSnapshot, |
| 3 | type ThreadId, |
| 4 | ThreadPullRequestLink, |
| 5 | } from "@t3tools/contracts"; |
| 6 | import * as Effect from "effect/Effect"; |
| 7 | import * as Predicate from "effect/Predicate"; |
| 8 | import * as Schema from "effect/Schema"; |
| 9 | |
| 10 | /** |
| 11 | * A shell snapshot whose threads may arrive before their pull request links are decoded. Those |
| 12 | * links are most of a large shell's decode cost and only feed PR badges, so the thread list |
| 13 | * can paint first. `loadPullRequests` then decodes the links by thread id; until it runs, |
| 14 | * those threads carry no `pullRequests`. |
| 15 | */ |
| 16 | export interface DeferredShellSnapshot extends OrchestrationV2ShellSnapshot { |
| 17 | readonly loadPullRequests?: Effect.Effect< |
| 18 | ReadonlyMap<ThreadId, ReadonlyArray<ThreadPullRequestLink>> |
| 19 | >; |
| 20 | } |
| 21 | |
| 22 | // A zero sleep is a microtask yield, which on React Native runs before the next frame. |
| 23 | // A 1ms sleep takes a host timer, so the rows can render before the deferred decode. |
| 24 | const yieldToHost = Effect.sleep("1 millis"); |
| 25 | |
| 26 | const decodeThreadLinks = Schema.decodeUnknownEffect( |
| 27 | Schema.UndefinedOr(Schema.toCodecJson(Schema.Array(ThreadPullRequestLink))), |
| 28 | ); |
| 29 | // Bounds each uninterrupted stretch of decoding, so taps and frames run between batches. |
| 30 | const LINKS_PER_BATCH = 400; |
| 31 | |
| 32 | /** |
| 33 | * Removes each parsed thread row's pull request links from `snapshot` in place and returns |
| 34 | * them in row order, for `deferPullRequests` once the rows are decoded. |
| 35 | */ |
| 36 | export function detachPullRequests(snapshot: unknown): ReadonlyArray<unknown> { |
| 37 | if (!Predicate.isObject(snapshot)) return []; |
| 38 | const rows: unknown = snapshot.threads; |
| 39 | if (!Array.isArray(rows)) return []; |
| 40 | return rows.map((row: unknown) => { |
| 41 | if (!Predicate.isObject(row)) return undefined; |
| 42 | const links = row.pullRequests; |
| 43 | delete row.pullRequests; |
| 44 | return links; |
| 45 | }); |
| 46 | } |
| 47 | |
| 48 | /** |
| 49 | * Attaches a `loadPullRequests` that decodes the detached links after yielding to the host. |
| 50 | * A thread whose links cannot be read gets none; its row stays usable. |
| 51 | */ |
| 52 | export function deferPullRequests<S extends OrchestrationV2ShellSnapshot>( |
| 53 | snapshot: S, |
| 54 | rawLinks: ReadonlyArray<unknown>, |
| 55 | ): S & Required<Pick<DeferredShellSnapshot, "loadPullRequests">> { |
| 56 | const threadIds = snapshot.threads.map((thread) => thread.id); |
| 57 | // Schema decoding runs when called, so each thread's decode is built after a yield. |
| 58 | const loadPullRequests = Effect.gen(function* () { |
| 59 | const linksByThreadId = new Map<ThreadId, ReadonlyArray<ThreadPullRequestLink>>(); |
| 60 | let batchSize = LINKS_PER_BATCH; |
| 61 | for (const [index, threadId] of threadIds.entries()) { |
| 62 | const raw = rawLinks[index]; |
| 63 | if (raw === undefined) continue; |
| 64 | const size = Array.isArray(raw) ? raw.length : 1; |
| 65 | if (batchSize + size > LINKS_PER_BATCH) { |
| 66 | yield* yieldToHost; |
| 67 | batchSize = 0; |
| 68 | } |
| 69 | batchSize += size; |
| 70 | // An unreadable thread keeps no links; the others still fill in. |
| 71 | const links = yield* decodeThreadLinks(raw).pipe( |
| 72 | Effect.catch((cause) => |
| 73 | Effect.logWarning("Discarding unreadable shell pull request links.", { |
| 74 | threadId, |
| 75 | cause: String(cause), |
| 76 | }).pipe(Effect.as(undefined)), |
| 77 | ), |
| 78 | ); |
| 79 | if (links !== undefined) linksByThreadId.set(threadId, links); |
| 80 | } |
| 81 | return linksByThreadId as ReadonlyMap<ThreadId, ReadonlyArray<ThreadPullRequestLink>>; |
| 82 | }); |
| 83 | return { ...snapshot, loadPullRequests }; |
| 84 | } |
| 85 | |
| 86 | const decodeShellSnapshotJson = Schema.decodeUnknownEffect( |
| 87 | Schema.toCodecJson(OrchestrationV2ShellSnapshot), |
| 88 | ); |
| 89 | |
| 90 | /** Decodes a JSON shell snapshot with its threads' pull request links deferred. */ |
| 91 | export const decodeShellSnapshotDeferringPullRequests = Effect.fnUntraced(function* ( |
| 92 | json: unknown, |
| 93 | ) { |
| 94 | const rawLinks = detachPullRequests(json); |
| 95 | const snapshot = yield* decodeShellSnapshotJson(json); |
| 96 | return deferPullRequests(snapshot, rawLinks); |
| 97 | }); |