@optimystic/db-p2p 0.22.0 → 0.24.1

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 (194) 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-repo.d.ts +39 -14
  6. package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
  7. package/dist/src/cluster/cluster-repo.js +226 -119
  8. package/dist/src/cluster/cluster-repo.js.map +1 -1
  9. package/dist/src/cluster/service.d.ts +32 -1
  10. package/dist/src/cluster/service.d.ts.map +1 -1
  11. package/dist/src/cluster/service.js +43 -2
  12. package/dist/src/cluster/service.js.map +1 -1
  13. package/dist/src/cohort-topic/host.js +34 -11
  14. package/dist/src/cohort-topic/host.js.map +1 -1
  15. package/dist/src/cohort-topic/stream-util.d.ts +37 -7
  16. package/dist/src/cohort-topic/stream-util.d.ts.map +1 -1
  17. package/dist/src/cohort-topic/stream-util.js +77 -19
  18. package/dist/src/cohort-topic/stream-util.js.map +1 -1
  19. package/dist/src/dispute/dispute-service.d.ts.map +1 -1
  20. package/dist/src/dispute/dispute-service.js +9 -3
  21. package/dist/src/dispute/dispute-service.js.map +1 -1
  22. package/dist/src/index.d.ts +3 -0
  23. package/dist/src/index.d.ts.map +1 -1
  24. package/dist/src/index.js +3 -0
  25. package/dist/src/index.js.map +1 -1
  26. package/dist/src/libp2p-key-network.d.ts +88 -2
  27. package/dist/src/libp2p-key-network.d.ts.map +1 -1
  28. package/dist/src/libp2p-key-network.js +134 -28
  29. package/dist/src/libp2p-key-network.js.map +1 -1
  30. package/dist/src/libp2p-node-base.d.ts.map +1 -1
  31. package/dist/src/libp2p-node-base.js +25 -1
  32. package/dist/src/libp2p-node-base.js.map +1 -1
  33. package/dist/src/logger.d.ts +17 -1
  34. package/dist/src/logger.d.ts.map +1 -1
  35. package/dist/src/logger.js +19 -2
  36. package/dist/src/logger.js.map +1 -1
  37. package/dist/src/matchmaking/query-transport.js +3 -3
  38. package/dist/src/matchmaking/query-transport.js.map +1 -1
  39. package/dist/src/owned-block-seed.d.ts +6 -3
  40. package/dist/src/owned-block-seed.d.ts.map +1 -1
  41. package/dist/src/owned-block-seed.js +16 -3
  42. package/dist/src/owned-block-seed.js.map +1 -1
  43. package/dist/src/peer-address-book.d.ts +72 -0
  44. package/dist/src/peer-address-book.d.ts.map +1 -0
  45. package/dist/src/peer-address-book.js +123 -0
  46. package/dist/src/peer-address-book.js.map +1 -0
  47. package/dist/src/reactivity/notify-transport.d.ts +4 -4
  48. package/dist/src/reactivity/notify-transport.js +6 -6
  49. package/dist/src/reactivity/notify-transport.js.map +1 -1
  50. package/dist/src/reactivity/push-state-gossip.js +2 -2
  51. package/dist/src/reactivity/push-state-gossip.js.map +1 -1
  52. package/dist/src/reactivity/recover-transport.d.ts +6 -2
  53. package/dist/src/reactivity/recover-transport.d.ts.map +1 -1
  54. package/dist/src/reactivity/recover-transport.js +7 -3
  55. package/dist/src/reactivity/recover-transport.js.map +1 -1
  56. package/dist/src/repo/client.d.ts.map +1 -1
  57. package/dist/src/repo/client.js +11 -2
  58. package/dist/src/repo/client.js.map +1 -1
  59. package/dist/src/repo/cluster-coordinator.d.ts +30 -0
  60. package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
  61. package/dist/src/repo/cluster-coordinator.js +95 -3
  62. package/dist/src/repo/cluster-coordinator.js.map +1 -1
  63. package/dist/src/repo/coordinator-repo.d.ts +62 -9
  64. package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
  65. package/dist/src/repo/coordinator-repo.js +242 -73
  66. package/dist/src/repo/coordinator-repo.js.map +1 -1
  67. package/dist/src/rn.d.ts +3 -0
  68. package/dist/src/rn.d.ts.map +1 -1
  69. package/dist/src/rn.js +3 -0
  70. package/dist/src/rn.js.map +1 -1
  71. package/dist/src/storage/cached-raw-storage.d.ts +83 -0
  72. package/dist/src/storage/cached-raw-storage.d.ts.map +1 -0
  73. package/dist/src/storage/cached-raw-storage.js +152 -0
  74. package/dist/src/storage/cached-raw-storage.js.map +1 -0
  75. package/dist/src/storage/cached-store-driver.d.ts +186 -0
  76. package/dist/src/storage/cached-store-driver.d.ts.map +1 -0
  77. package/dist/src/storage/cached-store-driver.js +775 -0
  78. package/dist/src/storage/cached-store-driver.js.map +1 -0
  79. package/dist/src/storage/i-raw-storage.d.ts +12 -5
  80. package/dist/src/storage/i-raw-storage.d.ts.map +1 -1
  81. package/dist/src/storage/shared-cache-pool.d.ts +234 -0
  82. package/dist/src/storage/shared-cache-pool.d.ts.map +1 -0
  83. package/dist/src/storage/shared-cache-pool.js +354 -0
  84. package/dist/src/storage/shared-cache-pool.js.map +1 -0
  85. package/dist/src/testing/cohort-topic-mesh-harness.d.ts +13 -6
  86. package/dist/src/testing/cohort-topic-mesh-harness.d.ts.map +1 -1
  87. package/dist/src/testing/cohort-topic-mesh-harness.js +15 -6
  88. package/dist/src/testing/cohort-topic-mesh-harness.js.map +1 -1
  89. package/dist/src/testing/raw-storage-conformance.d.ts +2 -1
  90. package/dist/src/testing/raw-storage-conformance.d.ts.map +1 -1
  91. package/dist/src/testing/raw-storage-conformance.js +35 -2
  92. package/dist/src/testing/raw-storage-conformance.js.map +1 -1
  93. package/package.json +3 -3
  94. package/readme.md +668 -668
  95. package/src/cluster/block-transfer.ts +424 -424
  96. package/src/cluster/client.ts +119 -88
  97. package/src/cluster/cluster-error.ts +64 -64
  98. package/src/cluster/cluster-policy.ts +203 -203
  99. package/src/cluster/cluster-repo.ts +242 -122
  100. package/src/cluster/cluster-size-coupling.ts +45 -45
  101. package/src/cluster/commit-cert.ts +139 -139
  102. package/src/cluster/i-transaction-state-store.ts +43 -43
  103. package/src/cluster/memory-transaction-state-store.ts +56 -56
  104. package/src/cluster/peer-key-binding.ts +37 -37
  105. package/src/cluster/persistent-transaction-state-store.ts +92 -92
  106. package/src/cluster/quorum-restore.ts +223 -223
  107. package/src/cluster/reconcile-block.ts +203 -203
  108. package/src/cluster/service.ts +293 -241
  109. package/src/cluster/supermajority-coupling.ts +37 -37
  110. package/src/cohort-topic/bootstrap-evidence-builder.ts +122 -122
  111. package/src/cohort-topic/bootstrap-evidence-verifiers.ts +132 -132
  112. package/src/cohort-topic/bootstrap-parent-reference.ts +159 -159
  113. package/src/cohort-topic/change-bridge.ts +109 -109
  114. package/src/cohort-topic/cohort-gossip-driver.ts +231 -231
  115. package/src/cohort-topic/cohort-gossip-transport.ts +84 -84
  116. package/src/cohort-topic/fret-trust-anchor.ts +153 -153
  117. package/src/cohort-topic/host.ts +42 -11
  118. package/src/cohort-topic/index.ts +13 -13
  119. package/src/cohort-topic/membership-publish-sink.ts +20 -20
  120. package/src/cohort-topic/membership-source.ts +68 -68
  121. package/src/cohort-topic/peer-codec.ts +31 -31
  122. package/src/cohort-topic/peer-sig.ts +86 -86
  123. package/src/cohort-topic/protocols.ts +71 -71
  124. package/src/cohort-topic/reactivity-membership-gate.ts +77 -77
  125. package/src/cohort-topic/size-estimator.ts +16 -16
  126. package/src/cohort-topic/stream-util.ts +79 -19
  127. package/src/cohort-topic/threshold-crypto.ts +239 -239
  128. package/src/cohort-topic/topic-router.ts +77 -77
  129. package/src/dispute/arbitrator-selection.ts +138 -138
  130. package/src/dispute/cascade.ts +524 -524
  131. package/src/dispute/dispute-service.ts +11 -5
  132. package/src/dispute/invalidation.ts +625 -625
  133. package/src/inbound-authorization.ts +190 -190
  134. package/src/index.ts +52 -49
  135. package/src/libp2p-key-network.ts +1120 -990
  136. package/src/libp2p-node-base.ts +1675 -1651
  137. package/src/libp2p-node-rn.ts +30 -30
  138. package/src/libp2p-node.ts +36 -36
  139. package/src/logger.ts +19 -2
  140. package/src/matchmaking/aggregate-counts.ts +104 -104
  141. package/src/matchmaking/index.ts +20 -20
  142. package/src/matchmaking/module.ts +363 -363
  143. package/src/matchmaking/protocols.ts +51 -51
  144. package/src/matchmaking/provider-manager.ts +95 -95
  145. package/src/matchmaking/query-handler.ts +88 -88
  146. package/src/matchmaking/query-transport.ts +3 -3
  147. package/src/matchmaking/seeker-manager.ts +64 -64
  148. package/src/matchmaking/seeker-walk-client.ts +293 -293
  149. package/src/matchmaking/traffic-validation.ts +195 -195
  150. package/src/optimystic-node.ts +36 -36
  151. package/src/owned-block-seed.ts +53 -40
  152. package/src/peer-address-book.ts +149 -0
  153. package/src/protocol-limits.ts +33 -33
  154. package/src/reactivity/forwarder-host.ts +438 -438
  155. package/src/reactivity/index.ts +19 -19
  156. package/src/reactivity/notify-transport.ts +144 -144
  157. package/src/reactivity/origination-manager.ts +192 -192
  158. package/src/reactivity/protocols.ts +61 -61
  159. package/src/reactivity/push-state-gossip.ts +291 -291
  160. package/src/reactivity/recover-transport.ts +7 -3
  161. package/src/reactivity/rotation-rereg-scheduler.ts +256 -256
  162. package/src/reactivity/subscriber-registry.ts +96 -96
  163. package/src/reactivity/subscription-manager.ts +450 -450
  164. package/src/reactivity/topic-bytes.ts +37 -37
  165. package/src/repo/client.ts +12 -2
  166. package/src/repo/cluster-coordinator.ts +99 -3
  167. package/src/repo/coordinator-repo.ts +281 -74
  168. package/src/repo/types.ts +7 -7
  169. package/src/rn.ts +39 -36
  170. package/src/rpc-deadline.ts +45 -45
  171. package/src/storage/arachnode-partition.ts +74 -74
  172. package/src/storage/cached-raw-storage.ts +180 -0
  173. package/src/storage/cached-store-driver.ts +859 -0
  174. package/src/storage/i-kv-store.ts +8 -8
  175. package/src/storage/i-raw-storage.ts +12 -5
  176. package/src/storage/kv-raw-storage.ts +135 -135
  177. package/src/storage/memory-kv-store.ts +28 -28
  178. package/src/storage/memory-storage.ts +25 -25
  179. package/src/storage/memory-store-driver.ts +157 -157
  180. package/src/storage/raw-store-codec.ts +42 -42
  181. package/src/storage/raw-store-driver.ts +80 -80
  182. package/src/storage/ring-selector.ts +317 -317
  183. package/src/storage/ring-shift-coordinator.ts +271 -271
  184. package/src/storage/shared-cache-pool.ts +452 -0
  185. package/src/storage/storage-repo.ts +1014 -1014
  186. package/src/testing/cohort-topic-mesh-harness.ts +673 -663
  187. package/src/testing/index.ts +8 -8
  188. package/src/testing/matchmaking-mesh-harness.ts +475 -475
  189. package/src/testing/raw-storage-conformance.ts +453 -417
  190. package/src/testing/reactivity-mesh-harness.ts +922 -922
  191. package/dist/src/storage/restoration-coordinator-v2.d.ts +0 -67
  192. package/dist/src/storage/restoration-coordinator-v2.d.ts.map +0 -1
  193. package/dist/src/storage/restoration-coordinator-v2.js +0 -172
  194. package/dist/src/storage/restoration-coordinator-v2.js.map +0 -1
