Skip to content

Commit 28d1019

Browse files
committed
🎨 overhaul app initialization
- Provide user visible feedback as to what’s happening during initialization - Add the notion of a fatal error state on the app view: something didn’t happen and we can’t proceed - When client encounters an error, we’re fatal - If the client doesnt become readable within 5 seconds we’re fatal (instead of folks staring at a white screen for 20 seconds going "uummm”) - report fatals back to bugging nicely
1 parent ee6dd1f commit 28d1019

File tree

2 files changed

+103
-42
lines changed

2 files changed

+103
-42
lines changed

src/app.js

Lines changed: 101 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
/* eslint no-console:0 */
2+
13
var pkg = require('../package.json');
24
var app = require('ampersand-app');
35
app.extend({
@@ -8,7 +10,9 @@ app.extend({
810
'App Version': pkg.version
911
}
1012
});
11-
require('./bugsnag').listen(app);
13+
14+
var bugsnag = require('./bugsnag');
15+
bugsnag.listen(app);
1216

1317
var _ = require('lodash');
1418
var domReady = require('domready');
@@ -17,6 +21,13 @@ var getOrCreateClient = require('scout-client');
1721
var ViewSwitcher = require('ampersand-view-switcher');
1822
var View = require('ampersand-view');
1923
var localLinks = require('local-links');
24+
25+
var QueryOptions = require('./models/query-options');
26+
var Connection = require('./models/connection');
27+
var MongoDBInstance = require('./models/mongodb-instance');
28+
var Router = require('./router');
29+
var Statusbar = require('./statusbar');
30+
2031
var debug = require('debug')('scout:app');
2132

2233
// Inter-process communication with main process (Electron window)
@@ -78,24 +89,59 @@ var Application = View.extend({
7889
/**
7990
* Enable/Disable features with one global switch
8091
*/
81-
features: 'object'
92+
features: 'object',
93+
clientStartedAt: 'date',
94+
clientStalledTimeout: 'number'
8295
},
8396
events: {
8497
'click a': 'onLinkClick'
8598
},
86-
/**
87-
* We have what we need, we can now start our router and show the appropriate page!
88-
*/
89-
_onDOMReady: function() {
90-
this.el = document.querySelector('#application');
91-
this.render();
99+
onClientReady: function() {
100+
debug('Client ready! Took %dms to become readable',
101+
new Date() - this.clientStartedAt);
102+
103+
debug('clearing client stall timeout...');
104+
clearTimeout(this.clientStalledTimeout);
92105

106+
debug('initializing singleton models... ');
107+
this.queryOptions = new QueryOptions();
108+
this.volatileQueryOptions = new QueryOptions();
109+
this.instance = new MongoDBInstance();
110+
111+
this.startRouter();
112+
},
113+
startRouter: function() {
114+
this.router = new Router();
115+
debug('Listening for page changes from the router...');
93116
this.listenTo(this.router, 'page', this.onPageChange);
94117

118+
debug('Starting router...');
95119
this.router.history.start({
96120
pushState: false,
97121
root: '/'
98122
});
123+
app.statusbar.hide();
124+
},
125+
onFatalError: function(id, err) {
126+
console.error('Fatal Error!: ', id, err);
127+
bugsnag.notifyException(err, 'fatal!' + id);
128+
window.alert('Fatal Error: ' + id + ': ' + err.message);
129+
},
130+
// ms we'll wait for a `scout-client` instance
131+
// to become readable before giving up and showing
132+
// a fatal error message.
133+
CLIENT_STALLED_REDLINE: 5 * 1000,
134+
startClientStalledTimer: function() {
135+
this.clientStartedAt = new Date();
136+
137+
debug('Starting client stalled timer to bail in %dms...',
138+
this.CLIENT_STALLED_REDLINE);
139+
140+
this.clientStalledTimeout = setTimeout(function() {
141+
this.onFatalError('client stalled',
142+
new Error('Error connecting to MongoDB. '
143+
+ 'Please reload the page.'));
144+
}.bind(this), this.CLIENT_STALLED_REDLINE);
99145
},
100146
/**
101147
* When you want to go to a different page in the app or just save
@@ -117,15 +163,25 @@ var Application = View.extend({
117163
trigger: !options.silent
118164
});
119165
},
166+
/**
167+
* Called a soon as the DOM is ready so we can
168+
* start showing status indicators as
169+
* quickly as possible.
170+
*/
120171
render: function() {
172+
debug('Rendering app container...');
173+
174+
this.el = document.querySelector('#application');
121175
this.renderWithTemplate(this);
122176
this.pageSwitcher = new ViewSwitcher(this.queryByHook('layout-container'), {
123177
show: function() {
124178
document.scrollTop = 0;
125179
}
126180
});
127-
128-
this.statusbar.el = this.queryByHook('statusbar');
181+
debug('rendering statusbar...');
182+
this.statusbar = new Statusbar({
183+
el: this.queryByHook('statusbar')
184+
});
129185
this.statusbar.render();
130186
},
131187
onPageChange: function(view) {
@@ -153,16 +209,6 @@ var state = new Application({
153209
connection_id: connection_id
154210
});
155211

156-
var QueryOptions = require('./models/query-options');
157-
var Connection = require('./models/connection');
158-
var MongoDBInstance = require('./models/mongodb-instance');
159-
var Router = require('./router');
160-
var Statusbar = require('./statusbar');
161-
162-
function start() {
163-
state.router = new Router();
164-
state._onDOMReady();
165-
}
166212
// @todo (imlucas): Feature flags can be overrideen
167213
// via `window.localStorage`.
168214
var FEATURES = {
@@ -188,29 +234,42 @@ app.extend({
188234
},
189235
init: function() {
190236
domReady(function() {
191-
state.statusbar = new Statusbar();
192-
193-
if (connection_id) {
194-
state.connection = new Connection({
195-
_id: connection_id
196-
});
197-
198-
199-
debug('looking up connection `%s`...', connection_id);
200-
state.connection.fetch({
201-
success: function() {
202-
debug('got connection `%j`...', state.connection.serialize());
203-
app.client = getOrCreateClient(app.endpoint, state.connection.serialize());
204-
205-
state.queryOptions = new QueryOptions();
206-
state.volatileQueryOptions = new QueryOptions();
207-
state.instance = new MongoDBInstance();
208-
start();
209-
}
210-
});
211-
} else {
212-
start();
237+
state.render();
238+
239+
if (!connection_id) {
240+
// Not serving a part of the app which uses the client,
241+
// so we can just start everything up now.
242+
state.start();
243+
return;
213244
}
245+
246+
app.statusbar.show('Retrieving connection details...');
247+
248+
state.connection = new Connection({
249+
_id: connection_id
250+
});
251+
252+
debug('looking up connection `%s`...', connection_id);
253+
state.connection.fetch({
254+
success: function() {
255+
app.statusbar.show('Connection details loaded! Initializing client...');
256+
257+
var endpoint = app.endpoint;
258+
var connection = state.connection.serialize();
259+
260+
app.client = getOrCreateClient(endpoint, connection)
261+
.on('readable', state.onClientReady.bind(state))
262+
.on('error', state.onFatalError.bind(state, 'create client'));
263+
264+
state.startClientStalledTimer();
265+
},
266+
error: function() {
267+
// @todo (imlucas) `ampersand-sync-localforage` currently drops
268+
// the real error so for now just use a generic.
269+
state.onFatalError(state, 'fetch connection',
270+
new Error('Error retrieving connection. Please reload the page.'));
271+
}
272+
});
214273
});
215274
// set up ipc
216275
ipc.on('message', state.onMessageReceived.bind(this));

src/bugsnag.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ function beforeNotify(d) {
2828
debug('redacted bugsnag report\n', JSON.stringify(d, null, 2));
2929
}
3030

31+
module.exports = bugsnag;
32+
3133
/**
3234
* Configure bugsnag's api client which attaches a handler to
3335
* `window.onerror` so any uncaught exceptions are trapped and logged

0 commit comments

Comments
 (0)