@optimystic/db-p2p 0.24.0 → 0.24.2
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.
- package/dist/src/cluster/service.d.ts +8 -0
- package/dist/src/cluster/service.d.ts.map +1 -1
- package/dist/src/cluster/service.js +16 -4
- package/dist/src/cluster/service.js.map +1 -1
- package/dist/src/cohort-topic/host.js +34 -11
- package/dist/src/cohort-topic/host.js.map +1 -1
- package/dist/src/cohort-topic/stream-util.d.ts +25 -11
- package/dist/src/cohort-topic/stream-util.d.ts.map +1 -1
- package/dist/src/cohort-topic/stream-util.js +31 -19
- package/dist/src/cohort-topic/stream-util.js.map +1 -1
- package/dist/src/libp2p-key-network.d.ts +68 -0
- package/dist/src/libp2p-key-network.d.ts.map +1 -1
- package/dist/src/libp2p-key-network.js +123 -14
- package/dist/src/libp2p-key-network.js.map +1 -1
- package/dist/src/libp2p-node-base.d.ts.map +1 -1
- package/dist/src/libp2p-node-base.js +8 -5
- package/dist/src/libp2p-node-base.js.map +1 -1
- package/dist/src/logger.d.ts +2 -2
- package/dist/src/logger.js +2 -2
- package/dist/src/matchmaking/query-transport.js +3 -3
- package/dist/src/matchmaking/query-transport.js.map +1 -1
- package/dist/src/peer-address-book.d.ts +69 -0
- package/dist/src/peer-address-book.d.ts.map +1 -1
- package/dist/src/peer-address-book.js +110 -15
- package/dist/src/peer-address-book.js.map +1 -1
- package/dist/src/reactivity/notify-transport.d.ts +4 -4
- package/dist/src/reactivity/notify-transport.js +6 -6
- package/dist/src/reactivity/notify-transport.js.map +1 -1
- package/dist/src/reactivity/push-state-gossip.js +2 -2
- package/dist/src/reactivity/push-state-gossip.js.map +1 -1
- package/dist/src/reactivity/recover-transport.d.ts +6 -2
- package/dist/src/reactivity/recover-transport.d.ts.map +1 -1
- package/dist/src/reactivity/recover-transport.js +7 -3
- package/dist/src/reactivity/recover-transport.js.map +1 -1
- package/dist/src/repo/service.d.ts +6 -0
- package/dist/src/repo/service.d.ts.map +1 -1
- package/dist/src/repo/service.js +12 -2
- package/dist/src/repo/service.js.map +1 -1
- package/dist/src/routing/libp2p-known-peers.d.ts.map +1 -1
- package/dist/src/routing/libp2p-known-peers.js +5 -0
- package/dist/src/routing/libp2p-known-peers.js.map +1 -1
- package/dist/src/testing/cohort-topic-mesh-harness.d.ts +13 -6
- package/dist/src/testing/cohort-topic-mesh-harness.d.ts.map +1 -1
- package/dist/src/testing/cohort-topic-mesh-harness.js +15 -6
- package/dist/src/testing/cohort-topic-mesh-harness.js.map +1 -1
- package/package.json +3 -3
- package/src/cluster/service.ts +305 -293
- package/src/cohort-topic/host.ts +2932 -2901
- package/src/cohort-topic/stream-util.ts +147 -135
- package/src/libp2p-key-network.ts +1235 -1120
- package/src/libp2p-node-base.ts +1678 -1675
- package/src/logger.ts +27 -27
- package/src/matchmaking/query-transport.ts +492 -492
- package/src/peer-address-book.ts +266 -149
- package/src/reactivity/notify-transport.ts +144 -144
- package/src/reactivity/push-state-gossip.ts +291 -291
- package/src/reactivity/recover-transport.ts +412 -408
- package/src/repo/service.ts +323 -313
- package/src/routing/libp2p-known-peers.ts +31 -26
- package/src/testing/cohort-topic-mesh-harness.ts +673 -663
package/src/cluster/service.ts
CHANGED
|
@@ -1,293 +1,305 @@
|
|
|
1
|
-
import { pipe } from 'it-pipe';
|
|
2
|
-
import { decode as lpDecode, encode as lpEncode } from 'it-length-prefixed';
|
|
3
|
-
import { peerIdFromString } from '@libp2p/peer-id';
|
|
4
|
-
import type { Startable, Logger, Stream, Connection, StreamHandler, PeerId } from '@libp2p/interface';
|
|
5
|
-
import type { ICluster, ClusterRecord } from '@optimystic/db-core';
|
|
6
|
-
import { encodePeers, type RedirectPayload } from '../repo/redirect.js';
|
|
7
|
-
import { toClusterErrorEnvelope } from './cluster-error.js';
|
|
8
|
-
import { mergeRecordPeerAddresses } from '../peer-address-book.js';
|
|
9
|
-
import { MAX_CONTROL_MESSAGE_BYTES } from '../protocol-limits.js';
|
|
10
|
-
import type { Uint8ArrayList } from 'uint8arraylist';
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
*
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
private readonly
|
|
69
|
-
private readonly
|
|
70
|
-
private readonly
|
|
71
|
-
private readonly
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
*
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
return
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
//
|
|
263
|
-
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
//
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
this.
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
1
|
+
import { pipe } from 'it-pipe';
|
|
2
|
+
import { decode as lpDecode, encode as lpEncode } from 'it-length-prefixed';
|
|
3
|
+
import { peerIdFromString } from '@libp2p/peer-id';
|
|
4
|
+
import type { Startable, Logger, Stream, Connection, StreamHandler, PeerId } from '@libp2p/interface';
|
|
5
|
+
import type { ICluster, ClusterRecord } from '@optimystic/db-core';
|
|
6
|
+
import { encodePeers, type RedirectPayload } from '../repo/redirect.js';
|
|
7
|
+
import { toClusterErrorEnvelope } from './cluster-error.js';
|
|
8
|
+
import { mergeRecordPeerAddresses, publishableConnectionAddr, type AddressLog, type DirectionalConnection } from '../peer-address-book.js';
|
|
9
|
+
import { MAX_CONTROL_MESSAGE_BYTES } from '../protocol-limits.js';
|
|
10
|
+
import type { Uint8ArrayList } from 'uint8arraylist';
|
|
11
|
+
import { createLogger } from '../logger.js';
|
|
12
|
+
import { createInboundStreamAuthorization, type InboundStreamAuthorization, type InboundStreamAuthorizationInit } from '../inbound-authorization.js';
|
|
13
|
+
|
|
14
|
+
interface BaseComponents {
|
|
15
|
+
logger: { forComponent: (name: string) => Logger },
|
|
16
|
+
registrar: {
|
|
17
|
+
handle: (protocol: string, handler: StreamHandler, options: any) => Promise<void>,
|
|
18
|
+
unhandle: (protocol: string) => Promise<void>
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ClusterServiceComponents extends BaseComponents {
|
|
23
|
+
cluster: ICluster
|
|
24
|
+
/**
|
|
25
|
+
* This node's own peer id, used to decide whether we are a member of a
|
|
26
|
+
* cluster record's peer set. When absent the service cannot scope membership
|
|
27
|
+
* and processes every update locally (no redirect).
|
|
28
|
+
*/
|
|
29
|
+
peerId?: PeerId
|
|
30
|
+
/**
|
|
31
|
+
* Optional resolver for a peer's dialable multiaddrs, used as a fallback when
|
|
32
|
+
* a redirect target has no multiaddrs embedded in `record.peers`.
|
|
33
|
+
*/
|
|
34
|
+
getConnectionAddrs?: (peerId: PeerId) => string[]
|
|
35
|
+
/**
|
|
36
|
+
* Optional sink for dialable addresses carried by an inbound cluster record, so this
|
|
37
|
+
* node can later dial a cohort sibling it has never had a connection to. Omitted →
|
|
38
|
+
* no address learning (the pre-existing behavior).
|
|
39
|
+
*/
|
|
40
|
+
recordPeerAddresses?: (peerId: PeerId, multiaddrs: string[]) => void
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ClusterServiceInit extends InboundStreamAuthorizationInit {
|
|
44
|
+
protocol?: string,
|
|
45
|
+
protocolPrefix?: string,
|
|
46
|
+
maxInboundStreams?: number,
|
|
47
|
+
maxOutboundStreams?: number,
|
|
48
|
+
logPrefix?: string,
|
|
49
|
+
/**
|
|
50
|
+
* Responsibility K - the replica set size for determining cluster membership.
|
|
51
|
+
* When the cluster record's peer set is smaller than this, the mesh is treated
|
|
52
|
+
* as "small" and the update is processed locally regardless of membership. When
|
|
53
|
+
* the peer set is at least this size and we are not a member, the update is
|
|
54
|
+
* redirected to the responsible peers.
|
|
55
|
+
* Default: 1 (only members process; any larger non-member set redirects)
|
|
56
|
+
*/
|
|
57
|
+
responsibilityK?: number,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function clusterService(init: ClusterServiceInit = {}): (components: ClusterServiceComponents) => ClusterService {
|
|
61
|
+
return (components: ClusterServiceComponents) => new ClusterService(components, init);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A libp2p service that handles cluster protocol messages
|
|
66
|
+
*/
|
|
67
|
+
export class ClusterService implements Startable {
|
|
68
|
+
private readonly protocol: string;
|
|
69
|
+
private readonly maxInboundStreams: number;
|
|
70
|
+
private readonly maxOutboundStreams: number;
|
|
71
|
+
private readonly log: Logger;
|
|
72
|
+
/**
|
|
73
|
+
* Sink for this service's `peer-address-book:*` lines. Deliberately NOT `this.log.error`, which
|
|
74
|
+
* lands them under libp2p's `db-p2p:cluster:error` namespace — invisible to the
|
|
75
|
+
* `DEBUG=optimystic:db-p2p:*` filter this package's docs recommend, and the reason
|
|
76
|
+
* gotchoices/Optimystic#12 read a zero log count as proof the mechanism never ran. One tag
|
|
77
|
+
* family, one namespace tree.
|
|
78
|
+
*/
|
|
79
|
+
private readonly addressLog: AddressLog;
|
|
80
|
+
private readonly cluster: ICluster;
|
|
81
|
+
private readonly components: ClusterServiceComponents;
|
|
82
|
+
private running: boolean;
|
|
83
|
+
/** Responsibility K - small-mesh bypass threshold for redirect decisions */
|
|
84
|
+
private readonly responsibilityK: number;
|
|
85
|
+
/** Optional embedder authorization gate; `undefined` (the default) means no check runs. */
|
|
86
|
+
private readonly authorization: InboundStreamAuthorization | undefined;
|
|
87
|
+
|
|
88
|
+
constructor(components: ClusterServiceComponents, init: ClusterServiceInit = {}) {
|
|
89
|
+
this.components = components;
|
|
90
|
+
this.protocol = init.protocol ?? (init.protocolPrefix ?? '/db-p2p') + '/cluster/1.0.0';
|
|
91
|
+
this.maxInboundStreams = init.maxInboundStreams ?? 32;
|
|
92
|
+
this.maxOutboundStreams = init.maxOutboundStreams ?? 64;
|
|
93
|
+
this.log = components.logger.forComponent(init.logPrefix ?? 'db-p2p:cluster');
|
|
94
|
+
this.addressLog = createLogger('peer-address-book', components.peerId?.toString());
|
|
95
|
+
this.cluster = components.cluster;
|
|
96
|
+
this.running = false;
|
|
97
|
+
this.responsibilityK = init.responsibilityK ?? 1;
|
|
98
|
+
this.authorization = createInboundStreamAuthorization(init, this.protocol, (msg, ...args) => this.log.error(msg, ...args));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
readonly [Symbol.toStringTag] = '@libp2p/cluster';
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Best-effort read of `components.libp2p`. When `components` is libp2p's own Proxy, the getter
|
|
105
|
+
* THROWS `MissingServiceError('libp2p not set')` for any key it does not hold — and `libp2p` is
|
|
106
|
+
* not a component — so the read itself must be guarded; `?.` and a following null check are both
|
|
107
|
+
* too late. Every fallback below is a convenience for embedders that register this service
|
|
108
|
+
* directly; the production wiring supplies `peerId`/`getConnectionAddrs` explicitly.
|
|
109
|
+
*/
|
|
110
|
+
private getLibp2p(): any {
|
|
111
|
+
try {
|
|
112
|
+
return (this.components as any).libp2p;
|
|
113
|
+
} catch {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private getSelfId(): PeerId | undefined {
|
|
119
|
+
if (this.components.peerId) return this.components.peerId;
|
|
120
|
+
return this.getLibp2p()?.peerId as PeerId | undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private getPeerAddrs(id: string): string[] {
|
|
124
|
+
let pid: PeerId;
|
|
125
|
+
try {
|
|
126
|
+
pid = peerIdFromString(id);
|
|
127
|
+
} catch {
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
if (this.components.getConnectionAddrs) return this.components.getConnectionAddrs(pid);
|
|
131
|
+
const libp2p = this.getLibp2p();
|
|
132
|
+
if (!libp2p?.getConnections) return [];
|
|
133
|
+
// A redirect payload goes to a THIRD party, so only an outbound connection's remoteAddr
|
|
134
|
+
// qualifies — see `publishableConnectionAddr`.
|
|
135
|
+
const conns: DirectionalConnection[] = libp2p.getConnections(pid) ?? [];
|
|
136
|
+
const addrs: string[] = [];
|
|
137
|
+
for (const c of conns) {
|
|
138
|
+
const addr = publishableConnectionAddr(c, this.addressLog);
|
|
139
|
+
if (addr !== undefined) addrs.push(addr);
|
|
140
|
+
}
|
|
141
|
+
return addrs;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Decide whether this node should redirect a cluster update instead of
|
|
146
|
+
* participating in its consensus.
|
|
147
|
+
*
|
|
148
|
+
* Membership is scoped against `record.peers` — the authoritative set the
|
|
149
|
+
* coordinator already computed and embedded (it only ever dials peers in this
|
|
150
|
+
* set). Using it directly (rather than independently recomputing the cluster
|
|
151
|
+
* from the key) is regression-proof against the "empty promises" symptom: a
|
|
152
|
+
* peer the coordinator legitimately included is, by construction, present in
|
|
153
|
+
* `record.peers` and is therefore never redirected.
|
|
154
|
+
*
|
|
155
|
+
* Returns a {@link RedirectPayload} when this node is not responsible, or null
|
|
156
|
+
* when the update should be processed locally (we are a member, the mesh is too
|
|
157
|
+
* small to scope, or we lack the identity/peer set to make a decision).
|
|
158
|
+
*/
|
|
159
|
+
checkRedirect(record: ClusterRecord): RedirectPayload | null {
|
|
160
|
+
const selfId = this.getSelfId();
|
|
161
|
+
if (!selfId) return null; // no identity → can't scope, process locally
|
|
162
|
+
|
|
163
|
+
const peers = record.peers ?? {};
|
|
164
|
+
const peerIds = Object.keys(peers);
|
|
165
|
+
if (peerIds.length === 0) return null; // nothing to scope against → process locally
|
|
166
|
+
|
|
167
|
+
const selfStr = selfId.toString();
|
|
168
|
+
const isMember = peerIds.includes(selfStr);
|
|
169
|
+
const smallMesh = peerIds.length < this.responsibilityK;
|
|
170
|
+
|
|
171
|
+
if (!smallMesh && !isMember) {
|
|
172
|
+
const others = peerIds.filter(id => id !== selfStr);
|
|
173
|
+
return encodePeers(others.map(id => {
|
|
174
|
+
const recAddrs = peers[id]?.multiaddrs ?? [];
|
|
175
|
+
const addrs = recAddrs.length > 0 ? recAddrs : this.getPeerAddrs(id);
|
|
176
|
+
return { id, addrs };
|
|
177
|
+
}));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async start(): Promise<void> {
|
|
184
|
+
if (this.running) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
await this.components.registrar.handle(this.protocol, this.handleIncomingStream.bind(this), {
|
|
189
|
+
maxInboundStreams: this.maxInboundStreams,
|
|
190
|
+
maxOutboundStreams: this.maxOutboundStreams
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
this.running = true;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async stop(): Promise<void> {
|
|
197
|
+
if (!this.running) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
await this.components.registrar.unhandle(this.protocol);
|
|
202
|
+
this.running = false;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Run a single decoded protocol message. An application-level throw
|
|
207
|
+
* (validation / signature / merge / consensus failure inside `cluster.update`)
|
|
208
|
+
* propagates to the caller, which turns it into a structured error envelope;
|
|
209
|
+
* a redirect or a successful {@link ClusterRecord} is returned as-is.
|
|
210
|
+
*
|
|
211
|
+
* Public for the same reason {@link checkRedirect} is: it is the whole wire-ingress decision
|
|
212
|
+
* for a cluster update, and a test that reconstructs it by hand stops proving anything about
|
|
213
|
+
* the real ordering (address learning before redirect before consensus).
|
|
214
|
+
*/
|
|
215
|
+
async processOperation(message: { operation: string; record: ClusterRecord }): Promise<unknown> {
|
|
216
|
+
if (message.operation === 'update') {
|
|
217
|
+
// Learn the cohort's addresses FIRST — before both the redirect decision and
|
|
218
|
+
// local consensus, since either can go on to dial these same peers. libp2p only
|
|
219
|
+
// tells us the addresses of peers we are directly connected to, so for a cohort
|
|
220
|
+
// picked by key position this record is often the only place a relay-only
|
|
221
|
+
// sibling's address ever reaches us.
|
|
222
|
+
this.learnPeerAddresses(message.record);
|
|
223
|
+
// Scope consensus to responsible peers: redirect when we are not a
|
|
224
|
+
// member of the record's authoritative peer set, otherwise process.
|
|
225
|
+
const redirect = this.checkRedirect(message.record);
|
|
226
|
+
return redirect ?? await this.cluster.update(message.record);
|
|
227
|
+
}
|
|
228
|
+
throw new Error(`Unknown operation: ${message.operation}`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Offer every address the record carries for its cohort members to the node's address book.
|
|
233
|
+
*
|
|
234
|
+
* This runs on a record NOTHING has validated yet — before {@link checkRedirect} and before
|
|
235
|
+
* `cluster.update` checks a signature — and inbound stream authorization is opt-in, so the
|
|
236
|
+
* peer map here is whatever the dialer chose to send. The traversal (and the cap on how many
|
|
237
|
+
* peers one record may introduce) is therefore shared with `ClusterClient`, in
|
|
238
|
+
* `peer-address-book.ts`, along with the per-address validation and the trust boundary.
|
|
239
|
+
*/
|
|
240
|
+
private learnPeerAddresses(record: ClusterRecord): void {
|
|
241
|
+
const sink = this.components.recordPeerAddresses;
|
|
242
|
+
if (!sink) return;
|
|
243
|
+
mergeRecordPeerAddresses(
|
|
244
|
+
record.peers,
|
|
245
|
+
sink,
|
|
246
|
+
this.addressLog,
|
|
247
|
+
this.getSelfId()?.toString()
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private handleIncomingStream(stream: Stream, connection?: Connection): void {
|
|
252
|
+
const peerId = connection?.remotePeer;
|
|
253
|
+
|
|
254
|
+
const processStream = async function* (this: ClusterService, source: AsyncIterable<Uint8ArrayList>) {
|
|
255
|
+
for await (const msg of source) {
|
|
256
|
+
// Decode the framing. A malformed/undecodable message is a transport
|
|
257
|
+
// fault handled by the outer abort path, not an application error.
|
|
258
|
+
const decoded = new TextDecoder().decode(msg.subarray());
|
|
259
|
+
const message = JSON.parse(decoded) as { operation: string; record: ClusterRecord };
|
|
260
|
+
|
|
261
|
+
// Application-level processing: surface any throw to the coordinator as
|
|
262
|
+
// a structured error envelope (closing the stream normally) instead of
|
|
263
|
+
// aborting, so the real cause — not an opaque StreamResetError — reaches
|
|
264
|
+
// the coordinator, which already enables debug logging. The abort path
|
|
265
|
+
// is reserved for genuinely unrecoverable framing/transport faults.
|
|
266
|
+
let response: unknown;
|
|
267
|
+
try {
|
|
268
|
+
response = await this.processOperation(message);
|
|
269
|
+
} catch (err) {
|
|
270
|
+
this.log.error('error processing cluster %s from %p - %e', message.operation, peerId, err);
|
|
271
|
+
response = toClusterErrorEnvelope(err);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Encode and yield the response
|
|
275
|
+
yield new TextEncoder().encode(JSON.stringify(response));
|
|
276
|
+
// One request per stream: every real ClusterClient sends exactly one
|
|
277
|
+
// request per dial (see ProtocolClient.processMessage), so complete the
|
|
278
|
+
// generator after the first response. A second frame a peer queued is
|
|
279
|
+
// then never read or parsed. Mirrors sync/block-transfer.
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
void (async () => {
|
|
285
|
+
try {
|
|
286
|
+
// Authorization runs before ANY decoding or execution. Guarded on the field so a
|
|
287
|
+
// node without a predicate keeps the original path untouched.
|
|
288
|
+
if (this.authorization && await this.authorization.deny(stream, peerId?.toString())) return;
|
|
289
|
+
const responses = pipe(
|
|
290
|
+
stream,
|
|
291
|
+
(source) => lpDecode(source, { maxDataLength: MAX_CONTROL_MESSAGE_BYTES }),
|
|
292
|
+
processStream.bind(this),
|
|
293
|
+
(source) => lpEncode(source)
|
|
294
|
+
);
|
|
295
|
+
for await (const chunk of responses) {
|
|
296
|
+
stream.send(chunk);
|
|
297
|
+
}
|
|
298
|
+
await stream.close();
|
|
299
|
+
} catch (err) {
|
|
300
|
+
this.log.error('error handling cluster protocol message from %p - %e', peerId, err);
|
|
301
|
+
stream.abort(err instanceof Error ? err : new Error(String(err)));
|
|
302
|
+
}
|
|
303
|
+
})();
|
|
304
|
+
}
|
|
305
|
+
}
|