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 +37 -0
- package/README.md +27 -0
- package/daemon/index.d.ts +30 -5
- package/daemon/index.js +221 -38
- package/logger.d.ts +43 -16
- package/logger.js +249 -60
- package/package.json +2 -1
- package/rpc/index.d.ts +4 -0
- package/rpc/index.js +75 -26
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]:
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
this.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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((
|
|
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(
|
|
124
|
-
return __awaiter(this,
|
|
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
|
-
|
|
134
|
-
|
|
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", {
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
390
|
+
const entry = this._responseQueue[request_id];
|
|
391
|
+
if (entry) {
|
|
392
|
+
clearTimeout(entry.timeout);
|
|
285
393
|
delete this._responseQueue[request_id];
|
|
286
|
-
|
|
394
|
+
(0, logger_1.getLogger)().debug(`Ws response received. origin=${origin} command=${command} reqId=${request_id}`);
|
|
395
|
+
entry.resolver(payload);
|
|
287
396
|
}
|
|
288
|
-
|
|
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
|
-
|
|
313
|
-
|
|
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
|
|
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,23 +1,157 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getLogger = exports.
|
|
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:
|
|
7
|
-
warning:
|
|
8
|
-
info:
|
|
9
|
-
debug:
|
|
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
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
seen
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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.
|
|
229
|
+
this._logLevel = normalized;
|
|
80
230
|
}
|
|
81
|
-
|
|
82
|
-
return
|
|
231
|
+
getLogLevel() {
|
|
232
|
+
return this._logLevel;
|
|
83
233
|
}
|
|
84
|
-
|
|
85
|
-
|
|
234
|
+
// Allow changing writer for this specific logger
|
|
235
|
+
setWriter(writer) {
|
|
236
|
+
this._writer = writer;
|
|
86
237
|
}
|
|
87
|
-
|
|
88
|
-
return
|
|
238
|
+
getWriter() {
|
|
239
|
+
return this._writer;
|
|
89
240
|
}
|
|
90
|
-
|
|
91
|
-
return
|
|
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.
|
|
95
|
-
this._writer.write(this.
|
|
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.
|
|
100
|
-
this._writer.write(this.
|
|
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.
|
|
105
|
-
this._writer.write(this.
|
|
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.
|
|
110
|
-
this._writer.write(this.
|
|
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
|
+
"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)([
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
({
|
|
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
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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 (
|
|
210
|
-
|
|
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(`
|
|
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(
|
|
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 (
|
|
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") &&
|
|
353
|
+
if ((METHOD === "POST" || METHOD === "PUT" || METHOD === "DELETE") &&
|
|
354
|
+
body) {
|
|
306
355
|
req.write(body);
|
|
307
356
|
}
|
|
308
357
|
req.end();
|