Commit e126b97d authored by Robert Knight's avatar Robert Knight

Move AnnotationSync class to annotator/ dir and remove unused code

Now that AnnotationSync is no longer used in the sidebar app, we can
remove all of the handlers for events and messages arriving through the
channel which are not needed by the page itself.
parent 55093ebb
extend = require('extend')
module.exports = class AnnotationSync module.exports = class AnnotationSync
# Default configuration
options:
# Formats an annotation into a message body for sending across the bridge.
formatter: (annotation) -> annotation
# Recieves an annotation extracted from the message body received
# via the bridge and returns an annotation for use in the local app.
parser: (annotation) -> annotation
# Merge function. If specified, it will be called with the local copy of
# an annotation and a parsed copy received as an argument to an RPC call
# to reconcile any differences. The default behavior is to merge all
# keys of the remote object into the local copy
merge: (local, remote) ->
for k, v of remote
local[k] = v
local
# Function used to emit annotation events
emit: (event, args...) ->
throw new Error('options.emit unspecified for AnnotationSync.')
# Function used to register handlers for annotation events
on: (event, handler) ->
throw new Error('options.on unspecified for AnnotationSync.')
# Cache of annotations which have crossed the bridge for fast, encapsulated # Cache of annotations which have crossed the bridge for fast, encapsulated
# association of annotations received in arguments to window-local copies. # association of annotations received in arguments to window-local copies.
cache: null cache: null
constructor: (@bridge, options) -> constructor: (@bridge, options) ->
@options = extend(true, {}, @options, options) if !options.on
throw new Error('options.on unspecified for AnnotationSync.')
if !options.emit
throw new Error('options.emit unspecified for AnnotationSync.')
@cache = {} @cache = {}
@_on = @options.on @_on = options.on
@_emit = @options.emit @_emit = options.emit
# Listen locally for interesting events # Listen locally for interesting events
for event, handler of @_eventListeners for event, handler of @_eventListeners
...@@ -48,11 +23,6 @@ module.exports = class AnnotationSync ...@@ -48,11 +23,6 @@ module.exports = class AnnotationSync
for method, func of @_channelListeners for method, func of @_channelListeners
@bridge.on(method, func.bind(this)) @bridge.on(method, func.bind(this))
# Upon new connections, send over the items in our cache
onConnect = (channel) =>
this._syncCache(channel)
@bridge.onConnect(onConnect)
sync: (annotations) -> sync: (annotations) ->
annotations = (this._format a for a in annotations) annotations = (this._format a for a in annotations)
@bridge.call 'sync', annotations, (err, annotations = []) => @bridge.call 'sync', annotations, (err, annotations = []) =>
...@@ -62,28 +32,6 @@ module.exports = class AnnotationSync ...@@ -62,28 +32,6 @@ module.exports = class AnnotationSync
# Handlers for messages arriving through a channel # Handlers for messages arriving through a channel
_channelListeners: _channelListeners:
'beforeCreateAnnotation': (body, cb) ->
annotation = this._parse(body)
delete @cache[annotation.$$tag]
@_emit 'beforeAnnotationCreated', annotation
@cache[annotation.$$tag] = annotation
cb(null, this._format(annotation))
'createAnnotation': (body, cb) ->
annotation = this._parse(body)
delete @cache[annotation.$$tag]
@_emit 'annotationCreated', annotation
@cache[annotation.$$tag] = annotation
cb(null, this._format(annotation))
'updateAnnotation': (body, cb) ->
annotation = this._parse(body)
delete @cache[annotation.$$tag]
@_emit('beforeAnnotationUpdated', annotation)
@_emit('annotationUpdated', annotation)
@cache[annotation.$$tag] = annotation
cb(null, this._format(annotation))
'deleteAnnotation': (body, cb) -> 'deleteAnnotation': (body, cb) ->
annotation = this._parse(body) annotation = this._parse(body)
delete @cache[annotation.$$tag] delete @cache[annotation.$$tag]
...@@ -95,51 +43,12 @@ module.exports = class AnnotationSync ...@@ -95,51 +43,12 @@ module.exports = class AnnotationSync
@_emit('annotationsLoaded', annotations) @_emit('annotationsLoaded', annotations)
cb(null, annotations) cb(null, annotations)
'sync': (bodies, cb) ->
annotations = (this._format(this._parse(b)) for b in bodies)
@_emit('sync', annotations)
cb(null, annotations)
# Handlers for events coming from this frame, to send them across the channel # Handlers for events coming from this frame, to send them across the channel
_eventListeners: _eventListeners:
'beforeAnnotationCreated': (annotation) -> 'beforeAnnotationCreated': (annotation) ->
return if annotation.$$tag? return if annotation.$$tag?
this._mkCallRemotelyAndParseResults('beforeCreateAnnotation')(annotation) this._mkCallRemotelyAndParseResults('beforeCreateAnnotation')(annotation)
'annotationCreated': (annotation) ->
return unless annotation.$$tag? and @cache[annotation.$$tag]
this._mkCallRemotelyAndParseResults('createAnnotation')(annotation)
'annotationUpdated': (annotation) ->
return unless annotation.$$tag? and @cache[annotation.$$tag]
this._mkCallRemotelyAndParseResults('updateAnnotation')(annotation)
'annotationDeleted': (annotation) ->
return unless annotation.$$tag? and @cache[annotation.$$tag]
onFailure = (err) =>
delete @cache[annotation.$$tag] unless err
this._mkCallRemotelyAndParseResults('deleteAnnotation', onFailure)(annotation)
'annotationsLoaded': (annotations) ->
bodies = (this._format a for a in annotations when not a.$$tag)
return unless bodies.length
@bridge.call('loadAnnotations', bodies)
'annotationsUnloaded': (annotations) ->
self = this
annotations.forEach (annotation) ->
# In the client, unloading an annotation is handled the same way as
# deleting an annotation. Within the app however, we handle the events
# differently in some cases
delete self.cache[annotation.$$tag]
self._mkCallRemotelyAndParseResults('deleteAnnotation')(annotation)
_syncCache: (channel) ->
# Synchronise (here to there) the items in our cache
annotations = (this._format a for t, a of @cache)
if annotations.length
channel.call('loadAnnotations', annotations)
_mkCallRemotelyAndParseResults: (method, callBack) -> _mkCallRemotelyAndParseResults: (method, callBack) ->
(annotation) => (annotation) =>
# Wrap the callback function to first parse returned items # Wrap the callback function to first parse returned items
...@@ -170,13 +79,8 @@ module.exports = class AnnotationSync ...@@ -170,13 +79,8 @@ module.exports = class AnnotationSync
# Parse a message body from a RPC call with the provided parser. # Parse a message body from a RPC call with the provided parser.
_parse: (body) -> _parse: (body) ->
local = @cache[body.tag] local = @cache[body.tag]
remote = @options.parser(body.msg) remote = body.msg
merged = Object.assign(local || {}, remote)
if local?
merged = @options.merge(local, remote)
else
merged = remote
this._tag(merged, body.tag) this._tag(merged, body.tag)
# Format an annotation into an RPC message body with the provided formatter. # Format an annotation into an RPC message body with the provided formatter.
...@@ -184,5 +88,5 @@ module.exports = class AnnotationSync ...@@ -184,5 +88,5 @@ module.exports = class AnnotationSync
this._tag(ann) this._tag(ann)
{ {
tag: ann.$$tag tag: ann.$$tag
msg: @options.formatter(ann) msg: ann
} }
...@@ -26,7 +26,7 @@ require('../vendor/annotator.document'); // Does not export the plugin :( ...@@ -26,7 +26,7 @@ require('../vendor/annotator.document'); // Does not export the plugin :(
// Cross-frame communication // Cross-frame communication
Annotator.Plugin.CrossFrame = require('./plugin/cross-frame'); Annotator.Plugin.CrossFrame = require('./plugin/cross-frame');
Annotator.Plugin.CrossFrame.AnnotationSync = require('../annotation-sync'); Annotator.Plugin.CrossFrame.AnnotationSync = require('./annotation-sync');
Annotator.Plugin.CrossFrame.Bridge = require('../bridge'); Annotator.Plugin.CrossFrame.Bridge = require('../bridge');
Annotator.Plugin.CrossFrame.Discovery = require('../discovery'); Annotator.Plugin.CrossFrame.Discovery = require('../discovery');
......
...@@ -20,7 +20,7 @@ module.exports = class CrossFrame extends Annotator.Plugin ...@@ -20,7 +20,7 @@ module.exports = class CrossFrame extends Annotator.Plugin
bridge = new CrossFrame.Bridge() bridge = new CrossFrame.Bridge()
opts = extract(options, 'on', 'emit', 'formatter', 'parser') opts = extract(options, 'on', 'emit')
annotationSync = new CrossFrame.AnnotationSync(bridge, opts) annotationSync = new CrossFrame.AnnotationSync(bridge, opts)
this.pluginInit = -> this.pluginInit = ->
......
...@@ -53,12 +53,10 @@ describe 'Annotator.Plugin.CrossFrame', -> ...@@ -53,12 +53,10 @@ describe 'Annotator.Plugin.CrossFrame', ->
assert.called(CrossFrame.AnnotationSync) assert.called(CrossFrame.AnnotationSync)
it 'passes along options to AnnotationSync', -> it 'passes along options to AnnotationSync', ->
formatter = (x) -> x createCrossFrame()
createCrossFrame(formatter: formatter)
assert.calledWith(CrossFrame.AnnotationSync, fakeBridge, { assert.calledWith(CrossFrame.AnnotationSync, fakeBridge, {
on: sinon.match.func on: sinon.match.func
emit: sinon.match.func emit: sinon.match.func
formatter: formatter
}) })
describe '.pluginInit', -> describe '.pluginInit', ->
......
EventEmitter = require('tiny-emitter')
AnnotationSync = require('../annotation-sync')
describe 'AnnotationSync', ->
sandbox = sinon.sandbox.create()
publish = null
fakeBridge = null
createAnnotationSync = null
createChannel = -> {call: sandbox.stub()}
options = null
PARENT_WINDOW = 'PARENT_WINDOW'
beforeEach ->
listeners = {}
publish = (method, args...) -> listeners[method](args...)
fakeWindow = parent: PARENT_WINDOW
fakeBridge =
on: sandbox.spy((method, fn) -> listeners[method] = fn)
call: sandbox.stub()
onConnect: sandbox.stub()
links: []
emitter = new EventEmitter();
options =
on: emitter.on.bind(emitter)
emit: emitter.emit.bind(emitter)
createAnnotationSync = ->
new AnnotationSync(fakeBridge, options)
afterEach: -> sandbox.restore()
describe 'channel event handlers', ->
assertBroadcast = (channelEvent, publishEvent) ->
it 'broadcasts the "' + publishEvent + '" event over the local event bus', ->
ann = {id: 1, $$tag: 'tag1'}
annSync = createAnnotationSync()
eventStub = sinon.stub()
options.on(publishEvent, eventStub)
publish(channelEvent, {msg: ann}, ->)
assert.calledWith(eventStub, ann)
assertReturnValue = (channelEvent) ->
it 'calls back with a formatted annotation', (done) ->
ann = {id: 1, $$tag: 'tag1'}
annSync = createAnnotationSync()
callback = (err, ret) ->
assert.isNull(err)
assert.deepEqual(ret, {tag: 'tag1', msg: ann})
done()
publish(channelEvent, {msg: ann}, callback)
assertCacheState = (channelEvent) ->
it 'removes an existing entry from the cache before the event is triggered', ->
options.emit = -> assert(!annSync.cache['tag1'])
ann = {id: 1, $$tag: 'tag1'}
annSync = createAnnotationSync()
annSync.cache['tag1'] = ann
publish(channelEvent, {msg: ann}, ->)
it 'ensures the annotation is inserted in the cache', ->
ann = {id: 1, $$tag: 'tag1'}
annSync = createAnnotationSync()
publish(channelEvent, {msg: ann}, ->)
assert.equal(annSync.cache['tag1'], ann)
describe 'the "deleteAnnotation" event', ->
assertBroadcast('deleteAnnotation', 'annotationDeleted')
assertReturnValue('deleteAnnotation')
it 'removes an existing entry from the cache before the event is triggered', ->
options.emit = -> assert(!annSync.cache['tag1'])
ann = {id: 1, $$tag: 'tag1'}
annSync = createAnnotationSync()
annSync.cache['tag1'] = ann
publish('deleteAnnotation', {msg: ann}, ->)
it 'removes the annotation from the cache', ->
ann = {id: 1, $$tag: 'tag1'}
annSync = createAnnotationSync()
publish('deleteAnnotation', {msg: ann}, ->)
assert(!annSync.cache['tag1'])
describe 'the "loadAnnotations" event', ->
it 'publishes the "annotationsLoaded" event', ->
loadedStub = sinon.stub()
options.on('annotationsLoaded', loadedStub)
annSync = createAnnotationSync()
annotations = [{id: 1, $$tag: 'tag1'}, {id: 2, $$tag: 'tag2'}, {id: 3, $$tag: 'tag3'}]
bodies = ({msg: ann, tag: ann.$$tag} for ann in annotations)
publish('loadAnnotations', bodies, ->)
assert.calledWith(loadedStub, annotations)
describe 'event handlers', ->
describe 'the "beforeAnnotationCreated" event', ->
it 'proxies the event over the bridge', ->
ann = {id: 1}
annSync = createAnnotationSync()
options.emit('beforeAnnotationCreated', ann)
assert.called(fakeBridge.call)
assert.calledWith(fakeBridge.call, 'beforeCreateAnnotation',
{msg: ann, tag: ann.$$tag}, sinon.match.func)
it 'returns early if the annotation has a tag', ->
ann = {id: 1, $$tag: 'tag1'}
annSync = createAnnotationSync()
options.emit('beforeAnnotationCreated', ann)
assert.notCalled(fakeBridge.call)
This diff is collapsed.
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