chia-agent 14.3.3 → 14.3.5

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/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
1
1
  # Changelog
2
2
 
3
+ ## [14.3.5]
4
+ ### Fixed
5
+ - Fixed an issue where it exhausts heap when logging an object with circular references
6
+
7
+ ## [14.3.4]
8
+ ### Breaking change
9
+ - Changed `daemon.connect()` API signature from `connect(url?, timeoutMs?)` to `connect(url?, options?)`
10
+ - All connection options are now consolidated into a single `options` parameter
11
+ - Existing code calling `connect()` without parameters remains compatible
12
+ ### Changed
13
+ - Improved logger system
14
+ - Added logger instance caching
15
+ - Added support for multiple loggers with per-instance configuration
16
+ - Added `NullWriter` for complete log suppression
17
+ - Updated `ConsoleWriter` to use proper console methods
18
+ - Added `trace` log level
19
+ - Added environment variable support: `LOG_LEVEL` and `LOG_SUPPRESS`
20
+ - Refactored API to use named loggers
21
+ - Added customizable log formatting with built-in formatters
22
+ - Auto-reconnection is now enabled by default
23
+ ### Fixed
24
+ - Fixed WebSocket message handling issues
25
+ - Added timeout handling for sent messages (default 30s)
26
+ - Fixed connection state check to use actual WebSocket readyState
27
+ - Preserved event listeners on connection close for reconnection support
28
+ - Added proper cleanup of message timeouts on response
29
+ - Fixed RPC agent to properly handle HTTP connections with explicit host/port
30
+ ### Added
31
+ - Added automatic retry/reconnection mechanism for WebSocket connection attempts
32
+ - Exponential backoff with configurable parameters
33
+ - Configurable retry parameters (maxAttempts, initialDelay, maxDelay, backoffMultiplier)
34
+ - Automatic re-subscription to services after reconnection
35
+ - Same retry configuration applies to both initial connection and reconnection
36
+ ### Internal change
37
+ - Removed `TDestination` type in favor of Writer-based approach
38
+
3
39
  ## [14.3.3]
4
40
  ### Changed
5
41
  - The default service name which [`Daemon`](./src/daemon/index.ts) client tries to register is now `wallet_ui`.
@@ -1720,6 +1756,7 @@ daemon.sendMessage(destination, get_block_record_by_height_command, data);
1720
1756
  Initial release.
1721
1757
 
1722
1758
  <!-- [Unreleased]: https://github.com/Chia-Mine/chia-agent/compare/v0.0.1...v0.0.2 -->
1759
+ [14.3.4]: https://github.com/Chia-Mine/chia-agent/compare/v14.3.3...v14.3.4
1723
1760
  [14.3.3]: https://github.com/Chia-Mine/chia-agent/compare/v14.3.2...v14.3.3
1724
1761
  [14.3.2]: https://github.com/Chia-Mine/chia-agent/compare/v14.3.1...v14.3.2
1725
1762
  [14.3.1]: https://github.com/Chia-Mine/chia-agent/compare/v14.3.0...v14.3.1
