Commit d5f3de36 authored by Robert Knight's avatar Robert Knight

Extract the WebSocket client into its own module and add tests

In preparation for adding additional push notification types,
separate out the client into its own module and add tests.

T-105
parent a329af98
......@@ -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