@optimystic/db-p2p 1.0.0-beta.3 → 1.1.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 (234) hide show
  1. package/dist/src/cluster/block-transfer-service.d.ts +73 -1
  2. package/dist/src/cluster/block-transfer-service.d.ts.map +1 -1
  3. package/dist/src/cluster/block-transfer-service.js +70 -3
  4. package/dist/src/cluster/block-transfer-service.js.map +1 -1
  5. package/dist/src/cluster/block-transfer.d.ts +16 -1
  6. package/dist/src/cluster/block-transfer.d.ts.map +1 -1
  7. package/dist/src/cluster/block-transfer.js +73 -72
  8. package/dist/src/cluster/block-transfer.js.map +1 -1
  9. package/dist/src/cluster/client.d.ts +4 -6
  10. package/dist/src/cluster/client.d.ts.map +1 -1
  11. package/dist/src/cluster/client.js +8 -13
  12. package/dist/src/cluster/client.js.map +1 -1
  13. package/dist/src/cluster/cluster-policy.d.ts +2 -1
  14. package/dist/src/cluster/cluster-policy.d.ts.map +1 -1
  15. package/dist/src/cluster/cluster-policy.js.map +1 -1
  16. package/dist/src/cluster/cluster-repo.d.ts +155 -9
  17. package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
  18. package/dist/src/cluster/cluster-repo.js +446 -58
  19. package/dist/src/cluster/cluster-repo.js.map +1 -1
  20. package/dist/src/cluster/commit-proof.js +2 -2
  21. package/dist/src/cluster/race-resolution.d.ts +51 -7
  22. package/dist/src/cluster/race-resolution.d.ts.map +1 -1
  23. package/dist/src/cluster/race-resolution.js +71 -7
  24. package/dist/src/cluster/race-resolution.js.map +1 -1
  25. package/dist/src/cluster/rebalance-monitor.d.ts +67 -5
  26. package/dist/src/cluster/rebalance-monitor.d.ts.map +1 -1
  27. package/dist/src/cluster/rebalance-monitor.js +120 -26
  28. package/dist/src/cluster/rebalance-monitor.js.map +1 -1
  29. package/dist/src/cluster/spread-on-churn.d.ts.map +1 -1
  30. package/dist/src/cluster/spread-on-churn.js +34 -47
  31. package/dist/src/cluster/spread-on-churn.js.map +1 -1
  32. package/dist/src/cohort-topic/host.d.ts.map +1 -1
  33. package/dist/src/cohort-topic/host.js +8 -6
  34. package/dist/src/cohort-topic/host.js.map +1 -1
  35. package/dist/src/cohort-topic/membership-source.js +1 -1
  36. package/dist/src/cohort-topic/membership-source.js.map +1 -1
  37. package/dist/src/cohort-topic/peer-codec.d.ts.map +1 -1
  38. package/dist/src/cohort-topic/peer-codec.js +5 -2
  39. package/dist/src/cohort-topic/peer-codec.js.map +1 -1
  40. package/dist/src/cohort-topic/stream-util.d.ts +24 -3
  41. package/dist/src/cohort-topic/stream-util.d.ts.map +1 -1
  42. package/dist/src/cohort-topic/stream-util.js +36 -3
  43. package/dist/src/cohort-topic/stream-util.js.map +1 -1
  44. package/dist/src/cohort-topic/topic-router.d.ts +9 -0
  45. package/dist/src/cohort-topic/topic-router.d.ts.map +1 -1
  46. package/dist/src/cohort-topic/topic-router.js +12 -2
  47. package/dist/src/cohort-topic/topic-router.js.map +1 -1
  48. package/dist/src/dispute/client.d.ts.map +1 -1
  49. package/dist/src/dispute/client.js +15 -3
  50. package/dist/src/dispute/client.js.map +1 -1
  51. package/dist/src/index.d.ts +5 -0
  52. package/dist/src/index.d.ts.map +1 -1
  53. package/dist/src/index.js +5 -0
  54. package/dist/src/index.js.map +1 -1
  55. package/dist/src/libp2p-key-network.d.ts +78 -17
  56. package/dist/src/libp2p-key-network.d.ts.map +1 -1
  57. package/dist/src/libp2p-key-network.js +234 -147
  58. package/dist/src/libp2p-key-network.js.map +1 -1
  59. package/dist/src/libp2p-node-base.d.ts +35 -1
  60. package/dist/src/libp2p-node-base.d.ts.map +1 -1
  61. package/dist/src/libp2p-node-base.js +151 -26
  62. package/dist/src/libp2p-node-base.js.map +1 -1
  63. package/dist/src/libp2p-node-rn.d.ts +2 -2
  64. package/dist/src/libp2p-node-rn.d.ts.map +1 -1
  65. package/dist/src/libp2p-node-rn.js.map +1 -1
  66. package/dist/src/libp2p-node.d.ts +2 -2
  67. package/dist/src/libp2p-node.d.ts.map +1 -1
  68. package/dist/src/libp2p-node.js.map +1 -1
  69. package/dist/src/logger.d.ts.map +1 -1
  70. package/dist/src/logger.js +6 -1
  71. package/dist/src/logger.js.map +1 -1
  72. package/dist/src/matchmaking/query-transport.d.ts +3 -4
  73. package/dist/src/matchmaking/query-transport.d.ts.map +1 -1
  74. package/dist/src/matchmaking/query-transport.js +9 -9
  75. package/dist/src/matchmaking/query-transport.js.map +1 -1
  76. package/dist/src/network/network-manager-service.d.ts +10 -5
  77. package/dist/src/network/network-manager-service.d.ts.map +1 -1
  78. package/dist/src/network/network-manager-service.js +11 -2
  79. package/dist/src/network/network-manager-service.js.map +1 -1
  80. package/dist/src/network/relay-reservation.d.ts +170 -0
  81. package/dist/src/network/relay-reservation.d.ts.map +1 -0
  82. package/dist/src/network/relay-reservation.js +527 -0
  83. package/dist/src/network/relay-reservation.js.map +1 -0
  84. package/dist/src/pend-validation.d.ts +3 -2
  85. package/dist/src/pend-validation.d.ts.map +1 -1
  86. package/dist/src/pend-validation.js +3 -2
  87. package/dist/src/pend-validation.js.map +1 -1
  88. package/dist/src/protocol-client.d.ts +8 -1
  89. package/dist/src/protocol-client.d.ts.map +1 -1
  90. package/dist/src/protocol-client.js +11 -0
  91. package/dist/src/protocol-client.js.map +1 -1
  92. package/dist/src/reactivity/forwarder-host.d.ts +2 -1
  93. package/dist/src/reactivity/forwarder-host.d.ts.map +1 -1
  94. package/dist/src/reactivity/forwarder-host.js +2 -1
  95. package/dist/src/reactivity/forwarder-host.js.map +1 -1
  96. package/dist/src/reactivity/origination-manager.d.ts +3 -3
  97. package/dist/src/reactivity/origination-manager.js +3 -3
  98. package/dist/src/reactivity/recover-transport.d.ts +22 -14
  99. package/dist/src/reactivity/recover-transport.d.ts.map +1 -1
  100. package/dist/src/reactivity/recover-transport.js +32 -19
  101. package/dist/src/reactivity/recover-transport.js.map +1 -1
  102. package/dist/src/reactivity/subscription-manager.d.ts +2 -2
  103. package/dist/src/reactivity/topic-bytes.d.ts +6 -7
  104. package/dist/src/reactivity/topic-bytes.d.ts.map +1 -1
  105. package/dist/src/reactivity/topic-bytes.js +8 -9
  106. package/dist/src/reactivity/topic-bytes.js.map +1 -1
  107. package/dist/src/repo/client.d.ts +5 -2
  108. package/dist/src/repo/client.d.ts.map +1 -1
  109. package/dist/src/repo/client.js +49 -40
  110. package/dist/src/repo/client.js.map +1 -1
  111. package/dist/src/repo/cluster-coordinator.d.ts +96 -11
  112. package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
  113. package/dist/src/repo/cluster-coordinator.js +164 -25
  114. package/dist/src/repo/cluster-coordinator.js.map +1 -1
  115. package/dist/src/repo/coordinator-repo.d.ts +240 -122
  116. package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
  117. package/dist/src/repo/coordinator-repo.js +627 -473
  118. package/dist/src/repo/coordinator-repo.js.map +1 -1
  119. package/dist/src/repo/i-under-replication-ledger.d.ts +87 -0
  120. package/dist/src/repo/i-under-replication-ledger.d.ts.map +1 -0
  121. package/dist/src/repo/i-under-replication-ledger.js +2 -0
  122. package/dist/src/repo/i-under-replication-ledger.js.map +1 -0
  123. package/dist/src/repo/kv-under-replication-ledger.d.ts +91 -0
  124. package/dist/src/repo/kv-under-replication-ledger.d.ts.map +1 -0
  125. package/dist/src/repo/kv-under-replication-ledger.js +287 -0
  126. package/dist/src/repo/kv-under-replication-ledger.js.map +1 -0
  127. package/dist/src/repo/responsibility.d.ts +37 -0
  128. package/dist/src/repo/responsibility.d.ts.map +1 -0
  129. package/dist/src/repo/responsibility.js +37 -0
  130. package/dist/src/repo/responsibility.js.map +1 -0
  131. package/dist/src/repo/service.d.ts +44 -15
  132. package/dist/src/repo/service.d.ts.map +1 -1
  133. package/dist/src/repo/service.js +64 -27
  134. package/dist/src/repo/service.js.map +1 -1
  135. package/dist/src/repo/stuck-reservation.d.ts +127 -0
  136. package/dist/src/repo/stuck-reservation.d.ts.map +1 -0
  137. package/dist/src/repo/stuck-reservation.js +179 -0
  138. package/dist/src/repo/stuck-reservation.js.map +1 -0
  139. package/dist/src/repo/under-replication-drain.d.ts +208 -0
  140. package/dist/src/repo/under-replication-drain.d.ts.map +1 -0
  141. package/dist/src/repo/under-replication-drain.js +461 -0
  142. package/dist/src/repo/under-replication-drain.js.map +1 -0
  143. package/dist/src/rn.d.ts +5 -0
  144. package/dist/src/rn.d.ts.map +1 -1
  145. package/dist/src/rn.js +5 -0
  146. package/dist/src/rn.js.map +1 -1
  147. package/dist/src/storage/block-latch.d.ts +3 -2
  148. package/dist/src/storage/block-latch.d.ts.map +1 -1
  149. package/dist/src/storage/block-latch.js +7 -4
  150. package/dist/src/storage/block-latch.js.map +1 -1
  151. package/dist/src/storage/block-storage.d.ts +45 -3
  152. package/dist/src/storage/block-storage.d.ts.map +1 -1
  153. package/dist/src/storage/block-storage.js +156 -9
  154. package/dist/src/storage/block-storage.js.map +1 -1
  155. package/dist/src/storage/i-block-storage.d.ts +57 -5
  156. package/dist/src/storage/i-block-storage.d.ts.map +1 -1
  157. package/dist/src/storage/i-block-storage.js.map +1 -1
  158. package/dist/src/storage/pending-claim.d.ts +167 -0
  159. package/dist/src/storage/pending-claim.d.ts.map +1 -0
  160. package/dist/src/storage/pending-claim.js +156 -0
  161. package/dist/src/storage/pending-claim.js.map +1 -0
  162. package/dist/src/storage/raw-store-codec.d.ts +13 -0
  163. package/dist/src/storage/raw-store-codec.d.ts.map +1 -1
  164. package/dist/src/storage/raw-store-codec.js +24 -3
  165. package/dist/src/storage/raw-store-codec.js.map +1 -1
  166. package/dist/src/storage/restoration-coordinator.d.ts.map +1 -1
  167. package/dist/src/storage/restoration-coordinator.js +2 -1
  168. package/dist/src/storage/restoration-coordinator.js.map +1 -1
  169. package/dist/src/storage/ring-shift-coordinator.d.ts.map +1 -1
  170. package/dist/src/storage/ring-shift-coordinator.js +3 -3
  171. package/dist/src/storage/ring-shift-coordinator.js.map +1 -1
  172. package/dist/src/storage/storage-repo.d.ts +122 -5
  173. package/dist/src/storage/storage-repo.d.ts.map +1 -1
  174. package/dist/src/storage/storage-repo.js +245 -68
  175. package/dist/src/storage/storage-repo.js.map +1 -1
  176. package/dist/src/storage/struct.d.ts +58 -0
  177. package/dist/src/storage/struct.d.ts.map +1 -1
  178. package/dist/src/testing/mesh-harness.d.ts +77 -11
  179. package/dist/src/testing/mesh-harness.d.ts.map +1 -1
  180. package/dist/src/testing/mesh-harness.js +226 -100
  181. package/dist/src/testing/mesh-harness.js.map +1 -1
  182. package/package.json +2 -2
  183. package/readme.md +27 -3
  184. package/src/cluster/block-transfer-service.ts +132 -4
  185. package/src/cluster/block-transfer.ts +79 -84
  186. package/src/cluster/client.ts +115 -119
  187. package/src/cluster/cluster-policy.ts +2 -1
  188. package/src/cluster/cluster-repo.ts +493 -65
  189. package/src/cluster/commit-proof.ts +2 -2
  190. package/src/cluster/race-resolution.ts +76 -7
  191. package/src/cluster/rebalance-monitor.ts +677 -539
  192. package/src/cluster/spread-on-churn.ts +34 -54
  193. package/src/cohort-topic/host.ts +8 -6
  194. package/src/cohort-topic/membership-source.ts +1 -1
  195. package/src/cohort-topic/peer-codec.ts +34 -31
  196. package/src/cohort-topic/stream-util.ts +39 -4
  197. package/src/cohort-topic/topic-router.ts +12 -2
  198. package/src/dispute/client.ts +17 -7
  199. package/src/index.ts +64 -59
  200. package/src/libp2p-key-network.ts +1377 -1260
  201. package/src/libp2p-node-base.ts +186 -27
  202. package/src/libp2p-node-rn.ts +31 -30
  203. package/src/libp2p-node.ts +37 -36
  204. package/src/logger.ts +6 -1
  205. package/src/matchmaking/query-transport.ts +9 -9
  206. package/src/network/network-manager-service.ts +16 -5
  207. package/src/network/relay-reservation.ts +636 -0
  208. package/src/pend-validation.ts +3 -2
  209. package/src/protocol-client.ts +13 -1
  210. package/src/reactivity/forwarder-host.ts +2 -1
  211. package/src/reactivity/origination-manager.ts +3 -3
  212. package/src/reactivity/recover-transport.ts +39 -22
  213. package/src/reactivity/subscription-manager.ts +2 -2
  214. package/src/reactivity/topic-bytes.ts +35 -37
  215. package/src/repo/client.ts +172 -173
  216. package/src/repo/cluster-coordinator.ts +183 -25
  217. package/src/repo/coordinator-repo.ts +656 -506
  218. package/src/repo/i-under-replication-ledger.ts +89 -0
  219. package/src/repo/kv-under-replication-ledger.ts +302 -0
  220. package/src/repo/responsibility.ts +48 -0
  221. package/src/repo/service.ts +76 -39
  222. package/src/repo/stuck-reservation.ts +224 -0
  223. package/src/repo/under-replication-drain.ts +582 -0
  224. package/src/rn.ts +74 -69
  225. package/src/storage/block-latch.ts +7 -4
  226. package/src/storage/block-storage.ts +161 -10
  227. package/src/storage/i-block-storage.ts +60 -5
  228. package/src/storage/pending-claim.ts +200 -0
  229. package/src/storage/raw-store-codec.ts +26 -3
  230. package/src/storage/restoration-coordinator.ts +212 -211
  231. package/src/storage/ring-shift-coordinator.ts +271 -271
  232. package/src/storage/storage-repo.ts +280 -72
  233. package/src/storage/struct.ts +58 -0
  234. package/src/testing/mesh-harness.ts +293 -112
