@run402/sdk 4.57.0 → 4.58.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.
@@ -761,6 +761,22 @@ export function createGitvaultHttpTransport(client, options = {}) {
761
761
  async function getObjectsBytes(repoId, paths) {
762
762
  if (paths.length === 0)
763
763
  return [];
764
+ const targets = await presignObjectBatch(repoId, paths);
765
+ if (!targets)
766
+ return paths.map(() => null);
767
+ return mapBounded(targets, GITVAULT_TRANSPORT_CONCURRENCY, async (target, i) => {
768
+ if (!target)
769
+ return null;
770
+ const r = await fetchGitvaultObjectBytes(client, target);
771
+ if (r.status === 404)
772
+ return null;
773
+ if (!r.ok)
774
+ fail("GITVAULT_OBJECT_READ_FAILED", `object GET failed (HTTP ${r.status}) for ${paths[i]}`, "reading gitvault object", { path: paths[i], status: r.status });
775
+ return new Uint8Array(await r.arrayBuffer());
776
+ });
777
+ }
778
+ /** The shared presign step of the batched read: ONE `object-reads` POST; `null` means the not-found shapes `getObjects` maps to an all-absent result. */
779
+ async function presignObjectBatch(repoId, paths) {
764
780
  const refs = paths.map((path) => {
765
781
  const ref = gitvaultWireRefForPath(path);
766
782
  if (!ref || ref.kind !== "object")
@@ -773,23 +789,67 @@ export function createGitvaultHttpTransport(client, options = {}) {
773
789
  }
774
790
  catch (e) {
775
791
  if (isRun402Error(e) && e.status === 404)
776
- return paths.map(() => null);
792
+ return null;
777
793
  if (isRun402Error(e) && e.code === "RESOURCE_NOT_FOUND")
778
- return paths.map(() => null);
794
+ return null;
779
795
  throw e;
780
796
  }
781
797
  const byLedgerId = new Map(presigned.reads.map((r) => [gitvaultLedgerId(r), r]));
782
- const targets = refs.map((r) => byLedgerId.get(gitvaultLedgerId(r)) ?? null);
783
- return mapBounded(targets, GITVAULT_TRANSPORT_CONCURRENCY, async (target, i) => {
784
- if (!target)
785
- return null;
786
- const r = await fetchGitvaultObjectBytes(client, target);
787
- if (r.status === 404)
788
- return null;
789
- if (!r.ok)
790
- fail("GITVAULT_OBJECT_READ_FAILED", `object GET failed (HTTP ${r.status}) for ${paths[i]}`, "reading gitvault object", { path: paths[i], status: r.status });
791
- return new Uint8Array(await r.arrayBuffer());
798
+ return refs.map((r) => byLedgerId.get(gitvaultLedgerId(r)) ?? null);
799
+ }
800
+ /**
801
+ * Per-object settlement over the same batch (gitvault-pipelined-restore
802
+ * D2): identical presign + bounded GETs, but each index's promise settles
803
+ * when ITS bytes land. Failure semantics per index match `getObjects`'s
804
+ * per-element behavior (absent → null, a failed GET → the same
805
+ * GITVAULT_OBJECT_READ_FAILED); every promise is pre-marked handled so an
806
+ * abandoned tail never becomes an unhandled rejection.
807
+ */
808
+ async function getObjectsSettledBytes(repoId, paths) {
809
+ if (paths.length === 0)
810
+ return [];
811
+ const targets = await presignObjectBatch(repoId, paths);
812
+ if (!targets)
813
+ return paths.map(() => Promise.resolve(null));
814
+ const deferreds = targets.map(() => {
815
+ let resolve;
816
+ let reject;
817
+ const promise = new Promise((res, rej) => {
818
+ resolve = res;
819
+ reject = rej;
820
+ });
821
+ void promise.catch(() => { });
822
+ return { promise, resolve, reject };
792
823
  });
824
+ let next = 0;
825
+ const worker = async () => {
826
+ for (;;) {
827
+ const i = next;
828
+ next += 1;
829
+ if (i >= targets.length)
830
+ return;
831
+ try {
832
+ const target = targets[i];
833
+ if (!target) {
834
+ deferreds[i].resolve(null);
835
+ continue;
836
+ }
837
+ const r = await fetchGitvaultObjectBytes(client, target);
838
+ if (r.status === 404) {
839
+ deferreds[i].resolve(null);
840
+ continue;
841
+ }
842
+ if (!r.ok)
843
+ fail("GITVAULT_OBJECT_READ_FAILED", `object GET failed (HTTP ${r.status}) for ${paths[i]}`, "reading gitvault object", { path: paths[i], status: r.status });
844
+ deferreds[i].resolve(new Uint8Array(await r.arrayBuffer()));
845
+ }
846
+ catch (e) {
847
+ deferreds[i].reject(e);
848
+ }
849
+ }
850
+ };
851
+ void Promise.all(Array.from({ length: Math.min(GITVAULT_TRANSPORT_CONCURRENCY, targets.length) }, () => worker()));
852
+ return deferreds.map((d) => d.promise);
793
853
  }
794
854
  /**
795
855
  * `POST …/head-reads` — many generation-addressed heads' EXACT stored bytes
@@ -973,6 +1033,7 @@ export function createGitvaultHttpTransport(client, options = {}) {
973
1033
  },
974
1034
  getObject: ({ repo_id, path }) => getObjectBytes(repo_id, path),
975
1035
  getObjects: ({ repo_id, paths }) => getObjectsBytes(repo_id, paths),
1036
+ getObjectsSettled: ({ repo_id, paths }) => getObjectsSettledBytes(repo_id, paths),
976
1037
  getHeads: ({ repo_id, generations }) => getHeadsBytes(repo_id, generations),
977
1038
  async admitGenesis(request) {
978
1039
  try {
@@ -3608,6 +3669,34 @@ export class GitvaultVault {
3608
3669
  return hexToBytes(hex);
3609
3670
  };
3610
3671
  const first = heads[0];
3672
+ // gitvault-pipelined-restore: per-object settlement when the transport
3673
+ // offers it, the `getObjects` barrier otherwise — pipelining is a
3674
+ // wall-clock property, never a correctness dependency, so a transport
3675
+ // without the method reproduces today's serial-after-barrier behavior
3676
+ // exactly (each per-index promise settles when the whole batch does).
3677
+ const settled = async (paths) => {
3678
+ if (this.transport.getObjectsSettled)
3679
+ return this.transport.getObjectsSettled({ repo_id: this.repoId, paths });
3680
+ const all = this.transport.getObjects({ repo_id: this.repoId, paths });
3681
+ const perIndex = paths.map((_, i) => all.then((frames) => frames[i] ?? null));
3682
+ // Mark every derived promise handled — the consumer awaits them in
3683
+ // order and stops at the first failure, abandoning the tail.
3684
+ for (const p of perIndex)
3685
+ void p.catch(() => { });
3686
+ return perIndex;
3687
+ };
3688
+ // gitvault-pipelined-restore D3: the WAL entry list derives from the
3689
+ // already-verified head walk, so its batched download is INITIATED here —
3690
+ // before the checkpoint branch — and its (small, many) objects land while
3691
+ // the checkpoint downloads, decrypts, and indexes. APPLICATION of WAL
3692
+ // packs still begins only after the checkpoint class completes, in the
3693
+ // same chain order as always. Each entry decrypts under its OWN carrying
3694
+ // head's epoch (D194) — the flattened list keeps that pairing so a
3695
+ // rotation-spanning restore never reuses one head's epoch for another's
3696
+ // pack.
3697
+ const walEntries = heads.flatMap((h) => h.wal_entries.map((w) => ({ w, epoch: h.epoch })));
3698
+ const walFramesP = settled(walEntries.map(({ w }) => gitvaultPaths.wal(w.object_id)));
3699
+ void walFramesP.catch(() => { });
3611
3700
  if (first.checkpoint) {
3612
3701
  const claimBytes = await this.transport.getObject({ repo_id: this.repoId, path: gitvaultPaths.claimSet(first.checkpoint.claim_set.object_id) });
3613
3702
  if (!claimBytes || sha256Hex(claimBytes) !== first.checkpoint.claim_set.stored_bytes_sha256)
@@ -3617,14 +3706,16 @@ export class GitvaultVault {
3617
3706
  fail("CHECKPOINT_INCOMPLETE", "claim set signature fails", "restoring gitvault objects");
3618
3707
  const manifest = await this.openCarrier("checkpoint_manifest", claimSet.manifest_receipt, gitvaultPaths.checkpointManifest(claimSet.manifest_receipt.object_id), writerKey, { epoch: first.epoch, k_repo: kRepoForEpoch(first.epoch) });
3619
3708
  checkClaimSetEquality(claimSet, manifest, first.checkpoint.covers_through_generation);
3620
- // Design D2: every checkpoint pack is independent — one batched
3621
- // presign for all of them, THEN applied via index-pack strictly in
3622
- // manifest order (the fetch is concurrent; the local git write is
3623
- // not, and does not need to be).
3624
- const frames = await this.transport.getObjects({ repo_id: this.repoId, paths: manifest.packs.map((p) => gitvaultPaths.checkpointPack(p.object_id)) });
3709
+ // Design D2 + gitvault-pipelined-restore D1: every checkpoint pack is
3710
+ // independent — one batched presign for all of them, applied via
3711
+ // index-pack strictly in manifest order, PIPELINED: apply(i) awaits
3712
+ // bytes(i), so decrypt/verify/index of an early pack overlaps the
3713
+ // later packs' downloads. Per-pack verification (AEAD open + plaintext
3714
+ // hash) still completes before any byte reaches git.
3715
+ const frames = await settled(manifest.packs.map((p) => gitvaultPaths.checkpointPack(p.object_id)));
3625
3716
  for (let i = 0; i < manifest.packs.length; i++) {
3626
3717
  const p = manifest.packs[i];
3627
- const frame = frames[i] ?? null;
3718
+ const frame = (await frames[i]) ?? null;
3628
3719
  if (!frame)
3629
3720
  fail("CHECKPOINT_INCOMPLETE", `checkpoint pack ${p.object_id} absent`, "restoring gitvault objects");
3630
3721
  const plain = openFrame({ k_obj: deriveObjectKey(kRepoForEpoch(first.epoch), this.repoId, first.epoch, "checkpoint_pack", p.object_id), repo_id: this.repoId, object_kind: "checkpoint_pack", object_id: p.object_id, epoch: first.epoch, frame, expected_ciphertext_sha256: p.ciphertext_sha256 });
@@ -3634,19 +3725,14 @@ export class GitvaultVault {
3634
3725
  }
3635
3726
  }
3636
3727
  // Design D2: every WAL pack across every head in this restore's range is
3637
- // independent — one batched presign for the whole set (this is the
3638
- // "restore pack set" the design's own D2 prose names alongside
3639
- // materialize's carriers), fetched concurrently, then applied via
3640
- // index-pack in the SAME chain order the wholesale path always used
3641
- // (git's pack application is sequential; the network fetch need not be).
3642
- // Each entry decrypts under its OWN carrying head's epoch (D194) — the
3643
- // flattened list keeps that pairing so a rotation-spanning restore never
3644
- // reuses one head's epoch for another's pack.
3645
- const walEntries = heads.flatMap((h) => h.wal_entries.map((w) => ({ w, epoch: h.epoch })));
3646
- const walFrames = await this.transport.getObjects({ repo_id: this.repoId, paths: walEntries.map(({ w }) => gitvaultPaths.wal(w.object_id)) });
3728
+ // independent — one batched presign for the whole set (initiated above,
3729
+ // before the checkpoint branch), applied via index-pack in the SAME
3730
+ // chain order the wholesale path always used, pipelined the same way:
3731
+ // apply(i) awaits bytes(i) while later packs finish downloading.
3732
+ const walFrames = await walFramesP;
3647
3733
  for (let i = 0; i < walEntries.length; i++) {
3648
3734
  const { w, epoch } = walEntries[i];
3649
- const frame = walFrames[i] ?? null;
3735
+ const frame = (await walFrames[i]) ?? null;
3650
3736
  if (!frame)
3651
3737
  fail("CHAIN_UNUSABLE", `WAL pack ${w.object_id} absent`, "restoring gitvault objects");
3652
3738
  const plain = openFrame({ k_obj: deriveObjectKey(kRepoForEpoch(epoch), this.repoId, epoch, "wal_pack", w.object_id), repo_id: this.repoId, object_kind: "wal_pack", object_id: w.object_id, epoch, frame, expected_ciphertext_sha256: w.ciphertext_sha256 });