package/src/rn.ts CHANGED
@@ -1,36 +1,39 @@
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/memory-store-driver.js';
14
- export * from './storage/memory-storage.js';
15
- export * from './storage/i-block-storage.js';
16
- export * from './storage/i-raw-storage.js';
17
- export * from './storage/struct.js';
18
- export * from './storage/storage-repo.js';
19
- export * from './storage/restoration-coordinator.js';
20
- export * from './storage/ring-selector.js';
21
- export * from './storage/storage-monitor.js';
22
- export * from './storage/arachnode-fret-adapter.js';
23
- export * from './sync/protocol.js';
24
- export * from './sync/client.js';
25
- export * from './sync/service.js';
26
- export * from './it-utility.js';
27
- export * from './libp2p-key-network.js';
28
- export * from './libp2p-node-rn.js';
29
- export * from './optimystic-node.js';
30
- export * from './routing/responsibility.js';
31
- export * from './routing/libp2p-known-peers.js';
32
- export * from './network/network-manager-service.js';
33
- export * from './network/get-network-manager.js';
34
- // Browser-safe peer signing seam. The rest of ./cohort-topic pulls node-heavy host.js,
35
- // so only peer-sig (@noble/curves + peer-id) is surfaced through the RN/browser entry.
36
- 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
+ }
@@ -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
+ }