@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.
- package/dist/src/cluster/block-transfer-service.d.ts +73 -1
- package/dist/src/cluster/block-transfer-service.d.ts.map +1 -1
- package/dist/src/cluster/block-transfer-service.js +70 -3
- package/dist/src/cluster/block-transfer-service.js.map +1 -1
- package/dist/src/cluster/block-transfer.d.ts +16 -1
- package/dist/src/cluster/block-transfer.d.ts.map +1 -1
- package/dist/src/cluster/block-transfer.js +73 -72
- package/dist/src/cluster/block-transfer.js.map +1 -1
- package/dist/src/cluster/client.d.ts +4 -6
- package/dist/src/cluster/client.d.ts.map +1 -1
- package/dist/src/cluster/client.js +8 -13
- package/dist/src/cluster/client.js.map +1 -1
- package/dist/src/cluster/cluster-policy.d.ts +2 -1
- package/dist/src/cluster/cluster-policy.d.ts.map +1 -1
- package/dist/src/cluster/cluster-policy.js.map +1 -1
- package/dist/src/cluster/cluster-repo.d.ts +155 -9
- package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
- package/dist/src/cluster/cluster-repo.js +446 -58
- package/dist/src/cluster/cluster-repo.js.map +1 -1
- package/dist/src/cluster/commit-proof.js +2 -2
- package/dist/src/cluster/race-resolution.d.ts +51 -7
- package/dist/src/cluster/race-resolution.d.ts.map +1 -1
- package/dist/src/cluster/race-resolution.js +71 -7
- package/dist/src/cluster/race-resolution.js.map +1 -1
- package/dist/src/cluster/rebalance-monitor.d.ts +67 -5
- package/dist/src/cluster/rebalance-monitor.d.ts.map +1 -1
- package/dist/src/cluster/rebalance-monitor.js +120 -26
- package/dist/src/cluster/rebalance-monitor.js.map +1 -1
- package/dist/src/cluster/spread-on-churn.d.ts.map +1 -1
- package/dist/src/cluster/spread-on-churn.js +34 -47
- package/dist/src/cluster/spread-on-churn.js.map +1 -1
- package/dist/src/cohort-topic/host.d.ts.map +1 -1
- package/dist/src/cohort-topic/host.js +8 -6
- package/dist/src/cohort-topic/host.js.map +1 -1
- package/dist/src/cohort-topic/membership-source.js +1 -1
- package/dist/src/cohort-topic/membership-source.js.map +1 -1
- package/dist/src/cohort-topic/peer-codec.d.ts.map +1 -1
- package/dist/src/cohort-topic/peer-codec.js +5 -2
- package/dist/src/cohort-topic/peer-codec.js.map +1 -1
- package/dist/src/cohort-topic/stream-util.d.ts +24 -3
- package/dist/src/cohort-topic/stream-util.d.ts.map +1 -1
- package/dist/src/cohort-topic/stream-util.js +36 -3
- package/dist/src/cohort-topic/stream-util.js.map +1 -1
- package/dist/src/cohort-topic/topic-router.d.ts +9 -0
- package/dist/src/cohort-topic/topic-router.d.ts.map +1 -1
- package/dist/src/cohort-topic/topic-router.js +12 -2
- package/dist/src/cohort-topic/topic-router.js.map +1 -1
- package/dist/src/dispute/client.d.ts.map +1 -1
- package/dist/src/dispute/client.js +15 -3
- package/dist/src/dispute/client.js.map +1 -1
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +5 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/libp2p-key-network.d.ts +78 -17
- package/dist/src/libp2p-key-network.d.ts.map +1 -1
- package/dist/src/libp2p-key-network.js +234 -147
- package/dist/src/libp2p-key-network.js.map +1 -1
- package/dist/src/libp2p-node-base.d.ts +35 -1
- package/dist/src/libp2p-node-base.d.ts.map +1 -1
- package/dist/src/libp2p-node-base.js +151 -26
- package/dist/src/libp2p-node-base.js.map +1 -1
- package/dist/src/libp2p-node-rn.d.ts +2 -2
- package/dist/src/libp2p-node-rn.d.ts.map +1 -1
- package/dist/src/libp2p-node-rn.js.map +1 -1
- package/dist/src/libp2p-node.d.ts +2 -2
- package/dist/src/libp2p-node.d.ts.map +1 -1
- package/dist/src/libp2p-node.js.map +1 -1
- package/dist/src/logger.d.ts.map +1 -1
- package/dist/src/logger.js +6 -1
- package/dist/src/logger.js.map +1 -1
- package/dist/src/matchmaking/query-transport.d.ts +3 -4
- package/dist/src/matchmaking/query-transport.d.ts.map +1 -1
- package/dist/src/matchmaking/query-transport.js +9 -9
- package/dist/src/matchmaking/query-transport.js.map +1 -1
- package/dist/src/network/network-manager-service.d.ts +10 -5
- package/dist/src/network/network-manager-service.d.ts.map +1 -1
- package/dist/src/network/network-manager-service.js +11 -2
- package/dist/src/network/network-manager-service.js.map +1 -1
- package/dist/src/network/relay-reservation.d.ts +170 -0
- package/dist/src/network/relay-reservation.d.ts.map +1 -0
- package/dist/src/network/relay-reservation.js +527 -0
- package/dist/src/network/relay-reservation.js.map +1 -0
- package/dist/src/pend-validation.d.ts +3 -2
- package/dist/src/pend-validation.d.ts.map +1 -1
- package/dist/src/pend-validation.js +3 -2
- package/dist/src/pend-validation.js.map +1 -1
- package/dist/src/protocol-client.d.ts +8 -1
- package/dist/src/protocol-client.d.ts.map +1 -1
- package/dist/src/protocol-client.js +11 -0
- package/dist/src/protocol-client.js.map +1 -1
- package/dist/src/reactivity/forwarder-host.d.ts +2 -1
- package/dist/src/reactivity/forwarder-host.d.ts.map +1 -1
- package/dist/src/reactivity/forwarder-host.js +2 -1
- package/dist/src/reactivity/forwarder-host.js.map +1 -1
- package/dist/src/reactivity/origination-manager.d.ts +3 -3
- package/dist/src/reactivity/origination-manager.js +3 -3
- package/dist/src/reactivity/recover-transport.d.ts +22 -14
- package/dist/src/reactivity/recover-transport.d.ts.map +1 -1
- package/dist/src/reactivity/recover-transport.js +32 -19
- package/dist/src/reactivity/recover-transport.js.map +1 -1
- package/dist/src/reactivity/subscription-manager.d.ts +2 -2
- package/dist/src/reactivity/topic-bytes.d.ts +6 -7
- package/dist/src/reactivity/topic-bytes.d.ts.map +1 -1
- package/dist/src/reactivity/topic-bytes.js +8 -9
- package/dist/src/reactivity/topic-bytes.js.map +1 -1
- package/dist/src/repo/client.d.ts +5 -2
- package/dist/src/repo/client.d.ts.map +1 -1
- package/dist/src/repo/client.js +49 -40
- package/dist/src/repo/client.js.map +1 -1
- package/dist/src/repo/cluster-coordinator.d.ts +96 -11
- package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
- package/dist/src/repo/cluster-coordinator.js +164 -25
- package/dist/src/repo/cluster-coordinator.js.map +1 -1
- package/dist/src/repo/coordinator-repo.d.ts +240 -122
- package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
- package/dist/src/repo/coordinator-repo.js +627 -473
- package/dist/src/repo/coordinator-repo.js.map +1 -1
- package/dist/src/repo/i-under-replication-ledger.d.ts +87 -0
- package/dist/src/repo/i-under-replication-ledger.d.ts.map +1 -0
- package/dist/src/repo/i-under-replication-ledger.js +2 -0
- package/dist/src/repo/i-under-replication-ledger.js.map +1 -0
- package/dist/src/repo/kv-under-replication-ledger.d.ts +91 -0
- package/dist/src/repo/kv-under-replication-ledger.d.ts.map +1 -0
- package/dist/src/repo/kv-under-replication-ledger.js +287 -0
- package/dist/src/repo/kv-under-replication-ledger.js.map +1 -0
- package/dist/src/repo/responsibility.d.ts +37 -0
- package/dist/src/repo/responsibility.d.ts.map +1 -0
- package/dist/src/repo/responsibility.js +37 -0
- package/dist/src/repo/responsibility.js.map +1 -0
- package/dist/src/repo/service.d.ts +44 -15
- package/dist/src/repo/service.d.ts.map +1 -1
- package/dist/src/repo/service.js +64 -27
- package/dist/src/repo/service.js.map +1 -1
- package/dist/src/repo/stuck-reservation.d.ts +127 -0
- package/dist/src/repo/stuck-reservation.d.ts.map +1 -0
- package/dist/src/repo/stuck-reservation.js +179 -0
- package/dist/src/repo/stuck-reservation.js.map +1 -0
- package/dist/src/repo/under-replication-drain.d.ts +208 -0
- package/dist/src/repo/under-replication-drain.d.ts.map +1 -0
- package/dist/src/repo/under-replication-drain.js +461 -0
- package/dist/src/repo/under-replication-drain.js.map +1 -0
- package/dist/src/rn.d.ts +5 -0
- package/dist/src/rn.d.ts.map +1 -1
- package/dist/src/rn.js +5 -0
- package/dist/src/rn.js.map +1 -1
- package/dist/src/storage/block-latch.d.ts +3 -2
- package/dist/src/storage/block-latch.d.ts.map +1 -1
- package/dist/src/storage/block-latch.js +7 -4
- package/dist/src/storage/block-latch.js.map +1 -1
- package/dist/src/storage/block-storage.d.ts +45 -3
- package/dist/src/storage/block-storage.d.ts.map +1 -1
- package/dist/src/storage/block-storage.js +156 -9
- package/dist/src/storage/block-storage.js.map +1 -1
- package/dist/src/storage/i-block-storage.d.ts +57 -5
- package/dist/src/storage/i-block-storage.d.ts.map +1 -1
- package/dist/src/storage/i-block-storage.js.map +1 -1
- package/dist/src/storage/pending-claim.d.ts +167 -0
- package/dist/src/storage/pending-claim.d.ts.map +1 -0
- package/dist/src/storage/pending-claim.js +156 -0
- package/dist/src/storage/pending-claim.js.map +1 -0
- package/dist/src/storage/raw-store-codec.d.ts +13 -0
- package/dist/src/storage/raw-store-codec.d.ts.map +1 -1
- package/dist/src/storage/raw-store-codec.js +24 -3
- package/dist/src/storage/raw-store-codec.js.map +1 -1
- package/dist/src/storage/restoration-coordinator.d.ts.map +1 -1
- package/dist/src/storage/restoration-coordinator.js +2 -1
- package/dist/src/storage/restoration-coordinator.js.map +1 -1
- package/dist/src/storage/ring-shift-coordinator.d.ts.map +1 -1
- package/dist/src/storage/ring-shift-coordinator.js +3 -3
- package/dist/src/storage/ring-shift-coordinator.js.map +1 -1
- package/dist/src/storage/storage-repo.d.ts +122 -5
- package/dist/src/storage/storage-repo.d.ts.map +1 -1
- package/dist/src/storage/storage-repo.js +245 -68
- package/dist/src/storage/storage-repo.js.map +1 -1
- package/dist/src/storage/struct.d.ts +58 -0
- package/dist/src/storage/struct.d.ts.map +1 -1
- package/dist/src/testing/mesh-harness.d.ts +77 -11
- package/dist/src/testing/mesh-harness.d.ts.map +1 -1
- package/dist/src/testing/mesh-harness.js +226 -100
- package/dist/src/testing/mesh-harness.js.map +1 -1
- package/package.json +2 -2
- package/readme.md +27 -3
- package/src/cluster/block-transfer-service.ts +132 -4
- package/src/cluster/block-transfer.ts +79 -84
- package/src/cluster/client.ts +115 -119
- package/src/cluster/cluster-policy.ts +2 -1
- package/src/cluster/cluster-repo.ts +493 -65
- package/src/cluster/commit-proof.ts +2 -2
- package/src/cluster/race-resolution.ts +76 -7
- package/src/cluster/rebalance-monitor.ts +677 -539
- package/src/cluster/spread-on-churn.ts +34 -54
- package/src/cohort-topic/host.ts +8 -6
- package/src/cohort-topic/membership-source.ts +1 -1
- package/src/cohort-topic/peer-codec.ts +34 -31
- package/src/cohort-topic/stream-util.ts +39 -4
- package/src/cohort-topic/topic-router.ts +12 -2
- package/src/dispute/client.ts +17 -7
- package/src/index.ts +64 -59
- package/src/libp2p-key-network.ts +1377 -1260
- package/src/libp2p-node-base.ts +186 -27
- package/src/libp2p-node-rn.ts +31 -30
- package/src/libp2p-node.ts +37 -36
- package/src/logger.ts +6 -1
- package/src/matchmaking/query-transport.ts +9 -9
- package/src/network/network-manager-service.ts +16 -5
- package/src/network/relay-reservation.ts +636 -0
- package/src/pend-validation.ts +3 -2
- package/src/protocol-client.ts +13 -1
- package/src/reactivity/forwarder-host.ts +2 -1
- package/src/reactivity/origination-manager.ts +3 -3
- package/src/reactivity/recover-transport.ts +39 -22
- package/src/reactivity/subscription-manager.ts +2 -2
- package/src/reactivity/topic-bytes.ts +35 -37
- package/src/repo/client.ts +172 -173
- package/src/repo/cluster-coordinator.ts +183 -25
- package/src/repo/coordinator-repo.ts +656 -506
- package/src/repo/i-under-replication-ledger.ts +89 -0
- package/src/repo/kv-under-replication-ledger.ts +302 -0
- package/src/repo/responsibility.ts +48 -0
- package/src/repo/service.ts +76 -39
- package/src/repo/stuck-reservation.ts +224 -0
- package/src/repo/under-replication-drain.ts +582 -0
- package/src/rn.ts +74 -69
- package/src/storage/block-latch.ts +7 -4
- package/src/storage/block-storage.ts +161 -10
- package/src/storage/i-block-storage.ts +60 -5
- package/src/storage/pending-claim.ts +200 -0
- package/src/storage/raw-store-codec.ts +26 -3
- package/src/storage/restoration-coordinator.ts +212 -211
- package/src/storage/ring-shift-coordinator.ts +271 -271
- package/src/storage/storage-repo.ts +280 -72
- package/src/storage/struct.ts +58 -0
- package/src/testing/mesh-harness.ts +293 -112
|
@@ -174,9 +174,9 @@ export class ReactivityOriginationManager {
|
|
|
174
174
|
* recover serve begins redirecting to the new tree.
|
|
175
175
|
*
|
|
176
176
|
* **Encoding contract.** `ctx.tailId` is the reactivity tail anchor bytes the node resolved
|
|
177
|
-
* (`reactivityTailBytes(event.tailId)` in production — the SAME utf8 encoding origination's membership
|
|
178
|
-
* and a subscriber's `reactivityTopicId(reactivityTailBytes(tail))` use,
|
|
179
|
-
*
|
|
177
|
+
* (`reactivityTailBytes(event.tailId)` in production — the SAME raw utf8 encoding origination's membership
|
|
178
|
+
* gate and a subscriber's `reactivityTopicId(reactivityTailBytes(tail))` use, never a pre-hashed digest).
|
|
179
|
+
* So `oldTopicId = reactivityTopicId(oldAnchorBytes)` is byte-identical to the topic a
|
|
180
180
|
* subscriber subscribed under — a mismatch would silently never redirect.
|
|
181
181
|
*/
|
|
182
182
|
private detectTailRotation(event: CollectionChangeEvent, ctx: OriginationCollectionContext): void {
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
* - {@link createRecoverRequestHandler} / {@link registerRecoverHandler} — the **inbound** serve handler.
|
|
20
20
|
* Decode (bounded) → verify the request's peer-key signature against the dialing peer → reject a
|
|
21
21
|
* replay/stale request → resolve the live `PushState` → `serveBackfill` / `serveResume` → reply. Any
|
|
22
|
-
* failure
|
|
22
|
+
* failure **declines** rather than throwing out of the handler: the member sends a zero-length reply (no
|
|
23
|
+
* result), and the dialing transport falls through to the next cohort candidate.
|
|
23
24
|
*
|
|
24
25
|
* ## Dial-target encoding bridge (load-bearing)
|
|
25
26
|
*
|
|
@@ -60,7 +61,7 @@ import {
|
|
|
60
61
|
type CorrelationReplayGuard,
|
|
61
62
|
type RotationRedirectV1,
|
|
62
63
|
} from "@optimystic/db-core";
|
|
63
|
-
import { requestResponse, handleRequestResponse, DEFAULT_STREAM_MAX_BYTES } from "../cohort-topic/stream-util.js";
|
|
64
|
+
import { requestResponse, handleRequestResponse, NoResultReplyError, DEFAULT_STREAM_MAX_BYTES } from "../cohort-topic/stream-util.js";
|
|
64
65
|
import { verifyPeerSig, signPeerSig } from "../cohort-topic/peer-sig.js";
|
|
65
66
|
import { peerIdToBytes, bytesToPeerIdString } from "../cohort-topic/peer-codec.js";
|
|
66
67
|
import { PROTOCOL_REACTIVITY_RECOVER } from "./protocols.js";
|
|
@@ -134,8 +135,12 @@ export class RotationRedirectError extends Error {
|
|
|
134
135
|
|
|
135
136
|
/** The wire exchange seam: open the recover protocol to `target`, send `frame`, return the bounded reply. */
|
|
136
137
|
export interface RecoverDialer {
|
|
137
|
-
/**
|
|
138
|
-
|
|
138
|
+
/**
|
|
139
|
+
* Exchange one request frame for one reply frame with `target` (peer-id string). Resolves `undefined` when
|
|
140
|
+
* the member declined (no served state, a failed signature / replay check, an undecodable request) — the
|
|
141
|
+
* same contract as {@link requestResponse}. Rejects on a dial failure.
|
|
142
|
+
*/
|
|
143
|
+
exchange(target: string, frame: Uint8Array): Promise<Uint8Array | undefined>;
|
|
139
144
|
}
|
|
140
145
|
|
|
141
146
|
/** Build the production libp2p-backed {@link RecoverDialer} over {@link requestResponse}. */
|
|
@@ -162,9 +167,11 @@ export interface Libp2pReactivityRecoverTransportOptions {
|
|
|
162
167
|
/**
|
|
163
168
|
* The outbound recover transport: one instance per node, exposing the two db-core function seams against it.
|
|
164
169
|
* Each returned transport dials the **sticky primary first** (one round trip after a brief flap), falling
|
|
165
|
-
* back to a **cohort-walk** member on a dial failure
|
|
166
|
-
* can answer
|
|
167
|
-
*
|
|
170
|
+
* back to a **cohort-walk** member on a dial failure *or* when the dialed member declines (any member that
|
|
171
|
+
* holds the topic's gossiped `PushState` can answer, so one member with nothing to serve must not end the
|
|
172
|
+
* walk). A kind mismatch, an undecodable reply, or every target failing / declining surfaces as a rejection
|
|
173
|
+
* so the caller's retry/escalation policy (the subscription manager's backfill escalation, or
|
|
174
|
+
* `manager.resume()`'s caller) takes over.
|
|
168
175
|
*/
|
|
169
176
|
export class Libp2pReactivityRecoverTransport {
|
|
170
177
|
private readonly dialer: RecoverDialer;
|
|
@@ -206,10 +213,13 @@ export class Libp2pReactivityRecoverTransport {
|
|
|
206
213
|
}
|
|
207
214
|
|
|
208
215
|
/**
|
|
209
|
-
* Exchange one recover frame with the first
|
|
210
|
-
* member. A **dial failure**
|
|
211
|
-
*
|
|
212
|
-
*
|
|
216
|
+
* Exchange one recover frame with the first target that answers: sticky primary, then each cohort-walk
|
|
217
|
+
* member. A **dial failure** and a **declined** request (the member had no result — no served state, a
|
|
218
|
+
* failed signature / replay check, an undecodable request) both fall through to the next candidate. A
|
|
219
|
+
* reply frame is **terminal** (the member answered for the cohort): a `kind: "rotated"` redirect throws
|
|
220
|
+
* {@link RotationRedirectError}, and a reply that fails to decode or decodes to the wrong `kind` is a
|
|
221
|
+
* protocol error. When no candidate answers, throws the last candidate's outcome — a
|
|
222
|
+
* {@link NoResultReplyError} if it declined, its dial error if it failed to dial.
|
|
213
223
|
*/
|
|
214
224
|
private async exchange(kind: RecoverKind, frame: Uint8Array, topicId: Uint8Array, collectionId: string): Promise<RecoverReplyV1> {
|
|
215
225
|
const targets = this.selectTargets(topicId, collectionId);
|
|
@@ -218,7 +228,7 @@ export class Libp2pReactivityRecoverTransport {
|
|
|
218
228
|
}
|
|
219
229
|
let lastErr: unknown;
|
|
220
230
|
for (const target of targets) {
|
|
221
|
-
let replyFrame: Uint8Array;
|
|
231
|
+
let replyFrame: Uint8Array | undefined;
|
|
222
232
|
try {
|
|
223
233
|
replyFrame = await this.dialer.exchange(target, frame);
|
|
224
234
|
} catch (err) {
|
|
@@ -226,6 +236,15 @@ export class Libp2pReactivityRecoverTransport {
|
|
|
226
236
|
log("recover dial to %s failed, trying next target: %o", target, err);
|
|
227
237
|
continue;
|
|
228
238
|
}
|
|
239
|
+
if (replyFrame === undefined) {
|
|
240
|
+
// Declined → fall back to the next candidate, resending the same signed frame.
|
|
241
|
+
// NOTE: the resend is admissible only because each node's `CorrelationReplayGuard` is node-local
|
|
242
|
+
// (`RecoverServeDeps.replayGuard`). If the guard is ever shared across a cohort, the retried frame
|
|
243
|
+
// would be rejected everywhere as a replay, and each fallthrough would need a freshly signed request.
|
|
244
|
+
lastErr = new NoResultReplyError(`reactivity recover ${kind} from ${target}`);
|
|
245
|
+
log("recover %s declined by %s, trying next target", kind, target);
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
229
248
|
const reply = decodeRecoverReplyV1(replyFrame, this.maxBytes); // a decode failure here is terminal
|
|
230
249
|
// A rotated cohort answered authoritatively: surface the redirect and stop the walk (terminal, never
|
|
231
250
|
// a fallthrough — the dialed member spoke for the cohort).
|
|
@@ -298,17 +317,17 @@ export interface RecoverServeDeps {
|
|
|
298
317
|
/**
|
|
299
318
|
* Verify the request's peer-key signature against the dialing peer, then admit it through the freshness +
|
|
300
319
|
* anti-replay guard. The dialing peer's id **is** the signer (no signer-id field on the wire); the signature
|
|
301
|
-
* bytes are the anti-replay key (globally unique + authenticated). Returns `false` (
|
|
320
|
+
* bytes are the anti-replay key (globally unique + authenticated). Returns `false` (decline) on a
|
|
302
321
|
* bad signature, a stale/future timestamp, or a replay.
|
|
303
322
|
*/
|
|
304
323
|
function verifyAndAdmit(deps: RecoverServeDeps, signerId: string, payload: Uint8Array, signatureB64: string, timestamp: number, now: number): boolean {
|
|
305
324
|
const signature = b64urlToBytes(signatureB64);
|
|
306
325
|
if (!verifyPeerSig(signerId, payload, signature)) {
|
|
307
|
-
log("recover serve: signature verification failed for %s (
|
|
326
|
+
log("recover serve: signature verification failed for %s (declined)", signerId);
|
|
308
327
|
return false;
|
|
309
328
|
}
|
|
310
329
|
if (!deps.replayGuard.accept(signature, peerIdToBytes(signerId), timestamp, now)) {
|
|
311
|
-
log("recover serve: replay/stale request from %s (
|
|
330
|
+
log("recover serve: replay/stale request from %s (declined)", signerId);
|
|
312
331
|
return false;
|
|
313
332
|
}
|
|
314
333
|
return true;
|
|
@@ -329,7 +348,7 @@ function serveBackfillReply(deps: RecoverServeDeps, req: BackfillV1, now: number
|
|
|
329
348
|
}
|
|
330
349
|
const ps = deps.pushStateForCollection(req.collectionId);
|
|
331
350
|
if (ps === undefined) {
|
|
332
|
-
return undefined; // not a serving member for this collection →
|
|
351
|
+
return undefined; // not a serving member for this collection → decline (the subscriber tries the next member)
|
|
333
352
|
}
|
|
334
353
|
const backfillReply = serveBackfill(ps.replayBuffer, req, ps.collectionId);
|
|
335
354
|
return encodeRecoverReplyV1({ v: 1, kind: "backfill", backfillReply }, maxBytes);
|
|
@@ -371,10 +390,8 @@ function serveResumeReply(deps: RecoverServeDeps, req: ResumeV1, now: number): U
|
|
|
371
390
|
* Build the recover serve callback for {@link handleRequestResponse}: it returns the reply frame, or
|
|
372
391
|
* `undefined` on a decode/verify/replay/resolve failure. It never throws out of the handler.
|
|
373
392
|
*
|
|
374
|
-
*
|
|
375
|
-
* {@link Libp2pReactivityRecoverTransport
|
|
376
|
-
* than falling through to the next cohort candidate — one member declining ends the whole recover walk.
|
|
377
|
-
* Tracked as `debt-stream-reply-no-result-untyped`.
|
|
393
|
+
* `undefined` reaches the dialer as a zero-length frame, which {@link requestResponse} resolves as no result
|
|
394
|
+
* and {@link Libp2pReactivityRecoverTransport} treats as a decline — it tries the next cohort candidate.
|
|
378
395
|
*/
|
|
379
396
|
export function createRecoverRequestHandler(deps: RecoverServeDeps): (frame: Uint8Array, fromPeer: PeerId) => Promise<Uint8Array | undefined> {
|
|
380
397
|
const maxBytes = deps.maxBytes ?? DEFAULT_STREAM_MAX_BYTES;
|
|
@@ -398,8 +415,8 @@ export function createRecoverRequestHandler(deps: RecoverServeDeps): (frame: Uin
|
|
|
398
415
|
return Promise.resolve(serveResumeReply(deps, r, now));
|
|
399
416
|
} catch (err) {
|
|
400
417
|
// A malformed/foreign request (decode failure, foreign collectionId from serve*) must never throw
|
|
401
|
-
// out of the stream handler: log +
|
|
402
|
-
log("recover serve: dropping request (
|
|
418
|
+
// out of the stream handler: log + decline (a zero-length reply; the dialer tries the next member).
|
|
419
|
+
log("recover serve: dropping request (declined): %o", err);
|
|
403
420
|
return Promise.resolve(undefined);
|
|
404
421
|
}
|
|
405
422
|
};
|
|
@@ -82,8 +82,8 @@ export interface ReactivitySubscriptionManagerOptions {
|
|
|
82
82
|
*
|
|
83
83
|
* **Load-bearing encoding contract.** When a production subscribe factory converts a `BlockId` tail to
|
|
84
84
|
* these bytes it MUST use `reactivityTailBytes(tailId)` (`reactivity/topic-bytes.ts`) — the SAME function
|
|
85
|
-
* origination's membership gate uses —
|
|
86
|
-
*
|
|
85
|
+
* origination's membership gate uses — never a pre-hashed digest of the id, which would double-hash.
|
|
86
|
+
* Origination derives the topic's `coord_0` cohort from `reactivityTopicId(reactivityTailBytes(
|
|
87
87
|
* tailId))`; if this side feeds differently-encoded bytes it subscribes to a *different* coord and
|
|
88
88
|
* origination silently never reaches it (the `topic-bytes-encoding` spec pins the coord-equality).
|
|
89
89
|
*/
|
|
@@ -1,37 +1,35 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Reactivity — the one pinned `BlockId` → raw tail-bytes encoding, shared by origination and subscription.
|
|
3
|
-
*
|
|
4
|
-
* `reactivityTopicId` (db-core) hashes `H(tailBytes ‖ "reactivity")` to derive the topic's `coord_0`
|
|
5
|
-
* cohort. Origination (the cohort-change membership gate, {@link
|
|
6
|
-
* import("../cohort-topic/reactivity-membership-gate.js").createReactivitySelfMembershipGate}) and the
|
|
7
|
-
* subscriber side (the production subscribe factory that converts a `BlockId` `tailIdAtAttach` to the bytes
|
|
8
|
-
* {@link import("./subscription-manager.js").ReactivitySubscriptionManager} feeds `reactivityTopicId`) MUST
|
|
9
|
-
* feed it the **same** bytes for a given tail, or they resolve different coords and origination silently
|
|
10
|
-
* never reaches subscribers (green tests, dead feature).
|
|
11
|
-
*
|
|
12
|
-
* This module is that single source of truth: `reactivityTailBytes(tailId)
|
|
13
|
-
* the
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
import
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return utf8.encode(tailId);
|
|
37
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Reactivity — the one pinned `BlockId` → raw tail-bytes encoding, shared by origination and subscription.
|
|
3
|
+
*
|
|
4
|
+
* `reactivityTopicId` (db-core) hashes `H(tailBytes ‖ "reactivity")` to derive the topic's `coord_0`
|
|
5
|
+
* cohort. Origination (the cohort-change membership gate, {@link
|
|
6
|
+
* import("../cohort-topic/reactivity-membership-gate.js").createReactivitySelfMembershipGate}) and the
|
|
7
|
+
* subscriber side (the production subscribe factory that converts a `BlockId` `tailIdAtAttach` to the bytes
|
|
8
|
+
* {@link import("./subscription-manager.js").ReactivitySubscriptionManager} feeds `reactivityTopicId`) MUST
|
|
9
|
+
* feed it the **same** bytes for a given tail, or they resolve different coords and origination silently
|
|
10
|
+
* never reaches subscribers (green tests, dead feature).
|
|
11
|
+
*
|
|
12
|
+
* This module is that single source of truth: `reactivityTailBytes(tailId)` is the tail's routing key
|
|
13
|
+
* (db-core's `routingKeyForBlock`, the raw utf8 of the id) — the same bytes every cohort lookup for that
|
|
14
|
+
* block is keyed on. It must stay raw: a pre-hashed digest would double-hash relative to the spec's
|
|
15
|
+
* `H(tailId ‖ "reactivity")`.
|
|
16
|
+
*
|
|
17
|
+
* It lives in the reactivity surface (not buried in the gate) so the subscribe path and the gate import the
|
|
18
|
+
* exact same function rather than re-deriving the encoding independently. The
|
|
19
|
+
* `cohort-topic/reactivity-membership-gate.ts` module re-exports it for its existing callers.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { BlockId } from "@optimystic/db-core";
|
|
23
|
+
import { routingKeyForBlock } from "@optimystic/db-core";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The pinned `BlockId` → raw tail bytes encoding fed into `reactivityTopicId`: the tail's routing key, i.e.
|
|
27
|
+
* the **raw** utf8 bytes of the tail block-id string (never a pre-hashed digest).
|
|
28
|
+
*
|
|
29
|
+
* Load-bearing: origination and the subscriber side must call this **same** function for a given tail (see
|
|
30
|
+
* the module doc) — a mismatch resolves different `coord_0`s and origination silently never reaches
|
|
31
|
+
* subscribers. The `topic-bytes-encoding` spec pins the coord-equality both ways.
|
|
32
|
+
*/
|
|
33
|
+
export function reactivityTailBytes(tailId: BlockId): Uint8Array {
|
|
34
|
+
return routingKeyForBlock(tailId);
|
|
35
|
+
}
|
package/src/repo/client.ts
CHANGED
|
@@ -1,173 +1,172 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
IRepo, GetBlockResults, PendSuccess, StaleFailure, ActionBlocks, MessageOptions, CommitResult,
|
|
3
|
-
PendRequest, CommitRequest, BlockGets, IPeerNetwork, PeerId
|
|
4
|
-
} from "@optimystic/db-core";
|
|
5
|
-
import type { RepoMessage } from "@optimystic/db-core";
|
|
6
|
-
import { blockIdsForTransforms
|
|
7
|
-
import { ProtocolClient } from "../protocol-client.js";
|
|
8
|
-
import type { RedirectPayload } from "./redirect.js";
|
|
9
|
-
import { MAX_BLOCK_MESSAGE_BYTES } from "../protocol-limits.js";
|
|
10
|
-
import { peerIdFromString } from "@libp2p/peer-id";
|
|
11
|
-
|
|
12
|
-
export class RepoClient extends ProtocolClient implements IRepo {
|
|
13
|
-
private constructor(peerId: PeerId, peerNetwork: IPeerNetwork, readonly protocolPrefix?: string) {
|
|
14
|
-
super(peerId, peerNetwork);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/** Create a new client instance */
|
|
18
|
-
public static create(peerId: PeerId, peerNetwork: IPeerNetwork, protocolPrefix?: string): RepoClient {
|
|
19
|
-
return new RepoClient(peerId, peerNetwork, protocolPrefix);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async get(blockGets: BlockGets, options: MessageOptions): Promise<GetBlockResults> {
|
|
23
|
-
return this.processRepoMessage<GetBlockResults>(
|
|
24
|
-
[{ get: blockGets }],
|
|
25
|
-
options
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async pend(request: PendRequest, options: MessageOptions): Promise<PendSuccess | StaleFailure> {
|
|
30
|
-
return this.processRepoMessage<PendSuccess | StaleFailure>(
|
|
31
|
-
[{ pend: request }],
|
|
32
|
-
options
|
|
33
|
-
);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
async cancel(actionRef: ActionBlocks, options: MessageOptions): Promise<void> {
|
|
37
|
-
return this.processRepoMessage<void>(
|
|
38
|
-
[{ cancel: { actionRef } }],
|
|
39
|
-
options
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async commit(request: CommitRequest, options: MessageOptions): Promise<CommitResult> {
|
|
44
|
-
return this.processRepoMessage<CommitResult>(
|
|
45
|
-
[{ commit: request }],
|
|
46
|
-
options
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
private extractCorrelationId(operations: RepoMessage['operations']): string | undefined {
|
|
51
|
-
const op = operations[0];
|
|
52
|
-
if (!op) return undefined;
|
|
53
|
-
if ('pend' in op) return op.pend.actionId;
|
|
54
|
-
if ('commit' in op) return op.commit.actionId;
|
|
55
|
-
if ('cancel' in op) return op.cancel.actionRef.actionId;
|
|
56
|
-
return undefined;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
private async processRepoMessage<T>(
|
|
60
|
-
operations: RepoMessage['operations'],
|
|
61
|
-
options: MessageOptions,
|
|
62
|
-
hop: number = 0
|
|
63
|
-
): Promise<T> {
|
|
64
|
-
const message: RepoMessage = {
|
|
65
|
-
operations,
|
|
66
|
-
expiration: options.expiration,
|
|
67
|
-
};
|
|
68
|
-
const correlationId = this.extractCorrelationId(operations);
|
|
69
|
-
const deadline = options.expiration ?? (Date.now() + 30_000)
|
|
70
|
-
const deadlineMs = Math.max(1, deadline - Date.now())
|
|
71
|
-
const preferred = (this.protocolPrefix ?? '/db-p2p') + '/repo/1.0.0'
|
|
72
|
-
|
|
73
|
-
// Drive the remaining `expiration` budget through an AbortController whose
|
|
74
|
-
// abort *reason* is the caller-facing `'RepoClient timeout'` Error, combined
|
|
75
|
-
// with any caller signal. processMessage forwards `signal` to both the dial
|
|
76
|
-
// and the response-read and calls `stream.abort(signal.reason)` on abort, so —
|
|
77
|
-
// unlike the old `Promise.race`, whose losing branch left the inner read
|
|
78
|
-
// running and leaked a pending read + stream on every timed-out RPC to a silent
|
|
79
|
-
// peer — the deadline now genuinely cancels the inner read while the caller
|
|
80
|
-
// still observes an error whose `.message === 'RepoClient timeout'`.
|
|
81
|
-
//
|
|
82
|
-
// Intentionally no `responseTimeoutMs`: the combined signal already bounds the
|
|
83
|
-
// read at `deadlineMs`. A second, shorter cap would surface as
|
|
84
|
-
// ResponseTimeoutError and mask the caller-facing 'RepoClient timeout' message.
|
|
85
|
-
const deadlineController = new AbortController()
|
|
86
|
-
const timer = setTimeout(
|
|
87
|
-
() => deadlineController.abort(new Error('RepoClient timeout')),
|
|
88
|
-
deadlineMs
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const
|
|
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
|
-
if (
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
}
|
|
1
|
+
import type {
|
|
2
|
+
IRepo, GetBlockResults, PendSuccess, StaleFailure, ActionBlocks, MessageOptions, CommitResult,
|
|
3
|
+
PendRequest, CommitRequest, BlockGets, IPeerNetwork, PeerId, BlockId
|
|
4
|
+
} from "@optimystic/db-core";
|
|
5
|
+
import type { RepoMessage } from "@optimystic/db-core";
|
|
6
|
+
import { blockIdsForTransforms } from "@optimystic/db-core";
|
|
7
|
+
import { ProtocolClient } from "../protocol-client.js";
|
|
8
|
+
import type { RedirectPayload } from "./redirect.js";
|
|
9
|
+
import { MAX_BLOCK_MESSAGE_BYTES } from "../protocol-limits.js";
|
|
10
|
+
import { peerIdFromString } from "@libp2p/peer-id";
|
|
11
|
+
|
|
12
|
+
export class RepoClient extends ProtocolClient implements IRepo {
|
|
13
|
+
private constructor(peerId: PeerId, peerNetwork: IPeerNetwork, readonly protocolPrefix?: string) {
|
|
14
|
+
super(peerId, peerNetwork);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Create a new client instance */
|
|
18
|
+
public static create(peerId: PeerId, peerNetwork: IPeerNetwork, protocolPrefix?: string): RepoClient {
|
|
19
|
+
return new RepoClient(peerId, peerNetwork, protocolPrefix);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async get(blockGets: BlockGets, options: MessageOptions): Promise<GetBlockResults> {
|
|
23
|
+
return this.processRepoMessage<GetBlockResults>(
|
|
24
|
+
[{ get: blockGets }],
|
|
25
|
+
options
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async pend(request: PendRequest, options: MessageOptions): Promise<PendSuccess | StaleFailure> {
|
|
30
|
+
return this.processRepoMessage<PendSuccess | StaleFailure>(
|
|
31
|
+
[{ pend: request }],
|
|
32
|
+
options
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async cancel(actionRef: ActionBlocks, options: MessageOptions): Promise<void> {
|
|
37
|
+
return this.processRepoMessage<void>(
|
|
38
|
+
[{ cancel: { actionRef } }],
|
|
39
|
+
options
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async commit(request: CommitRequest, options: MessageOptions): Promise<CommitResult> {
|
|
44
|
+
return this.processRepoMessage<CommitResult>(
|
|
45
|
+
[{ commit: request }],
|
|
46
|
+
options
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private extractCorrelationId(operations: RepoMessage['operations']): string | undefined {
|
|
51
|
+
const op = operations[0];
|
|
52
|
+
if (!op) return undefined;
|
|
53
|
+
if ('pend' in op) return op.pend.actionId;
|
|
54
|
+
if ('commit' in op) return op.commit.actionId;
|
|
55
|
+
if ('cancel' in op) return op.cancel.actionRef.actionId;
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private async processRepoMessage<T>(
|
|
60
|
+
operations: RepoMessage['operations'],
|
|
61
|
+
options: MessageOptions,
|
|
62
|
+
hop: number = 0
|
|
63
|
+
): Promise<T> {
|
|
64
|
+
const message: RepoMessage = {
|
|
65
|
+
operations,
|
|
66
|
+
expiration: options.expiration,
|
|
67
|
+
};
|
|
68
|
+
const correlationId = this.extractCorrelationId(operations);
|
|
69
|
+
const deadline = options.expiration ?? (Date.now() + 30_000)
|
|
70
|
+
const deadlineMs = Math.max(1, deadline - Date.now())
|
|
71
|
+
const preferred = (this.protocolPrefix ?? '/db-p2p') + '/repo/1.0.0'
|
|
72
|
+
|
|
73
|
+
// Drive the remaining `expiration` budget through an AbortController whose
|
|
74
|
+
// abort *reason* is the caller-facing `'RepoClient timeout'` Error, combined
|
|
75
|
+
// with any caller signal. processMessage forwards `signal` to both the dial
|
|
76
|
+
// and the response-read and calls `stream.abort(signal.reason)` on abort, so —
|
|
77
|
+
// unlike the old `Promise.race`, whose losing branch left the inner read
|
|
78
|
+
// running and leaked a pending read + stream on every timed-out RPC to a silent
|
|
79
|
+
// peer — the deadline now genuinely cancels the inner read while the caller
|
|
80
|
+
// still observes an error whose `.message === 'RepoClient timeout'`.
|
|
81
|
+
//
|
|
82
|
+
// Intentionally no `responseTimeoutMs`: the combined signal already bounds the
|
|
83
|
+
// read at `deadlineMs`. A second, shorter cap would surface as
|
|
84
|
+
// ResponseTimeoutError and mask the caller-facing 'RepoClient timeout' message.
|
|
85
|
+
const deadlineController = new AbortController()
|
|
86
|
+
const timer = setTimeout(
|
|
87
|
+
() => deadlineController.abort(new Error('RepoClient timeout')),
|
|
88
|
+
deadlineMs
|
|
89
|
+
)
|
|
90
|
+
// Explicit combinator rather than the native `AbortSignal.any`, which Hermes
|
|
91
|
+
// (React Native's JS engine) does not provide. This has to forward whichever
|
|
92
|
+
// source signal's *reason* fired — that's how the caller-facing 'RepoClient
|
|
93
|
+
// timeout' message above (and any reason a caller's own `options.signal` carries)
|
|
94
|
+
// survives the combine. `any-signal` (the package libp2p itself uses for this) was
|
|
95
|
+
// tried and rejected here: it calls the composite `AbortController.abort()` with no
|
|
96
|
+
// argument, which discards the source reason and replaces it with a generic
|
|
97
|
+
// AbortError — confirmed against both Node's native `AbortSignal.any` and
|
|
98
|
+
// `any-signal@4.1.1` directly. Listeners are removed in the `finally` below so a
|
|
99
|
+
// long-lived caller `options.signal` never accumulates one per call.
|
|
100
|
+
const abortController = new AbortController()
|
|
101
|
+
const forwardAbort = (source: AbortSignal) => (): void => abortController.abort(source.reason)
|
|
102
|
+
const onCallerAbort = options?.signal ? forwardAbort(options.signal) : undefined
|
|
103
|
+
const onDeadlineAbort = forwardAbort(deadlineController.signal)
|
|
104
|
+
deadlineController.signal.addEventListener('abort', onDeadlineAbort, { once: true })
|
|
105
|
+
if (options?.signal) {
|
|
106
|
+
if (options.signal.aborted) abortController.abort(options.signal.reason)
|
|
107
|
+
else options.signal.addEventListener('abort', onCallerAbort!, { once: true })
|
|
108
|
+
}
|
|
109
|
+
const combinedSignal = abortController.signal
|
|
110
|
+
let response: any
|
|
111
|
+
try {
|
|
112
|
+
response = await super.processMessage<any>(message, preferred, {
|
|
113
|
+
signal: combinedSignal,
|
|
114
|
+
correlationId,
|
|
115
|
+
dialTimeoutMs: options?.dialTimeoutMs,
|
|
116
|
+
// A get response carries block data → block cap (not control).
|
|
117
|
+
maxDataLength: MAX_BLOCK_MESSAGE_BYTES,
|
|
118
|
+
})
|
|
119
|
+
} finally {
|
|
120
|
+
clearTimeout(timer)
|
|
121
|
+
deadlineController.signal.removeEventListener('abort', onDeadlineAbort)
|
|
122
|
+
if (onCallerAbort) options?.signal?.removeEventListener('abort', onCallerAbort)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Type the redirect branch against the payload the service actually produces
|
|
126
|
+
// (`RedirectPayload`) rather than reading it off the `any` response — the untyped read
|
|
127
|
+
// is what let the `addrs` the sender embedded fall on the floor.
|
|
128
|
+
const redirectResponse = response as Partial<RedirectPayload>
|
|
129
|
+
if (redirectResponse?.redirect?.peers?.length) {
|
|
130
|
+
if (hop >= 2) {
|
|
131
|
+
throw new Error('Redirect loop detected in RepoClient (max hops reached)')
|
|
132
|
+
}
|
|
133
|
+
const currentIdStr = this.peerId.toString()
|
|
134
|
+
const peers = redirectResponse.redirect.peers
|
|
135
|
+
const next = peers.find((p) => p.id !== currentIdStr) ?? peers[0]!
|
|
136
|
+
const nextId = peerIdFromString(next.id)
|
|
137
|
+
if (next.id === currentIdStr) {
|
|
138
|
+
throw new Error('Redirect loop detected in RepoClient (same peer)')
|
|
139
|
+
}
|
|
140
|
+
// Learn the redirect target's addresses BEFORE dialing it: the redirect is the only
|
|
141
|
+
// notice we get that this peer matters, and if it is relay-only we have no address
|
|
142
|
+
// for it at all. Merging after the dial would help only some later hop.
|
|
143
|
+
this.peerNetwork.recordPeerAddresses?.(nextId, next.addrs ?? [])
|
|
144
|
+
// Cache hint: a follow-up op on this block can dial the target directly.
|
|
145
|
+
const coordinated = this.coordinatedBlockId(operations)
|
|
146
|
+
if (coordinated) this.recordCoordinatorHint(coordinated, nextId)
|
|
147
|
+
// single-hop retry against target peer using repo protocol
|
|
148
|
+
const nextClient = RepoClient.create(nextId, this.peerNetwork, this.protocolPrefix)
|
|
149
|
+
return await nextClient.processRepoMessage<T>(operations, options, hop + 1)
|
|
150
|
+
}
|
|
151
|
+
return response as T;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The block a redirected op is coordinated on — the block `RepoService.deriveBlockKey` redirected
|
|
156
|
+
* it by, and so the block its coordinator hint belongs to.
|
|
157
|
+
*/
|
|
158
|
+
private coordinatedBlockId(ops: RepoMessage['operations']): BlockId | undefined {
|
|
159
|
+
const op = ops[0];
|
|
160
|
+
if (!op) return undefined;
|
|
161
|
+
if ('get' in op) return op.get.blockIds[0];
|
|
162
|
+
// Key on a real block id the pend touches, NOT a structural transforms field
|
|
163
|
+
// name ('inserts'/'updates'/'deletes'); see RepoService.deriveBlockKey.
|
|
164
|
+
if ('pend' in op) return blockIdsForTransforms(op.pend.transforms)[0];
|
|
165
|
+
// Anchor on blockIds[0] (where CoordinatorRepo.commit runs consensus +
|
|
166
|
+
// verifyResponsibility), NOT tailId — they differ for a non-tail batch.
|
|
167
|
+
if ('commit' in op) return op.commit.blockIds[0];
|
|
168
|
+
if ('cancel' in op) return op.cancel.actionRef.blockIds[0];
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
}
|