@optimystic/db-p2p 0.21.0 → 0.24.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 (219) hide show
  1. package/dist/src/cluster/client.d.ts +10 -0
  2. package/dist/src/cluster/client.d.ts.map +1 -1
  3. package/dist/src/cluster/client.js +30 -1
  4. package/dist/src/cluster/client.js.map +1 -1
  5. package/dist/src/cluster/cluster-policy.d.ts +13 -2
  6. package/dist/src/cluster/cluster-policy.d.ts.map +1 -1
  7. package/dist/src/cluster/cluster-policy.js +51 -4
  8. package/dist/src/cluster/cluster-policy.js.map +1 -1
  9. package/dist/src/cluster/cluster-repo.d.ts +42 -17
  10. package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
  11. package/dist/src/cluster/cluster-repo.js +229 -122
  12. package/dist/src/cluster/cluster-repo.js.map +1 -1
  13. package/dist/src/cluster/cluster-size-coupling.d.ts +28 -0
  14. package/dist/src/cluster/cluster-size-coupling.d.ts.map +1 -0
  15. package/dist/src/cluster/cluster-size-coupling.js +35 -0
  16. package/dist/src/cluster/cluster-size-coupling.js.map +1 -0
  17. package/dist/src/cluster/quorum-restore.d.ts +6 -0
  18. package/dist/src/cluster/quorum-restore.d.ts.map +1 -1
  19. package/dist/src/cluster/quorum-restore.js +1 -1
  20. package/dist/src/cluster/quorum-restore.js.map +1 -1
  21. package/dist/src/cluster/reconcile-block.d.ts.map +1 -1
  22. package/dist/src/cluster/reconcile-block.js +15 -3
  23. package/dist/src/cluster/reconcile-block.js.map +1 -1
  24. package/dist/src/cluster/service.d.ts +32 -1
  25. package/dist/src/cluster/service.d.ts.map +1 -1
  26. package/dist/src/cluster/service.js +43 -2
  27. package/dist/src/cluster/service.js.map +1 -1
  28. package/dist/src/cohort-topic/stream-util.d.ts +22 -6
  29. package/dist/src/cohort-topic/stream-util.d.ts.map +1 -1
  30. package/dist/src/cohort-topic/stream-util.js +56 -10
  31. package/dist/src/cohort-topic/stream-util.js.map +1 -1
  32. package/dist/src/dispute/dispute-service.d.ts.map +1 -1
  33. package/dist/src/dispute/dispute-service.js +9 -3
  34. package/dist/src/dispute/dispute-service.js.map +1 -1
  35. package/dist/src/index.d.ts +5 -0
  36. package/dist/src/index.d.ts.map +1 -1
  37. package/dist/src/index.js +5 -0
  38. package/dist/src/index.js.map +1 -1
  39. package/dist/src/libp2p-key-network.d.ts +134 -7
  40. package/dist/src/libp2p-key-network.d.ts.map +1 -1
  41. package/dist/src/libp2p-key-network.js +174 -37
  42. package/dist/src/libp2p-key-network.js.map +1 -1
  43. package/dist/src/libp2p-node-base.d.ts +3 -2
  44. package/dist/src/libp2p-node-base.d.ts.map +1 -1
  45. package/dist/src/libp2p-node-base.js +859 -778
  46. package/dist/src/libp2p-node-base.js.map +1 -1
  47. package/dist/src/libp2p-node-rn.d.ts +2 -2
  48. package/dist/src/libp2p-node-rn.d.ts.map +1 -1
  49. package/dist/src/libp2p-node-rn.js.map +1 -1
  50. package/dist/src/libp2p-node.d.ts +2 -2
  51. package/dist/src/libp2p-node.d.ts.map +1 -1
  52. package/dist/src/libp2p-node.js.map +1 -1
  53. package/dist/src/logger.d.ts +17 -1
  54. package/dist/src/logger.d.ts.map +1 -1
  55. package/dist/src/logger.js +19 -2
  56. package/dist/src/logger.js.map +1 -1
  57. package/dist/src/network/network-manager-service.d.ts +2 -0
  58. package/dist/src/network/network-manager-service.d.ts.map +1 -1
  59. package/dist/src/network/network-manager-service.js +4 -0
  60. package/dist/src/network/network-manager-service.js.map +1 -1
  61. package/dist/src/optimystic-node.d.ts +35 -0
  62. package/dist/src/optimystic-node.d.ts.map +1 -0
  63. package/dist/src/optimystic-node.js +2 -0
  64. package/dist/src/optimystic-node.js.map +1 -0
  65. package/dist/src/owned-block-seed.d.ts +6 -3
  66. package/dist/src/owned-block-seed.d.ts.map +1 -1
  67. package/dist/src/owned-block-seed.js +16 -3
  68. package/dist/src/owned-block-seed.js.map +1 -1
  69. package/dist/src/peer-address-book.d.ts +72 -0
  70. package/dist/src/peer-address-book.d.ts.map +1 -0
  71. package/dist/src/peer-address-book.js +123 -0
  72. package/dist/src/peer-address-book.js.map +1 -0
  73. package/dist/src/repo/client.d.ts.map +1 -1
  74. package/dist/src/repo/client.js +11 -2
  75. package/dist/src/repo/client.js.map +1 -1
  76. package/dist/src/repo/cluster-coordinator.d.ts +30 -0
  77. package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
  78. package/dist/src/repo/cluster-coordinator.js +95 -3
  79. package/dist/src/repo/cluster-coordinator.js.map +1 -1
  80. package/dist/src/repo/coordinator-repo.d.ts +78 -14
  81. package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
  82. package/dist/src/repo/coordinator-repo.js +266 -81
  83. package/dist/src/repo/coordinator-repo.js.map +1 -1
  84. package/dist/src/rn.d.ts +5 -0
  85. package/dist/src/rn.d.ts.map +1 -1
  86. package/dist/src/rn.js +5 -0
  87. package/dist/src/rn.js.map +1 -1
  88. package/dist/src/storage/block-storage.d.ts.map +1 -1
  89. package/dist/src/storage/block-storage.js +57 -5
  90. package/dist/src/storage/block-storage.js.map +1 -1
  91. package/dist/src/storage/cached-raw-storage.d.ts +83 -0
  92. package/dist/src/storage/cached-raw-storage.d.ts.map +1 -0
  93. package/dist/src/storage/cached-raw-storage.js +152 -0
  94. package/dist/src/storage/cached-raw-storage.js.map +1 -0
  95. package/dist/src/storage/cached-store-driver.d.ts +186 -0
  96. package/dist/src/storage/cached-store-driver.d.ts.map +1 -0
  97. package/dist/src/storage/cached-store-driver.js +775 -0
  98. package/dist/src/storage/cached-store-driver.js.map +1 -0
  99. package/dist/src/storage/i-block-storage.d.ts +20 -1
  100. package/dist/src/storage/i-block-storage.d.ts.map +1 -1
  101. package/dist/src/storage/i-raw-storage.d.ts +12 -5
  102. package/dist/src/storage/i-raw-storage.d.ts.map +1 -1
  103. package/dist/src/storage/shared-cache-pool.d.ts +234 -0
  104. package/dist/src/storage/shared-cache-pool.d.ts.map +1 -0
  105. package/dist/src/storage/shared-cache-pool.js +354 -0
  106. package/dist/src/storage/shared-cache-pool.js.map +1 -0
  107. package/dist/src/storage/storage-repo.d.ts +56 -3
  108. package/dist/src/storage/storage-repo.d.ts.map +1 -1
  109. package/dist/src/storage/storage-repo.js +124 -18
  110. package/dist/src/storage/storage-repo.js.map +1 -1
  111. package/dist/src/testing/raw-storage-conformance.d.ts +2 -1
  112. package/dist/src/testing/raw-storage-conformance.d.ts.map +1 -1
  113. package/dist/src/testing/raw-storage-conformance.js +52 -2
  114. package/dist/src/testing/raw-storage-conformance.js.map +1 -1
  115. package/package.json +3 -3
  116. package/readme.md +668 -653
  117. package/src/cluster/block-transfer.ts +424 -424
  118. package/src/cluster/client.ts +119 -88
  119. package/src/cluster/cluster-error.ts +64 -64
  120. package/src/cluster/cluster-policy.ts +203 -152
  121. package/src/cluster/cluster-repo.ts +245 -125
  122. package/src/cluster/cluster-size-coupling.ts +45 -0
  123. package/src/cluster/commit-cert.ts +139 -139
  124. package/src/cluster/i-transaction-state-store.ts +43 -43
  125. package/src/cluster/memory-transaction-state-store.ts +56 -56
  126. package/src/cluster/peer-key-binding.ts +37 -37
  127. package/src/cluster/persistent-transaction-state-store.ts +92 -92
  128. package/src/cluster/quorum-restore.ts +223 -223
  129. package/src/cluster/reconcile-block.ts +203 -191
  130. package/src/cluster/service.ts +293 -241
  131. package/src/cluster/supermajority-coupling.ts +37 -37
  132. package/src/cohort-topic/bootstrap-evidence-builder.ts +122 -122
  133. package/src/cohort-topic/bootstrap-evidence-verifiers.ts +132 -132
  134. package/src/cohort-topic/bootstrap-parent-reference.ts +159 -159
  135. package/src/cohort-topic/change-bridge.ts +109 -109
  136. package/src/cohort-topic/cohort-gossip-driver.ts +231 -231
  137. package/src/cohort-topic/cohort-gossip-transport.ts +84 -84
  138. package/src/cohort-topic/fret-trust-anchor.ts +153 -153
  139. package/src/cohort-topic/host.ts +2901 -2901
  140. package/src/cohort-topic/index.ts +13 -13
  141. package/src/cohort-topic/membership-publish-sink.ts +20 -20
  142. package/src/cohort-topic/membership-source.ts +68 -68
  143. package/src/cohort-topic/peer-codec.ts +31 -31
  144. package/src/cohort-topic/peer-sig.ts +86 -86
  145. package/src/cohort-topic/protocols.ts +71 -71
  146. package/src/cohort-topic/reactivity-membership-gate.ts +77 -77
  147. package/src/cohort-topic/size-estimator.ts +16 -16
  148. package/src/cohort-topic/stream-util.ts +135 -87
  149. package/src/cohort-topic/threshold-crypto.ts +239 -239
  150. package/src/cohort-topic/topic-router.ts +77 -77
  151. package/src/dispute/arbitrator-selection.ts +138 -138
  152. package/src/dispute/cascade.ts +524 -524
  153. package/src/dispute/dispute-service.ts +11 -5
  154. package/src/dispute/invalidation.ts +625 -625
  155. package/src/inbound-authorization.ts +190 -190
  156. package/src/index.ts +52 -47
  157. package/src/libp2p-key-network.ts +1120 -958
  158. package/src/libp2p-node-base.ts +1675 -1591
  159. package/src/libp2p-node-rn.ts +30 -30
  160. package/src/libp2p-node.ts +36 -36
  161. package/src/logger.ts +19 -2
  162. package/src/matchmaking/aggregate-counts.ts +104 -104
  163. package/src/matchmaking/index.ts +20 -20
  164. package/src/matchmaking/module.ts +363 -363
  165. package/src/matchmaking/protocols.ts +51 -51
  166. package/src/matchmaking/provider-manager.ts +95 -95
  167. package/src/matchmaking/query-handler.ts +88 -88
  168. package/src/matchmaking/query-transport.ts +492 -492
  169. package/src/matchmaking/seeker-manager.ts +64 -64
  170. package/src/matchmaking/seeker-walk-client.ts +293 -293
  171. package/src/matchmaking/traffic-validation.ts +195 -195
  172. package/src/network/network-manager-service.ts +5 -0
  173. package/src/optimystic-node.ts +36 -0
  174. package/src/owned-block-seed.ts +53 -40
  175. package/src/peer-address-book.ts +149 -0
  176. package/src/protocol-limits.ts +33 -33
  177. package/src/reactivity/forwarder-host.ts +438 -438
  178. package/src/reactivity/index.ts +19 -19
  179. package/src/reactivity/notify-transport.ts +144 -144
  180. package/src/reactivity/origination-manager.ts +192 -192
  181. package/src/reactivity/protocols.ts +61 -61
  182. package/src/reactivity/push-state-gossip.ts +291 -291
  183. package/src/reactivity/recover-transport.ts +408 -408
  184. package/src/reactivity/rotation-rereg-scheduler.ts +256 -256
  185. package/src/reactivity/subscriber-registry.ts +96 -96
  186. package/src/reactivity/subscription-manager.ts +450 -450
  187. package/src/reactivity/topic-bytes.ts +37 -37
  188. package/src/repo/client.ts +12 -2
  189. package/src/repo/cluster-coordinator.ts +99 -3
  190. package/src/repo/coordinator-repo.ts +305 -82
  191. package/src/repo/types.ts +7 -7
  192. package/src/rn.ts +39 -34
  193. package/src/rpc-deadline.ts +45 -45
  194. package/src/storage/arachnode-partition.ts +74 -74
  195. package/src/storage/block-storage.ts +59 -6
  196. package/src/storage/cached-raw-storage.ts +180 -0
  197. package/src/storage/cached-store-driver.ts +859 -0
  198. package/src/storage/i-block-storage.ts +20 -1
  199. package/src/storage/i-kv-store.ts +8 -8
  200. package/src/storage/i-raw-storage.ts +12 -5
  201. package/src/storage/kv-raw-storage.ts +135 -135
  202. package/src/storage/memory-kv-store.ts +28 -28
  203. package/src/storage/memory-storage.ts +25 -25
  204. package/src/storage/memory-store-driver.ts +157 -157
  205. package/src/storage/raw-store-codec.ts +42 -42
  206. package/src/storage/raw-store-driver.ts +80 -80
  207. package/src/storage/ring-selector.ts +317 -317
  208. package/src/storage/ring-shift-coordinator.ts +271 -271
  209. package/src/storage/shared-cache-pool.ts +452 -0
  210. package/src/storage/storage-repo.ts +1014 -903
  211. package/src/testing/cohort-topic-mesh-harness.ts +663 -663
  212. package/src/testing/index.ts +8 -8
  213. package/src/testing/matchmaking-mesh-harness.ts +475 -475
  214. package/src/testing/raw-storage-conformance.ts +453 -397
  215. package/src/testing/reactivity-mesh-harness.ts +922 -922
  216. package/dist/src/storage/restoration-coordinator-v2.d.ts +0 -67
  217. package/dist/src/storage/restoration-coordinator-v2.d.ts.map +0 -1
  218. package/dist/src/storage/restoration-coordinator-v2.js +0 -172
  219. package/dist/src/storage/restoration-coordinator-v2.js.map +0 -1
