gnutella 1.0.0 → 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.
Files changed (75) hide show
  1. package/CLI.md +1 -0
  2. package/gnutella.json.example +1 -0
  3. package/package.json +4 -3
  4. package/src/cli_shared.ts +32 -43
  5. package/src/const.ts +1 -9
  6. package/src/descriptor_routing/index.ts +17 -0
  7. package/src/descriptor_routing/pong_cache.ts +32 -0
  8. package/src/descriptor_routing/response_routes.ts +15 -0
  9. package/src/descriptor_routing/seen.ts +20 -0
  10. package/src/descriptor_routing/ttl.ts +37 -0
  11. package/src/descriptor_routing/types.ts +27 -0
  12. package/src/gwebcache/bootstrap.ts +21 -58
  13. package/src/gwebcache/types.ts +6 -10
  14. package/src/handshake_policy/admission.ts +17 -0
  15. package/src/handshake_policy/capabilities.ts +167 -0
  16. package/src/handshake_policy/headers.ts +157 -0
  17. package/src/handshake_policy/index.ts +21 -0
  18. package/src/handshake_policy/types.ts +36 -0
  19. package/src/peer_address.ts +68 -0
  20. package/src/peer_discovery/candidate_policy.ts +80 -0
  21. package/src/peer_discovery/index.ts +8 -0
  22. package/src/peer_discovery/types.ts +26 -0
  23. package/src/persistence/config_doc.ts +61 -0
  24. package/src/persistence/index.ts +14 -0
  25. package/src/persistence/peer_state.ts +113 -0
  26. package/src/persistence/types.ts +28 -0
  27. package/src/protocol/codec.ts +27 -67
  28. package/src/protocol/content_urn.ts +5 -1
  29. package/src/protocol/file_hash.ts +12 -0
  30. package/src/protocol/file_server.ts +1 -1
  31. package/src/protocol/ggep.ts +13 -8
  32. package/src/protocol/handshake.ts +18 -161
  33. package/src/protocol/http_download_reader.ts +9 -7
  34. package/src/protocol/magnet.ts +15 -13
  35. package/src/protocol/node.ts +1 -1
  36. package/src/protocol/node_handshake.ts +55 -113
  37. package/src/protocol/node_protocol_runtime.ts +69 -60
  38. package/src/protocol/node_qrp_runtime.ts +7 -6
  39. package/src/protocol/node_query_routing.ts +43 -132
  40. package/src/protocol/node_state.ts +2 -3
  41. package/src/protocol/node_topology.ts +38 -82
  42. package/src/protocol/node_transfer.ts +52 -35
  43. package/src/protocol/peer_state.ts +36 -207
  44. package/src/protocol/qrp.ts +1 -549
  45. package/src/protocol/query_matching.ts +22 -0
  46. package/src/protocol/share_index.ts +8 -70
  47. package/src/protocol/share_library.ts +30 -73
  48. package/src/query_routing/dynamic_query.ts +117 -0
  49. package/src/query_routing/index.ts +27 -0
  50. package/src/query_routing/qrp/constants.ts +9 -0
  51. package/src/query_routing/qrp/hash.ts +27 -0
  52. package/src/query_routing/qrp/patch_values.ts +29 -0
  53. package/src/query_routing/qrp/remote_state.ts +98 -0
  54. package/src/query_routing/qrp/routing.ts +46 -0
  55. package/src/query_routing/qrp/table.ts +319 -0
  56. package/src/query_routing/qrp/terms.ts +62 -0
  57. package/src/query_routing/qrp/types.ts +31 -0
  58. package/src/query_routing/qrp.ts +13 -0
  59. package/src/share_catalog/catalog.ts +108 -0
  60. package/src/share_catalog/index.ts +16 -0
  61. package/src/share_catalog/keywords.ts +15 -0
  62. package/src/share_catalog/manifest.ts +81 -0
  63. package/src/share_catalog/types.ts +43 -0
  64. package/src/shared.ts +9 -68
  65. package/src/topology/admission.ts +51 -0
  66. package/src/topology/classify.ts +19 -0
  67. package/src/topology/index.ts +17 -0
  68. package/src/topology/slots.ts +43 -0
  69. package/src/topology/types.ts +25 -0
  70. package/src/transfers/index.ts +13 -0
  71. package/src/transfers/planner.ts +52 -0
  72. package/src/transfers/ranges.ts +57 -0
  73. package/src/transfers/results.ts +45 -0
  74. package/src/transfers/types.ts +43 -0
  75. package/src/types.ts +43 -55
