From a3ede94423a03de7439c05885e4c2aa627a176b8 Mon Sep 17 00:00:00 2001 From: Ben Gotow Date: Mon, 4 Apr 2016 17:11:09 -0700 Subject: [PATCH] feat(offline-status): Show a bar when not connected to the API Summary: The TaskQueue does it's own throttling and has it's own processQueue retry timeout, no need for longPollConnected Remove dead code (OfflineError) Rename long connection state to status so we don't ask for `state.state` Remove long poll actions related to online/offline in favor of exposing connection state through NylasSyncStatusStore Consoliate notifications and account-error-heaer into a single package and organize files into sidebar vs. header. Update the DeveloperBarStore to query the sync status store for long poll statuses Test Plan: All existing tests pass Reviewers: juan, evan Reviewed By: evan Differential Revision: https://phab.nylas.com/D2835 --- .../account-error-header/lib/main.es6 | 12 --- .../account-error-header/package.json | 13 --- .../stylesheets/account-error-header.less | 22 ---- .../assets/icon-alert-onred@1x.png | Bin .../assets/icon-alert-onred@2x.png | Bin .../assets/icon-alert-sourcelist@1x.png | Bin .../assets/icon-alert-sourcelist@2x.png | Bin .../activity-sidebar-long-poll-store.coffee | 8 -- .../lib/headers}/account-error-header.jsx | 15 +-- .../lib/headers/connection-status-header.jsx | 101 ++++++++++++++++++ .../lib/headers/notifications-header.cjsx | 40 +++++++ .../lib/headers/notifications-item.cjsx | 39 +++++++ internal_packages/notifications/lib/main.cjsx | 21 ---- internal_packages/notifications/lib/main.es6 | 24 +++++ .../lib/notifications-sticky-bar.cjsx | 76 ------------- .../lib/{ => sidebar}/activity-sidebar.cjsx | 36 +------ .../{ => sidebar}/initial-sync-activity.cjsx | 2 +- .../lib/sidebar/streaming-sync-activity.cjsx | 44 ++++++++ .../stylesheets/notifications.less | 31 +++++- .../lib/nylas-long-connection.coffee | 84 +++++++-------- .../lib/nylas-sync-worker-pool.coffee | 10 -- .../worker-sync/lib/nylas-sync-worker.coffee | 60 +++++++---- .../spec/nylas-sync-worker-spec.coffee | 67 ++++++------ .../worker-ui/lib/developer-bar-store.coffee | 10 +- .../worker-ui/stylesheets/worker-ui.less | 9 +- spec/stores/task-queue-spec.coffee | 1 - spec/tasks/task-spec.coffee | 1 - src/flux/actions.coffee | 5 +- src/flux/errors.coffee | 4 - .../stores/nylas-sync-status-store.coffee | 17 +++ src/flux/stores/task-queue.coffee | 2 - src/flux/tasks/event-rsvp-task.es6 | 4 - src/flux/tasks/task.es6 | 9 +- src/global/nylas-exports.coffee | 1 - 34 files changed, 432 insertions(+), 336 deletions(-) delete mode 100644 internal_packages/account-error-header/lib/main.es6 delete mode 100755 internal_packages/account-error-header/package.json delete mode 100644 internal_packages/account-error-header/stylesheets/account-error-header.less rename internal_packages/{account-error-header => notifications}/assets/icon-alert-onred@1x.png (100%) rename internal_packages/{account-error-header => notifications}/assets/icon-alert-onred@2x.png (100%) rename internal_packages/{account-error-header => notifications}/assets/icon-alert-sourcelist@1x.png (100%) rename internal_packages/{account-error-header => notifications}/assets/icon-alert-sourcelist@2x.png (100%) delete mode 100644 internal_packages/notifications/lib/activity-sidebar-long-poll-store.coffee rename internal_packages/{account-error-header/lib => notifications/lib/headers}/account-error-header.jsx (90%) create mode 100644 internal_packages/notifications/lib/headers/connection-status-header.jsx create mode 100644 internal_packages/notifications/lib/headers/notifications-header.cjsx create mode 100644 internal_packages/notifications/lib/headers/notifications-item.cjsx delete mode 100644 internal_packages/notifications/lib/main.cjsx create mode 100644 internal_packages/notifications/lib/main.es6 delete mode 100644 internal_packages/notifications/lib/notifications-sticky-bar.cjsx rename internal_packages/notifications/lib/{ => sidebar}/activity-sidebar.cjsx (73%) rename internal_packages/notifications/lib/{ => sidebar}/initial-sync-activity.cjsx (99%) create mode 100644 internal_packages/notifications/lib/sidebar/streaming-sync-activity.cjsx 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}
+ +
+ {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