Netty-based WebSocket client for CCXT. Matches the architecture of C# Client.cs and Go exchange_wsclient.go: - Single async connection per URL - ConcurrentHashMap for futures and subscriptions - Ping/pong keep-alive loop - Reconnection via lazy re-connect on next watch() call
| 67 | * - Reconnection via lazy re-connect on next watch() call |
| 68 | */ |
| 69 | public class WsClient { |
| 70 | |
| 71 | private static final ObjectMapper JSON_MAPPER = new ObjectMapper(); |
| 72 | |
| 73 | // Shared event loop group across all WsClient instances (one per JVM) |
| 74 | private static final EventLoopGroup SHARED_EVENT_LOOP = new NioEventLoopGroup( |
| 75 | Math.max(2, Runtime.getRuntime().availableProcessors())); |
| 76 | |
| 77 | static { |
| 78 | Runtime.getRuntime().addShutdownHook(new Thread(() -> { |
| 79 | SHARED_EVENT_LOOP.shutdownGracefully(); |
| 80 | }, "ccxt-ws-shutdown")); |
| 81 | } |
| 82 | |
| 83 | // State — typed as Object for compatibility with transpiled code that casts these |
| 84 | public String url; |
| 85 | public Object futures = new ConcurrentHashMap<String, Future>(); |
| 86 | public Object subscriptions = new ConcurrentHashMap<String, Object>(); |
| 87 | public Object rejections = new ConcurrentHashMap<String, Object>(); |
| 88 | public volatile boolean isConnected = false; |
| 89 | public final AtomicBoolean startedConnecting = new AtomicBoolean(false); |
| 90 | public volatile long connectionEstablished = 0; |
| 91 | public volatile CompletableFuture<Boolean> connected; |
| 92 | public volatile long lastPong = 0; |
| 93 | public boolean error = false; |
| 94 | /** |
| 95 | * Optional typed reason for a deliberate close. Set by Exchange.close() |
| 96 | * to an ExchangeClosedByUser before invoking this.close(); the close path |
| 97 | * then uses it to reject in-flight futures so callers can distinguish |
| 98 | * "I asked for this" from a remote-side disconnect. |
| 99 | */ |
| 100 | public volatile Throwable closeReason = null; |
| 101 | |
| 102 | // Guards atomic complete-then-replace of `connected` and ping-thread bookkeeping. |
| 103 | private final Object connectedLock = new Object(); |
| 104 | private volatile Thread pingThread; |
| 105 | |
| 106 | // Typed accessors for internal use |
| 107 | @SuppressWarnings("unchecked") |
| 108 | private ConcurrentHashMap<String, Future> futuresMap() { return (ConcurrentHashMap<String, Future>) futures; } |
| 109 | @SuppressWarnings("unchecked") |
| 110 | public ConcurrentHashMap<String, Object> subscriptionsMap() { return (ConcurrentHashMap<String, Object>) subscriptions; } |
| 111 | @SuppressWarnings("unchecked") |
| 112 | private ConcurrentHashMap<String, Object> rejectionsMap() { return (ConcurrentHashMap<String, Object>) rejections; } |
| 113 | |
| 114 | // Config |
| 115 | public long keepAlive = 30000; |
| 116 | public int maxPingPongMisses = 3; |
| 117 | public boolean verbose = false; |
| 118 | public boolean validateServerSsl = true; |
| 119 | public boolean decompressBinary = true; |
| 120 | |
| 121 | // Callbacks (set by Exchange) |
| 122 | public BiConsumer<WsClient, Object> handleMessageCallback; |
| 123 | public Function<WsClient, Object> pingCallback; |
| 124 | public BiConsumer<WsClient, Object> onCloseCallback; |
| 125 | public BiConsumer<WsClient, Object> onErrorCallback; |
| 126 |