@serve.zone/dcrouter 9.1.4 → 9.1.6

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.
@@ -3,8 +3,15 @@ import type { OpsServer } from '../classes.opsserver.js';
3
3
  import * as interfaces from '../../../ts_interfaces/index.js';
4
4
  import { logBuffer, baseLogger } from '../../logger.js';
5
5
 
6
+ // Module-level singleton: the log push destination is added once and reuses
7
+ // the current OpsServer reference so it survives OpsServer restarts without
8
+ // accumulating duplicate destinations.
9
+ let logPushDestinationInstalled = false;
10
+ let currentOpsServerRef: OpsServer | null = null;
11
+
6
12
  export class LogsHandler {
7
13
  public typedrouter = new plugins.typedrequest.TypedRouter();
14
+ private activeStreamStops: Set<() => void> = new Set();
8
15
 
9
16
  constructor(private opsServerRef: OpsServer) {
10
17
  // Add this handler's router to the parent
@@ -12,7 +19,21 @@ export class LogsHandler {
12
19
  this.registerHandlers();
13
20
  this.setupLogPushDestination();
14
21
  }
15
-
22
+
23
+ /**
24
+ * Clean up all active log streams and deactivate the push destination.
25
+ * Called when OpsServer stops.
26
+ */
27
+ public cleanup(): void {
28
+ // Stop all active follow-mode log streams
29
+ for (const stop of this.activeStreamStops) {
30
+ stop();
31
+ }
32
+ this.activeStreamStops.clear();
33
+ // Deactivate the push destination (it stays registered but becomes a no-op)
34
+ currentOpsServerRef = null;
35
+ }
36
+
16
37
  private registerHandlers(): void {
17
38
  // Get Recent Logs Handler
18
39
  this.typedrouter.addTypedHandler(
@@ -27,16 +48,16 @@ export class LogsHandler {
27
48
  dataArg.search,
28
49
  dataArg.timeRange
29
50
  );
30
-
51
+
31
52
  return {
32
53
  logs,
33
- total: logs.length, // TODO: Implement proper total count
34
- hasMore: false, // TODO: Implement proper pagination
54
+ total: logs.length,
55
+ hasMore: false,
35
56
  };
36
57
  }
37
58
  )
38
59
  );
39
-
60
+
40
61
  // Get Log Stream Handler
41
62
  this.typedrouter.addTypedHandler(
42
63
  new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetLogStream>(
@@ -44,7 +65,7 @@ export class LogsHandler {
44
65
  async (dataArg, toolsArg) => {
45
66
  // Create a virtual stream for log streaming
46
67
  const virtualStream = new plugins.typedrequest.VirtualStream<Uint8Array>();
47
-
68
+
48
69
  // Set up log streaming
49
70
  const streamLogs = this.setupLogStream(
50
71
  virtualStream,
@@ -52,20 +73,21 @@ export class LogsHandler {
52
73
  dataArg.filters?.category,
53
74
  dataArg.follow
54
75
  );
55
-
76
+
56
77
  // Start streaming
57
78
  streamLogs.start();
58
-
59
- // VirtualStream handles cleanup automatically
60
-
79
+
80
+ // Track the stop function so we can clean up on shutdown
81
+ this.activeStreamStops.add(streamLogs.stop);
82
+
61
83
  return {
62
- logStream: virtualStream as any, // Cast to IVirtualStream interface
84
+ logStream: virtualStream as any,
63
85
  };
64
86
  }
65
87
  )
66
88
  );
67
89
  }
68
-
90
+
69
91
  private static mapLogLevel(smartlogLevel: string): 'debug' | 'info' | 'warn' | 'error' {
70
92
  switch (smartlogLevel) {
71
93
  case 'silly':
@@ -165,18 +187,30 @@ export class LogsHandler {
165
187
 
166
188
  return mapped;
167
189
  }
168
-
190
+
169
191
  /**
170
192
  * Add a log destination to the base logger that pushes entries
171
193
  * to all connected ops_dashboard TypedSocket clients.
194
+ *
195
+ * Uses a module-level singleton so the destination is added only once,
196
+ * even across OpsServer restart cycles. The destination reads
197
+ * `currentOpsServerRef` dynamically so it always uses the active server.
172
198
  */
173
199
  private setupLogPushDestination(): void {
174
- const opsServerRef = this.opsServerRef;
200
+ // Update the module-level reference so the existing destination uses the new server
201
+ currentOpsServerRef = this.opsServerRef;
202
+
203
+ if (logPushDestinationInstalled) {
204
+ return; // destination already registered — just updated the ref
205
+ }
206
+ logPushDestinationInstalled = true;
175
207
 
176
208
  baseLogger.addLogDestination({
177
209
  async handleLog(logPackage: any) {
178
- // Access the TypedSocket server instance from OpsServer
179
- const typedsocket = opsServerRef.server?.typedserver?.typedsocket;
210
+ const opsServer = currentOpsServerRef;
211
+ if (!opsServer) return;
212
+
213
+ const typedsocket = opsServer.server?.typedserver?.typedsocket;
180
214
  if (!typedsocket) return;
181
215
 
182
216
  let connections: any[];
@@ -220,8 +254,18 @@ export class LogsHandler {
220
254
  stop: () => void;
221
255
  } {
222
256
  let intervalId: NodeJS.Timeout | null = null;
257
+ let stopped = false;
223
258
  let logIndex = 0;
224
-
259
+
260
+ const stop = () => {
261
+ stopped = true;
262
+ if (intervalId) {
263
+ clearInterval(intervalId);
264
+ intervalId = null;
265
+ }
266
+ this.activeStreamStops.delete(stop);
267
+ };
268
+
225
269
  const start = () => {
226
270
  if (!follow) {
227
271
  // Send existing logs and close
@@ -236,13 +280,19 @@ export class LogsHandler {
236
280
  const encoder = new TextEncoder();
237
281
  virtualStream.sendData(encoder.encode(logData));
238
282
  });
239
- // VirtualStream doesn't have end() method - it closes automatically
240
283
  });
241
284
  return;
242
285
  }
243
-
286
+
244
287
  // For follow mode, simulate real-time log streaming
245
288
  intervalId = setInterval(async () => {
289
+ if (stopped) {
290
+ // Guard: clear interval if stop() was called between ticks
291
+ clearInterval(intervalId!);
292
+ intervalId = null;
293
+ return;
294
+ }
295
+
246
296
  const categories: Array<'smtp' | 'dns' | 'security' | 'system' | 'email'> = ['smtp', 'dns', 'security', 'system', 'email'];
247
297
  const levels: Array<'debug' | 'info' | 'warn' | 'error'> = ['info', 'warn', 'error', 'debug'];
248
298
 
@@ -266,30 +316,21 @@ export class LogsHandler {
266
316
  const logData = JSON.stringify(logEntry);
267
317
  const encoder = new TextEncoder();
268
318
  try {
269
- await virtualStream.sendData(encoder.encode(logData));
319
+ // Use a timeout to detect hung streams (sendData can hang if the
320
+ // VirtualStream's keepAlive loop has ended)
321
+ await Promise.race([
322
+ virtualStream.sendData(encoder.encode(logData)),
323
+ new Promise<never>((_, reject) =>
324
+ setTimeout(() => reject(new Error('stream send timeout')), 10_000)
325
+ ),
326
+ ]);
270
327
  } catch {
271
- // Stream closed or errored — clean up to prevent interval leak
272
- clearInterval(intervalId!);
273
- intervalId = null;
328
+ // Stream closed, errored, or timed out — clean up
329
+ stop();
274
330
  }
275
- }, 2000); // Send a log every 2 seconds
276
-
277
- // TODO: Hook into actual logger events
278
- // logger.on('log', (logEntry) => {
279
- // if (matchesCriteria(logEntry, level, service)) {
280
- // virtualStream.sendData(formatLogEntry(logEntry));
281
- // }
282
- // });
283
- };
284
-
285
- const stop = () => {
286
- if (intervalId) {
287
- clearInterval(intervalId);
288
- intervalId = null;
289
- }
290
- // TODO: Unhook from logger events
331
+ }, 2000);
291
332
  };
292
-
333
+
293
334
  return { start, stop };
294
335
  }
295
- }
336
+ }
@@ -15,6 +15,7 @@ export class TunnelManager {
15
15
  private manager: RemoteIngressManager;
16
16
  private config: ITunnelManagerConfig;
17
17
  private edgeStatuses: Map<string, IRemoteIngressStatus> = new Map();
18
+ private reconcileInterval: ReturnType<typeof setInterval> | null = null;
18
19
 
19
20
  constructor(manager: RemoteIngressManager, config: ITunnelManagerConfig = {}) {
20
21
  this.manager = manager;
@@ -65,16 +66,64 @@ export class TunnelManager {
65
66
 
66
67
  // Send allowed edges to the hub
67
68
  await this.syncAllowedEdges();
69
+
70
+ // Periodically reconcile with authoritative Rust hub status
71
+ this.reconcileInterval = setInterval(() => {
72
+ this.reconcile().catch(() => {});
73
+ }, 15_000);
68
74
  }
69
75
 
70
76
  /**
71
77
  * Stop the tunnel hub.
72
78
  */
73
79
  public async stop(): Promise<void> {
80
+ if (this.reconcileInterval) {
81
+ clearInterval(this.reconcileInterval);
82
+ this.reconcileInterval = null;
83
+ }
84
+ // Remove event listeners before stopping to prevent leaks
85
+ this.hub.removeAllListeners();
74
86
  await this.hub.stop();
75
87
  this.edgeStatuses.clear();
76
88
  }
77
89
 
90
+ /**
91
+ * Reconcile TS-side edge statuses with the authoritative Rust hub status.
92
+ * Overwrites event-derived activeTunnels with the real activeStreams count.
93
+ */
94
+ private async reconcile(): Promise<void> {
95
+ const hubStatus = await this.hub.getStatus();
96
+ if (!hubStatus || !hubStatus.connectedEdges) return;
97
+
98
+ const rustEdgeIds = new Set<string>();
99
+
100
+ for (const rustEdge of hubStatus.connectedEdges) {
101
+ rustEdgeIds.add(rustEdge.edgeId);
102
+ const existing = this.edgeStatuses.get(rustEdge.edgeId);
103
+ if (existing) {
104
+ existing.activeTunnels = rustEdge.activeStreams;
105
+ existing.lastHeartbeat = Date.now();
106
+ } else {
107
+ // Missed edgeConnected event — add entry
108
+ this.edgeStatuses.set(rustEdge.edgeId, {
109
+ edgeId: rustEdge.edgeId,
110
+ connected: true,
111
+ publicIp: null,
112
+ activeTunnels: rustEdge.activeStreams,
113
+ lastHeartbeat: Date.now(),
114
+ connectedAt: rustEdge.connectedAt * 1000,
115
+ });
116
+ }
117
+ }
118
+
119
+ // Remove entries for edges no longer connected in Rust (missed edgeDisconnected)
120
+ for (const edgeId of this.edgeStatuses.keys()) {
121
+ if (!rustEdgeIds.has(edgeId)) {
122
+ this.edgeStatuses.delete(edgeId);
123
+ }
124
+ }
125
+ }
126
+
78
127
  /**
79
128
  * Sync allowed edges from the manager to the hub.
80
129
  * Call this after creating/deleting/updating edges.
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '9.1.4',
6
+ version: '9.1.6',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  }