package/src/repo/types.ts CHANGED
@@ -1,7 +1,7 @@
1
- export interface ClusterLogPeerOutcome {
2
- peerId: string;
3
- success: boolean;
4
- /** Optional error message that explains why the peer failed. */
5
- error?: string;
6
- }
7
-
1
+ export interface ClusterLogPeerOutcome {
2
+ peerId: string;
3
+ success: boolean;
4
+ /** Optional error message that explains why the peer failed. */
5
+ error?: string;
6
+ }
7
+
package/src/rn.ts CHANGED
@@ -1,34 +1,39 @@
1
- export * from './cluster/client.js';
2
- export * from './cluster/cluster-repo.js';
3
- export * from './cluster/service.js';
4
- export * from './protocol-client.js';
5
- export * from './repo/client.js';
6
- export * from './repo/cluster-coordinator.js';
7
- export * from './repo/coordinator-repo.js';
8
- export * from './repo/service.js';
9
- export * from './storage/block-storage.js';
10
- export * from './storage/raw-store-driver.js';
11
- export * from './storage/kv-raw-storage.js';
12
- export * from './storage/memory-store-driver.js';
13
- export * from './storage/memory-storage.js';
14
- export * from './storage/i-block-storage.js';
15
- export * from './storage/i-raw-storage.js';
16
- export * from './storage/struct.js';
17
- export * from './storage/storage-repo.js';
18
- export * from './storage/restoration-coordinator.js';
19
- export * from './storage/ring-selector.js';
20
- export * from './storage/storage-monitor.js';
21
- export * from './storage/arachnode-fret-adapter.js';
22
- export * from './sync/protocol.js';
23
- export * from './sync/client.js';
24
- export * from './sync/service.js';
25
- export * from './it-utility.js';
26
- export * from './libp2p-key-network.js';
27
- export * from './libp2p-node-rn.js';
28
- export * from './routing/responsibility.js';
29
- export * from './routing/libp2p-known-peers.js';
30
- export * from './network/network-manager-service.js';
31
- export * from './network/get-network-manager.js';
32
- // Browser-safe peer signing seam. The rest of ./cohort-topic pulls node-heavy host.js,
33
- // so only peer-sig (@noble/curves + peer-id) is surfaced through the RN/browser entry.
34
- export * from './cohort-topic/peer-sig.js';
1
+ export * from './cluster/client.js';
2
+ export * from './cluster/cluster-policy.js';
3
+ export * from './cluster/cluster-repo.js';
4
+ export * from './cluster/service.js';
5
+ export * from './protocol-client.js';
6
+ export * from './repo/client.js';
7
+ export * from './repo/cluster-coordinator.js';
8
+ export * from './repo/coordinator-repo.js';
9
+ export * from './repo/service.js';
10
+ export * from './storage/block-storage.js';
11
+ export * from './storage/raw-store-driver.js';
12
+ export * from './storage/kv-raw-storage.js';
13
+ export * from './storage/shared-cache-pool.js';
14
+ export * from './storage/cached-store-driver.js';
15
+ export * from './storage/cached-raw-storage.js';
16
+ export * from './storage/memory-store-driver.js';
17
+ export * from './storage/memory-storage.js';
18
+ export * from './storage/i-block-storage.js';
19
+ export * from './storage/i-raw-storage.js';
20
+ export * from './storage/struct.js';
21
+ export * from './storage/storage-repo.js';
22
+ export * from './storage/restoration-coordinator.js';
23
+ export * from './storage/ring-selector.js';
24
+ export * from './storage/storage-monitor.js';
25
+ export * from './storage/arachnode-fret-adapter.js';
26
+ export * from './sync/protocol.js';
27
+ export * from './sync/client.js';
28
+ export * from './sync/service.js';
29
+ export * from './it-utility.js';
30
+ export * from './libp2p-key-network.js';
31
+ export * from './libp2p-node-rn.js';
32
+ export * from './optimystic-node.js';
33
+ export * from './routing/responsibility.js';
34
+ export * from './routing/libp2p-known-peers.js';
35
+ export * from './network/network-manager-service.js';
36
+ export * from './network/get-network-manager.js';
37
+ // Browser-safe peer signing seam. The rest of ./cohort-topic pulls node-heavy host.js,
38
+ // so only peer-sig (@noble/curves + peer-id) is surfaced through the RN/browser entry.
39
+ export * from './cohort-topic/peer-sig.js';
@@ -1,45 +1,45 @@
1
- /**
2
- * Per-RPC deadline knobs shared by the simple {@link ProtocolClient} subclasses
3
- * (cluster / sync / dispute). `dialTimeoutMs` bounds connecting; `responseTimeoutMs`
4
- * bounds waiting for the reply once connected (so a peer that connects then goes
5
- * silent throws {@link ResponseTimeoutError} instead of hanging the caller forever);
6
- * `signal` cancels the whole request. All optional.
7
- */
8
- export type RpcDeadlineOptions = {
9
- signal?: AbortSignal;
10
- dialTimeoutMs?: number;
11
- responseTimeoutMs?: number;
12
- };
13
-
14
- /**
15
- * Default per-peer dial deadline. Matches `spread-on-churn.ts` `pushDialTimeoutMs`
16
- * (3000ms) — an unreachable peer fails the dial fast so the caller can re-pick a
17
- * different coordinator rather than blocking on a dead route.
18
- */
19
- export const DEFAULT_DIAL_TIMEOUT_MS = 3000;
20
-
21
- /**
22
- * Default per-peer response deadline. Matches `spread-on-churn.ts`
23
- * `pushResponseTimeoutMs` (10000ms) — a peer that connects then never writes a
24
- * reply is abandoned instead of hanging the caller forever.
25
- */
26
- export const DEFAULT_RESPONSE_TIMEOUT_MS = 10000;
27
-
28
- /**
29
- * Merge caller-supplied deadline options with the client-level defaults. An
30
- * explicitly-supplied value wins (including a deliberate `0`, which
31
- * {@link ProtocolClient.processMessage} reads as "no cap"); an absent key falls
32
- * back to the default so a caller that passes nothing still gets a deadline.
33
- * `signal` has no default — cancellation is always caller-driven.
34
- *
35
- * Unlike `BlockTransferClient` (which leaves its defaults to the owning
36
- * `SpreadOnChurnMonitor` config), these clients have many callers and no single
37
- * owning monitor, so the deadline default belongs here on the client.
38
- */
39
- export function withRpcDeadlineDefaults(options?: RpcDeadlineOptions): RpcDeadlineOptions {
40
- return {
41
- dialTimeoutMs: options?.dialTimeoutMs ?? DEFAULT_DIAL_TIMEOUT_MS,
42
- responseTimeoutMs: options?.responseTimeoutMs ?? DEFAULT_RESPONSE_TIMEOUT_MS,
43
- signal: options?.signal,
44
- };
45
- }
1
+ /**
2
+ * Per-RPC deadline knobs shared by the simple {@link ProtocolClient} subclasses
3
+ * (cluster / sync / dispute). `dialTimeoutMs` bounds connecting; `responseTimeoutMs`
4
+ * bounds waiting for the reply once connected (so a peer that connects then goes
5
+ * silent throws {@link ResponseTimeoutError} instead of hanging the caller forever);
6
+ * `signal` cancels the whole request. All optional.
7
+ */
8
+ export type RpcDeadlineOptions = {
9
+ signal?: AbortSignal;
10
+ dialTimeoutMs?: number;
11
+ responseTimeoutMs?: number;
12
+ };
13
+
14
+ /**
15
+ * Default per-peer dial deadline. Matches `spread-on-churn.ts` `pushDialTimeoutMs`
16
+ * (3000ms) — an unreachable peer fails the dial fast so the caller can re-pick a
17
+ * different coordinator rather than blocking on a dead route.
18
+ */
19
+ export const DEFAULT_DIAL_TIMEOUT_MS = 3000;
20
+
21
+ /**
22
+ * Default per-peer response deadline. Matches `spread-on-churn.ts`
23
+ * `pushResponseTimeoutMs` (10000ms) — a peer that connects then never writes a
24
+ * reply is abandoned instead of hanging the caller forever.
25
+ */
26
+ export const DEFAULT_RESPONSE_TIMEOUT_MS = 10000;
27
+
28
+ /**
29
+ * Merge caller-supplied deadline options with the client-level defaults. An
30
+ * explicitly-supplied value wins (including a deliberate `0`, which
31
+ * {@link ProtocolClient.processMessage} reads as "no cap"); an absent key falls
32
+ * back to the default so a caller that passes nothing still gets a deadline.
33
+ * `signal` has no default — cancellation is always caller-driven.
34
+ *
35
+ * Unlike `BlockTransferClient` (which leaves its defaults to the owning
36
+ * `SpreadOnChurnMonitor` config), these clients have many callers and no single
37
+ * owning monitor, so the deadline default belongs here on the client.
38
+ */
39
+ export function withRpcDeadlineDefaults(options?: RpcDeadlineOptions): RpcDeadlineOptions {
40
+ return {
41
+ dialTimeoutMs: options?.dialTimeoutMs ?? DEFAULT_DIAL_TIMEOUT_MS,
42
+ responseTimeoutMs: options?.responseTimeoutMs ?? DEFAULT_RESPONSE_TIMEOUT_MS,
43
+ signal: options?.signal,
44
+ };
45
+ }
@@ -1,74 +1,74 @@
1
- import type { ArachnodeInfo } from './arachnode-fret-adapter.js';
2
-
3
- /**
4
- * An Arachnode ring partition: the first `prefixBits` bits of a hashed coordinate must equal
5
- * `prefixValue` for a key to fall inside it. Structurally identical to `ArachnodeInfo.partition`.
6
- */
7
- export interface RingPartition {
8
- prefixBits: number;
9
- prefixValue: number;
10
- }
11
-
12
- /**
13
- * Extract the first `bits` bits of a hashed coordinate as an integer (MSB-first).
14
- *
15
- * The single source of truth for the block/peer prefix comparison Arachnode uses to decide
16
- * responsibility. `RingSelector.calculatePartition` (peer side) and `RestorationCoordinator`
17
- * (block side) both route through this, so a peer's advertised partition and a block's derived
18
- * prefix are computed the same way — otherwise "this peer owns this block's slice" would silently
19
- * stop meaning what it says.
20
- */
21
- export function extractPrefix(coord: Uint8Array, bits: number): number {
22
- let value = 0;
23
- for (let i = 0; i < bits; i++) {
24
- const byteIndex = Math.floor(i / 8);
25
- const bitIndex = 7 - (i % 8);
26
- const bit = (coord[byteIndex]! >> bitIndex) & 1;
27
- value = (value << 1) | bit;
28
- }
29
- return value;
30
- }
31
-
32
- /**
33
- * Does `partition` cover the key at `coord`? Ring 0 (an undefined partition) covers the whole
34
- * keyspace, so it always returns true.
35
- */
36
- export function partitionCovers(partition: RingPartition | undefined, coord: Uint8Array): boolean {
37
- if (!partition) {
38
- return true;
39
- }
40
- return extractPrefix(coord, partition.prefixBits) === partition.prefixValue;
41
- }
42
-
43
- /**
44
- * Is the node described by `info` a currently-serving responsible holder for the key at `coord`?
45
- *
46
- * **Fail-toward-old-holder.** A node mid-move (`status === 'moving'`) advertises its *target*
47
- * `partition`, but it keeps serving its *old* range (`moveFrom`) until it releases. So a moving
48
- * node covers the key if EITHER its old range or its target range covers it — it is still counted
49
- * as a serving holder for everything it served before the move until it transitions to `active` at
50
- * the new ring. This is what keeps a shed key covered through Phases A–B and across a mid-handoff
51
- * crash. See `docs/arachnode-ring-handoff.md` § Part 3.
52
- */
53
- export function isServingHolder(info: ArachnodeInfo, coord: Uint8Array): boolean {
54
- if (info.status === 'moving' && info.moveFrom) {
55
- return partitionCovers(info.moveFrom.partition, coord) || partitionCovers(info.partition, coord);
56
- }
57
- return partitionCovers(info.partition, coord);
58
- }
59
-
60
- /**
61
- * Does the node described by `info` qualify toward *another* mover's Phase-B replication floor for
62
- * the key at `coord`?
63
- *
64
- * A qualifying holder must still cover the key **after its own advertised move** — i.e. its
65
- * *target* `partition` (the one it currently advertises) must cover it. A concurrent mover that is
66
- * shedding the same sub-range advertises a target that does NOT cover the key, so it is excluded:
67
- * two adjacent movers can never both count the other, so at most one reaches "confirmed" on the
68
- * shared overlap and the other rolls back. For a non-moving (`active`) node, the advertised
69
- * partition IS its serving range, so this reduces to plain partition coverage. See
70
- * `docs/arachnode-ring-handoff.md` § Part 3 (concurrent moves).
71
- */
72
- export function qualifiesForFloor(info: ArachnodeInfo, coord: Uint8Array): boolean {
73
- return partitionCovers(info.partition, coord);
74
- }
1
+ import type { ArachnodeInfo } from './arachnode-fret-adapter.js';
2
+
3
+ /**
4
+ * An Arachnode ring partition: the first `prefixBits` bits of a hashed coordinate must equal
5
+ * `prefixValue` for a key to fall inside it. Structurally identical to `ArachnodeInfo.partition`.
6
+ */
7
+ export interface RingPartition {
8
+ prefixBits: number;
9
+ prefixValue: number;
10
+ }
11
+
12
+ /**
13
+ * Extract the first `bits` bits of a hashed coordinate as an integer (MSB-first).
14
+ *
15
+ * The single source of truth for the block/peer prefix comparison Arachnode uses to decide
16
+ * responsibility. `RingSelector.calculatePartition` (peer side) and `RestorationCoordinator`
17
+ * (block side) both route through this, so a peer's advertised partition and a block's derived
18
+ * prefix are computed the same way — otherwise "this peer owns this block's slice" would silently
19
+ * stop meaning what it says.
20
+ */
21
+ export function extractPrefix(coord: Uint8Array, bits: number): number {
22
+ let value = 0;
23
+ for (let i = 0; i < bits; i++) {
24
+ const byteIndex = Math.floor(i / 8);
25
+ const bitIndex = 7 - (i % 8);
26
+ const bit = (coord[byteIndex]! >> bitIndex) & 1;
27
+ value = (value << 1) | bit;
28
+ }
29
+ return value;
30
+ }
31
+
32
+ /**
33
+ * Does `partition` cover the key at `coord`? Ring 0 (an undefined partition) covers the whole
34
+ * keyspace, so it always returns true.
35
+ */
36
+ export function partitionCovers(partition: RingPartition | undefined, coord: Uint8Array): boolean {
37
+ if (!partition) {
38
+ return true;
39
+ }
40
+ return extractPrefix(coord, partition.prefixBits) === partition.prefixValue;
41
+ }
42
+
43
+ /**
44
+ * Is the node described by `info` a currently-serving responsible holder for the key at `coord`?
45
+ *
46
+ * **Fail-toward-old-holder.** A node mid-move (`status === 'moving'`) advertises its *target*
47
+ * `partition`, but it keeps serving its *old* range (`moveFrom`) until it releases. So a moving
48
+ * node covers the key if EITHER its old range or its target range covers it — it is still counted
49
+ * as a serving holder for everything it served before the move until it transitions to `active` at
50
+ * the new ring. This is what keeps a shed key covered through Phases A–B and across a mid-handoff
51
+ * crash. See `docs/arachnode-ring-handoff.md` § Part 3.
52
+ */
53
+ export function isServingHolder(info: ArachnodeInfo, coord: Uint8Array): boolean {
54
+ if (info.status === 'moving' && info.moveFrom) {
55
+ return partitionCovers(info.moveFrom.partition, coord) || partitionCovers(info.partition, coord);
56
+ }
57
+ return partitionCovers(info.partition, coord);
58
+ }
59
+
60
+ /**
61
+ * Does the node described by `info` qualify toward *another* mover's Phase-B replication floor for
62
+ * the key at `coord`?
63
+ *
64
+ * A qualifying holder must still cover the key **after its own advertised move** — i.e. its
65
+ * *target* `partition` (the one it currently advertises) must cover it. A concurrent mover that is
66
+ * shedding the same sub-range advertises a target that does NOT cover the key, so it is excluded:
67
+ * two adjacent movers can never both count the other, so at most one reaches "confirmed" on the
68
+ * shared overlap and the other rolls back. For a non-moving (`active`) node, the advertised
69
+ * partition IS its serving range, so this reduces to plain partition coverage. See
70
+ * `docs/arachnode-ring-handoff.md` § Part 3 (concurrent moves).
71
+ */
72
+ export function qualifiesForFloor(info: ArachnodeInfo, coord: Uint8Array): boolean {
73
+ return partitionCovers(info.partition, coord);
74
+ }
@@ -50,14 +50,44 @@ export class BlockStorage implements IBlockStorage {
50
50
  return undefined;
51
51
  }
