1
- // src/PeerRanker.ts
2
-
3
1
import axios , { AxiosRequestConfig } from 'axios' ;
4
2
import fs from 'fs' ;
5
3
import https from 'https' ;
6
4
import { getOrCreateSSLCerts } from './ssl' ;
5
+ import { asyncPool } from './promiseUtils' ;
7
6
8
7
/**
9
8
* Interface representing the metrics of a peer.
@@ -14,23 +13,13 @@ export interface PeerMetrics {
14
13
bandwidth : number ; // in bytes per second (upload speed)
15
14
}
16
15
17
- /**
18
- * Configuration options for the PeerRanker.
19
- */
20
- interface PeerRankerOptions {
21
- pingPath ?: string ; // Optional: Path for latency ping (e.g., '/ping')
22
- timeout ?: number ; // Timeout for requests in milliseconds
23
- uploadTestSize ?: number ; // Size of the data to upload in bytes
24
- }
25
-
26
16
/**
27
17
* Utility class to rank peers based on latency and upload bandwidth using HTTPS with mTLS.
28
18
*/
29
19
export class PeerRanker {
30
20
private ipAddresses : string [ ] ;
31
21
private static certPath : string ;
32
22
private static keyPath : string ;
33
- private pingPath : string ;
34
23
private timeout : number ;
35
24
private uploadTestSize : number ;
36
25
@@ -41,14 +30,13 @@ export class PeerRanker {
41
30
/**
42
31
* Constructs a PeerRanker instance.
43
32
* @param ipAddresses - Array of IP addresses to rank.
44
- * @param options - Configuration options including paths to client certificates.
45
33
*/
46
- constructor ( ipAddresses : string [ ] , options : PeerRankerOptions ) {
34
+ constructor ( ipAddresses : string [ ] , timeout : number = 5000 , uploadTestSize : number = 1024 * 1024 ) {
47
35
this . ipAddresses = ipAddresses ;
48
- this . pingPath = options . pingPath || '/' ; // Default to root path if not provided
49
- this . timeout = options . timeout || 5000 ; // Default timeout: 5 seconds
50
- this . uploadTestSize = options . uploadTestSize || 1024 * 1024 ; // Default: 1MB
36
+ this . timeout = timeout ; // Allow customizable timeout
37
+ this . uploadTestSize = uploadTestSize ; // Default upload size: 1MB
51
38
39
+ // Fetch the SSL certificates used for mTLS.
52
40
const { certPath, keyPath } = getOrCreateSSLCerts ( ) ;
53
41
PeerRanker . certPath = certPath ;
54
42
PeerRanker . keyPath = keyPath ;
@@ -58,41 +46,38 @@ export class PeerRanker {
58
46
* Measures the latency of a given IP address using an HTTPS request.
59
47
* Tries HEAD first, then falls back to GET if HEAD is not supported.
60
48
* @param ip - The IP address of the peer.
61
- * @returns Promise resolving to the latency in milliseconds.
49
+ * @returns Promise resolving to the latency in milliseconds or rejecting if the peer fails .
62
50
*/
63
51
private async measureLatency ( ip : string ) : Promise < number > {
64
- const path = this . pingPath ;
65
- const url = `https://${ ip } ${ path } ` ;
52
+ const url = `https://${ ip } :4159/diagnostics/ping` ;
66
53
67
- // Configuration for HEAD request
68
54
const configHead : AxiosRequestConfig = {
69
55
url : url ,
70
56
method : 'HEAD' ,
71
57
httpsAgent : new https . Agent ( {
72
58
cert : fs . readFileSync ( PeerRanker . certPath ) ,
73
59
key : fs . readFileSync ( PeerRanker . keyPath ) ,
74
- rejectUnauthorized : false , // Set to true in production
60
+ rejectUnauthorized : false ,
75
61
} ) ,
76
62
timeout : this . timeout ,
77
- validateStatus : ( status ) => status < 500 , // Resolve only if status is less than 500
63
+ validateStatus : ( status ) => status < 500 ,
78
64
} ;
79
65
80
66
const startTime = Date . now ( ) ;
81
67
try {
82
68
const response = await axios ( configHead ) ;
83
- if ( response . status === 405 ) { // Method Not Allowed
84
- // Fallback to GET with Range header to minimize data transfer
69
+ if ( response . status === 405 ) {
85
70
const configGet : AxiosRequestConfig = {
86
71
url : url ,
87
72
method : 'GET' ,
88
73
httpsAgent : new https . Agent ( {
89
74
cert : fs . readFileSync ( PeerRanker . certPath ) ,
90
75
key : fs . readFileSync ( PeerRanker . keyPath ) ,
91
- rejectUnauthorized : false , // Set to true in production
76
+ rejectUnauthorized : false ,
92
77
} ) ,
93
78
timeout : this . timeout ,
94
79
headers : {
95
- 'Range' : 'bytes=0-0' , // Request only the first byte
80
+ 'Range' : 'bytes=0-0' ,
96
81
} ,
97
82
validateStatus : ( status ) => status < 500 ,
98
83
} ;
@@ -102,20 +87,18 @@ export class PeerRanker {
102
87
return latency ;
103
88
} catch ( error : any ) {
104
89
console . error ( `Latency measurement failed for IP ${ ip } :` , error . message ) ;
105
- return Infinity ; // Indicate unreachable or unresponsive peer
90
+ throw new Error ( `Latency measurement failed for IP ${ ip } ` ) ;
106
91
}
107
92
}
108
93
109
94
/**
110
95
* Measures the upload bandwidth of a given IP address by sending random data.
111
96
* @param ip - The IP address of the peer.
112
- * @returns Promise resolving to the upload bandwidth in bytes per second.
97
+ * @returns Promise resolving to the upload bandwidth in bytes per second or rejecting if the peer fails .
113
98
*/
114
99
private async measureBandwidth ( ip : string ) : Promise < number > {
115
- const url = `https://${ ip } /upload` ; // Assume /upload as the endpoint for upload testing
116
-
117
- // Generate random data
118
- const randomData = Buffer . alloc ( this . uploadTestSize , 'a' ) ; // 1MB of 'a's
100
+ const url = `https://${ ip } :4159/diagnostics/bandwidth` ;
101
+ const randomData = Buffer . alloc ( this . uploadTestSize , 'a' ) ;
119
102
120
103
const config : AxiosRequestConfig = {
121
104
url : url ,
@@ -128,44 +111,52 @@ export class PeerRanker {
128
111
httpsAgent : new https . Agent ( {
129
112
cert : fs . readFileSync ( PeerRanker . certPath ) ,
130
113
key : fs . readFileSync ( PeerRanker . keyPath ) ,
131
- rejectUnauthorized : false , // Set to true in production
114
+ rejectUnauthorized : false ,
132
115
} ) ,
133
116
timeout : this . timeout ,
134
117
maxContentLength : Infinity ,
135
118
maxBodyLength : Infinity ,
136
119
} ;
137
120
138
- return new Promise < number > ( ( resolve ) => {
139
- const startTime = Date . now ( ) ;
140
-
141
- axios ( config )
142
- . then ( ( ) => {
143
- const timeElapsed = ( Date . now ( ) - startTime ) / 1000 ; // seconds
144
- const bandwidth = this . uploadTestSize / timeElapsed ; // bytes per second
145
- resolve ( bandwidth ) ;
146
- } )
147
- . catch ( ( error : any ) => {
148
- console . error ( `Bandwidth measurement failed for IP ${ ip } :` , error . message ) ;
149
- resolve ( 0 ) ; // Indicate failure in measuring bandwidth
150
- } ) ;
151
- } ) ;
121
+ const startTime = Date . now ( ) ;
122
+
123
+ try {
124
+ await axios ( config ) ;
125
+ const timeElapsed = ( Date . now ( ) - startTime ) / 1000 ;
126
+ const bandwidth = this . uploadTestSize / timeElapsed ;
127
+ return bandwidth ;
128
+ } catch ( error : any ) {
129
+ console . error ( `Bandwidth measurement failed for IP ${ ip } :` , error . message ) ;
130
+ throw new Error ( `Bandwidth measurement failed for IP ${ ip } ` ) ;
131
+ }
152
132
}
153
133
154
134
/**
155
135
* Ranks the peers based on measured latency and upload bandwidth.
136
+ * Unresponsive peers are excluded from the final ranking.
137
+ * @param cooldown - Cooldown time in milliseconds between batches.
156
138
* @returns Promise resolving to an array of PeerMetrics sorted by latency and bandwidth.
157
139
*/
158
- public async rankPeers ( ) : Promise < PeerMetrics [ ] > {
159
- const metricsPromises = this . ipAddresses . map ( async ( ip ) => {
160
- const [ latency , bandwidth ] = await Promise . all ( [
161
- this . measureLatency ( ip ) ,
162
- this . measureBandwidth ( ip ) ,
163
- ] ) ;
164
-
165
- return { ip, latency, bandwidth } ;
166
- } ) ;
140
+ public async rankPeers ( cooldown : number = 500 ) : Promise < PeerMetrics [ ] > {
141
+ const limit = 5 ; // Limit to 5 parallel requests at a time
142
+
143
+ const iteratorFn = async ( ip : string ) : Promise < PeerMetrics | null > => {
144
+ try {
145
+ const [ latency , bandwidth ] = await Promise . all ( [
146
+ this . measureLatency ( ip ) ,
147
+ this . measureBandwidth ( ip ) ,
148
+ ] ) ;
149
+ return { ip, latency, bandwidth } ;
150
+ } catch ( error ) {
151
+ // Peer failed, skip it by returning null
152
+ return null ;
153
+ }
154
+ } ;
167
155
168
- const peerMetrics : PeerMetrics [ ] = await Promise . all ( metricsPromises ) ;
156
+ // Process all peers with a concurrency limit and cooldown between batches
157
+ const peerMetrics : PeerMetrics [ ] = (
158
+ await asyncPool ( limit , this . ipAddresses , iteratorFn , cooldown )
159
+ ) . filter ( ( metrics : any ) : metrics is PeerMetrics => metrics !== null ) ; // Use a type guard
169
160
170
161
// Sort by lowest latency first, then by highest bandwidth
171
162
peerMetrics . sort ( ( a , b ) => {
@@ -175,9 +166,7 @@ export class PeerRanker {
175
166
return a . latency - b . latency ; // Lower latency is better
176
167
} ) ;
177
168
178
- // Update the internal sorted list
179
169
this . sortedPeers = peerMetrics ;
180
- // Reset the iterator index
181
170
this . currentIndex = 0 ;
182
171
183
172
return peerMetrics ;
0 commit comments