@stoqey/ib
Version:
Interactive Brokers TWS/IB Gateway API client library for Node.js (TS)
389 lines • 14.1 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Socket = exports.ConnectionStatus = void 0;
const net_1 = __importDefault(require("net"));
const util_1 = require("util");
const api_1 = require("../../api/api");
const event_name_1 = require("../../api/data/enum/event-name");
const min_server_version_1 = __importDefault(require("../../api/data/enum/min-server-version"));
const configuration_1 = __importDefault(require("../../common/configuration"));
const errorCode_1 = require("../../common/errorCode");
const encoder_1 = require("./encoder");
/**
* @hidden
* envelope encoding, applicable to useV100Plus mode only
*/
const MIN_VERSION_V100 = 100;
/**
* @hidden
* max message size, taken from Java client, applicable to useV100Plus mode only
*/
const MAX_V100_MESSAGE_LENGTH = 0xffffff;
/** @hidden */
const EOL = "\0";
/**
* @hidden
* add a delay after connect before sending commands
*/
// const CONNECT_DELAY = 600;
exports.ConnectionStatus = {
Disconnected: 0,
Disconnecting: 1,
Connecting: 2,
Connected: 3,
};
/**
* @internal
*
* This class implements low-level details on the communication protocol of the
* TWS/IB Gateway API server.
*/
class Socket {
/**
* Create a new [[Socket]] object.
*
* @param controller The parent [[Controller]] object.
* @param options The API creation options.
*/
constructor(controller, options = {}) {
this.controller = controller;
this.options = options;
/** `connected` if the TCP socket is connected and [[OUT_MSG_ID.START_API]] has been sent. */
this._status = exports.ConnectionStatus.Disconnected;
/** The IB API Server version, or 0 if not connected yet. */
this._serverVersion = 0;
/** The server connection time. */
this._serverConnectionTime = "";
/** Data fragment accumulation buffer. */
this.dataFragment = "";
/** `true` if no message from server has been received yet, `false` otherwise. */
this.neverReceived = true;
/** `true` if waiting for completion of an async operation, `false` otherwise. */
this.waitingAsync = false;
/** `true` if V!00Pls protocol shall be used, `false` otherwise. */
this.useV100Plus = true;
/** Accumulation buffer for fragmented V100 messages */
this._v100MessageBuffer = Buffer.alloc(0);
this._clientId =
this.options.clientId !== undefined
? Math.floor(this.options.clientId)
: configuration_1.default.default_client_id;
// this.options.host = this.options.host;
// this.options.port = this.options.port;
}
/** Returns `true` if connected to TWS/IB Gateway, `false` otherwise. */
get connected() {
return this._status === exports.ConnectionStatus.Connected;
}
/** Returns connection status */
get status() {
return this._status;
}
/** Returns the IB API Server version. */
get serverVersion() {
return this._serverVersion;
}
/** The server connection time. */
get serverConnectionTime() {
return this._serverConnectionTime;
}
/** Get the current client id. */
get clientId() {
return this._clientId;
}
/**
* Disable usage of V100Plus protocol.
*/
disableUseV100Plus() {
this.useV100Plus = false;
}
/**
* Connect to the API server.
*
* @param clientId A unique client id (per TWS or IB Gateway instance).
* When not specified, the client from [[IBApiCreationOptions]] or the
* default client id (0) will used.
*/
connect(clientId) {
// Reject any connect attempt is not disconnected
if (this._status >= exports.ConnectionStatus.Connecting)
return;
this._status = exports.ConnectionStatus.Connecting;
// update client id
if (clientId !== undefined) {
this._clientId = Math.floor(clientId);
}
// pause controller while API startup sequence
this.controller.pause();
// reset state
this.dataFragment = "";
this.neverReceived = true;
this.waitingAsync = false;
this._v100MessageBuffer = Buffer.alloc(0);
// create and connect TCP socket
this.client = net_1.default
.connect({
host: this.options.host ?? configuration_1.default.ib_host,
port: this.options.port ?? configuration_1.default.ib_port,
}, () => this.onConnect())
.on("data", (data) => this.onData(data))
.on("close", () => this.onEnd())
.on("end", () => this.onEnd())
.on("error", (error) => this.onError(error));
}
/**
* Disconnect from API server.
*/
disconnect() {
this._status = exports.ConnectionStatus.Disconnecting;
// pause controller while connection is down.
this.controller.pause();
// disconnect TCP socket.
this.client?.end();
this.client?.destroy();
}
/**
* Send tokens to API server.
*/
send(tokens) {
// flatten arrays and convert boolean types to 0/1
tokens = this.flattenDeep(tokens);
tokens.forEach((value, i) => {
if (value === true || value === false || value instanceof Boolean) {
tokens[i] = value ? 1 : 0;
}
});
let stringData = tokens.join(EOL);
if (this.useV100Plus) {
let utf8Data;
if (tokens[0] === "API\0") {
// this is the initial API version message, which is special:
// length is encoded after the 'API\0', followed by the actual tokens.
const skip = 5; // 1 x 'API\0' token + 4 x length tokens
stringData = tokens.slice(skip)[0];
utf8Data = [
...this.stringToUTF8Array(tokens[0]),
...tokens.slice(1, skip),
...this.stringToUTF8Array(stringData),
];
}
else {
utf8Data = this.stringToUTF8Array(stringData);
}
// add length prefix only if not a string (strings use pre-V100 style)
if (typeof tokens[0] !== "string") {
utf8Data = [
...this.numberTo32BitBigEndian(utf8Data.length + 1),
...utf8Data,
0,
];
}
this.client?.write(Buffer.from(new Uint8Array(utf8Data)));
}
else {
this.client?.write(stringData + EOL);
}
this.controller.emitEvent(event_name_1.EventName.sent, tokens, stringData);
}
/**
* Called when data on the TCP socket has been arrived.
*/
onData(data) {
if (this.useV100Plus) {
this._v100MessageBuffer = Buffer.concat([this._v100MessageBuffer, data]);
if (this._v100MessageBuffer.length > MAX_V100_MESSAGE_LENGTH) {
// At this point we have buffered enough data that we have exceeded the max known message length,
// at which point this is likely an unrecoverable state and we should discard all prior data,
// and disconnect the socket
const size = this._v100MessageBuffer.length;
this._v100MessageBuffer = Buffer.alloc(0);
this.onError(new Error(`Message of size ${size} exceeded max message length ${MAX_V100_MESSAGE_LENGTH}`));
this.disconnect();
return;
}
while (this._v100MessageBuffer.length > 4) {
const msgSize = this._v100MessageBuffer.readInt32BE();
if (this._v100MessageBuffer.length >= 4 + msgSize) {
const segment = this._v100MessageBuffer.slice(4, 4 + msgSize);
this._v100MessageBuffer = this._v100MessageBuffer.slice(4 + msgSize);
this.onMessage(segment.toString("utf8"));
}
else {
// else keep data for later
return;
}
}
}
else {
this.onMessage(data.toString());
}
}
/**
* Called when new tokens have been received from server.
*/
onMessage(data) {
// tokenize
const dataWithFragment = this.dataFragment + data;
let tokens = dataWithFragment.split(EOL);
if (tokens[tokens.length - 1] !== "") {
this.dataFragment = tokens[tokens.length - 1];
}
else {
this.dataFragment = "";
}
tokens = tokens.slice(0, -1);
this.controller.emitEvent(event_name_1.EventName.received, tokens.slice(0), data);
// handle message data
if (this.neverReceived) {
// first message
this.neverReceived = false;
this.onServerVersion(tokens);
}
else {
// post to queue
if (this.useV100Plus) {
this.controller.onMessage(tokens);
}
else {
this.controller.onTokens(tokens);
}
// process queue
this.controller.processIngressQueue();
}
// resume from async state
if (this.waitingAsync) {
this.waitingAsync = false;
this.controller.resume();
}
}
/**
* Called when first data has arrived on the connection.
*/
onServerVersion(tokens) {
this._status = exports.ConnectionStatus.Connected;
this._serverVersion = parseInt(tokens[0], 10);
this._serverConnectionTime = tokens[1];
if (this.useV100Plus &&
(this._serverVersion < MIN_VERSION_V100 ||
this._serverVersion > api_1.MAX_SUPPORTED_SERVER_VERSION)) {
this.disconnect();
this.controller.emitError(`Unsupported Version ${this._serverVersion}`, errorCode_1.ErrorCode.UNSUPPORTED_VERSION);
return;
}
if (this._serverVersion < api_1.MIN_SERVER_VER_SUPPORTED) {
this.disconnect();
this.controller.emitError("The TWS is out of date and must be upgraded.", errorCode_1.ErrorCode.UPDATE_TWS);
return;
}
this.startAPI();
this.controller.emitEvent(event_name_1.EventName.connected);
this.controller.emitEvent(event_name_1.EventName.server, this.serverVersion, this.serverConnectionTime);
}
/**
* Start the TWS/IB Gateway API.
*/
startAPI() {
// start API
const VERSION = 2;
if (this.serverVersion >= 3) {
if (this.serverVersion < min_server_version_1.default.LINKING) {
this.send([this._clientId]);
}
else {
if (this.serverVersion >= min_server_version_1.default.OPTIONAL_CAPABILITIES) {
this.send([encoder_1.OUT_MSG_ID.START_API, VERSION, this._clientId, ""]);
}
else {
this.send([encoder_1.OUT_MSG_ID.START_API, VERSION, this._clientId]);
}
}
}
// resume controller moved to crontroller
// setTimeout(() => {
// this.controller.resume();
// }, CONNECT_DELAY);
}
/**
* Called when TCP socket has been connected.
*/
onConnect() {
// send client version (unless Version > 100)
if (!this.useV100Plus) {
this.send([configuration_1.default.client_version]);
this.send([this._clientId]);
}
else {
// Switch to GW API (Version 100+ requires length prefix)
const config = this.buildVersionString(MIN_VERSION_V100, api_1.MAX_SUPPORTED_SERVER_VERSION);
// config = config + connectOptions --- connectOptions are for IB internal use only: not supported
this.send([
"API\0",
...this.numberTo32BitBigEndian(config.length),
config,
]);
}
}
/**
* Called when TCP socket connection has been closed.
*/
onEnd() {
if (this._status) {
this._status = exports.ConnectionStatus.Disconnected;
this.controller.emitEvent(event_name_1.EventName.disconnected);
}
this.controller.pause();
}
/**
* Called when an error occurred on the TCP socket connection.
*/
onError(err) {
this.controller.emitError(err.message, errorCode_1.ErrorCode.CONNECT_FAIL);
}
/**
* Build a V100Plus API version string.
*/
buildVersionString(minVersion, maxVersion) {
return ("v" +
(minVersion < maxVersion ? minVersion + ".." + maxVersion : minVersion));
}
/**
* Convert a (integer) number to a 4-byte big endian byte array.
*/
numberTo32BitBigEndian(val) {
const result = new Array(4);
let pos = 0;
result[pos++] = 0xff & (val >> 24);
result[pos++] = 0xff & (val >> 16);
result[pos++] = 0xff & (val >> 8);
result[pos++] = 0xff & val;
return result;
}
/**
* Encode a string to a UTF8 byte array.
*/
stringToUTF8Array(val) {
return Array.from(new util_1.TextEncoder().encode(val));
}
/**
* Flatten an array.
*
* Also works for nested arrays (i.e. arrays inside arrays inside arrays)
*/
flattenDeep(arr, result = []) {
for (let i = 0, length = arr.length; i < length; i++) {
const value = arr[i];
if (Array.isArray(value)) {
this.flattenDeep(value, result);
}
else {
result.push(value);
}
}
return result;
}
}
exports.Socket = Socket;
//# sourceMappingURL=socket.js.map