diff --git a/packages/nylas-api/app.js b/packages/nylas-api/app.js index 365e53904..29c4ee2f0 100644 --- a/packages/nylas-api/app.js +++ b/packages/nylas-api/app.js @@ -7,6 +7,8 @@ const Package = require('./package'); const fs = require('fs'); const path = require('path'); +global.Promise = require('bluebird'); + const server = new Hapi.Server(); server.connection({ port: process.env.PORT || 5100 }); diff --git a/packages/nylas-core/imap-connection.js b/packages/nylas-core/imap-connection.js index 11782518a..8a047f830 100644 --- a/packages/nylas-core/imap-connection.js +++ b/packages/nylas-core/imap-connection.js @@ -1,6 +1,5 @@ const Imap = require('imap'); const EventEmitter = require('events'); -const Promise = require('bluebird'); const Capabilities = { Gmail: 'X-GM-EXT-1', diff --git a/packages/nylas-core/index.js b/packages/nylas-core/index.js index 5365e0289..59733f444 100644 --- a/packages/nylas-core/index.js +++ b/packages/nylas-core/index.js @@ -1,7 +1,10 @@ +global.Promise = require('bluebird'); + module.exports = { DatabaseConnector: require('./database-connector'), PubsubConnector: require('./pubsub-connector'), IMAPConnection: require('./imap-connection'), SyncPolicy: require('./sync-policy'), + SchedulerUtils: require('./scheduler-utils'), Config: require(`./config/${process.env.ENV || 'development'}`), } diff --git a/packages/nylas-core/pubsub-connector.js b/packages/nylas-core/pubsub-connector.js index c13d01ccf..2022e0dca 100644 --- a/packages/nylas-core/pubsub-connector.js +++ b/packages/nylas-core/pubsub-connector.js @@ -1,11 +1,10 @@ const Rx = require('rx') -const bluebird = require('bluebird') const redis = require("redis"); const SyncPolicy = require('./sync-policy'); -bluebird.promisifyAll(redis.RedisClient.prototype); -bluebird.promisifyAll(redis.Multi.prototype); +Promise.promisifyAll(redis.RedisClient.prototype); +Promise.promisifyAll(redis.Multi.prototype); class PubsubConnector { constructor() { @@ -32,7 +31,7 @@ class PubsubConnector { } channelForAccountDeltas(accountId) { - return `a-${accountId}-deltas`; + return `deltas-${accountId}`; } // Shared channel diff --git a/packages/nylas-core/scheduler-utils.js b/packages/nylas-core/scheduler-utils.js new file mode 100644 index 000000000..f6595326d --- /dev/null +++ b/packages/nylas-core/scheduler-utils.js @@ -0,0 +1,29 @@ +const ACCOUNTS_UNCLAIMED = 'accounts:unclaimed'; +const ACCOUNTS_CLAIMED_PREFIX = 'accounts:id-'; +const ACCOUNTS_FOR = (id) => `${ACCOUNTS_CLAIMED_PREFIX}${id}`; +const HEARTBEAT_FOR = (id) => `heartbeat:${id}`; +const HEARTBEAT_EXPIRES = 30; // 2 min in prod? +const CLAIM_DURATION = 10 * 60 * 1000; // 2 hours on prod? + +const PubsubConnector = require('./pubsub-connector') + +const forEachAccountList = (forEachCallback) => { + const client = PubsubConnector.broadcastClient(); + return Promise.each(client.keysAsync(`accounts:*`), (key) => { + const processId = key.replace('accounts:', ''); + return client.lrangeAsync(key, 0, 20000).then((foundIds) => + forEachCallback(processId, foundIds) + ) + }); +} + +module.exports = { + ACCOUNTS_UNCLAIMED, + ACCOUNTS_CLAIMED_PREFIX, + ACCOUNTS_FOR, + HEARTBEAT_FOR, + HEARTBEAT_EXPIRES, + CLAIM_DURATION, + + forEachAccountList, +} diff --git a/packages/nylas-core/sync-policy.js b/packages/nylas-core/sync-policy.js index 5d2d59e0d..5443f3d28 100644 --- a/packages/nylas-core/sync-policy.js +++ b/packages/nylas-core/sync-policy.js @@ -1,7 +1,7 @@ class SyncPolicy { static defaultPolicy() { return { - afterSync: 'close', + afterSync: 'idle', interval: 120 * 1000, folderSyncOptions: { deepFolderScan: 10 * 60 * 1000, diff --git a/packages/nylas-dashboard/app.js b/packages/nylas-dashboard/app.js new file mode 100644 index 000000000..cb0b6c909 --- /dev/null +++ b/packages/nylas-dashboard/app.js @@ -0,0 +1,75 @@ +const Hapi = require('hapi'); +const HapiWebSocket = require('hapi-plugin-websocket'); +const Inert = require('inert'); +const {DatabaseConnector, PubsubConnector, SchedulerUtils} = require(`nylas-core`); +const {forEachAccountList} = SchedulerUtils; + +global.Promise = require('bluebird'); + +const server = new Hapi.Server(); +server.connection({ port: process.env.PORT || 5101 }); + +DatabaseConnector.forShared().then(({Account}) => { + server.register([HapiWebSocket, Inert], () => { + server.route({ + method: "POST", + path: "/accounts", + config: { + plugins: { + websocket: { + only: true, + connect: (wss, ws) => { + Account.findAll().then((accts) => { + accts.forEach((acct) => { + ws.send(JSON.stringify({ cmd: "ACCOUNT", payload: acct })); + }); + }); + this.redis = PubsubConnector.buildClient(); + this.redis.on('pmessage', (pattern, channel) => { + Account.find({where: {id: channel.replace('a-', '')}}).then((acct) => { + ws.send(JSON.stringify({ cmd: "ACCOUNT", payload: acct })); + }); + }); + this.redis.psubscribe(PubsubConnector.channelForAccount('*')); + this.assignmentsInterval = setInterval(() => { + const assignments = {}; + forEachAccountList((identity, accountIds) => { + for (const accountId of accountIds) { + assignments[accountId] = identity; + } + }).then(() => + ws.send(JSON.stringify({ cmd: "ASSIGNMENTS", payload: assignments})) + ) + }, 1000); + }, + disconnect: () => { + clearInterval(this.assignmentsInterval); + this.redis.quit(); + }, + }, + }, + }, + handler: (request, reply) => { + if (request.payload.cmd === "PING") { + reply(JSON.stringify({ result: "PONG" })); + return; + } + }, + }); + + server.route({ + method: 'GET', + path: '/{param*}', + handler: { + directory: { + path: 'public', + }, + }, + }); + + server.start((startErr) => { + if (startErr) { throw startErr; } + console.log('Server running at:', server.info.uri); + }); + }); +}); diff --git a/packages/nylas-dashboard/package.json b/packages/nylas-dashboard/package.json new file mode 100644 index 000000000..a05f0fc24 --- /dev/null +++ b/packages/nylas-dashboard/package.json @@ -0,0 +1,17 @@ +{ + "name": "nylas-dashboard", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "hapi": "^13.4.1", + "hapi-plugin-websocket": "^0.9.2", + "inert": "^4.0.0", + "nylas-core": "0.x.x" + } +} diff --git a/packages/nylas-dashboard/public/css/app.css b/packages/nylas-dashboard/public/css/app.css new file mode 100644 index 000000000..056b94d01 --- /dev/null +++ b/packages/nylas-dashboard/public/css/app.css @@ -0,0 +1,17 @@ +body { + background-image: -webkit-linear-gradient(top, rgba(232, 244, 250, 0.6), rgba(231, 231, 233, 1)), url(http://news.nationalgeographic.com/content/dam/news/2015/12/13/BookTalk%20K2/01BookTalkK2.jpg); + background-size: cover; + font-family: sans-serif; +} + +.account { + display:inline-block; + width: 300px; + height: 100px; + background-color: white; + padding:15px; +} + +.account h3 { + margin: 0; padding: 0; +} diff --git a/packages/nylas-dashboard/public/index.html b/packages/nylas-dashboard/public/index.html new file mode 100644 index 000000000..dac87884b --- /dev/null +++ b/packages/nylas-dashboard/public/index.html @@ -0,0 +1,13 @@ + + + + + + + + + +

Dashboard

+
+ + diff --git a/packages/nylas-dashboard/public/js/app.jsx b/packages/nylas-dashboard/public/js/app.jsx new file mode 100644 index 000000000..dfc9e28ba --- /dev/null +++ b/packages/nylas-dashboard/public/js/app.jsx @@ -0,0 +1,91 @@ +/* eslint react/react-in-jsx-scope: 0*/ + +class Account extends React.Component { + propTypes: { + account: React.PropTypes.object, + assignment: React.PropTypes.string, + } + + render() { + const {account, assignment} = this.props; + return ( +
+

{account.email_address}

+ {assignment} +
Sync Interval: {account.sync_policy.interval}ms
+
Sync Idle Behavior: {account.sync_policy.afterSync}
+
+ ); + } +} + +class Root extends React.Component { + + constructor() { + super(); + this.state = { + accounts: {}, + assignments: {}, + }; + } + + componentDidMount() { + let url = null; + if (window.location.protocol === "https:") { + url = `wss://${window.location.host}/accounts`; + } else { + url = `ws://${window.location.host}/accounts`; + } + this.websocket = new WebSocket(url); + this.websocket.onopen = () => { + this.websocket.send("Message to send"); + }; + this.websocket.onmessage = (evt) => { + try { + const msg = JSON.parse(evt.data); + if (msg.cmd === 'ACCOUNT') { + this.onReceivedAccount(msg.payload); + } + if (msg.cmd === 'ASSIGNMENTS') { + this.onReceivedAssignments(msg.payload); + } + } catch (err) { + console.error(err); + } + }; + this.websocket.onclose = () => { + window.location.reload(); + }; + } + + onReceivedAssignments(assignments) { + this.setState({assignments}) + } + + onReceivedAccount(account) { + const accounts = Object.assign({}, this.state.accounts); + accounts[account.id] = account; + this.setState({accounts}); + } + + render() { + return ( +
+ { + Object.keys(this.state.accounts).sort((a, b) => a.compare(b)).map((key) => + + ) + } +
+ ) + } +} + +ReactDOM.render( + , + document.getElementById('root') +); diff --git a/packages/nylas-dashboard/public/js/react-dom.js b/packages/nylas-dashboard/public/js/react-dom.js new file mode 100644 index 000000000..1cf5496b5 --- /dev/null +++ b/packages/nylas-dashboard/public/js/react-dom.js @@ -0,0 +1,42 @@ +/** + * ReactDOM v15.1.0 + * + * Copyright 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ +// Based off https://github.com/ForbesLindesay/umd/blob/master/template.js +;(function(f) { + // CommonJS + if (typeof exports === "object" && typeof module !== "undefined") { + module.exports = f(require('react')); + + // RequireJS + } else if (typeof define === "function" && define.amd) { + define(['react'], f); + + //