@proton/ccxt
Version:
A JavaScript / TypeScript / Python / C# / PHP cryptocurrency trading library with support for 130+ exchanges
294 lines (290 loc) • 10.9 kB
JavaScript
'use strict';
var errors = require('../errors.js');
var browser = require('../../static_dependencies/fflake/browser.js');
var Future = require('./Future.js');
var platform = require('../functions/platform.js');
var generic = require('../functions/generic.js');
var encode = require('../functions/encode.js');
require('../functions/crypto.js');
var time = require('../functions/time.js');
var index = require('../../static_dependencies/scure-base/index.js');
class Client {
constructor(url, onMessageCallback, onErrorCallback, onCloseCallback, onConnectedCallback, config = {}) {
const defaults = {
url,
onMessageCallback,
onErrorCallback,
onCloseCallback,
onConnectedCallback,
verbose: false,
protocols: undefined,
options: undefined,
futures: {},
subscriptions: {},
rejections: {},
connected: undefined,
error: undefined,
connectionStarted: undefined,
connectionEstablished: undefined,
isConnected: false,
connectionTimer: undefined,
connectionTimeout: 10000,
pingInterval: undefined,
ping: undefined,
keepAlive: 30000,
maxPingPongMisses: 2.0,
// timeout is not used atm
// timeout: 30000, // throw if a request is not satisfied in 30 seconds, false to disable
connection: undefined,
startedConnecting: false,
gunzip: false,
inflate: false,
};
Object.assign(this, generic.deepExtend(defaults, config));
// connection-related Future
this.connected = Future.createFuture();
}
future(messageHash) {
if (!(messageHash in this.futures)) {
this.futures[messageHash] = Future.createFuture();
}
const future = this.futures[messageHash];
if (messageHash in this.rejections) {
future.reject(this.rejections[messageHash]);
delete this.rejections[messageHash];
}
return future;
}
resolve(result, messageHash) {
if (this.verbose && (messageHash === undefined)) {
this.log(new Date(), 'resolve received undefined messageHash');
}
if (messageHash in this.futures) {
const promise = this.futures[messageHash];
promise.resolve(result);
delete this.futures[messageHash];
}
return result;
}
reject(result, messageHash = undefined) {
if (messageHash) {
if (messageHash in this.futures) {
const promise = this.futures[messageHash];
promise.reject(result);
delete this.futures[messageHash];
}
else {
// in the case that a promise was already fulfilled
// and the client has not yet called watchMethod to create a new future
// calling client.reject will do nothing
// this means the rejection will be ignored and the code will continue executing
// instead we store the rejection for later
this.rejections[messageHash] = result;
}
}
else {
const messageHashes = Object.keys(this.futures);
for (let i = 0; i < messageHashes.length; i++) {
this.reject(result, messageHashes[i]);
}
}
return result;
}
log(...args) {
console.log(...args);
// console.dir (args, { depth: null })
}
connect(backoffDelay = 0) {
throw new errors.NotSupported('connect() not implemented yet');
}
isOpen() {
throw new errors.NotSupported('isOpen() not implemented yet');
}
reset(error) {
this.clearConnectionTimeout();
this.clearPingInterval();
this.reject(error);
}
onConnectionTimeout() {
if (!this.isOpen()) {
const error = new errors.RequestTimeout('Connection to ' + this.url + ' failed due to a connection timeout');
this.onError(error);
this.connection.close(1006);
}
}
setConnectionTimeout() {
if (this.connectionTimeout) {
const onConnectionTimeout = this.onConnectionTimeout.bind(this);
this.connectionTimer = setTimeout(onConnectionTimeout, this.connectionTimeout);
}
}
clearConnectionTimeout() {
if (this.connectionTimer) {
this.connectionTimer = clearTimeout(this.connectionTimer);
}
}
setPingInterval() {
if (this.keepAlive) {
const onPingInterval = this.onPingInterval.bind(this);
this.pingInterval = setInterval(onPingInterval, this.keepAlive);
}
}
clearPingInterval() {
if (this.pingInterval) {
this.pingInterval = clearInterval(this.pingInterval);
}
}
onPingInterval() {
if (this.keepAlive && this.isOpen()) {
const now = time.milliseconds();
this.lastPong = this.lastPong || now;
if ((this.lastPong + this.keepAlive * this.maxPingPongMisses) < now) {
this.onError(new errors.RequestTimeout('Connection to ' + this.url + ' timed out due to a ping-pong keepalive missing on time'));
}
else {
if (this.ping) {
this.send(this.ping(this));
}
else if (platform.isNode) {
// can't do this inside browser
// https://stackoverflow.com/questions/10585355/sending-websocket-ping-pong-frame-from-browser
this.connection.ping();
}
else {
// browsers handle ping-pong automatically therefore
// in a browser we update lastPong on every call to
// this function as if pong just came in to prevent the
// client from thinking it's a stalled connection
this.lastPong = now;
}
}
}
}
onOpen() {
if (this.verbose) {
this.log(new Date(), 'onOpen');
}
this.connectionEstablished = time.milliseconds();
this.isConnected = true;
this.connected.resolve(this.url);
// this.connection.terminate () // debugging
this.clearConnectionTimeout();
this.setPingInterval();
this.onConnectedCallback(this);
}
// this method is not used at this time, because in JS the ws client will
// respond to pings coming from the server with pongs automatically
// however, some devs may want to track connection states in their app
onPing() {
if (this.verbose) {
this.log(new Date(), 'onPing');
}
}
onPong() {
this.lastPong = time.milliseconds();
if (this.verbose) {
this.log(new Date(), 'onPong');
}
}
onError(error) {
if (this.verbose) {
this.log(new Date(), 'onError', error.message);
}
if (!(error instanceof errors.BaseError)) {
// in case of ErrorEvent from node_modules/ws/lib/event-target.js
error = new errors.NetworkError(error.message);
}
this.error = error;
this.reset(this.error);
this.onErrorCallback(this, this.error);
}
onClose(event) {
if (this.verbose) {
this.log(new Date(), 'onClose', event);
}
if (!this.error) {
// todo: exception types for server-side disconnects
this.reset(new errors.NetworkError('connection closed by remote server, closing code ' + String(event.code)));
}
this.onCloseCallback(this, event);
}
// this method is not used at this time
// but may be used to read protocol-level data like cookies, headers, etc
onUpgrade(message) {
if (this.verbose) {
this.log(new Date(), 'onUpgrade');
}
}
async send(message) {
if (this.verbose) {
this.log(new Date(), 'sending', message);
}
message = (typeof message === 'string') ? message : JSON.stringify(message);
const future = Future.createFuture();
if (platform.isNode) {
function onSendComplete(error) {
if (error) {
future.reject(error);
}
else {
future.resolve(null);
}
}
this.connection.send(message, {}, onSendComplete);
}
else {
this.connection.send(message);
future.resolve(null);
}
return future;
}
close() {
throw new errors.NotSupported('close() not implemented yet');
}
onMessage(messageEvent) {
// if we use onmessage we get MessageEvent objects
// MessageEvent {isTrusted: true, data: "{"e":"depthUpdate","E":1581358737706,"s":"ETHBTC",…"0.06200000"]],"a":[["0.02261300","0.00000000"]]}", origin: "wss://stream.binance.com:9443", lastEventId: "", source: null, …}
let message = messageEvent.data;
let arrayBuffer;
if (this.gunzip || this.inflate) {
if (typeof message === 'string') {
arrayBuffer = index.utf8.decode(message);
}
else {
arrayBuffer = new Uint8Array(message.buffer.slice(message.byteOffset, message.byteOffset + message.byteLength));
}
if (this.gunzip) {
arrayBuffer = browser.gunzipSync(arrayBuffer);
}
else if (this.inflate) {
arrayBuffer = browser.inflateSync(arrayBuffer);
}
message = index.utf8.encode(arrayBuffer);
}
if (typeof message !== 'string') {
message = message.toString();
}
try {
if (encode.isJsonEncodedObject(message)) {
message = JSON.parse(message.replace(/:(\d{15,}),/g, ':"$1",'));
}
if (this.verbose) {
this.log(new Date(), 'onMessage', message);
// unlimited depth
// this.log (new Date (), 'onMessage', util.inspect (message, false, null, true))
// this.log (new Date (), 'onMessage', JSON.stringify (message, null, 4))
}
}
catch (e) {
this.log(new Date(), 'onMessage JSON.parse', e);
// reset with a json encoding error ?
}
try {
this.onMessageCallback(this, message);
}
catch (error) {
this.reject(error);
}
}
}
module.exports = Client;