tensorfs 0.3.57__tar.gz → 0.3.58__tar.gz

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.
Files changed (150) hide show
  1. {tensorfs-0.3.57 → tensorfs-0.3.58}/Cargo.lock +2 -2
  2. {tensorfs-0.3.57 → tensorfs-0.3.58}/Cargo.toml +1 -1
  3. {tensorfs-0.3.57 → tensorfs-0.3.58}/PKG-INFO +1 -1
  4. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/providers.rs +50 -30
  5. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/transport/http.rs +63 -3
  6. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/transport/http_reuse_tests.rs +14 -12
  7. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/transport/ledger.rs +28 -1
  8. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/transport/mod.rs +1 -1
  9. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/transport/policy.rs +13 -0
  10. tensorfs-0.3.58/crates/tensorfs-core/src/transport/prefix.rs +1094 -0
  11. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/transport/pull.rs +16 -4
  12. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/transport/push.rs +19 -3
  13. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/transport/ranged.rs +40 -8
  14. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/transport/tests.rs +9 -4
  15. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/tests/source_artifacts.rs +2 -2
  16. tensorfs-0.3.58/crates/tensorfs-core/tests/source_download.rs +755 -0
  17. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/tests/source_prefixes.rs +4 -1
  18. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-py/src/store.rs +1 -1
  19. {tensorfs-0.3.57 → tensorfs-0.3.58}/pyproject.toml +1 -1
  20. tensorfs-0.3.57/crates/tensorfs-core/src/transport/prefix.rs +0 -505
  21. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/Cargo.toml +0 -0
  22. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/build.rs +0 -0
  23. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/examples/h3-swiglu-layout.md +0 -0
  24. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/examples/optional-cache-backfill.md +0 -0
  25. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/examples/resident-conversion.md +0 -0
  26. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/examples/resident_conversion_profile.rs +0 -0
  27. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/asset_update.rs +0 -0
  28. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/b64.rs +0 -0
  29. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/bin/tfs/ckpt.rs +0 -0
  30. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/bin/tfs/conform.rs +0 -0
  31. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/bin/tfs/fetch.rs +0 -0
  32. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/bin/tfs/ingest.rs +0 -0
  33. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/bin/tfs/main.rs +0 -0
  34. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/bin/tfs/rd.rs +0 -0
  35. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/bin/tfs/repo.rs +0 -0
  36. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/bin/tfs/snap.rs +0 -0
  37. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/bin/tfs/watch.rs +0 -0
  38. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/cache_roots.rs +0 -0
  39. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/canon.rs +0 -0
  40. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/capability.rs +0 -0
  41. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/catalog.rs +0 -0
  42. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/cbor.rs +0 -0
  43. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/checkpoint.rs +0 -0
  44. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/checkpoint_root.rs +0 -0
  45. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/corpus.rs +0 -0
  46. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/derived/adoption.rs +0 -0
  47. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/derived/companion_tests.rs +0 -0
  48. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/derived/estimate.rs +0 -0
  49. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/derived/graft_tests.rs +0 -0
  50. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/derived/progress.rs +0 -0
  51. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/derived/resume_tests.rs +0 -0
  52. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/derived/retention.rs +0 -0
  53. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/derived/roots.rs +0 -0
  54. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/derived.rs +0 -0
  55. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/dtype.rs +0 -0
  56. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/durability.rs +0 -0
  57. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/err.rs +0 -0
  58. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/fetch.rs +0 -0
  59. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/fill.rs +0 -0
  60. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/fit.rs +0 -0
  61. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/gc.rs +0 -0
  62. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/header.rs +0 -0
  63. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/home.rs +0 -0
  64. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/ids.rs +0 -0
  65. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/ingest/carrier.rs +0 -0
  66. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/ingest/convert.rs +0 -0
  67. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/ingest/fingerprint.rs +0 -0
  68. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/ingest/gguf.rs +0 -0
  69. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/ingest/h3_lora.rs +0 -0
  70. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/ingest/journal.rs +0 -0
  71. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/ingest/mod.rs +0 -0
  72. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/ingest/preflight.rs +0 -0
  73. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/ingest/source.rs +0 -0
  74. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/ingest/stamp.rs +0 -0
  75. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/ingest/transaction.rs +0 -0
  76. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/jcs.rs +0 -0
  77. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/lib.rs +0 -0
  78. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/limits.rs +0 -0
  79. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/machine.rs +0 -0
  80. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/manifest.rs +0 -0
  81. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/meta.rs +0 -0
  82. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/project.rs +0 -0
  83. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/read.rs +0 -0
  84. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/receipt.rs +0 -0
  85. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/reclaim.rs +0 -0
  86. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/registry.rs +0 -0
  87. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/relation.rs +0 -0
  88. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/repo_cache/identity.rs +0 -0
  89. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/repo_cache.rs +0 -0
  90. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/repository.rs +0 -0
  91. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/sha256.rs +0 -0
  92. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/source_artifact.rs +0 -0
  93. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/spec.rs +0 -0
  94. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/staging.rs +0 -0
  95. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/stats.rs +0 -0
  96. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/storage.rs +0 -0
  97. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/store.rs +0 -0
  98. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/transport/api.rs +0 -0
  99. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/transport/hub.rs +0 -0
  100. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/transport/speculate.rs +0 -0
  101. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/src/vectors.rs +0 -0
  102. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/tests/additive_fields.rs +0 -0
  103. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/tests/checkpoint_roots.rs +0 -0
  104. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/tests/fd_headroom.rs +0 -0
  105. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/tests/fetch_url_progress.rs +0 -0
  106. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/tests/fit.rs +0 -0
  107. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/tests/fixtures/repo_apply/blobs.jsonl +0 -0
  108. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_list.jsonl +0 -0
  109. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_repository.json +0 -0
  110. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_rows.jsonl +0 -0
  111. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/tests/fixtures/repo_apply/header.cbor +0 -0
  112. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/tests/fixtures/repo_apply/manifest.json +0 -0
  113. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/tests/fixtures/repo_apply/mutation.json +0 -0
  114. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/tests/foreign_json.rs +0 -0
  115. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/tests/h3_lora.rs +0 -0
  116. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/tests/h3_swiglu.rs +0 -0
  117. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/tests/home.rs +0 -0
  118. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/tests/import_tree.rs +0 -0
  119. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/tests/instrument_profiles.rs +0 -0
  120. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/tests/liveness.rs +0 -0
  121. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/tests/qwen_image21_profile.rs +0 -0
  122. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/tests/read_plan.rs +0 -0
  123. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/tests/reclaim.rs +0 -0
  124. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/tests/reclaim_run.rs +0 -0
  125. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/tests/repo_cache.rs +0 -0
  126. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/tests/repo_store.rs +0 -0
  127. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/tests/resident_admission.rs +0 -0
  128. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/tests/sha256_backends.rs +0 -0
  129. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/tests/sharded_cas.rs +0 -0
  130. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/tests/source_plan.rs +0 -0
  131. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/tests/source_preflight.rs +0 -0
  132. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/tests/staging.rs +0 -0
  133. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/tests/store_boundaries.rs +0 -0
  134. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/tests/tensor_schema.rs +0 -0
  135. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/tests/transfer_memory.rs +0 -0
  136. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/tests/trust_index.rs +0 -0
  137. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/tests/version.rs +0 -0
  138. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-core/tests/write_through.rs +0 -0
  139. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-py/Cargo.toml +0 -0
  140. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-py/src/docs.rs +0 -0
  141. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-py/src/errors.rs +0 -0
  142. {tensorfs-0.3.57 → tensorfs-0.3.58}/crates/tensorfs-py/src/lib.rs +0 -0
  143. {tensorfs-0.3.57 → tensorfs-0.3.58}/python/tensorfs/__init__.py +0 -0
  144. {tensorfs-0.3.57 → tensorfs-0.3.58}/python/tensorfs/_derived_channel.py +0 -0
  145. {tensorfs-0.3.57 → tensorfs-0.3.58}/python/tensorfs/_ext.pyi +0 -0
  146. {tensorfs-0.3.57 → tensorfs-0.3.58}/python/tensorfs/_source_channel.py +0 -0
  147. {tensorfs-0.3.57 → tensorfs-0.3.58}/python/tensorfs/_tensors.py +0 -0
  148. {tensorfs-0.3.57 → tensorfs-0.3.58}/python/tensorfs/derived.py +0 -0
  149. {tensorfs-0.3.57 → tensorfs-0.3.58}/python/tensorfs/errors.py +0 -0
  150. {tensorfs-0.3.57 → tensorfs-0.3.58}/python/tensorfs/py.typed +0 -0
