Commit 9497b01f authored by Alice Wyan's avatar Alice Wyan

Defer initialization of websocket client

until sidebar is interacted with for logged-out users

https://trello.com/c/53uY3Xit/386-defer-initialization-of-websocket-client-until-sidebar-is-interacted-with-for-logged-out-users#
parent d6a13929
...@@ -56,6 +56,9 @@ function AnnotationUISync($rootScope, $window, annotationUI, bridge) { ...@@ -56,6 +56,9 @@ function AnnotationUISync($rootScope, $window, annotationUI, bridge) {
bridge.call('setVisibleHighlights', state); bridge.call('setVisibleHighlights', state);
} }
}, },
sidebarOpened: function () {
$rootScope.$broadcast('sidebarOpened');
},
}; };
for (var channel in channelListeners) { for (var channel in channelListeners) {
......
...@@ -133,6 +133,8 @@ module.exports = class Sidebar extends Host ...@@ -133,6 +133,8 @@ module.exports = class Sidebar extends Host
this.hide() this.hide()
show: -> show: ->
@crossframe.call('sidebarOpened')
@frame.css 'margin-left': "#{-1 * @frame.width()}px" @frame.css 'margin-left': "#{-1 * @frame.width()}px"
@frame.removeClass 'annotator-collapsed' @frame.removeClass 'annotator-collapsed'
......
...@@ -63,6 +63,41 @@ function Streamer($rootScope, annotationMapper, groups, session, settings) { ...@@ -63,6 +63,41 @@ function Streamer($rootScope, annotationMapper, groups, session, settings) {
session.update(message.model); session.update(message.model);
} }
function handleSocketOnError (event) {
console.warn('Error connecting to H push notification service:', event);
// In development, warn if the connection failure might be due to
// the app's origin not having been whitelisted in the H service's config.
//
// Unfortunately the error event does not provide a way to get at the
// HTTP status code for HTTP -> WS upgrade requests.
var websocketHost = new URL(settings.websocketUrl).hostname;
if (['localhost', '127.0.0.1'].indexOf(websocketHost) !== -1) {
console.warn('Check that your H service is configured to allow ' +
'WebSocket connections from ' + window.location.origin);
}
}
function handleSocketOnMessage (event) {
// Wrap message dispatches in $rootScope.$apply() so that
// scope watches on app state affected by the received message
// are updated
$rootScope.$apply(function () {
var message = JSON.parse(event.data);
if (!message) {
return;
}
if (message.type === 'annotation-notification') {
handleAnnotationNotification(message);
} else if (message.type === 'session-change') {
handleSessionChangeNotification(message);
} else {
console.warn('received unsupported notification', message.type);
}
});
}
function sendClientConfig () { function sendClientConfig () {
Object.keys(configMessages).forEach(function (key) { Object.keys(configMessages).forEach(function (key) {
if (configMessages[key]) { if (configMessages[key]) {
...@@ -83,73 +118,53 @@ function Streamer($rootScope, annotationMapper, groups, session, settings) { ...@@ -83,73 +118,53 @@ function Streamer($rootScope, annotationMapper, groups, session, settings) {
} }
} }
var connect = function () { var _connect = function () {
// If we have no URL configured, don't do anything.
var url = settings.websocketUrl; var url = settings.websocketUrl;
// If we have no URL configured, don't do anything.
if (!url) { if (!url) {
return; return;
} }
// Open a new socket
if (socket) {
socket.close();
}
socket = new Socket(url); socket = new Socket(url);
socket.on('open', sendClientConfig);
socket.on('error', handleSocketOnError);
socket.on('message', handleSocketOnMessage);
// Configure the client ID
setConfig('client-id', { setConfig('client-id', {
messageType: 'client_id', messageType: 'client_id',
value: clientId, value: clientId,
}); });
};
socket.on('open', function () { var connect = function () {
sendClientConfig(); if (socket) {
});
socket.on('error', function (event) {
console.warn('Error connecting to H push notification service:', event);
// In development, warn if the connection failure might be due to
// the app's origin not having been whitelisted in the H service's config.
//
// Unfortunately the error event does not provide a way to get at the
// HTTP status code for HTTP -> WS upgrade requests.
var websocketHost = new URL(url).hostname;
if (['localhost', '127.0.0.1'].indexOf(websocketHost) !== -1) {
console.warn('Check that your H service is configured to allow ' +
'WebSocket connections from ' + window.location.origin);
}
});
socket.on('message', function (event) {
// Wrap message dispatches in $rootScope.$apply() so that
// scope watches on app state affected by the received message
// are updated
//
// Note: The use of $apply() here will no longer be needed once session
// state is moved to the Redux store in `annotationUI`.
$rootScope.$apply(function () {
var message = JSON.parse(event.data);
if (!message) {
return; return;
} }
if (message.type === 'annotation-notification') { _connect();
handleAnnotationNotification(message);
} else if (message.type === 'session-change') {
handleSessionChangeNotification(message);
} else {
console.warn('received unsupported notification', message.type);
}
});
});
}; };
connect(); var reconnect = function () {
if (socket) {
socket.close();
}
_connect();
};
this.connect = connect;
this.clientId = clientId; this.clientId = clientId;
this.configMessages = configMessages;
this.connect = connect;
this.reconnect = reconnect;
this.setConfig = setConfig; this.setConfig = setConfig;
this.socket = socket;
// If the user is logged in, we connect nevertheless
if (session && session.state && session.state.userid) {
connect();
}
} }
module.exports = Streamer; module.exports = Streamer;
...@@ -30,7 +30,7 @@ function FakeSocket() { ...@@ -30,7 +30,7 @@ function FakeSocket() {
} }
inherits(FakeSocket, EventEmitter); inherits(FakeSocket, EventEmitter);
describe('streamer', function () { describe('Streamer', function () {
var fakeAnnotationMapper; var fakeAnnotationMapper;
var fakeGroups; var fakeGroups;
var fakeRootScope; var fakeRootScope;
...@@ -82,28 +82,67 @@ describe('streamer', function () { ...@@ -82,28 +82,67 @@ describe('streamer', function () {
it('should not create a websocket connection if websocketUrl is not provided', function () { it('should not create a websocket connection if websocketUrl is not provided', function () {
fakeSettings = {}; fakeSettings = {};
createDefaultStreamer();
activeStreamer.connect();
assert.isNull(fakeWebSocket);
});
it('should not create a websocket connection if the user is not logged in', function () {
createDefaultStreamer(); createDefaultStreamer();
assert.isNull(fakeWebSocket); assert.isNull(fakeWebSocket);
}); });
it('should send a client ID', function () { it('should create a websocket connection if the user is logged in', function () {
fakeSession.state = {userid: 'foo'};
createDefaultStreamer();
assert.ok(fakeWebSocket);
});
it('should create a websocket connection if explicitly connected', function () {
createDefaultStreamer();
activeStreamer.connect();
assert.ok(fakeWebSocket);
});
it('should have a non-null client ID', function () {
createDefaultStreamer();
assert.ok(activeStreamer.clientId);
});
it('should send the client ID on connection', function () {
createDefaultStreamer(); createDefaultStreamer();
activeStreamer.connect();
assert.equal(fakeWebSocket.messages.length, 1); assert.equal(fakeWebSocket.messages.length, 1);
assert.equal(fakeWebSocket.messages[0].messageType, 'client_id'); assert.equal(fakeWebSocket.messages[0].messageType, 'client_id');
assert.equal(fakeWebSocket.messages[0].value, activeStreamer.clientId); assert.equal(fakeWebSocket.messages[0].value, activeStreamer.clientId);
}); });
it('should close any existing socket', function () { describe('#connect()', function () {
it('should not close any existing socket', function () {
createDefaultStreamer(); createDefaultStreamer();
activeStreamer.connect();
var oldWebSocket = fakeWebSocket; var oldWebSocket = fakeWebSocket;
activeStreamer.connect(); activeStreamer.connect();
assert.ok(!oldWebSocket.didClose);
assert.ok(!fakeWebSocket.didClose);
});
});
describe('#reconnect()', function () {
it('should close the existing socket', function () {
createDefaultStreamer();
activeStreamer.connect();
var oldWebSocket = fakeWebSocket;
activeStreamer.reconnect();
assert.ok(oldWebSocket.didClose); assert.ok(oldWebSocket.didClose);
assert.ok(!fakeWebSocket.didClose); assert.ok(!fakeWebSocket.didClose);
}); });
});
describe('annotation notifications', function () { describe('annotation notifications', function () {
it('should load new annotations', function () { it('should load new annotations', function () {
createDefaultStreamer(); createDefaultStreamer();
activeStreamer.connect();
fakeWebSocket.notify({ fakeWebSocket.notify({
type: 'annotation-notification', type: 'annotation-notification',
options: { options: {
...@@ -118,6 +157,7 @@ describe('streamer', function () { ...@@ -118,6 +157,7 @@ describe('streamer', function () {
it('should unload deleted annotations', function () { it('should unload deleted annotations', function () {
createDefaultStreamer(); createDefaultStreamer();
activeStreamer.connect();
fakeWebSocket.notify({ fakeWebSocket.notify({
type: 'annotation-notification', type: 'annotation-notification',
options: { options: {
...@@ -134,6 +174,7 @@ describe('streamer', function () { ...@@ -134,6 +174,7 @@ describe('streamer', function () {
describe('session change notifications', function () { describe('session change notifications', function () {
it('updates the session when a notification is received', function () { it('updates the session when a notification is received', function () {
createDefaultStreamer(); createDefaultStreamer();
activeStreamer.connect();
var model = { var model = {
groups: [{ groups: [{
id: 'new-group', id: 'new-group',
...@@ -150,6 +191,7 @@ describe('streamer', function () { ...@@ -150,6 +191,7 @@ describe('streamer', function () {
describe('reconnections', function () { describe('reconnections', function () {
it('resends configuration messages when a reconnection occurs', function () { it('resends configuration messages when a reconnection occurs', function () {
createDefaultStreamer(); createDefaultStreamer();
activeStreamer.connect();
fakeWebSocket.messages = []; fakeWebSocket.messages = [];
fakeWebSocket.emit('open'); fakeWebSocket.emit('open');
assert.equal(fakeWebSocket.messages.length, 1); assert.equal(fakeWebSocket.messages.length, 1);
......
...@@ -99,6 +99,7 @@ describe('WidgetController', function () { ...@@ -99,6 +99,7 @@ describe('WidgetController', function () {
fakeStreamer = { fakeStreamer = {
setConfig: sandbox.spy(), setConfig: sandbox.spy(),
connect: sandbox.spy(),
}; };
fakeStreamFilter = { fakeStreamFilter = {
...@@ -441,6 +442,27 @@ describe('WidgetController', function () { ...@@ -441,6 +442,27 @@ describe('WidgetController', function () {
}); });
}); });
describe('deferred websocket connection', function () {
it('should connect the websocket the first time the sidebar opens', function () {
$rootScope.$emit('sidebarOpened');
assert.called(fakeStreamer.connect);
});
describe('when logged in user changes', function () {
it('should not reconnect if the sidebar is closed', function () {
$rootScope.$emit(events.USER_CHANGED);
assert.notCalled(fakeStreamer.connect);
});
it('should reconnect if the sidebar is open', function () {
$rootScope.$emit('sidebarOpened');
fakeStreamer.connect.reset();
$rootScope.$emit(events.USER_CHANGED);
assert.called(fakeStreamer.connect);
});
});
});
describe('#forceVisible', function () { describe('#forceVisible', function () {
it('shows the thread', function () { it('shows the thread', function () {
var thread = {id: '1'}; var thread = {id: '1'};
......
...@@ -39,6 +39,8 @@ module.exports = function WidgetController( ...@@ -39,6 +39,8 @@ module.exports = function WidgetController(
VirtualThreadList VirtualThreadList
) { ) {
var sidebarOpen = false;
/** /**
* Returns the number of top level annotations which are of type annotations * Returns the number of top level annotations which are of type annotations
* and not notes or replies. * and not notes or replies.
...@@ -258,8 +260,10 @@ module.exports = function WidgetController( ...@@ -258,8 +260,10 @@ module.exports = function WidgetController(
* @param {Array<{uri:string}>} frames - Hypothesis client frames * @param {Array<{uri:string}>} frames - Hypothesis client frames
* to load annotations for. * to load annotations for.
*/ */
function loadAnnotations(frames) { function loadAnnotations(frames, reset) {
if (reset || typeof reset === 'undefined') {
_resetAnnotations(); _resetAnnotations();
}
searchClients.forEach(function (client) { searchClients.forEach(function (client) {
client.cancel(); client.cancel();
...@@ -297,6 +301,17 @@ module.exports = function WidgetController( ...@@ -297,6 +301,17 @@ module.exports = function WidgetController(
} }
} }
$rootScope.$on('sidebarOpened', function () {
sidebarOpen = true;
streamer.connect();
});
$rootScope.$on(events.USER_CHANGED, function () {
if (sidebarOpen) {
streamer.connect();
}
});
// When a direct-linked annotation is successfully anchored in the page, // When a direct-linked annotation is successfully anchored in the page,
// focus and scroll to it // focus and scroll to it
$rootScope.$on(events.ANNOTATIONS_SYNCED, function (event, tags) { $rootScope.$on(events.ANNOTATIONS_SYNCED, function (event, tags) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment