chia-agent 14.3.2 → 14.3.4
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 +39 -0
- package/README.md +27 -0
- package/daemon/connection.js +3 -3
- package/daemon/index.d.ts +33 -7
- package/daemon/index.js +235 -48
- package/logger.d.ts +43 -16
- package/logger.js +219 -49
- 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,42 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [14.3.4]
|
|
4
|
+
### Breaking change
|
|
5
|
+
- Changed `daemon.connect()` API signature from `connect(url?, timeoutMs?)` to `connect(url?, options?)`
|
|
6
|
+
- All connection options are now consolidated into a single `options` parameter
|
|
7
|
+
- Existing code calling `connect()` without parameters remains compatible
|
|
8
|
+
### Changed
|
|
9
|
+
- Improved logger system
|
|
10
|
+
- Added logger instance caching
|
|
11
|
+
- Added support for multiple loggers with per-instance configuration
|
|
12
|
+
- Added `NullWriter` for complete log suppression
|
|
13
|
+
- Updated `ConsoleWriter` to use proper console methods
|
|
14
|
+
- Added `trace` log level
|
|
15
|
+
- Added environment variable support: `LOG_LEVEL` and `LOG_SUPPRESS`
|
|
16
|
+
- Refactored API to use named loggers
|
|
17
|
+
- Added customizable log formatting with built-in formatters
|
|
18
|
+
- Auto-reconnection is now enabled by default
|
|
19
|
+
### Fixed
|
|
20
|
+
- Fixed WebSocket message handling issues
|
|
21
|
+
- Added timeout handling for sent messages (default 30s)
|
|
22
|
+
- Fixed connection state check to use actual WebSocket readyState
|
|
23
|
+
- Preserved event listeners on connection close for reconnection support
|
|
24
|
+
- Added proper cleanup of message timeouts on response
|
|
25
|
+
- Fixed RPC agent to properly handle HTTP connections with explicit host/port
|
|
26
|
+
### Added
|
|
27
|
+
- Added automatic retry/reconnection mechanism for WebSocket connection attempts
|
|
28
|
+
- Exponential backoff with configurable parameters
|
|
29
|
+
- Configurable retry parameters (maxAttempts, initialDelay, maxDelay, backoffMultiplier)
|
|
30
|
+
- Automatic re-subscription to services after reconnection
|
|
31
|
+
- Same retry configuration applies to both initial connection and reconnection
|
|
32
|
+
### Internal change
|
|
33
|
+
- Removed `TDestination` type in favor of Writer-based approach
|
|
34
|
+
|
|
35
|
+
## [14.3.3]
|
|
36
|
+
### Changed
|
|
37
|
+
- The default service name which [`Daemon`](./src/daemon/index.ts) client tries to register is now `wallet_ui`.
|
|
38
|
+
Previously `chia_agent` service and optionally `wallet_ui` service were registered to `chia-blockchain`'s `Daemon`.
|
|
39
|
+
|
|
3
40
|
## [14.3.2]
|
|
4
41
|
### Changed
|
|
5
42
|
- Error logs are generated when
|
|
@@ -1715,6 +1752,8 @@ daemon.sendMessage(destination, get_block_record_by_height_command, data);
|
|
|
1715
1752
|
Initial release.
|
|
1716
1753
|
|
|
1717
1754
|
<!-- [Unreleased]: https://github.com/Chia-Mine/chia-agent/compare/v0.0.1...v0.0.2 -->
|
|
1755
|
+
[14.3.4]: https://github.com/Chia-Mine/chia-agent/compare/v14.3.3...v14.3.4
|
|
1756
|
+
[14.3.3]: https://github.com/Chia-Mine/chia-agent/compare/v14.3.2...v14.3.3
|
|
1718
1757
|
[14.3.2]: https://github.com/Chia-Mine/chia-agent/compare/v14.3.1...v14.3.2
|
|
1719
1758
|
[14.3.1]: https://github.com/Chia-Mine/chia-agent/compare/v14.3.0...v14.3.1
|
|
1720
1759
|
[14.3.0]: https://github.com/Chia-Mine/chia-agent/compare/v14.2.2...v14.3.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/connection.js
CHANGED
|
@@ -42,16 +42,16 @@ function open(url, timeoutMs) {
|
|
|
42
42
|
reject(err);
|
|
43
43
|
}
|
|
44
44
|
};
|
|
45
|
-
ws.
|
|
45
|
+
ws.on("open", (openEvent) => {
|
|
46
46
|
opened = true;
|
|
47
|
-
ws.
|
|
47
|
+
ws.off("error", onOpenError);
|
|
48
48
|
if (timer !== null) {
|
|
49
49
|
clearTimeout(timer);
|
|
50
50
|
timer = null;
|
|
51
51
|
resolve({ ws, openEvent });
|
|
52
52
|
}
|
|
53
53
|
});
|
|
54
|
-
ws.
|
|
54
|
+
ws.on("error", onOpenError);
|
|
55
55
|
});
|
|
56
56
|
}
|
|
57
57
|
exports.open = open;
|
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;
|
|
10
|
-
export
|
|
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
|
+
}
|
|
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>;
|
|
@@ -22,17 +38,26 @@ declare class Daemon {
|
|
|
22
38
|
protected _closing: boolean;
|
|
23
39
|
protected _onClosePromise: (() => unknown) | undefined;
|
|
24
40
|
protected _subscriptions: string[];
|
|
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;
|
|
25
49
|
get connected(): boolean;
|
|
26
50
|
get closing(): boolean;
|
|
27
|
-
constructor();
|
|
51
|
+
constructor(serviceName?: string);
|
|
52
|
+
protected onRejection(e: unknown): null;
|
|
28
53
|
/**
|
|
29
54
|
* Connect to local daemon via websocket.
|
|
30
|
-
* @param daemonServerURL
|
|
31
|
-
* @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
|
|
32
57
|
*/
|
|
33
|
-
connect(daemonServerURL?: string,
|
|
58
|
+
connect(daemonServerURL?: string, options?: ConnectOptions): Promise<boolean>;
|
|
34
59
|
close(): Promise<unknown>;
|
|
35
|
-
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>;
|
|
36
61
|
createMessageTemplate(command: string, destination: string, data: Record<string, unknown>): {
|
|
37
62
|
command: string;
|
|
38
63
|
data: Record<string, unknown>;
|
|
@@ -59,6 +84,7 @@ declare class Daemon {
|
|
|
59
84
|
protected onClose(event: CloseEvent): void;
|
|
60
85
|
protected onPing(): void;
|
|
61
86
|
protected onPong(): void;
|
|
87
|
+
protected _attemptReconnection(previousSubscriptions: string[]): void;
|
|
62
88
|
}
|
|
63
89
|
export type TDaemon = InstanceType<typeof Daemon>;
|
|
64
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
|
-
const
|
|
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
|
-
function getDaemon() {
|
|
28
|
+
function getDaemon(serviceName) {
|
|
20
29
|
if (daemon) {
|
|
21
30
|
return daemon;
|
|
22
31
|
}
|
|
23
|
-
return daemon = new Daemon();
|
|
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,12 +57,14 @@ 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;
|
|
55
66
|
}
|
|
56
|
-
constructor() {
|
|
67
|
+
constructor(serviceName) {
|
|
57
68
|
this._socket = null;
|
|
58
69
|
this._connectedUrl = "";
|
|
59
70
|
this._responseQueue = {};
|
|
@@ -64,19 +75,51 @@ class Daemon {
|
|
|
64
75
|
this._messageListeners = {};
|
|
65
76
|
this._closing = false;
|
|
66
77
|
this._subscriptions = [];
|
|
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;
|
|
67
86
|
this.onOpen = this.onOpen.bind(this);
|
|
68
87
|
this.onError = this.onError.bind(this);
|
|
69
88
|
this.onMessage = this.onMessage.bind(this);
|
|
70
89
|
this.onClose = this.onClose.bind(this);
|
|
71
90
|
this.onPing = this.onPing.bind(this);
|
|
72
91
|
this.onPong = this.onPong.bind(this);
|
|
92
|
+
this.onRejection = this.onRejection.bind(this);
|
|
93
|
+
if (serviceName) {
|
|
94
|
+
this._serviceName = serviceName;
|
|
95
|
+
}
|
|
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;
|
|
73
116
|
}
|
|
74
117
|
/**
|
|
75
118
|
* Connect to local daemon via websocket.
|
|
76
|
-
* @param daemonServerURL
|
|
77
|
-
* @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
|
|
78
121
|
*/
|
|
79
|
-
connect(daemonServerURL,
|
|
122
|
+
connect(daemonServerURL, options) {
|
|
80
123
|
return __awaiter(this, void 0, void 0, function* () {
|
|
81
124
|
if (!daemonServerURL) {
|
|
82
125
|
const config = (0, index_1.getConfig)();
|
|
@@ -84,6 +127,19 @@ class Daemon {
|
|
|
84
127
|
const daemonPort = config["/ui/daemon_port"];
|
|
85
128
|
daemonServerURL = `wss://${daemonHost}:${daemonPort}`;
|
|
86
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
|
+
}
|
|
87
143
|
if (this._connectedUrl === daemonServerURL) {
|
|
88
144
|
return true;
|
|
89
145
|
}
|
|
@@ -91,33 +147,65 @@ class Daemon {
|
|
|
91
147
|
(0, logger_1.getLogger)().error("Connection is still active. Please close living connection first");
|
|
92
148
|
return false;
|
|
93
149
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
this.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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;
|
|
104
183
|
});
|
|
105
184
|
}
|
|
106
185
|
close() {
|
|
107
186
|
return __awaiter(this, void 0, void 0, function* () {
|
|
108
|
-
return new Promise((
|
|
187
|
+
return new Promise((resolve) => {
|
|
109
188
|
if (this._closing || !this._socket) {
|
|
110
189
|
return;
|
|
111
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;
|
|
112
199
|
(0, logger_1.getLogger)().debug("Closing web socket connection");
|
|
113
200
|
this._socket.close();
|
|
114
201
|
this._closing = true;
|
|
202
|
+
this._connectedUrl = "";
|
|
115
203
|
this._onClosePromise = resolve; // Resolved in onClose function.
|
|
116
|
-
})
|
|
204
|
+
});
|
|
117
205
|
});
|
|
118
206
|
}
|
|
119
|
-
sendMessage(
|
|
120
|
-
return __awaiter(this,
|
|
207
|
+
sendMessage(destination_1, command_1, data_1) {
|
|
208
|
+
return __awaiter(this, arguments, void 0, function* (destination, command, data, timeoutMs = 30000) {
|
|
121
209
|
return new Promise((resolve, reject) => {
|
|
122
210
|
if (!this.connected || !this._socket) {
|
|
123
211
|
(0, logger_1.getLogger)().error("Tried to send message without active connection");
|
|
@@ -126,13 +214,32 @@ class Daemon {
|
|
|
126
214
|
}
|
|
127
215
|
const message = this.createMessageTemplate(command, destination, data || {});
|
|
128
216
|
const reqId = message.request_id;
|
|
129
|
-
|
|
130
|
-
|
|
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}`);
|
|
131
231
|
const messageStr = JSON.stringify(message);
|
|
132
232
|
this._socket.send(messageStr, (err) => {
|
|
133
233
|
if (err) {
|
|
134
234
|
(0, logger_1.getLogger)().error(`Error while sending message: ${messageStr}`);
|
|
135
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
|
+
}
|
|
136
243
|
}
|
|
137
244
|
});
|
|
138
245
|
});
|
|
@@ -143,7 +250,7 @@ class Daemon {
|
|
|
143
250
|
command,
|
|
144
251
|
data,
|
|
145
252
|
ack: false,
|
|
146
|
-
origin:
|
|
253
|
+
origin: this._serviceName,
|
|
147
254
|
destination,
|
|
148
255
|
request_id: (0, crypto_1.randomBytes)(32).toString("hex"),
|
|
149
256
|
};
|
|
@@ -154,23 +261,28 @@ class Daemon {
|
|
|
154
261
|
(0, logger_1.getLogger)().error(`Tried to subscribe '${service}' without active connection`);
|
|
155
262
|
throw new Error("Not connected");
|
|
156
263
|
}
|
|
157
|
-
if (this._subscriptions.findIndex(s => s === service) > -1) {
|
|
264
|
+
if (this._subscriptions.findIndex((s) => s === service) > -1) {
|
|
158
265
|
return {
|
|
159
266
|
command: "register_service",
|
|
160
267
|
data: { success: true },
|
|
161
268
|
ack: true,
|
|
162
269
|
origin: "daemon",
|
|
163
|
-
destination:
|
|
270
|
+
destination: service,
|
|
164
271
|
request_id: "",
|
|
165
272
|
};
|
|
166
273
|
}
|
|
167
274
|
let error;
|
|
168
|
-
const result = yield this.sendMessage("daemon", "register_service", {
|
|
275
|
+
const result = yield this.sendMessage("daemon", "register_service", {
|
|
276
|
+
service,
|
|
277
|
+
}).catch((e) => {
|
|
169
278
|
error = e;
|
|
170
279
|
return null;
|
|
171
280
|
});
|
|
172
281
|
if (error || !result) {
|
|
173
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));
|
|
174
286
|
throw new Error("Subscribe failed");
|
|
175
287
|
}
|
|
176
288
|
this._subscriptions.push(service);
|
|
@@ -241,7 +353,7 @@ class Daemon {
|
|
|
241
353
|
if (!listeners) {
|
|
242
354
|
return;
|
|
243
355
|
}
|
|
244
|
-
const index = listeners.findIndex(l => l === listener);
|
|
356
|
+
const index = listeners.findIndex((l) => l === listener);
|
|
245
357
|
if (index > -1) {
|
|
246
358
|
listeners.splice(index, 1);
|
|
247
359
|
}
|
|
@@ -253,13 +365,13 @@ class Daemon {
|
|
|
253
365
|
return __awaiter(this, void 0, void 0, function* () {
|
|
254
366
|
(0, logger_1.getLogger)().info("ws connection opened");
|
|
255
367
|
this._connectedUrl = url;
|
|
256
|
-
this._openEventListeners.forEach(l => l(event));
|
|
257
|
-
return this.subscribe(
|
|
368
|
+
this._openEventListeners.forEach((l) => l(event));
|
|
369
|
+
return this.subscribe(this._serviceName);
|
|
258
370
|
});
|
|
259
371
|
}
|
|
260
372
|
onError(error) {
|
|
261
|
-
(0, logger_1.getLogger)().error(`ws connection error: ${error.message}`);
|
|
262
|
-
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));
|
|
263
375
|
}
|
|
264
376
|
onMessage(event) {
|
|
265
377
|
let payload;
|
|
@@ -271,46 +383,62 @@ class Daemon {
|
|
|
271
383
|
({ request_id, origin, command } = payload);
|
|
272
384
|
}
|
|
273
385
|
catch (err) {
|
|
274
|
-
(0, logger_1.getLogger)().error(`Failed to parse message data: ${JSON.stringify(err)}`);
|
|
275
|
-
(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}`);
|
|
276
388
|
return;
|
|
277
389
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
390
|
+
const entry = this._responseQueue[request_id];
|
|
391
|
+
if (entry) {
|
|
392
|
+
clearTimeout(entry.timeout);
|
|
281
393
|
delete this._responseQueue[request_id];
|
|
282
|
-
|
|
394
|
+
(0, logger_1.getLogger)().debug(`Ws response received. origin=${origin} command=${command} reqId=${request_id}`);
|
|
395
|
+
entry.resolver(payload);
|
|
283
396
|
}
|
|
284
|
-
|
|
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));
|
|
285
402
|
for (const o in this._messageListeners) {
|
|
286
403
|
if (!Object.prototype.hasOwnProperty.call(this._messageListeners, o)) {
|
|
287
404
|
continue;
|
|
288
405
|
}
|
|
289
406
|
const listeners = this._messageListeners[o];
|
|
290
407
|
if (origin === o || o === "all") {
|
|
291
|
-
listeners.forEach(l => l(payload));
|
|
408
|
+
listeners.forEach((l) => l(payload));
|
|
292
409
|
}
|
|
293
410
|
}
|
|
294
411
|
}
|
|
295
412
|
onClose(event) {
|
|
413
|
+
const previousSubscriptions = [...this._subscriptions];
|
|
296
414
|
if (this._socket) {
|
|
297
|
-
this._socket.
|
|
415
|
+
this._socket.off("error", this.onError);
|
|
298
416
|
this._socket.removeEventListener("message", this.onMessage);
|
|
299
|
-
this._socket.
|
|
300
|
-
this._socket.
|
|
301
|
-
this._socket.
|
|
417
|
+
this._socket.off("close", this.onClose);
|
|
418
|
+
this._socket.off("ping", this.onPing);
|
|
419
|
+
this._socket.off("pong", this.onPong);
|
|
302
420
|
this._socket = null;
|
|
303
421
|
}
|
|
304
422
|
this._closing = false;
|
|
305
423
|
this._connectedUrl = "";
|
|
306
424
|
this._subscriptions = [];
|
|
307
|
-
this._closeEventListeners.forEach(l => l(event));
|
|
308
|
-
|
|
309
|
-
|
|
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}`);
|
|
310
429
|
if (this._onClosePromise) {
|
|
311
430
|
this._onClosePromise();
|
|
312
431
|
this._onClosePromise = undefined;
|
|
313
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
|
+
}
|
|
314
442
|
}
|
|
315
443
|
onPing() {
|
|
316
444
|
(0, logger_1.getLogger)().debug("Received ping");
|
|
@@ -318,4 +446,63 @@ class Daemon {
|
|
|
318
446
|
onPong() {
|
|
319
447
|
(0, logger_1.getLogger)().debug("Received pong");
|
|
320
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
|
+
}
|
|
321
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;
|
|
@@ -41,7 +175,7 @@ function stringify(obj, indent) {
|
|
|
41
175
|
return "[Function]";
|
|
42
176
|
}
|
|
43
177
|
const seen = new WeakSet();
|
|
44
|
-
return JSON.stringify(obj, (
|
|
178
|
+
return JSON.stringify(obj, (_k, v) => {
|
|
45
179
|
if (typeof v === "object" && v !== null) {
|
|
46
180
|
if (seen.has(v)) {
|
|
47
181
|
return undefined;
|
|
@@ -54,60 +188,96 @@ function stringify(obj, indent) {
|
|
|
54
188
|
return v;
|
|
55
189
|
}, indent);
|
|
56
190
|
}
|
|
57
|
-
const loggers = {};
|
|
58
|
-
function getLogger(writer) {
|
|
59
|
-
const w = writer || "console";
|
|
60
|
-
const logger = loggers[w];
|
|
61
|
-
if (logger && logger.loglevel === currentLogLevel) {
|
|
62
|
-
return logger;
|
|
63
|
-
}
|
|
64
|
-
return loggers[w] = Logger.getLogger(currentLogLevel, w);
|
|
65
|
-
}
|
|
66
|
-
exports.getLogger = getLogger;
|
|
67
191
|
class Logger {
|
|
68
|
-
constructor(logLevel, writer) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
else if (writer) {
|
|
74
|
-
this._writer = writer;
|
|
192
|
+
constructor(name, logLevel, writer, formatter) {
|
|
193
|
+
// Normalize for JavaScript users who might pass uppercase
|
|
194
|
+
const normalized = normalizeLogLevel(logLevel);
|
|
195
|
+
if (!normalized) {
|
|
196
|
+
throw new Error(`Invalid log level: ${logLevel}. Valid levels are: ${LOG_LEVELS.join(", ")}`);
|
|
75
197
|
}
|
|
76
|
-
|
|
77
|
-
|
|
198
|
+
this._name = name;
|
|
199
|
+
this._logLevel = normalized;
|
|
200
|
+
this._writer = writer;
|
|
201
|
+
this._formatter = formatter || defaultFormatter;
|
|
202
|
+
}
|
|
203
|
+
// Allow changing log level for this specific logger
|
|
204
|
+
setLogLevel(level) {
|
|
205
|
+
// Normalize for JavaScript users who might pass uppercase
|
|
206
|
+
const normalized = normalizeLogLevel(level);
|
|
207
|
+
if (!normalized) {
|
|
208
|
+
throw new Error(`Invalid log level: ${level}. Valid levels are: ${LOG_LEVELS.join(", ")}`);
|
|
78
209
|
}
|
|
79
|
-
this.
|
|
210
|
+
this._logLevel = normalized;
|
|
80
211
|
}
|
|
81
|
-
|
|
82
|
-
return
|
|
212
|
+
getLogLevel() {
|
|
213
|
+
return this._logLevel;
|
|
83
214
|
}
|
|
84
|
-
|
|
85
|
-
|
|
215
|
+
// Allow changing writer for this specific logger
|
|
216
|
+
setWriter(writer) {
|
|
217
|
+
this._writer = writer;
|
|
86
218
|
}
|
|
87
|
-
|
|
88
|
-
return
|
|
219
|
+
getWriter() {
|
|
220
|
+
return this._writer;
|
|
89
221
|
}
|
|
90
|
-
|
|
91
|
-
return
|
|
222
|
+
getName() {
|
|
223
|
+
return this._name;
|
|
224
|
+
}
|
|
225
|
+
setFormatter(formatter) {
|
|
226
|
+
this._formatter = formatter;
|
|
227
|
+
}
|
|
228
|
+
getFormatter() {
|
|
229
|
+
return this._formatter;
|
|
230
|
+
}
|
|
231
|
+
_shouldWrite(logLevel) {
|
|
232
|
+
return logPriority[this._logLevel] <= logPriority[logLevel];
|
|
233
|
+
}
|
|
234
|
+
_formatMessage(level, body) {
|
|
235
|
+
const context = {
|
|
236
|
+
timestamp: new Date(),
|
|
237
|
+
level,
|
|
238
|
+
loggerName: this._name,
|
|
239
|
+
message: body,
|
|
240
|
+
};
|
|
241
|
+
return this._formatter(context);
|
|
242
|
+
}
|
|
243
|
+
trace(msg) {
|
|
244
|
+
if (this._shouldWrite("trace")) {
|
|
245
|
+
this._writer.write(this._formatMessage("trace", stringify(msg)), "trace");
|
|
246
|
+
}
|
|
92
247
|
}
|
|
93
248
|
debug(msg) {
|
|
94
|
-
if (this.
|
|
95
|
-
this._writer.write(this.
|
|
249
|
+
if (this._shouldWrite("debug")) {
|
|
250
|
+
this._writer.write(this._formatMessage("debug", stringify(msg)), "debug");
|
|
96
251
|
}
|
|
97
252
|
}
|
|
98
253
|
info(msg) {
|
|
99
|
-
if (this.
|
|
100
|
-
this._writer.write(this.
|
|
254
|
+
if (this._shouldWrite("info")) {
|
|
255
|
+
this._writer.write(this._formatMessage("info", stringify(msg)), "info");
|
|
101
256
|
}
|
|
102
257
|
}
|
|
103
258
|
warning(msg) {
|
|
104
|
-
if (this.
|
|
105
|
-
this._writer.write(this.
|
|
259
|
+
if (this._shouldWrite("warning")) {
|
|
260
|
+
this._writer.write(this._formatMessage("warning", stringify(msg)), "warning");
|
|
106
261
|
}
|
|
107
262
|
}
|
|
108
263
|
error(msg) {
|
|
109
|
-
if (this.
|
|
110
|
-
this._writer.write(this.
|
|
264
|
+
if (this._shouldWrite("error")) {
|
|
265
|
+
this._writer.write(this._formatMessage("error", stringify(msg)), "error");
|
|
111
266
|
}
|
|
112
267
|
}
|
|
113
268
|
}
|
|
269
|
+
exports.Logger = Logger;
|
|
270
|
+
// For backward compatibility
|
|
271
|
+
function getLogLevel() {
|
|
272
|
+
return getDefaultLogLevel();
|
|
273
|
+
}
|
|
274
|
+
exports.getLogLevel = getLogLevel;
|
|
275
|
+
function setLogLevel(level) {
|
|
276
|
+
// Normalize for JavaScript users who might pass uppercase
|
|
277
|
+
const normalized = normalizeLogLevel(level);
|
|
278
|
+
if (!normalized) {
|
|
279
|
+
throw new Error(`Invalid log level: ${level}. Valid levels are: ${LOG_LEVELS.join(", ")}`);
|
|
280
|
+
}
|
|
281
|
+
setDefaultLogLevel(normalized);
|
|
282
|
+
}
|
|
283
|
+
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.4",
|
|
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();
|