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
|
@ -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);
|
||||
}
|
|
@ -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": {
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
@ -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()
|
|
@ -38,15 +38,16 @@ export default class AccountErrorHeader extends React.Component {
|
|||
|
||||
renderErrorHeader(message, buttonName, actionCallback) {
|
||||
return (
|
||||
<div className="sync-issue notifications-sticky">
|
||||
<div className="account-error-header notifications-sticky">
|
||||
<div className={"notifications-sticky-item notification-error has-default-action"}
|
||||
onClick={actionCallback}>
|
||||
<div>
|
||||
<div className="icon">
|
||||
<RetinaImg
|
||||
name="icon-alert-onred.png"
|
||||
mode={RetinaImg.Mode.ContentPreserve} />
|
||||
</div>{message}</div>
|
||||
<RetinaImg
|
||||
className="icon"
|
||||
name="icon-alert-onred.png"
|
||||
mode={RetinaImg.Mode.ContentPreserve} />
|
||||
<div className="message">
|
||||
{message}
|
||||
</div>
|
||||
<a className="action default" onClick={actionCallback}>
|
||||
{buttonName}
|
||||
</a>
|
|
@ -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 (<span/>);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="connection-status-header notifications-sticky">
|
||||
<div className={"notifications-sticky-item notification-offline"}>
|
||||
<RetinaImg
|
||||
className="icon"
|
||||
name="icon-alert-onred.png"
|
||||
mode={RetinaImg.Mode.ContentPreserve} />
|
||||
<div className="message">
|
||||
Nylas N1 isn't able to reach api.nylas.com. Retrying {nextRetryText}.
|
||||
</div>
|
||||
<a className="action default" onClick={this.onTryAgain}>
|
||||
Try Again Now
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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: =>
|
||||
<div className="notifications-sticky">
|
||||
{@_notificationComponents()}
|
||||
</div>
|
||||
|
||||
_notificationComponents: =>
|
||||
@state.items.map (notif) ->
|
||||
<NotificationsItem notification={notif} key={notif.message} />
|
||||
|
||||
|
||||
module.exports = NotificationsHeader
|
|
@ -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()
|
||||
|
||||
<a className={classname} key={action.label} onClick={actionClick}>
|
||||
{action.label}
|
||||
</a>
|
||||
|
||||
if actionDefault
|
||||
<div className={"notifications-sticky-item notification-#{notif.type} has-default-action"}
|
||||
onClick={=> @_fireItemAction(notif, actionDefault)}>
|
||||
<i className={iconClass}></i><div className="message">{notif.message}</div>{actionComponents}
|
||||
</div>
|
||||
else
|
||||
<div className={"notifications-sticky-item notification-#{notif.type}"}>
|
||||
<i className={iconClass}></i><div className="message">{notif.message}</div>{actionComponents}
|
||||
</div>
|
||||
|
||||
_fireItemAction: (notification, action) =>
|
||||
Actions.notificationActionTaken({notification, action})
|
||||
|
||||
module.exports = NotificationsItem
|
|
@ -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
|
24
internal_packages/notifications/lib/main.es6
Normal file
|
@ -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);
|
||||
}
|
|
@ -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()
|
||||
|
||||
<a className={classname} key={action.label} onClick={actionClick}>
|
||||
{action.label}
|
||||
</a>
|
||||
|
||||
if actionDefault
|
||||
<div className={"notifications-sticky-item notification-#{notif.type} has-default-action"}
|
||||
onClick={=> @_fireItemAction(notif, actionDefault)}>
|
||||
<i className={iconClass}></i><div>{notif.message}</div>{actionComponents}
|
||||
</div>
|
||||
else
|
||||
<div className={"notifications-sticky-item notification-#{notif.type}"}>
|
||||
<i className={iconClass}></i><div>{notif.message}</div>{actionComponents}
|
||||
</div>
|
||||
|
||||
_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: =>
|
||||
<div className="notifications-sticky">
|
||||
{@_notificationComponents()}
|
||||
</div>
|
||||
|
||||
_notificationComponents: =>
|
||||
@state.items.map (notif) ->
|
||||
<NotificationStickyItem notification={notif} key={notif.message} />
|
||||
|
||||
|
||||
module.exports = NotificationStickyBar
|
|
@ -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 <StreamingSyncActivity key="streaming-sync" />
|
||||
else
|
||||
items.push <InitialSyncActivity key="initial-sync" />
|
||||
|
||||
|
||||
names = classNames
|
||||
"sidebar-activity": true
|
||||
"sidebar-activity-error": error?
|
||||
|
@ -87,16 +84,6 @@ class ActivitySidebar extends React.Component
|
|||
</div>
|
||||
</div>
|
||||
|
||||
_renderDeltaSyncActivityItem: =>
|
||||
<div className="item" key="delta-sync-item">
|
||||
<div style={padding: "9px 9px 0 12px", float: "left"}>
|
||||
<RetinaImg name="sending-spinner.gif" width={18} mode={RetinaImg.Mode.ContentPreserve} />
|
||||
</div>
|
||||
<div className="inner">
|
||||
Syncing your mailbox…
|
||||
</div>
|
||||
</div>
|
||||
|
||||
_renderNotificationActivityItems: =>
|
||||
@state.notifications.map (notification) ->
|
||||
<div className="item" key={notification.id}>
|
||||
|
@ -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
|
|
@ -108,6 +108,6 @@ class InitialSyncActivity extends React.Component
|
|||
</div>
|
||||
|
||||
_onTryAgain: =>
|
||||
Actions.retryInitialSync()
|
||||
Actions.retrySync()
|
||||
|
||||
module.exports = InitialSyncActivity
|
|
@ -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
|
||||
<div className="item" key="delta-sync-item">
|
||||
<div style={padding: "9px 9px 0 12px", float: "left"}>
|
||||
<RetinaImg name="sending-spinner.gif" width={18} mode={RetinaImg.Mode.ContentPreserve} />
|
||||
</div>
|
||||
<div className="inner">
|
||||
Syncing your mailbox…
|
||||
</div>
|
||||
</div>
|
||||
|
||||
_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
|
|
@ -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%);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 = ->
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -87,8 +87,6 @@ class TaskQueue
|
|||
@listenTo Actions.dequeueAllTasks, @dequeueAll
|
||||
@listenTo Actions.dequeueMatchingTask, @dequeueMatching
|
||||
@listenTo Actions.clearDeveloperConsole, @clearCompleted
|
||||
@listenTo Actions.longPollConnected, =>
|
||||
@_processQueue()
|
||||
|
||||
queue: =>
|
||||
@_queue
|
||||
|
|
|
@ -59,8 +59,4 @@ export default class EventRSVPTask extends Task {
|
|||
onTimeoutError() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
onOfflineError() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
//
|
||||
|
|
|
@ -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
|
||||
|
|