@finbheara/archive-client 0.40.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";
@@ -503,6 +552,18 @@ var FEATURES2 = [
503
552
  since: 345,
504
553
  calls: "the same on a row of results.stream and results.entries (scope majors)"
505
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
+ },
506
567
  {
507
568
  feature: "majors.tabs",
508
569
  needs: ["majors:tabs.address", "majors:tabs.true_round"],
@@ -1414,7 +1475,7 @@ function streamSql(q, shape = {}) {
1414
1475
  if (q.primary === true)
1415
1476
  where.push("(r.primary_source IS NULL OR r.primary_source = '')");
1416
1477
  return {
1417
- sql: `SELECT ${COLUMNS}${shape.competition === true ? COMPETITION_COLUMNS : ""}${shape.competitorsPrinted === true ? ", c.competitors_printed AS competitors_printed" : ""} 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`,
1418
1479
  args
1419
1480
  };
1420
1481
  }
@@ -1495,7 +1556,7 @@ var COLUMNS2 = `r.address AS address, r.competition AS competition, c.event AS e
1495
1556
  r.place_printed AS place_printed, c.competitors AS competitors, r.name_printed AS name_printed,
1496
1557
  r.dancer_id AS dancer_id, r.school_printed AS school_printed, r.school_id AS school_id,
1497
1558
  r.yob AS yob, r.qualifier AS qualifier`;
1498
- function queryResults(rel, db, q) {
1559
+ function queryResults(rel, db, q, basis = false) {
1499
1560
  const where = [];
1500
1561
  const args = [];
1501
1562
  if (q.dancer_id !== undefined) {
@@ -1521,7 +1582,7 @@ function queryResults(rel, db, q) {
1521
1582
  args.push(q.year);
1522
1583
  }
1523
1584
  const asked = q.limit ?? 500;
1524
- 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
1525
1586
  WHERE ${where.join(" AND ")}
1526
1587
  ORDER BY c.year, c.event, r.competition, r.place IS NULL, r.place, r.address`).all(...args);
1527
1588
  return {
@@ -1544,7 +1605,8 @@ class ResultFinder2 {
1544
1605
  async query(q) {
1545
1606
  await this.rel.fetch("results");
1546
1607
  await this.rel.require("results.level");
1547
- 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);
1548
1610
  }
1549
1611
  async dancerLevel(q) {
1550
1612
  await this.rel.fetch("results");
@@ -1557,7 +1619,8 @@ class ResultFinder2 {
1557
1619
  await this.rel.require(db === "majors" ? "majors.stream" : "results.stream");
1558
1620
  const competition = (await this.rel.capability(`${db}.stream.competition`)).state === "full";
1559
1621
  const competitorsPrinted = (await this.rel.capability(`${db}.stream.competitors_printed`)).state === "full";
1560
- return { db: this.rel.db(db), shape: { competition, competitorsPrinted } };
1622
+ const attachBasis = (await this.rel.capability(`${db}.attach_basis`)).state === "full";
1623
+ return { db: this.rel.db(db), shape: { competition, competitorsPrinted, attachBasis } };
1561
1624
  }
1562
1625
  async* stream(q) {
1563
1626
  const { db, shape } = await this.streamDb(q);
@@ -1621,7 +1684,8 @@ class MajorsFinder2 {
1621
1684
  async results(q) {
1622
1685
  await this.db();
1623
1686
  await this.rel.require("results.level");
1624
- 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);
1625
1689
  }
1626
1690
  }
1627
1691
 
@@ -3306,8 +3370,8 @@ class CompResolver2 {
3306
3370
 
3307
3371
  import { Database } from "bun:sqlite";
3308
3372
  import { createHash as createHash2 } from "crypto";
3309
- import { existsSync, mkdirSync, readdirSync, renameSync, rmSync, statSync } from "fs";
3310
- 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";
3311
3375
 
3312
3376
  import {
3313
3377
  createIndex as createIndex3,
@@ -3547,6 +3611,34 @@ class StaffFinder2 {
3547
3611
  }
3548
3612
  }
3549
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
+
3550
3642
  import { parquetMetadata, parquetReadObjects, parquetSchema } from "hyparquet";
3551
3643
  var COMPRESSORS = {
3552
3644
  ZSTD: (input) => new Uint8Array(Bun.zstdDecompressSync(input))
@@ -3793,8 +3885,8 @@ class EventFinder2 {
3793
3885
  function defaultBuilders2() {
3794
3886
  return [schoolsIndex, feisesIndex, compsIndex, dancersIndex, staffIndex, teamsIndex, eventsIndex];
3795
3887
  }
3796
- var indexDir = (rel) => join(rel.dir, "index");
3797
- 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`);
3798
3890
  function indexKey2(rel, b) {
3799
3891
  const files = rel.receipt().files;
3800
3892
  const parts = [b.name, b.version, ...b.keyParts?.(rel) ?? []];
@@ -3805,7 +3897,7 @@ function indexKey2(rel, b) {
3805
3897
  return createHash2("sha256").update(parts.join("\x00")).digest("hex");
3806
3898
  }
3807
3899
  function keyOf(path) {
3808
- if (!existsSync(path))
3900
+ if (!existsSync2(path))
3809
3901
  return null;
3810
3902
  try {
3811
3903
  const db = new Database(path, { readonly: true });
@@ -3830,8 +3922,9 @@ function buildIndex2(rel, b, o) {
3830
3922
  if (b.load !== undefined && o.loaded === undefined)
3831
3923
  throw new Error(`archive-client: index "${b.name}" reads its inputs first: build it with ensureIndex (or rel.index)`);
3832
3924
  mkdirSync(indexDir(rel), { recursive: true });
3925
+ sweepDead(indexDir(rel), BUILDING_FILE);
3833
3926
  const tmp = `${out}.building.${process.pid}`;
3834
- rmSync(tmp, { force: true });
3927
+ rmSync2(tmp, { force: true });
3835
3928
  const t0 = performance.now();
3836
3929
  const db = new Database(tmp, { create: true });
3837
3930
  try {
@@ -3847,7 +3940,7 @@ function buildIndex2(rel, b, o) {
3847
3940
  db.run("VACUUM");
3848
3941
  } catch (e) {
3849
3942
  db.close();
3850
- rmSync(tmp, { force: true });
3943
+ rmSync2(tmp, { force: true });
3851
3944
  throw e;
3852
3945
  }
3853
3946
  db.close();
@@ -3858,16 +3951,42 @@ function buildIndex2(rel, b, o) {
3858
3951
  async function ensureIndex2(rel, b, o) {
3859
3952
  if (!o.force && isCurrent2(rel, b))
3860
3953
  return indexPath2(rel, b.name);
3861
- const loaded = b.load === undefined ? undefined : await b.load(rel);
3862
- 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
+ }
3863
3982
  }
3864
3983
  function listIndexes2(rel) {
3865
3984
  const dir = indexDir(rel);
3866
- if (!existsSync(dir))
3985
+ if (!existsSync2(dir))
3867
3986
  return [];
3868
- return readdirSync(dir).filter((f) => f.endsWith(".sqlite")).sort().map((f) => {
3987
+ return readdirSync2(dir).filter((f) => f.endsWith(".sqlite")).sort().map((f) => {
3869
3988
  const name = f.slice(0, -".sqlite".length);
3870
- const path = join(dir, f);
3989
+ const path = join2(dir, f);
3871
3990
  const b = rel.builders().find((x) => x.name === name);
3872
3991
  return {
3873
3992
  name,
@@ -3881,35 +4000,35 @@ function listIndexes2(rel) {
3881
4000
  import { createHash as createHash3 } from "crypto";
3882
4001
  import {
3883
4002
  copyFileSync as copyFileSync2,
3884
- existsSync as existsSync6,
4003
+ existsSync as existsSync7,
3885
4004
  linkSync as linkSync2,
3886
4005
  mkdirSync as mkdirSync5,
3887
4006
  readFileSync as readFileSync5,
3888
4007
  readdirSync as readdirSync4,
3889
4008
  renameSync as renameSync3,
3890
- rmSync as rmSync5,
4009
+ rmSync as rmSync6,
3891
4010
  statSync as statSync4
3892
4011
  } from "fs";
3893
- import { join as join5 } from "path";
4012
+ import { join as join6 } from "path";
3894
4013
 
3895
4014
  import {
3896
4015
  copyFileSync,
3897
- existsSync as existsSync5,
4016
+ existsSync as existsSync6,
3898
4017
  linkSync,
3899
4018
  mkdirSync as mkdirSync4,
3900
4019
  readFileSync as readFileSync4,
3901
4020
  readdirSync as readdirSync3,
3902
- rmSync as rmSync4,
4021
+ rmSync as rmSync5,
3903
4022
  statSync as statSync3
3904
4023
  } from "fs";
3905
- import { isAbsolute, join as join4, relative, resolve, sep } from "path";
4024
+ import { isAbsolute, join as join5, relative, resolve, sep } from "path";
3906
4025
 
3907
- import { existsSync as existsSync2, readFileSync as readFileSync2, rmSync as rmSync2 } from "fs";
3908
- import { join as join2 } from "path";
3909
- 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");
3910
4029
  function readPin2(root) {
3911
4030
  const p = pinPath(root);
3912
- if (!existsSync2(p))
4031
+ if (!existsSync3(p))
3913
4032
  return null;
3914
4033
  try {
3915
4034
  const v = JSON.parse(readFileSync2(p, "utf8"));
@@ -3925,23 +4044,24 @@ function writePin(root, pin) {
3925
4044
  }
3926
4045
  function clearPin(root) {
3927
4046
  const p = pinPath(root);
3928
- if (!existsSync2(p))
4047
+ if (!existsSync3(p))
3929
4048
  return false;
3930
- rmSync2(p);
4049
+ rmSync3(p);
3931
4050
  return true;
3932
4051
  }
3933
4052
 
3934
4053
  import { Database as Database2 } from "bun:sqlite";
4054
+ import { randomBytes } from "crypto";
3935
4055
  import {
3936
- existsSync as existsSync4,
4056
+ createReadStream as createReadStream2,
4057
+ existsSync as existsSync5,
3937
4058
  mkdirSync as mkdirSync3,
3938
4059
  readFileSync as readFileSync3,
3939
- readdirSync as readdirSync2,
3940
4060
  renameSync as renameSync2,
3941
- rmSync as rmSync3,
4061
+ rmSync as rmSync4,
3942
4062
  statSync as statSync2
3943
4063
  } from "fs";
3944
- import { join as join3 } from "path";
4064
+ import { basename, dirname as dirname2, join as join4 } from "path";
3945
4065
 
3946
4066
  var CALENDAR_STAGES2 = [
3947
4067
  "announced",
@@ -4277,9 +4397,9 @@ class RegistrationResolver2 {
4277
4397
  }
4278
4398
 
4279
4399
  import { spawn } from "child_process";
4280
- 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";
4281
4401
  import { dirname } from "path";
4282
- import { Readable } from "stream";
4402
+ import { Readable as Readable2 } from "stream";
4283
4403
  import { pipeline as pipeline2 } from "stream/promises";
4284
4404
  function run(cmd, args) {
4285
4405
  return new Promise((resolve) => {
@@ -4345,8 +4465,59 @@ var ghRemote2 = {
4345
4465
  dest,
4346
4466
  "--clobber"
4347
4467
  ]);
4348
- if (r.status !== 0 || !existsSync3(dest))
4468
+ if (r.status !== 0 || !existsSync4(dest))
4349
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
+ }();
4350
4521
  }
4351
4522
  };
4352
4523
  var API = "https://api.github.com";
@@ -4399,7 +4570,19 @@ function httpsRemote2(env = process.env, fetchImpl = fetch) {
4399
4570
  if (!res.ok || res.body === null)
4400
4571
  throw new Error(`GET ${a.url}: ${res.status} ${res.statusText}`);
4401
4572
  mkdirSync2(dirname(dest), { recursive: true });
4402
- 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));
4403
4586
  }
4404
4587
  };
4405
4588
  }
@@ -4419,7 +4602,10 @@ function fallbackRemote2(first, second) {
4419
4602
  name: `${first.name}+${second.name}`,
4420
4603
  tags: both((r) => r.tags.bind(r)),
4421
4604
  assets: both((r) => r.assets.bind(r)),
4422
- 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
+ } : {}
4423
4609
  };
4424
4610
  }
4425
4611
  var installed = null;
@@ -4658,12 +4844,12 @@ class UpcomingFinder2 {
4658
4844
  function tagDir(root, tag) {
4659
4845
  if (parseArchiveTag2(tag) === null)
4660
4846
  throw new Error(`archive-client: "${tag}" is not a release tag (archive-YYYY.MM.DD[-N[.k]])`);
4661
- return join3(root, tag);
4847
+ return join4(root, tag);
4662
4848
  }
4663
- var receiptPath = (dir) => join3(dir, ".client.json");
4849
+ var receiptPath = (dir) => join4(dir, ".client.json");
4664
4850
  function readReceipt(dir) {
4665
4851
  const p = receiptPath(dir);
4666
- if (!existsSync4(p))
4852
+ if (!existsSync5(p))
4667
4853
  return null;
4668
4854
  try {
4669
4855
  return JSON.parse(readFileSync3(p, "utf8"));
@@ -4680,7 +4866,7 @@ async function resolveTag2(spec, opts = {}) {
4680
4866
  const pin = readPin2(root);
4681
4867
  return resolveTag2(pin === null ? { kind: "latest" } : { kind: "tag", tag: pin.tag }, opts);
4682
4868
  }
4683
- 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")))
4684
4870
  return s.tag;
4685
4871
  const repo = requireRepo2(opts);
4686
4872
  const tags = await (opts.remote ?? defaultRemote()).tags(repo);
@@ -4689,9 +4875,30 @@ async function resolveTag2(spec, opts = {}) {
4689
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"}`);
4690
4876
  return tag;
4691
4877
  }
4692
- function dropIfEmpty(dir) {
4693
- if (existsSync4(dir) && readdirSync2(dir).length === 0)
4694
- 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
+ }
4695
4902
  }
4696
4903
 
4697
4904
  class Release2 {
@@ -4718,7 +4925,7 @@ class Release2 {
4718
4925
  feisPanels;
4719
4926
  opts;
4720
4927
  handles = new Map;
4721
- remoteAssets = null;
4928
+ listing = null;
4722
4929
  constructor(tag, root, manifest, opts) {
4723
4930
  const t = parseArchiveTag2(tag);
4724
4931
  this.tag = tag;
@@ -4751,19 +4958,65 @@ class Release2 {
4751
4958
  return planAsset2(asset, this.manifest);
4752
4959
  }
4753
4960
  has(asset) {
4754
- return existsSync4(join3(this.dir, this.plan(asset).local));
4961
+ return existsSync5(join4(this.dir, this.plan(asset).local));
4755
4962
  }
4756
4963
  path(asset) {
4757
- const p = join3(this.dir, this.plan(asset).local);
4758
- if (!existsSync4(p))
4964
+ const p = join4(this.dir, this.plan(asset).local);
4965
+ if (!existsSync5(p))
4759
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}\`)`);
4760
4967
  return p;
4761
4968
  }
4762
4969
  async fetch(...assets) {
4763
- const out = [];
4764
- for (const a of assets)
4765
- out.push(await this.fetchOne(a));
4766
- 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
+ };
4767
5020
  }
4768
5021
  async publishes(asset) {
4769
5022
  const plan = this.plan(asset);
@@ -4819,25 +5072,29 @@ class Release2 {
4819
5072
  if (c.state === "missing")
4820
5073
  throw needsRelease2(this.tag, c);
4821
5074
  }
4822
- async listed() {
4823
- if (this.remoteAssets === null) {
4824
- this.remoteAssets = await (this.opts.remote ?? defaultRemote()).assets(requireRepo2(this.opts), this.tag);
4825
- 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)) {
4826
5079
  const r = this.receipt();
4827
- r.published = this.remoteAssets.map((a) => a.name).sort();
5080
+ r.published = assets.map((a) => a.name).sort();
4828
5081
  writeJsonAtomic2(receiptPath(this.dir), r);
4829
5082
  }
4830
- }
4831
- return this.remoteAssets;
5083
+ return assets;
5084
+ })();
5085
+ this.listing.catch(() => {
5086
+ this.listing = null;
5087
+ });
5088
+ return this.listing;
4832
5089
  }
4833
- async fetchOne(asset) {
5090
+ async fetchOne(asset, signal) {
4834
5091
  const plan = this.plan(asset);
4835
- const dest = join3(this.dir, plan.local);
5092
+ const dest = join4(this.dir, plan.local);
4836
5093
  const have = this.receipt().files[plan.local];
4837
- if (existsSync4(dest) && have !== undefined)
5094
+ if (existsSync5(dest) && have !== undefined)
4838
5095
  return have;
4839
5096
  const log = this.opts.log ?? (() => {});
4840
- if (existsSync4(dest) && plan.contentSha256 !== null) {
5097
+ if (existsSync5(dest) && plan.contentSha256 !== null) {
4841
5098
  const sha = await sha256File2(dest);
4842
5099
  if (sha === plan.contentSha256) {
4843
5100
  const adopted = {
@@ -4846,7 +5103,8 @@ class Release2 {
4846
5103
  bytes: statSync2(dest).size,
4847
5104
  verified: ["manifest"]
4848
5105
  };
4849
- 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)`);
4850
5108
  this.record(plan.local, adopted);
4851
5109
  return adopted;
4852
5110
  }
@@ -4855,14 +5113,24 @@ class Release2 {
4855
5113
  const pub = plan.published.map((n) => listed.find((a) => a.name === n)).find((a) => a !== undefined);
4856
5114
  if (pub === undefined)
4857
5115
  throw new Error(`archive-client: ${this.tag} publishes no ${plan.published.join(" or ")} (asked: ${asset})`);
4858
- const part = join3(this.dir, ".part");
4859
- mkdirSync3(part, { recursive: true });
4860
- const raw = join3(part, pub.name);
4861
- log(`fetch ${this.tag} ${pub.name} (${pub.size} bytes)`);
4862
- await (this.opts.remote ?? defaultRemote()).download(requireRepo2(this.opts), this.tag, pub.name, raw);
4863
- 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
+ });
4864
5133
  this.record(plan.local, landed);
4865
- dropIfEmpty(part);
4866
5134
  return landed;
4867
5135
  }
4868
5136
  record(local, f) {
@@ -4891,7 +5159,11 @@ class Release2 {
4891
5159
  if (FEATURES2.some((f) => f.feature === b.name))
4892
5160
  await this.require(b.name);
4893
5161
  await this.fetch(...b.inputs);
4894
- 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
+ });
4895
5167
  }
4896
5168
  async indexDb(name) {
4897
5169
  const key = `index:${name}`;
@@ -4911,69 +5183,172 @@ class Release2 {
4911
5183
  this.close();
4912
5184
  }
4913
5185
  }
4914
- async function land(raw, pub, plan, dest, opts, log) {
5186
+ function verifyLanded(got, pub, plan, strict) {
4915
5187
  const verified = [];
4916
5188
  const host = sha256OfDigest(pub.digest);
4917
5189
  if (host !== null) {
4918
- const got = await sha256File2(raw);
4919
- if (got !== host) {
4920
- rmSync3(raw, { force: true });
4921
- throw new Error(`archive-client: ${pub.name} does not match the host's digest (sha256 ${got}, the host says ${host})`);
4922
- }
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})`);
4923
5192
  verified.push("host-digest");
4924
5193
  }
4925
- let landedAt = raw;
4926
- let sha;
4927
- if (plan.decompress && pub.name.endsWith(".zst")) {
4928
- landedAt = `${raw.slice(0, -".zst".length)}.plain`;
4929
- log(`decompress ${pub.name}`);
4930
- sha = await unzstdTo2(raw, landedAt);
4931
- } else
4932
- sha = await sha256File2(raw);
4933
5194
  if (plan.contentSha256 !== null) {
4934
- if (sha !== plan.contentSha256) {
4935
- rmSync3(landedAt, { force: true });
4936
- throw new Error(`archive-client: ${plan.local} does not match MANIFEST.json (sha256 ${sha}, the manifest says ${plan.contentSha256})`);
4937
- }
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})`);
4938
5197
  verified.push("manifest");
4939
5198
  }
4940
- if (verified.length === 0 && opts.strict === true) {
4941
- rmSync3(landedAt, { force: true });
5199
+ if (verified.length === 0 && strict)
4942
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;
4943
5312
  }
4944
5313
  renameSync2(landedAt, dest);
4945
5314
  if (landedAt !== raw) {
4946
5315
  if (opts.keepCompressed === true)
4947
5316
  renameSync2(raw, `${dest}.zst`);
4948
5317
  else
4949
- rmSync3(raw, { force: true });
5318
+ rmSync4(raw, { force: true });
4950
5319
  }
4951
- return { from: pub.name, sha256: sha, bytes: statSync2(dest).size, verified };
5320
+ return { from: pub.name, sha256: got.sha256, bytes: statSync2(dest).size, verified };
4952
5321
  }
4953
5322
  async function openRelease2(spec = "pinned", opts = {}) {
4954
5323
  const root = cacheRoot2(opts);
4955
5324
  const tag = await resolveTag2(spec, opts);
4956
5325
  const dir = tagDir(root, tag);
4957
- const manifestPath = join3(dir, "MANIFEST.json");
4958
- if (!existsSync4(manifestPath)) {
5326
+ const manifestPath = join4(dir, "MANIFEST.json");
5327
+ if (!existsSync5(manifestPath)) {
4959
5328
  const remote = opts.remote ?? defaultRemote();
4960
5329
  const repo = requireRepo2(opts);
4961
5330
  const listed = await remote.assets(repo, tag);
4962
5331
  const pub = listed.find((a) => a.name === "MANIFEST.json");
4963
5332
  if (pub === undefined)
4964
5333
  throw new Error(`archive-client: ${tag} publishes no MANIFEST.json`);
4965
- const part = join3(dir, ".part");
4966
- mkdirSync3(part, { recursive: true });
4967
- const raw = join3(part, "MANIFEST.json");
4968
- await remote.download(repo, tag, "MANIFEST.json", raw);
4969
- 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
+ });
4970
5346
  writeJsonAtomic2(receiptPath(dir), {
4971
5347
  tag,
4972
5348
  source: repo,
4973
5349
  files: { "MANIFEST.json": landed },
4974
5350
  published: listed.map((a) => a.name).sort()
4975
5351
  });
4976
- dropIfEmpty(part);
4977
5352
  }
4978
5353
  const manifest = JSON.parse(readFileSync3(manifestPath, "utf8"));
4979
5354
  const rel = new Release2(tag, root, manifest, opts);
@@ -4982,8 +5357,8 @@ async function openRelease2(spec = "pinned", opts = {}) {
4982
5357
  }
4983
5358
  function openCached2(tag, opts = {}) {
4984
5359
  const root = cacheRoot2(opts);
4985
- const p = join3(tagDir(root, tag), "MANIFEST.json");
4986
- if (!existsSync4(p))
5360
+ const p = join4(tagDir(root, tag), "MANIFEST.json");
5361
+ if (!existsSync5(p))
4987
5362
  return null;
4988
5363
  return new Release2(tag, root, JSON.parse(readFileSync3(p, "utf8")), opts);
4989
5364
  }
@@ -4995,14 +5370,14 @@ function checkKeep(keep) {
4995
5370
  return keep;
4996
5371
  }
4997
5372
  function duBytes(p) {
4998
- if (!existsSync5(p))
5373
+ if (!existsSync6(p))
4999
5374
  return 0;
5000
5375
  const s = statSync3(p);
5001
5376
  if (!s.isDirectory())
5002
5377
  return s.size;
5003
5378
  let n = 0;
5004
5379
  for (const e of readdirSync3(p))
5005
- n += duBytes(join4(p, e));
5380
+ n += duBytes(join5(p, e));
5006
5381
  return n;
5007
5382
  }
5008
5383
 
@@ -5014,9 +5389,9 @@ class ReleaseManager2 {
5014
5389
  this.root = cacheRoot2(opts);
5015
5390
  }
5016
5391
  cachedTags() {
5017
- if (!existsSync5(this.root))
5392
+ if (!existsSync6(this.root))
5018
5393
  return [];
5019
- 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);
5020
5395
  }
5021
5396
  cached(tag) {
5022
5397
  const r = openCached2(tag, this.opts);
@@ -5056,10 +5431,14 @@ class ReleaseManager2 {
5056
5431
  async use(spec, o = {}) {
5057
5432
  const keep = o.prune === false ? null : this.keepCount(o.keep);
5058
5433
  const was = readPin2(this.root)?.tag ?? null;
5059
- const rel = await this.fetch(spec, o);
5434
+ const rel = await openRelease2(spec, { ...this.opts, assets: [] });
5060
5435
  const tag = rel.tag;
5061
- rel.close();
5062
- 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
+ }
5063
5442
  const pin = await this.pin(tag);
5064
5443
  if (keep === null)
5065
5444
  return { tag, was, pin, built, pruned: null };
@@ -5093,22 +5472,22 @@ class ReleaseManager2 {
5093
5472
  async add(dir, tag) {
5094
5473
  if (parseArchiveTag2(tag) === null)
5095
5474
  throw new Error(`add: "${tag}" is not a release tag`);
5096
- const mf = join4(dir, "MANIFEST.json");
5097
- if (!existsSync5(mf))
5475
+ const mf = join5(dir, "MANIFEST.json");
5476
+ if (!existsSync6(mf))
5098
5477
  throw new Error(`add: ${dir} holds no MANIFEST.json`);
5099
5478
  const dest = tagDir(this.root, tag);
5100
- if (existsSync5(join4(dest, "MANIFEST.json")))
5479
+ if (existsSync6(join5(dest, "MANIFEST.json")))
5101
5480
  throw new Error(`add: ${tag} is already in the cache (${dest}); \`rm ${tag}\` first`);
5102
5481
  const manifest = JSON.parse(readFileSync4(mf, "utf8"));
5103
- const part = join4(dest, ".part");
5482
+ const part = join5(dest, ".part");
5104
5483
  mkdirSync4(part, { recursive: true });
5105
5484
  const receipt = { tag, source: `local:${dir}`, files: {} };
5106
5485
  const log = this.opts.log ?? (() => {});
5107
5486
  for (const name of readdirSync3(dir).sort()) {
5108
- const src = join4(dir, name);
5487
+ const src = join5(dir, name);
5109
5488
  if (!statSync3(src).isFile())
5110
5489
  continue;
5111
- const raw = join4(part, name);
5490
+ const raw = join5(part, name);
5112
5491
  try {
5113
5492
  linkSync(src, raw);
5114
5493
  } catch {
@@ -5118,10 +5497,10 @@ class ReleaseManager2 {
5118
5497
  const side = Object.entries(manifest.sideFiles ?? {}).find(([k, f]) => (f.file ?? `${k}.zst`) === name)?.[0];
5119
5498
  const asked = base !== name && isDatabase(base) ? base : side ?? name;
5120
5499
  const plan = planAsset2(asked, manifest);
5121
- 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);
5122
5501
  receipt.files[plan.local] = landed;
5123
5502
  }
5124
- rmSync4(part, { recursive: true, force: true });
5503
+ rmSync5(part, { recursive: true, force: true });
5125
5504
  writeJsonAtomic2(receiptPath(dest), receipt);
5126
5505
  const rel = openCached2(tag, this.opts);
5127
5506
  if (rel === null)
@@ -5130,20 +5509,51 @@ class ReleaseManager2 {
5130
5509
  }
5131
5510
  async index(spec, names = [], o = {}) {
5132
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) {
5133
5519
  const want = names.length > 0 ? names : rel.builders().map((b) => b.name);
5134
- const built = [];
5135
- for (const name of want) {
5520
+ const builders = want.map((name) => {
5136
5521
  const b = rel.builders().find((x) => x.name === name);
5137
5522
  if (b === undefined)
5138
5523
  throw new Error(`index: no index "${name}"`);
5139
- await rel.fetch(...b.inputs);
5140
- built.push({
5141
- name,
5142
- path: await ensureIndex2(rel, b, { force: o.force ?? false, log: this.opts.log })
5143
- });
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
+ }
5144
5554
  }
5145
- rel.close();
5146
- return { tag: rel.tag, built };
5555
+ await f.all;
5556
+ return want.map((name) => ({ name, path: paths.get(name) }));
5147
5557
  }
5148
5558
  plan(dryRun) {
5149
5559
  return { dryRun, remove: [], keep: [], bytes: 0 };
@@ -5158,7 +5568,7 @@ class ReleaseManager2 {
5158
5568
  p.bytes = p.remove.reduce((n, r) => n + r.bytes, 0);
5159
5569
  if (!p.dryRun)
5160
5570
  for (const r of p.remove)
5161
- rmSync4(r.path, { recursive: true, force: true });
5571
+ rmSync5(r.path, { recursive: true, force: true });
5162
5572
  return p;
5163
5573
  }
5164
5574
  prune(o = {}) {
@@ -5180,7 +5590,7 @@ class ReleaseManager2 {
5180
5590
  }
5181
5591
  remove(tag, o = {}) {
5182
5592
  const path = tagDir(this.root, tag);
5183
- if (!existsSync5(path))
5593
+ if (!existsSync6(path))
5184
5594
  throw new Error(`${tag} is not in the cache`);
5185
5595
  if (readPin2(this.root)?.tag === tag)
5186
5596
  throw new Error(`${tag} is pinned: \`unpin\` first`);
@@ -5192,7 +5602,7 @@ class ReleaseManager2 {
5192
5602
  const p = this.plan(o.dryRun ?? false);
5193
5603
  for (const t of tag === undefined ? this.cachedTags() : [tag]) {
5194
5604
  const path = indexDir({ dir: tagDir(this.root, t) });
5195
- if (existsSync5(path))
5605
+ if (existsSync6(path))
5196
5606
  this.planRemove(p, path, `${t}'s indexes`);
5197
5607
  }
5198
5608
  return this.apply(p);
@@ -5201,8 +5611,8 @@ class ReleaseManager2 {
5201
5611
  const rel = this.cached(tag);
5202
5612
  const bad = [];
5203
5613
  for (const [name, f] of Object.entries(rel.receipt().files)) {
5204
- const p = join4(rel.dir, name);
5205
- const got = existsSync5(p) ? await sha256File2(p) : "missing";
5614
+ const p = join5(rel.dir, name);
5615
+ const got = existsSync6(p) ? await sha256File2(p) : "missing";
5206
5616
  if (got !== f.sha256)
5207
5617
  bad.push({ file: name, want: f.sha256, got });
5208
5618
  }
@@ -5214,11 +5624,11 @@ function releases2(opts = {}) {
5214
5624
  }
5215
5625
 
5216
5626
  var LOCAL_DIR2 = "local";
5217
- var localRoot2 = (root) => join5(root, LOCAL_DIR2);
5627
+ var localRoot2 = (root) => join6(root, LOCAL_DIR2);
5218
5628
  var newerThan2 = (a, b) => a.cut > b.cut || a.cut === b.cut && a.fraction > b.fraction;
5219
5629
  function readManifest(dir) {
5220
- const p = join5(dir, "MANIFEST.json");
5221
- if (!existsSync6(p))
5630
+ const p = join6(dir, "MANIFEST.json");
5631
+ if (!existsSync7(p))
5222
5632
  return null;
5223
5633
  try {
5224
5634
  const v = JSON.parse(readFileSync5(p, "utf8"));
@@ -5240,8 +5650,8 @@ function incomplete2(dir) {
5240
5650
  const d = dbs[name];
5241
5651
  if (d === undefined)
5242
5652
  continue;
5243
- const p = join5(dir, d.file ?? `${name}.sqlite`);
5244
- if (!existsSync6(p))
5653
+ const p = join6(dir, d.file ?? `${name}.sqlite`);
5654
+ if (!existsSync7(p))
5245
5655
  return `no ${d.file ?? `${name}.sqlite`}`;
5246
5656
  if (d.bytes !== undefined && statSync4(p).size !== d.bytes)
5247
5657
  return `${d.file ?? `${name}.sqlite`} is ${statSync4(p).size} bytes, the manifest says ${d.bytes}`;
@@ -5274,18 +5684,18 @@ function pickBuild2(builds, base) {
5274
5684
  return best;
5275
5685
  }
5276
5686
  function buildTag2(p) {
5277
- 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;
5278
5688
  const date = at.toISOString().slice(0, 10).replaceAll("-", ".");
5279
5689
  return renderArchiveTag2({ date, cut: p.version.cut, fraction: p.version.fraction });
5280
5690
  }
5281
5691
  function buildFiles(dir) {
5282
- 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) => {
5283
5693
  const m = /^(.*)\.sqlite(\.zst)?$/.exec(n);
5284
5694
  return m === null || isDatabase(m[1]);
5285
5695
  }).sort();
5286
5696
  }
5287
5697
  function lacks(b, manifest, inputs) {
5288
- 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))));
5289
5699
  }
5290
5700
  function buildRemote(label) {
5291
5701
  const refuse = async () => {
@@ -5302,7 +5712,7 @@ async function openBuild2(root, p, opts) {
5302
5712
  const o = buildOptions(root, p.build.label, opts);
5303
5713
  const local = localRoot2(root);
5304
5714
  const tag = buildTag2(p);
5305
- const want = sha256Of(join5(p.build.dir, "MANIFEST.json"));
5715
+ const want = sha256Of(join6(p.build.dir, "MANIFEST.json"));
5306
5716
  const current = () => {
5307
5717
  const rel = openCached2(tag, o);
5308
5718
  return rel !== null && rel.receipt().files["MANIFEST.json"]?.sha256 === want ? rel : null;
@@ -5311,33 +5721,33 @@ async function openBuild2(root, p, opts) {
5311
5721
  if (have !== null)
5312
5722
  return have;
5313
5723
  mkdirSync5(local, { recursive: true });
5314
- const work = join5(local, scratchName("add"));
5724
+ const work = join6(local, scratchName("add"));
5315
5725
  try {
5316
- const stage = join5(work, "stage");
5726
+ const stage = join6(work, "stage");
5317
5727
  mkdirSync5(stage, { recursive: true });
5318
5728
  for (const f of buildFiles(p.build.dir)) {
5319
5729
  try {
5320
- linkSync2(join5(p.build.dir, f), join5(stage, f));
5730
+ linkSync2(join6(p.build.dir, f), join6(stage, f));
5321
5731
  } catch {
5322
- copyFileSync2(join5(p.build.dir, f), join5(stage, f));
5732
+ copyFileSync2(join6(p.build.dir, f), join6(stage, f));
5323
5733
  }
5324
5734
  }
5325
- 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);
5326
5736
  const receipt = added.receipt();
5327
5737
  receipt.source = `local:${p.build.dir}`;
5328
5738
  writeJsonAtomic2(receiptPath(added.dir), receipt);
5329
5739
  added.close();
5330
- const dest = join5(local, tag);
5331
- if (existsSync6(dest) && current() === null) {
5740
+ const dest = join6(local, tag);
5741
+ if (existsSync7(dest) && current() === null) {
5332
5742
  try {
5333
- renameSync3(dest, join5(local, scratchName("old")));
5743
+ renameSync3(dest, join6(local, scratchName("old")));
5334
5744
  } catch {}
5335
5745
  }
5336
5746
  try {
5337
5747
  renameSync3(added.dir, dest);
5338
5748
  } catch {}
5339
5749
  } finally {
5340
- rmSync5(work, { recursive: true, force: true });
5750
+ rmSync6(work, { recursive: true, force: true });
5341
5751
  }
5342
5752
  const rel = current();
5343
5753
  if (rel === null)
@@ -5354,7 +5764,7 @@ function alive(pid) {
5354
5764
  }
5355
5765
  function sweepBuilds2(root, keep) {
5356
5766
  const local = localRoot2(root);
5357
- if (!existsSync6(local))
5767
+ if (!existsSync7(local))
5358
5768
  return [];
5359
5769
  const gone = [];
5360
5770
  for (const name of readdirSync4(local).sort()) {
@@ -5365,23 +5775,23 @@ function sweepBuilds2(root, keep) {
5365
5775
  continue;
5366
5776
  if (m === null && parseArchiveTag2(name) === null)
5367
5777
  continue;
5368
- const path = join5(local, name);
5778
+ const path = join6(local, name);
5369
5779
  try {
5370
- rmSync5(path, { recursive: true, force: true });
5780
+ rmSync6(path, { recursive: true, force: true });
5371
5781
  gone.push(path);
5372
5782
  } catch {}
5373
5783
  }
5374
5784
  return gone;
5375
5785
  }
5376
5786
 
5377
- import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
5378
- import { join as join6 } from "path";
5787
+ import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
5788
+ import { join as join7 } from "path";
5379
5789
  var LATEST_CHECK_MS2 = 60 * 60 * 1000;
5380
5790
  var LATEST_CHECK_BUDGET_MS2 = 1500;
5381
- var latestCheckPath2 = (root) => join6(root, "LATEST.json");
5791
+ var latestCheckPath2 = (root) => join7(root, "LATEST.json");
5382
5792
  function readLatestCheck2(root) {
5383
5793
  const p = latestCheckPath2(root);
5384
- if (!existsSync7(p))
5794
+ if (!existsSync8(p))
5385
5795
  return null;
5386
5796
  try {
5387
5797
  const v = JSON.parse(readFileSync6(p, "utf8"));
@@ -5463,4 +5873,126 @@ function staleWarning2(answered, latest, floor, tags = [], command = "idq") {
5463
5873
  return `answered from ${answered}. ${floorTag ?? `cut ${floor.cut}`} is required${why}: run \`${command} use ${floor.cut}\` to get it`;
5464
5874
  }
5465
5875
 
5466
- 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 };