@@ -0,0 +1,167 @@
1
+ import { DEFAULT_USER_AGENT } from "../const";
2
+ import type { PeerCapabilities } from "../types";
3
+ import {
4
+ hasToken,
5
+ lowerCaseHeaders,
6
+ parseBoolHeader,
7
+ parseListenIpHeader,
8
+ parsePositiveIntHeader,
9
+ parseRemoteIpHeader,
10
+ } from "./headers";
11
+ import type { CapabilityPolicy, LocalHandshakePolicy } from "./types";
12
+
13
+ const GTK_MODERN_ULTRAPEER_MIN_DEGREE = 16;
14
+
15
+ function ultrapeerNeededHeader(
16
+ policy: LocalHandshakePolicy,
17
+ ): string | undefined {
18
+ if (policy.nodeMode !== "ultrapeer") return undefined;
19
+ if (policy.connectedMeshPeerCount < policy.maxUltrapeerConnections)
20
+ return "True";
21
+ if (policy.connectedLeafCount < policy.maxLeafConnections)
22
+ return "False";
23
+ return undefined;
24
+ }
25
+
26
+ function baseRoleHeaders(
27
+ policy: LocalHandshakePolicy,
28
+ ): Record<string, string> {
29
+ const headers: Record<string, string> = {
30
+ "x-ultrapeer": policy.nodeMode === "ultrapeer" ? "True" : "False",
31
+ };
32
+ const ultrapeerNeeded = ultrapeerNeededHeader(policy);
33
+ if (ultrapeerNeeded) headers["x-ultrapeer-needed"] = ultrapeerNeeded;
34
+ if (policy.nodeMode === "ultrapeer") {
35
+ headers["x-ultrapeer-query-routing"] = "0.1";
36
+ headers["x-dynamic-querying"] = "0.1";
37
+ headers["x-ext-probes"] = "0.1";
38
+ headers["x-degree"] = String(
39
+ Math.max(
40
+ GTK_MODERN_ULTRAPEER_MIN_DEGREE,
41
+ policy.maxUltrapeerConnections,
42
+ ),
43
+ );
44
+ }
45
+ return headers;
46
+ }
47
+
48
+ function baseFeatureHeaders(
49
+ policy: LocalHandshakePolicy,
50
+ ): Record<string, string> {
51
+ const headers: Record<string, string> = {};
52
+ if (policy.enableQrp)
53
+ headers["x-query-routing"] = policy.queryRoutingVersion || "0.1";
54
+ if (policy.enableCompression) headers["accept-encoding"] = "deflate";
55
+ if (policy.enablePongCaching) headers["pong-caching"] = "0.1";
56
+ if (policy.enableGgep) headers.ggep = "0.5";
57
+ if (policy.enableBye) headers["bye-packet"] = "0.1";
58
+ return headers;
59
+ }
60
+
61
+ export function buildBaseHandshakeHeaders(
62
+ policy: LocalHandshakePolicy,
63
+ remoteIp?: string,
64
+ ): Record<string, string> {
65
+ const headers: Record<string, string> = {
66
+ "user-agent": policy.userAgent || DEFAULT_USER_AGENT,
67
+ "listen-ip": `${policy.advertisedHost}:${policy.advertisedPort}`,
68
+ "x-max-ttl": String(policy.maxTtl),
69
+ ...baseRoleHeaders(policy),
70
+ ...baseFeatureHeaders(policy),
71
+ };
72
+ const observedRemote = parseRemoteIpHeader(remoteIp);
73
+ if (observedRemote) headers["remote-ip"] = observedRemote;
74
+ return headers;
75
+ }
76
+
77
+ function peerRequestedTlsUpgrade(
78
+ headers: Record<string, string>,
79
+ upgradeToken: string,
80
+ ): boolean {
81
+ return hasToken(headers.upgrade, upgradeToken);
82
+ }
83
+
84
+ function peerAcceptedTlsUpgrade(
85
+ headers: Record<string, string>,
86
+ upgradeToken: string,
87
+ ): boolean {
88
+ return (
89
+ hasToken(headers.upgrade, upgradeToken) &&
90
+ hasToken(headers.connection, "upgrade")
91
+ );
92
+ }
93
+
94
+ export function buildServerHandshakeHeaders(
95
+ policy: LocalHandshakePolicy,
96
+ requestHeaders: Record<string, string>,
97
+ remoteIp?: string,
98
+ ): Record<string, string> {
99
+ const headers = buildBaseHandshakeHeaders(policy, remoteIp);
100
+ if (
101
+ policy.tlsEnabled &&
102
+ peerRequestedTlsUpgrade(requestHeaders, policy.tlsUpgradeToken)
103
+ ) {
104
+ headers.upgrade = policy.tlsUpgradeToken;
105
+ headers.connection = "Upgrade";
106
+ }
107
+ if (
108
+ policy.enableCompression &&
109
+ hasToken(requestHeaders["accept-encoding"], "deflate")
110
+ ) {
111
+ headers["content-encoding"] = "deflate";
112
+ }
113
+ return headers;
114
+ }
115
+
116
+ export function buildClientFinalHeaders(
117
+ policy: LocalHandshakePolicy,
118
+ serverHeaders: Record<string, string>,
119
+ remoteIp?: string,
120
+ ): Record<string, string> {
121
+ const headers: Record<string, string> = {};
122
+ const observedRemote = parseRemoteIpHeader(remoteIp);
123
+ if (observedRemote) headers["remote-ip"] = observedRemote;
124
+ if (
125
+ policy.tlsEnabled &&
126
+ peerAcceptedTlsUpgrade(serverHeaders, policy.tlsUpgradeToken)
127
+ )
128
+ headers.connection = "Upgrade";
129
+ if (
130
+ policy.enableCompression &&
131
+ hasToken(serverHeaders["accept-encoding"], "deflate")
132
+ ) {
133
+ headers["content-encoding"] = "deflate";
134
+ }
135
+ return headers;
136
+ }
137
+
138
+ export function buildPeerCapabilities(
139
+ policy: CapabilityPolicy,
140
+ ): PeerCapabilities {
141
+ const h = lowerCaseHeaders(policy.headers);
142
+ return {
143
+ version: policy.version,
144
+ headers: h,
145
+ userAgent: h["user-agent"],
146
+ supportsGgep: !!h.ggep,
147
+ supportsPongCaching: !!h["pong-caching"],
148
+ supportsBye: !!h["bye-packet"],
149
+ supportsTls:
150
+ policy.tlsEnabled &&
151
+ peerRequestedTlsUpgrade(h, policy.tlsUpgradeToken),
152
+ supportsCompression:
153
+ hasToken(h["accept-encoding"], "deflate") ||
154
+ hasToken(h["content-encoding"], "deflate"),
155
+ compressIn: policy.compressIn,
156
+ compressOut: policy.compressOut,
157
+ isUltrapeer: parseBoolHeader(h["x-ultrapeer"]),
158
+ ultrapeerNeeded: parseBoolHeader(h["x-ultrapeer-needed"]),
159
+ queryRoutingVersion: h["x-query-routing"],
160
+ ultrapeerQueryRoutingVersion: h["x-ultrapeer-query-routing"],
161
+ dynamicQueryingVersion: h["x-dynamic-querying"],
162
+ extProbesVersion: h["x-ext-probes"],
163
+ degree: parsePositiveIntHeader(h["x-degree"]),
164
+ isCrawler: !!h.crawler,
165
+ listenIp: parseListenIpHeader(h["listen-ip"]),
166
+ };
167
+ }
@@ -0,0 +1,157 @@
1
+ import {
2
+ CANONICAL_HEADER_NAMES,
3
+ INTERESTING_HANDSHAKE_HEADERS,
4
+ } from "../const";
5
+ import { normalizeIpv4, parsePeer } from "../peer_address";
6
+ import type { PeerAddr } from "../types";
7
+
8
+ export function hasToken(
9
+ value: string | undefined,
10
+ token: string,
11
+ ): boolean {
12
+ if (!value) return false;
13
+ return value
14
+ .split(",")
15
+ .map((x) => x.trim().toLowerCase())
16
+ .includes(token.toLowerCase());
17
+ }
18
+
19
+ export function parseBoolHeader(
20
+ v: string | undefined,
21
+ ): boolean | undefined {
22
+ if (v == null) return undefined;
23
+ const s = v.trim().toLowerCase();
24
+ if (s === "true") return true;
25
+ if (s === "false") return false;
26
+ return undefined;
27
+ }
28
+
29
+ export function parsePositiveIntHeader(
30
+ v: string | undefined,
31
+ ): number | undefined {
32
+ if (!v) return undefined;
33
+ const n = Number(v.trim());
34
+ return Number.isInteger(n) && n > 0 ? n : undefined;
35
+ }
36
+
37
+ export function parseRemoteIpHeader(
38
+ v: string | undefined,
39
+ ): string | undefined {
40
+ if (!v) return undefined;
41
+ return normalizeIpv4(v.split(",")[0]?.trim());
42
+ }
43
+
44
+ export function parsePeerHeaderList(v: string | undefined): PeerAddr[] {
45
+ if (!v) return [];
46
+ return v
47
+ .split(",")
48
+ .map((x) => parsePeer(x.trim()))
49
+ .filter((x): x is PeerAddr => !!x);
50
+ }
51
+
52
+ export function parseListenIpHeader(
53
+ v: string | undefined,
54
+ ): PeerAddr | undefined {
55
+ if (!v) return undefined;
56
+ return parsePeer(v.trim()) || undefined;
57
+ }
58
+
59
+ export function lowerCaseHeaders(
60
+ headers: Record<string, string>,
61
+ ): Record<string, string> {
62
+ const out: Record<string, string> = {};
63
+ for (const [k, v] of Object.entries(headers)) out[k.toLowerCase()] = v;
64
+ return out;
65
+ }
66
+
67
+ export function mergeHeaders(
68
+ ...parts: Array<Record<string, string> | undefined>
69
+ ): Record<string, string> {
70
+ const out: Record<string, string> = {};
71
+ for (const part of parts) {
72
+ if (!part) continue;
73
+ for (const [k, v] of Object.entries(part)) out[k.toLowerCase()] = v;
74
+ }
75
+ return out;
76
+ }
77
+
78
+ export function findHeaderEnd(raw: string): number {
79
+ const crlf = raw.indexOf("\r\n\r\n");
80
+ const lf = raw.indexOf("\n\n");
81
+ if (crlf !== -1 && (lf === -1 || crlf < lf)) return crlf + 4;
82
+ if (lf !== -1) return lf + 2;
83
+ return -1;
84
+ }
85
+
86
+ function appendContinuationLine(
87
+ headers: Record<string, string>,
88
+ current: string,
89
+ line: string,
90
+ ): boolean {
91
+ if (!/^[ \t]/.test(line) || !current) return false;
92
+ headers[current] = `${headers[current]} ${line.trim()}`.trim();
93
+ return true;
94
+ }
95
+
96
+ function recordHeaderLine(
97
+ headers: Record<string, string>,
98
+ line: string,
99
+ ): string {
100
+ const idx = line.indexOf(":");
101
+ if (idx === -1) return "";
102
+ const key = line.slice(0, idx).trim().toLowerCase();
103
+ const value = line.slice(idx + 1).trim();
104
+ headers[key] = headers[key] ? `${headers[key]},${value}` : value;
105
+ return key;
106
+ }
107
+
108
+ export function parseHandshakeBlock(raw: string): {
109
+ startLine: string;
110
+ headers: Record<string, string>;
111
+ } {
112
+ const end = findHeaderEnd(raw);
113
+ const text = end === -1 ? raw : raw.slice(0, end);
114
+ const lines = text.replace(/\r\n/g, "\n").split("\n");
115
+ const startLine = lines.shift()?.trim() || "";
116
+ const headers: Record<string, string> = {};
117
+ let current = "";
118
+
119
+ for (const line of lines) {
120
+ if (!line) continue;
121
+ if (appendContinuationLine(headers, current, line)) continue;
122
+ current = recordHeaderLine(headers, line);
123
+ }
124
+ return { startLine, headers };
125
+ }
126
+
127
+ function canonicalHeaderName(key: string): string {
128
+ return CANONICAL_HEADER_NAMES[key.toLowerCase()] || key;
129
+ }
130
+
131
+ export function buildHandshakeBlock(
132
+ startLine: string,
133
+ headers: Record<string, string>,
134
+ ): Buffer {
135
+ const lines = [startLine];
136
+ for (const [k, v] of Object.entries(headers)) {
137
+ lines.push(`${canonicalHeaderName(k)}: ${v}`);
138
+ }
139
+ lines.push("", "");
140
+ return Buffer.from(lines.join("\r\n"), "latin1");
141
+ }
142
+
143
+ function summarizeHandshakeHeaders(
144
+ headers: Record<string, string>,
145
+ ): string {
146
+ const parts = INTERESTING_HANDSHAKE_HEADERS.filter(
147
+ (key) => !!headers[key],
148
+ ).map((key) => `${canonicalHeaderName(key)}=${headers[key]}`);
149
+ return parts.length ? ` [${parts.join("; ")}]` : "";
150
+ }
151
+
152
+ export function describeHandshakeResponse(
153
+ startLine: string,
154
+ headers: Record<string, string>,
155
+ ): string {
156
+ return `${startLine}${summarizeHandshakeHeaders(headers)}`;
157
+ }
@@ -0,0 +1,21 @@
1
+ export { buildRejectHeaders } from "./admission";
2
+ export {
3
+ buildBaseHandshakeHeaders,
4
+ buildClientFinalHeaders,
5
+ buildPeerCapabilities,
6
+ buildServerHandshakeHeaders,
7
+ } from "./capabilities";
8
+ export {
9
+ buildHandshakeBlock,
10
+ describeHandshakeResponse,
11
+ findHeaderEnd,
12
+ hasToken,
13
+ mergeHeaders,
14
+ parseBoolHeader,
15
+ parseHandshakeBlock,
16
+ parseListenIpHeader,
17
+ parsePeerHeaderList,
18
+ parsePositiveIntHeader,
19
+ parseRemoteIpHeader,
20
+ } from "./headers";
21
+ export type { LocalHandshakePolicy } from "./types";
@@ -0,0 +1,36 @@
1
+ type HandshakeNodeMode = "leaf" | "ultrapeer";
2
+
3
+ export type LocalHandshakePolicy = {
4
+ userAgent?: string;
5
+ advertisedHost: string;
6
+ advertisedPort: number;
7
+ maxTtl: number;
8
+ nodeMode: HandshakeNodeMode;
9
+ maxUltrapeerConnections: number;
10
+ maxLeafConnections: number;
11
+ connectedMeshPeerCount: number;
12
+ connectedLeafCount: number;
13
+ enableQrp: boolean;
14
+ queryRoutingVersion?: string;
15
+ enableCompression: boolean;
16
+ enablePongCaching: boolean;
17
+ enableGgep: boolean;
18
+ enableBye: boolean;
19
+ tlsEnabled: boolean;
20
+ tlsUpgradeToken: string;
21
+ };
22
+
23
+ export type CapabilityPolicy = {
24
+ version: string;
25
+ headers: Record<string, string>;
26
+ compressIn: boolean;
27
+ compressOut: boolean;
28
+ tlsEnabled: boolean;
29
+ tlsUpgradeToken: string;
30
+ };
31
+
32
+ export type RejectHandshakePolicy = {
33
+ extraHeaders?: Record<string, string>;
34
+ remoteIp?: string;
35
+ tryPeers?: string[];
36
+ };
@@ -0,0 +1,68 @@
1
+ import net from "node:net";
2
+
3
+ import type { PeerAddr } from "./types";
4
+
5
+ export function normalizeIpv4(
6
+ host: string | undefined,
7
+ ): string | undefined {
8
+ if (!host) return undefined;
9
+ const trimmed = host.trim();
10
+ if (net.isIPv4(trimmed)) return trimmed;
11
+ const mapped = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/i.exec(trimmed);
12
+ if (mapped && net.isIPv4(mapped[1])) return mapped[1];
13
+ return undefined;
14
+ }
15
+
16
+ export function normalizePeer(host: string, port: number): string {
17
+ return `${normalizeIpv4(host) || host.trim()}:${port}`;
18
+ }
19
+
20
+ export function parsePeer(s: string): PeerAddr | null {
21
+ const t = String(s || "").trim();
22
+ const m = /^(\d+\.\d+\.\d+\.\d+):(\d+)$/.exec(t);
23
+ if (!m) return null;
24
+ const host = normalizeIpv4(m[1]);
25
+ if (!host) return null;
26
+ const port = Number(m[2]);
27
+ if (!Number.isInteger(port) || port < 1 || port > 65535) return null;
28
+ return { host, port };
29
+ }
30
+
31
+ function ipv4Octets(host: string): number[] | null {
32
+ const normalized = normalizeIpv4(host);
33
+ if (!normalized) return null;
34
+ return normalized.split(".").map((part) => Number(part));
35
+ }
36
+
37
+ type Ipv4Matcher = (parts: readonly number[]) => boolean;
38
+
39
+ const NON_ROUTABLE_IPV4_MATCHERS: Ipv4Matcher[] = [
40
+ ([a]) => a === 0 || a >= 224,
41
+ ([a]) => a === 10 || a === 127,
42
+ ([a, b]) => a === 100 && b >= 64 && b <= 127,
43
+ ([a, b]) => a === 169 && b === 254,
44
+ ([a, b]) => a === 172 && b >= 16 && b <= 31,
45
+ ([a, b]) => a === 192 && b === 168,
46
+ ([a, b, c]) => a === 192 && b === 0 && c === 0,
47
+ ([a, b, c]) => a === 192 && b === 0 && c === 2,
48
+ ([a, b]) => a === 198 && (b === 18 || b === 19),
49
+ ([a, b, c]) => a === 198 && b === 51 && c === 100,
50
+ ([a, b, c]) => a === 203 && b === 0 && c === 113,
51
+ ];
52
+
53
+ export function isUnspecifiedIpv4(host: string): boolean {
54
+ return normalizeIpv4(host) === "0.0.0.0";
55
+ }
56
+
57
+ export function isRoutableIpv4(host: string): boolean {
58
+ const parts = ipv4Octets(host);
59
+ return (
60
+ !!parts && !NON_ROUTABLE_IPV4_MATCHERS.some((match) => match(parts))
61
+ );
62
+ }
63
+
64
+ export function ipv4Subnet16(host: string): string | undefined {
65
+ const parts = ipv4Octets(host);
66
+ if (!parts) return undefined;
67
+ return `${parts[0]}.${parts[1]}`;
68
+ }
@@ -0,0 +1,80 @@
1
+ import { normalizePeer, parsePeer } from "../peer_address";
2
+ import type {
3
+ BootstrapAttemptSummary,
4
+ FreshPeerCandidateFetchInput,
5
+ PeerCandidate,
6
+ TimestampedPeerCandidate,
7
+ } from "./types";
8
+
9
+ export function normalizePeerCandidates(
10
+ peers: readonly string[],
11
+ isSelfPeer?: (host: string, port: number) => boolean,
12
+ ): PeerCandidate[] {
13
+ const out: PeerCandidate[] = [];
14
+ const seen = new Set<string>();
15
+
16
+ for (const peer of peers) {
17
+ const parsed = parsePeer(peer);
18
+ if (!parsed) continue;
19
+ if (isSelfPeer?.(parsed.host, parsed.port)) continue;
20
+
21
+ const normalized = normalizePeer(parsed.host, parsed.port);
22
+ if (seen.has(normalized)) continue;
23
+ seen.add(normalized);
24
+ out.push({
25
+ host: parsed.host,
26
+ port: parsed.port,
27
+ peer: normalized,
28
+ });
29
+ }
30
+
31
+ return out;
32
+ }
33
+
34
+ export function peerCandidateSetKey(
35
+ candidates: readonly PeerCandidate[],
36
+ ): string {
37
+ return candidates
38
+ .map((peer) => peer.peer)
39
+ .sort((a, b) => a.localeCompare(b))
40
+ .join(",");
41
+ }
42
+
43
+ export function exhaustedPeerSet(
44
+ candidates: readonly PeerCandidate[],
45
+ attempt: BootstrapAttemptSummary,
46
+ ): boolean {
47
+ return (
48
+ candidates.length === 0 ||
49
+ attempt.attemptedPeers.length >= candidates.length
50
+ );
51
+ }
52
+
53
+ export function shouldFetchFreshPeerCandidates(
54
+ input: FreshPeerCandidateFetchInput,
55
+ ): boolean {
56
+ if (!exhaustedPeerSet(input.candidates, input.initialAttempt)) {
57
+ return false;
58
+ }
59
+ if (input.state?.active) return false;
60
+ return input.state?.lastExhaustedPeerSet !== input.candidateKey;
61
+ }
62
+
63
+ export function addPeerCandidatesToKnownSet(
64
+ candidates: readonly PeerCandidate[],
65
+ knownPeers: Set<string>,
66
+ ): string[] {
67
+ const addedPeers: string[] = [];
68
+ for (const candidate of candidates) {
69
+ if (knownPeers.has(candidate.peer)) continue;
70
+ knownPeers.add(candidate.peer);
71
+ addedPeers.push(candidate.peer);
72
+ }
73
+ return addedPeers;
74
+ }
75
+
76
+ export function rankPeerCandidatesByLastSeen<
77
+ T extends TimestampedPeerCandidate,
78
+ >(candidates: readonly T[]): T[] {
79
+ return [...candidates].sort((a, b) => b.lastSeen - a.lastSeen);
80
+ }
@@ -0,0 +1,8 @@
1
+ export {
2
+ addPeerCandidatesToKnownSet,
3
+ exhaustedPeerSet,
4
+ normalizePeerCandidates,
5
+ peerCandidateSetKey,
6
+ rankPeerCandidatesByLastSeen,
7
+ shouldFetchFreshPeerCandidates,
8
+ } from "./candidate_policy";
@@ -0,0 +1,26 @@
1
+ export type PeerCandidate = {
2
+ host: string;
3
+ port: number;
4
+ peer: string;
5
+ };
6
+
7
+ export type BootstrapAttemptSummary = {
8
+ attemptedPeers: readonly string[];
9
+ };
10
+
11
+ type DiscoveryBootstrapState = {
12
+ active?: boolean;
13
+ lastExhaustedPeerSet?: string;
14
+ };
15
+
16
+ export type FreshPeerCandidateFetchInput = {
17
+ candidates: readonly PeerCandidate[];
18
+ initialAttempt: BootstrapAttemptSummary;
19
+ candidateKey: string;
20
+ state?: DiscoveryBootstrapState;
21
+ };
22
+
23
+ export type TimestampedPeerCandidate = {
24
+ peer: string;
25
+ lastSeen: number;
26
+ };
@@ -0,0 +1,61 @@
1
+ import type { ConfigDoc, RuntimeConfig } from "../types";
2
+ import { trimPeerState } from "./peer_state";
3
+ import type {
4
+ PersistedConfig,
5
+ PersistedDoc,
6
+ PersistedState,
7
+ } from "./types";
8
+
9
+ export function persistedConfigForRuntime(
10
+ runtime: RuntimeConfig,
11
+ ): PersistedConfig {
12
+ const cleanConfig: PersistedConfig = {
13
+ listen_ip: runtime.listenHost,
14
+ listen_port: runtime.listenPort,
15
+ gwebcache_urls: [...runtime.gwebCacheUrls],
16
+ ultrapeer: runtime.ultrapeer,
17
+ max_ultrapeer_connections: runtime.maxUltrapeerConnections,
18
+ max_leaf_connections: runtime.maxLeafConnections,
19
+ max_ttl: runtime.maxTtl,
20
+ enable_tls: runtime.enableTls,
21
+ data_dir: runtime.dataDir,
22
+ };
23
+ if (runtime.advertisedHost)
24
+ cleanConfig.advertised_ip = runtime.advertisedHost;
25
+ if (
26
+ runtime.advertisedPort != null &&
27
+ runtime.advertisedPort !== runtime.listenPort
28
+ ) {
29
+ cleanConfig.advertised_port = runtime.advertisedPort;
30
+ }
31
+ if (runtime.blockedIps.length)
32
+ cleanConfig.blocked_ips = runtime.blockedIps;
33
+ if (runtime.monitorIgnoreEvents.length)
34
+ cleanConfig.log_ignore = runtime.monitorIgnoreEvents;
35
+ return cleanConfig;
36
+ }
37
+
38
+ export function persistedStateForDoc(
39
+ doc: ConfigDoc,
40
+ fallbackServentIdHex: string,
41
+ ): PersistedState {
42
+ return {
43
+ servent_id_hex:
44
+ typeof doc.state.serventIdHex === "string" &&
45
+ /^[0-9a-f]{32}$/i.test(doc.state.serventIdHex)
46
+ ? doc.state.serventIdHex.toLowerCase()
47
+ : fallbackServentIdHex,
48
+ peers: trimPeerState(doc.state.peers),
49
+ };
50
+ }
51
+
52
+ export function persistedDocForRuntime(
53
+ runtime: RuntimeConfig,
54
+ doc: ConfigDoc,
55
+ fallbackServentIdHex: string,
56
+ ): PersistedDoc {
57
+ return {
58
+ config: persistedConfigForRuntime(runtime),
59
+ state: persistedStateForDoc(doc, fallbackServentIdHex),
60
+ };
61
+ }
@@ -0,0 +1,14 @@
1
+ export {
2
+ persistedConfigForRuntime,
3
+ persistedDocForRuntime,
4
+ persistedStateForDoc,
5
+ } from "./config_doc";
6
+ export {
7
+ filterBlockedPeerState,
8
+ normalizePeerState,
9
+ peerStateEquals,
10
+ peerStateTargets,
11
+ rememberPeerInState,
12
+ sortPeerStateEntries,
13
+ trimPeerState,
14
+ } from "./peer_state";