Skip to content

Commit 7eff8a0

Browse files
authored
Merge branch 'main' into feature/enforce-type-checks
2 parents 565a89a + 77d60ad commit 7eff8a0

4 files changed

Lines changed: 314 additions & 12 deletions

File tree

PR_DESCRIPTION_362.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ Closes #362
66

77
Implements an opt-in telemetry hook system that allows application developers to integrate their own monitoring solutions (Sentry, Datadog, custom telemetry) without the SDK having any direct dependencies on monitoring libraries.
88

9-
## Changes
10-
119
### Core Implementation
1210

1311
- **New file**: `src/telemetryHooks.ts` - Telemetry hook manager and TypeScript types

src/client.ts

Lines changed: 81 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,8 @@ import type {
154154
SSEInvoiceEvent,
155155
} from "./sse.js";
156156
import { ConnectionPool } from "./connectionPool.js";
157+
import { WebSocketTransport } from "./websocket.js";
158+
import type { TransportType, TransportStatus } from "./websocket.js";
157159
import { snapshotInvoice as _snapshotInvoice } from "./snapshot.js";
158160
import type { InvoiceSnapshot } from "./snapshot.js";
159161
import { SimpleCache } from "./cache.js";
@@ -327,6 +329,21 @@ export interface StellarSplitClientConfig {
327329
* a MockRpcClient) or alternative transport environments.
328330
*/
329331
rpcClient?: RpcClient;
332+
/**
333+
* Transport selection for real-time invoice event streaming.
334+
* - `'http'` (default): Use polling-based RPC event fetching.
335+
* - `'websocket'`: Use WebSocket connection to the RPC's event-streaming
336+
* endpoint for pushed events. Falls back to HTTP polling if the WebSocket
337+
* connection fails after 3 reconnect attempts.
338+
*/
339+
transport?: TransportType;
340+
/**
341+
* Optional WebSocket URL override. When not provided, the WebSocket URL is
342+
* derived from the RPC URL by replacing `https://` with `wss://` (or
343+
* `http://` with `ws://`).
344+
* Only used when `transport: 'websocket'`.
345+
*/
346+
wsUrl?: string;
330347
}
331348

