@preventive/triage 1.0.0-alpha.51 → 1.0.0-alpha.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/common/managed/finding-detail.d.ts +4 -2
- package/common/managed/finding-detail.js +29 -20
- package/common/managed/finding-values.d.ts +2 -0
- package/common/managed/finding-values.js +44 -0
- package/common/managed/lite-name.d.ts +2 -0
- package/common/managed/lite-name.js +20 -0
- package/common/managed/team-app.d.ts +2 -0
- package/common/managed/team-app.js +32 -3
- package/common/workspace-app.js +26 -6
- package/out/activity.js +4 -4
- package/out/chunk-JEFZTVNX.js +3 -0
- package/out/chunk-LJ7XRXFU.js +52 -0
- package/out/{chunk-PBZPYIG7.js → chunk-MYGJBYYJ.js} +1 -1
- package/out/{chunk-7JBSSRD7.js → chunk-NE2UGKRJ.js} +1 -1
- package/out/chunk-TSIFIFBX.js +232 -0
- package/out/{chunk-WRAJVD35.js → chunk-YA4ETCFU.js} +1 -1
- package/out/client-managed.js +26 -26
- package/out/compare.js +5 -5
- package/out/dialogs.js +41 -41
- package/out/index.html +3 -3
- package/out/scan.js +2 -2
- package/out/view.js +11 -11
- package/package.json +7 -1
- package/server-managed/github-requests.ts +54 -24
- package/server-managed/history-windows.ts +172 -0
- package/server-managed/http.ts +192 -62
- package/server-managed/npm-kept.ts +4 -0
- package/server-managed/repository-history.ts +2 -2
- package/server-managed/repository-pulse.ts +15 -4
- package/server-managed/repository-stamp.ts +41 -0
- package/server-managed/team-app.ts +3 -1
- package/server-managed/team-reports.ts +21 -13
- package/out/chunk-6LNQTKAV.js +0 -52
- package/out/chunk-IPAH4LKF.js +0 -232
- package/out/chunk-VDRXHIHN.js +0 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@preventive/triage",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.53",
|
|
4
4
|
"description": "Client & relay server for triaging of automated reports",
|
|
5
5
|
"license": "MIT OR Apache-2.0",
|
|
6
6
|
"author": {
|
|
@@ -141,6 +141,7 @@
|
|
|
141
141
|
"server-managed/github-oauth.ts",
|
|
142
142
|
"server-managed/github-pulls.ts",
|
|
143
143
|
"server-managed/github-requests.ts",
|
|
144
|
+
"server-managed/history-windows.ts",
|
|
144
145
|
"server-managed/http-response.ts",
|
|
145
146
|
"server-managed/http.ts",
|
|
146
147
|
"server-managed/link-reports.ts",
|
|
@@ -198,6 +199,7 @@
|
|
|
198
199
|
"server-managed/repository-pulse.ts",
|
|
199
200
|
"server-managed/repository-pulls.ts",
|
|
200
201
|
"server-managed/listed-activity.ts",
|
|
202
|
+
"server-managed/repository-stamp.ts",
|
|
201
203
|
"server-managed/repository-aliases.ts",
|
|
202
204
|
"server-managed/repository-discovery.ts",
|
|
203
205
|
"server-managed/repository-policy.ts",
|
|
@@ -285,6 +287,10 @@
|
|
|
285
287
|
"common/managed/comments.ts",
|
|
286
288
|
"common/managed/finding-detail.d.ts",
|
|
287
289
|
"common/managed/finding-detail.js",
|
|
290
|
+
"common/managed/finding-values.d.ts",
|
|
291
|
+
"common/managed/finding-values.js",
|
|
292
|
+
"common/managed/lite-name.d.ts",
|
|
293
|
+
"common/managed/lite-name.js",
|
|
288
294
|
"common/managed/github-agents.ts",
|
|
289
295
|
"common/managed/github-logins.ts",
|
|
290
296
|
"common/managed/npm-packages.d.ts",
|
|
@@ -44,15 +44,21 @@ export async function shareGithubRead<T>(fetchImpl: typeof fetch, request: strin
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
// GitHub's answers to GETs that came with an ETag, by the request (its URL
|
|
47
|
-
// and credential, hashed as outstanding reads are
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
47
|
+
// and credential, hashed as outstanding reads are; an installation's token,
|
|
48
|
+
// which the App mints anew every few minutes on every instance, as the
|
|
49
|
+
// installations' one credential, GitHub telling apart which may read what):
|
|
50
|
+
// the next asked again If-None-Match, GitHub checking the credential as
|
|
51
|
+
// ever, and a 304, which it doesn't count against the rate limit, answered
|
|
52
|
+
// by the body kept. Each transport's own, as many as MAX_TAGGED_READS and
|
|
53
|
+
// MAX_TAGGED_BYTES hold, the least recently used first to go; none larger
|
|
54
|
+
// than MAX_TAGGED_READ_BYTES. Those one this process doesn't hold, read of
|
|
55
|
+
// the store kept beyond it (keepTaggedReadsIn), as a serverless instance
|
|
56
|
+
// starting anew holds none, as many as fit it (MAX_STORED_READ_BYTES).
|
|
52
57
|
const MAX_TAGGED_READS = 1000
|
|
53
58
|
const MAX_TAGGED_BYTES = 32 * 1024 * 1024
|
|
54
59
|
const MAX_TAGGED_READ_BYTES = 1024 * 1024
|
|
55
|
-
|
|
60
|
+
const MAX_STORED_READ_BYTES = 256 * 1024
|
|
61
|
+
export type TaggedRead = { etag: string; text: string }
|
|
56
62
|
const taggedReads = new WeakMap<typeof fetch, { reads: Map<string, TaggedRead>; bytes: number }>()
|
|
57
63
|
const taggedOf = (fetchImpl: typeof fetch) => {
|
|
58
64
|
let tagged = taggedReads.get(fetchImpl)
|
|
@@ -60,29 +66,50 @@ const taggedOf = (fetchImpl: typeof fetch) => {
|
|
|
60
66
|
return tagged
|
|
61
67
|
}
|
|
62
68
|
const requestKey = (request: string) => createHash('sha256').update(request).digest('hex')
|
|
69
|
+
// A GET's request as its answer is kept by: its URL and credential.
|
|
70
|
+
const taggedRequest = (url: string, token: string | null) => JSON.stringify([url, token?.startsWith('ghs_') ? { installation: true } : token])
|
|
63
71
|
|
|
64
|
-
//
|
|
65
|
-
|
|
72
|
+
// Where GitHub's tagged answers are kept beyond this process (http.ts
|
|
73
|
+
// registers the database's), by the request's key; its failures are none
|
|
74
|
+
// kept, or none written.
|
|
75
|
+
export interface TaggedStore { read(key: string): Promise<TaggedRead | null>; write(key: string, read: TaggedRead): Promise<void> }
|
|
76
|
+
let taggedStore: TaggedStore | null = null
|
|
77
|
+
export function keepTaggedReadsIn(store: TaggedStore | null): void { taggedStore = store }
|
|
78
|
+
|
|
79
|
+
// The answer kept to `request`, its ETag and body as GitHub wrote it: as
|
|
80
|
+
// this process holds it, else, where a store is kept beyond it, as that
|
|
81
|
+
// keeps it.
|
|
82
|
+
export function taggedRead(fetchImpl: typeof fetch, request: string): TaggedRead | undefined | Promise<TaggedRead | undefined> {
|
|
66
83
|
const { reads } = taggedOf(fetchImpl)
|
|
67
84
|
const key = requestKey(request)
|
|
68
85
|
const read = reads.get(key)
|
|
69
|
-
if (read) { reads.delete(key); reads.set(key, read) }
|
|
70
|
-
return
|
|
86
|
+
if (read) { reads.delete(key); reads.set(key, read); return read }
|
|
87
|
+
return taggedStore ? storedTaggedRead(fetchImpl, key, taggedStore) : undefined
|
|
88
|
+
}
|
|
89
|
+
async function storedTaggedRead(fetchImpl: typeof fetch, key: string, store: TaggedStore) {
|
|
90
|
+
const stored = await store.read(key).catch(() => null)
|
|
91
|
+
if (!stored || typeof stored.etag !== 'string' || typeof stored.text !== 'string') return undefined
|
|
92
|
+
holdTaggedRead(fetchImpl, key, stored)
|
|
93
|
+
return stored
|
|
71
94
|
}
|
|
72
95
|
|
|
73
96
|
// `text` kept as GitHub's answer to `request`, by its ETag.
|
|
74
|
-
export function keepTaggedRead(fetchImpl: typeof fetch, request: string, etag: string, text: string): void {
|
|
75
|
-
const tagged = taggedOf(fetchImpl)
|
|
97
|
+
export async function keepTaggedRead(fetchImpl: typeof fetch, request: string, etag: string, text: string): Promise<void> {
|
|
76
98
|
const key = requestKey(request)
|
|
99
|
+
holdTaggedRead(fetchImpl, key, { etag, text })
|
|
100
|
+
if (text.length <= MAX_STORED_READ_BYTES) await taggedStore?.write(key, { etag, text }).catch(() => {})
|
|
101
|
+
}
|
|
102
|
+
function holdTaggedRead(fetchImpl: typeof fetch, key: string, read: TaggedRead) {
|
|
103
|
+
const tagged = taggedOf(fetchImpl)
|
|
77
104
|
const before = tagged.reads.get(key)
|
|
78
105
|
if (before) { tagged.reads.delete(key); tagged.bytes -= before.text.length }
|
|
79
|
-
if (text.length > MAX_TAGGED_READ_BYTES) return
|
|
80
|
-
tagged.reads.set(key,
|
|
81
|
-
tagged.bytes += text.length
|
|
82
|
-
for (const [oldest,
|
|
106
|
+
if (read.text.length > MAX_TAGGED_READ_BYTES) return
|
|
107
|
+
tagged.reads.set(key, read)
|
|
108
|
+
tagged.bytes += read.text.length
|
|
109
|
+
for (const [oldest, held] of tagged.reads) {
|
|
83
110
|
if (tagged.reads.size <= MAX_TAGGED_READS && tagged.bytes <= MAX_TAGGED_BYTES) break
|
|
84
111
|
tagged.reads.delete(oldest)
|
|
85
|
-
tagged.bytes -=
|
|
112
|
+
tagged.bytes -= held.text.length
|
|
86
113
|
}
|
|
87
114
|
}
|
|
88
115
|
|
|
@@ -97,10 +124,11 @@ export function keepTaggedRead(fetchImpl: typeof fetch, request: string, etag: s
|
|
|
97
124
|
export function githubJson(url: string, token: string | null, fetchImpl: typeof fetch, method: 'GET' | 'POST' = 'GET', body?: unknown,
|
|
98
125
|
readOnly = method === 'GET'): Promise<unknown> {
|
|
99
126
|
if (readOnly) return shareGithubRead(fetchImpl, JSON.stringify([url, token, method, body]), () => githubJson(url, token, fetchImpl, method, body, false))
|
|
100
|
-
const request = method === 'GET' && body === undefined ?
|
|
101
|
-
const tagged = request === null ? undefined : taggedRead(fetchImpl, request)
|
|
127
|
+
const request = method === 'GET' && body === undefined ? taggedRequest(url, token) : null
|
|
102
128
|
// Each call told in its request's Server-Timing (request-timing.ts).
|
|
103
129
|
return timed('gh', async () => {
|
|
130
|
+
const held = request === null ? undefined : taggedRead(fetchImpl, request)
|
|
131
|
+
const tagged = held instanceof Promise ? await held : held
|
|
104
132
|
let res: Response
|
|
105
133
|
const started = Date.now()
|
|
106
134
|
try {
|
|
@@ -122,15 +150,17 @@ export function githubJson(url: string, token: string | null, fetchImpl: typeof
|
|
|
122
150
|
// The answer's body is read within the same timeout: one not read whole is
|
|
123
151
|
// GitHub not reached, as one not answered is; only one that isn't JSON is
|
|
124
152
|
// malformed.
|
|
125
|
-
|
|
153
|
+
// Kept only whole: never a 202's, GitHub still working out its answer.
|
|
154
|
+
const etag = request === null || res.status !== 200 ? null : res.headers?.get('etag') ?? null
|
|
155
|
+
let text: string | null = null, value: unknown
|
|
126
156
|
try {
|
|
127
157
|
if (etag === null) return await res.json()
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
keepTaggedRead(fetchImpl, request!, etag, text)
|
|
131
|
-
return value
|
|
158
|
+
text = await res.text()
|
|
159
|
+
value = JSON.parse(text)
|
|
132
160
|
} catch (error) {
|
|
133
161
|
throw error instanceof SyntaxError ? new GithubApiError(502, 'github-malformed') : githubUnreachable(error, started)
|
|
134
162
|
}
|
|
163
|
+
await keepTaggedRead(fetchImpl, request!, etag, text)
|
|
164
|
+
return value
|
|
135
165
|
})
|
|
136
166
|
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// A default branch's history read by when its commits were made, for the
|
|
2
|
+
// Pulse (repository-pulse.ts yearCommits): where the pages kept from a head
|
|
3
|
+
// run out, the rest of the time it needs read at once, in windows of time
|
|
4
|
+
// (GitHub's `history(since:, until:)`), not a page after another, each page
|
|
5
|
+
// waiting on the last; and the pages GitHub would list from the commits read
|
|
6
|
+
// (git log) kept by their tops, as reading a page at a time keeps them, so a
|
|
7
|
+
// walk later, from a head moved on or in the Commits tab, reads them kept.
|
|
8
|
+
import { linkNoreplyUsers } from './github-accounts.ts'
|
|
9
|
+
import { githubGraphql } from './github-graphql.ts'
|
|
10
|
+
import { COMMIT_FRAGMENT, type GraphCommit, HISTORY_PAGE, type HistoryCommit, type HistoryPages, PAGES_PARALLEL, PAGES_PER_QUERY, commitActors, graphCommit,
|
|
11
|
+
inBatches, variablesOf } from './repository-history.ts'
|
|
12
|
+
|
|
13
|
+
// Windows a read is split into at most, each meant to hold as many commits
|
|
14
|
+
// as WINDOW_COMMITS, a page's worth with room to spare, as often as the
|
|
15
|
+
// commits read before it were made, as far back as they reach, the rest left
|
|
16
|
+
// to the next read; pages read of them at most a read, those over going on
|
|
17
|
+
// where a window holds more, as many times as MAX_WAVES, each waiting on the
|
|
18
|
+
// last; and pages kept of what was read at most.
|
|
19
|
+
const MAX_WINDOWS = 40
|
|
20
|
+
const WINDOW_COMMITS = 70
|
|
21
|
+
const WINDOW_PAGES = 50
|
|
22
|
+
const MAX_WAVES = 5
|
|
23
|
+
const MAX_KEPT = 500
|
|
24
|
+
const KEPT_AT_ONCE = 10
|
|
25
|
+
const WINDOWS_AFTER = 2
|
|
26
|
+
const HOUR_MS = 60 * 60_000
|
|
27
|
+
|
|
28
|
+
// Each window's commits from `$head`, `w<n>` the `n`th's: those made since
|
|
29
|
+
// `$s<n>` (none, any time) and until `$u<n>`, after `$a<n>`.
|
|
30
|
+
const windowsQuery = (count: number) => `query($owner: String!, $name: String!, $head: GitObjectID!, ${variablesOf('s', count, 'GitTimestamp')}, ${
|
|
31
|
+
variablesOf('u', count, 'GitTimestamp!')}, ${variablesOf('a', count, 'String')}) {
|
|
32
|
+
repository(owner: $owner, name: $name) {
|
|
33
|
+
object(oid: $head) { ... on Commit {
|
|
34
|
+
${Array.from({ length: count }, (_, at) => `w${at}: history(first: ${HISTORY_PAGE}, since: $s${at}, until: $u${at}, after: $a${at}) {
|
|
35
|
+
pageInfo { hasNextPage endCursor } nodes { ...historyCommit } }`).join('\n ')}
|
|
36
|
+
} }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
${COMMIT_FRAGMENT}`
|
|
40
|
+
|
|
41
|
+
type Window = { since: number | null; until: number; after: string | null; oldest?: number }
|
|
42
|
+
type WindowPage = { pageInfo?: { hasNextPage?: unknown; endCursor?: unknown }; nodes?: GraphCommit[] } | null
|
|
43
|
+
type Windows = { repository?: { object?: Record<string, WindowPage> | null } | null } | null
|
|
44
|
+
// A time as GitHub takes it, to the second: a window's start rounded down,
|
|
45
|
+
// its end up, so that those beside each other overlap rather than part.
|
|
46
|
+
const stamp = (at: number, up: boolean) => new Date((up ? Math.ceil : Math.floor)(at / 1000) * 1000).toISOString()
|
|
47
|
+
|
|
48
|
+
// The commits from `head` made from `low` to `high`, as often as `perMs` (commits
|
|
49
|
+
// a millisecond) says they are made, as many windows as MAX_WINDOWS reach
|
|
50
|
+
// down from `high`, read at once (inBatches), the latest first, each paged on
|
|
51
|
+
// as far as it goes, as many pages as WINDOW_PAGES; and, those reaching
|
|
52
|
+
// `low`, a page of those made before it besides, where a walk reading them
|
|
53
|
+
// goes on from. Since when every one is read (`through`: where the windows
|
|
54
|
+
// reach, each read whole, else, of the latest not, just after the earliest
|
|
55
|
+
// made of those it read, as GitHub lists them the latest made first, those
|
|
56
|
+
// made together left out; or its end, none read), and how many pages were
|
|
57
|
+
// read (`read`).
|
|
58
|
+
export async function readWindows(fullName: string, token: string, head: string, low: number, high: number, perMs: number, fetchImpl: typeof fetch) {
|
|
59
|
+
const [owner, name] = fullName.split('/') as [string, string]
|
|
60
|
+
const bottom = Math.max(low, high - MAX_WINDOWS * WINDOW_COMMITS / perMs)
|
|
61
|
+
const count = Math.min(MAX_WINDOWS, Math.max(1, Math.ceil((high - bottom) * perMs / WINDOW_COMMITS)))
|
|
62
|
+
const width = (high - bottom) / count
|
|
63
|
+
let open: Window[] = [...Array.from({ length: count }, (_, at) => ({ since: high - (at + 1) * width, until: high - at * width, after: null })),
|
|
64
|
+
...bottom > low ? [] : [{ since: null, until: low - 1000, after: null }]]
|
|
65
|
+
const commits = new Map<string, HistoryCommit>()
|
|
66
|
+
let [read, waves] = [0, 0]
|
|
67
|
+
while (open.length > 0 && read < WINDOW_PAGES && waves++ < MAX_WAVES) {
|
|
68
|
+
const wave = open.slice(0, WINDOW_PAGES - read)
|
|
69
|
+
const next: Window[] = []
|
|
70
|
+
read += wave.length
|
|
71
|
+
await inBatches(wave, PAGES_PER_QUERY, PAGES_PARALLEL, async batch => {
|
|
72
|
+
const data = await githubGraphql(windowsQuery(batch.length), { owner, name, head, ...Object.fromEntries(batch.flatMap((window, at) => [
|
|
73
|
+
[`s${at}`, window.since === null ? null : stamp(window.since, false)], [`u${at}`, stamp(window.until, true)], [`a${at}`, window.after]])) }, token, fetchImpl) as Windows
|
|
74
|
+
const pages = batch.map((_, at) => data?.repository?.object?.[`w${at}`] ?? null)
|
|
75
|
+
await linkNoreplyUsers(pages.flatMap(page => page?.nodes ?? []).flatMap(commitActors), token, fetchImpl)
|
|
76
|
+
for (const [at, window] of batch.entries()) {
|
|
77
|
+
let oldest = window.oldest ?? Number.POSITIVE_INFINITY
|
|
78
|
+
for (const commit of (pages[at]?.nodes ?? []).flatMap(node => graphCommit(node) ?? [])) {
|
|
79
|
+
if (!commits.has(commit.sha)) commits.set(commit.sha, commit)
|
|
80
|
+
oldest = Math.min(oldest, commit.committedAt ?? Number.POSITIVE_INFINITY)
|
|
81
|
+
}
|
|
82
|
+
const info = pages[at]?.pageInfo
|
|
83
|
+
if (window.since !== null && info?.hasNextPage === true && typeof info.endCursor === 'string') next.push({ ...window, after: info.endCursor, oldest })
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
open = [...next, ...open.slice(wave.length)]
|
|
87
|
+
}
|
|
88
|
+
const through = Math.max(bottom, ...open.flatMap(window => window.since === null ? [] : [Number.isFinite(window.oldest) ? window.oldest! + 1 : window.until]))
|
|
89
|
+
return { commits: [...commits.values()], through, read }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// The page GitHub would list from `top` (git log: of those reached, the
|
|
93
|
+
// latest made first, of those made together the first reached) of `known`
|
|
94
|
+
// commits, every one made since `low` known; none where it would list one not
|
|
95
|
+
// known, or one made before `low` while one not known waits beside it.
|
|
96
|
+
export function gitLogPage(top: string, known: Map<string, HistoryCommit>, low: number): HistoryCommit[] | null {
|
|
97
|
+
if (!known.has(top)) return null
|
|
98
|
+
const page: HistoryCommit[] = []
|
|
99
|
+
const reached = new Set<string>()
|
|
100
|
+
const waiting: HistoryCommit[] = []
|
|
101
|
+
let unknown = 0
|
|
102
|
+
const reach = (sha: string) => {
|
|
103
|
+
if (reached.has(sha)) return
|
|
104
|
+
reached.add(sha)
|
|
105
|
+
const commit = known.get(sha)
|
|
106
|
+
if (!commit || commit.committedAt === null) { unknown++; return }
|
|
107
|
+
const at = waiting.findIndex(other => other.committedAt! < commit.committedAt!)
|
|
108
|
+
waiting.splice(at < 0 ? waiting.length : at, 0, commit)
|
|
109
|
+
}
|
|
110
|
+
reach(top)
|
|
111
|
+
while (page.length < HISTORY_PAGE && waiting.length > 0) {
|
|
112
|
+
const commit = waiting.shift()!
|
|
113
|
+
if (commit.committedAt! < low && unknown > 0) return null
|
|
114
|
+
page.push(commit)
|
|
115
|
+
for (const parent of commit.parents) reach(parent)
|
|
116
|
+
}
|
|
117
|
+
return page.length < HISTORY_PAGE && unknown > 0 ? null : page
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// The pages from each of `tops` GitHub would list of `known` commits
|
|
121
|
+
// (gitLogPage), kept by their tops, KEPT_AT_ONCE at a time, and from the
|
|
122
|
+
// parents of those they list that none of `listed` is nor any kept lists, on
|
|
123
|
+
// till none is left, as many as MAX_KEPT: as a walk reading pages goes on to
|
|
124
|
+
// them.
|
|
125
|
+
export async function keepPages(tops: string[], known: Map<string, HistoryCommit>, listed: Set<string>, low: number, pages: HistoryPages) {
|
|
126
|
+
const covered = new Set(listed)
|
|
127
|
+
const kept: [string, HistoryCommit[]][] = []
|
|
128
|
+
const waiting = [...tops]
|
|
129
|
+
while (waiting.length > 0 && kept.length < MAX_KEPT) {
|
|
130
|
+
const top = waiting.shift()!
|
|
131
|
+
if (covered.has(top)) continue
|
|
132
|
+
const page = gitLogPage(top, known, low)
|
|
133
|
+
if (!page) continue
|
|
134
|
+
kept.push([top, page])
|
|
135
|
+
for (const commit of page) covered.add(commit.sha)
|
|
136
|
+
for (const commit of page) for (const parent of commit.parents) if (!covered.has(parent) && known.has(parent)) waiting.push(parent)
|
|
137
|
+
}
|
|
138
|
+
for (let at = 0; at < kept.length; at += KEPT_AT_ONCE) await Promise.all(kept.slice(at, at + KEPT_AT_ONCE).map(([top, page]) => pages.write(top, page)))
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// What a walk from `head` (yearCommits) reads next of `starts`: those a page
|
|
142
|
+
// is kept from (`batch`), where any is, or all of them, a page at a time, as
|
|
143
|
+
// long as it has read GitHub fewer times than WINDOWS_AFTER (`queries`), a
|
|
144
|
+
// head moved on soon reaching the pages kept from where it was; else, none
|
|
145
|
+
// kept, those made from
|
|
146
|
+
// `low` to `high` read at once (readWindows), as often as those `found`
|
|
147
|
+
// were made, told it (`found`), and the pages from `starts` on kept of them,
|
|
148
|
+
// as far down as every one is read (keepPages), the queries that took
|
|
149
|
+
// (`queries`, PAGES_PER_QUERY pages each, as many at once as PAGES_PARALLEL
|
|
150
|
+
// a query of its) and none to read (`windowed`); where the windows can't be
|
|
151
|
+
// read, all of them, a page at a time (`windowed` all the same).
|
|
152
|
+
export async function windowsOn(fullName: string, token: string, head: string, starts: string[], found: Map<string, HistoryCommit>, low: number, high: number,
|
|
153
|
+
pages: HistoryPages, fetchImpl: typeof fetch, queries: number): Promise<{ batch: string[]; queries: number; windowed: boolean }> {
|
|
154
|
+
const kept = await pages.read(starts)
|
|
155
|
+
if (kept.size > 0 || queries < WINDOWS_AFTER || !(high > low) || !Number.isFinite(high)) {
|
|
156
|
+
return { batch: kept.size > 0 ? starts.filter(sha => kept.has(sha)) : starts, queries: 0, windowed: false }
|
|
157
|
+
}
|
|
158
|
+
let [newest, oldest, dated] = [Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY, 0]
|
|
159
|
+
for (const { committedAt } of found.values()) {
|
|
160
|
+
if (committedAt === null) continue
|
|
161
|
+
[newest, oldest, dated] = [Math.max(newest, committedAt), Math.min(oldest, committedAt), dated + 1]
|
|
162
|
+
}
|
|
163
|
+
// From the one commit the walk goes on from, where there is one, none of
|
|
164
|
+
// those read listed again; else from the head.
|
|
165
|
+
const read = await readWindows(fullName, token, starts.length === 1 ? starts[0]! : head, low, high, dated / Math.max(newest - oldest, HOUR_MS), fetchImpl)
|
|
166
|
+
.catch((err: unknown) => { console.warn('managed: history windows read failed:', err); return null })
|
|
167
|
+
if (!read) return { batch: starts, queries: 0, windowed: true }
|
|
168
|
+
const listed = new Set(found.keys())
|
|
169
|
+
for (const commit of read.commits) if (!found.has(commit.sha)) found.set(commit.sha, commit)
|
|
170
|
+
await keepPages(starts, found, listed, read.through, pages)
|
|
171
|
+
return { batch: [], queries: Math.ceil(read.read / (PAGES_PER_QUERY * PAGES_PARALLEL)), windowed: true }
|
|
172
|
+
}
|