@@ -1,119 +1,115 @@
1
- import type { PeerId, IPeerNetwork, ICluster, ClusterRecord } from '@optimystic/db-core';
2
- import { blockIdToBytes, blockIdsForTransforms } from '@optimystic/db-core';
3
- import { ProtocolClient } from '../protocol-client.js';
4
- import { peerIdFromString } from '@libp2p/peer-id';
5
- import { isClusterErrorEnvelope, clusterErrorFromEnvelope } from './cluster-error.js';
6
- import type { RedirectPayload } from '../repo/redirect.js';
7
- import { mergeRecordPeerAddresses } from '../peer-address-book.js';
8
- import { createLogger } from '../logger.js';
9
- import { withRpcDeadlineDefaults, type RpcDeadlineOptions } from '../rpc-deadline.js';
10
- import { MAX_CONTROL_MESSAGE_BYTES } from '../protocol-limits.js';
11
-
12
- const log = createLogger('cluster-client');
13
-
14
- export class ClusterClient extends ProtocolClient implements ICluster {
15
- private constructor(peerId: PeerId, peerNetwork: IPeerNetwork, readonly protocolPrefix?: string) {
16
- super(peerId, peerNetwork);
17
- }
18
-
19
- /** Create a new client instance */
20
- public static create(peerId: PeerId, peerNetwork: IPeerNetwork, protocolPrefix?: string): ClusterClient {
21
- return new ClusterClient(peerId, peerNetwork, protocolPrefix);
22
- }
23
-
24
- async update(record: ClusterRecord, hop: number = 0, options?: RpcDeadlineOptions): Promise<ClusterRecord> {
25
- const message = {
26
- operation: 'update',
27
- record
28
- };
29
- // Every node registers the cluster service under its network-prefixed
30
- // protocol id (`/optimystic/<network>/cluster/1.0.0`); the bare
31
- // `/db-p2p/cluster/1.0.0` is never registered, so there is no legacy
32
- // fallback to attempt — a single dial whose error propagates directly.
33
- const protocol = (this.protocolPrefix ?? '/db-p2p') + '/cluster/1.0.0';
34
- // Apply the client-level dial/response deadline so a silent cluster peer
35
- // can't hang the coordinator forever; an explicit caller override wins.
36
- // Response is a ClusterRecord (peer set + signatures + small metadata) → control cap.
37
- const response = await this.processMessage<unknown>(message, protocol, { ...withRpcDeadlineDefaults(options), maxDataLength: MAX_CONTROL_MESSAGE_BYTES });
38
-
39
- // A member that threw inside `update` replies with a structured error
40
- // envelope rather than aborting the stream; rethrow the server's real
41
- // error (preserving name/code) so the coordinator sees the true cause.
42
- if (isClusterErrorEnvelope(response)) {
43
- throw clusterErrorFromEnvelope(response);
44
- }
45
-
46
- // Type the redirect branch against the payload the service actually produces
47
- // (`RedirectPayload`), not a hand-written narrower shape — the old `{ id: string }`
48
- // literal is what structurally discarded the `addrs` the sender took the trouble to embed.
49
- const redirectResponse = response as Partial<RedirectPayload>;
50
- if (redirectResponse?.redirect?.peers?.length) {
51
- if (hop >= 2) {
52
- throw new Error('Redirect loop detected in ClusterClient (max hops reached)')
53
- }
54
- const currentIdStr = this.peerId.toString()
55
- const next = redirectResponse.redirect.peers.find((p) => p.id !== currentIdStr) ?? redirectResponse.redirect.peers[0]!
56
- const nextId = peerIdFromString(next.id)
57
- if (next.id === currentIdStr) {
58
- throw new Error('Redirect loop detected in ClusterClient (same peer)')
59
- }
60
- // Learn the redirect target's addresses BEFORE dialing it: the redirect is the only
61
- // notice we get that this peer matters, and if it is relay-only we have no address
62
- // for it at all. Merging after the dial would help only some later hop.
63
- this.peerNetwork.recordPeerAddresses?.(nextId, next.addrs ?? [])
64
- await this.recordCoordinatorForRecordIfSupported(record, nextId)
65
- const nextClient = ClusterClient.create(nextId, this.peerNetwork, this.protocolPrefix)
66
- // Thread the caller's *original* options through the redirect hop (the
67
- // recursive call re-applies its own defaults) so the deadline survives a redirect.
68
- return await nextClient.update(record, hop + 1, options)
69
- }
70
- const updated = response as ClusterRecord;
71
- // The member's reply carries the cohort's peer map too, and it may name addresses this
72
- // node lacks — the coordinator's own half of the same exchange the service handles inbound.
73
- this.recordRecordPeerAddresses(updated)
74
- return updated;
75
- }
76
-
77
- /**
78
- * Offer every address a cluster record carries for its cohort to the dialer's address book.
79
- *
80
- * Shares its traversal — and its cap on how many peers one record may introduce — with the
81
- * inbound side (`ClusterService.learnPeerAddresses`); validation, the per-peer cap, and the
82
- * trust boundary live in the implementation behind `recordPeerAddresses`. No self-filter is
83
- * passed: `this.peerId` is the peer we DIALED, not this node, and only the sink knows the
84
- * local node's identity.
85
- */
86
- private recordRecordPeerAddresses(record: ClusterRecord): void {
87
- const sink = this.peerNetwork.recordPeerAddresses
88
- if (!sink) return
89
- mergeRecordPeerAddresses(record?.peers, (pid, addrs) => sink.call(this.peerNetwork, pid, addrs), log)
90
- }
91
-
92
- /**
93
- * Record a coordinator-affinity hint for the block this record's op is
94
- * coordinated on, so a follow-up op can dial `peerId` directly.
95
- *
96
- * The op lives at `record.message.operations[0]` — `record.message` is a
97
- * RepoMessage `{ operations: [...] }`, NOT a bare `{ commit }`/`{ pend }`.
98
- * Reading `record.message.commit`/`.pend` (the old code) always saw
99
- * `undefined`, so this hint was dead code and never recorded anything.
100
- *
101
- * The key is `blockIdToBytes(<the coordinated block id>)` — the sha256 digest
102
- * findCoordinator/recordCoordinator key the cache on NOT raw utf8 of the id
103
- * (which would never be retrieved). Commit anchors on blockIds[0] (where
104
- * CoordinatorRepo runs consensus + verifyResponsibility), not tailId. Pend
105
- * anchors on a real block id (blockIdsForTransforms), not a structural
106
- * transforms field name. ClusterClient only ever carries commit/pend.
107
- */
108
- private async recordCoordinatorForRecordIfSupported(record: ClusterRecord, peerId: PeerId): Promise<void> {
109
- const op = record.message.operations[0]
110
- if (!op) return
111
- let id: string | undefined
112
- if ('commit' in op) id = op.commit.blockIds[0]
113
- else if ('pend' in op) id = blockIdsForTransforms(op.pend.transforms)[0]
114
- if (id == null) return
115
- const kbytes = await blockIdToBytes(id)
116
- const pn: any = this.peerNetwork as any
117
- if (typeof pn?.recordCoordinator === 'function') pn.recordCoordinator(kbytes, peerId)
118
- }
119
- }
1
+ import type { PeerId, IPeerNetwork, ICluster, ClusterRecord } from '@optimystic/db-core';
2
+ import { blockIdsForTransforms } from '@optimystic/db-core';
3
+ import { ProtocolClient } from '../protocol-client.js';
4
+ import { peerIdFromString } from '@libp2p/peer-id';
5
+ import { isClusterErrorEnvelope, clusterErrorFromEnvelope } from './cluster-error.js';
6
+ import type { RedirectPayload } from '../repo/redirect.js';
7
+ import { mergeRecordPeerAddresses } from '../peer-address-book.js';
8
+ import { createLogger } from '../logger.js';
9
+ import { withRpcDeadlineDefaults, type RpcDeadlineOptions } from '../rpc-deadline.js';
10
+ import { MAX_CONTROL_MESSAGE_BYTES } from '../protocol-limits.js';
11
+
12
+ const log = createLogger('cluster-client');
13
+
14
+ export class ClusterClient extends ProtocolClient implements ICluster {
15
+ private constructor(peerId: PeerId, peerNetwork: IPeerNetwork, readonly protocolPrefix?: string) {
16
+ super(peerId, peerNetwork);
17
+ }
18
+
19
+ /** Create a new client instance */
20
+ public static create(peerId: PeerId, peerNetwork: IPeerNetwork, protocolPrefix?: string): ClusterClient {
21
+ return new ClusterClient(peerId, peerNetwork, protocolPrefix);
22
+ }
23
+
24
+ async update(record: ClusterRecord, hop: number = 0, options?: RpcDeadlineOptions): Promise<ClusterRecord> {
25
+ const message = {
26
+ operation: 'update',
27
+ record
28
+ };
29
+ // Every node registers the cluster service under its network-prefixed
30
+ // protocol id (`/optimystic/<network>/cluster/1.0.0`); the bare
31
+ // `/db-p2p/cluster/1.0.0` is never registered, so there is no legacy
32
+ // fallback to attempt — a single dial whose error propagates directly.
33
+ const protocol = (this.protocolPrefix ?? '/db-p2p') + '/cluster/1.0.0';
34
+ // Apply the client-level dial/response deadline so a silent cluster peer
35
+ // can't hang the coordinator forever; an explicit caller override wins.
36
+ // Response is a ClusterRecord (peer set + signatures + small metadata) → control cap.
37
+ const response = await this.processMessage<unknown>(message, protocol, { ...withRpcDeadlineDefaults(options), maxDataLength: MAX_CONTROL_MESSAGE_BYTES });
38
+
39
+ // A member that threw inside `update` replies with a structured error
40
+ // envelope rather than aborting the stream; rethrow the server's real
41
+ // error (preserving name/code) so the coordinator sees the true cause.
42
+ if (isClusterErrorEnvelope(response)) {
43
+ throw clusterErrorFromEnvelope(response);
44
+ }
45
+
46
+ // Type the redirect branch against the payload the service actually produces
47
+ // (`RedirectPayload`), not a hand-written narrower shape — the old `{ id: string }`
48
+ // literal is what structurally discarded the `addrs` the sender took the trouble to embed.
49
+ const redirectResponse = response as Partial<RedirectPayload>;
50
+ if (redirectResponse?.redirect?.peers?.length) {
51
+ if (hop >= 2) {
52
+ throw new Error('Redirect loop detected in ClusterClient (max hops reached)')
53
+ }
54
+ const currentIdStr = this.peerId.toString()
55
+ const next = redirectResponse.redirect.peers.find((p) => p.id !== currentIdStr) ?? redirectResponse.redirect.peers[0]!
56
+ const nextId = peerIdFromString(next.id)
57
+ if (next.id === currentIdStr) {
58
+ throw new Error('Redirect loop detected in ClusterClient (same peer)')
59
+ }
60
+ // Learn the redirect target's addresses BEFORE dialing it: the redirect is the only
61
+ // notice we get that this peer matters, and if it is relay-only we have no address
62
+ // for it at all. Merging after the dial would help only some later hop.
63
+ this.peerNetwork.recordPeerAddresses?.(nextId, next.addrs ?? [])
64
+ this.recordCoordinatorForRecordIfSupported(record, nextId)
65
+ const nextClient = ClusterClient.create(nextId, this.peerNetwork, this.protocolPrefix)
66
+ // Thread the caller's *original* options through the redirect hop (the
67
+ // recursive call re-applies its own defaults) so the deadline survives a redirect.
68
+ return await nextClient.update(record, hop + 1, options)
69
+ }
70
+ const updated = response as ClusterRecord;
71
+ // The member's reply carries the cohort's peer map too, and it may name addresses this
72
+ // node lacks — the coordinator's own half of the same exchange the service handles inbound.
73
+ this.recordRecordPeerAddresses(updated)
74
+ return updated;
75
+ }
76
+
77
+ /**
78
+ * Offer every address a cluster record carries for its cohort to the dialer's address book.
79
+ *
80
+ * Shares its traversal — and its cap on how many peers one record may introduce — with the
81
+ * inbound side (`ClusterService.learnPeerAddresses`); validation, the per-peer cap, and the
82
+ * trust boundary live in the implementation behind `recordPeerAddresses`. No self-filter is
83
+ * passed: `this.peerId` is the peer we DIALED, not this node, and only the sink knows the
84
+ * local node's identity.
85
+ */
86
+ private recordRecordPeerAddresses(record: ClusterRecord): void {
87
+ const sink = this.peerNetwork.recordPeerAddresses
88
+ if (!sink) return
89
+ mergeRecordPeerAddresses(record?.peers, (pid, addrs) => sink.call(this.peerNetwork, pid, addrs), log)
90
+ }
91
+
92
+ /**
93
+ * Record a coordinator-affinity hint for the block this record's op is
94
+ * coordinated on, so a follow-up op can dial `peerId` directly.
95
+ *
96
+ * The op lives at `record.message.operations[0]` — `record.message` is a
97
+ * RepoMessage `{ operations: [...] }`, NOT a bare `{ commit }`/`{ pend }`.
98
+ * Reading `record.message.commit`/`.pend` (the old code) always saw
99
+ * `undefined`, so this hint was dead code and never recorded anything.
100
+ *
101
+ * Commit anchors on blockIds[0] (where CoordinatorRepo runs consensus +
102
+ * verifyResponsibility), not tailId. Pend anchors on a real block id
103
+ * (blockIdsForTransforms), not a structural transforms field name.
104
+ * ClusterClient only ever carries commit/pend.
105
+ */
106
+ private recordCoordinatorForRecordIfSupported(record: ClusterRecord, peerId: PeerId): void {
107
+ const op = record.message.operations[0]
108
+ if (!op) return
109
+ let id: string | undefined
110
+ if ('commit' in op) id = op.commit.blockIds[0]
111
+ else if ('pend' in op) id = blockIdsForTransforms(op.pend.transforms)[0]
112
+ if (id == null) return
113
+ this.recordCoordinatorHint(id, peerId)
114
+ }
115
+ }
@@ -150,7 +150,8 @@ export interface ClusterPolicyOptions {
150
150
  repairCorroborationClusterSize?: number;
151
151
  /**
152
152
  * What a validator-configured member does with a pend that carries no `validation` payload
153
- * (nothing to re-execute — the single-collection `Collection.sync` shape). Default 'accept';
153
+ * (nothing to re-execute — a single-collection `Collection.sync` pend, or the Quereus
154
+ * adapter's legacy multi-tree commit, which pends through an unvalidated coordinator). Default 'accept';
154
155
  * see {@link UnvalidatablePendPolicy}.
155
156
  */
156
157
  unvalidatablePendPolicy?: UnvalidatablePendPolicy;