Skip to content

Commit 15cf913

Browse files
authored
Merge pull request #3 from MeshJS/initial-commit
update isConnecting
2 parents bb7f2e4 + 885e1b8 commit 15cf913

4 files changed

Lines changed: 204 additions & 11 deletions

File tree

src/hydra-provider.ts

Lines changed: 195 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ import {
2626
hydraUTxO,
2727
hydraUTxOs,
2828
ServerOutput,
29-
} from "./types";
29+
ConnectionState,
30+
} from ".";
3031
import { handleHydraErrors } from "./types/events/handler";
3132
import {
3233
PostTxOnChainFailed,
@@ -52,6 +53,11 @@ export class HydraProvider implements IFetcher, ISubmitter {
5253
| ((data: ServerOutput | ClientMessage) => void)
5354
| null = null;
5455
private _messageQueue: (ServerOutput | ClientMessage)[] = [];
56+
private _disconnectTimeout: NodeJS.Timeout | null = null;
57+
private _isDisconnecting: boolean = false;
58+
private _currentStatus: hydraStatus = "IDLE";
59+
private _connectionState: ConnectionState = "IDLE";
60+
private _connectingPromise: Promise<boolean> | null = null;
5561

5662
constructor({
5763
httpUrl,
@@ -86,27 +92,134 @@ export class HydraProvider implements IFetcher, ISubmitter {
8692
}
8793
},
8894
);
95+
96+
this._eventEmitter.on("onstatuschange", (status: hydraStatus) => {
97+
this._currentStatus = status;
98+
});
8999
}
90100

91101
/**
92-
* Connects to the Hydra Head.
93-
*/
94-
async connect() {
95-
this._connection.connect();
102+
* Connects to the Hydra Head socket only.
103+
*/
104+
async connect(): Promise<void> {
105+
try {
106+
await this._connection.connect();
107+
} catch (error) {
108+
throw new Error(
109+
`Failed to connect to Hydra Head: ${error instanceof Error ? error.message : String(error)
110+
}`,
111+
);
112+
}
113+
}
114+
/**
115+
* Connects (if needed) and waits until Hydra confirms readiness via "Greetings".
116+
*
117+
* - Idempotent
118+
* - No dangling handlers
119+
* - Accurate state tracking
120+
*/
121+
async isConnected(timeoutMs = 30_000): Promise<boolean> {
122+
// Fast path
123+
if (this._connectionState === "CONNECTED") {
124+
return true;
125+
}
126+
127+
// Reuse in-flight connection
128+
if (this._connectingPromise) {
129+
return this._connectingPromise;
130+
}
131+
132+
this._connectingPromise = new Promise<boolean>(async (resolve, reject) => {
133+
let timeout: NodeJS.Timeout;
134+
135+
const finalize = (state: ConnectionState, result?: boolean, error?: Error) => {
136+
clearTimeout(timeout);
137+
this._connectingPromise = null;
138+
this._connectionState = state;
139+
140+
if (error) reject(error);
141+
else resolve(result ?? false);
142+
};
143+
144+
try {
145+
await this.connect();
146+
} catch (err) {
147+
finalize("FAILED", false, err as Error);
148+
return;
149+
}
150+
151+
timeout = setTimeout(() => {
152+
finalize(
153+
"FAILED",
154+
false,
155+
new Error("Connection timed out: no Greetings from Hydra node"),
156+
);
157+
}, timeoutMs);
158+
159+
this.onMessage((msg) => {
160+
if (this._connectionState !== "CONNECTING") return;
161+
162+
if (msg.tag === "Greetings") {
163+
finalize("CONNECTED", true);
164+
return;
165+
}
166+
167+
if (handleHydraErrors(msg as ClientMessage, (err) => {
168+
finalize("FAILED", false, err);
169+
})) {
170+
return;
171+
}
172+
});
173+
});
174+
175+
return this._connectingPromise;
96176
}
97177

178+
98179
/**
99180
* Disconnects from the Hydra Head.
100181
*
101182
* @param timeout Optional timeout in milliseconds (defaults to 5 minutes) to wait for the disconnect operation to complete.
183+
* If set to 0, disconnects immediately (reactive to clicks/events).
102184
* If not provided, the default disconnect timeout will be used.
103185
* Useful for customizing how long to wait before disconnecting.
186+
* @throws {Error} If timeout is less than 0 or between 1 and 59,999 ms
104187
*/
105188
async disconnect(timeout: number = 300_000) {
106-
if (timeout < 60_000) {
107-
throw new Error("Timeout must be at least 60,000 ms (1 minute)");
189+
if (timeout < 0) {
190+
throw new Error("Timeout must be a non-negative number");
191+
}
192+
if (timeout > 0 && timeout < 60_000) {
193+
throw new Error(
194+
"Timeout must be at least 60,000 ms (1 minute) or 0 for immediate disconnect",
195+
);
196+
}
197+
198+
const clearPendingTimeout = () => {
199+
if (this._disconnectTimeout) {
200+
clearTimeout(this._disconnectTimeout);
201+
this._disconnectTimeout = null;
202+
}
203+
};
204+
205+
if (timeout === 0) {
206+
clearPendingTimeout();
207+
this._isDisconnecting = false;
208+
await this._connection.disconnect(0);
209+
return;
108210
}
109-
await this._connection.disconnect(timeout);
211+
212+
if (this._isDisconnecting) return;
213+
214+
this._isDisconnecting = true;
215+
this._disconnectTimeout = setTimeout(async () => {
216+
try {
217+
await this._connection.disconnect(0);
218+
} finally {
219+
this._disconnectTimeout = null;
220+
this._isDisconnecting = false;
221+
}
222+
}, timeout);
110223
}
111224

112225
/**
@@ -139,6 +252,8 @@ export class HydraProvider implements IFetcher, ISubmitter {
139252
}
140253
if (handleHydraErrors(msg as ClientMessage, reject)) {
141254
return;
255+
} else {
256+
reject(new Error("Failed to initialize, head is not in Idle state"));
142257
}
143258
});
144259
});
@@ -161,6 +276,10 @@ export class HydraProvider implements IFetcher, ISubmitter {
161276
}
162277
if (handleHydraErrors(msg as ClientMessage, reject)) {
163278
return;
279+
} else {
280+
reject(
281+
new Error("Failed to abort, head is not in Initializing state"),
282+
);
164283
}
165284
});
166285
});
@@ -203,6 +322,12 @@ export class HydraProvider implements IFetcher, ISubmitter {
203322
`Transaction invalid: ${JSON.stringify(msg.validationError)}`,
204323
),
205324
);
325+
} else {
326+
reject(
327+
new Error(
328+
"Failed to submit transaction, head is not in Open state",
329+
),
330+
);
206331
}
207332
});
208333
});
@@ -233,6 +358,12 @@ export class HydraProvider implements IFetcher, ISubmitter {
233358
}
234359
if (handleHydraErrors(msg as ClientMessage, reject)) {
235360
return;
361+
} else {
362+
reject(
363+
new Error(
364+
"Failed to recover transaction, head is not in Open state",
365+
),
366+
);
236367
}
237368
});
238369
});
@@ -270,6 +401,8 @@ export class HydraProvider implements IFetcher, ISubmitter {
270401
}
271402
if (handleHydraErrors(msg as ClientMessage, reject)) {
272403
return;
404+
} else {
405+
reject(new Error("Failed to decommit, head is not in Open state"));
273406
}
274407
});
275408
});
@@ -303,6 +436,8 @@ export class HydraProvider implements IFetcher, ISubmitter {
303436
if (handleHydraErrors(message as ClientMessage, reject)) {
304437
reject(new Error("Failed to close head"));
305438
return;
439+
} else {
440+
reject(new Error("Failed to close, head is not in Open state"));
306441
}
307442
});
308443
});
@@ -325,6 +460,8 @@ export class HydraProvider implements IFetcher, ISubmitter {
325460
if (handleHydraErrors(msg as ClientMessage, reject)) {
326461
reject(new Error("Failed to contest head"));
327462
return;
463+
} else {
464+
reject(new Error("Failed to contest, head is not in Closed state"));
328465
}
329466
});
330467
});
@@ -349,6 +486,8 @@ export class HydraProvider implements IFetcher, ISubmitter {
349486
if (handleHydraErrors(msg as ClientMessage, reject)) {
350487
reject(new Error("Failed to fanout head"));
351488
return;
489+
} else {
490+
reject(new Error("Failed to fanout, head is not in Closed state"));
352491
}
353492
});
354493
});
@@ -738,16 +877,62 @@ export class HydraProvider implements IFetcher, ISubmitter {
738877
return protocolParams;
739878
}
740879

880+
/**
881+
* Registers a callback to receive messages from the Hydra Head.
882+
* When called, the callback will immediately be invoked for all messages that have already been received
883+
* (queued in the message queue), and subsequently for each new incoming message.
884+
*
885+
* @param callback - The function to call with each ServerOutput or ClientMessage received.
886+
*
887+
* @example
888+
* ```ts
889+
* hydraProvider.onMessage((message) => {
890+
* console.log("Received Hydra message:", message);
891+
* });
892+
* ```
893+
*/
741894
onMessage(callback: (data: ServerOutput | ClientMessage) => void) {
742895
this._messageCallback = callback;
743896
this._messageQueue.forEach((message) => {
744897
callback(message);
745898
});
746899
}
747-
onStatusChange(callback: (status: hydraStatus) => void) {
748-
this._eventEmitter.on("onstatuschange", (status) => {
900+
901+
/**
902+
* Subscribe to status changes of the Hydra Head.
903+
* The callback will be called whenever the status changes.
904+
*
905+
* @param callback Function to call when status changes, receives the new hydraStatus
906+
* @returns The current status
907+
*
908+
* @example
909+
* ```ts
910+
* hydraProvider.onStatusChange((status) => {
911+
* console.log(`Hydra Head status changed to: ${status}`);
912+
* });
913+
* ```
914+
*/
915+
onStatusChange(callback: (status: hydraStatus) => void): hydraStatus {
916+
this._eventEmitter.on("onstatuschange", (status: hydraStatus) => {
917+
this._currentStatus = status;
749918
callback(status);
750919
});
920+
return this._currentStatus;
921+
}
922+
923+
/**
924+
* Get the current status of the Hydra Head.
925+
*
926+
* @returns The current hydraStatus
927+
*
928+
* @example
929+
* ```ts
930+
* const currentStatus = hydraProvider.getStatus();
931+
* console.log(`Current status: ${currentStatus}`);
932+
* ```
933+
*/
934+
getStatus(): hydraStatus {
935+
return this._currentStatus;
751936
}
752937

753938
/**

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from "./hydra-instance";
22
export * from "./hydra-provider";
3+
export * from "./types"

src/types/events/connection.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export type ConnectionState =
2+
| "IDLE"
3+
| "CONNECTING"
4+
| "CONNECTED"
5+
| "FAILED"
6+
| "DISCONNECTED";

src/types/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ export * from "./hydra/hydraTransaction";
55
export * from "./hydra/hydraUTxOs";
66
export * from "./client-input";
77
export * from "./client-message";
8-
export * from "./server-output";
8+
export * from "./server-output";
9+
export * from "./events/connection";

0 commit comments

Comments
 (0)