Compare commits

...

4 Commits

Author SHA1 Message Date
9cf4e433bf 4.0.2
Some checks failed
Default (tags) / security (push) Successful in 36s
Default (tags) / test (push) Failing after 15m18s
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2025-05-19 09:15:42 +00:00
7c88ecd82a fix(tests): Update dev dependencies and refactor test assertions for improved clarity 2025-05-19 09:15:42 +00:00
771bfe94e7 4.0.1
Some checks failed
Default (tags) / security (push) Successful in 38s
Default (tags) / test (push) Failing after 1m34s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-05-03 18:56:00 +00:00
def467a27b fix(formatting): Fix minor formatting issues and newline consistency across project files 2025-05-03 18:56:00 +00:00
13 changed files with 708 additions and 1705 deletions

View File

@ -1,5 +1,21 @@
# Changelog
## 2025-05-19 - 4.0.2 - fix(tests)
Update dev dependencies and refactor test assertions for improved clarity
- Bumped @git.zone/tsbuild version to ^2.5.1
- Bumped @git.zone/tstest version to ^1.9.0
- Updated npm test script to include the verbose flag
- Replaced expectAsync assertions with resolves based assertions in test files
## 2025-05-03 - 4.0.1 - fix(formatting)
Fix minor formatting issues and newline consistency across project files
- Ensure newline at end of package.json, errors.ts, logging.ts, and test files
- Refine code block formatting in readme.md
- Adjust whitespace and code style in smartnetwork classes and cloudflarespeed module
- Minor commitinfo data format update
## 2025-04-28 - 4.0.0 - BREAKING CHANGE(smartnetwork)
Enhance documentation and add configurable speed test options with plugin architecture improvements

View File

