@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.
- package/.dependency-cruiser.cjs +6 -0
- package/.turbo/turbo-build.log +20 -0
- package/.turbo/turbo-lint.log +8 -0
- package/dist/index.cjs +139 -0
- package/dist/index.d.mts +40 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.mjs +112 -0
- package/eslint.config.mjs +4 -0
- package/package.json +50 -0
- package/src/index.ts +1 -0
- package/src/swarm_routing.ts +153 -0
- package/tests/swarm_routing.test.d.ts.map +1 -0
- package/tests/swarm_routing.test.ts +362 -0
- package/tsconfig.build.json +7 -0
- package/tsconfig.json +30 -0
- package/tsconfig.test.json +13 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/tsup.config.ts +10 -0
|
@@ -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
|
+
[34mCLI[39m Building entry: src/index.ts
|
|
7
|
+
[34mCLI[39m Using tsconfig: tsconfig.build.json
|
|
8
|
+
[34mCLI[39m tsup v8.5.1
|
|
9
|
+
[34mCLI[39m Using tsup config: /home/audric/pro/contribs/session-pkgs/packages/swarm-routing/tsup.config.ts
|
|
10
|
+
[34mCLI[39m Target: esnext
|
|
11
|
+
[34mCJS[39m Build start
|
|
12
|
+
[34mESM[39m Build start
|
|
13
|
+
[32mESM[39m [1mdist/index.mjs [22m[32m4.05 KB[39m
|
|
14
|
+
[32mESM[39m ⚡️ Build success in 47ms
|
|
15
|
+
[32mCJS[39m [1mdist/index.cjs [22m[32m5.08 KB[39m
|
|
16
|
+
[32mCJS[39m ⚡️ 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
|
+
[32m✔[39m 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
|
+
});
|
package/dist/index.d.mts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
};
|
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":""}
|