332349
/** Network configuration. */
@@ -468,6 +485,10 @@ export class StellarSplitClient {
468485
private _timeoutManager: TimeoutManager | null = null;
469486
private _traceIdManager = new TraceIdManager();
470487
private _injectedRpcClient: RpcClient | null = null;
488+
private _wsTransport: WebSocketTransport | null = null;
489+
private _transportType: TransportType = 'http';
490+
private _activeTransportType: TransportType = 'http';
491+
private _fallbackListeners: Array<(event: { from: 'websocket'; to: 'http' }) => void> = [];
471492

472493
// eslint-disable-next-line @typescript-eslint/no-explicit-any
473494
private get server(): any {
@@ -640,6 +661,19 @@ export class StellarSplitClient {
640661
this._idempotency = new IdempotencyManager(config.idempotency);
641662
}
642663

664+
// WebSocket transport (Issue #377)
665+
if (config.transport === 'websocket') {
666+
this._transportType = 'websocket';
667+
this._activeTransportType = 'websocket';
668+
this._wsTransport = new WebSocketTransport(primaryUrl, config.wsUrl);
669+
this._wsTransport.onFallback((event: { from: 'websocket'; to: 'http' }) => {
670+
this._activeTransportType = 'http';
671+
for (const cb of this._fallbackListeners) {
672+
try { cb(event); } catch { }
673+
}
674+
});
675+
}
676+
643677
initHealthDashboard(this.server, this._dedup);
644678

645679
// Register and initialize config-level plugins
@@ -1447,12 +1481,7 @@ export class StellarSplitClient {
14471481
*/
14481482
async getInvoice(
14491483
invoiceId: string,
1450-
opts?: {
1451-
retry?: PerMethodRetryOptions;
1452-
dedupe?: boolean;
1453-
traceId?: string;
1454-
timeout?: number;
1455-
},
1484+
opts?: { retry?: PerMethodRetryOptions; dedupe?: boolean; traceId?: string; timeout?: number }
14561485
): Promise<Invoice> {
14571486
return this._withCache("getInvoice", [invoiceId], async () => {
14581487
const fetcher = this._batcher
@@ -1482,10 +1511,7 @@ export class StellarSplitClient {
14821511
return this._dedup.getDedupStats();
14831512
}
14841513

1485-
private async _fetchInvoice(
1486-
invoiceId: string,
1487-
traceId?: string,
1488-
): Promise<Invoice> {
1514+
private async _fetchInvoice(invoiceId: string, traceId?: string): Promise<Invoice> {
14891515
const startTime = Date.now();
14901516
const req = {
14911517
method: "getInvoice",
@@ -1878,6 +1904,9 @@ export class StellarSplitClient {
18781904
} finally {
18791905
this._standby?.stop();
18801906

1907+
this._wsTransport?.disconnect();
1908+
this._wsTransport = null;
1909+
18811910
this._pool?.dispose();
18821911
this._pool = null;
18831912

@@ -2579,6 +2608,26 @@ export class StellarSplitClient {
25792608
| ((events: SSEInvoiceEvent[]) => void),
25802609
optionsOrInterval?: Partial<SubscribeToInvoiceOptions> | number,
25812610
): () => void {
2611+
// WebSocket transport: use the active WebSocket connection when configured
2612+
if (this._wsTransport && this._transportType === 'websocket') {
2613+
if (typeof handlerOrCallbacks !== "function") {
2614+
throw new ValidationError(
2615+
"WebSocket transport requires a function handler. Callbacks object is not supported."
2616+
);
2617+
}
2618+
2619+
const handler = handlerOrCallbacks as InvoiceEventHandler;
2620+
const wrappedHandler = (event: unknown) => {
2621+
handler(event as SSEInvoiceEvent);
2622+
};
2623+
2624+
this._wsTransport.subscribe(invoiceId, wrappedHandler);
2625+
2626+
return () => {
2627+
this._wsTransport?.unsubscribe(invoiceId, wrappedHandler);
2628+
};
2629+
}
2630+
25822631
// A function handler with options object selects the SSE transport.
25832632
// A function handler with number interval selects the RPC polling transport (new API).
25842633
// A callbacks object selects the legacy RPC-polling transport.
@@ -2622,6 +2671,28 @@ export class StellarSplitClient {
26222671
);
26232672
}
26242673

2674+
/**
2675+
* Returns the current status of the active transport.
2676+
*
2677+
* When `transport: 'websocket'` was configured, returns `{ type: 'websocket', connected, reconnectAttempts }`.
2678+
* Otherwise returns `{ type: 'http', connected: true, reconnectAttempts: 0 }`.
2679+
*/
2680+
getTransportStatus(): TransportStatus {
2681+
if (this._wsTransport) {
2682+
return this._wsTransport.getStatus();
2683+
}
2684+
return { type: 'http', connected: true, reconnectAttempts: 0 };
2685+
}
2686+
2687+
/**
2688+
* Register a callback for the `transport:fallback` event.
2689+
* Fired when the WebSocket transport fails to connect after 3 attempts
2690+
* and the client falls back to HTTP polling.
2691+
*/
2692+
onTransportFallback(cb: (event: { from: 'websocket'; to: 'http' }) => void): void {
2693+
this._fallbackListeners.push(cb);
2694+
}
2695+
26252696
// ---------------------------------------------------------------------------
26262697
// Issue #3 — offline signing flow
26272698
// ---------------------------------------------------------------------------

src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,10 @@ export type {
459459
EventSourceLike,
460460
} from "./sse.js";
461461
export type { PollingInvoiceEventHandler } from "./stream.js";
462+
463+
// WebSocket transport (Issue #377)
464+
export { WebSocketTransport } from "./websocket.js";
465+
export type { TransportType, TransportStatus, TransportEventMap } from "./websocket.js";
462466
export {
463467
bundleDisputeEvidence,
464468
computeBundleChecksum,

0 commit comments

Comments
 (0)