117 lines
3.8 KiB
JavaScript
117 lines
3.8 KiB
JavaScript
// ZeroMQ client for connecting to Flink control channels
|
|
import * as zmq from 'zeromq';
|
|
import { decodeMessage } from './proto/messages.js';
|
|
|
|
export class ZmqClient {
|
|
constructor(config, logger) {
|
|
this.config = config;
|
|
this.logger = logger;
|
|
|
|
// Work queue - SUB socket to receive data requests with exchange prefix filtering
|
|
this.workSocket = null;
|
|
|
|
// NOTE: NO RESPONSE SOCKET - Async architecture via Kafka!
|
|
// Ingestors write data to Kafka only
|
|
// Flink processes and publishes notifications
|
|
|
|
this.isShutdown = false;
|
|
this.supportedExchanges = config.supported_exchanges || ['BINANCE', 'COINBASE'];
|
|
}
|
|
|
|
/**
|
|
* Connect to Relay ZMQ endpoints
|
|
*/
|
|
async connect() {
|
|
const { flink_hostname, ingestor_work_port } = this.config;
|
|
|
|
// Connect to work queue (SUB with exchange prefix filtering)
|
|
this.workSocket = new zmq.Subscriber();
|
|
const workEndpoint = `tcp://${flink_hostname}:${ingestor_work_port}`;
|
|
await this.workSocket.connect(workEndpoint);
|
|
|
|
// Subscribe to each supported exchange suffix (Nautilus format: "BTC/USDT.BINANCE")
|
|
for (const exchange of this.supportedExchanges) {
|
|
const prefix = `${exchange}.`;
|
|
this.workSocket.subscribe(prefix);
|
|
this.logger.info(`Subscribed to exchange prefix: ${prefix}`);
|
|
}
|
|
this.logger.info(`Connected to work queue at ${workEndpoint}`);
|
|
this.logger.info('ASYNC MODE: No response socket - data flows via Kafka → Flink → pub/sub notification');
|
|
}
|
|
|
|
/**
|
|
* Pull a data request from the work queue
|
|
* @returns {Promise<object>} Decoded DataRequest message
|
|
*/
|
|
async pullDataRequest() {
|
|
if (this.isShutdown) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
const frames = await this.workSocket.receive();
|
|
this.logger.info({
|
|
frameCount: frames.length,
|
|
frame0Len: frames[0]?.length,
|
|
frame1Len: frames[1]?.length,
|
|
frame2Len: frames[2]?.length,
|
|
frame0: frames[0]?.toString('utf8').substring(0, 50),
|
|
frame1Hex: frames[1]?.toString('hex').substring(0, 20),
|
|
frame2Hex: frames[2]?.toString('hex').substring(0, 20)
|
|
}, 'Received raw ZMQ frames');
|
|
|
|
// First frame is the topic (exchange prefix), skip it
|
|
// Remaining frames are: [version_frame, message_frame]
|
|
if (frames.length < 3) {
|
|
this.logger.warn({ frameCount: frames.length }, 'Unexpected frame count');
|
|
return null;
|
|
}
|
|
const messageFrames = frames.slice(1); // Skip topic, keep version + message
|
|
const { version, typeId, message } = decodeMessage(messageFrames);
|
|
this.logger.info({
|
|
version,
|
|
typeId: `0x${typeId.toString(16)}`,
|
|
requestId: message.requestId,
|
|
type: message.type,
|
|
typeOf: typeof message.type,
|
|
ticker: message.ticker
|
|
}, 'Decoded data request');
|
|
return message;
|
|
} catch (error) {
|
|
if (!this.isShutdown) {
|
|
this.logger.error({ error: error.message, stack: error.stack }, 'Error receiving data request');
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Start listening for control messages in the background
|
|
* @param {Function} handler - Callback function to handle control messages
|
|
*
|
|
* NOTE: Control channel not implemented yet. This is a stub for future use.
|
|
* For now, just log and ignore.
|
|
*/
|
|
startControlListener(handler) {
|
|
this.logger.info('Control channel listener stub - not implemented yet');
|
|
// TODO: Implement control channel when needed
|
|
// Control messages would be used for:
|
|
// - Canceling realtime subscriptions
|
|
// - Graceful shutdown signals
|
|
// - Configuration updates
|
|
}
|
|
|
|
/**
|
|
* Shutdown and close connections
|
|
*/
|
|
async shutdown() {
|
|
this.isShutdown = true;
|
|
this.logger.info('Shutting down ZMQ connections');
|
|
|
|
if (this.workSocket) {
|
|
await this.workSocket.close();
|
|
}
|
|
}
|
|
}
|