Commit 2863e008 authored by Robert Knight's avatar Robert Knight

Merge pull request #2583 from robertknight/t105-refactor_notification_client

Extract the WebSocket client into its own module and add tests
parents a329af98 d5f3de36
......@@ -4,51 +4,11 @@ angular = require('angular')
require('angular-websocket')
require('angular-jwt')
uuid = require('node-uuid')
clientId = uuid.v4()
socket = null
streamer = require('./streamer')
resolve =
store: ['store', (store) -> store.$promise]
streamer: [
'$websocket', 'annotationMapper', 'groups'
($websocket, annotationMapper, groups) ->
# Get the socket URL
url = new URL('/ws', baseURI)
url.protocol = url.protocol.replace('http', 'ws')
# Close any existing socket
socket?.close()
# Open the socket
socket = $websocket(url.href, [], {reconnectIfNotNormalClose: true})
socket.send(messageType: 'client_id', value: clientId)
# Listen for updates
socket.onMessage (event) ->
message = JSON.parse(event.data)
return if !message or message.type != 'annotation-notification'
action = message.options.action
annotations = message.payload
return unless annotations?.length
# Discard annotations that aren't from the currently focused group.
# FIXME: Have the server only send us annotations from the focused
# group in the first place.
annotations = annotations.filter((ann) ->
return ann.group == groups.focused().id
)
switch action
when 'create', 'update', 'past'
annotationMapper.loadAnnotations annotations
when 'delete'
annotationMapper.unloadAnnotations annotations
return socket
]
streamer: streamer.connect
threading: [
'annotationMapper', 'drafts', 'threading'
(annotationMapper, drafts, threading) ->
......@@ -112,7 +72,7 @@ configureTemplates = ['$sceDelegateProvider', ($sceDelegateProvider) ->
setupCrossFrame = ['crossframe', (crossframe) -> crossframe.connect()]
setupHttp = ['$http', ($http) ->
$http.defaults.headers.common['X-Client-Id'] = clientId
$http.defaults.headers.common['X-Client-Id'] = streamer.clientId
]
setupHost = ['host', (host) -> ]
......
var baseURI = require('document-base-uri')
var uuid = require('node-uuid')
// the randomly generated session UUID
var clientId = uuid.v4();
// the singleton socket instance, only one may
// be open at a time
var socket;
/**
* Open a new WebSocket connection to the Hypothesis push notification service.
* Only one websocket connection may exist at a time, any existing socket is
* closed.
*
* @param $websocket - angular-websocket constructor
* @param annotationMapper - The local annotation store
* @param groups - The local groups store
*
* @return An angular-websocket wrapper around the socket.
*/
// @ngInject
function connect($websocket, annotationMapper, groups) {
// Get the socket URL
var url = new URL('/ws', baseURI);
url.protocol = url.protocol.replace('http', 'ws');
// Close any existing socket
if (socket) {
socket.close();
}
// Open the socket
socket = $websocket(url.href, [], {
reconnectIfNotNormalClose: true
});
socket.send({
messageType: 'client_id',
value: clientId
})
// Listen for updates
socket.onMessage(function (event) {
message = JSON.parse(event.data)
if (!message || message.type !== 'annotation-notification') {
return;
}
action = message.options.action
annotations = message.payload
if (annotations.length === 0) {
return;
}
// Discard annotations that aren't from the currently focused group.
// FIXME: Have the server only send us annotations from the focused
// group in the first place.
annotations = annotations.filter(function (ann) {
return ann.group == groups.focused().id
});
switch (action) {
case 'create':
case 'update':
case 'past':
annotationMapper.loadAnnotations(annotations);
break;
case 'delete':
annotationMapper.unloadAnnotations(annotations);
break;
}
});
return socket
}
module.exports = {
connect: connect,
clientId: clientId
};
'use strict';
var streamer = require('../streamer');
function fakeSocketConstructor(url) {
return {
messages: [],
onMessageCallbacks: [],
didClose: false,
send: function (message) {
this.messages.push(message);
},
onMessage: function (callback) {
this.onMessageCallbacks.push(callback);
},
notify: function (message) {
this.onMessageCallbacks.forEach(function (callback) {
callback({
data: JSON.stringify(message)
});
});
},
close: function () {
this.didClose = true
}
};
}
describe('streamer', function () {
var fakeAnnotationMapper;
var fakeGroups;
var socket;
beforeEach(function () {
fakeAnnotationMapper = {
loadAnnotations: sinon.stub(),
unloadAnnotations: sinon.stub(),
};
fakeGroups = {
focused: function () {
return 'public';
},
};
socket = streamer.connect(
fakeSocketConstructor,
fakeAnnotationMapper,
fakeGroups
);
});
it('should send a client ID', function () {
assert.equal(socket.messages.length, 1);
assert.equal(socket.messages[0].messageType, 'client_id');
assert.equal(socket.messages[0].value, streamer.clientId);
});
it('should close any existing socket', function () {
var oldSocket = socket;
var newSocket = streamer.connect(fakeSocketConstructor,
fakeAnnotationMapper,
fakeGroups
);
assert.ok(oldSocket.didClose);
assert.ok(!newSocket.didClose);
});
describe('annotation notifications', function () {
it('should load new annotations', function () {
socket.notify({
type: 'annotation-notification',
options: {
action: 'create',
},
payload: [{
group: 'public'
}]
});
assert.ok(fakeAnnotationMapper.loadAnnotations.calledOnce);
});
it('should unload deleted annotations', function () {
socket.notify({
type: 'annotation-notification',
options: {
action: 'delete',
},
payload: [{
group: 'public'
}]
});
assert.ok(fakeAnnotationMapper.unloadAnnotations.calledOnce);
});
});
});
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