Skip to content

Commit a705ac5

Browse files
committed
feat(websocket): add user-thread dispatch and shared-memory examples
- Upgrade slick-net integration to 3.0.0 dynamic-buffer backend - Add caller-thread WebSocket dispatch APIs and external mux support - Add market data examples for user-thread dispatch and shared-memory reading - Add offline tests for dispatch routing and shared-memory attachment - Update README and changelog for the new APIs, examples, and logging hooks
1 parent 58e9800 commit a705ac5

15 files changed

Lines changed: 916 additions & 38 deletions

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
### Added
1313
- Add a `market_data_websocket` example that subscribes to public testnet `allMids` and per-coin `l2Book` WebSocket updates for one or more coins.
1414
- Add a `market_data_websocket_per_coin` example that subscribes to ETH and BTC market data using one WebSocket connection per coin.
15+
- Add user-thread WebSocket dispatch support through `Info::dispatch()`, `WebsocketManager::dispatch()`, and `user_thread_dispatch` constructor options.
16+
- Add `Info` and `WebsocketManager` constructors that accept an external `slick::stream_buffer_multiplexer` for shared dispatch queues.
17+
- Add `market_data_websocket_user_thread_dispatch`, `market_data_websocket_per_coin_user_thread_dispatch`, and `market_data_websocket_shm_reader` examples.
18+
- Add shared-memory WebSocket buffer configuration options so external readers can attach to the market-data stream.
1519

1620
### Changed
1721
- Normalize line ending to LF
22+
- Update the WebSocket transport integration for `slick-net` 3.0.0 and the slick dynamic-buffer stream-buffer-multiplexer backend.
23+
- Move example target registration into `examples/CMakeLists.txt`.
24+
- Publish `market_data_websocket` records to named shared-memory segments that `market_data_websocket_shm_reader` can open.
1825
- Move test CMake setup into `tests/CMakeLists.txt`, register GoogleTest cases with `gtest_discover_tests()`, and keep CI integration tests running as a single executable to avoid repeated testnet setup per discovered test case.
1926
- Defer GoogleTest discovery to CTest with a longer discovery timeout so test executables are not run during the build.
2027
- Stabilize the WebSocket partial-unsubscribe integration test by waiting for the next `allMids` event instead of assuming one arrives within a fixed one-second sleep.
2128

29+
### Tests
30+
- Add offline regression tests for user-thread WebSocket dispatch routing, foreign producer filtering, and shared-memory writer/reader attachment.
31+
2232
## [0.1.2] - 2026-06-09
2333

2434
### Added

CMakeLists.txt

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,20 @@ endif()
1414
find_package(nlohmann_json CONFIG REQUIRED)
1515
find_package(OpenSSL REQUIRED)
1616

17-
find_package(slick-net 2.1.0 CONFIG QUIET)
17+
find_package(slick-net 3.0.0 CONFIG QUIET)
1818
if (NOT slick-net_FOUND)
1919
message(STATUS "Fetching slick-net...")
2020
include(FetchContent)
21+
set(BUILD_SLICK_NET_EXAMPLES OFF CACHE BOOL "" FORCE)
22+
set(BUILD_SLICK_NET_TESTS OFF CACHE BOOL "" FORCE)
2123
FetchContent_Declare(
2224
slick-net
2325
GIT_REPOSITORY https://github.com/SlickQuant/slick-net.git
24-
GIT_TAG v2.1.0
26+
GIT_TAG v3.0.0
2527
)
2628
FetchContent_MakeAvailable(slick-net)
2729
else()
28-
message(STATUS "Found slick-net: ${slick-net_DIR}")
30+
message(STATUS "Found slick-net ${slick-net_VERSION}: ${slick-net_DIR}")
2931
endif()
3032

