@mu1147-legend/cf-speed-test 1.0.2
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/README.md +137 -0
- package/index.js +98 -0
- package/package.json +27 -0
- package/utils.js +287 -0
package/README.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# ๐ cf-speed-test
|
|
2
|
+
|
|
3
|
+
A fast, lightweight CLI tool to test your internet speed directly from the terminal.
|
|
4
|
+
|
|
5
|
+
Includes:
|
|
6
|
+
|
|
7
|
+
* โก Download speed
|
|
8
|
+
* โฌ Upload speed
|
|
9
|
+
* ๐ก Latency (Ping)
|
|
10
|
+
* ๐ Jitter
|
|
11
|
+
* ๐ Multiple test rounds
|
|
12
|
+
* ๐ฏ Parallel connections
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## ๐ฆ Installation
|
|
17
|
+
|
|
18
|
+
### Run instantly (no install)
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npx cf-speed-test
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
### Install globally
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install -g cf-speed-test
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Then run:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
netspeed
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## โ๏ธ Usage
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
netspeed
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## ๐ ๏ธ Options
|
|
49
|
+
|
|
50
|
+
You can customize the test using CLI flags:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
netspeed --connections=4 --download=50 --upload=20 --rounds=2
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Available Options
|
|
57
|
+
|
|
58
|
+
| Flag | Description | Default |
|
|
59
|
+
| --------------- | --------------------------------- | ------- |
|
|
60
|
+
| `--connections` | Number of parallel connections | `4` |
|
|
61
|
+
| `--download` | Download size per connection (MB) | `50` |
|
|
62
|
+
| `--upload` | Upload size per connection (MB) | `20` |
|
|
63
|
+
| `--rounds` | Number of test rounds | `2` |
|
|
64
|
+
| `--json` | Output result as JSON | `false` |
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## ๐ Example Output
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
72
|
+
๐ Legendary Speed Test
|
|
73
|
+
|
|
74
|
+
Round 1
|
|
75
|
+
|
|
76
|
+
โฌ Testing download...
|
|
77
|
+
[โโโโโโโโโโโโโโโโโโโโ] 100% 240 Mbps
|
|
78
|
+
โ Download: 238 Mbps
|
|
79
|
+
|
|
80
|
+
โฌ Testing upload...
|
|
81
|
+
[โโโโโโโโโโโโโโโโโโโโ] 100% 230 Mbps
|
|
82
|
+
โ Upload: 225 Mbps
|
|
83
|
+
|
|
84
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
85
|
+
Connections: 4
|
|
86
|
+
Latency: 22 ms
|
|
87
|
+
Jitter: 3 ms
|
|
88
|
+
|
|
89
|
+
โฌ Download: 235 Mbps
|
|
90
|
+
โฌ Upload: 220 Mbps
|
|
91
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## ๐ How it works
|
|
97
|
+
|
|
98
|
+
* Uses parallel HTTP requests to measure bandwidth
|
|
99
|
+
* Streams data in chunks for real-time speed calculation
|
|
100
|
+
* Measures latency using lightweight HEAD requests
|
|
101
|
+
* Calculates jitter based on multiple ping attempts
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## โ ๏ธ Notes
|
|
106
|
+
|
|
107
|
+
* Results may vary depending on your ISP and routing
|
|
108
|
+
* CDN-based testing (Cloudflare) may show higher peak speeds
|
|
109
|
+
* For best results:
|
|
110
|
+
|
|
111
|
+
* Use stable connection
|
|
112
|
+
* Avoid background downloads
|
|
113
|
+
* Run multiple rounds
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## ๐งช Advanced Usage
|
|
118
|
+
|
|
119
|
+
### JSON Output (for scripting)
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
netspeed --json
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
### High Accuracy Mode
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
netspeed --connections=6 --download=100 --rounds=3
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## ๐ License
|
|
136
|
+
|
|
137
|
+
MIT License
|
package/index.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { downloadTest, measureLatency, uploadTest } from "./utils.js";
|
|
5
|
+
|
|
6
|
+
const controller = new AbortController();
|
|
7
|
+
|
|
8
|
+
process.on("SIGINT", () => {
|
|
9
|
+
console.log("\nโ ๏ธ Test aborted");
|
|
10
|
+
controller.abort();
|
|
11
|
+
process.exit(1);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// args
|
|
15
|
+
const args = process.argv.slice(2);
|
|
16
|
+
|
|
17
|
+
const getArg = (name, def) => {
|
|
18
|
+
const found = args.find((a) => a.startsWith(name));
|
|
19
|
+
return found ? parseInt(found.split("=")[1]) : def;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const isJSON = args.includes("--json");
|
|
23
|
+
|
|
24
|
+
const CONNECTIONS = getArg("--connections", 2);
|
|
25
|
+
const DOWNLOAD_MB = getArg("--download", 20);
|
|
26
|
+
const UPLOAD_MB = getArg("--upload", 20);
|
|
27
|
+
const ROUNDS = getArg("--rounds", 2);
|
|
28
|
+
|
|
29
|
+
const DOWNLOAD_URL = `https://speed.cloudflare.com/__down?bytes=${DOWNLOAD_MB * 1024 * 1024}`;
|
|
30
|
+
const UPLOAD_URL = `https://speed.cloudflare.com/__up`;
|
|
31
|
+
|
|
32
|
+
async function run() {
|
|
33
|
+
console.log(chalk.gray("โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ"));
|
|
34
|
+
console.log(chalk.green("๐ Legendary Speed Test\n"));
|
|
35
|
+
|
|
36
|
+
// latency
|
|
37
|
+
const { latency, jitter } = await measureLatency(DOWNLOAD_URL);
|
|
38
|
+
|
|
39
|
+
let dlArr = [];
|
|
40
|
+
let ulArr = [];
|
|
41
|
+
|
|
42
|
+
for (let i = 0; i < ROUNDS; i++) {
|
|
43
|
+
console.log(chalk.gray(`\nRound ${i + 1}\n`));
|
|
44
|
+
|
|
45
|
+
// ๐ฅ download with glowing animation
|
|
46
|
+
const dl = await downloadTest(
|
|
47
|
+
DOWNLOAD_URL,
|
|
48
|
+
CONNECTIONS,
|
|
49
|
+
controller.signal,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
console.log(chalk.blue(`โ Download: ${dl.toFixed(2)} Mbps`));
|
|
53
|
+
|
|
54
|
+
// ๐ฅ upload with glowing animation
|
|
55
|
+
const ul = await uploadTest(
|
|
56
|
+
UPLOAD_URL,
|
|
57
|
+
UPLOAD_MB,
|
|
58
|
+
CONNECTIONS,
|
|
59
|
+
controller.signal,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
console.log(chalk.magenta(`โ Upload: ${ul.toFixed(2)} Mbps`));
|
|
63
|
+
|
|
64
|
+
dlArr.push(dl);
|
|
65
|
+
ulArr.push(ul);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const avg = (arr) => arr.reduce((a, b) => a + b, 0) / arr.length;
|
|
69
|
+
|
|
70
|
+
const result = {
|
|
71
|
+
download: avg(dlArr),
|
|
72
|
+
upload: avg(ulArr),
|
|
73
|
+
latency,
|
|
74
|
+
jitter,
|
|
75
|
+
connections: CONNECTIONS,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
if (isJSON) {
|
|
79
|
+
console.log(JSON.stringify(result, null, 2));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// final output
|
|
84
|
+
console.log(chalk.gray("\nโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ"));
|
|
85
|
+
console.log(chalk.cyan(`Connections: ${CONNECTIONS}`));
|
|
86
|
+
console.log(chalk.yellow(`Latency: ${latency.toFixed(2)} ms`));
|
|
87
|
+
console.log(chalk.yellow(`Jitter: ${jitter.toFixed(2)} ms\n`));
|
|
88
|
+
|
|
89
|
+
console.log(
|
|
90
|
+
chalk.blueBright(`โฌ Download: ${result.download.toFixed(2)} Mbps`),
|
|
91
|
+
);
|
|
92
|
+
console.log(
|
|
93
|
+
chalk.magentaBright(`โฌ Upload: ${result.upload.toFixed(2)} Mbps`),
|
|
94
|
+
);
|
|
95
|
+
console.log(chalk.gray("โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ\n"));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
run();
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mu1147-legend/cf-speed-test",
|
|
3
|
+
"author": "Muhammad Ullah",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"version": "1.0.2",
|
|
6
|
+
"description": "CLI Speed Test Tool (Download, Upload, Ping, Jitter)",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"speedtest",
|
|
9
|
+
"cli",
|
|
10
|
+
"internet",
|
|
11
|
+
"network",
|
|
12
|
+
"download",
|
|
13
|
+
"upload",
|
|
14
|
+
"ping",
|
|
15
|
+
"jitter",
|
|
16
|
+
"netspeed"
|
|
17
|
+
],
|
|
18
|
+
"type": "module",
|
|
19
|
+
"bin": {
|
|
20
|
+
"netspeed": "index.js"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"chalk": "^5.3.0",
|
|
24
|
+
"gradient-string": "^3.0.0",
|
|
25
|
+
"ora": "^7.0.1"
|
|
26
|
+
}
|
|
27
|
+
}
|
package/utils.js
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import gradient from "gradient-string";
|
|
2
|
+
import { performance } from "node:perf_hooks";
|
|
3
|
+
|
|
4
|
+
// ---------------- SAFE FETCH ----------------
|
|
5
|
+
async function safeFetch(url, options = {}, timeout = 8000, retries = 2) {
|
|
6
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
7
|
+
const controller = new AbortController();
|
|
8
|
+
const id = setTimeout(() => controller.abort(), timeout);
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const res = await fetch(url, {
|
|
12
|
+
...options,
|
|
13
|
+
signal: controller.signal,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
clearTimeout(id);
|
|
17
|
+
return res;
|
|
18
|
+
} catch (err) {
|
|
19
|
+
clearTimeout(id);
|
|
20
|
+
|
|
21
|
+
if (attempt === retries) {
|
|
22
|
+
throw err;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// small delay before retry
|
|
26
|
+
await new Promise((r) => setTimeout(r, 800));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Simplified fetch for POST (no retry to avoid stream reuse issues)
|
|
32
|
+
async function postFetch(url, options = {}) {
|
|
33
|
+
return fetch(url, options);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ---------------- LATENCY ----------------
|
|
37
|
+
export async function measureLatency(url, attempts = 5) {
|
|
38
|
+
const times = [];
|
|
39
|
+
|
|
40
|
+
for (let i = 0; i < attempts; i++) {
|
|
41
|
+
const start = performance.now();
|
|
42
|
+
await safeFetch(url, { method: "HEAD", cache: "no-store" });
|
|
43
|
+
const end = performance.now();
|
|
44
|
+
|
|
45
|
+
times.push(end - start);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const avg = times.reduce((a, b) => a + b, 0) / times.length;
|
|
49
|
+
|
|
50
|
+
const jitter =
|
|
51
|
+
times.reduce((a, b) => a + Math.abs(b - avg), 0) / times.length;
|
|
52
|
+
|
|
53
|
+
return { latency: avg, jitter };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ---------------- PROGRESS ----------------
|
|
57
|
+
function renderProgress(percent, speed) {
|
|
58
|
+
const width = 20;
|
|
59
|
+
const safePercent = Math.max(0, Math.min(100, percent));
|
|
60
|
+
|
|
61
|
+
const filled = Math.round((safePercent / 100) * width);
|
|
62
|
+
const bar = "โ".repeat(filled) + "โ".repeat(width - filled);
|
|
63
|
+
|
|
64
|
+
process.stdout.write(
|
|
65
|
+
`\r[${bar}] ${safePercent.toFixed(0)}% ${speed.toFixed(2)} Mbps`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ---------------- DOWNLOAD (FIXED) ----------------
|
|
70
|
+
export async function downloadTest(url, connections, signal) {
|
|
71
|
+
const sizeMatch = url.match(/bytes=(\d+)/);
|
|
72
|
+
const fileSize = sizeMatch ? parseInt(sizeMatch[1]) : 0;
|
|
73
|
+
|
|
74
|
+
const totalBytes = fileSize * connections;
|
|
75
|
+
|
|
76
|
+
const progressArr = new Array(connections).fill(0);
|
|
77
|
+
let frameCount = 0;
|
|
78
|
+
|
|
79
|
+
const start = performance.now();
|
|
80
|
+
|
|
81
|
+
function renderDownloadProgress(percent, speed) {
|
|
82
|
+
const width = 20;
|
|
83
|
+
const safePercent = Math.max(0, Math.min(100, percent));
|
|
84
|
+
const filled = Math.round((safePercent / 100) * width);
|
|
85
|
+
const bar = "โ".repeat(filled) + "โ".repeat(width - filled);
|
|
86
|
+
|
|
87
|
+
// Stone gradient glowing animation - every frame
|
|
88
|
+
const clamp = (v) => Math.max(0, Math.min(1, v));
|
|
89
|
+
const shift = (frameCount % 20) / 20;
|
|
90
|
+
|
|
91
|
+
const animatedText = gradient([
|
|
92
|
+
{ color: "#555555", pos: 0 },
|
|
93
|
+
{ color: "#aaaaaa", pos: clamp(shift) },
|
|
94
|
+
{ color: "#ffffff", pos: clamp(shift + 0.1) },
|
|
95
|
+
{ color: "#aaaaaa", pos: clamp(shift + 0.2) },
|
|
96
|
+
{ color: "#555555", pos: 1 },
|
|
97
|
+
])(`โฌ Testing download...`);
|
|
98
|
+
|
|
99
|
+
process.stdout.write(
|
|
100
|
+
`\r${animatedText} [${bar}] ${speed.toFixed(2)} Mbps`,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
frameCount++;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const streams = await Promise.all(
|
|
107
|
+
Array.from({ length: connections }, async (_, i) => {
|
|
108
|
+
// ๐ฅ random delay per connection
|
|
109
|
+
const randomDelay = Math.random() * 2000;
|
|
110
|
+
await new Promise((r) => setTimeout(r, randomDelay));
|
|
111
|
+
|
|
112
|
+
const res = await safeFetch(url, {
|
|
113
|
+
method: "GET", // โ
MUST
|
|
114
|
+
cache: "no-store",
|
|
115
|
+
signal,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
if (!res.body) return null;
|
|
119
|
+
|
|
120
|
+
return { stream: res.body, index: i };
|
|
121
|
+
}),
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
await Promise.all(
|
|
125
|
+
streams.map(async (item) => {
|
|
126
|
+
if (!item) return;
|
|
127
|
+
|
|
128
|
+
const { stream, index } = item;
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
for await (const chunk of stream) {
|
|
132
|
+
if (signal?.aborted) return;
|
|
133
|
+
|
|
134
|
+
progressArr[index] += chunk.length;
|
|
135
|
+
|
|
136
|
+
const receivedBytes = progressArr.reduce(
|
|
137
|
+
(a, b) => a + b,
|
|
138
|
+
0,
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
const elapsed = (performance.now() - start) / 1000;
|
|
142
|
+
if (elapsed <= 0) continue;
|
|
143
|
+
|
|
144
|
+
const speed = (receivedBytes * 8) / (elapsed * 1024 * 1024);
|
|
145
|
+
|
|
146
|
+
const percent = totalBytes
|
|
147
|
+
? (receivedBytes / totalBytes) * 100
|
|
148
|
+
: 0;
|
|
149
|
+
|
|
150
|
+
renderDownloadProgress(percent, speed);
|
|
151
|
+
}
|
|
152
|
+
} catch (err) {
|
|
153
|
+
if (err.name !== "AbortError") {
|
|
154
|
+
console.error("\nDownload error:", err.message);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}),
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
process.stdout.write("\n");
|
|
161
|
+
|
|
162
|
+
const duration = (performance.now() - start) / 1000;
|
|
163
|
+
const finalBytes = progressArr.reduce((a, b) => a + b, 0);
|
|
164
|
+
|
|
165
|
+
if (duration <= 0) return 0;
|
|
166
|
+
|
|
167
|
+
return (finalBytes * 8) / (duration * 1024 * 1024);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ---------------- UPLOAD ----------------
|
|
171
|
+
export async function uploadTest(url, sizeMB, connections, signal) {
|
|
172
|
+
const totalBytes = sizeMB * 1024 * 1024 * connections;
|
|
173
|
+
|
|
174
|
+
let uploadedBytes = 0;
|
|
175
|
+
let frameCount = 0;
|
|
176
|
+
|
|
177
|
+
const start = performance.now();
|
|
178
|
+
|
|
179
|
+
function renderProgress(percent, speed) {
|
|
180
|
+
const width = 20;
|
|
181
|
+
const filled = Math.round((percent / 100) * width);
|
|
182
|
+
const bar = "โ".repeat(filled) + "โ".repeat(width - filled);
|
|
183
|
+
|
|
184
|
+
// Stone gradient glowing animation - every frame
|
|
185
|
+
const clamp = (v) => Math.max(0, Math.min(1, v));
|
|
186
|
+
const shift = (frameCount % 20) / 20;
|
|
187
|
+
|
|
188
|
+
const animatedText = gradient([
|
|
189
|
+
{ color: "#555555", pos: 0 },
|
|
190
|
+
{ color: "#aaaaaa", pos: clamp(shift) },
|
|
191
|
+
{ color: "#ffffff", pos: clamp(shift + 0.1) },
|
|
192
|
+
{ color: "#aaaaaa", pos: clamp(shift + 0.2) },
|
|
193
|
+
{ color: "#555555", pos: 1 },
|
|
194
|
+
])(`โฌ Testing upload...`);
|
|
195
|
+
|
|
196
|
+
process.stdout.write(
|
|
197
|
+
`\r${animatedText} [${bar}] ${speed.toFixed(2)} Mbps`,
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
frameCount++;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const uploadOne = async () => {
|
|
204
|
+
const chunkSize = 256 * 1024; // 256KB
|
|
205
|
+
const totalChunks = (sizeMB * 1024 * 1024) / chunkSize;
|
|
206
|
+
|
|
207
|
+
const stream = new ReadableStream({
|
|
208
|
+
async pull(controller) {
|
|
209
|
+
if (signal?.aborted) {
|
|
210
|
+
controller.close();
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (uploadedBytes >= totalBytes) {
|
|
215
|
+
controller.close();
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const chunk = new Uint8Array(chunkSize);
|
|
220
|
+
controller.enqueue(chunk);
|
|
221
|
+
|
|
222
|
+
uploadedBytes += chunk.length;
|
|
223
|
+
|
|
224
|
+
const elapsed = (performance.now() - start) / 1000;
|
|
225
|
+
if (elapsed <= 0) return;
|
|
226
|
+
|
|
227
|
+
const speed = (uploadedBytes * 8) / (elapsed * 1024 * 1024);
|
|
228
|
+
|
|
229
|
+
const percent = (uploadedBytes / totalBytes) * 100;
|
|
230
|
+
|
|
231
|
+
renderProgress(percent, speed);
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
await postFetch(url, {
|
|
236
|
+
method: "POST",
|
|
237
|
+
body: stream,
|
|
238
|
+
duplex: "half",
|
|
239
|
+
signal,
|
|
240
|
+
});
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
// parallel uploads
|
|
244
|
+
await Promise.all(Array.from({ length: connections }, () => uploadOne()));
|
|
245
|
+
|
|
246
|
+
process.stdout.write("\n");
|
|
247
|
+
|
|
248
|
+
const duration = (performance.now() - start) / 1000;
|
|
249
|
+
|
|
250
|
+
if (duration <= 0) return 0;
|
|
251
|
+
|
|
252
|
+
return (uploadedBytes * 8) / (duration * 1024 * 1024);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ---------------- ANIMATION ----------------
|
|
256
|
+
export function animateLoop(text) {
|
|
257
|
+
let i = 0;
|
|
258
|
+
let running = true;
|
|
259
|
+
|
|
260
|
+
const clamp = (v) => Math.max(0, Math.min(1, v));
|
|
261
|
+
|
|
262
|
+
const loop = () => {
|
|
263
|
+
if (!running) return;
|
|
264
|
+
|
|
265
|
+
const shift = (i % 20) / 20;
|
|
266
|
+
|
|
267
|
+
const colored = gradient([
|
|
268
|
+
{ color: "#555555", pos: 0 },
|
|
269
|
+
{ color: "#aaaaaa", pos: clamp(shift) },
|
|
270
|
+
{ color: "#ffffff", pos: clamp(shift + 0.1) },
|
|
271
|
+
{ color: "#aaaaaa", pos: clamp(shift + 0.2) },
|
|
272
|
+
{ color: "#555555", pos: 1 },
|
|
273
|
+
])(text);
|
|
274
|
+
|
|
275
|
+
process.stdout.write("\r" + colored);
|
|
276
|
+
|
|
277
|
+
i++;
|
|
278
|
+
setTimeout(loop, 50);
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
loop();
|
|
282
|
+
|
|
283
|
+
return () => {
|
|
284
|
+
running = false;
|
|
285
|
+
process.stdout.write("\r");
|
|
286
|
+
};
|
|
287
|
+
}
|