@preventive/triage 1.0.0-alpha.45 → 1.0.0-alpha.47
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/client/linked-findings.js +3 -0
- package/common/bundle-metadata.d.ts +1 -0
- package/common/bundle-metadata.js +1 -1
- package/common/bundle-sources.js +5 -1
- package/common/line-diff.js +5 -4
- package/common/managed/agent-tasks.ts +5 -3
- package/common/managed/github-agents.ts +28 -10
- package/out/activity.js +4 -0
- package/out/{chunk-NPW52BM2.js → chunk-4HI2MSAV.js} +13 -13
- package/out/chunk-5AHIQFFG.js +36 -0
- package/out/chunk-6ANSHRFQ.js +6 -0
- package/out/chunk-6WF3KNTL.js +3 -0
- package/out/chunk-7BKKYVWR.js +7 -0
- package/out/{chunk-HMAJDVXN.js → chunk-FH24VHSJ.js} +1 -1
- package/out/chunk-NOZJULWS.js +52 -0
- package/out/{chunk-OKRLLE5I.js → chunk-PWFUOLZK.js} +1 -1
- package/out/chunk-S6U2J62U.js +232 -0
- package/out/chunk-SPEFPML6.js +2 -0
- package/out/chunk-UENXT2HK.js +3 -0
- package/out/chunk-VL43BTSO.js +1 -0
- package/out/{chunk-3PS4KWD2.js → chunk-WCPJDAYA.js} +1 -1
- package/out/chunk-ZCS4UYHI.js +1 -0
- package/out/chunk-ZGNMDGSB.js +1 -0
- package/out/client-managed-import.js +1 -1
- package/out/client-managed.js +15 -15
- package/out/client-sync.js +2 -2
- package/out/compare.js +12 -12
- package/out/dialogs.js +28 -28
- package/out/graph.js +11 -11
- package/out/index.html +11 -11
- package/out/scan.js +2 -2
- package/out/terminal.js +6 -6
- package/out/view.css +1 -1
- package/out/view.js +15 -15
- package/package.json +4 -1
- package/risk/actions/findings.js +14 -9
- package/risk/actions/scripts/shell.js +5 -1
- package/risk/index.d.ts +9 -4
- package/risk/npm/assess.js +15 -4
- package/risk/npm/registry.js +6 -1
- package/server-e2e/app.ts +5 -4
- package/server-e2e/http.ts +5 -2
- package/server-managed/activity.ts +24 -11
- package/server-managed/app-agents.ts +10 -21
- package/server-managed/bundle-build-worker.js +7 -3
- package/server-managed/bundle-build.ts +5 -3
- package/server-managed/bundle-cache.ts +16 -4
- package/server-managed/bundle.ts +3 -4
- package/server-managed/combined.ts +3 -2
- package/server-managed/commit-coauthors.ts +5 -9
- package/server-managed/commit-lockfiles.ts +21 -11
- package/server-managed/config.ts +6 -0
- package/server-managed/db-methods.ts +321 -156
- package/server-managed/db-neon.ts +71 -24
- package/server-managed/db-schema.ts +4 -0
- package/server-managed/db.ts +2 -1
- package/server-managed/dependency-reviews.ts +13 -3
- package/server-managed/github-app.ts +12 -6
- package/server-managed/github-issue-links.ts +11 -5
- package/server-managed/github-pulls.ts +2 -2
- package/server-managed/github-requests.ts +39 -0
- package/server-managed/http-response.ts +10 -2
- package/server-managed/http.ts +269 -143
- package/server-managed/index.ts +8 -7
- package/server-managed/kept-briefly.ts +37 -0
- package/server-managed/link-reports.ts +5 -1
- package/server-managed/npm-delta.ts +11 -6
- package/server-managed/npm-kept.ts +51 -7
- package/server-managed/npm-loads.ts +117 -35
- package/server-managed/npm-packages.ts +107 -14
- package/server-managed/npm-risk.ts +68 -18
- package/server-managed/pretty-print.ts +112 -35
- package/server-managed/pull-dependencies.ts +29 -9
- package/server-managed/pull-dependency-reviews.ts +8 -8
- package/server-managed/queue.ts +16 -0
- package/server-managed/rename-diff.ts +22 -13
- package/server-managed/report-sources.ts +1 -1
- package/server-managed/report-store.ts +50 -12
- package/server-managed/repository-actions.ts +15 -6
- package/server-managed/repository-agents.ts +1 -1
- package/server-managed/repository-browser.ts +123 -33
- package/server-managed/repository-code.ts +28 -11
- package/server-managed/repository-dependency-reviews.ts +41 -19
- package/server-managed/repository-history.ts +24 -8
- package/server-managed/repository-pulse.ts +9 -2
- package/server-managed/repository-risk-audit.ts +13 -0
- package/server-managed/repository-risk-read.ts +11 -6
- package/server-managed/repository-risk.ts +27 -20
- package/server-managed/session.ts +18 -4
- package/server-managed/solidity-entry-points.ts +5 -3
- package/server-managed/sql-postgres.ts +3 -0
- package/server-managed/sql.ts +6 -2
- package/server-managed/storage-db.ts +11 -3
- package/server-managed/storage-encryption.ts +7 -4
- package/server-managed/storage.ts +8 -4
- package/server-managed/team-feed.ts +3 -1
- package/server-managed/team-reports.ts +30 -13
- package/server-managed/uploads.ts +29 -9
- package/server-managed/upstream-cache.ts +12 -0
- package/server.ts +14 -5
- package/out/chunk-5DHL5NG3.js +0 -2
- package/out/chunk-6HNXNIQY.js +0 -52
- package/out/chunk-AFLZH7WG.js +0 -1
- package/out/chunk-NV7ES4EW.js +0 -1
- package/out/chunk-PTGDYLNS.js +0 -36
- package/out/chunk-Q6MVYGYV.js +0 -3
- package/out/chunk-QRD6GIG5.js +0 -229
- package/out/chunk-WI7Z5A2T.js +0 -6
- package/out/chunk-WQ74ZBZF.js +0 -7
- package/out/chunk-XVBEOGOS.js +0 -3
|
@@ -31,11 +31,12 @@ const PACKUMENT_BYTES = 64 * 1024 * 1024
|
|
|
31
31
|
// limit until it is read and parsed: four packuments, or 32 version documents.
|
|
32
32
|
const MAX_DOCUMENT_BYTES = 4 * PACKUMENT_BYTES
|
|
33
33
|
// What a package may unpack to: the files' bytes and their count; the tar
|
|
34
|
-
// stream holding them is bounded in npm-loads.ts.
|
|
34
|
+
// stream holding them is bounded in npm-loads.ts. A repository directory's
|
|
35
|
+
// are its own (repository-code.ts).
|
|
35
36
|
export const MAX_NPM_PACKAGE_BYTES = 64 * 1024 * 1024
|
|
36
37
|
export const MAX_NPM_PACKAGE_FILES = 20_000
|
|
37
38
|
// The entries its tar stream may hold, read before any is counted as a file:
|
|
38
|
-
// a directory's too,
|
|
39
|
+
// a directory's too, though npm packs none.
|
|
39
40
|
export const MAX_NPM_TAR_ENTRIES = 2 * MAX_NPM_PACKAGE_FILES
|
|
40
41
|
// What the files may take as the viewer's JSON. Escaping grows a text up to
|
|
41
42
|
// six times (a control character becomes `\u0001`), so it is counted before
|
|
@@ -153,14 +154,60 @@ async function publicWholeDocument(name: string, url: string, { limit, signal }:
|
|
|
153
154
|
return json
|
|
154
155
|
}
|
|
155
156
|
|
|
157
|
+
// Whole documents being asked of npm as anyone asks, by URL and limit:
|
|
158
|
+
// readers asking for one meanwhile share it, as opening a version asks for
|
|
159
|
+
// its package's for its versions, its advisories and its risk at once; each
|
|
160
|
+
// gives up on it alone once its own signal aborts, and it is given up on once
|
|
161
|
+
// they all have. None is asked with the token. Read alike by every reader,
|
|
162
|
+
// none changing it.
|
|
163
|
+
const wholeAsks = new Map<string, { asked: Promise<Record<string, unknown> | null>; controller: AbortController; readers: number }>()
|
|
164
|
+
|
|
165
|
+
function sharedWholeDocument(name: string, url: string, { limit, signal }: { limit: number; signal: AbortSignal }): Promise<Record<string, unknown> | null> {
|
|
166
|
+
if (signal.aborted) return Promise.reject(signal.reason)
|
|
167
|
+
const key = `${url} ${limit}`
|
|
168
|
+
let ask = wholeAsks.get(key)
|
|
169
|
+
if (!ask) {
|
|
170
|
+
const controller = new AbortController()
|
|
171
|
+
const created = { asked: publicWholeDocument(name, url, { limit, signal: controller.signal }), controller, readers: 0 }
|
|
172
|
+
const settle = () => { if (wholeAsks.get(key) === created) wholeAsks.delete(key) }
|
|
173
|
+
created.asked.then(settle, settle)
|
|
174
|
+
wholeAsks.set(key, ask = created)
|
|
175
|
+
}
|
|
176
|
+
const held = ask
|
|
177
|
+
held.readers++
|
|
178
|
+
return new Promise((answer, fail) => {
|
|
179
|
+
let left = false
|
|
180
|
+
const leave = () => {
|
|
181
|
+
if (left) return false
|
|
182
|
+
left = true
|
|
183
|
+
held.readers--
|
|
184
|
+
signal.removeEventListener('abort', onAbort)
|
|
185
|
+
return true
|
|
186
|
+
}
|
|
187
|
+
const onAbort = () => {
|
|
188
|
+
if (!leave()) return
|
|
189
|
+
if (held.readers === 0) {
|
|
190
|
+
if (wholeAsks.get(key) === held) wholeAsks.delete(key)
|
|
191
|
+
held.controller.abort(signal.reason)
|
|
192
|
+
}
|
|
193
|
+
fail(signal.reason)
|
|
194
|
+
}
|
|
195
|
+
const answered = (json: Record<string, unknown> | null) => { if (leave()) answer(json) }
|
|
196
|
+
const failed = (err: unknown) => { if (leave()) fail(err) }
|
|
197
|
+
signal.addEventListener('abort', onAbort, { once: true })
|
|
198
|
+
held.asked.then(answered, failed)
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
|
|
156
202
|
// Asked anonymously first, unless its caller knows npm has none for anyone
|
|
157
203
|
// (`anonymous` false): a reader without private access stops there. One
|
|
158
204
|
// with it asks again with the token where npmTokenReads says so, and the
|
|
159
205
|
// answer is private. A whole document (`whole`) anyone may read is kept
|
|
160
|
-
// (publicWholeDocument)
|
|
206
|
+
// (publicWholeDocument), and asked once for those asking at once
|
|
207
|
+
// (sharedWholeDocument).
|
|
161
208
|
async function readDocument(name: string, url: string, privileged: boolean,
|
|
162
209
|
{ whole = false, anonymous = true, ...options }: { accept: string; limit: number; signal: AbortSignal; whole?: boolean; anonymous?: boolean }) {
|
|
163
|
-
const open = anonymous ? whole ? await
|
|
210
|
+
const open = anonymous ? whole ? await sharedWholeDocument(name, url, options) : await registryDocument(url, { ...options, token: null }) : null
|
|
164
211
|
if (open) return { json: open, private: false }
|
|
165
212
|
if (!npmTokenReads(name, privileged)) return null
|
|
166
213
|
const json = await registryDocument(url, { ...options, token: npmToken() })
|
|
@@ -203,6 +250,8 @@ function versionDocument(name: string, json: Record<string, unknown>, isPrivate:
|
|
|
203
250
|
// (npm-manifest.ts npmVersionFields), as the database keeps them (npm-kept.ts).
|
|
204
251
|
export interface NpmVersionStore {
|
|
205
252
|
read(name: string, version: string): Promise<{ at: number; json: Record<string, unknown> } | null>
|
|
253
|
+
// Those of `versions` kept, by `name@version`, in one read.
|
|
254
|
+
readMany?(versions: { name: string; version: string }[]): Promise<Map<string, { at: number; json: Record<string, unknown> }>>
|
|
206
255
|
write(name: string, version: string, json: Record<string, unknown>): Promise<void>
|
|
207
256
|
}
|
|
208
257
|
|
|
@@ -228,6 +277,20 @@ export function readNpmVersion(name: string, spec: string, privileged: boolean,
|
|
|
228
277
|
})
|
|
229
278
|
}
|
|
230
279
|
|
|
280
|
+
// `store`, where it reads many at once, with the exact ones of `versions`
|
|
281
|
+
// read from it in one read, for readNpmVersion to read each of those from as
|
|
282
|
+
// it asks: a version and the one it is compared with, read together.
|
|
283
|
+
export function readTogether(store: NpmVersionStore, versions: { name: string; version: string }[]): NpmVersionStore {
|
|
284
|
+
const exact = versions.filter(({ version }) => isExactVersion(version))
|
|
285
|
+
if (!store.readMany || exact.length < 2) return store
|
|
286
|
+
const keys = new Set(exact.map(({ name, version }) => `${name}@${version}`))
|
|
287
|
+
const kept = store.readMany(exact)
|
|
288
|
+
return { ...store, async read(name, version) {
|
|
289
|
+
const key = `${name}@${version}`
|
|
290
|
+
return keys.has(key) ? (await kept).get(key) ?? null : store.read(name, version)
|
|
291
|
+
} }
|
|
292
|
+
}
|
|
293
|
+
|
|
231
294
|
// The package's dist-tags and versions, newest published first, from its
|
|
232
295
|
// abbreviated document; with `times`, from its whole one, which alone has
|
|
233
296
|
// when each was published, `times` by version (ISO dates), and `created`,
|
|
@@ -277,22 +340,51 @@ const isPublishTime = (value: string | undefined) => typeof value === 'string' &
|
|
|
277
340
|
const PUBLISH_TIMES = 100_000
|
|
278
341
|
const publishTimes = new Map<string, string>()
|
|
279
342
|
|
|
280
|
-
|
|
343
|
+
// Where public versions' publish times are kept beyond the instance, as the
|
|
344
|
+
// database keeps them (npm-kept.ts npmPublishTimeStore).
|
|
345
|
+
export interface NpmPublishTimeStore {
|
|
346
|
+
read(name: string, version: string): Promise<string | null>
|
|
347
|
+
write(name: string, version: string, at: string): Promise<void>
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// When `doc` was published: as noted here, else as `store` keeps a public
|
|
351
|
+
// version's, so that another instance, or this one restarted, has it before
|
|
352
|
+
// it reads the whole document again. The store is read once, at once, beside
|
|
353
|
+
// whatever its caller reads meanwhile, a failure read as none; `read` looks
|
|
354
|
+
// here again as it is asked, as the whole document read meanwhile may have
|
|
355
|
+
// noted it by then, and `keep` keeps there the time noted here where it read
|
|
356
|
+
// none.
|
|
357
|
+
export function npmPublishTime(doc: Pick<NpmVersionDocument, 'name' | 'version' | 'private'>, store?: NpmPublishTimeStore) {
|
|
358
|
+
const noted = () => publishTimes.get(`${doc.name}@${doc.version}`) ?? null
|
|
359
|
+
const kept = store && !doc.private ? store.read(doc.name, doc.version).catch(() => null) : Promise.resolve(null)
|
|
360
|
+
return {
|
|
361
|
+
read: async (): Promise<string | null> => noted() ?? await kept ?? noted(),
|
|
362
|
+
async keep() {
|
|
363
|
+
const at = noted()
|
|
364
|
+
if (at !== null && store && !doc.private && await kept === null) await store.write(doc.name, doc.version, at)
|
|
365
|
+
},
|
|
366
|
+
}
|
|
367
|
+
}
|
|
281
368
|
|
|
282
369
|
export interface NpmPackageFile { path: string; bytes: Uint8Array }
|
|
283
370
|
|
|
371
|
+
// How a tar stream is read (readNpmTar): how far its symlinks may climb above
|
|
372
|
+
// it, and the entries, files and bytes it may hold, a package's unless given.
|
|
373
|
+
export interface TarReading { symlinksAbove?: number; maxEntries?: number; maxFiles?: number; maxBytes?: number }
|
|
374
|
+
|
|
284
375
|
// The regular files of a tar stream, by package path: npm drops the first
|
|
285
376
|
// component (`package/`, or whatever the archive used), and a file at the top
|
|
286
377
|
// is not extracted. The stream is read by @preventive/archive, which refuses
|
|
287
378
|
// one malformed, a name or a symlink that would climb out of it -- a symlink
|
|
288
379
|
// past `symlinksAbove` levels above it, none unless given -- and a name
|
|
289
|
-
// repeated as another entry, and gives up past
|
|
290
|
-
//
|
|
291
|
-
//
|
|
380
|
+
// repeated as another entry, and gives up past `maxEntries` before reading
|
|
381
|
+
// on; a file under another top directory at the same path replaces an
|
|
382
|
+
// earlier one, as extraction would. Directories, links and devices hold no
|
|
292
383
|
// file content and are skipped.
|
|
293
|
-
export function readNpmTar(tar: Uint8Array, { symlinksAbove = 0
|
|
384
|
+
export function readNpmTar(tar: Uint8Array, { symlinksAbove = 0, maxEntries = MAX_NPM_TAR_ENTRIES, maxFiles = MAX_NPM_PACKAGE_FILES,
|
|
385
|
+
maxBytes = MAX_NPM_PACKAGE_BYTES }: TarReading = {}): NpmPackageFile[] {
|
|
294
386
|
let entries: Entry[]
|
|
295
|
-
try { entries = unpack(tar, { maxEntries
|
|
387
|
+
try { entries = unpack(tar, { maxEntries, symlinksAbove }) }
|
|
296
388
|
catch (err) {
|
|
297
389
|
if (err instanceof ArchiveError) throw err.limited ? new NpmPackageError(413, 'package-too-large') : new NpmPackageError(422, 'bad-tarball')
|
|
298
390
|
throw err
|
|
@@ -305,7 +397,7 @@ export function readNpmTar(tar: Uint8Array, { symlinksAbove = 0 }: { symlinksAbo
|
|
|
305
397
|
const path = name.slice(slash + 1)
|
|
306
398
|
total += data.length - (files.get(path)?.length ?? 0)
|
|
307
399
|
files.set(path, data)
|
|
308
|
-
if (files.size >
|
|
400
|
+
if (files.size > maxFiles || total > maxBytes) throw new NpmPackageError(413, 'package-too-large')
|
|
309
401
|
}
|
|
310
402
|
return [...files].map(([path, bytes]) => ({ path, bytes })).toSorted((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0)
|
|
311
403
|
}
|
|
@@ -335,13 +427,14 @@ const ROW_LENGTH = 80
|
|
|
335
427
|
|
|
336
428
|
// The files as the viewer gets them: `[path, bytes, text]`, text null for a
|
|
337
429
|
// file that is not UTF-8, which instead carries its bytes' sha256, so a
|
|
338
|
-
// comparison of two versions tells a changed one from an unchanged one.
|
|
339
|
-
|
|
430
|
+
// comparison of two versions tells a changed one from an unchanged one. They
|
|
431
|
+
// may take `maxLength` characters of JSON, a package's unless given.
|
|
432
|
+
export function npmFileRows(files: NpmPackageFile[], maxLength = MAX_NPM_JSON_LENGTH): ([string, number, string] | [string, number, null, string])[] {
|
|
340
433
|
let length = 0
|
|
341
434
|
return files.map(({ path, bytes }) => {
|
|
342
435
|
const text = npmFileText(bytes)
|
|
343
436
|
length += ROW_LENGTH + jsonLength(path) + (text === null ? 0 : jsonLength(text))
|
|
344
|
-
if (length >
|
|
437
|
+
if (length > maxLength) throw new NpmPackageError(413, 'package-too-large')
|
|
345
438
|
return text === null ? [path, bytes.byteLength, null, `sha256-${createHash('sha256').update(bytes).digest('base64')}`] : [path, bytes.byteLength, text]
|
|
346
439
|
})
|
|
347
440
|
}
|
|
@@ -14,8 +14,15 @@
|
|
|
14
14
|
// instance, or this one restarted, asks npm's downloads API, which answers
|
|
15
15
|
// some 40 lookups a minute, none of them again that week, each removed a
|
|
16
16
|
// day past it (npm-kept.ts pruneKept); a private one's in memory.
|
|
17
|
-
// - `risk/npm/packages`, `risk/npm/
|
|
18
|
-
//
|
|
17
|
+
// - `risk/npm/packages`, `risk/npm/old-versions`, `risk/npm/public` and
|
|
18
|
+
// `risk/npm/advisories`, which risk asks again after an hour: a public
|
|
19
|
+
// package's in memory and in the database too, read from memory first, so
|
|
20
|
+
// that a cold instance, as a serverless one starts, reads no whole document
|
|
21
|
+
// of npm's again that one before it read within the hour; by a hash of
|
|
22
|
+
// their type and key, as a dependency's spec runs to any length, each
|
|
23
|
+
// removed a day on, and one past 256 KiB as JSON (a package's deprecated
|
|
24
|
+
// versions, its messages and all, run long) in memory alone. A private
|
|
25
|
+
// one's in memory.
|
|
19
26
|
// - `npm/tarballs`: none asked, as tarballs are read as the viewer's loads
|
|
20
27
|
// read them, kept where they keep them (npm-loads.ts loadNpmTarball), and
|
|
21
28
|
// an assessment kept needs none.
|
|
@@ -35,6 +42,7 @@ import { buffer } from 'node:stream/consumers'
|
|
|
35
42
|
import type { CacheStore } from '@preventive/upstream/npm.js'
|
|
36
43
|
import { DEFAULT_MINIMUM_RELEASE_AGE, type NpmAssessment, assessNpm, npmReadyDownload, npmReadyDownloads } from '../risk/index.js'
|
|
37
44
|
import { CacheMissError, type CacheStorage } from './cache-storage.ts'
|
|
45
|
+
import { npmVersionStore } from './npm-kept.ts'
|
|
38
46
|
import { loadNpmTarball } from './npm-loads.ts'
|
|
39
47
|
import { NpmPackageError, type NpmVersionDocument, npmTokenReads, readNpmPackument, readNpmVersion } from './npm-packages.ts'
|
|
40
48
|
import { npmPackageScope } from '../common/managed/npm-packages.js'
|
|
@@ -68,11 +76,20 @@ function remember(key: string, value: unknown) {
|
|
|
68
76
|
while (memory.size > MEMORY_ENTRIES || memoryLength > MEMORY_LENGTH) forget(memory.keys().next().value!)
|
|
69
77
|
}
|
|
70
78
|
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
//
|
|
79
|
+
const digest = (type: string, key: string) => createHash('sha256').update(`${type}\0${key}`).digest('hex')
|
|
80
|
+
const hashed = (type: string, key: string) => `risk/${digest(type, key)}`
|
|
81
|
+
// What of a public package is kept in the database by its key, and for how
|
|
82
|
+
// long its downloads are: risk's week, and a day more.
|
|
74
83
|
const IN_DATABASE = new Set(['risk/npm/versions', 'risk/npm/downloads'])
|
|
75
84
|
const DOWNLOADS_KEPT_MS = 8 * 24 * 60 * 60_000
|
|
85
|
+
// What of a public package risk asks again after an hour, kept in memory and
|
|
86
|
+
// in the database (HOURLY_PREFIX), each a day, none past HOURLY_LENGTH as
|
|
87
|
+
// JSON there.
|
|
88
|
+
const HOURLY = new Set(['risk/npm/packages', 'risk/npm/old-versions', 'risk/npm/public', 'risk/npm/advisories'])
|
|
89
|
+
const HOURLY_PREFIX = 'risk/hourly/'
|
|
90
|
+
const HOURLY_KEPT_MS = 24 * 60 * 60_000
|
|
91
|
+
const HOURLY_LENGTH = 256 * 1024
|
|
92
|
+
const databaseKey = (type: string, key: string) => HOURLY.has(type) ? `${HOURLY_PREFIX}${digest(type, key)}` : `${type}/${key}`
|
|
76
93
|
|
|
77
94
|
export interface NpmRiskStores {
|
|
78
95
|
db: UpstreamCacheStore
|
|
@@ -81,10 +98,13 @@ export interface NpmRiskStores {
|
|
|
81
98
|
}
|
|
82
99
|
|
|
83
100
|
// A cache that reads and writes many of a type at once too, as risk's
|
|
84
|
-
// readying of downloads asks (npmReadyDownloads)
|
|
85
|
-
//
|
|
86
|
-
//
|
|
101
|
+
// readying of downloads asks (npmReadyDownloads), and reads records of any
|
|
102
|
+
// types at once, as risk reads a package's record and its versions'
|
|
103
|
+
// (registry.js npmPackageFacts): one read and one write of the database each,
|
|
104
|
+
// not one a key, as a request's queries take turns on its connection
|
|
105
|
+
// (db-neon.ts).
|
|
87
106
|
export interface NpmRiskCache extends CacheStore {
|
|
107
|
+
readEach(entries: [type: string, key: string][]): Promise<unknown[]>
|
|
88
108
|
readMany(type: string, keys: string[]): Promise<Map<string, unknown>>
|
|
89
109
|
writeMany(type: string, entries: [key: string, value: unknown][]): Promise<void>
|
|
90
110
|
}
|
|
@@ -93,13 +113,32 @@ export interface NpmRiskCache extends CacheStore {
|
|
|
93
113
|
// that what is kept of it can be shared.
|
|
94
114
|
export function npmRiskCache({ db, npmCache, debug }: NpmRiskStores, isPublic: boolean): NpmRiskCache {
|
|
95
115
|
const warn = (what: string, error: unknown) => { if (debug) console.warn(`managed: risk cache ${what} failed:`, error) }
|
|
116
|
+
const memoryKey = (type: string, key: string) => `${isPublic ? '' : 'private\0'}${type}\0${key}`
|
|
117
|
+
// Whether a record of `type` is read of the database: unless it is one
|
|
118
|
+
// kept in memory too (HOURLY) and there.
|
|
119
|
+
const ofDatabase = (type: string, key: string) => isPublic && (IN_DATABASE.has(type) || HOURLY.has(type) && !memory.has(memoryKey(type, key)))
|
|
120
|
+
// A record as the database keeps it, read: an hourly one kept in memory
|
|
121
|
+
// too, for the next to ask.
|
|
122
|
+
const readOf = (type: string, key: string, row: string | null | undefined) => {
|
|
123
|
+
const value: unknown = row == null ? null : JSON.parse(row)
|
|
124
|
+
if (value !== null && HOURLY.has(type)) remember(memoryKey(type, key), value)
|
|
125
|
+
return value
|
|
126
|
+
}
|
|
96
127
|
const cache: NpmRiskCache = {
|
|
128
|
+
async readEach(entries) {
|
|
129
|
+
const asked = new Set(entries.flatMap(([type, key], i) => ofDatabase(type, key) ? [i] : []))
|
|
130
|
+
let rows = new Map<string, string>()
|
|
131
|
+
if (asked.size > 0) {
|
|
132
|
+
try { rows = await db.getUpstreamCacheEntries([...asked].map(i => databaseKey(...entries[i]!))) } catch (error) { warn('read', error) }
|
|
133
|
+
}
|
|
134
|
+
return Promise.all(entries.map(([type, key], i) => {
|
|
135
|
+
if (!asked.has(i)) return cache.read(type, key)
|
|
136
|
+
try { return readOf(type, key, rows.get(databaseKey(type, key))) } catch (error) { warn('read', error); return null }
|
|
137
|
+
}))
|
|
138
|
+
},
|
|
97
139
|
async readMany(type, keys) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const found = await db.getUpstreamCacheEntries(keys.map(key => `${type}/${key}`))
|
|
101
|
-
return new Map(keys.map(key => { const value = found.get(`${type}/${key}`); return [key, value === undefined ? null : JSON.parse(value)] }))
|
|
102
|
-
} catch (error) { warn('read', error); return new Map() }
|
|
140
|
+
const values = await cache.readEach(keys.map(key => [type, key]))
|
|
141
|
+
return new Map(keys.map((key, i) => [key, values[i]]))
|
|
103
142
|
},
|
|
104
143
|
async writeMany(type, entries) {
|
|
105
144
|
if (!isPublic || !IN_DATABASE.has(type)) { for (const [key, value] of entries) await cache.write(type, key, value); return }
|
|
@@ -111,22 +150,31 @@ export function npmRiskCache({ db, npmCache, debug }: NpmRiskStores, isPublic: b
|
|
|
111
150
|
if (type === 'npm/tarballs') return null
|
|
112
151
|
try {
|
|
113
152
|
if (isPublic && type === 'risk/npm/assessments' && npmCache) return JSON.parse((await buffer((await npmCache.open(hashed(type, key))).stream)).toString('utf8'))
|
|
114
|
-
if (
|
|
153
|
+
if (ofDatabase(type, key)) return readOf(type, key, await db.getUpstreamCacheEntry(databaseKey(type, key)))
|
|
115
154
|
} catch (error) { if (!(error instanceof CacheMissError)) warn('read', error); return null }
|
|
116
|
-
return memory.get(
|
|
155
|
+
return memory.get(memoryKey(type, key))?.value ?? null
|
|
117
156
|
},
|
|
118
157
|
async write(type, key, value) {
|
|
119
158
|
if (type === 'npm/tarballs') return
|
|
159
|
+
if (isPublic && HOURLY.has(type)) remember(memoryKey(type, key), value)
|
|
120
160
|
try {
|
|
121
161
|
if (isPublic && type === 'risk/npm/assessments' && npmCache) { await npmCache.put(hashed(type, key), Buffer.from(JSON.stringify(value))); return }
|
|
122
162
|
if (isPublic && IN_DATABASE.has(type)) {
|
|
123
163
|
const at = Date.now()
|
|
124
|
-
await db.setUpstreamCacheEntry(
|
|
164
|
+
await db.setUpstreamCacheEntry(databaseKey(type, key), JSON.stringify(value), at)
|
|
125
165
|
if (type === 'risk/npm/downloads') await pruneKept(db, `${type}/`, DOWNLOADS_KEPT_MS, at, error => { warn('prune', error) })
|
|
126
166
|
return
|
|
127
167
|
}
|
|
168
|
+
if (isPublic && HOURLY.has(type)) {
|
|
169
|
+
const json = JSON.stringify(value)
|
|
170
|
+
if (json.length > HOURLY_LENGTH) return
|
|
171
|
+
const at = Date.now()
|
|
172
|
+
await db.setUpstreamCacheEntry(databaseKey(type, key), json, at)
|
|
173
|
+
await pruneKept(db, HOURLY_PREFIX, HOURLY_KEPT_MS, at, error => { warn('prune', error) })
|
|
174
|
+
return
|
|
175
|
+
}
|
|
128
176
|
} catch (error) { warn('write', error); return }
|
|
129
|
-
remember(
|
|
177
|
+
remember(memoryKey(type, key), value)
|
|
130
178
|
},
|
|
131
179
|
}
|
|
132
180
|
return cache
|
|
@@ -176,7 +224,9 @@ export function npmRisk(doc: NpmVersionDocument, old: NpmVersionDocument | 'pick
|
|
|
176
224
|
return scopes.get(scope)!
|
|
177
225
|
}
|
|
178
226
|
const given = old === 'pick' ? null : old
|
|
179
|
-
|
|
227
|
+
// The one picked, read as the viewer reads a version, kept a day where public.
|
|
228
|
+
const documentOf = async (version: string) => [doc, given].find(read => read?.version === version)
|
|
229
|
+
?? await readNpmVersion(doc.name, version, privileged, signal, npmVersionStore(stores.db, stores.debug)) ?? undefined
|
|
180
230
|
return bounded(spare, () => assessNpm(doc.name, doc.version, { oldVersion: old === 'pick' ? true : old?.version ?? null, fromEmpty,
|
|
181
231
|
minimumReleaseAge: teamScoped ? 0 : DEFAULT_MINIMUM_RELEASE_AGE,
|
|
182
232
|
popularity: !teamScoped, cache: npmRiskCache(stores, !doc.private && !given?.private), signal, dependencies: !spare, advisories: !spare, osv: !doc.private, owned,
|
|
@@ -12,16 +12,17 @@ import { promisify } from 'node:util'
|
|
|
12
12
|
import { brotliDecompress } from 'node:zlib'
|
|
13
13
|
import { format } from '@preventive/compare/pretty.js'
|
|
14
14
|
import { computeFileHash } from '@preventive/report'
|
|
15
|
-
import { FILE_HASH } from '../common/bundle-metadata.js'
|
|
15
|
+
import { FILE_HASH, computeBundleFileHashes } from '../common/bundle-metadata.js'
|
|
16
16
|
import { bundleCompareFiles } from '../common/bundle-compare-inputs.js'
|
|
17
17
|
import { bundlePackageDirs, bundleSourcesAsMap } from '../common/bundle-sources.js'
|
|
18
18
|
import { MAX_PRETTY_BYTES, needsPrettyPrint, prettyExtension } from '../common/pretty-print.js'
|
|
19
19
|
import type { OpenedBlob } from './blob-store.ts'
|
|
20
|
-
import { type BundleCacheStorage,
|
|
20
|
+
import { type BundleCacheStorage, readSizedBundleDetails } from './bundle-cache.ts'
|
|
21
21
|
import type { BundleStore } from './bundle-store.ts'
|
|
22
22
|
import { encodeBrotli } from './brotli.ts'
|
|
23
23
|
import { CacheMissError, type CacheStorage } from './cache-storage.ts'
|
|
24
24
|
import type { ManagedBundle, ManagedDb } from './db.ts'
|
|
25
|
+
import { keptBriefly } from './kept-briefly.ts'
|
|
25
26
|
import { loadNpmFiles } from './npm-loads.ts'
|
|
26
27
|
import { type NpmVersionDocument, npmFileText } from './npm-packages.ts'
|
|
27
28
|
|
|
@@ -34,6 +35,20 @@ const PRETTY_VERSION = 4
|
|
|
34
35
|
// after another, beside other readers'), any more refused.
|
|
35
36
|
const MAX_ACTIVE_FORMATS = 2
|
|
36
37
|
const MAX_WAITING_FORMATS = 16
|
|
38
|
+
// How long a bundle's details are kept once read, and what the two last read
|
|
39
|
+
// may have decoded from between them, the least lately read going first past
|
|
40
|
+
// it: Compare's requests for a pair of bundles come one after another, each
|
|
41
|
+
// reading both, and the Code tab's for one, each reading it.
|
|
42
|
+
const DETAILS_KEPT_MS = 60_000
|
|
43
|
+
const MAX_KEPT_DETAILS = 2
|
|
44
|
+
const MAX_KEPT_DETAILS_BYTES = 256 * 1024 * 1024
|
|
45
|
+
// Files hashed at once: one at a time, each waits its own round trip to the
|
|
46
|
+
// thread pool; all at once, every one's job is held together.
|
|
47
|
+
const HASHES_AT_ONCE = 32
|
|
48
|
+
// Kept copies a stream reads ahead of the line it is sending (readAhead):
|
|
49
|
+
// one at a time, each waits its own storage round trip; each read whole, as
|
|
50
|
+
// its line will send it.
|
|
51
|
+
const KEPT_AHEAD = 8
|
|
37
52
|
|
|
38
53
|
export class PrettyError extends Error {
|
|
39
54
|
status: number
|
|
@@ -108,6 +123,18 @@ export async function prettyBody(text: string, extension: string): Promise<Buffe
|
|
|
108
123
|
|
|
109
124
|
const opened = (body: Buffer): OpenedBlob => ({ size: body.byteLength, stream: Readable.from([body]) })
|
|
110
125
|
|
|
126
|
+
// A version's file's hash, by its bytes: those of a version's files loaded
|
|
127
|
+
// a little while since are the same bytes (npm-loads.ts), hashed once.
|
|
128
|
+
const fileHashes = new WeakMap<Uint8Array, Promise<string>>()
|
|
129
|
+
function npmFileHash(bytes: Uint8Array): Promise<string> {
|
|
130
|
+
let hash = fileHashes.get(bytes)
|
|
131
|
+
if (!hash) {
|
|
132
|
+
fileHashes.set(bytes, hash = computeFileHash(bytes))
|
|
133
|
+
hash.catch(() => { fileHashes.delete(bytes) })
|
|
134
|
+
}
|
|
135
|
+
return hash
|
|
136
|
+
}
|
|
137
|
+
|
|
111
138
|
const decompress = promisify(brotliDecompress)
|
|
112
139
|
|
|
113
140
|
// A pretty-printed copy's text, from its Brotli-encoded bytes.
|
|
@@ -127,7 +154,7 @@ async function npmPrintable(doc: NpmVersionDocument): Promise<Array<PrettyReques
|
|
|
127
154
|
const extension = prettyExtension(path)
|
|
128
155
|
if (extension === null || bytes.byteLength > MAX_PRETTY_BYTES) continue
|
|
129
156
|
const text = npmFileText(bytes)
|
|
130
|
-
if (text !== null && needsPrettyPrint(path, text)) files.push({ path, hash: await
|
|
157
|
+
if (text !== null && needsPrettyPrint(path, text)) files.push({ path, hash: await npmFileHash(bytes), extension, text })
|
|
131
158
|
}
|
|
132
159
|
} finally { load.release() }
|
|
133
160
|
return files.toSorted((a, b) => (a.path < b.path ? -1 : 1))
|
|
@@ -143,6 +170,29 @@ async function prettyLine({ path, hash }: PrettyRequest, copy: () => Promise<Ope
|
|
|
143
170
|
}
|
|
144
171
|
}
|
|
145
172
|
|
|
173
|
+
// A file's line from its kept copy, `open` opening it, or null where none is
|
|
174
|
+
// kept.
|
|
175
|
+
async function keptLine(request: PrettyRequest, open: () => Promise<OpenedBlob | null>): Promise<PrettyLine | null> {
|
|
176
|
+
const copy = await open()
|
|
177
|
+
return copy && prettyLine(request, () => Promise.resolve(copy))
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Each of `items` with what `read` reads of it, in their order, up to
|
|
181
|
+
// KEPT_AHEAD of them read at once ahead of the one iterated, each answered
|
|
182
|
+
// (or failing) in its turn. One iterated no further starts no more; those
|
|
183
|
+
// under way settle unread.
|
|
184
|
+
async function* readAhead<T, R>(items: readonly T[], read: (item: T) => Promise<R>): AsyncGenerator<[T, R]> {
|
|
185
|
+
const reads: Promise<R>[] = []
|
|
186
|
+
for (const [i, item] of items.entries()) {
|
|
187
|
+
while (reads.length < Math.min(items.length, i + KEPT_AHEAD)) {
|
|
188
|
+
const reading = read(items[reads.length]!)
|
|
189
|
+
reading.catch(() => {})
|
|
190
|
+
reads.push(reading)
|
|
191
|
+
}
|
|
192
|
+
yield [item, await reads[i]!]
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
146
196
|
// The files that changed between two sides, pretty-printed: each side's
|
|
147
197
|
// formattable files the other has another text at the path of, or, where it
|
|
148
198
|
// has none there, none of their text at a path of its own (a file renamed, or
|
|
@@ -151,8 +201,9 @@ async function prettyLine({ path, hash }: PrettyRequest, copy: () => Promise<Ope
|
|
|
151
201
|
// the viewer diffs but where it pairs files otherwise (among copies of one
|
|
152
202
|
// text, across packages), whose copies it asks for alone. As they are
|
|
153
203
|
// iterated, in path order, the base's first where both have one: those kept,
|
|
154
|
-
// at once; then the rest, formatted one after
|
|
155
|
-
// the others (needsPrettyPrint), as the viewer
|
|
204
|
+
// at once, a few read ahead (readAhead); then the rest, formatted one after
|
|
205
|
+
// another, the minified before the others (needsPrettyPrint), as the viewer
|
|
206
|
+
// prints those unasked.
|
|
156
207
|
export function compared(sides: readonly [ComparedSide, ComparedSide]): AsyncGenerator<ComparedLine> {
|
|
157
208
|
// Each side's texts at the paths the other hasn't: where a file moved has its own.
|
|
158
209
|
const moved = sides.map((own, i) => new Set([...own.hashes].filter(([path]) => !sides[1 - i]!.hashes.has(path)).map(([, hash]) => hash)))
|
|
@@ -162,9 +213,8 @@ export function compared(sides: readonly [ComparedSide, ComparedSide]): AsyncGen
|
|
|
162
213
|
}).toSorted((a, b) => (a.file.path < b.file.path ? -1 : a.file.path > b.file.path ? 1 : 0))
|
|
163
214
|
return (async function* () {
|
|
164
215
|
const unkept: typeof changed = []
|
|
165
|
-
for (const { own, file } of changed) {
|
|
166
|
-
|
|
167
|
-
if (copy) yield { side: own.side, ...await prettyLine(file, () => Promise.resolve(copy)) }
|
|
216
|
+
for await (const [{ own, file }, line] of readAhead(changed, item => keptLine(item.file, () => item.own.kept(item.file)))) {
|
|
217
|
+
if (line) yield { side: own.side, ...line }
|
|
168
218
|
else unkept.push({ own, file })
|
|
169
219
|
}
|
|
170
220
|
const minified = new Map(unkept.map(({ file }) => [file, needsPrettyPrint(file.path, file.text)]))
|
|
@@ -206,26 +256,35 @@ async function kept(open: () => Promise<OpenedBlob>): Promise<OpenedBlob | null>
|
|
|
206
256
|
|
|
207
257
|
// A side's files that can be formatted, with their texts, and every one of
|
|
208
258
|
// its files' hashes by path, from their paths and contents: texts, or bytes,
|
|
209
|
-
// read as text where they are.
|
|
210
|
-
async function sideFiles(entries:
|
|
259
|
+
// read as text where they are; hashed by `hashOf`, a few at once.
|
|
260
|
+
async function sideFiles<T extends string | Uint8Array>(entries: Array<[string, T]>, hashOf: (file: string, content: T) => Promise<string>): Promise<Pick<ComparedSide, 'files' | 'hashes'>> {
|
|
211
261
|
const files: ComparedSide['files'] = [], hashes = new Map<string, string>()
|
|
262
|
+
for (let i = 0; i < entries.length; i += HASHES_AT_ONCE) {
|
|
263
|
+
const chunk = entries.slice(i, i + HASHES_AT_ONCE)
|
|
264
|
+
const hashed = await Promise.all(chunk.map(async ([path, content]) => [path, await hashOf(path, content)] as const))
|
|
265
|
+
for (const [path, hash] of hashed) hashes.set(path, hash)
|
|
266
|
+
}
|
|
212
267
|
for (const [path, content] of entries) {
|
|
213
|
-
const hash = await computeFileHash(content)
|
|
214
|
-
hashes.set(path, hash)
|
|
215
268
|
const extension = prettyExtension(path)
|
|
216
269
|
if (extension === null || (typeof content === 'string' ? Buffer.byteLength(content) : content.byteLength) > MAX_PRETTY_BYTES) continue
|
|
217
270
|
const text = typeof content === 'string' ? content : npmFileText(content)
|
|
218
|
-
if (text !== null) files.push({ path, hash
|
|
271
|
+
if (text !== null) files.push({ path, hash: hashes.get(path)!, extension, text })
|
|
219
272
|
}
|
|
220
273
|
return { files, hashes }
|
|
221
274
|
}
|
|
222
275
|
|
|
223
276
|
// A bundle's side, from its details: its files, as sideFiles reads them,
|
|
224
277
|
// within a reason `scope` as Compare reads them there (bundleCompareFiles),
|
|
225
|
-
// and its packages' directories
|
|
278
|
+
// and its packages' directories; hashed once for its details, as long as
|
|
279
|
+
// they are kept (computeBundleFileHashes).
|
|
226
280
|
async function bundleSide(details: Parameters<typeof bundlePackageDirs>[0], scope: string): Promise<Pick<ComparedSide, 'files' | 'hashes' | 'packageDirs'>> {
|
|
227
281
|
const scoped = scope ? new Set(bundleCompareFiles(details, scope).keys()) : null, sources = bundleSourcesAsMap(details)
|
|
228
|
-
|
|
282
|
+
const hashed: Map<string, string> = await computeBundleFileHashes(details)
|
|
283
|
+
return { ...await sideFiles(scoped ? [...sources].filter(([path]) => scoped.has(path)) : [...sources], (path, content) => {
|
|
284
|
+
const hash = hashed.get(path)
|
|
285
|
+
return hash === undefined ? computeFileHash(content) : Promise.resolve(hash)
|
|
286
|
+
}),
|
|
287
|
+
packageDirs: bundlePackageDirs(details) }
|
|
229
288
|
}
|
|
230
289
|
|
|
231
290
|
// A version's files by path, from one load of them.
|
|
@@ -251,14 +310,25 @@ export function stalePrettyRemover(npm: CacheStorage): (signal?: AbortSignal) =>
|
|
|
251
310
|
}
|
|
252
311
|
|
|
253
312
|
// A bundle's details, from its bytes in `store`: bundles are read one at a
|
|
254
|
-
// time, as reading one decodes all of it
|
|
313
|
+
// time, as reading one decodes all of it, readers asking for one waiting its
|
|
314
|
+
// turn or being read sharing it; and kept a little while once read
|
|
315
|
+
// (DETAILS_KEPT_MS), each reader's access to the bundle the caller's to check
|
|
316
|
+
// as ever.
|
|
255
317
|
function detailsReader(store: BundleStore) {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
318
|
+
type Details = NonNullable<Awaited<ReturnType<typeof readSizedBundleDetails>>>['details']
|
|
319
|
+
const enqueue = serial(), share = sharing<Details>()
|
|
320
|
+
const held = keptBriefly<Details>({ keptMs: DETAILS_KEPT_MS, maxBytes: MAX_KEPT_DETAILS_BYTES, maxEntries: MAX_KEPT_DETAILS })
|
|
321
|
+
return (record: ManagedBundle): Promise<Details> => {
|
|
322
|
+
const key = `${record.id} ${record.integrity}`
|
|
323
|
+
const hit = held.get(key)
|
|
324
|
+
if (hit) return Promise.resolve(hit)
|
|
325
|
+
return share(key, () => enqueue(async () => {
|
|
326
|
+
const read = await readSizedBundleDetails(record, store)
|
|
327
|
+
if (!read) throw new PrettyError(422, 'bundle-unavailable')
|
|
328
|
+
held.keep(key, read.details, read.decodedBytes)
|
|
329
|
+
return read.details
|
|
330
|
+
}))
|
|
331
|
+
}
|
|
262
332
|
}
|
|
263
333
|
|
|
264
334
|
export function createPrettyCache(bundles: BundleCacheStorage, npm: CacheStorage, db: Pick<ManagedDb, 'getBundle'>, store: BundleStore) {
|
|
@@ -282,15 +352,18 @@ export function createPrettyCache(bundles: BundleCacheStorage, npm: CacheStorage
|
|
|
282
352
|
return body
|
|
283
353
|
})
|
|
284
354
|
}
|
|
355
|
+
// The copy kept for a public version's file, or null where none is (and
|
|
356
|
+
// for a private version's, which none is kept for).
|
|
357
|
+
const npmKept = (doc: NpmVersionDocument, request: PrettyRequest) => (doc.private ? Promise.resolve(null) : kept(() => npm.open(prettyFile(request))))
|
|
285
358
|
// A version's file pretty-printed, `read` reading its text (and checking
|
|
286
359
|
// it is the file named): from the copy kept for a public version, else
|
|
287
360
|
// made, and kept for a public one.
|
|
288
361
|
async function npmCopy(doc: NpmVersionDocument, request: PrettyRequest, read: () => Promise<string>): Promise<OpenedBlob> {
|
|
362
|
+
return await npmKept(doc, request) ?? npmMade(doc, request, read)
|
|
363
|
+
}
|
|
364
|
+
// The file pretty-printed from the text `read` reads, kept for a public version.
|
|
365
|
+
async function npmMade(doc: NpmVersionDocument, request: PrettyRequest, read: () => Promise<string>): Promise<OpenedBlob> {
|
|
289
366
|
const file = prettyFile(request)
|
|
290
|
-
if (!doc.private) {
|
|
291
|
-
try { return await npm.open(file) }
|
|
292
|
-
catch (err) { if (!(err instanceof CacheMissError)) throw err }
|
|
293
|
-
}
|
|
294
367
|
return opened(await shared(doc.private ? `npm ${doc.dist.integrity} ${file}` : `npm ${file}`, async () => {
|
|
295
368
|
const body = await prettyBody(await read(), request.extension)
|
|
296
369
|
if (!doc.private) await keep(() => npm.put(file, body))
|
|
@@ -317,7 +390,7 @@ export function createPrettyCache(bundles: BundleCacheStorage, npm: CacheStorage
|
|
|
317
390
|
try {
|
|
318
391
|
const bytes = (await load.result).find(entry => entry.path === request.path)?.bytes
|
|
319
392
|
if (!bytes) throw new PrettyError(404, 'no-file')
|
|
320
|
-
if (await
|
|
393
|
+
if (await npmFileHash(bytes) !== request.hash) throw new PrettyError(409, 'hash-mismatch')
|
|
321
394
|
text = npmFileText(bytes)
|
|
322
395
|
} finally { load.release() }
|
|
323
396
|
if (text === null) throw new PrettyError(422, 'unformattable')
|
|
@@ -326,11 +399,14 @@ export function createPrettyCache(bundles: BundleCacheStorage, npm: CacheStorage
|
|
|
326
399
|
},
|
|
327
400
|
// Every file of the version that needs pretty-printing (npmPrintable),
|
|
328
401
|
// read before this resolves; then, as they are iterated, each
|
|
329
|
-
// pretty-printed as `npm` has it, one after another
|
|
402
|
+
// pretty-printed as `npm` has it, one after another, those kept a few
|
|
403
|
+
// read ahead (readAhead).
|
|
330
404
|
async npmAll(doc: NpmVersionDocument): Promise<AsyncGenerator<PrettyLine>> {
|
|
331
405
|
const files = await npmPrintable(doc)
|
|
332
406
|
return (async function* () {
|
|
333
|
-
for (const
|
|
407
|
+
for await (const [file, line] of readAhead(files, request => keptLine(request, () => npmKept(doc, request)))) {
|
|
408
|
+
yield line ?? await prettyLine(file, () => npmMade(doc, file, () => Promise.resolve(file.text)))
|
|
409
|
+
}
|
|
334
410
|
})()
|
|
335
411
|
},
|
|
336
412
|
// Two bundles' sides, or two versions', as a comparison reads them, read
|
|
@@ -338,19 +414,20 @@ export function createPrettyCache(bundles: BundleCacheStorage, npm: CacheStorage
|
|
|
338
414
|
// pretty-printed (compared), and their diffs with renamed names hidden
|
|
339
415
|
// (rename-diff.ts renamedPairs); a bundle's within a reason `scope`
|
|
340
416
|
// where one is asked for, as Compare reads it then.
|
|
341
|
-
|
|
417
|
+
bundleSides(base: ManagedBundle, other: ManagedBundle, scope = ''): Promise<[ComparedSide, ComparedSide]> {
|
|
342
418
|
const sideOf = async (record: ManagedBundle, side: Side): Promise<ComparedSide> => ({ side, ...await bundleSide(await bundleDetails(record), scope),
|
|
343
419
|
kept: request => kept(() => bundles.open(record.id, prettyFile(request))),
|
|
344
420
|
made: async (request, text) => opened(await bundleCopy(record, request, () => Promise.resolve(text))) })
|
|
345
|
-
return [
|
|
421
|
+
return Promise.all([sideOf(base, 'base'), sideOf(other, 'other')])
|
|
346
422
|
},
|
|
347
423
|
// A bundle's details, read in its turn, as Compare pairs them (comparison.ts).
|
|
348
424
|
bundleDetails,
|
|
349
|
-
|
|
350
|
-
const sideOf = async (doc: NpmVersionDocument, side: Side): Promise<ComparedSide> => ({ side, ...await sideFiles(await npmFiles(doc)
|
|
351
|
-
|
|
425
|
+
npmSides(base: NpmVersionDocument, other: NpmVersionDocument): Promise<[ComparedSide, ComparedSide]> {
|
|
426
|
+
const sideOf = async (doc: NpmVersionDocument, side: Side): Promise<ComparedSide> => ({ side, ...await sideFiles(await npmFiles(doc), (_, bytes) => npmFileHash(bytes)),
|
|
427
|
+
packageDirs: null,
|
|
428
|
+
kept: request => npmKept(doc, request),
|
|
352
429
|
made: (request, text) => npmCopy(doc, request, () => Promise.resolve(text)) })
|
|
353
|
-
return [
|
|
430
|
+
return Promise.all([sideOf(base, 'base'), sideOf(other, 'other')])
|
|
354
431
|
},
|
|
355
432
|
}
|
|
356
433
|
}
|