@ -1,6 +1,6 @@
{
"name": "@push.rocks/smartnetwork",
"version": "4.0.0",
"version": "4.0.2",
"private": false,
"description": "A toolkit for network diagnostics including speed tests, port availability checks, and more.",
"main": "dist_ts/index.js",
@ -9,17 +9,16 @@
"author": "Lossless GmbH",
"license": "MIT",
"scripts": {
"test": "(tstest test/)",
"test": "(tstest test/ --verbose)",
"build": "(tsbuild --web --allowimplicitany)",
"buildDocs": "tsdoc"
},
"devDependencies": {
"@git.zone/tsbuild": "^2.1.61",
"@git.zone/tsbuild": "^2.5.1",
"@git.zone/tsrun": "^1.2.39",
"@git.zone/tstest": "^1.0.69",
"@git.zone/tstest": "^1.9.0",
"@push.rocks/smartenv": "^5.0.0",
"@push.rocks/tapbundle": "^5.0.3",
"@types/node": "^22.15.3"
"@types/node": "^22.15.19"
},
"dependencies": {
"@push.rocks/smartping": "^1.0.7",

2197
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1 +1,73 @@
# Project Analysis
## Architecture Overview
This is a comprehensive network diagnostics toolkit that provides various network-related utilities. The main entry point is the `SmartNetwork` class which orchestrates all functionality.
Key features:
- Speed testing via Cloudflare (parallelizable with duration support)
- Ping operations with statistics
- Port availability checks (local and remote)
- Network gateway discovery
- Public IP retrieval
- DNS resolution
- HTTP endpoint health checks
- Traceroute functionality (with fallback stub)
## Key Components
### SmartNetwork Class
- Central orchestrator for all network operations
- Supports caching via `cacheTtl` option for gateway and public IP lookups
- Plugin architecture for extensibility
### CloudflareSpeed Class
- Handles internet speed testing using Cloudflare's infrastructure
- Supports parallel streams and customizable test duration
- Measures both download and upload speeds using progressive chunk sizes
- Includes latency measurements (jitter, median, average)
### Error Handling
- Custom `NetworkError` and `TimeoutError` classes for better error context
- Error codes follow Node.js conventions (ENOTSUP, EINVAL, ETIMEOUT)
### Logging
- Global logger interface for consistent logging across the codebase
- Replaceable logger implementation (defaults to console)
- Used primarily for error reporting in speed tests
### Statistics Helpers
- Utility functions for statistical calculations (average, median, quartile, jitter)
- Used extensively by speed testing and ping operations
## Recent Changes (v4.0.0 - v4.0.1)
- Added configurable speed test options (parallelStreams, duration)
- Introduced plugin architecture for runtime extensibility
- Enhanced error handling with custom error classes
- Added global logging interface
- Improved connection management by disabling HTTP connection pooling
- Fixed memory leaks from listener accumulation
- Minor formatting fixes for consistency
## Testing
- Comprehensive test suite covering all major features
- Tests run on both browser and node environments
- Uses @push.rocks/tapbundle for testing with expectAsync
- Performance tests for speed testing functionality
- Edge case handling for network errors and timeouts
## Technical Details
- ESM-only package (module type)
- TypeScript with strict typing
- Depends on external modules for specific functionality:
- @push.rocks/smartping for ICMP operations
- public-ip for external IP discovery
- systeminformation for network interface details
- isopen for remote port checking
- Uses native Node.js modules for DNS, HTTP/HTTPS, and network operations
## Design Patterns
- Factory pattern for plugin registration
- Caching pattern with TTL for expensive operations
- Promise-based async/await throughout
- Deferred promises for complex async coordination
- Error propagation with custom error types

View File

@ -16,9 +16,7 @@ You can perform a hop-by-hop traceroute to measure latency per hop. Falls back t
```typescript
const hops = await myNetwork.traceroute('google.com', { maxHops: 10, timeout: 5000 });
hops.forEach(h =>
console.log(`${h.ttl}\t${h.ip}\t${h.rtt === null ? '*' : h.rtt + ' ms'}`),
);
hops.forEach((h) => console.log(`${h.ttl}\t${h.ip}\t${h.rtt === null ? '*' : h.rtt + ' ms'}`));
```
This command will download `@push.rocks/smartnetwork` and add it to your project's `package.json` file.
@ -44,6 +42,7 @@ const myNetwork = new SmartNetwork();
### Performing a Speed Test
You can measure the network speed using the `getSpeed` method. It supports optional parameters:
- `parallelStreams`: number of concurrent streams (default: 1)
- `duration`: test duration in seconds (default: fixed segments)
@ -143,6 +142,7 @@ console.log(`Public IPv6: ${publicIps.v6}`);
The `@push.rocks/smartnetwork` package provides an easy-to-use, comprehensive suite of tools for network diagnostics and monitoring, encapsulating complex network operations into simple asynchronous methods. By leveraging TypeScript, developers can benefit from type checking, ensuring that they can work with clear structures and expectations.
These examples offer a glimpse into the module's utility in real-world scenarios, demonstrating its versatility in handling common network tasks. Whether you're developing a network-sensitive application, diagnosing connectivity issues, or simply curious about your network performance, `@push.rocks/smartnetwork` equips you with the tools you need.
### Plugin Architecture
You can extend `SmartNetwork` with custom plugins by registering them at runtime:

View File

@ -1,4 +1,4 @@
import { tap, expect, expectAsync } from '@push.rocks/tapbundle';
import { tap, expect } from '@git.zone/tstest/tapbundle';
import { SmartNetwork, NetworkError } from '../ts/index.js';
import * as net from 'net';
import type { AddressInfo } from 'net';
@ -61,7 +61,7 @@ tap.test('traceroute fallback stub returns a single-hop stub', async () => {
const sn = new SmartNetwork();
const hops = await sn.traceroute('example.com', { maxHops: 5 });
expect(Array.isArray(hops)).toBeTrue();
expect(hops).toHaveLength(1);
expect(hops).array.toHaveLength(1);
expect(hops[0]).toEqual({ ttl: 1, ip: 'example.com', rtt: null });
});
@ -166,7 +166,7 @@ tap.test('isLocalPortUnused should detect used local port', async () => {
// port is now in use
const inUse = await sn.isLocalPortUnused(addr.port);
expect(inUse).toBeFalse();
await new Promise<void>((res) => server.close(res));
await new Promise<void>((resolve) => server.close(() => resolve()));
});
// Real traceroute integration test (skipped if `traceroute` binary is unavailable)
tap.test('traceroute real integration against google.com', async () => {
@ -188,4 +188,4 @@ tap.test('traceroute real integration against google.com', async () => {
}
});
tap.start();
tap.start();

View File

@ -1,4 +1,4 @@
import { tap, expect, expectAsync } from '@push.rocks/tapbundle';
import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as smartnetwork from '../ts/index.js';
@ -20,11 +20,11 @@ tap.test('should send a ping to Google', async () => {
});
tap.test('should state when a ping is not alive ', async () => {
await expectAsync(testSmartnetwork.ping('notthere.lossless.com')).property('alive').toBeFalse();
await expect(testSmartnetwork.ping('notthere.lossless.com')).resolves.property('alive').toBeFalse();
});
tap.test('should send a ping to an IP', async () => {
await expectAsync(testSmartnetwork.ping('192.168.186.999')).property('alive').toBeFalse();
await expect(testSmartnetwork.ping('192.168.186.999')).resolves.property('alive').toBeFalse();
});
tap.start();

View File

@ -1,4 +1,4 @@
import { expect, expectAsync, tap } from '@push.rocks/tapbundle';
import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as smartnetwork from '../ts/index.js';
let testSmartNetwork: smartnetwork.SmartNetwork;
@ -20,13 +20,13 @@ tap.test('should perform a speedtest', async () => {
});
tap.test('should determine wether a port is free', async () => {
await expectAsync(testSmartNetwork.isLocalPortUnused(8080)).toBeTrue();
await expect(testSmartNetwork.isLocalPortUnused(8080)).resolves.toBeTrue();
});
tap.test('should scan a port', async () => {
await expectAsync(testSmartNetwork.isRemotePortAvailable('lossless.com:443')).toBeTrue();
await expectAsync(testSmartNetwork.isRemotePortAvailable('lossless.com', 443)).toBeTrue();
await expectAsync(testSmartNetwork.isRemotePortAvailable('lossless.com:444')).toBeFalse();
await expect(testSmartNetwork.isRemotePortAvailable('lossless.com:443')).resolves.toBeTrue();
await expect(testSmartNetwork.isRemotePortAvailable('lossless.com', 443)).resolves.toBeTrue();
await expect(testSmartNetwork.isRemotePortAvailable('lossless.com:444')).resolves.toBeFalse();
});
tap.test('should get gateways', async () => {

View File

@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@push.rocks/smartnetwork',
version: '4.0.0',
version: '4.0.2',
description: 'A toolkit for network diagnostics including speed tests, port availability checks, and more.'
}

View File

@ -17,4 +17,4 @@ export class TimeoutError extends NetworkError {
this.name = 'TimeoutError';
Object.setPrototypeOf(this, new.target.prototype);
}
}
}

View File

@ -27,4 +27,4 @@ export function setLogger(logger: Logger): void {
*/
export function getLogger(): Logger {
return globalLogger;
}
}

View File

@ -164,9 +164,10 @@ export class CloudflareSpeed {
}
public async fetchServerLocations(): Promise<{ [key: string]: string }> {
const res = JSON.parse(
await this.get('speed.cloudflare.com', '/locations'),
) as Array<{ iata: string; city: string }>;
const res = JSON.parse(await this.get('speed.cloudflare.com', '/locations')) as Array<{
iata: string;
city: string;
}>;
return res.reduce(
(data: Record<string, string>, optionsArg) => {
data[optionsArg.iata] = optionsArg.city;
@ -198,9 +199,9 @@ export class CloudflareSpeed {
reject(e);
}
});
req.on('error', (err: Error & { code?: string }) => {
reject(new NetworkError(err.message, err.code));
});
req.on('error', (err: Error & { code?: string }) => {
reject(new NetworkError(err.message, err.code));
});
},
);
@ -251,15 +252,15 @@ export class CloudflareSpeed {
res.on('data', () => {});
res.on('end', () => {
ended = plugins.perfHooks.performance.now();
resolve([
started,
dnsLookup,
tcpHandshake,
sslHandshake,
ttfb,
ended,
parseFloat((res.headers['server-timing'] as string).slice(22)),
]);
resolve([
started,
dnsLookup,
tcpHandshake,
sslHandshake,
ttfb,
ended,
parseFloat((res.headers['server-timing'] as string).slice(22)),
]);
});
});
@ -296,11 +297,14 @@ export class CloudflareSpeed {
const parts = i.split('=');
return [parts[0], parts[1]];
})
.reduce((data: Record<string, string>, [k, v]) => {
if (v === undefined) return data;
data[k] = v;
return data;
}, {} as Record<string, string>);
.reduce(
(data: Record<string, string>, [k, v]) => {
if (v === undefined) return data;
data[k] = v;
return data;
},
{} as Record<string, string>,
);
return this.get('speed.cloudflare.com', '/cdn-cgi/trace').then(parseCfCdnCgiTrace);
}