package/README.md CHANGED
@@ -113,6 +113,33 @@ setTimeout(async () => {
113
113
  */
114
114
  ```
115
115
 
116
+ ## Logging
117
+ The logger system supports multiple log levels and formatters:
118
+
119
+ ```js
120
+ const {setLogLevel, getLogger, setDefaultFormatter, simpleLogFormatter} = require("chia-agent");
121
+
122
+ // Set log level (case-insensitive)
123
+ setLogLevel("DEBUG"); // "error", "warning", "info", "debug", "trace", "none"
124
+
125
+ // Use built-in formatters
126
+ setDefaultFormatter(simpleLogFormatter); // Simple format without timestamp
127
+
128
+ // Custom formatter
129
+ const myFormatter = (context) => {
130
+ return `[${context.level}] ${context.message}`;
131
+ };
132
+ setDefaultFormatter(myFormatter);
133
+
134
+ // Named loggers
135
+ const logger = getLogger("MyModule");
136
+ logger.info("Module initialized");
137
+
138
+ // Per-logger configuration
139
+ logger.setLogLevel("debug");
140
+ logger.setFormatter(myFormatter);
141
+ ```
142
+
116
143
  ## API Reference
117
144
  [See Documentation here](https://github.com/Chia-Mine/chia-agent/blob/main/src/api/README.md)
118
145
 
package/daemon/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ /// <reference types="node" />
1
2
  import type { CloseEvent, ErrorEvent, MessageEvent, Event } from "ws";
2
3
  import * as WS from "ws";
3
4
  import { WsMessage } from "../api/ws";
@@ -7,12 +8,27 @@ export type WsEvent = Event | MessageEvent | ErrorEvent | CloseEvent;
7
8
  export type EventListener<T = WsEvent> = (ev: T) => unknown;
8
9
  type EventListenerOf<T> = T extends "open" ? EventListener<Event> : T extends "message" ? EventListener<MessageEvent> : T extends "error" ? EventListener<ErrorEvent> : T extends "close" ? EventListener<CloseEvent> : never;
9
10
  export type MessageListener<D extends WsMessage> = (msg: D) => unknown;
11
+ export interface RetryOptions {
12
+ maxAttempts?: number;
13
+ initialDelay?: number;
14
+ maxDelay?: number;
15
+ backoffMultiplier?: number;
16
+ }
17
+ export interface ConnectOptions {
18
+ timeoutMs?: number;
19
+ autoReconnect?: boolean;
20
+ retryOptions?: RetryOptions;
21
+ }
10
22
  export declare function getDaemon(serviceName?: string): Daemon;
11
23
  declare class Daemon {
12
24
  protected _socket: WS | null;
13
25
  protected _connectedUrl: string;
14
26
  protected _responseQueue: {
15
- [request_id: string]: (value: unknown) => void;
27
+ [request_id: string]: {
28
+ resolver: (value: unknown) => void;
29
+ rejecter: (error: unknown) => void;
30
+ timeout: NodeJS.Timeout;
31
+ };
16
32
  };
17
33
  protected _openEventListeners: Array<(e: Event) => unknown>;
18
34
  protected _messageEventListeners: Array<(e: MessageEvent) => unknown>;
@@ -23,17 +39,25 @@ declare class Daemon {
23
39
  protected _onClosePromise: (() => unknown) | undefined;
24
40
  protected _subscriptions: string[];
25
41
  protected _serviceName: string;
42
+ protected _autoReconnect: boolean;
43
+ protected _retryOptions: Required<RetryOptions>;
44
+ protected _timeoutMs: number;
45
+ protected _reconnectAttempts: number;
46
+ protected _reconnectTimer: NodeJS.Timeout | null;
47
+ protected _lastConnectionUrl: string;
48
+ protected _isReconnecting: boolean;
26
49
  get connected(): boolean;
27
50
  get closing(): boolean;
28
51
  constructor(serviceName?: string);
52
+ protected onRejection(e: unknown): null;
29
53
  /**
30
54
  * Connect to local daemon via websocket.
31
- * @param daemonServerURL
32
- * @param timeoutMs
55
+ * @param daemonServerURL - The websocket URL to connect to. If not provided, uses config values.
56
+ * @param options - Connection options including timeout, reconnect settings, and retry settings
33
57
  */
34
- connect(daemonServerURL?: string, timeoutMs?: number): Promise<boolean>;
58
+ connect(daemonServerURL?: string, options?: ConnectOptions): Promise<boolean>;
35
59
  close(): Promise<unknown>;
36
- sendMessage<M = unknown>(destination: string, command: string, data?: Record<string, unknown>): Promise<M>;
60
+ sendMessage<M = unknown>(destination: string, command: string, data?: Record<string, unknown>, timeoutMs?: number): Promise<M>;
37
61
  createMessageTemplate(command: string, destination: string, data: Record<string, unknown>): {
38
62
  command: string;
39
63
  data: Record<string, unknown>;
@@ -60,6 +84,7 @@ declare class Daemon {
60
84
  protected onClose(event: CloseEvent): void;
61
85
  protected onPing(): void;
62
86
  protected onPong(): void;
87
+ protected _attemptReconnection(previousSubscriptions: string[]): void;
63
88
  }
64
89
  export type TDaemon = InstanceType<typeof Daemon>;
65
90
  export {};
package/daemon/index.js CHANGED
@@ -10,17 +10,26 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.getDaemon = void 0;
13
+ const WS = require("ws");
13
14
  const crypto_1 = require("crypto");
14
15
  const logger_1 = require("../logger");
15
16
  const connection_1 = require("./connection");
16
17
  const index_1 = require("../config/index");
17
18
  const DEFAULT_SERVICE_NAME = "wallet_ui";
19
+ const DEFAULT_AUTO_RECONNECT = true;
20
+ const DEFAULT_TIMEOUT_MS = 30000;
21
+ const DEFAULT_RETRY_OPTIONS = {
22
+ maxAttempts: 5,
23
+ initialDelay: 1000,
24
+ maxDelay: 30000,
25
+ backoffMultiplier: 1.5,
26
+ };
18
27
  let daemon = null;
19
28
  function getDaemon(serviceName) {
20
29
  if (daemon) {
21
30
  return daemon;
22
31
  }
23
- return daemon = new Daemon(serviceName);
32
+ return (daemon = new Daemon(serviceName));
24
33
  }
25
34
  exports.getDaemon = getDaemon;
26
35
  // Gracefully disconnect from remote daemon server on Ctrl+C.
@@ -48,7 +57,9 @@ const onProcessExit = () => {
48
57
  process.addListener("SIGINT", onProcessExit);
49
58
  class Daemon {
50
59
  get connected() {
51
- return Boolean(this._connectedUrl);
60
+ return (Boolean(this._connectedUrl) &&
61
+ this._socket !== null &&
62
+ this._socket.readyState === WS.OPEN);
52
63
  }
53
64
  get closing() {
54
65
  return this._closing;
@@ -65,22 +76,50 @@ class Daemon {
65
76
  this._closing = false;
66
77
  this._subscriptions = [];
67
78
  this._serviceName = DEFAULT_SERVICE_NAME;
79
+ this._autoReconnect = DEFAULT_AUTO_RECONNECT;
80
+ this._retryOptions = DEFAULT_RETRY_OPTIONS;
81
+ this._timeoutMs = DEFAULT_TIMEOUT_MS;
82
+ this._reconnectAttempts = 0;
83
+ this._reconnectTimer = null;
84
+ this._lastConnectionUrl = "";
85
+ this._isReconnecting = false;
68
86
  this.onOpen = this.onOpen.bind(this);
69
87
  this.onError = this.onError.bind(this);
70
88
  this.onMessage = this.onMessage.bind(this);
71
89
  this.onClose = this.onClose.bind(this);
72
90
  this.onPing = this.onPing.bind(this);
73
91
  this.onPong = this.onPong.bind(this);
92
+ this.onRejection = this.onRejection.bind(this);
74
93
  if (serviceName) {
75
94
  this._serviceName = serviceName;
76
95
  }
77
96
  }
97
+ onRejection(e) {
98
+ if (typeof e === "string") {
99
+ (0, logger_1.getLogger)().error(`Error: ${e}`);
100
+ }
101
+ else if (e instanceof Error) {
102
+ (0, logger_1.getLogger)().error(`Error ${e.name}: ${e.message}`);
103
+ if (e.stack) {
104
+ (0, logger_1.getLogger)().error(e.stack);
105
+ }
106
+ }
107
+ else {
108
+ try {
109
+ (0, logger_1.getLogger)().error(`Error: ${JSON.stringify(e)}`);
110
+ }
111
+ catch (_e) {
112
+ (0, logger_1.getLogger)().error("Unknown error");
113
+ }
114
+ }
115
+ return null;
116
+ }
78
117
  /**
79
118
  * Connect to local daemon via websocket.
80
- * @param daemonServerURL
81
- * @param timeoutMs
119
+ * @param daemonServerURL - The websocket URL to connect to. If not provided, uses config values.
120
+ * @param options - Connection options including timeout, reconnect settings, and retry settings
82
121
  */
83
- connect(daemonServerURL, timeoutMs) {
122
+ connect(daemonServerURL, options) {
84
123
  return __awaiter(this, void 0, void 0, function* () {
85
124
  if (!daemonServerURL) {
86
125
  const config = (0, index_1.getConfig)();
@@ -88,6 +127,19 @@ class Daemon {
88
127
  const daemonPort = config["/ui/daemon_port"];
89
128
  daemonServerURL = `wss://${daemonHost}:${daemonPort}`;
90
129
  }
130
+ // Extract options with defaults
131
+ const timeoutMs = (options === null || options === void 0 ? void 0 : options.timeoutMs) || this._timeoutMs;
132
+ // Store timeout for reconnection attempts
133
+ if ((options === null || options === void 0 ? void 0 : options.timeoutMs) !== undefined) {
134
+ this._timeoutMs = options.timeoutMs;
135
+ }
136
+ // Update settings from options
137
+ if ((options === null || options === void 0 ? void 0 : options.autoReconnect) !== undefined) {
138
+ this._autoReconnect = options.autoReconnect;
139
+ }
140
+ if ((options === null || options === void 0 ? void 0 : options.retryOptions) !== undefined) {
141
+ this._retryOptions = Object.assign(Object.assign({}, DEFAULT_RETRY_OPTIONS), options.retryOptions);
142
+ }
91
143
  if (this._connectedUrl === daemonServerURL) {
92
144
  return true;
93
145
  }
@@ -95,33 +147,65 @@ class Daemon {
95
147
  (0, logger_1.getLogger)().error("Connection is still active. Please close living connection first");
96
148
  return false;
97
149
  }
98
- (0, logger_1.getLogger)().debug(`Opening websocket connection to ${daemonServerURL}`);
99
- const result = yield (0, connection_1.open)(daemonServerURL, timeoutMs);
100
- this._socket = result.ws;
101
- this._socket.on("error", this.onError);
102
- this._socket.addEventListener("message", this.onMessage);
103
- this._socket.on("close", this.onClose);
104
- this._socket.on("ping", this.onPing);
105
- this._socket.on("pong", this.onPong);
106
- yield this.onOpen(result.openEvent, daemonServerURL);
107
- return true;
150
+ // Store URL for reconnection
151
+ this._lastConnectionUrl = daemonServerURL;
152
+ // Attempt connection with retry logic
153
+ let lastError;
154
+ for (let attempt = 1; attempt <= this._retryOptions.maxAttempts; attempt++) {
155
+ (0, logger_1.getLogger)().debug(`Opening websocket connection to ${daemonServerURL} (attempt ${attempt}/${this._retryOptions.maxAttempts})`);
156
+ const result = yield (0, connection_1.open)(daemonServerURL, timeoutMs).catch((error) => {
157
+ lastError = error;
158
+ return null;
159
+ });
160
+ if (result) {
161
+ this._socket = result.ws;
162
+ this._socket.on("error", this.onError);
163
+ this._socket.addEventListener("message", this.onMessage);
164
+ this._socket.on("close", this.onClose);
165
+ this._socket.on("ping", this.onPing);
166
+ this._socket.on("pong", this.onPong);
167
+ // Call onOpen but don't check result (maintain original behavior)
168
+ yield this.onOpen(result.openEvent, daemonServerURL).catch(this.onRejection);
169
+ return true;
170
+ }
171
+ // If not the last attempt, wait before retrying
172
+ if (attempt < this._retryOptions.maxAttempts) {
173
+ const delay = Math.min(this._retryOptions.initialDelay *
174
+ Math.pow(this._retryOptions.backoffMultiplier, attempt - 1), this._retryOptions.maxDelay);
175
+ (0, logger_1.getLogger)().info(`Connection attempt ${attempt} failed. Retrying in ${delay}ms...`);
176
+ yield new Promise((resolve) => setTimeout(resolve, delay));
177
+ }
178
+ }
179
+ // All attempts failed
180
+ (0, logger_1.getLogger)().error(`Failed to connect after ${this._retryOptions.maxAttempts} attempts`);
181
+ this.onRejection(lastError);
182
+ return false;
108
183
  });
