mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-01-01 13:14:16 +08:00
Merge branch 'master' of github.com:nylas/K2
This commit is contained in:
commit
c5dce43da7
13 changed files with 122 additions and 52 deletions
|
@ -7,6 +7,7 @@
|
||||||
"bluebird": "3.x.x",
|
"bluebird": "3.x.x",
|
||||||
"bunyan": "1.8.0",
|
"bunyan": "1.8.0",
|
||||||
"bunyan-cloudwatch": "2.0.0",
|
"bunyan-cloudwatch": "2.0.0",
|
||||||
|
"bunyan-prettystream": "^0.1.3",
|
||||||
"lerna": "2.0.0-beta.23",
|
"lerna": "2.0.0-beta.23",
|
||||||
"mysql": "^2.11.1",
|
"mysql": "^2.11.1",
|
||||||
"newrelic": "^1.28.1",
|
"newrelic": "^1.28.1",
|
||||||
|
@ -27,8 +28,7 @@
|
||||||
"sqlite3": "https://github.com/bengotow/node-sqlite3/archive/bengotow/usleep-v3.1.4.tar.gz"
|
"sqlite3": "https://github.com/bengotow/node-sqlite3/archive/bengotow/usleep-v3.1.4.tar.gz"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "pm2 kill && pm2 start ./pm2-dev.yml --watch && pm2 logs --raw | bunyan -o short",
|
"start": "pm2 start ./pm2-dev.yml --no-daemon",
|
||||||
"logs": "pm2 logs --raw | bunyan -o short",
|
|
||||||
"stop": "pm2 kill",
|
"stop": "pm2 kill",
|
||||||
"postinstall": "lerna bootstrap"
|
"postinstall": "lerna bootstrap"
|
||||||
},
|
},
|
||||||
|
|
|
@ -14,6 +14,10 @@ const {DatabaseConnector, SchedulerUtils, Logger} = require(`nylas-core`);
|
||||||
global.Promise = require('bluebird');
|
global.Promise = require('bluebird');
|
||||||
global.Logger = Logger.createLogger('nylas-k2-api')
|
global.Logger = Logger.createLogger('nylas-k2-api')
|
||||||
|
|
||||||
|
const onUnhandledError = (err) => global.Logger.fatal(err, 'Unhandled error')
|
||||||
|
process.on('uncaughtException', onUnhandledError)
|
||||||
|
process.on('unhandledRejection', onUnhandledError)
|
||||||
|
|
||||||
const server = new Hapi.Server({
|
const server = new Hapi.Server({
|
||||||
connections: {
|
connections: {
|
||||||
router: {
|
router: {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
const Serialization = require('../serialization');
|
const Serialization = require('../serialization');
|
||||||
|
const {DatabaseConnector} = require('nylas-core');
|
||||||
|
|
||||||
module.exports = (server) => {
|
module.exports = (server) => {
|
||||||
server.route({
|
server.route({
|
||||||
|
@ -21,4 +22,28 @@ module.exports = (server) => {
|
||||||
reply(Serialization.jsonStringify(account));
|
reply(Serialization.jsonStringify(account));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: 'DELETE',
|
||||||
|
path: '/account',
|
||||||
|
config: {
|
||||||
|
description: 'Deletes the current account and all data from the Nylas Cloud.',
|
||||||
|
notes: 'Notes go here',
|
||||||
|
tags: ['accounts'],
|
||||||
|
validate: {
|
||||||
|
params: {
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
handler: (request, reply) => {
|
||||||
|
const account = request.auth.credentials;
|
||||||
|
account.destroy().then((saved) =>
|
||||||
|
DatabaseConnector.destroyAccountDatabase(saved.id).then(() =>
|
||||||
|
reply(Serialization.jsonStringify({status: 'success'}))
|
||||||
|
)
|
||||||
|
).catch((err) => {
|
||||||
|
reply(err).code(500);
|
||||||
|
})
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -79,11 +79,7 @@ class DatabaseConnector {
|
||||||
const dbname = `a-${accountId}`;
|
const dbname = `a-${accountId}`;
|
||||||
|
|
||||||
if (process.env.DB_HOSTNAME) {
|
if (process.env.DB_HOSTNAME) {
|
||||||
const sequelize = new Sequelize(null, process.env.DB_USERNAME, process.env.DB_PASSWORD, {
|
const sequelize = this._sequelizePoolForDatabase(null);
|
||||||
host: process.env.DB_HOSTNAME,
|
|
||||||
dialect: "mysql",
|
|
||||||
logging: false,
|
|
||||||
})
|
|
||||||
return sequelize.authenticate().then(() =>
|
return sequelize.authenticate().then(() =>
|
||||||
sequelize.query(`CREATE DATABASE \`${dbname}\``)
|
sequelize.query(`CREATE DATABASE \`${dbname}\``)
|
||||||
);
|
);
|
||||||
|
@ -91,6 +87,18 @@ class DatabaseConnector {
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
destroyAccountDatabase(accountId) {
|
||||||
|
const dbname = `a-${accountId}`;
|
||||||
|
if (process.env.DB_HOSTNAME) {
|
||||||
|
const sequelize = this._sequelizePoolForDatabase(null);
|
||||||
|
return sequelize.authenticate().then(() =>
|
||||||
|
sequelize.query(`CREATE DATABASE \`${dbname}\``)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
fs.removeFileSync(path.join(STORAGE_DIR, `${dbname}.sqlite`));
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
_sequelizeForShared() {
|
_sequelizeForShared() {
|
||||||
const sequelize = this._sequelizePoolForDatabase(`shared`);
|
const sequelize = this._sequelizePoolForDatabase(`shared`);
|
||||||
const modelsPath = path.join(__dirname, 'models/shared');
|
const modelsPath = path.join(__dirname, 'models/shared');
|
||||||
|
|
|
@ -17,7 +17,11 @@ module.exports = (db, sequelize) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// TODO delete account from redis
|
sequelize.addHook("afterDestroy", ({dataValues, $modelOptions}) => {
|
||||||
// sequelize.addHook("afterDelete", ({dataValues, $modelOptions}) => {
|
if ($modelOptions.name.singular === 'account') {
|
||||||
// })
|
PubsubConnector.notifyAccount(dataValues.id, {
|
||||||
|
type: MessageTypes.ACCOUNT_DELETED,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -270,9 +270,11 @@ class IMAPConnection extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
end() {
|
end() {
|
||||||
|
if (this._imap) {
|
||||||
|
this._imap.end();
|
||||||
|
this._imap = null;
|
||||||
|
}
|
||||||
this._queue = [];
|
this._queue = [];
|
||||||
this._imap.end();
|
|
||||||
this._imap = null;
|
|
||||||
this._connectPromise = null;
|
this._connectPromise = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,21 +1,30 @@
|
||||||
|
const os = require('os');
|
||||||
const bunyan = require('bunyan')
|
const bunyan = require('bunyan')
|
||||||
const createCWStream = require('bunyan-cloudwatch')
|
const createCWStream = require('bunyan-cloudwatch')
|
||||||
|
const PrettyStream = require('bunyan-prettystream');
|
||||||
const NODE_ENV = process.env.NODE_ENV || 'unknown'
|
const NODE_ENV = process.env.NODE_ENV || 'unknown'
|
||||||
|
|
||||||
|
|
||||||
function getLogStreams(name, env) {
|
function getLogStreams(name, env) {
|
||||||
|
if (env === 'development') {
|
||||||
|
const prettyStdOut = new PrettyStream();
|
||||||
|
prettyStdOut.pipe(process.stdout);
|
||||||
|
const stdoutStream = {
|
||||||
|
type: 'raw',
|
||||||
|
level: 'debug',
|
||||||
|
stream: prettyStdOut,
|
||||||
|
}
|
||||||
|
return [stdoutStream]
|
||||||
|
}
|
||||||
|
|
||||||
const stdoutStream = {
|
const stdoutStream = {
|
||||||
stream: process.stdout,
|
stream: process.stdout,
|
||||||
level: 'info',
|
level: 'info',
|
||||||
}
|
}
|
||||||
if (env === 'development') {
|
|
||||||
return [stdoutStream]
|
|
||||||
}
|
|
||||||
|
|
||||||
const cloudwatchStream = {
|
const cloudwatchStream = {
|
||||||
stream: createCWStream({
|
stream: createCWStream({
|
||||||
logGroupName: `k2-${env}`,
|
logGroupName: `k2-${env}`,
|
||||||
logStreamName: `${name}-${env}`,
|
logStreamName: `${name}-${env}-${os.hostname()}`,
|
||||||
cloudWatchLogsOptions: {
|
cloudWatchLogsOptions: {
|
||||||
region: 'us-east-1',
|
region: 'us-east-1',
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
ACCOUNT_CREATED: "ACCOUNT_CREATED",
|
ACCOUNT_CREATED: "ACCOUNT_CREATED",
|
||||||
ACCOUNT_UPDATED: "ACCOUNT_UPDATED",
|
ACCOUNT_UPDATED: "ACCOUNT_UPDATED",
|
||||||
|
ACCOUNT_DELETED: "ACCOUNT_DELETED",
|
||||||
SYNCBACK_REQUESTED: "SYNCBACK_REQUESTED",
|
SYNCBACK_REQUESTED: "SYNCBACK_REQUESTED",
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,17 +4,36 @@ const ReactDOM = window.ReactDOM;
|
||||||
class SyncGraph extends React.Component {
|
class SyncGraph extends React.Component {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.drawGraph();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate() {
|
|
||||||
this.drawGraph(true);
|
this.drawGraph(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
drawGraph(isUpdate) {
|
componentDidUpdate() {
|
||||||
|
this.drawGraph(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
drawGraph(isInitial) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const config = SyncGraph.config;
|
const config = SyncGraph.config;
|
||||||
const context = ReactDOM.findDOMNode(this).getContext('2d');
|
const node = ReactDOM.findDOMNode(this);
|
||||||
|
const context = node.getContext('2d');
|
||||||
|
|
||||||
|
if (isInitial) {
|
||||||
|
const totalHeight = config.height + config.labelFontSize + config.labelTopMargin;
|
||||||
|
node.width = config.width * 2;
|
||||||
|
node.height = totalHeight * 2;
|
||||||
|
node.style.width = `${config.width}px`;
|
||||||
|
node.style.height = `${totalHeight}px`;
|
||||||
|
context.scale(2, 2);
|
||||||
|
|
||||||
|
// Axis labels
|
||||||
|
context.fillStyle = config.labelColor;
|
||||||
|
context.font = `${config.labelFontSize}px sans-serif`;
|
||||||
|
const fontY = config.height + config.labelFontSize + config.labelTopMargin;
|
||||||
|
const nowText = "now";
|
||||||
|
const nowWidth = context.measureText(nowText).width;
|
||||||
|
context.fillText(nowText, config.width - nowWidth - 1, fontY);
|
||||||
|
context.fillText("-30m", 1, fontY);
|
||||||
|
}
|
||||||
|
|
||||||
// Background
|
// Background
|
||||||
// (This hides any previous data points, so we don't have to clear the canvas)
|
// (This hides any previous data points, so we don't have to clear the canvas)
|
||||||
|
@ -25,6 +44,7 @@ class SyncGraph extends React.Component {
|
||||||
const pxPerSec = config.width / config.timeLength;
|
const pxPerSec = config.width / config.timeLength;
|
||||||
context.strokeStyle = config.dataColor;
|
context.strokeStyle = config.dataColor;
|
||||||
context.beginPath();
|
context.beginPath();
|
||||||
|
|
||||||
for (const syncTimeMs of this.props.syncTimestamps) {
|
for (const syncTimeMs of this.props.syncTimestamps) {
|
||||||
const secsAgo = (now - syncTimeMs) / 1000;
|
const secsAgo = (now - syncTimeMs) / 1000;
|
||||||
const pxFromRight = secsAgo * pxPerSec;
|
const pxFromRight = secsAgo * pxPerSec;
|
||||||
|
@ -43,17 +63,6 @@ class SyncGraph extends React.Component {
|
||||||
context.lineTo(px, config.height);
|
context.lineTo(px, config.height);
|
||||||
}
|
}
|
||||||
context.stroke();
|
context.stroke();
|
||||||
|
|
||||||
// Axis labels
|
|
||||||
if (!isUpdate) { // only draw these on the initial render
|
|
||||||
context.fillStyle = config.labelColor;
|
|
||||||
context.font = `${config.labelFontSize}px sans-serif`;
|
|
||||||
const fontY = config.height + config.labelFontSize + config.labelTopMargin;
|
|
||||||
const nowText = "now";
|
|
||||||
const nowWidth = context.measureText(nowText).width;
|
|
||||||
context.fillText(nowText, config.width - nowWidth - 1, fontY);
|
|
||||||
context.fillText("-30m", 1, fontY);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -82,7 +91,7 @@ SyncGraph.config = {
|
||||||
labelTopMargin: 2,
|
labelTopMargin: 2,
|
||||||
labelColor: 'black',
|
labelColor: 'black',
|
||||||
backgroundColor: 'black',
|
backgroundColor: 'black',
|
||||||
dataColor: 'blue',
|
dataColor: '#43a1ff',
|
||||||
}
|
}
|
||||||
|
|
||||||
SyncGraph.propTypes = {
|
SyncGraph.propTypes = {
|
||||||
|
|
|
@ -5,6 +5,10 @@ const SyncProcessManager = require('./sync-process-manager');
|
||||||
|
|
||||||
global.Logger = Logger.createLogger('nylas-k2-sync')
|
global.Logger = Logger.createLogger('nylas-k2-sync')
|
||||||
|
|
||||||
|
const onUnhandledError = (err) => global.Logger.fatal(err, 'Unhandled error')
|
||||||
|
process.on('uncaughtException', onUnhandledError)
|
||||||
|
process.on('unhandledRejection', onUnhandledError)
|
||||||
|
|
||||||
const manager = new SyncProcessManager();
|
const manager = new SyncProcessManager();
|
||||||
|
|
||||||
DatabaseConnector.forShared().then((db) => {
|
DatabaseConnector.forShared().then((db) => {
|
||||||
|
|
|
@ -61,13 +61,13 @@ class SyncProcessManager {
|
||||||
updateHeartbeat() {
|
updateHeartbeat() {
|
||||||
const key = HEARTBEAT_FOR(IDENTITY);
|
const key = HEARTBEAT_FOR(IDENTITY);
|
||||||
const client = PubsubConnector.broadcastClient();
|
const client = PubsubConnector.broadcastClient();
|
||||||
client.setAsync(key, Date.now()).then(() =>
|
client.setAsync(key, Date.now())
|
||||||
client.expireAsync(key, HEARTBEAT_EXPIRES)
|
.then(() => client.expireAsync(key, HEARTBEAT_EXPIRES))
|
||||||
).then(() =>
|
.then(() => {
|
||||||
this._logger.info({
|
this._logger.info({
|
||||||
accounts_syncing_count: Object.keys(this._workers).length,
|
accounts_syncing_count: Object.keys(this._workers).length,
|
||||||
}, "ProcessManager: 💘")
|
}, "ProcessManager: 💘")
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onSigInt() {
|
onSigInt() {
|
||||||
|
@ -180,6 +180,7 @@ class SyncProcessManager {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._logger.info({account_id: accountId}, `ProcessManager: Starting worker for Account`)
|
this._logger.info({account_id: accountId}, `ProcessManager: Starting worker for Account`)
|
||||||
|
|
||||||
this._workers[account.id] = new SyncWorker(account, db, () => {
|
this._workers[account.id] = new SyncWorker(account, db, () => {
|
||||||
this.removeWorkerForAccountId(accountId)
|
this.removeWorkerForAccountId(accountId)
|
||||||
});
|
});
|
||||||
|
|
|
@ -27,7 +27,6 @@ class SyncWorker {
|
||||||
this._logger = global.Logger.forAccount(account)
|
this._logger = global.Logger.forAccount(account)
|
||||||
|
|
||||||
this._syncTimer = null;
|
this._syncTimer = null;
|
||||||
this._expirationTimer = null;
|
|
||||||
this._destroyed = false;
|
this._destroyed = false;
|
||||||
|
|
||||||
this.syncNow({reason: 'Initial'});
|
this.syncNow({reason: 'Initial'});
|
||||||
|
@ -37,6 +36,8 @@ class SyncWorker {
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
|
clearTimeout(this._syncTimer);
|
||||||
|
this._syncTimer = null;
|
||||||
this._destroyed = true;
|
this._destroyed = true;
|
||||||
this._listener.dispose();
|
this._listener.dispose();
|
||||||
this.closeConnection()
|
this.closeConnection()
|
||||||
|
@ -51,13 +52,19 @@ class SyncWorker {
|
||||||
_onMessage(msg) {
|
_onMessage(msg) {
|
||||||
const {type} = JSON.parse(msg);
|
const {type} = JSON.parse(msg);
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case MessageTypes.ACCOUNT_UPDATED:
|
|
||||||
this._onAccountUpdated(); break;
|
|
||||||
case MessageTypes.SYNCBACK_REQUESTED:
|
|
||||||
this.syncNow({reason: 'Syncback Action Queued'}); break;
|
|
||||||
case MessageTypes.ACCOUNT_CREATED:
|
case MessageTypes.ACCOUNT_CREATED:
|
||||||
// No other processing currently required for account creation
|
// No other processing currently required for account creation
|
||||||
break;
|
break;
|
||||||
|
case MessageTypes.ACCOUNT_UPDATED:
|
||||||
|
this._onAccountUpdated();
|
||||||
|
break;
|
||||||
|
case MessageTypes.ACCOUNT_DELETED:
|
||||||
|
this.cleanup();
|
||||||
|
this._onExpired();
|
||||||
|
break;
|
||||||
|
case MessageTypes.SYNCBACK_REQUESTED:
|
||||||
|
this.syncNow({reason: 'Syncback Action Queued'});
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
this._logger.error({message: msg}, 'SyncWorker: Invalid message')
|
this._logger.error({message: msg}, 'SyncWorker: Invalid message')
|
||||||
}
|
}
|
||||||
|
@ -208,7 +215,7 @@ class SyncWorker {
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const syncGraphTimeLength = 60 * 30; // 30 minutes, should be the same as SyncGraph.config.timeLength
|
const syncGraphTimeLength = 60 * 30; // 30 minutes, should be the same as SyncGraph.config.timeLength
|
||||||
let lastSyncCompletions = [...this._account.lastSyncCompletions]
|
let lastSyncCompletions = [].concat(this._account.lastSyncCompletions)
|
||||||
lastSyncCompletions = [now, ...lastSyncCompletions]
|
lastSyncCompletions = [now, ...lastSyncCompletions]
|
||||||
while (now - lastSyncCompletions[lastSyncCompletions.length - 1] > 1000 * syncGraphTimeLength) {
|
while (now - lastSyncCompletions[lastSyncCompletions.length - 1] > 1000 * syncGraphTimeLength) {
|
||||||
lastSyncCompletions.pop();
|
lastSyncCompletions.pop();
|
||||||
|
|
|
@ -1,8 +1,4 @@
|
||||||
apps:
|
apps:
|
||||||
- script : redis-server
|
|
||||||
name : redis
|
|
||||||
|
|
||||||
|
|
||||||
- script : packages/nylas-api/app.js
|
- script : packages/nylas-api/app.js
|
||||||
watch : ["packages"]
|
watch : ["packages"]
|
||||||
name : api
|
name : api
|
||||||
|
|
Loading…
Reference in a new issue