@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
@@ -1,88 +1,119 @@
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 { withRpcDeadlineDefaults, type RpcDeadlineOptions } from '../rpc-deadline.js';
7
- import { MAX_CONTROL_MESSAGE_BYTES } from '../protocol-limits.js';
8
-
9
- export class ClusterClient extends ProtocolClient implements ICluster {
10
- private constructor(peerId: PeerId, peerNetwork: IPeerNetwork, readonly protocolPrefix?: string) {
11
- super(peerId, peerNetwork);
12
- }
13
-
14
- /** Create a new client instance */
15
- public static create(peerId: PeerId, peerNetwork: IPeerNetwork, protocolPrefix?: string): ClusterClient {
16
- return new ClusterClient(peerId, peerNetwork, protocolPrefix);
17
- }
18
-
19
- async update(record: ClusterRecord, hop: number = 0, options?: RpcDeadlineOptions): Promise<ClusterRecord> {
20
- const message = {
21
- operation: 'update',
22
- record
23
- };
24
- // Every node registers the cluster service under its network-prefixed
25
- // protocol id (`/optimystic/<network>/cluster/1.0.0`); the bare
26
- // `/db-p2p/cluster/1.0.0` is never registered, so there is no legacy
27
- // fallback to attempt — a single dial whose error propagates directly.
28
- const protocol = (this.protocolPrefix ?? '/db-p2p') + '/cluster/1.0.0';
29
- // Apply the client-level dial/response deadline so a silent cluster peer
30
- // can't hang the coordinator forever; an explicit caller override wins.
31
- // Response is a ClusterRecord (peer set + signatures + small metadata) → control cap.
32
- const response = await this.processMessage<unknown>(message, protocol, { ...withRpcDeadlineDefaults(options), maxDataLength: MAX_CONTROL_MESSAGE_BYTES });
33
-
34
- // A member that threw inside `update` replies with a structured error
35
- // envelope rather than aborting the stream; rethrow the server's real
36
- // error (preserving name/code) so the coordinator sees the true cause.
37
- if (isClusterErrorEnvelope(response)) {
38
- throw clusterErrorFromEnvelope(response);
39
- }
40
-
41
- const redirectResponse = response as { redirect?: { peers?: Array<{ id: string }> } };
42
- if (redirectResponse?.redirect?.peers?.length) {
43
- if (hop >= 2) {
44
- throw new Error('Redirect loop detected in ClusterClient (max hops reached)')
45
- }
46
- const currentIdStr = this.peerId.toString()
47
- const next = redirectResponse.redirect.peers.find((p) => p.id !== currentIdStr) ?? redirectResponse.redirect.peers[0]!
48
- const nextId = peerIdFromString(next.id)
49
- if (next.id === currentIdStr) {
50
- throw new Error('Redirect loop detected in ClusterClient (same peer)')
51
- }
52
- await this.recordCoordinatorForRecordIfSupported(record, nextId)
53
- const nextClient = ClusterClient.create(nextId, this.peerNetwork, this.protocolPrefix)
54
- // Thread the caller's *original* options through the redirect hop (the
55
- // recursive call re-applies its own defaults) so the deadline survives a redirect.
56
- return await nextClient.update(record, hop + 1, options)
57
- }
58
- return response as ClusterRecord;
59
- }
60
-
61
- /**
62
- * Record a coordinator-affinity hint for the block this record's op is
63
- * coordinated on, so a follow-up op can dial `peerId` directly.
64
- *
65
- * The op lives at `record.message.operations[0]` — `record.message` is a
66
- * RepoMessage `{ operations: [...] }`, NOT a bare `{ commit }`/`{ pend }`.
67
- * Reading `record.message.commit`/`.pend` (the old code) always saw
68
- * `undefined`, so this hint was dead code and never recorded anything.
69
- *
70
- * The key is `blockIdToBytes(<the coordinated block id>)` — the sha256 digest
71
- * findCoordinator/recordCoordinator key the cache on NOT raw utf8 of the id
72
- * (which would never be retrieved). Commit anchors on blockIds[0] (where
73
- * CoordinatorRepo runs consensus + verifyResponsibility), not tailId. Pend
74
- * anchors on a real block id (blockIdsForTransforms), not a structural
75
- * transforms field name. ClusterClient only ever carries commit/pend.
76
- */
77
- private async recordCoordinatorForRecordIfSupported(record: ClusterRecord, peerId: PeerId): Promise<void> {
78
- const op = record.message.operations[0]
79
- if (!op) return
80
- let id: string | undefined
81
- if ('commit' in op) id = op.commit.blockIds[0]
82
- else if ('pend' in op) id = blockIdsForTransforms(op.pend.transforms)[0]
83
- if (id == null) return
84
- const kbytes = await blockIdToBytes(id)
85
- const pn: any = this.peerNetwork as any
86
- if (typeof pn?.recordCoordinator === 'function') pn.recordCoordinator(kbytes, peerId)
87
- }
88
- }
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,64 +1,64 @@
1
- /**
2
- * Structured error envelope for the cluster protocol.
3
- *
4
- * When a cluster member throws while processing an `update` (validation,
5
- * signature, merge, or consensus failure), the service serializes the error as
6
- * this envelope and closes the stream normally instead of aborting it. The
7
- * coordinator's {@link ClusterClient} detects the envelope and rethrows a real
8
- * `Error` carrying the server's message/name/code — so the genuine cause is
9
- * visible on the coordinator side (which already enables `optimystic:*` debug)
10
- * rather than collapsing into an opaque `StreamResetError`.
11
- *
12
- * The marker key `__clusterError` cannot collide with a successful response: a
13
- * {@link ClusterRecord} never carries a top-level field by this name, and the
14
- * redirect payload is keyed under `redirect`.
15
- */
16
-
17
- /** Top-level key that marks a response as a structured cluster error. */
18
- export const CLUSTER_ERROR_KEY = '__clusterError';
19
-
20
- /** Serializable error detail carried in a {@link ClusterErrorEnvelope}. */
21
- export interface ClusterErrorDetail {
22
- message: string;
23
- name: string;
24
- /** Stable error code (e.g. libp2p `ERR_*`) when the source error carries one. */
25
- code?: string;
26
- }
27
-
28
- /** Wire shape of a structured cluster error response. */
29
- export interface ClusterErrorEnvelope {
30
- __clusterError: ClusterErrorDetail;
31
- }
32
-
33
- /** Build a {@link ClusterErrorEnvelope} from any thrown value. */
34
- export function toClusterErrorEnvelope(err: unknown): ClusterErrorEnvelope {
35
- const error = err instanceof Error ? err : new Error(String(err));
36
- const rawCode = (error as { code?: unknown }).code;
37
- const detail: ClusterErrorDetail = {
38
- message: error.message,
39
- name: error.name
40
- };
41
- if (typeof rawCode === 'string' && rawCode.length > 0) {
42
- detail.code = rawCode;
43
- }
44
- return { __clusterError: detail };
45
- }
46
-
47
- /** Type guard: is `value` a structured cluster error envelope (not a record/redirect)? */
48
- export function isClusterErrorEnvelope(value: unknown): value is ClusterErrorEnvelope {
49
- if (typeof value !== 'object' || value === null) return false;
50
- const detail = (value as { [CLUSTER_ERROR_KEY]?: unknown })[CLUSTER_ERROR_KEY];
51
- return typeof detail === 'object' && detail !== null;
52
- }
53
-
54
- /**
55
- * Reconstruct a real `Error` from a cluster error envelope, preserving `name`
56
- * and `code` so the coordinator's reputation/penalty classification still works.
57
- */
58
- export function clusterErrorFromEnvelope(envelope: ClusterErrorEnvelope): Error {
59
- const detail = envelope[CLUSTER_ERROR_KEY];
60
- const error = new Error(detail.message || 'cluster update failed');
61
- if (detail.name) error.name = detail.name;
62
- if (detail.code) (error as { code?: string }).code = detail.code;
63
- return error;
64
- }
1
+ /**
2
+ * Structured error envelope for the cluster protocol.
3
+ *
4
+ * When a cluster member throws while processing an `update` (validation,
5
+ * signature, merge, or consensus failure), the service serializes the error as
6
+ * this envelope and closes the stream normally instead of aborting it. The
7
+ * coordinator's {@link ClusterClient} detects the envelope and rethrows a real
8
+ * `Error` carrying the server's message/name/code — so the genuine cause is
9
+ * visible on the coordinator side (which already enables `optimystic:*` debug)
10
+ * rather than collapsing into an opaque `StreamResetError`.
11
+ *
12
+ * The marker key `__clusterError` cannot collide with a successful response: a
13
+ * {@link ClusterRecord} never carries a top-level field by this name, and the
14
+ * redirect payload is keyed under `redirect`.
15
+ */
16
+
17
+ /** Top-level key that marks a response as a structured cluster error. */
18
+ export const CLUSTER_ERROR_KEY = '__clusterError';
19
+
20
+ /** Serializable error detail carried in a {@link ClusterErrorEnvelope}. */
21
+ export interface ClusterErrorDetail {
22
+ message: string;
23
+ name: string;
24
+ /** Stable error code (e.g. libp2p `ERR_*`) when the source error carries one. */
25
+ code?: string;
26
+ }
27
+
28
+ /** Wire shape of a structured cluster error response. */
29
+ export interface ClusterErrorEnvelope {
30
+ __clusterError: ClusterErrorDetail;
31
+ }
32
+
33
+ /** Build a {@link ClusterErrorEnvelope} from any thrown value. */
34
+ export function toClusterErrorEnvelope(err: unknown): ClusterErrorEnvelope {
35
+ const error = err instanceof Error ? err : new Error(String(err));
36
+ const rawCode = (error as { code?: unknown }).code;
37
+ const detail: ClusterErrorDetail = {
38
+ message: error.message,
39
+ name: error.name
40
+ };
41
+ if (typeof rawCode === 'string' && rawCode.length > 0) {
42
+ detail.code = rawCode;
43
+ }
44
+ return { __clusterError: detail };
45
+ }
46
+
47
+ /** Type guard: is `value` a structured cluster error envelope (not a record/redirect)? */
48
+ export function isClusterErrorEnvelope(value: unknown): value is ClusterErrorEnvelope {
49
+ if (typeof value !== 'object' || value === null) return false;
50
+ const detail = (value as { [CLUSTER_ERROR_KEY]?: unknown })[CLUSTER_ERROR_KEY];
51
+ return typeof detail === 'object' && detail !== null;
52
+ }
53
+
54
+ /**
55
+ * Reconstruct a real `Error` from a cluster error envelope, preserving `name`
56
+ * and `code` so the coordinator's reputation/penalty classification still works.
57
+ */
58
+ export function clusterErrorFromEnvelope(envelope: ClusterErrorEnvelope): Error {
59
+ const detail = envelope[CLUSTER_ERROR_KEY];
60
+ const error = new Error(detail.message || 'cluster update failed');
61
+ if (detail.name) error.name = detail.name;
62
+ if (detail.code) (error as { code?: string }).code = detail.code;
63
+ return error;
64
+ }