52
52
 
53
- // Pending-only state: metadata was seeded by savePendingTransaction but no
54
- // revision has been committed yet. Treat as "doesn't exist" for the default
55
- // request pathmatches StorageRepo.get()'s contract that undefined => empty.
56
- if (rev === undefined && meta.latest === undefined) {
57
- return undefined;
53
+ // Pending-only state: metadata was seeded by savePendingTransaction but no revision has been
54
+ // committed yet. "No committed base here" is an ABSENCE, not a fault — nothing is being
55
+ // FAILED to reconstruct so both arms below answer `undefined` rather than throwing, whether
56
+ // or not the caller named a revision. StorageRepo.get then applies any pending overlay over
57
+ // that absent base; a throw here would instead be caught into `unavailable: 'unmaterializable'`
58
+ // and a writer reading back its own not-yet-committed insert would be told it is unreadable.
59
+ // `unmaterializable` must keep its one meaning: records prove the block exists and this node
60
+ // cannot reconstruct it.
61
+ if (meta.latest === undefined) {
62
+ if (rev === undefined) {
63
+ return undefined;
64
+ }
65
+ // A named rev still ATTEMPTS the restore: `restoreCallback` may be able to supply that
66
+ // revision even though nothing is committed locally, and a successful restore serves real
67
+ // content with `latest` still undefined. That capability is pinned by the 'getBlock for an
68
+ // absent revision fires restoreCallback (restore not short-circuited)' test in
69
+ // test/block-storage.spec.ts — do not short-circuit it away.
70
+ //
71
+ // Only ensureRevision's FAILURE is swallowed (no callback wired, or restore could not
72
+ // supply the rev): that is precisely the "no committed base here" absence. materializeBlock
73
+ // below is deliberately OUTSIDE the try — a throw from there means revision records exist
74
+ // with no materialization anywhere under them, which is genuine corruption and must keep
75
+ // reading as `unmaterializable`.
76
+ //
77
+ // NOTE: a contextful read of a pending-only block still attempts a network restore before
78
+ // falling back to absent (same cost as the pre-fix throw path); if pending-only read-backs
79
+ // ever show as hot, short-circuit when ranges are empty.
80
+ try {
81
+ await this.ensureRevision(meta, rev);
82
+ } catch (err) {
83
+ log('getBlock:no-committed-base blockId=%s rev=%d error=%s', this.blockId, rev,
84
+ err instanceof Error ? err.message : String(err));
85
+ return undefined;
86
+ }
87
+ return await this.materializeBlock(meta, rev);
58
88
  }
59
89
 
60
- const targetRev = rev ?? meta.latest!.rev;
90
+ const targetRev = rev ?? meta.latest.rev;
61
91
  await this.ensureRevision(meta, targetRev);
62
92
  return await this.materializeBlock(meta, targetRev);
63
93
  }
