@lerna-labs/hydra-sdk 1.0.0-beta.9 → 2.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +159 -0
- package/dist/cache/disk-cache.d.ts +37 -0
- package/dist/cache/disk-cache.js +84 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.js +12 -0
- package/dist/hydra/hydra-http-client.d.ts +36 -0
- package/dist/hydra/hydra-http-client.js +66 -0
- package/dist/hydra/hydra-monitor.d.ts +88 -0
- package/dist/hydra/hydra-monitor.js +270 -0
- package/dist/hydra/hydra-websocket.d.ts +46 -0
- package/dist/hydra/hydra-websocket.js +181 -0
- package/dist/hydra/messages.d.ts +14 -0
- package/dist/hydra/messages.js +1 -0
- package/dist/hydra/types.d.ts +486 -0
- package/dist/hydra/types.js +2 -0
- package/dist/hydra/utxo-conversion.d.ts +10 -0
- package/dist/hydra/utxo-conversion.js +111 -0
- package/dist/hydra/utxo.d.ts +25 -5
- package/dist/hydra/utxo.js +37 -31
- package/dist/index.d.ts +15 -7
- package/dist/index.js +11 -7
- package/dist/ipfs/ipfs.d.ts +22 -0
- package/dist/ipfs/ipfs.js +90 -0
- package/dist/mesh/get-admin.d.ts +13 -1
- package/dist/mesh/get-admin.js +37 -7
- package/dist/mesh/native-script.d.ts +30 -5
- package/dist/mesh/native-script.js +38 -10
- package/dist/test.js +3 -3
- package/dist/tx3/submit-tx.d.ts +8 -0
- package/dist/tx3/submit-tx.js +8 -0
- package/dist/utils/chunk-string.d.ts +7 -0
- package/dist/utils/chunk-string.js +7 -0
- package/dist/utils/verify-signature.d.ts +28 -5
- package/dist/utils/verify-signature.js +39 -18
- package/dist/wrangler.d.ts +179 -0
- package/dist/wrangler.js +452 -0
- package/package.json +25 -6
- package/dist/mesh/wrangler.d.ts +0 -29
- package/dist/mesh/wrangler.js +0 -277
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { HEAD_STATUS_TO_HYDRA, HydraWebSocket, TAG_TO_HYDRA } from './hydra-websocket.js';
|
|
3
|
+
const HYDRA_TO_HEAD_STATUS = {
|
|
4
|
+
IDLE: 'Idle',
|
|
5
|
+
OPEN: 'Open',
|
|
6
|
+
CLOSED: 'Closed',
|
|
7
|
+
FANOUT_POSSIBLE: 'FanoutPossible',
|
|
8
|
+
FINAL: 'Final',
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Persistent WebSocket monitor for a Hydra head.
|
|
12
|
+
*
|
|
13
|
+
* Maintains a single long-lived WebSocket connection with auto-reconnect,
|
|
14
|
+
* real-time head state tracking, and proactive error surfacing.
|
|
15
|
+
*
|
|
16
|
+
* Events emitted:
|
|
17
|
+
* - `'message'` — `(msg: HydraWsMessage)` for every incoming message
|
|
18
|
+
* - `'status'` — `(status: HydraStatus, previous: HydraStatus)` on head-status changes
|
|
19
|
+
* - `'error:tx'` — `(msg)` on PostTxOnChainFailed or TxInvalid
|
|
20
|
+
* - `'error:command'` — `(msg)` on CommandFailed
|
|
21
|
+
* - `'error:decommit'` — `(msg)` on DecommitInvalid
|
|
22
|
+
* - `'connected'` — `()` WebSocket open + Greetings received
|
|
23
|
+
* - `'disconnected'` — `()` WebSocket closed (reconnect may follow)
|
|
24
|
+
* - `'reconnecting'` — `(attempt: number, delayMs: number)`
|
|
25
|
+
* - `'reconnect_failed'` — `()` maxAttempts exhausted
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* const monitor = new HydraMonitor({ wsUrl: 'ws://hydra-node:4102' });
|
|
30
|
+
* await monitor.start();
|
|
31
|
+
* console.log(monitor.headStatus); // 'IDLE'
|
|
32
|
+
* monitor.on('status', (s, prev) => console.log(`${prev} → ${s}`));
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export class HydraMonitor extends EventEmitter {
|
|
36
|
+
ws;
|
|
37
|
+
_headStatus = 'IDLE';
|
|
38
|
+
_previousStatus = 'IDLE';
|
|
39
|
+
_headId = null;
|
|
40
|
+
_events = [];
|
|
41
|
+
_stopped = true;
|
|
42
|
+
_reconnecting = false;
|
|
43
|
+
reconnectEnabled;
|
|
44
|
+
baseDelayMs;
|
|
45
|
+
maxDelayMs;
|
|
46
|
+
maxAttempts;
|
|
47
|
+
eventBufferSize;
|
|
48
|
+
boundOnMessage = (msg) => this.onMessage(msg);
|
|
49
|
+
boundOnClose = () => this.onClose();
|
|
50
|
+
constructor(options) {
|
|
51
|
+
super();
|
|
52
|
+
this.ws = new HydraWebSocket(options.wsUrl);
|
|
53
|
+
this.reconnectEnabled = options.reconnect?.enabled ?? true;
|
|
54
|
+
this.baseDelayMs = options.reconnect?.baseDelayMs ?? 1000;
|
|
55
|
+
this.maxDelayMs = options.reconnect?.maxDelayMs ?? 30_000;
|
|
56
|
+
this.maxAttempts = options.reconnect?.maxAttempts ?? Number.POSITIVE_INFINITY;
|
|
57
|
+
this.eventBufferSize = options.eventBufferSize ?? 100;
|
|
58
|
+
}
|
|
59
|
+
/** Connect to the Hydra node. Resolves after Greetings received. */
|
|
60
|
+
async start() {
|
|
61
|
+
this._stopped = false;
|
|
62
|
+
await this.ws.waitForGreetings();
|
|
63
|
+
this.ws.on('message', this.boundOnMessage);
|
|
64
|
+
this.ws.on('close', this.boundOnClose);
|
|
65
|
+
// Process the Greetings that was received during waitForGreetings
|
|
66
|
+
if (this.ws.lastGreetings) {
|
|
67
|
+
this.onMessage(this.ws.lastGreetings);
|
|
68
|
+
}
|
|
69
|
+
this.emit('connected');
|
|
70
|
+
}
|
|
71
|
+
/** Disconnect and stop reconnecting. */
|
|
72
|
+
async stop() {
|
|
73
|
+
this._stopped = true;
|
|
74
|
+
this.ws.removeListener('message', this.boundOnMessage);
|
|
75
|
+
this.ws.removeListener('close', this.boundOnClose);
|
|
76
|
+
await this.ws.disconnect();
|
|
77
|
+
this.emit('disconnected');
|
|
78
|
+
}
|
|
79
|
+
/** Whether the monitor is actively connected and listening. */
|
|
80
|
+
get connected() {
|
|
81
|
+
return this.ws.connectionState === 'CONNECTED';
|
|
82
|
+
}
|
|
83
|
+
/** Current head status (uppercase). */
|
|
84
|
+
get headStatus() {
|
|
85
|
+
return this._headStatus;
|
|
86
|
+
}
|
|
87
|
+
/** Current head status (mixed-case, as reported in Greetings). */
|
|
88
|
+
get headStatusMixed() {
|
|
89
|
+
return HYDRA_TO_HEAD_STATUS[this._headStatus];
|
|
90
|
+
}
|
|
91
|
+
/** The full Greetings message from the most recent connection. */
|
|
92
|
+
get greetings() {
|
|
93
|
+
return this.ws.lastGreetings;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Summary of Hydra head info derived from live state plus the last Greetings.
|
|
97
|
+
*
|
|
98
|
+
* `headStatus` and `headId` reflect the current state tracked from transition
|
|
99
|
+
* messages (`HeadIsOpen`, `HeadIsClosed`, etc.), not the snapshot taken
|
|
100
|
+
* at connection time. The remaining fields (node version, participants,
|
|
101
|
+
* contestation period, network info) come from the cached Greetings since
|
|
102
|
+
* they are static for the life of the head.
|
|
103
|
+
*
|
|
104
|
+
* Excludes the full UTxO snapshot to keep payloads small.
|
|
105
|
+
* Returns `null` if no Greetings has been received yet.
|
|
106
|
+
*/
|
|
107
|
+
get headInfo() {
|
|
108
|
+
const g = this.greetings;
|
|
109
|
+
if (!g)
|
|
110
|
+
return null;
|
|
111
|
+
const peers = g.env?.configuredPeers;
|
|
112
|
+
const peerCount = peers ? peers.split(',').filter(Boolean).length : 0;
|
|
113
|
+
return {
|
|
114
|
+
headStatus: HYDRA_TO_HEAD_STATUS[this._headStatus],
|
|
115
|
+
headId: this._headId,
|
|
116
|
+
nodeVersion: g.hydraNodeVersion ?? null,
|
|
117
|
+
me: g.me.vkey,
|
|
118
|
+
contestationPeriod: g.env?.contestationPeriod ?? null,
|
|
119
|
+
depositPeriod: g.env?.depositPeriod ?? null,
|
|
120
|
+
participants: g.env?.participants ?? [],
|
|
121
|
+
networkConnected: g.networkInfo?.networkConnected ?? false,
|
|
122
|
+
peerCount,
|
|
123
|
+
chainSyncedStatus: g.chainSyncedStatus ?? null,
|
|
124
|
+
currentSlot: g.currentSlot ?? null,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/** The last N events (configurable via eventBufferSize). Most recent last. */
|
|
128
|
+
get recentEvents() {
|
|
129
|
+
return this._events;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Wait for headStatus to reach the target. Resolves immediately if already there.
|
|
133
|
+
* @param target - The HydraStatus to wait for.
|
|
134
|
+
* @param timeoutMs - Maximum wait time (default 60s).
|
|
135
|
+
*/
|
|
136
|
+
waitForStatus(target, timeoutMs = 60_000) {
|
|
137
|
+
if (this._headStatus === target)
|
|
138
|
+
return Promise.resolve();
|
|
139
|
+
return new Promise((resolve, reject) => {
|
|
140
|
+
const timer = setTimeout(() => {
|
|
141
|
+
this.removeListener('status', onStatus);
|
|
142
|
+
reject(new Error(`Timeout waiting for status "${target}" (current: "${this._headStatus}")`));
|
|
143
|
+
}, timeoutMs);
|
|
144
|
+
const onStatus = (status) => {
|
|
145
|
+
if (status === target) {
|
|
146
|
+
clearTimeout(timer);
|
|
147
|
+
this.removeListener('status', onStatus);
|
|
148
|
+
resolve();
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
this.on('status', onStatus);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Wait for the next message matching the given tag.
|
|
156
|
+
* @param tag - The message tag to wait for.
|
|
157
|
+
* @param timeoutMs - Maximum wait time (default 60s).
|
|
158
|
+
*/
|
|
159
|
+
waitForMessage(tag, timeoutMs = 60_000) {
|
|
160
|
+
return new Promise((resolve, reject) => {
|
|
161
|
+
const timer = setTimeout(() => {
|
|
162
|
+
this.removeListener('message', onMsg);
|
|
163
|
+
reject(new Error(`Timeout waiting for message "${tag}"`));
|
|
164
|
+
}, timeoutMs);
|
|
165
|
+
const onMsg = (msg) => {
|
|
166
|
+
if (msg.tag === tag) {
|
|
167
|
+
clearTimeout(timer);
|
|
168
|
+
this.removeListener('message', onMsg);
|
|
169
|
+
resolve(msg);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
this.on('message', onMsg);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
onMessage(msg) {
|
|
176
|
+
// Ring buffer
|
|
177
|
+
this._events.push({ timestamp: Date.now(), message: msg });
|
|
178
|
+
if (this._events.length > this.eventBufferSize) {
|
|
179
|
+
this._events.shift();
|
|
180
|
+
}
|
|
181
|
+
// Forward to listeners
|
|
182
|
+
this.emit('message', msg);
|
|
183
|
+
// Status tracking
|
|
184
|
+
if (msg.tag === 'Greetings' && 'headStatus' in msg) {
|
|
185
|
+
const mapped = HEAD_STATUS_TO_HYDRA[msg.headStatus];
|
|
186
|
+
if (mapped)
|
|
187
|
+
this.updateStatus(mapped);
|
|
188
|
+
const greetingHeadId = msg.hydraHeadId;
|
|
189
|
+
if (greetingHeadId)
|
|
190
|
+
this._headId = greetingHeadId;
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
const mapped = TAG_TO_HYDRA[msg.tag];
|
|
194
|
+
if (mapped)
|
|
195
|
+
this.updateStatus(mapped);
|
|
196
|
+
// Track headId from transition messages (Greetings may not include
|
|
197
|
+
// it for a fresh node — under Hydra v2's direct-open flow, HeadIsOpen
|
|
198
|
+
// is the first place it reliably appears).
|
|
199
|
+
const msgHeadId = msg.headId;
|
|
200
|
+
if (typeof msgHeadId === 'string' && msgHeadId) {
|
|
201
|
+
this._headId = msgHeadId;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// Error routing
|
|
205
|
+
if (msg.tag === 'PostTxOnChainFailed' || msg.tag === 'TxInvalid') {
|
|
206
|
+
this.emit('error:tx', msg);
|
|
207
|
+
}
|
|
208
|
+
else if (msg.tag === 'CommandFailed') {
|
|
209
|
+
this.emit('error:command', msg);
|
|
210
|
+
}
|
|
211
|
+
else if (msg.tag === 'DecommitInvalid') {
|
|
212
|
+
this.emit('error:decommit', msg);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
updateStatus(newStatus) {
|
|
216
|
+
if (this._headStatus !== newStatus) {
|
|
217
|
+
this._previousStatus = this._headStatus;
|
|
218
|
+
this._headStatus = newStatus;
|
|
219
|
+
this.emit('status', newStatus, this._previousStatus);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
onClose() {
|
|
223
|
+
if (this._stopped)
|
|
224
|
+
return;
|
|
225
|
+
this.ws.removeListener('message', this.boundOnMessage);
|
|
226
|
+
this.ws.removeListener('close', this.boundOnClose);
|
|
227
|
+
if (this.reconnectEnabled) {
|
|
228
|
+
this.reconnectLoop();
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
this.emit('disconnected');
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
async reconnectLoop() {
|
|
235
|
+
if (this._reconnecting)
|
|
236
|
+
return;
|
|
237
|
+
this._reconnecting = true;
|
|
238
|
+
this.emit('disconnected');
|
|
239
|
+
for (let attempt = 0; attempt < this.maxAttempts; attempt++) {
|
|
240
|
+
if (this._stopped) {
|
|
241
|
+
this._reconnecting = false;
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const delay = Math.min(this.baseDelayMs * 2 ** attempt, this.maxDelayMs);
|
|
245
|
+
this.emit('reconnecting', attempt + 1, delay);
|
|
246
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
247
|
+
if (this._stopped) {
|
|
248
|
+
this._reconnecting = false;
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
await this.ws.waitForGreetings();
|
|
253
|
+
this.ws.on('message', this.boundOnMessage);
|
|
254
|
+
this.ws.on('close', this.boundOnClose);
|
|
255
|
+
// Process the Greetings from the new connection
|
|
256
|
+
if (this.ws.lastGreetings) {
|
|
257
|
+
this.onMessage(this.ws.lastGreetings);
|
|
258
|
+
}
|
|
259
|
+
this._reconnecting = false;
|
|
260
|
+
this.emit('connected');
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
// Retry on next iteration
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
this._reconnecting = false;
|
|
268
|
+
this.emit('reconnect_failed');
|
|
269
|
+
}
|
|
270
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import type { ClientInput, ConnectionState, HeadStatus, HydraStatus, HydraWsMessage } from './types.js';
|
|
3
|
+
export declare const HEAD_STATUS_TO_HYDRA: Record<HeadStatus, HydraStatus>;
|
|
4
|
+
export declare const TAG_TO_HYDRA: Record<string, HydraStatus>;
|
|
5
|
+
/**
|
|
6
|
+
* Thin WebSocket wrapper for the Hydra node.
|
|
7
|
+
*
|
|
8
|
+
* Uses `EventEmitter` for multi-listener message dispatch (no single-callback
|
|
9
|
+
* overwrite). Emits:
|
|
10
|
+
*
|
|
11
|
+
* - `'message'` — `(msg: HydraWsMessage)` for every incoming message
|
|
12
|
+
* - `'status'` — `(status: HydraStatus)` on head-status changes
|
|
13
|
+
* - `'error'` — `(err: Error)`
|
|
14
|
+
* - `'close'` — `()`
|
|
15
|
+
*/
|
|
16
|
+
export declare class HydraWebSocket extends EventEmitter {
|
|
17
|
+
private ws;
|
|
18
|
+
private readonly url;
|
|
19
|
+
private _status;
|
|
20
|
+
private _connectionState;
|
|
21
|
+
private _lastGreetings;
|
|
22
|
+
constructor(wsUrl: string);
|
|
23
|
+
/** Current connection state. */
|
|
24
|
+
get connectionState(): ConnectionState;
|
|
25
|
+
/** Open the WebSocket connection. Resolves when the socket is open. */
|
|
26
|
+
connect(): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Connect and wait for the Greetings message from the Hydra node.
|
|
29
|
+
*
|
|
30
|
+
* Unlike `HydraProvider.isConnected()`, this does NOT overwrite existing
|
|
31
|
+
* message handlers — it uses a one-time EventEmitter listener.
|
|
32
|
+
*/
|
|
33
|
+
waitForGreetings(timeoutMs?: number): Promise<boolean>;
|
|
34
|
+
/** Close the WebSocket. */
|
|
35
|
+
disconnect(timeoutMs?: number): Promise<void>;
|
|
36
|
+
/** Send a ClientInput message as JSON over the WebSocket. */
|
|
37
|
+
send(message: ClientInput): void;
|
|
38
|
+
/** Current Hydra head status derived from messages. */
|
|
39
|
+
getStatus(): HydraStatus;
|
|
40
|
+
/** Register a status-change listener. Returns current status. */
|
|
41
|
+
onStatusChange(callback: (status: HydraStatus) => void): HydraStatus;
|
|
42
|
+
/** The last Greetings message received, if any. */
|
|
43
|
+
get lastGreetings(): HydraWsMessage | null;
|
|
44
|
+
private handleMessage;
|
|
45
|
+
private updateStatus;
|
|
46
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import WebSocket from 'ws';
|
|
3
|
+
export const HEAD_STATUS_TO_HYDRA = {
|
|
4
|
+
Idle: 'IDLE',
|
|
5
|
+
Open: 'OPEN',
|
|
6
|
+
Closed: 'CLOSED',
|
|
7
|
+
FanoutPossible: 'FANOUT_POSSIBLE',
|
|
8
|
+
Final: 'FINAL',
|
|
9
|
+
};
|
|
10
|
+
export const TAG_TO_HYDRA = {
|
|
11
|
+
HeadIsOpen: 'OPEN',
|
|
12
|
+
HeadIsClosed: 'CLOSED',
|
|
13
|
+
ReadyToFanout: 'FANOUT_POSSIBLE',
|
|
14
|
+
HeadIsFinalized: 'FINAL',
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Thin WebSocket wrapper for the Hydra node.
|
|
18
|
+
*
|
|
19
|
+
* Uses `EventEmitter` for multi-listener message dispatch (no single-callback
|
|
20
|
+
* overwrite). Emits:
|
|
21
|
+
*
|
|
22
|
+
* - `'message'` — `(msg: HydraWsMessage)` for every incoming message
|
|
23
|
+
* - `'status'` — `(status: HydraStatus)` on head-status changes
|
|
24
|
+
* - `'error'` — `(err: Error)`
|
|
25
|
+
* - `'close'` — `()`
|
|
26
|
+
*/
|
|
27
|
+
export class HydraWebSocket extends EventEmitter {
|
|
28
|
+
ws = null;
|
|
29
|
+
url;
|
|
30
|
+
_status = 'IDLE';
|
|
31
|
+
_connectionState = 'IDLE';
|
|
32
|
+
_lastGreetings = null;
|
|
33
|
+
constructor(wsUrl) {
|
|
34
|
+
super();
|
|
35
|
+
this.url = wsUrl;
|
|
36
|
+
}
|
|
37
|
+
/** Current connection state. */
|
|
38
|
+
get connectionState() {
|
|
39
|
+
return this._connectionState;
|
|
40
|
+
}
|
|
41
|
+
/** Open the WebSocket connection. Resolves when the socket is open. */
|
|
42
|
+
async connect() {
|
|
43
|
+
if (this.ws?.readyState === WebSocket.OPEN)
|
|
44
|
+
return;
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
const ws = new WebSocket(this.url);
|
|
47
|
+
ws.on('open', () => {
|
|
48
|
+
this.ws = ws;
|
|
49
|
+
this._connectionState = 'CONNECTING';
|
|
50
|
+
resolve();
|
|
51
|
+
});
|
|
52
|
+
ws.on('message', (data) => {
|
|
53
|
+
try {
|
|
54
|
+
const msg = JSON.parse(data.toString());
|
|
55
|
+
this.handleMessage(msg);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// Ignore non-JSON messages
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
ws.on('error', (err) => {
|
|
62
|
+
this._connectionState = 'FAILED';
|
|
63
|
+
this.emit('error', err);
|
|
64
|
+
reject(new Error(`WebSocket error: ${err.message}`));
|
|
65
|
+
});
|
|
66
|
+
ws.on('close', () => {
|
|
67
|
+
this._connectionState = 'DISCONNECTED';
|
|
68
|
+
this.ws = null;
|
|
69
|
+
this.emit('close');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Connect and wait for the Greetings message from the Hydra node.
|
|
75
|
+
*
|
|
76
|
+
* Unlike `HydraProvider.isConnected()`, this does NOT overwrite existing
|
|
77
|
+
* message handlers — it uses a one-time EventEmitter listener.
|
|
78
|
+
*/
|
|
79
|
+
async waitForGreetings(timeoutMs = 30_000) {
|
|
80
|
+
if (this._connectionState === 'CONNECTED')
|
|
81
|
+
return true;
|
|
82
|
+
// Register the listener BEFORE connecting so the Greetings message
|
|
83
|
+
// (which the Hydra node sends immediately after the socket opens)
|
|
84
|
+
// cannot arrive before we're listening for it.
|
|
85
|
+
return new Promise((resolve, reject) => {
|
|
86
|
+
const timer = setTimeout(() => {
|
|
87
|
+
this.removeListener('message', onMsg);
|
|
88
|
+
this._connectionState = 'FAILED';
|
|
89
|
+
reject(new Error('Connection timed out: no Greetings from Hydra node'));
|
|
90
|
+
}, timeoutMs);
|
|
91
|
+
const onMsg = (msg) => {
|
|
92
|
+
if (msg.tag === 'Greetings') {
|
|
93
|
+
clearTimeout(timer);
|
|
94
|
+
this.removeListener('message', onMsg);
|
|
95
|
+
this._connectionState = 'CONNECTED';
|
|
96
|
+
resolve(true);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
this.on('message', onMsg);
|
|
100
|
+
this.connect().catch((err) => {
|
|
101
|
+
clearTimeout(timer);
|
|
102
|
+
this.removeListener('message', onMsg);
|
|
103
|
+
reject(err);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/** Close the WebSocket. */
|
|
108
|
+
async disconnect(timeoutMs = 5000) {
|
|
109
|
+
if (!this.ws || this.ws.readyState === WebSocket.CLOSED) {
|
|
110
|
+
this._connectionState = 'DISCONNECTED';
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
return new Promise((resolve) => {
|
|
114
|
+
const timer = setTimeout(() => {
|
|
115
|
+
this.ws?.terminate();
|
|
116
|
+
this._connectionState = 'DISCONNECTED';
|
|
117
|
+
this.ws = null;
|
|
118
|
+
resolve();
|
|
119
|
+
}, timeoutMs);
|
|
120
|
+
this.ws.on('close', () => {
|
|
121
|
+
clearTimeout(timer);
|
|
122
|
+
this._connectionState = 'DISCONNECTED';
|
|
123
|
+
this.ws = null;
|
|
124
|
+
resolve();
|
|
125
|
+
});
|
|
126
|
+
this.ws.close();
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
/** Send a ClientInput message as JSON over the WebSocket. */
|
|
130
|
+
send(message) {
|
|
131
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
132
|
+
throw new Error('WebSocket is not connected');
|
|
133
|
+
}
|
|
134
|
+
this.ws.send(JSON.stringify(message));
|
|
135
|
+
}
|
|
136
|
+
/** Current Hydra head status derived from messages. */
|
|
137
|
+
getStatus() {
|
|
138
|
+
return this._status;
|
|
139
|
+
}
|
|
140
|
+
/** Register a status-change listener. Returns current status. */
|
|
141
|
+
onStatusChange(callback) {
|
|
142
|
+
this.on('status', callback);
|
|
143
|
+
return this._status;
|
|
144
|
+
}
|
|
145
|
+
/** The last Greetings message received, if any. */
|
|
146
|
+
get lastGreetings() {
|
|
147
|
+
return this._lastGreetings;
|
|
148
|
+
}
|
|
149
|
+
handleMessage(msg) {
|
|
150
|
+
// Strip sensitive fields before caching or emitting
|
|
151
|
+
if (msg.tag === 'Greetings') {
|
|
152
|
+
const sanitized = { ...msg };
|
|
153
|
+
if (sanitized.env && typeof sanitized.env === 'object') {
|
|
154
|
+
const { signingKey: _, ...safeEnv } = sanitized.env;
|
|
155
|
+
sanitized.env = safeEnv;
|
|
156
|
+
}
|
|
157
|
+
this._lastGreetings = sanitized;
|
|
158
|
+
this.emit('message', sanitized);
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
this.emit('message', msg);
|
|
162
|
+
}
|
|
163
|
+
// Update status from Greetings headStatus
|
|
164
|
+
if (msg.tag === 'Greetings' && 'headStatus' in msg) {
|
|
165
|
+
const mapped = HEAD_STATUS_TO_HYDRA[msg.headStatus];
|
|
166
|
+
if (mapped)
|
|
167
|
+
this.updateStatus(mapped);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
// Update status from state transition messages
|
|
171
|
+
const mapped = TAG_TO_HYDRA[msg.tag];
|
|
172
|
+
if (mapped)
|
|
173
|
+
this.updateStatus(mapped);
|
|
174
|
+
}
|
|
175
|
+
updateStatus(newStatus) {
|
|
176
|
+
if (this._status !== newStatus) {
|
|
177
|
+
this._status = newStatus;
|
|
178
|
+
this.emit('status', newStatus);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-exported Hydra WebSocket message types.
|
|
3
|
+
*
|
|
4
|
+
* `ServerOutput` is a discriminated union (on `.tag`) of all possible messages
|
|
5
|
+
* the Hydra node can send over its WebSocket API. Use `Extract` to narrow:
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* function handleOpen(msg: HydraMessage<"HeadIsOpen">) {
|
|
10
|
+
* console.log(msg.tag);
|
|
11
|
+
* }
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export type { ClientInput, ClientMessage, ConfirmedSnapshot, ConnectionState, HeadStatus, HydraHeadInfo, HydraMessage, HydraMonitorOptions, HydraSnapshot, HydraStatus, HydraTransaction, HydraWsMessage, hydraStatus, hydraTransaction, ServerOutput, TimestampedEvent, } from './types.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|