├── .prettierrc.js ├── .editorconfig ├── helpers ├── constants.js ├── redis.js ├── utils.js └── expoNotifications.js ├── routes ├── index.js └── notifications.js ├── middlewares ├── expoToken.js └── steemConnectAuth.js ├── package.json ├── README.md ├── .gitignore ├── server.js └── yarn.lock /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | trailingComma: 'all', 4 | printWidth: 100, 5 | }; 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # rules for all the files 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | indent_style = space 11 | indent_size = 2 12 | -------------------------------------------------------------------------------- /helpers/constants.js: -------------------------------------------------------------------------------- 1 | const notificationTypes = { 2 | FOLLOW: 'follow', 3 | REPLY: 'reply', 4 | TRANSFER: 'transfer', 5 | VOTE: 'vote', 6 | REBLOG: 'reblog', 7 | MENTION: 'mention', 8 | }; 9 | 10 | module.exports = { 11 | notificationTypes, 12 | }; 13 | -------------------------------------------------------------------------------- /helpers/redis.js: -------------------------------------------------------------------------------- 1 | const redis = require('redis'); 2 | const bluebird = require('bluebird'); 3 | 4 | bluebird.promisifyAll(redis.RedisClient.prototype); 5 | bluebird.promisifyAll(redis.Multi.prototype); 6 | const client = redis.createClient(process.env.REDISCLOUD_URL); 7 | 8 | module.exports = client; 9 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const authMiddleware = require('../middlewares/steemConnectAuth'); 4 | const notifications = require('./notifications'); 5 | 6 | const router = express.Router(); 7 | 8 | router.use('/notifications', authMiddleware, notifications); 9 | 10 | module.exports = router; 11 | -------------------------------------------------------------------------------- /middlewares/expoToken.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const Expo = require('expo-server-sdk'); 3 | 4 | function validTokenMiddleware(req, res, next) { 5 | const expoToken = _.get(req, 'body.token'); 6 | 7 | if (!Expo.isExpoPushToken(expoToken)) { 8 | return res.status(400).send({ 9 | error: 'valid token is required', 10 | }); 11 | } 12 | 13 | req.expoToken = expoToken; 14 | next(); 15 | } 16 | 17 | module.exports = validTokenMiddleware; 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "busy-api", 3 | "version": "1.0.0", 4 | "license": "MIT", 5 | "engines": { 6 | "node": "8.11.3" 7 | }, 8 | "scripts": { 9 | "start": "node --expose-gc ./server.js" 10 | }, 11 | "dependencies": { 12 | "bluebird": "^3.5.1", 13 | "body-parser": "^1.18.3", 14 | "busyjs": "^1.0.2", 15 | "expo-server-sdk": "^2.4.0", 16 | "express": "^4.16.2", 17 | "lightrpc": "^1.0.0-beta.4", 18 | "lodash": "^4.17.5", 19 | "redis": "^2.8.0", 20 | "sc2-sdk": "^1.0.2", 21 | "ws": "^4.0.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BUSY API 2 | 3 | The API server for [Busy](https://busy.org/) - Blockchain-based social network where anyone can earn rewards :rocket:. 4 | 5 | ## Development 6 | 7 | [Yarn](https://yarnpkg.com/) package manager is used for this project. To install yarn, use 8 | 9 | ```shell 10 | $ npm i -g yarn 11 | ``` 12 | 13 | You may require `sudo`. 14 | 15 | **Let's start development** 16 | 17 | But before that, you would need to have a [redis](https://redis.io/) server up and running. 18 | 19 | ```shell 20 | $ yarn # Install dependencies 21 | $ yarn start # Start server 22 | ``` 23 | 24 | You should be able to access the server at http://localhost:4000. 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | package-lock.json 33 | 34 | # Optional npm cache directory 35 | .npm 36 | 37 | # Optional REPL history 38 | .node_repl_history 39 | 40 | # WebStorm 41 | .idea -------------------------------------------------------------------------------- /middlewares/steemConnectAuth.js: -------------------------------------------------------------------------------- 1 | const Bluebird = require('bluebird'); 2 | const sc2 = require('sc2-sdk'); 3 | 4 | function createTimeout(timeout, promise) { 5 | return new Promise((resolve, reject) => { 6 | setTimeout(() => { 7 | reject(new Error(`Request has timed out. It should take no longer than ${timeout}ms.`)); 8 | }, timeout); 9 | promise.then(resolve, reject); 10 | }); 11 | } 12 | 13 | async function authMiddleware(req, res, next) { 14 | const token = req.get('Authorization'); 15 | if (!token) { 16 | return res.sendStatus(401); 17 | } 18 | 19 | try { 20 | const api = sc2.Initialize({ 21 | app: 'busy.app', 22 | }); 23 | 24 | api.setAccessToken(token); 25 | 26 | const me = Bluebird.promisify(api.me, { context: api }); 27 | 28 | const user = await createTimeout(10000, me()); 29 | 30 | if (!user) { 31 | return res.sendStatus(401); 32 | } 33 | 34 | req.user = user; 35 | 36 | next(); 37 | } catch (err) { 38 | return res.sendStatus(401); 39 | } 40 | } 41 | 42 | module.exports = authMiddleware; 43 | -------------------------------------------------------------------------------- /helpers/utils.js: -------------------------------------------------------------------------------- 1 | const Client = require('lightrpc'); 2 | const bluebird = require('bluebird'); 3 | const client = new Client(process.env.STEEMJS_URL || 'https://api.steemit.com'); 4 | bluebird.promisifyAll(client); 5 | 6 | const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); 7 | 8 | const getBlock = blockNum => client.sendAsync({ method: 'get_block', params: [blockNum] }, null); 9 | 10 | const getOpsInBlock = (blockNum, onlyVirtual = false) => 11 | client.sendAsync({ method: 'get_ops_in_block', params: [blockNum, onlyVirtual] }, null); 12 | 13 | const getGlobalProps = () => 14 | client.sendAsync({ method: 'get_dynamic_global_properties', params: [] }, null); 15 | 16 | const mutliOpsInBlock = (start, limit, onlyVirtual = false) => { 17 | const request = []; 18 | for (let i = start; i < start + limit; i++) { 19 | request.push({ method: 'get_ops_in_block', params: [i, onlyVirtual] }); 20 | } 21 | return client.sendBatchAsync(request, { timeout: 20000 }); 22 | }; 23 | 24 | const getBlockOps = block => { 25 | const operations = []; 26 | block.transactions.forEach(transaction => { 27 | operations.push(...transaction.operations); 28 | }); 29 | return operations; 30 | }; 31 | 32 | module.exports = { 33 | sleep, 34 | getBlock, 35 | getOpsInBlock, 36 | getGlobalProps, 37 | mutliOpsInBlock, 38 | getBlockOps, 39 | }; 40 | -------------------------------------------------------------------------------- /routes/notifications.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const _ = require('lodash'); 3 | 4 | const redis = require('../helpers/redis'); 5 | const validTokenMiddleware = require('../middlewares/expoToken'); 6 | 7 | const router = express.Router(); 8 | 9 | router.get('/', async (req, res) => { 10 | redis 11 | .lrangeAsync(`notifications:${req.user.name}`, 0, -1) 12 | .then(results => { 13 | const notifications = results.map(notification => JSON.parse(notification)); 14 | res.send(notifications); 15 | }) 16 | .catch(() => res.sendStatus(500)); 17 | }); 18 | 19 | router.post('/register', validTokenMiddleware, async (req, res) => { 20 | redis 21 | .saddAsync(`tokens:${req.user.name}`, req.expoToken) 22 | .then(result => { 23 | if (result === 1) { 24 | // 1 token was added 25 | res.send({ message: 'registered' }); 26 | } else { 27 | res.status(400).send({ error: 'already registered with this token' }); 28 | } 29 | }) 30 | .catch(() => res.sendStatus(500)); 31 | }); 32 | 33 | router.post('/unregister', validTokenMiddleware, async (req, res) => { 34 | redis 35 | .sremAsync(`tokens:${req.user.name}`, req.expoToken) 36 | .then(result => { 37 | if (result === 1) { 38 | // 1 token removed from set 39 | res.send({ message: 'unregistered' }); 40 | } else { 41 | res.status(404).send({ error: 'token not already registered' }); 42 | } 43 | }) 44 | .catch(() => res.sendStatus(500)); 45 | }); 46 | 47 | module.exports = router; 48 | -------------------------------------------------------------------------------- /helpers/expoNotifications.js: -------------------------------------------------------------------------------- 1 | const Expo = require('expo-server-sdk'); 2 | const _ = require('lodash'); 3 | const redis = require('./redis'); 4 | const notificationTypes = require('./constants').notificationTypes; 5 | 6 | const expo = new Expo(); 7 | 8 | const sendAllNotifications = notifications => { 9 | // Collect notifications by user 10 | const userNotifications = {}; 11 | notifications.forEach(notification => { 12 | const user = notification[0]; 13 | if (!userNotifications[user]) { 14 | userNotifications[user] = []; 15 | } 16 | userNotifications[user].push(notification); 17 | }); 18 | 19 | Object.keys(userNotifications).forEach(user => { 20 | const currentUserNotifications = userNotifications[user]; 21 | redis.smembersAsync(`tokens:${user}`).then(async tokens => { 22 | if (tokens.length === 0) return; 23 | 24 | const messages = []; 25 | currentUserNotifications.forEach(currentUserNotification => { 26 | tokens.forEach(token => 27 | messages.push(getNotificationMessage(currentUserNotification, token)), 28 | ); 29 | }); 30 | const chunks = expo.chunkPushNotifications(messages); 31 | for (const chunk of chunks) { 32 | try { 33 | const resp = await expo.sendPushNotificationsAsync(chunk); 34 | console.log('Expo chunk set', resp); 35 | } catch (error) { 36 | console.log('Error sending expo chunk', error); 37 | } 38 | } 39 | }); 40 | }); 41 | }; 42 | 43 | const getNotificationMessage = (notification, token) => { 44 | const data = notification[1]; 45 | const template = { to: token, data }; 46 | 47 | let message = {}; 48 | switch (notification[1].type) { 49 | case notificationTypes.VOTE: 50 | message = { 51 | body: 52 | data.weight > 0 53 | ? `${data.voter} upvoted your post.` 54 | : `${data.voter} downvoted your post.`, 55 | }; 56 | break; 57 | 58 | case notificationTypes.TRANSFER: 59 | message = { 60 | body: `${data.from} sent you ${data.amount}.`, 61 | }; 62 | break; 63 | 64 | case notificationTypes.REPLY: 65 | message = { 66 | body: `${data.author} replied to your post.`, 67 | }; 68 | break; 69 | 70 | case notificationTypes.FOLLOW: 71 | message = { 72 | body: `${data.follower} followed you.`, 73 | }; 74 | break; 75 | 76 | case notificationTypes.REBLOG: 77 | message = { 78 | body: `${data.account} reblogged your post.`, 79 | }; 80 | break; 81 | 82 | case notificationTypes.MENTION: 83 | message = { 84 | body: `${data.author} mentioned you in a ` + (data.is_root_post ? 'post.' : 'comment.'), 85 | }; 86 | break; 87 | 88 | default: 89 | message = { 90 | body: 'Something happened in the app.', 91 | }; 92 | } 93 | 94 | return { ...template, ...message }; 95 | }; 96 | 97 | module.exports = { 98 | expo, 99 | sendAllNotifications, 100 | getNotificationMessage, 101 | }; 102 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const express = require('express'); 3 | const SocketServer = require('ws').Server; 4 | const { Client } = require('busyjs'); 5 | const sdk = require('sc2-sdk'); 6 | const bodyParser = require('body-parser'); 7 | const redis = require('./helpers/redis'); 8 | const utils = require('./helpers/utils'); 9 | const router = require('./routes'); 10 | const notificationUtils = require('./helpers/expoNotifications'); 11 | 12 | const NOTIFICATION_EXPIRY = 5 * 24 * 3600; 13 | const LIMIT = 25; 14 | 15 | const sc2 = sdk.Initialize({ app: 'busy.app' }); 16 | 17 | const app = express(); 18 | app.use(bodyParser.json()); 19 | app.use('/', router); 20 | 21 | const port = process.env.PORT || 4000; 22 | const server = app.listen(port, () => console.log(`Listening on ${port}`)); 23 | 24 | const wss = new SocketServer({ server }); 25 | 26 | const steemdWsUrl = process.env.STEEMD_WS_URL || 'wss://rpc.buildteam.io'; 27 | const client = new Client(steemdWsUrl); 28 | 29 | const cache = {}; 30 | const useCache = false; 31 | 32 | const clearGC = () => { 33 | try { 34 | global.gc(); 35 | } catch (e) { 36 | console.log("You must run program with 'node --expose-gc index.js' or 'npm start'"); 37 | } 38 | }; 39 | 40 | setInterval(clearGC, 60 * 1000); 41 | 42 | /** Init websocket server */ 43 | 44 | wss.on('connection', ws => { 45 | console.log('Got connection from new peer'); 46 | ws.on('message', message => { 47 | console.log('Message', message); 48 | let call = {}; 49 | try { 50 | call = JSON.parse(message); 51 | } catch (e) { 52 | console.error('Error WS parse JSON message', message, e); 53 | } 54 | // const key = new Buffer(JSON.stringify([call.method, call.params])).toString('base64'); 55 | if (call.method === 'get_notifications' && call.params && call.params[0]) { 56 | redis 57 | .lrangeAsync(`notifications:${call.params[0]}`, 0, -1) 58 | .then(res => { 59 | console.log('Send notifications', call.params[0], res.length); 60 | const notifications = res.map(notification => JSON.parse(notification)); 61 | ws.send(JSON.stringify({ id: call.id, result: notifications })); 62 | }) 63 | .catch(err => { 64 | console.log('Redis get_notifications failed', err); 65 | }); 66 | // } else if (useCache && cache[key]) { 67 | // ws.send(JSON.stringify({ id: call.id, cache: true, result: cache[key] })); 68 | } else if (call.method === 'login' && call.params && call.params[0]) { 69 | sc2.setAccessToken(call.params[0]); 70 | sc2 71 | .me() 72 | .then(result => { 73 | console.log('Login success', result.name); 74 | ws.name = result.name; 75 | ws.verified = true; 76 | ws.account = result.account; 77 | ws.user_metadata = result.user_metadata; 78 | ws.send(JSON.stringify({ id: call.id, result: { login: true, username: result.name } })); 79 | }) 80 | .catch(err => { 81 | console.error('Login failed', err); 82 | ws.send( 83 | JSON.stringify({ 84 | id: call.id, 85 | result: {}, 86 | error: 'Something is wrong', 87 | }), 88 | ); 89 | }); 90 | } else if (call.method === 'subscribe' && call.params && call.params[0]) { 91 | console.log('Subscribe success', call.params[0]); 92 | ws.name = call.params[0]; 93 | ws.send( 94 | JSON.stringify({ id: call.id, result: { subscribe: true, username: call.params[0] } }), 95 | ); 96 | } else if (call.method && call.params) { 97 | client.call(call.method, call.params, (err, result) => { 98 | ws.send(JSON.stringify({ id: call.id, result })); 99 | // if (useCache) { 100 | // cache[key] = result; 101 | // } 102 | }); 103 | } else { 104 | ws.send( 105 | JSON.stringify({ 106 | id: call.id, 107 | result: {}, 108 | error: 'Something is wrong', 109 | }), 110 | ); 111 | } 112 | }); 113 | ws.on('error', () => console.log('Error on connection with peer')); 114 | ws.on('close', () => console.log('Connection with peer closed')); 115 | }); 116 | 117 | /** Stream the blockchain for notifications */ 118 | 119 | const getNotifications = ops => { 120 | const notifications = []; 121 | ops.forEach(op => { 122 | const type = op.op[0]; 123 | const params = op.op[1]; 124 | switch (type) { 125 | case 'comment': { 126 | const isRootPost = !params.parent_author; 127 | 128 | /** Find replies */ 129 | if (!isRootPost) { 130 | const notification = { 131 | type: 'reply', 132 | parent_permlink: params.parent_permlink, 133 | author: params.author, 134 | permlink: params.permlink, 135 | timestamp: Date.parse(op.timestamp) / 1000, 136 | block: op.block, 137 | }; 138 | notifications.push([params.parent_author, notification]); 139 | } 140 | 141 | /** Find mentions */ 142 | const pattern = /(@[a-z][-\.a-z\d]+[a-z\d])/gi; 143 | const content = `${params.title} ${params.body}`; 144 | const mentions = _ 145 | .without( 146 | _ 147 | .uniq( 148 | (content.match(pattern) || []) 149 | .join('@') 150 | .toLowerCase() 151 | .split('@'), 152 | ) 153 | .filter(n => n), 154 | params.author, 155 | ) 156 | .slice(0, 9); // Handle maximum 10 mentions per post 157 | if (mentions.length) { 158 | mentions.forEach(mention => { 159 | const notification = { 160 | type: 'mention', 161 | is_root_post: isRootPost, 162 | author: params.author, 163 | permlink: params.permlink, 164 | timestamp: Date.parse(op.timestamp) / 1000, 165 | block: op.block, 166 | }; 167 | notifications.push([mention, notification]); 168 | }); 169 | } 170 | break; 171 | } 172 | case 'custom_json': { 173 | let json = {}; 174 | try { 175 | json = JSON.parse(params.json); 176 | } catch (err) { 177 | console.log('Wrong json format on custom_json', err); 178 | } 179 | switch (params.id) { 180 | case 'follow': { 181 | /** Find follow */ 182 | if ( 183 | json[0] === 'follow' && 184 | json[1].follower && 185 | json[1].following && 186 | _.has(json, '[1].what[0]') && 187 | json[1].what[0] === 'blog' 188 | ) { 189 | const notification = { 190 | type: 'follow', 191 | follower: json[1].follower, 192 | timestamp: Date.parse(op.timestamp) / 1000, 193 | block: op.block, 194 | }; 195 | notifications.push([json[1].following, notification]); 196 | } 197 | /** Find reblog */ 198 | if (json[0] === 'reblog' && json[1].account && json[1].author && json[1].permlink) { 199 | const notification = { 200 | type: 'reblog', 201 | account: json[1].account, 202 | permlink: json[1].permlink, 203 | timestamp: Date.parse(op.timestamp) / 1000, 204 | block: op.block, 205 | }; 206 | // console.log('Reblog', [json[1].author, JSON.stringify(notification)]); 207 | notifications.push([json[1].author, notification]); 208 | } 209 | break; 210 | } 211 | } 212 | break; 213 | } 214 | case 'account_witness_vote': { 215 | /** Find witness vote */ 216 | const notification = { 217 | type: 'witness_vote', 218 | account: params.account, 219 | approve: params.approve, 220 | timestamp: Date.parse(op.timestamp) / 1000, 221 | block: op.block, 222 | }; 223 | // console.log('Witness vote', [params.witness, notification]); 224 | notifications.push([params.witness, notification]); 225 | break; 226 | } 227 | case 'vote': { 228 | /** Find downvote */ 229 | if (params.weight < 0) { 230 | const notification = { 231 | type: 'vote', 232 | voter: params.voter, 233 | permlink: params.permlink, 234 | weight: params.weight, 235 | timestamp: Date.parse(op.timestamp) / 1000, 236 | block: op.block, 237 | }; 238 | // console.log('Downvote', JSON.stringify([params.author, notification])); 239 | notifications.push([params.author, notification]); 240 | } 241 | break; 242 | } 243 | case 'transfer': { 244 | /** Find transfer */ 245 | const notification = { 246 | type: 'transfer', 247 | from: params.from, 248 | amount: params.amount, 249 | memo: params.memo, 250 | timestamp: Date.parse(op.timestamp) / 1000, 251 | block: op.block, 252 | }; 253 | // console.log('Transfer', JSON.stringify([params.to, notification])); 254 | notifications.push([params.to, notification]); 255 | break; 256 | } 257 | } 258 | }); 259 | return notifications; 260 | }; 261 | 262 | const loadBlock = blockNum => { 263 | utils 264 | .getOpsInBlock(blockNum, false) 265 | .then(ops => { 266 | if (!ops.length) { 267 | console.error('Block does not exit?', blockNum); 268 | utils 269 | .getBlock(blockNum) 270 | .then(block => { 271 | if (block && block.previous && block.transactions.length === 0) { 272 | console.log('Block exist and is empty, load next', blockNum); 273 | redis 274 | .setAsync('last_block_num', blockNum) 275 | .then(() => { 276 | loadNextBlock(); 277 | }) 278 | .catch(err => { 279 | console.error('Redis set last_block_num failed', err); 280 | loadBlock(blockNum); 281 | }); 282 | } else { 283 | console.log('Sleep and retry', blockNum); 284 | utils.sleep(2000).then(() => { 285 | loadBlock(blockNum); 286 | }); 287 | } 288 | }) 289 | .catch(err => { 290 | console.log( 291 | 'Error lightrpc (getBlock), sleep and retry', 292 | blockNum, 293 | JSON.stringify(err), 294 | ); 295 | utils.sleep(2000).then(() => { 296 | loadBlock(blockNum); 297 | }); 298 | }); 299 | } else { 300 | const notifications = getNotifications(ops); 301 | /** Create redis operations array */ 302 | const redisOps = []; 303 | notifications.forEach(notification => { 304 | const key = `notifications:${notification[0]}` 305 | redisOps.push([ 306 | 'lpush', 307 | key, 308 | JSON.stringify(notification[1]), 309 | ]); 310 | redisOps.push(['expire', key, NOTIFICATION_EXPIRY]); 311 | redisOps.push(['ltrim', key, 0, LIMIT - 1]); 312 | }); 313 | redisOps.push(['set', 'last_block_num', blockNum]); 314 | redis 315 | .multi(redisOps) 316 | .execAsync() 317 | .then(() => { 318 | console.log('Block loaded', blockNum, 'notification stored', notifications.length); 319 | 320 | /** Send push notification for logged peers */ 321 | notifications.forEach(notification => { 322 | wss.clients.forEach(client => { 323 | if (client.name && client.name === notification[0]) { 324 | console.log('Send push notification', notification[0]); 325 | client.send( 326 | JSON.stringify({ 327 | type: 'notification', 328 | notification: notification[1], 329 | }), 330 | ); 331 | } 332 | }); 333 | }); 334 | /** Send notifications to all devices */ 335 | notificationUtils.sendAllNotifications(notifications); 336 | loadNextBlock(); 337 | }) 338 | .catch(err => { 339 | console.error('Redis store notification multi failed', err); 340 | loadBlock(blockNum); 341 | }); 342 | } 343 | }) 344 | .catch(err => { 345 | console.error('Call failed with lightrpc (getOpsInBlock)', err); 346 | console.log('Retry', blockNum); 347 | loadBlock(blockNum); 348 | }); 349 | }; 350 | 351 | const loadNextBlock = () => { 352 | redis 353 | .getAsync('last_block_num') 354 | .then(res => { 355 | let nextBlockNum = res === null ? 20000000 : parseInt(res) + 1; 356 | utils 357 | .getGlobalProps() 358 | .then(globalProps => { 359 | const lastIrreversibleBlockNum = globalProps.last_irreversible_block_num; 360 | if (lastIrreversibleBlockNum >= nextBlockNum) { 361 | loadBlock(nextBlockNum); 362 | } else { 363 | utils.sleep(2000).then(() => { 364 | console.log( 365 | 'Waiting to be on the lastIrreversibleBlockNum', 366 | lastIrreversibleBlockNum, 367 | 'now nextBlockNum', 368 | nextBlockNum, 369 | ); 370 | loadNextBlock(); 371 | }); 372 | } 373 | }) 374 | .catch(err => { 375 | console.error('Call failed with lightrpc (getGlobalProps)', err); 376 | utils.sleep(2000).then(() => { 377 | console.log('Retry loadNextBlock', nextBlockNum); 378 | loadNextBlock(); 379 | }); 380 | }); 381 | }) 382 | .catch(err => { 383 | console.error('Redis get last_block_num failed', err); 384 | }); 385 | }; 386 | 387 | const start = () => { 388 | console.info('Start streaming blockchain'); 389 | loadNextBlock(); 390 | 391 | /** Send heartbeat to peers */ 392 | setInterval(() => { 393 | wss.clients.forEach(client => { 394 | client.send(JSON.stringify({ type: 'heartbeat' })); 395 | }); 396 | }, 20 * 1000); 397 | }; 398 | 399 | // redis.flushallAsync(); 400 | start(); 401 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | accepts@~1.3.5: 6 | version "1.3.5" 7 | resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.5.tgz#eb777df6011723a3b14e8a72c0805c8e86746bd2" 8 | dependencies: 9 | mime-types "~2.1.18" 10 | negotiator "0.6.1" 11 | 12 | array-flatten@1.1.1: 13 | version "1.1.1" 14 | resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" 15 | 16 | async-limiter@~1.0.0: 17 | version "1.0.0" 18 | resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8" 19 | 20 | babel-runtime@^6.11.6: 21 | version "6.26.0" 22 | resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" 23 | dependencies: 24 | core-js "^2.4.0" 25 | regenerator-runtime "^0.11.0" 26 | 27 | bluebird@^3.5.1: 28 | version "3.5.1" 29 | resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" 30 | 31 | body-parser@1.18.2: 32 | version "1.18.2" 33 | resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.2.tgz#87678a19d84b47d859b83199bd59bce222b10454" 34 | dependencies: 35 | bytes "3.0.0" 36 | content-type "~1.0.4" 37 | debug "2.6.9" 38 | depd "~1.1.1" 39 | http-errors "~1.6.2" 40 | iconv-lite "0.4.19" 41 | on-finished "~2.3.0" 42 | qs "6.5.1" 43 | raw-body "2.3.2" 44 | type-is "~1.6.15" 45 | 46 | body-parser@^1.18.3: 47 | version "1.18.3" 48 | resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.3.tgz#5b292198ffdd553b3a0f20ded0592b956955c8b4" 49 | dependencies: 50 | bytes "3.0.0" 51 | content-type "~1.0.4" 52 | debug "2.6.9" 53 | depd "~1.1.2" 54 | http-errors "~1.6.3" 55 | iconv-lite "0.4.23" 56 | on-finished "~2.3.0" 57 | qs "6.5.2" 58 | raw-body "2.3.3" 59 | type-is "~1.6.16" 60 | 61 | busyjs@^1.0.2: 62 | version "1.0.2" 63 | resolved "https://registry.yarnpkg.com/busyjs/-/busyjs-1.0.2.tgz#d52d8310fce923ad242093566ab41b6df53bbf62" 64 | dependencies: 65 | ws "^4.0.0" 66 | 67 | bytes@3.0.0: 68 | version "3.0.0" 69 | resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" 70 | 71 | content-disposition@0.5.2: 72 | version "0.5.2" 73 | resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" 74 | 75 | content-type@~1.0.4: 76 | version "1.0.4" 77 | resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" 78 | 79 | cookie-signature@1.0.6: 80 | version "1.0.6" 81 | resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" 82 | 83 | cookie@0.3.1: 84 | version "0.3.1" 85 | resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" 86 | 87 | core-js@^2.4.0: 88 | version "2.5.7" 89 | resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.7.tgz#f972608ff0cead68b841a16a932d0b183791814e" 90 | 91 | cross-fetch@^1.1.1: 92 | version "1.1.1" 93 | resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-1.1.1.tgz#dede6865ae30f37eae62ac90ebb7bdac002b05a0" 94 | dependencies: 95 | node-fetch "1.7.3" 96 | whatwg-fetch "2.0.3" 97 | 98 | debug@2.6.9, debug@^2.2.0: 99 | version "2.6.9" 100 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" 101 | dependencies: 102 | ms "2.0.0" 103 | 104 | depd@1.1.1: 105 | version "1.1.1" 106 | resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359" 107 | 108 | depd@~1.1.1, depd@~1.1.2: 109 | version "1.1.2" 110 | resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" 111 | 112 | destroy@~1.0.4: 113 | version "1.0.4" 114 | resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" 115 | 116 | double-ended-queue@^2.1.0-0: 117 | version "2.1.0-0" 118 | resolved "https://registry.yarnpkg.com/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz#103d3527fd31528f40188130c841efdd78264e5c" 119 | 120 | ee-first@1.1.1: 121 | version "1.1.1" 122 | resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" 123 | 124 | encodeurl@~1.0.2: 125 | version "1.0.2" 126 | resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" 127 | 128 | encoding@^0.1.11: 129 | version "0.1.12" 130 | resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb" 131 | dependencies: 132 | iconv-lite "~0.4.13" 133 | 134 | escape-html@~1.0.3: 135 | version "1.0.3" 136 | resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" 137 | 138 | etag@~1.8.1: 139 | version "1.8.1" 140 | resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" 141 | 142 | expo-server-sdk@^2.4.0: 143 | version "2.4.0" 144 | resolved "https://registry.yarnpkg.com/expo-server-sdk/-/expo-server-sdk-2.4.0.tgz#aa5e02415ba3268546ff61dddb5b92cb796f6e3a" 145 | dependencies: 146 | babel-runtime "^6.11.6" 147 | invariant "^2.2.4" 148 | node-fetch "^2.1.2" 149 | promise-limit "^2.6.0" 150 | 151 | express@^4.16.2: 152 | version "4.16.3" 153 | resolved "https://registry.yarnpkg.com/express/-/express-4.16.3.tgz#6af8a502350db3246ecc4becf6b5a34d22f7ed53" 154 | dependencies: 155 | accepts "~1.3.5" 156 | array-flatten "1.1.1" 157 | body-parser "1.18.2" 158 | content-disposition "0.5.2" 159 | content-type "~1.0.4" 160 | cookie "0.3.1" 161 | cookie-signature "1.0.6" 162 | debug "2.6.9" 163 | depd "~1.1.2" 164 | encodeurl "~1.0.2" 165 | escape-html "~1.0.3" 166 | etag "~1.8.1" 167 | finalhandler "1.1.1" 168 | fresh "0.5.2" 169 | merge-descriptors "1.0.1" 170 | methods "~1.1.2" 171 | on-finished "~2.3.0" 172 | parseurl "~1.3.2" 173 | path-to-regexp "0.1.7" 174 | proxy-addr "~2.0.3" 175 | qs "6.5.1" 176 | range-parser "~1.2.0" 177 | safe-buffer "5.1.1" 178 | send "0.16.2" 179 | serve-static "1.13.2" 180 | setprototypeof "1.1.0" 181 | statuses "~1.4.0" 182 | type-is "~1.6.16" 183 | utils-merge "1.0.1" 184 | vary "~1.1.2" 185 | 186 | finalhandler@1.1.1: 187 | version "1.1.1" 188 | resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.1.tgz#eebf4ed840079c83f4249038c9d703008301b105" 189 | dependencies: 190 | debug "2.6.9" 191 | encodeurl "~1.0.2" 192 | escape-html "~1.0.3" 193 | on-finished "~2.3.0" 194 | parseurl "~1.3.2" 195 | statuses "~1.4.0" 196 | unpipe "~1.0.0" 197 | 198 | forwarded@~0.1.2: 199 | version "0.1.2" 200 | resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" 201 | 202 | fresh@0.5.2: 203 | version "0.5.2" 204 | resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" 205 | 206 | http-errors@1.6.2: 207 | version "1.6.2" 208 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736" 209 | dependencies: 210 | depd "1.1.1" 211 | inherits "2.0.3" 212 | setprototypeof "1.0.3" 213 | statuses ">= 1.3.1 < 2" 214 | 215 | http-errors@1.6.3, http-errors@~1.6.2, http-errors@~1.6.3: 216 | version "1.6.3" 217 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" 218 | dependencies: 219 | depd "~1.1.2" 220 | inherits "2.0.3" 221 | setprototypeof "1.1.0" 222 | statuses ">= 1.4.0 < 2" 223 | 224 | iconv-lite@0.4.19: 225 | version "0.4.19" 226 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" 227 | 228 | iconv-lite@0.4.23, iconv-lite@~0.4.13: 229 | version "0.4.23" 230 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63" 231 | dependencies: 232 | safer-buffer ">= 2.1.2 < 3" 233 | 234 | inherits@2.0.3: 235 | version "2.0.3" 236 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 237 | 238 | invariant@^2.2.4: 239 | version "2.2.4" 240 | resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" 241 | dependencies: 242 | loose-envify "^1.0.0" 243 | 244 | ipaddr.js@1.6.0: 245 | version "1.6.0" 246 | resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.6.0.tgz#e3fa357b773da619f26e95f049d055c72796f86b" 247 | 248 | is-stream@^1.0.1: 249 | version "1.1.0" 250 | resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" 251 | 252 | js-tokens@^3.0.0: 253 | version "3.0.2" 254 | resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" 255 | 256 | lightrpc@^1.0.0-beta.4: 257 | version "1.0.1" 258 | resolved "https://registry.yarnpkg.com/lightrpc/-/lightrpc-1.0.1.tgz#1b03ebe4c33d6212a57e87f6096142e67495993e" 259 | dependencies: 260 | cross-fetch "^1.1.1" 261 | 262 | lodash@^4.17.5: 263 | version "4.17.10" 264 | resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" 265 | 266 | loose-envify@^1.0.0: 267 | version "1.3.1" 268 | resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848" 269 | dependencies: 270 | js-tokens "^3.0.0" 271 | 272 | media-typer@0.3.0: 273 | version "0.3.0" 274 | resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" 275 | 276 | merge-descriptors@1.0.1: 277 | version "1.0.1" 278 | resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" 279 | 280 | methods@~1.1.2: 281 | version "1.1.2" 282 | resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" 283 | 284 | mime-db@~1.33.0: 285 | version "1.33.0" 286 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" 287 | 288 | mime-types@~2.1.18: 289 | version "2.1.18" 290 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8" 291 | dependencies: 292 | mime-db "~1.33.0" 293 | 294 | mime@1.4.1: 295 | version "1.4.1" 296 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" 297 | 298 | ms@2.0.0: 299 | version "2.0.0" 300 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 301 | 302 | negotiator@0.6.1: 303 | version "0.6.1" 304 | resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" 305 | 306 | node-fetch@1.7.3: 307 | version "1.7.3" 308 | resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" 309 | dependencies: 310 | encoding "^0.1.11" 311 | is-stream "^1.0.1" 312 | 313 | node-fetch@^2.1.2: 314 | version "2.1.2" 315 | resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.1.2.tgz#ab884e8e7e57e38a944753cec706f788d1768bb5" 316 | 317 | on-finished@~2.3.0: 318 | version "2.3.0" 319 | resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" 320 | dependencies: 321 | ee-first "1.1.1" 322 | 323 | parseurl@~1.3.2: 324 | version "1.3.2" 325 | resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3" 326 | 327 | path-to-regexp@0.1.7: 328 | version "0.1.7" 329 | resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" 330 | 331 | promise-limit@^2.6.0: 332 | version "2.6.0" 333 | resolved "https://registry.yarnpkg.com/promise-limit/-/promise-limit-2.6.0.tgz#cb6959fcfdd0ee6ec694ec58b2b3b856ca52ffed" 334 | 335 | proxy-addr@~2.0.3: 336 | version "2.0.3" 337 | resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.3.tgz#355f262505a621646b3130a728eb647e22055341" 338 | dependencies: 339 | forwarded "~0.1.2" 340 | ipaddr.js "1.6.0" 341 | 342 | qs@6.5.1: 343 | version "6.5.1" 344 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" 345 | 346 | qs@6.5.2: 347 | version "6.5.2" 348 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" 349 | 350 | range-parser@~1.2.0: 351 | version "1.2.0" 352 | resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" 353 | 354 | raw-body@2.3.2: 355 | version "2.3.2" 356 | resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.2.tgz#bcd60c77d3eb93cde0050295c3f379389bc88f89" 357 | dependencies: 358 | bytes "3.0.0" 359 | http-errors "1.6.2" 360 | iconv-lite "0.4.19" 361 | unpipe "1.0.0" 362 | 363 | raw-body@2.3.3: 364 | version "2.3.3" 365 | resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.3.tgz#1b324ece6b5706e153855bc1148c65bb7f6ea0c3" 366 | dependencies: 367 | bytes "3.0.0" 368 | http-errors "1.6.3" 369 | iconv-lite "0.4.23" 370 | unpipe "1.0.0" 371 | 372 | redis-commands@^1.2.0: 373 | version "1.3.5" 374 | resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.3.5.tgz#4495889414f1e886261180b1442e7295602d83a2" 375 | 376 | redis-parser@^2.6.0: 377 | version "2.6.0" 378 | resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-2.6.0.tgz#52ed09dacac108f1a631c07e9b69941e7a19504b" 379 | 380 | redis@^2.8.0: 381 | version "2.8.0" 382 | resolved "https://registry.yarnpkg.com/redis/-/redis-2.8.0.tgz#202288e3f58c49f6079d97af7a10e1303ae14b02" 383 | dependencies: 384 | double-ended-queue "^2.1.0-0" 385 | redis-commands "^1.2.0" 386 | redis-parser "^2.6.0" 387 | 388 | regenerator-runtime@^0.11.0: 389 | version "0.11.1" 390 | resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" 391 | 392 | safe-buffer@5.1.1: 393 | version "5.1.1" 394 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" 395 | 396 | safe-buffer@~5.1.0: 397 | version "5.1.2" 398 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" 399 | 400 | "safer-buffer@>= 2.1.2 < 3": 401 | version "2.1.2" 402 | resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" 403 | 404 | sc2-sdk@^1.0.2: 405 | version "1.0.2" 406 | resolved "https://registry.yarnpkg.com/sc2-sdk/-/sc2-sdk-1.0.2.tgz#6a83d8a55d50ef5c861f80c84da433e4e29fcf56" 407 | dependencies: 408 | cross-fetch "^1.1.1" 409 | debug "^2.2.0" 410 | 411 | send@0.16.2: 412 | version "0.16.2" 413 | resolved "https://registry.yarnpkg.com/send/-/send-0.16.2.tgz#6ecca1e0f8c156d141597559848df64730a6bbc1" 414 | dependencies: 415 | debug "2.6.9" 416 | depd "~1.1.2" 417 | destroy "~1.0.4" 418 | encodeurl "~1.0.2" 419 | escape-html "~1.0.3" 420 | etag "~1.8.1" 421 | fresh "0.5.2" 422 | http-errors "~1.6.2" 423 | mime "1.4.1" 424 | ms "2.0.0" 425 | on-finished "~2.3.0" 426 | range-parser "~1.2.0" 427 | statuses "~1.4.0" 428 | 429 | serve-static@1.13.2: 430 | version "1.13.2" 431 | resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.13.2.tgz#095e8472fd5b46237db50ce486a43f4b86c6cec1" 432 | dependencies: 433 | encodeurl "~1.0.2" 434 | escape-html "~1.0.3" 435 | parseurl "~1.3.2" 436 | send "0.16.2" 437 | 438 | setprototypeof@1.0.3: 439 | version "1.0.3" 440 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04" 441 | 442 | setprototypeof@1.1.0: 443 | version "1.1.0" 444 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" 445 | 446 | "statuses@>= 1.3.1 < 2", "statuses@>= 1.4.0 < 2": 447 | version "1.5.0" 448 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" 449 | 450 | statuses@~1.4.0: 451 | version "1.4.0" 452 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087" 453 | 454 | type-is@~1.6.15, type-is@~1.6.16: 455 | version "1.6.16" 456 | resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.16.tgz#f89ce341541c672b25ee7ae3c73dee3b2be50194" 457 | dependencies: 458 | media-typer "0.3.0" 459 | mime-types "~2.1.18" 460 | 461 | unpipe@1.0.0, unpipe@~1.0.0: 462 | version "1.0.0" 463 | resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" 464 | 465 | utils-merge@1.0.1: 466 | version "1.0.1" 467 | resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" 468 | 469 | vary@~1.1.2: 470 | version "1.1.2" 471 | resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" 472 | 473 | whatwg-fetch@2.0.3: 474 | version "2.0.3" 475 | resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz#9c84ec2dcf68187ff00bc64e1274b442176e1c84" 476 | 477 | ws@^4.0.0: 478 | version "4.1.0" 479 | resolved "https://registry.yarnpkg.com/ws/-/ws-4.1.0.tgz#a979b5d7d4da68bf54efe0408967c324869a7289" 480 | dependencies: 481 | async-limiter "~1.0.0" 482 | safe-buffer "~5.1.0" 483 | --------------------------------------------------------------------------------