@@ -271,6 +301,29 @@ export class BlockStorage implements IBlockStorage {
271
301
  };
272
302
  await this.saveRestored(archive);
273
303
 
304
+ // INVARIANT P: a block never holds a pending record AND a committed record for the same
305
+ // action id. On the commit path `promotePendingTransaction` maintains it by MOVING the
306
+ // record atomically; this forward path writes the committed transform directly (via
307
+ // saveRestored above), so it owes the deletion itself. Without it, a node that pended the
308
+ // action but diverged before committing keeps a record nothing can ever promote — reported
309
+ // as a phantom conflicting action by every later `pend` on the block, which under
310
+ // `policy: 'f'` refuses that node's participation in the block's writes permanently.
311
+ //
312
+ // Deliberately on the WRITE path only: the monotonic guard above returns before here, and
313
+ // that early return must stay a true no-op (the earlier call that wrote the revision is the
314
+ // one that owed the deletion). Deliberately here rather than in `saveRestored`, which is
315
+ // also reached from ensureRevision's historical restore under a different latch, where a
316
+ // deletion could race a concurrent promotePendingTransaction; this path holds
317
+ // `BlockStorage.saveReplica:<id>` and (via StorageRepo.saveReplicatedBlock) the per-block
318
+ // commit latch, so it is already mutually exclusive with a live commit.
319
+ //
320
+ // NOTE: deletes only this revision's actionId, not every pending whose action is already
321
+ // committed. A broader sweep would repair records orphaned by routes that do not carry the
322
+ // committing actionId; if orphaned pendings ever show up in the field on blocks whose
323
+ // committing action id differs, widen to a sweep over listPendingTransactions filtered by
324
+ // getTransaction.
325
+ await this.storage.deletePendingTransaction(this.blockId, actionId);
326
+
274
327
  // Seed metadata when absent, advance latest, and merge the covered range.
