p2p-fret 0.5.0 → 0.6.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/estimate/size-estimator.d.ts +8 -2
- package/dist/src/estimate/size-estimator.js +8 -2
- package/dist/src/index.d.ts +1 -1
- package/dist/src/rpc/leave.js +2 -8
- package/dist/src/rpc/maybe-act.js +2 -8
- package/dist/src/rpc/neighbors.js +10 -12
- package/dist/src/rpc/ping.js +2 -8
- package/dist/src/rpc/protocols.d.ts +30 -0
- package/dist/src/rpc/protocols.js +64 -0
- package/dist/src/service/cohort.d.ts +7 -2
- package/dist/src/service/cohort.js +8 -3
- package/dist/src/service/fret-service.d.ts +66 -2
- package/dist/src/service/fret-service.js +313 -37
- package/dist/src/service/peer-discovery.js +5 -0
- package/dist/src/store/digitree-store.d.ts +24 -5
- package/dist/src/store/digitree-store.js +103 -26
- package/package.json +2 -1
- package/src/estimate/size-estimator.ts +9 -3
- package/src/index.ts +1 -1
- package/src/rpc/leave.ts +4 -9
- package/src/rpc/maybe-act.ts +5 -10
- package/src/rpc/neighbors.ts +10 -11
- package/src/rpc/ping.ts +3 -8
- package/src/rpc/protocols.ts +68 -0
- package/src/service/cohort.ts +10 -4
- package/src/service/fret-service.ts +1632 -1377
- package/src/service/peer-discovery.ts +4 -0
- package/src/store/digitree-store.ts +117 -23
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
import type { DigitreeStore } from '../store/digitree-store.js';
|
|
1
|
+
import type { DigitreeStore, PeerEntry } from '../store/digitree-store.js';
|
|
2
2
|
export type SizeEstimate = {
|
|
3
3
|
n: number;
|
|
4
4
|
confidence: number;
|
|
5
5
|
};
|
|
6
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Online network-size estimate from inter-peer ring gaps. An optional `filter` scopes the
|
|
8
|
+
* estimate to a subset of the store (e.g. same-network members only) so foreign peers from
|
|
9
|
+
* a co-resident network can't inflate `n_est` and the derived cluster span / near-radius.
|
|
10
|
+
* Defaults to counting every entry, leaving the standalone/simulator path unchanged.
|
|
11
|
+
*/
|
|
12
|
+
export declare function estimateSizeAndConfidence(store: DigitreeStore, m: number, filter?: (e: PeerEntry) => boolean): SizeEstimate;
|
|
@@ -11,8 +11,14 @@ function medianBigInt(values) {
|
|
|
11
11
|
const mid = Math.floor(arr.length / 2);
|
|
12
12
|
return arr.length % 2 === 0 ? (arr[mid - 1] + arr[mid]) / 2n : arr[mid];
|
|
13
13
|
}
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Online network-size estimate from inter-peer ring gaps. An optional `filter` scopes the
|
|
16
|
+
* estimate to a subset of the store (e.g. same-network members only) so foreign peers from
|
|
17
|
+
* a co-resident network can't inflate `n_est` and the derived cluster span / near-radius.
|
|
18
|
+
* Defaults to counting every entry, leaving the standalone/simulator path unchanged.
|
|
19
|
+
*/
|
|
20
|
+
export function estimateSizeAndConfidence(store, m, filter) {
|
|
21
|
+
const peers = filter ? store.list().filter(filter) : store.list();
|
|
16
22
|
const count = peers.length;
|
|
17
23
|
if (count === 0)
|
|
18
24
|
return { n: 0, confidence: 0 };
|
package/dist/src/index.d.ts
CHANGED
|
@@ -120,7 +120,7 @@ export { hashKey, hashPeerId } from './ring/hash.js';
|
|
|
120
120
|
export type { RingCoord } from './ring/hash.js';
|
|
121
121
|
export { xorDistance, clockwiseDistance, minDistance, lexLess } from './ring/distance.js';
|
|
122
122
|
export { DigitreeStore } from './store/digitree-store.js';
|
|
123
|
-
export type { PeerEntry, PeerState } from './store/digitree-store.js';
|
|
123
|
+
export type { PeerEntry, PeerState, MembershipState } from './store/digitree-store.js';
|
|
124
124
|
export { estimateSizeAndConfidence } from './estimate/size-estimator.js';
|
|
125
125
|
export type { SizeEstimate } from './estimate/size-estimator.js';
|
|
126
126
|
export { assembleCohort } from './service/cohort.js';
|
package/dist/src/rpc/leave.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { peerIdFromString } from '@libp2p/peer-id';
|
|
2
|
-
import { PROTOCOL_LEAVE, encodeJson, decodeJson, readAllBounded } from './protocols.js';
|
|
2
|
+
import { PROTOCOL_LEAVE, encodeJson, decodeJson, readAllBounded, openRpcStream } from './protocols.js';
|
|
3
3
|
import { createLogger } from '../logger.js';
|
|
4
4
|
const log = createLogger('rpc:leave');
|
|
5
5
|
const MAX_REPLACEMENTS = 12;
|
|
@@ -33,15 +33,9 @@ export function registerLeave(node, onLeave, protocol = PROTOCOL_LEAVE) {
|
|
|
33
33
|
}
|
|
34
34
|
export async function sendLeave(node, peerIdStr, notice, protocol = PROTOCOL_LEAVE) {
|
|
35
35
|
const pid = peerIdFromString(peerIdStr);
|
|
36
|
-
const conns = node.getConnections(pid);
|
|
37
36
|
let stream;
|
|
38
37
|
try {
|
|
39
|
-
|
|
40
|
-
stream = await conns[0].newStream([protocol]);
|
|
41
|
-
}
|
|
42
|
-
else {
|
|
43
|
-
stream = await node.dialProtocol(pid, [protocol]);
|
|
44
|
-
}
|
|
38
|
+
stream = await openRpcStream(node, pid, [protocol]);
|
|
45
39
|
stream.send(await encodeJson(notice));
|
|
46
40
|
await stream.close();
|
|
47
41
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { peerIdFromString } from '@libp2p/peer-id';
|
|
2
|
-
import { PROTOCOL_MAYBE_ACT, encodeJson, decodeJson, readAllBounded } from './protocols.js';
|
|
2
|
+
import { PROTOCOL_MAYBE_ACT, encodeJson, decodeJson, readAllBounded, openRpcStream } from './protocols.js';
|
|
3
3
|
import { createLogger } from '../logger.js';
|
|
4
4
|
const log = createLogger('rpc:maybeAct');
|
|
5
5
|
export function registerMaybeAct(node, handle, protocol = PROTOCOL_MAYBE_ACT, maxBytes = 512 * 1024) {
|
|
@@ -18,15 +18,9 @@ export function registerMaybeAct(node, handle, protocol = PROTOCOL_MAYBE_ACT, ma
|
|
|
18
18
|
}
|
|
19
19
|
export async function sendMaybeAct(node, peerIdStr, msg, protocol = PROTOCOL_MAYBE_ACT) {
|
|
20
20
|
const pid = peerIdFromString(peerIdStr);
|
|
21
|
-
const conns = node.getConnections(pid);
|
|
22
21
|
let stream;
|
|
23
22
|
try {
|
|
24
|
-
|
|
25
|
-
stream = await conns[0].newStream([protocol]);
|
|
26
|
-
}
|
|
27
|
-
else {
|
|
28
|
-
stream = await node.dialProtocol(pid, [protocol]);
|
|
29
|
-
}
|
|
23
|
+
stream = await openRpcStream(node, pid, [protocol]);
|
|
30
24
|
stream.send(await encodeJson(msg));
|
|
31
25
|
await stream.close();
|
|
32
26
|
const bytes = await readAllBounded(stream, 512 * 1024);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { peerIdFromString } from '@libp2p/peer-id';
|
|
2
|
-
import { PROTOCOL_NEIGHBORS, PROTOCOL_NEIGHBORS_ANNOUNCE, encodeJson, decodeJson, readAllBounded, } from './protocols.js';
|
|
2
|
+
import { PROTOCOL_NEIGHBORS, PROTOCOL_NEIGHBORS_ANNOUNCE, encodeJson, decodeJson, readAllBounded, openRpcStream, } from './protocols.js';
|
|
3
3
|
import { createLogger } from '../logger.js';
|
|
4
4
|
const log = createLogger('rpc:neighbors');
|
|
5
5
|
export function registerNeighbors(node, getSnapshot, onAnnounce, protocols = { PROTOCOL_NEIGHBORS, PROTOCOL_NEIGHBORS_ANNOUNCE }, maxBytes = 128 * 1024) {
|
|
@@ -30,14 +30,13 @@ export function registerNeighbors(node, getSnapshot, onAnnounce, protocols = { P
|
|
|
30
30
|
}
|
|
31
31
|
export async function fetchNeighbors(node, peerIdOrStr, protocol = PROTOCOL_NEIGHBORS) {
|
|
32
32
|
const pid = peerIdFromString(peerIdOrStr);
|
|
33
|
-
const conns = node.getConnections(pid);
|
|
34
|
-
if (conns.length === 0) {
|
|
35
|
-
// No existing connection - skip to reduce churn
|
|
36
|
-
return { v: 1, from: peerIdOrStr, timestamp: Date.now(), successors: [], predecessors: [], sig: '' };
|
|
37
|
-
}
|
|
38
33
|
let stream;
|
|
39
34
|
try {
|
|
40
|
-
stream = await
|
|
35
|
+
stream = await openRpcStream(node, pid, [protocol], { requireExisting: true });
|
|
36
|
+
if (stream == null) {
|
|
37
|
+
// No existing connection - skip to reduce churn
|
|
38
|
+
return { v: 1, from: peerIdOrStr, timestamp: Date.now(), successors: [], predecessors: [], sig: '' };
|
|
39
|
+
}
|
|
41
40
|
const bytes = await readAllBounded(stream, 128 * 1024);
|
|
42
41
|
const res = await decodeJson(bytes);
|
|
43
42
|
if ('busy' in res && res.busy) {
|
|
@@ -60,13 +59,12 @@ export async function fetchNeighbors(node, peerIdOrStr, protocol = PROTOCOL_NEIG
|
|
|
60
59
|
}
|
|
61
60
|
export async function announceNeighbors(node, peerIdOrStr, snapshot, protocol = PROTOCOL_NEIGHBORS_ANNOUNCE) {
|
|
62
61
|
const pid = peerIdFromString(peerIdOrStr);
|
|
63
|
-
const conns = node.getConnections(pid);
|
|
64
|
-
if (conns.length === 0) {
|
|
65
|
-
return; // skip if not connected
|
|
66
|
-
}
|
|
67
62
|
let stream;
|
|
68
63
|
try {
|
|
69
|
-
stream = await
|
|
64
|
+
stream = await openRpcStream(node, pid, [protocol], { requireExisting: true });
|
|
65
|
+
if (stream == null) {
|
|
66
|
+
return; // skip if not connected
|
|
67
|
+
}
|
|
70
68
|
stream.send(await encodeJson(snapshot));
|
|
71
69
|
await stream.close();
|
|
72
70
|
}
|
package/dist/src/rpc/ping.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { peerIdFromString } from '@libp2p/peer-id';
|
|
2
|
-
import { PROTOCOL_PING, encodeJson, decodeJson, readAllBounded } from './protocols.js';
|
|
2
|
+
import { PROTOCOL_PING, encodeJson, decodeJson, readAllBounded, openRpcStream } from './protocols.js';
|
|
3
3
|
import { createLogger } from '../logger.js';
|
|
4
4
|
const log = createLogger('rpc:ping');
|
|
5
5
|
function isBusy(res) {
|
|
@@ -37,13 +37,7 @@ export async function sendPing(node, peer, protocol = PROTOCOL_PING) {
|
|
|
37
37
|
const pid = peerIdFromString(peer);
|
|
38
38
|
let stream;
|
|
39
39
|
try {
|
|
40
|
-
|
|
41
|
-
if (conns.length > 0) {
|
|
42
|
-
stream = await conns[0].newStream([protocol]);
|
|
43
|
-
}
|
|
44
|
-
else {
|
|
45
|
-
stream = await node.dialProtocol(pid, [protocol]);
|
|
46
|
-
}
|
|
40
|
+
stream = await openRpcStream(node, pid, [protocol]);
|
|
47
41
|
const bytes = await readAllBounded(stream, 1024);
|
|
48
42
|
const rttMs = Math.max(0, Date.now() - start);
|
|
49
43
|
if (bytes.length === 0)
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { Libp2p } from 'libp2p';
|
|
2
|
+
import type { PeerId, Stream } from '@libp2p/interface';
|
|
1
3
|
export declare function makeProtocols(networkName?: string): {
|
|
2
4
|
PROTOCOL_NEIGHBORS: string;
|
|
3
5
|
PROTOCOL_NEIGHBORS_ANNOUNCE: string;
|
|
@@ -10,6 +12,17 @@ export declare const PROTOCOL_NEIGHBORS_ANNOUNCE = "/optimystic/default/fret/1.0
|
|
|
10
12
|
export declare const PROTOCOL_MAYBE_ACT = "/optimystic/default/fret/1.0.0/maybeAct";
|
|
11
13
|
export declare const PROTOCOL_LEAVE = "/optimystic/default/fret/1.0.0/leave";
|
|
12
14
|
export declare const PROTOCOL_PING = "/optimystic/default/fret/1.0.0/ping";
|
|
15
|
+
/**
|
|
16
|
+
* True when `err` is libp2p's protocol-negotiation failure — i.e. the remote does
|
|
17
|
+
* not support the dialed protocol. Thrown by `dialProtocol`/`newStream` (via
|
|
18
|
+
* multistream-select) as `UnsupportedProtocolError`.
|
|
19
|
+
*
|
|
20
|
+
* This is the definitive "foreign network" signal for namespaced FRET RPCs: it must
|
|
21
|
+
* be distinguished from a generic timeout / transient failure (which leaves a peer
|
|
22
|
+
* unclassified). Matches by `name` (modern libp2p) and `code` (legacy
|
|
23
|
+
* `ERR_UNSUPPORTED_PROTOCOL`), with a message-substring fallback.
|
|
24
|
+
*/
|
|
25
|
+
export declare function isUnsupportedProtocolError(err: unknown): boolean;
|
|
13
26
|
export declare function encodeJson(obj: unknown): Promise<Uint8Array>;
|
|
14
27
|
export declare function decodeJson<T = unknown>(bytes: Uint8Array): Promise<T>;
|
|
15
28
|
export declare function toBytes(chunk: Uint8Array | {
|
|
@@ -19,3 +32,20 @@ export declare function readAllBounded(stream: AsyncIterable<Uint8Array | {
|
|
|
19
32
|
subarray(): Uint8Array;
|
|
20
33
|
}>, maxBytes: number, timeoutMs?: number): Promise<Uint8Array>;
|
|
21
34
|
export declare function validateTimestamp(ts: number, maxDriftMs?: number): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Open an RPC stream to `pid`, preferring a DIRECT open connection and falling
|
|
37
|
+
* back to a limited (circuit-relay) one.
|
|
38
|
+
*
|
|
39
|
+
* `runOnLimitedConnection: true` is REQUIRED for the relayed path — libp2p
|
|
40
|
+
* rejects a stream over a limited connection without it — and is a harmless
|
|
41
|
+
* no-op on a direct connection. Preferring a direct connection avoids riding a
|
|
42
|
+
* circuit that the relay can reset once a per-circuit cap or reservation lapses
|
|
43
|
+
* (and which briefly coexists with the upgraded direct link after DCUtR).
|
|
44
|
+
*
|
|
45
|
+
* When `requireExisting` is set the caller skips dialing if no connection
|
|
46
|
+
* exists (neighbors fetch/announce reduce churn this way) and `undefined` is
|
|
47
|
+
* returned; otherwise we `dialProtocol`.
|
|
48
|
+
*/
|
|
49
|
+
export declare function openRpcStream(node: Libp2p, pid: PeerId, protocols: string[], opts?: {
|
|
50
|
+
requireExisting?: boolean;
|
|
51
|
+
}): Promise<Stream | undefined>;
|
|
@@ -14,6 +14,31 @@ export const PROTOCOL_NEIGHBORS_ANNOUNCE = '/optimystic/default/fret/1.0.0/neigh
|
|
|
14
14
|
export const PROTOCOL_MAYBE_ACT = '/optimystic/default/fret/1.0.0/maybeAct';
|
|
15
15
|
export const PROTOCOL_LEAVE = '/optimystic/default/fret/1.0.0/leave';
|
|
16
16
|
export const PROTOCOL_PING = '/optimystic/default/fret/1.0.0/ping';
|
|
17
|
+
/**
|
|
18
|
+
* True when `err` is libp2p's protocol-negotiation failure — i.e. the remote does
|
|
19
|
+
* not support the dialed protocol. Thrown by `dialProtocol`/`newStream` (via
|
|
20
|
+
* multistream-select) as `UnsupportedProtocolError`.
|
|
21
|
+
*
|
|
22
|
+
* This is the definitive "foreign network" signal for namespaced FRET RPCs: it must
|
|
23
|
+
* be distinguished from a generic timeout / transient failure (which leaves a peer
|
|
24
|
+
* unclassified). Matches by `name` (modern libp2p) and `code` (legacy
|
|
25
|
+
* `ERR_UNSUPPORTED_PROTOCOL`), with a message-substring fallback.
|
|
26
|
+
*/
|
|
27
|
+
export function isUnsupportedProtocolError(err) {
|
|
28
|
+
if (err == null || typeof err !== 'object')
|
|
29
|
+
return false;
|
|
30
|
+
const e = err;
|
|
31
|
+
if (e.name === 'UnsupportedProtocolError')
|
|
32
|
+
return true;
|
|
33
|
+
if (e.code === 'ERR_UNSUPPORTED_PROTOCOL')
|
|
34
|
+
return true;
|
|
35
|
+
if (typeof e.message === 'string') {
|
|
36
|
+
const m = e.message.toLowerCase();
|
|
37
|
+
if (m.includes('could not negotiate') || m.includes('protocol selection failed'))
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
17
42
|
export async function encodeJson(obj) {
|
|
18
43
|
const text = JSON.stringify(obj);
|
|
19
44
|
return new TextEncoder().encode(text);
|
|
@@ -79,3 +104,42 @@ export async function readAllBounded(stream, maxBytes, timeoutMs = 5000) {
|
|
|
79
104
|
export function validateTimestamp(ts, maxDriftMs = 300_000) {
|
|
80
105
|
return Math.abs(Date.now() - ts) <= maxDriftMs;
|
|
81
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* True for a circuit-relay ("limited") connection. libp2p stamps a relayed
|
|
109
|
+
* connection with `limits` (per-circuit data/duration caps); we additionally
|
|
110
|
+
* sniff the multiaddr for `/p2p-circuit` as a fallback for transports/versions
|
|
111
|
+
* that don't populate `limits`.
|
|
112
|
+
*/
|
|
113
|
+
function isLimitedConnection(c) {
|
|
114
|
+
if (c.limits != null)
|
|
115
|
+
return true;
|
|
116
|
+
const addr = c.remoteAddr?.toString?.();
|
|
117
|
+
return addr != null && addr.includes('/p2p-circuit');
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Open an RPC stream to `pid`, preferring a DIRECT open connection and falling
|
|
121
|
+
* back to a limited (circuit-relay) one.
|
|
122
|
+
*
|
|
123
|
+
* `runOnLimitedConnection: true` is REQUIRED for the relayed path — libp2p
|
|
124
|
+
* rejects a stream over a limited connection without it — and is a harmless
|
|
125
|
+
* no-op on a direct connection. Preferring a direct connection avoids riding a
|
|
126
|
+
* circuit that the relay can reset once a per-circuit cap or reservation lapses
|
|
127
|
+
* (and which briefly coexists with the upgraded direct link after DCUtR).
|
|
128
|
+
*
|
|
129
|
+
* When `requireExisting` is set the caller skips dialing if no connection
|
|
130
|
+
* exists (neighbors fetch/announce reduce churn this way) and `undefined` is
|
|
131
|
+
* returned; otherwise we `dialProtocol`.
|
|
132
|
+
*/
|
|
133
|
+
export async function openRpcStream(node, pid, protocols, opts = {}) {
|
|
134
|
+
const open = node.getConnections(pid)
|
|
135
|
+
.filter(c => c?.status === 'open' && typeof c?.newStream === 'function');
|
|
136
|
+
// Prefer a direct connection; fall back to the limited one only when it is
|
|
137
|
+
// the only open path (the steady state for browsers and NATed peers).
|
|
138
|
+
const chosen = open.find(c => !isLimitedConnection(c)) ?? open[0];
|
|
139
|
+
const streamOpts = { runOnLimitedConnection: true, negotiateFully: false };
|
|
140
|
+
if (chosen)
|
|
141
|
+
return chosen.newStream(protocols, streamOpts);
|
|
142
|
+
if (opts.requireExisting)
|
|
143
|
+
return undefined;
|
|
144
|
+
return node.dialProtocol(pid, protocols, streamOpts);
|
|
145
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DigitreeStore } from '../store/digitree-store.js';
|
|
1
|
+
import type { DigitreeStore, PeerEntry } from '../store/digitree-store.js';
|
|
2
2
|
/**
|
|
3
3
|
* Two-sided cohort assembly: an alternating successor/predecessor walk outward from
|
|
4
4
|
* `hashedCoord`, returning up to `wants` distinct peer ids. Auto-adapts when fewer than
|
|
@@ -7,5 +7,10 @@ import type { DigitreeStore } from '../store/digitree-store.js';
|
|
|
7
7
|
* Pure over the store — no libp2p/node state — so the same selection logic is shared by
|
|
8
8
|
* `FretService.assembleCohort` and out-of-band consumers (e.g. the Optimystic design
|
|
9
9
|
* simulator) with zero divergence.
|
|
10
|
+
*
|
|
11
|
+
* `filter` is forwarded to the underlying ring walks: with it, the walk skips non-matching
|
|
12
|
+
* entries and keeps advancing (so a cluster of foreign peers near the key can't starve the
|
|
13
|
+
* cohort — the over-fetch of `wants * 2` plus skip-and-continue still reaches `wants`
|
|
14
|
+
* matching members). Defaults to no filter, leaving the standalone/simulator path unchanged.
|
|
10
15
|
*/
|
|
11
|
-
export declare function assembleCohort(store: DigitreeStore, hashedCoord: Uint8Array, wants: number, exclude?: Set<string
|
|
16
|
+
export declare function assembleCohort(store: DigitreeStore, hashedCoord: Uint8Array, wants: number, exclude?: Set<string>, filter?: (e: PeerEntry) => boolean): string[];
|
|
@@ -6,12 +6,17 @@
|
|
|
6
6
|
* Pure over the store — no libp2p/node state — so the same selection logic is shared by
|
|
7
7
|
* `FretService.assembleCohort` and out-of-band consumers (e.g. the Optimystic design
|
|
8
8
|
* simulator) with zero divergence.
|
|
9
|
+
*
|
|
10
|
+
* `filter` is forwarded to the underlying ring walks: with it, the walk skips non-matching
|
|
11
|
+
* entries and keeps advancing (so a cluster of foreign peers near the key can't starve the
|
|
12
|
+
* cohort — the over-fetch of `wants * 2` plus skip-and-continue still reaches `wants`
|
|
13
|
+
* matching members). Defaults to no filter, leaving the standalone/simulator path unchanged.
|
|
9
14
|
*/
|
|
10
|
-
export function assembleCohort(store, hashedCoord, wants, exclude) {
|
|
15
|
+
export function assembleCohort(store, hashedCoord, wants, exclude, filter) {
|
|
11
16
|
const out = [];
|
|
12
17
|
const ex = exclude ?? new Set();
|
|
13
|
-
const succIds = store.neighborsRight(hashedCoord, wants * 2);
|
|
14
|
-
const predIds = store.neighborsLeft(hashedCoord, wants * 2);
|
|
18
|
+
const succIds = store.neighborsRight(hashedCoord, wants * 2, filter);
|
|
19
|
+
const predIds = store.neighborsLeft(hashedCoord, wants * 2, filter);
|
|
15
20
|
let si = 0, pi = 0;
|
|
16
21
|
while (out.length < wants && (si < succIds.length || pi < predIds.length)) {
|
|
17
22
|
if (out.length % 2 === 0 && si < succIds.length) {
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import type { Startable } from '@libp2p/interface';
|
|
2
2
|
import type { FretService as IFretService, FretMode, FretConfig, ReportEvent, RouteAndMaybeActV1, NearAnchorV1, SerializedTable, ActivityHandler, RouteProgress, LookupOptions } from '../index.js';
|
|
3
|
-
import { DigitreeStore } from '../store/digitree-store.js';
|
|
3
|
+
import { DigitreeStore, type PeerEntry } from '../store/digitree-store.js';
|
|
4
4
|
import type { Libp2p } from 'libp2p';
|
|
5
5
|
import { type SparsityModel } from '../store/relevance.js';
|
|
6
6
|
/**
|
|
7
7
|
* Select sample entries spread across diverse ring positions using sparsity-biased scoring.
|
|
8
8
|
* Excludes self and entries already in successors/predecessors (they're redundant).
|
|
9
|
+
*
|
|
10
|
+
* `filter` scopes the candidate set (FretService passes the member predicate so an outgoing
|
|
11
|
+
* snapshot never advertises a foreign peer to same-network neighbors). Defaults to no filter,
|
|
12
|
+
* leaving the exported standalone unchanged.
|
|
9
13
|
*/
|
|
10
|
-
export declare function selectDiverseSample(store: DigitreeStore, selfCoord: Uint8Array, sparsity: SparsityModel, excludeIds: Set<string>, cap: number): Array<{
|
|
14
|
+
export declare function selectDiverseSample(store: DigitreeStore, selfCoord: Uint8Array, sparsity: SparsityModel, excludeIds: Set<string>, cap: number, filter?: (e: PeerEntry) => boolean): Array<{
|
|
11
15
|
id: string;
|
|
12
16
|
coord: string;
|
|
13
17
|
relevance: number;
|
|
@@ -18,6 +22,8 @@ export declare class FretService implements IFretService, Startable {
|
|
|
18
22
|
private readonly cfg;
|
|
19
23
|
private readonly node;
|
|
20
24
|
private stabilizing;
|
|
25
|
+
private stopped;
|
|
26
|
+
private readonly nodeListeners;
|
|
21
27
|
private inflightAct;
|
|
22
28
|
private readonly bucketNeighbors;
|
|
23
29
|
private readonly bucketMaybeAct;
|
|
@@ -51,8 +57,27 @@ export declare class FretService implements IFretService, Startable {
|
|
|
51
57
|
private applyTouch;
|
|
52
58
|
private applySuccess;
|
|
53
59
|
private applyFailure;
|
|
60
|
+
/** Confirm `id` serves this network (idempotent; no-op if absent or already member). */
|
|
61
|
+
private markMember;
|
|
62
|
+
/**
|
|
63
|
+
* Confirm `id` does NOT serve this network (idempotent). Foreign is reversible —
|
|
64
|
+
* a later successful namespaced RPC or re-identify promotes it back to member.
|
|
65
|
+
* We tag and retain rather than evict: an evicted foreign peer is just re-added by
|
|
66
|
+
* the next peer:connect/peerStore seed and re-probed in a loop.
|
|
67
|
+
*/
|
|
68
|
+
private markForeign;
|
|
69
|
+
/**
|
|
70
|
+
* Classify `id` from a libp2p-reported protocol list (available once identify has
|
|
71
|
+
* run). Member if any of our namespaced protocols appears; foreign if the list is
|
|
72
|
+
* non-empty but contains none of them; left unknown if empty (identify pending).
|
|
73
|
+
*/
|
|
74
|
+
private classifyByProtocols;
|
|
54
75
|
start(): Promise<void>;
|
|
55
76
|
stop(): Promise<void>;
|
|
77
|
+
/** Register a node event listener and track it so stop() can detach it. */
|
|
78
|
+
private addNodeListener;
|
|
79
|
+
/** Detach all node event listeners registered via addNodeListener. */
|
|
80
|
+
private removeNodeListeners;
|
|
56
81
|
setMode(mode: FretMode): void;
|
|
57
82
|
ready(): Promise<void>;
|
|
58
83
|
private maxBytesNeighbors;
|
|
@@ -75,12 +100,50 @@ export declare class FretService implements IFretService, Startable {
|
|
|
75
100
|
private announceOnDeparture;
|
|
76
101
|
private isNearNeighbor;
|
|
77
102
|
private announceToNewPeers;
|
|
103
|
+
/**
|
|
104
|
+
* Feed a received snapshot's network-size estimate into the local estimator.
|
|
105
|
+
* No-op unless both estimate and confidence are present and positive.
|
|
106
|
+
*/
|
|
107
|
+
private calibrateSizeFromSnapshot;
|
|
78
108
|
private mergeAnnounceSnapshot;
|
|
79
109
|
private seedFromPeerStore;
|
|
80
110
|
private startStabilizationLoop;
|
|
81
111
|
private seedFromBootstraps;
|
|
82
112
|
private stabilizeOnce;
|
|
83
113
|
private probeNeighborsLatency;
|
|
114
|
+
/**
|
|
115
|
+
* Bounded classification probe pass over `unknown` peers.
|
|
116
|
+
*
|
|
117
|
+
* Normal traffic only touches peers the ring already selects, but the follow-on
|
|
118
|
+
* gating work will exclude `unknown` peers from the ring — so without a dedicated
|
|
119
|
+
* pass an unknown same-network peer would never be selected, never probed, and be
|
|
120
|
+
* permanently starved. This iterates unknowns directly from the store (not through
|
|
121
|
+
* ring views), prefers connected / has-addresses ones, and sends a namespaced ping
|
|
122
|
+
* to at most N per tick. It runs only while unknowns exist; in single-network
|
|
123
|
+
* steady state every peer becomes member and this is a no-op (no extra traffic).
|
|
124
|
+
*/
|
|
125
|
+
private classifyUnknownPeers;
|
|
126
|
+
/**
|
|
127
|
+
* Occasionally re-probe a few `foreign` peers so a same-network peer that was *mislabeled*
|
|
128
|
+
* foreign (e.g. identify completed before the peer registered our protocol handlers) is
|
|
129
|
+
* re-admitted via a successful namespaced ping. Before ring-membership gating this self-
|
|
130
|
+
* healed for free — `probeNeighborsLatency` pinged near ring peers regardless of label —
|
|
131
|
+
* but gating excludes foreign peers from the ring, closing that path. The `peer:update`
|
|
132
|
+
* identify path also re-admits, but it is not guaranteed (it depends on the remote pushing
|
|
133
|
+
* an identify update), so this RPC path is the backstop the gating work owes (per the
|
|
134
|
+
* `ring-membership-classification` review tripwire).
|
|
135
|
+
*
|
|
136
|
+
* Bounded and self-limiting: only a couple of off-backoff foreign peers per tick, and a
|
|
137
|
+
* confirmed-foreign probe records a growing backoff (see `probeMembership`), so a genuinely
|
|
138
|
+
* foreign peer is re-probed at most ~once per backoff window rather than every tick.
|
|
139
|
+
*/
|
|
140
|
+
private reprobeForeignPeers;
|
|
141
|
+
/**
|
|
142
|
+
* Probe a single peer's membership with a namespaced ping. Success → member;
|
|
143
|
+
* unsupported-protocol → foreign; timeout / transient → stay unknown but back off
|
|
144
|
+
* so we don't hammer an unreachable-but-connected peer every tick.
|
|
145
|
+
*/
|
|
146
|
+
private probeMembership;
|
|
84
147
|
private mergeNeighborSnapshots;
|
|
85
148
|
private snapshot;
|
|
86
149
|
neighborDistance(selfId: string, hashedCoord: Uint8Array, k: number): number;
|
|
@@ -101,6 +164,7 @@ export declare class FretService implements IFretService, Startable {
|
|
|
101
164
|
private recordBackoff;
|
|
102
165
|
private clearBackoff;
|
|
103
166
|
private getBackoffPenalty;
|
|
167
|
+
private pruneBackoffMap;
|
|
104
168
|
report(_evt: ReportEvent): void;
|
|
105
169
|
/**
|
|
106
170
|
* Add an external network size observation (e.g., from cluster messages, peer queries)
|