diff --git a/README.md b/README.md index 6e6e3a3..2571638 100644 --- a/README.md +++ b/README.md @@ -145,35 +145,22 @@ Signature is not required for this part ## Websocket Datafeed -DEMO: -``` -// ws demo -const datafeed = new API.websocket.Datafeed(); - -// close callback -datafeed.onClose(() => { - console.log('ws closed, status ', datafeed.trustConnected); -}); - -// connect -datafeed.connectSocket(); - -// subscribe -const topic = `/market/ticker:BTC-USDT`; -const callbackId = datafeed.subscribe(topic, (message) => { - if (message.topic === topic) { - console.log(message.data); - } -}); - -console.log(`subscribe id: ${callbackId}`); -setTimeout(() => { - // unsubscribe - datafeed.unsubscribe(topic, callbackId); - console.log(`unsubscribed: ${topic} ${callbackId}`); -}, 5000); +### API.websocket.Datafeed -``` +Manage websocket connect/private/subscribe/unsubscribe and get realtime datafeed. + +DEMO: [demo/ticker_demo.js](demo/ticker_demo.js) + +### API.websocket.Level2 + +Get realtime orderbook in level2 datafeed. + +DEMO: [demo/level2_demo.js](demo/level2_demo.js) + + +### API.websocket.Level3 + +// TODO ## LICENSE diff --git a/demo/index.js b/demo/index.js index ada5055..1c5e916 100644 --- a/demo/index.js +++ b/demo/index.js @@ -10,31 +10,5 @@ const main = async () => { console.log(getStatusRl); }; -// run demo +// run rest main main(); - -// ws demo -const datafeed = new API.websocket.Datafeed(); - -// close callback -datafeed.onClose(() => { - console.log('ws closed, status ', datafeed.trustConnected); -}); - -// connect -datafeed.connectSocket(); - -// subscribe -const topic = `/market/ticker:BTC-USDT`; -const callbackId = datafeed.subscribe(topic, (message) => { - if (message.topic === topic) { - console.log(message.data); - } -}); - -console.log(`subscribe id: ${callbackId}`); -setTimeout(() => { - // unsubscribe - datafeed.unsubscribe(topic, callbackId); - console.log(`unsubscribed: ${topic} ${callbackId}`); -}, 5000); diff --git a/demo/level2_demo.js b/demo/level2_demo.js new file mode 100644 index 0000000..a4672f5 --- /dev/null +++ b/demo/level2_demo.js @@ -0,0 +1,70 @@ +const _ = require('lodash'); +const logUpdate = require('log-update'); +const API = require('../src'); + +const config = require('./config'); +API.init({ + ...config, + baseUrl: 'https://api.kucoin.io', +}); + +// ws demo +const datafeed = new API.websocket.Datafeed(); + +/* +// close callback +datafeed.onClose(() => { + console.log('ws closed, status ', datafeed.trustConnected); +}); + +// connect +datafeed.connectSocket(); + +// subscribe +const topic = `/market/level2:BTC-USDT`; +const callbackId = datafeed.subscribe(topic, (message) => { + if (message.topic === topic) { + console.log(JSON.stringify(message.data)); + } +}); + +console.log(`subscribe id: ${callbackId}`); +setTimeout(() => { + // unsubscribe + datafeed.unsubscribe(topic, callbackId); + console.log(`unsubscribed: ${topic} ${callbackId}`); +}, 5000); +*/ + +const { Level2 } = API.websocket; + +Level2.setLogger(() => {}); + +const l2 = new Level2('BTC-USDT', datafeed); +l2.listen(); + +const interval = setInterval(async () => { + // read orderbook + const orderbook = l2.getOrderBook(5); + + // show Level2 + let asksStr = ''; + _.eachRight(orderbook.asks, ([price, size]) => { + asksStr += `${price} -> ${size}\n`; + }); + + let bidsStr = ''; + _.each(orderbook.bids, ([price, size]) => { + bidsStr += `${price} -> ${size}\n`; + }); + + logUpdate.clear(); + logUpdate(`------------------------\n` + + `l2 ${orderbook.dirty ? 'Dirty Data' : 'Trust Data'}\n` + + `l2 seq: ${orderbook.sequence}\n` + + `ping: ${orderbook.ping} (ms)\n` + + `------------------------\n` + + `${asksStr}----------sep-----------\n` + + `${bidsStr}------------------------` + ); +}, 200); diff --git a/demo/ticker_demo.js b/demo/ticker_demo.js new file mode 100644 index 0000000..65ade8d --- /dev/null +++ b/demo/ticker_demo.js @@ -0,0 +1,29 @@ +const API = require('../src'); + +API.init(require('./config')); + +// ws demo +const datafeed = new API.websocket.Datafeed(); + +// close callback +datafeed.onClose(() => { + console.log('ws closed, status ', datafeed.trustConnected); +}); + +// connect +datafeed.connectSocket(); + +// subscribe +const topic = `/market/ticker:BTC-USDT`; +const callbackId = datafeed.subscribe(topic, (message) => { + if (message.topic === topic) { + console.log(message.data); + } +}); + +console.log(`subscribe id: ${callbackId}`); +setTimeout(() => { + // unsubscribe + datafeed.unsubscribe(topic, callbackId); + console.log(`unsubscribed: ${topic} ${callbackId}`); +}, 5000); diff --git a/package.json b/package.json index b4a2c56..24dfab8 100755 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "dependencies": { "event-emitter": "^0.3.5", "lodash": "^4.17.15", + "log-update": "^4.0.0", "md5": "^2.2.1", "node-fetch": "^2.6.0", "node-schedule": "^1.3.2", diff --git a/src/datafeed/Level2.js b/src/datafeed/Level2.js new file mode 100644 index 0000000..6c65921 --- /dev/null +++ b/src/datafeed/Level2.js @@ -0,0 +1,248 @@ +const _ = require('lodash'); +const Datafeed = require('../lib/datafeed'); +const Http = require('../lib/http'); +const delay = require('../lib/delay'); +const { + checkL2BufferContinue, + mapArr, + arrMap, +} = require('../lib/utils'); +const OrderBook = require('../rest/Market/OrderBook'); + +let log = (...args) => { console.log(...args); }; +const changeTypes = ['asks', 'bids']; + +class Level2 { + constructor(symbol, datafeed) { + this.buffer = []; + this.fullSnapshot = { + dirty: true, + sequence: 0, + asks: {}, + bids: {}, + }; + this.messageEventCallback = null; + + this.symbol = symbol; + if (datafeed instanceof Datafeed) { + this.datafeed = datafeed; + } else { + this.datafeed = new Datafeed(); + } + + /** private */ + this._rebuilding = false; + + /** bind functions */ + this.bufferMessage = this.bufferMessage.bind(this); + this.getFilteredBuffer = this.getFilteredBuffer.bind(this); + this.rebuild = this.rebuild.bind(this); + this.fetch = this.fetch.bind(this); + this.updateFullByMessage = this.updateFullByMessage.bind(this); + this.listen = this.listen.bind(this); + this.handleMessageEvent = this.handleMessageEvent.bind(this); + this.getOrderBook = this.getOrderBook.bind(this); + } + + /** + * @name setLogger + * @description set level2 mod log + * @param {function} fn + */ + static setLogger(fn) { + if (typeof fn === 'function') { + log = fn; + } + } + + bufferMessage(message) { + const { sequenceStart, sequenceEnd, changes } = message || {}; + if (sequenceStart && sequenceEnd && changes) { + const lastSeq = this.fullSnapshot.sequence; + // log('check', sequenceStart, lastSeq); + if (this.fullSnapshot.dirty === false && sequenceStart === lastSeq + 1) { + // update + this.updateFullByMessage(message); + } else { + this.buffer.push(message); + // rebuild + this.rebuild(); + } + } + } + + getFilteredBuffer(lastSeq) { + const seq = lastSeq + 1; + const buffer = this.buffer.filter((message) => { + const { sequenceStart, sequenceEnd, changes } = message || {}; + return (sequenceStart <= seq && sequenceEnd >= seq) || sequenceStart >= seq; + }); + // console.log(buffer); + + // check changes seq + if(buffer[0]) { + const { sequenceStart, sequenceEnd, symbol, changes } = buffer[0]; + const { asks, bids } = changes; + + const fAsks = asks.filter(item => +item[2] == seq); + const fBids = bids.filter(item => +item[2] == seq); + if (!fAsks[0] && !fBids[0]) { + return []; + } + + buffer[0] = { + sequenceStart: seq, + sequenceEnd, + symbol, + changes: { + asks: asks.filter(item => +item[2] >= seq), + bids: bids.filter(item => +item[2] >= seq), + }, + } + } + + return buffer; + } + + async rebuild() { + if (this._rebuilding) { + log('rebuilding dirty level2, return', + this.fullSnapshot.sequence, + this.buffer.length && this.buffer[this.buffer.length - 1].sequenceEnd, + ); + return; + } + log('build dirty level2'); + this._rebuilding = true; + this.fullSnapshot.dirty = true; + + await delay(5000); + const fetchSuccess = await this.fetch(); + const lastSeq = this.fullSnapshot.sequence; + + if (fetchSuccess && this.datafeed.trustConnected) { + const bufferArr = this.getFilteredBuffer(lastSeq); + // if (bufferArr.length === 0 && this.buffer.length > 0) { + // console.log('snapshot before', lastSeq, this.buffer[this.buffer.length - 1].sequenceEnd); + // } + + if (bufferArr.length > 0 || + (bufferArr.length === 0 && this.buffer.length === 0) || + (bufferArr.length === 0 && (lastSeq === this.buffer[this.buffer.length - 1].sequenceEnd)) + ) { + const continu = checkL2BufferContinue(bufferArr, lastSeq); + if (continu) { + log('lastSeq & len', this.fullSnapshot.sequence, bufferArr.length, this.buffer.length); + _.each(bufferArr, (item) => { + // update + this.updateFullByMessage(item); + }); + this.fullSnapshot.dirty = false; + this.buffer = []; + log('level2 checked'); + } else { + log('level2 buffer is not continue with snapshot'); + } + } + } + this._rebuilding = false; + } + + async fetch() { + /* + { + code: '200000', + data: { + sequence: 75017803, + asks: [], + bids: [], + } + } + */ + let fetchSuccess = false; + try { + const result = await OrderBook.getLevel2_full(this.symbol); + // console.log(result); + if (result.code === '200000' && result.data) { + const { sequence, asks, bids } = result.data; + + this.fullSnapshot.dirty = true; + this.fullSnapshot.sequence = +sequence; + this.fullSnapshot.asks = mapArr(asks); + this.fullSnapshot.bids = mapArr(bids); + fetchSuccess = true; + } + } catch (e) { + log('fetch level2 error', e); + } + return fetchSuccess; + } + + updateFullByMessage(message) { + const { sequenceStart, sequenceEnd, changes } = message || {}; + // const [sequence, price, type, size] = message; + + _.each(changes, (arr, targetType) => { + if (_.indexOf(changeTypes, targetType) > -1) { + _.each(arr, (item) => { + const [price, size, sequence] = item; + if (size == 0) { + delete this.fullSnapshot[targetType][price]; + } else { + this.fullSnapshot[targetType][price] = size; + } + this.fullSnapshot.sequence = +sequence; + }); + } + }); + this.fullSnapshot.sequence = +sequenceEnd; + + // callback message + if (typeof this.messageEventCallback === 'function') { + this.messageEventCallback(message); + } + } + + /** public */ + listen() { + this.datafeed.connectSocket(); + this.datafeed.onClose(() => { + log('ws closed, status ', this.datafeed.trustConnected); + this.rebuild(); + }); + + const topic = `/market/level2:${this.symbol}`; + this.datafeed.subscribe(topic, (message) => { + if (message.topic === topic) { + // log(message.data); + this.bufferMessage(message.data); + } + }); + this.rebuild(); + } + + // message event handler + handleMessageEvent(callback) { + if (typeof callback === 'function') { + this.messageEventCallback = callback; + } + } + + getOrderBook(limit = 10) { + const dirty = this.fullSnapshot.dirty; + const sequence = this.fullSnapshot.sequence; + const asks = arrMap(this.fullSnapshot.asks, 'asc').slice(0, limit); + const bids = arrMap(this.fullSnapshot.bids, 'desc').slice(0, limit); + const ping = this.datafeed.ping; + + return { + dirty, + sequence, + asks, + bids, + ping, + }; + } +} + +module.exports = Level2; diff --git a/src/index.js b/src/index.js index 2458e38..58a8e4d 100644 --- a/src/index.js +++ b/src/index.js @@ -47,4 +47,5 @@ exports.rest = { /** Exports Datafeed */ exports.websocket = { Datafeed: require('./lib/datafeed'), + Level2: require('./datafeed/Level2'), }; diff --git a/src/lib/delay.js b/src/lib/delay.js new file mode 100644 index 0000000..25e162b --- /dev/null +++ b/src/lib/delay.js @@ -0,0 +1,11 @@ +const _ = require('lodash'); + +const delay = (ms = 1000) => { + return new Promise((resolve) => { + _.delay(() => { + resolve(); + }, ms); + }); +}; + +module.exports = delay; diff --git a/src/lib/utils.js b/src/lib/utils.js index 8facddd..9905b17 100755 --- a/src/lib/utils.js +++ b/src/lib/utils.js @@ -1,3 +1,4 @@ +const _ = require('lodash'); const CryptoJS = require('crypto'); const path = require('path'); const fs = require('fs'); @@ -10,9 +11,7 @@ function sign(text, secret, outputType = 'base64') { .digest(outputType); } -exports.sign = sign; - -exports.auth = function auth(ApiKey, method, url, data) { +function auth(ApiKey, method, url, data) { const timestamp = Date.now(); const signature = sign(timestamp + method.toUpperCase() + url + data, ApiKey.secret); @@ -25,8 +24,7 @@ exports.auth = function auth(ApiKey, method, url, data) { }; } - -exports.readFile = async function readFile(filePath) { +async function readFile(filePath) { const fileAbsolutePath = path.join(__dirname, filePath); // console.log(fileAbsolutePath); const readStream = fs.createReadStream(fileAbsolutePath); @@ -44,7 +42,7 @@ exports.readFile = async function readFile(filePath) { }); } -exports.writeFile = async function writeFile(filePath, content) { +async function writeFile(filePath, content) { const fileAbsolutePath = path.join(__dirname, filePath); const writeStream = fs.createWriteStream(fileAbsolutePath); @@ -58,15 +56,13 @@ exports.writeFile = async function writeFile(filePath, content) { resolve(err.message); }); }); - } - -exports.genClientOid = function genClientOid() { +function genClientOid() { return uuid.v4(); } -exports.strTo2String = function strTo2String(str) { +function strTo2String(str) { var result = []; var list = str.split(""); for (var i = 0; i < list.length; i++) { @@ -76,3 +72,92 @@ exports.strTo2String = function strTo2String(str) { } return result.join(""); } + +function checkL2BufferContinue(arrBuffer = [], lastSeq) { + if (arrBuffer.length) { + if (arrBuffer[0].sequenceStart !== lastSeq +1) { + return false; + } + + for (let i = 0; i < arrBuffer.length; i++) { + if (arrBuffer[i + 1] && arrBuffer[i + 1].sequenceStart !== arrBuffer[i].sequenceEnd + 1) { + return false; + } + } + } + return true; +} + +// ----> for level2 +function mapArr(arr = [], parseKey = (str) => str) { + const res = {}; + for (let i = 0; i< arr.length; i++) { + const item = arr[i]; + res[parseKey(item[0])] = item[1]; + } + return res; +} + +// ----> for level2 +function arrMap(map = {}, order = 'asc') { + const res = []; + _.each(map, (value, key) => { + res.push([key, value]); + }); + res.sort((a, b) => { + if (order === 'desc') { + return (+b[0]) - (+a[0]); + } else { + return (a[0]) - (b[0]); + } + }); + return res; +} + +// ----> for level3 +function mapl3Arr(arr = []) { + const res = {}; + for (let i = 0; i< arr.length; i++) { + const item = arr[i]; + res[item[1]] = item; // orderId + } + return res; +} + +// ----> for level3 +function arrl3Map(map = {}, side, order = 'asc', merge = 1) { + const res = []; + _.each(map, (item) => { + // [下单时间, 订单号, 价格, 数量, 进入买卖盘时间] + const [orderTime, orderId, price, size, ts] = item; + res.push([price, size, ts, orderId]); + }); + res.sort((a, b) => { + if (a[0] === b[0]) { + // 价格相同的订单以进入买卖盘的时间从低到高排序 + return a[2] - b[2]; + } else { + // 价格排序 + if (order === 'desc') { + return b[0] - a[0]; + } else { + return a[0] - b[0]; + } + } + }); + return res; +} + +module.exports = { + sign, + auth, + readFile, + writeFile, + genClientOid, + strTo2String, + checkL2BufferContinue, + mapArr, + arrMap, + mapl3Arr, + arrl3Map, +}; diff --git a/yarn.lock b/yarn.lock index 87d046f..3f22c19 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,11 @@ # yarn lockfile v1 +"@types/color-name@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" + integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== + abbrev@1: version "1.1.1" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" @@ -14,11 +19,23 @@ ansi-align@^2.0.0: dependencies: string-width "^2.0.0" +ansi-escapes@^4.3.0: + version "4.3.1" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.1.tgz#a5c47cc43181f1f38ffd7076837700d395522a61" + integrity sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA== + dependencies: + type-fest "^0.11.0" + ansi-regex@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= +ansi-regex@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" + integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== + ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" @@ -26,6 +43,14 @@ ansi-styles@^3.2.1: dependencies: color-convert "^1.9.0" +ansi-styles@^4.0.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" + integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA== + dependencies: + "@types/color-name" "^1.1.1" + color-convert "^2.0.1" + anymatch@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" @@ -59,6 +84,11 @@ assign-symbols@^1.0.0: resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= +astral-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" + integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== + async-each@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" @@ -214,6 +244,13 @@ cli-boxes@^1.0.0: resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143" integrity sha1-T6kXw+WclKAEzWH47lCdplFocUM= +cli-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" + integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== + dependencies: + restore-cursor "^3.1.0" + collection-visit@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" @@ -229,11 +266,23 @@ color-convert@^1.9.0: dependencies: color-name "1.1.3" +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + color-name@1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + component-emitter@^1.2.1: version "1.3.0" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" @@ -389,6 +438,11 @@ duplexer3@^0.1.4: resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI= +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + es5-ext@^0.10.35, es5-ext@^0.10.50, es5-ext@~0.10.14: version "0.10.53" resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.53.tgz#93c5a3acfdbef275220ad72644ad02ee18368de1" @@ -720,6 +774,11 @@ is-fullwidth-code-point@^2.0.0: resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + is-glob@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" @@ -858,6 +917,16 @@ lodash@^4.17.15: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== +log-update@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1" + integrity sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg== + dependencies: + ansi-escapes "^4.3.0" + cli-cursor "^3.1.0" + slice-ansi "^4.0.0" + wrap-ansi "^6.2.0" + long-timeout@0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/long-timeout/-/long-timeout-0.1.1.tgz#9721d788b47e0bcb5a24c2e2bee1a0da55dab514" @@ -923,6 +992,11 @@ micromatch@^3.1.10, micromatch@^3.1.4: snapdragon "^0.8.1" to-regex "^3.0.2" +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" @@ -1076,6 +1150,13 @@ object.pick@^1.3.0: dependencies: isobject "^3.0.1" +onetime@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.0.tgz#fff0f3c91617fe62bb50189636e99ac8a6df7be5" + integrity sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q== + dependencies: + mimic-fn "^2.1.0" + p-finally@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" @@ -1226,6 +1307,14 @@ resolve-url@^0.2.1: resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= +restore-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" + integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + ret@~0.1.10: version "0.1.15" resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" @@ -1299,6 +1388,15 @@ signal-exit@^3.0.0, signal-exit@^3.0.2: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== +slice-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" + integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + snapdragon-node@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" @@ -1378,6 +1476,15 @@ string-width@^2.0.0, string-width@^2.1.1: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" +string-width@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" + integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.0" + string_decoder@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" @@ -1392,6 +1499,13 @@ strip-ansi@^4.0.0: dependencies: ansi-regex "^3.0.0" +strip-ansi@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" + integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== + dependencies: + ansi-regex "^5.0.0" + strip-eof@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" @@ -1453,6 +1567,11 @@ touch@^3.1.0: dependencies: nopt "~1.0.10" +type-fest@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1" + integrity sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ== + type@^1.0.1: version "1.2.0" resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0" @@ -1569,6 +1688,15 @@ widest-line@^2.0.0: dependencies: string-width "^2.1.1" +wrap-ansi@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + write-file-atomic@^2.0.0: version "2.4.3" resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.4.3.tgz#1fd2e9ae1df3e75b8d8c367443c692d4ca81f481"