p2p-fret 0.6.0 → 1.0.0-beta.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/README.md +5 -5
- package/dist/src/estimate/size-estimator.d.ts +21 -5
- package/dist/src/estimate/size-estimator.js +138 -29
- package/dist/src/index.d.ts +31 -14
- package/dist/src/index.js +37 -5
- package/dist/src/ring/distance.d.ts +28 -1
- package/dist/src/ring/distance.js +52 -15
- package/dist/src/ring/hash.d.ts +12 -0
- package/dist/src/ring/hash.js +25 -6
- package/dist/src/rpc/leave.d.ts +6 -2
- package/dist/src/rpc/leave.js +32 -44
- package/dist/src/rpc/maybe-act.d.ts +16 -4
- package/dist/src/rpc/maybe-act.js +31 -34
- package/dist/src/rpc/neighbors.d.ts +38 -3
- package/dist/src/rpc/neighbors.js +100 -72
- package/dist/src/rpc/outcome.d.ts +79 -0
- package/dist/src/rpc/outcome.js +1 -0
- package/dist/src/rpc/ping.d.ts +13 -4
- package/dist/src/rpc/ping.js +59 -61
- package/dist/src/rpc/protocols.d.ts +275 -7
- package/dist/src/rpc/protocols.js +559 -49
- package/dist/src/rpc/request.d.ts +59 -0
- package/dist/src/rpc/request.js +200 -0
- package/dist/src/rpc/validate.d.ts +189 -0
- package/dist/src/rpc/validate.js +394 -0
- package/dist/src/selector/next-hop.d.ts +14 -0
- package/dist/src/selector/next-hop.js +114 -71
- package/dist/src/service/cohort.d.ts +7 -2
- package/dist/src/service/cohort.js +29 -14
- package/dist/src/service/dedup-cache.d.ts +29 -6
- package/dist/src/service/dedup-cache.js +34 -34
- package/dist/src/service/fret-service.d.ts +788 -38
- package/dist/src/service/fret-service.js +2051 -553
- package/dist/src/service/libp2p-fret-service.d.ts +44 -9
- package/dist/src/service/libp2p-fret-service.js +62 -18
- package/dist/src/service/live-member.d.ts +28 -0
- package/dist/src/service/live-member.js +27 -0
- package/dist/src/service/payload-heuristic.d.ts +4 -2
- package/dist/src/service/payload-heuristic.js +12 -3
- package/dist/src/service/peer-discovery.d.ts +74 -8
- package/dist/src/service/peer-discovery.js +105 -36
- package/dist/src/store/digitree-store.d.ts +140 -6
- package/dist/src/store/digitree-store.js +142 -52
- package/dist/src/store/relevance.d.ts +40 -1
- package/dist/src/store/relevance.js +59 -20
- package/dist/src/utils/deadline.d.ts +37 -0
- package/dist/src/utils/deadline.js +64 -0
- package/dist/src/utils/expiring-map.d.ts +74 -0
- package/dist/src/utils/expiring-map.js +129 -0
- package/dist/src/utils/pool.d.ts +48 -0
- package/dist/src/utils/pool.js +60 -0
- package/dist/src/utils/token-bucket.d.ts +15 -0
- package/dist/src/utils/token-bucket.js +33 -4
- package/package.json +6 -2
- package/src/estimate/size-estimator.ts +176 -54
- package/src/index.ts +199 -142
- package/src/logger.ts +7 -7
- package/src/ring/distance.ts +49 -16
- package/src/ring/hash.ts +29 -6
- package/src/rpc/leave.ts +65 -63
- package/src/rpc/maybe-act.ts +58 -49
- package/src/rpc/neighbors.ts +111 -69
- package/src/rpc/outcome.ts +57 -0
- package/src/rpc/ping.ts +95 -83
- package/src/rpc/protocols.ts +718 -156
- package/src/rpc/request.ts +296 -0
- package/src/rpc/validate.ts +403 -0
- package/src/selector/next-hop.ts +318 -234
- package/src/service/cohort.ts +61 -43
- package/src/service/dedup-cache.ts +53 -44
- package/src/service/fret-service.ts +2094 -509
- package/src/service/libp2p-fret-service.ts +85 -23
- package/src/service/live-member.ts +29 -0
- package/src/service/payload-heuristic.ts +12 -3
- package/src/service/peer-discovery.ts +205 -98
- package/src/store/digitree-store.ts +226 -54
- package/src/store/relevance.ts +59 -18
- package/src/utils/deadline.ts +85 -0
- package/src/utils/expiring-map.ts +164 -0
- package/src/utils/pool.ts +79 -0
- package/src/utils/token-bucket.ts +70 -39
- package/dist/src/service/discovery.d.ts +0 -3
- package/dist/src/service/discovery.js +0 -15
- package/src/service/discovery.ts +0 -16
package/README.md
CHANGED
|
@@ -79,7 +79,7 @@ FRET detects potential partitions using multiple signals:
|
|
|
79
79
|
|
|
80
80
|
```typescript
|
|
81
81
|
// FRET automatically includes size estimates in ping responses
|
|
82
|
-
registerPing(node, PROTOCOL_PING, () => {
|
|
82
|
+
await registerPing(node, PROTOCOL_PING, () => {
|
|
83
83
|
return fretService.getNetworkSizeEstimate();
|
|
84
84
|
});
|
|
85
85
|
|
|
@@ -104,7 +104,7 @@ interface FretService {
|
|
|
104
104
|
exportTable(): SerializedTable;
|
|
105
105
|
|
|
106
106
|
// Import a previously exported snapshot; returns number of entries loaded
|
|
107
|
-
importTable(table: SerializedTable): number
|
|
107
|
+
importTable(table: SerializedTable): Promise<number>;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
interface SerializedTable {
|
|
@@ -123,7 +123,7 @@ interface SerializedPeerEntry {
|
|
|
123
123
|
accessCount: number;
|
|
124
124
|
successCount: number;
|
|
125
125
|
failureCount: number;
|
|
126
|
-
avgLatencyMs: number;
|
|
126
|
+
avgLatencyMs: number | null; // null = never measured (absent field reads back as null)
|
|
127
127
|
metadata?: Record<string, any>;
|
|
128
128
|
}
|
|
129
129
|
```
|
|
@@ -144,7 +144,7 @@ await fs.writeFile('fret-table.json', JSON.stringify(table));
|
|
|
144
144
|
const saved: SerializedTable = JSON.parse(
|
|
145
145
|
await fs.readFile('fret-table.json', 'utf-8')
|
|
146
146
|
);
|
|
147
|
-
const count = fret.importTable(saved);
|
|
147
|
+
const count = await fret.importTable(saved);
|
|
148
148
|
console.log(`Restored ${count} routing entries`);
|
|
149
149
|
|
|
150
150
|
await fret.start();
|
|
@@ -153,7 +153,7 @@ await fret.start();
|
|
|
153
153
|
### Behavior
|
|
154
154
|
|
|
155
155
|
- **State reset**: All imported entries get `state: 'disconnected'` regardless of their exported state — connection liveness must be re-established through pings and stabilization.
|
|
156
|
-
- **Capacity enforcement**: `importTable` enforces the configured capacity after loading. A table exported from a node with a larger capacity won't exceed the importer's limit.
|
|
156
|
+
- **Capacity enforcement**: `importTable` enforces the configured capacity after loading. A table exported from a node with a larger capacity won't exceed the importer's limit. `importTable` is `async` — enforcement needs the self ring coordinate, which is commonly not yet cached when import runs before `start()` — so always `await` it.
|
|
157
157
|
- **Stale data safety**: After import, the normal stabilization loop probes peers to update connection states and relevance scores. Unreachable peers are decayed and eventually evicted.
|
|
158
158
|
- **JSON round-trip safe**: `SerializedTable` survives `JSON.stringify` / `JSON.parse`.
|
|
159
159
|
|
|
@@ -3,10 +3,26 @@ export type SizeEstimate = {
|
|
|
3
3
|
n: number;
|
|
4
4
|
confidence: number;
|
|
5
5
|
};
|
|
6
|
+
export interface SizeEstimateOptions {
|
|
7
|
+
/** Restrict the estimate to a subset of the store (e.g. same-network members only). */
|
|
8
|
+
filter?: (e: PeerEntry) => boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Ring coordinate of the local node. When supplied, gaps are drawn from the
|
|
11
|
+
* successor/predecessor window around it — the intended arc-length method. Omitting it
|
|
12
|
+
* selects the degraded whole-store fallback (see {@link estimateSizeAndConfidence}).
|
|
13
|
+
*/
|
|
14
|
+
selfCoord?: Uint8Array;
|
|
15
|
+
}
|
|
6
16
|
/**
|
|
7
|
-
* Online network-size estimate from inter-peer ring gaps
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
17
|
+
* Online network-size estimate from inter-peer ring gaps: `n = 2^256 / gap`.
|
|
18
|
+
*
|
|
19
|
+
* `options.selfCoord` selects the successor/predecessor window as the gap population and is the
|
|
20
|
+
* intended path; omitting it falls back to whole-store gaps (see {@link collectGaps}).
|
|
21
|
+
* `options.filter` scopes the estimate to a subset of the store (e.g. same-network members only)
|
|
22
|
+
* so foreign peers from a co-resident network can't inflate `n` and the derived cluster span /
|
|
23
|
+
* near-radius. It defaults to counting every entry, leaving the standalone/simulator path
|
|
24
|
+
* unchanged.
|
|
25
|
+
*
|
|
26
|
+
* Confidence blends sample count against 2m with the dispersion factor above.
|
|
11
27
|
*/
|
|
12
|
-
export declare function estimateSizeAndConfidence(store: DigitreeStore, m: number,
|
|
28
|
+
export declare function estimateSizeAndConfidence(store: DigitreeStore, m: number, options?: SizeEstimateOptions): SizeEstimate;
|
|
@@ -1,51 +1,160 @@
|
|
|
1
|
+
const RING_SIZE = 1n << 256n;
|
|
2
|
+
const HALF_RING = 1n << 255n;
|
|
1
3
|
function bytesToBigInt(u8) {
|
|
2
4
|
let v = 0n;
|
|
3
5
|
for (let i = 0; i < u8.length; i++)
|
|
4
6
|
v = (v << 8n) | BigInt(u8[i]);
|
|
5
7
|
return v;
|
|
6
8
|
}
|
|
9
|
+
function ascending(a, b) {
|
|
10
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
11
|
+
}
|
|
7
12
|
function medianBigInt(values) {
|
|
8
13
|
if (values.length === 0)
|
|
9
14
|
return 0n;
|
|
10
|
-
const arr = [...values].sort(
|
|
15
|
+
const arr = [...values].sort(ascending);
|
|
11
16
|
const mid = Math.floor(arr.length / 2);
|
|
12
17
|
return arr.length % 2 === 0 ? (arr[mid - 1] + arr[mid]) / 2n : arr[mid];
|
|
13
18
|
}
|
|
19
|
+
function meanBigInt(values) {
|
|
20
|
+
if (values.length === 0)
|
|
21
|
+
return 0n;
|
|
22
|
+
let sum = 0n;
|
|
23
|
+
for (const v of values)
|
|
24
|
+
sum += v;
|
|
25
|
+
return sum / BigInt(values.length);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Differences between adjacent entries of an ascending list.
|
|
29
|
+
*
|
|
30
|
+
* The wrap-around gap (last back round to first) is deliberately **not** included. A node's
|
|
31
|
+
* knowledge of the ring is never complete, so that gap spans the arc it has not sampled — it is
|
|
32
|
+
* the single largest outlier in the population and says nothing about inter-peer spacing.
|
|
33
|
+
*/
|
|
34
|
+
function consecutiveGaps(ascendingValues) {
|
|
35
|
+
const gaps = [];
|
|
36
|
+
for (let i = 1; i < ascendingValues.length; i++)
|
|
37
|
+
gaps.push(ascendingValues[i] - ascendingValues[i - 1]);
|
|
38
|
+
return gaps;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Gaps between adjacent members of the successor/predecessor window around `selfCoord`.
|
|
42
|
+
*
|
|
43
|
+
* Coordinates are re-centred on self as *signed* offsets in (−2^255, 2^255] rather than compared
|
|
44
|
+
* raw, so a window straddling coordinate 0 stays one contiguous run. Sorting raw coordinates
|
|
45
|
+
* would split such a window in two and manufacture an interior gap the size of the rest of the
|
|
46
|
+
* ring — precisely the outlier this method exists to avoid.
|
|
47
|
+
*
|
|
48
|
+
* Self is normally present in the store, and a walk anchored exactly at `selfCoord` returns it
|
|
49
|
+
* as its own first result on *both* sides — so each side is asked for `m + 1` to still yield the
|
|
50
|
+
* m successors and m predecessors the S/P window is defined as, and offsets are collected in a
|
|
51
|
+
* `Set` so self (and any coincident coordinate) contributes once. Asking for plain `m` instead
|
|
52
|
+
* costs two of the 2m gaps. When self is absent from the store the extra slot simply widens the
|
|
53
|
+
* window by one peer per side, which is harmless.
|
|
54
|
+
*/
|
|
55
|
+
function windowGaps(store, m, selfCoord, filter) {
|
|
56
|
+
const selfBig = bytesToBigInt(selfCoord);
|
|
57
|
+
const offsets = new Set([0n]);
|
|
58
|
+
const addById = (id) => {
|
|
59
|
+
const entry = store.getById(id);
|
|
60
|
+
if (!entry)
|
|
61
|
+
return;
|
|
62
|
+
let d = (bytesToBigInt(entry.coord) - selfBig) % RING_SIZE;
|
|
63
|
+
if (d < 0n)
|
|
64
|
+
d += RING_SIZE;
|
|
65
|
+
offsets.add(d > HALF_RING ? d - RING_SIZE : d);
|
|
66
|
+
};
|
|
67
|
+
const reach = m + 1;
|
|
68
|
+
for (const id of store.neighborsRight(selfCoord, reach, filter))
|
|
69
|
+
addById(id);
|
|
70
|
+
for (const id of store.neighborsLeft(selfCoord, reach, filter))
|
|
71
|
+
addById(id);
|
|
72
|
+
return consecutiveGaps([...offsets].sort(ascending));
|
|
73
|
+
}
|
|
14
74
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* a
|
|
18
|
-
*
|
|
75
|
+
* Relative standard error of the mean gap, as a [0,1] sample-quality factor.
|
|
76
|
+
*
|
|
77
|
+
* Gaps on a uniform-random ring are exponentially distributed, so a coefficient of variation
|
|
78
|
+
* near 1 is the *healthy* value rather than a defect. Flooring cv at 1 encodes that prior: a
|
|
79
|
+
* synthetically perfect (evenly spaced) sample cannot claim zero sampling error from a handful
|
|
80
|
+
* of gaps. The factor is then monotone in window size, capping confidence at 0.5 + 0.5·(1 −
|
|
81
|
+
* 1/√G) — 0.875 at G = 16 (a full S/P window at the default m = 8), ~0.65 at G = 2.
|
|
82
|
+
*
|
|
83
|
+
* NOTE: this measures *local* spacing regularity only. A node whose neighbors are all packed
|
|
84
|
+
* into a tiny, evenly-spaced arc — an eclipse, or a very young ring — scores high here while
|
|
85
|
+
* `n` is wildly wrong, because no statistic over the sampled arc can see the arc that was never
|
|
86
|
+
* sampled. The defense is corroboration from peer-reported estimates (`reportNetworkSize` /
|
|
87
|
+
* `calibrateSizeFromSnapshot`), not a better local formula.
|
|
19
88
|
*/
|
|
20
|
-
|
|
21
|
-
const
|
|
89
|
+
function dispersionFactor(gaps) {
|
|
90
|
+
const count = gaps.length;
|
|
91
|
+
if (count === 0)
|
|
92
|
+
return 0;
|
|
93
|
+
const values = gaps.map(Number);
|
|
94
|
+
let sum = 0;
|
|
95
|
+
for (const v of values)
|
|
96
|
+
sum += v;
|
|
97
|
+
const mean = sum / count;
|
|
98
|
+
// Degenerate: every sampled peer sits at one coordinate, so there is no spacing to judge.
|
|
99
|
+
if (mean <= 0)
|
|
100
|
+
return 0;
|
|
101
|
+
let squared = 0;
|
|
102
|
+
for (const v of values)
|
|
103
|
+
squared += (v - mean) ** 2;
|
|
104
|
+
const cv = Math.sqrt(squared / count) / mean;
|
|
105
|
+
const rse = Math.max(cv, 1) / Math.sqrt(count);
|
|
106
|
+
return Math.max(0, Math.min(1, 1 - rse));
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* The gap population the estimate rests on, plus the representative gap derived from it.
|
|
110
|
+
*
|
|
111
|
+
* With `selfCoord` this is the successor/predecessor window and its arithmetic **mean** — the
|
|
112
|
+
* arc-length method from docs/fret.md. Without it, the degraded fallback: every known
|
|
113
|
+
* coordinate, and the **median** to blunt the outliers a whole-store population carries.
|
|
114
|
+
*
|
|
115
|
+
* The fallback is degraded because a node's knowledge is deliberately non-uniform — it knows
|
|
116
|
+
* every peer near itself but only a sparsity-weighted scattering of far ones — so whole-store
|
|
117
|
+
* gaps mix ~2m near-true spacings with a long tail of huge far-peer gaps. Once far peers
|
|
118
|
+
* outnumber near ones (the normal steady state) even the median lands in that tail and `n`
|
|
119
|
+
* collapses by an order of magnitude.
|
|
120
|
+
*/
|
|
121
|
+
function collectGaps(store, m, peers, filter, selfCoord) {
|
|
122
|
+
if (selfCoord) {
|
|
123
|
+
const gaps = windowGaps(store, m, selfCoord, filter);
|
|
124
|
+
if (gaps.length > 0)
|
|
125
|
+
return { gaps, representativeGap: meanBigInt(gaps) };
|
|
126
|
+
}
|
|
127
|
+
const gaps = consecutiveGaps(peers.map((p) => bytesToBigInt(p.coord)).sort(ascending));
|
|
128
|
+
return { gaps, representativeGap: medianBigInt(gaps) };
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Online network-size estimate from inter-peer ring gaps: `n = 2^256 / gap`.
|
|
132
|
+
*
|
|
133
|
+
* `options.selfCoord` selects the successor/predecessor window as the gap population and is the
|
|
134
|
+
* intended path; omitting it falls back to whole-store gaps (see {@link collectGaps}).
|
|
135
|
+
* `options.filter` scopes the estimate to a subset of the store (e.g. same-network members only)
|
|
136
|
+
* so foreign peers from a co-resident network can't inflate `n` and the derived cluster span /
|
|
137
|
+
* near-radius. It defaults to counting every entry, leaving the standalone/simulator path
|
|
138
|
+
* unchanged.
|
|
139
|
+
*
|
|
140
|
+
* Confidence blends sample count against 2m with the dispersion factor above.
|
|
141
|
+
*/
|
|
142
|
+
export function estimateSizeAndConfidence(store, m, options) {
|
|
143
|
+
// NOTE: `list()` walks and materializes every entry, but on the `selfCoord` path only its
|
|
144
|
+
// length is used (for `sizeFactor`) — the gap population comes from the S/P window instead.
|
|
145
|
+
// That is an O(store) allocation per call, and the estimator is called once per inbound
|
|
146
|
+
// maybeAct. Fine at today's capacity (2048) and message rates; if either grows, give the
|
|
147
|
+
// store a `countWhere(filter)` and build the list only for the whole-store fallback.
|
|
148
|
+
const peers = options?.filter ? store.list().filter(options.filter) : store.list();
|
|
22
149
|
const count = peers.length;
|
|
23
150
|
if (count === 0)
|
|
24
151
|
return { n: 0, confidence: 0 };
|
|
25
152
|
if (count === 1)
|
|
26
153
|
return { n: 1, confidence: 0.2 };
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
for (let i = 1; i < coords.length; i++)
|
|
31
|
-
gaps.push(coords[i] - coords[i - 1]);
|
|
32
|
-
gaps.push((coords[0] + ringSize) - coords[coords.length - 1]);
|
|
33
|
-
// Use median gap to reduce skew from sparse knowledge
|
|
34
|
-
const medGap = medianBigInt(gaps);
|
|
35
|
-
const safeGap = medGap > 0n ? medGap : ringSize / BigInt(Math.max(1, count));
|
|
36
|
-
const nEstBig = ringSize / safeGap;
|
|
37
|
-
const nEst = Math.max(1, Math.min(Number(nEstBig), 1_000_000_000));
|
|
38
|
-
// Confidence from sample size and gap variance
|
|
39
|
-
let minGap = gaps[0];
|
|
40
|
-
let maxGap = gaps[0];
|
|
41
|
-
for (const g of gaps) {
|
|
42
|
-
if (g < minGap)
|
|
43
|
-
minGap = g;
|
|
44
|
-
if (g > maxGap)
|
|
45
|
-
maxGap = g;
|
|
46
|
-
}
|
|
154
|
+
const { gaps, representativeGap } = collectGaps(store, m, peers, options?.filter, options?.selfCoord);
|
|
155
|
+
const safeGap = representativeGap > 0n ? representativeGap : RING_SIZE / BigInt(count);
|
|
156
|
+
const nEst = Math.max(1, Math.min(Number(RING_SIZE / safeGap), 1_000_000_000));
|
|
47
157
|
const sizeFactor = Math.min(1, count / Math.max(1, m * 2));
|
|
48
|
-
const
|
|
49
|
-
const confidence = Math.max(0.05, Math.min(1, 0.5 * sizeFactor + 0.5 * varianceFactor));
|
|
158
|
+
const confidence = Math.max(0.05, Math.min(1, 0.5 * sizeFactor + 0.5 * dispersionFactor(gaps)));
|
|
50
159
|
return { n: nEst, confidence };
|
|
51
160
|
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import type { Libp2p } from 'libp2p';
|
|
1
2
|
import type { SerializedPeerEntry, SerializedTable } from './store/digitree-store.js';
|
|
3
|
+
import { FretService as FretServiceClass } from './service/fret-service.js';
|
|
2
4
|
export type FretMode = 'active' | 'passive';
|
|
3
5
|
export interface FretConfig {
|
|
4
6
|
k: number;
|
|
@@ -7,6 +9,14 @@ export interface FretConfig {
|
|
|
7
9
|
profile: 'edge' | 'core';
|
|
8
10
|
bootstraps?: string[];
|
|
9
11
|
networkName?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Consecutive failed contact attempts before a peer is marked `dead` (default 3).
|
|
14
|
+
*
|
|
15
|
+
* A "failed contact" is failure to reach the peer at all — not a peer that answered and
|
|
16
|
+
* refused, which is membership evidence instead. Failures closer together than the service's
|
|
17
|
+
* spacing window count once, so the run is genuinely spread over time.
|
|
18
|
+
*/
|
|
19
|
+
deadAfterFailures?: number;
|
|
10
20
|
}
|
|
11
21
|
export interface NeighborSnapshotV1 {
|
|
12
22
|
v: 1;
|
|
@@ -22,7 +32,7 @@ export interface NeighborSnapshotV1 {
|
|
|
22
32
|
size_estimate?: number;
|
|
23
33
|
confidence?: number;
|
|
24
34
|
sig: string;
|
|
25
|
-
metadata?: Record<string,
|
|
35
|
+
metadata?: Record<string, unknown>;
|
|
26
36
|
}
|
|
27
37
|
export interface RouteAndMaybeActV1 {
|
|
28
38
|
v: 1;
|
|
@@ -92,11 +102,11 @@ export interface FretService {
|
|
|
92
102
|
commitCertificate: string;
|
|
93
103
|
}>;
|
|
94
104
|
report(evt: ReportEvent): void;
|
|
95
|
-
setMetadata(metadata: Record<string,
|
|
96
|
-
getMetadata(peerId: string): Record<string,
|
|
105
|
+
setMetadata(metadata: Record<string, unknown>): void;
|
|
106
|
+
getMetadata(peerId: string): Record<string, unknown> | undefined;
|
|
97
107
|
listPeers(): Array<{
|
|
98
108
|
id: string;
|
|
99
|
-
metadata?: Record<string,
|
|
109
|
+
metadata?: Record<string, unknown>;
|
|
100
110
|
}>;
|
|
101
111
|
reportNetworkSize(estimate: number, confidence: number, source?: string): void;
|
|
102
112
|
getNetworkSizeEstimate(): {
|
|
@@ -109,22 +119,29 @@ export interface FretService {
|
|
|
109
119
|
setActivityHandler(handler: ActivityHandler): void;
|
|
110
120
|
iterativeLookup(key: Uint8Array, options: LookupOptions): AsyncGenerator<RouteProgress>;
|
|
111
121
|
exportTable(): SerializedTable;
|
|
112
|
-
importTable(table: SerializedTable): number
|
|
122
|
+
importTable(table: SerializedTable): Promise<number>;
|
|
113
123
|
}
|
|
114
124
|
export type { SerializedPeerEntry, SerializedTable };
|
|
115
|
-
export {
|
|
116
|
-
export {
|
|
117
|
-
export { FretPeerDiscovery, type FretPeerDiscoveryConfig } from './service/peer-discovery.js';
|
|
125
|
+
export { FretServiceClass as FretServiceImpl };
|
|
126
|
+
export { FretPeerDiscovery, type DiscoverySnapshotSource, type FretPeerDiscoveryInput, type FretPeerDiscoveryConfig } from './service/peer-discovery.js';
|
|
118
127
|
export { Libp2pFretService, fretService } from './service/libp2p-fret-service.js';
|
|
119
128
|
export { hashKey, hashPeerId } from './ring/hash.js';
|
|
120
129
|
export type { RingCoord } from './ring/hash.js';
|
|
121
|
-
export {
|
|
130
|
+
export { clockwiseDistance, minDistance, lexLess } from './ring/distance.js';
|
|
122
131
|
export { DigitreeStore } from './store/digitree-store.js';
|
|
123
|
-
export type { PeerEntry, PeerState, MembershipState } from './store/digitree-store.js';
|
|
132
|
+
export type { PeerEntry, PeerPatch, PeerState, MembershipState, RingCursor, RingWalkPage } from './store/digitree-store.js';
|
|
124
133
|
export { estimateSizeAndConfidence } from './estimate/size-estimator.js';
|
|
125
|
-
export type { SizeEstimate } from './estimate/size-estimator.js';
|
|
134
|
+
export type { SizeEstimate, SizeEstimateOptions } from './estimate/size-estimator.js';
|
|
126
135
|
export { assembleCohort } from './service/cohort.js';
|
|
127
136
|
export { shouldIncludePayload, computeNearRadius } from './service/payload-heuristic.js';
|
|
128
|
-
export { DedupCache } from './service/dedup-cache.js';
|
|
129
|
-
export { validateTimestamp,
|
|
130
|
-
export
|
|
137
|
+
export { DedupCache, DEDUP_TTL_MS } from './service/dedup-cache.js';
|
|
138
|
+
export { validateTimestamp, sendFramed, readFramed, openRpcStream, releaseRpcStream } from './rpc/protocols.js';
|
|
139
|
+
export { rpcRequest } from './rpc/request.js';
|
|
140
|
+
export type { RpcRequestOptions } from './rpc/request.js';
|
|
141
|
+
export type { RpcOutcome } from './rpc/outcome.js';
|
|
142
|
+
export type { Stream } from '@libp2p/interface';
|
|
143
|
+
export { registerRpcHandler, registerJsonHandler } from './rpc/protocols.js';
|
|
144
|
+
export type { JsonRequestHandlerOpts, JsonReplyOnlyHandlerOpts } from './rpc/protocols.js';
|
|
145
|
+
export { parseRouteAndMaybeAct, parseLeaveNotice, makeSnapshotParser, parsePingResponse, parseNearAnchor, parseMaybeActReply, sanitizeReplacements, parseOrThrow, ReplyRejectedError, isReplyRejectedError, } from './rpc/validate.js';
|
|
146
|
+
export type { Parser, SnapshotCaps } from './rpc/validate.js';
|
|
147
|
+
export declare function createFret(node: Libp2p, cfg?: Partial<FretConfig>): FretService;
|
package/dist/src/index.js
CHANGED
|
@@ -1,16 +1,48 @@
|
|
|
1
|
-
export { FretService as FretServiceImpl } from './service/fret-service.js';
|
|
2
1
|
import { FretService as FretServiceClass } from './service/fret-service.js';
|
|
3
|
-
export {
|
|
2
|
+
export { FretServiceClass as FretServiceImpl };
|
|
4
3
|
export { FretPeerDiscovery } from './service/peer-discovery.js';
|
|
5
4
|
export { Libp2pFretService, fretService } from './service/libp2p-fret-service.js';
|
|
6
5
|
export { hashKey, hashPeerId } from './ring/hash.js';
|
|
7
|
-
export {
|
|
6
|
+
export { clockwiseDistance, minDistance, lexLess } from './ring/distance.js';
|
|
8
7
|
export { DigitreeStore } from './store/digitree-store.js';
|
|
9
8
|
export { estimateSizeAndConfidence } from './estimate/size-estimator.js';
|
|
10
9
|
export { assembleCohort } from './service/cohort.js';
|
|
11
10
|
export { shouldIncludePayload, computeNearRadius } from './service/payload-heuristic.js';
|
|
12
|
-
export { DedupCache } from './service/dedup-cache.js';
|
|
13
|
-
|
|
11
|
+
export { DedupCache, DEDUP_TTL_MS } from './service/dedup-cache.js';
|
|
12
|
+
// `openRpcStream` and `releaseRpcStream` are one seam: an opened stream must be released, and the
|
|
13
|
+
// release rule (abort once the caller's signal has fired, close otherwise) is the half a consumer
|
|
14
|
+
// gets wrong — a bare `close()` is unbounded against a stalled remote. Exporting only the opener
|
|
15
|
+
// invites a hand-rolled releaser, which is the class of bug this export exists to retire.
|
|
16
|
+
// `sendFramed` and `readFramed` ship together for the same reason: a consumer given only the
|
|
17
|
+
// reader hand-rolls the writer, and the framing has to match.
|
|
18
|
+
export { validateTimestamp, sendFramed, readFramed, openRpcStream, releaseRpcStream } from './rpc/protocols.js';
|
|
19
|
+
// `rpcRequest` is the one owner of the open/write/read/close sequence those four primitives
|
|
20
|
+
// compose, so a consumer sending a request never re-derives the order or the release rule. It
|
|
21
|
+
// never throws for a network outcome — an unreachable peer, a foreign protocol, a truncated
|
|
22
|
+
// reply, a busy answer and a timeout are all variants of the `RpcOutcome` it returns, and only a
|
|
23
|
+
// caller bug (a malformed peer id, an unsatisfiable dial policy) throws. Callers that read the
|
|
24
|
+
// outcome therefore branch on evidence about the peer rather than on error identity.
|
|
25
|
+
export { rpcRequest } from './rpc/request.js';
|
|
26
|
+
// The receive-side mirror of `rpcRequest`: a consumer wrapping its own protocol over the same
|
|
27
|
+
// libp2p node has exactly the drop-vs-abort and stream-release problem these two solve, and the
|
|
28
|
+
// hand-rolled copy is what leaks streams (a handler that throws mid-message leaves the inbound
|
|
29
|
+
// stream open forever, and streams are counted per protocol per connection). `registerRpcHandler`
|
|
30
|
+
// owns the budgeted success close and the synchronous error `abort()`; `registerJsonHandler`
|
|
31
|
+
// stacks the framed-JSON decode + parse on top of it. Their option interfaces ship with them —
|
|
32
|
+
// without those a consumer cannot name what it is passing.
|
|
33
|
+
export { registerRpcHandler, registerJsonHandler } from './rpc/protocols.js';
|
|
34
|
+
// The wire-shape parsers are exported for the same reason `registerRpcHandler` / `rpcRequest` /
|
|
35
|
+
// `openRpcStream` are: a consumer registering its own handler over this node otherwise re-derives
|
|
36
|
+
// the shape rules by hand, and a hand-rolled copy is what drifts. They are pure functions with no
|
|
37
|
+
// service state behind them. The primitives they are spelled with (peer-id parsing, bounded string
|
|
38
|
+
// arrays) stay module-scoped — implementation detail, not surface.
|
|
39
|
+
export { parseRouteAndMaybeAct, parseLeaveNotice, makeSnapshotParser, parsePingResponse, parseNearAnchor, parseMaybeActReply, sanitizeReplacements,
|
|
40
|
+
// The adapter that wires a `Parser` into `rpcRequest`'s `decode`. It ships with the parsers
|
|
41
|
+
// because passing one in raw is a silent bug rather than a compile error: `rpcRequest` has no
|
|
42
|
+
// `undefined` check, so a returned rejection becomes `{ kind: 'ok', value: undefined }` and
|
|
43
|
+
// `T` infers as `Reply | undefined`. Its error type is exported alongside so a consumer can
|
|
44
|
+
// recognise the rejection by identity, not by message text.
|
|
45
|
+
parseOrThrow, ReplyRejectedError, isReplyRejectedError, } from './rpc/validate.js';
|
|
14
46
|
export function createFret(node, cfg) {
|
|
15
47
|
return new FretServiceClass(node, cfg);
|
|
16
48
|
}
|
|
@@ -1,4 +1,31 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Magnitude compare of two big-endian unsigned integers: `a < b`.
|
|
3
|
+
*
|
|
4
|
+
* Operands are right-aligned — a shorter array is read as zero-padded on the
|
|
5
|
+
* *left*, which is what it is numerically, and matches the arithmetic in
|
|
6
|
+
* `clockwiseDistance`. Comparing from index 0 would instead read a 16-byte
|
|
7
|
+
* value as a 32-byte one scaled by 2^128. Equal-length inputs (every caller
|
|
8
|
+
* today) compare byte-for-byte from the top, unchanged.
|
|
9
|
+
*/
|
|
2
10
|
export declare function lexLess(a: Uint8Array, b: Uint8Array): boolean;
|
|
3
11
|
export declare function clockwiseDistance(a: Uint8Array, b: Uint8Array): Uint8Array;
|
|
12
|
+
/**
|
|
13
|
+
* True ring distance: the shorter of the two arcs between a and b, on a ring of
|
|
14
|
+
* 2^(8·len). This is the single distance metric FRET uses — routing, payload
|
|
15
|
+
* inclusion, and the relevance sparsity model all measure with it, so the
|
|
16
|
+
* wrap-around point behaves like every other point on the ring. Maximum value
|
|
17
|
+
* is 2^(8·len − 1) (half the ring), not 2^(8·len) − 1.
|
|
18
|
+
*/
|
|
4
19
|
export declare function minDistance(a: Uint8Array, b: Uint8Array): Uint8Array;
|
|
20
|
+
/**
|
|
21
|
+
* Position of a distance magnitude on a log scale, normalized to [0,1]: 0 for a
|
|
22
|
+
* zero distance, rising toward 1 as the leading-zero-bit count falls. Shared by
|
|
23
|
+
* the next-hop cost function and the relevance sparsity model so both read the
|
|
24
|
+
* ring at the same resolution.
|
|
25
|
+
*
|
|
26
|
+
* A `minDistance` result reaches exactly 1 only at the antipode (2^255, whose
|
|
27
|
+
* top bit is set); every other pair lands at 1 − 1/256 or below. The value is
|
|
28
|
+
* consumed as a relative position, so that near-unreachable top step needs no
|
|
29
|
+
* rescaling.
|
|
30
|
+
*/
|
|
31
|
+
export declare function normalizedLogMagnitude(dist: Uint8Array): number;
|
|
@@ -1,18 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Magnitude compare of two big-endian unsigned integers: `a < b`.
|
|
3
|
+
*
|
|
4
|
+
* Operands are right-aligned — a shorter array is read as zero-padded on the
|
|
5
|
+
* *left*, which is what it is numerically, and matches the arithmetic in
|
|
6
|
+
* `clockwiseDistance`. Comparing from index 0 would instead read a 16-byte
|
|
7
|
+
* value as a 32-byte one scaled by 2^128. Equal-length inputs (every caller
|
|
8
|
+
* today) compare byte-for-byte from the top, unchanged.
|
|
9
|
+
*/
|
|
11
10
|
export function lexLess(a, b) {
|
|
12
11
|
const len = Math.max(a.length, b.length);
|
|
13
|
-
for (let i =
|
|
14
|
-
const av = a[i] ?? 0;
|
|
15
|
-
const bv = b[i] ?? 0;
|
|
12
|
+
for (let i = len - 1; i >= 0; i--) {
|
|
13
|
+
const av = a[a.length - 1 - i] ?? 0;
|
|
14
|
+
const bv = b[b.length - 1 - i] ?? 0;
|
|
16
15
|
if (av < bv)
|
|
17
16
|
return true;
|
|
18
17
|
if (av > bv)
|
|
@@ -41,7 +40,45 @@ export function clockwiseDistance(a, b) {
|
|
|
41
40
|
}
|
|
42
41
|
return out;
|
|
43
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* True ring distance: the shorter of the two arcs between a and b, on a ring of
|
|
45
|
+
* 2^(8·len). This is the single distance metric FRET uses — routing, payload
|
|
46
|
+
* inclusion, and the relevance sparsity model all measure with it, so the
|
|
47
|
+
* wrap-around point behaves like every other point on the ring. Maximum value
|
|
48
|
+
* is 2^(8·len − 1) (half the ring), not 2^(8·len) − 1.
|
|
49
|
+
*/
|
|
44
50
|
export function minDistance(a, b) {
|
|
45
|
-
//
|
|
46
|
-
|
|
51
|
+
// NOTE: allocates both arcs (2 × 32 bytes) to discard one, on a path walked
|
|
52
|
+
// per routing candidate and per relevance update. Measured cost has not been
|
|
53
|
+
// an issue; if distance ever shows up in a profile, compare the arcs in place
|
|
54
|
+
// (cw < half-ring ⇒ cw is the shorter one) and build only the winner.
|
|
55
|
+
const cw = clockwiseDistance(a, b);
|
|
56
|
+
const ccw = clockwiseDistance(b, a);
|
|
57
|
+
return lexLess(ccw, cw) ? ccw : cw;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Position of a distance magnitude on a log scale, normalized to [0,1]: 0 for a
|
|
61
|
+
* zero distance, rising toward 1 as the leading-zero-bit count falls. Shared by
|
|
62
|
+
* the next-hop cost function and the relevance sparsity model so both read the
|
|
63
|
+
* ring at the same resolution.
|
|
64
|
+
*
|
|
65
|
+
* A `minDistance` result reaches exactly 1 only at the antipode (2^255, whose
|
|
66
|
+
* top bit is set); every other pair lands at 1 − 1/256 or below. The value is
|
|
67
|
+
* consumed as a relative position, so that near-unreachable top step needs no
|
|
68
|
+
* rescaling.
|
|
69
|
+
*/
|
|
70
|
+
export function normalizedLogMagnitude(dist) {
|
|
71
|
+
const totalBits = dist.length * 8;
|
|
72
|
+
if (totalBits === 0)
|
|
73
|
+
return 0;
|
|
74
|
+
let lzBits = 0;
|
|
75
|
+
for (const byte of dist) {
|
|
76
|
+
if (byte === 0) {
|
|
77
|
+
lzBits += 8;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
lzBits += Math.clz32(byte) - 24; // clz32 counts over 32 bits; drop the 24 above the byte
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
return 1 - lzBits / totalBits;
|
|
47
84
|
}
|
package/dist/src/ring/hash.d.ts
CHANGED
|
@@ -4,6 +4,18 @@ export declare const COORD_BYTES = 32;
|
|
|
4
4
|
export type RingCoord = Uint8Array;
|
|
5
5
|
export declare function hashPeerId(peerId: PeerId): Promise<RingCoord>;
|
|
6
6
|
export declare function hashKey(key: Uint8Array): Promise<RingCoord>;
|
|
7
|
+
/**
|
|
8
|
+
* Fixed-length 64 hex chars — the encoding the routing store's tree key is built from.
|
|
9
|
+
*
|
|
10
|
+
* This is the hottest pure function in FRET: `DigitreeStore` embeds the hex coordinate in
|
|
11
|
+
* every tree key, so it runs once per store write *and* once per ring walk (each walk seeks
|
|
12
|
+
* by `hex(coord)`), which puts it under neighbor exchange, cohort assembly, next-hop
|
|
13
|
+
* selection and size estimation alike. Formatting each byte with
|
|
14
|
+
* `toString(16).padStart(2, '0')` allocates two throwaway strings per byte — 64 per
|
|
15
|
+
* coordinate — and measured **38% of total CPU** in the N=100 simulation spec. The lookup
|
|
16
|
+
* table is therefore load-bearing rather than a micro-optimization: the output is identical,
|
|
17
|
+
* but the per-byte allocations are gone.
|
|
18
|
+
*/
|
|
7
19
|
export declare function coordToHex(coord: RingCoord): string;
|
|
8
20
|
export declare function hexToCoord(hex: string): RingCoord;
|
|
9
21
|
export declare function coordToBase64url(coord: RingCoord): string;
|
package/dist/src/ring/hash.js
CHANGED
|
@@ -13,16 +13,31 @@ export async function hashKey(key) {
|
|
|
13
13
|
const digest = await sha256.encode(key);
|
|
14
14
|
return digest;
|
|
15
15
|
}
|
|
16
|
+
/** Byte value → its fixed-width hex pair. Built once; see {@link coordToHex}. */
|
|
17
|
+
const HEX_PAIR = Array.from({ length: 256 }, (_, b) => b.toString(16).padStart(2, '0'));
|
|
18
|
+
/**
|
|
19
|
+
* Fixed-length 64 hex chars — the encoding the routing store's tree key is built from.
|
|
20
|
+
*
|
|
21
|
+
* This is the hottest pure function in FRET: `DigitreeStore` embeds the hex coordinate in
|
|
22
|
+
* every tree key, so it runs once per store write *and* once per ring walk (each walk seeks
|
|
23
|
+
* by `hex(coord)`), which puts it under neighbor exchange, cohort assembly, next-hop
|
|
24
|
+
* selection and size estimation alike. Formatting each byte with
|
|
25
|
+
* `toString(16).padStart(2, '0')` allocates two throwaway strings per byte — 64 per
|
|
26
|
+
* coordinate — and measured **38% of total CPU** in the N=100 simulation spec. The lookup
|
|
27
|
+
* table is therefore load-bearing rather than a micro-optimization: the output is identical,
|
|
28
|
+
* but the per-byte allocations are gone.
|
|
29
|
+
*/
|
|
16
30
|
export function coordToHex(coord) {
|
|
17
|
-
// Fixed-length 64 hex chars
|
|
18
31
|
let s = '';
|
|
19
|
-
for (let i = 0; i < coord.length; i++)
|
|
20
|
-
|
|
21
|
-
s += b;
|
|
22
|
-
}
|
|
32
|
+
for (let i = 0; i < coord.length; i++)
|
|
33
|
+
s += HEX_PAIR[coord[i]];
|
|
23
34
|
return s;
|
|
24
35
|
}
|
|
36
|
+
const HEX_COORD_RE = new RegExp(`^[0-9a-fA-F]{${COORD_BYTES * 2}}$`);
|
|
25
37
|
export function hexToCoord(hex) {
|
|
38
|
+
if (!HEX_COORD_RE.test(hex)) {
|
|
39
|
+
throw new Error(`hexToCoord: expected ${COORD_BYTES * 2} hex chars, got ${JSON.stringify(hex)}`);
|
|
40
|
+
}
|
|
26
41
|
const out = new Uint8Array(COORD_BYTES);
|
|
27
42
|
for (let i = 0; i < COORD_BYTES; i++) {
|
|
28
43
|
out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
@@ -33,5 +48,9 @@ export function coordToBase64url(coord) {
|
|
|
33
48
|
return u8ToString(coord, 'base64url');
|
|
34
49
|
}
|
|
35
50
|
export function base64urlToCoord(s) {
|
|
36
|
-
|
|
51
|
+
const coord = u8FromString(s, 'base64url');
|
|
52
|
+
if (coord.length !== COORD_BYTES) {
|
|
53
|
+
throw new Error(`base64urlToCoord: expected ${COORD_BYTES} bytes, got ${coord.length}`);
|
|
54
|
+
}
|
|
55
|
+
return coord;
|
|
37
56
|
}
|
package/dist/src/rpc/leave.d.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import type { Libp2p } from 'libp2p';
|
|
2
|
+
import type { RpcOutcome } from './outcome.js';
|
|
2
3
|
export interface LeaveNoticeV1 {
|
|
3
4
|
v: 1;
|
|
4
5
|
from: string;
|
|
5
6
|
replacements?: string[];
|
|
6
7
|
timestamp: number;
|
|
7
8
|
}
|
|
8
|
-
export declare function registerLeave(node: Libp2p, onLeave: (notice: LeaveNoticeV1) => Promise<void> | void, protocol?: string): void
|
|
9
|
-
export declare function sendLeave(node: Libp2p, peerIdStr: string, notice: LeaveNoticeV1, protocol?: string
|
|
9
|
+
export declare function registerLeave(node: Libp2p, onLeave: (notice: LeaveNoticeV1) => Promise<void> | void, protocol?: string, onIdentityMismatch?: (claimed: string, actual: string) => void, onMalformed?: (reason: 'decode' | 'parse') => void): Promise<void>;
|
|
10
|
+
export declare function sendLeave(node: Libp2p, peerIdStr: string, notice: LeaveNoticeV1, protocol?: string, opts?: {
|
|
11
|
+
signal?: AbortSignal;
|
|
12
|
+
timeoutMs?: number;
|
|
13
|
+
}): Promise<RpcOutcome<undefined>>;
|