From 36a3cf998af4d81539cd0711171e1ea4c435a227 Mon Sep 17 00:00:00 2001 From: Habib Alkhabbaz <31035020+habibalkhabbaz@users.noreply.github.com> Date: Sun, 14 Aug 2022 10:09:19 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20introduce=20queue=20=E2=9C=A8=20=20(#46?= =?UTF-8?q?4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/__tests__/server-binance.test.js | 146 +- app/__tests__/server-frontend.test.js | 13 + app/binance/__tests__/tickers.test.js | 42 +- app/binance/__tests__/user.test.js | 343 +++-- app/binance/tickers.js | 5 +- app/binance/user.js | 23 +- app/cronjob/__tests__/trailingTrade.test.js | 292 ---- app/cronjob/trailingTrade.js | 34 +- .../__tests__/queue.test.js | 143 ++ app/cronjob/trailingTradeHelper/queue.js | 68 + app/error-handler.js | 2 +- .../bull-board/__tests__/configure.test.js | 105 ++ app/frontend/bull-board/configure.js | 35 + app/frontend/webserver/handlers/404.js | 10 +- .../webserver/handlers/__tests__/404.test.js | 20 +- .../handlers/__tests__/cancel-order.test.js | 19 +- .../manual-trade-all-symbols.test.js | 18 +- .../handlers/__tests__/manual-trade.test.js | 17 +- .../__tests__/symbol-enable-action.test.js | 18 +- .../symbol-grid-trade-delete.test.js | 35 +- .../__tests__/symbol-setting-delete.test.js | 21 +- .../__tests__/symbol-setting-update.test.js | 18 +- .../__tests__/symbol-trigger-buy.test.js | 18 +- .../__tests__/symbol-trigger-sell.test.js | 18 +- .../symbol-update-last-buy-price.test.js | 22 +- .../websocket/handlers/cancel-order.js | 4 +- .../handlers/manual-trade-all-symbols.js | 6 +- .../websocket/handlers/manual-trade.js | 4 +- .../handlers/symbol-enable-action.js | 4 +- .../handlers/symbol-grid-trade-delete.js | 5 +- .../handlers/symbol-setting-delete.js | 4 +- .../handlers/symbol-setting-update.js | 4 +- .../websocket/handlers/symbol-trigger-buy.js | 4 +- .../websocket/handlers/symbol-trigger-sell.js | 4 +- .../handlers/symbol-update-last-buy-price.js | 6 +- app/server-binance.js | 3 + app/server-frontend.js | 4 + package-lock.json | 1196 ++++++++--------- package.json | 10 +- public/js/CoinWrapperAction.js | 4 +- 40 files changed, 1404 insertions(+), 1343 deletions(-) create mode 100644 app/cronjob/trailingTradeHelper/__tests__/queue.test.js create mode 100644 app/cronjob/trailingTradeHelper/queue.js create mode 100644 app/frontend/bull-board/__tests__/configure.test.js create mode 100644 app/frontend/bull-board/configure.js diff --git a/app/__tests__/server-binance.test.js b/app/__tests__/server-binance.test.js index 01c7e5ea..679a7e1c 100644 --- a/app/__tests__/server-binance.test.js +++ b/app/__tests__/server-binance.test.js @@ -1,10 +1,12 @@ /* eslint-disable global-require */ // eslint-disable-next-line max-classes-per-file +const { logger } = require('../helpers'); + describe('server-binance', () => { - let PubSubMock; - let loggerMock; - let cacheMock; - let mongoMock; + let mockPubSub; + let mockCache; + let mockMongo; + let mockQueue; let mockGetGlobalConfiguration; @@ -37,19 +39,48 @@ describe('server-binance', () => { jest.clearAllMocks().resetModules(); jest.useFakeTimers(); - const { PubSub, logger, cache, mongo, slack } = require('../helpers'); - jest.mock('config'); + jest.mock('../cronjob'); config = require('config'); - PubSubMock = PubSub; - loggerMock = logger; - cacheMock = cache; - mongoMock = mongo; - - mockSlack = slack; - mockSlack.sendMessage = jest.fn().mockResolvedValue(true); + mockCache = { + hget: jest + .fn() + .mockResolvedValue( + JSON.stringify(require('./fixtures/exchange-symbols.json')) + ), + hset: jest.fn().mockResolvedValue(true) + }; + mockMongo = { + deleteAll: jest.fn().mockResolvedValue(true) + }; + mockQueue = { + init: jest.fn().mockResolvedValue(true) + }; + mockSlack = { + sendMessage: jest.fn().mockResolvedValue(true) + }; + mockPubSub = { + subscribe: jest.fn(), + publish: jest.fn() + }; + + jest.mock('../helpers', () => ({ + logger: { + info: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + debug: jest.fn(), + child: jest.fn() + }, + slack: mockSlack, + mongo: mockMongo, + PubSub: mockPubSub, + cache: mockCache + })); + + jest.mock('../cronjob/trailingTradeHelper/queue', () => mockQueue); }); describe('when the bot is running live mode', () => { @@ -132,30 +163,19 @@ describe('server-binance', () => { getWebsocketTickersClean: mockGetWebsocketTickersClean })); - mongoMock.deleteAll = jest.fn().mockResolvedValue(true); - - PubSubMock.subscribe = jest.fn().mockResolvedValue(true); - - cacheMock.hget = jest - .fn() - .mockResolvedValue( - JSON.stringify(require('./fixtures/exchange-symbols.json')) - ); - cacheMock.hset = jest.fn().mockResolvedValue(true); - const { runBinance } = require('../server-binance'); - await runBinance(loggerMock); + await runBinance(logger); }); it('triggers PubSub.subscribe for reset-all-websockets', () => { - expect(PubSubMock.subscribe).toHaveBeenCalledWith( + expect(mockPubSub.subscribe).toHaveBeenCalledWith( 'reset-all-websockets', expect.any(Function) ); }); it('triggers PubSub.subscribe for reset-symbol-websockets', () => { - expect(PubSubMock.subscribe).toHaveBeenCalledWith( + expect(mockPubSub.subscribe).toHaveBeenCalledWith( 'reset-symbol-websockets', expect.any(Function) ); @@ -170,50 +190,54 @@ describe('server-binance', () => { }); it('triggers refreshCandles', () => { - expect(mongoMock.deleteAll).toHaveBeenCalledWith( - loggerMock, + expect(mockMongo.deleteAll).toHaveBeenCalledWith( + logger, 'trailing-trade-candles', {} ); - expect(mongoMock.deleteAll).toHaveBeenCalledWith( - loggerMock, + expect(mockMongo.deleteAll).toHaveBeenCalledWith( + logger, 'trailing-trade-ath-candles', {} ); }); it('triggers syncCandles', () => { - expect(mockSyncCandles).toHaveBeenCalledWith(loggerMock, [ + expect(mockSyncCandles).toHaveBeenCalledWith(logger, [ 'BTCUSDT', 'BNBUSDT' ]); }); it('triggers syncATHCandles', () => { - expect(mockSyncATHCandles).toHaveBeenCalledWith(loggerMock, [ + expect(mockSyncATHCandles).toHaveBeenCalledWith(logger, [ 'BTCUSDT', 'BNBUSDT' ]); }); it('triggers syncOpenOrders', () => { - expect(mockSyncOpenOrders).toHaveBeenCalledWith(loggerMock, [ + expect(mockSyncOpenOrders).toHaveBeenCalledWith(logger, [ 'BTCUSDT', 'BNBUSDT' ]); }); it('triggers syncDatabaseOrders', () => { - expect(mockSyncDatabaseOrders).toHaveBeenCalledWith(loggerMock); + expect(mockSyncDatabaseOrders).toHaveBeenCalledWith(logger); }); it('triggers cache.hset', () => { - expect(cacheMock.hset).toHaveBeenCalledWith( + expect(mockCache.hset).toHaveBeenCalledWith( 'trailing-trade-streams', `count`, 1 ); }); + + it('triggers queue.init', () => { + expect(mockQueue.init).toHaveBeenCalled(); + }); }); describe('calculates number of open streams', () => { @@ -303,15 +327,9 @@ describe('server-binance', () => { getWebsocketTickersClean: mockGetWebsocketTickersClean })); - PubSubMock.subscribe = jest.fn().mockResolvedValue(true); - - mongoMock.deleteAll = jest.fn().mockResolvedValue(true); - - cacheMock.hset = jest.fn().mockResolvedValue(true); - const { runBinance } = require('../server-binance'); - await runBinance(loggerMock); + await runBinance(logger); }); it('triggers getWebsocketTickersClean', () => { @@ -331,7 +349,7 @@ describe('server-binance', () => { }); it('triggers cache.hset', () => { - expect(cacheMock.hset).toHaveBeenCalledWith( + expect(mockCache.hset).toHaveBeenCalledWith( 'trailing-trade-streams', `count`, 1 + 5 @@ -419,27 +437,23 @@ describe('server-binance', () => { getWebsocketTickersClean: mockGetWebsocketTickersClean })); - PubSubMock.subscribe = jest.fn().mockImplementation((_key, cb) => { + mockPubSub.subscribe = jest.fn().mockImplementation((_key, cb) => { cb('message', 'data'); }); - mongoMock.deleteAll = jest.fn().mockResolvedValue(true); - - cacheMock.hset = jest.fn().mockResolvedValue(true); - const { runBinance } = require('../server-binance'); - await runBinance(loggerMock); + await runBinance(logger); }); it('triggers PubSub.subscribe for reset-all-websockets', () => { - expect(PubSubMock.subscribe).toHaveBeenCalledWith( + expect(mockPubSub.subscribe).toHaveBeenCalledWith( 'reset-all-websockets', expect.any(Function) ); }); it('triggers PubSub.subscribe for reset-symbol-websockets', () => { - expect(PubSubMock.subscribe).toHaveBeenCalledWith( + expect(mockPubSub.subscribe).toHaveBeenCalledWith( 'reset-symbol-websockets', expect.any(Function) ); @@ -454,13 +468,13 @@ describe('server-binance', () => { }); it('triggers refreshCandles', () => { - expect(mongoMock.deleteAll).toHaveBeenCalledWith( - loggerMock, + expect(mockMongo.deleteAll).toHaveBeenCalledWith( + logger, 'trailing-trade-candles', {} ); - expect(mongoMock.deleteAll).toHaveBeenCalledWith( - loggerMock, + expect(mockMongo.deleteAll).toHaveBeenCalledWith( + logger, 'trailing-trade-ath-candles', {} ); @@ -471,7 +485,7 @@ describe('server-binance', () => { }); it('triggers syncCandles', () => { - expect(mockSyncCandles).toHaveBeenCalledWith(loggerMock, [ + expect(mockSyncCandles).toHaveBeenCalledWith(logger, [ 'BTCUSDT', 'ETHUSDT', 'LTCUSDT' @@ -479,7 +493,7 @@ describe('server-binance', () => { }); it('triggers syncATHCandles', () => { - expect(mockSyncATHCandles).toHaveBeenCalledWith(loggerMock, [ + expect(mockSyncATHCandles).toHaveBeenCalledWith(logger, [ 'BTCUSDT', 'ETHUSDT', 'LTCUSDT' @@ -487,7 +501,7 @@ describe('server-binance', () => { }); it('triggers syncOpenOrders', () => { - expect(mockSyncOpenOrders).toHaveBeenCalledWith(loggerMock, [ + expect(mockSyncOpenOrders).toHaveBeenCalledWith(logger, [ 'BTCUSDT', 'ETHUSDT', 'LTCUSDT' @@ -495,11 +509,11 @@ describe('server-binance', () => { }); it('triggers syncDatabaseOrders', () => { - expect(mockSyncDatabaseOrders).toHaveBeenCalledWith(loggerMock); + expect(mockSyncDatabaseOrders).toHaveBeenCalledWith(logger); }); it('triggers cache.hset', () => { - expect(cacheMock.hset).toHaveBeenCalledWith( + expect(mockCache.hset).toHaveBeenCalledWith( 'trailing-trade-streams', `count`, 1 @@ -586,15 +600,11 @@ describe('server-binance', () => { getWebsocketTickersClean: mockGetWebsocketTickersClean })); - PubSubMock.subscribe = jest.fn().mockResolvedValue(true); - - mongoMock.deleteAll = jest.fn().mockResolvedValue(true); - - cacheMock.hset = jest.fn().mockResolvedValue(true); + mockPubSub.subscribe = jest.fn().mockResolvedValue(true); const { runBinance } = require('../server-binance'); - await runBinance(loggerMock); - await runBinance(loggerMock); + await runBinance(logger); + await runBinance(logger); }); it('triggers cacheExchangeSymbols', () => { diff --git a/app/__tests__/server-frontend.test.js b/app/__tests__/server-frontend.test.js index 934dbcd9..faa29bb8 100644 --- a/app/__tests__/server-frontend.test.js +++ b/app/__tests__/server-frontend.test.js @@ -15,6 +15,7 @@ describe('server-frontend', () => { let mockConfigureWebServer; let mockConfigureWebSocket; let mockConfigureLocalTunnel; + let mockConfigureBullBoard; let mockRateLimiterRedisGet; let mockRateLimiterRedis; @@ -59,6 +60,7 @@ describe('server-frontend', () => { mockConfigureWebServer = jest.fn().mockReturnValue(true); mockConfigureWebSocket = jest.fn().mockReturnValue(true); mockConfigureLocalTunnel = jest.fn().mockReturnValue(true); + mockConfigureBullBoard = jest.fn().mockReturnValue(true); mockExpressStatic = jest.fn().mockReturnValue(true); @@ -126,6 +128,10 @@ describe('server-frontend', () => { jest.mock('../frontend/local-tunnel/configure', () => ({ configureLocalTunnel: mockConfigureLocalTunnel })); + + jest.mock('../frontend/bull-board/configure', () => ({ + configureBullBoard: mockConfigureBullBoard + })); }); describe('web server', () => { @@ -169,6 +175,13 @@ describe('server-frontend', () => { }); }); + it('triggers configureBullBoard', () => { + expect(mockConfigureBullBoard).toHaveBeenCalledWith( + expect.any(Object), + loggerMock + ); + }); + it('triggers server.listen', () => { expect(mockExpressListen).toHaveBeenCalledWith(80); }); diff --git a/app/binance/__tests__/tickers.test.js b/app/binance/__tests__/tickers.test.js index 390d0bfa..f78a3eb3 100644 --- a/app/binance/__tests__/tickers.test.js +++ b/app/binance/__tests__/tickers.test.js @@ -4,10 +4,10 @@ describe('tickers.js', () => { let binanceMock; let loggerMock; let cacheMock; + let mockQueue; let mockGetAccountInfo; let mockGetCachedExchangeSymbols; - let mockExecuteTrailingTrade; let mockWebsocketTickersClean; let mockErrorHandlerWrapper; @@ -33,6 +33,12 @@ describe('tickers.js', () => { loggerMock = logger; cacheMock = cache; + mockQueue = { + executeFor: jest.fn().mockResolvedValue(true) + }; + + jest.mock('../../cronjob/trailingTradeHelper/queue', () => mockQueue); + mockGetAccountInfo = jest.fn().mockResolvedValue({ balances: [ { asset: 'BTC', free: '0.00100000', locked: '0.99900000' }, @@ -66,16 +72,12 @@ describe('tickers.js', () => { return mockWebsocketTickersClean; }); - mockExecuteTrailingTrade = jest.fn().mockResolvedValue(true); - jest.mock('../../cronjob/trailingTradeHelper/common', () => ({ getAccountInfo: mockGetAccountInfo, getCachedExchangeSymbols: mockGetCachedExchangeSymbols })); - jest.mock('../../cronjob', () => ({ - executeTrailingTrade: mockExecuteTrailingTrade - })); + jest.mock('../../cronjob'); tickers = require('../tickers'); @@ -103,22 +105,16 @@ describe('tickers.js', () => { ); }); - it('triggers executeTrailingTrade twice', () => { - expect(mockExecuteTrailingTrade).toHaveBeenCalledTimes(2); + it('triggers queue.executeFor twice', () => { + expect(mockQueue.executeFor).toHaveBeenCalledTimes(2); }); - it('triggers executeTrailingTrade for BTCUSDT', () => { - expect(mockExecuteTrailingTrade).toHaveBeenCalledWith( - loggerMock, - 'BTCUSDT' - ); + it('triggers queue.executeFor for BTCUSDT', () => { + expect(mockQueue.executeFor).toHaveBeenCalledWith(loggerMock, 'BTCUSDT'); }); - it('triggers executeTrailingTrade for BNBUSDT', () => { - expect(mockExecuteTrailingTrade).toHaveBeenCalledWith( - loggerMock, - 'BNBUSDT' - ); + it('triggers queue.executeFor for BNBUSDT', () => { + expect(mockQueue.executeFor).toHaveBeenCalledWith(loggerMock, 'BNBUSDT'); }); it('checks websocketTickersClean', () => { @@ -136,16 +132,16 @@ describe('tickers.js', () => { describe('when called again', () => { beforeEach(async () => { // Reset mock counter - mockExecuteTrailingTrade.mockClear(); + mockQueue.executeFor.mockClear(); await tickers.setupTickersWebsocket(loggerMock, ['BTCUSDT']); }); - it('triggers executeTrailingTrade twice', () => { - expect(mockExecuteTrailingTrade).toHaveBeenCalledTimes(1); + it('triggers queue.executeFor twice', () => { + expect(mockQueue.executeFor).toHaveBeenCalledTimes(1); }); - it('triggers executeTrailingTrade for BTCUSDT', () => { - expect(mockExecuteTrailingTrade).toHaveBeenCalledWith( + it('triggers queue.executeFor for BTCUSDT', () => { + expect(mockQueue.executeFor).toHaveBeenCalledWith( loggerMock, 'BTCUSDT' ); diff --git a/app/binance/__tests__/user.test.js b/app/binance/__tests__/user.test.js index 2925eaab..9781fc59 100644 --- a/app/binance/__tests__/user.test.js +++ b/app/binance/__tests__/user.test.js @@ -3,6 +3,7 @@ describe('user.js', () => { let binanceMock; let loggerMock; + let mockQueue; let mockGetAccountInfoFromAPI; let mockUpdateAccountInfo; @@ -10,7 +11,6 @@ describe('user.js', () => { let mockUpdateGridTradeLastOrder; let mockGetManualOrder; let mockSaveManualOrder; - let mockExecuteTrailingTrade; let mockUserClean; @@ -18,9 +18,17 @@ describe('user.js', () => { beforeEach(async () => { jest.clearAllMocks().resetModules(); + jest.mock('../../cronjob'); + const { binance, logger } = require('../../helpers'); binanceMock = binance; loggerMock = logger; + + mockQueue = { + executeFor: jest.fn().mockResolvedValue(true) + }; + + jest.mock('../../cronjob/trailingTradeHelper/queue', () => mockQueue); }); describe('when balanceUpdate event received', () => { @@ -138,14 +146,6 @@ describe('user.js', () => { }); describe('when executionReport event received', () => { - beforeEach(() => { - mockExecuteTrailingTrade = jest.fn().mockResolvedValue(true); - - jest.mock('../../cronjob', () => ({ - executeTrailingTrade: mockExecuteTrailingTrade - })); - }); - describe('when last order not found', () => { beforeEach(async () => { mockUserClean = jest.fn().mockResolvedValue(true); @@ -234,8 +234,8 @@ describe('user.js', () => { expect(mockUpdateGridTradeLastOrder).not.toHaveBeenCalled(); }); - it('does not trigger executeTrailingTrade', () => { - expect(mockExecuteTrailingTrade).not.toHaveBeenCalled(); + it('does not trigger queue.executeFor', () => { + expect(mockQueue.executeFor).not.toHaveBeenCalled(); }); it('does not trigger userClean', () => { @@ -244,121 +244,230 @@ describe('user.js', () => { }); describe('when last order found', () => { - beforeEach(async () => { - mockUserClean = jest.fn().mockResolvedValue(true); - - mockGridTradeLastOrder = jest - .fn() - .mockResolvedValue({ orderId: 7479643460 }); - mockUpdateGridTradeLastOrder = jest.fn().mockResolvedValue(null); - mockGetManualOrder = jest.fn().mockResolvedValue(null); - mockSaveManualOrder = jest.fn().mockResolvedValue(null); + describe('received transaction time > existing transaction time', () => { + beforeEach(async () => { + mockUserClean = jest.fn().mockResolvedValue(true); - jest.mock('../../cronjob/trailingTradeHelper/order', () => ({ - getGridTradeLastOrder: mockGridTradeLastOrder, - updateGridTradeLastOrder: mockUpdateGridTradeLastOrder, - getManualOrder: mockGetManualOrder, - saveManualOrder: mockSaveManualOrder - })); - - binanceMock.client.ws.user = jest.fn().mockImplementationOnce(cb => { - /** - * Sample execution report - * { - * "eventType": "executionReport", - * "eventTime": 1642713283562, - * "symbol": "ETHUSDT", - * "newClientOrderId": "R4gzUYn9pQbOA3vAkgKTSw", - * "originalClientOrderId": "", - * "side": "BUY", - * "orderType": "STOP_LOSS_LIMIT", - * "timeInForce": "GTC", - * "quantity": "0.00920000", - * "price": "3248.37000000", - * "executionType": "NEW", - * "stopPrice": "3245.19000000", - * "icebergQuantity": "0.00000000", - * "orderStatus": "NEW", - * "orderRejectReason": "NONE", - * "orderId": 7479643460, - * "orderTime": 1642713283561, - * "lastTradeQuantity": "0.00000000", - * "totalTradeQuantity": "0.00000000", - * "priceLastTrade": "0.00000000", - * "commission": "0", - * "commissionAsset": null, - * "tradeId": -1, - * "isOrderWorking": false, - * "isBuyerMaker": false, - * "creationTime": 1642713283561, - * "totalQuoteTradeQuantity": "0.00000000", - * "orderListId": -1, - * "quoteOrderQuantity": "0.00000000", - * "lastQuoteTransacted": "0.00000000" - * } - */ - cb({ - eventType: 'executionReport', - eventTime: 1642713283562, - symbol: 'ETHUSDT', - side: 'BUY', - orderStatus: 'NEW', - orderType: 'STOP_LOSS_LIMIT', - stopPrice: '3245.19000000', - price: '3248.37000000', + mockGridTradeLastOrder = jest.fn().mockResolvedValue({ orderId: 7479643460, - quantity: '0.00920000', - isOrderWorking: false, - totalQuoteTradeQuantity: '0.00000000', - totalTradeQuantity: '0.00000000' + transactTime: 1642713282000 }); + mockUpdateGridTradeLastOrder = jest.fn().mockResolvedValue(null); + mockGetManualOrder = jest.fn().mockResolvedValue(null); + mockSaveManualOrder = jest.fn().mockResolvedValue(null); + + jest.mock('../../cronjob/trailingTradeHelper/order', () => ({ + getGridTradeLastOrder: mockGridTradeLastOrder, + updateGridTradeLastOrder: mockUpdateGridTradeLastOrder, + getManualOrder: mockGetManualOrder, + saveManualOrder: mockSaveManualOrder + })); + + binanceMock.client.ws.user = jest + .fn() + .mockImplementationOnce(cb => { + /** + * Sample execution report + * { + * "eventType": "executionReport", + * "eventTime": 1642713283562, + * "symbol": "ETHUSDT", + * "newClientOrderId": "R4gzUYn9pQbOA3vAkgKTSw", + * "originalClientOrderId": "", + * "side": "BUY", + * "orderType": "STOP_LOSS_LIMIT", + * "timeInForce": "GTC", + * "quantity": "0.00920000", + * "price": "3248.37000000", + * "executionType": "NEW", + * "stopPrice": "3245.19000000", + * "icebergQuantity": "0.00000000", + * "orderStatus": "NEW", + * "orderRejectReason": "NONE", + * "orderId": 7479643460, + * "orderTime": 1642713283561, + * "lastTradeQuantity": "0.00000000", + * "totalTradeQuantity": "0.00000000", + * "priceLastTrade": "0.00000000", + * "commission": "0", + * "commissionAsset": null, + * "tradeId": -1, + * "isOrderWorking": false, + * "isBuyerMaker": false, + * "creationTime": 1642713283561, + * "totalQuoteTradeQuantity": "0.00000000", + * "orderListId": -1, + * "quoteOrderQuantity": "0.00000000", + * "lastQuoteTransacted": "0.00000000" + * } + */ + cb({ + eventType: 'executionReport', + eventTime: 1642713283562, + symbol: 'ETHUSDT', + side: 'BUY', + orderStatus: 'NEW', + orderType: 'STOP_LOSS_LIMIT', + stopPrice: '3245.19000000', + price: '3248.37000000', + orderId: 7479643460, + quantity: '0.00920000', + isOrderWorking: false, + totalQuoteTradeQuantity: '0.00000000', + totalTradeQuantity: '0.00000000', + orderTime: 1642713283561 + }); + + return mockUserClean; + }); + + const { setupUserWebsocket } = require('../user'); + + await setupUserWebsocket(loggerMock); + }); - return mockUserClean; + it('triggers getGridTradeLastOrder', () => { + expect(mockGridTradeLastOrder).toHaveBeenCalledWith( + loggerMock, + 'ETHUSDT', + 'buy' + ); }); - const { setupUserWebsocket } = require('../user'); + it('triggers updateGridTradeLastOrder', () => { + expect(mockUpdateGridTradeLastOrder).toHaveBeenCalledWith( + loggerMock, + 'ETHUSDT', + 'buy', + { + cummulativeQuoteQty: '0.00000000', + executedQty: '0.00000000', + isWorking: false, + orderId: 7479643460, + origQty: '0.00920000', + price: '3248.37000000', + side: 'BUY', + status: 'NEW', + stopPrice: '3245.19000000', + type: 'STOP_LOSS_LIMIT', + updateTime: 1642713283562, + transactTime: 1642713283561 + } + ); + }); - await setupUserWebsocket(loggerMock); - }); + it('triggers queue.executeFor', () => { + expect(mockQueue.executeFor).toHaveBeenCalledWith( + loggerMock, + 'ETHUSDT' + ); + }); - it('triggers getGridTradeLastOrder', () => { - expect(mockGridTradeLastOrder).toHaveBeenCalledWith( - loggerMock, - 'ETHUSDT', - 'buy' - ); + it('does not trigger userClean', () => { + expect(mockUserClean).not.toHaveBeenCalled(); + }); }); + describe('received transaction time < existing transaction time', () => { + beforeEach(async () => { + mockUserClean = jest.fn().mockResolvedValue(true); - it('triggers updateGridTradeLastOrder', () => { - expect(mockUpdateGridTradeLastOrder).toHaveBeenCalledWith( - loggerMock, - 'ETHUSDT', - 'buy', - { - cummulativeQuoteQty: '0.00000000', - executedQty: '0.00000000', - isWorking: false, + mockGridTradeLastOrder = jest.fn().mockResolvedValue({ orderId: 7479643460, - origQty: '0.00920000', - price: '3248.37000000', - side: 'BUY', - status: 'NEW', - stopPrice: '3245.19000000', - type: 'STOP_LOSS_LIMIT', - updateTime: 1642713283562 - } - ); - }); + transactTime: 1642713282000 + }); + mockUpdateGridTradeLastOrder = jest.fn().mockResolvedValue(null); + mockGetManualOrder = jest.fn().mockResolvedValue(null); + mockSaveManualOrder = jest.fn().mockResolvedValue(null); + + jest.mock('../../cronjob/trailingTradeHelper/order', () => ({ + getGridTradeLastOrder: mockGridTradeLastOrder, + updateGridTradeLastOrder: mockUpdateGridTradeLastOrder, + getManualOrder: mockGetManualOrder, + saveManualOrder: mockSaveManualOrder + })); + + binanceMock.client.ws.user = jest + .fn() + .mockImplementationOnce(cb => { + /** + * Sample execution report + * { + * "eventType": "executionReport", + * "eventTime": 1642713283562, + * "symbol": "ETHUSDT", + * "newClientOrderId": "R4gzUYn9pQbOA3vAkgKTSw", + * "originalClientOrderId": "", + * "side": "BUY", + * "orderType": "STOP_LOSS_LIMIT", + * "timeInForce": "GTC", + * "quantity": "0.00920000", + * "price": "3248.37000000", + * "executionType": "NEW", + * "stopPrice": "3245.19000000", + * "icebergQuantity": "0.00000000", + * "orderStatus": "NEW", + * "orderRejectReason": "NONE", + * "orderId": 7479643460, + * "orderTime": 1642713283561, + * "lastTradeQuantity": "0.00000000", + * "totalTradeQuantity": "0.00000000", + * "priceLastTrade": "0.00000000", + * "commission": "0", + * "commissionAsset": null, + * "tradeId": -1, + * "isOrderWorking": false, + * "isBuyerMaker": false, + * "creationTime": 1642713283561, + * "totalQuoteTradeQuantity": "0.00000000", + * "orderListId": -1, + * "quoteOrderQuantity": "0.00000000", + * "lastQuoteTransacted": "0.00000000" + * } + */ + cb({ + eventType: 'executionReport', + eventTime: 1642713283562, + symbol: 'ETHUSDT', + side: 'BUY', + orderStatus: 'NEW', + orderType: 'STOP_LOSS_LIMIT', + stopPrice: '3245.19000000', + price: '3248.37000000', + orderId: 7479643460, + quantity: '0.00920000', + isOrderWorking: false, + totalQuoteTradeQuantity: '0.00000000', + totalTradeQuantity: '0.00000000', + orderTime: 1642713281000 + }); + + return mockUserClean; + }); + + const { setupUserWebsocket } = require('../user'); + + await setupUserWebsocket(loggerMock); + }); - it('triggers executeTrailingTrade', () => { - expect(mockExecuteTrailingTrade).toHaveBeenCalledWith( - loggerMock, - 'ETHUSDT' - ); - }); + it('triggers getGridTradeLastOrder', () => { + expect(mockGridTradeLastOrder).toHaveBeenCalledWith( + loggerMock, + 'ETHUSDT', + 'buy' + ); + }); - it('does not trigger userClean', () => { - expect(mockUserClean).not.toHaveBeenCalled(); + it('does not trigger updateGridTradeLastOrder', () => { + expect(mockUpdateGridTradeLastOrder).not.toHaveBeenCalled(); + }); + + it('does not trigger queue.executeFor', () => { + expect(mockQueue.executeFor).not.toHaveBeenCalled(); + }); + + it('does not trigger userClean', () => { + expect(mockUserClean).not.toHaveBeenCalled(); + }); }); }); @@ -450,8 +559,8 @@ describe('user.js', () => { expect(mockSaveManualOrder).not.toHaveBeenCalled(); }); - it('does not trigger executeTrailingTrade', () => { - expect(mockExecuteTrailingTrade).not.toHaveBeenCalled(); + it('does not trigger queue.executeFor', () => { + expect(mockQueue.executeFor).not.toHaveBeenCalled(); }); it('does not trigger userClean', () => { @@ -565,8 +674,8 @@ describe('user.js', () => { ); }); - it('triggers executeTrailingTrade', () => { - expect(mockExecuteTrailingTrade).toHaveBeenCalledWith( + it('triggers queue.executeFor', () => { + expect(mockQueue.executeFor).toHaveBeenCalledWith( loggerMock, 'ETHUSDT' ); diff --git a/app/binance/tickers.js b/app/binance/tickers.js index 17a1e887..a2eddc78 100644 --- a/app/binance/tickers.js +++ b/app/binance/tickers.js @@ -1,10 +1,11 @@ const _ = require('lodash'); const { binance, cache } = require('../helpers'); +const queue = require('../cronjob/trailingTradeHelper/queue'); + const { getAccountInfo, getCachedExchangeSymbols } = require('../cronjob/trailingTradeHelper/common'); -const { executeTrailingTrade } = require('../cronjob'); const { errorHandlerWrapper } = require('../error-handler'); let websocketTickersClean = {}; @@ -65,7 +66,7 @@ const setupTickersWebsocket = async (logger, symbols) => { ); if (canExecuteTrailingTrade) { - executeTrailingTrade(logger, monitoringSymbol); + queue.executeFor(logger, monitoringSymbol); } }); } diff --git a/app/binance/user.js b/app/binance/user.js index f6da5f04..ddbe7df0 100644 --- a/app/binance/user.js +++ b/app/binance/user.js @@ -1,6 +1,6 @@ const _ = require('lodash'); - const { binance } = require('../helpers'); +const queue = require('../cronjob/trailingTradeHelper/queue'); const { updateAccountInfo, @@ -13,7 +13,6 @@ const { getManualOrder, saveManualOrder } = require('../cronjob/trailingTradeHelper/order'); -const { executeTrailingTrade } = require('../cronjob'); let userClean; @@ -50,7 +49,8 @@ const setupUserWebsocket = async logger => { quantity, isOrderWorking, totalQuoteTradeQuantity, - totalTradeQuantity + totalTradeQuantity, + orderTime: transactTime // Transaction time } = evt; logger.info( { symbol, evt, saveLog: true }, @@ -65,6 +65,16 @@ const setupUserWebsocket = async logger => { ); if (_.isEmpty(lastOrder) === false) { + // Skip if the orderId is not match with the existing orderId + // or Skip if the transaction time is older than the existing order transaction time + // This is helpful when we received a delayed event for any reason + if ( + orderId !== lastOrder.orderId || + transactTime < lastOrder.transactTime + ) { + return; + } + await updateGridTradeLastOrder(logger, symbol, side.toLowerCase(), { ...lastOrder, status: orderStatus, @@ -76,14 +86,15 @@ const setupUserWebsocket = async logger => { cummulativeQuoteQty: totalQuoteTradeQuantity, executedQty: totalTradeQuantity, isWorking: isOrderWorking, - updateTime: eventTime + updateTime: eventTime, + transactTime }); logger.info( { symbol, lastOrder, saveLog: true }, 'The last order has been updated.' ); - executeTrailingTrade(logger, symbol); + queue.executeFor(logger, symbol); } }; @@ -112,7 +123,7 @@ const setupUserWebsocket = async logger => { 'The manual order has been updated.' ); - executeTrailingTrade(logger, symbol); + queue.executeFor(logger, symbol); } }; diff --git a/app/cronjob/__tests__/trailingTrade.test.js b/app/cronjob/__tests__/trailingTrade.test.js index d187d96f..d50d5d57 100644 --- a/app/cronjob/__tests__/trailingTrade.test.js +++ b/app/cronjob/__tests__/trailingTrade.test.js @@ -308,7 +308,6 @@ describe('trailingTrade', () => { it('returns expected result for BTCUSDT', () => { expect(mockLoggerInfo).toHaveBeenCalledWith( { - symbol: 'BTCUSDT', data: { symbol: 'BTCUSDT', isLocked: false, @@ -356,7 +355,6 @@ describe('trailingTrade', () => { it('returns expected result for ETHUSDT', () => { expect(mockLoggerInfo).toHaveBeenCalledWith( { - symbol: 'ETHUSDT', data: { symbol: 'ETHUSDT', isLocked: false, @@ -404,7 +402,6 @@ describe('trailingTrade', () => { it('returns expected result for LTCUSDT', async () => { expect(mockLoggerInfo).toHaveBeenCalledWith( { - symbol: 'LTCUSDT', data: { symbol: 'LTCUSDT', isLocked: false, @@ -439,293 +436,4 @@ describe('trailingTrade', () => { }); }); }); - - describe('when symbol is locked', () => { - beforeEach(async () => { - mockConfigGet = jest.fn(key => { - if (key === 'featureToggle') { - return { - feature1Enabled: false - }; - } - return null; - }); - - jest.mock('config', () => ({ - get: mockConfigGet - })); - - mockIsSymbolLocked = jest - .fn() - .mockResolvedValueOnce(true) - .mockResolvedValueOnce(false); - - mockGetAccountInfo = jest.fn().mockResolvedValue({ - account: 'info' - }); - - mockGetSymbolConfiguration = jest - .fn() - .mockImplementation((_logger, rawData) => ({ - ...rawData, - ...{ - symbolConfiguration: { - symbol: 'configuration data' - } - } - })); - - mockGetSymbolInfo = jest.fn().mockImplementation((_logger, rawData) => ({ - ...rawData, - ...{ - symbolInfo: { - symbol: 'info' - } - } - })); - - mockGetOverrideAction = jest - .fn() - .mockImplementation((_logger, rawData) => ({ - ...rawData, - ...{ - overrideAction: { - action: 'override-action' - } - } - })); - - mockEnsureManualOrder = jest - .fn() - .mockImplementation((_logger, rawData) => ({ - ...rawData, - ...{ - ensureManualOrder: { - ensured: 'manual-buy-order' - } - } - })); - - mockEnsureGridTradeOrderExecuted = jest - .fn() - .mockImplementation((_logger, rawData) => ({ - ...rawData, - ensureGridTradeOrder: { - ensured: 'grid-trade' - } - })); - - mockGetBalances = jest.fn().mockImplementation((_logger, rawData) => ({ - ...rawData, - ...{ - baseAssetBalance: { baseAsset: 'balance' }, - quoteAssetBalance: { quoteAsset: 'balance' } - } - })); - - mockGetOpenOrders = jest.fn().mockImplementation((_logger, rawData) => ({ - ...rawData, - ...{ - openOrders: [ - { - orderId: `order-id-${rawData.symbol}`, - symbol: rawData.symbol - } - ] - } - })); - - mockGetIndicators = jest.fn().mockImplementation((_logger, rawData) => ({ - ...rawData, - ...{ - lastCandle: { - got: 'lowest value' - }, - indicators: { - some: 'value' - }, - buy: { - should: 'buy?' - }, - sell: { - should: 'sell?' - } - } - })); - - mockHandleOpenOrders = jest - .fn() - .mockImplementation((_logger, rawData) => ({ - ...rawData, - ...{ - handled: 'open-orders' - } - })); - - mockDetermineAction = jest - .fn() - .mockImplementation((_logger, rawData) => ({ - ...rawData, - ...{ action: 'determined' } - })); - - mockPlaceManualTrade = jest - .fn() - .mockImplementation((_logger, rawData) => ({ - ...rawData, - ...{ - placeManualTrade: { - placed: 'manual-trade' - } - } - })); - - mockCancelOrder = jest.fn().mockImplementation((_logger, rawData) => ({ - ...rawData, - ...{ - cancelOrder: { - cancelled: 'existing-order' - } - } - })); - - mockPlaceBuyOrder = jest.fn().mockImplementation((_logger, rawData) => ({ - ...rawData, - ...{ - buy: { - should: 'buy?', - actioned: 'yes' - } - } - })); - - mockPlaceSellOrder = jest.fn().mockImplementation((_logger, rawData) => ({ - ...rawData, - ...{ - sell: { - should: 'sell?', - actioned: 'yes' - } - } - })); - - mockPlaceSellStopLossOrder = jest - .fn() - .mockImplementation((_logger, rawData) => ({ - ...rawData, - ...{ - stopLoss: 'processed' - } - })); - - mockRemoveLastBuyPrice = jest - .fn() - .mockImplementation((_logger, rawData) => ({ - ...rawData, - ...{ - removed: 'last-buy-price' - } - })); - - mockSaveDataToCache = jest - .fn() - .mockImplementation((_logger, rawData) => ({ - ...rawData, - ...{ - saved: 'data-to-cache' - } - })); - - jest.mock('../trailingTradeHelper/common', () => ({ - getAccountInfo: mockGetAccountInfo, - lockSymbol: mockLockSymbol, - isSymbolLocked: mockIsSymbolLocked, - unlockSymbol: mockUnlockSymbol - })); - - jest.mock('../trailingTrade/steps', () => ({ - getSymbolConfiguration: mockGetSymbolConfiguration, - getSymbolInfo: mockGetSymbolInfo, - getOverrideAction: mockGetOverrideAction, - ensureManualOrder: mockEnsureManualOrder, - ensureGridTradeOrderExecuted: mockEnsureGridTradeOrderExecuted, - getBalances: mockGetBalances, - getOpenOrders: mockGetOpenOrders, - getIndicators: mockGetIndicators, - handleOpenOrders: mockHandleOpenOrders, - determineAction: mockDetermineAction, - placeManualTrade: mockPlaceManualTrade, - cancelOrder: mockCancelOrder, - placeBuyOrder: mockPlaceBuyOrder, - placeSellOrder: mockPlaceSellOrder, - placeSellStopLossOrder: mockPlaceSellStopLossOrder, - removeLastBuyPrice: mockRemoveLastBuyPrice, - saveDataToCache: mockSaveDataToCache - })); - }); - - describe('execute trailing trade for BTCUSDT', () => { - beforeEach(async () => { - const { execute: trailingTradeExecute } = require('../trailingTrade'); - await trailingTradeExecute(logger, 'BTCUSDT'); - - jest.runOnlyPendingTimers(); - }); - - it(`triggers isSymbolLocked - BTCUSDT`, () => { - expect(mockIsSymbolLocked).toHaveBeenCalledWith(logger, 'BTCUSDT'); - }); - - it('returns expected result for BTCUSDT 1st execution', async () => { - expect(mockLoggerInfo).toHaveBeenCalledWith( - { - debug: true, - symbol: 'BTCUSDT' - }, - '⏯ TrailingTrade: Skip process as the symbol is currently locked. It will be re-execute 10 seconds later.' - ); - }); - - it('returns expected result for BTCUSDT 2nd execution', async () => { - setTimeout(() => { - expect(mockLoggerInfo).toHaveBeenCalledWith( - { - symbol: 'BTCUSDT', - data: { - symbol: 'BTCUSDT', - isLocked: false, - featureToggle: { feature1Enabled: false }, - lastCandle: { got: 'lowest value' }, - accountInfo: { account: 'info' }, - symbolConfiguration: { symbol: 'configuration data' }, - indicators: { some: 'value' }, - symbolInfo: { symbol: 'info' }, - openOrders: [ - { orderId: 'order-id-BTCUSDT', symbol: 'BTCUSDT' } - ], - action: 'determined', - baseAssetBalance: { baseAsset: 'balance' }, - quoteAssetBalance: { quoteAsset: 'balance' }, - buy: { should: 'buy?', actioned: 'yes' }, - sell: { should: 'sell?', actioned: 'yes' }, - order: {}, - canDisable: true, - saveToCache: true, - ensureManualOrder: { ensured: 'manual-buy-order' }, - ensureGridTradeOrder: { ensured: 'grid-trade' }, - overrideAction: { action: 'override-action' }, - handled: 'open-orders', - placeManualTrade: { placed: 'manual-trade' }, - cancelOrder: { cancelled: 'existing-order' }, - stopLoss: 'processed', - removed: 'last-buy-price', - saved: 'data-to-cache' - } - }, - 'TrailingTrade: Finish process...' - ); - }, 10000); - }); - }); - }); }); diff --git a/app/cronjob/trailingTrade.js b/app/cronjob/trailingTrade.js index 18a65cbe..5626d38a 100644 --- a/app/cronjob/trailingTrade.js +++ b/app/cronjob/trailingTrade.js @@ -3,9 +3,7 @@ const config = require('config'); const { getAccountInfo, - isSymbolLocked, - lockSymbol, - unlockSymbol + isSymbolLocked } = require('./trailingTradeHelper/common'); const { @@ -32,25 +30,15 @@ const { errorHandlerWrapper } = require('../error-handler'); const execute = async (rawLogger, symbol) => { const logger = rawLogger.child({ jobName: 'trailingTrade', - correlationId: uuidv4() + correlationId: uuidv4(), + symbol }); await errorHandlerWrapper(logger, 'Trailing Trade', async () => { - // Check if the symbol is locked, if it is locked, it means the symbol is still trading. + // Check if the symbol is locked, if it is locked, it means the symbol is updating in the indicator. const isLocked = await isSymbolLocked(logger, symbol); - if (isLocked === true) { - logger.info( - { debug: true, symbol }, - '⏯ TrailingTrade: Skip process as the symbol is currently locked. It will be re-execute 10 seconds later.' - ); - setTimeout(() => execute(logger, symbol), 10000); - return; - } - - logger.info({ debug: true, symbol }, '▶ TrailingTrade: Start process...'); - - await lockSymbol(logger, symbol); + logger.info({ debug: true }, '▶ TrailingTrade: Start process...'); // Retrieve account info from cache const accountInfo = await getAccountInfo(logger); @@ -160,7 +148,7 @@ const execute = async (rawLogger, symbol) => { stepFunc: saveDataToCache } ]) { - const stepLogger = logger.child({ stepName, symbol: data.symbol }); + const stepLogger = logger.child({ stepName }); stepLogger.info({ data }, `Start step - ${stepName}`); @@ -170,15 +158,9 @@ const execute = async (rawLogger, symbol) => { stepLogger.info({ data }, `Finish step - ${stepName}`); } - // Unlock symbol for processing - await unlockSymbol(logger, symbol); - - logger.info( - { symbol, debug: true }, - '⏹ TrailingTrade: Finish process (Debug)...' - ); + logger.info({ debug: true }, '⏹ TrailingTrade: Finish process (Debug)...'); - logger.info({ symbol, data }, 'TrailingTrade: Finish process...'); + logger.info({ data }, 'TrailingTrade: Finish process...'); }); }; diff --git a/app/cronjob/trailingTradeHelper/__tests__/queue.test.js b/app/cronjob/trailingTradeHelper/__tests__/queue.test.js new file mode 100644 index 00000000..fc01a0e7 --- /dev/null +++ b/app/cronjob/trailingTradeHelper/__tests__/queue.test.js @@ -0,0 +1,143 @@ +/* eslint-disable global-require */ +const logger = require('../../../helpers/logger'); + +describe('queue', () => { + let queue; + let mockQueueProcess; + let mockQueueObliterate; + let mockQueueAdd; + let mockQueue; + + let mockExecuteTrailingTrade; + let mockSetBullBoardQueues; + + beforeEach(() => { + jest.clearAllMocks().resetModules(); + jest.mock('config'); + + mockQueueProcess = jest.fn().mockImplementation((_concurrent, cb) => { + const job = jest.fn(); + cb(job); + }); + + mockQueueObliterate = jest.fn().mockResolvedValue(true); + mockQueueAdd = jest.fn().mockResolvedValue(true); + mockExecuteTrailingTrade = jest.fn().mockResolvedValue(true); + mockSetBullBoardQueues = jest.fn().mockResolvedValue(true); + + mockQueue = jest.fn().mockImplementation((_queueName, _redisUrl) => ({ + process: mockQueueProcess, + add: mockQueueAdd, + obliterate: mockQueueObliterate + })); + + jest.mock('bull', () => mockQueue); + + jest.mock('../../../cronjob', () => ({ + executeTrailingTrade: mockExecuteTrailingTrade + })); + + jest.mock('../../../frontend/bull-board/configure', () => ({ + setBullBoardQueues: mockSetBullBoardQueues + })); + }); + + describe('init', () => { + describe('called one time', () => { + beforeEach(async () => { + queue = require('../queue'); + + await queue.init(logger, ['BTCUSDT', 'ETHUSDT', 'BNBUSDT']); + }); + + it('triggers new Queue for BTCUSDT', () => { + expect(mockQueue).toHaveBeenCalledWith('BTCUSDT', expect.any(String), { + prefix: `bull`, + settings: { + guardInterval: 1000 + } + }); + }); + + it('triggers executeTrailingTrade for BTCUSDT', () => { + expect(mockExecuteTrailingTrade).toHaveBeenCalledWith( + logger, + 'BTCUSDT' + ); + }); + + it('triggers executeTrailingTrade for ETHUSDT', () => { + expect(mockExecuteTrailingTrade).toHaveBeenCalledWith( + logger, + 'ETHUSDT' + ); + }); + + it('triggers executeTrailingTrade for BNBUSDT', () => { + expect(mockExecuteTrailingTrade).toHaveBeenCalledWith( + logger, + 'BNBUSDT' + ); + }); + + it('triggers queue.process 3 times', () => { + expect(mockQueueProcess).toHaveBeenCalledTimes(3); + }); + + it('does not trigger queue.obliterate', () => { + expect(mockQueueObliterate).not.toHaveBeenCalled(); + }); + }); + + describe('called two times', () => { + beforeEach(async () => { + queue = require('../queue'); + + await queue.init(logger, ['BTCUSDT', 'ETHUSDT', 'BNBUSDT']); + await queue.init(logger, ['BTCUSDT', 'ETHUSDT', 'BNBUSDT']); + }); + + it('triggers queue.obliterate 3 times', () => { + expect(mockQueueObliterate).toHaveBeenCalledTimes(3); + }); + + it('triggers queue.process 6 times', () => { + expect(mockQueueProcess).toHaveBeenCalledTimes(6); + }); + + it('triggers setBullBoardQueues 2 times', () => { + expect(mockSetBullBoardQueues).toHaveBeenCalledTimes(2); + }); + }); + }); + + describe('executeFor', () => { + describe('when the symbol exists in the queue', () => { + beforeEach(async () => { + queue = require('../queue'); + + await queue.init(logger, ['BTCUSDT']); + await queue.executeFor(logger, 'BTCUSDT'); + }); + + it('triggers queue.add for BTCUSDT', () => { + expect(mockQueueAdd).toHaveBeenCalledWith( + {}, + { removeOnComplete: 100 } + ); + }); + }); + describe('when symbol does not exist in the queue', () => { + beforeEach(async () => { + queue = require('../queue'); + + await queue.init(logger, ['BTCUSDT']); + await queue.executeFor(logger, 'ETHUSDT'); + }); + + it('does not trigger queue.add for ETHUSDT', () => { + expect(mockQueueAdd).not.toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/app/cronjob/trailingTradeHelper/queue.js b/app/cronjob/trailingTradeHelper/queue.js new file mode 100644 index 00000000..63e78df1 --- /dev/null +++ b/app/cronjob/trailingTradeHelper/queue.js @@ -0,0 +1,68 @@ +const config = require('config'); +const Queue = require('bull'); +const _ = require('lodash'); +const { executeTrailingTrade } = require('../index'); +const { setBullBoardQueues } = require('../../frontend/bull-board/configure'); + +let queues = {}; + +const REDIS_URL = `redis://:${config.get('redis.password')}@${config.get( + 'redis.host' +)}:${config.get('redis.port')}/${config.get('redis.db')}`; + +const create = (funcLogger, symbol) => { + const logger = funcLogger.child({ helper: 'queue' }); + + const queue = new Queue(symbol, REDIS_URL, { + prefix: `bull`, + settings: { + guardInterval: 1000 // Poll interval for delayed jobs and added jobs. + } + }); + // Set concurrent for the job + queue.process(1, async _job => executeTrailingTrade(logger, symbol)); + + return queue; +}; + +const init = async (funcLogger, symbols) => { + // Completely remove all queues with their data + await Promise.all(_.map(queues, queue => queue.obliterate({ force: true }))); + queues = {}; + + await Promise.all( + _.map(symbols, async symbol => { + queues[symbol] = create(funcLogger, symbol); + }) + ); + + // Set bull board queues + setBullBoardQueues(queues, funcLogger); +}; + +/** + * Add executeTrailingTrade job to the queue of a symbol + * + * @param {*} funcLogger + * @param {*} symbol + */ +const executeFor = async (funcLogger, symbol) => { + const logger = funcLogger.child({ helper: 'queue' }); + + if (!(symbol in queues)) { + logger.error({ symbol }, `No queue created for ${symbol}`); + return; + } + + await queues[symbol].add( + {}, + { + removeOnComplete: 100 // number specified the amount of jobs to keep. + } + ); +}; + +module.exports = { + init, + executeFor +}; diff --git a/app/error-handler.js b/app/error-handler.js index d36745aa..744057d9 100644 --- a/app/error-handler.js +++ b/app/error-handler.js @@ -41,7 +41,7 @@ const handleError = (logger, job, err) => { } logger.error( - { err, errorCode: err.code, debug: true }, + { err, errorCode: err.code, debug: true, saveLog: true }, `⚠ Execution failed.` ); if ( diff --git a/app/frontend/bull-board/__tests__/configure.test.js b/app/frontend/bull-board/__tests__/configure.test.js new file mode 100644 index 00000000..1d13fbc4 --- /dev/null +++ b/app/frontend/bull-board/__tests__/configure.test.js @@ -0,0 +1,105 @@ +/* eslint-disable global-require */ +describe('bull-board/configure.js', () => { + let mockLogger; + + let mockExpressUse; + + let mockReplaceQueues; + let mockCreateBullBoard; + let mockBullAdapter; + + let mockExpressAdapter; + let mockExpressAdapterSetBasePath; + let mockExpressAdapterGetRouter; + + beforeEach(() => { + jest.clearAllMocks().resetModules(); + + const { logger } = require('../../../helpers'); + mockLogger = logger; + + mockReplaceQueues = jest.fn(); + mockCreateBullBoard = jest.fn(() => ({ + replaceQueues: mockReplaceQueues + })); + jest.mock('@bull-board/api', () => ({ + createBullBoard: mockCreateBullBoard + })); + + mockExpressAdapterSetBasePath = jest.fn(); + mockExpressAdapterGetRouter = jest.fn(() => ({ router: 'here' })); + mockExpressAdapter = jest.fn().mockImplementation(() => ({ + setBasePath: mockExpressAdapterSetBasePath, + getRouter: mockExpressAdapterGetRouter + })); + + jest.mock('@bull-board/express', () => ({ + ExpressAdapter: mockExpressAdapter + })); + + mockBullAdapter = jest + .fn() + .mockImplementation(() => ({ queue: 'instance' })); + jest.mock('@bull-board/api/bullAdapter', () => ({ + BullAdapter: mockBullAdapter + })); + + mockExpressUse = jest.fn(); + }); + + describe('configureBullBoard', () => { + beforeEach(() => { + const { configureBullBoard } = require('../configure'); + configureBullBoard( + { + use: mockExpressUse + }, + mockLogger + ); + }); + + it('triggers ExpressAdapter.setBasePath', () => { + expect(mockExpressAdapterSetBasePath).toHaveBeenCalledWith('/queue'); + }); + + it('triggers createBullBoard', () => { + expect(mockCreateBullBoard).toHaveBeenCalledWith({ + queues: [], + serverAdapter: expect.any(Object) + }); + }); + + it('triggers ExpressAdapter.getRouter', () => { + expect(mockExpressAdapterGetRouter).toHaveBeenCalled(); + }); + + it('triggers app.use', () => { + expect(mockExpressUse).toHaveBeenCalledWith('/queue', { router: 'here' }); + }); + }); + + describe('setBullBoardQueues', () => { + beforeEach(() => { + const { + configureBullBoard, + setBullBoardQueues + } = require('../configure'); + + configureBullBoard( + { + use: mockExpressUse + }, + mockLogger + ); + setBullBoardQueues({ BTCUSDT: 'queue1', ETHUSDT: 'queue2' }, mockLogger); + }); + + it('triggers BullAdapter 2 times', () => { + expect(mockBullAdapter).toHaveBeenCalledTimes(2); + }); + + it('triggers callReplaceQueues', () => { + expect(mockReplaceQueues).toHaveBeenCalled(); + }); + }); +}); diff --git a/app/frontend/bull-board/configure.js b/app/frontend/bull-board/configure.js new file mode 100644 index 00000000..603cf26b --- /dev/null +++ b/app/frontend/bull-board/configure.js @@ -0,0 +1,35 @@ +const { createBullBoard } = require('@bull-board/api'); +const { BullAdapter } = require('@bull-board/api/bullAdapter'); +const { ExpressAdapter } = require('@bull-board/express'); + +const basePath = '/queue'; + +let callReplaceQueues; + +const configureBullBoard = (app, funcLogger) => { + const logger = funcLogger.child({ server: 'bull-board' }); + + const serverAdapter = new ExpressAdapter(); + + serverAdapter.setBasePath(basePath); + + const { replaceQueues } = createBullBoard({ + queues: [], + serverAdapter + }); + callReplaceQueues = replaceQueues; + + app.use(basePath, serverAdapter.getRouter()); + logger.info('Bull board is configured.'); +}; + +const setBullBoardQueues = (queues, funcLogger) => { + const logger = funcLogger.child({ server: 'bull-board' }); + + const queuesForBoard = Object.values(queues).map(q => new BullAdapter(q)); + callReplaceQueues(queuesForBoard); + + logger.info('Bull board is updated.'); +}; + +module.exports = { configureBullBoard, setBullBoardQueues }; diff --git a/app/frontend/webserver/handlers/404.js b/app/frontend/webserver/handlers/404.js index ebd804c1..5d0b23da 100644 --- a/app/frontend/webserver/handlers/404.js +++ b/app/frontend/webserver/handlers/404.js @@ -1,10 +1,12 @@ const handle404 = async (_logger, app) => { // catch 404 and forward to error handler app.get('*', (_req, res) => { - res.send( - { success: false, status: 404, message: 'Route not found.', data: {} }, - 404 - ); + res.status(404).send({ + success: false, + status: 404, + message: 'Route not found.', + data: {} + }); }); }; diff --git a/app/frontend/webserver/handlers/__tests__/404.test.js b/app/frontend/webserver/handlers/__tests__/404.test.js index 40175eb9..1cd91f59 100644 --- a/app/frontend/webserver/handlers/__tests__/404.test.js +++ b/app/frontend/webserver/handlers/__tests__/404.test.js @@ -3,11 +3,13 @@ describe('webserver/handlers/404', () => { const appMock = {}; let resSendMock; + let resStatusMock; beforeEach(async () => { - resSendMock = jest.fn().mockResolvedValue(true); + resSendMock = jest.fn().mockReturnValue(true); + resStatusMock = jest.fn().mockReturnValue({ send: resSendMock }); appMock.get = jest.fn().mockImplementation((_path, func) => { - func(null, { send: resSendMock }); + func(null, { status: resStatusMock }); }); const { handle404 } = require('../404'); @@ -15,10 +17,16 @@ describe('webserver/handlers/404', () => { await handle404(null, appMock); }); + it('triggers status', () => { + expect(resStatusMock).toHaveBeenCalledWith(404); + }); + it('triggers res.send', () => { - expect(resSendMock).toHaveBeenCalledWith( - { success: false, status: 404, message: 'Route not found.', data: {} }, - 404 - ); + expect(resSendMock).toHaveBeenCalledWith({ + success: false, + status: 404, + message: 'Route not found.', + data: {} + }); }); }); diff --git a/app/frontend/websocket/handlers/__tests__/cancel-order.test.js b/app/frontend/websocket/handlers/__tests__/cancel-order.test.js index a45b9b54..09f8595d 100644 --- a/app/frontend/websocket/handlers/__tests__/cancel-order.test.js +++ b/app/frontend/websocket/handlers/__tests__/cancel-order.test.js @@ -5,8 +5,9 @@ describe('cancel-order.js', () => { let loggerMock; + let mockQueue; + let mockSaveOverrideAction; - let mockExecuteTrailingTrade; beforeEach(() => { jest.clearAllMocks().resetModules(); @@ -18,15 +19,16 @@ describe('cancel-order.js', () => { }; mockSaveOverrideAction = jest.fn().mockResolvedValue(true); - mockExecuteTrailingTrade = jest.fn().mockResolvedValue(true); jest.mock('../../../../cronjob/trailingTradeHelper/common', () => ({ saveOverrideAction: mockSaveOverrideAction })); - jest.mock('../../../../cronjob', () => ({ - executeTrailingTrade: mockExecuteTrailingTrade - })); + mockQueue = { + executeFor: jest.fn().mockResolvedValue(true) + }; + + jest.mock('../../../../cronjob/trailingTradeHelper/queue', () => mockQueue); }); beforeEach(async () => { @@ -59,11 +61,8 @@ describe('cancel-order.js', () => { ); }); - it('triggers executeTrailingTrade', () => { - expect(mockExecuteTrailingTrade).toHaveBeenCalledWith( - loggerMock, - 'BTCUSDT' - ); + it('triggers queue.executeFor', () => { + expect(mockQueue.executeFor).toHaveBeenCalledWith(loggerMock, 'BTCUSDT'); }); it('triggers ws.send', () => { diff --git a/app/frontend/websocket/handlers/__tests__/manual-trade-all-symbols.test.js b/app/frontend/websocket/handlers/__tests__/manual-trade-all-symbols.test.js index 6cb63d4a..30b09ab4 100644 --- a/app/frontend/websocket/handlers/__tests__/manual-trade-all-symbols.test.js +++ b/app/frontend/websocket/handlers/__tests__/manual-trade-all-symbols.test.js @@ -7,11 +7,11 @@ describe('manual-trade-all-symbols.js', () => { let loggerMock; let PubSubMock; + let mockQueue; let mockGetGlobalConfiguration; let mockSaveOverrideAction; - let mockExecuteTrailingTrade; const orders = { side: 'buy', @@ -238,11 +238,11 @@ describe('manual-trade-all-symbols.js', () => { saveOverrideAction: mockSaveOverrideAction })); - mockExecuteTrailingTrade = jest.fn().mockResolvedValue(true); + mockQueue = { + executeFor: jest.fn().mockResolvedValue(true) + }; - jest.mock('../../../../cronjob', () => ({ - executeTrailingTrade: mockExecuteTrailingTrade - })); + jest.mock('../../../../cronjob/trailingTradeHelper/queue', () => mockQueue); }); beforeEach(async () => { @@ -310,8 +310,8 @@ describe('manual-trade-all-symbols.js', () => { ); }); - it('triggers executeTrailingTrade', () => { - expect(mockExecuteTrailingTrade).toHaveBeenCalledWith( + it('triggers queue.executeFor', () => { + expect(mockQueue.executeFor).toHaveBeenCalledWith( loggerMock, symbol ); @@ -410,8 +410,8 @@ describe('manual-trade-all-symbols.js', () => { ); }); - it('triggers executeTrailingTrade', () => { - expect(mockExecuteTrailingTrade).toHaveBeenCalledWith( + it('triggers queue.executeFor', () => { + expect(mockQueue.executeFor).toHaveBeenCalledWith( loggerMock, 'BTCUSDT' ); diff --git a/app/frontend/websocket/handlers/__tests__/manual-trade.test.js b/app/frontend/websocket/handlers/__tests__/manual-trade.test.js index cad37129..b596cf4d 100644 --- a/app/frontend/websocket/handlers/__tests__/manual-trade.test.js +++ b/app/frontend/websocket/handlers/__tests__/manual-trade.test.js @@ -4,9 +4,9 @@ describe('manual-trade.js', () => { let mockWebSocketServerWebSocketSend; let loggerMock; + let mockQueue; let mockSaveOverrideAction; - let mockExecuteTrailingTrade; beforeEach(() => { jest.clearAllMocks().resetModules(); @@ -23,11 +23,11 @@ describe('manual-trade.js', () => { saveOverrideAction: mockSaveOverrideAction })); - mockExecuteTrailingTrade = jest.fn().mockResolvedValue(true); + mockQueue = { + executeFor: jest.fn().mockResolvedValue(true) + }; - jest.mock('../../../../cronjob', () => ({ - executeTrailingTrade: mockExecuteTrailingTrade - })); + jest.mock('../../../../cronjob/trailingTradeHelper/queue', () => mockQueue); }); beforeEach(async () => { @@ -62,11 +62,8 @@ describe('manual-trade.js', () => { ); }); - it('triggers executeTrailingTrade', () => { - expect(mockExecuteTrailingTrade).toHaveBeenCalledWith( - loggerMock, - 'BTCUSDT' - ); + it('triggers queue.executeFor', () => { + expect(mockQueue.executeFor).toHaveBeenCalledWith(loggerMock, 'BTCUSDT'); }); it('triggers ws.send', () => { diff --git a/app/frontend/websocket/handlers/__tests__/symbol-enable-action.test.js b/app/frontend/websocket/handlers/__tests__/symbol-enable-action.test.js index d753e198..f1a7f35e 100644 --- a/app/frontend/websocket/handlers/__tests__/symbol-enable-action.test.js +++ b/app/frontend/websocket/handlers/__tests__/symbol-enable-action.test.js @@ -6,8 +6,9 @@ describe('symbol-enable-action.test.js', () => { let mockLogger; + let mockQueue; + let mockDeleteDisableAction; - let mockExecuteTrailingTrade; beforeEach(() => { jest.clearAllMocks().resetModules(); @@ -18,11 +19,11 @@ describe('symbol-enable-action.test.js', () => { send: mockWebSocketServerWebSocketSend }; - mockExecuteTrailingTrade = jest.fn().mockResolvedValue(true); + mockQueue = { + executeFor: jest.fn().mockResolvedValue(true) + }; - jest.mock('../../../../cronjob', () => ({ - executeTrailingTrade: mockExecuteTrailingTrade - })); + jest.mock('../../../../cronjob/trailingTradeHelper/queue', () => mockQueue); }); describe('when symbol is provided', () => { @@ -51,11 +52,8 @@ describe('symbol-enable-action.test.js', () => { ); }); - it('triggers executeTrailingTrade', () => { - expect(mockExecuteTrailingTrade).toHaveBeenCalledWith( - mockLogger, - 'BTCUSDT' - ); + it('triggers queue.executeFor', () => { + expect(mockQueue.executeFor).toHaveBeenCalledWith(mockLogger, 'BTCUSDT'); }); it('triggers ws.send', () => { diff --git a/app/frontend/websocket/handlers/__tests__/symbol-grid-trade-delete.test.js b/app/frontend/websocket/handlers/__tests__/symbol-grid-trade-delete.test.js index e509c601..866a3b15 100644 --- a/app/frontend/websocket/handlers/__tests__/symbol-grid-trade-delete.test.js +++ b/app/frontend/websocket/handlers/__tests__/symbol-grid-trade-delete.test.js @@ -7,11 +7,11 @@ describe('symbol-grid-trade-delete.test.js', () => { let mockLogger; let mockSlack; + let mockQueue; + let mockArchiveSymbolGridTrade; let mockDeleteSymbolGridTrade; - let mockExecuteTrailingTrade; - beforeEach(() => { jest.clearAllMocks().resetModules(); @@ -22,17 +22,24 @@ describe('symbol-grid-trade-delete.test.js', () => { jest.requireActual('moment')(nextCheck || '2020-01-02T00:00:00.000Z') ); + // Mock moment to return static date + jest.mock( + 'moment-timezone', + () => nextCheck => + jest.requireActual('moment')(nextCheck || '2020-01-02T00:00:00.000Z') + ); + mockWebSocketServerWebSocketSend = jest.fn().mockResolvedValue(true); mockWebSocketServer = { send: mockWebSocketServerWebSocketSend }; - mockExecuteTrailingTrade = jest.fn().mockResolvedValue(true); + mockQueue = { + executeFor: jest.fn().mockResolvedValue(true) + }; - jest.mock('../../../../cronjob', () => ({ - executeTrailingTrade: mockExecuteTrailingTrade - })); + jest.mock('../../../../cronjob/trailingTradeHelper/queue', () => mockQueue); }); describe('when symbol is provided', () => { @@ -91,8 +98,8 @@ describe('symbol-grid-trade-delete.test.js', () => { ); }); - it('triggers executeTrailingTrade', () => { - expect(mockExecuteTrailingTrade).toHaveBeenCalledWith( + it('triggers queue.executeFor', () => { + expect(mockQueue.executeFor).toHaveBeenCalledWith( mockLogger, 'BTCUSDT' ); @@ -163,8 +170,8 @@ describe('symbol-grid-trade-delete.test.js', () => { ); }); - it('triggers executeTrailingTrade', () => { - expect(mockExecuteTrailingTrade).toHaveBeenCalledWith( + it('triggers queue.executeFor', () => { + expect(mockQueue.executeFor).toHaveBeenCalledWith( mockLogger, 'BTCUSDT' ); @@ -228,8 +235,8 @@ describe('symbol-grid-trade-delete.test.js', () => { ); }); - it('triggers executeTrailingTrade', () => { - expect(mockExecuteTrailingTrade).toHaveBeenCalledWith( + it('triggers queue.executeFor', () => { + expect(mockQueue.executeFor).toHaveBeenCalledWith( mockLogger, 'BTCUSDT' ); @@ -290,8 +297,8 @@ describe('symbol-grid-trade-delete.test.js', () => { ); }); - it('triggers executeTrailingTrade', () => { - expect(mockExecuteTrailingTrade).toHaveBeenCalledWith( + it('triggers queue.executeFor', () => { + expect(mockQueue.executeFor).toHaveBeenCalledWith( mockLogger, 'BTCUSDT' ); diff --git a/app/frontend/websocket/handlers/__tests__/symbol-setting-delete.test.js b/app/frontend/websocket/handlers/__tests__/symbol-setting-delete.test.js index 57cda4e3..d455016c 100644 --- a/app/frontend/websocket/handlers/__tests__/symbol-setting-delete.test.js +++ b/app/frontend/websocket/handlers/__tests__/symbol-setting-delete.test.js @@ -8,7 +8,7 @@ describe('symbol-setting-delete.test.js', () => { let mockDeleteSymbolConfiguration; - let mockExecuteTrailingTrade; + let mockQueue; beforeEach(() => { jest.clearAllMocks().resetModules(); @@ -18,6 +18,12 @@ describe('symbol-setting-delete.test.js', () => { mockWebSocketServer = { send: mockWebSocketServerWebSocketSend }; + + mockQueue = { + executeFor: jest.fn().mockResolvedValue(true) + }; + + jest.mock('../../../../cronjob/trailingTradeHelper/queue', () => mockQueue); }); describe('when symbol is provided', () => { @@ -34,12 +40,6 @@ describe('symbol-setting-delete.test.js', () => { }) ); - mockExecuteTrailingTrade = jest.fn().mockResolvedValue(true); - - jest.mock('../../../../cronjob', () => ({ - executeTrailingTrade: mockExecuteTrailingTrade - })); - const { handleSymbolSettingDelete } = require('../symbol-setting-delete'); await handleSymbolSettingDelete(logger, mockWebSocketServer, { data: { @@ -55,11 +55,8 @@ describe('symbol-setting-delete.test.js', () => { ); }); - it('triggers executeTrailingTrade', () => { - expect(mockExecuteTrailingTrade).toHaveBeenCalledWith( - mockLogger, - 'BTCUSDT' - ); + it('triggers queue.executeFor', () => { + expect(mockQueue.executeFor).toHaveBeenCalledWith(mockLogger, 'BTCUSDT'); }); it('triggers ws.send', () => { diff --git a/app/frontend/websocket/handlers/__tests__/symbol-setting-update.test.js b/app/frontend/websocket/handlers/__tests__/symbol-setting-update.test.js index 76b4e90b..7bcdd8e7 100644 --- a/app/frontend/websocket/handlers/__tests__/symbol-setting-update.test.js +++ b/app/frontend/websocket/handlers/__tests__/symbol-setting-update.test.js @@ -6,9 +6,10 @@ describe('symbol-setting-update.test.js', () => { let mockLogger; + let mockQueue; + let mockGetSymbolConfiguration; let mockSaveSymbolConfiguration; - let mockExecuteTrailingTrade; beforeEach(() => { jest.clearAllMocks().resetModules(); @@ -19,11 +20,11 @@ describe('symbol-setting-update.test.js', () => { send: mockWebSocketServerWebSocketSend }; - mockExecuteTrailingTrade = jest.fn().mockResolvedValue(true); + mockQueue = { + executeFor: jest.fn().mockResolvedValue(true) + }; - jest.mock('../../../../cronjob', () => ({ - executeTrailingTrade: mockExecuteTrailingTrade - })); + jest.mock('../../../../cronjob/trailingTradeHelper/queue', () => mockQueue); }); describe('when configuration is valid', () => { @@ -265,11 +266,8 @@ describe('symbol-setting-update.test.js', () => { ); }); - it('triggers executeTrailingTrade', () => { - expect(mockExecuteTrailingTrade).toHaveBeenCalledWith( - mockLogger, - 'BTCUSDT' - ); + it('triggers queue.executeFor', () => { + expect(mockQueue.executeFor).toHaveBeenCalledWith(mockLogger, 'BTCUSDT'); }); it('triggers ws.send', () => { diff --git a/app/frontend/websocket/handlers/__tests__/symbol-trigger-buy.test.js b/app/frontend/websocket/handlers/__tests__/symbol-trigger-buy.test.js index 3313d9be..cec5fe20 100644 --- a/app/frontend/websocket/handlers/__tests__/symbol-trigger-buy.test.js +++ b/app/frontend/websocket/handlers/__tests__/symbol-trigger-buy.test.js @@ -6,8 +6,9 @@ describe('symbol-trigger-buy.test.js', () => { let mockLogger; + let mockQueue; + let mockSaveOverrideAction; - let mockExecuteTrailingTrade; beforeEach(() => { jest.clearAllMocks().resetModules(); @@ -24,11 +25,11 @@ describe('symbol-trigger-buy.test.js', () => { saveOverrideAction: mockSaveOverrideAction })); - mockExecuteTrailingTrade = jest.fn().mockResolvedValue(true); + mockQueue = { + executeFor: jest.fn().mockResolvedValue(true) + }; - jest.mock('../../../../cronjob', () => ({ - executeTrailingTrade: mockExecuteTrailingTrade - })); + jest.mock('../../../../cronjob/trailingTradeHelper/queue', () => mockQueue); }); describe('when symbol is provided', () => { @@ -59,11 +60,8 @@ describe('symbol-trigger-buy.test.js', () => { ); }); - it('triggers executeTrailingTrade', () => { - expect(mockExecuteTrailingTrade).toHaveBeenCalledWith( - mockLogger, - 'BTCUSDT' - ); + it('triggers queue.executeFor', () => { + expect(mockQueue.executeFor).toHaveBeenCalledWith(mockLogger, 'BTCUSDT'); }); it('triggers ws.send', () => { diff --git a/app/frontend/websocket/handlers/__tests__/symbol-trigger-sell.test.js b/app/frontend/websocket/handlers/__tests__/symbol-trigger-sell.test.js index ebde0721..0d84b2c4 100644 --- a/app/frontend/websocket/handlers/__tests__/symbol-trigger-sell.test.js +++ b/app/frontend/websocket/handlers/__tests__/symbol-trigger-sell.test.js @@ -6,8 +6,9 @@ describe('symbol-trigger-sell.test.js', () => { let mockLogger; + let mockQueue; + let mockSaveOverrideAction; - let mockExecuteTrailingTrade; beforeEach(() => { jest.clearAllMocks().resetModules(); @@ -24,11 +25,11 @@ describe('symbol-trigger-sell.test.js', () => { saveOverrideAction: mockSaveOverrideAction })); - mockExecuteTrailingTrade = jest.fn().mockResolvedValue(true); + mockQueue = { + executeFor: jest.fn().mockResolvedValue(true) + }; - jest.mock('../../../../cronjob', () => ({ - executeTrailingTrade: mockExecuteTrailingTrade - })); + jest.mock('../../../../cronjob/trailingTradeHelper/queue', () => mockQueue); }); describe('when symbol is provided', () => { @@ -57,11 +58,8 @@ describe('symbol-trigger-sell.test.js', () => { ); }); - it('triggers executeTrailingTrade', () => { - expect(mockExecuteTrailingTrade).toHaveBeenCalledWith( - mockLogger, - 'BTCUSDT' - ); + it('triggers queue.executeFor', () => { + expect(mockQueue.executeFor).toHaveBeenCalledWith(mockLogger, 'BTCUSDT'); }); it('triggers ws.send', () => { diff --git a/app/frontend/websocket/handlers/__tests__/symbol-update-last-buy-price.test.js b/app/frontend/websocket/handlers/__tests__/symbol-update-last-buy-price.test.js index 0dc07c62..a800603c 100644 --- a/app/frontend/websocket/handlers/__tests__/symbol-update-last-buy-price.test.js +++ b/app/frontend/websocket/handlers/__tests__/symbol-update-last-buy-price.test.js @@ -6,12 +6,12 @@ describe('symbol-update-last-buy-price.test.js', () => { let mockGetAccountInfo; let mockSaveLastBuyPrice; - let mockExecuteTrailingTrade; let loggerMock; let mongoMock; let cacheMock; let PubSubMock; + let mockQueue; beforeEach(() => { jest.clearAllMocks().resetModules(); @@ -22,11 +22,11 @@ describe('symbol-update-last-buy-price.test.js', () => { send: mockWebSocketServerWebSocketSend }; - mockExecuteTrailingTrade = jest.fn().mockResolvedValue(true); + mockQueue = { + executeFor: jest.fn().mockResolvedValue(true) + }; - jest.mock('../../../../cronjob', () => ({ - executeTrailingTrade: mockExecuteTrailingTrade - })); + jest.mock('../../../../cronjob/trailingTradeHelper/queue', () => mockQueue); }); describe('update symbol last buy price', () => { @@ -61,8 +61,8 @@ describe('symbol-update-last-buy-price.test.js', () => { ); }); - it('triggers executeTrailingTrade', () => { - expect(mockExecuteTrailingTrade).toHaveBeenCalledWith( + it('triggers queue.executeFor', () => { + expect(mockQueue.executeFor).toHaveBeenCalledWith( loggerMock, 'BTCUSDT' ); @@ -213,8 +213,8 @@ describe('symbol-update-last-buy-price.test.js', () => { ); }); - it('triggers executeTrailingTrade', () => { - expect(mockExecuteTrailingTrade).toHaveBeenCalledWith( + it('triggers queue.executeFor', () => { + expect(mockQueue.executeFor).toHaveBeenCalledWith( loggerMock, 'BTCUSDT' ); @@ -307,8 +307,8 @@ describe('symbol-update-last-buy-price.test.js', () => { ); }); - it('triggers executeTrailingTrade', () => { - expect(mockExecuteTrailingTrade).toHaveBeenCalledWith( + it('triggers queue.executeFor', () => { + expect(mockQueue.executeFor).toHaveBeenCalledWith( loggerMock, 'BTCUSDT' ); diff --git a/app/frontend/websocket/handlers/cancel-order.js b/app/frontend/websocket/handlers/cancel-order.js index ee3d01e1..ac814161 100644 --- a/app/frontend/websocket/handlers/cancel-order.js +++ b/app/frontend/websocket/handlers/cancel-order.js @@ -2,7 +2,7 @@ const moment = require('moment'); const { saveOverrideAction } = require('../../../cronjob/trailingTradeHelper/common'); -const { executeTrailingTrade } = require('../../../cronjob'); +const queue = require('../../../cronjob/trailingTradeHelper/queue'); const handleCancelOrder = async (logger, ws, payload) => { logger.info({ payload }, 'Start cancel order'); @@ -23,7 +23,7 @@ const handleCancelOrder = async (logger, ws, payload) => { 'Cancelling the order action has been received. Wait for cancelling the order.' ); - executeTrailingTrade(logger, symbol); + queue.executeFor(logger, symbol); ws.send( JSON.stringify({ diff --git a/app/frontend/websocket/handlers/manual-trade-all-symbols.js b/app/frontend/websocket/handlers/manual-trade-all-symbols.js index bdbcbbb5..763f1708 100644 --- a/app/frontend/websocket/handlers/manual-trade-all-symbols.js +++ b/app/frontend/websocket/handlers/manual-trade-all-symbols.js @@ -7,7 +7,7 @@ const { const { saveOverrideAction } = require('../../../cronjob/trailingTradeHelper/common'); -const { executeTrailingTrade } = require('../../../cronjob'); +const queue = require('../../../cronjob/trailingTradeHelper/queue'); const handleManualTradeAllSymbols = async (logger, ws, payload) => { logger.info({ payload }, 'Start manual trade all symbols'); @@ -62,7 +62,7 @@ const handleManualTradeAllSymbols = async (logger, ws, payload) => { `Order for ${symbol} has been queued.` ); - executeTrailingTrade(logger, symbol); + queue.executeFor(logger, symbol); currentTime = moment(currentTime).add( placeManualOrderInterval, @@ -103,7 +103,7 @@ const handleManualTradeAllSymbols = async (logger, ws, payload) => { `Order for ${symbol} has been queued.` ); - executeTrailingTrade(logger, symbol); + queue.executeFor(logger, symbol); currentTime = moment(currentTime).add( placeManualOrderInterval, diff --git a/app/frontend/websocket/handlers/manual-trade.js b/app/frontend/websocket/handlers/manual-trade.js index 9495c587..e996c415 100644 --- a/app/frontend/websocket/handlers/manual-trade.js +++ b/app/frontend/websocket/handlers/manual-trade.js @@ -1,8 +1,8 @@ const moment = require('moment'); -const { executeTrailingTrade } = require('../../../cronjob'); const { saveOverrideAction } = require('../../../cronjob/trailingTradeHelper/common'); +const queue = require('../../../cronjob/trailingTradeHelper/queue'); const handleManualTrade = async (logger, ws, payload) => { logger.info({ payload }, 'Start manual trade'); @@ -23,7 +23,7 @@ const handleManualTrade = async (logger, ws, payload) => { 'The manual order received by the bot. Wait for placing the order.' ); - executeTrailingTrade(logger, symbol); + queue.executeFor(logger, symbol); ws.send( JSON.stringify({ diff --git a/app/frontend/websocket/handlers/symbol-enable-action.js b/app/frontend/websocket/handlers/symbol-enable-action.js index 8bc35268..81d0fd40 100644 --- a/app/frontend/websocket/handlers/symbol-enable-action.js +++ b/app/frontend/websocket/handlers/symbol-enable-action.js @@ -1,7 +1,7 @@ -const { executeTrailingTrade } = require('../../../cronjob'); const { deleteDisableAction } = require('../../../cronjob/trailingTradeHelper/common'); +const queue = require('../../../cronjob/trailingTradeHelper/queue'); const handleSymbolEnableAction = async (logger, ws, payload) => { logger.info({ payload }, 'Start symbol enable action'); @@ -12,7 +12,7 @@ const handleSymbolEnableAction = async (logger, ws, payload) => { await deleteDisableAction(logger, symbol); - executeTrailingTrade(logger, symbol); + queue.executeFor(logger, symbol); ws.send( JSON.stringify({ result: true, type: 'symbol-enable-action-result' }) diff --git a/app/frontend/websocket/handlers/symbol-grid-trade-delete.js b/app/frontend/websocket/handlers/symbol-grid-trade-delete.js index a8a07a3a..7851912c 100644 --- a/app/frontend/websocket/handlers/symbol-grid-trade-delete.js +++ b/app/frontend/websocket/handlers/symbol-grid-trade-delete.js @@ -8,7 +8,8 @@ const { archiveSymbolGridTrade, deleteSymbolGridTrade } = require('../../../cronjob/trailingTradeHelper/configuration'); -const { executeTrailingTrade } = require('../../../cronjob'); + +const queue = require('../../../cronjob/trailingTradeHelper/queue'); const handleSymbolGridTradeDelete = async (logger, ws, payload) => { logger.info({ payload }, 'Start grid trade delete'); @@ -41,7 +42,7 @@ const handleSymbolGridTradeDelete = async (logger, ws, payload) => { await deleteSymbolGridTrade(logger, symbol); - executeTrailingTrade(logger, symbol); + queue.executeFor(logger, symbol); ws.send( JSON.stringify({ result: true, type: 'symbol-grid-trade-delete-result' }) diff --git a/app/frontend/websocket/handlers/symbol-setting-delete.js b/app/frontend/websocket/handlers/symbol-setting-delete.js index 288c44e4..a54ee483 100644 --- a/app/frontend/websocket/handlers/symbol-setting-delete.js +++ b/app/frontend/websocket/handlers/symbol-setting-delete.js @@ -1,7 +1,7 @@ const { deleteSymbolConfiguration } = require('../../../cronjob/trailingTradeHelper/configuration'); -const { executeTrailingTrade } = require('../../../cronjob'); +const queue = require('../../../cronjob/trailingTradeHelper/queue'); const handleSymbolSettingDelete = async (logger, ws, payload) => { logger.info({ payload }, 'Start symbol setting delete'); @@ -12,7 +12,7 @@ const handleSymbolSettingDelete = async (logger, ws, payload) => { await deleteSymbolConfiguration(logger, symbol); - executeTrailingTrade(logger, symbol); + queue.executeFor(logger, symbol); ws.send( JSON.stringify({ result: true, type: 'symbol-setting-delete-result' }) diff --git a/app/frontend/websocket/handlers/symbol-setting-update.js b/app/frontend/websocket/handlers/symbol-setting-update.js index 011dc115..3b480ab4 100644 --- a/app/frontend/websocket/handlers/symbol-setting-update.js +++ b/app/frontend/websocket/handlers/symbol-setting-update.js @@ -3,7 +3,7 @@ const { getSymbolConfiguration, saveSymbolConfiguration } = require('../../../cronjob/trailingTradeHelper/configuration'); -const { executeTrailingTrade } = require('../../../cronjob'); +const queue = require('../../../cronjob/trailingTradeHelper/queue'); const handleSymbolSettingUpdate = async (logger, ws, payload) => { logger.info({ payload }, 'Start symbol setting update'); @@ -44,7 +44,7 @@ const handleSymbolSettingUpdate = async (logger, ws, payload) => { await saveSymbolConfiguration(logger, symbol, symbolConfiguration); - executeTrailingTrade(logger, symbol); + queue.executeFor(logger, symbol); ws.send( JSON.stringify({ diff --git a/app/frontend/websocket/handlers/symbol-trigger-buy.js b/app/frontend/websocket/handlers/symbol-trigger-buy.js index 3da7d351..f94b536a 100644 --- a/app/frontend/websocket/handlers/symbol-trigger-buy.js +++ b/app/frontend/websocket/handlers/symbol-trigger-buy.js @@ -2,7 +2,7 @@ const moment = require('moment'); const { saveOverrideAction } = require('../../../cronjob/trailingTradeHelper/common'); -const { executeTrailingTrade } = require('../../../cronjob'); +const queue = require('../../../cronjob/trailingTradeHelper/queue'); const handleSymbolTriggerBuy = async (logger, ws, payload) => { logger.info({ payload }, 'Start symbol trigger buy'); @@ -25,7 +25,7 @@ const handleSymbolTriggerBuy = async (logger, ws, payload) => { 'The buy order received by the bot. Wait for placing the order.' ); - executeTrailingTrade(logger, symbol); + queue.executeFor(logger, symbol); ws.send(JSON.stringify({ result: true, type: 'symbol-trigger-buy-result' })); }; diff --git a/app/frontend/websocket/handlers/symbol-trigger-sell.js b/app/frontend/websocket/handlers/symbol-trigger-sell.js index aba16fc2..58a8f7f6 100644 --- a/app/frontend/websocket/handlers/symbol-trigger-sell.js +++ b/app/frontend/websocket/handlers/symbol-trigger-sell.js @@ -2,7 +2,7 @@ const moment = require('moment'); const { saveOverrideAction } = require('../../../cronjob/trailingTradeHelper/common'); -const { executeTrailingTrade } = require('../../../cronjob'); +const queue = require('../../../cronjob/trailingTradeHelper/queue'); const handleSymbolTriggerSell = async (logger, ws, payload) => { logger.info({ payload }, 'Start symbol trigger sell'); @@ -22,7 +22,7 @@ const handleSymbolTriggerSell = async (logger, ws, payload) => { 'The sell order received by the bot. Wait for placing the order.' ); - executeTrailingTrade(logger, symbol); + queue.executeFor(logger, symbol); ws.send(JSON.stringify({ result: true, type: 'symbol-trigger-sell-result' })); }; diff --git a/app/frontend/websocket/handlers/symbol-update-last-buy-price.js b/app/frontend/websocket/handlers/symbol-update-last-buy-price.js index b80a8063..68f55b2f 100644 --- a/app/frontend/websocket/handlers/symbol-update-last-buy-price.js +++ b/app/frontend/websocket/handlers/symbol-update-last-buy-price.js @@ -4,7 +4,7 @@ const { getAccountInfo, saveLastBuyPrice } = require('../../../cronjob/trailingTradeHelper/common'); -const { executeTrailingTrade } = require('../../../cronjob'); +const queue = require('../../../cronjob/trailingTradeHelper/queue'); /** * Delete last buy price @@ -19,7 +19,7 @@ const deleteLastBuyPrice = async (logger, ws, symbol) => { key: `${symbol}-last-buy-price` }); - executeTrailingTrade(logger, symbol); + queue.executeFor(logger, symbol); PubSub.publish('frontend-notification', { type: 'success', @@ -97,7 +97,7 @@ const updateLastBuyPrice = async (logger, ws, symbol, lastBuyPrice) => { quantity: baseAssetTotalBalance }); - executeTrailingTrade(logger, symbol); + queue.executeFor(logger, symbol); PubSub.publish('frontend-notification', { type: 'success', diff --git a/app/server-binance.js b/app/server-binance.js index 0f8983f5..e853e475 100644 --- a/app/server-binance.js +++ b/app/server-binance.js @@ -1,6 +1,7 @@ const _ = require('lodash'); const config = require('config'); const { PubSub, cache, mongo } = require('./helpers'); +const queue = require('./cronjob/trailingTradeHelper/queue'); const { maskConfig } = require('./cronjob/trailingTradeHelper/util'); const { @@ -105,6 +106,8 @@ const syncAll = async logger => { logger.info({ symbols }, 'Retrieved symbols'); + await queue.init(logger, symbols); + // Lock all symbols for 5 minutes to ensure nothing will be executed unless all data retrieved await Promise.all(symbols.map(symbol => lockSymbol(logger, symbol, 300))); diff --git a/app/server-frontend.js b/app/server-frontend.js index 4fbbb8fd..504595a5 100644 --- a/app/server-frontend.js +++ b/app/server-frontend.js @@ -24,6 +24,7 @@ const loginLimiter = new RateLimiterRedis({ const { configureWebServer } = require('./frontend/webserver/configure'); const { configureWebSocket } = require('./frontend/websocket/configure'); const { configureLocalTunnel } = require('./frontend/local-tunnel/configure'); +const { configureBullBoard } = require('./frontend/bull-board/configure'); const runFrontend = async serverLogger => { const logger = serverLogger.child({ server: 'frontend' }); @@ -40,6 +41,9 @@ const runFrontend = async serverLogger => { app.use(express.static(path.join(__dirname, '/../public'))); + // Must configure bull board before listen. + configureBullBoard(app, logger); + const server = app.listen(80); if (config.get('authentication.enabled')) { diff --git a/package-lock.json b/package-lock.json index 11c67be3..1c62f7ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1411,6 +1411,220 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "@bull-board/api": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@bull-board/api/-/api-4.2.2.tgz", + "integrity": "sha512-YFkkeWvMit0P04k+xu4ZZ22i24m+Tq/w82LBtpt3z9Xu1rGrZoui8CI/YRsaJJE0o9TsqL5tY653oFVcdg35pQ==", + "requires": { + "redis-info": "^3.0.8" + } + }, + "@bull-board/express": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@bull-board/express/-/express-4.2.2.tgz", + "integrity": "sha512-Qd5+8zBoYfphxo6vxSJ/aRGJKM1AWv84ToSKqWvWLnRicxTz3fFNJ3ht0hXr9NrmaeprHLmgkS1w4mIpqG2x4A==", + "requires": { + "@bull-board/api": "4.2.2", + "@bull-board/ui": "4.2.2", + "ejs": "3.1.7", + "express": "4.17.3" + }, + "dependencies": { + "body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-SAAwOxgoCKMGs9uUAUFHygfLAyaniaoun6I8mFY9pRAJL9+Kec34aU+oIjDhTycub1jozEfEwx1W1IuOYxVSFw==", + "requires": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.8.1", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.9.7", + "raw-body": "2.4.3", + "type-is": "~1.6.18" + } + }, + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" + }, + "cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg==" + }, + "express": { + "version": "4.17.3", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.3.tgz", + "integrity": "sha512-yuSQpz5I+Ch7gFrPCk4/c+dIBKlQUxtgwqzph132bsT6qhuzss6I8cLJQz7B3rFblzd6wtcI0ZbGltH/C4LjUg==", + "requires": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.19.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.4.2", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.9.7", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.17.2", + "serve-static": "1.14.2", + "setprototypeof": "1.2.0", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + } + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + } + }, + "http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "requires": { + "ee-first": "1.1.1" + } + }, + "qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==" + }, + "raw-body": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.3.tgz", + "integrity": "sha512-UlTNLIcu0uzb4D2f4WltY6cVjLi+/jEN4lgEUj3E04tpMDpUlkBo/eSn6zou9hum2VMNpCCUone0O0WeJim07g==", + "requires": { + "bytes": "3.1.2", + "http-errors": "1.8.1", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "send": { + "version": "0.17.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.2.tgz", + "integrity": "sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww==", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "1.8.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "dependencies": { + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, + "serve-static": { + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.2.tgz", + "integrity": "sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.2" + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==" + } + } + }, + "@bull-board/ui": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@bull-board/ui/-/ui-4.2.2.tgz", + "integrity": "sha512-QLWWTtVj6kQ01ox4OqCs/IdKm+jWFtLvhBU7RwYt8UxmxA6dZ8ffS6hWmjWk5sJ4cKk9GzPoASYMgFv0AMuh0w==", + "requires": { + "@bull-board/api": "4.2.2" + } + }, "@commitlint/cli": { "version": "17.0.1", "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-17.0.1.tgz", @@ -2356,6 +2570,16 @@ "integrity": "sha512-Ct5MqZkLGEXTVmQYbGtx9SVqD2fqwvdubdps5D3djjAkgkKwT918VNOz65pEHFaYTeWcukmJmH5SwsA9Tn2ObQ==", "dev": true }, + "@jridgewell/source-map": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", + "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, "@jridgewell/sourcemap-codec": { "version": "1.4.13", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.13.tgz", @@ -2372,6 +2596,42 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-2.1.2.tgz", + "integrity": "sha512-TyVLn3S/+ikMDsh0gbKv2YydKClN8HaJDDpONlaZR+LVJmsxLFUgA+O7zu59h9+f9gX1aj/ahw9wqa6rosmrYQ==", + "optional": true + }, + "@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-2.1.2.tgz", + "integrity": "sha512-YPXtcVkhmVNoMGlqp81ZHW4dMxK09msWgnxtsDpSiZwTzUBG2N+No2bsr7WMtBKCVJMSD6mbAl7YhKUqkp/Few==", + "optional": true + }, + "@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-2.1.2.tgz", + "integrity": "sha512-42R4MAFeIeNn+L98qwxAt360bwzX2Kf0ZQkBBucJ2Ircza3asoY4CDbgiu9VWklq8gWJVSJSJBwDI+c/THiWkA==", + "optional": true + }, + "@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-2.1.2.tgz", + "integrity": "sha512-vHZ2JiOWF2+DN9lzltGbhtQNzDo8fKFGrf37UJrgqxU0yvtERrzUugnfnX1wmVfFhSsF8OxrfqiNOUc5hko1Zg==", + "optional": true + }, + "@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-2.1.2.tgz", + "integrity": "sha512-RjRoRxg7Q3kPAdUSC5EUUPlwfMkIVhmaRTIe+cqHbKrGZ4M6TyCA/b5qMaukQ/1CHWrqYY2FbKOAU8Hg0pQFzg==", + "optional": true + }, + "@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-2.1.2.tgz", + "integrity": "sha512-rIZVR48zA8hGkHIK7ED6+ZiXsjRCcAVBJbm8o89OKAMTmEAQ2QvoOxoiu3w2isAaWwzgtQIOFIqHwvZDyLKCvw==", + "optional": true + }, "@nicolo-ribaudo/chokidar-2": { "version": "2.1.8-no-fsevents.3", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz", @@ -2417,12 +2677,6 @@ "integrity": "sha512-AFBVi/iT4g20DHoujvMH1aEDn8fGJh4xsRGCP6d8RpLPMqsNPvW01Jcn0QysXTsg++/xj25NmJsGyH9xug/wKg==", "dev": true }, - "@sindresorhus/is": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", - "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==", - "dev": true - }, "@sinonjs/commons": { "version": "1.8.3", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", @@ -2441,15 +2695,6 @@ "@sinonjs/commons": "^1.7.0" } }, - "@szmarczak/http-timer": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", - "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", - "dev": true, - "requires": { - "defer-to-connect": "^1.0.1" - } - }, "@tsconfig/node10": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz", @@ -2525,6 +2770,16 @@ "@types/node": "*" } }, + "@types/bull": { + "version": "3.15.9", + "resolved": "https://registry.npmjs.org/@types/bull/-/bull-3.15.9.tgz", + "integrity": "sha512-MPUcyPPQauAmynoO3ezHAmCOhbB0pWmYyijr/5ctaCqhbKWsjW0YCod38ZcLzUBprosfZ9dPqfYIcfdKjk7RNQ==", + "dev": true, + "requires": { + "@types/ioredis": "*", + "@types/redis": "^2.8.0" + } + }, "@types/connect": { "version": "3.4.35", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", @@ -2601,6 +2856,15 @@ "@types/node": "*" } }, + "@types/ioredis": { + "version": "4.28.10", + "resolved": "https://registry.npmjs.org/@types/ioredis/-/ioredis-4.28.10.tgz", + "integrity": "sha512-69LyhUgrXdgcNDv7ogs1qXZomnfOEnSmrmMFqKgt1XMJxmoOSG/u3wYy13yACIfKuMJ8IhKgHafDO3sx19zVQQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/istanbul-lib-coverage": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", @@ -2699,6 +2963,15 @@ "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", "dev": true }, + "@types/redis": { + "version": "2.8.32", + "resolved": "https://registry.npmjs.org/@types/redis/-/redis-2.8.32.tgz", + "integrity": "sha512-7jkMKxcGq9p242exlbsVzuJb57KqHRhNl4dHoQu2Y5v9bCAbtIXXH0R3HleSQW4CTOqpHIYUW3t6tpUj4BVQ+w==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/serve-static": { "version": "1.13.10", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", @@ -3186,15 +3459,6 @@ "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", "dev": true }, - "ansi-align": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", - "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", - "dev": true, - "requires": { - "string-width": "^4.1.0" - } - }, "ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -3389,8 +3653,7 @@ "async": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", - "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==", - "dev": true + "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==" }, "async-each": { "version": "1.0.3", @@ -5173,61 +5436,6 @@ } } }, - "boxen": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz", - "integrity": "sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==", - "dev": true, - "requires": { - "ansi-align": "^3.0.0", - "camelcase": "^6.2.0", - "chalk": "^4.1.0", - "cli-boxes": "^2.2.1", - "string-width": "^4.2.2", - "type-fest": "^0.20.2", - "widest-line": "^3.1.0", - "wrap-ansi": "^7.0.0" - }, - "dependencies": { - "camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true - } - } - }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -9787,6 +9995,55 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "bull": { + "version": "4.8.5", + "resolved": "https://registry.npmjs.org/bull/-/bull-4.8.5.tgz", + "integrity": "sha512-2Z630e4f6VsLJnWMAtfEHwIqJYmND4W3dcG48RIbXeWpvb4UnYtpe/zxEdslJu0PKrltB4IkFj5YtBsdeQRn8w==", + "requires": { + "cron-parser": "^4.2.1", + "debuglog": "^1.0.0", + "get-port": "^5.1.1", + "ioredis": "^4.28.5", + "lodash": "^4.17.21", + "msgpackr": "^1.5.2", + "p-timeout": "^3.2.0", + "semver": "^7.3.2", + "uuid": "^8.3.0" + }, + "dependencies": { + "denque": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/denque/-/denque-1.5.1.tgz", + "integrity": "sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw==" + }, + "ioredis": { + "version": "4.28.5", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.28.5.tgz", + "integrity": "sha512-3GYo0GJtLqgNXj4YhrisLaNNvWSNwSS2wS4OELGfGxH8I69+XfNdnmV1AyN+ZqMh0i7eX+SWjrwFKDBDgfBC1A==", + "requires": { + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.1", + "denque": "^1.1.0", + "lodash.defaults": "^4.2.0", + "lodash.flatten": "^4.4.0", + "lodash.isarguments": "^3.1.0", + "p-map": "^2.1.0", + "redis-commands": "1.7.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + } + }, + "semver": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, "bunyan": { "version": "1.8.15", "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.15.tgz", @@ -9830,38 +10087,6 @@ } } }, - "cacheable-request": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", - "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", - "dev": true, - "requires": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^3.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^4.1.0", - "responselike": "^1.0.2" - }, - "dependencies": { - "get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "lowercase-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", - "dev": true - } - } - }, "call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -10042,12 +10267,6 @@ "del": "^4.1.1" } }, - "cli-boxes": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", - "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==", - "dev": true - }, "cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -10122,15 +10341,6 @@ "shallow-clone": "^3.0.0" } }, - "clone-response": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", - "integrity": "sha512-yjLXh88P599UOyPTFX0POsd7WxnbsVsGohcwzHOLspIhhpalPw1BcqED8NblyZLKcGrL8dTgMlcaZxV2jAD41Q==", - "dev": true, - "requires": { - "mimic-response": "^1.0.0" - } - }, "cluster-key-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz", @@ -10270,49 +10480,6 @@ "json5": "^2.1.1" } }, - "configstore": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", - "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", - "dev": true, - "requires": { - "dot-prop": "^5.2.0", - "graceful-fs": "^4.1.2", - "make-dir": "^3.0.0", - "unique-string": "^2.0.0", - "write-file-atomic": "^3.0.0", - "xdg-basedir": "^4.0.0" - }, - "dependencies": { - "make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, - "requires": { - "semver": "^6.0.0" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, - "write-file-atomic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", - "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", - "dev": true, - "requires": { - "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" - } - } - } - }, "confusing-browser-globals": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", @@ -10483,10 +10650,25 @@ "luxon": "^1.23.x" } }, - "cross-env": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", - "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "cron-parser": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.6.0.tgz", + "integrity": "sha512-guZNLMGUgg6z4+eGhmHGw7ft+v6OQeuHzd1gcLxCo9Yg/qoxmG3nindp2/uwGCLizEisf2H0ptqeVXeoCpP6FA==", + "requires": { + "luxon": "^3.0.1" + }, + "dependencies": { + "luxon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.0.1.tgz", + "integrity": "sha512-hF3kv0e5gwHQZKz4wtm4c+inDtyc7elkanAsBq+fundaCdUBNJB1dHEGUZIM6SfSBUlbVFduPwEtNjFK8wLtcw==" + } + } + }, + "cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", "requires": { "cross-spawn": "^7.0.1" } @@ -10501,12 +10683,6 @@ "which": "^2.0.1" } }, - "crypto-random-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", - "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", - "dev": true - }, "damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -10532,6 +10708,11 @@ "ms": "2.1.2" } }, + "debuglog": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz", + "integrity": "sha512-syBZ+rnAK3EgMsH2aYEOLUW7mZSY9Gb+0wUMCFsZvcmiz+HigA0LOcq/HoQqVuGG+EKykunc7QG2bzrponfaSw==" + }, "decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", @@ -10563,27 +10744,12 @@ "dev": true, "optional": true }, - "decompress-response": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", - "integrity": "sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==", - "dev": true, - "requires": { - "mimic-response": "^1.0.0" - } - }, "dedent": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", "dev": true }, - "deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true - }, "deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -10596,12 +10762,6 @@ "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", "dev": true }, - "defer-to-connect": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", - "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==", - "dev": true - }, "define-properties": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", @@ -10801,12 +10961,6 @@ "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", "dev": true }, - "duplexer3": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", - "integrity": "sha512-CEj8FwwNA4cVH2uFCoHUrmojhYh1vmCdOaneKJXwkeY1i9jnlslVo9dx+hQ5Hl9GnH/Bwy/IjxAyOePyPKYnzA==", - "dev": true - }, "eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -10826,6 +10980,14 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, + "ejs": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.7.tgz", + "integrity": "sha512-BIar7R6abbUxDA3bfXrO4DSgwo8I+fB5/1zgujl3HLLjwd6+9iOnrT+t3grn2qbk9vOgBubXOFwX2m9axoFaGw==", + "requires": { + "jake": "^10.8.5" + } + }, "electron-to-chromium": { "version": "1.4.141", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.141.tgz", @@ -10854,15 +11016,6 @@ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" }, - "end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, - "requires": { - "once": "^1.4.0" - } - }, "enhanced-resolve": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.3.tgz", @@ -10950,12 +11103,6 @@ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" }, - "escape-goat": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", - "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==", - "dev": true - }, "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -11950,6 +12097,32 @@ "flat-cache": "^3.0.4" } }, + "filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "requires": { + "minimatch": "^5.0.1" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", + "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, "filename-regex": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", @@ -12252,6 +12425,11 @@ "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", "dev": true }, + "get-port": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", + "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==" + }, "get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -12444,36 +12622,6 @@ } } }, - "got": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", - "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", - "dev": true, - "requires": { - "@sindresorhus/is": "^0.14.0", - "@szmarczak/http-timer": "^1.1.2", - "cacheable-request": "^6.0.0", - "decompress-response": "^3.3.0", - "duplexer3": "^0.1.4", - "get-stream": "^4.1.0", - "lowercase-keys": "^1.0.1", - "mimic-response": "^1.0.1", - "p-cancelable": "^1.0.0", - "to-readable-stream": "^1.0.0", - "url-parse-lax": "^3.0.0" - }, - "dependencies": { - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - } - } - }, "graceful-fs": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", @@ -12929,12 +13077,6 @@ } } }, - "has-yarn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz", - "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==", - "dev": true - }, "home-or-tmp": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", @@ -12975,12 +13117,6 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, - "http-cache-semantics": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", - "dev": true - }, "http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -13035,7 +13171,7 @@ "ignore-by-default": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", "dev": true }, "import-fresh": { @@ -13056,12 +13192,6 @@ } } }, - "import-lazy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", - "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=", - "dev": true - }, "import-local": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", @@ -13230,23 +13360,6 @@ "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", "dev": true }, - "is-ci": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", - "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", - "dev": true, - "requires": { - "ci-info": "^2.0.0" - }, - "dependencies": { - "ci-info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", - "dev": true - } - } - }, "is-core-module": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", @@ -13364,51 +13477,12 @@ "is-extglob": "^2.1.1" } }, - "is-installed-globally": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", - "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", - "dev": true, - "requires": { - "global-dirs": "^3.0.0", - "is-path-inside": "^3.0.2" - }, - "dependencies": { - "global-dirs": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.0.tgz", - "integrity": "sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA==", - "dev": true, - "requires": { - "ini": "2.0.0" - } - }, - "ini": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", - "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", - "dev": true - }, - "is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true - } - } - }, "is-negative-zero": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", "dev": true }, - "is-npm": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-5.0.0.tgz", - "integrity": "sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA==", - "dev": true - }, "is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -13549,12 +13623,6 @@ "text-extensions": "^1.0.0" } }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true - }, "is-unc-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", @@ -13579,12 +13647,6 @@ "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", "dev": true }, - "is-yarn-global": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", - "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==", - "dev": true - }, "is_js": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/is_js/-/is_js-0.9.0.tgz", @@ -13725,6 +13787,41 @@ "istanbul-lib-report": "^3.0.0" } }, + "jake": { + "version": "10.8.5", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.5.tgz", + "integrity": "sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==", + "requires": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.1", + "minimatch": "^3.0.4" + }, + "dependencies": { + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "jest": { "version": "28.1.0", "resolved": "https://registry.npmjs.org/jest/-/jest-28.1.0.tgz", @@ -14980,12 +15077,6 @@ "bignumber.js": "^9.0.0" } }, - "json-buffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", - "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=", - "dev": true - }, "json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -15071,15 +15162,6 @@ "safe-buffer": "^5.0.1" } }, - "keyv": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", - "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", - "dev": true, - "requires": { - "json-buffer": "3.0.0" - } - }, "kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -15107,15 +15189,6 @@ "language-subtag-registry": "~0.3.2" } }, - "latest-version": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz", - "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==", - "dev": true, - "requires": { - "package-json": "^6.3.0" - } - }, "leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -15357,6 +15430,11 @@ "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=" }, + "lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==" + }, "lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -15403,12 +15481,6 @@ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" }, - "lodash.sortby": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", - "dev": true - }, "lodash.zipobject": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/lodash.zipobject/-/lodash.zipobject-4.1.3.tgz", @@ -15459,17 +15531,10 @@ "js-tokens": "^3.0.0 || ^4.0.0" } }, - "lowercase-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", - "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", - "dev": true - }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "requires": { "yallist": "^4.0.0" } @@ -15763,12 +15828,6 @@ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true }, - "mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", - "dev": true - }, "min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -15831,9 +15890,9 @@ } }, "moment": { - "version": "2.29.3", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz", - "integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==" + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==" }, "moment-timezone": { "version": "0.5.34", @@ -15900,6 +15959,29 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "msgpackr": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.6.2.tgz", + "integrity": "sha512-bqSQ0DYJbXbrJcrZFmMygUZmqQiDfI2ewFVWcrZY12w5XHWtPuW4WppDT/e63Uu311ajwkRRXSoF0uILroBeTA==", + "requires": { + "msgpackr-extract": "^2.0.2" + } + }, + "msgpackr-extract": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-2.1.2.tgz", + "integrity": "sha512-cmrmERQFb19NX2JABOGtrKdHMyI6RUyceaPBQ2iRz9GnDkjBWFjNJC0jyyoOfZl2U/LZE3tQCCQc4dlRyA8mcA==", + "optional": true, + "requires": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "2.1.2", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "2.1.2", + "@msgpackr-extract/msgpackr-extract-linux-arm": "2.1.2", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "2.1.2", + "@msgpackr-extract/msgpackr-extract-linux-x64": "2.1.2", + "@msgpackr-extract/msgpackr-extract-win32-x64": "2.1.2", + "node-gyp-build-optional-packages": "5.0.3" + } + }, "mv": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", @@ -15984,6 +16066,12 @@ "whatwg-url": "^5.0.0" } }, + "node-gyp-build-optional-packages": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.3.tgz", + "integrity": "sha512-k75jcVzk5wnnc/FMxsf4udAoTEUv2jY3ycfdSd3yWu6Cnd1oee6/CfZJApyscA4FJOmdoixWwiwOyf16RzD5JA==", + "optional": true + }, "node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -15997,9 +16085,9 @@ "dev": true }, "nodemon": { - "version": "2.0.16", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.16.tgz", - "integrity": "sha512-zsrcaOfTWRuUzBn3P44RDliLlp263Z/76FPoHFr3cFFkOz0lTPAcIw8dCzfdVIx/t3AtDYCZRCDkoCojJqaG3w==", + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.19.tgz", + "integrity": "sha512-4pv1f2bMDj0Eeg/MhGqxrtveeQ5/G/UVe9iO6uTZzjnRluSA4PVWf8CW99LUPwGB3eNIA7zUFoP77YuI7hOc0A==", "dev": true, "requires": { "chokidar": "^3.5.2", @@ -16008,10 +16096,10 @@ "minimatch": "^3.0.4", "pstree.remy": "^1.1.8", "semver": "^5.7.1", + "simple-update-notifier": "^1.0.7", "supports-color": "^5.5.0", "touch": "^3.1.0", - "undefsafe": "^2.0.5", - "update-notifier": "^5.1.0" + "undefsafe": "^2.0.5" }, "dependencies": { "debug": { @@ -16063,12 +16151,6 @@ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true }, - "normalize-url": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz", - "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==", - "dev": true - }, "npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", @@ -16362,11 +16444,10 @@ "object-assign": "^4.1.0" } }, - "p-cancelable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", - "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==", - "dev": true + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==" }, "p-limit": { "version": "2.3.0", @@ -16391,32 +16472,20 @@ "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==" }, + "p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "requires": { + "p-finally": "^1.0.0" + } + }, "p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true }, - "package-json": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz", - "integrity": "sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==", - "dev": true, - "requires": { - "got": "^9.6.0", - "registry-auth-token": "^4.0.0", - "registry-url": "^5.0.0", - "semver": "^6.2.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -16616,12 +16685,6 @@ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true }, - "prepend-http": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", - "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=", - "dev": true - }, "preserve": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", @@ -16731,30 +16794,11 @@ "resolved": "https://registry.npmjs.org/pubsub-js/-/pubsub-js-1.9.4.tgz", "integrity": "sha512-hJYpaDvPH4w8ZX/0Fdf9ma1AwRgU353GfbaVfPjfJQf1KxZ2iHaHl3fAUw1qlJIR5dr4F3RzjGaWohYUEyoh7A==" }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" }, - "pupa": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz", - "integrity": "sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==", - "dev": true, - "requires": { - "escape-goat": "^2.0.0" - } - }, "q": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", @@ -16839,26 +16883,6 @@ } } }, - "rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true - } - } - }, "react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -16966,11 +16990,24 @@ "strip-indent": "^3.0.0" } }, + "redis-commands": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.7.0.tgz", + "integrity": "sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ==" + }, "redis-errors": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", "integrity": "sha1-62LSrbFeTq9GEMBK/hUpOEJQq60=" }, + "redis-info": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redis-info/-/redis-info-3.1.0.tgz", + "integrity": "sha512-ER4L9Sh/vm63DkIE0bkSjxluQlioBiBgf5w1UuldaW/3vPcecdljVDisZhmnCMvsxHNiARTTDDHGg9cGwTfrKg==", + "requires": { + "lodash": "^4.17.11" + } + }, "redis-parser": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", @@ -17069,24 +17106,6 @@ "unicode-match-property-value-ecmascript": "^2.0.0" } }, - "registry-auth-token": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.1.tgz", - "integrity": "sha512-6gkSb4U6aWJB4SF2ZvLb76yCBjcvufXBqvvEx1HbmKPkutswjW1xNVRY0+daljIYRbogN7O0etYSlbiaEQyMyw==", - "dev": true, - "requires": { - "rc": "^1.2.8" - } - }, - "registry-url": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz", - "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==", - "dev": true, - "requires": { - "rc": "^1.2.8" - } - }, "regjsgen": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.6.0.tgz", @@ -17217,15 +17236,6 @@ "integrity": "sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ==", "dev": true }, - "responselike": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", - "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", - "dev": true, - "requires": { - "lowercase-keys": "^1.0.0" - } - }, "restore-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", @@ -17341,23 +17351,6 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" }, - "semver-diff": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz", - "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==", - "dev": true, - "requires": { - "semver": "^6.3.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, "send": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", @@ -17488,6 +17481,23 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "simple-update-notifier": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.0.7.tgz", + "integrity": "sha512-BBKgR84BJQJm6WjWFMHgLVuo61FBDSj1z/xSFUIozqO6wO7ii0JxCqlIud7Enr/+LhlbNI0whErq96P2qHNWew==", + "dev": true, + "requires": { + "semver": "~7.0.0" + }, + "dependencies": { + "semver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", + "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", + "dev": true + } + } + }, "sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -18002,25 +18012,22 @@ } }, "terser": { - "version": "5.13.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.13.1.tgz", - "integrity": "sha512-hn4WKOfwnwbYfe48NgrQjqNOH9jzLqRcIfbYytOXCOv46LBfWr9bDS17MQqOi+BWGD0sJK3Sj5NC/gJjiojaoA==", + "version": "5.14.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz", + "integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==", "dev": true, "requires": { + "@jridgewell/source-map": "^0.3.2", "acorn": "^8.5.0", "commander": "^2.20.0", - "source-map": "~0.8.0-beta.0", "source-map-support": "~0.5.20" }, "dependencies": { "source-map": { - "version": "0.8.0-beta.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", - "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", - "dev": true, - "requires": { - "whatwg-url": "^7.0.0" - } + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true }, "source-map-support": { "version": "0.5.21", @@ -18030,40 +18037,6 @@ "requires": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "tr46": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", - "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", - "dev": true - }, - "whatwg-url": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", - "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", - "dev": true, - "requires": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" } } } @@ -18220,12 +18193,6 @@ } } }, - "to-readable-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", - "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==", - "dev": true - }, "to-regex": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", @@ -18265,7 +18232,7 @@ "nopt": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", - "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", + "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", "dev": true, "requires": { "abbrev": "1" @@ -18379,15 +18346,6 @@ "mime-types": "~2.1.24" } }, - "typedarray-to-buffer": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", - "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "dev": true, - "requires": { - "is-typedarray": "^1.0.0" - } - }, "typescript": { "version": "4.7.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.2.tgz", @@ -18469,15 +18427,6 @@ "set-value": "^2.0.1" } }, - "unique-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", - "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", - "dev": true, - "requires": { - "crypto-random-string": "^2.0.0" - } - }, "universalify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", @@ -18540,64 +18489,6 @@ } } }, - "update-notifier": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-5.1.0.tgz", - "integrity": "sha512-ItnICHbeMh9GqUy31hFPrD1kcuZ3rpxDZbf4KUDavXwS0bW5m7SLbDQpGX3UYr072cbrF5hFUs3r5tUsPwjfHw==", - "dev": true, - "requires": { - "boxen": "^5.0.0", - "chalk": "^4.1.0", - "configstore": "^5.0.1", - "has-yarn": "^2.1.0", - "import-lazy": "^2.1.0", - "is-ci": "^2.0.0", - "is-installed-globally": "^0.4.0", - "is-npm": "^5.0.0", - "is-yarn-global": "^0.3.0", - "latest-version": "^5.1.0", - "pupa": "^2.1.1", - "semver": "^7.3.4", - "semver-diff": "^3.1.1", - "xdg-basedir": "^4.0.0" - }, - "dependencies": { - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, "uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -18614,15 +18505,6 @@ "dev": true, "optional": true }, - "url-parse-lax": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", - "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", - "dev": true, - "requires": { - "prepend-http": "^2.0.0" - } - }, "use": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", @@ -18869,15 +18751,6 @@ "is-symbol": "^1.0.3" } }, - "widest-line": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", - "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", - "dev": true, - "requires": { - "string-width": "^4.0.0" - } - }, "wildcard": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", @@ -18920,12 +18793,6 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.7.0.tgz", "integrity": "sha512-c2gsP0PRwcLFzUiA8Mkr37/MI7ilIlHQxaEAtd0uNMbVMoy8puJyafRlm0bV9MbGSabUPeLrRRaqIBcFcA2Pqg==" }, - "xdg-basedir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", - "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", - "dev": true - }, "y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -18934,8 +18801,7 @@ "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "yaml": { "version": "1.10.2", diff --git a/package.json b/package.json index f1b6e415..e2276b5b 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "scripts": { "prepare": "husky install", "start": "node dist/server.js", - "dev": "cross-env NODE_ENV=development nodemon app/server.js", + "dev": "cross-env NODE_ENV=development nodemon --trace-warnings app/server.js", "build:webpack": "cross-env NODE_ENV=production webpack --config webpack.config.prod.js --progress --profile", "build:grunt": "grunt", "lint": "eslint ./app --fix", @@ -37,9 +37,12 @@ }, "homepage": "https://github.com/chrisleekr/binance-traiding-bot#readme", "dependencies": { + "@bull-board/api": "^4.2.2", + "@bull-board/express": "^4.2.2", "axios": "^0.27.2", "bcryptjs": "^2.4.3", "binance-api-node": "^0.11.39", + "bull": "^4.8.5", "bunyan": "^1.8.15", "clean-webpack-plugin": "^4.0.0", "compression": "^1.7.4", @@ -55,7 +58,7 @@ "lodash": "^4.17.21", "lodash-webpack-plugin": "^0.11.6", "migrate": "^1.8.0", - "moment": "^2.29.3", + "moment": "^2.29.4", "moment-timezone": "^0.5.34", "mongodb": "4.1.0", "pubsub-js": "^1.9.4", @@ -72,6 +75,7 @@ "@babel/preset-env": "^7.18.2", "@commitlint/cli": "^17.0.1", "@commitlint/config-conventional": "^17.0.0", + "@types/bull": "^3.15.9", "@types/express": "^4.17.13", "@types/jest": "^27.5.1", "@types/node": "^17.0.36", @@ -103,7 +107,7 @@ "grunt-contrib-cssmin": "^4.0.0", "jest": "^28.1.0", "lint-staged": "^12.4.2", - "nodemon": "^2.0.16", + "nodemon": "^2.0.19", "prettier": "^2.6.2", "webpack": "^5.72.1", "webpack-cli": "^4.9.2" diff --git a/public/js/CoinWrapperAction.js b/public/js/CoinWrapperAction.js index de650bc6..71824483 100644 --- a/public/js/CoinWrapperAction.js +++ b/public/js/CoinWrapperAction.js @@ -85,9 +85,9 @@ class CoinWrapperAction extends React.Component {
Action -{' '} - + {updatedAt.format('HH:mm:ss')} - + {isLocked === true ? : ''} {isActionDisabled.isDisabled === true ? (