@finbheara/archive-client 0.39.0 → 0.42.0

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.
@@ -51,7 +51,7 @@ function planAsset2(name, manifest) {
51
51
 
52
52
  import { createHash } from "crypto";
53
53
  import { createReadStream, createWriteStream } from "fs";
54
- import { Transform } from "stream";
54
+ import { Readable, Transform } from "stream";
55
55
  import { pipeline } from "stream/promises";
56
56
  import { createZstdDecompress } from "zlib";
57
57
  async function sha256File2(path) {
@@ -79,6 +79,55 @@ function sha256OfDigest(digest) {
79
79
  const m = /^sha256:([0-9a-f]{64})$/i.exec(digest.trim());
80
80
  return m === null ? null : m[1].toLowerCase();
81
81
  }
82
+ async function streamTo2(source, dest, o) {
83
+ const rawHash = createHash("sha256");
84
+ const outHash = o.decompress ? createHash("sha256") : rawHash;
85
+ let received = 0;
86
+ let written = 0;
87
+ const keep = o.raw === undefined ? null : createWriteStream(o.raw);
88
+ const tell = () => o.onBytes?.(received, o.decompress ? written : received);
89
+ const inTap = new Transform({
90
+ transform(chunk, _enc, cb) {
91
+ rawHash.update(chunk);
92
+ received += chunk.length;
93
+ if (!o.decompress)
94
+ tell();
95
+ if (keep !== null && !keep.write(chunk))
96
+ keep.once("drain", () => cb(null, chunk));
97
+ else
98
+ cb(null, chunk);
99
+ }
100
+ });
101
+ const outTap = new Transform({
102
+ transform(chunk, _enc, cb) {
103
+ outHash.update(chunk);
104
+ written += chunk.length;
105
+ tell();
106
+ cb(null, chunk);
107
+ }
108
+ });
109
+ const chain = [
110
+ Readable.from(source),
111
+ inTap,
112
+ ...o.decompress ? [createZstdDecompress(), outTap] : [],
113
+ createWriteStream(dest)
114
+ ];
115
+ try {
116
+ await pipeline(chain, o.signal !== undefined ? { signal: o.signal } : {});
117
+ } catch (e) {
118
+ keep?.destroy();
119
+ throw e;
120
+ }
121
+ if (keep !== null)
122
+ await new Promise((res, rej) => keep.end((e) => e ? rej(e) : res()));
123
+ const rawSha256 = rawHash.digest("hex");
124
+ return {
125
+ rawSha256,
126
+ rawBytes: received,
127
+ sha256: o.decompress ? outHash.digest("hex") : rawSha256,
128
+ bytes: o.decompress ? written : received
129
+ };
130
+ }
82
131
 
83
132
  import { Age, DANCE_UP_YEARS, birthYearsMeet } from "@finbheara/domain/age";
84
133
  import { createIndex as createIndex2, indexSchema as indexSchema2, personEncoder } from "@finbheara/domain/noun-index";
@@ -491,6 +540,30 @@ var FEATURES2 = [
491
540
  since: 270,
492
541
  calls: "the same competition fields on a row of results.stream and results.entries (scope majors): absent, never refused, on a release without them"
493
542
  },
543
+ {
544
+ feature: "results.stream.competitors_printed",
545
+ needs: ["results:competitions.competitors_printed"],
546
+ since: 345,
547
+ calls: "the `competitors_printed` a row of results.stream and results.entries carries (the competed count the page printed): absent, never refused, on a release without it"
548
+ },
549
+ {
550
+ feature: "majors.stream.competitors_printed",
551
+ needs: ["majors:competitions.competitors_printed"],
552
+ since: 345,
553
+ calls: "the same on a row of results.stream and results.entries (scope majors)"
554
+ },
555
+ {
556
+ feature: "results.attach_basis",
557
+ needs: ["results:results.attach_basis"],
558
+ since: 345,
559
+ calls: "the `attach_basis` a row of results.query, results.stream and results.entries carries (how its dancer_id was answered, or why not): absent, never refused, on a release without it"
560
+ },
561
+ {
562
+ feature: "majors.attach_basis",
563
+ needs: ["majors:results.attach_basis"],
564
+ since: 345,
565
+ calls: "the same on a row of majors.results, and of results.stream and results.entries (scope majors)"
566
+ },
494
567
  {
495
568
  feature: "majors.tabs",
496
569
  needs: ["majors:tabs.address", "majors:tabs.true_round"],
@@ -1402,7 +1475,7 @@ function streamSql(q, shape = {}) {
1402
1475
  if (q.primary === true)
1403
1476
  where.push("(r.primary_source IS NULL OR r.primary_source = '')");
1404
1477
  return {
1405
- sql: `SELECT ${COLUMNS}${shape.competition === true ? COMPETITION_COLUMNS : ""} FROM results r JOIN competitions c ON c.file = r.file AND c.line = r.competition_line${where.length === 0 ? "" : ` WHERE ${where.join(" AND ")}`} ORDER BY r.file, r.line`,
1478
+ sql: `SELECT ${COLUMNS}${shape.competition === true ? COMPETITION_COLUMNS : ""}${shape.competitorsPrinted === true ? ", c.competitors_printed AS competitors_printed" : ""}${shape.attachBasis === true ? ", r.attach_basis AS attach_basis" : ""} FROM results r JOIN competitions c ON c.file = r.file AND c.line = r.competition_line${where.length === 0 ? "" : ` WHERE ${where.join(" AND ")}`} ORDER BY r.file, r.line`,
1406
1479
  args
1407
1480
  };
1408
1481
  }
@@ -1483,7 +1556,7 @@ var COLUMNS2 = `r.address AS address, r.competition AS competition, c.event AS e
1483
1556
  r.place_printed AS place_printed, c.competitors AS competitors, r.name_printed AS name_printed,
1484
1557
  r.dancer_id AS dancer_id, r.school_printed AS school_printed, r.school_id AS school_id,
1485
1558
  r.yob AS yob, r.qualifier AS qualifier`;
1486
- function queryResults(rel, db, q) {
1559
+ function queryResults(rel, db, q, basis = false) {
1487
1560
  const where = [];
1488
1561
  const args = [];
1489
1562
  if (q.dancer_id !== undefined) {
@@ -1509,7 +1582,7 @@ function queryResults(rel, db, q) {
1509
1582
  args.push(q.year);
1510
1583
  }
1511
1584
  const asked = q.limit ?? 500;
1512
- const all = rel.db(db).query(`SELECT ${COLUMNS2} FROM results r JOIN competitions c ON c.file = r.file AND c.line = r.competition_line
1585
+ const all = rel.db(db).query(`SELECT ${COLUMNS2}${basis ? ", r.attach_basis AS attach_basis" : ""} FROM results r JOIN competitions c ON c.file = r.file AND c.line = r.competition_line
1513
1586
  WHERE ${where.join(" AND ")}
1514
1587
  ORDER BY c.year, c.event, r.competition, r.place IS NULL, r.place, r.address`).all(...args);
1515
1588
  return {
@@ -1532,7 +1605,8 @@ class ResultFinder2 {
1532
1605
  async query(q) {
1533
1606
  await this.rel.fetch("results");
1534
1607
  await this.rel.require("results.level");
1535
- return queryResults(this.rel, "results", q);
1608
+ const basis = (await this.rel.capability("results.attach_basis")).state === "full";
1609
+ return queryResults(this.rel, "results", q, basis);
1536
1610
  }
1537
1611
  async dancerLevel(q) {
1538
1612
  await this.rel.fetch("results");
@@ -1544,7 +1618,9 @@ class ResultFinder2 {
1544
1618
  await this.rel.fetch(db);
1545
1619
  await this.rel.require(db === "majors" ? "majors.stream" : "results.stream");
1546
1620
  const competition = (await this.rel.capability(`${db}.stream.competition`)).state === "full";
1547
- return { db: this.rel.db(db), shape: { competition } };
1621
+ const competitorsPrinted = (await this.rel.capability(`${db}.stream.competitors_printed`)).state === "full";
1622
+ const attachBasis = (await this.rel.capability(`${db}.attach_basis`)).state === "full";
1623
+ return { db: this.rel.db(db), shape: { competition, competitorsPrinted, attachBasis } };
1548
1624
  }
1549
1625
  async* stream(q) {
1550
1626
  const { db, shape } = await this.streamDb(q);
@@ -1608,7 +1684,8 @@ class MajorsFinder2 {
1608
1684
  async results(q) {
1609
1685
  await this.db();
1610
1686
  await this.rel.require("results.level");
1611
- return queryResults(this.rel, "majors", q);
1687
+ const basis = (await this.rel.capability("majors.attach_basis")).state === "full";
1688
+ return queryResults(this.rel, "majors", q, basis);
1612
1689
  }
1613
1690
  }
1614
1691
 
@@ -3293,8 +3370,8 @@ class CompResolver2 {
3293
3370
 
3294
3371
  import { Database } from "bun:sqlite";
3295
3372
  import { createHash as createHash2 } from "crypto";
3296
- import { existsSync, mkdirSync, readdirSync, renameSync, rmSync, statSync } from "fs";
3297
- import { join } from "path";
3373
+ import { existsSync as existsSync2, mkdirSync, readdirSync as readdirSync2, renameSync, rmSync as rmSync2, statSync } from "fs";
3374
+ import { join as join2 } from "path";
3298
3375
 
3299
3376
  import {
3300
3377
  createIndex as createIndex3,
@@ -3534,6 +3611,34 @@ class StaffFinder2 {
3534
3611
  }
3535
3612
  }
3536
3613
 
3614
+ import { existsSync, readdirSync, rmSync } from "fs";
3615
+ import { join } from "path";
3616
+ function pidAlive(pid) {
3617
+ if (pid === process.pid)
3618
+ return true;
3619
+ try {
3620
+ process.kill(pid, 0);
3621
+ return true;
3622
+ } catch (e) {
3623
+ return e.code === "EPERM";
3624
+ }
3625
+ }
3626
+ var PART_FILE = /^\..+\.(\d+)-[0-9a-f]{8}\.part(?:\.zst|\.dl)?$/;
3627
+ var BUILDING_FILE = /^.+\.sqlite\.building\.(\d+)$/;
3628
+ function sweepDead(dir, re) {
3629
+ if (!existsSync(dir))
3630
+ return [];
3631
+ const gone = [];
3632
+ for (const f of readdirSync(dir)) {
3633
+ const m = re.exec(f);
3634
+ if (m === null || pidAlive(Number(m[1])))
3635
+ continue;
3636
+ rmSync(join(dir, f), { force: true });
3637
+ gone.push(f);
3638
+ }
3639
+ return gone.sort();
3640
+ }
3641
+
3537
3642
  import { parquetMetadata, parquetReadObjects, parquetSchema } from "hyparquet";
3538
3643
  var COMPRESSORS = {
3539
3644
  ZSTD: (input) => new Uint8Array(Bun.zstdDecompressSync(input))
@@ -3780,8 +3885,8 @@ class EventFinder2 {
3780
3885
  function defaultBuilders2() {
3781
3886
  return [schoolsIndex, feisesIndex, compsIndex, dancersIndex, staffIndex, teamsIndex, eventsIndex];
3782
3887
  }
3783
- var indexDir = (rel) => join(rel.dir, "index");
3784
- var indexPath2 = (rel, name) => join(indexDir(rel), `${name}.sqlite`);
3888
+ var indexDir = (rel) => join2(rel.dir, "index");
3889
+ var indexPath2 = (rel, name) => join2(indexDir(rel), `${name}.sqlite`);
3785
3890
  function indexKey2(rel, b) {
3786
3891
  const files = rel.receipt().files;
3787
3892
  const parts = [b.name, b.version, ...b.keyParts?.(rel) ?? []];
@@ -3792,7 +3897,7 @@ function indexKey2(rel, b) {
3792
3897
  return createHash2("sha256").update(parts.join("\x00")).digest("hex");
3793
3898
  }
3794
3899
  function keyOf(path) {
3795
- if (!existsSync(path))
3900
+ if (!existsSync2(path))
3796
3901
  return null;
3797
3902
  try {
3798
3903
  const db = new Database(path, { readonly: true });
@@ -3817,8 +3922,9 @@ function buildIndex2(rel, b, o) {
3817
3922
  if (b.load !== undefined && o.loaded === undefined)
3818
3923
  throw new Error(`archive-client: index "${b.name}" reads its inputs first: build it with ensureIndex (or rel.index)`);
3819
3924
  mkdirSync(indexDir(rel), { recursive: true });
3925
+ sweepDead(indexDir(rel), BUILDING_FILE);
3820
3926
  const tmp = `${out}.building.${process.pid}`;
3821
- rmSync(tmp, { force: true });
3927
+ rmSync2(tmp, { force: true });
3822
3928
  const t0 = performance.now();
3823
3929
  const db = new Database(tmp, { create: true });
3824
3930
  try {
@@ -3834,7 +3940,7 @@ function buildIndex2(rel, b, o) {
3834
3940
  db.run("VACUUM");
3835
3941
  } catch (e) {
3836
3942
  db.close();
3837
- rmSync(tmp, { force: true });
3943
+ rmSync2(tmp, { force: true });
3838
3944
  throw e;
3839
3945
  }
3840
3946
  db.close();
@@ -3845,16 +3951,42 @@ function buildIndex2(rel, b, o) {
3845
3951
  async function ensureIndex2(rel, b, o) {
3846
3952
  if (!o.force && isCurrent2(rel, b))
3847
3953
  return indexPath2(rel, b.name);
3848
- const loaded = b.load === undefined ? undefined : await b.load(rel);
3849
- return buildIndex2(rel, b, { ...o, loaded });
3954
+ const t0 = performance.now();
3955
+ const tell = (state, error) => o.progress?.({
3956
+ tag: rel.tag,
3957
+ kind: "index",
3958
+ item: b.name,
3959
+ state,
3960
+ bytes: 0,
3961
+ total: null,
3962
+ unpacked: 0,
3963
+ ms: performance.now() - t0,
3964
+ ...error !== undefined ? { error } : {}
3965
+ });
3966
+ tell("indexing");
3967
+ try {
3968
+ const loaded = b.load === undefined ? undefined : await b.load(rel);
3969
+ if (o.progress !== undefined)
3970
+ await new Promise((r) => setTimeout(r, 0));
3971
+ const path = buildIndex2(rel, b, {
3972
+ force: o.force,
3973
+ loaded,
3974
+ ...o.progress === undefined && o.log !== undefined ? { log: o.log } : {}
3975
+ });
3976
+ tell("done");
3977
+ return path;
3978
+ } catch (e) {
3979
+ tell("failed", e.message);
3980
+ throw e;
3981
+ }
3850
3982
  }
3851
3983
  function listIndexes2(rel) {
3852
3984
  const dir = indexDir(rel);
3853
- if (!existsSync(dir))
3985
+ if (!existsSync2(dir))
3854
3986
  return [];
3855
- return readdirSync(dir).filter((f) => f.endsWith(".sqlite")).sort().map((f) => {
3987
+ return readdirSync2(dir).filter((f) => f.endsWith(".sqlite")).sort().map((f) => {
3856
3988
  const name = f.slice(0, -".sqlite".length);
3857
- const path = join(dir, f);
3989
+ const path = join2(dir, f);
3858
3990
  const b = rel.builders().find((x) => x.name === name);
3859
3991
  return {
3860
3992
  name,
@@ -3868,35 +4000,35 @@ function listIndexes2(rel) {
3868
4000
  import { createHash as createHash3 } from "crypto";
3869
4001
  import {
3870
4002
  copyFileSync as copyFileSync2,
3871
- existsSync as existsSync6,
4003
+ existsSync as existsSync7,
3872
4004
  linkSync as linkSync2,
3873
4005
  mkdirSync as mkdirSync5,
3874
4006
  readFileSync as readFileSync5,
3875
4007
  readdirSync as readdirSync4,
3876
4008
  renameSync as renameSync3,
3877
- rmSync as rmSync5,
4009
+ rmSync as rmSync6,
3878
4010
  statSync as statSync4
3879
4011
  } from "fs";
3880
- import { join as join5 } from "path";
4012
+ import { join as join6 } from "path";
3881
4013
 
3882
4014
  import {
3883
4015
  copyFileSync,
3884
- existsSync as existsSync5,
4016
+ existsSync as existsSync6,
3885
4017
  linkSync,
3886
4018
  mkdirSync as mkdirSync4,
3887
4019
  readFileSync as readFileSync4,
3888
4020
  readdirSync as readdirSync3,
3889
- rmSync as rmSync4,
4021
+ rmSync as rmSync5,
3890
4022
  statSync as statSync3
3891
4023
  } from "fs";
3892
- import { isAbsolute, join as join4, relative, resolve, sep } from "path";
4024
+ import { isAbsolute, join as join5, relative, resolve, sep } from "path";
3893
4025
 
3894
- import { existsSync as existsSync2, readFileSync as readFileSync2, rmSync as rmSync2 } from "fs";
3895
- import { join as join2 } from "path";
3896
- var pinPath = (root) => join2(root, "PINNED.json");
4026
+ import { existsSync as existsSync3, readFileSync as readFileSync2, rmSync as rmSync3 } from "fs";
4027
+ import { join as join3 } from "path";
4028
+ var pinPath = (root) => join3(root, "PINNED.json");
3897
4029
  function readPin2(root) {
3898
4030
  const p = pinPath(root);
3899
- if (!existsSync2(p))
4031
+ if (!existsSync3(p))
3900
4032
  return null;
3901
4033
  try {
3902
4034
  const v = JSON.parse(readFileSync2(p, "utf8"));
@@ -3912,23 +4044,24 @@ function writePin(root, pin) {
3912
4044
  }
3913
4045
  function clearPin(root) {
3914
4046
  const p = pinPath(root);
3915
- if (!existsSync2(p))
4047
+ if (!existsSync3(p))
3916
4048
  return false;
3917
- rmSync2(p);
4049
+ rmSync3(p);
3918
4050
  return true;
3919
4051
  }
3920
4052
 
3921
4053
  import { Database as Database2 } from "bun:sqlite";
4054
+ import { randomBytes } from "crypto";
3922
4055
  import {
3923
- existsSync as existsSync4,
4056
+ createReadStream as createReadStream2,
4057
+ existsSync as existsSync5,
3924
4058
  mkdirSync as mkdirSync3,
3925
4059
  readFileSync as readFileSync3,
3926
- readdirSync as readdirSync2,
3927
4060
  renameSync as renameSync2,
3928
- rmSync as rmSync3,
4061
+ rmSync as rmSync4,
3929
4062
  statSync as statSync2
3930
4063
  } from "fs";
3931
- import { join as join3 } from "path";
4064
+ import { basename, dirname as dirname2, join as join4 } from "path";
3932
4065
 
3933
4066
  var CALENDAR_STAGES2 = [
3934
4067
  "announced",
@@ -4264,9 +4397,9 @@ class RegistrationResolver2 {
4264
4397
  }
4265
4398
 
4266
4399
  import { spawn } from "child_process";
4267
- import { createWriteStream as createWriteStream2, existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
4400
+ import { createWriteStream as createWriteStream2, existsSync as existsSync4, mkdirSync as mkdirSync2 } from "fs";
4268
4401
  import { dirname } from "path";
4269
- import { Readable } from "stream";
4402
+ import { Readable as Readable2 } from "stream";
4270
4403
  import { pipeline as pipeline2 } from "stream/promises";
4271
4404
  function run(cmd, args) {
4272
4405
  return new Promise((resolve) => {
@@ -4332,8 +4465,59 @@ var ghRemote2 = {
4332
4465
  dest,
4333
4466
  "--clobber"
4334
4467
  ]);
4335
- if (r.status !== 0 || !existsSync3(dest))
4468
+ if (r.status !== 0 || !existsSync4(dest))
4336
4469
  throw new Error(`gh release download ${tag} -p ${name}: ${firstLine(r.stderr)}`);
4470
+ },
4471
+ async bytes(repo, tag, name, signal) {
4472
+ const what = `gh release download ${tag} -p ${name}`;
4473
+ let child;
4474
+ try {
4475
+ child = spawn("gh", ["release", "download", tag, "-R", repo, "-p", name, "-O", "-"], {
4476
+ stdio: ["ignore", "pipe", "pipe"]
4477
+ });
4478
+ } catch (e) {
4479
+ throw new Error(`${what}: ${e.message}`);
4480
+ }
4481
+ let stderr = "";
4482
+ child.stderr?.on("data", (d) => {
4483
+ stderr += String(d);
4484
+ });
4485
+ const closed = new Promise((resolve) => {
4486
+ child.on("error", (e) => {
4487
+ stderr += e.message;
4488
+ resolve(null);
4489
+ });
4490
+ child.on("close", (status) => resolve(status));
4491
+ });
4492
+ const kill = () => {
4493
+ child.kill("SIGTERM");
4494
+ };
4495
+ signal?.addEventListener("abort", kill, { once: true });
4496
+ const out = child.stdout;
4497
+ if (out === null || out === undefined)
4498
+ throw new Error(`${what}: no stdout`);
4499
+ const it = out[Symbol.asyncIterator]();
4500
+ const failed = async () => {
4501
+ const status = await closed;
4502
+ signal?.removeEventListener("abort", kill);
4503
+ if (status !== 0)
4504
+ throw new Error(`${what}: ${firstLine(stderr) || `exit ${status}`}`);
4505
+ };
4506
+ const head = await it.next();
4507
+ if (head.done === true) {
4508
+ await failed();
4509
+ return async function* () {}();
4510
+ }
4511
+ return async function* () {
4512
+ yield head.value;
4513
+ for (;; ) {
4514
+ const n = await it.next();
4515
+ if (n.done === true)
4516
+ break;
4517
+ yield n.value;
4518
+ }
4519
+ await failed();
4520
+ }();
4337
4521
  }
4338
4522
  };
4339
4523
  var API = "https://api.github.com";
@@ -4386,7 +4570,19 @@ function httpsRemote2(env = process.env, fetchImpl = fetch) {
4386
4570
  if (!res.ok || res.body === null)
4387
4571
  throw new Error(`GET ${a.url}: ${res.status} ${res.statusText}`);
4388
4572
  mkdirSync2(dirname(dest), { recursive: true });
4389
- await pipeline2(Readable.fromWeb(nodeWebStream(res.body)), createWriteStream2(dest));
4573
+ await pipeline2(Readable2.fromWeb(nodeWebStream(res.body)), createWriteStream2(dest));
4574
+ },
4575
+ async bytes(repo, tag, name, signal) {
4576
+ const a = (await release(repo, tag)).assets.find((x) => x.name === name);
4577
+ if (a === undefined)
4578
+ throw new Error(`${tag} has no asset ${name}`);
4579
+ const res = await fetchImpl(a.url, {
4580
+ headers: headers(env, "application/octet-stream"),
4581
+ ...signal !== undefined ? { signal } : {}
4582
+ });
4583
+ if (!res.ok || res.body === null)
4584
+ throw new Error(`GET ${a.url}: ${res.status} ${res.statusText}`);
4585
+ return Readable2.fromWeb(nodeWebStream(res.body));
4390
4586
  }
4391
4587
  };
4392
4588
  }
@@ -4406,7 +4602,10 @@ function fallbackRemote2(first, second) {
4406
4602
  name: `${first.name}+${second.name}`,
4407
4603
  tags: both((r) => r.tags.bind(r)),
4408
4604
  assets: both((r) => r.assets.bind(r)),
4409
- download: both((r) => r.download.bind(r))
4605
+ download: both((r) => r.download.bind(r)),
4606
+ ...first.bytes !== undefined && second.bytes !== undefined ? {
4607
+ bytes: both((r) => r.bytes.bind(r))
4608
+ } : {}
4410
4609
  };
4411
4610
  }
4412
4611
  var installed = null;
@@ -4645,12 +4844,12 @@ class UpcomingFinder2 {
4645
4844
  function tagDir(root, tag) {
4646
4845
  if (parseArchiveTag2(tag) === null)
4647
4846
  throw new Error(`archive-client: "${tag}" is not a release tag (archive-YYYY.MM.DD[-N[.k]])`);
4648
- return join3(root, tag);
4847
+ return join4(root, tag);
4649
4848
  }
4650
- var receiptPath = (dir) => join3(dir, ".client.json");
4849
+ var receiptPath = (dir) => join4(dir, ".client.json");
4651
4850
  function readReceipt(dir) {
4652
4851
  const p = receiptPath(dir);
4653
- if (!existsSync4(p))
4852
+ if (!existsSync5(p))
4654
4853
  return null;
4655
4854
  try {
4656
4855
  return JSON.parse(readFileSync3(p, "utf8"));
@@ -4667,7 +4866,7 @@ async function resolveTag2(spec, opts = {}) {
4667
4866
  const pin = readPin2(root);
4668
4867
  return resolveTag2(pin === null ? { kind: "latest" } : { kind: "tag", tag: pin.tag }, opts);
4669
4868
  }
4670
- if (s.kind === "tag" && existsSync4(join3(tagDir(root, s.tag), "MANIFEST.json")))
4869
+ if (s.kind === "tag" && existsSync5(join4(tagDir(root, s.tag), "MANIFEST.json")))
4671
4870
  return s.tag;
4672
4871
  const repo = requireRepo2(opts);
4673
4872
  const tags = await (opts.remote ?? defaultRemote()).tags(repo);
@@ -4676,9 +4875,30 @@ async function resolveTag2(spec, opts = {}) {
4676
4875
  throw new Error(`archive-client: ${repo} publishes no release ${s.kind === "tag" ? s.tag : s.kind === "cut" ? `of cut ${s.cut}${s.fraction > 0 ? `.${s.fraction}` : ""}` : "at all"}`);
4677
4876
  return tag;
4678
4877
  }
4679
- function dropIfEmpty(dir) {
4680
- if (existsSync4(dir) && readdirSync2(dir).length === 0)
4681
- rmSync3(dir, { recursive: true });
4878
+ var DEFAULT_CONCURRENCY2 = 4;
4879
+ function partPath(dest) {
4880
+ return join4(dirname2(dest), `.${basename(dest)}.${process.pid}-${randomBytes(4).toString("hex")}.part`);
4881
+ }
4882
+
4883
+ class Slots {
4884
+ n;
4885
+ running = 0;
4886
+ waiting = [];
4887
+ constructor(n) {
4888
+ this.n = n;
4889
+ }
4890
+ async run(job, signal) {
4891
+ if (this.running >= this.n)
4892
+ await new Promise((r) => this.waiting.push(r));
4893
+ this.running++;
4894
+ try {
4895
+ signal.throwIfAborted();
4896
+ return await job();
4897
+ } finally {
4898
+ this.running--;
4899
+ this.waiting.shift()?.();
4900
+ }
4901
+ }
4682
4902
  }
4683
4903
 
4684
4904
  class Release2 {
@@ -4705,7 +4925,7 @@ class Release2 {
4705
4925
  feisPanels;
4706
4926
  opts;
4707
4927
  handles = new Map;
4708
- remoteAssets = null;
4928
+ listing = null;
4709
4929
  constructor(tag, root, manifest, opts) {
4710
4930
  const t = parseArchiveTag2(tag);
4711
4931
  this.tag = tag;
@@ -4738,19 +4958,65 @@ class Release2 {
4738
4958
  return planAsset2(asset, this.manifest);
4739
4959
  }
4740
4960
  has(asset) {
4741
- return existsSync4(join3(this.dir, this.plan(asset).local));
4961
+ return existsSync5(join4(this.dir, this.plan(asset).local));
4742
4962
  }
4743
4963
  path(asset) {
4744
- const p = join3(this.dir, this.plan(asset).local);
4745
- if (!existsSync4(p))
4964
+ const p = join4(this.dir, this.plan(asset).local);
4965
+ if (!existsSync5(p))
4746
4966
  throw new Error(`archive-client: ${this.tag} has no ${asset} in the cache: fetch it first (\`await rel.fetch("${asset}")\`, or \`fetch --release ${this.tag} --assets ${asset}\`)`);
4747
4967
  return p;
4748
4968
  }
4749
4969
  async fetch(...assets) {
4750
- const out = [];
4751
- for (const a of assets)
4752
- out.push(await this.fetchOne(a));
4753
- return out;
4970
+ return this.fetching(assets).all;
4971
+ }
4972
+ inflight = new Map;
4973
+ slots = null;
4974
+ swept = false;
4975
+ fetching(assets) {
4976
+ this.slots ??= new Slots(Math.max(1, this.opts.concurrency ?? DEFAULT_CONCURRENCY2));
4977
+ const slots = this.slots;
4978
+ const ac = new AbortController;
4979
+ const mine = new Map;
4980
+ let first = null;
4981
+ for (const a of assets) {
4982
+ if (mine.has(a))
4983
+ continue;
4984
+ const local = this.plan(a).local;
4985
+ let p = this.inflight.get(local);
4986
+ if (p === undefined) {
4987
+ p = slots.run(() => this.fetchOne(a, ac.signal), ac.signal);
4988
+ this.inflight.set(local, p);
4989
+ const started = p;
4990
+ started.then(() => this.inflight.delete(local), () => this.inflight.delete(local));
4991
+ }
4992
+ p.catch((e) => {
4993
+ first ??= { e };
4994
+ ac.abort();
4995
+ });
4996
+ mine.set(a, p);
4997
+ }
4998
+ const all = (async () => {
4999
+ const settled = await Promise.allSettled([...mine.values()]);
5000
+ if (first !== null)
5001
+ throw first.e;
5002
+ return assets.map((a) => {
5003
+ const s = settled[[...mine.keys()].indexOf(a)];
5004
+ return s.value;
5005
+ });
5006
+ })();
5007
+ return {
5008
+ each: (asset) => {
5009
+ const p = mine.get(asset);
5010
+ if (p === undefined)
5011
+ throw new Error(`archive-client: ${asset} was not asked of this fetch`);
5012
+ return p;
5013
+ },
5014
+ all,
5015
+ cancel: () => {
5016
+ first ??= { e: new Error("archive-client: the fetch was cancelled") };
5017
+ ac.abort();
5018
+ }
5019
+ };
4754
5020
  }
4755
5021
  async publishes(asset) {
4756
5022
  const plan = this.plan(asset);
@@ -4806,25 +5072,29 @@ class Release2 {
4806
5072
  if (c.state === "missing")
4807
5073
  throw needsRelease2(this.tag, c);
4808
5074
  }
4809
- async listed() {
4810
- if (this.remoteAssets === null) {
4811
- this.remoteAssets = await (this.opts.remote ?? defaultRemote()).assets(requireRepo2(this.opts), this.tag);
4812
- if (existsSync4(this.dir)) {
5075
+ listed() {
5076
+ this.listing ??= (async () => {
5077
+ const assets = await (this.opts.remote ?? defaultRemote()).assets(requireRepo2(this.opts), this.tag);
5078
+ if (existsSync5(this.dir)) {
4813
5079
  const r = this.receipt();
4814
- r.published = this.remoteAssets.map((a) => a.name).sort();
5080
+ r.published = assets.map((a) => a.name).sort();
4815
5081
  writeJsonAtomic2(receiptPath(this.dir), r);
4816
5082
  }
4817
- }
4818
- return this.remoteAssets;
5083
+ return assets;
5084
+ })();
5085
+ this.listing.catch(() => {
5086
+ this.listing = null;
5087
+ });
5088
+ return this.listing;
4819
5089
  }
4820
- async fetchOne(asset) {
5090
+ async fetchOne(asset, signal) {
4821
5091
  const plan = this.plan(asset);
4822
- const dest = join3(this.dir, plan.local);
5092
+ const dest = join4(this.dir, plan.local);
4823
5093
  const have = this.receipt().files[plan.local];
4824
- if (existsSync4(dest) && have !== undefined)
5094
+ if (existsSync5(dest) && have !== undefined)
4825
5095
  return have;
4826
5096
  const log = this.opts.log ?? (() => {});
4827
- if (existsSync4(dest) && plan.contentSha256 !== null) {
5097
+ if (existsSync5(dest) && plan.contentSha256 !== null) {
4828
5098
  const sha = await sha256File2(dest);
4829
5099
  if (sha === plan.contentSha256) {
4830
5100
  const adopted = {
@@ -4833,7 +5103,8 @@ class Release2 {
4833
5103
  bytes: statSync2(dest).size,
4834
5104
  verified: ["manifest"]
4835
5105
  };
4836
- log(`adopt ${this.tag} ${plan.local} (the manifest's sha256)`);
5106
+ if (this.opts.progress === undefined)
5107
+ log(`adopt ${this.tag} ${plan.local} (the manifest's sha256)`);
4837
5108
  this.record(plan.local, adopted);
4838
5109
  return adopted;
4839
5110
  }
@@ -4842,14 +5113,24 @@ class Release2 {
4842
5113
  const pub = plan.published.map((n) => listed.find((a) => a.name === n)).find((a) => a !== undefined);
4843
5114
  if (pub === undefined)
4844
5115
  throw new Error(`archive-client: ${this.tag} publishes no ${plan.published.join(" or ")} (asked: ${asset})`);
4845
- const part = join3(this.dir, ".part");
4846
- mkdirSync3(part, { recursive: true });
4847
- const raw = join3(part, pub.name);
4848
- log(`fetch ${this.tag} ${pub.name} (${pub.size} bytes)`);
4849
- await (this.opts.remote ?? defaultRemote()).download(requireRepo2(this.opts), this.tag, pub.name, raw);
4850
- const landed = await land(raw, pub, plan, dest, this.opts, log);
5116
+ mkdirSync3(this.dir, { recursive: true });
5117
+ if (!this.swept) {
5118
+ sweepDead(this.dir, PART_FILE);
5119
+ this.swept = true;
5120
+ }
5121
+ const landed = await fetchAsset({
5122
+ remote: this.opts.remote ?? defaultRemote(),
5123
+ repo: requireRepo2(this.opts),
5124
+ tag: this.tag,
5125
+ pub,
5126
+ plan,
5127
+ dest,
5128
+ opts: this.opts,
5129
+ log,
5130
+ ...this.opts.progress !== undefined ? { progress: this.opts.progress } : {},
5131
+ ...signal !== undefined ? { signal } : {}
5132
+ });
4851
5133
  this.record(plan.local, landed);
4852
- dropIfEmpty(part);
4853
5134
  return landed;
4854
5135
  }
4855
5136
  record(local, f) {
@@ -4878,7 +5159,11 @@ class Release2 {
4878
5159
  if (FEATURES2.some((f) => f.feature === b.name))
4879
5160
  await this.require(b.name);
4880
5161
  await this.fetch(...b.inputs);
4881
- return ensureIndex2(this, b, { force: o.force ?? false, log: this.opts.log });
5162
+ return ensureIndex2(this, b, {
5163
+ force: o.force ?? false,
5164
+ log: this.opts.log,
5165
+ ...this.opts.progress !== undefined ? { progress: this.opts.progress } : {}
5166
+ });
4882
5167
  }
4883
5168
  async indexDb(name) {
4884
5169
  const key = `index:${name}`;
@@ -4898,69 +5183,172 @@ class Release2 {
4898
5183
  this.close();
4899
5184
  }
4900
5185
  }
4901
- async function land(raw, pub, plan, dest, opts, log) {
5186
+ function verifyLanded(got, pub, plan, strict) {
4902
5187
  const verified = [];
4903
5188
  const host = sha256OfDigest(pub.digest);
4904
5189
  if (host !== null) {
4905
- const got = await sha256File2(raw);
4906
- if (got !== host) {
4907
- rmSync3(raw, { force: true });
4908
- throw new Error(`archive-client: ${pub.name} does not match the host's digest (sha256 ${got}, the host says ${host})`);
4909
- }
5190
+ if (got.rawSha256 !== host)
5191
+ throw new Error(`archive-client: ${pub.name} does not match the host's digest (sha256 ${got.rawSha256}, the host says ${host})`);
4910
5192
  verified.push("host-digest");
4911
5193
  }
4912
- let landedAt = raw;
4913
- let sha;
4914
- if (plan.decompress && pub.name.endsWith(".zst")) {
4915
- landedAt = `${raw.slice(0, -".zst".length)}.plain`;
4916
- log(`decompress ${pub.name}`);
4917
- sha = await unzstdTo2(raw, landedAt);
4918
- } else
4919
- sha = await sha256File2(raw);
4920
5194
  if (plan.contentSha256 !== null) {
4921
- if (sha !== plan.contentSha256) {
4922
- rmSync3(landedAt, { force: true });
4923
- throw new Error(`archive-client: ${plan.local} does not match MANIFEST.json (sha256 ${sha}, the manifest says ${plan.contentSha256})`);
4924
- }
5195
+ if (got.sha256 !== plan.contentSha256)
5196
+ throw new Error(`archive-client: ${plan.local} does not match MANIFEST.json (sha256 ${got.sha256}, the manifest says ${plan.contentSha256})`);
4925
5197
  verified.push("manifest");
4926
5198
  }
4927
- if (verified.length === 0 && opts.strict === true) {
4928
- rmSync3(landedAt, { force: true });
5199
+ if (verified.length === 0 && strict)
4929
5200
  throw new Error(`archive-client: nothing verifies ${pub.name} (no host digest, no manifest hash) and strict is set`);
5201
+ return verified;
5202
+ }
5203
+ async function fetchAsset(j) {
5204
+ const t0 = performance.now();
5205
+ const { pub, plan, tag } = j;
5206
+ const decompress = plan.decompress && pub.name.endsWith(".zst");
5207
+ const tmp = partPath(j.dest);
5208
+ const raw = decompress && j.opts.keepCompressed === true ? `${tmp}.zst` : undefined;
5209
+ let dl = null;
5210
+ let bytes = 0;
5211
+ let unpacked = 0;
5212
+ let state = "downloading";
5213
+ const total = Number.isFinite(pub.size) ? pub.size : null;
5214
+ const tell = (s, error) => j.progress?.({
5215
+ tag,
5216
+ kind: "asset",
5217
+ item: pub.name,
5218
+ state: s,
5219
+ bytes,
5220
+ total,
5221
+ unpacked,
5222
+ ms: performance.now() - t0,
5223
+ ...error !== undefined ? { error } : {}
5224
+ });
5225
+ const onBytes = (received, written) => {
5226
+ if (state === "downloading")
5227
+ bytes = received;
5228
+ unpacked = decompress ? written : 0;
5229
+ tell(state);
5230
+ };
5231
+ if (j.progress === undefined)
5232
+ j.log(`fetch ${tag} ${pub.name} (${pub.size} bytes)`);
5233
+ tell("downloading");
5234
+ try {
5235
+ let got;
5236
+ if (j.remote.bytes !== undefined) {
5237
+ const body = await j.remote.bytes(j.repo, tag, pub.name, j.signal);
5238
+ if (decompress && j.progress === undefined)
5239
+ j.log(`decompress ${pub.name}`);
5240
+ got = await streamTo2(body, tmp, {
5241
+ decompress,
5242
+ ...raw !== undefined ? { raw } : {},
5243
+ onBytes,
5244
+ ...j.signal !== undefined ? { signal: j.signal } : {}
5245
+ });
5246
+ } else {
5247
+ dl = `${tmp}.dl`;
5248
+ await j.remote.download(j.repo, tag, pub.name, dl);
5249
+ j.signal?.throwIfAborted();
5250
+ bytes = statSync2(dl).size;
5251
+ if (decompress) {
5252
+ if (j.progress === undefined)
5253
+ j.log(`decompress ${pub.name}`);
5254
+ state = "unpacking";
5255
+ tell(state);
5256
+ got = await streamTo2(createReadStream2(dl), tmp, {
5257
+ decompress,
5258
+ onBytes,
5259
+ ...j.signal !== undefined ? { signal: j.signal } : {}
5260
+ });
5261
+ if (raw !== undefined)
5262
+ renameSync2(dl, raw);
5263
+ else
5264
+ rmSync4(dl, { force: true });
5265
+ } else {
5266
+ state = "verifying";
5267
+ tell(state);
5268
+ const sha = await sha256File2(dl);
5269
+ got = { rawSha256: sha, rawBytes: bytes, sha256: sha, bytes };
5270
+ renameSync2(dl, tmp);
5271
+ }
5272
+ dl = null;
5273
+ }
5274
+ state = "verifying";
5275
+ tell(state);
5276
+ const verified = verifyLanded(got, pub, plan, j.opts.strict === true);
5277
+ renameSync2(tmp, j.dest);
5278
+ if (raw !== undefined)
5279
+ renameSync2(raw, `${j.dest}.zst`);
5280
+ tell("done");
5281
+ return { from: pub.name, sha256: got.sha256, bytes: got.bytes, verified };
5282
+ } catch (e) {
5283
+ for (const f of [tmp, raw, dl])
5284
+ if (f !== undefined && f !== null)
5285
+ rmSync4(f, { force: true });
5286
+ const cancelled = j.signal?.aborted === true;
5287
+ tell(cancelled ? "cancelled" : "failed", cancelled ? undefined : e.message);
5288
+ throw e;
5289
+ }
5290
+ }
5291
+ async function land(raw, pub, plan, dest, opts, log) {
5292
+ const decompress = plan.decompress && pub.name.endsWith(".zst");
5293
+ let got;
5294
+ let landedAt = raw;
5295
+ if (decompress) {
5296
+ landedAt = partPath(dest);
5297
+ log(`decompress ${pub.name}`);
5298
+ got = await streamTo2(createReadStream2(raw), landedAt, { decompress: true });
5299
+ } else {
5300
+ const sha = await sha256File2(raw);
5301
+ const n = statSync2(raw).size;
5302
+ got = { rawSha256: sha, rawBytes: n, sha256: sha, bytes: n };
5303
+ }
5304
+ let verified;
5305
+ try {
5306
+ verified = verifyLanded(got, pub, plan, opts.strict === true);
5307
+ } catch (e) {
5308
+ rmSync4(landedAt, { force: true });
5309
+ if (landedAt !== raw)
5310
+ rmSync4(raw, { force: true });
5311
+ throw e;
4930
5312
  }
4931
5313
  renameSync2(landedAt, dest);
4932
5314
  if (landedAt !== raw) {
4933
5315
  if (opts.keepCompressed === true)
4934
5316
  renameSync2(raw, `${dest}.zst`);
4935
5317
  else
4936
- rmSync3(raw, { force: true });
5318
+ rmSync4(raw, { force: true });
4937
5319
  }
4938
- return { from: pub.name, sha256: sha, bytes: statSync2(dest).size, verified };
5320
+ return { from: pub.name, sha256: got.sha256, bytes: statSync2(dest).size, verified };
4939
5321
  }
4940
5322
  async function openRelease2(spec = "pinned", opts = {}) {
4941
5323
  const root = cacheRoot2(opts);
4942
5324
  const tag = await resolveTag2(spec, opts);
4943
5325
  const dir = tagDir(root, tag);
4944
- const manifestPath = join3(dir, "MANIFEST.json");
4945
- if (!existsSync4(manifestPath)) {
5326
+ const manifestPath = join4(dir, "MANIFEST.json");
5327
+ if (!existsSync5(manifestPath)) {
4946
5328
  const remote = opts.remote ?? defaultRemote();
4947
5329
  const repo = requireRepo2(opts);
4948
5330
  const listed = await remote.assets(repo, tag);
4949
5331
  const pub = listed.find((a) => a.name === "MANIFEST.json");
4950
5332
  if (pub === undefined)
4951
5333
  throw new Error(`archive-client: ${tag} publishes no MANIFEST.json`);
4952
- const part = join3(dir, ".part");
4953
- mkdirSync3(part, { recursive: true });
4954
- const raw = join3(part, "MANIFEST.json");
4955
- await remote.download(repo, tag, "MANIFEST.json", raw);
4956
- const landed = await land(raw, pub, planAsset2("MANIFEST.json", null), manifestPath, opts, () => {});
5334
+ mkdirSync3(dir, { recursive: true });
5335
+ const landed = await fetchAsset({
5336
+ remote,
5337
+ repo,
5338
+ tag,
5339
+ pub,
5340
+ plan: planAsset2("MANIFEST.json", null),
5341
+ dest: manifestPath,
5342
+ opts,
5343
+ log: () => {},
5344
+ ...opts.progress !== undefined ? { progress: opts.progress } : {}
5345
+ });
4957
5346
  writeJsonAtomic2(receiptPath(dir), {
4958
5347
  tag,
4959
5348
  source: repo,
4960
5349
  files: { "MANIFEST.json": landed },
4961
5350
  published: listed.map((a) => a.name).sort()
4962
5351
  });
4963
- dropIfEmpty(part);
4964
5352
  }
4965
5353
  const manifest = JSON.parse(readFileSync3(manifestPath, "utf8"));
4966
5354
  const rel = new Release2(tag, root, manifest, opts);
@@ -4969,8 +5357,8 @@ async function openRelease2(spec = "pinned", opts = {}) {
4969
5357
  }
4970
5358
  function openCached2(tag, opts = {}) {
4971
5359
  const root = cacheRoot2(opts);
4972
- const p = join3(tagDir(root, tag), "MANIFEST.json");
4973
- if (!existsSync4(p))
5360
+ const p = join4(tagDir(root, tag), "MANIFEST.json");
5361
+ if (!existsSync5(p))
4974
5362
  return null;
4975
5363
  return new Release2(tag, root, JSON.parse(readFileSync3(p, "utf8")), opts);
4976
5364
  }
@@ -4982,14 +5370,14 @@ function checkKeep(keep) {
4982
5370
  return keep;
4983
5371
  }
4984
5372
  function duBytes(p) {
4985
- if (!existsSync5(p))
5373
+ if (!existsSync6(p))
4986
5374
  return 0;
4987
5375
  const s = statSync3(p);
4988
5376
  if (!s.isDirectory())
4989
5377
  return s.size;
4990
5378
  let n = 0;
4991
5379
  for (const e of readdirSync3(p))
4992
- n += duBytes(join4(p, e));
5380
+ n += duBytes(join5(p, e));
4993
5381
  return n;
4994
5382
  }
4995
5383
 
@@ -5001,9 +5389,9 @@ class ReleaseManager2 {
5001
5389
  this.root = cacheRoot2(opts);
5002
5390
  }
5003
5391
  cachedTags() {
5004
- if (!existsSync5(this.root))
5392
+ if (!existsSync6(this.root))
5005
5393
  return [];
5006
- return readdirSync3(this.root).filter((t) => parseArchiveTag2(t) !== null && existsSync5(join4(this.root, t, "MANIFEST.json"))).sort(compareTagsNewestFirst2);
5394
+ return readdirSync3(this.root).filter((t) => parseArchiveTag2(t) !== null && existsSync6(join5(this.root, t, "MANIFEST.json"))).sort(compareTagsNewestFirst2);
5007
5395
  }
5008
5396
  cached(tag) {
5009
5397
  const r = openCached2(tag, this.opts);
@@ -5043,10 +5431,14 @@ class ReleaseManager2 {
5043
5431
  async use(spec, o = {}) {
5044
5432
  const keep = o.prune === false ? null : this.keepCount(o.keep);
5045
5433
  const was = readPin2(this.root)?.tag ?? null;
5046
- const rel = await this.fetch(spec, o);
5434
+ const rel = await openRelease2(spec, { ...this.opts, assets: [] });
5047
5435
  const tag = rel.tag;
5048
- rel.close();
5049
- const { built } = await this.index(tag);
5436
+ let built;
5437
+ try {
5438
+ built = await this.buildAsLanded(rel, [], o.assets ?? ["results"], false);
5439
+ } finally {
5440
+ rel.close();
5441
+ }
5050
5442
  const pin = await this.pin(tag);
5051
5443
  if (keep === null)
5052
5444
  return { tag, was, pin, built, pruned: null };
@@ -5080,22 +5472,22 @@ class ReleaseManager2 {
5080
5472
  async add(dir, tag) {
5081
5473
  if (parseArchiveTag2(tag) === null)
5082
5474
  throw new Error(`add: "${tag}" is not a release tag`);
5083
- const mf = join4(dir, "MANIFEST.json");
5084
- if (!existsSync5(mf))
5475
+ const mf = join5(dir, "MANIFEST.json");
5476
+ if (!existsSync6(mf))
5085
5477
  throw new Error(`add: ${dir} holds no MANIFEST.json`);
5086
5478
  const dest = tagDir(this.root, tag);
5087
- if (existsSync5(join4(dest, "MANIFEST.json")))
5479
+ if (existsSync6(join5(dest, "MANIFEST.json")))
5088
5480
  throw new Error(`add: ${tag} is already in the cache (${dest}); \`rm ${tag}\` first`);
5089
5481
  const manifest = JSON.parse(readFileSync4(mf, "utf8"));
5090
- const part = join4(dest, ".part");
5482
+ const part = join5(dest, ".part");
5091
5483
  mkdirSync4(part, { recursive: true });
5092
5484
  const receipt = { tag, source: `local:${dir}`, files: {} };
5093
5485
  const log = this.opts.log ?? (() => {});
5094
5486
  for (const name of readdirSync3(dir).sort()) {
5095
- const src = join4(dir, name);
5487
+ const src = join5(dir, name);
5096
5488
  if (!statSync3(src).isFile())
5097
5489
  continue;
5098
- const raw = join4(part, name);
5490
+ const raw = join5(part, name);
5099
5491
  try {
5100
5492
  linkSync(src, raw);
5101
5493
  } catch {
@@ -5105,10 +5497,10 @@ class ReleaseManager2 {
5105
5497
  const side = Object.entries(manifest.sideFiles ?? {}).find(([k, f]) => (f.file ?? `${k}.zst`) === name)?.[0];
5106
5498
  const asked = base !== name && isDatabase(base) ? base : side ?? name;
5107
5499
  const plan = planAsset2(asked, manifest);
5108
- const landed = await land(raw, { name, digest: null }, plan, join4(dest, plan.local), this.opts, log);
5500
+ const landed = await land(raw, { name, digest: null }, plan, join5(dest, plan.local), this.opts, log);
5109
5501
  receipt.files[plan.local] = landed;
5110
5502
  }
5111
- rmSync4(part, { recursive: true, force: true });
5503
+ rmSync5(part, { recursive: true, force: true });
5112
5504
  writeJsonAtomic2(receiptPath(dest), receipt);
5113
5505
  const rel = openCached2(tag, this.opts);
5114
5506
  if (rel === null)
@@ -5117,20 +5509,51 @@ class ReleaseManager2 {
5117
5509
  }
5118
5510
  async index(spec, names = [], o = {}) {
5119
5511
  const rel = await openRelease2(spec, { ...this.opts, assets: [] });
5512
+ try {
5513
+ return { tag: rel.tag, built: await this.buildAsLanded(rel, names, [], o.force ?? false) };
5514
+ } finally {
5515
+ rel.close();
5516
+ }
5517
+ }
5518
+ async buildAsLanded(rel, names, extra, force) {
5120
5519
  const want = names.length > 0 ? names : rel.builders().map((b) => b.name);
5121
- const built = [];
5122
- for (const name of want) {
5520
+ const builders = want.map((name) => {
5123
5521
  const b = rel.builders().find((x) => x.name === name);
5124
5522
  if (b === undefined)
5125
5523
  throw new Error(`index: no index "${name}"`);
5126
- await rel.fetch(...b.inputs);
5127
- built.push({
5128
- name,
5129
- path: await ensureIndex2(rel, b, { force: o.force ?? false, log: this.opts.log })
5130
- });
5524
+ return b;
5525
+ });
5526
+ const assets = [...new Set([...extra, ...builders.flatMap((b) => b.inputs)])];
5527
+ const f = rel.fetching(assets);
5528
+ f.all.catch(() => {});
5529
+ const waiting = new Map(builders.map((b) => [b.name, Promise.all(b.inputs.map((a) => f.each(a))).then(() => b.name)]));
5530
+ for (const p of waiting.values())
5531
+ p.catch(() => {});
5532
+ const paths = new Map;
5533
+ while (waiting.size > 0) {
5534
+ let name;
5535
+ try {
5536
+ name = await Promise.race(waiting.values());
5537
+ } catch {
5538
+ await f.all;
5539
+ throw new Error("archive-client: a fetch failed and said nothing");
5540
+ }
5541
+ waiting.delete(name);
5542
+ const b = builders.find((x) => x.name === name);
5543
+ try {
5544
+ paths.set(name, await ensureIndex2(rel, b, {
5545
+ force,
5546
+ ...this.opts.log !== undefined ? { log: this.opts.log } : {},
5547
+ ...this.opts.progress !== undefined ? { progress: this.opts.progress } : {}
5548
+ }));
5549
+ } catch (e) {
5550
+ f.cancel();
5551
+ await f.all.catch(() => {});
5552
+ throw e;
5553
+ }
5131
5554
  }
5132
- rel.close();
5133
- return { tag: rel.tag, built };
5555
+ await f.all;
5556
+ return want.map((name) => ({ name, path: paths.get(name) }));
5134
5557
  }
5135
5558
  plan(dryRun) {
5136
5559
  return { dryRun, remove: [], keep: [], bytes: 0 };
@@ -5145,7 +5568,7 @@ class ReleaseManager2 {
5145
5568
  p.bytes = p.remove.reduce((n, r) => n + r.bytes, 0);
5146
5569
  if (!p.dryRun)
5147
5570
  for (const r of p.remove)
5148
- rmSync4(r.path, { recursive: true, force: true });
5571
+ rmSync5(r.path, { recursive: true, force: true });
5149
5572
  return p;
5150
5573
  }
5151
5574
  prune(o = {}) {
@@ -5167,7 +5590,7 @@ class ReleaseManager2 {
5167
5590
  }
5168
5591
  remove(tag, o = {}) {
5169
5592
  const path = tagDir(this.root, tag);
5170
- if (!existsSync5(path))
5593
+ if (!existsSync6(path))
5171
5594
  throw new Error(`${tag} is not in the cache`);
5172
5595
  if (readPin2(this.root)?.tag === tag)
5173
5596
  throw new Error(`${tag} is pinned: \`unpin\` first`);
@@ -5179,7 +5602,7 @@ class ReleaseManager2 {
5179
5602
  const p = this.plan(o.dryRun ?? false);
5180
5603
  for (const t of tag === undefined ? this.cachedTags() : [tag]) {
5181
5604
  const path = indexDir({ dir: tagDir(this.root, t) });
5182
- if (existsSync5(path))
5605
+ if (existsSync6(path))
5183
5606
  this.planRemove(p, path, `${t}'s indexes`);
5184
5607
  }
5185
5608
  return this.apply(p);
@@ -5188,8 +5611,8 @@ class ReleaseManager2 {
5188
5611
  const rel = this.cached(tag);
5189
5612
  const bad = [];
5190
5613
  for (const [name, f] of Object.entries(rel.receipt().files)) {
5191
- const p = join4(rel.dir, name);
5192
- const got = existsSync5(p) ? await sha256File2(p) : "missing";
5614
+ const p = join5(rel.dir, name);
5615
+ const got = existsSync6(p) ? await sha256File2(p) : "missing";
5193
5616
  if (got !== f.sha256)
5194
5617
  bad.push({ file: name, want: f.sha256, got });
5195
5618
  }
@@ -5201,11 +5624,11 @@ function releases2(opts = {}) {
5201
5624
  }
5202
5625
 
5203
5626
  var LOCAL_DIR2 = "local";
5204
- var localRoot2 = (root) => join5(root, LOCAL_DIR2);
5627
+ var localRoot2 = (root) => join6(root, LOCAL_DIR2);
5205
5628
  var newerThan2 = (a, b) => a.cut > b.cut || a.cut === b.cut && a.fraction > b.fraction;
5206
5629
  function readManifest(dir) {
5207
- const p = join5(dir, "MANIFEST.json");
5208
- if (!existsSync6(p))
5630
+ const p = join6(dir, "MANIFEST.json");
5631
+ if (!existsSync7(p))
5209
5632
  return null;
5210
5633
  try {
5211
5634
  const v = JSON.parse(readFileSync5(p, "utf8"));
@@ -5227,8 +5650,8 @@ function incomplete2(dir) {
5227
5650
  const d = dbs[name];
5228
5651
  if (d === undefined)
5229
5652
  continue;
5230
- const p = join5(dir, d.file ?? `${name}.sqlite`);
5231
- if (!existsSync6(p))
5653
+ const p = join6(dir, d.file ?? `${name}.sqlite`);
5654
+ if (!existsSync7(p))
5232
5655
  return `no ${d.file ?? `${name}.sqlite`}`;
5233
5656
  if (d.bytes !== undefined && statSync4(p).size !== d.bytes)
5234
5657
  return `${d.file ?? `${name}.sqlite`} is ${statSync4(p).size} bytes, the manifest says ${d.bytes}`;
@@ -5261,18 +5684,18 @@ function pickBuild2(builds, base) {
5261
5684
  return best;
5262
5685
  }
5263
5686
  function buildTag2(p) {
5264
- const at = typeof p.manifest.emittedAt === "string" && !Number.isNaN(Date.parse(p.manifest.emittedAt)) ? new Date(p.manifest.emittedAt) : statSync4(join5(p.build.dir, "MANIFEST.json")).mtime;
5687
+ const at = typeof p.manifest.emittedAt === "string" && !Number.isNaN(Date.parse(p.manifest.emittedAt)) ? new Date(p.manifest.emittedAt) : statSync4(join6(p.build.dir, "MANIFEST.json")).mtime;
5265
5688
  const date = at.toISOString().slice(0, 10).replaceAll("-", ".");
5266
5689
  return renderArchiveTag2({ date, cut: p.version.cut, fraction: p.version.fraction });
5267
5690
  }
5268
5691
  function buildFiles(dir) {
5269
- return readdirSync4(dir).filter((n) => statSync4(join5(dir, n)).isFile()).filter((n) => {
5692
+ return readdirSync4(dir).filter((n) => statSync4(join6(dir, n)).isFile()).filter((n) => {
5270
5693
  const m = /^(.*)\.sqlite(\.zst)?$/.exec(n);
5271
5694
  return m === null || isDatabase(m[1]);
5272
5695
  }).sort();
5273
5696
  }
5274
5697
  function lacks(b, manifest, inputs) {
5275
- return inputs.filter((a) => !planAsset2(a, manifest).published.some((n) => existsSync6(join5(b.dir, n))));
5698
+ return inputs.filter((a) => !planAsset2(a, manifest).published.some((n) => existsSync7(join6(b.dir, n))));
5276
5699
  }
5277
5700
  function buildRemote(label) {
5278
5701
  const refuse = async () => {
@@ -5289,7 +5712,7 @@ async function openBuild2(root, p, opts) {
5289
5712
  const o = buildOptions(root, p.build.label, opts);
5290
5713
  const local = localRoot2(root);
5291
5714
  const tag = buildTag2(p);
5292
- const want = sha256Of(join5(p.build.dir, "MANIFEST.json"));
5715
+ const want = sha256Of(join6(p.build.dir, "MANIFEST.json"));
5293
5716
  const current = () => {
5294
5717
  const rel = openCached2(tag, o);
5295
5718
  return rel !== null && rel.receipt().files["MANIFEST.json"]?.sha256 === want ? rel : null;
@@ -5298,33 +5721,33 @@ async function openBuild2(root, p, opts) {
5298
5721
  if (have !== null)
5299
5722
  return have;
5300
5723
  mkdirSync5(local, { recursive: true });
5301
- const work = join5(local, scratchName("add"));
5724
+ const work = join6(local, scratchName("add"));
5302
5725
  try {
5303
- const stage = join5(work, "stage");
5726
+ const stage = join6(work, "stage");
5304
5727
  mkdirSync5(stage, { recursive: true });
5305
5728
  for (const f of buildFiles(p.build.dir)) {
5306
5729
  try {
5307
- linkSync2(join5(p.build.dir, f), join5(stage, f));
5730
+ linkSync2(join6(p.build.dir, f), join6(stage, f));
5308
5731
  } catch {
5309
- copyFileSync2(join5(p.build.dir, f), join5(stage, f));
5732
+ copyFileSync2(join6(p.build.dir, f), join6(stage, f));
5310
5733
  }
5311
5734
  }
5312
- const added = await new ReleaseManager2({ ...o, cache: join5(work, "cache") }).add(stage, tag);
5735
+ const added = await new ReleaseManager2({ ...o, cache: join6(work, "cache") }).add(stage, tag);
5313
5736
  const receipt = added.receipt();
5314
5737
  receipt.source = `local:${p.build.dir}`;
5315
5738
  writeJsonAtomic2(receiptPath(added.dir), receipt);
5316
5739
  added.close();
5317
- const dest = join5(local, tag);
5318
- if (existsSync6(dest) && current() === null) {
5740
+ const dest = join6(local, tag);
5741
+ if (existsSync7(dest) && current() === null) {
5319
5742
  try {
5320
- renameSync3(dest, join5(local, scratchName("old")));
5743
+ renameSync3(dest, join6(local, scratchName("old")));
5321
5744
  } catch {}
5322
5745
  }
5323
5746
  try {
5324
5747
  renameSync3(added.dir, dest);
5325
5748
  } catch {}
5326
5749
  } finally {
5327
- rmSync5(work, { recursive: true, force: true });
5750
+ rmSync6(work, { recursive: true, force: true });
5328
5751
  }
5329
5752
  const rel = current();
5330
5753
  if (rel === null)
@@ -5341,7 +5764,7 @@ function alive(pid) {
5341
5764
  }
5342
5765
  function sweepBuilds2(root, keep) {
5343
5766
  const local = localRoot2(root);
5344
- if (!existsSync6(local))
5767
+ if (!existsSync7(local))
5345
5768
  return [];
5346
5769
  const gone = [];
5347
5770
  for (const name of readdirSync4(local).sort()) {
@@ -5352,23 +5775,23 @@ function sweepBuilds2(root, keep) {
5352
5775
  continue;
5353
5776
  if (m === null && parseArchiveTag2(name) === null)
5354
5777
  continue;
5355
- const path = join5(local, name);
5778
+ const path = join6(local, name);
5356
5779
  try {
5357
- rmSync5(path, { recursive: true, force: true });
5780
+ rmSync6(path, { recursive: true, force: true });
5358
5781
  gone.push(path);
5359
5782
  } catch {}
5360
5783
  }
5361
5784
  return gone;
5362
5785
  }
5363
5786
 
5364
- import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
5365
- import { join as join6 } from "path";
5787
+ import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
5788
+ import { join as join7 } from "path";
5366
5789
  var LATEST_CHECK_MS2 = 60 * 60 * 1000;
5367
5790
  var LATEST_CHECK_BUDGET_MS2 = 1500;
5368
- var latestCheckPath2 = (root) => join6(root, "LATEST.json");
5791
+ var latestCheckPath2 = (root) => join7(root, "LATEST.json");
5369
5792
  function readLatestCheck2(root) {
5370
5793
  const p = latestCheckPath2(root);
5371
- if (!existsSync7(p))
5794
+ if (!existsSync8(p))
5372
5795
  return null;
5373
5796
  try {
5374
5797
  const v = JSON.parse(readFileSync6(p, "utf8"));
@@ -5450,4 +5873,126 @@ function staleWarning2(answered, latest, floor, tags = [], command = "idq") {
5450
5873
  return `answered from ${answered}. ${floorTag ?? `cut ${floor.cut}`} is required${why}: run \`${command} use ${floor.cut}\` to get it`;
5451
5874
  }
5452
5875
 
5453
- export { DATABASES2, planAsset2, sha256File2, unzstdTo2, NAME_LEVELS2, nameLevel2, TabFinder2, PanelFinder2, FeisPanelFinder2, columnsOf2, FEATURES2, capabilityOf2, needsRelease2, rosterOf2, NO_CALENDAR2, NO_UPCOMING2, FeisFinder2, ADVANCEMENT_DANCES2, entryRank2, FEIS_LOCAL_ID_FLOOR2, ResultFinder2, MajorsFinder2, LINEAGE_FILE2, linksOfLineage2, releaseSchoolGraph2, readsSpanishNames2, SchoolResolver2, initialsOnly2, nameOrderOf, DancerResolver2, readPopulation2, syllabusSourceOf2, canonicalOf2, CompResolver2, StaffFinder2, TeamFinder2, EventFinder2, defaultBuilders2, indexPath2, indexKey2, isCurrent2, buildIndex2, ensureIndex2, listIndexes2, readPin2, CALENDAR_STAGES2, CALENDAR_STAGE_STATES2, CALENDAR_TERMINALS2, CalendarFinder2, youngest2, dancerResolverOf2, RegistrationResolver2, ghRemote2, httpsRemote2, fallbackRemote2, setRemote2, defaultRemote, FIELD_STATES2, DEPARTURE_REASONS2, UpcomingFinder2, resolveTag2, Release2, openRelease2, openCached2, DEFAULT_KEEP2, ReleaseManager2, releases2, LOCAL_DIR2, localRoot2, newerThan2, incomplete2, releaseVersion2, pickBuild2, buildTag2, lacks, openBuild2, sweepBuilds2, LATEST_CHECK_MS2, LATEST_CHECK_BUDGET_MS2, latestCheckPath2, readLatestCheck2, latestCheck2, within, staleWarning2 };
5876
+ var PROGRESS_MODES2 = ["tty", "plain", "none"];
5877
+ function progressMode2(flag, env, isTTY) {
5878
+ for (const [v, from] of [
5879
+ [flag, "--progress"],
5880
+ [env.IDQ_PROGRESS, "IDQ_PROGRESS"]
5881
+ ]) {
5882
+ if (v === undefined || v === "")
5883
+ continue;
5884
+ if (PROGRESS_MODES2.includes(v))
5885
+ return v;
5886
+ throw new Error(`${from} ${v}: expected ${PROGRESS_MODES2.join(", ")}`);
5887
+ }
5888
+ return isTTY ? "tty" : "plain";
5889
+ }
5890
+ var ENDS = new Set(["done", "failed", "cancelled"]);
5891
+ function bytesText(n) {
5892
+ if (n >= 1e9)
5893
+ return `${(n / 1e9).toFixed(2)} GB`;
5894
+ if (n >= 1e6)
5895
+ return `${(n / 1e6).toFixed(1)} MB`;
5896
+ if (n >= 1000)
5897
+ return `${(n / 1000).toFixed(1)} kB`;
5898
+ return `${n} B`;
5899
+ }
5900
+ var secs = (ms) => `${(ms / 1000).toFixed(1)} s`;
5901
+ function plainLine(e, started) {
5902
+ const what = e.kind === "index" ? `${e.tag} index ${e.item}` : `${e.tag} ${e.item}`;
5903
+ if (e.state === "done") {
5904
+ if (e.kind === "index")
5905
+ return `done ${what} in ${secs(e.ms)}`;
5906
+ const size = e.unpacked > 0 ? `${bytesText(e.bytes)} -> ${bytesText(e.unpacked)}` : bytesText(e.bytes);
5907
+ return `done ${what} ${size} in ${secs(e.ms)}`;
5908
+ }
5909
+ if (e.state === "failed")
5910
+ return `failed ${what} after ${secs(e.ms)}: ${e.error ?? "failed"}`;
5911
+ if (e.state === "cancelled")
5912
+ return `cancelled ${what} after ${secs(e.ms)}`;
5913
+ if (started)
5914
+ return null;
5915
+ return `start ${what}${e.total !== null ? ` (${bytesText(e.total)})` : ""}`;
5916
+ }
5917
+ function ttyLine(e, width) {
5918
+ const name = e.kind === "index" ? `index ${e.item}` : e.item;
5919
+ let amount = "";
5920
+ if (e.kind === "asset") {
5921
+ amount = e.total !== null ? `${bytesText(e.bytes)} / ${bytesText(e.total)}` : bytesText(e.bytes);
5922
+ if (e.unpacked > 0)
5923
+ amount += ` unpacked ${bytesText(e.unpacked)}`;
5924
+ if (!ENDS.has(e.state) && e.ms > 0)
5925
+ amount += ` ${bytesText(e.bytes * 1000 / e.ms)}/s`;
5926
+ }
5927
+ const state = e.state === "failed" ? `failed: ${e.error ?? ""}` : ENDS.has(e.state) ? `${e.state} in ${secs(e.ms)}` : e.state;
5928
+ const line = `${name.padEnd(34)} ${amount.padEnd(44)} ${state}`;
5929
+ return line.length > width ? line.slice(0, Math.max(0, width - 1)) : line;
5930
+ }
5931
+ function progressRenderer2(mode, io) {
5932
+ if (mode === "none")
5933
+ return { mode, sink: () => {}, close: () => {} };
5934
+ if (mode === "plain") {
5935
+ const started = new Set;
5936
+ return {
5937
+ mode,
5938
+ sink: (e) => {
5939
+ const key = `${e.tag}\x00${e.kind}\x00${e.item}`;
5940
+ const l = plainLine(e, started.has(key));
5941
+ started.add(key);
5942
+ if (ENDS.has(e.state))
5943
+ started.delete(key);
5944
+ if (l !== null)
5945
+ io.line(l);
5946
+ },
5947
+ close: () => {}
5948
+ };
5949
+ }
5950
+ const write = io.write ?? ((s) => process.stderr.write(s));
5951
+ const columns = io.columns ?? (() => process.stderr.columns ?? 0);
5952
+ const interval = io.intervalMs ?? 100;
5953
+ const items = new Map;
5954
+ let drawn = 0;
5955
+ let timer = null;
5956
+ let dirty = false;
5957
+ const draw = () => {
5958
+ timer = null;
5959
+ if (!dirty)
5960
+ return;
5961
+ dirty = false;
5962
+ const width = Math.max(40, columns() || 120);
5963
+ let out = drawn > 0 ? `\x1B[${drawn}A` : "";
5964
+ for (const e of items.values())
5965
+ out += `\r\x1B[2K${ttyLine(e, width)}
5966
+ `;
5967
+ drawn = items.size;
5968
+ write(out);
5969
+ if ([...items.values()].every((e) => ENDS.has(e.state))) {
5970
+ items.clear();
5971
+ drawn = 0;
5972
+ }
5973
+ };
5974
+ return {
5975
+ mode,
5976
+ sink: (e) => {
5977
+ const key = `${e.tag}\x00${e.kind}\x00${e.item}`;
5978
+ const moved = items.get(key)?.state !== e.state;
5979
+ items.set(key, e);
5980
+ dirty = true;
5981
+ if (moved) {
5982
+ if (timer !== null)
5983
+ clearTimeout(timer);
5984
+ draw();
5985
+ } else if (timer === null) {
5986
+ timer = setTimeout(draw, interval);
5987
+ timer.unref?.();
5988
+ }
5989
+ },
5990
+ close: () => {
5991
+ if (timer !== null)
5992
+ clearTimeout(timer);
5993
+ draw();
5994
+ }
5995
+ };
5996
+ }
5997
+
5998
+ export { DATABASES2, planAsset2, sha256File2, unzstdTo2, streamTo2, NAME_LEVELS2, nameLevel2, TabFinder2, PanelFinder2, FeisPanelFinder2, columnsOf2, FEATURES2, capabilityOf2, needsRelease2, rosterOf2, NO_CALENDAR2, NO_UPCOMING2, FeisFinder2, ADVANCEMENT_DANCES2, entryRank2, FEIS_LOCAL_ID_FLOOR2, ResultFinder2, MajorsFinder2, LINEAGE_FILE2, linksOfLineage2, releaseSchoolGraph2, readsSpanishNames2, SchoolResolver2, initialsOnly2, nameOrderOf, DancerResolver2, readPopulation2, syllabusSourceOf2, canonicalOf2, CompResolver2, StaffFinder2, TeamFinder2, EventFinder2, defaultBuilders2, indexPath2, indexKey2, isCurrent2, buildIndex2, ensureIndex2, listIndexes2, readPin2, CALENDAR_STAGES2, CALENDAR_STAGE_STATES2, CALENDAR_TERMINALS2, CalendarFinder2, youngest2, dancerResolverOf2, RegistrationResolver2, ghRemote2, httpsRemote2, fallbackRemote2, setRemote2, defaultRemote, FIELD_STATES2, DEPARTURE_REASONS2, UpcomingFinder2, resolveTag2, DEFAULT_CONCURRENCY2, Release2, openRelease2, openCached2, DEFAULT_KEEP2, ReleaseManager2, releases2, LOCAL_DIR2, localRoot2, newerThan2, incomplete2, releaseVersion2, pickBuild2, buildTag2, lacks, openBuild2, sweepBuilds2, LATEST_CHECK_MS2, LATEST_CHECK_BUDGET_MS2, latestCheckPath2, readLatestCheck2, latestCheck2, within, staleWarning2, PROGRESS_MODES2, progressMode2, progressRenderer2 };