3133
add_library(hyperliquid STATIC
@@ -51,14 +53,7 @@ target_link_libraries(hyperliquid PUBLIC
5153
)
5254

5355
if(HYPERLIQUID_BUILD_EXAMPLES)
54-
add_executable(basic_order examples/basic_order.cpp)
55-
target_link_libraries(basic_order PRIVATE hyperliquid)
56-
57-
add_executable(market_data_websocket examples/market_data_websocket.cpp)
58-
target_link_libraries(market_data_websocket PRIVATE hyperliquid)
59-
60-
add_executable(market_data_websocket_per_coin examples/market_data_websocket_per_coin.cpp)
61-
target_link_libraries(market_data_websocket_per_coin PRIVATE hyperliquid)
56+
add_subdirectory(examples)
6257
endif()
6358

6459
if(HYPERLIQUID_BUILD_TESTS)

README.md

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ All authenticated actions are signed locally using EIP-712 / secp256k1; your pri
2626
- Read-only market data (`Info`): universe metadata, mid prices, order books, candles, funding history, account state
2727
- Authenticated trading (`Exchange`): limit/market/trigger orders, bulk ops, cancel, modify, leverage, margin, USD/spot transfers, bridge withdrawal
2828
- Real-time WebSocket subscriptions via `Info::subscribe` with multi-callback fan-out per channel
29+
- Optional caller-thread WebSocket dispatch via `Info::dispatch()` for applications that own their event loop
30+
- Optional shared-memory WebSocket buffers backed by `slick::stream_buffer_multiplexer`
2931
- Native EIP-712 signing for both L1 actions (orders, leverage) and user-signed actions (transfers) using OpenSSL secp256k1
3032
- `nlohmann::ordered_json` + msgpack action hashing — byte-for-byte compatible with the Python SDK signing output
3133
- Embedded Keccak-256 (Ethereum's pre-FIPS variant — distinct from OpenSSL's SHA3-256)
@@ -58,7 +60,7 @@ cd vcpkg
5860
vcpkg install nlohmann-json openssl gtest
5961
```
6062

61-
`slick-net` is the HTTP/WebSocket library used by this SDK. Install it through its own distribution and register it in the same vcpkg instance:
63+
`slick-net` 3.0.0 is the HTTP/WebSocket library used by this SDK. Install it through its own distribution and register it in the same vcpkg instance:
6264

6365
```bash
6466
vcpkg install slick-net
@@ -88,7 +90,10 @@ Targets produced:
8890
| `hyperliquid` | Static library |
8991
| `basic_order` | Example executable |
9092
| `market_data_websocket` | Public market data WebSocket example |
93+
| `market_data_websocket_user_thread_dispatch` | Public market data WebSocket example that dispatches callbacks on the caller thread |
9194
| `market_data_websocket_per_coin` | Public market data WebSocket example using one connection per coin and slick-net logging hooks |
95+
| `market_data_websocket_per_coin_user_thread_dispatch` | Per-coin WebSocket example using a shared dispatch multiplexer and slick-net logging hooks |
96+
| `market_data_websocket_shm_reader` | Shared-memory reader for records published by `market_data_websocket` |
9297
| `hyperliquid_tests` | Offline unit tests |
9398
| `hyperliquid_integration_tests` | Live testnet integration tests |
9499

@@ -189,17 +194,57 @@ info->unsubscribe({{"type", "l2Book"}, {"coin", "ETH"}}, sid2);
189194
The WebSocket manager sends periodic pings to keep connections alive and uses
190195
bounded atomic shutdown checks so teardown does not wait for the full ping interval.
191196
197+
For applications that need callbacks on their own thread, enable caller-thread dispatch
198+
and poll queued WebSocket records:
199+
200+
```cpp
201+
hyperliquid::Info info(
202+
hyperliquid::TESTNET_API_URL,
203+
/*skip_ws=*/false,
204+
/*user_thread_dispatch=*/true);
205+
206+
info.subscribe({{"type", "allMids"}}, [](const nlohmann::json& msg) {
207+
std::cout << msg.dump() << "\n";
208+
});
209+
210+
while (running) {
211+
info.dispatch();
212+
std::this_thread::sleep_for(std::chrono::milliseconds(1));
213+
}
214+
```
215+
192216
---
193217

194218
## API Reference — Info
195219

196220
Construct with:
197221

198222
```cpp
199-
hyperliquid::Info info(base_url, skip_ws = false);
223+
hyperliquid::Info info(
224+
base_url,
225+
skip_ws = false,
226+
user_thread_dispatch = false,
227+
mux_record_size = 1u << 18,
228+
mux_shm_name = nullptr,
229+
read_buffer_size = 1u << 24,
230+
read_control_size = 1u << 16,
231+
read_buffer_shm_name = nullptr,
232+
write_buffer_size = 1u << 20);
233+
234+
hyperliquid::Info info_with_mux(
235+
base_url,
236+
mux,
237+
skip_ws = false,
238+
user_thread_dispatch = false,
239+
read_buffer_size = 1u << 24,
240+
read_control_size = 1u << 16,
241+
read_buffer_shm_name = nullptr,
242+
write_buffer_size = 1u << 20);
200243
```
201244
202245
Setting `skip_ws = true` disables the WebSocket connection (REST-only mode, lower overhead).
246+
Setting `user_thread_dispatch = true` queues inbound WebSocket records in the slick stream-buffer multiplexer; call `dispatch()` to invoke matching callbacks on the caller thread.
247+
The multiplexer parameters can name shared-memory segments, and the external `mux` overload lets multiple `Info` instances share one dispatch queue.
203248
204249
### Market data
205250
@@ -237,6 +282,10 @@ Candle `interval` values: `"1m"` `"5m"` `"15m"` `"30m"` `"1h"` `"4h"` `"8h"` `"1
237282
|--------|-------------|
238283
| `subscribe(sub_json, callback)` | Subscribe to a channel; returns `subscription_id` (positive int) |
239284
| `unsubscribe(sub_json, subscription_id)` | Remove one callback; double-unsubscribe is a no-op |
285+
| `websocket()` | Return the underlying `WebsocketManager*`, or `nullptr` when WebSocket support is disabled |
286+
| `ws_producer_id()` | Return this WebSocket's stream-buffer producer id, or `INVALID_PRODUCER_ID` when unavailable |
287+
| `dispatch(max_count=100)` | Scan queued records and invoke this `Info` instance's callbacks on the caller thread |
288+
| `dispatch(producer_id, data, length)` | Dispatch one queued message for this `Info` instance's producer id |
240289
| `load_meta()` | Populate `coin_to_asset`, `name_to_coin`, and `asset_to_sz_decimals` (called automatically by `Exchange`) |
241290
242291
`coin_to_asset` maps canonical wire coins to asset ids. Perps are 0-based; spot assets use `10000 + spot index`.
@@ -419,15 +468,33 @@ Each callback receives the full message object:
419468
# Subscribe to allMids and l2Book updates on one WebSocket connection
420469
./build/Debug/market_data_websocket ETH BTC --seconds 30
421470

471+
# Same subscriptions, but callbacks are dispatched by the caller thread
472+
./build/Debug/market_data_websocket_user_thread_dispatch ETH BTC --seconds 30
473+
422474
# Subscribe to ETH and BTC l2Book updates using one WebSocket connection per coin
423475
./build/Debug/market_data_websocket_per_coin 30
476+
477+
# Per-coin connections sharing one dispatch multiplexer
478+
./build/Debug/market_data_websocket_per_coin_user_thread_dispatch 30
479+
480+
# Terminal 1: publish WebSocket records to named shared-memory segments
481+
./build/Debug/market_data_websocket ETH BTC --seconds 30
482+
483+
# Terminal 2: read records from the same shared-memory segments
484+
./build/Debug/market_data_websocket_shm_reader --seconds 30
424485
```
425486

426487
`basic_order` places a resting limit buy on testnet and cancels it if it is resting.
427488
The market data examples subscribe to public testnet WebSocket feeds and print
428489
received updates for the requested duration.
429490
`market_data_websocket_per_coin` also demonstrates configuring slick-net's
430491
runtime logging hooks and routing `LOG_INFO` / `LOG_ERROR` output to `std::cout`.
492+
`market_data_websocket_per_coin_user_thread_dispatch` demonstrates the same
493+
slick-net logging hooks while dispatching one feed on the caller thread through
494+
a shared `slick::stream_buffer_multiplexer`.
495+
`market_data_websocket` publishes records to the `mux_queue` and `md_buf`
496+
shared-memory segments so `market_data_websocket_shm_reader` can attach from a
497+
separate process.
431498

432499
---
433500

@@ -440,7 +507,7 @@ cmake --build build --config Debug --target hyperliquid_tests
440507
ctest --test-dir build -C Debug -R hyperliquid_tests -V
441508
```
442509

443-
Covers: Keccak-256 vectors, EIP-712 signing round-trips, type serialisation (`float_to_wire`, `Cloid`, `Tif`), WebSocket URL conversion, and channel identifier generation.
510+
Covers: Keccak-256 vectors, EIP-712 signing round-trips, type serialisation (`float_to_wire`, `Cloid`, `Tif`), WebSocket URL conversion, channel identifier generation, caller-thread WebSocket dispatch routing, and shared-memory stream-buffer attachment.
444511

445512
### Integration tests (requires testnet)
446513

cmake/hyperliquid-config.cmake.in

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
@PACKAGE_INIT@
22

33
include(CMakeFindDependencyMacro)
4-
find_dependency(nlohmann_json CONFIG)
5-
find_dependency(OpenSSL)
4+
find_dependency(nlohmann_json CONFIG REQUIRED)
5+
find_dependency(OpenSSL REQUIRED)
66

7-
find_package(slick-net 2.1.0 CONFIG QUIET)
7+
find_package(slick-net 3.0.0 CONFIG QUIET)
88
if (NOT slick-net_FOUND)
99
message(STATUS "Fetching slick-net...")
1010
include(FetchContent)
11+
set(BUILD_SLICK_NET_EXAMPLES OFF CACHE BOOL "" FORCE)
12+
set(BUILD_SLICK_NET_TESTS OFF CACHE BOOL "" FORCE)
1113
FetchContent_Declare(
1214
slick-net
1315
GIT_REPOSITORY https://github.com/SlickQuant/slick-net.git
14-
GIT_TAG v2.1.0
16+
GIT_TAG v3.0.0
1517
)
1618
FetchContent_MakeAvailable(slick-net)
1719
endif()

examples/CMakeLists.txt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
add_executable(basic_order basic_order.cpp)
2+
target_link_libraries(basic_order PRIVATE hyperliquid)
3+
4+
add_executable(market_data_websocket market_data_websocket.cpp)
5+
target_link_libraries(market_data_websocket PRIVATE hyperliquid)
6+
7+
add_executable(market_data_websocket_user_thread_dispatch market_data_websocket_user_thread_dispatch.cpp)
8+
target_link_libraries(market_data_websocket_user_thread_dispatch PRIVATE hyperliquid)
9+
10+
add_executable(market_data_websocket_per_coin market_data_websocket_per_coin.cpp)
11+
target_link_libraries(market_data_websocket_per_coin PRIVATE hyperliquid)
12+
13+
add_executable(market_data_websocket_per_coin_user_thread_dispatch market_data_websocket_per_coin_user_thread_dispatch.cpp)
14+
target_link_libraries(market_data_websocket_per_coin_user_thread_dispatch PRIVATE hyperliquid)
15+
16+
add_executable(market_data_websocket_shm_reader market_data_websocket_shm_reader.cpp)
17+
target_link_libraries(market_data_websocket_shm_reader PRIVATE hyperliquid slick::stream_buffer_multiplexer)

examples/market_data_websocket.cpp

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@
1717

1818
namespace {
1919

20+
static constexpr const char* SHM_MUX_QUEUE_NAME = "mux_queue";
21+
static constexpr const char* SHM_MD_BUF_NAME = "md_buf";
22+
static constexpr uint32_t mux_record_size = 1 << 20;
23+
static constexpr uint32_t read_buffer_size = 1 << 24;
24+
static constexpr uint32_t read_control_size = 1 << 16;
25+
2026
struct Options {
2127
std::vector<std::string> coins{"ETH"};
2228
int seconds = 30;
@@ -99,7 +105,16 @@ int main(int argc, char* argv[]) {
99105
return 1;
100106
}
101107

102-
hyperliquid::Info info(hyperliquid::TESTNET_API_URL, /*skip_ws=*/false);
108+
hyperliquid::Info info(
109+
hyperliquid::TESTNET_API_URL,
110+
/*skip_ws=*/false,
111+
/*user_thread_dispatch*/false,
112+
mux_record_size,
113+
SHM_MUX_QUEUE_NAME,
114+
read_buffer_size,
115+
read_control_size,
116+
SHM_MD_BUF_NAME
117+
);
103118

104119
std::mutex cout_mutex;
105120
std::atomic<int> all_mids_updates{0};

0 commit comments

Comments
 (0)