BREAKING CHANGE(smartnetwork): Enhance documentation and add configurable speed test options with plugin architecture improvements

This commit is contained in:
2025-04-28 19:27:13 +00:00
parent d6be2e27b0
commit 26e1d5142a
10 changed files with 663 additions and 175 deletions

View File

@@ -3,8 +3,15 @@ import { getLogger } from './logging.js';
import { NetworkError, TimeoutError } from './errors.js';
import * as stats from './helpers/stats.js';
export interface SpeedOptions {
parallelStreams?: number;
duration?: number;
}
export class CloudflareSpeed {
constructor() {}
private opts: SpeedOptions;
constructor(opts?: SpeedOptions) {
this.opts = opts || {};
}
public async speedTest() {
const latency = await this.measureLatency();
@@ -12,20 +19,71 @@ export class CloudflareSpeed {
const serverLocations = await this.fetchServerLocations();
const cgiData = await this.fetchCfCdnCgiTrace();
// lets test the download speed
const testDown1 = await this.measureDownload(101000, 10);
const testDown2 = await this.measureDownload(1001000, 8);
const testDown3 = await this.measureDownload(10001000, 6);
const testDown4 = await this.measureDownload(25001000, 4);
const testDown5 = await this.measureDownload(100001000, 1);
const downloadTests = [...testDown1, ...testDown2, ...testDown3, ...testDown4, ...testDown5];
// speed tests: either fixed segments or duration-based mode
const parallel = this.opts.parallelStreams ?? 1;
const measureDownloadParallel = (bytes: number, iterations: number) => {
if (parallel <= 1) {
return this.measureDownload(bytes, iterations);
}
return Promise.all(
Array(parallel)
.fill(null)
.map(() => this.measureDownload(bytes, iterations)),
).then((arrays) => arrays.flat());
};
let downloadTests: number[];
if (this.opts.duration && this.opts.duration > 0) {
// duration-based download: run for specified seconds
downloadTests = [];
const durMs = this.opts.duration * 1000;
const startMs = Date.now();
// use medium chunk size for download
const chunkBytes = 25001000;
while (Date.now() - startMs < durMs) {
const speeds = await measureDownloadParallel(chunkBytes, 1);
downloadTests.push(...speeds);
}
if (downloadTests.length === 0) downloadTests = [0];
} else {
// fixed download segments
const t1 = await measureDownloadParallel(101000, 10);
const t2 = await measureDownloadParallel(1001000, 8);
const t3 = await measureDownloadParallel(10001000, 6);
const t4 = await measureDownloadParallel(25001000, 4);
const t5 = await measureDownloadParallel(100001000, 1);
downloadTests = [...t1, ...t2, ...t3, ...t4, ...t5];
}
const speedDownload = stats.quartile(downloadTests, 0.9).toFixed(2);
// lets test the upload speed
const testUp1 = await this.measureUpload(11000, 10);
const testUp2 = await this.measureUpload(101000, 10);
const testUp3 = await this.measureUpload(1001000, 8);
const uploadTests = [...testUp1, ...testUp2, ...testUp3];
// lets test the upload speed with configurable parallel streams
const measureUploadParallel = (bytes: number, iterations: number) => {
if (parallel <= 1) {
return this.measureUpload(bytes, iterations);
}
return Promise.all(
Array(parallel)
.fill(null)
.map(() => this.measureUpload(bytes, iterations)),
).then((arrays) => arrays.flat());
};
let uploadTests: number[];
if (this.opts.duration && this.opts.duration > 0) {
// duration-based upload: run for specified seconds
uploadTests = [];
const durMsUp = this.opts.duration * 1000;
const startMsUp = Date.now();
const chunkBytesUp = 1001000;
while (Date.now() - startMsUp < durMsUp) {
const speeds = await measureUploadParallel(chunkBytesUp, 1);
uploadTests.push(...speeds);
}
if (uploadTests.length === 0) uploadTests = [0];
} else {
const u1 = await measureUploadParallel(11000, 10);
const u2 = await measureUploadParallel(101000, 10);
const u3 = await measureUploadParallel(1001000, 8);
uploadTests = [...u1, ...u2, ...u3];
}
const speedUpload = stats.quartile(uploadTests, 0.9).toFixed(2);
return {
@@ -218,8 +276,8 @@ export class CloudflareSpeed {
});
});
req.on('error', (error) => {
reject(error);
req.on('error', (error: Error & { code?: string }) => {
reject(new NetworkError(error.message, error.code));
});
req.write(data);
@@ -227,21 +285,10 @@ export class CloudflareSpeed {
});
}
public async fetchCfCdnCgiTrace(): Promise<{
fl: string;
h: string;
ip: string;
ts: string;
visit_scheme: string;
uag: string;
colo: string;
http: string;
loc: string;
tls: string;
sni: string;
warp: string;
gateway: string;
}> {
/**
* Fetch Cloudflare's trace endpoint and parse key=value lines to a record.
*/
public async fetchCfCdnCgiTrace(): Promise<Record<string, string>> {
const parseCfCdnCgiTrace = (text: string) =>
text
.split('\n')