@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/repo/service.ts
CHANGED
|
@@ -1,313 +1,323 @@
|
|
|
1
|
-
import { pipe } from 'it-pipe'
|
|
2
|
-
import { decode as lpDecode, encode as lpEncode } from 'it-length-prefixed'
|
|
3
|
-
import type { Startable, Logger, Stream, Connection, StreamHandler, PeerId, Libp2p } from '@libp2p/interface'
|
|
4
|
-
import type { IRepo, RepoMessage } from '@optimystic/db-core'
|
|
5
|
-
import { blockIdsForTransforms } from '@optimystic/db-core'
|
|
6
|
-
import { peersEqual } from '../peer-utils.js'
|
|
7
|
-
import { encodePeers, type RedirectPayload } from './redirect.js'
|
|
8
|
-
import { MAX_BLOCK_MESSAGE_BYTES } from '../protocol-limits.js'
|
|
9
|
-
import type { Uint8ArrayList } from 'uint8arraylist'
|
|
10
|
-
import { createLogger } from '../logger.js'
|
|
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
|
-
|
|
69
|
-
private readonly
|
|
70
|
-
private readonly
|
|
71
|
-
private readonly
|
|
72
|
-
private readonly
|
|
73
|
-
private readonly
|
|
74
|
-
private
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
* libp2p
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
this.
|
|
97
|
-
|
|
98
|
-
this.
|
|
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
|
-
|
|
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
|
-
const
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
await
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
1
|
+
import { pipe } from 'it-pipe'
|
|
2
|
+
import { decode as lpDecode, encode as lpEncode } from 'it-length-prefixed'
|
|
3
|
+
import type { Startable, Logger, Stream, Connection, StreamHandler, PeerId, Libp2p } from '@libp2p/interface'
|
|
4
|
+
import type { IRepo, RepoMessage } from '@optimystic/db-core'
|
|
5
|
+
import { blockIdsForTransforms } from '@optimystic/db-core'
|
|
6
|
+
import { peersEqual } from '../peer-utils.js'
|
|
7
|
+
import { encodePeers, type RedirectPayload } from './redirect.js'
|
|
8
|
+
import { MAX_BLOCK_MESSAGE_BYTES } from '../protocol-limits.js'
|
|
9
|
+
import type { Uint8ArrayList } from 'uint8arraylist'
|
|
10
|
+
import { createLogger } from '../logger.js'
|
|
11
|
+
import { publishableConnectionAddr, type AddressLog, type DirectionalConnection } from '../peer-address-book.js'
|
|
12
|
+
import { createInboundStreamAuthorization, type InboundStreamAuthorization, type InboundStreamAuthorizationInit } from '../inbound-authorization.js'
|
|
13
|
+
|
|
14
|
+
const debugLog = createLogger('repo-service')
|
|
15
|
+
|
|
16
|
+
// Define Components interface
|
|
17
|
+
interface BaseComponents {
|
|
18
|
+
logger: { forComponent: (name: string) => Logger },
|
|
19
|
+
registrar: {
|
|
20
|
+
handle: (protocol: string, handler: StreamHandler, options: any) => Promise<void>
|
|
21
|
+
unhandle: (protocol: string) => Promise<void>
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface NetworkManagerLike {
|
|
26
|
+
getCluster(key: Uint8Array): Promise<PeerId[]>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type RepoServiceComponents = BaseComponents & {
|
|
30
|
+
repo: IRepo
|
|
31
|
+
networkManager?: NetworkManagerLike
|
|
32
|
+
peerId?: PeerId
|
|
33
|
+
getConnectionAddrs?: (peerId: PeerId) => string[]
|
|
34
|
+
/**
|
|
35
|
+
* Optional libp2p node. The production wiring injects the node post-construction
|
|
36
|
+
* via {@link RepoService.setLibp2p} (the `components.libp2p` proxy does not
|
|
37
|
+
* reliably resolve from inside a service at request time); this field is a
|
|
38
|
+
* best-effort fallback resolver used only when no node has been injected.
|
|
39
|
+
*/
|
|
40
|
+
libp2p?: Libp2p
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type RepoServiceInit = InboundStreamAuthorizationInit & {
|
|
44
|
+
protocol?: string,
|
|
45
|
+
protocolPrefix?: string,
|
|
46
|
+
maxInboundStreams?: number,
|
|
47
|
+
maxOutboundStreams?: number,
|
|
48
|
+
logPrefix?: string,
|
|
49
|
+
kBucketSize?: number,
|
|
50
|
+
/**
|
|
51
|
+
* Responsibility K - the replica set size for determining cluster membership.
|
|
52
|
+
* This is distinct from kBucketSize (DHT routing).
|
|
53
|
+
* When set, this determines how many peers (by XOR distance) are considered
|
|
54
|
+
* responsible for a key. If this node is not in the top responsibilityK peers,
|
|
55
|
+
* it will redirect requests to closer peers.
|
|
56
|
+
* Default: 1 (only the closest peer handles requests)
|
|
57
|
+
*/
|
|
58
|
+
responsibilityK?: number,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function repoService(init: RepoServiceInit = {}): (components: RepoServiceComponents) => RepoService {
|
|
62
|
+
return (components: RepoServiceComponents) => new RepoService(components, init);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A libp2p service that handles repo protocol messages
|
|
67
|
+
*/
|
|
68
|
+
export class RepoService implements Startable {
|
|
69
|
+
private readonly protocol: string
|
|
70
|
+
private readonly maxInboundStreams: number
|
|
71
|
+
private readonly maxOutboundStreams: number
|
|
72
|
+
private readonly log: Logger
|
|
73
|
+
private readonly repo: IRepo
|
|
74
|
+
private readonly components: RepoServiceComponents
|
|
75
|
+
private running: boolean
|
|
76
|
+
/** Responsibility K - how many peers are responsible for a key (for redirect decisions) */
|
|
77
|
+
private readonly responsibilityK: number
|
|
78
|
+
/**
|
|
79
|
+
* The libp2p node, injected post-construction by the node wiring (see
|
|
80
|
+
* libp2p-node-base.ts, mirroring how `networkManager`/`fret` receive theirs).
|
|
81
|
+
* The libp2p `components.libp2p` proxy does NOT reliably resolve from inside a
|
|
82
|
+
* service at request time, so the redirect path resolves the network manager,
|
|
83
|
+
* self identity, and connection addrs through this explicitly-set reference.
|
|
84
|
+
*/
|
|
85
|
+
private libp2pRef: Libp2p | undefined
|
|
86
|
+
/** Optional embedder authorization gate; `undefined` (the default) means no check runs. */
|
|
87
|
+
private readonly authorization: InboundStreamAuthorization | undefined
|
|
88
|
+
/**
|
|
89
|
+
* Sink for this service's `peer-address-book:*` lines — same reasoning as `ClusterService`'s:
|
|
90
|
+
* `this.log.error` would strand them under `db-p2p:repo-service:error`, outside the
|
|
91
|
+
* `optimystic:db-p2p:*` tree every other address-book line lives in.
|
|
92
|
+
*/
|
|
93
|
+
private readonly addressLog: AddressLog
|
|
94
|
+
|
|
95
|
+
constructor(components: RepoServiceComponents, init: RepoServiceInit = {}) {
|
|
96
|
+
this.components = components
|
|
97
|
+
const computed = init.protocol ?? (init.protocolPrefix ?? '/db-p2p') + '/repo/1.0.0'
|
|
98
|
+
this.protocol = computed
|
|
99
|
+
this.maxInboundStreams = init.maxInboundStreams ?? 32
|
|
100
|
+
this.maxOutboundStreams = init.maxOutboundStreams ?? 64
|
|
101
|
+
this.log = components.logger.forComponent(init.logPrefix ?? 'db-p2p:repo-service')
|
|
102
|
+
this.addressLog = createLogger('peer-address-book', components.peerId?.toString())
|
|
103
|
+
this.repo = components.repo
|
|
104
|
+
this.running = false
|
|
105
|
+
this.responsibilityK = init.responsibilityK ?? 1
|
|
106
|
+
this.authorization = createInboundStreamAuthorization(init, this.protocol, (msg, ...args) => this.log.error(msg, ...args))
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
readonly [Symbol.toStringTag] = '@libp2p/repo-service'
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Inject the running libp2p node. Called once post-construction by the node
|
|
113
|
+
* wiring so the redirect path can resolve the network manager / self id / addrs.
|
|
114
|
+
*/
|
|
115
|
+
setLibp2p(libp2p: Libp2p): void {
|
|
116
|
+
this.libp2pRef = libp2p
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Resolve the libp2p node: the injected ref first, then the (best-effort) components proxy. */
|
|
120
|
+
private getLibp2p(): Libp2p | undefined {
|
|
121
|
+
return this.libp2pRef ?? (this.components as any).libp2p
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Start the service
|
|
126
|
+
*/
|
|
127
|
+
async start(): Promise<void> {
|
|
128
|
+
if (this.running) {
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
await this.components.registrar.handle(this.protocol, this.handleIncomingStream.bind(this), {
|
|
133
|
+
maxInboundStreams: this.maxInboundStreams,
|
|
134
|
+
maxOutboundStreams: this.maxOutboundStreams
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
this.running = true
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Stop the service
|
|
142
|
+
*/
|
|
143
|
+
async stop(): Promise<void> {
|
|
144
|
+
if (!this.running) {
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
await this.components.registrar.unhandle(this.protocol)
|
|
149
|
+
this.running = false
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private getNetworkManager(): NetworkManagerLike | undefined {
|
|
153
|
+
if (this.components.networkManager) return this.components.networkManager
|
|
154
|
+
return (this.getLibp2p() as any)?.services?.networkManager as NetworkManagerLike | undefined
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private getSelfId(): PeerId | undefined {
|
|
158
|
+
if (this.components.peerId) return this.components.peerId
|
|
159
|
+
return this.getLibp2p()?.peerId as PeerId | undefined
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private getPeerAddrs(peerId: PeerId): string[] {
|
|
163
|
+
if (this.components.getConnectionAddrs) return this.components.getConnectionAddrs(peerId)
|
|
164
|
+
const libp2p = this.getLibp2p() as any
|
|
165
|
+
if (!libp2p?.getConnections) return []
|
|
166
|
+
// A redirect payload goes to a THIRD party, so only an outbound connection's remoteAddr
|
|
167
|
+
// qualifies — see `publishableConnectionAddr`.
|
|
168
|
+
const conns: DirectionalConnection[] = libp2p.getConnections(peerId) ?? []
|
|
169
|
+
const addrs: string[] = []
|
|
170
|
+
for (const c of conns) {
|
|
171
|
+
const addr = publishableConnectionAddr(c, this.addressLog)
|
|
172
|
+
if (addr !== undefined) addrs.push(addr)
|
|
173
|
+
}
|
|
174
|
+
return addrs
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Derive the redirect routing key and op name for a single operation.
|
|
179
|
+
*
|
|
180
|
+
* The key MUST be the block the corresponding handler actually coordinates and
|
|
181
|
+
* verifies responsibility on, so redirect routing stays consistent with where the
|
|
182
|
+
* op is executed:
|
|
183
|
+
* - get → blockIds[0]
|
|
184
|
+
* - pend → blockIdsForTransforms(transforms)[0]
|
|
185
|
+
* - cancel → actionRef.blockIds[0]
|
|
186
|
+
* - commit → blockIds[0] (CoordinatorRepo.commit anchors consensus on
|
|
187
|
+
* getClusterSize(blockIds[0]) / executeClusterTransaction(blockIds[0]) and guards
|
|
188
|
+
* with verifyResponsibility(blockIds) — NOT tailId; for a per-block commit batch
|
|
189
|
+
* whose blockIds[0] !== tailId, keying on tailId redirected the commit to the
|
|
190
|
+
* collection tail's cluster, which then fails verifyResponsibility for the non-tail block.)
|
|
191
|
+
*
|
|
192
|
+
* Returns blockKey === undefined when the op carries no routable key (e.g. a cancel
|
|
193
|
+
* with an empty blockIds list), in which case the caller handles it locally without a
|
|
194
|
+
* redirect check.
|
|
195
|
+
*/
|
|
196
|
+
deriveBlockKey(operation: RepoMessage['operations'][number]): { blockKey: string | undefined, opName: string } {
|
|
197
|
+
if ('get' in operation) {
|
|
198
|
+
return { blockKey: operation.get.blockIds[0], opName: 'get' }
|
|
199
|
+
}
|
|
200
|
+
if ('pend' in operation) {
|
|
201
|
+
return { blockKey: blockIdsForTransforms(operation.pend.transforms)[0], opName: 'pend' }
|
|
202
|
+
}
|
|
203
|
+
if ('cancel' in operation) {
|
|
204
|
+
return { blockKey: operation.cancel.actionRef.blockIds[0], opName: 'cancel' }
|
|
205
|
+
}
|
|
206
|
+
if ('commit' in operation) {
|
|
207
|
+
return { blockKey: operation.commit.blockIds[0], opName: 'commit' }
|
|
208
|
+
}
|
|
209
|
+
return { blockKey: undefined, opName: 'unknown' }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Check if this node should redirect the request for a given key.
|
|
214
|
+
* Returns a RedirectPayload if not responsible, null if should handle locally.
|
|
215
|
+
* Also attaches cluster info to the message for downstream use.
|
|
216
|
+
*/
|
|
217
|
+
async checkRedirect(blockKey: string, opName: string, message: RepoMessage): Promise<RedirectPayload | null> {
|
|
218
|
+
const nm = this.getNetworkManager()
|
|
219
|
+
if (!nm) return null
|
|
220
|
+
|
|
221
|
+
// Pass the RAW encoded block-key bytes to getCluster. getCluster hashes
|
|
222
|
+
// internally (hashKey == sha256), so the responsible-set coordinate becomes
|
|
223
|
+
// hashKey(encode(blockKey)) — identical to how the cluster coordinator
|
|
224
|
+
// derives it (ClusterCoordinator.getClusterForBlock → findCluster(encode(blockId))).
|
|
225
|
+
// Pre-hashing here would double-hash (hashKey(sha256(encode(blockKey)))), placing
|
|
226
|
+
// the cohort at an unrelated ring coordinate and redirecting requests the
|
|
227
|
+
// coordinator legitimately routed to this peer.
|
|
228
|
+
const key = new TextEncoder().encode(blockKey)
|
|
229
|
+
const cluster = await nm.getCluster(key)
|
|
230
|
+
;(message as any).cluster = cluster.map((p: PeerId) => p.toString?.() ?? String(p))
|
|
231
|
+
|
|
232
|
+
const selfId = this.getSelfId()
|
|
233
|
+
if (!selfId) return null
|
|
234
|
+
|
|
235
|
+
const isMember = cluster.some((p: PeerId) => peersEqual(p, selfId))
|
|
236
|
+
const smallMesh = cluster.length < this.responsibilityK
|
|
237
|
+
|
|
238
|
+
if (!smallMesh && !isMember) {
|
|
239
|
+
const peers = cluster.filter((p: PeerId) => !peersEqual(p, selfId))
|
|
240
|
+
debugLog('redirect op=%s blockKey=%s cluster=%d', opName, blockKey, cluster.length)
|
|
241
|
+
return encodePeers(peers.map((pid: PeerId) => ({
|
|
242
|
+
id: pid.toString(),
|
|
243
|
+
addrs: this.getPeerAddrs(pid)
|
|
244
|
+
})))
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return null
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Handle incoming streams on the repo protocol
|
|
252
|
+
*/
|
|
253
|
+
private handleIncomingStream(stream: Stream, connection?: Connection): void {
|
|
254
|
+
const peerId = connection?.remotePeer
|
|
255
|
+
|
|
256
|
+
const processStream = async function* (this: RepoService, source: AsyncIterable<Uint8ArrayList>) {
|
|
257
|
+
for await (const msg of source) {
|
|
258
|
+
// Decode the message
|
|
259
|
+
const decoded = new TextDecoder().decode(msg.subarray())
|
|
260
|
+
const message = JSON.parse(decoded) as RepoMessage
|
|
261
|
+
|
|
262
|
+
// Process each operation. Derive the redirect routing key once (keyed on the
|
|
263
|
+
// block the handler actually coordinates), redirect-check it, then dispatch.
|
|
264
|
+
const operation = message.operations[0]
|
|
265
|
+
const { blockKey, opName } = this.deriveBlockKey(operation)
|
|
266
|
+
const redirect = blockKey !== undefined
|
|
267
|
+
? await this.checkRedirect(blockKey, opName, message)
|
|
268
|
+
: null
|
|
269
|
+
|
|
270
|
+
let response: any
|
|
271
|
+
if (redirect) {
|
|
272
|
+
response = redirect
|
|
273
|
+
} else if ('get' in operation) {
|
|
274
|
+
// No `skipClusterFetch` here: a read on this protocol comes from ANOTHER node, so
|
|
275
|
+
// it must reach `CoordinatorRepo`'s cohort consult — answering a bare absent for
|
|
276
|
+
// a block a cohort peer holds is an authoritative lie the transactor never
|
|
277
|
+
// retries. Only the sync protocol keeps the flag (`sync/service.ts`, where the
|
|
278
|
+
// consult itself lands), and that is what stops the recursion.
|
|
279
|
+
// NOTE: this also puts lazy read-repair on remote reads of locally-present blocks
|
|
280
|
+
// — one consult per block per `readRepairWindowMs`, damped by a 1000-entry LRU of
|
|
281
|
+
// block ids. If a working set wider than that LRU ever shows a consult on every
|
|
282
|
+
// read, widen the LRU rather than reinstating the skip.
|
|
283
|
+
response = await this.repo.get(operation.get, { expiration: message.expiration })
|
|
284
|
+
} else if ('pend' in operation) {
|
|
285
|
+
response = await this.repo.pend(operation.pend, { expiration: message.expiration })
|
|
286
|
+
} else if ('cancel' in operation) {
|
|
287
|
+
response = await this.repo.cancel(operation.cancel.actionRef, { expiration: message.expiration })
|
|
288
|
+
} else if ('commit' in operation) {
|
|
289
|
+
response = await this.repo.commit(operation.commit, { expiration: message.expiration })
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Encode and yield the response
|
|
293
|
+
yield new TextEncoder().encode(JSON.stringify(response))
|
|
294
|
+
// One request per stream: every real RepoClient sends exactly one request
|
|
295
|
+
// per dial (see ProtocolClient.processMessage), so complete the generator
|
|
296
|
+
// after the first response. A second frame a peer queued is then never read
|
|
297
|
+
// or parsed. Mirrors sync/block-transfer.
|
|
298
|
+
return
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
void (async () => {
|
|
303
|
+
try {
|
|
304
|
+
// Authorization runs before ANY decoding or execution. Guarded on the field so a
|
|
305
|
+
// node without a predicate keeps the original path untouched.
|
|
306
|
+
if (this.authorization && await this.authorization.deny(stream, peerId?.toString())) return
|
|
307
|
+
const responses = pipe(
|
|
308
|
+
stream,
|
|
309
|
+
(source) => lpDecode(source, { maxDataLength: MAX_BLOCK_MESSAGE_BYTES }),
|
|
310
|
+
processStream.bind(this),
|
|
311
|
+
(source) => lpEncode(source)
|
|
312
|
+
)
|
|
313
|
+
for await (const chunk of responses) {
|
|
314
|
+
stream.send(chunk)
|
|
315
|
+
}
|
|
316
|
+
await stream.close()
|
|
317
|
+
} catch (err) {
|
|
318
|
+
this.log.error('error handling repo protocol message from %p - %e', peerId, err)
|
|
319
|
+
stream.abort(err instanceof Error ? err : new Error(String(err)))
|
|
320
|
+
}
|
|
321
|
+
})()
|
|
322
|
+
}
|
|
323
|
+
}
|