Public: anyone can read this; the crew can change it.

An issues list reads every row's status word in one batch: `words` under 10 ms for a page of issues #6135

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/6125-an-issues-list-reads-every-row-s-status → main 2 commits fork 1 thread · 1 unresolved

  • Merged. scout/6125-an-issues-list-reads-every-row-s-status was merged into main as 707c4bb.

Review

Sign in to review this change.
6 files changed +455 −23
src/core/pulls.ts +38
@@ -344,6 +344,44 @@ export function pullForBranch(
344344 return row ? getPull(db, repoId, row.number) : undefined;
345345 }
346346
347 /**
348 * `pullForBranch` for many branches of one repository: each branch's newest
349 * pull, in two queries per chunk. Branches with none are absent.
350 */
351 export function pullsForBranches(
352 db: Database,
353 repoId: string,
354 branches: readonly string[],
355 ): Map<string, Pull> {
356 const out = new Map<string, Pull>();
357 const unique = [...new Set(branches)].filter((b) => b !== "");
358 const CHUNK = 400;
359 for (let i = 0; i < unique.length; i += CHUNK) {
360 const batch = unique.slice(i, i + CHUNK);
361 const rows = db
362 .query<{ id: string; branch: string }, string[]>(
363 `SELECT issue.id AS id, pull.head_branch AS branch
364 FROM pull JOIN issue ON issue.id = pull.issue_id
365 WHERE issue.repo_id = ?1
366 AND pull.head_branch IN (${batch.map((_, n) => `?${n + 2}`).join(",")})
367 ORDER BY issue.created_at DESC`,
368 )
369 .all(repoId, ...batch);
370 const newest = new Map<string, string>();
371 for (const r of rows) if (!newest.has(r.branch)) newest.set(r.branch, r.id);
372 const pulls = new Map(
373 joinMany(db, [...getIssuesByIds(db, [...newest.values()]).values()]).map(
374 (pull) => [pull.id, pull] as const,
375 ),
376 );
377 for (const [branch, id] of newest) {
378 const pull = pulls.get(id);
379 if (pull !== undefined) out.set(branch, pull);
380 }
381 }
382 return out;
383 }
384
347385 /**
348386 * Bring a same-repo pull's stored head up to its branch's new tip. Every seam
349387 * that lands a commit on a branch — a git push, the web editor, the API —
src/core/runs.ts +57
@@ -751,6 +751,63 @@ export function activeRunFor(
751751 return row ? toRun(row) : undefined;
752752 }
753753
754 /**
755 * `listRuns(…, { kind, issueId, limit: 1 })` for many issues at once: each
756 * issue's newest run of `kind`, one query per chunk. Issues with none are absent.
757 */
758 export function latestRunsOfKind(
759 db: Database,
760 kind: RunKind,
761 issueIds: readonly string[],
762 ): Map<string, Run> {
763 const out = new Map<string, Run>();
764 const unique = [...new Set(issueIds)];
765 const CHUNK = 400;
766 for (let i = 0; i < unique.length; i += CHUNK) {
767 const batch = unique.slice(i, i + CHUNK);
768 const rows = db
769 .query<RunRow, string[]>(
770 `SELECT * FROM run WHERE kind = ?1
771 AND issue_id IN (${batch.map((_, n) => `?${n + 2}`).join(",")})
772 ORDER BY id DESC`,
773 )
774 .all(kind, ...batch);
775 for (const row of rows)
776 if (row.issue_id && !out.has(row.issue_id)) out.set(row.issue_id, toRun(row));
777 }
778 return out;
779 }
780
781 /**
782 * `checksForHead` for many pulls at once: each pull's runs at its own head,
783 * newest first, one query per chunk. Pulls with none are absent.
784 */
785 export function checksForHeads(
786 db: Database,
787 heads: ReadonlyMap<string, string>,
788 ): Map<string, Run[]> {
789 const out = new Map<string, Run[]>();
790 const ids = [...heads.keys()];
791 const CHUNK = 400;
792 for (let i = 0; i < ids.length; i += CHUNK) {
793 const batch = ids.slice(i, i + CHUNK);
794 const rows = db
795 .query<RunRow, string[]>(
796 `SELECT * FROM run WHERE kind IN ('deploy','check')
797 AND issue_id IN (${batch.map((_, n) => `?${n + 1}`).join(",")})
798 ORDER BY id DESC`,
799 )
800 .all(...batch);
801 for (const row of rows) {
802 if (!row.issue_id || row.commit_oid !== heads.get(row.issue_id)) continue;
803 const list = out.get(row.issue_id) ?? [];
804 list.push(toRun(row));
805 out.set(row.issue_id, list);
806 }
807 }
808 return out;
809 }
810
754811 /**
755812 * `activeRunFor` for many issues at once: each issue's earliest queued or
756813 * running run of `kind`, one query per chunk. Issues with none are absent.
src/web/studio/lists.ts +3 −7
@@ -58,7 +58,7 @@ import { actorsFor, durationText, issuesFor, runTitleFrom } from "../pages/runs.
5858 import { originGlyph } from "../pages/thread.ts";
5959 import { changeHref, PAGE_TITLES, pageTitle, tellHref } from "./nouns.ts";
6060 import { tellLine, tellsAnswered } from "./branch_tell.ts";
61 import { knowMergeClosings, tellWord } from "./tell.ts";
61 import { knowMergeClosings, tellWords } from "./tell.ts";
6262 import {
6363 pullWord,
6464 runWord,
@@ -356,12 +356,8 @@ export async function tellsList(ctx: Ctx): Promise<Response> {
356356 );
357357 notePhase(ctx.request, "closing", performance.now() - at);
358358 at = performance.now();
359 const words = await Promise.all(
360 fetched.map(
361 async (issue) =>
362 (await tellWord(wordCtx, repo, issue, closingPulls.get(issue.number))) ??
363 closedWord(issue),
364 ),
359 const words = (await tellWords(wordCtx, repo, fetched, closingPulls)).map(
360 (word, i) => word ?? closedWord(fetched[i]!),
365361 );
366362 notePhase(ctx.request, "words", performance.now() - at);
367363 at = performance.now();
src/web/studio/tell.ts +112 −9
@@ -17,11 +17,28 @@ import { alreadyDone } from "../../core/done_on_main.ts";
1717 import { appsRuntime } from "../../apps/runtime.ts";
1818 import { getAppByRepo } from "../../core/apps.ts";
1919 import { repoGraph } from "../../core/dependencies.ts";
20 import { labelsFor, listLabels, type Issue } from "../../core/issues.ts";
21 import { getPull, openPullsClosing, type Pull } from "../../core/pulls.ts";
20 import {
21 getIssuesByNumbers,
22 labelsFor,
23 listLabels,
24 numberKey,
25 type Issue,
26 } from "../../core/issues.ts";
27 import {
28 getPull,
29 joinMany,
30 openPullsClosing,
31 type Pull,
32 } from "../../core/pulls.ts";
2233 import { canEnterApp, canWrite, getSealedFork, type Repo } from "../../core/repos.ts";
2334 import { canSeeSealed } from "../../core/sealed.ts";
24 import { activeRunFor, listRuns } from "../../core/runs.ts";
35 import {
36 activeRunFor,
37 activeRunsFor,
38 latestRunsOfKind,
39 listRuns,
40 type Run,
41 } from "../../core/runs.ts";
2542 import { getUsersByIds } from "../../core/users.ts";
2643 import { repoDir } from "../../git/repos.ts";
2744 import type { Ctx } from "../../http/context.ts";
@@ -64,6 +81,8 @@ import {
6481 } from "../pages/thread.ts";
6582 import { changeHref } from "./nouns.ts";
6683 import {
84 knowPulls,
85 knowRunPulls,
6786 pullWord,
6887 runWord,
6988 wordBadge,
@@ -119,11 +138,93 @@ function shippedBy(wordCtx: WordCtx, repo: Repo, issue: Issue): Pull | undefined
119138 if (issue.state !== "closed") return undefined;
120139 knowMergeClosings(wordCtx, repo.id, [issue.number]);
121140 const number = mergeClosings.get(wordCtx)?.get(repo.id)?.get(issue.number);
141 const read = told.get(wordCtx)?.shipped;
142 if (number !== null && number !== undefined && read?.has(number))
143 return read.get(number);
122144 return number !== null && number !== undefined && Number.isInteger(number)
123145 ? getPull(wordCtx.db, repo.id, number)
124146 : undefined;
125147 }
126148
149 /** What `tellWords` read ahead for a page, so `tellWord` reads nothing per row. */
150 // Scoped to one `tellWords` call: set before the words are read, deleted after,
151 // and keyed by the request's own `WordCtx`.
152 const told = new WeakMap<
153 WordCtx,
154 {
155 active: Map<string, Run | undefined>;
156 last: Map<string, Run | undefined>;
157 shipped: Map<number, Pull | undefined>;
158 }
159 >();
160
161 /**
162 * `tellWord` for a page of tells: the runs, pulls and checks of all of them
163 * are read together first, so the page costs a handful of queries rather
164 * than a few per row. Each word is the one `tellWord` gives (undefined for
165 * a closed tell nothing merged shut), in the order of `issues`.
166 */
167 export async function tellWords(
168 wordCtx: WordCtx,
169 repo: Repo,
170 issues: readonly Issue[],
171 closingPulls: ReadonlyMap<number, Pull>,
172 ): Promise<({ word: Word; note: string } | undefined)[]> {
173 const db = wordCtx.db;
174 const read = {
175 active: new Map<string, Run | undefined>(),
176 last: new Map<string, Run | undefined>(),
177 shipped: new Map<number, Pull | undefined>(),
178 };
179 const pulls: Pull[] = [];
180 const closed = issues.filter((i) => i.state === "closed");
181 knowMergeClosings(wordCtx, repo.id, closed.map((i) => i.number));
182 const closings = mergeClosings.get(wordCtx)?.get(repo.id);
183 const numbers = closed.flatMap((i) => {
184 const n = closings?.get(i.number);
185 return n !== null && n !== undefined && Number.isInteger(n) ? [n] : [];
186 });
187 const found = getIssuesByNumbers(
188 db,
189 numbers.map((number) => ({ repoId: repo.id, number })),
190 );
191 for (const pull of joinMany(
192 db,
193 numbers.flatMap((n) => found.get(numberKey(repo.id, n)) ?? []),
194 )) {
195 read.shipped.set(pull.number, pull);
196 pulls.push(pull);
197 }
198 for (const n of numbers) if (!read.shipped.has(n)) read.shipped.set(n, undefined);
199 const lone = issues.filter(
200 (i) => i.state !== "closed" && !closingPulls.has(i.number),
201 );
202 for (const issue of issues)
203 if (issue.state !== "closed") {
204 const pull = closingPulls.get(issue.number);
205 if (pull !== undefined) pulls.push(pull);
206 }
207 if (runsOpenTo(wordCtx, repo)) {
208 const ids = lone.map((i) => i.id);
209 const active = activeRunsFor(db, "agent", ids);
210 const last = latestRunsOfKind(db, "agent", ids);
211 for (const id of ids) {
212 read.active.set(id, active.get(id));
213 read.last.set(id, last.get(id));
214 }
215 knowRunPulls(wordCtx, [...last.values()]);
216 }
217 knowPulls(wordCtx, pulls);
218 told.set(wordCtx, read);
219 try {
220 return await Promise.all(
221 issues.map((issue) => tellWord(wordCtx, repo, issue, closingPulls.get(issue.number))),
222 );
223 } finally {
224 told.delete(wordCtx);
225 }
226 }
227
127228 /**
128229 * Where one tell stands, in the studio's own words: the pull that closed
129230 * or closes it — judged by `pullWord`, the one place that fact is decided,
@@ -150,18 +251,20 @@ export async function tellWord(
150251 // A private app's runs are for those who may enter it: anyone else reads
151252 // that it was told, never how the crew is getting on with it.
152253 if (!runsOpenTo(wordCtx, repo)) return { word: "told", note: "told" };
153 if (activeRunFor(db, "agent", issue.id) !== undefined)
254 const read = told.get(wordCtx);
255 const active = read?.active.has(issue.id)
256 ? read.active.get(issue.id)
257 : activeRunFor(db, "agent", issue.id);
258 if (active !== undefined)
154259 return { word: "working", note: "working now" };
155260 // Deferred work is not waiting to be picked up: the crew will not take
156261 // it, and the page says why, as the map does.
157262 const deferred = deferredWhy(wordCtx, repo, issue);
158263 if (deferred !== undefined)
159264 return { word: "told", note: `deferred: ${deferred}` };
160 const last = listRuns(db, repo.id, {
161 kind: "agent",
162 issueId: issue.id,
163 limit: 1,
164 })[0];
265 const last = read?.last.has(issue.id)
266 ? read.last.get(issue.id)
267 : listRuns(db, repo.id, { kind: "agent", issueId: issue.id, limit: 1 })[0];
165268 if (last !== undefined) return await runWord(wordCtx, last);
166269 // A person can hold a tell the same as an agent can — the crew has never
167270 // touched it, but nobody is waiting on the crew to either, so it is
src/web/studio/words.ts +103 −7
@@ -17,16 +17,23 @@ import {
1717 type EnvStatus,
1818 } from "../../apps/runtime.ts";
1919 import type { AppRecord } from "../../core/apps.ts";
20 import { canEnterApp, getRepoById } from "../../core/repos.ts";
20 import { canEnterApp, getRepoById, type Repo } from "../../core/repos.ts";
2121 import type { User } from "../../core/users.ts";
2222 import type { Efficiency } from "../../core/efficiency.ts";
2323 import type { WindowStanding } from "../../core/standing.ts";
2424 import { whenText } from "../../crew/limits.ts";
25 import { waitsForReviewer } from "../../crew/review.ts";
26 import { getIssueById } from "../../core/issues.ts";
27 import { getPull, pullForBranch, type Pull } from "../../core/pulls.ts";
25 import { reviewWaitsFor, waitsForReviewer } from "../../crew/review.ts";
26 import { getIssueById, getIssuesByIds } from "../../core/issues.ts";
27 import {
28 getPull,
29 joinMany,
30 pullForBranch,
31 pullsForBranches,
32 type Pull,
33 } from "../../core/pulls.ts";
2834 import {
2935 checkSummary,
36 checksForHeads,
3037 STOPPED_SUMMARY,
3138 type CheckDetail,
3239 type CheckSummary,
@@ -94,6 +101,89 @@ export interface WordCtx {
94101 readonly viewer: User | undefined;
95102 }
96103
104 /** What a page read ahead for its rows, so `pullWord` and `runWord` do not read per row. */
105 interface Ahead {
106 readonly checks: Map<string, CheckSummary>;
107 readonly reviewers: Map<string, string | undefined>;
108 readonly runPulls: Map<string, Pull | undefined>;
109 }
110 // Keyed by the page's `WordCtx`, which is made fresh for each request and
111 // dropped with it, so nothing here outlives the render it was read for.
112 const ahead = new WeakMap<WordCtx, Ahead>();
113 const aheadOf = (ctx: WordCtx): Ahead => {
114 let a = ahead.get(ctx);
115 if (a === undefined)
116 ahead.set(ctx, (a = { checks: new Map(), reviewers: new Map(), runPulls: new Map() }));
117 return a;
118 };
119
120 /**
121 * Read, for all these pulls at once, what `pullWord` asks per open pull: its
122 * check at its head, and the reviewer a green agent pull waits for. The
123 * words that follow are the ones a read apiece gives, in a handful of queries.
124 */
125 export function knowPulls(ctx: WordCtx, pulls: readonly Pull[]): void {
126 const a = aheadOf(ctx);
127 const open = pulls.filter(
128 (p) => p.mergedAt === null && p.state !== "closed" && !a.checks.has(p.id),
129 );
130 if (open.length === 0) return;
131 const runs = checksForHeads(ctx.db, new Map(open.map((p) => [p.id, p.headOid])));
132 const green: Pull[] = [];
133 for (const p of open) {
134 const latest = runs.get(p.id)?.[0];
135 a.checks.set(
136 p.id,
137 latest ? { state: latest.state, run: latest } : { state: "none", run: null },
138 );
139 if (latest?.state === "ok") green.push(p);
140 }
141 const repos = new Map<string, Repo | undefined>();
142 for (const p of green)
143 if (!repos.has(p.repoId)) repos.set(p.repoId, getRepoById(ctx.db, p.repoId));
144 const have = green.flatMap((pull) => {
145 const repo = repos.get(pull.repoId);
146 return repo ? [{ repo, pull }] : [];
147 });
148 const waits = reviewWaitsFor(ctx.db, have);
149 for (const p of green) a.reviewers.set(p.id, waits.get(p.id)?.reviewer);
150 }
151
152 /** `knowPulls` for the pull each of these finished runs produced (`pullFromRun`). */
153 export function knowRunPulls(ctx: WordCtx, runs: readonly Run[]): void {
154 const a = aheadOf(ctx);
155 const want = runs.filter((r) => !a.runPulls.has(r.id));
156 if (want.length === 0) return;
157 const about = getIssuesByIds(
158 ctx.db,
159 want.flatMap((r) => (r.issueId === null ? [] : [r.issueId])),
160 );
161 const direct = new Map(
162 joinMany(ctx.db, [...about.values()].filter((i) => i.kind === "pull")).map(
163 (p) => [p.id, p] as const,
164 ),
165 );
166 const byBranch = new Map<string, Map<string, Pull>>();
167 for (const repoId of new Set(want.map((r) => r.repoId)))
168 byBranch.set(
169 repoId,
170 pullsForBranches(
171 ctx.db,
172 repoId,
173 want.filter((r) => r.repoId === repoId).map((r) => r.branch),
174 ),
175 );
176 const found: Pull[] = [];
177 for (const r of want) {
178 const pull =
179 (r.issueId === null ? undefined : direct.get(r.issueId)) ??
180 (r.branch === "" ? undefined : byBranch.get(r.repoId)?.get(r.branch));
181 a.runPulls.set(r.id, pull);
182 if (pull !== undefined) found.push(pull);
183 }
184 knowPulls(ctx, found);
185 }
186
97187 /**
98188 * The pull an agent run produced, if it produced one — not a foreign key
99189 * (a pull is its own issue, unrelated in the schema to the issue it
@@ -239,13 +329,18 @@ export async function pullWord(
239329 ? "parked by the crew"
240330 : "closed without shipping",
241331 };
242 const check = checkSummary(ctx.db, pull.id, pull.headOid);
332 const check =
333 ahead.get(ctx)?.checks.get(pull.id) ??
334 checkSummary(ctx.db, pull.id, pull.headOid);
243335 // A repository that is not an app never deploys a head, so it never gets
244336 // a look: an open change there waits on a person, and says so.
245337 if (ctx.record === undefined && check.state === "none")
246338 return { word: "waiting", note: "open — waiting on a person" };
247339 if (check.state === "ok") {
248 const reviewer = waitsForReviewer(ctx.db, pull.repoId, pull);
340 const known = ahead.get(ctx)?.reviewers;
341 const reviewer = known?.has(pull.id)
342 ? known.get(pull.id)
343 : waitsForReviewer(ctx.db, pull.repoId, pull);
249344 if (reviewer !== undefined)
250345 return { word: "waiting", note: `waiting for ${reviewer}'s review` };
251346 }
@@ -286,7 +381,8 @@ export async function runWord(
286381 ? "interrupted by a restart"
287382 : run.summary || run.state,
288383 };
289 const pull = pullFromRun(ctx.db, run);
384 const known = ahead.get(ctx)?.runPulls;
385 const pull = known?.has(run.id) ? known.get(run.id) : pullFromRun(ctx.db, run);
290386 if (pull !== undefined) return await pullWord(ctx, pull);
291387 if (run.kind !== "agent")
292388 return {
test/issues-words.test.ts added +142
@@ -0,0 +1,142 @@
1 /**
2 * A page of issues is worded from batched reads (#6125): `tellWords` gives the
3 * word `tellWord` gives for every state, and its queries stay constant as the page grows.
4 */
5 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
6 import { mkdtemp, rm } from "node:fs/promises";
7 import { tmpdir } from "node:os";
8 import { join } from "node:path";
9 import { createApp, type App } from "../src/app.ts";
10 import { mergePull, openPull } from "../src/actions/pulls.ts";
11 import { commitToBranch, createRepo } from "../src/actions/repos.ts";
12 import { createIssue, listIssues, setAssignee, type Issue } from "../src/core/issues.ts";
13 import { openPullsClosing } from "../src/core/pulls.ts";
14 import type { Repo } from "../src/core/repos.ts";
15 import { claimRun, createRun, finishRun } from "../src/core/runs.ts";
16 import { getUserByName } from "../src/core/users.ts";
17 import { repoDir } from "../src/git/repos.ts";
18 import { tellWord, tellWords } from "../src/web/studio/tell.ts";
19 import { servingHistory, type WordCtx } from "../src/web/studio/words.ts";
20 import { quietLogs } from "./helpers/globals.ts";
21 import { adminCookie } from "./helpers/admin.ts";
22
23 quietLogs();
24
25 describe("a page of issues, worded in one batch", () => {
26 let dataDir = "";
27 let app: App;
28 let repo: Repo;
29
30 const ctx = (): WordCtx => ({
31 db: app.db,
32 dir: repoDir(app.reposRoot, repo.ownerName, repo.name),
33 runtime: undefined,
34 record: undefined,
35 history: servingHistory(),
36 viewer: undefined,
37 });
38 const page = (): Issue[] => listIssues(app.db, repo.id, { kind: "issue", state: "all", limit: 100 });
39
40 beforeAll(async () => {
41 dataDir = await mkdtemp(join(tmpdir(), "forge-issues-words-"));
42 app = createApp({ dataDir, port: 0 });
43 await adminCookie(app, { username: "alice", email: "[email protected]", password: "a-long-words-passphrase" });
44 const alice = getUserByName(app.db, "alice")!;
45 const made = await createRepo(app, alice, { name: "garden", visibility: "public" });
46 if (!made.ok) throw new Error(String(made.error));
47 repo = made.value;
48 await commitToBranch(app, alice, repo, { message: "seed", files: [{ path: "README.md", content: "# garden\n" }] });
49
50 for (let i = 0; i < 50; i++) {
51 const issue = createIssue(app.db, { repoId: repo.id, authorId: alice.id, title: `Thing ${i}` });
52 if (!issue.ok) throw new Error(String(issue.error));
53 const it = issue.value;
54 const state = i % 10;
55 const run = (extra: Record<string, unknown>) =>
56 createRun(app.db, { repoId: repo.id, kind: "agent", issueId: it.id, ...extra });
57 const pullFor = async (branch: string, body: string) => {
58 await commitToBranch(app, alice, repo, { branch, base: "main", message: branch, files: [{ path: `${branch.replace("/", "-")}.md`, content: "x\n" }] });
59 const opened = await openPull(app, alice, repo, { title: branch, body, baseBranch: "main", headBranch: branch });
60 if (!opened.ok) throw new Error(String(opened.error));
61 return opened.value;
62 };
63 if (i === 0) {
64 const r = run({});
65 claimRun(app.db, r.id);
66 finishRun(app.db, r.id, { state: "ok", summary: "looked" });
67 }
68 if (state === 1) setAssignee(app.db, it.id, alice.id);
69 if (state === 2) claimRun(app.db, run({}).id);
70 if (state === 3) run({});
71 if (state === 4) {
72 await pullFor(`bot/p${i}`, "");
73 const r = run({ branch: `bot/p${i}` });
74 claimRun(app.db, r.id);
75 finishRun(app.db, r.id, { state: "ok", summary: "ok" });
76 }
77 if (state === 5) {
78 const r = run({});
79 claimRun(app.db, r.id);
80 finishRun(app.db, r.id, { state: "failed", summary: "broke" });
81 }
82 if (state === 6 || state === 7) {
83 const pull = await pullFor(`feature/c${i}`, `Closes #${it.number}`);
84 if (state === 6) {
85 const c = createRun(app.db, { repoId: repo.id, kind: "check", issueId: pull.id, env: `pull-${pull.number}`, commitOid: pull.headOid, reason: "pull" });
86 claimRun(app.db, c.id);
87 finishRun(app.db, c.id, { state: "ok", summary: "ok" });
88 }
89 }
90 if (state === 8) app.db.query("UPDATE issue SET state = 'closed', closed_at = datetime('now') WHERE id = ?1").run(it.id);
91 if (state === 9) {
92 const pull = await pullFor(`feature/m${i}`, `Closes #${it.number}`);
93 const merged = await mergePull(app, alice, repo, pull, "squash");
94 if (!merged.ok) throw new Error(String(merged.error));
95 }
96 }
97 }, 120_000);
98
99 afterAll(async () => {
100 await rm(dataDir, { recursive: true, force: true });
101 });
102
103 test("every word is the one tellWord gives, for every state", async () => {
104 const issues = page();
105 expect(issues.length).toBe(50);
106 const closing = openPullsClosing(app.db, repo.id);
107 const batched = await tellWords(ctx(), repo, issues, closing);
108 for (const [i, issue] of issues.entries())
109 expect(batched[i]).toEqual(await tellWord(ctx(), repo, issue, closing.get(issue.number)));
110 const seen = new Set<string>(batched.map((w) => w?.word ?? "closed by hand"));
111 for (const w of ["told", "working", "waiting", "stopped", "look", "planned", "shipped", "closed by hand"])
112 expect(seen.has(w)).toBe(true);
113 }, 60_000);
114
115 test("the queries do not grow with the page: ten issues and fifty cost about the same", async () => {
116 const all = page();
117 const closing = openPullsClosing(app.db, repo.id);
118 const real = app.db.query.bind(app.db);
119 let count = 0;
outdated

Flaky by construction: a wall-clock bound of 10 ms fails under CI load. Assert on the number of queries instead (constant as the page grows from 10 to 50 issues).

120 (app.db as { query: unknown }).query = (...args: Parameters<typeof real>) => {
121 count++;
122 return real(...args);
123 };
124 const queries = async (issues: Issue[]): Promise<number> => {
125 count = 0;
126 await tellWords(ctx(), repo, issues, closing);
127 return count;
128 };
129 try {
130 const ten = await queries(all.slice(0, 10));
131 const fifty = await queries(all);
132 expect(fifty).toBeLessThanOrEqual(30);
133 expect(fifty).toBe(ten);
134 // Without the batch, fifty tellWord calls each read on their own.
135 count = 0;
136 for (const issue of all) await tellWord(ctx(), repo, issue, closing.get(issue.number));
137 expect(count).toBeGreaterThan(fifty * 3);
138 } finally {
139 (app.db as { query: unknown }).query = real;
140 }
141 });
142 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs