@capgo/capacitor-network-diagnostics 8.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/CapgoCapacitorNetworkDiagnostics.podspec +17 -0
- package/LICENSE +373 -0
- package/Package.swift +28 -0
- package/README.md +467 -0
- package/android/build.gradle +59 -0
- package/android/src/main/AndroidManifest.xml +4 -0
- package/android/src/main/java/app/capgo/networkdiagnostics/NetworkDiagnostics.java +681 -0
- package/android/src/main/java/app/capgo/networkdiagnostics/NetworkDiagnosticsPlugin.java +141 -0
- package/android/src/main/res/.gitkeep +0 -0
- package/dist/docs.json +961 -0
- package/dist/esm/definitions.d.ts +276 -0
- package/dist/esm/definitions.js +2 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +24 -0
- package/dist/esm/web.js +388 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +402 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +405 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Sources/NetworkDiagnosticsPlugin/NetworkDiagnostics+Download.swift +71 -0
- package/ios/Sources/NetworkDiagnosticsPlugin/NetworkDiagnostics+PacketLoss.swift +91 -0
- package/ios/Sources/NetworkDiagnosticsPlugin/NetworkDiagnostics+Run.swift +163 -0
- package/ios/Sources/NetworkDiagnosticsPlugin/NetworkDiagnostics+Utils.swift +202 -0
- package/ios/Sources/NetworkDiagnosticsPlugin/NetworkDiagnostics.swift +151 -0
- package/ios/Sources/NetworkDiagnosticsPlugin/NetworkDiagnosticsPlugin.swift +139 -0
- package/ios/Tests/NetworkDiagnosticsPluginTests/NetworkDiagnosticsTests.swift +11 -0
- package/package.json +92 -0
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
var capacitorNetworkDiagnostics = (function (exports, core) {
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const NetworkDiagnostics = core.registerPlugin('NetworkDiagnostics', {
|
|
5
|
+
web: () => Promise.resolve().then(function () { return web; }).then((m) => new m.NetworkDiagnosticsWeb()),
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
class NetworkDiagnosticsWeb extends core.WebPlugin {
|
|
9
|
+
async getNetworkStatus() {
|
|
10
|
+
var _a, _b, _c;
|
|
11
|
+
const nav = navigator;
|
|
12
|
+
const connection = (_b = (_a = nav.connection) !== null && _a !== void 0 ? _a : nav.mozConnection) !== null && _b !== void 0 ? _b : nav.webkitConnection;
|
|
13
|
+
const connectionType = this.mapConnectionType((_c = connection === null || connection === void 0 ? void 0 : connection.type) !== null && _c !== void 0 ? _c : connection === null || connection === void 0 ? void 0 : connection.effectiveType);
|
|
14
|
+
const connected = navigator.onLine;
|
|
15
|
+
const details = {};
|
|
16
|
+
if (connection === null || connection === void 0 ? void 0 : connection.effectiveType) {
|
|
17
|
+
details.effectiveType = connection.effectiveType;
|
|
18
|
+
}
|
|
19
|
+
if (typeof (connection === null || connection === void 0 ? void 0 : connection.downlink) === 'number') {
|
|
20
|
+
details.downlinkMbps = connection.downlink;
|
|
21
|
+
}
|
|
22
|
+
if (typeof (connection === null || connection === void 0 ? void 0 : connection.rtt) === 'number') {
|
|
23
|
+
details.rttMs = connection.rtt;
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
connected,
|
|
27
|
+
connectionType,
|
|
28
|
+
constrained: connection === null || connection === void 0 ? void 0 : connection.saveData,
|
|
29
|
+
details,
|
|
30
|
+
expensive: false,
|
|
31
|
+
internetReachable: connected,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
async testUrl(options) {
|
|
35
|
+
const started = performance.now();
|
|
36
|
+
const method = this.normalizeMethod(options.method);
|
|
37
|
+
const controller = new AbortController();
|
|
38
|
+
const timeout = window.setTimeout(() => controller.abort(), this.timeout(options.timeoutMs, 10000));
|
|
39
|
+
try {
|
|
40
|
+
const response = await fetch(options.url, {
|
|
41
|
+
method,
|
|
42
|
+
redirect: options.followRedirects === false ? 'manual' : 'follow',
|
|
43
|
+
signal: controller.signal,
|
|
44
|
+
});
|
|
45
|
+
return {
|
|
46
|
+
durationMs: this.elapsed(started),
|
|
47
|
+
finalUrl: response.url || options.url,
|
|
48
|
+
method,
|
|
49
|
+
ok: response.status >= 200 && response.status < 400,
|
|
50
|
+
reachable: true,
|
|
51
|
+
statusCode: response.status,
|
|
52
|
+
url: options.url,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
return {
|
|
57
|
+
durationMs: this.elapsed(started),
|
|
58
|
+
errorCode: this.errorCode(error),
|
|
59
|
+
errorMessage: this.errorMessage(error),
|
|
60
|
+
method,
|
|
61
|
+
ok: false,
|
|
62
|
+
reachable: false,
|
|
63
|
+
url: options.url,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
window.clearTimeout(timeout);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async testPort(options) {
|
|
71
|
+
return {
|
|
72
|
+
durationMs: 0,
|
|
73
|
+
errorCode: 'UNSUPPORTED_WEB',
|
|
74
|
+
errorMessage: 'Browsers cannot open raw TCP sockets. Use iOS or Android for native port diagnostics.',
|
|
75
|
+
host: options.host,
|
|
76
|
+
open: false,
|
|
77
|
+
port: options.port,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
async testWebSocket(options) {
|
|
81
|
+
const started = performance.now();
|
|
82
|
+
return new Promise((resolve) => {
|
|
83
|
+
let settled = false;
|
|
84
|
+
let socket;
|
|
85
|
+
const timeout = window.setTimeout(() => {
|
|
86
|
+
if (settled) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
settled = true;
|
|
90
|
+
socket === null || socket === void 0 ? void 0 : socket.close();
|
|
91
|
+
resolve({
|
|
92
|
+
durationMs: this.elapsed(started),
|
|
93
|
+
errorCode: 'TIMEOUT',
|
|
94
|
+
errorMessage: 'WebSocket handshake timed out',
|
|
95
|
+
open: false,
|
|
96
|
+
url: options.url,
|
|
97
|
+
});
|
|
98
|
+
}, this.timeout(options.timeoutMs, 10000));
|
|
99
|
+
const finish = (result) => {
|
|
100
|
+
if (settled) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
settled = true;
|
|
104
|
+
window.clearTimeout(timeout);
|
|
105
|
+
socket === null || socket === void 0 ? void 0 : socket.close();
|
|
106
|
+
resolve(result);
|
|
107
|
+
};
|
|
108
|
+
try {
|
|
109
|
+
socket = new WebSocket(options.url);
|
|
110
|
+
socket.onopen = () => {
|
|
111
|
+
finish({
|
|
112
|
+
durationMs: this.elapsed(started),
|
|
113
|
+
open: true,
|
|
114
|
+
protocol: socket === null || socket === void 0 ? void 0 : socket.protocol,
|
|
115
|
+
url: options.url,
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
socket.onerror = () => {
|
|
119
|
+
finish({
|
|
120
|
+
durationMs: this.elapsed(started),
|
|
121
|
+
errorCode: 'WEBSOCKET_ERROR',
|
|
122
|
+
errorMessage: 'WebSocket handshake failed',
|
|
123
|
+
open: false,
|
|
124
|
+
url: options.url,
|
|
125
|
+
});
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
finish({
|
|
130
|
+
durationMs: this.elapsed(started),
|
|
131
|
+
errorCode: this.errorCode(error),
|
|
132
|
+
errorMessage: this.errorMessage(error),
|
|
133
|
+
open: false,
|
|
134
|
+
url: options.url,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
async testDownloadSpeed(options) {
|
|
140
|
+
const started = performance.now();
|
|
141
|
+
const maxBytes = this.positiveNumber(options.maxBytes, 5 * 1024 * 1024);
|
|
142
|
+
const controller = new AbortController();
|
|
143
|
+
const timeout = window.setTimeout(() => controller.abort(), this.timeout(options.timeoutMs, 30000));
|
|
144
|
+
let bytesDownloaded = 0;
|
|
145
|
+
let statusCode;
|
|
146
|
+
try {
|
|
147
|
+
const response = await fetch(options.url, {
|
|
148
|
+
method: 'GET',
|
|
149
|
+
signal: controller.signal,
|
|
150
|
+
});
|
|
151
|
+
statusCode = response.status;
|
|
152
|
+
if (response.body) {
|
|
153
|
+
const reader = response.body.getReader();
|
|
154
|
+
while (bytesDownloaded < maxBytes) {
|
|
155
|
+
const read = await reader.read();
|
|
156
|
+
if (read.done) {
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
bytesDownloaded += read.value.byteLength;
|
|
160
|
+
}
|
|
161
|
+
await reader.cancel().catch(() => undefined);
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
const buffer = await response.arrayBuffer();
|
|
165
|
+
bytesDownloaded = Math.min(buffer.byteLength, maxBytes);
|
|
166
|
+
}
|
|
167
|
+
const durationMs = Math.max(this.elapsed(started), 1);
|
|
168
|
+
const bytesPerSecond = bytesDownloaded / (durationMs / 1000);
|
|
169
|
+
return {
|
|
170
|
+
bytesDownloaded,
|
|
171
|
+
bytesPerSecond,
|
|
172
|
+
durationMs,
|
|
173
|
+
mbps: (bytesPerSecond * 8) / 1000000,
|
|
174
|
+
ok: response.ok,
|
|
175
|
+
statusCode,
|
|
176
|
+
url: options.url,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
const durationMs = Math.max(this.elapsed(started), 1);
|
|
181
|
+
const bytesPerSecond = bytesDownloaded / (durationMs / 1000);
|
|
182
|
+
return {
|
|
183
|
+
bytesDownloaded,
|
|
184
|
+
bytesPerSecond,
|
|
185
|
+
durationMs,
|
|
186
|
+
errorCode: this.errorCode(error),
|
|
187
|
+
errorMessage: this.errorMessage(error),
|
|
188
|
+
mbps: (bytesPerSecond * 8) / 1000000,
|
|
189
|
+
ok: false,
|
|
190
|
+
statusCode,
|
|
191
|
+
url: options.url,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
finally {
|
|
195
|
+
window.clearTimeout(timeout);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
async testPacketLoss(options) {
|
|
199
|
+
var _a;
|
|
200
|
+
const mode = this.packetLossMode(options);
|
|
201
|
+
const count = Math.max(1, Math.floor(this.positiveNumber(options.count, 10)));
|
|
202
|
+
const intervalMs = this.positiveNumber(options.intervalMs, 250);
|
|
203
|
+
const latencies = [];
|
|
204
|
+
let received = 0;
|
|
205
|
+
let lastErrorCode;
|
|
206
|
+
let lastErrorMessage;
|
|
207
|
+
if (mode === 'tcp') {
|
|
208
|
+
return {
|
|
209
|
+
errorCode: 'UNSUPPORTED_WEB',
|
|
210
|
+
errorMessage: 'Browsers cannot open raw TCP sockets. Use iOS or Android for native packet loss diagnostics.',
|
|
211
|
+
lost: count,
|
|
212
|
+
lossPercent: 100,
|
|
213
|
+
mode,
|
|
214
|
+
received: 0,
|
|
215
|
+
sent: count,
|
|
216
|
+
target: this.packetLossTarget(options, mode),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
for (let index = 0; index < count; index++) {
|
|
220
|
+
const result = await this.testUrl({
|
|
221
|
+
method: 'HEAD',
|
|
222
|
+
timeoutMs: options.timeoutMs,
|
|
223
|
+
url: (_a = options.url) !== null && _a !== void 0 ? _a : '',
|
|
224
|
+
});
|
|
225
|
+
if (result.reachable) {
|
|
226
|
+
received += 1;
|
|
227
|
+
latencies.push(result.durationMs);
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
lastErrorCode = result.errorCode;
|
|
231
|
+
lastErrorMessage = result.errorMessage;
|
|
232
|
+
}
|
|
233
|
+
if (index < count - 1) {
|
|
234
|
+
await this.sleep(intervalMs);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return this.packetLossResult(mode, this.packetLossTarget(options, mode), count, received, latencies, lastErrorCode, lastErrorMessage);
|
|
238
|
+
}
|
|
239
|
+
async runDiagnostics(options = {}) {
|
|
240
|
+
var _a, _b, _c;
|
|
241
|
+
const status = await this.getNetworkStatus();
|
|
242
|
+
const urls = [];
|
|
243
|
+
const ports = [];
|
|
244
|
+
const websockets = [];
|
|
245
|
+
for (const url of (_a = options.urls) !== null && _a !== void 0 ? _a : []) {
|
|
246
|
+
urls.push(await this.testUrl(url));
|
|
247
|
+
}
|
|
248
|
+
for (const port of (_b = options.ports) !== null && _b !== void 0 ? _b : []) {
|
|
249
|
+
ports.push(await this.testPort(port));
|
|
250
|
+
}
|
|
251
|
+
for (const websocket of (_c = options.websockets) !== null && _c !== void 0 ? _c : []) {
|
|
252
|
+
websockets.push(await this.testWebSocket(websocket));
|
|
253
|
+
}
|
|
254
|
+
const download = options.download ? await this.testDownloadSpeed(options.download) : undefined;
|
|
255
|
+
const packetLoss = options.packetLoss ? await this.testPacketLoss(options.packetLoss) : undefined;
|
|
256
|
+
return {
|
|
257
|
+
download,
|
|
258
|
+
issues: this.buildIssues(status, urls, ports, websockets, download, packetLoss),
|
|
259
|
+
packetLoss,
|
|
260
|
+
ports,
|
|
261
|
+
status,
|
|
262
|
+
urls,
|
|
263
|
+
websockets,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
async getPluginVersion() {
|
|
267
|
+
return {
|
|
268
|
+
version: 'web',
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
buildIssues(status, urls, ports, websockets, download, packetLoss) {
|
|
272
|
+
const issues = [];
|
|
273
|
+
if (!status.connected) {
|
|
274
|
+
issues.push('No active network connection');
|
|
275
|
+
}
|
|
276
|
+
else if (!status.internetReachable) {
|
|
277
|
+
issues.push('Network is connected but internet reachability is not confirmed');
|
|
278
|
+
}
|
|
279
|
+
for (const result of urls) {
|
|
280
|
+
if (!result.reachable) {
|
|
281
|
+
issues.push(`URL unreachable: ${result.url}`);
|
|
282
|
+
}
|
|
283
|
+
else if (!result.ok) {
|
|
284
|
+
issues.push(`URL returned non-success status: ${result.url}`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
for (const result of ports) {
|
|
288
|
+
if (!result.open) {
|
|
289
|
+
issues.push(`TCP port blocked or unreachable: ${result.host}:${result.port}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
for (const result of websockets) {
|
|
293
|
+
if (!result.open) {
|
|
294
|
+
issues.push(`WebSocket failed: ${result.url}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (download && !download.ok) {
|
|
298
|
+
issues.push(`Download speed test failed: ${download.url}`);
|
|
299
|
+
}
|
|
300
|
+
if (packetLoss && packetLoss.lossPercent > 0) {
|
|
301
|
+
issues.push(`Packet loss detected: ${packetLoss.lossPercent}% to ${packetLoss.target}`);
|
|
302
|
+
}
|
|
303
|
+
return issues;
|
|
304
|
+
}
|
|
305
|
+
elapsed(started) {
|
|
306
|
+
return Math.round(performance.now() - started);
|
|
307
|
+
}
|
|
308
|
+
errorCode(error) {
|
|
309
|
+
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
310
|
+
return 'TIMEOUT';
|
|
311
|
+
}
|
|
312
|
+
if (error instanceof Error && error.name) {
|
|
313
|
+
return error.name;
|
|
314
|
+
}
|
|
315
|
+
return 'ERROR';
|
|
316
|
+
}
|
|
317
|
+
errorMessage(error) {
|
|
318
|
+
if (error instanceof Error && error.message) {
|
|
319
|
+
return error.message;
|
|
320
|
+
}
|
|
321
|
+
return String(error);
|
|
322
|
+
}
|
|
323
|
+
mapConnectionType(type) {
|
|
324
|
+
if (!type) {
|
|
325
|
+
return navigator.onLine ? 'unknown' : 'none';
|
|
326
|
+
}
|
|
327
|
+
if (type === 'wifi') {
|
|
328
|
+
return 'wifi';
|
|
329
|
+
}
|
|
330
|
+
if (type === 'cellular' ||
|
|
331
|
+
type.includes('2g') ||
|
|
332
|
+
type.includes('3g') ||
|
|
333
|
+
type.includes('4g') ||
|
|
334
|
+
type.includes('5g')) {
|
|
335
|
+
return 'cellular';
|
|
336
|
+
}
|
|
337
|
+
if (type === 'ethernet') {
|
|
338
|
+
return 'ethernet';
|
|
339
|
+
}
|
|
340
|
+
if (type === 'none') {
|
|
341
|
+
return 'none';
|
|
342
|
+
}
|
|
343
|
+
return 'unknown';
|
|
344
|
+
}
|
|
345
|
+
normalizeMethod(method) {
|
|
346
|
+
return method === 'GET' ? 'GET' : 'HEAD';
|
|
347
|
+
}
|
|
348
|
+
packetLossMode(options) {
|
|
349
|
+
if (options.mode) {
|
|
350
|
+
return options.mode;
|
|
351
|
+
}
|
|
352
|
+
return options.host && options.port ? 'tcp' : 'http';
|
|
353
|
+
}
|
|
354
|
+
packetLossResult(mode, target, sent, received, latencies, errorCode, errorMessage) {
|
|
355
|
+
const lost = sent - received;
|
|
356
|
+
const result = {
|
|
357
|
+
lost,
|
|
358
|
+
lossPercent: (lost / sent) * 100,
|
|
359
|
+
mode,
|
|
360
|
+
received,
|
|
361
|
+
sent,
|
|
362
|
+
target,
|
|
363
|
+
};
|
|
364
|
+
if (latencies.length > 0) {
|
|
365
|
+
result.averageLatencyMs = latencies.reduce((sum, latency) => sum + latency, 0) / latencies.length;
|
|
366
|
+
result.minLatencyMs = Math.min(...latencies);
|
|
367
|
+
result.maxLatencyMs = Math.max(...latencies);
|
|
368
|
+
}
|
|
369
|
+
if (errorCode) {
|
|
370
|
+
result.errorCode = errorCode;
|
|
371
|
+
}
|
|
372
|
+
if (errorMessage) {
|
|
373
|
+
result.errorMessage = errorMessage;
|
|
374
|
+
}
|
|
375
|
+
return result;
|
|
376
|
+
}
|
|
377
|
+
packetLossTarget(options, mode) {
|
|
378
|
+
var _a, _b, _c;
|
|
379
|
+
if (mode === 'http') {
|
|
380
|
+
return (_a = options.url) !== null && _a !== void 0 ? _a : '';
|
|
381
|
+
}
|
|
382
|
+
return `${(_b = options.host) !== null && _b !== void 0 ? _b : ''}:${(_c = options.port) !== null && _c !== void 0 ? _c : 0}`;
|
|
383
|
+
}
|
|
384
|
+
positiveNumber(value, fallback) {
|
|
385
|
+
return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : fallback;
|
|
386
|
+
}
|
|
387
|
+
sleep(ms) {
|
|
388
|
+
return new Promise((resolve) => window.setTimeout(resolve, ms));
|
|
389
|
+
}
|
|
390
|
+
timeout(value, fallback) {
|
|
391
|
+
return Math.floor(this.positiveNumber(value, fallback));
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
var web = /*#__PURE__*/Object.freeze({
|
|
396
|
+
__proto__: null,
|
|
397
|
+
NetworkDiagnosticsWeb: NetworkDiagnosticsWeb
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
exports.NetworkDiagnostics = NetworkDiagnostics;
|
|
401
|
+
|
|
402
|
+
return exports;
|
|
403
|
+
|
|
404
|
+
})({}, capacitorExports);
|
|
405
|
+
//# sourceMappingURL=plugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\nconst NetworkDiagnostics = registerPlugin('NetworkDiagnostics', {\n web: () => import('./web').then((m) => new m.NetworkDiagnosticsWeb()),\n});\nexport * from './definitions';\nexport { NetworkDiagnostics };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nexport class NetworkDiagnosticsWeb extends WebPlugin {\n async getNetworkStatus() {\n var _a, _b, _c;\n const nav = navigator;\n const connection = (_b = (_a = nav.connection) !== null && _a !== void 0 ? _a : nav.mozConnection) !== null && _b !== void 0 ? _b : nav.webkitConnection;\n const connectionType = this.mapConnectionType((_c = connection === null || connection === void 0 ? void 0 : connection.type) !== null && _c !== void 0 ? _c : connection === null || connection === void 0 ? void 0 : connection.effectiveType);\n const connected = navigator.onLine;\n const details = {};\n if (connection === null || connection === void 0 ? void 0 : connection.effectiveType) {\n details.effectiveType = connection.effectiveType;\n }\n if (typeof (connection === null || connection === void 0 ? void 0 : connection.downlink) === 'number') {\n details.downlinkMbps = connection.downlink;\n }\n if (typeof (connection === null || connection === void 0 ? void 0 : connection.rtt) === 'number') {\n details.rttMs = connection.rtt;\n }\n return {\n connected,\n connectionType,\n constrained: connection === null || connection === void 0 ? void 0 : connection.saveData,\n details,\n expensive: false,\n internetReachable: connected,\n };\n }\n async testUrl(options) {\n const started = performance.now();\n const method = this.normalizeMethod(options.method);\n const controller = new AbortController();\n const timeout = window.setTimeout(() => controller.abort(), this.timeout(options.timeoutMs, 10000));\n try {\n const response = await fetch(options.url, {\n method,\n redirect: options.followRedirects === false ? 'manual' : 'follow',\n signal: controller.signal,\n });\n return {\n durationMs: this.elapsed(started),\n finalUrl: response.url || options.url,\n method,\n ok: response.status >= 200 && response.status < 400,\n reachable: true,\n statusCode: response.status,\n url: options.url,\n };\n }\n catch (error) {\n return {\n durationMs: this.elapsed(started),\n errorCode: this.errorCode(error),\n errorMessage: this.errorMessage(error),\n method,\n ok: false,\n reachable: false,\n url: options.url,\n };\n }\n finally {\n window.clearTimeout(timeout);\n }\n }\n async testPort(options) {\n return {\n durationMs: 0,\n errorCode: 'UNSUPPORTED_WEB',\n errorMessage: 'Browsers cannot open raw TCP sockets. Use iOS or Android for native port diagnostics.',\n host: options.host,\n open: false,\n port: options.port,\n };\n }\n async testWebSocket(options) {\n const started = performance.now();\n return new Promise((resolve) => {\n let settled = false;\n let socket;\n const timeout = window.setTimeout(() => {\n if (settled) {\n return;\n }\n settled = true;\n socket === null || socket === void 0 ? void 0 : socket.close();\n resolve({\n durationMs: this.elapsed(started),\n errorCode: 'TIMEOUT',\n errorMessage: 'WebSocket handshake timed out',\n open: false,\n url: options.url,\n });\n }, this.timeout(options.timeoutMs, 10000));\n const finish = (result) => {\n if (settled) {\n return;\n }\n settled = true;\n window.clearTimeout(timeout);\n socket === null || socket === void 0 ? void 0 : socket.close();\n resolve(result);\n };\n try {\n socket = new WebSocket(options.url);\n socket.onopen = () => {\n finish({\n durationMs: this.elapsed(started),\n open: true,\n protocol: socket === null || socket === void 0 ? void 0 : socket.protocol,\n url: options.url,\n });\n };\n socket.onerror = () => {\n finish({\n durationMs: this.elapsed(started),\n errorCode: 'WEBSOCKET_ERROR',\n errorMessage: 'WebSocket handshake failed',\n open: false,\n url: options.url,\n });\n };\n }\n catch (error) {\n finish({\n durationMs: this.elapsed(started),\n errorCode: this.errorCode(error),\n errorMessage: this.errorMessage(error),\n open: false,\n url: options.url,\n });\n }\n });\n }\n async testDownloadSpeed(options) {\n const started = performance.now();\n const maxBytes = this.positiveNumber(options.maxBytes, 5 * 1024 * 1024);\n const controller = new AbortController();\n const timeout = window.setTimeout(() => controller.abort(), this.timeout(options.timeoutMs, 30000));\n let bytesDownloaded = 0;\n let statusCode;\n try {\n const response = await fetch(options.url, {\n method: 'GET',\n signal: controller.signal,\n });\n statusCode = response.status;\n if (response.body) {\n const reader = response.body.getReader();\n while (bytesDownloaded < maxBytes) {\n const read = await reader.read();\n if (read.done) {\n break;\n }\n bytesDownloaded += read.value.byteLength;\n }\n await reader.cancel().catch(() => undefined);\n }\n else {\n const buffer = await response.arrayBuffer();\n bytesDownloaded = Math.min(buffer.byteLength, maxBytes);\n }\n const durationMs = Math.max(this.elapsed(started), 1);\n const bytesPerSecond = bytesDownloaded / (durationMs / 1000);\n return {\n bytesDownloaded,\n bytesPerSecond,\n durationMs,\n mbps: (bytesPerSecond * 8) / 1000000,\n ok: response.ok,\n statusCode,\n url: options.url,\n };\n }\n catch (error) {\n const durationMs = Math.max(this.elapsed(started), 1);\n const bytesPerSecond = bytesDownloaded / (durationMs / 1000);\n return {\n bytesDownloaded,\n bytesPerSecond,\n durationMs,\n errorCode: this.errorCode(error),\n errorMessage: this.errorMessage(error),\n mbps: (bytesPerSecond * 8) / 1000000,\n ok: false,\n statusCode,\n url: options.url,\n };\n }\n finally {\n window.clearTimeout(timeout);\n }\n }\n async testPacketLoss(options) {\n var _a;\n const mode = this.packetLossMode(options);\n const count = Math.max(1, Math.floor(this.positiveNumber(options.count, 10)));\n const intervalMs = this.positiveNumber(options.intervalMs, 250);\n const latencies = [];\n let received = 0;\n let lastErrorCode;\n let lastErrorMessage;\n if (mode === 'tcp') {\n return {\n errorCode: 'UNSUPPORTED_WEB',\n errorMessage: 'Browsers cannot open raw TCP sockets. Use iOS or Android for native packet loss diagnostics.',\n lost: count,\n lossPercent: 100,\n mode,\n received: 0,\n sent: count,\n target: this.packetLossTarget(options, mode),\n };\n }\n for (let index = 0; index < count; index++) {\n const result = await this.testUrl({\n method: 'HEAD',\n timeoutMs: options.timeoutMs,\n url: (_a = options.url) !== null && _a !== void 0 ? _a : '',\n });\n if (result.reachable) {\n received += 1;\n latencies.push(result.durationMs);\n }\n else {\n lastErrorCode = result.errorCode;\n lastErrorMessage = result.errorMessage;\n }\n if (index < count - 1) {\n await this.sleep(intervalMs);\n }\n }\n return this.packetLossResult(mode, this.packetLossTarget(options, mode), count, received, latencies, lastErrorCode, lastErrorMessage);\n }\n async runDiagnostics(options = {}) {\n var _a, _b, _c;\n const status = await this.getNetworkStatus();\n const urls = [];\n const ports = [];\n const websockets = [];\n for (const url of (_a = options.urls) !== null && _a !== void 0 ? _a : []) {\n urls.push(await this.testUrl(url));\n }\n for (const port of (_b = options.ports) !== null && _b !== void 0 ? _b : []) {\n ports.push(await this.testPort(port));\n }\n for (const websocket of (_c = options.websockets) !== null && _c !== void 0 ? _c : []) {\n websockets.push(await this.testWebSocket(websocket));\n }\n const download = options.download ? await this.testDownloadSpeed(options.download) : undefined;\n const packetLoss = options.packetLoss ? await this.testPacketLoss(options.packetLoss) : undefined;\n return {\n download,\n issues: this.buildIssues(status, urls, ports, websockets, download, packetLoss),\n packetLoss,\n ports,\n status,\n urls,\n websockets,\n };\n }\n async getPluginVersion() {\n return {\n version: 'web',\n };\n }\n buildIssues(status, urls, ports, websockets, download, packetLoss) {\n const issues = [];\n if (!status.connected) {\n issues.push('No active network connection');\n }\n else if (!status.internetReachable) {\n issues.push('Network is connected but internet reachability is not confirmed');\n }\n for (const result of urls) {\n if (!result.reachable) {\n issues.push(`URL unreachable: ${result.url}`);\n }\n else if (!result.ok) {\n issues.push(`URL returned non-success status: ${result.url}`);\n }\n }\n for (const result of ports) {\n if (!result.open) {\n issues.push(`TCP port blocked or unreachable: ${result.host}:${result.port}`);\n }\n }\n for (const result of websockets) {\n if (!result.open) {\n issues.push(`WebSocket failed: ${result.url}`);\n }\n }\n if (download && !download.ok) {\n issues.push(`Download speed test failed: ${download.url}`);\n }\n if (packetLoss && packetLoss.lossPercent > 0) {\n issues.push(`Packet loss detected: ${packetLoss.lossPercent}% to ${packetLoss.target}`);\n }\n return issues;\n }\n elapsed(started) {\n return Math.round(performance.now() - started);\n }\n errorCode(error) {\n if (error instanceof DOMException && error.name === 'AbortError') {\n return 'TIMEOUT';\n }\n if (error instanceof Error && error.name) {\n return error.name;\n }\n return 'ERROR';\n }\n errorMessage(error) {\n if (error instanceof Error && error.message) {\n return error.message;\n }\n return String(error);\n }\n mapConnectionType(type) {\n if (!type) {\n return navigator.onLine ? 'unknown' : 'none';\n }\n if (type === 'wifi') {\n return 'wifi';\n }\n if (type === 'cellular' ||\n type.includes('2g') ||\n type.includes('3g') ||\n type.includes('4g') ||\n type.includes('5g')) {\n return 'cellular';\n }\n if (type === 'ethernet') {\n return 'ethernet';\n }\n if (type === 'none') {\n return 'none';\n }\n return 'unknown';\n }\n normalizeMethod(method) {\n return method === 'GET' ? 'GET' : 'HEAD';\n }\n packetLossMode(options) {\n if (options.mode) {\n return options.mode;\n }\n return options.host && options.port ? 'tcp' : 'http';\n }\n packetLossResult(mode, target, sent, received, latencies, errorCode, errorMessage) {\n const lost = sent - received;\n const result = {\n lost,\n lossPercent: (lost / sent) * 100,\n mode,\n received,\n sent,\n target,\n };\n if (latencies.length > 0) {\n result.averageLatencyMs = latencies.reduce((sum, latency) => sum + latency, 0) / latencies.length;\n result.minLatencyMs = Math.min(...latencies);\n result.maxLatencyMs = Math.max(...latencies);\n }\n if (errorCode) {\n result.errorCode = errorCode;\n }\n if (errorMessage) {\n result.errorMessage = errorMessage;\n }\n return result;\n }\n packetLossTarget(options, mode) {\n var _a, _b, _c;\n if (mode === 'http') {\n return (_a = options.url) !== null && _a !== void 0 ? _a : '';\n }\n return `${(_b = options.host) !== null && _b !== void 0 ? _b : ''}:${(_c = options.port) !== null && _c !== void 0 ? _c : 0}`;\n }\n positiveNumber(value, fallback) {\n return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : fallback;\n }\n sleep(ms) {\n return new Promise((resolve) => window.setTimeout(resolve, ms));\n }\n timeout(value, fallback) {\n return Math.floor(this.positiveNumber(value, fallback));\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":"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"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
extension NetworkDiagnostics {
|
|
4
|
+
public func testDownloadSpeed(url: String, maxBytes: Int, timeoutMs: Int) async -> [String: Any] {
|
|
5
|
+
let byteLimit = positive(maxBytes, fallback: 5 * 1024 * 1024)
|
|
6
|
+
var context = DownloadContext(url: url, started: Date())
|
|
7
|
+
|
|
8
|
+
guard let parsedUrl = URL(string: url) else {
|
|
9
|
+
return downloadResult(context, errorCode: "INVALID_URL", errorMessage: "Invalid URL")
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let session = URLSession(configuration: downloadConfiguration(timeoutMs))
|
|
13
|
+
defer { session.finishTasksAndInvalidate() }
|
|
14
|
+
|
|
15
|
+
do {
|
|
16
|
+
let (data, response) = try await session.data(for: downloadRequest(parsedUrl, byteLimit: byteLimit, timeoutMs: timeoutMs))
|
|
17
|
+
context.bytesDownloaded = min(data.count, byteLimit)
|
|
18
|
+
context.statusCode = (response as? HTTPURLResponse)?.statusCode
|
|
19
|
+
return downloadResult(context)
|
|
20
|
+
} catch {
|
|
21
|
+
return downloadResult(context, errorCode: errorCode(error), errorMessage: error.localizedDescription)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
func downloadConfiguration(_ timeoutMs: Int) -> URLSessionConfiguration {
|
|
26
|
+
let configuration = URLSessionConfiguration.ephemeral
|
|
27
|
+
configuration.timeoutIntervalForRequest = timeout(timeoutMs, fallback: 30)
|
|
28
|
+
configuration.timeoutIntervalForResource = timeout(timeoutMs, fallback: 30)
|
|
29
|
+
return configuration
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
func downloadRequest(_ url: URL, byteLimit: Int, timeoutMs: Int) -> URLRequest {
|
|
33
|
+
var request = URLRequest(url: url)
|
|
34
|
+
request.httpMethod = "GET"
|
|
35
|
+
request.timeoutInterval = timeout(timeoutMs, fallback: 30)
|
|
36
|
+
request.setValue("bytes=0-\(byteLimit - 1)", forHTTPHeaderField: "Range")
|
|
37
|
+
return request
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
func downloadResult(_ context: DownloadContext, errorCode: String? = nil, errorMessage: String? = nil) -> [String: Any] {
|
|
41
|
+
let durationMs = max(elapsedMs(context.started), 1)
|
|
42
|
+
let bytesPerSecond = Double(context.bytesDownloaded) / (Double(durationMs) / 1000.0)
|
|
43
|
+
var result: [String: Any] = [
|
|
44
|
+
"url": context.url,
|
|
45
|
+
"ok": errorCode == nil && (context.statusCode ?? 0) >= 200 && (context.statusCode ?? 0) < 400,
|
|
46
|
+
"durationMs": durationMs,
|
|
47
|
+
"bytesDownloaded": context.bytesDownloaded,
|
|
48
|
+
"bytesPerSecond": bytesPerSecond,
|
|
49
|
+
"mbps": (bytesPerSecond * 8) / 1_000_000
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
if let statusCode = context.statusCode {
|
|
53
|
+
result["statusCode"] = statusCode
|
|
54
|
+
}
|
|
55
|
+
if let errorCode = errorCode {
|
|
56
|
+
result["errorCode"] = errorCode
|
|
57
|
+
}
|
|
58
|
+
if let errorMessage = errorMessage {
|
|
59
|
+
result["errorMessage"] = errorMessage
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return result
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
struct DownloadContext {
|
|
67
|
+
let url: String
|
|
68
|
+
let started: Date
|
|
69
|
+
var bytesDownloaded = 0
|
|
70
|
+
var statusCode: Int?
|
|
71
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
extension NetworkDiagnostics {
|
|
4
|
+
func testPacketLoss(_ options: PacketLossOptions) async -> [String: Any] {
|
|
5
|
+
let mode = normalizePacketLossMode(mode: options.mode, host: options.host, port: options.port, url: options.url)
|
|
6
|
+
let probeCount = positive(options.count, fallback: 10)
|
|
7
|
+
let delayMs = positive(options.intervalMs, fallback: 250)
|
|
8
|
+
var report = PacketLossReport(mode: mode, target: packetLossTarget(mode: mode, options: options), sent: probeCount)
|
|
9
|
+
|
|
10
|
+
for index in 0..<probeCount {
|
|
11
|
+
let probe = mode == "http"
|
|
12
|
+
? await probeHttp(url: options.url, timeoutMs: options.timeoutMs)
|
|
13
|
+
: await probeTcp(host: options.host, port: options.port, timeoutMs: options.timeoutMs)
|
|
14
|
+
report.record(probe)
|
|
15
|
+
|
|
16
|
+
if index < probeCount - 1 {
|
|
17
|
+
try? await Task.sleep(nanoseconds: UInt64(delayMs) * 1_000_000)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return packetLossResult(report)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
func packetLossTarget(mode: String, options: PacketLossOptions) -> String {
|
|
25
|
+
mode == "http" ? options.url : "\(options.host):\(options.port)"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
func packetLossResult(_ report: PacketLossReport) -> [String: Any] {
|
|
29
|
+
let lost = report.sent - report.received
|
|
30
|
+
var result: [String: Any] = [
|
|
31
|
+
"mode": report.mode,
|
|
32
|
+
"target": report.target,
|
|
33
|
+
"sent": report.sent,
|
|
34
|
+
"received": report.received,
|
|
35
|
+
"lost": lost,
|
|
36
|
+
"lossPercent": report.sent == 0 ? 0 : (Double(lost) * 100.0) / Double(report.sent)
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
if !report.latencies.isEmpty {
|
|
40
|
+
result["averageLatencyMs"] = Double(report.latencies.reduce(0, +)) / Double(report.latencies.count)
|
|
41
|
+
result["minLatencyMs"] = report.latencies.min()
|
|
42
|
+
result["maxLatencyMs"] = report.latencies.max()
|
|
43
|
+
}
|
|
44
|
+
if let errorCode = report.errorCode {
|
|
45
|
+
result["errorCode"] = errorCode
|
|
46
|
+
}
|
|
47
|
+
if let errorMessage = report.errorMessage {
|
|
48
|
+
result["errorMessage"] = errorMessage
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return result
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
struct PacketLossOptions {
|
|
56
|
+
let mode: String
|
|
57
|
+
let host: String
|
|
58
|
+
let port: Int
|
|
59
|
+
let url: String
|
|
60
|
+
let count: Int
|
|
61
|
+
let timeoutMs: Int
|
|
62
|
+
let intervalMs: Int
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
struct PacketLossReport {
|
|
66
|
+
let mode: String
|
|
67
|
+
let target: String
|
|
68
|
+
let sent: Int
|
|
69
|
+
var received = 0
|
|
70
|
+
var latencies: [Int] = []
|
|
71
|
+
var errorCode: String?
|
|
72
|
+
var errorMessage: String?
|
|
73
|
+
|
|
74
|
+
mutating func record(_ probe: ProbeResult) {
|
|
75
|
+
if probe.success {
|
|
76
|
+
received += 1
|
|
77
|
+
latencies.append(probe.durationMs)
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
errorCode = probe.errorCode
|
|
82
|
+
errorMessage = probe.errorMessage
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
struct ProbeResult {
|
|
87
|
+
let success: Bool
|
|
88
|
+
let durationMs: Int
|
|
89
|
+
let errorCode: String?
|
|
90
|
+
let errorMessage: String?
|
|
91
|
+
}
|