@optimystic/db-p2p 0.24.2 → 0.25.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.
Files changed (249) hide show
  1. package/{readme.md → README.md} +688 -668
  2. package/dist/src/cluster/block-transfer-service.d.ts +133 -10
  3. package/dist/src/cluster/block-transfer-service.d.ts.map +1 -1
  4. package/dist/src/cluster/block-transfer-service.js +155 -9
  5. package/dist/src/cluster/block-transfer-service.js.map +1 -1
  6. package/dist/src/cluster/block-transfer.d.ts +67 -7
  7. package/dist/src/cluster/block-transfer.d.ts.map +1 -1
  8. package/dist/src/cluster/block-transfer.js +127 -24
  9. package/dist/src/cluster/block-transfer.js.map +1 -1
  10. package/dist/src/cluster/certified-claims.d.ts +170 -0
  11. package/dist/src/cluster/certified-claims.d.ts.map +1 -0
  12. package/dist/src/cluster/certified-claims.js +200 -0
  13. package/dist/src/cluster/certified-claims.js.map +1 -0
  14. package/dist/src/cluster/client-signature-verifier.d.ts +23 -0
  15. package/dist/src/cluster/client-signature-verifier.d.ts.map +1 -0
  16. package/dist/src/cluster/client-signature-verifier.js +33 -0
  17. package/dist/src/cluster/client-signature-verifier.js.map +1 -0
  18. package/dist/src/cluster/cluster-policy.d.ts +9 -3
  19. package/dist/src/cluster/cluster-policy.d.ts.map +1 -1
  20. package/dist/src/cluster/cluster-policy.js +111 -27
  21. package/dist/src/cluster/cluster-policy.js.map +1 -1
  22. package/dist/src/cluster/cluster-repo.d.ts +140 -11
  23. package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
  24. package/dist/src/cluster/cluster-repo.js +485 -29
  25. package/dist/src/cluster/cluster-repo.js.map +1 -1
  26. package/dist/src/cluster/commit-proof.d.ts +134 -0
  27. package/dist/src/cluster/commit-proof.d.ts.map +1 -0
  28. package/dist/src/cluster/commit-proof.js +263 -0
  29. package/dist/src/cluster/commit-proof.js.map +1 -0
  30. package/dist/src/cluster/quorum-restore.d.ts +95 -8
  31. package/dist/src/cluster/quorum-restore.d.ts.map +1 -1
  32. package/dist/src/cluster/quorum-restore.js +113 -14
  33. package/dist/src/cluster/quorum-restore.js.map +1 -1
  34. package/dist/src/cluster/rebalance-monitor.d.ts +84 -0
  35. package/dist/src/cluster/rebalance-monitor.d.ts.map +1 -1
  36. package/dist/src/cluster/rebalance-monitor.js +207 -5
  37. package/dist/src/cluster/rebalance-monitor.js.map +1 -1
  38. package/dist/src/cluster/reconcile-block.d.ts +27 -7
  39. package/dist/src/cluster/reconcile-block.d.ts.map +1 -1
  40. package/dist/src/cluster/reconcile-block.js +179 -38
  41. package/dist/src/cluster/reconcile-block.js.map +1 -1
  42. package/dist/src/cluster/service.d.ts +13 -4
  43. package/dist/src/cluster/service.d.ts.map +1 -1
  44. package/dist/src/cluster/service.js +17 -18
  45. package/dist/src/cluster/service.js.map +1 -1
  46. package/dist/src/cluster/spread-on-churn.d.ts +8 -2
  47. package/dist/src/cluster/spread-on-churn.d.ts.map +1 -1
  48. package/dist/src/cluster/spread-on-churn.js +7 -7
  49. package/dist/src/cluster/spread-on-churn.js.map +1 -1
  50. package/dist/src/cohort-topic/host.d.ts.map +1 -1
  51. package/dist/src/cohort-topic/host.js +8 -7
  52. package/dist/src/cohort-topic/host.js.map +1 -1
  53. package/dist/src/cohort-topic/stream-util.d.ts +7 -6
  54. package/dist/src/cohort-topic/stream-util.d.ts.map +1 -1
  55. package/dist/src/cohort-topic/stream-util.js +14 -41
  56. package/dist/src/cohort-topic/stream-util.js.map +1 -1
  57. package/dist/src/dispute/cascade.d.ts +7 -8
  58. package/dist/src/dispute/cascade.d.ts.map +1 -1
  59. package/dist/src/dispute/cascade.js +24 -7
  60. package/dist/src/dispute/cascade.js.map +1 -1
  61. package/dist/src/dispute/dispute-service.d.ts +5 -0
  62. package/dist/src/dispute/dispute-service.d.ts.map +1 -1
  63. package/dist/src/dispute/dispute-service.js +13 -3
  64. package/dist/src/dispute/dispute-service.js.map +1 -1
  65. package/dist/src/dispute/invalidation.d.ts +33 -15
  66. package/dist/src/dispute/invalidation.d.ts.map +1 -1
  67. package/dist/src/dispute/invalidation.js +127 -34
  68. package/dist/src/dispute/invalidation.js.map +1 -1
  69. package/dist/src/dispute/service.d.ts.map +1 -1
  70. package/dist/src/dispute/service.js +2 -1
  71. package/dist/src/dispute/service.js.map +1 -1
  72. package/dist/src/index.d.ts +7 -0
  73. package/dist/src/index.d.ts.map +1 -1
  74. package/dist/src/index.js +7 -0
  75. package/dist/src/index.js.map +1 -1
  76. package/dist/src/libp2p-key-network.d.ts +14 -10
  77. package/dist/src/libp2p-key-network.d.ts.map +1 -1
  78. package/dist/src/libp2p-key-network.js +28 -58
  79. package/dist/src/libp2p-key-network.js.map +1 -1
  80. package/dist/src/libp2p-node-base.d.ts +38 -7
  81. package/dist/src/libp2p-node-base.d.ts.map +1 -1
  82. package/dist/src/libp2p-node-base.js +148 -63
  83. package/dist/src/libp2p-node-base.js.map +1 -1
  84. package/dist/src/network/open-protocol-stream.d.ts +56 -0
  85. package/dist/src/network/open-protocol-stream.d.ts.map +1 -0
  86. package/dist/src/network/open-protocol-stream.js +60 -0
  87. package/dist/src/network/open-protocol-stream.js.map +1 -0
  88. package/dist/src/network/register-protocol-handler.d.ts +66 -0
  89. package/dist/src/network/register-protocol-handler.d.ts.map +1 -0
  90. package/dist/src/network/register-protocol-handler.js +51 -0
  91. package/dist/src/network/register-protocol-handler.js.map +1 -0
  92. package/dist/src/peer-address-book.d.ts +60 -2
  93. package/dist/src/peer-address-book.d.ts.map +1 -1
  94. package/dist/src/peer-address-book.js +89 -1
  95. package/dist/src/peer-address-book.js.map +1 -1
  96. package/dist/src/pend-validation.d.ts +65 -0
  97. package/dist/src/pend-validation.d.ts.map +1 -0
  98. package/dist/src/pend-validation.js +69 -0
  99. package/dist/src/pend-validation.js.map +1 -0
  100. package/dist/src/reactivity/notify-transport.d.ts.map +1 -1
  101. package/dist/src/reactivity/notify-transport.js +2 -1
  102. package/dist/src/reactivity/notify-transport.js.map +1 -1
  103. package/dist/src/reactivity/push-state-gossip.d.ts.map +1 -1
  104. package/dist/src/reactivity/push-state-gossip.js +2 -1
  105. package/dist/src/reactivity/push-state-gossip.js.map +1 -1
  106. package/dist/src/repo/cluster-coordinator.d.ts +32 -6
  107. package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
  108. package/dist/src/repo/cluster-coordinator.js +44 -10
  109. package/dist/src/repo/cluster-coordinator.js.map +1 -1
  110. package/dist/src/repo/coordinator-repo.d.ts +181 -25
  111. package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
  112. package/dist/src/repo/coordinator-repo.js +650 -62
  113. package/dist/src/repo/coordinator-repo.js.map +1 -1
  114. package/dist/src/repo/served-repo-proxy.d.ts +30 -0
  115. package/dist/src/repo/served-repo-proxy.d.ts.map +1 -0
  116. package/dist/src/repo/served-repo-proxy.js +50 -0
  117. package/dist/src/repo/served-repo-proxy.js.map +1 -0
  118. package/dist/src/repo/service.d.ts +16 -1
  119. package/dist/src/repo/service.d.ts.map +1 -1
  120. package/dist/src/repo/service.js +18 -16
  121. package/dist/src/repo/service.js.map +1 -1
  122. package/dist/src/reputation/types.d.ts +3 -1
  123. package/dist/src/reputation/types.d.ts.map +1 -1
  124. package/dist/src/reputation/types.js +3 -1
  125. package/dist/src/reputation/types.js.map +1 -1
  126. package/dist/src/rn.d.ts +24 -2
  127. package/dist/src/rn.d.ts.map +1 -1
  128. package/dist/src/rn.js +33 -4
  129. package/dist/src/rn.js.map +1 -1
  130. package/dist/src/storage/block-archive.d.ts +153 -0
  131. package/dist/src/storage/block-archive.d.ts.map +1 -0
  132. package/dist/src/storage/block-archive.js +194 -0
  133. package/dist/src/storage/block-archive.js.map +1 -0
  134. package/dist/src/storage/block-latch.d.ts +92 -0
  135. package/dist/src/storage/block-latch.d.ts.map +1 -0
  136. package/dist/src/storage/block-latch.js +132 -0
  137. package/dist/src/storage/block-latch.js.map +1 -0
  138. package/dist/src/storage/block-storage.d.ts +169 -12
  139. package/dist/src/storage/block-storage.d.ts.map +1 -1
  140. package/dist/src/storage/block-storage.js +415 -152
  141. package/dist/src/storage/block-storage.js.map +1 -1
  142. package/dist/src/storage/cached-raw-storage.d.ts +5 -1
  143. package/dist/src/storage/cached-raw-storage.d.ts.map +1 -1
  144. package/dist/src/storage/cached-raw-storage.js +16 -1
  145. package/dist/src/storage/cached-raw-storage.js.map +1 -1
  146. package/dist/src/storage/cached-store-driver.d.ts +14 -4
  147. package/dist/src/storage/cached-store-driver.d.ts.map +1 -1
  148. package/dist/src/storage/cached-store-driver.js +39 -5
  149. package/dist/src/storage/cached-store-driver.js.map +1 -1
  150. package/dist/src/storage/i-block-storage.d.ts +70 -19
  151. package/dist/src/storage/i-block-storage.d.ts.map +1 -1
  152. package/dist/src/storage/i-block-storage.js +21 -1
  153. package/dist/src/storage/i-block-storage.js.map +1 -1
  154. package/dist/src/storage/i-raw-storage.d.ts +38 -0
  155. package/dist/src/storage/i-raw-storage.d.ts.map +1 -1
  156. package/dist/src/storage/kv-raw-storage.d.ts +14 -8
  157. package/dist/src/storage/kv-raw-storage.d.ts.map +1 -1
  158. package/dist/src/storage/kv-raw-storage.js +29 -9
  159. package/dist/src/storage/kv-raw-storage.js.map +1 -1
  160. package/dist/src/storage/memory-store-driver.d.ts +4 -1
  161. package/dist/src/storage/memory-store-driver.d.ts.map +1 -1
  162. package/dist/src/storage/memory-store-driver.js +22 -2
  163. package/dist/src/storage/memory-store-driver.js.map +1 -1
  164. package/dist/src/storage/raw-store-codec.js +2 -2
  165. package/dist/src/storage/raw-store-driver.d.ts +44 -3
  166. package/dist/src/storage/raw-store-driver.d.ts.map +1 -1
  167. package/dist/src/storage/shared-cache-pool.d.ts +60 -3
  168. package/dist/src/storage/shared-cache-pool.d.ts.map +1 -1
  169. package/dist/src/storage/shared-cache-pool.js +68 -4
  170. package/dist/src/storage/shared-cache-pool.js.map +1 -1
  171. package/dist/src/storage/storage-repo.d.ts +204 -26
  172. package/dist/src/storage/storage-repo.d.ts.map +1 -1
  173. package/dist/src/storage/storage-repo.js +422 -119
  174. package/dist/src/storage/storage-repo.js.map +1 -1
  175. package/dist/src/storage/store-identity.d.ts +21 -0
  176. package/dist/src/storage/store-identity.d.ts.map +1 -0
  177. package/dist/src/storage/store-identity.js +33 -0
  178. package/dist/src/storage/store-identity.js.map +1 -0
  179. package/dist/src/storage/struct.d.ts +15 -0
  180. package/dist/src/storage/struct.d.ts.map +1 -1
  181. package/dist/src/storage/with-read-cache.d.ts +92 -0
  182. package/dist/src/storage/with-read-cache.d.ts.map +1 -0
  183. package/dist/src/storage/with-read-cache.js +132 -0
  184. package/dist/src/storage/with-read-cache.js.map +1 -0
  185. package/dist/src/sync/service.d.ts +13 -3
  186. package/dist/src/sync/service.d.ts.map +1 -1
  187. package/dist/src/sync/service.js +7 -30
  188. package/dist/src/sync/service.js.map +1 -1
  189. package/dist/src/testing/mesh-harness.d.ts +75 -2
  190. package/dist/src/testing/mesh-harness.d.ts.map +1 -1
  191. package/dist/src/testing/mesh-harness.js +193 -81
  192. package/dist/src/testing/mesh-harness.js.map +1 -1
  193. package/dist/src/testing/raw-storage-conformance.d.ts +8 -0
  194. package/dist/src/testing/raw-storage-conformance.d.ts.map +1 -1
  195. package/dist/src/testing/raw-storage-conformance.js +97 -2
  196. package/dist/src/testing/raw-storage-conformance.js.map +1 -1
  197. package/package.json +5 -6
  198. package/src/cluster/block-transfer-service.ts +528 -312
  199. package/src/cluster/block-transfer.ts +547 -424
  200. package/src/cluster/certified-claims.ts +319 -0
  201. package/src/cluster/client-signature-verifier.ts +34 -0
  202. package/src/cluster/cluster-policy.ts +294 -203
  203. package/src/cluster/cluster-repo.ts +2501 -2034
  204. package/src/cluster/commit-proof.ts +344 -0
  205. package/src/cluster/quorum-restore.ts +348 -223
  206. package/src/cluster/rebalance-monitor.ts +539 -254
  207. package/src/cluster/reconcile-block.ts +404 -203
  208. package/src/cluster/service.ts +23 -20
  209. package/src/cluster/spread-on-churn.ts +352 -346
  210. package/src/cohort-topic/host.ts +8 -7
  211. package/src/cohort-topic/stream-util.ts +14 -43
  212. package/src/dispute/cascade.ts +31 -15
  213. package/src/dispute/dispute-service.ts +706 -696
  214. package/src/dispute/invalidation.ts +162 -52
  215. package/src/dispute/service.ts +126 -125
  216. package/src/index.ts +7 -0
  217. package/src/libp2p-key-network.ts +1204 -1235
  218. package/src/libp2p-node-base.ts +185 -70
  219. package/src/network/open-protocol-stream.ts +87 -0
  220. package/src/network/register-protocol-handler.ts +85 -0
  221. package/src/peer-address-book.ts +96 -2
  222. package/src/pend-validation.ts +89 -0
  223. package/src/reactivity/notify-transport.ts +2 -1
  224. package/src/reactivity/push-state-gossip.ts +2 -1
  225. package/src/repo/cluster-coordinator.ts +65 -11
  226. package/src/repo/coordinator-repo.ts +729 -71
  227. package/src/repo/served-repo-proxy.ts +55 -0
  228. package/src/repo/service.ts +25 -16
  229. package/src/reputation/types.ts +3 -1
  230. package/src/rn.ts +34 -4
  231. package/src/storage/block-archive.ts +242 -0
  232. package/src/storage/block-latch.ts +144 -0
  233. package/src/storage/block-storage.ts +787 -498
  234. package/src/storage/cached-raw-storage.ts +21 -1
  235. package/src/storage/cached-store-driver.ts +898 -859
  236. package/src/storage/i-block-storage.ts +172 -116
  237. package/src/storage/i-raw-storage.ts +40 -0
  238. package/src/storage/kv-raw-storage.ts +34 -9
  239. package/src/storage/memory-store-driver.ts +26 -2
  240. package/src/storage/raw-store-codec.ts +42 -42
  241. package/src/storage/raw-store-driver.ts +45 -3
  242. package/src/storage/shared-cache-pool.ts +516 -452
  243. package/src/storage/storage-repo.ts +1421 -1014
  244. package/src/storage/store-identity.ts +41 -0
  245. package/src/storage/struct.ts +52 -34
  246. package/src/storage/with-read-cache.ts +182 -0
  247. package/src/sync/service.ts +178 -198
  248. package/src/testing/mesh-harness.ts +524 -344
  249. package/src/testing/raw-storage-conformance.ts +114 -2
