chia-agent 15.0.0 → 16.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/README.md +27 -0
- package/daemon/index.d.ts +28 -5
- package/daemon/index.js +183 -27
- package/logger.d.ts +43 -16
- package/logger.js +247 -59
- package/package.json +2 -1
- package/rpc/index.d.ts +4 -0
- package/rpc/index.js +37 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [16.0.1]
|
|
4
|
+
### Fixed
|
|
5
|
+
- Fixed an issue where it exhausts heap when logging an object with circular references
|
|
6
|
+
|
|
7
|
+
## [16.0.0]
|
|
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
|
## [15.0.0]
|
|
4
40
|
### Breaking change
|
|
5
41
|
- The following Wallet RPC APIs for DAO were removed
|
|
@@ -1850,6 +1886,8 @@ daemon.sendMessage(destination, get_block_record_by_height_command, data);
|
|
|
1850
1886
|
Initial release.
|
|
1851
1887
|
|
|
1852
1888
|
<!-- [Unreleased]: https://github.com/Chia-Mine/chia-agent/compare/v0.0.1...v0.0.2 -->
|
|
1889
|
+
[16.0.1]: https://github.com/Chia-Mine/chia-agent/compare/v16.0.0...v16.0.1
|
|
1890
|
+
[16.0.0]: https://github.com/Chia-Mine/chia-agent/compare/v15.0.0...v16.0.0
|
|
1853
1891
|
[15.0.0]: https://github.com/Chia-Mine/chia-agent/compare/v14.5.0...v15.0.0
|
|
1854
1892
|
[14.5.0]: https://github.com/Chia-Mine/chia-agent/compare/v14.4.0...v14.5.0
|
|
1855
1893
|
[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]:
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
this.
|
|
116
|
-
|
|
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
|
-
|
|
144
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
373
|
+
const entry = this._responseQueue[request_id];
|
|
374
|
+
if (entry) {
|
|
375
|
+
clearTimeout(entry.timeout);
|
|
295
376
|
delete this._responseQueue[request_id];
|
|
296
|
-
|
|
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
|
-
|
|
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
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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:
|
|
9
|
-
warning:
|
|
10
|
-
info:
|
|
11
|
-
debug:
|
|
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
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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;
|
|
21
145
|
}
|
|
22
|
-
|
|
23
|
-
|
|
146
|
+
// Clear logger cache (useful for testing)
|
|
147
|
+
function clearLoggerCache() {
|
|
148
|
+
loggerCache.clear();
|
|
149
|
+
}
|
|
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") {
|
|
@@ -44,73 +176,129 @@ function stringify(obj, indent) {
|
|
|
44
176
|
else if (typeof obj === "function") {
|
|
45
177
|
return "[Function]";
|
|
46
178
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
seen
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
179
|
+
else if (obj === null) {
|
|
180
|
+
return "null";
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
// Custom replacer for circular references
|
|
184
|
+
const getCircularReplacer = () => {
|
|
185
|
+
const seen = new WeakSet();
|
|
186
|
+
return (_key, value) => {
|
|
187
|
+
if (typeof value === "object" && value !== null) {
|
|
188
|
+
if (seen.has(value)) {
|
|
189
|
+
return "[Circular]";
|
|
190
|
+
}
|
|
191
|
+
seen.add(value);
|
|
192
|
+
}
|
|
193
|
+
else if (typeof value === "bigint") {
|
|
194
|
+
return `${value}n`;
|
|
195
|
+
}
|
|
196
|
+
else if (typeof value === "function") {
|
|
197
|
+
return "[Function]";
|
|
198
|
+
}
|
|
199
|
+
else if (typeof value === "symbol") {
|
|
200
|
+
return value.toString();
|
|
201
|
+
}
|
|
202
|
+
return value;
|
|
203
|
+
};
|
|
204
|
+
};
|
|
205
|
+
return JSON.stringify(obj, getCircularReplacer(), indent);
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
const msg = error && typeof error === "object" && "message" in error
|
|
209
|
+
? error.message
|
|
210
|
+
: "Unknown error";
|
|
211
|
+
return `[Error stringifying object: ${msg}]`;
|
|
67
212
|
}
|
|
68
|
-
return (loggers[w] = Logger.getLogger(currentLogLevel, w));
|
|
69
213
|
}
|
|
70
214
|
class Logger {
|
|
71
|
-
constructor(logLevel, writer) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
else if (writer) {
|
|
77
|
-
this._writer = writer;
|
|
215
|
+
constructor(name, logLevel, writer, formatter) {
|
|
216
|
+
// Normalize for JavaScript users who might pass uppercase
|
|
217
|
+
const normalized = normalizeLogLevel(logLevel);
|
|
218
|
+
if (!normalized) {
|
|
219
|
+
throw new Error(`Invalid log level: ${logLevel}. Valid levels are: ${LOG_LEVELS.join(", ")}`);
|
|
78
220
|
}
|
|
79
|
-
|
|
80
|
-
|
|
221
|
+
this._name = name;
|
|
222
|
+
this._logLevel = normalized;
|
|
223
|
+
this._writer = writer;
|
|
224
|
+
this._formatter = formatter || defaultFormatter;
|
|
225
|
+
}
|
|
226
|
+
// Allow changing log level for this specific logger
|
|
227
|
+
setLogLevel(level) {
|
|
228
|
+
// Normalize for JavaScript users who might pass uppercase
|
|
229
|
+
const normalized = normalizeLogLevel(level);
|
|
230
|
+
if (!normalized) {
|
|
231
|
+
throw new Error(`Invalid log level: ${level}. Valid levels are: ${LOG_LEVELS.join(", ")}`);
|
|
81
232
|
}
|
|
82
|
-
this.
|
|
233
|
+
this._logLevel = normalized;
|
|
83
234
|
}
|
|
84
|
-
|
|
85
|
-
return
|
|
235
|
+
getLogLevel() {
|
|
236
|
+
return this._logLevel;
|
|
86
237
|
}
|
|
87
|
-
|
|
88
|
-
|
|
238
|
+
// Allow changing writer for this specific logger
|
|
239
|
+
setWriter(writer) {
|
|
240
|
+
this._writer = writer;
|
|
89
241
|
}
|
|
90
|
-
|
|
91
|
-
return
|
|
242
|
+
getWriter() {
|
|
243
|
+
return this._writer;
|
|
92
244
|
}
|
|
93
|
-
|
|
94
|
-
return
|
|
245
|
+
getName() {
|
|
246
|
+
return this._name;
|
|
247
|
+
}
|
|
248
|
+
setFormatter(formatter) {
|
|
249
|
+
this._formatter = formatter;
|
|
250
|
+
}
|
|
251
|
+
getFormatter() {
|
|
252
|
+
return this._formatter;
|
|
253
|
+
}
|
|
254
|
+
_shouldWrite(logLevel) {
|
|
255
|
+
return logPriority[this._logLevel] <= logPriority[logLevel];
|
|
256
|
+
}
|
|
257
|
+
_formatMessage(level, body) {
|
|
258
|
+
const context = {
|
|
259
|
+
timestamp: new Date(),
|
|
260
|
+
level,
|
|
261
|
+
loggerName: this._name,
|
|
262
|
+
message: body,
|
|
263
|
+
};
|
|
264
|
+
return this._formatter(context);
|
|
265
|
+
}
|
|
266
|
+
trace(msg) {
|
|
267
|
+
if (this._shouldWrite("trace")) {
|
|
268
|
+
this._writer.write(this._formatMessage("trace", stringify(msg)), "trace");
|
|
269
|
+
}
|
|
95
270
|
}
|
|
96
271
|
debug(msg) {
|
|
97
|
-
if (this.
|
|
98
|
-
this._writer.write(this.
|
|
272
|
+
if (this._shouldWrite("debug")) {
|
|
273
|
+
this._writer.write(this._formatMessage("debug", stringify(msg)), "debug");
|
|
99
274
|
}
|
|
100
275
|
}
|
|
101
276
|
info(msg) {
|
|
102
|
-
if (this.
|
|
103
|
-
this._writer.write(this.
|
|
277
|
+
if (this._shouldWrite("info")) {
|
|
278
|
+
this._writer.write(this._formatMessage("info", stringify(msg)), "info");
|
|
104
279
|
}
|
|
105
280
|
}
|
|
106
281
|
warning(msg) {
|
|
107
|
-
if (this.
|
|
108
|
-
this._writer.write(this.
|
|
282
|
+
if (this._shouldWrite("warning")) {
|
|
283
|
+
this._writer.write(this._formatMessage("warning", stringify(msg)), "warning");
|
|
109
284
|
}
|
|
110
285
|
}
|
|
111
286
|
error(msg) {
|
|
112
|
-
if (this.
|
|
113
|
-
this._writer.write(this.
|
|
287
|
+
if (this._shouldWrite("error")) {
|
|
288
|
+
this._writer.write(this._formatMessage("error", stringify(msg)), "error");
|
|
114
289
|
}
|
|
115
290
|
}
|
|
116
291
|
}
|
|
292
|
+
exports.Logger = Logger;
|
|
293
|
+
// For backward compatibility
|
|
294
|
+
function getLogLevel() {
|
|
295
|
+
return getDefaultLogLevel();
|
|
296
|
+
}
|
|
297
|
+
function setLogLevel(level) {
|
|
298
|
+
// Normalize for JavaScript users who might pass uppercase
|
|
299
|
+
const normalized = normalizeLogLevel(level);
|
|
300
|
+
if (!normalized) {
|
|
301
|
+
throw new Error(`Invalid log level: ${level}. Valid levels are: ${LOG_LEVELS.join(", ")}`);
|
|
302
|
+
}
|
|
303
|
+
setDefaultLogLevel(normalized);
|
|
304
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chia-agent",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "16.0.1",
|
|
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 (
|
|
214
|
-
|
|
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(`
|
|
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 (
|
|
301
|
-
(0, logger_1.getLogger)().error(
|
|
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
|
}
|