@push.rocks/smartnetwork 4.9.0 → 4.10.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.
@@ -5,7 +5,10 @@ import { IpIntelligence, type IIpIntelligenceResult } from './smartnetwork.class
5
5
  import { DomainIntelligence, type IDomainIntelligenceResult } from './smartnetwork.classes.domainintelligence.js';
6
6
  import { getLogger } from './logging.js';
7
7
  import { NetworkError } from './errors.js';
8
- import { RustNetworkBridge } from './smartnetwork.classes.rustbridge.js';
8
+ import {
9
+ RustNetworkBridge,
10
+ RustNetworkBridgeActivationCleanupError,
11
+ } from './smartnetwork.classes.rustbridge.js';
9
12
 
10
13
  /** Type alias for the shared Smartdns client instance */
11
14
  type TSmartdnsClient = InstanceType<typeof plugins.smartdns.dnsClientMod.Smartdns>;
@@ -39,6 +42,8 @@ export interface IFindFreePortOptions {
39
42
  exclude?: number[];
40
43
  }
41
44
 
45
+ type TBridgeLifecycleState = 'idle' | 'active' | 'release-failed';
46
+
42
47
  /**
43
48
  * SmartNetwork simplifies actions within the network.
44
49
  * Uses a Rust binary for system-dependent operations (ping, traceroute, port scanning, gateway detection).
@@ -58,7 +63,11 @@ export class SmartNetwork {
58
63
  private options: SmartNetworkOptions;
59
64
  private cache: Map<string, { value: any; expiry: number }>;
60
65
  private rustBridge: RustNetworkBridge;
61
- private bridgeStarted = false;
66
+ private bridgeLifecycleState: TBridgeLifecycleState = 'idle';
67
+ private bridgeLifecycleQueue: Promise<void> = Promise.resolve();
68
+ private activeDnsOperations = 0;
69
+ private dnsDrainPromise: Promise<void> | null = null;
70
+ private resolveDnsDrain: (() => void) | null = null;
62
71
  private ipIntelligence: IpIntelligence | null = null;
63
72
  private domainIntelligence: DomainIntelligence | null = null;
64
73
  private dnsClient: TSmartdnsClient | null = null;
@@ -73,11 +82,21 @@ export class SmartNetwork {
73
82
  * Start the Rust binary bridge. Must be called before using ping, traceroute,
74
83
  * port scanning, or gateway operations. Safe to call multiple times.
75
84
  */
