@powerhousedao/registry 6.2.3-dev.39 → 6.2.3-dev.40

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/dist/cli.d.mts CHANGED
@@ -10,6 +10,7 @@ export declare const registryCommand: Partial<import("cmd-ts/dist/cjs/argparser.
10
10
  s3SecretAccessKey: string | undefined;
11
11
  s3KeyPrefix: string | undefined;
12
12
  s3ForcePathStyle: boolean;
13
+ s3MaxWritesPerSecond: number | undefined;
13
14
  databaseUrl: string | undefined;
14
15
  listenDatabaseUrl: string | undefined;
15
16
  webhooks: string | undefined;
@@ -40,6 +41,7 @@ export declare const registryCommand: Partial<import("cmd-ts/dist/cjs/argparser.
40
41
  s3SecretAccessKey: string | undefined;
41
42
  s3KeyPrefix: string | undefined;
42
43
  s3ForcePathStyle: boolean;
44
+ s3MaxWritesPerSecond: number | undefined;
43
45
  databaseUrl: string | undefined;
44
46
  listenDatabaseUrl: string | undefined;
45
47
  webhooks: string | undefined;
@@ -73,6 +75,7 @@ export declare const workerCommand: Partial<import("cmd-ts/dist/cjs/argparser.js
73
75
  s3SecretAccessKey: string | undefined;
74
76
  s3KeyPrefix: string | undefined;
75
77
  s3ForcePathStyle: boolean;
78
+ s3MaxWritesPerSecond: number | undefined;
76
79
  databaseUrl: string | undefined;
77
80
  listenDatabaseUrl: string | undefined;
78
81
  webhooks: string | undefined;
@@ -92,6 +95,7 @@ export declare const workerCommand: Partial<import("cmd-ts/dist/cjs/argparser.js
92
95
  s3SecretAccessKey: string | undefined;
93
96
  s3KeyPrefix: string | undefined;
94
97
  s3ForcePathStyle: boolean;
98
+ s3MaxWritesPerSecond: number | undefined;
95
99
  databaseUrl: string | undefined;
96
100
  listenDatabaseUrl: string | undefined;
97
101
  webhooks: string | undefined;
@@ -127,6 +131,7 @@ export declare const importVerdaccioStateCommand: Partial<import("cmd-ts/dist/cj
127
131
  s3SecretAccessKey: string | undefined;
128
132
  s3KeyPrefix: string | undefined;
129
133
  s3ForcePathStyle: boolean;
134
+ s3MaxWritesPerSecond: number | undefined;
130
135
  databaseUrl: string | undefined;
131
136
  listenDatabaseUrl: string | undefined;
132
137
  webhooks: string | undefined;
@@ -141,6 +146,7 @@ export declare const importVerdaccioStateCommand: Partial<import("cmd-ts/dist/cj
141
146
  s3SecretAccessKey: string | undefined;
142
147
  s3KeyPrefix: string | undefined;
143
148
  s3ForcePathStyle: boolean;
149
+ s3MaxWritesPerSecond: number | undefined;
144
150
  databaseUrl: string | undefined;
145
151
  listenDatabaseUrl: string | undefined;
146
152
  webhooks: string | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.mts","names":[],"sources":["../cli.ts"],"mappings":";qBA2Ia,iBAAe,+CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IA2I1B,6CAAA,iDAAA,mDAAA;qBAEW,eAAa,+CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAyCxB,6CAAA,iDAAA,mDAAA;qBAEW,gBAAc,+CAAA;;;;;;;;;;;;IAkBzB,6CAAA,iDAAA,mDAAA;qBAEW,6BAA2B,+CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAatC,6CAAA,iDAAA,mDAAA"}
1
+ {"version":3,"file":"cli.d.mts","names":[],"sources":["../cli.ts"],"mappings":";qBAkJa,iBAAe,+CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IA2I1B,6CAAA,iDAAA,mDAAA;qBAEW,eAAa,+CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAyCxB,6CAAA,iDAAA,mDAAA;qBAEW,gBAAc,+CAAA;;;;;;;;;;;;IAkBzB,6CAAA,iDAAA,mDAAA;qBAEW,6BAA2B,+CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAatC,6CAAA,iDAAA,mDAAA"}
package/dist/cli.mjs CHANGED
@@ -1882,9 +1882,27 @@ async function forgetWith(query, name) {
1882
1882
  await query("DELETE FROM verdaccio_manifests WHERE name = $1", [name]);
1883
1883
  }
1884
1884
  //#endregion
1885
+ //#region src/pace.ts
1886
+ /** Spaces calls to at most `perSecond`, in the order they ask. */
1887
+ function pacer(perSecond) {
1888
+ const gap = 1e3 / perSecond;
1889
+ let next = 0;
1890
+ return () => {
1891
+ const now = Date.now();
1892
+ const at = Math.max(now, next);
1893
+ next = at + gap;
1894
+ return at > now ? new Promise((resolve) => setTimeout(resolve, at - now)) : Promise.resolve();
1895
+ };
1896
+ }
1897
+ //#endregion
1885
1898
  //#region src/artifacts.ts
1899
+ const BUFFERED_UPLOAD_BYTES = 4194304;
1900
+ const DEFAULT_WRITES_PER_SECOND = 150;
1886
1901
  function createS3ArtifactStore(config) {
1902
+ const paceWrite = pacer(config.maxWritesPerSecond ?? DEFAULT_WRITES_PER_SECOND);
1887
1903
  const s3 = new S3Client({
1904
+ retryMode: "adaptive",
1905
+ maxAttempts: 5,
1888
1906
  endpoint: config.endpoint,
1889
1907
  region: config.region,
1890
1908
  forcePathStyle: config.s3ForcePathStyle ?? true,
@@ -1910,6 +1928,7 @@ function createS3ArtifactStore(config) {
1910
1928
  const root = `${config.keyPrefix ?? ""}artifacts/`;
1911
1929
  return {
1912
1930
  async put(key, body, contentType) {
1931
+ await paceWrite();
1913
1932
  await s3.send(new PutObjectCommand({
1914
1933
  Bucket: config.bucket,
1915
1934
  Key: root + key,
@@ -1919,9 +1938,22 @@ function createS3ArtifactStore(config) {
1919
1938
  },
1920
1939
  async putFile(key, file, contentType) {
1921
1940
  const { size } = await fs.promises.stat(file);
1941
+ if (size <= BUFFERED_UPLOAD_BYTES) {
1942
+ const body = await fs.promises.readFile(file);
1943
+ await paceWrite();
1944
+ await s3.send(new PutObjectCommand({
1945
+ Bucket: config.bucket,
1946
+ Key: root + key,
1947
+ Body: body,
1948
+ ContentLength: size,
1949
+ ContentType: contentType
1950
+ }));
1951
+ return;
1952
+ }
1922
1953
  const body = fs.createReadStream(file);
1923
1954
  body.on("error", () => void 0);
1924
1955
  try {
1956
+ await paceWrite();
1925
1957
  await s3.send(new PutObjectCommand({
1926
1958
  Bucket: config.bucket,
1927
1959
  Key: root + key,
@@ -1959,10 +1991,13 @@ function createS3ArtifactStore(config) {
1959
1991
  ContinuationToken: token
1960
1992
  }));
1961
1993
  const keys = (listed.Contents ?? []).flatMap((o) => o.Key ? [{ Key: o.Key }] : []);
1962
- if (keys.length > 0) await s3.send(new DeleteObjectsCommand({
1963
- Bucket: config.bucket,
1964
- Delete: { Objects: keys }
1965
- }));
1994
+ if (keys.length > 0) {
1995
+ await paceWrite();
1996
+ await s3.send(new DeleteObjectsCommand({
1997
+ Bucket: config.bucket,
1998
+ Delete: { Objects: keys }
1999
+ }));
2000
+ }
1966
2001
  token = listed.IsTruncated ? listed.NextContinuationToken : void 0;
1967
2002
  } while (token);
1968
2003
  }
@@ -2406,6 +2441,12 @@ const MIGRATIONS = [
2406
2441
  name: "reconcile by revision",
2407
2442
  transaction: false,
2408
2443
  statements: ["ALTER TABLE registry_packages ADD COLUMN IF NOT EXISTS manifest_rev text", ...indexConcurrently("registry_versions_pending", "ON registry_versions (updated_at) WHERE status = 'pending'")]
2444
+ },
2445
+ {
2446
+ id: 7,
2447
+ name: "permanent version failures",
2448
+ transaction: false,
2449
+ statements: ["ALTER TABLE registry_versions ADD COLUMN IF NOT EXISTS permanent boolean NOT NULL DEFAULT false"]
2409
2450
  }
2410
2451
  ];
2411
2452
  const MIGRATIONS_TABLE = `CREATE TABLE IF NOT EXISTS registry_migrations (
@@ -3036,9 +3077,11 @@ function publisherOf$1(job) {
3036
3077
  async function processVersion(ctx, job, finish) {
3037
3078
  const pkg = job.package;
3038
3079
  const version = job.version;
3039
- const local = await publishedHere(ctx.db, pkg) ?? job.payload.local === true;
3080
+ const published = await publishedHere(ctx.db, pkg);
3081
+ const local = published ?? job.payload.local === true;
3040
3082
  const manifestRev = await indexedRevision(ctx.db, pkg);
3041
3083
  const packument = await fetchPackument(ctx.registryUrl, pkg);
3084
+ if (!packument && published) throw new Error(`metadata for published ${pkg} returned 404`);
3042
3085
  if (!packument?.versions?.[version]) {
3043
3086
  await ctx.db.transaction(async (tx) => {
3044
3087
  await removeVersionRows(tx, pkg, [version]);
@@ -3098,14 +3141,16 @@ async function processVersion(ctx, job, finish) {
3098
3141
  return true;
3099
3142
  }
3100
3143
  /** Records a version that can't be processed. */
3101
- async function failVersion(ctx, job, error) {
3102
- await ctx.db.query(`INSERT INTO registry_versions (package, version, status, error)
3103
- VALUES ($1, $2, 'failed', $3)
3144
+ async function failVersion(ctx, job, error, permanent = false) {
3145
+ await ctx.db.query(`INSERT INTO registry_versions (package, version, status, error, permanent)
3146
+ VALUES ($1, $2, 'failed', $3, $4)
3104
3147
  ON CONFLICT (package, version) DO UPDATE SET
3105
- status = 'failed', error = EXCLUDED.error, updated_at = now()`, [
3148
+ status = 'failed', error = EXCLUDED.error,
3149
+ permanent = EXCLUDED.permanent, updated_at = now()`, [
3106
3150
  job.package,
3107
3151
  job.version,
3108
- error
3152
+ error,
3153
+ permanent
3109
3154
  ]);
3110
3155
  await ctx.events.publish({
3111
3156
  type: "version-failed",
@@ -3163,6 +3208,7 @@ const CACHE_TTL_MS = 3e4;
3163
3208
  const POLL_MS = 1e3;
3164
3209
  const ON_DEMAND_TIMEOUT_MS = 6e4;
3165
3210
  const VERSION_CACHE_MAX = 5e3;
3211
+ const FAILED_RETRY_AFTER = "15 minutes";
3166
3212
  function pieceColumns(table) {
3167
3213
  return `${table}.name, ${table}.version, ${table}.package,
3168
3214
  ${table}.display_name AS "displayName", ${table}.description,
@@ -3330,8 +3376,14 @@ var Catalog = class {
3330
3376
  const cached = this.#versions.get(key);
3331
3377
  if (this.#fresh(cached) && cached.value?.status === "ready") return cached.value;
3332
3378
  const row = (await this.#db.query(`SELECT package, version, status, manifest,
3333
- package_json_version AS "packageJsonVersion", files, error
3334
- FROM registry_versions WHERE package = $1 AND version = $2`, [pkg, version])).rows[0] ?? null;
3379
+ package_json_version AS "packageJsonVersion", files, error,
3380
+ (status = 'failed' AND NOT permanent
3381
+ AND updated_at < now() - $3::interval) AS retryable
3382
+ FROM registry_versions WHERE package = $1 AND version = $2`, [
3383
+ pkg,
3384
+ version,
3385
+ FAILED_RETRY_AFTER
3386
+ ])).rows[0] ?? null;
3335
3387
  if (row?.status === "ready") {
3336
3388
  this.#versions.delete(key);
3337
3389
  this.#versions.set(key, {
@@ -3357,11 +3409,20 @@ var Catalog = class {
3357
3409
  kind: "ready",
3358
3410
  row
3359
3411
  };
3360
- if (row?.status === "failed") return {
3412
+ if (row?.status === "failed" && !row.retryable) return {
3361
3413
  kind: "failed",
3362
3414
  error: row.error ?? ""
3363
3415
  };
3364
- if (!row) {
3416
+ if (row?.status === "failed") {
3417
+ const local = (await this.packageRow(pkg))?.local === true;
3418
+ await this.#db.transaction(async (tx) => {
3419
+ if ((await tx.query(`UPDATE registry_versions SET status = 'pending', error = NULL, updated_at = now()
3420
+ WHERE package = $1 AND version = $2 AND status = 'failed'
3421
+ RETURNING version`, [pkg, version])).rows.length === 0) return;
3422
+ await enqueue(tx, "process", pkg, version, {}, local ? 0 : 5);
3423
+ });
3424
+ await wakeWorkers(this.#db);
3425
+ } else if (!row) {
3365
3426
  if (!(await fetchPackument(this.#registryUrl(), pkg))?.versions?.[version]) return { kind: "missing" };
3366
3427
  const local = (await this.packageRow(pkg))?.local === true;
3367
3428
  await this.#db.transaction(async (tx) => {
@@ -3623,7 +3684,9 @@ const SECONDS_BUCKETS = [
3623
3684
  1,
3624
3685
  2.5,
3625
3686
  5,
3626
- 10
3687
+ 10,
3688
+ 30,
3689
+ 60
3627
3690
  ];
3628
3691
  const JOB_BUCKETS = [
3629
3692
  .05,
@@ -3637,9 +3700,12 @@ const JOB_BUCKETS = [
3637
3700
  30,
3638
3701
  60,
3639
3702
  120,
3640
- 300
3703
+ 300,
3704
+ 600,
3705
+ 1800
3641
3706
  ];
3642
3707
  const QUEUE_CACHE_MS = 5e3;
3708
+ const CATALOG_CACHE_MS = 3e4;
3643
3709
  /** Event-loop delay, memory and CPU of this process. */
3644
3710
  function processMetrics(metrics) {
3645
3711
  const loop = monitorEventLoopDelay({ resolution: 10 });
@@ -3734,9 +3800,65 @@ function databaseMetrics(metrics, db) {
3734
3800
  metrics.gauge("registry_jobs_oldest_due_seconds", "Age of the oldest unclaimed due job, by priority", byPriority("oldest_due"));
3735
3801
  metrics.gauge("registry_listen_connected", "1 while the LISTEN connection is up", () => [{ value: db.listening() ? 1 : 0 }]);
3736
3802
  }
3803
+ /**
3804
+ * Package and version counts. They're the same from every process, so only
3805
+ * one that runs jobs registers them.
3806
+ */
3807
+ function catalogMetrics(metrics, db) {
3808
+ let catalogCached;
3809
+ const catalog = () => {
3810
+ if (!catalogCached || Date.now() - catalogCached.at > CATALOG_CACHE_MS) {
3811
+ const row = db.query(`SELECT
3812
+ (SELECT count(*) FROM registry_packages WHERE local)::int AS local,
3813
+ (SELECT count(*) FROM registry_packages
3814
+ WHERE local AND listed_manifest IS NOT NULL)::int AS listed,
3815
+ count(*) FILTER (WHERE status = 'ready')::int AS ready,
3816
+ count(*) FILTER (WHERE status = 'pending')::int AS pending,
3817
+ count(*) FILTER (WHERE status = 'failed' AND NOT permanent)::int AS failed,
3818
+ count(*) FILTER (WHERE status = 'failed' AND permanent)::int AS failed_permanent
3819
+ FROM registry_versions`).then((r) => r.rows[0]);
3820
+ catalogCached = {
3821
+ at: Date.now(),
3822
+ row
3823
+ };
3824
+ row.catch(() => catalogCached = void 0);
3825
+ }
3826
+ return catalogCached.row;
3827
+ };
3828
+ metrics.gauge("registry_packages", "Packages published here (local), and those /packages lists (listed)", async () => {
3829
+ const row = await catalog();
3830
+ return ["local", "listed"].map((state) => ({
3831
+ labels: { state },
3832
+ value: row[state]
3833
+ }));
3834
+ });
3835
+ metrics.gauge("registry_versions", "Processed versions, by status; failed_permanent is never retried", async () => {
3836
+ const row = await catalog();
3837
+ return [
3838
+ "ready",
3839
+ "pending",
3840
+ "failed",
3841
+ "failed_permanent"
3842
+ ].map((status) => ({
3843
+ labels: { status },
3844
+ value: row[status]
3845
+ }));
3846
+ });
3847
+ }
3848
+ /** A bounded label for why a job failed. */
3849
+ function errorReason(err, permanent = false) {
3850
+ const name = err.name ?? "";
3851
+ const message = err instanceof Error ? err.message : String(err);
3852
+ if (name === "SlowDown" || /reduce your request rate/i.test(message)) return "s3_throttled";
3853
+ if (permanent) return "permanent";
3854
+ if (/returned 404/.test(message)) return "not_found";
3855
+ if (name === "TimeoutError" || /timed? ?out/i.test(message)) return "timeout";
3856
+ return "other";
3857
+ }
3737
3858
  function workerMetrics(metrics) {
3738
3859
  return {
3739
3860
  jobDuration: metrics.histogram("registry_job_duration_seconds", "Time to run a job, by kind and outcome (done, retry, failed)", JOB_BUCKETS),
3861
+ jobErrors: metrics.counter("registry_job_errors_total", "Job failures, by kind and reason (s3_throttled, not_found, timeout, permanent, other)"),
3740
3862
  reconcileQueued: metrics.counter("registry_reconcile_queued_total", "Jobs reconcile queued, by pass (quick, full)"),
3741
3863
  reconcileRuns: metrics.counter("registry_reconcile_runs_total", "Reconcile passes, by pass (quick, full)")
3742
3864
  };
@@ -4859,6 +4981,11 @@ const FULL_MISSING = `
4859
4981
  LEFT JOIN registry_versions r ON r.package = m.name AND r.version = v.version
4860
4982
  WHERE m.updated_at < now() - interval '30 seconds' AND ${PUBLISHED}
4861
4983
  AND (r.package IS NULL
4984
+ OR (r.status = 'failed' AND NOT r.permanent
4985
+ AND r.updated_at < now() - interval '1 hour'
4986
+ AND NOT EXISTS (SELECT 1 FROM registry_jobs j
4987
+ WHERE j.kind = 'process' AND j.package = m.name
4988
+ AND j.version = v.version))
4862
4989
  OR (r.status = 'pending' AND r.updated_at < now() - interval '5 minutes'
4863
4990
  AND NOT EXISTS (SELECT 1 FROM registry_jobs j
4864
4991
  WHERE j.kind = 'process' AND j.package = m.name
@@ -4907,7 +5034,10 @@ function publisherOf(job) {
4907
5034
  async function runJob(ctx, job, options) {
4908
5035
  const finish = (tx) => complete(tx, job);
4909
5036
  if (job.priority === 10 && await publishedHere(ctx.db, job.package) === false) {
4910
- await ctx.db.transaction(finish);
5037
+ await ctx.db.transaction(async (tx) => {
5038
+ await tx.query("UPDATE registry_packages SET local = false WHERE name = $1 AND local", [job.package]);
5039
+ await finish(tx);
5040
+ });
4911
5041
  return;
4912
5042
  }
4913
5043
  if (job.kind === "process") {
@@ -4978,10 +5108,14 @@ async function startWorker(ctx, options) {
4978
5108
  console.error(`[registry] ${job.kind} ${job.package}${job.version ? `@${job.version}` : ""} failed (attempt ${job.attempts}):`, message);
4979
5109
  const finalAttempt = err instanceof PermanentError || job.attempts >= 5;
4980
5110
  observe(finalAttempt ? "failed" : "retry");
5111
+ options.metrics?.jobErrors.inc({
5112
+ kind: job.kind,
5113
+ reason: errorReason(err, err instanceof PermanentError)
5114
+ });
4981
5115
  try {
4982
5116
  if (!finalAttempt) await retry(ctx.db, job, message);
4983
5117
  else {
4984
- if (job.kind === "process") await failVersion(ctx, job, message);
5118
+ if (job.kind === "process") await failVersion(ctx, job, message, err instanceof PermanentError);
4985
5119
  await complete(ctx.db, job);
4986
5120
  }
4987
5121
  } catch (recordErr) {
@@ -5051,7 +5185,8 @@ function s3From(args) {
5051
5185
  accessKeyId: args.s3AccessKeyId,
5052
5186
  secretAccessKey: args.s3SecretAccessKey,
5053
5187
  keyPrefix: args.s3KeyPrefix,
5054
- s3ForcePathStyle: args.s3ForcePathStyle
5188
+ s3ForcePathStyle: args.s3ForcePathStyle,
5189
+ maxWritesPerSecond: args.s3MaxWritesPerSecond
5055
5190
  };
5056
5191
  }
5057
5192
  function webhooksFrom(list) {
@@ -5244,12 +5379,15 @@ async function runRegistry(args) {
5244
5379
  console.log(` Data: ${cdnCachePath}`);
5245
5380
  if (config.s3) console.log(` S3: ${config.s3.endpoint}/${config.s3.bucket}`);
5246
5381
  if (config.renown) console.log(` Renown auth: ${config.renown.publicUrl}`);
5247
- if (workers > 0) worker = startWorker(processorContext(runtime, localUrl), {
5248
- concurrency: workers,
5249
- webhooks: runtime.webhooks,
5250
- sweep: true,
5251
- metrics: jobMetrics
5252
- });
5382
+ if (workers > 0) {
5383
+ catalogMetrics(metrics, runtime.db);
5384
+ worker = startWorker(processorContext(runtime, localUrl), {
5385
+ concurrency: workers,
5386
+ webhooks: runtime.webhooks,
5387
+ sweep: true,
5388
+ metrics: jobMetrics
5389
+ });
5390
+ }
5253
5391
  });
5254
5392
  server.keepAliveTimeout = 65e3;
5255
5393
  server.headersTimeout = 66e3;
@@ -5290,6 +5428,7 @@ async function runWorker(args) {
5290
5428
  if (args.metricsPort !== void 0) {
5291
5429
  processMetrics(metrics);
5292
5430
  databaseMetrics(metrics, runtime.db);
5431
+ catalogMetrics(metrics, runtime.db);
5293
5432
  metricsServer = await serveMetrics(metrics, args.metricsPort);
5294
5433
  }
5295
5434
  const worker = await startWorker(processorContext(runtime, () => args.registryUrl.replace(/\/$/, "")), {
@@ -5333,8 +5472,10 @@ async function readStoredManifest(s3, config, name) {
5333
5472
  Key: `${config.keyPrefix ?? ""}${name}/package.json`
5334
5473
  }));
5335
5474
  return JSON.parse(await res.Body?.transformToString() ?? "null");
5336
- } catch {
5337
- return null;
5475
+ } catch (err) {
5476
+ const name = err.name;
5477
+ if (name === "NoSuchKey" || name === "NotFound") return null;
5478
+ throw err;
5338
5479
  }
5339
5480
  }
5340
5481
  /** Moves Verdaccio's S3-file state into the Postgres store the S3 plugin reads. */
@@ -5364,15 +5505,21 @@ async function runImportVerdaccioState(args) {
5364
5505
  const names = new Set(legacy.list ?? []);
5365
5506
  const pool = new pg.Pool({ connectionString: args.databaseUrl });
5366
5507
  let pruned = [];
5508
+ let gone = 0;
5367
5509
  try {
5368
5510
  const store = new S3DatabasePostgres(pool, pluginLogger);
5369
5511
  await store.init();
5512
+ const missing = [];
5370
5513
  for (const name of names) {
5371
5514
  const manifest = await readStoredManifest(s3, s3Config, name);
5372
5515
  if (manifest) await store.record(name, manifest);
5516
+ else missing.push(name);
5373
5517
  }
5518
+ for (const name of missing) names.delete(name);
5519
+ await pool.query("DELETE FROM verdaccio_packages WHERE name = ANY($1)", [missing]);
5520
+ gone = missing.length;
5374
5521
  for (const name of names) await pool.query("INSERT INTO verdaccio_packages (name) VALUES ($1) ON CONFLICT (name) DO NOTHING", [name]);
5375
- pruned = await pruneCachedPackages(pool, [...names]);
5522
+ if (legacy.list) pruned = await pruneCachedPackages(pool, [...names]);
5376
5523
  for (const token of legacy.tokens ?? []) await pool.query(`INSERT INTO verdaccio_tokens ("user", key, token) VALUES ($1, $2, $3)
5377
5524
  ON CONFLICT ("user", key) DO NOTHING`, [
5378
5525
  token.user,
@@ -5384,10 +5531,14 @@ async function runImportVerdaccioState(args) {
5384
5531
  } finally {
5385
5532
  await pool.end();
5386
5533
  }
5387
- console.log(`[registry] imported ${names.size} package name(s), ${legacy.tokens?.length ?? 0} token(s)${legacy.secret ? " and the secret" : ""}; pruned ${pruned.length} cached package(s)`);
5534
+ console.log(`[registry] imported ${names.size} package name(s), ${legacy.tokens?.length ?? 0} token(s)${legacy.secret ? " and the secret" : ""}; pruned ${pruned.length} cached package(s), dropped ${gone} with no stored manifest`);
5388
5535
  }
5389
5536
  //#endregion
5390
5537
  //#region cli.ts
5538
+ const positiveFromEnv = (name) => () => {
5539
+ const n = Number(process.env[name]);
5540
+ return Number.isInteger(n) && n > 0 ? n : void 0;
5541
+ };
5391
5542
  const storageArgs = {
5392
5543
  storageDir: option({
5393
5544
  long: "storage-dir",
@@ -5443,6 +5594,12 @@ const storageArgs = {
5443
5594
  defaultValue: () => process.env.S3_FORCE_PATH_STYLE !== "false",
5444
5595
  defaultValueIsSerializable: true
5445
5596
  }),
5597
+ s3MaxWritesPerSecond: option({
5598
+ long: "s3-max-writes-per-second",
5599
+ type: optional(number),
5600
+ description: "Artifact uploads per second (default 150); Hetzner throttles a bucket past 750 requests per second. Env PH_REGISTRY_S3_MAX_WRITES_PER_SECOND",
5601
+ defaultValue: positiveFromEnv("PH_REGISTRY_S3_MAX_WRITES_PER_SECOND")
5602
+ }),
5446
5603
  databaseUrl: option({
5447
5604
  long: "database-url",
5448
5605
  type: optional(string),
@@ -5473,10 +5630,6 @@ const migrateOnBootArg = option({
5473
5630
  }),
5474
5631
  description: "Apply database migrations at startup (true/false). Defaults to true without --database-url, else false; env PH_REGISTRY_MIGRATE_ON_BOOT"
5475
5632
  });
5476
- const positiveFromEnv = (name) => () => {
5477
- const n = Number(process.env[name]);
5478
- return Number.isInteger(n) && n > 0 ? n : void 0;
5479
- };
5480
5633
  const dbPoolMaxArg = option({
5481
5634
  long: "db-pool-max",
5482
5635
  type: optional(number),