109
184
  }
110
185
  close() {
111
186
  return __awaiter(this, void 0, void 0, function* () {
112
- return new Promise(((resolve) => {
187
+ return new Promise((resolve) => {
113
188
  if (this._closing || !this._socket) {
114
189
  return;
115
190
  }
191
+ // Cancel any pending reconnection
192
+ if (this._reconnectTimer) {
193
+ clearTimeout(this._reconnectTimer);
194
+ this._reconnectTimer = null;
195
+ }
196
+ // Disable reconnection for manual close
197
+ this._autoReconnect = false;
198
+ this._isReconnecting = false;
116
199
  (0, logger_1.getLogger)().debug("Closing web socket connection");
117
200
  this._socket.close();
118
201
  this._closing = true;
202
+ this._connectedUrl = "";
119
203
  this._onClosePromise = resolve; // Resolved in onClose function.
120
- }));
204
+ });
121
205
  });
122
206
  }
123
- sendMessage(destination, command, data) {
124
- return __awaiter(this, void 0, void 0, function* () {
207
+ sendMessage(destination_1, command_1, data_1) {
208
+ return __awaiter(this, arguments, void 0, function* (destination, command, data, timeoutMs = 30000) {
125
209
  return new Promise((resolve, reject) => {
126
210
  if (!this.connected || !this._socket) {
127
211
  (0, logger_1.getLogger)().error("Tried to send message without active connection");
@@ -130,13 +214,32 @@ class Daemon {
130
214
  }
131
215
  const message = this.createMessageTemplate(command, destination, data || {});
132
216
  const reqId = message.request_id;
133
- this._responseQueue[reqId] = resolve;
134
- (0, logger_1.getLogger)().debug(`Sending message. dest=${destination} command=${command} reqId=${reqId}`);
217
+ // Set up timeout
218
+ const timeout = setTimeout(() => {
219
+ const entry = this._responseQueue[reqId];
220
+ if (entry) {
221
+ delete this._responseQueue[reqId];
222
+ entry.rejecter(new Error(`Message timeout after ${timeoutMs}ms. dest=${destination} command=${command} reqId=${reqId}`));
223
+ }
224
+ }, timeoutMs);
225
+ this._responseQueue[reqId] = {
226
+ resolver: resolve,
227
+ rejecter: reject,
228
+ timeout,
229
+ };
230
+ (0, logger_1.getLogger)().debug(`Sending Ws message. dest=${destination} command=${command} reqId=${reqId}`);
135
231
  const messageStr = JSON.stringify(message);
136
232
  this._socket.send(messageStr, (err) => {
137
233
  if (err) {
138
234
  (0, logger_1.getLogger)().error(`Error while sending message: ${messageStr}`);
139
235
  (0, logger_1.getLogger)().error(JSON.stringify(err));
236
+ // Clean up on send error
237
+ const entry = this._responseQueue[reqId];
238
+ if (entry) {
239
+ clearTimeout(entry.timeout);
240
+ delete this._responseQueue[reqId];
241
+ reject(err);
242
+ }
140
243
  }
141
244
  });
142
245
  });
@@ -158,7 +261,7 @@ class Daemon {
158
261
  (0, logger_1.getLogger)().error(`Tried to subscribe '${service}' without active connection`);
159
262
  throw new Error("Not connected");
160
263
  }
161
- if (this._subscriptions.findIndex(s => s === service) > -1) {
264
+ if (this._subscriptions.findIndex((s) => s === service) > -1) {
162
265
  return {
163
266
  command: "register_service",
164
267
  data: { success: true },
@@ -169,12 +272,17 @@ class Daemon {
169
272
  };
170
273
  }
171
274
  let error;
172
- const result = yield this.sendMessage("daemon", "register_service", { service }).catch(e => {
275
+ const result = yield this.sendMessage("daemon", "register_service", {
276
+ service,
277
+ }).catch((e) => {
173
278
  error = e;
174
279
  return null;
175
280
  });
176
281
  if (error || !result) {
177
282
  (0, logger_1.getLogger)().error("Failed to register agent service to daemon");
283
+ (0, logger_1.getLogger)().error(error instanceof Error
284
+ ? `${error.name}: ${error.message}`
285
+ : JSON.stringify(error));
178
286
  throw new Error("Subscribe failed");
179
287
  }
180
288
  this._subscriptions.push(service);
@@ -245,7 +353,7 @@ class Daemon {
245
353
  if (!listeners) {
246
354
  return;
247
355
  }
248
- const index = listeners.findIndex(l => l === listener);
356
+ const index = listeners.findIndex((l) => l === listener);
249
357
  if (index > -1) {
250
358
  listeners.splice(index, 1);
251
359
  }
@@ -257,13 +365,13 @@ class Daemon {
257
365
  return __awaiter(this, void 0, void 0, function* () {
258
366
  (0, logger_1.getLogger)().info("ws connection opened");
259
367
  this._connectedUrl = url;
260
- this._openEventListeners.forEach(l => l(event));
368
+ this._openEventListeners.forEach((l) => l(event));
261
369
  return this.subscribe(this._serviceName);
262
370
  });
263
371
  }
264
372
  onError(error) {
265
- (0, logger_1.getLogger)().error(`ws connection error: ${error.message}`);
266
- this._errorEventListeners.forEach(l => l(error));
373
+ (0, logger_1.getLogger)().error(`ws connection error: ${error.type} ${error.target} ${error.error} ${error.message}`);
374
+ this._errorEventListeners.forEach((l) => l(error));
267
375
  }
268
376
  onMessage(event) {
269
377
  let payload;
@@ -275,28 +383,34 @@ class Daemon {
275
383
  ({ request_id, origin, command } = payload);
276
384
  }
277
385
  catch (err) {
278
- (0, logger_1.getLogger)().error(`Failed to parse message data: ${JSON.stringify(err)}`);
279
- (0, logger_1.getLogger)().error(`payload: ${event.data}`);
386
+ (0, logger_1.getLogger)().error(`Failed to parse ws message data: ${JSON.stringify(err)}`);
387
+ (0, logger_1.getLogger)().error(`ws payload: ${event.data}`);
280
388
  return;
281
389
  }
282
- (0, logger_1.getLogger)().debug(`Arrived message. origin=${origin} command=${command} reqId=${request_id}`);
283
- const resolver = this._responseQueue[request_id];
284
- if (resolver) {
390
+ const entry = this._responseQueue[request_id];
391
+ if (entry) {
392
+ clearTimeout(entry.timeout);
285
393
  delete this._responseQueue[request_id];
286
- resolver(payload);
394
+ (0, logger_1.getLogger)().debug(`Ws response received. origin=${origin} command=${command} reqId=${request_id}`);
395
+ entry.resolver(payload);
287
396
  }
288
- this._messageEventListeners.forEach(l => l(event));
397
+ else {
398
+ (0, logger_1.getLogger)().debug(`Ws message arrived. origin=${origin} command=${command} reqId=${request_id}`);
399
+ }
400
+ (0, logger_1.getLogger)().trace(`Ws message: ${JSON.stringify(payload)}`);
401
+ this._messageEventListeners.forEach((l) => l(event));
289
402
  for (const o in this._messageListeners) {
290
403
  if (!Object.prototype.hasOwnProperty.call(this._messageListeners, o)) {
291
404
  continue;
292
405
  }
293
406
  const listeners = this._messageListeners[o];
294
407
  if (origin === o || o === "all") {
295
- listeners.forEach(l => l(payload));
408
+ listeners.forEach((l) => l(payload));
296
409
  }
297
410
  }
298
411
  }
299
412
  onClose(event) {
413
+ const previousSubscriptions = [...this._subscriptions];
300
414
  if (this._socket) {
301
415
  this._socket.off("error", this.onError);
302
416
  this._socket.removeEventListener("message", this.onMessage);
@@ -308,13 +422,23 @@ class Daemon {
308
422
  this._closing = false;
309
423
  this._connectedUrl = "";
310
424
  this._subscriptions = [];
311
- this._closeEventListeners.forEach(l => l(event));
312
- this.clearAllEventListeners();
313
- (0, logger_1.getLogger)().info("Closed ws connection");
425
+ this._closeEventListeners.forEach((l) => l(event));
426
+ // Don't clear event listeners - preserve them for reconnection
427
+ // this.clearAllEventListeners();
428
+ (0, logger_1.getLogger)().info(`Closed ws connection. code:${event.code} wasClean:${event.wasClean} reason:${event.reason}`);
314
429
  if (this._onClosePromise) {
315
430
  this._onClosePromise();
316
431
  this._onClosePromise = undefined;
317
432
  }
433
+ // Attempt reconnection if enabled and not manually closed
434
+ if (this._autoReconnect &&
435
+ this._lastConnectionUrl &&
436
+ !this._isReconnecting &&
437
+ event.code !== 1000 // 1000 = normal closure
438
+ ) {
439
+ this._isReconnecting = true;
440
+ this._attemptReconnection(previousSubscriptions);
441
+ }
318
442
  }
319
443
  onPing() {
320
444
  (0, logger_1.getLogger)().debug("Received ping");
@@ -322,4 +446,63 @@ class Daemon {
322
446
  onPong() {
323
447
  (0, logger_1.getLogger)().debug("Received pong");
324
448
  }
449
+ _attemptReconnection(previousSubscriptions) {
450
+ if (this._reconnectAttempts >= this._retryOptions.maxAttempts) {
451
+ (0, logger_1.getLogger)().error(`Max reconnection attempts (${this._retryOptions.maxAttempts}) reached. Giving up.`);
452
+ this._isReconnecting = false;
453
+ this._reconnectAttempts = 0;
454
+ // Emit a custom event for max retries reached
455
+ const errorEvent = {
456
+ type: "error",
457
+ message: "Max reconnection attempts reached",
458
+ error: new Error("Max reconnection attempts reached"),
459
+ target: this._socket,
460
+ };
461
+ this._errorEventListeners.forEach((l) => l(errorEvent));
462
+ return;
463
+ }
464
+ const delay = Math.min(this._retryOptions.initialDelay *
465
+ Math.pow(this._retryOptions.backoffMultiplier, this._reconnectAttempts), this._retryOptions.maxDelay);
466
+ this._reconnectAttempts++;
467
+ (0, logger_1.getLogger)().info(`Attempting reconnection ${this._reconnectAttempts}/${this._retryOptions.maxAttempts} in ${delay}ms...`);
468
+ this._reconnectTimer = setTimeout(() => __awaiter(this, void 0, void 0, function* () {
469
+ this._reconnectTimer = null;
470
+ try {
471
+ const connected = yield this.connect(this._lastConnectionUrl, {
472
+ timeoutMs: this._timeoutMs,
473
+ autoReconnect: this._autoReconnect,
474
+ retryOptions: this._retryOptions,
475
+ });
476
+ if (connected) {
477
+ (0, logger_1.getLogger)().info("Reconnection successful");
478
+ this._reconnectAttempts = 0;
479
+ this._isReconnecting = false;
480
+ // Re-establish previous subscriptions
481
+ for (const service of previousSubscriptions) {
482
+ try {
483
+ yield this.subscribe(service);
484
+ (0, logger_1.getLogger)().debug(`Re-subscribed to ${service}`);
485
+ }
486
+ catch (e) {
487
+ (0, logger_1.getLogger)().error(`Failed to re-subscribe to ${service}: ${e}`);
488
+ }
489
+ }
490
+ // Emit successful reconnection event
491
+ const reconnectedEvent = {
492
+ type: "reconnected",
493
+ target: this._socket,
494
+ };
495
+ this._openEventListeners.forEach((l) => l(reconnectedEvent));
496
+ }
497
+ else {
498
+ // Connection failed, try again
499
+ this._attemptReconnection(previousSubscriptions);
500
+ }
501
+ }
502
+ catch (error) {
503
+ (0, logger_1.getLogger)().error(`Reconnection attempt failed: ${error}`);
504
+ this._attemptReconnection(previousSubscriptions);
505
+ }
506
+ }), delay);
507
+ }
325
508
  }
package/logger.d.ts CHANGED
@@ -1,22 +1,49 @@
1
- export type TLogLevel = "error" | "warning" | "info" | "debug" | "none";
2
- export type TDestination = "console";
3
- export type Writer = {
4
- write: (message: string) => void;
5
- };
6
- export declare function getLogLevel(): TLogLevel;
7
- export declare function setLogLevel(logLevel: TLogLevel): TLogLevel;
8
- export declare function getLogger(writer?: TDestination): Logger;
9
- declare class Logger {
10
- loglevel: TLogLevel;
11
- protected _writer: Writer;
12
- protected constructor(logLevel: TLogLevel, writer?: TDestination | Writer);
13
- static getLogger(logLevel: TLogLevel, writer?: TDestination): Logger;
1
+ export type TLogLevel = "error" | "warning" | "info" | "debug" | "trace" | "none";
2
+ export declare function isValidLogLevel(level: string): level is TLogLevel;
3
+ export declare function normalizeLogLevel(level: string): TLogLevel | null;
4
+ export interface Writer {
5
+ write: (message: string, level?: TLogLevel) => void;
6
+ }
7
+ export interface LogContext {
8
+ timestamp: Date;
9
+ level: TLogLevel;
10
+ loggerName: string;
11
+ message: string;
12
+ }
13
+ export type LogFormatter = (context: LogContext) => string;
14
+ export declare function setDefaultLogLevel(level: TLogLevel): void;
15
+ export declare function getDefaultLogLevel(): TLogLevel;
16
+ export declare function setDefaultWriter(writer: Writer): void;
17
+ export declare function setDefaultFormatter(formatter: LogFormatter): void;
18
+ export declare function getDefaultFormatter(): LogFormatter;
19
+ export declare const DEFAULT_LOGGER_NAME = "default";
20
+ export declare const defaultLogFormatter: LogFormatter;
21
+ export declare const simpleLogFormatter: LogFormatter;
22
+ export declare const jsonLogFormatter: LogFormatter;
23
+ export declare function getLogger(name?: string): Logger;
24
+ export declare function clearLoggerCache(): void;
25
+ export declare function createConsoleWriter(): Writer;
26
+ export declare function createNullWriter(): Writer;
27
+ export declare class Logger {
28
+ private _name;
29
+ private _logLevel;
30
+ private _writer;
31
+ private _formatter;
32
+ constructor(name: string, logLevel: TLogLevel, writer: Writer, formatter?: LogFormatter);
14
33
  setLogLevel(level: TLogLevel): void;
15
- shouldWrite(logLevel: TLogLevel): boolean;
16
- formatMessage(level: TLogLevel, body: string): string;
34
+ getLogLevel(): TLogLevel;
35
+ setWriter(writer: Writer): void;
36
+ getWriter(): Writer;
37
+ getName(): string;
38
+ setFormatter(formatter: LogFormatter): void;
39
+ getFormatter(): LogFormatter;
40
+ private _shouldWrite;
41
+ private _formatMessage;
42
+ trace(msg: any): void;
17
43
  debug(msg: any): void;
18
44
  info(msg: any): void;
19
45
  warning(msg: any): void;
20
46
  error(msg: any): void;
21
47
  }
22
- export {};
48
+ export declare function getLogLevel(): TLogLevel;
49
+ export declare function setLogLevel(level: TLogLevel): void;
package/logger.js CHANGED
@@ -1,23 +1,157 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getLogger = exports.setLogLevel = exports.getLogLevel = void 0;
3
+ exports.setLogLevel = exports.getLogLevel = exports.Logger = exports.createNullWriter = exports.createConsoleWriter = exports.clearLoggerCache = exports.getLogger = exports.jsonLogFormatter = exports.simpleLogFormatter = exports.defaultLogFormatter = exports.DEFAULT_LOGGER_NAME = exports.getDefaultFormatter = exports.setDefaultFormatter = exports.setDefaultWriter = exports.getDefaultLogLevel = exports.setDefaultLogLevel = exports.normalizeLogLevel = exports.isValidLogLevel = void 0;
4
+ const LOG_LEVELS = [
5
+ "error",
6
+ "warning",
7
+ "info",
8
+ "debug",
9
+ "trace",
10
+ "none",
11
+ ];
12
+ function isValidLogLevel(level) {
13
+ return LOG_LEVELS.includes(level.toLowerCase());
14
+ }
15
+ exports.isValidLogLevel = isValidLogLevel;
16
+ function normalizeLogLevel(level) {
17
+ const normalized = level.toLowerCase();
18
+ if (isValidLogLevel(normalized)) {
19
+ return normalized;
20
+ }
21
+ return null;
22
+ }
23
+ exports.normalizeLogLevel = normalizeLogLevel;
4
24
  const logPriority = {
5
25
  none: 9999,
6
- error: 4,
7
- warning: 3,
8
- info: 2,
9
- debug: 1,
26
+ error: 5,
27
+ warning: 4,
28
+ info: 3,
29
+ debug: 2,
30
+ trace: 1,
10
31
  };
11
32
  class ConsoleWriter {
12
- write(message) {
13
- console.log(message);
33
+ write(message, level) {
34
+ switch (level) {
35
+ case "error":
36
+ console.error(message);
37
+ break;
38
+ case "warning":
39
+ console.warn(message);
40
+ break;
41
+ case "info":
42
+ console.info(message);
43
+ break;
44
+ case "debug":
45
+ console.debug(message);
46
+ break;
47
+ case "trace":
48
+ console.trace(message);
49
+ break;
50
+ default:
51
+ console.log(message);
52
+ }
14
53
  }
15
54
  }
16
- let currentLogLevel = "error";
17
- function getLogLevel() { return currentLogLevel; }
18
- exports.getLogLevel = getLogLevel;
19
- function setLogLevel(logLevel) { return currentLogLevel = logLevel; }
20
- exports.setLogLevel = setLogLevel;
55
+ class NullWriter {
56
+ write(_message, _level) {
57
+ // Suppress all output
58
+ }
59
+ }
60
+ // Global defaults
61
+ let defaultLogLevel = "error";
62
+ if (process.env.LOG_LEVEL) {
63
+ const normalizedLevel = normalizeLogLevel(process.env.LOG_LEVEL);
64
+ if (normalizedLevel) {
65
+ defaultLogLevel = normalizedLevel;
66
+ }
67
+ else {
68
+ console.warn(`Invalid LOG_LEVEL environment variable: ${process.env.LOG_LEVEL}. Using default: error`);
69
+ }
70
+ }
71
+ let defaultWriter = process.env.LOG_SUPPRESS === "true" ? new NullWriter() : new ConsoleWriter();
72
+ // Logger instance cache
73
+ const loggerCache = new Map();
74
+ // Configuration functions for global defaults
75
+ function setDefaultLogLevel(level) {
76
+ // Normalize for JavaScript users who might pass uppercase
77
+ const normalized = normalizeLogLevel(level);
78
+ if (!normalized) {
79
+ throw new Error(`Invalid log level: ${level}. Valid levels are: ${LOG_LEVELS.join(", ")}`);
80
+ }
81
+ defaultLogLevel = normalized;
82
+ }
83
+ exports.setDefaultLogLevel = setDefaultLogLevel;
84
+ function getDefaultLogLevel() {
85
+ return defaultLogLevel;
86
+ }
87
+ exports.getDefaultLogLevel = getDefaultLogLevel;
88
+ function setDefaultWriter(writer) {
89
+ defaultWriter = writer;
90
+ }
91
+ exports.setDefaultWriter = setDefaultWriter;
92
+ function setDefaultFormatter(formatter) {
93
+ defaultFormatter = formatter;
94
+ }
95
+ exports.setDefaultFormatter = setDefaultFormatter;
96
+ function getDefaultFormatter() {
97
+ return defaultFormatter;
98
+ }
99
+ exports.getDefaultFormatter = getDefaultFormatter;
100
+ exports.DEFAULT_LOGGER_NAME = "default";
101
+ // Default formatter
102
+ const defaultLogFormatter = (context) => {
103
+ const timestamp = context.timestamp.toISOString();
104
+ const levelStr = context.level.toUpperCase();
105
+ const nameStr = context.loggerName === exports.DEFAULT_LOGGER_NAME
106
+ ? ""
107
+ : ` [${context.loggerName}]`;
108
+ return `${timestamp} [${levelStr}]${nameStr} - ${context.message}`;
109
+ };
110
+ exports.defaultLogFormatter = defaultLogFormatter;
111
+ // Simple formatter without timestamp
112
+ const simpleLogFormatter = (context) => {
113
+ const levelStr = context.level.toUpperCase();
114
+ const nameStr = context.loggerName === exports.DEFAULT_LOGGER_NAME
115
+ ? ""
116
+ : `[${context.loggerName}] `;
117
+ return `[${levelStr}] ${nameStr}${context.message}`;
118
+ };
119
+ exports.simpleLogFormatter = simpleLogFormatter;
120
+ // JSON formatter
121
+ const jsonLogFormatter = (context) => {
122
+ return JSON.stringify({
123
+ timestamp: context.timestamp.toISOString(),
124
+ level: context.level,
125
+ logger: context.loggerName,
126
+ message: context.message,
127
+ });
128
+ };
129
+ exports.jsonLogFormatter = jsonLogFormatter;
130
+ let defaultFormatter = exports.defaultLogFormatter;
131
+ // Factory function with caching
132
+ function getLogger(name = exports.DEFAULT_LOGGER_NAME) {
133
+ let logger = loggerCache.get(name);
134
+ if (!logger) {
135
+ logger = new Logger(name, defaultLogLevel, defaultWriter, defaultFormatter);
136
+ loggerCache.set(name, logger);
137
+ }
138
+ return logger;
139
+ }
140
+ exports.getLogger = getLogger;
141
+ // Clear logger cache (useful for testing)
142
+ function clearLoggerCache() {
143
+ loggerCache.clear();
144
+ }
145
+ exports.clearLoggerCache = clearLoggerCache;
146
+ // Helper to create writers
147
+ function createConsoleWriter() {
148
+ return new ConsoleWriter();
149
+ }
150
+ exports.createConsoleWriter = createConsoleWriter;
151
+ function createNullWriter() {
152
+ return new NullWriter();
153
+ }
154
+ exports.createNullWriter = createNullWriter;
21
155
  function stringify(obj, indent) {
22
156
  if (typeof obj === "string") {
23
157
  return obj;
@@ -40,74 +174,129 @@ function stringify(obj, indent) {
40
174
  else if (typeof obj === "function") {
41
175
  return "[Function]";
42
176
  }
43
- const seen = new WeakSet();
44
- return JSON.stringify(obj, (k, v) => {
45
- if (typeof v === "object" && v !== null) {
46
- if (seen.has(v)) {
47
- return undefined;
48
- }
49
- seen.add(v);
50
- }
51
- else if (typeof v === "bigint") {
52
- return `${v}n`;
53
- }
54
- return v;
55
- }, indent);
56
- }
57
- const loggers = {};
58
- function getLogger(writer) {
59
- const w = writer || "console";
60
- const logger = loggers[w];
61
- if (logger && logger.loglevel === currentLogLevel) {
62
- return logger;
177
+ else if (obj === null) {
178
+ return "null";
179
+ }
180
+ try {
181
+ // Custom replacer for circular references
182
+ const getCircularReplacer = () => {
183
+ const seen = new WeakSet();
184
+ return (_key, value) => {
185
+ if (typeof value === "object" && value !== null) {
186
+ if (seen.has(value)) {
187
+ return "[Circular]";
188
+ }
189
+ seen.add(value);
190
+ }
191
+ else if (typeof value === "bigint") {
192
+ return `${value}n`;
193
+ }
194
+ else if (typeof value === "function") {
195
+ return "[Function]";
196
+ }
197
+ else if (typeof value === "symbol") {
198
+ return value.toString();
199
+ }
200
+ return value;
201
+ };
202
+ };
203
+ return JSON.stringify(obj, getCircularReplacer(), indent);
204
+ }
205
+ catch (error) {
206
+ const msg = error && typeof error === "object" && "message" in error ? error.message : "Unknown error";
207
+ return `[Error stringifying object: ${msg}]`;
63
208
  }
64
- return loggers[w] = Logger.getLogger(currentLogLevel, w);
65
209
  }
66
- exports.getLogger = getLogger;
67
210
  class Logger {
68
- constructor(logLevel, writer) {
69
- this.loglevel = "error";
70
- if (writer === "console") {
71
- this._writer = new ConsoleWriter();
72
- }
73
- else if (writer) {
74
- this._writer = writer;
211
+ constructor(name, logLevel, writer, formatter) {
212
+ // Normalize for JavaScript users who might pass uppercase
213
+ const normalized = normalizeLogLevel(logLevel);
214
+ if (!normalized) {
215
+ throw new Error(`Invalid log level: ${logLevel}. Valid levels are: ${LOG_LEVELS.join(", ")}`);
75
216
  }
76
- else {
77
- this._writer = new ConsoleWriter();
217
+ this._name = name;
218
+ this._logLevel = normalized;
219
+ this._writer = writer;
220
+ this._formatter = formatter || defaultFormatter;
221
+ }
222
+ // Allow changing log level for this specific logger
223
+ setLogLevel(level) {
224
+ // Normalize for JavaScript users who might pass uppercase
225
+ const normalized = normalizeLogLevel(level);
226
+ if (!normalized) {
227
+ throw new Error(`Invalid log level: ${level}. Valid levels are: ${LOG_LEVELS.join(", ")}`);
78
228
  }
79
- this.loglevel = logLevel;
229
+ this._logLevel = normalized;
80
230
  }
81
- static getLogger(logLevel, writer) {
82
- return new Logger(logLevel, writer);
231
+ getLogLevel() {
232
+ return this._logLevel;
83
233
  }
84
- setLogLevel(level) {
85
- this.loglevel = level;
234
+ // Allow changing writer for this specific logger
235
+ setWriter(writer) {
236
+ this._writer = writer;
86
237
  }
87
- shouldWrite(logLevel) {
88
- return logPriority[this.loglevel] <= logPriority[logLevel];
238
+ getWriter() {
239
+ return this._writer;
89
240
  }
90
- formatMessage(level, body) {
91
- return `${(new Date()).toLocaleString()} [${level.toUpperCase()}] ${body}`;
241
+ getName() {
242
+ return this._name;
243
+ }
244
+ setFormatter(formatter) {
245
+ this._formatter = formatter;
246
+ }
247
+ getFormatter() {
248
+ return this._formatter;
249
+ }
250
+ _shouldWrite(logLevel) {
251
+ return logPriority[this._logLevel] <= logPriority[logLevel];
252
+ }
253
+ _formatMessage(level, body) {
254
+ const context = {
255
+ timestamp: new Date(),
256
+ level,
257
+ loggerName: this._name,
258
+ message: body,
259
+ };
260
+ return this._formatter(context);
261
+ }
262
+ trace(msg) {
263
+ if (this._shouldWrite("trace")) {
264
+ this._writer.write(this._formatMessage("trace", stringify(msg)), "trace");
265
+ }
92
266
  }
93
267
  debug(msg) {
94
- if (this.shouldWrite("debug")) {
95
- this._writer.write(this.formatMessage("debug", stringify(msg)));
268
+ if (this._shouldWrite("debug")) {
269
+ this._writer.write(this._formatMessage("debug", stringify(msg)), "debug");
96
270
  }
97
271
  }
98
272
  info(msg) {
99
- if (this.shouldWrite("info")) {
100
- this._writer.write(this.formatMessage("info", stringify(msg)));
273
+ if (this._shouldWrite("info")) {
274
+ this._writer.write(this._formatMessage("info", stringify(msg)), "info");
101
275
  }
102
276
  }
103
277
  warning(msg) {
104
- if (this.shouldWrite("warning")) {
105
- this._writer.write(this.formatMessage("warning", stringify(msg)));
278
+ if (this._shouldWrite("warning")) {
279
+ this._writer.write(this._formatMessage("warning", stringify(msg)), "warning");
106
280
  }
107
281
  }
108
282
  error(msg) {
109
- if (this.shouldWrite("error")) {
110
- this._writer.write(this.formatMessage("error", stringify(msg)));
283
+ if (this._shouldWrite("error")) {
284
+ this._writer.write(this._formatMessage("error", stringify(msg)), "error");
111
285
  }
112
286
  }
113
287
  }
288
+ exports.Logger = Logger;
289
+ // For backward compatibility
290
+ function getLogLevel() {
291
+ return getDefaultLogLevel();
292
+ }
293
+ exports.getLogLevel = getLogLevel;
294
+ function setLogLevel(level) {
295
+ // Normalize for JavaScript users who might pass uppercase
296
+ const normalized = normalizeLogLevel(level);
297
+ if (!normalized) {
298
+ throw new Error(`Invalid log level: ${level}. Valid levels are: ${LOG_LEVELS.join(", ")}`);
299
+ }
300
+ setDefaultLogLevel(normalized);
301
+ }
302
+ exports.setLogLevel = setLogLevel;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chia-agent",
3
- "version": "14.3.3",
3
+ "version": "14.3.5",
4
4
  "author": "ChiaMineJP <admin@chiamine.jp>",
5
5
  "description": "chia rpc/websocket client library",
6
6
  "license": "MIT",
@@ -10,6 +10,7 @@
10
10
  },
11
11
  "bugs": "https://github.com/Chia-Mine/chia-agent/issues",
12
12
  "main": "./index.js",
13
+ "types": "./index.d.ts",
13
14
  "bin": {
14
15
  "chia-agent": "./bin/cli.js"
15
16
  },
package/rpc/index.d.ts CHANGED
@@ -61,12 +61,16 @@ export type TRPCAgentProps = {
61
61
  skip_hostname_verification?: boolean;
62
62
  } | {
63
63
  httpAgent: HttpAgent;
64
+ host: string;
65
+ port: number;
64
66
  skip_hostname_verification?: boolean;
65
67
  };
66
68
  export declare class RPCAgent implements APIAgent {
67
69
  protected _protocol: "http" | "https";
68
70
  protected _agent: HttpsAgent | HttpAgent;
69
71
  protected _skip_hostname_verification: boolean;
72
+ protected _host: string;
73
+ protected _port: number;
70
74
  constructor(props: TRPCAgentProps);
71
75
  sendMessage<M>(destination: string, command: string, data?: Record<string, unknown>): Promise<M>;
72
76
  request<R>(method: string, path: string, data?: any): Promise<R>;
package/rpc/index.js CHANGED
@@ -70,9 +70,15 @@ function getConf(configPath) {
70
70
  }
71
71
  exports.getConf = getConf;
72
72
  function loadCertFilesFromConfig(config) {
73
- const clientCertPath = (0, index_1.resolveFromChiaRoot)([config["/daemon_ssl/private_crt"]]);
74
- const clientKeyPath = (0, index_1.resolveFromChiaRoot)([config["/daemon_ssl/private_key"]]);
75
- const caCertPath = (0, index_1.resolveFromChiaRoot)([config["/private_ssl_ca/crt"]]);
73
+ const clientCertPath = (0, index_1.resolveFromChiaRoot)([
74
+ config["/daemon_ssl/private_crt"],
75
+ ]);
76
+ const clientKeyPath = (0, index_1.resolveFromChiaRoot)([
77
+ config["/daemon_ssl/private_key"],
78
+ ]);
79
+ const caCertPath = (0, index_1.resolveFromChiaRoot)([
80
+ config["/private_ssl_ca/crt"],
81
+ ]);
76
82
  (0, logger_1.getLogger)().debug(`Loading client cert file from ${clientCertPath}`);
77
83
  (0, logger_1.getLogger)().debug(`Loading client key file from ${clientKeyPath}`);
78
84
  (0, logger_1.getLogger)().debug(`Loading ca cert file from ${caCertPath}`);
@@ -93,15 +99,31 @@ const userAgent = "chia-agent/1.0.0";
93
99
  class RPCAgent {
94
100
  constructor(props) {
95
101
  this._skip_hostname_verification = false;
102
+ this._host = "";
103
+ this._port = 0;
96
104
  if ("httpsAgent" in props) {
97
105
  this._protocol = "https";
98
106
  this._agent = props.httpsAgent;
99
107
  this._skip_hostname_verification = Boolean(props.skip_hostname_verification);
108
+ // Extract host/port from httpsAgent options
109
+ // Note: TypeScript doesn't expose options property, but it exists at runtime
110
+ const agent = this._agent;
111
+ if (agent.options && agent.options.host && agent.options.port) {
112
+ this._host = agent.options.host;
113
+ this._port = agent.options.port;
114
+ (0, logger_1.getLogger)().debug(`Constructing RPCAgent with httpsAgent: ${this._host}:${this._port}`);
115
+ }
116
+ else {
117
+ (0, logger_1.getLogger)().debug("Constructing RPCAgent with httpsAgent (host/port not available in agent options)");
118
+ }
100
119
  }
101
120
  else if ("httpAgent" in props) {
102
121
  this._protocol = "http";
103
122
  this._agent = props.httpAgent;
123
+ this._host = props.host;
124
+ this._port = props.port;
104
125
  this._skip_hostname_verification = Boolean(props.skip_hostname_verification);
126
+ (0, logger_1.getLogger)().debug(`Constructing RPCAgent with httpAgent: ${this._host}:${this._port}`);
105
127
  }
106
128
  else if ("protocol" in props) {
107
129
  this._protocol = props.protocol;
@@ -110,12 +132,17 @@ class RPCAgent {
110
132
  let clientKey;
111
133
  let caCert;
112
134
  const keepAlive = props.keepAlive !== false;
113
- const keepAliveMsecs = typeof props.keepAliveMsecs === "number" && props.keepAliveMsecs > 0 ?
114
- props.keepAliveMsecs : 1000;
115
- const maxSockets = typeof props.maxSockets === "number" && props.maxSockets > 0 ?
116
- props.maxSockets : Infinity;
117
- const timeout = typeof props.timeout === "number" && props.timeout > 0 ?
118
- props.timeout : undefined;
135
+ const keepAliveMsecs = typeof props.keepAliveMsecs === "number" && props.keepAliveMsecs > 0
136
+ ? props.keepAliveMsecs
137
+ : 1000;
138
+ const maxSockets = typeof props.maxSockets === "number" && props.maxSockets > 0
139
+ ? props.maxSockets
140
+ : Infinity;
141
+ const timeout = typeof props.timeout === "number" && props.timeout > 0
142
+ ? props.timeout
143
+ : undefined;
144
+ this._host = host;
145
+ this._port = port;
119
146
  if (props.protocol === "https") {
120
147
  if ("configPath" in props) {
121
148
  const config = getConf(props.configPath);
@@ -126,7 +153,11 @@ class RPCAgent {
126
153
  this._skip_hostname_verification = Boolean(props.skip_hostname_verification);
127
154
  }
128
155
  else {
129
- ({ client_cert: clientCert, client_key: clientKey, ca_cert: caCert } = props);
156
+ ({
157
+ client_cert: clientCert,
158
+ client_key: clientKey,
159
+ ca_cert: caCert,
160
+ } = props);
130
161
  this._skip_hostname_verification = Boolean(props.skip_hostname_verification);
131
162
  }
132
163
  this._agent = new https_1.Agent({
@@ -141,6 +172,7 @@ class RPCAgent {
141
172
  maxSockets,
142
173
  timeout,
143
174
  });
175
+ (0, logger_1.getLogger)().debug(`Constructed RPCAgent with httpsAgent: ${host}:${port}`);
144
176
  }
145
177
  else {
146
178
  this._agent = new http_1.Agent({
@@ -156,12 +188,15 @@ class RPCAgent {
156
188
  let host;
157
189
  let port;
158
190
  const keepAlive = props.keepAlive !== false;
159
- const keepAliveMsecs = typeof props.keepAliveMsecs === "number" && props.keepAliveMsecs > 0 ?
160
- props.keepAliveMsecs : 1000;
161
- const maxSockets = typeof props.maxSockets === "number" && props.maxSockets > 0 ?
162
- props.maxSockets : Infinity;
163
- const timeout = typeof props.timeout === "number" && props.timeout > 0 ?
164
- props.timeout : undefined;
191
+ const keepAliveMsecs = typeof props.keepAliveMsecs === "number" && props.keepAliveMsecs > 0
192
+ ? props.keepAliveMsecs
193
+ : 1000;
194
+ const maxSockets = typeof props.maxSockets === "number" && props.maxSockets > 0
195
+ ? props.maxSockets
196
+ : Infinity;
197
+ const timeout = typeof props.timeout === "number" && props.timeout > 0
198
+ ? props.timeout
199
+ : undefined;
165
200
  const config = getConf("configPath" in props ? props.configPath : undefined);
166
201
  if (props.host && typeof props.port === "number") {
167
202
  ({ host, port } = props);
@@ -175,6 +210,8 @@ class RPCAgent {
175
210
  const certs = loadCertFilesFromConfig(config);
176
211
  const { clientCert, clientKey, caCert } = certs;
177
212
  this._skip_hostname_verification = Boolean(props.skip_hostname_verification);
213
+ this._host = host;
214
+ this._port = port;
178
215
  this._agent = new https_1.Agent({
179
216
  host: host,
180
217
  port: port,
@@ -192,7 +229,7 @@ class RPCAgent {
192
229
  sendMessage(destination, command, data) {
193
230
  return __awaiter(this, void 0, void 0, function* () {
194
231
  // parameter `destination` is not used because target rpc server is determined by url.
195
- (0, logger_1.getLogger)().debug(`Sending message. dest=${destination} command=${command}`);
232
+ (0, logger_1.getLogger)().debug(`Sending RPC message. dest=${destination} command=${command}`);
196
233
  return this.request("POST", command, data);
197
234
  });
198
235
  }
@@ -206,9 +243,8 @@ class RPCAgent {
206
243
  Accept: "application/json, text/plain, */*",
207
244
  "User-Agent": userAgent,
208
245
  };
209
- if ("options" in this._agent && typeof this._agent.options.host === "string") {
210
- // Assuming `this._agent instanceof HttpsAgent` is true.
211
- headers.Host = this._agent.options.host;
246
+ if (this._host) {
247
+ headers.Host = this._host;
212
248
  }
213
249
  const options = {
214
250
  path: pathname,
@@ -216,6 +252,11 @@ class RPCAgent {
216
252
  agent: this._agent,
217
253
  headers,
218
254
  };
255
+ // For HTTP protocol, we need to explicitly set hostname and port
256
+ if (this._protocol === "http") {
257
+ options.hostname = this._host;
258
+ options.port = this._port;
259
+ }
219
260
  if (this._skip_hostname_verification) {
220
261
  options.checkServerIdentity = () => {
221
262
  return undefined;
@@ -243,7 +284,9 @@ class RPCAgent {
243
284
  }
244
285
  }
245
286
  const transporter = this._protocol === "https" ? https_1.request : http_1.request;
246
- (0, logger_1.getLogger)().debug(`Requesting to ${options.protocol}//${options.hostname}:${options.port}${options.path}`);
287
+ (0, logger_1.getLogger)().debug(`Dispatching RPC ${METHOD} request to ${this._protocol}//${this._host}:${this._port}${options.path}`);
288
+ (0, logger_1.getLogger)().trace(`Request options: ${JSON.stringify(options)}`);
289
+ (0, logger_1.getLogger)().trace(`Request body: ${body}`);
247
290
  const req = transporter(options, (res) => {
248
291
  if (!res.statusCode || res.statusCode < 200 || res.statusCode >= 300) {
249
292
  (0, logger_1.getLogger)().error(`Status not ok: ${res.statusCode}`);
@@ -256,11 +299,12 @@ class RPCAgent {
256
299
  return reject(new Error(`Status not ok: ${res.statusCode}`));
257
300
  }
258
301
  const chunks = [];
259
- res.on("data", chunk => {
302
+ res.on("data", (chunk) => {
260
303
  chunks.push(chunk);
261
304
  if (chunks.length === 0) {
262
305
  (0, logger_1.getLogger)().debug("The first response chunk data arrived");
263
306
  }
307
+ (0, logger_1.getLogger)().trace(`Response chunk #${chunks.length} - ${chunk.length} bytes: ${chunk.toString()}`);
264
308
  });
265
309
  res.on("end", () => {
266
310
  try {
@@ -280,6 +324,8 @@ class RPCAgent {
280
324
  (0, logger_1.getLogger)().info(`API failure: ${d.error}`);
281
325
  return reject(d);
282
326
  }
327
+ (0, logger_1.getLogger)().debug(`RPC response received from ${this._protocol}//${this._host}:${this._port}${options.path}`);
328
+ (0, logger_1.getLogger)().trace(`RPC response data: ${JSON.stringify(d)}`);
283
329
  return resolve(d);
284
330
  }
285
331
  // RPC Server should return response like
@@ -289,20 +335,23 @@ class RPCAgent {
289
335
  reject(new Error("Server responded without expected data"));
290
336
  }
291
337
  catch (e) {
292
- (0, logger_1.getLogger)().error("Failed to parse response data");
338
+ (0, logger_1.getLogger)().error(`Failed to parse response data: ${JSON.stringify(e)}`);
293
339
  try {
294
340
  (0, logger_1.getLogger)().error(Buffer.concat(chunks).toString());
295
341
  }
296
- catch (_) { /* Do nothing */ }
342
+ catch (_e2) {
343
+ /* Do nothing */
344
+ }
297
345
  reject(new Error("Server responded without expected data"));
298
346
  }
299
347
  });
300
348
  });
301
- req.on("error", error => {
349
+ req.on("error", (error) => {
302
350
  (0, logger_1.getLogger)().error(JSON.stringify(error));
303
351
  reject(error);
304
352
  });
305
- if ((METHOD === "POST" || METHOD === "PUT" || METHOD === "DELETE") && body) {
353
+ if ((METHOD === "POST" || METHOD === "PUT" || METHOD === "DELETE") &&
354
+ body) {
306
355
  req.write(body);
307
356
  }
308
357
  req.end();