diff --git a/internal_packages/account-error-header/lib/main.es6 b/internal_packages/account-error-header/lib/main.es6
deleted file mode 100644
index 51f23443b..000000000
--- a/internal_packages/account-error-header/lib/main.es6
+++ /dev/null
@@ -1,12 +0,0 @@
-import {ComponentRegistry, WorkspaceStore} from 'nylas-exports';
-import AccountErrorHeader from './account-error-header';
-
-export function activate() {
- ComponentRegistry.register(AccountErrorHeader, {location: WorkspaceStore.Sheet.Threads.Header});
-}
-
-export function serialize() {}
-
-export function deactivate() {
- ComponentRegistry.unregister(AccountErrorHeader);
-}
diff --git a/internal_packages/account-error-header/package.json b/internal_packages/account-error-header/package.json
deleted file mode 100755
index 6562099c6..000000000
--- a/internal_packages/account-error-header/package.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "name": "account-error-header",
- "version": "0.1.0",
- "main": "./lib/main",
- "description": "Header to display errors syncing the active account",
- "license": "GPL-3.0",
- "private": true,
- "engines": {
- "nylas": "*"
- },
- "dependencies": {
- }
-}
diff --git a/internal_packages/account-error-header/stylesheets/account-error-header.less b/internal_packages/account-error-header/stylesheets/account-error-header.less
deleted file mode 100644
index 47a45a88a..000000000
--- a/internal_packages/account-error-header/stylesheets/account-error-header.less
+++ /dev/null
@@ -1,22 +0,0 @@
-@import "ui-variables";
-@import "ui-mixins";
-
-
-.sync-issue.notifications-sticky {
- .notifications-sticky-item {
- background-color: initial;
- background: linear-gradient(to top, #ca2541 0%, #d55268 100%);
- .icon {
- display: inline-block;
- vertical-align: bottom;
- line-height: 16px;
- height: 100%;
- margin-right: 9px;
-
- img {
- vertical-align: initial;
- }
- }
- }
-}
-
diff --git a/internal_packages/account-error-header/assets/icon-alert-onred@1x.png b/internal_packages/notifications/assets/icon-alert-onred@1x.png
similarity index 100%
rename from internal_packages/account-error-header/assets/icon-alert-onred@1x.png
rename to internal_packages/notifications/assets/icon-alert-onred@1x.png
diff --git a/internal_packages/account-error-header/assets/icon-alert-onred@2x.png b/internal_packages/notifications/assets/icon-alert-onred@2x.png
similarity index 100%
rename from internal_packages/account-error-header/assets/icon-alert-onred@2x.png
rename to internal_packages/notifications/assets/icon-alert-onred@2x.png
diff --git a/internal_packages/account-error-header/assets/icon-alert-sourcelist@1x.png b/internal_packages/notifications/assets/icon-alert-sourcelist@1x.png
similarity index 100%
rename from internal_packages/account-error-header/assets/icon-alert-sourcelist@1x.png
rename to internal_packages/notifications/assets/icon-alert-sourcelist@1x.png
diff --git a/internal_packages/account-error-header/assets/icon-alert-sourcelist@2x.png b/internal_packages/notifications/assets/icon-alert-sourcelist@2x.png
similarity index 100%
rename from internal_packages/account-error-header/assets/icon-alert-sourcelist@2x.png
rename to internal_packages/notifications/assets/icon-alert-sourcelist@2x.png
diff --git a/internal_packages/notifications/lib/activity-sidebar-long-poll-store.coffee b/internal_packages/notifications/lib/activity-sidebar-long-poll-store.coffee
deleted file mode 100644
index 034d19871..000000000
--- a/internal_packages/notifications/lib/activity-sidebar-long-poll-store.coffee
+++ /dev/null
@@ -1,8 +0,0 @@
-{Actions} = require 'nylas-exports'
-NylasStore = require 'nylas-store'
-
-class AccountSidebarLongPollStore extends NylasStore
- constructor: ->
- @listenTo Actions.longPollReceivedRawDeltasPing, (n) => @trigger(n)
-
-module.exports = new AccountSidebarLongPollStore()
diff --git a/internal_packages/account-error-header/lib/account-error-header.jsx b/internal_packages/notifications/lib/headers/account-error-header.jsx
similarity index 90%
rename from internal_packages/account-error-header/lib/account-error-header.jsx
rename to internal_packages/notifications/lib/headers/account-error-header.jsx
index c6ec5d39f..404a83a62 100644
--- a/internal_packages/account-error-header/lib/account-error-header.jsx
+++ b/internal_packages/notifications/lib/headers/account-error-header.jsx
@@ -38,15 +38,16 @@ export default class AccountErrorHeader extends React.Component {
renderErrorHeader(message, buttonName, actionCallback) {
return (
-
+
-
+
+
+ {message}
+
{buttonName}
diff --git a/internal_packages/notifications/lib/headers/connection-status-header.jsx b/internal_packages/notifications/lib/headers/connection-status-header.jsx
new file mode 100644
index 000000000..b612036cd
--- /dev/null
+++ b/internal_packages/notifications/lib/headers/connection-status-header.jsx
@@ -0,0 +1,101 @@
+import {NylasSyncStatusStore, React, Actions} from 'nylas-exports';
+import {RetinaImg} from 'nylas-component-kit';
+
+export default class ConnectionStatusHeader extends React.Component {
+ static displayName = 'ConnectionStatusHeader';
+
+ constructor() {
+ super();
+ this._updateInterval = null;
+ this.state = this.getStateFromStores();
+ }
+
+ componentDidMount() {
+ this.unsubscribe = NylasSyncStatusStore.listen(()=> {
+ const nextState = this.getStateFromStores();
+ if ((nextState.connected !== this.state.connected) || (nextState.nextRetryText !== this.state.nextRetryText)) {
+ this.setState(nextState);
+ }
+ });
+
+ window.addEventListener('browser-window-focus', this.onWindowFocusChanged);
+ window.addEventListener('browser-window-blur', this.onWindowFocusChanged);
+ this.ensureCountdownInterval();
+ }
+
+ componentDidUpdate() {
+ this.ensureCountdownInterval();
+ }
+
+ componentWillUnmount() {
+ window.removeEventListener('browser-window-focus', this.onWindowFocusChanged);
+ window.removeEventListener('browser-window-blur', this.onWindowFocusChanged);
+ }
+
+ onTryAgain = () => {
+ Actions.retrySync();
+ }
+
+ onWindowFocusChanged = () => {
+ this.setState(this.getStateFromStores());
+ this.ensureCountdownInterval();
+ }
+
+ getStateFromStores() {
+ const nextRetryTimestamp = NylasSyncStatusStore.nextRetryTimestamp();
+ const connected = NylasSyncStatusStore.connected();
+
+ let nextRetryText = null;
+ if (!connected) {
+ if (document.body.classList.contains('is-blurred')) {
+ nextRetryText = 'soon';
+ } else {
+ const seconds = Math.ceil((nextRetryTimestamp - Date.now()) / 1000.0);
+ if (seconds > 1) {
+ nextRetryText = `in ${seconds} seconds`;
+ } else {
+ nextRetryText = `now`;
+ }
+ }
+ }
+ return {connected, nextRetryText};
+ }
+
+ ensureCountdownInterval = () => {
+ if (this._updateInterval) {
+ clearInterval(this._updateInterval);
+ }
+ // only count down the "Reconnecting in..." label if the window is in the
+ // foreground to avoid the battery hit.
+ if (!this.state.connected && !document.body.classList.contains('is-blurred')) {
+ this._updateInterval = setInterval(() => {
+ this.setState(this.getStateFromStores());
+ }, 1000);
+ }
+ }
+
+ render() {
+ const {connected, nextRetryText} = this.state;
+
+ if (connected) {
+ return (
);
+ }
+
+ return (
+
+
+
+
+ Nylas N1 isn't able to reach api.nylas.com. Retrying {nextRetryText}.
+
+
+ Try Again Now
+
+
+
+ );
+ }
+}
diff --git a/internal_packages/notifications/lib/headers/notifications-header.cjsx b/internal_packages/notifications/lib/headers/notifications-header.cjsx
new file mode 100644
index 000000000..923d9d121
--- /dev/null
+++ b/internal_packages/notifications/lib/headers/notifications-header.cjsx
@@ -0,0 +1,40 @@
+React = require 'react'
+NotificationStore = require '../notifications-store'
+NotificationsItem = require './notifications-item'
+
+class NotificationsHeader extends React.Component
+ @displayName: "NotificationsHeader"
+
+ @containerRequired: false
+
+ constructor: (@props) ->
+ @state = @_getStateFromStores()
+
+ _getStateFromStores: =>
+ items: NotificationStore.stickyNotifications()
+
+ _onDataChanged: =>
+ @setState @_getStateFromStores()
+
+ componentDidMount: =>
+ @_unlistener = NotificationStore.listen(@_onDataChanged, @)
+ @
+
+ # It's important that every React class explicitly stops listening to
+ # N1 events before it unmounts. Thank you event-kit
+ # This can be fixed via a Reflux mixin
+ componentWillUnmount: =>
+ @_unlistener() if @_unlistener
+ @
+
+ render: =>
+
+ {@_notificationComponents()}
+
+
+ _notificationComponents: =>
+ @state.items.map (notif) ->
+
+
+
+module.exports = NotificationsHeader
diff --git a/internal_packages/notifications/lib/headers/notifications-item.cjsx b/internal_packages/notifications/lib/headers/notifications-item.cjsx
new file mode 100644
index 000000000..383d3c359
--- /dev/null
+++ b/internal_packages/notifications/lib/headers/notifications-item.cjsx
@@ -0,0 +1,39 @@
+React = require 'react'
+{Actions} = require 'nylas-exports'
+
+class NotificationsItem extends React.Component
+ @displayName: "NotificationsItem"
+
+ render: =>
+ notif = @props.notification
+ iconClass = if notif.icon then "fa #{notif.icon}" else ""
+ actionDefault = null
+ actionComponents = notif.actions?.map (action) =>
+ classname = "action "
+ if action.default
+ actionDefault = action
+ classname += "default"
+
+ actionClick = (event) =>
+ @_fireItemAction(notif, action)
+ event.stopPropagation()
+ event.preventDefault()
+
+
+ {action.label}
+
+
+ if actionDefault
+
@_fireItemAction(notif, actionDefault)}>
+
{notif.message}
{actionComponents}
+
+ else
+
+
{notif.message}
{actionComponents}
+
+
+ _fireItemAction: (notification, action) =>
+ Actions.notificationActionTaken({notification, action})
+
+module.exports = NotificationsItem
diff --git a/internal_packages/notifications/lib/main.cjsx b/internal_packages/notifications/lib/main.cjsx
deleted file mode 100644
index e8e955a1f..000000000
--- a/internal_packages/notifications/lib/main.cjsx
+++ /dev/null
@@ -1,21 +0,0 @@
-React = require "react"
-ActivitySidebar = require "./activity-sidebar"
-NotificationStore = require './notifications-store'
-NotificationsStickyBar = require "./notifications-sticky-bar"
-{ComponentRegistry, WorkspaceStore} = require("nylas-exports")
-
-module.exports =
- item: null # The DOM item the main React component renders into
-
- activate: (@state={}) ->
- ComponentRegistry.register ActivitySidebar,
- location: WorkspaceStore.Location.RootSidebar
-
- ComponentRegistry.register NotificationsStickyBar,
- location: WorkspaceStore.Sheet.Global.Header
-
- deactivate: ->
- ComponentRegistry.unregister(ActivitySidebar)
- ComponentRegistry.unregister(NotificationsStickyBar)
-
- serialize: -> @state
diff --git a/internal_packages/notifications/lib/main.es6 b/internal_packages/notifications/lib/main.es6
new file mode 100644
index 000000000..4c1e59b9e
--- /dev/null
+++ b/internal_packages/notifications/lib/main.es6
@@ -0,0 +1,24 @@
+/* eslint no-unused-vars:0 */
+
+import {ComponentRegistry, WorkspaceStore} from 'nylas-exports';
+import ActivitySidebar from "./sidebar/activity-sidebar";
+import NotificationStore from './notifications-store';
+import ConnectionStatusHeader from './headers/connection-status-header';
+import AccountErrorHeader from './headers/account-error-header';
+import NotificationsHeader from "./headers/notifications-header";
+
+export function activate() {
+ ComponentRegistry.register(ActivitySidebar, {location: WorkspaceStore.Location.RootSidebar});
+ ComponentRegistry.register(NotificationsHeader, {location: WorkspaceStore.Sheet.Global.Header});
+ ComponentRegistry.register(ConnectionStatusHeader, {location: WorkspaceStore.Sheet.Global.Header});
+ ComponentRegistry.register(AccountErrorHeader, {location: WorkspaceStore.Sheet.Threads.Header});
+}
+
+export function serialize() {}
+
+export function deactivate() {
+ ComponentRegistry.unregister(ActivitySidebar);
+ ComponentRegistry.unregister(NotificationsHeader);
+ ComponentRegistry.unregister(ConnectionStatusHeader);
+ ComponentRegistry.unregister(AccountErrorHeader);
+}
diff --git a/internal_packages/notifications/lib/notifications-sticky-bar.cjsx b/internal_packages/notifications/lib/notifications-sticky-bar.cjsx
deleted file mode 100644
index d2399daac..000000000
--- a/internal_packages/notifications/lib/notifications-sticky-bar.cjsx
+++ /dev/null
@@ -1,76 +0,0 @@
-React = require 'react'
-{Actions} = require 'nylas-exports'
-NotificationStore = require './notifications-store'
-
-class NotificationStickyItem extends React.Component
- @displayName: "NotificationStickyItem"
-
- render: =>
- notif = @props.notification
- iconClass = if notif.icon then "fa #{notif.icon}" else ""
- actionDefault = null
- actionComponents = notif.actions?.map (action) =>
- classname = "action "
- if action.default
- actionDefault = action
- classname += "default"
-
- actionClick = (event) =>
- @_fireItemAction(notif, action)
- event.stopPropagation()
- event.preventDefault()
-
-
- {action.label}
-
-
- if actionDefault
-
@_fireItemAction(notif, actionDefault)}>
-
{notif.message}
{actionComponents}
-
- else
-
-
{notif.message}
{actionComponents}
-
-
- _fireItemAction: (notification, action) =>
- Actions.notificationActionTaken({notification, action})
-
-
-class NotificationStickyBar extends React.Component
- @displayName: "NotificationsStickyBar"
-
- @containerRequired: false
-
- constructor: (@props) ->
- @state = @_getStateFromStores()
-
- _getStateFromStores: =>
- items: NotificationStore.stickyNotifications()
-
- _onDataChanged: =>
- @setState @_getStateFromStores()
-
- componentDidMount: =>
- @_unlistener = NotificationStore.listen(@_onDataChanged, @)
- @
-
- # It's important that every React class explicitly stops listening to
- # N1 events before it unmounts. Thank you event-kit
- # This can be fixed via a Reflux mixin
- componentWillUnmount: =>
- @_unlistener() if @_unlistener
- @
-
- render: =>
-
- {@_notificationComponents()}
-
-
- _notificationComponents: =>
- @state.items.map (notif) ->
-
-
-
-module.exports = NotificationStickyBar
diff --git a/internal_packages/notifications/lib/activity-sidebar.cjsx b/internal_packages/notifications/lib/sidebar/activity-sidebar.cjsx
similarity index 73%
rename from internal_packages/notifications/lib/activity-sidebar.cjsx
rename to internal_packages/notifications/lib/sidebar/activity-sidebar.cjsx
index 472c5cc30..61a0629fa 100644
--- a/internal_packages/notifications/lib/activity-sidebar.cjsx
+++ b/internal_packages/notifications/lib/sidebar/activity-sidebar.cjsx
@@ -4,15 +4,15 @@ ReactCSSTransitionGroup = require 'react-addons-css-transition-group'
_ = require 'underscore'
classNames = require 'classnames'
-NotificationStore = require './notifications-store'
+NotificationStore = require '../notifications-store'
+StreamingSyncActivity = require './streaming-sync-activity'
InitialSyncActivity = require './initial-sync-activity'
+
{Actions,
TaskQueue,
AccountStore,
NylasSyncStatusStore,
TaskQueueStatusStore} = require 'nylas-exports'
-ActivitySidebarLongPollStore = require './activity-sidebar-long-poll-store'
-{RetinaImg} = require 'nylas-component-kit'
class ActivitySidebar extends React.Component
@displayName: 'ActivitySidebar'
@@ -30,7 +30,6 @@ class ActivitySidebar extends React.Component
@_unlisteners.push TaskQueueStatusStore.listen @_onDataChanged
@_unlisteners.push NotificationStore.listen @_onDataChanged
@_unlisteners.push NylasSyncStatusStore.listen @_onDataChanged
- @_unlisteners.push ActivitySidebarLongPollStore.listen @_onDeltaReceived
componentWillUnmount: =>
unlisten() for unlisten in @_unlisteners
@@ -39,12 +38,10 @@ class ActivitySidebar extends React.Component
items = [@_renderNotificationActivityItems(), @_renderTaskActivityItems()]
if @state.isInitialSyncComplete
- if @state.receivingDelta
- items.push @_renderDeltaSyncActivityItem()
+ items.push
else
items.push
-
names = classNames
"sidebar-activity": true
"sidebar-activity-error": error?
@@ -87,16 +84,6 @@ class ActivitySidebar extends React.Component
- _renderDeltaSyncActivityItem: =>
-
-
-
-
-
- Syncing your mailbox…
-
-
-
_renderNotificationActivityItems: =>
@state.notifications.map (notification) ->
@@ -113,19 +100,4 @@ class ActivitySidebar extends React.Component
tasks: TaskQueueStatusStore.queue()
isInitialSyncComplete: NylasSyncStatusStore.isSyncComplete()
- _onDeltaReceived: (countDeltas) =>
- tooSmallForNotification = countDeltas <= 10
- return if tooSmallForNotification
-
- if @_timeoutId
- clearTimeout @_timeoutId
-
- @_timeoutId = setTimeout(( =>
- delete @_timeoutId
- @setState receivingDelta: false
- ), 30000)
-
- @setState receivingDelta: true
-
-
module.exports = ActivitySidebar
diff --git a/internal_packages/notifications/lib/initial-sync-activity.cjsx b/internal_packages/notifications/lib/sidebar/initial-sync-activity.cjsx
similarity index 99%
rename from internal_packages/notifications/lib/initial-sync-activity.cjsx
rename to internal_packages/notifications/lib/sidebar/initial-sync-activity.cjsx
index c8aeb3cf1..b0a95ee61 100644
--- a/internal_packages/notifications/lib/initial-sync-activity.cjsx
+++ b/internal_packages/notifications/lib/sidebar/initial-sync-activity.cjsx
@@ -108,6 +108,6 @@ class InitialSyncActivity extends React.Component
_onTryAgain: =>
- Actions.retryInitialSync()
+ Actions.retrySync()
module.exports = InitialSyncActivity
diff --git a/internal_packages/notifications/lib/sidebar/streaming-sync-activity.cjsx b/internal_packages/notifications/lib/sidebar/streaming-sync-activity.cjsx
new file mode 100644
index 000000000..9c1cf89db
--- /dev/null
+++ b/internal_packages/notifications/lib/sidebar/streaming-sync-activity.cjsx
@@ -0,0 +1,44 @@
+{Actions, React} = require 'nylas-exports'
+{RetinaImg} = require 'nylas-component-kit'
+
+class StreamingSyncActivity extends React.Component
+
+ constructor: (@props) ->
+ @_timeoutId = null
+ @state =
+ receivingDelta: false
+
+ componentDidMount: =>
+ @_unlistener = Actions.longPollReceivedRawDeltasPing.listen(@_onDeltaReceived)
+
+ componentWillUnmount: =>
+ @_unlistener() if @_unlistener
+ clearTimeout(@_timeoutId) if @_timeoutId
+
+ render: =>
+ return false unless @state.receivingDelta
+
+
+
+
+
+ Syncing your mailbox…
+
+
+
+ _onDeltaReceived: (countDeltas) =>
+ tooSmallForNotification = countDeltas <= 10
+ return if tooSmallForNotification
+
+ if @_timeoutId
+ clearTimeout(@_timeoutId)
+
+ @_timeoutId = setTimeout(( =>
+ delete(@_timeoutId)
+ @setState(receivingDelta: false)
+ ), 20000)
+
+ @setState(receivingDelta: true)
+
+
+module.exports = StreamingSyncActivity
diff --git a/internal_packages/notifications/stylesheets/notifications.less b/internal_packages/notifications/stylesheets/notifications.less
index 2dc16f7ca..cccfab416 100644
--- a/internal_packages/notifications/stylesheets/notifications.less
+++ b/internal_packages/notifications/stylesheets/notifications.less
@@ -126,7 +126,6 @@
opacity:0;
}
-
.notifications-sticky {
width:100%;
@@ -144,6 +143,10 @@
.notification-success {
border-color: @background-color-success;
}
+ .notification-offline {
+ background-color: #CC9900;
+ border-color: darken(#CC9900, 5%);
+ }
.notifications-sticky-item {
display:flex;
@@ -173,11 +176,23 @@
i {
margin-right:@padding-base-horizontal;
}
- div {
+ .icon {
+ display: inline-block;
+ align-self: center;
+ line-height: 16px;
+ margin-right:@padding-base-horizontal;
+
+ img {
+ vertical-align: initial;
+ }
+ }
+
+ div.message {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
line-height: @line-height-base * 1.1;
+ padding: @padding-small-vertical 0;
}
&.has-default-action:hover {
@@ -186,6 +201,9 @@
}
}
}
+
+// Windows Changes
+
body.platform-win32 {
.notifications-sticky {
.notifications-sticky-item {
@@ -195,3 +213,12 @@ body.platform-win32 {
}
}
}
+
+// Activity Error Header
+
+.account-error-header.notifications-sticky {
+ .notifications-sticky-item {
+ background-color: initial;
+ background: linear-gradient(to top, #ca2541 0%, #d55268 100%);
+ }
+}
diff --git a/internal_packages/worker-sync/lib/nylas-long-connection.coffee b/internal_packages/worker-sync/lib/nylas-long-connection.coffee
index e76333d33..338ff52a9 100644
--- a/internal_packages/worker-sync/lib/nylas-long-connection.coffee
+++ b/internal_packages/worker-sync/lib/nylas-long-connection.coffee
@@ -4,21 +4,21 @@ _ = require 'underscore'
class NylasLongConnection
- @State =
- Idle: 'idle'
- Ended: 'ended'
+ @Status =
+ None: 'none'
Connecting: 'connecting'
Connected: 'connected'
- Retrying: 'retrying'
+ Closed: 'closed' # Socket has been closed for any reason
+ Ended: 'ended' # We have received 'end()' and will never open again.
constructor: (api, accountId, config) ->
@_api = api
@_accountId = accountId
@_config = config
@_emitter = new Emitter
- @_state = 'idle'
+ @_status = NylasLongConnection.Status.None
@_req = null
- @_reqForceReconnectInterval = null
+ @_pingTimeout = null
@_buffer = null
@_deltas = []
@@ -54,15 +54,13 @@ class NylasLongConnection
@_config.setCursor(cursor)
callback(cursor)
- state: ->
- @state
+ status: ->
+ @status
- setState: (state) ->
- @_state = state
- @_emitter.emit('state-change', state)
-
- onStateChange: (callback) ->
- @_emitter.on('state-change', callback)
+ setStatus: (status) ->
+ return if @_status is status
+ @_status = status
+ @_config.setStatus(status)
onDeltas: (callback) ->
@_emitter.on('deltas-stopped-arriving', callback)
@@ -89,15 +87,15 @@ class NylasLongConnection
start: ->
return unless @_config.ready()
+ return unless @_status in [NylasLongConnection.Status.None, NylasLongConnection.Status.Closed]
token = @_api.accessTokenForAccountId(@_accountId)
return if not token?
- return if @_state is NylasLongConnection.State.Ended
return if @_req
@withCursor (cursor) =>
- return if @state is NylasLongConnection.State.Ended
- console.log("Delta Connection: Starting for account #{@_accountId}, token #{token}, with cursor #{cursor}")
+ return if @status is NylasLongConnection.Status.Ended
+
options = url.parse("#{@_api.APIRoot}/delta/streaming?cursor=#{cursor}&exclude_folders=false&exclude_metadata=false&exclude_account=false")
options.auth = "#{token}:"
@@ -107,62 +105,58 @@ class NylasLongConnection
options.port = 443
lib = require 'https'
- req = lib.request options, (res) =>
+ @_req = lib.request options, (res) =>
if res.statusCode isnt 200
res.on 'data', (chunk) =>
if chunk.toString().indexOf('Invalid cursor') > 0
console.log('Delta Connection: Cursor is invalid. Need to blow away local cache.')
# TODO THIS!
else
- @retry()
+ @close()
return
@_buffer = ''
processBufferThrottled = _.throttle(@onProcessBuffer, 400, {leading: false})
res.setEncoding('utf8')
- res.on 'close', => @retry()
+ res.on 'close', => @close()
res.on 'data', (chunk) =>
+ @closeIfDataStops()
# Ignore redundant newlines sent as pings. Want to avoid
# calls to @onProcessBuffer that contain no actual updates
return if chunk is '\n' and (@_buffer.length is 0 or @_buffer[-1] is '\n')
@_buffer += chunk
processBufferThrottled()
- req.setTimeout(60*60*1000)
- req.setSocketKeepAlive(true)
- req.on 'error', => @retry()
- req.on 'socket', (socket) =>
- @setState(NylasLongConnection.State.Connecting)
+ @_req.setTimeout(60*60*1000)
+ @_req.setSocketKeepAlive(true)
+ @_req.on 'error', => @close()
+ @_req.on 'socket', (socket) =>
+ @setStatus(NylasLongConnection.Status.Connecting)
socket.on 'connect', =>
- @setState(NylasLongConnection.State.Connected)
- req.write("1")
+ @setStatus(NylasLongConnection.Status.Connected)
+ @closeIfDataStops()
+ @_req.write("1")
- @_req = req
- # Currently we have trouble identifying when the connection has closed.
- # Instead of trying to fix that, just reconnect every 120 seconds.
- @_reqForceReconnectInterval = setInterval =>
- @retry(true)
- ,30000
-
- retry: (immediate = false) ->
- return if @_state is NylasLongConnection.State.Ended
- @setState(NylasLongConnection.State.Retrying)
+ close: ->
+ return if @_status is NylasLongConnection.Status.Closed
+ @setStatus(NylasLongConnection.Status.Closed)
@cleanup()
- startDelay = if immediate then 0 else 10000
- setTimeout =>
- @start()
- , startDelay
+ closeIfDataStops: =>
+ clearTimeout(@_pingTimeout) if @_pingTimeout
+ @_pingTimeout = setTimeout =>
+ @_pingTimeout = null
+ @close()
+ , 15 * 1000
end: ->
- console.log("Delta Connection: Closed.")
- @setState(NylasLongConnection.State.Ended)
+ @setStatus(NylasLongConnection.Status.Ended)
@cleanup()
cleanup: ->
- clearInterval(@_reqForceReconnectInterval) if @_reqForceReconnectInterval
- @_reqForceReconnectInterval = null
+ clearInterval(@_pingTimeout) if @_pingTimeout
+ @_pingTimeout = null
@_buffer = ''
if @_req
@_req.end()
diff --git a/internal_packages/worker-sync/lib/nylas-sync-worker-pool.coffee b/internal_packages/worker-sync/lib/nylas-sync-worker-pool.coffee
index 28b58eef9..c138e3b54 100644
--- a/internal_packages/worker-sync/lib/nylas-sync-worker-pool.coffee
+++ b/internal_packages/worker-sync/lib/nylas-sync-worker-pool.coffee
@@ -41,16 +41,6 @@ class NylasSyncWorkerPool
worker = new NylasSyncWorker(NylasAPI, account)
connection = worker.connection()
-
- connection.onStateChange (state) ->
- Actions.longPollStateChanged({accountId: account.id, state: state})
- if state == NylasLongConnection.State.Connected
- ## TODO use OfflineStatusStore
- Actions.longPollConnected()
- else
- ## TODO use OfflineStatusStore
- Actions.longPollOffline()
-
connection.onDeltas (deltas) =>
@_handleDeltas(deltas)
diff --git a/internal_packages/worker-sync/lib/nylas-sync-worker.coffee b/internal_packages/worker-sync/lib/nylas-sync-worker.coffee
index 696d82b23..cd7b1be8d 100644
--- a/internal_packages/worker-sync/lib/nylas-sync-worker.coffee
+++ b/internal_packages/worker-sync/lib/nylas-sync-worker.coffee
@@ -11,16 +11,12 @@ MAX_PAGE_SIZE = 200
#
class BackoffTimer
constructor: (@fn) ->
- @reset()
+ @resetDelay()
cancel: =>
clearTimeout(@_timeout) if @_timeout
@_timeout = null
- reset: =>
- @cancel()
- @_delay = 20 * 1000
-
backoff: =>
@_delay = Math.min(@_delay * 1.4, 5 * 1000 * 60) # Cap at 5 minutes
if not NylasEnv.inSpecMode()
@@ -33,6 +29,12 @@ class BackoffTimer
@fn()
, @_delay
+ resetDelay: =>
+ @_delay = 20 * 1000
+
+ getCurrentDelay: =>
+ @_delay
+
module.exports =
class NylasSyncWorker
@@ -41,31 +43,37 @@ class NylasSyncWorker
@_api = api
@_account = account
+ # indirection needed so resumeFetches can be spied on
+ @_resumeTimer = new BackoffTimer => @resume()
+ @_refreshingCaches = [new ContactRankingsCache(account.id)]
+
@_terminated = false
@_connection = new NylasLongConnection(api, account.id, {
ready: => @_state isnt null
getCursor: =>
return null if @_state is null
- @_state['cursor'] || NylasEnv.config.get("nylas.#{@_account.id}.cursor")
+ @_state.cursor || NylasEnv.config.get("nylas.#{@_account.id}.cursor")
setCursor: (val) =>
- @_state['cursor'] = val
+ @_state.cursor = val
+ @writeState()
+ setStatus: (status) =>
+ @_state.longConnectionStatus = status
+ if status is NylasLongConnection.Status.Closed
+ @_backoff()
+ if status is NylasLongConnection.Status.Connected
+ @_resumeTimer.resetDelay()
@writeState()
})
- @_refreshingCaches = [new ContactRankingsCache(account.id)]
- @_resumeTimer = new BackoffTimer =>
- # indirection needed so resumeFetches can be spied on
- @resumeFetches()
-
- @_unlisten = Actions.retryInitialSync.listen(@_onRetryInitialSync, @)
+ @_unlisten = Actions.retrySync.listen(@_onRetrySync, @)
@_state = null
DatabaseStore.findJSONBlob("NylasSyncWorker:#{@_account.id}").then (json) =>
@_state = json ? {}
+ @_state.longConnectionStatus = NylasLongConnection.Status.Idle
for key in ['threads', 'labels', 'folders', 'drafts', 'contacts', 'calendars', 'events']
@_state[key].busy = false if @_state[key]
- @resumeFetches()
- @_connection.start()
+ @resume()
@
@@ -89,7 +97,7 @@ class NylasSyncWorker
@_resumeTimer.start()
@_connection.start()
@_refreshingCaches.map (c) -> c.start()
- @resumeFetches()
+ @resume()
cleanup: ->
@_unlisten?()
@@ -99,9 +107,11 @@ class NylasSyncWorker
@_terminated = true
@
- resumeFetches: =>
+ resume: =>
return unless @_state
+ @_connection.start()
+
# Stop the timer. If one or more network requests fails during the fetch process
# we'll backoff and restart the timer.
@_resumeTimer.cancel()
@@ -239,13 +249,11 @@ class NylasSyncWorker
success(response) if success
error: (err) =>
return if @_terminated
- @_resumeTimer.backoff()
- @_resumeTimer.start()
+ @_backoff()
error(err) if error
_fetchCollectionPageError: (model, params, err) ->
- @_resumeTimer.backoff()
- @_resumeTimer.start()
+ @_backoff()
@updateTransferState(model, {
busy: false,
complete: false,
@@ -253,6 +261,11 @@ class NylasSyncWorker
errorRequestRange: {offset: params.offset, limit: params.limit}
})
+ _backoff: =>
+ @_resumeTimer.backoff()
+ @_resumeTimer.start()
+ @_state.nextRetryTimestamp = Date.now() + @_resumeTimer.getCurrentDelay()
+
updateTransferState: (model, updatedKeys) ->
@_state[model] = _.extend(@_state[model], updatedKeys)
@writeState()
@@ -264,8 +277,9 @@ class NylasSyncWorker
,100
@_writeState()
- _onRetryInitialSync: =>
- @resumeFetches()
+ _onRetrySync: =>
+ @_resumeTimer.resetDelay()
+ @resume()
NylasSyncWorker.BackoffTimer = BackoffTimer
NylasSyncWorker.INITIAL_PAGE_SIZE = INITIAL_PAGE_SIZE
diff --git a/internal_packages/worker-sync/spec/nylas-sync-worker-spec.coffee b/internal_packages/worker-sync/spec/nylas-sync-worker-spec.coffee
index 2fd60267a..ec5a9b1c1 100644
--- a/internal_packages/worker-sync/spec/nylas-sync-worker-spec.coffee
+++ b/internal_packages/worker-sync/spec/nylas-sync-worker-spec.coffee
@@ -7,6 +7,7 @@ describe "NylasSyncWorker", ->
beforeEach ->
@apiRequests = []
@api =
+ APIRoot: 'https://api.nylas.com'
pluginsSupported: true
accessTokenForAccountId: =>
'123'
@@ -46,7 +47,7 @@ describe "NylasSyncWorker", ->
it "should reset `busy` to false when reading state from disk", ->
@worker = new NylasSyncWorker(@api, @account)
- spyOn(@worker, 'resumeFetches')
+ spyOn(@worker, 'resume')
advanceClock()
expect(@worker.state().contacts.busy).toEqual(false)
@@ -96,42 +97,42 @@ describe "NylasSyncWorker", ->
@apiRequests[1].requestOptions.error({statusCode: 400})
@apiRequests = []
- spyOn(@worker, 'resumeFetches').andCallThrough()
+ spyOn(@worker, 'resume').andCallThrough()
@worker.start()
- expect(@worker.resumeFetches.callCount).toBe(1)
- simulateNetworkFailure(); expect(@worker.resumeFetches.callCount).toBe(1)
- advanceClock(30000); expect(@worker.resumeFetches.callCount).toBe(2)
- simulateNetworkFailure(); expect(@worker.resumeFetches.callCount).toBe(2)
- advanceClock(30000); expect(@worker.resumeFetches.callCount).toBe(2)
- advanceClock(30000); expect(@worker.resumeFetches.callCount).toBe(3)
- simulateNetworkFailure(); expect(@worker.resumeFetches.callCount).toBe(3)
- advanceClock(30000); expect(@worker.resumeFetches.callCount).toBe(3)
- advanceClock(30000); expect(@worker.resumeFetches.callCount).toBe(4)
- simulateNetworkFailure(); expect(@worker.resumeFetches.callCount).toBe(4)
- advanceClock(30000); expect(@worker.resumeFetches.callCount).toBe(4)
- advanceClock(30000); expect(@worker.resumeFetches.callCount).toBe(4)
- advanceClock(30000); expect(@worker.resumeFetches.callCount).toBe(5)
+ expect(@worker.resume.callCount).toBe(1)
+ simulateNetworkFailure(); expect(@worker.resume.callCount).toBe(1)
+ advanceClock(30000); expect(@worker.resume.callCount).toBe(2)
+ simulateNetworkFailure(); expect(@worker.resume.callCount).toBe(2)
+ advanceClock(30000); expect(@worker.resume.callCount).toBe(2)
+ advanceClock(30000); expect(@worker.resume.callCount).toBe(3)
+ simulateNetworkFailure(); expect(@worker.resume.callCount).toBe(3)
+ advanceClock(30000); expect(@worker.resume.callCount).toBe(3)
+ advanceClock(30000); expect(@worker.resume.callCount).toBe(4)
+ simulateNetworkFailure(); expect(@worker.resume.callCount).toBe(4)
+ advanceClock(30000); expect(@worker.resume.callCount).toBe(4)
+ advanceClock(30000); expect(@worker.resume.callCount).toBe(4)
+ advanceClock(30000); expect(@worker.resume.callCount).toBe(5)
it "handles the request as a failure if we try and grab labels or folders without an 'inbox'", ->
- spyOn(@worker, 'resumeFetches').andCallThrough()
+ spyOn(@worker, 'resume').andCallThrough()
@worker.start()
- expect(@worker.resumeFetches.callCount).toBe(1)
+ expect(@worker.resume.callCount).toBe(1)
request = _.findWhere(@apiRequests, model: 'labels')
request.requestOptions.success([])
- expect(@worker.resumeFetches.callCount).toBe(1)
+ expect(@worker.resume.callCount).toBe(1)
advanceClock(30000)
- expect(@worker.resumeFetches.callCount).toBe(2)
+ expect(@worker.resume.callCount).toBe(2)
it "handles the request as a success if we try and grab labels or folders and it includes the 'inbox'", ->
- spyOn(@worker, 'resumeFetches').andCallThrough()
+ spyOn(@worker, 'resume').andCallThrough()
@worker.start()
- expect(@worker.resumeFetches.callCount).toBe(1)
+ expect(@worker.resume.callCount).toBe(1)
request = _.findWhere(@apiRequests, model: 'labels')
request.requestOptions.success([{name: "inbox"}, {name: "archive"}])
- expect(@worker.resumeFetches.callCount).toBe(1)
+ expect(@worker.resume.callCount).toBe(1)
advanceClock(30000)
- expect(@worker.resumeFetches.callCount).toBe(1)
+ expect(@worker.resume.callCount).toBe(1)
describe "delta streaming cursor", ->
it "should read the cursor from the database, and the old config format", ->
@@ -179,7 +180,7 @@ describe "NylasSyncWorker", ->
nextState = @worker.state()
expect(nextState.threads.count).toEqual(1001)
- describe "resumeFetches", ->
+ describe "resume", ->
it "should fetch metadata first and fetch other collections when metadata is ready", ->
fetchAllMetadataCallback = null
jasmine.unspy(NylasSyncWorker.prototype, 'fetchAllMetadata')
@@ -187,7 +188,7 @@ describe "NylasSyncWorker", ->
fetchAllMetadataCallback = cb
spyOn(@worker, 'fetchCollection')
@worker._state = {}
- @worker.resumeFetches()
+ @worker.resume()
expect(@worker.fetchAllMetadata).toHaveBeenCalled()
expect(@worker.fetchCollection.calls.length).toBe(0)
fetchAllMetadataCallback()
@@ -198,7 +199,7 @@ describe "NylasSyncWorker", ->
spyOn(NylasSyncWorker.prototype, '_fetchWithErrorHandling')
spyOn(@worker, 'fetchCollection')
@worker._state = {}
- @worker.resumeFetches()
+ @worker.resume()
expect(@worker._fetchWithErrorHandling).not.toHaveBeenCalled()
expect(@worker.fetchCollection.calls.length).not.toBe(0)
@@ -206,13 +207,13 @@ describe "NylasSyncWorker", ->
spyOn(@worker, 'fetchCollection')
spyOn(@worker, 'shouldFetchCollection').andCallFake (collection) =>
return collection in ['threads', 'labels', 'drafts']
- @worker.resumeFetches()
+ @worker.resume()
expect(@worker.fetchCollection.calls.map (call) -> call.args[0]).toEqual(['threads', 'labels', 'drafts'])
- it "should be called when Actions.retryInitialSync is received", ->
- spyOn(@worker, 'resumeFetches').andCallThrough()
- Actions.retryInitialSync()
- expect(@worker.resumeFetches).toHaveBeenCalled()
+ it "should be called when Actions.retrySync is received", ->
+ spyOn(@worker, 'resume').andCallThrough()
+ Actions.retrySync()
+ expect(@worker.resume).toHaveBeenCalled()
describe "shouldFetchCollection", ->
it "should return false if the collection sync is already in progress", ->
@@ -398,7 +399,7 @@ describe "NylasSyncWorker", ->
it "should stop trying to restart failed collection syncs", ->
spyOn(console, 'log')
- spyOn(@worker, 'resumeFetches').andCallThrough()
+ spyOn(@worker, 'resume').andCallThrough()
@worker.cleanup()
advanceClock(50000)
- expect(@worker.resumeFetches.callCount).toBe(0)
+ expect(@worker.resume.callCount).toBe(0)
diff --git a/internal_packages/worker-ui/lib/developer-bar-store.coffee b/internal_packages/worker-ui/lib/developer-bar-store.coffee
index 49f924d6a..dad9279d4 100644
--- a/internal_packages/worker-ui/lib/developer-bar-store.coffee
+++ b/internal_packages/worker-ui/lib/developer-bar-store.coffee
@@ -1,5 +1,5 @@
NylasStore = require 'nylas-store'
-{Actions} = require 'nylas-exports'
+{Actions, NylasSyncStatusStore} = require 'nylas-exports'
qs = require 'querystring'
_ = require 'underscore'
moment = require 'moment'
@@ -55,11 +55,11 @@ class DeveloperBarStore extends NylasStore
@_longPollState = {}
_registerListeners: ->
+ @listenTo NylasSyncStatusStore, @_onSyncStatusChanged
@listenTo Actions.willMakeAPIRequest, @_onWillMakeAPIRequest
@listenTo Actions.didMakeAPIRequest, @_onDidMakeAPIRequest
@listenTo Actions.longPollReceivedRawDeltas, @_onLongPollDeltas
@listenTo Actions.longPollProcessedDeltas, @_onLongPollProcessedDeltas
- @listenTo Actions.longPollStateChanged, @_onLongPollStateChange
@listenTo Actions.clearDeveloperConsole, @_onClear
_onClear: ->
@@ -68,6 +68,12 @@ class DeveloperBarStore extends NylasStore
@_longPollHistory = []
@trigger(@)
+ _onSyncStatusChanged: ->
+ @_longPollState = {}
+ _.forEach NylasSyncStatusStore.state(), (state, accountId) =>
+ @_longPollState[accountId] = state.longConnectionStatus
+ @trigger()
+
_onLongPollDeltas: (deltas) ->
# Add a local timestamp to deltas so we can display it
now = new Date()
diff --git a/internal_packages/worker-ui/stylesheets/worker-ui.less b/internal_packages/worker-ui/stylesheets/worker-ui.less
index eceab4969..5cbaaed12 100755
--- a/internal_packages/worker-ui/stylesheets/worker-ui.less
+++ b/internal_packages/worker-ui/stylesheets/worker-ui.less
@@ -79,13 +79,12 @@
&.state-connecting {
background-color:#aff2a7;
}
- &.state-connected,
- &.state-running {
+ &.state-connected {
background-color:#94E864;
}
- &.state-paused,
- &.state-idle,
- &.state-retrying, {
+ &.state-none,
+ &.state-closed,
+ &.state-ended, {
background-color:gray;
}
}
diff --git a/spec/stores/task-queue-spec.coffee b/spec/stores/task-queue-spec.coffee
index 23c3fe12d..77b01e092 100644
--- a/spec/stores/task-queue-spec.coffee
+++ b/spec/stores/task-queue-spec.coffee
@@ -4,7 +4,6 @@ TaskQueue = require '../../src/flux/stores/task-queue'
Task = require '../../src/flux/tasks/task'
{APIError,
- OfflineError,
TimeoutError} = require '../../src/flux/errors'
class TaskSubclassA extends Task
diff --git a/spec/tasks/task-spec.coffee b/spec/tasks/task-spec.coffee
index aab468276..4fea9ae8a 100644
--- a/spec/tasks/task-spec.coffee
+++ b/spec/tasks/task-spec.coffee
@@ -3,7 +3,6 @@ TaskQueue = require '../../src/flux/stores/task-queue'
Task = require '../../src/flux/tasks/task'
{APIError,
- OfflineError,
TimeoutError} = require '../../src/flux/errors'
noop = ->
diff --git a/src/flux/actions.coffee b/src/flux/actions.coffee
index c50fcbb17..9a1241ec0 100644
--- a/src/flux/actions.coffee
+++ b/src/flux/actions.coffee
@@ -118,12 +118,9 @@ class Actions
###
@dequeueMatchingTask: ActionScopeWorkWindow
- @longPollStateChanged: ActionScopeWorkWindow
@longPollReceivedRawDeltas: ActionScopeWorkWindow
@longPollReceivedRawDeltasPing: ActionScopeGlobal
@longPollProcessedDeltas: ActionScopeWorkWindow
- @longPollConnected: ActionScopeWorkWindow
- @longPollOffline: ActionScopeWorkWindow
@willMakeAPIRequest: ActionScopeWorkWindow
@didMakeAPIRequest: ActionScopeWorkWindow
@@ -132,7 +129,7 @@ class Actions
*Scope: Work Window*
###
- @retryInitialSync: ActionScopeWorkWindow
+ @retrySync: ActionScopeWorkWindow
###
Public: Open the preferences view.
diff --git a/src/flux/errors.coffee b/src/flux/errors.coffee
index 7d3e9a6fc..01e74cf90 100644
--- a/src/flux/errors.coffee
+++ b/src/flux/errors.coffee
@@ -14,13 +14,9 @@ class APIError extends Error
@name = "APIError"
@message = @body?.message ? @body ? @error?.toString?()
-class OfflineError extends Error
- constructor: ->
-
class TimeoutError extends Error
constructor: ->
module.exports =
"APIError": APIError
- "OfflineError": OfflineError
"TimeoutError": TimeoutError
diff --git a/src/flux/stores/nylas-sync-status-store.coffee b/src/flux/stores/nylas-sync-status-store.coffee
index 3d2f053b6..a8720c39c 100644
--- a/src/flux/stores/nylas-sync-status-store.coffee
+++ b/src/flux/stores/nylas-sync-status-store.coffee
@@ -45,4 +45,21 @@ class NylasSyncStatusStore extends NylasStore
return true
false
+ connected: =>
+ # Return true if any account is in a state other than `retrying`.
+ # When data isn't received, NylasLongConnection closes the socket and
+ # goes into `retrying` state.
+ statuses = _.values(@_statesByAccount).map (state) ->
+ state.longConnectionStatus
+
+ if statuses.length is 0
+ return true
+
+ return _.any statuses, (status) -> status isnt 'closed'
+
+ nextRetryTimestamp: =>
+ retryDates = _.values(@_statesByAccount).map (state) ->
+ state.nextRetryTimestamp
+ _.compact(retryDates).sort((a, b) => a < b).pop()
+
module.exports = new NylasSyncStatusStore()
diff --git a/src/flux/stores/task-queue.coffee b/src/flux/stores/task-queue.coffee
index cd0e87d52..9234be60e 100644
--- a/src/flux/stores/task-queue.coffee
+++ b/src/flux/stores/task-queue.coffee
@@ -87,8 +87,6 @@ class TaskQueue
@listenTo Actions.dequeueAllTasks, @dequeueAll
@listenTo Actions.dequeueMatchingTask, @dequeueMatching
@listenTo Actions.clearDeveloperConsole, @clearCompleted
- @listenTo Actions.longPollConnected, =>
- @_processQueue()
queue: =>
@_queue
diff --git a/src/flux/tasks/event-rsvp-task.es6 b/src/flux/tasks/event-rsvp-task.es6
index 3c57cbf85..a7f7b389d 100644
--- a/src/flux/tasks/event-rsvp-task.es6
+++ b/src/flux/tasks/event-rsvp-task.es6
@@ -59,8 +59,4 @@ export default class EventRSVPTask extends Task {
onTimeoutError() {
return Promise.resolve();
}
-
- onOfflineError() {
- return Promise.resolve();
- }
}
diff --git a/src/flux/tasks/task.es6 b/src/flux/tasks/task.es6
index 2991f5d8c..4ae07e5fb 100644
--- a/src/flux/tasks/task.es6
+++ b/src/flux/tasks/task.es6
@@ -70,13 +70,12 @@ const TaskDebugStatus = {
// All tasks should gracefully handle the case when there is no network
// connection.
//
-// if (we're offline the common behavior is for a task to:)
+// if we're offline the common behavior is for a task to:
//
// 1. Perform its local change
-// 2. Attempt the remote request and get a timeout or offline code
+// 2. Attempt the remote request, which will fail
// 3. Have `performRemote` resolve a `Task.Status.Retry`
// 3. Sit queued up waiting to be retried
-// 4. Wait for {Actions::longPollConnected} to restart the {TaskQueue}
//
// Remember that a user may be offline for hours and perform thousands of
// tasks in the meantime. It's important that your tasks implement
@@ -445,10 +444,6 @@ export default class Task {
// and tried again later. Any other task dependent on the current one
// will also continue waiting.
//
- // The queue is re-processed whenever a new task is enqueued, dequeued,
- // or the internet connection comes back online via
- // {Actions::longPollConnected}.
- //
// `Task.Status.Retry` is useful if (it looks like we're offline, or you)
// get an API error code that indicates temporary failure.
//
diff --git a/src/global/nylas-exports.coffee b/src/global/nylas-exports.coffee
index 7dd2ce68c..d74bb2398 100644
--- a/src/global/nylas-exports.coffee
+++ b/src/global/nylas-exports.coffee
@@ -197,7 +197,6 @@ class NylasExports
# Errors
@get "APIError", -> require('../flux/errors').APIError
- @get "OfflineError", -> require('../flux/errors').OfflineError
@get "TimeoutError", -> require('../flux/errors').TimeoutError
# Process Internals