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) {
bridge.call('setVisibleHighlights', state);
}
},
sidebarOpened: function () {
$rootScope.$broadcast('sidebarOpened');
},
};
for (var channel in channelListeners) {
......
......@@ -133,6 +133,8 @@ module.exports = class Sidebar extends Host
this.hide()
show: ->
@crossframe.call('sidebarOpened')
@frame.css 'margin-left': "#{-1 * @frame.width()}px"
@frame.removeClass 'annotator-collapsed'
......
......@@ -63,6 +63,41 @@ function Streamer($rootScope, annotationMapper, groups, session, settings) {
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 () {
Object.keys(configMessages).forEach(function (key) {
if (configMessages[key]) {
......@@ -83,73 +118,53 @@ function Streamer($rootScope, annotationMapper, groups, session, settings) {
}
}
var connect = function () {
// If we have no URL configured, don't do anything.
var _connect = function () {
var url = settings.websocketUrl;
// If we have no URL configured, don't do anything.
if (!url) {
return;
}
// Open a new socket
if (socket) {
socket.close();
}
socket = new Socket(url);
socket.on('open', sendClientConfig);
socket.on('error', handleSocketOnError);
socket.on('message', handleSocketOnMessage);
// Configure the client ID
setConfig('client-id', {
messageType: 'client_id',
value: clientId,
});
};
socket.on('open', function () {
sendClientConfig();
});
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) {
var connect = function () {
if (socket) {
return;
}
if (message.type === 'annotation-notification') {
handleAnnotationNotification(message);
} else if (message.type === 'session-change') {
handleSessionChangeNotification(message);
} else {
console.warn('received unsupported notification', message.type);
}
});
});
_connect();
};
connect();
var reconnect = function () {
if (socket) {
socket.close();
}
_connect();
};
this.connect = connect;
this.clientId = clientId;
this.configMessages = configMessages;
this.connect = connect;
this.reconnect = reconnect;
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;
......@@ -30,7 +30,7 @@ function FakeSocket() {
}
inherits(FakeSocket, EventEmitter);
describe('streamer', function () {
describe('Streamer', function () {
var fakeAnnotationMapper;
var fakeGroups;
var fakeRootScope;
......@@ -82,28 +82,67 @@ describe('streamer', function () {
it('should not create a websocket connection if websocketUrl is not provided', function () {
fakeSettings = {};
createDefaultStreamer();
activeStreamer.connect();
assert.isNull(fakeWebSocket);
});
it('should not create a websocket connection if the user is not logged in', function () {
createDefaultStreamer();
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();
activeStreamer.connect();
assert.equal(fakeWebSocket.messages.length, 1);
assert.equal(fakeWebSocket.messages[0].messageType, 'client_id');
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();
activeStreamer.connect();
var oldWebSocket = fakeWebSocket;
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(!fakeWebSocket.didClose);
});
});
describe('annotation notifications', function () {
it('should load new annotations', function () {
createDefaultStreamer();
activeStreamer.connect();
fakeWebSocket.notify({
type: 'annotation-notification',
options: {
......@@ -118,6 +157,7 @@ describe('streamer', function () {
it('should unload deleted annotations', function () {
createDefaultStreamer();
activeStreamer.connect();
fakeWebSocket.notify({
type: 'annotation-notification',
options: {
......@@ -134,6 +174,7 @@ describe('streamer', function () {
describe('session change notifications', function () {
it('updates the session when a notification is received', function () {
createDefaultStreamer();
activeStreamer.connect();
var model = {
groups: [{
id: 'new-group',
......@@ -150,6 +191,7 @@ describe('streamer', function () {
describe('reconnections', function () {
it('resends configuration messages when a reconnection occurs', function () {
createDefaultStreamer();
activeStreamer.connect();
fakeWebSocket.messages = [];
fakeWebSocket.emit('open');
assert.equal(fakeWebSocket.messages.length, 1);
......
......@@ -99,6 +99,7 @@ describe('WidgetController', function () {
fakeStreamer = {
setConfig: sandbox.spy(),
connect: sandbox.spy(),
};
fakeStreamFilter = {
......@@ -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 () {
it('shows the thread', function () {
var thread = {id: '1'};
......
......@@ -39,6 +39,8 @@ module.exports = function WidgetController(
VirtualThreadList
) {
var sidebarOpen = false;
/**
* Returns the number of top level annotations which are of type annotations
* and not notes or replies.
......@@ -258,8 +260,10 @@ module.exports = function WidgetController(
* @param {Array<{uri:string}>} frames - Hypothesis client frames
* to load annotations for.
*/
function loadAnnotations(frames) {
function loadAnnotations(frames, reset) {
if (reset || typeof reset === 'undefined') {
_resetAnnotations();
}
searchClients.forEach(function (client) {
client.cancel();
......@@ -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,
// focus and scroll to it
$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