@session-foundation/swarm-routing 0.0.1

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.
@@ -0,0 +1,6 @@
1
+ /* eslint-disable no-undef */
2
+ /* eslint-disable @typescript-eslint/no-require-imports */
3
+ const {
4
+ extendShared,
5
+ } = require('@session-foundation/dep-cruiser-config-shared');
6
+ module.exports = extendShared();
@@ -0,0 +1,20 @@
1
+
2
+
3
+ > @session-foundation/swarm-routing@0.0.1 build /home/audric/pro/contribs/session-pkgs/packages/swarm-routing
4
+ > tsup src/index.ts --format cjs,esm --dts --tsconfig tsconfig.build.json
5
+
6
+ CLI Building entry: src/index.ts
7
+ CLI Using tsconfig: tsconfig.build.json
8
+ CLI tsup v8.5.1
9
+ CLI Using tsup config: /home/audric/pro/contribs/session-pkgs/packages/swarm-routing/tsup.config.ts
10
+ CLI Target: esnext
11
+ CJS Build start
12
+ ESM Build start
13
+ ESM dist/index.mjs 4.05 KB
14
+ ESM ⚡️ Build success in 47ms
15
+ CJS dist/index.cjs 5.08 KB
16
+ CJS ⚡️ Build success in 50ms
17
+ DTS Build start
18
+ DTS ⚡️ Build success in 834ms
19
+ DTS dist/index.d.ts 1.55 KB
20
+ DTS dist/index.d.mts 1.55 KB
@@ -0,0 +1,8 @@
1
+
2
+
3
+ > @session-foundation/swarm-routing@0.0.1 lint /home/audric/pro/contribs/session-pkgs/packages/swarm-routing
4
+ > eslint . --max-warnings 0 && dependency-cruiser --config .dependency-cruiser.cjs .
5
+
6
+
7
+ ✔ no dependency violations found (21 modules, 21 dependencies cruised)
8
+
package/dist/index.cjs ADDED
@@ -0,0 +1,139 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ Swarm: () => Swarm
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+
27
+ // src/swarm_routing.ts
28
+ var import_basic_types = require("@session-foundation/basic-types");
29
+ var Swarm = class _Swarm {
30
+ hi;
31
+ // upper 32 bits, unsigned
32
+ lo;
33
+ // lower 32 bits, unsigned
34
+ constructor(hi, lo) {
35
+ this.hi = hi >>> 0;
36
+ this.lo = lo >>> 0;
37
+ }
38
+ /** Build from 8 big-endian bytes (output of the XOR reduction step). */
39
+ static fromBytes(bytes) {
40
+ const hi = (bytes[0] << 24 | bytes[1] << 16 | bytes[2] << 8 | bytes[3]) >>> 0;
41
+ const lo = (bytes[4] << 24 | bytes[5] << 16 | bytes[6] << 8 | bytes[7]) >>> 0;
42
+ return new _Swarm(hi, lo);
43
+ }
44
+ /** Parse a hex string (with or without 0x prefix, any length up to 16 chars). */
45
+ static fromHex(hex) {
46
+ const clean = hex.startsWith("0x") || hex.startsWith("0X") ? hex.slice(2) : hex;
47
+ const padded = clean.padStart(16, "0");
48
+ const hi = parseInt(padded.slice(0, 8), 16);
49
+ const lo = parseInt(padded.slice(8), 16);
50
+ return new _Swarm(hi, lo);
51
+ }
52
+ // ── Comparisons ────────────────────────────────────────────────────────────
53
+ /** -1 if this < other, 0 if equal, 1 if this > other (unsigned 64-bit). */
54
+ compareTo(other) {
55
+ if (this.hi !== other.hi) return this.hi < other.hi ? -1 : 1;
56
+ if (this.lo !== other.lo) return this.lo < other.lo ? -1 : 1;
57
+ return 0;
58
+ }
59
+ isLessThan(other) {
60
+ return this.compareTo(other) < 0;
61
+ }
62
+ equals(other) {
63
+ return this.hi === other.hi && this.lo === other.lo;
64
+ }
65
+ // ── Arithmetic ─────────────────────────────────────────────────────────────
66
+ /** Unsigned 64-bit subtraction with wraparound (mod 2^64). */
67
+ subtract(other) {
68
+ const lo = this.lo - other.lo >>> 0;
69
+ const borrow = this.lo < other.lo ? 1 : 0;
70
+ const hi = this.hi - other.hi - borrow >>> 0;
71
+ return new _Swarm(hi, lo);
72
+ }
73
+ // ── Serialisation ──────────────────────────────────────────────────────────
74
+ /** 16-character zero-padded lowercase hex string. */
75
+ toString() {
76
+ return this.hi.toString(16).padStart(8, "0") + this.lo.toString(16).padStart(8, "0");
77
+ }
78
+ toJSON() {
79
+ return this.toString();
80
+ }
81
+ // ── Routing ────────────────────────────────────────────────────────────────
82
+ /**
83
+ * XOR the four 8-byte chunks of a 32-byte x25519 pubkey, then interpret
84
+ * the result as a big-endian uint64. Equivalent of C++ pubkey_to_swarm_space.
85
+ */
86
+ static pubkeyToSwarmSpace(pk) {
87
+ const xored = new Uint8Array(8);
88
+ for (let i = 0; i < 4; i++)
89
+ for (let j = 0; j < 8; j++) xored[j] ^= pk[i * 8 + j];
90
+ return _Swarm.fromBytes(xored);
91
+ }
92
+ /**
93
+ * Group nodes by swarm and return them sorted by swarm ID ascending.
94
+ * Equivalent of C++ generate_swarms.
95
+ */
96
+ static generateSwarms(nodes) {
97
+ const grouped = /* @__PURE__ */ new Map();
98
+ for (const node of nodes) {
99
+ const bucket = grouped.get(node.swarm) ?? [];
100
+ bucket.push(node);
101
+ grouped.set(node.swarm, bucket);
102
+ }
103
+ const result = [];
104
+ for (const [swarmHex, members] of grouped)
105
+ result.push([_Swarm.fromHex(swarmHex), members]);
106
+ result.sort((a, b) => a[0].compareTo(b[0]));
107
+ return result;
108
+ }
109
+ /**
110
+ * Find the swarm closest to the given x25519 pubkey using circular uint64
111
+ * distance. Equivalent of C++ get_swarm.
112
+ */
113
+ static getFor(swarmPubkeyHex, allSwarms) {
114
+ if (allSwarms.length === 0) {
115
+ throw new Error("No swarms found: allSwarms.length === 0");
116
+ }
117
+ if (allSwarms.length === 1) {
118
+ return allSwarms[0];
119
+ }
120
+ const pkNoPrefix = swarmPubkeyHex.length === 66 ? swarmPubkeyHex.slice(2) : swarmPubkeyHex;
121
+ const pk = (0, import_basic_types.fromHex)((0, import_basic_types.createHexString)(pkNoPrefix));
122
+ const swarmId = _Swarm.pubkeyToSwarmSpace(pk);
123
+ let lo = 0, hi = allSwarms.length;
124
+ while (lo < hi) {
125
+ const mid = lo + hi >> 1;
126
+ if (allSwarms[mid][0].isLessThan(swarmId)) lo = mid + 1;
127
+ else hi = mid;
128
+ }
129
+ const rightIdx = lo === allSwarms.length ? 0 : lo;
130
+ const leftIdx = rightIdx === 0 ? allSwarms.length - 1 : rightIdx - 1;
131
+ const dright = allSwarms[rightIdx][0].subtract(swarmId);
132
+ const dleft = swarmId.subtract(allSwarms[leftIdx][0]);
133
+ return dright.isLessThan(dleft) ? allSwarms[rightIdx] : allSwarms[leftIdx];
134
+ }
135
+ };
136
+ // Annotate the CommonJS export names for ESM import in node:
137
+ 0 && (module.exports = {
138
+ Swarm
139
+ });
@@ -0,0 +1,40 @@
1
+ import { WithSwarmStr } from '@session-foundation/basic-types';
2
+
3
+ type WithSwarm = {
4
+ swarm: Swarm;
5
+ };
6
+ declare class Swarm {
7
+ private readonly hi;
8
+ private readonly lo;
9
+ private constructor();
10
+ /** Build from 8 big-endian bytes (output of the XOR reduction step). */
11
+ static fromBytes(bytes: Uint8Array): Swarm;
12
+ /** Parse a hex string (with or without 0x prefix, any length up to 16 chars). */
13
+ static fromHex(hex: string): Swarm;
14
+ /** -1 if this < other, 0 if equal, 1 if this > other (unsigned 64-bit). */
15
+ compareTo(other: Swarm): number;
16
+ isLessThan(other: Swarm): boolean;
17
+ equals(other: Swarm): boolean;
18
+ /** Unsigned 64-bit subtraction with wraparound (mod 2^64). */
19
+ subtract(other: Swarm): Swarm;
20
+ /** 16-character zero-padded lowercase hex string. */
21
+ toString(): string;
22
+ toJSON(): string;
23
+ /**
24
+ * XOR the four 8-byte chunks of a 32-byte x25519 pubkey, then interpret
25
+ * the result as a big-endian uint64. Equivalent of C++ pubkey_to_swarm_space.
26
+ */
27
+ static pubkeyToSwarmSpace(pk: Uint8Array): Swarm;
28
+ /**
29
+ * Group nodes by swarm and return them sorted by swarm ID ascending.
30
+ * Equivalent of C++ generate_swarms.
31
+ */
32
+ static generateSwarms<T extends WithSwarmStr>(nodes: T[]): Array<[Swarm, T[]]>;
33
+ /**
34
+ * Find the swarm closest to the given x25519 pubkey using circular uint64
35
+ * distance. Equivalent of C++ get_swarm.
36
+ */
37
+ static getFor<T extends WithSwarmStr>(swarmPubkeyHex: string, allSwarms: Array<[Swarm, T[]]>): [Swarm, T[]];
38
+ }
39
+
40
+ export { Swarm, type WithSwarm };
@@ -0,0 +1,40 @@
1
+ import { WithSwarmStr } from '@session-foundation/basic-types';
2
+
3
+ type WithSwarm = {
4
+ swarm: Swarm;
5
+ };
6
+ declare class Swarm {
7
+ private readonly hi;
8
+ private readonly lo;
9
+ private constructor();
10
+ /** Build from 8 big-endian bytes (output of the XOR reduction step). */
11
+ static fromBytes(bytes: Uint8Array): Swarm;
12
+ /** Parse a hex string (with or without 0x prefix, any length up to 16 chars). */
13
+ static fromHex(hex: string): Swarm;
14
+ /** -1 if this < other, 0 if equal, 1 if this > other (unsigned 64-bit). */
15
+ compareTo(other: Swarm): number;
16
+ isLessThan(other: Swarm): boolean;
17
+ equals(other: Swarm): boolean;
18
+ /** Unsigned 64-bit subtraction with wraparound (mod 2^64). */
19
+ subtract(other: Swarm): Swarm;
20
+ /** 16-character zero-padded lowercase hex string. */
21
+ toString(): string;
22
+ toJSON(): string;
23
+ /**
24
+ * XOR the four 8-byte chunks of a 32-byte x25519 pubkey, then interpret
25
+ * the result as a big-endian uint64. Equivalent of C++ pubkey_to_swarm_space.
26
+ */
27
+ static pubkeyToSwarmSpace(pk: Uint8Array): Swarm;
28
+ /**
29
+ * Group nodes by swarm and return them sorted by swarm ID ascending.
30
+ * Equivalent of C++ generate_swarms.
31
+ */
32
+ static generateSwarms<T extends WithSwarmStr>(nodes: T[]): Array<[Swarm, T[]]>;
33
+ /**
34
+ * Find the swarm closest to the given x25519 pubkey using circular uint64
35
+ * distance. Equivalent of C++ get_swarm.
36
+ */
37
+ static getFor<T extends WithSwarmStr>(swarmPubkeyHex: string, allSwarms: Array<[Swarm, T[]]>): [Swarm, T[]];
38
+ }
39
+
40
+ export { Swarm, type WithSwarm };
package/dist/index.mjs ADDED
@@ -0,0 +1,112 @@
1
+ // src/swarm_routing.ts
2
+ import { createHexString, fromHex } from "@session-foundation/basic-types";
3
+ var Swarm = class _Swarm {
4
+ hi;
5
+ // upper 32 bits, unsigned
6
+ lo;
7
+ // lower 32 bits, unsigned
8
+ constructor(hi, lo) {
9
+ this.hi = hi >>> 0;
10
+ this.lo = lo >>> 0;
11
+ }
12
+ /** Build from 8 big-endian bytes (output of the XOR reduction step). */
13
+ static fromBytes(bytes) {
14
+ const hi = (bytes[0] << 24 | bytes[1] << 16 | bytes[2] << 8 | bytes[3]) >>> 0;
15
+ const lo = (bytes[4] << 24 | bytes[5] << 16 | bytes[6] << 8 | bytes[7]) >>> 0;
16
+ return new _Swarm(hi, lo);
17
+ }
18
+ /** Parse a hex string (with or without 0x prefix, any length up to 16 chars). */
19
+ static fromHex(hex) {
20
+ const clean = hex.startsWith("0x") || hex.startsWith("0X") ? hex.slice(2) : hex;
21
+ const padded = clean.padStart(16, "0");
22
+ const hi = parseInt(padded.slice(0, 8), 16);
23
+ const lo = parseInt(padded.slice(8), 16);
24
+ return new _Swarm(hi, lo);
25
+ }
26
+ // ── Comparisons ────────────────────────────────────────────────────────────
27
+ /** -1 if this < other, 0 if equal, 1 if this > other (unsigned 64-bit). */
28
+ compareTo(other) {
29
+ if (this.hi !== other.hi) return this.hi < other.hi ? -1 : 1;
30
+ if (this.lo !== other.lo) return this.lo < other.lo ? -1 : 1;
31
+ return 0;
32
+ }
33
+ isLessThan(other) {
34
+ return this.compareTo(other) < 0;
35
+ }
36
+ equals(other) {
37
+ return this.hi === other.hi && this.lo === other.lo;
38
+ }
39
+ // ── Arithmetic ─────────────────────────────────────────────────────────────
40
+ /** Unsigned 64-bit subtraction with wraparound (mod 2^64). */
41
+ subtract(other) {
42
+ const lo = this.lo - other.lo >>> 0;
43
+ const borrow = this.lo < other.lo ? 1 : 0;
44
+ const hi = this.hi - other.hi - borrow >>> 0;
45
+ return new _Swarm(hi, lo);
46
+ }
47
+ // ── Serialisation ──────────────────────────────────────────────────────────
48
+ /** 16-character zero-padded lowercase hex string. */
49
+ toString() {
50
+ return this.hi.toString(16).padStart(8, "0") + this.lo.toString(16).padStart(8, "0");
51
+ }
52
+ toJSON() {
53
+ return this.toString();
54
+ }
55
+ // ── Routing ────────────────────────────────────────────────────────────────
56
+ /**
57
+ * XOR the four 8-byte chunks of a 32-byte x25519 pubkey, then interpret
58
+ * the result as a big-endian uint64. Equivalent of C++ pubkey_to_swarm_space.
59
+ */
60
+ static pubkeyToSwarmSpace(pk) {
61
+ const xored = new Uint8Array(8);
62
+ for (let i = 0; i < 4; i++)
63
+ for (let j = 0; j < 8; j++) xored[j] ^= pk[i * 8 + j];
64
+ return _Swarm.fromBytes(xored);
65
+ }
66
+ /**
67
+ * Group nodes by swarm and return them sorted by swarm ID ascending.
68
+ * Equivalent of C++ generate_swarms.
69
+ */
70
+ static generateSwarms(nodes) {
71
+ const grouped = /* @__PURE__ */ new Map();
72
+ for (const node of nodes) {
73
+ const bucket = grouped.get(node.swarm) ?? [];
74
+ bucket.push(node);
75
+ grouped.set(node.swarm, bucket);
76
+ }
77
+ const result = [];
78
+ for (const [swarmHex, members] of grouped)
79
+ result.push([_Swarm.fromHex(swarmHex), members]);
80
+ result.sort((a, b) => a[0].compareTo(b[0]));
81
+ return result;
82
+ }
83
+ /**
84
+ * Find the swarm closest to the given x25519 pubkey using circular uint64
85
+ * distance. Equivalent of C++ get_swarm.
86
+ */
87
+ static getFor(swarmPubkeyHex, allSwarms) {
88
+ if (allSwarms.length === 0) {
89
+ throw new Error("No swarms found: allSwarms.length === 0");
90
+ }
91
+ if (allSwarms.length === 1) {
92
+ return allSwarms[0];
93
+ }
94
+ const pkNoPrefix = swarmPubkeyHex.length === 66 ? swarmPubkeyHex.slice(2) : swarmPubkeyHex;
95
+ const pk = fromHex(createHexString(pkNoPrefix));
96
+ const swarmId = _Swarm.pubkeyToSwarmSpace(pk);
97
+ let lo = 0, hi = allSwarms.length;
98
+ while (lo < hi) {
99
+ const mid = lo + hi >> 1;
100
+ if (allSwarms[mid][0].isLessThan(swarmId)) lo = mid + 1;
101
+ else hi = mid;
102
+ }
103
+ const rightIdx = lo === allSwarms.length ? 0 : lo;
104
+ const leftIdx = rightIdx === 0 ? allSwarms.length - 1 : rightIdx - 1;
105
+ const dright = allSwarms[rightIdx][0].subtract(swarmId);
106
+ const dleft = swarmId.subtract(allSwarms[leftIdx][0]);
107
+ return dright.isLessThan(dleft) ? allSwarms[rightIdx] : allSwarms[leftIdx];
108
+ }
109
+ };
110
+ export {
111
+ Swarm
112
+ };
@@ -0,0 +1,4 @@
1
+ import { config } from "@repo/eslint-config/base";
2
+
3
+ /** @type {import("eslint").Linter.Config} */
4
+ export default config;
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@session-foundation/swarm-routing",
3
+ "version": "0.0.1",
4
+ "description": "Routes requests across storage nodes by pubkey XOR distance",
5
+ "main": "./dist/index.cjs",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.cjs"
13
+ }
14
+ },
15
+ "dependencies": {
16
+ "lodash": "^4.17.21",
17
+ "@session-foundation/network-requests": "^0.0.6",
18
+ "@session-foundation/basic-types": "^0.0.7"
19
+ },
20
+ "devDependencies": {
21
+ "@turbo/gen": "^2.4.4",
22
+ "@types/chai": "^5.2.2",
23
+ "@types/lodash": "^4.17.16",
24
+ "@types/mocha": "^10.0.10",
25
+ "chai": "^5.2.0",
26
+ "dependency-cruiser": "^16.3.3",
27
+ "eslint": "^9.23.0",
28
+ "mocha": "^11.7.1",
29
+ "ts-node": "^10.9.2",
30
+ "tsup": "^8.4.0",
31
+ "tsx": "^4.21.0",
32
+ "typescript": "5.8.2",
33
+ "@repo/tsup-config": "^0.0.0",
34
+ "@repo/typescript-config": "^0.0.0",
35
+ "@repo/eslint-config": "^0.0.0",
36
+ "@session-foundation/dep-cruiser-config-shared": "^1.0.0"
37
+ },
38
+ "publishConfig": {
39
+ "access": "public",
40
+ "registry": "https://registry.npmjs.org/"
41
+ },
42
+ "scripts": {
43
+ "lint": "eslint . --max-warnings 0 && dependency-cruiser --config .dependency-cruiser.cjs .",
44
+ "check-types": "tsc --noEmit",
45
+ "build": "tsup src/index.ts --format cjs,esm --dts --tsconfig tsconfig.build.json",
46
+ "dev": "pnpm build --watch",
47
+ "pretest": "tsc --project tsconfig.test.json",
48
+ "test": "mocha"
49
+ }
50
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './swarm_routing';
@@ -0,0 +1,153 @@
1
+ import type { WithSwarmStr } from '@session-foundation/basic-types';
2
+ import { createHexString, fromHex } from '@session-foundation/basic-types';
3
+
4
+ export type WithSwarm = { swarm: Swarm };
5
+
6
+ export class Swarm {
7
+ private readonly hi: number; // upper 32 bits, unsigned
8
+ private readonly lo: number; // lower 32 bits, unsigned
9
+
10
+ private constructor(hi: number, lo: number) {
11
+ this.hi = hi >>> 0;
12
+ this.lo = lo >>> 0;
13
+ }
14
+
15
+ /** Build from 8 big-endian bytes (output of the XOR reduction step). */
16
+ static fromBytes(bytes: Uint8Array): Swarm {
17
+ const hi =
18
+ ((bytes[0]! << 24) | (bytes[1]! << 16) | (bytes[2]! << 8) | bytes[3]!) >>>
19
+ 0;
20
+ const lo =
21
+ ((bytes[4]! << 24) | (bytes[5]! << 16) | (bytes[6]! << 8) | bytes[7]!) >>>
22
+ 0;
23
+ return new Swarm(hi, lo);
24
+ }
25
+
26
+ /** Parse a hex string (with or without 0x prefix, any length up to 16 chars). */
27
+ static fromHex(hex: string): Swarm {
28
+ const clean =
29
+ hex.startsWith('0x') || hex.startsWith('0X') ? hex.slice(2) : hex;
30
+ const padded = clean.padStart(16, '0');
31
+ const hi = parseInt(padded.slice(0, 8), 16);
32
+ const lo = parseInt(padded.slice(8), 16);
33
+ return new Swarm(hi, lo);
34
+ }
35
+
36
+ // ── Comparisons ────────────────────────────────────────────────────────────
37
+
38
+ /** -1 if this < other, 0 if equal, 1 if this > other (unsigned 64-bit). */
39
+ compareTo(other: Swarm): number {
40
+ if (this.hi !== other.hi) return this.hi < other.hi ? -1 : 1;
41
+ if (this.lo !== other.lo) return this.lo < other.lo ? -1 : 1;
42
+ return 0;
43
+ }
44
+
45
+ isLessThan(other: Swarm): boolean {
46
+ return this.compareTo(other) < 0;
47
+ }
48
+
49
+ equals(other: Swarm): boolean {
50
+ return this.hi === other.hi && this.lo === other.lo;
51
+ }
52
+
53
+ // ── Arithmetic ─────────────────────────────────────────────────────────────
54
+
55
+ /** Unsigned 64-bit subtraction with wraparound (mod 2^64). */
56
+ subtract(other: Swarm): Swarm {
57
+ const lo = (this.lo - other.lo) >>> 0;
58
+ const borrow = this.lo < other.lo ? 1 : 0;
59
+ const hi = (this.hi - other.hi - borrow) >>> 0;
60
+ return new Swarm(hi, lo);
61
+ }
62
+
63
+ // ── Serialisation ──────────────────────────────────────────────────────────
64
+
65
+ /** 16-character zero-padded lowercase hex string. */
66
+ toString(): string {
67
+ return (
68
+ this.hi.toString(16).padStart(8, '0') +
69
+ this.lo.toString(16).padStart(8, '0')
70
+ );
71
+ }
72
+
73
+ toJSON(): string {
74
+ return this.toString();
75
+ }
76
+
77
+ // ── Routing ────────────────────────────────────────────────────────────────
78
+
79
+ /**
80
+ * XOR the four 8-byte chunks of a 32-byte x25519 pubkey, then interpret
81
+ * the result as a big-endian uint64. Equivalent of C++ pubkey_to_swarm_space.
82
+ */
83
+ static pubkeyToSwarmSpace(pk: Uint8Array): Swarm {
84
+ const xored = new Uint8Array(8);
85
+ for (let i = 0; i < 4; i++)
86
+ for (let j = 0; j < 8; j++) xored[j]! ^= pk[i * 8 + j]!;
87
+ return Swarm.fromBytes(xored);
88
+ }
89
+
90
+ /**
91
+ * Group nodes by swarm and return them sorted by swarm ID ascending.
92
+ * Equivalent of C++ generate_swarms.
93
+ */
94
+ static generateSwarms<T extends WithSwarmStr>(
95
+ nodes: T[],
96
+ ): Array<[Swarm, T[]]> {
97
+ const grouped = new Map<string, T[]>();
98
+ for (const node of nodes) {
99
+ const bucket = grouped.get(node.swarm) ?? [];
100
+ bucket.push(node);
101
+ grouped.set(node.swarm, bucket);
102
+ }
103
+
104
+ const result: Array<[Swarm, T[]]> = [];
105
+ for (const [swarmHex, members] of grouped)
106
+ result.push([Swarm.fromHex(swarmHex), members]);
107
+
108
+ result.sort((a, b) => a[0].compareTo(b[0]));
109
+ return result;
110
+ }
111
+
112
+ /**
113
+ * Find the swarm closest to the given x25519 pubkey using circular uint64
114
+ * distance. Equivalent of C++ get_swarm.
115
+ */
116
+ static getFor<T extends WithSwarmStr>(
117
+ swarmPubkeyHex: string,
118
+ allSwarms: Array<[Swarm, T[]]>,
119
+ ): [Swarm, T[]] {
120
+ if (allSwarms.length === 0) {
121
+ throw new Error('No swarms found: allSwarms.length === 0');
122
+ }
123
+
124
+ if (allSwarms.length === 1) {
125
+ return allSwarms[0]!;
126
+ }
127
+
128
+ const pkNoPrefix =
129
+ swarmPubkeyHex.length === 66 ? swarmPubkeyHex.slice(2) : swarmPubkeyHex;
130
+
131
+ const pk = fromHex(createHexString(pkNoPrefix));
132
+ const swarmId = Swarm.pubkeyToSwarmSpace(pk);
133
+
134
+ // lower_bound: first index where allSwarms[i][0] >= swarmId
135
+ let lo = 0,
136
+ hi = allSwarms.length;
137
+ while (lo < hi) {
138
+ const mid = (lo + hi) >> 1;
139
+ if (allSwarms[mid]![0].isLessThan(swarmId)) lo = mid + 1;
140
+ else hi = mid;
141
+ }
142
+
143
+ const rightIdx = lo === allSwarms.length ? 0 : lo;
144
+ const leftIdx = rightIdx === 0 ? allSwarms.length - 1 : rightIdx - 1;
145
+
146
+ const dright = allSwarms[rightIdx]![0].subtract(swarmId);
147
+ const dleft = swarmId.subtract(allSwarms[leftIdx]![0]);
148
+
149
+ return dright.isLessThan(dleft)
150
+ ? allSwarms[rightIdx]!
151
+ : allSwarms[leftIdx]!;
152
+ }
153
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"swarm_routing.test.d.ts","sourceRoot":"","sources":["swarm_routing.test.ts"],"names":[],"mappings":""}