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
package/CLI.md CHANGED
@@ -61,6 +61,7 @@ GnutellaBun keeps both settings and remembered state in the same JSON file.
61
61
  | `config.ultrapeer` | Set `true` if you want GnutellaBun to behave like a larger relay-style node. Leave `false` for a lighter client. |
62
62
  | `config.max_ultrapeer_connections` | Cap for ultrapeer-to-ultrapeer links. |
63
63
  | `config.max_leaf_connections` | Cap for leaf links. |
64
+ | `config.max_ttl` | Maximum descriptor TTL to advertise and relay. Defaults to `4`. |
64
65
  | `config.log_ignore` | Event categories to hide when `monitor` is enabled. |
65
66
 
66
67
  ### Saved State
@@ -6,6 +6,7 @@
6
6
  "ultrapeer": false,
7
7
  "max_ultrapeer_connections": 64,
8
8
  "max_leaf_connections": 64,
9
+ "max_ttl": 4,
9
10
  "log_ignore": [],
10
11
  "data_dir": "."
11
12
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gnutella",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "GnutellaBun is a small Gnutella client for Bun, usable as a CLI or TypeScript library.",
5
5
  "license": "GPL-3.0-only",
6
6
  "bin": {
@@ -11,7 +11,8 @@
11
11
  "exports": {
12
12
  ".": "./src/protocol.ts",
13
13
  "./types": "./src/types.ts",
14
- "./gwebcache": "./src/gwebcache_client.ts"
14
+ "./gwebcache": "./src/gwebcache_client.ts",
15
+ "./query-routing": "./src/query_routing/index.ts"
15
16
  },
16
17
  "files": [
17
18
  "bin",
@@ -65,7 +66,7 @@
65
66
  "test:integration": "bun test tests/integration/protocol.test.ts",
66
67
  "test:unit": "bun test tests/unit",
67
68
  "typecheck": "tsc --noEmit",
68
- "unused-exports": "ts-unused-exports tsconfig.json --excludePathsFromReport=src/protocol.ts --exitWithCount",
69
+ "unused-exports": "ts-unused-exports tsconfig.json \"--excludePathsFromReport=src/protocol.ts;src/query_routing/index.ts\" --exitWithCount",
69
70
  "fix": "./scripts/fix-all.sh",
70
71
  "verify": "./scripts/verify-all.sh"
71
72
  }
package/src/cli_shared.ts CHANGED
@@ -1,18 +1,22 @@
1
1
  import { errMsg } from "./shared";
2
- import { RESULT_NAME_WIDTH_MAX } from "./const";
3
2
  import type { CliNode, ParsedCli } from "./types";
4
3
  import { buildMagnetUri } from "./protocol/magnet";
5
4
 
6
5
  const SIZE_FORMAT = new Intl.NumberFormat("en-US", {
6
+ minimumFractionDigits: 1,
7
7
  maximumFractionDigits: 1,
8
+ useGrouping: false,
8
9
  });
9
- const SIZE_UNITS = ["B", "KB", "MB", "GB", "TB", "PB"];
10
+ const SIZE_UNITS = ["b", "kb", "mb", "gb", "tb", "pb"];
11
+ const SIZE_VALUE_WIDTH = 5;
12
+ const SIZE_UNIT_WIDTH = 2;
10
13
  const RESULT_COUNT_DISPLAY_MAX = 999;
11
14
  const PEER_TABLE_WIDTH_MAX = 80;
12
15
 
13
16
  type ResultInfo = ReturnType<CliNode["getResults"]>[number];
17
+ type FormattedSize = { value: string; unit: string };
14
18
 
15
- function formatSize(bytes: number): string {
19
+ function formattedSize(bytes: number): FormattedSize {
16
20
  const safeBytes =
17
21
  Number.isFinite(bytes) && bytes > 0 ? Math.floor(bytes) : 0;
18
22
  let value = safeBytes;
@@ -21,7 +25,20 @@ function formatSize(bytes: number): string {
21
25
  value /= 1024;
22
26
  unitIndex++;
23
27
  }
24
- return `${SIZE_FORMAT.format(value)}${SIZE_UNITS[unitIndex]}`;
28
+ return {
29
+ value: unitIndex === 0 ? String(value) : SIZE_FORMAT.format(value),
30
+ unit: SIZE_UNITS[unitIndex],
31
+ };
32
+ }
33
+
34
+ function formatSize(bytes: number): string {
35
+ const size = formattedSize(bytes);
36
+ return `${size.value} ${size.unit}`;
37
+ }
38
+
39
+ function formatResultSize(bytes: number): string {
40
+ const size = formattedSize(bytes);
41
+ return `${size.value.padStart(SIZE_VALUE_WIDTH, " ")} ${size.unit.padEnd(SIZE_UNIT_WIDTH, " ")}`;
25
42
  }
26
43
 
27
44
  export function displayResultCount(count: number): number {
@@ -234,19 +251,12 @@ export function printResults(
234
251
  const rows = [...results]
235
252
  .sort(
236
253
  (a, b) =>
237
- a.resultNo - b.resultNo ||
238
- a.fileName.localeCompare(b.fileName) ||
239
- a.fileSize - b.fileSize ||
240
- a.remoteHost.localeCompare(b.remoteHost) ||
241
- a.remotePort - b.remotePort,
254
+ a.resultNo - b.resultNo || a.fileName.localeCompare(b.fileName),
242
255
  )
243
256
  .map((result) => ({
244
257
  resultNo: String(result.resultNo),
258
+ fileSize: formatResultSize(result.fileSize),
245
259
  fileName: sanitizeTableCell(result.fileName),
246
- fileSize: formatSize(result.fileSize),
247
- remote: sanitizeTableCell(
248
- `${result.remoteHost}:${result.remotePort}`,
249
- ),
250
260
  }));
251
261
 
252
262
  const widths = {
@@ -254,49 +264,28 @@ export function printResults(
254
264
  "No".length,
255
265
  ...rows.map((row) => row.resultNo.length),
256
266
  ),
257
- fileName: RESULT_NAME_WIDTH_MAX,
258
267
  fileSize: Math.max(
259
268
  "Size".length,
260
269
  ...rows.map((row) => row.fileSize.length),
261
270
  ),
262
- remote: Math.max("IP".length, ...rows.map((row) => row.remote.length)),
271
+ fileName: Math.max(
272
+ "File".length,
273
+ ...rows.map((row) => row.fileName.length),
274
+ ),
263
275
  };
264
276
 
265
- const line = (
266
- resultNo: string,
267
- fileName: string,
268
- fileSize: string,
269
- remote: string,
270
- ) =>
271
- `${resultNo.padStart(widths.resultNo, " ")} ${fileName} ${fileSize.padStart(widths.fileSize, " ")} ${remote}`.trimEnd();
272
-
273
- const fitName = (fileName: string): string => {
274
- if (fileName.length <= widths.fileName - 2)
275
- return fileName.padEnd(widths.fileName, " ");
276
- if (widths.fileName <= 2) return fileName.slice(0, widths.fileName);
277
- const kept = widths.fileName - 2;
278
- const head = Math.floor(kept / 2);
279
- const tail = kept - head;
280
- return `${fileName.slice(0, head)}..${fileName.slice(-tail)}`;
281
- };
277
+ const line = (resultNo: string, fileSize: string, fileName: string) =>
278
+ `${resultNo.padStart(widths.resultNo, " ")} ${fileSize.padStart(widths.fileSize, " ")} ${fileName.padEnd(widths.fileName, " ")}`.trimEnd();
282
279
 
283
280
  log(
284
281
  [
285
- line("No", "File".padEnd(widths.fileName, " "), "Size", "IP"),
282
+ line("No", "Size", "File"),
286
283
  line(
287
284
  "-".repeat(widths.resultNo),
288
- "-".repeat(widths.fileName),
289
285
  "-".repeat(widths.fileSize),
290
- "-".repeat(widths.remote),
291
- ),
292
- ...rows.map((row) =>
293
- line(
294
- row.resultNo,
295
- fitName(row.fileName),
296
- row.fileSize,
297
- row.remote,
298
- ),
286
+ "-".repeat(widths.fileName),
299
287
  ),
288
+ ...rows.map((row) => line(row.resultNo, row.fileSize, row.fileName)),
300
289
  ].join("\n"),
301
290
  );
302
291
  }
package/src/const.ts CHANGED
@@ -1,8 +1,5 @@
1
1
  export const HEADER_LEN = 23;
2
2
  export const LOCAL_ROUTE = "__local__";
3
- export const DEFAULT_QRP_TABLE_SIZE = 65536;
4
- export const DEFAULT_QRP_INFINITY = 7;
5
- export const DEFAULT_QRP_ENTRY_BITS = 1;
6
3
  export const DEFAULT_LISTEN_HOST = "0.0.0.0";
7
4
  export const DEFAULT_LISTEN_PORT_MIN = 20000;
8
5
  export const DEFAULT_LISTEN_PORT_MAX = 29999;
@@ -18,7 +15,7 @@ export const RESCAN_SHARES_SEC = 30;
18
15
  export const ROUTE_TTL_SEC = 600;
19
16
  export const SEEN_TTL_SEC = 600;
20
17
  export const MAX_PAYLOAD_BYTES = 1024 * 1024;
21
- export const MAX_TTL = 7;
18
+ export const MAX_TTL = 4;
22
19
  export const DEFAULT_PING_TTL = 1;
23
20
  export const DEFAULT_QUERY_TTL = 4;
24
21
  export const ADVERTISED_SPEED_KBPS = 512;
@@ -37,8 +34,6 @@ export const ENABLE_BYE = true;
37
34
  export const ENABLE_PONG_CACHING = true;
38
35
  export const ENABLE_GGEP = true;
39
36
  export const SERVE_URI_RES = true;
40
- export const QRP_COMPRESSOR_NONE = 0;
41
- export const QRP_COMPRESSOR_DEFLATE = 1;
42
37
  export const MAX_XTRY = 10;
43
38
  export const BYE_DEFAULT_CODE = 200;
44
39
  export const BOOTSTRAP_CONNECT_CONCURRENCY = 8;
@@ -104,7 +99,6 @@ export const INTERESTING_HANDSHAKE_HEADERS = [
104
99
  "remote-ip",
105
100
  ] as const;
106
101
 
107
- export const QRP_HASH_MULTIPLIER = 0x4f1bbcdc;
108
102
  export const BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
109
103
 
110
104
  export const PROMPT_THROBBER_FRAMES = ["*", "o", ".", " "] as const;
@@ -134,5 +128,3 @@ export const CLI_HELP_LINES = [
134
128
  "quit",
135
129
  "sleep",
136
130
  ] as const;
137
-
138
- export const RESULT_NAME_WIDTH_MAX = 48;
@@ -0,0 +1,17 @@
1
+ export {
2
+ overflowPongCacheKeys,
3
+ pongCacheKey,
4
+ selectCachedPongPayloads,
5
+ } from "./pong_cache";
6
+ export { responseRouteDecision } from "./response_routes";
7
+ export {
8
+ shouldMarkDescriptorSeen,
9
+ shouldSuppressDescriptor,
10
+ } from "./seen";
11
+ export {
12
+ forwardedDescriptorLifetime,
13
+ normalizeQueryLifetime,
14
+ pongReplyTtl,
15
+ queryHitReplyTtl,
16
+ shouldRelayPing,
17
+ } from "./ttl";
@@ -0,0 +1,32 @@
1
+ import crypto from "node:crypto";
2
+
3
+ import type { PongCacheEntry } from "./types";
4
+
5
+ export function pongCacheKey(payload: Buffer): string {
6
+ return crypto.createHash("sha1").update(payload).digest("hex");
7
+ }
8
+
9
+ export function overflowPongCacheKeys(
10
+ entries: Iterable<[string, Pick<PongCacheEntry, "at">]>,
11
+ maxSize: number,
12
+ ): string[] {
13
+ const all = [...entries];
14
+ if (all.length <= maxSize) return [];
15
+ return all
16
+ .sort((a, b) => a[1].at - b[1].at)
17
+ .slice(0, all.length - maxSize)
18
+ .map(([key]) => key);
19
+ }
20
+
21
+ export function selectCachedPongPayloads(
22
+ entries: Iterable<PongCacheEntry>,
23
+ alreadySent: number,
24
+ maxSent: number,
25
+ ): Buffer[] {
26
+ const available = Math.max(0, maxSent - alreadySent);
27
+ if (available === 0) return [];
28
+ return [...entries]
29
+ .sort((a, b) => b.at - a.at)
30
+ .slice(0, available)
31
+ .map((entry) => entry.payload);
32
+ }
@@ -0,0 +1,15 @@
1
+ import { LOCAL_ROUTE } from "../const";
2
+ import type { Route } from "../types";
3
+ import type { ResponseRouteDecision, ResponseRouteOptions } from "./types";
4
+
5
+ export function responseRouteDecision(
6
+ route: Route | typeof LOCAL_ROUTE | undefined,
7
+ options: ResponseRouteOptions = {},
8
+ ): ResponseRouteDecision {
9
+ if (!route) return { kind: "drop" };
10
+ if (route === LOCAL_ROUTE) return { kind: "local" };
11
+ if (options.nodeMode === "leaf" && !options.forwardInLeaf) {
12
+ return { kind: "drop" };
13
+ }
14
+ return { kind: "forward", route };
15
+ }
@@ -0,0 +1,20 @@
1
+ import { TYPE } from "../const";
2
+ import type { DescriptorSuppressionInput } from "./types";
3
+
4
+ export function shouldSuppressDescriptor(
5
+ input: DescriptorSuppressionInput,
6
+ ): boolean {
7
+ if (
8
+ input.closingAfterBye &&
9
+ input.payloadType !== TYPE.QUERY_HIT &&
10
+ input.payloadType !== TYPE.PUSH
11
+ ) {
12
+ return true;
13
+ }
14
+ if (input.payloadType === TYPE.ROUTE_TABLE_UPDATE) return false;
15
+ return input.alreadySeen;
16
+ }
17
+
18
+ export function shouldMarkDescriptorSeen(payloadType: number): boolean {
19
+ return payloadType !== TYPE.ROUTE_TABLE_UPDATE;
20
+ }
@@ -0,0 +1,37 @@
1
+ import type { DescriptorLifetime } from "./types";
2
+
3
+ export function normalizeQueryLifetime(
4
+ ttl: number,
5
+ hops: number,
6
+ maxTtl: number,
7
+ ): DescriptorLifetime | null {
8
+ if (ttl > 15) return null;
9
+ const maxLife = Math.max(1, maxTtl);
10
+ if (hops > maxLife) return null;
11
+ return { ttl: Math.max(0, Math.min(ttl, maxLife - hops)), hops };
12
+ }
13
+
14
+ export function forwardedDescriptorLifetime(
15
+ ttl: number,
16
+ hops: number,
17
+ ): DescriptorLifetime | undefined {
18
+ if (ttl <= 0) return undefined;
19
+ return { ttl: Math.max(0, ttl - 1), hops: hops + 1 };
20
+ }
21
+
22
+ export function pongReplyTtl(hops: number): number {
23
+ return Math.max(1, hops);
24
+ }
25
+
26
+ export function queryHitReplyTtl(hops: number, maxTtl: number): number {
27
+ return Math.min(maxTtl, Math.max(1, hops + 2));
28
+ }
29
+
30
+ export function shouldRelayPing(
31
+ ttl: number,
32
+ now: number,
33
+ lastPingAt: number,
34
+ minIntervalMs: number,
35
+ ): boolean {
36
+ return ttl > 1 && now - lastPingAt >= minIntervalMs;
37
+ }
@@ -0,0 +1,27 @@
1
+ import type { Route, RuntimeConfig } from "../types";
2
+
3
+ export type DescriptorLifetime = {
4
+ ttl: number;
5
+ hops: number;
6
+ };
7
+
8
+ export type DescriptorSuppressionInput = {
9
+ closingAfterBye: boolean;
10
+ payloadType: number;
11
+ alreadySeen: boolean;
12
+ };
13
+
14
+ export type ResponseRouteOptions = {
15
+ nodeMode?: RuntimeConfig["nodeMode"];
16
+ forwardInLeaf?: boolean;
17
+ };
18
+
19
+ export type ResponseRouteDecision =
20
+ | { kind: "drop" }
21
+ | { kind: "local" }
22
+ | { kind: "forward"; route: Route };
23
+
24
+ export type PongCacheEntry = {
25
+ payload: Buffer;
26
+ at: number;
27
+ };
@@ -1,4 +1,9 @@
1
- import { normalizePeer, parsePeer } from "../shared";
1
+ import {
2
+ addPeerCandidatesToKnownSet,
3
+ normalizePeerCandidates,
4
+ peerCandidateSetKey,
5
+ shouldFetchFreshPeerCandidates,
6
+ } from "../peer_discovery";
2
7
  import {
3
8
  DEFAULT_MAX_BOOTSTRAP_CACHES,
4
9
  DEFAULT_MAX_BOOTSTRAP_PEERS,
@@ -16,7 +21,6 @@ import {
16
21
  } from "./response";
17
22
  import type {
18
23
  BootstrapOptions,
19
- BootstrapPeer,
20
24
  BootstrapResult,
21
25
  ConnectBootstrapOptions,
22
26
  ConnectBootstrapResult,
@@ -27,38 +31,6 @@ import type {
27
31
  ReportSelfResult,
28
32
  } from "./types";
29
33
 
30
- function normalizeBootstrapPeers(
31
- peers: readonly string[],
32
- isSelfPeer?: (host: string, port: number) => boolean,
33
- ): BootstrapPeer[] {
34
- const out: BootstrapPeer[] = [];
35
- const seen = new Set<string>();
36
-
37
- for (const peer of peers) {
38
- const parsed = parsePeer(peer);
39
- if (!parsed) continue;
40
- if (isSelfPeer?.(parsed.host, parsed.port)) continue;
41
-
42
- const normalized = normalizePeer(parsed.host, parsed.port);
43
- if (seen.has(normalized)) continue;
44
- seen.add(normalized);
45
- out.push({
46
- host: parsed.host,
47
- port: parsed.port,
48
- peer: normalized,
49
- });
50
- }
51
-
52
- return out;
53
- }
54
-
55
- function bootstrapPeerSetKey(peers: BootstrapPeer[]): string {
56
- return peers
57
- .map((peer) => peer.peer)
58
- .sort((a, b) => a.localeCompare(b))
59
- .join(",");
60
- }
61
-
62
34
  function buildReportReferenceUrl(
63
35
  cache: string,
64
36
  knownAliveCaches: readonly string[],
@@ -117,25 +89,18 @@ function emptyConnectBootstrapResult(
117
89
  };
118
90
  }
119
91
 
120
- function exhaustedBootstrapPeerSet(
121
- candidates: BootstrapPeer[],
122
- initialAttempt: Awaited<ReturnType<typeof connectBootstrapPeerSet>>,
123
- ): boolean {
124
- return (
125
- candidates.length === 0 ||
126
- initialAttempt.attemptedPeers.length >= candidates.length
127
- );
128
- }
129
-
130
92
  function shouldSkipCacheBootstrap(
131
- candidates: BootstrapPeer[],
93
+ candidates: ReturnType<typeof normalizePeerCandidates>,
132
94
  initialAttempt: Awaited<ReturnType<typeof connectBootstrapPeerSet>>,
133
95
  candidateKey: string,
134
96
  state: GWebCacheBootstrapState | undefined,
135
97
  ): boolean {
136
- if (!exhaustedBootstrapPeerSet(candidates, initialAttempt)) return true;
137
- if (state?.active) return true;
138
- return state?.lastExhaustedPeerSet === candidateKey;
98
+ return !shouldFetchFreshPeerCandidates({
99
+ candidates,
100
+ initialAttempt,
101
+ candidateKey,
102
+ state,
103
+ });
139
104
  }
140
105
 
141
106
  function buildBootstrapFetchOptions(
@@ -156,15 +121,13 @@ function buildBootstrapFetchOptions(
156
121
  }
157
122
 
158
123
  function addDiscoveredBootstrapPeers(
159
- peers: BootstrapPeer[],
124
+ peers: ReturnType<typeof normalizePeerCandidates>,
160
125
  knownPeers: Set<string>,
161
126
  addPeer: ConnectBootstrapOptions["addPeer"],
162
127
  ): string[] {
163
- const addedPeers: string[] = [];
164
- for (const peer of peers) {
165
- knownPeers.add(peer.peer);
166
- addedPeers.push(peer.peer);
167
- addPeer?.(peer.peer);
128
+ const addedPeers = addPeerCandidatesToKnownSet(peers, knownPeers);
129
+ for (const peer of addedPeers) {
130
+ addPeer?.(peer);
168
131
  }
169
132
  return addedPeers;
170
133
  }
@@ -182,7 +145,7 @@ async function fetchAndRetryBootstrapPeers(
182
145
  buildBootstrapFetchOptions(options),
183
146
  );
184
147
  rememberAliveCaches(options.state, bootstrap.successfulCaches);
185
- const discovered = normalizeBootstrapPeers(
148
+ const discovered = normalizePeerCandidates(
186
149
  bootstrap.peers,
187
150
  options.isSelfPeer,
188
151
  ).filter((peer) => !knownPeers.has(peer.peer));
@@ -200,7 +163,7 @@ async function fetchAndRetryBootstrapPeers(
200
163
  }
201
164
 
202
165
  async function connectBootstrapPeerSet(
203
- peers: BootstrapPeer[],
166
+ peers: ReturnType<typeof normalizePeerCandidates>,
204
167
  options: Pick<
205
168
  ConnectBootstrapOptions,
206
169
  | "availableSlots"
@@ -433,11 +396,11 @@ function finishCacheBootstrap(
433
396
  export async function connectBootstrapPeers(
434
397
  options: ConnectBootstrapOptions,
435
398
  ): Promise<ConnectBootstrapResult> {
436
- const candidates = normalizeBootstrapPeers(
399
+ const candidates = normalizePeerCandidates(
437
400
  options.peers,
438
401
  options.isSelfPeer,
439
402
  );
440
- const candidateKey = bootstrapPeerSetKey(candidates);
403
+ const candidateKey = peerCandidateSetKey(candidates);
441
404
  const knownPeers = new Set(candidates.map((peer) => peer.peer));
442
405
  const initialAttempt = await connectBootstrapPeerSet(
443
406
  candidates,
@@ -92,21 +92,17 @@ export type BootstrapOptions = {
92
92
  fetchImpl?: FetchLike;
93
93
  };
94
94
 
95
+ type BootstrapCacheError = {
96
+ cache: string;
97
+ message: string;
98
+ };
99
+
95
100
  export type BootstrapResult = {
96
101
  peers: string[];
97
102
  caches: string[];
98
103
  queriedCaches: string[];
99
104
  successfulCaches: string[];
100
- errors: Array<{
101
- cache: string;
102
- message: string;
103
- }>;
104
- };
105
-
106
- export type BootstrapPeer = {
107
- host: string;
108
- port: number;
109
- peer: string;
105
+ errors: BootstrapCacheError[];
110
106
  };
111
107
 
112
108
  export type GWebCacheBootstrapState = {
@@ -0,0 +1,17 @@
1
+ import { lowerCaseHeaders, parseRemoteIpHeader } from "./headers";
2
+ import type { RejectHandshakePolicy } from "./types";
3
+
4
+ export function buildRejectHeaders(
5
+ policy: RejectHandshakePolicy,
6
+ ): Record<string, string> {
7
+ const headers = lowerCaseHeaders(policy.extraHeaders || {});
8
+ const observedRemote = parseRemoteIpHeader(policy.remoteIp);
9
+ const tryPeers = policy.tryPeers || [];
10
+ if (observedRemote) headers["remote-ip"] = observedRemote;
11
+ if (tryPeers.length) {
12
+ const value = tryPeers.join(",");
13
+ headers["x-try"] = value;
14
+ headers["x-try-ultrapeers"] = value;
15
+ }
16
+ return headers;
17
+ }