chia-agent 15.0.0 → 16.0.0

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