@@ -439,7 +439,7 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
439
439
 
440
440
  [[package]]
441
441
  name = "tensorfs-core"
442
- version = "0.3.57"
442
+ version = "0.3.58"
443
443
  dependencies = [
444
444
  "fs2",
445
445
  "libc",
@@ -454,7 +454,7 @@ dependencies = [
454
454
 
455
455
  [[package]]
456
456
  name = "tensorfs-py"
457
- version = "0.3.57"
457
+ version = "0.3.58"
458
458
  dependencies = [
459
459
  "pyo3",
460
460
  "tensorfs-core",
@@ -3,5 +3,5 @@ resolver = "2"
3
3
  members = ["crates/tensorfs-core", "crates/tensorfs-py"]
4
4
 
5
5
  [workspace.package]
6
- version = "0.3.57"
6
+ version = "0.3.58"
7
7
  edition = "2021"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tensorfs
3
- Version: 0.3.57
3
+ Version: 0.3.58
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Rust
@@ -793,6 +793,7 @@ pub fn content_manifest(resolution: &Resolution) -> Result<crate::manifest::Mani
793
793
  }
794
794
 
795
795
  /// Materialize precisely an accepted selection; never re-list or broaden its roster.
796
+ /// `progress` hears (present bytes, total bytes) of the selection's distinct objects.
796
797
  #[allow(clippy::too_many_arguments)]
797
798
  pub fn materialize_selected(
798
799
  store: &Store,
@@ -801,7 +802,7 @@ pub fn materialize_selected(
801
802
  policy: &SourcePolicy,
802
803
  credentials: &dyn CredentialProvider,
803
804
  deadline: Deadline,
804
- progress: &mut dyn FnMut(&str, u64),
805
+ progress: crate::transport::SourceProgress<'_>,
805
806
  ) -> Result<(crate::source_artifact::TreeRoot, Vec<Pulled>)> {
806
807
  materialize_selected_with_options(
807
808
  store,
@@ -815,7 +816,7 @@ pub fn materialize_selected(
815
816
  )
816
817
  }
817
818
 
818
- /// Native development fault points and checkpoint sizing for source qualification.
819
+ /// Stream count, checkpoint sizing, cancellation and development fault points.
819
820
  #[allow(clippy::too_many_arguments)]
820
821
  pub fn materialize_selected_with_options(
821
822
  store: &Store,
@@ -824,7 +825,7 @@ pub fn materialize_selected_with_options(
824
825
  policy: &SourcePolicy,
825
826
  credentials: &dyn CredentialProvider,
826
827
  deadline: Deadline,
827
- progress: &mut dyn FnMut(&str, u64),
828
+ progress: crate::transport::SourceProgress<'_>,
828
829
  options: &crate::transport::SourceDownload,
829
830
  ) -> Result<(crate::source_artifact::TreeRoot, Vec<Pulled>)> {
830
831
  use crate::ids::Doc;
@@ -833,35 +834,54 @@ pub fn materialize_selected_with_options(
833
834
  let mut writer = crate::source_artifact::Writer::open(store, owner, tree.object_ref())?;
834
835
  let objects = resolution.objects();
835
836
  let (plan, _) = FetchPlan::of_objects(store, "selected-source", &objects)?;
836
- let ledger = Ledger::new();
837
- let mut pulled = vec![];
837
+ let mut ledger = Ledger::new();
838
+ if let Some(cancellation) = &options.cancellation {
839
+ ledger = ledger.with_cancellation(cancellation.clone());
840
+ }
841
+ // Every member's liveness is published before any of their bytes move.
842
+ let mut jobs: Vec<crate::transport::prefix::SourceJob> = vec![];
843
+ let (mut seen, mut held_bytes) = (std::collections::BTreeSet::new(), 0);
838
844
  for member in &resolution.members {
839
845
  writer.hold_expected(&member.object)?;
840
- let held = matches!(store.record_valid(&member.object.sha256), Ok(row) if row.length == member.object.length);
841
- let transferred = if held {
842
- 0
843
- } else {
844
- let grant = DeliveryGrant::mint(&plan, &member.object)?;
845
- crate::transport::prefix::fetch_source(
846
- store,
847
- owner,
848
- &grant,
849
- &member.url,
850
- policy,
851
- credentials,
852
- deadline,
853
- &ledger,
854
- options,
855
- Some(&mut |bytes| progress(&member.member, bytes)),
856
- )?
857
- .transferred
858
- };
846
+ let object = &member.object;
847
+ if !seen.insert(&object.sha256) {
848
+ continue;
849
+ }
850
+ if matches!(store.record_valid(&object.sha256), Ok(row) if row.length == object.length) {
851
+ held_bytes += object.length;
852
+ continue;
853
+ }
854
+ jobs.push(crate::transport::prefix::SourceJob {
855
+ grant: DeliveryGrant::mint(&plan, object)?,
856
+ url: member.url.clone(),
857
+ });
858
+ }
859
+ let transferred = crate::transport::prefix::fetch_sources(
860
+ store,
861
+ owner,
862
+ &jobs,
863
+ policy,
864
+ credentials,
865
+ deadline,
866
+ &ledger,
867
+ options,
868
+ held_bytes,
869
+ progress,
870
+ )?;
871
+ let mut pulled = vec![];
872
+ let mut counted = std::collections::BTreeSet::new();
873
+ for member in &resolution.members {
859
874
  writer.landed(&member.object)?;
875
+ let first = counted.insert(&member.object.sha256);
876
+ let fetched = jobs
877
+ .iter()
878
+ .position(|job| job.grant.object == member.object)
879
+ .filter(|_| first);
860
880
  pulled.push(Pulled {
861
881
  member: member.member.clone(),
862
882
  object: member.object.clone(),
863
- transferred,
864
- held,
883
+ transferred: fetched.map_or(0, |at| transferred[at]),
884
+ held: fetched.is_none(),
865
885
  });
866
886
  }
867
887
  plan.complete(store)?;
@@ -2475,7 +2495,7 @@ mod tests {
2475
2495
  &endpoints.policy(&uri),
2476
2496
  &Anonymous,
2477
2497
  Deadline::none(),
2478
- &mut |_, _| {},
2498
+ &|_, _| {},
2479
2499
  )
2480
2500
  .unwrap();
2481
2501
  assert_eq!(
@@ -2492,7 +2512,7 @@ mod tests {
2492
2512
  &endpoints.policy(&uri),
2493
2513
  &Anonymous,
2494
2514
  Deadline::none(),
2495
- &mut |_, _| {},
2515
+ &|_, _| {},
2496
2516
  )
2497
2517
  .unwrap();
2498
2518
  assert_eq!(warm, cold);
@@ -2646,7 +2666,7 @@ mod tests {
2646
2666
  &endpoints.policy(&uri),
2647
2667
  &Anonymous,
2648
2668
  Deadline::none(),
2649
- &mut |_, _| {},
2669
+ &|_, _| {},
2650
2670
  )
2651
2671
  .unwrap_err();
2652
2672
  assert_eq!(error.code, Code::OBJECT_ID_MISMATCH);
@@ -2698,7 +2718,7 @@ mod tests {
2698
2718
  &endpoints.policy(&uri),
2699
2719
  &Anonymous,
2700
2720
  Deadline::none(),
2701
- &mut |_, _| {},
2721
+ &|_, _| {},
2702
2722
  )
2703
2723
  .unwrap();
2704
2724
  assert!(retained.complete && !retained.released);
@@ -157,20 +157,21 @@ type IdlePool = Arc<Mutex<Vec<Idle>>>;
157
157
  // This bounds retained descriptors, not in-flight transfers (the caller owns those).
158
158
  const MAX_IDLE: usize = 8;
159
159
 
160
+ /// A connection belongs to its origin and the credentials sent on it, never to one
161
+ /// request target: a signed CDN URL changes per resolve, and keying on it meant the
162
+ /// delivery hop never reused a socket.
160
163
  #[derive(PartialEq, Eq)]
161
164
  struct Scope {
162
165
  https: bool,
163
166
  host: String,
164
167
  port: u16,
165
- // Do not retain signed targets or credentials in an idle connection's key.
168
+ // Do not retain credentials in an idle connection's key.
166
169
  request: [u8; 32],
167
170
  }
168
171
 
169
172
  impl Scope {
170
173
  fn ranged(checked: &CheckedUrl, headers: &[(String, String)]) -> Self {
171
174
  let mut hash = crate::sha256::Sha256::new();
172
- hash.update(&(checked.target.len() as u64).to_le_bytes());
173
- hash.update(checked.target.as_bytes());
174
175
  for (name, value) in headers
175
176
  .iter()
176
177
  .filter(|(name, _)| !name.eq_ignore_ascii_case("range"))
@@ -532,6 +533,34 @@ impl<'l> Response<'l> {
532
533
  self.body
533
534
  }
534
535
 
536
+ /// Drop a small non-artifact body (a redirect's, a refusal's) by reading it to its end,
537
+ /// so the connection can serve the next ask. Nothing read here is counted; a body
538
+ /// larger than a response head is simply left, and its connection closes.
539
+ pub fn discard(self) {
540
+ let mut body = self.body;
541
+ let mut buf = [0u8; 4096];
542
+ let mut left = HEAD_MAX_BYTES;
543
+ while left > 0 {
544
+ match body.read_framed(&mut buf[..left.min(4096)]) {
545
+ Ok(0) | Err(_) => return,
546
+ Ok(n) => left -= n,
547
+ }
548
+ }
549
+ }
550
+
551
+ /// The wait this answer states (`Retry-After`), read in the origin's own clock:
552
+ /// delta-seconds, or an HTTP-date against the answer's own `Date`. `None` when it
553
+ /// states nothing readable.
554
+ pub fn stated_wait(&self) -> Option<Duration> {
555
+ let value = self.header("retry-after")?.trim();
556
+ if let Ok(seconds) = value.parse::<u64>() {
557
+ return Some(Duration::from_secs(seconds));
558
+ }
559
+ let at = http_date(value)?;
560
+ let now = http_date(self.header("date")?)?;
561
+ Some(Duration::from_secs(at.saturating_sub(now)))
562
+ }
563
+
535
564
  /// The whole body, refused above `cap` rather than truncated.
536
565
  pub fn read_capped(mut self, cap: usize) -> Result<Vec<u8>> {
537
566
  let mut out = Vec::new();
@@ -552,6 +581,37 @@ impl<'l> Response<'l> {
552
581
  }
553
582
  }
554
583
 
584
+ /// RFC 9110 IMF-fixdate (`Sun, 06 Nov 1994 08:49:37 GMT`) as Unix seconds.
585
+ fn http_date(text: &str) -> Option<u64> {
586
+ const MONTHS: [&str; 12] = [
587
+ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
588
+ ];
589
+ let mut parts = text.split_whitespace().skip(1);
590
+ let day: i64 = parts.next()?.parse().ok()?;
591
+ let month = parts.next()?;
592
+ let month = MONTHS.iter().position(|name| *name == month)? as i64 + 1;
593
+ let year: i64 = parts.next()?.parse().ok()?;
594
+ let clock: Vec<i64> = parts
595
+ .next()?
596
+ .split(':')
597
+ .map(|part| part.parse().ok())
598
+ .collect::<Option<_>>()?;
599
+ if parts.next()? != "GMT" || clock.len() != 3 {
600
+ return None;
601
+ }
602
+ // Days from the civil calendar (Hinnant).
603
+ let (y, m) = if month <= 2 {
604
+ (year - 1, month + 9)
605
+ } else {
606
+ (year, month - 3)
607
+ };
608
+ let era = y.div_euclid(400);
609
+ let yoe = y - era * 400;
610
+ let doe = yoe * 365 + yoe / 4 - yoe / 100 + (153 * m + 2) / 5 + day - 1;
611
+ let days = era * 146_097 + doe - 719_468;
612
+ u64::try_from(days * 86_400 + clock[0] * 3600 + clock[1] * 60 + clock[2]).ok()
613
+ }
614
+
555
615
  #[derive(Clone, Copy)]
556
616
  enum Framing {
557
617
  Length { remaining: u64 },
@@ -243,7 +243,7 @@ fn complete_ranged_http_and_tls_bodies_reuse_one_connection() {
243
243
  }
244
244
 
245
245
  #[test]
246
- fn cached_peer_must_remain_checked_and_credentials_and_signed_target_stay_scoped() {
246
+ fn cached_peer_must_remain_checked_and_credentials_stay_scoped_but_signed_targets_share() {
247
247
  let origin = Origin::new(false, Duration::ZERO, |_, _| Reply::body(b"checked"));
248
248
  let client = origin.client();
249
249
  let checked = origin.checked("/object?signature=one");
@@ -260,8 +260,9 @@ fn cached_peer_must_remain_checked_and_credentials_and_signed_target_stay_scoped
260
260
  ask(&client, &checked, &headers).unwrap();
261
261
  headers[1].1 = "Bearer second".into();
262
262
  ask(&client, &checked, &headers).unwrap();
263
+ // A re-signed delivery URL on the same origin and credentials rides the same socket.
263
264
  ask(&client, &origin.checked("/object?signature=two"), &headers).unwrap();
264
- assert_eq!(origin.count(), 4);
265
+ assert_eq!(origin.count(), 3);
265
266
  let calls = origin.asks.lock().unwrap();
266
267
  assert_eq!(
267
268
  calls
@@ -357,12 +358,9 @@ fn idle_descriptor_retention_is_bounded_and_non_range_gets_do_not_pool() {
357
358
  let origin = Origin::new(false, Duration::ZERO, |_, _| Reply::body(b"bytes"));
358
359
  let client = origin.client();
359
360
  for index in 0..32 {
360
- ask(
361
- &client,
362
- &origin.checked(&format!("/object/{index}")),
363
- &ranged(),
364
- )
365
- .unwrap();
361
+ let mut headers = ranged();
362
+ headers.push(("authorization".into(), format!("Bearer {index}")));
363
+ ask(&client, &origin.checked("/object"), &headers).unwrap();
366
364
  }
367
365
  assert_eq!(client.idle.lock().unwrap().len(), MAX_IDLE);
368
366
  assert_eq!(origin.count(), 32);
@@ -583,21 +581,25 @@ fn retained_source_redirect_ranges_reduce_connections_without_changing_asks_or_b
583
581
  &Origin::policy(),
584
582
  credentials,
585
583
  Deadline::none(),
586
- &mut |_, _| {},
584
+ &|_, _| {},
587
585
  &SourceDownload {
588
586
  checkpoint_bytes: 1 << 20,
587
+ streams: 1,
589
588
  fault: Fault::default(),
589
+ ..Default::default()
590
590
  },
591
591
  )
592
592
  .unwrap();
593
593
  let elapsed = start.elapsed();
594
594
  assert_eq!(rows[0].transferred, bytes.len() as u64);
595
595
  assert!(store.record_valid(&object.sha256).is_ok());
596
- assert_eq!(origin.asks.lock().unwrap().len(), 16);
597
- assert_eq!(origin.count(), if close { 16 } else { 2 });
596
+ // One ask walks the redirect, one GET carries all eight checkpoints of the run,
597
+ // and the drained redirect's socket serves the delivery hop.
598
+ assert_eq!(origin.asks.lock().unwrap().len(), 2);
599
+ assert_eq!(origin.count(), if close { 2 } else { 1 });
598
600
  timings.push((close, elapsed.as_secs_f64(), origin.count()));
599
601
  drop(store);
600
602
  std::fs::remove_dir_all(root).unwrap();
601
603
  }
602
- println!("retained source 8MiB, eight 1MiB ranges, one redirect/range, 30ms connection setup: {timings:?}");
604
+ println!("retained source 8MiB, one redirected ask, eight 1MiB checkpoints, 30ms connection setup: {timings:?}");
603
605
  }
@@ -135,7 +135,7 @@ pub struct Still {
135
135
 
136
136
  /// One caller's cooperative stop signal. Clones name the same pull intent;
137
137
  /// a resumed operation uses a new token and retains existing Store bytes.
138
- #[derive(Clone, Default)]
138
+ #[derive(Clone, Debug, Default)]
139
139
  pub struct PullCancellation(Arc<AtomicBool>);
140
140
 
141
141
  impl PullCancellation {
@@ -357,6 +357,33 @@ impl Ledger {
357
357
  worst.mul_f64(STALL_FACTOR).max(self.patience())
358
358
  }
359
359
 
360
+ /// What a retryable answer costs before the re-ask: the origin's own stated wait,
361
+ /// which is not a failed attempt, or — when it states none — this pull's patience,
362
+ /// which is (`true`). The hub's rule, applied to every origin.
363
+ pub(crate) fn retry_wait(&self, stated: Option<Duration>) -> (Duration, bool) {
364
+ match stated.filter(|wait| !wait.is_zero()) {
365
+ Some(wait) => (wait, false),
366
+ None => (self.patience(), true),
367
+ }
368
+ }
369
+
370
+ /// Sleep `wait` in sampling ticks; cancellation and the caller's deadline end it.
371
+ pub(crate) fn pause(&self, wait: Duration, deadline: Deadline) -> Result<()> {
372
+ let until = Instant::now() + wait;
373
+ loop {
374
+ self.check_running()?;
375
+ let now = Instant::now();
376
+ if now >= until {
377
+ return Ok(());
378
+ }
379
+ let mut tick = (until - now).min(self.sample);
380
+ if let Some(left) = deadline.remaining()? {
381
+ tick = tick.min(left);
382
+ }
383
+ std::thread::sleep(tick);
384
+ }
385
+ }
386
+
360
387
  /// What this pull has moved and how long it has been still.
361
388
  pub fn still(&self) -> Still {
362
389
  let progress = self.progress.lock().unwrap();
@@ -63,4 +63,4 @@ pub use pull::{
63
63
  pub use push::{push_object, Pushed, UploadGrant, MAX_GRANT_BYTES, PUT_ATTEMPTS};
64
64
  pub use ranged::{fetch_ranged, Ranged, PART_BYTES, RANGED_MIN_BYTES, SOURCE_STREAMS};
65
65
 
66
- pub use prefix::SourceDownload;
66
+ pub use prefix::{SourceDownload, SourceProgress};
@@ -145,6 +145,19 @@ pub struct CheckedUrl {
145
145
  pub addrs: Vec<SocketAddr>,
146
146
  }
147
147
 
148
+ impl CheckedUrl {
149
+ /// The URL this check admitted, as a string a later ask can start from.
150
+ pub fn url(&self) -> String {
151
+ let scheme = if self.https { "https" } else { "http" };
152
+ let host = if self.host.contains(':') {
153
+ format!("[{}]", self.host)
154
+ } else {
155
+ self.host.clone()
156
+ };
157
+ format!("{scheme}://{host}:{}{}", self.port, self.target)
158
+ }
159
+ }
160
+
148
161
  /// The deployment's declared egress bound. Carried by every caller that hands this module
149
162
  /// a URL; enforced on the first URL and on every redirect hop, because the hops are only
150
163
  /// visible here.