@@ -0,0 +1,153 @@
1
+ import type { ActionRev, BlockId, IBlock, IRepo } from "@optimystic/db-core";
2
+ import type { BlockArchive } from "./struct.js";
3
+ import { type BlockCommitProof } from "../cluster/commit-proof.js";
4
+ /**
5
+ * The archive shape every block-repair path exchanges: ONE revision — the one being served —
6
+ * carrying its action, the block itself when the serving repo materialized it, and the cohort's
7
+ * commit proof for that revision when the serving repo retained one.
8
+ *
9
+ * One function rather than the shape re-typed at each site, because three sites had already drifted
10
+ * while each claimed to mirror the others: the sync service served `block: undefined` for a block
11
+ * with no materialized content, the mesh test harness served no archive at all for that same repo
12
+ * state (turning a corroborable revision claim into a phantom non-holder), and the reconcile unit
13
+ * spec's stand-in emitted a third shape again. `createReconcileBlock` reads
14
+ * `revisions[rev].action.actionId` and `revisions[rev].block`, and the difference between "absent
15
+ * block" and "absent archive" decides whether a peer votes in the revision quorum at all — so the
16
+ * shape is a contract, not a detail.
17
+ *
18
+ * `block` stays optional on purpose: a revision whose content the serving repo cannot materialize
19
+ * (a deleted block — see `GetBlockResult.block`) is still a revision that peer legitimately claims.
20
+ * It votes on `(rev, actionId)` and abstains from the content quorum, which is exactly the evidence
21
+ * it holds.
22
+ *
23
+ * `proof` is optional for the same kind of reason (see `ArchiveRevisions.proof`): a pre-proof
24
+ * revision, a diverged member, or an un-upgraded peer serves none, and every consumer must behave
25
+ * exactly as it did before proofs existed when it is absent.
26
+ *
27
+ * NOTE: the `range` emitted here is a COMPATIBILITY CONTRACT with `BlockStorage.vetRestoredArchive`,
28
+ * which refuses an archive whose declared range does not start at its lowest revision, end past its
29
+ * highest, or which is open-ended. `[rev, rev + 1]` around a single revision satisfies all three
30
+ * trivially, and this is currently the ONLY producer of an archive that crosses the restore wire —
31
+ * which is why no test drives a refusal end to end through `RestorationCoordinator`. If a second
32
+ * producer ever appears (a multi-revision or sparse archive, say), that contract stops being
33
+ * trivially true and needs a test pinning the two ends together.
34
+ */
35
+ export declare function singleRevisionArchive(blockId: BlockId, source: ActionRev, block: IBlock | undefined, proof?: BlockCommitProof): BlockArchive;
36
+ /**
37
+ * A peer's answer to the latest-revision consult: the `(rev, actionId)` it claims, plus the cohort's
38
+ * commit proof for that revision when it retained one.
39
+ *
40
+ * Defined HERE, alongside the archive shape, rather than beside the callback that returns it
41
+ * (`ClusterLatestCallback` in `repo/coordinator-repo.ts`, which re-exports this name): a remote
42
+ * peer's answer IS a projection of the archive it served, so keeping the two in one file is what
43
+ * stops the projection from drifting from the shape — the same reason `singleRevisionArchive` is a
44
+ * function rather than a shape re-typed at each site.
45
+ *
46
+ * The proof is OPTIONAL and means nothing until a consumer verifies it. A pre-proof revision, a
47
+ * diverged member, and an un-upgraded peer all legitimately answer without one, and a peer is free
48
+ * to attach whatever it likes — verification, never presence, is what makes a proof evidence.
49
+ */
50
+ export type CertifiedActionRev = ActionRev & {
51
+ proof?: BlockCommitProof;
52
+ };
53
+ /**
54
+ * Highest revision an archive covers, or `undefined` when it covers none.
55
+ *
56
+ * ONE implementation for every site that asks an untrusted archive this question — the two repair
57
+ * wires ({@link latestClaimFromArchive}) and the reconcile pass (`cluster/reconcile-block.ts`) — for
58
+ * the same reason {@link singleRevisionArchive} is one function: two copies is how one of them ends
59
+ * up without the guards below.
60
+ *
61
+ * Both guards matter on input a remote peer chose. `Object.keys` on a JSON-parsed archive yields
62
+ * strings, so a non-numeric key coerces to `NaN` and is skipped rather than poisoning the maximum.
63
+ * And the fold is deliberate rather than `Math.max(...keys)`: the spread passes one ARGUMENT per
64
+ * revision, which throws `RangeError: Maximum call stack size exceeded` past ~125k arguments —
65
+ * comfortably inside the 8 MiB `MAX_BLOCK_MESSAGE_BYTES` a sync response may carry (130k minimal
66
+ * revision entries serialize to ~6.3 MiB), so a peer could otherwise choose to make this throw.
67
+ */
68
+ export declare function maxArchiveRevision(revisions: BlockArchive['revisions'] | undefined): number | undefined;
69
+ /**
70
+ * The highest-revision claim an archive carries, or `undefined` when it holds no usable revision.
71
+ * `undefined` is the peer having ANSWERED without data — an absent claim, never silence (see
72
+ * `ClusterLatestCallback`'s three-way contract).
73
+ *
74
+ * The proof is read from the SAME revision entry as the `(rev, actionId)`, so a serving peer cannot
75
+ * pair a genuine proof with a revision it does not certify by choosing a different layout. Nothing
76
+ * here verifies anything: the result is the peer's unverified assertion until a caller checks it.
77
+ */
78
+ export declare function latestClaimFromArchive(archive: BlockArchive): CertifiedActionRev | undefined;
79
+ /**
80
+ * What {@link serveBlockArchive} needs of the repo it reads: `IRepo`, plus — OPTIONALLY — the
81
+ * revision-keyed commit-proof accessor `StorageRepo` implements.
82
+ *
83
+ * Optional rather than required so the unit-test doubles and any other plain `IRepo` that serves
84
+ * archives keep compiling and keep working (they simply serve no proof). A repo that CAN serve
85
+ * proofs and does not is not a type error here — it is the pre-proof behaviour, which stays valid.
86
+ */
87
+ export type ArchiveServingRepo = IRepo & {
88
+ getBlockProof?(blockId: BlockId, rev: number): Promise<BlockCommitProof | undefined>;
89
+ };
90
+ /**
91
+ * An {@link ArchiveServingRepo} that definitely CAN serve proofs — the accessor required rather
92
+ * than optional.
93
+ *
94
+ * The optionality above exists for a serving repo that legitimately has no proofs (a test double, a
95
+ * plain-`IRepo` embedder). It is the wrong default for a node's OWN store: forgetting the accessor
96
+ * there degrades every archive it serves to proof-less, silently and without a type error. Naming
97
+ * the stronger shape lets a composition root demand it (`createServedRepoProxy`).
98
+ */
99
+ export type ProofRetainingRepo = IRepo & Required<Pick<ArchiveServingRepo, 'getBlockProof'>>;
100
+ /**
101
+ * Serve `blockId` out of a local repo as a {@link singleRevisionArchive} — what a peer answers a
102
+ * block-repair fetch with. `undefined` when the repo holds no revision of the block at all, which
103
+ * callers report as "holds nothing" (`ReconcileBlockDeps.fetchArchive`'s contract folds
104
+ * "unreachable" into that same answer).
105
+ *
106
+ * The read skips the cluster deliberately: a peer answering a repair fetch reports what IT holds,
107
+ * and one that re-asked its own cohort would launder another peer's claim as its own.
108
+ *
109
+ * `rev` pins the read: `StorageRepo.get` materializes the highest committed revision of the block
110
+ * at or below it and reports that revision as `GetBlockResult.materialized`, which is what the
111
+ * archive is labelled with — its revision number, its action id, and its proof all belong to the
112
+ * bytes actually served. `state.latest` (the repo's NEWEST revision) is never the label: a caller
113
+ * asking for an older revision (`RestorationCoordinator` is the one that does) is answered with
114
+ * that revision as itself, or with nothing. Never with old bytes under a newer label — that pairing
115
+ * is what a receiver keyed by action id writes over its own good copy of the newer revision.
116
+ *
117
+ * The proof served is looked up for the revision ACTUALLY served, never for one chosen
118
+ * independently, so the archive can never publish a proof paired with a revision it does not
119
+ * certify. (A newer revision's proof over older bytes would pass `verifyBlockCommitProofClaim` and
120
+ * fail `verifyBlockCommitProofContent` — strictly worse than no proof.)
121
+ *
122
+ * NOTE: a single-revision archive's proof is a rounding error against the wire cap. A sync
123
+ * *response* is bounded by `MAX_BLOCK_MESSAGE_BYTES` (8 MiB — `SyncClient.requestBlock` sets the
124
+ * response decoder's `maxDataLength`; the 1 MiB `MAX_CONTROL_MESSAGE_BYTES` bounds the inbound
125
+ * REQUEST, not this). A proof's serialized size is dominated by two signatures plus a peer id per
126
+ * cohort member: a whole proof-carrying single-revision archive measured 4801 bytes at a 10-peer
127
+ * cohort and 8851 bytes at 20 (`test/block-archive-proof.spec.ts`, "far below the sync response
128
+ * cap", which prints both), i.e. ~405 bytes per additional peer. Reaching 8 MiB would take a cohort in the tens of thousands, so
129
+ * no plausible cluster size puts this near the cap — the block bytes the archive already carries
130
+ * are the term that matters. Revisit only if a proof ever grows a per-peer payload beyond its two
131
+ * signatures.
132
+ */
133
+ export declare function serveBlockArchive(repo: ArchiveServingRepo, blockId: BlockId, rev?: number): Promise<BlockArchive | undefined>;
134
+ /**
135
+ * The stored proof for the revision being served, or `undefined` — including whenever anything is
136
+ * off about it. Serving no proof is always safe (every consumer must already handle its absence),
137
+ * so this fails closed on all three unhappy paths:
138
+ *
139
+ * - the repo has no proof accessor at all (a plain `IRepo`, a test double);
140
+ * - the lookup throws — a storage fault must not turn a servable archive into "holds nothing",
141
+ * because that would recreate the phantom-non-holder bug the archive shape exists to prevent;
142
+ * - the stored proof's own message does not name this `(blockId, rev, actionId)`. That is a local
143
+ * storage-integrity fault, not a peer's doing, and publishing a mis-paired proof would hand
144
+ * every receiver an artifact that cannot verify. Logged loudly, because it means a proof was
145
+ * written under a key its content contradicts.
146
+ *
147
+ * Exported for the SAME reason {@link singleRevisionArchive} is: the mesh test harness answers the
148
+ * latest-revision consult out of a sibling's repo directly rather than over the sync protocol, and
149
+ * a harness that attached proofs by a different rule than a real peer would let every mesh-tier
150
+ * test silently exercise a path production does not have.
151
+ */
152
+ export declare function servableProof(repo: ArchiveServingRepo, blockId: BlockId, latest: ActionRev): Promise<BlockCommitProof | undefined>;
153
+ //# sourceMappingURL=block-archive.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"block-archive.d.ts","sourceRoot":"","sources":["../../../src/storage/block-archive.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAC5F,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAqB,KAAK,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAKtF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,qBAAqB,CACpC,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,KAAK,CAAC,EAAE,gBAAgB,GACtB,YAAY,CAYd;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,kBAAkB,GAAG,SAAS,GAAG;IAAE,KAAK,CAAC,EAAE,gBAAgB,CAAA;CAAE,CAAC;AAE1E;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,YAAY,CAAC,WAAW,CAAC,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAOvG;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,YAAY,GAAG,kBAAkB,GAAG,SAAS,CAU5F;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,kBAAkB,GAAG,KAAK,GAAG;IACxC,aAAa,CAAC,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC,CAAC;CACrF,CAAC;AAEF;;;;;;;;GAQG;AACH,MAAM,MAAM,kBAAkB,GAAG,KAAK,GAAG,QAAQ,CAAC,IAAI,CAAC,kBAAkB,EAAE,eAAe,CAAC,CAAC,CAAC;AAE7F;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,kBAAkB,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC,CA6BnI;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,aAAa,CAClC,IAAI,EAAE,kBAAkB,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,GAC3D,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC,CAgBvC"}
@@ -0,0 +1,194 @@
1
+ import { proofClaimsCommit } from "../cluster/commit-proof.js";
2
+ import { createLogger } from "../logger.js";
3
+ const log = createLogger('block-archive');
4
+ /**
5
+ * The archive shape every block-repair path exchanges: ONE revision — the one being served —
6
+ * carrying its action, the block itself when the serving repo materialized it, and the cohort's
7
+ * commit proof for that revision when the serving repo retained one.
8
+ *
9
+ * One function rather than the shape re-typed at each site, because three sites had already drifted
10
+ * while each claimed to mirror the others: the sync service served `block: undefined` for a block
11
+ * with no materialized content, the mesh test harness served no archive at all for that same repo
12
+ * state (turning a corroborable revision claim into a phantom non-holder), and the reconcile unit
13
+ * spec's stand-in emitted a third shape again. `createReconcileBlock` reads
14
+ * `revisions[rev].action.actionId` and `revisions[rev].block`, and the difference between "absent
15
+ * block" and "absent archive" decides whether a peer votes in the revision quorum at all — so the
16
+ * shape is a contract, not a detail.
17
+ *
18
+ * `block` stays optional on purpose: a revision whose content the serving repo cannot materialize
19
+ * (a deleted block — see `GetBlockResult.block`) is still a revision that peer legitimately claims.
20
+ * It votes on `(rev, actionId)` and abstains from the content quorum, which is exactly the evidence
21
+ * it holds.
22
+ *
23
+ * `proof` is optional for the same kind of reason (see `ArchiveRevisions.proof`): a pre-proof
24
+ * revision, a diverged member, or an un-upgraded peer serves none, and every consumer must behave
25
+ * exactly as it did before proofs existed when it is absent.
26
+ *
27
+ * NOTE: the `range` emitted here is a COMPATIBILITY CONTRACT with `BlockStorage.vetRestoredArchive`,
28
+ * which refuses an archive whose declared range does not start at its lowest revision, end past its
29
+ * highest, or which is open-ended. `[rev, rev + 1]` around a single revision satisfies all three
30
+ * trivially, and this is currently the ONLY producer of an archive that crosses the restore wire —
31
+ * which is why no test drives a refusal end to end through `RestorationCoordinator`. If a second
32
+ * producer ever appears (a multi-revision or sparse archive, say), that contract stops being
33
+ * trivially true and needs a test pinning the two ends together.
34
+ */
35
+ export function singleRevisionArchive(blockId, source, block, proof) {
36
+ return {
37
+ blockId,
38
+ revisions: {
39
+ [source.rev]: {
40
+ action: { actionId: source.actionId, transform: { insert: block } },
41
+ ...(block ? { block } : {}),
42
+ ...(proof ? { proof } : {})
43
+ }
44
+ },
45
+ range: [source.rev, source.rev + 1]
46
+ };
47
+ }
48
+ /**
49
+ * Highest revision an archive covers, or `undefined` when it covers none.
50
+ *
51
+ * ONE implementation for every site that asks an untrusted archive this question — the two repair
52
+ * wires ({@link latestClaimFromArchive}) and the reconcile pass (`cluster/reconcile-block.ts`) — for
53
+ * the same reason {@link singleRevisionArchive} is one function: two copies is how one of them ends
54
+ * up without the guards below.
55
+ *
56
+ * Both guards matter on input a remote peer chose. `Object.keys` on a JSON-parsed archive yields
57
+ * strings, so a non-numeric key coerces to `NaN` and is skipped rather than poisoning the maximum.
58
+ * And the fold is deliberate rather than `Math.max(...keys)`: the spread passes one ARGUMENT per
59
+ * revision, which throws `RangeError: Maximum call stack size exceeded` past ~125k arguments —
60
+ * comfortably inside the 8 MiB `MAX_BLOCK_MESSAGE_BYTES` a sync response may carry (130k minimal
61
+ * revision entries serialize to ~6.3 MiB), so a peer could otherwise choose to make this throw.
62
+ */
63
+ export function maxArchiveRevision(revisions) {
64
+ let max;
65
+ for (const key of Object.keys(revisions ?? {})) {
66
+ const rev = Number(key);
67
+ if (Number.isFinite(rev) && (max === undefined || rev > max))
68
+ max = rev;
69
+ }
70
+ return max;
71
+ }
72
+ /**
73
+ * The highest-revision claim an archive carries, or `undefined` when it holds no usable revision.
74
+ * `undefined` is the peer having ANSWERED without data — an absent claim, never silence (see
75
+ * `ClusterLatestCallback`'s three-way contract).
76
+ *
77
+ * The proof is read from the SAME revision entry as the `(rev, actionId)`, so a serving peer cannot
78
+ * pair a genuine proof with a revision it does not certify by choosing a different layout. Nothing
79
+ * here verifies anything: the result is the peer's unverified assertion until a caller checks it.
80
+ */
81
+ export function latestClaimFromArchive(archive) {
82
+ const maxRev = maxArchiveRevision(archive.revisions);
83
+ if (maxRev === undefined)
84
+ return undefined;
85
+ const entry = archive.revisions[maxRev];
86
+ if (!entry?.action)
87
+ return undefined;
88
+ return {
89
+ actionId: entry.action.actionId,
90
+ rev: maxRev,
91
+ ...(entry.proof ? { proof: entry.proof } : {})
92
+ };
93
+ }
94
+ /**
95
+ * Serve `blockId` out of a local repo as a {@link singleRevisionArchive} — what a peer answers a
96
+ * block-repair fetch with. `undefined` when the repo holds no revision of the block at all, which
97
+ * callers report as "holds nothing" (`ReconcileBlockDeps.fetchArchive`'s contract folds
98
+ * "unreachable" into that same answer).
99
+ *
100
+ * The read skips the cluster deliberately: a peer answering a repair fetch reports what IT holds,
101
+ * and one that re-asked its own cohort would launder another peer's claim as its own.
102
+ *
103
+ * `rev` pins the read: `StorageRepo.get` materializes the highest committed revision of the block
104
+ * at or below it and reports that revision as `GetBlockResult.materialized`, which is what the
105
+ * archive is labelled with — its revision number, its action id, and its proof all belong to the
106
+ * bytes actually served. `state.latest` (the repo's NEWEST revision) is never the label: a caller
107
+ * asking for an older revision (`RestorationCoordinator` is the one that does) is answered with
108
+ * that revision as itself, or with nothing. Never with old bytes under a newer label — that pairing
109
+ * is what a receiver keyed by action id writes over its own good copy of the newer revision.
110
+ *
111
+ * The proof served is looked up for the revision ACTUALLY served, never for one chosen
112
+ * independently, so the archive can never publish a proof paired with a revision it does not
113
+ * certify. (A newer revision's proof over older bytes would pass `verifyBlockCommitProofClaim` and
114
+ * fail `verifyBlockCommitProofContent` — strictly worse than no proof.)
115
+ *
116
+ * NOTE: a single-revision archive's proof is a rounding error against the wire cap. A sync
117
+ * *response* is bounded by `MAX_BLOCK_MESSAGE_BYTES` (8 MiB — `SyncClient.requestBlock` sets the
118
+ * response decoder's `maxDataLength`; the 1 MiB `MAX_CONTROL_MESSAGE_BYTES` bounds the inbound
119
+ * REQUEST, not this). A proof's serialized size is dominated by two signatures plus a peer id per
120
+ * cohort member: a whole proof-carrying single-revision archive measured 4801 bytes at a 10-peer
121
+ * cohort and 8851 bytes at 20 (`test/block-archive-proof.spec.ts`, "far below the sync response
122
+ * cap", which prints both), i.e. ~405 bytes per additional peer. Reaching 8 MiB would take a cohort in the tens of thousands, so
123
+ * no plausible cluster size puts this near the cap — the block bytes the archive already carries
124
+ * are the term that matters. Revisit only if a proof ever grows a per-peer payload beyond its two
125
+ * signatures.
126
+ */
127
+ export async function serveBlockArchive(repo, blockId, rev) {
128
+ // `ActionContext` is `{ committed, rev, actionId? }` — nothing else. `committed: []` claims no
129
+ // uncommitted-but-known action, and omitting `actionId` is what keeps this off `StorageRepo`'s
130
+ // pending-overlay path, so the read is purely "the highest COMMITTED revision at or below `rev`".
131
+ const context = rev !== undefined ? { rev, committed: [] } : undefined;
132
+ const result = await repo.get({ blockIds: [blockId], context }, { skipClusterFetch: true });
133
+ const entry = result[blockId];
134
+ const latest = entry?.state?.latest;
135
+ if (!latest)
136
+ return undefined;
137
+ // The revision the content in hand IS. A repo that reports `materialized` (`StorageRepo`) has
138
+ // served the highest committed revision at or below the pin, and that — never `state.latest`,
139
+ // the repo's NEWEST revision — is the archive's label. A repo that does not report it (a plain
140
+ // `IRepo`) can only be describing its latest.
141
+ const served = entry.materialized ?? latest;
142
+ // Fail closed rather than mislabel. A served revision ABOVE the pin is never a right answer:
143
+ // either the repo could not say what it materialized and its latest is newer than what was
144
+ // asked for (so the bytes may be pinned or may not — and the only label in hand is wrong for
145
+ // one of them), or the repo misreported. Labelling old bytes with a newer revision's number and
146
+ // action id is what the asker's `saveRestored` — keyed by action id — then writes over the good
147
+ // copy it already holds. Serve nothing instead: every caller already handles "holds nothing",
148
+ // and `restoreRevision` turns it into a loud "not found during restore attempt". A served
149
+ // revision AT OR BELOW the pin is the block unchanged since the pin, served exactly as before.
150
+ if (rev !== undefined && served.rev > rev) {
151
+ log('serve:skip blockId=%s served=%d requested=%d latest=%d (pinned read — refusing to mislabel content)', blockId, served.rev, rev, latest.rev);
152
+ return undefined;
153
+ }
154
+ const proof = await servableProof(repo, blockId, served);
155
+ return singleRevisionArchive(blockId, served, entry.block, proof);
156
+ }
157
+ /**
158
+ * The stored proof for the revision being served, or `undefined` — including whenever anything is
159
+ * off about it. Serving no proof is always safe (every consumer must already handle its absence),
160
+ * so this fails closed on all three unhappy paths:
161
+ *
162
+ * - the repo has no proof accessor at all (a plain `IRepo`, a test double);
163
+ * - the lookup throws — a storage fault must not turn a servable archive into "holds nothing",
164
+ * because that would recreate the phantom-non-holder bug the archive shape exists to prevent;
165
+ * - the stored proof's own message does not name this `(blockId, rev, actionId)`. That is a local
166
+ * storage-integrity fault, not a peer's doing, and publishing a mis-paired proof would hand
167
+ * every receiver an artifact that cannot verify. Logged loudly, because it means a proof was
168
+ * written under a key its content contradicts.
169
+ *
170
+ * Exported for the SAME reason {@link singleRevisionArchive} is: the mesh test harness answers the
171
+ * latest-revision consult out of a sibling's repo directly rather than over the sync protocol, and
172
+ * a harness that attached proofs by a different rule than a real peer would let every mesh-tier
173
+ * test silently exercise a path production does not have.
174
+ */
175
+ export async function servableProof(repo, blockId, latest) {
176
+ if (typeof repo.getBlockProof !== 'function')
177
+ return undefined;
178
+ let proof;
179
+ try {
180
+ proof = await repo.getBlockProof(blockId, latest.rev);
181
+ }
182
+ catch (error) {
183
+ log('serve:proof-lookup-failed blockId=%s rev=%d error=%s', blockId, latest.rev, error instanceof Error ? error.message : String(error));
184
+ return undefined;
185
+ }
186
+ if (!proof)
187
+ return undefined;
188
+ if (!proofClaimsCommit(proof, { blockId, rev: latest.rev, actionId: latest.actionId })) {
189
+ log('serve:proof-claim-mismatch blockId=%s rev=%d actionId=%s', blockId, latest.rev, latest.actionId);
190
+ return undefined;
191
+ }
192
+ return proof;
193
+ }
194
+ //# sourceMappingURL=block-archive.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"block-archive.js","sourceRoot":"","sources":["../../../src/storage/block-archive.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,92 @@
1
+ import type { BlockId } from "@optimystic/db-core";
2
+ /**
3
+ * The ONE per-block write lock.
4
+ *
5
+ * A block's metadata is stored as a single blob — `{ latest, ranges }` — read and written whole, so
6
+ * any read-modify-write of it overwrites `latest` whether it meant to or not. The invariant is
7
+ * therefore stated over the whole blob, not over `latest`:
8
+ *
9
+ * > A block's metadata, revision records, action transforms, pending records, and stored proofs are
10
+ * > only ever written while holding {@link blockWriteLatchKey}`(blockId)`.
11
+ *
12
+ * This module is the single acquirer of that key. The check, which deliberately matches the call
13
+ * shape (the escapes keep this very comment from matching):
14
+ *
15
+ * > grep -rnE "Latches\.acquire\(" packages/db-p2p/src
16
+ *
17
+ * That must return exactly one line — the call in `acquireBlockWriteLatch` below. A second hit
18
+ * anywhere means a caller has started taking the key directly and the token discipline has a hole.
19
+ * Every writing method on `IBlockStorage` demands a
20
+ * {@link BlockWriteLatch} token, which only this module can mint, so an unlatched write does not
21
+ * type-check rather than merely being documented as forbidden.
22
+ *
23
+ * `Latches` is a plain FIFO promise-chain mutex — no owner tracking, no re-entrancy — so a holder
24
+ * must never call back into something that acquires the same block's key. The token is what lets a
25
+ * callee prove it is already inside the latch instead of re-acquiring.
26
+ */
27
+ export declare const blockWriteLatchKey: (blockId: BlockId) => string;
28
+ /**
29
+ * Opaque proof that the bearer is executing inside {@link blockWriteLatchKey}`(blockId)`. Only
30
+ * {@link acquireBlockWriteLatch} (and {@link withBlockWriteLatch} through it) can construct one:
31
+ * the constructor is private and the module-scoped minter is assigned from a static block, where
32
+ * the private constructor is callable — no cast, nothing outside this module can build a token.
33
+ *
34
+ * A token is only valid while the latch it proves is actually held: releasing expires it, so a
35
+ * callback that stashes its token and writes after its scope closed is rejected instead of silently
36
+ * writing unlatched. `live` is the check; only this module can clear it.
37
+ *
38
+ * `BlockStorage` checks `latch.blockId` against its own id on every write, so a token for one block
39
+ * cannot be presented for another.
40
+ */
41
+ export declare class BlockWriteLatch {
42
+ #private;
43
+ readonly blockId: BlockId;
44
+ private constructor();
45
+ /** False once the latch this token proves has been released. */
46
+ get live(): boolean;
47
+ }
48
+ /**
49
+ * Acquire the write latch for `blockId`. The non-scoped single-block form is the building block for
50
+ * {@link acquireBlockWriteLatches} and for a caller that must hold one latch across control flow a
51
+ * callback cannot express; everything else should prefer {@link withBlockWriteLatch}. A caller that
52
+ * wants N latches must go through {@link acquireBlockWriteLatches}, not loop over this. The caller
53
+ * MUST call `release` exactly once, in a `finally`. Releasing expires the token, so a write attempted
54
+ * with it afterwards is rejected rather than running outside the latch.
55
+ */
56
+ export declare function acquireBlockWriteLatch(blockId: BlockId): Promise<{
57
+ latch: BlockWriteLatch;
58
+ release: () => void;
59
+ }>;
60
+ /** The N-latch hold {@link acquireBlockWriteLatches} returns: a token per block, and one release. */
61
+ export type BlockWriteLatches = {
62
+ /** Token per requested block id, so each write can prove it runs inside that block's latch. */
63
+ readonly latches: ReadonlyMap<BlockId, BlockWriteLatch>;
64
+ /** Releases every latch, in reverse acquisition order. Call exactly once, in a `finally`. */
65
+ readonly release: () => void;
66
+ };
67
+ /**
68
+ * Acquire the write latches for a whole set of blocks at once — the only sanctioned way to hold more
69
+ * than one. Two callers need it (`StorageRepo.commit` and `applyInvalidation`), and the property that
70
+ * keeps them from deadlocking against each other is not local to either: it is that EVERY multi-latch
71
+ * holder acquires in the one global order. Owning that here makes it a property of the module that
72
+ * owns the key rather than a rule each call site restates and a third one could get wrong.
73
+ *
74
+ * Three things this does that a hand-rolled loop keeps getting wrong:
75
+ * - **Dedups** the ids. `Latches` is a plain FIFO mutex with no re-entrancy, so a repeated id in the
76
+ * request would deadlock the acquirer against itself.
77
+ * - **Sorts** them, so any two multi-latch holders acquire in the same order and no cycle exists.
78
+ * - **Releases what it already took** if an acquisition partway through the set throws, rather than
79
+ * stranding those latches forever.
80
+ *
81
+ * The returned map is keyed by the ids actually held, so a caller that wants its own (e.g. request)
82
+ * ordering can iterate its own list and look each token up.
83
+ */
84
+ export declare function acquireBlockWriteLatches(blockIds: Iterable<BlockId>): Promise<BlockWriteLatches>;
85
+ /**
86
+ * Run `fn` while holding the write latch for `blockId`, handing it the token to pass down to the
87
+ * storage writes it makes. Acquire/release is per call, so a caller holds at most one block latch at
88
+ * a time and cannot deadlock against a sorted, up-front {@link acquireBlockWriteLatches} hold — as
89
+ * long as `fn` does not itself acquire another block's latch (nothing in this package does).
90
+ */
91
+ export declare function withBlockWriteLatch<T>(blockId: BlockId, fn: (latch: BlockWriteLatch) => Promise<T>): Promise<T>;
92
+ //# sourceMappingURL=block-latch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"block-latch.d.ts","sourceRoot":"","sources":["../../../src/storage/block-latch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAGnD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,kBAAkB,GAAI,SAAS,OAAO,KAAG,MAAkC,CAAC;AAKzF;;;;;;;;;;;;GAYG;AACH,qBAAa,eAAe;;IAGP,QAAQ,CAAC,OAAO,EAAE,OAAO;IAA7C,OAAO;IAEP,gEAAgE;IAChE,IAAI,IAAI,IAAI,OAAO,CAElB;CAMD;AAED;;;;;;;GAOG;AACH,wBAAsB,sBAAsB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC;IAAE,KAAK,EAAE,eAAe,CAAC;IAAC,OAAO,EAAE,MAAM,IAAI,CAAA;CAAE,CAAC,CAUvH;AAED,qGAAqG;AACrG,MAAM,MAAM,iBAAiB,GAAG;IAC/B,+FAA+F;IAC/F,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,OAAO,EAAE,eAAe,CAAC,CAAC;IACxD,6FAA6F;IAC7F,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAC;CAC7B,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,wBAAwB,CAAC,QAAQ,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAqBtG;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAOrH"}
@@ -0,0 +1,132 @@
1
+ import { Latches } from "@optimystic/db-core";
2
+ /**
3
+ * The ONE per-block write lock.
4
+ *
5
+ * A block's metadata is stored as a single blob — `{ latest, ranges }` — read and written whole, so
6
+ * any read-modify-write of it overwrites `latest` whether it meant to or not. The invariant is
7
+ * therefore stated over the whole blob, not over `latest`:
8
+ *
9
+ * > A block's metadata, revision records, action transforms, pending records, and stored proofs are
10
+ * > only ever written while holding {@link blockWriteLatchKey}`(blockId)`.
11
+ *
12
+ * This module is the single acquirer of that key. The check, which deliberately matches the call
13
+ * shape (the escapes keep this very comment from matching):
14
+ *
15
+ * > grep -rnE "Latches\.acquire\(" packages/db-p2p/src
16
+ *
17
+ * That must return exactly one line — the call in `acquireBlockWriteLatch` below. A second hit
18
+ * anywhere means a caller has started taking the key directly and the token discipline has a hole.
19
+ * Every writing method on `IBlockStorage` demands a
20
+ * {@link BlockWriteLatch} token, which only this module can mint, so an unlatched write does not
21
+ * type-check rather than merely being documented as forbidden.
22
+ *
23
+ * `Latches` is a plain FIFO promise-chain mutex — no owner tracking, no re-entrancy — so a holder
24
+ * must never call back into something that acquires the same block's key. The token is what lets a
25
+ * callee prove it is already inside the latch instead of re-acquiring.
26
+ */
27
+ export const blockWriteLatchKey = (blockId) => `Block.write:${blockId}`;
28
+ let mint;
29
+ let expire;
30
+ /**
31
+ * Opaque proof that the bearer is executing inside {@link blockWriteLatchKey}`(blockId)`. Only
32
+ * {@link acquireBlockWriteLatch} (and {@link withBlockWriteLatch} through it) can construct one:
33
+ * the constructor is private and the module-scoped minter is assigned from a static block, where
34
+ * the private constructor is callable — no cast, nothing outside this module can build a token.
35
+ *
36
+ * A token is only valid while the latch it proves is actually held: releasing expires it, so a
37
+ * callback that stashes its token and writes after its scope closed is rejected instead of silently
38
+ * writing unlatched. `live` is the check; only this module can clear it.
39
+ *
40
+ * `BlockStorage` checks `latch.blockId` against its own id on every write, so a token for one block
41
+ * cannot be presented for another.
42
+ */
43
+ export class BlockWriteLatch {
44
+ blockId;
45
+ #live = true;
46
+ constructor(blockId) {
47
+ this.blockId = blockId;
48
+ }
49
+ /** False once the latch this token proves has been released. */
50
+ get live() {
51
+ return this.#live;
52
+ }
53
+ static {
54
+ mint = (blockId) => new BlockWriteLatch(blockId);
55
+ expire = (latch) => { latch.#live = false; };
56
+ }
57
+ }
58
+ /**
59
+ * Acquire the write latch for `blockId`. The non-scoped single-block form is the building block for
60
+ * {@link acquireBlockWriteLatches} and for a caller that must hold one latch across control flow a
61
+ * callback cannot express; everything else should prefer {@link withBlockWriteLatch}. A caller that
62
+ * wants N latches must go through {@link acquireBlockWriteLatches}, not loop over this. The caller
63
+ * MUST call `release` exactly once, in a `finally`. Releasing expires the token, so a write attempted
64
+ * with it afterwards is rejected rather than running outside the latch.
65
+ */
66
+ export async function acquireBlockWriteLatch(blockId) {
67
+ const releaseLatch = await Latches.acquire(blockWriteLatchKey(blockId));
68
+ const latch = mint(blockId);
69
+ return {
70
+ latch,
71
+ release: () => {
72
+ expire(latch);
73
+ releaseLatch();
74
+ }
75
+ };
76
+ }
77
+ /**
78
+ * Acquire the write latches for a whole set of blocks at once — the only sanctioned way to hold more
79
+ * than one. Two callers need it (`StorageRepo.commit` and `applyInvalidation`), and the property that
80
+ * keeps them from deadlocking against each other is not local to either: it is that EVERY multi-latch
81
+ * holder acquires in the one global order. Owning that here makes it a property of the module that
82
+ * owns the key rather than a rule each call site restates and a third one could get wrong.
83
+ *
84
+ * Three things this does that a hand-rolled loop keeps getting wrong:
85
+ * - **Dedups** the ids. `Latches` is a plain FIFO mutex with no re-entrancy, so a repeated id in the
86
+ * request would deadlock the acquirer against itself.
87
+ * - **Sorts** them, so any two multi-latch holders acquire in the same order and no cycle exists.
88
+ * - **Releases what it already took** if an acquisition partway through the set throws, rather than
89
+ * stranding those latches forever.
90
+ *
91
+ * The returned map is keyed by the ids actually held, so a caller that wants its own (e.g. request)
92
+ * ordering can iterate its own list and look each token up.
93
+ */
94
+ export async function acquireBlockWriteLatches(blockIds) {
95
+ const ordered = Array.from(new Set(blockIds)).sort();
96
+ const releases = [];
97
+ const release = () => {
98
+ for (let i = releases.length - 1; i >= 0; i--) {
99
+ releases[i]();
100
+ }
101
+ releases.length = 0;
102
+ };
103
+ const latches = new Map();
104
+ try {
105
+ for (const id of ordered) {
106
+ const acquired = await acquireBlockWriteLatch(id);
107
+ releases.push(acquired.release);
108
+ latches.set(id, acquired.latch);
109
+ }
110
+ }
111
+ catch (err) {
112
+ release();
113
+ throw err;
114
+ }
115
+ return { latches, release };
116
+ }
117
+ /**
118
+ * Run `fn` while holding the write latch for `blockId`, handing it the token to pass down to the
119
+ * storage writes it makes. Acquire/release is per call, so a caller holds at most one block latch at
120
+ * a time and cannot deadlock against a sorted, up-front {@link acquireBlockWriteLatches} hold — as
121
+ * long as `fn` does not itself acquire another block's latch (nothing in this package does).
122
+ */
123
+ export async function withBlockWriteLatch(blockId, fn) {
124
+ const { latch, release } = await acquireBlockWriteLatch(blockId);
125
+ try {
126
+ return await fn(latch);
127
+ }
128
+ finally {
129
+ release();
130
+ }
131
+ }
132
+ //# sourceMappingURL=block-latch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"block-latch.js","sourceRoot":"","sources":["../../../src/storage/block-latch.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,OAAgB,EAAU,EAAE,CAAC,eAAe,OAAO,EAAE,CAAC;AAEzF,IAAI,IAA4C,CAAC;AACjD,IAAI,MAAyC,CAAC;AAE9C;;;;;;;;;;;;GAYG;AACH,MAAM,OAAO,eAAe;IAGE;IAF7B,KAAK,GAAG,IAAI,CAAC;IAEb,YAA6B,OAAgB;QAAhB,YAAO,GAAP,OAAO,CAAS;IAAI,CAAC;IAElD,gEAAgE;IAChE,IAAI,IAAI;QACP,OAAO,IAAI,CAAC,KAAK,CAAC;IACnB,CAAC;IAED;QACC,IAAI,GAAG,CAAC,OAAO,EAAE,EAAE,CAAC,IAAI,eAAe,CAAC,OAAO,CAAC,CAAC;QACjD,MAAM,GAAG,CAAC,KAAK,EAAE,EAAE,GAAG,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;IAC9C,CAAC;CACD;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAAC,OAAgB;IAC5D,MAAM,YAAY,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC,CAAC;IACxE,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC;IAC5B,OAAO;QACN,KAAK;QACL,OAAO,EAAE,GAAG,EAAE;YACb,MAAM,CAAC,KAAK,CAAC,CAAC;YACd,YAAY,EAAE,CAAC;QAChB,CAAC;KACD,CAAC;AACH,CAAC;AAUD;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAAC,QAA2B;IACzE,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IACrD,MAAM,QAAQ,GAAmB,EAAE,CAAC;IACpC,MAAM,OAAO,GAAG,GAAG,EAAE;QACpB,KAAK,IAAI,CAAC,GAAG,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;YAC/C,QAAQ,CAAC,CAAC,CAAE,EAAE,CAAC;QAChB,CAAC;QACD,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;IACrB,CAAC,CAAC;IACF,MAAM,OAAO,GAAG,IAAI,GAAG,EAA4B,CAAC;IACpD,IAAI,CAAC;QACJ,KAAK,MAAM,EAAE,IAAI,OAAO,EAAE,CAAC;YAC1B,MAAM,QAAQ,GAAG,MAAM,sBAAsB,CAAC,EAAE,CAAC,CAAC;YAClD,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;YAChC,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC;QACjC,CAAC;IACF,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACd,OAAO,EAAE,CAAC;QACV,MAAM,GAAG,CAAC;IACX,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;AAC7B,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAI,OAAgB,EAAE,EAA0C;IACxG,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,MAAM,sBAAsB,CAAC,OAAO,CAAC,CAAC;IACjE,IAAI,CAAC;QACJ,OAAO,MAAM,EAAE,CAAC,KAAK,CAAC,CAAC;IACxB,CAAC;YAAS,CAAC;QACV,OAAO,EAAE,CAAC;IACX,CAAC;AACF,CAAC"}