@preventive/triage 1.0.0-alpha.51 → 1.0.0-alpha.52

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@preventive/triage",
3
- "version": "1.0.0-alpha.51",
3
+ "version": "1.0.0-alpha.52",
4
4
  "description": "Client & relay server for triaging of automated reports",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "author": {
@@ -198,6 +198,7 @@
198
198
  "server-managed/repository-pulse.ts",
199
199
  "server-managed/repository-pulls.ts",
200
200
  "server-managed/listed-activity.ts",
201
+ "server-managed/repository-stamp.ts",
201
202
  "server-managed/repository-aliases.ts",
202
203
  "server-managed/repository-discovery.ts",
203
204
  "server-managed/repository-policy.ts",
@@ -285,6 +286,10 @@
285
286
  "common/managed/comments.ts",
286
287
  "common/managed/finding-detail.d.ts",
287
288
  "common/managed/finding-detail.js",
289
+ "common/managed/finding-values.d.ts",
290
+ "common/managed/finding-values.js",
291
+ "common/managed/lite-name.d.ts",
292
+ "common/managed/lite-name.js",
288
293
  "common/managed/github-agents.ts",
289
294
  "common/managed/github-logins.ts",
290
295
  "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): the next asked again
48
- // If-None-Match, GitHub checking the credential as ever, and a 304, which
49
- // it doesn't count against the rate limit, answered by the body kept. Each
50
- // transport's own, as many as MAX_TAGGED_READS and MAX_TAGGED_BYTES hold,
51
- // the least recently used first to go; none larger than MAX_TAGGED_READ_BYTES.
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
- type TaggedRead = { etag: string; text: string }
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
- // The answer kept to `request`, its ETag and body as GitHub wrote it.
65
- export function taggedRead(fetchImpl: typeof fetch, request: string): TaggedRead | undefined {
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 read
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, { etag, text })
81
- tagged.bytes += text.length
82
- for (const [oldest, read] of tagged.reads) {
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 -= read.text.length
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 ? JSON.stringify([url, token]) : null
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
- const etag = request === null ? null : res.headers?.get('etag') ?? null
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
- const text = await res.text()
129
- const value = JSON.parse(text) as unknown
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
  }