76
- public async start(): Promise<void> {
77
- if (!this.bridgeStarted) {
78
- await this.rustBridge.start();
79
- this.bridgeStarted = true;
80
- }
85
+ public start(): Promise<void> {
86
+ return this.enqueueBridgeLifecycle(async () => {
87
+ if (this.bridgeLifecycleState === 'release-failed') {
88
+ throw new Error('SmartNetwork cleanup must succeed before it can start again');
89
+ }
90
+ try {
91
+ await this.rustBridge.start(this);
92
+ this.bridgeLifecycleState = 'active';
93
+ } catch (error) {
94
+ if (error instanceof RustNetworkBridgeActivationCleanupError) {
95
+ this.bridgeLifecycleState = 'release-failed';
96
+ }
97
+ throw error;
98
+ }
99
+ });
81
100
  }
82
101
 
83
102
  /**
@@ -86,35 +105,100 @@ export class SmartNetwork {
86
105
  * Rust-backed features, otherwise the smartdns Rust backend may keep
87
106
  * the event loop alive.
88
107
  */
89
- public async stop(): Promise<void> {
90
- if (this.bridgeStarted) {
91
- await this.rustBridge.stop();
92
- this.bridgeStarted = false;
108
+ public stop(): Promise<void> {
109
+ return this.enqueueBridgeLifecycle(async () => {
110
+ await this.waitForDnsOperations();
111
+
112
+ let bridgeError: unknown;
113
+ if (this.bridgeLifecycleState !== 'idle') {
114
+ try {
115
+ await this.rustBridge.stop(this);
116
+ } catch (error) {
117
+ bridgeError = error;
118
+ }
119
+ }
120
+
121
+ let dnsError: unknown;
122
+ if (this.dnsClient) {
123
+ try {
124
+ await this.dnsClient.terminate();
125
+ this.dnsClient = null;
126
+ // Intelligence instances hold a stale reference to the terminated
127
+ // client; drop them so the next call rebuilds with a fresh one.
128
+ this.ipIntelligence = null;
129
+ this.domainIntelligence = null;
130
+ } catch (error) {
131
+ dnsError = error;
132
+ }
133
+ }
134
+
135
+ if (bridgeError || dnsError) {
136
+ this.bridgeLifecycleState = 'release-failed';
137
+ } else {
138
+ this.bridgeLifecycleState = 'idle';
139
+ }
140
+
141
+ if (bridgeError && dnsError) {
142
+ throw new AggregateError([bridgeError, dnsError], 'SmartNetwork cleanup failed');
143
+ }
144
+ if (bridgeError) throw bridgeError;
145
+ if (dnsError) throw dnsError;
146
+ });
147
+ }
148
+
149
+ private enqueueBridgeLifecycle(operationArg: () => Promise<void>): Promise<void> {
150
+ const result = this.bridgeLifecycleQueue.then(operationArg);
151
+ this.bridgeLifecycleQueue = result.catch(() => {});
152
+ return result;
153
+ }
154
+
155
+ private async withDnsClient<T>(
156
+ operationArg: (dnsClientArg: TSmartdnsClient) => Promise<T>,
157
+ ): Promise<T> {
158
+ let dnsClient: TSmartdnsClient;
159
+ await this.enqueueBridgeLifecycle(async () => {
160
+ if (this.bridgeLifecycleState === 'release-failed') {
161
+ throw new Error('SmartNetwork cleanup must succeed before DNS can be used again');
162
+ }
163
+ dnsClient = this.ensureDnsClient();
164
+ this.activeDnsOperations++;
165
+ });
166
+
167
+ try {
168
+ return await operationArg(dnsClient!);
169
+ } finally {
170
+ this.activeDnsOperations--;
171
+ if (this.activeDnsOperations === 0 && this.resolveDnsDrain) {
172
+ const resolve = this.resolveDnsDrain;
173
+ this.resolveDnsDrain = null;
174
+ this.dnsDrainPromise = null;
175
+ resolve();
176
+ }
93
177
  }
94
- if (this.dnsClient) {
95
- this.dnsClient.destroy();
96
- this.dnsClient = null;
97
- // Intelligence instances hold a stale reference to the destroyed
98
- // client; drop them so the next call rebuilds with a fresh one.
99
- this.ipIntelligence = null;
100
- this.domainIntelligence = null;
178
+ }
179
+
180
+ private waitForDnsOperations(): Promise<void> {
181
+ if (this.activeDnsOperations === 0) return Promise.resolve();
182
+ if (!this.dnsDrainPromise) {
183
+ this.dnsDrainPromise = new Promise<void>((resolve) => {
184
+ this.resolveDnsDrain = resolve;
185
+ });
101
186
  }
187
+ return this.dnsDrainPromise;
102
188
  }
103
189
 
104
190
  /**
105
191
  * Ensure the Rust bridge is running before sending commands.
106
192
  */
107
193
  private async ensureBridge(): Promise<void> {
108
- if (!this.bridgeStarted) {
109
- await this.start();
110
- }
194
+ await this.start();
111
195
  }
112
196
 
113
197
  /**
114
198
  * Lazily create the shared Smartdns client. The Rust backend inside
115
199
  * Smartdns is only spawned on first query that requires it (NS/MX/SOA
116
200
  * with prefer-system strategy, or any query with doh/udp strategy).
117
- * The client is destroyed by stop().
201
+ * The client is terminated by stop().
118
202
  */
119
203
  private ensureDnsClient(): TSmartdnsClient {
120
204
  if (!this.dnsClient) {
@@ -348,31 +432,31 @@ export class SmartNetwork {
348
432
  /**
349
433
  * Resolve DNS records (A, AAAA, MX) via the shared smartdns client.
350
434
  * The client is lifecycle-managed by start()/stop() — MX queries spawn
351
- * the smartdns Rust bridge, which is torn down by stop().
435
+ * the smartdns Rust bridge, whose termination is confirmed by stop().
352
436
  */
353
437
  public async resolveDns(
354
438
  host: string,
355
439
  ): Promise<{ A: string[]; AAAA: string[]; MX: { exchange: string; priority: number }[] }> {
356
440
  try {
357
- const dnsClient = this.ensureDnsClient();
358
-
359
- const [aRecords, aaaaRecords, mxRecords] = await Promise.all([
360
- dnsClient.getRecordsA(host).catch((): any[] => []),
361
- dnsClient.getRecordsAAAA(host).catch((): any[] => []),
362
- dnsClient.getRecords(host, 'MX').catch((): any[] => []),
363
- ]);
364
-
365
- const A = aRecords.map((record: any) => record.value);
366
- const AAAA = aaaaRecords.map((record: any) => record.value);
367
- const MX = mxRecords.map((record: any) => {
368
- const parts = record.value.split(' ');
369
- return {
370
- priority: parseInt(parts[0], 10),
371
- exchange: parts[1] || '',
372
- };
441
+ return await this.withDnsClient(async (dnsClient) => {
442
+ const [aRecords, aaaaRecords, mxRecords] = await Promise.all([
443
+ dnsClient.getRecordsA(host).catch((): any[] => []),
444
+ dnsClient.getRecordsAAAA(host).catch((): any[] => []),
445
+ dnsClient.getRecords(host, 'MX').catch((): any[] => []),
446
+ ]);
447
+
448
+ const A = aRecords.map((record: any) => record.value);
449
+ const AAAA = aaaaRecords.map((record: any) => record.value);
450
+ const MX = mxRecords.map((record: any) => {
451
+ const parts = record.value.split(' ');
452
+ return {
453
+ priority: parseInt(parts[0], 10),
454
+ exchange: parts[1] || '',
455
+ };
456
+ });
457
+
458
+ return { A, AAAA, MX };
373
459
  });
374
-
375
- return { A, AAAA, MX };
376
460
  } catch (err: any) {
377
461
  throw new NetworkError(err.message, err.code);
378
462
  }
@@ -438,13 +522,15 @@ export class SmartNetwork {
438
522
  * Pure TS, no Rust needed.
439
523
  */
440
524
  public async getIpIntelligence(ip: string): Promise<IIpIntelligenceResult> {
441
- if (!this.ipIntelligence) {
442
- this.ipIntelligence = new IpIntelligence({
443
- dnsClient: this.ensureDnsClient(),
444
- timeout: this.options.ipIntelligenceTimeout,
445
- });
446
- }
447
- const fetcher = () => this.ipIntelligence!.getIntelligence(ip);
525
+ const fetcher = () => this.withDnsClient(async (dnsClient) => {
526
+ if (!this.ipIntelligence) {
527
+ this.ipIntelligence = new IpIntelligence({
528
+ dnsClient,
529
+ timeout: this.options.ipIntelligenceTimeout,
530
+ });
531
+ }
532
+ return this.ipIntelligence.getIntelligence(ip);
533
+ });
448
534
  if (this.options.cacheTtl && this.options.cacheTtl > 0) {
449
535
  return this.getCached(`ipIntelligence:${ip}`, fetcher);
450
536
  }
@@ -457,10 +543,12 @@ export class SmartNetwork {
457
543
  * Rust needed.
458
544
  */
459
545
  public async getDomainIntelligence(domain: string): Promise<IDomainIntelligenceResult> {
460
- if (!this.domainIntelligence) {
461
- this.domainIntelligence = new DomainIntelligence({ dnsClient: this.ensureDnsClient() });
462
- }
463
- const fetcher = () => this.domainIntelligence!.getIntelligence(domain);
546
+ const fetcher = () => this.withDnsClient(async (dnsClient) => {
547
+ if (!this.domainIntelligence) {
548
+ this.domainIntelligence = new DomainIntelligence({ dnsClient });
549
+ }
550
+ return this.domainIntelligence.getIntelligence(domain);
551
+ });
464
552
  if (this.options.cacheTtl && this.options.cacheTtl > 0) {
465
553
  return this.getCached(`domainIntelligence:${domain}`, fetcher);
466
554
  }
@@ -8,8 +8,9 @@ export { os, https, perfHooks };
8
8
  // @pushrocks scope
9
9
  import * as smartdns from '@push.rocks/smartdns';
10
10
  import * as smartrust from '@push.rocks/smartrust';
11
+ import * as smartwhois from '@push.rocks/smartwhois';
11
12
 
12
- export { smartdns, smartrust };
13
+ export { smartdns, smartrust, smartwhois };
13
14
 
14
15
  // third party
15
16
  import * as maxmind from 'maxmind';