275
328
  const prevRev = meta?.latest?.rev;
276
329
  if (!meta) {
@@ -0,0 +1,180 @@
1
+ import type { BlockId, ActionId } from "@optimystic/db-core";
2
+ import type { IRawStorage } from "./i-raw-storage.js";
3
+ import type { RawStoreDriver } from "./raw-store-driver.js";
4
+ import { KvRawStorage } from "./kv-raw-storage.js";
5
+ import { CachedStoreDriver } from "./cached-store-driver.js";
6
+ import type { SharedCachePool } from "./shared-cache-pool.js";
7
+ import { encodeJson, decodeJson, encodeActionId, decodeActionId } from "./raw-store-codec.js";
8
+
9
+ /**
10
+ * Presents a plain {@link IRawStorage} as a {@link RawStoreDriver}, so the
11
+ * {@link CachedStoreDriver} (and the `KvRawStorage` kernel above it) can wrap a backend
12
+ * that does NOT expose its driver — the composition {@link CachedRawStorage} uses.
13
+ *
14
+ * Every value crossing this adapter is re-encoded to bytes on read and re-decoded on
15
+ * write, on top of whatever (de)serialization the inner storage does itself. For a
16
+ * kernel-backed inner storage that is a double codec pass per COLD miss — cache hits
17
+ * never reach this adapter. Wrapping the backend's `RawStoreDriver` directly with
18
+ * `CachedStoreDriver` avoids the double pass entirely; prefer that when the driver is
19
+ * reachable, and this adapter when only the `IRawStorage` surface is.
20
+ */
21
+ export class RawStorageDriverAdapter implements RawStoreDriver {
22
+ /**
23
+ * Optional passthroughs wired only when the inner storage provides them, mirroring
24
+ * `KvRawStorage`'s constructor: feature-detection above must observe the inner
25
+ * storage's true capability. `IRawStorage` has no `close`/`approximateBytesUsed`
26
+ * driver names — they map from `listBlockIds`/`getApproximateBytesUsed`.
27
+ */
28
+ listBlockIds?: () => AsyncIterable<BlockId>;
29
+ approximateBytesUsed?: () => Promise<number>;
30
+
31
+ constructor(private readonly inner: IRawStorage) {
32
+ if (inner.listBlockIds) {
33
+ this.listBlockIds = () => inner.listBlockIds!();
34
+ }
35
+ if (inner.getApproximateBytesUsed) {
36
+ this.approximateBytesUsed = () => inner.getApproximateBytesUsed!();
37
+ }
38
+ }
39
+
40
+ // --- metadata ---
41
+
42
+ async getMetadata(blockId: BlockId): Promise<Uint8Array | undefined> {
43
+ const meta = await this.inner.getMetadata(blockId);
44
+ return meta === undefined ? undefined : encodeJson(meta);
45
+ }
46
+
47
+ async putMetadata(blockId: BlockId, value: Uint8Array): Promise<void> {
48
+ await this.inner.saveMetadata(blockId, decodeJson(value));
49
+ }
50
+
51
+ // --- revisions ---
52
+
53
+ async getRevision(blockId: BlockId, rev: number): Promise<Uint8Array | undefined> {
54
+ const actionId = await this.inner.getRevision(blockId, rev);
55
+ return actionId === undefined ? undefined : encodeActionId(actionId);
56
+ }
57
+
58
+ async putRevision(blockId: BlockId, rev: number, value: Uint8Array): Promise<void> {
59
+ await this.inner.saveRevision(blockId, rev, decodeActionId(value));
60
+ }
61
+
62
+ async *rangeRevisions(blockId: BlockId, lo: number, hi: number, reverse: boolean): AsyncIterable<[number, Uint8Array]> {
63
+ // `listRevisions` derives direction from bound order (start > end ⇒ descending);
64
+ // map (lo, hi, reverse) back onto that. Drain before yielding — the inner
65
+ // iterable already drained per its own contract, but the consumer's awaits must
66
+ // not straddle OUR loop over it either.
67
+ const startRev = reverse ? hi : lo;
68
+ const endRev = reverse ? lo : hi;
69
+ const drained: [number, Uint8Array][] = [];
70
+ for await (const { rev, actionId } of this.inner.listRevisions(blockId, startRev, endRev)) {
71
+ drained.push([rev, encodeActionId(actionId)]);
72
+ }
73
+ yield* drained;
74
+ }
75
+
76
+ // --- pending ---
77
+
78
+ async getPending(blockId: BlockId, actionId: ActionId): Promise<Uint8Array | undefined> {
79
+ const transform = await this.inner.getPendingTransaction(blockId, actionId);
80
+ return transform === undefined ? undefined : encodeJson(transform);
81
+ }
82
+
83
+ async putPending(blockId: BlockId, actionId: ActionId, value: Uint8Array): Promise<void> {
84
+ await this.inner.savePendingTransaction(blockId, actionId, decodeJson(value));
85
+ }
86
+
87
+ async deletePending(blockId: BlockId, actionId: ActionId): Promise<void> {
88
+ await this.inner.deletePendingTransaction(blockId, actionId);
89
+ }
90
+
91
+ async *listPendingActionIds(blockId: BlockId): AsyncIterable<ActionId> {
92
+ const drained: ActionId[] = [];
93
+ for await (const id of this.inner.listPendingTransactions(blockId)) {
94
+ drained.push(id);
95
+ }
96
+ yield* drained;
97
+ }
98
+
99
+ // --- transactions ---
100
+
101
+ async getTransaction(blockId: BlockId, actionId: ActionId): Promise<Uint8Array | undefined> {
102
+ const transform = await this.inner.getTransaction(blockId, actionId);
103
+ return transform === undefined ? undefined : encodeJson(transform);
104
+ }
105
+
106
+ async putTransaction(blockId: BlockId, actionId: ActionId, value: Uint8Array): Promise<void> {
107
+ await this.inner.saveTransaction(blockId, actionId, decodeJson(value));
108
+ }
109
+
110
+ // --- materialized ---
111
+
112
+ async getMaterialized(blockId: BlockId, actionId: ActionId): Promise<Uint8Array | undefined> {
113
+ const block = await this.inner.getMaterializedBlock(blockId, actionId);
114
+ return block === undefined ? undefined : encodeJson(block);
115
+ }
116
+
117
+ async putMaterialized(blockId: BlockId, actionId: ActionId, value: Uint8Array): Promise<void> {
118
+ await this.inner.saveMaterializedBlock(blockId, actionId, decodeJson(value));
119
+ }
120
+
121
+ async deleteMaterialized(blockId: BlockId, actionId: ActionId): Promise<void> {
122
+ await this.inner.saveMaterializedBlock(blockId, actionId, undefined);
123
+ }
124
+
125
+ // --- promote ---
126
+
127
+ async promote(blockId: BlockId, actionId: ActionId): Promise<void> {
128
+ await this.inner.promotePendingTransaction(blockId, actionId);
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Write-through cached view over a plain {@link IRawStorage} — the wrapper form for a
134
+ * backend whose {@link RawStoreDriver} is not reachable. Composition:
135
+ *
136
+ * ```
137
+ * KvRawStorage kernel → CachedStoreDriver → RawStorageDriverAdapter → inner IRawStorage
138
+ * ```
139
+ *
140
+ * All caching semantics (write-through coherence, proven-absence negatives, list
141
+ * completeness, promote mirroring) live in {@link CachedStoreDriver}; see its class doc
142
+ * and docs/storage.md "Write-through raw-storage cache" for the invariants this relies
143
+ * on — including the single-process-owner precondition.
144
+ *
145
+ * Prefer wrapping the backend's driver directly — `new KvRawStorage(new
146
+ * CachedStoreDriver(driver))` — when the driver is available: it skips this adapter's
147
+ * extra codec pass on cold misses. Do not wrap `MemoryRawStorage` in production wiring
148
+ * (already in-memory; the cache adds bookkeeping with nothing to save) — tests do, to
149
+ * prove semantics are identical through the full composition.
150
+ *
151
+ * By default the cache joins the process-wide shared pool (`defaultCachePool()`), so
152
+ * every workspace's cache competes inside ONE memory budget; pass a specific
153
+ * {@link SharedCachePool} only for isolation (tests) or host-specific sizing, and a
154
+ * `label` to make this store recognizable in the pool's `stats()`.
155
+ */
156
+ export class CachedRawStorage extends KvRawStorage {
157
+ private readonly cacheDriver: CachedStoreDriver;
158
+
159
+ constructor(inner: IRawStorage, pool?: SharedCachePool, label?: string) {
160
+ const cacheDriver = new CachedStoreDriver(new RawStorageDriverAdapter(inner), pool, label);
161
+ super(cacheDriver);
162
+ this.cacheDriver = cacheDriver;
163
+ }
164
+
165
+ /** Drop every cached entry. Always safe — the cache is clean (write-through, never write-behind). */
166
+ clearCache(): void {
167
+ this.cacheDriver.clear();
168
+ }
169
+
170
+ /**
171
+ * Release this storage's cache registration with the shared pool (drops all entries,
172
+ * retires the store id). Call when the workspace departs; a skipped dispose leaks only
173
+ * cold entries the pool will evict under pressure, but the polite release keeps a
174
+ * long-lived process's occupancy honest. `IRawStorage` itself has no close, so this is
175
+ * the wrapper's own lifecycle method.
176
+ */
177
+ async dispose(): Promise<void> {
178
+ await this.cacheDriver.close();
179
+ }
180
+ }