View File

@ -44,9 +44,7 @@ export class SmartNetwork {
* get network speed
* @param opts optional speed test parameters
*/
public async getSpeed(
opts?: { parallelStreams?: number; duration?: number },
) {
public async getSpeed(opts?: { parallelStreams?: number; duration?: number }) {
const cloudflareSpeedInstance = new CloudflareSpeed(opts);
return cloudflareSpeedInstance.speedTest();
}
@ -54,10 +52,7 @@ export class SmartNetwork {
/**
* Send ICMP pings to a host. Optionally specify count for multiple pings.
*/
public async ping(
host: string,
opts?: { timeout?: number; count?: number },
): Promise<any> {
public async ping(host: string, opts?: { timeout?: number; count?: number }): Promise<any> {
const timeout = opts?.timeout ?? 500;
const count = opts?.count && opts.count > 1 ? opts.count : 1;
const pinger = new plugins.smartping.Smartping();
@ -85,11 +80,7 @@ export class SmartNetwork {
const min = valid.length ? Math.min(...valid) : NaN;
const max = valid.length ? Math.max(...valid) : NaN;
const avg = valid.length ? stats.average(valid) : NaN;
const stddev = valid.length
? Math.sqrt(
stats.average(valid.map((v) => (v - avg) ** 2)),
)
: NaN;
const stddev = valid.length ? Math.sqrt(stats.average(valid.map((v) => (v - avg) ** 2))) : NaN;
const packetLoss = ((count - aliveCount) / count) * 100;
return {
host,
@ -168,7 +159,9 @@ export class SmartNetwork {
*/
public async isRemotePortAvailable(
target: string,
portOrOpts?: number | { port?: number; protocol?: 'tcp' | 'udp'; timeout?: number; retries?: number },
portOrOpts?:
| number
| { port?: number; protocol?: 'tcp' | 'udp'; timeout?: number; retries?: number },
): Promise<boolean> {
let hostPart: string;
let port: number | undefined;
@ -252,7 +245,9 @@ export class SmartNetwork {
/**
* Resolve DNS records (A, AAAA, MX)
*/
public async resolveDns(host: string): Promise<{ A: string[]; AAAA: string[]; MX: { exchange: string; priority: number }[] }> {
public async resolveDns(
host: string,
): Promise<{ A: string[]; AAAA: string[]; MX: { exchange: string; priority: number }[] }> {
try {
const dns = await import('dns');
const { resolve4, resolve6, resolveMx } = dns.promises;
@ -316,14 +311,10 @@ export class SmartNetwork {
const { exec } = await import('child_process');
const cmd = `traceroute -n -m ${maxHops} ${host}`;
const stdout: string = await new Promise((resolve, reject) => {
exec(
cmd,
{ encoding: 'utf8', timeout },
(err, stdout) => {
if (err) return reject(err);
resolve(stdout);
},
);
exec(cmd, { encoding: 'utf8', timeout }, (err, stdout) => {
if (err) return reject(err);
resolve(stdout);
});
});
const hops: Hop[] = [];
for (const raw of stdout.split('\n')) {