@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,41 @@
1
+ /**
2
+ * A stable, process-scoped name for what a store is backed by — a resolved directory, an
3
+ * open database handle, and so on.
4
+ *
5
+ * Compared for EQUALITY ONLY. Never parsed, never split on its scheme prefix, and never used
6
+ * as a cache key for values — it names the store, not its contents.
7
+ */
8
+ export type StoreIdentity = string;
9
+
10
+ /**
11
+ * Per-scheme tag maps. Weak, so tagging a handle never keeps it alive: once the handle is
12
+ * unreachable its entry goes with it.
13
+ */
14
+ const handleTags = new Map<string, WeakMap<object, StoreIdentity>>();
15
+ let nextHandleOrdinal = 0;
16
+
17
+ /**
18
+ * A stable identity for a backend reachable only as an already-open handle object (a SQLite
19
+ * db, an IndexedDB handle, a LevelDB instance). Returns the same string for the same object
20
+ * for the life of the process, and a different one for every other object.
21
+ *
22
+ * `scheme` prefixes the result so identities from different backends can never collide.
23
+ *
24
+ * Object identity is the whole contract here: two handles opened over the SAME underlying
25
+ * file/name/path read as two DIFFERENT identities. That is a deliberate under-approximation —
26
+ * see the `NOTE:` at each handle-based driver for why it is the reachable case in practice.
27
+ */
28
+ export function identityForHandle(scheme: string, handle: object): StoreIdentity {
29
+ let tags = handleTags.get(scheme);
30
+ if (!tags) {
31
+ tags = new WeakMap<object, StoreIdentity>();
32
+ handleTags.set(scheme, tags);
33
+ }
34
+ const existing = tags.get(handle);
35
+ if (existing !== undefined) return existing;
36
+ // The ordinal is global rather than per-scheme so the same handle under two schemes gets
37
+ // two visibly different suffixes as well as two different prefixes.
38
+ const identity = `${scheme}:${nextHandleOrdinal++}`;
39
+ tags.set(handle, identity);
40
+ return identity;
41
+ }
@@ -1,34 +1,52 @@
1
- import type { BlockId, IBlock, ActionId, ActionRev, ActionTransform, ActionTransforms } from "@optimystic/db-core";
2
-
3
- export type RevisionRange = [
4
- /** Inclusive start */
5
- startRev: number,
6
- /** Exclusive end, or open-ended if undefined */
7
- endRev?: number,
8
- ];
9
-
10
- export type BlockMetadata = {
11
- // Revision ranges that are present in storage
12
- ranges: RevisionRange[];
13
- /** Latest revision - present if the repo is not empty */
14
- latest?: ActionRev;
15
- };
16
-
17
- export type ArchiveRevisions = Record<number, { action: ActionTransform, block?: IBlock }>;
18
-
19
- export type BlockArchive = {
20
- blockId: BlockId;
21
- /** Revisions in this archive */
22
- revisions: ArchiveRevisions;
23
- /** Explicit range covered by this archive since revisions may be sparse */
24
- range: RevisionRange;
25
- /** Pending actions - present if this range is open-ended */
26
- pending?: Record<ActionId, ActionTransforms>;
27
- }
28
-
29
- /** Should return a BlockRepo with the given rev (materialized) if given,
30
- * else (no rev) at least the latest revision and any given pending transactions */
31
- export type RestoreCallback = (blockId: BlockId, rev?: number) => Promise<BlockArchive | undefined>;
32
-
33
-
34
-
1
+ import type { BlockId, IBlock, ActionId, ActionRev, ActionTransform, ActionTransforms } from "@optimystic/db-core";
2
+ import type { BlockCommitProof } from "../cluster/commit-proof.js";
3
+
4
+ export type RevisionRange = [
5
+ /** Inclusive start */
6
+ startRev: number,
7
+ /** Exclusive end, or open-ended if undefined */
8
+ endRev?: number,
9
+ ];
10
+
11
+ export type BlockMetadata = {
12
+ // Revision ranges that are present in storage
13
+ ranges: RevisionRange[];
14
+ /** Latest revision - present if the repo is not empty */
15
+ latest?: ActionRev;
16
+ };
17
+
18
+ export type ArchiveRevisions = Record<number, {
19
+ action: ActionTransform;
20
+ block?: IBlock;
21
+ /**
22
+ * The cohort's commit proof for this revision, when the serving repo retained one.
23
+ *
24
+ * Absent in three legitimate cases, all of which every consumer must tolerate exactly as it
25
+ * tolerated the pre-proof shape: a revision committed before proofs were persisted at all; a
26
+ * member whose own materialization diverged from the digest the commit declared (it deliberately
27
+ * stores no proof — see `StorageRepo.persistProofIfContentMatches`); and a peer running an
28
+ * un-upgraded build.
29
+ *
30
+ * Keyed INSIDE the revision entry on purpose: the proof and the `(rev, actionId)` it certifies
31
+ * travel together, so a serving bug or a hostile peer cannot pair a genuine proof with a
32
+ * different revision by construction of the wire shape alone.
33
+ */
34
+ proof?: BlockCommitProof;
35
+ }>;
36
+
37
+ export type BlockArchive = {
38
+ blockId: BlockId;
39
+ /** Revisions in this archive */
40
+ revisions: ArchiveRevisions;
41
+ /** Explicit range covered by this archive since revisions may be sparse */
42
+ range: RevisionRange;
43
+ /** Pending actions - present if this range is open-ended */
44
+ pending?: Record<ActionId, ActionTransforms>;
45
+ }
46
+
47
+ /** Should return a BlockRepo with the given rev (materialized) if given,
48
+ * else (no rev) at least the latest revision and any given pending transactions */
49
+ export type RestoreCallback = (blockId: BlockId, rev?: number) => Promise<BlockArchive | undefined>;
50
+
51
+
52
+
@@ -0,0 +1,182 @@
1
+ import type { IRawStorage } from "./i-raw-storage.js";
2
+ import { MemoryRawStorage } from "./memory-storage.js";
3
+ import { CachedRawStorage } from "./cached-raw-storage.js";
4
+ import type { SharedCachePool } from "./shared-cache-pool.js";
5
+ import type { StoreIdentity } from "./store-identity.js";
6
+
7
+ /**
8
+ * One consumer's claim on a shared read cache. Release exactly once, when the consumer departs.
9
+ *
10
+ * The cache behind a lease may be serving OTHER consumers — every lease over one store hands
11
+ * back the same {@link CachedRawStorage} — so a lease holder never disposes the cache itself.
12
+ * It releases its claim; the cache is torn down when the last claim goes.
13
+ */
14
+ export interface ReadCacheLease {
15
+ /** The cache this lease is a claim on — the same object for every lease over one store. */
16
+ readonly cache: CachedRawStorage;
17
+ /**
18
+ * Drop this claim. Idempotent. The cache is cleared, unregistered from its pool, and
19
+ * forgotten only when the LAST lease releases; until then other consumers keep reading
20
+ * through it. A later {@link withReadCache} over the same store after the last release
21
+ * builds a fresh, cold cache.
22
+ */
23
+ release(): Promise<void>;
24
+ }
25
+
26
+ /**
27
+ * What {@link withReadCache} resolved: the storage to use, and this caller's claim on the cache
28
+ * behind it — `undefined` when it returned the argument unchanged.
29
+ *
30
+ * The split exists because "the result is a `CachedRawStorage`" and "the result is mine to
31
+ * release" are different questions, and answering the second with the first is a bug: the
32
+ * pass-through branch hands back a cache the CALLER built and may still be sharing with other
33
+ * consumers. Release `lease`; never dispose `storage`.
34
+ */
35
+ export type ResolvedReadCache = {
36
+ /** The storage to build on — the shared cache, or the argument unchanged. */
37
+ storage: IRawStorage;
38
+ /** This caller's claim, or `undefined` when the argument passed through unwrapped. */
39
+ lease: ReadCacheLease | undefined;
40
+ };
41
+
42
+ /**
43
+ * One live shared cache and the number of leases claiming it. `retire` removes it from
44
+ * whichever registry map it lives in — called exactly once, by the last release, BEFORE the
45
+ * async dispose, so a concurrent re-wrap over the same store never finds a dying entry.
46
+ */
47
+ type RegistryEntry = {
48
+ readonly cache: CachedRawStorage;
49
+ refs: number;
50
+ readonly retire: () => void;
51
+ };
52
+
53
+ /**
54
+ * The registry of live shared caches, keyed two ways because the two keys close different
55
+ * holes:
56
+ *
57
+ * - `byIdentity` — by {@link IRawStorage.getStoreIdentity}, so two DIFFERENT storage objects
58
+ * over one backing location (`new FileRawStorage(dir)` twice) resolve to one cache.
59
+ * - `byObject` — by the storage object itself, for backends that report no identity, so one
60
+ * unwrapped instance handed to two consumers still resolves to one cache. Weak: an entry
61
+ * never keeps its storage alive.
62
+ *
63
+ * An identity-bearing storage is keyed by identity only (two wraps of one object have one
64
+ * identity, so the object key would be redundant).
65
+ *
66
+ * NOTE: an entry lives until its last lease releases, so a host that never releases (e.g.
67
+ * never calls `plugin.dispose()`) holds one cache per store for the life of the process. This
68
+ * is the same class of retention as before dedupe — an undisposed cache already kept its pool
69
+ * registration forever — and the pool still evicts its entries under pressure, so it is
70
+ * hygiene, not correctness.
71
+ */
72
+ const byIdentity = new Map<StoreIdentity, RegistryEntry>();
73
+ const byObject = new WeakMap<IRawStorage, RegistryEntry>();
74
+
75
+ class Lease implements ReadCacheLease {
76
+ private released = false;
77
+
78
+ constructor(private readonly entry: RegistryEntry) {}
79
+
80
+ get cache(): CachedRawStorage {
81
+ return this.entry.cache;
82
+ }
83
+
84
+ async release(): Promise<void> {
85
+ // Latch first, then decrement, all synchronously: a double release on one lease counts
86
+ // once, and two leases releasing concurrently can only land ONE of them on zero.
87
+ if (this.released) return;
88
+ this.released = true;
89
+ this.entry.refs -= 1;
90
+ if (this.entry.refs > 0) return;
91
+ this.entry.retire();
92
+ await this.entry.cache.dispose();
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Wrap a raw storage in the write-through read cache ({@link CachedRawStorage}) at a
98
+ * composition seam — the ONE helper every production seam that resolves an `IRawStorage`
99
+ * goes through, so the exclusion and sharing rules below are stated once rather than
100
+ * re-derived per site.
101
+ *
102
+ * Returns the storage **unchanged**, with no `lease`, when caching would not pay:
103
+ * - `MemoryRawStorage` is already in memory; the cache would duplicate every map entry's
104
+ * bookkeeping with nothing to save (see `CachedStoreDriver`'s class doc).
105
+ * - An already-cached storage (a host that wrapped before handing it over) is not wrapped twice,
106
+ * and stays the host's to dispose.
107
+ *
108
+ * Why this is needed at all: `BlockStorage` re-reads block metadata on essentially every
109
+ * operation and `StorageRepo` builds a fresh `BlockStorage` per block per call, so nothing above
110
+ * this seam memoizes. Over a filesystem backend that is hundreds of reads of the same tiny files
111
+ * per statement. Measured A/B on a create/insert/update/select workload with only the wrap
112
+ * decision changed: 113 → 6 `getMetadata` and 207 → 14 total reads at the `RawStoreDriver` seam;
113
+ * over `FileRawStorage` the same workload went from 184 `readFile` + 29 `readdir` to 9 + 6.
114
+ *
115
+ * **One cache per backing store, shared under a lease.** The cache is write-through, so it is
116
+ * coherent only while every in-process writer to a store goes through the SAME cache; two caches
117
+ * over one store each serve their own stale view forever (measured: peer A still reads 1 row
118
+ * after peer B commits 3). So this helper dedupes: the second call over a store that already
119
+ * has a live cache returns that same cache with a fresh {@link ReadCacheLease}, and constructs
120
+ * only on a miss. "Same store" is decided by `storage.getStoreIdentity()` when the backend
121
+ * reports one (two `FileRawStorage` over one directory), else by storage object identity (one
122
+ * unwrapped instance handed to two consumers). Identity is one-directional — equal proves
123
+ * sameness, unequal proves nothing — so backends whose identity under-approximates (path
124
+ * aliases, two handles over one database; each backend's `NOTE:` lists its gaps) can still end
125
+ * up with two caches; and a host that builds its own `CachedRawStorage` never enters the
126
+ * registry, so a second consumer wrapping a fresh instance over that store still gets a second
127
+ * cache. What remains of Invariant 5 (`packages/db-p2p/docs/storage.md`) is the cross-process
128
+ * case: the filesystem driver takes no lock, and a second process's writes bypass this cache.
129
+ *
130
+ * On a dedupe hit the FIRST caller's `label` and `pool` stick: `pool.stats()` shows whoever
131
+ * wrapped first, and a second caller's different pool is ignored. Identity beats sizing — a
132
+ * same-store-different-pool pair would diverge, which is the thing being removed.
133
+ *
134
+ * **Lifecycle: release {@link ResolvedReadCache.lease} and nothing else.** The cache is
135
+ * cleared, unregistered from its pool, and forgotten when the LAST lease over the store
136
+ * releases; a later wrap then starts cold. Disposing `storage` directly would clear and
137
+ * unregister a cache other consumers are still reading through — the pool would keep charging
138
+ * that store's entries while dropping its row from `stats()`. A skipped release leaks only cold
139
+ * entries the pool evicts under pressure — hygiene, not correctness.
140
+ *
141
+ * Deliberately synchronous, with no `await` between lookup and insert: two seams resolving
142
+ * concurrently cannot both construct. Keep it that way.
143
+ *
144
+ * @param label Shown in `SharedCachePool.stats()` so this store is recognizable. First caller's
145
+ * label wins on a dedupe hit.
146
+ * @param pool Pool to join; defaults to the process-wide `defaultCachePool()`. Pass one only
147
+ * for isolation (tests) or host-specific sizing. First caller's pool wins on a
148
+ * dedupe hit.
149
+ */
150
+ export function withReadCache(storage: IRawStorage, label?: string, pool?: SharedCachePool): ResolvedReadCache {
151
+ if (storage instanceof MemoryRawStorage || storage instanceof CachedRawStorage) {
152
+ return { storage, lease: undefined };
153
+ }
154
+
155
+ const identity = storage.getStoreIdentity?.();
156
+ const existing = identity === undefined ? byObject.get(storage) : byIdentity.get(identity);
157
+ if (existing) {
158
+ // NOTE: on a hit the `storage` argument is dropped unused — the FIRST caller's instance is
159
+ // the one the shared cache fronts, and the only one the last release closes. Free today:
160
+ // `IRawStorage` has no close, and the only backend whose identity can make two DISTINCT
161
+ // objects collide is the filesystem one, which holds nothing open. If a backend ever pairs
162
+ // a location-derived identity with an open handle, the discarded instance would leak that
163
+ // handle — close it here, or key such backends by handle (`identityForHandle`) instead.
164
+ existing.refs += 1;
165
+ return { storage: existing.cache, lease: new Lease(existing) };
166
+ }
167
+
168
+ const cache = new CachedRawStorage(storage, pool, label);
169
+ const entry: RegistryEntry = {
170
+ cache,
171
+ refs: 1,
172
+ retire: identity === undefined
173
+ ? () => { byObject.delete(storage); }
174
+ : () => { byIdentity.delete(identity); },
175
+ };
176
+ if (identity === undefined) {
177
+ byObject.set(storage, entry);
178
+ } else {
179
+ byIdentity.set(identity, entry);
180
+ }
181
+ return { storage: cache, lease: new Lease(entry) };
182
+ }