Skip to content

Commit 3af3928

Browse files
committed
Switch out jschannel for frame-rpc
The frame-rpc module is a proper CommonJS module and it's footprint is really tiny. Its protocol is simpler. It doesn't handle connection and buffering, but with our onConnect callback we don't need buffering. The connection handshake that jschannel did was so trivial it's been re-implemented here (each side tries to connect to the other once). We have to handle timeouts ourselves, but that's all hidden in the bridge code. In exchange, we get a simpler API, we get rid of the call/notify distinction in favor of just passing a callback or not and we avoid the excess overhead of the recursion guard in the serialization code that was giving us false positive issues with the document title. This is one step toward removing all the browserify-shim requiring libraries from the injected bundle, which will eventually fix #2397.
1 parent 306dc79 commit 3af3928

18 files changed

+318
-1118
lines changed

h/static/scripts/annotation-sync.coffee

Lines changed: 18 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -58,49 +58,47 @@ module.exports = class AnnotationSync
5858
getAnnotationForTag: (tag) ->
5959
@cache[tag] or null
6060

61-
sync: (annotations, cb) ->
61+
sync: (annotations) ->
6262
annotations = (this._format a for a in annotations)
63-
@bridge.call
64-
method: 'sync'
65-
params: annotations
66-
callback: cb
63+
@bridge.call 'sync', annotations, (err, annotations = []) =>
64+
for a in annotations
65+
this._parse(a)
6766
this
6867

6968
# Handlers for messages arriving through a channel
7069
_channelListeners:
71-
'beforeCreateAnnotation': (txn, body) ->
70+
'beforeCreateAnnotation': (body, cb) ->
7271
annotation = this._parse(body)
7372
delete @cache[annotation.$$tag]
7473
@_emit 'beforeAnnotationCreated', annotation
7574
@cache[annotation.$$tag] = annotation
76-
this._format annotation
75+
cb(null, this._format(annotation))
7776

78-
'createAnnotation': (txn, body) ->
77+
'createAnnotation': (body, cb) ->
7978
annotation = this._parse(body)
8079
delete @cache[annotation.$$tag]
8180
@_emit 'annotationCreated', annotation
8281
@cache[annotation.$$tag] = annotation
83-
this._format annotation
82+
cb(null, this._format(annotation))
8483

85-
'updateAnnotation': (txn, body) ->
84+
'updateAnnotation': (body, cb) ->
8685
annotation = this._parse(body)
8786
delete @cache[annotation.$$tag]
8887
@_emit('beforeAnnotationUpdated', annotation)
8988
@_emit('annotationUpdated', annotation)
9089
@cache[annotation.$$tag] = annotation
91-
this._format annotation
90+
cb(null, this._format(annotation))
9291

93-
'deleteAnnotation': (txn, body) ->
92+
'deleteAnnotation': (body, cb) ->
9493
annotation = this._parse(body)
9594
delete @cache[annotation.$$tag]
9695
@_emit('annotationDeleted', annotation)
97-
res = this._format(annotation)
98-
res
96+
cb(null, this._format(annotation))
9997

100-
'sync': (ctx, bodies) ->
101-
(this._format(this._parse(b)) for b in bodies)
98+
'sync': (bodies, cb) ->
99+
cb(null, (this._format(this._parse(b)) for b in bodies))
102100

103-
'loadAnnotations': (txn, bodies) ->
101+
'loadAnnotations': (bodies) ->
104102
annotations = (this._parse(a) for a in bodies)
105103
@_emit('loadAnnotations', annotations)
106104

@@ -127,17 +125,13 @@ module.exports = class AnnotationSync
127125
'annotationsLoaded': (annotations) ->
128126
bodies = (this._format a for a in annotations when not a.$$tag)
129127
return unless bodies.length
130-
@bridge.notify
131-
method: 'loadAnnotations'
132-
params: bodies
128+
@bridge.call('loadAnnotations', bodies)
133129

134130
_syncCache: (channel) ->
135131
# Synchronise (here to there) the items in our cache
136132
annotations = (this._format a for t, a of @cache)
137133
if annotations.length
138-
channel.notify
139-
method: 'loadAnnotations'
140-
params: annotations
134+
channel.call('loadAnnotations', annotations)
141135

142136
_mkCallRemotelyAndParseResults: (method, callBack) ->
143137
(annotation) =>
@@ -148,11 +142,7 @@ module.exports = class AnnotationSync
148142
callBack? failure, results
149143

150144
# Call the remote method
151-
options =
152-
method: method
153-
callback: wrappedCallback
154-
params: this._format(annotation)
155-
@bridge.call(options)
145+
@bridge.call(method, this._format(annotation), wrappedCallback)
156146

157147
# Parse returned message bodies to update cache with any changes made remotely
158148
_parseResults: (results) ->

h/static/scripts/annotation-ui-sync.coffee

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,18 @@ module.exports = class AnnotationUISync
1818
tags.map(annotationSync.getAnnotationForTag, annotationSync)
1919

2020
channelListeners =
21-
showAnnotations: (ctx, tags=[]) ->
21+
showAnnotations: (tags=[]) ->
2222
annotations = getAnnotationsByTags(tags)
2323
annotationUI.selectAnnotations(annotations)
24-
focusAnnotations: (ctx, tags=[]) ->
24+
focusAnnotations: (tags=[]) ->
2525
annotations = getAnnotationsByTags(tags)
2626
annotationUI.focusAnnotations(annotations)
27-
toggleAnnotationSelection: (ctx, tags=[]) ->
27+
toggleAnnotationSelection: (tags=[]) ->
2828
annotations = getAnnotationsByTags(tags)
2929
annotationUI.xorSelectedAnnotations(annotations)
30-
setVisibleHighlights: (ctx, state) ->
30+
setVisibleHighlights: (state) ->
3131
annotationUI.visibleHighlights = Boolean(state)
32-
bridge.notify(method: 'setVisibleHighlights', params: state)
32+
bridge.call('setVisibleHighlights', state)
3333

3434
# Because the channel events are all outside of the angular framework we
3535
# need to inform Angular that it needs to re-check it's state and re-draw
@@ -45,8 +45,6 @@ module.exports = class AnnotationUISync
4545
onConnect = (channel, source) ->
4646
# Allow the host to define its own state
4747
unless source is $window.parent
48-
channel.notify
49-
method: 'setVisibleHighlights'
50-
params: annotationUI.visibleHighlights
48+
channel.call('setVisibleHighlights', annotationUI.visibleHighlights)
5149

5250
bridge.onConnect(onConnect)

h/static/scripts/annotator/guest.coffee

Lines changed: 17 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,6 @@ module.exports = class Guest extends Annotator
7272
formatted = {}
7373
for k, v of annotation when k isnt 'anchors'
7474
formatted[k] = v
75-
# Work around issue in jschannel where a repeated object is considered
76-
# recursive, even if it is not its own ancestor.
77-
if formatted.document?.title
78-
formatted.document.title = formatted.document.title.slice()
7975
formatted
8076

8177
this.addPlugin('CrossFrame', cfOptions)
@@ -115,21 +111,20 @@ module.exports = class Guest extends Annotator
115111
crossframe.onConnect(=> this.publish('panelReady'))
116112
crossframe.on('onEditorHide', this.onEditorHide)
117113
crossframe.on('onEditorSubmit', this.onEditorSubmit)
118-
crossframe.on 'focusAnnotations', (ctx, tags=[]) =>
114+
crossframe.on 'focusAnnotations', (tags=[]) =>
119115
for anchor in @anchors when anchor.highlights?
120116
toggle = anchor.annotation.$$tag in tags
121117
$(anchor.highlights).toggleClass('annotator-hl-focused', toggle)
122-
crossframe.on 'scrollToAnnotation', (ctx, tag) =>
118+
crossframe.on 'scrollToAnnotation', (tag) =>
123119
for anchor in @anchors when anchor.highlights?
124120
if anchor.annotation.$$tag is tag
125121
$(anchor.highlights).scrollintoview()
126122
return
127-
crossframe.on 'getDocumentInfo', (trans) =>
128-
trans.delayReturn(true)
123+
crossframe.on 'getDocumentInfo', (cb) =>
129124
this.getDocumentInfo()
130-
.then((info) -> trans.complete(info))
131-
.catch((reason) -> trans.error(reason))
132-
crossframe.on 'setVisibleHighlights', (ctx, state) =>
125+
.then((info) -> cb(null, info))
126+
.catch((reason) -> cb(reason))
127+
crossframe.on 'setVisibleHighlights', (state) =>
133128
this.publish 'setVisibleHighlights', state
134129

135130
_setupWrapper: ->
@@ -315,24 +310,20 @@ module.exports = class Guest extends Annotator
315310
return annotation
316311

317312
showAnnotations: (annotations) =>
318-
@crossframe?.notify
319-
method: "showAnnotations"
320-
params: (a.$$tag for a in annotations)
313+
tags = (a.$$tag for a in annotations)
314+
@crossframe?.call('showAnnotations', tags)
321315

322316
toggleAnnotationSelection: (annotations) =>
323-
@crossframe?.notify
324-
method: "toggleAnnotationSelection"
325-
params: (a.$$tag for a in annotations)
317+
tags = (a.$$tag for a in annotations)
318+
@crossframe?.call('toggleAnnotationSelection', tags)
326319

327320
updateAnnotations: (annotations) =>
328-
@crossframe?.notify
329-
method: "updateAnnotations"
330-
params: (a.$$tag for a in annotations)
321+
tags = (a.$$tag for a in annotations)
322+
@crossframe?.call('updateAnnotations', tags)
331323

332324
focusAnnotations: (annotations) =>
333-
@crossframe?.notify
334-
method: "focusAnnotations"
335-
params: (a.$$tag for a in annotations)
325+
tags = (a.$$tag for a in annotations)
326+
@crossframe?.call('focusAnnotations', tags)
336327

337328
onSuccessfulSelection: (event, immediate) ->
338329
unless event?
@@ -396,11 +387,7 @@ module.exports = class Guest extends Annotator
396387
# Pass true to show the highlights in the frame or false to disable.
397388
setVisibleHighlights: (shouldShowHighlights) ->
398389
return if @visibleHighlights == shouldShowHighlights
399-
400-
@crossframe?.notify
401-
method: 'setVisibleHighlights'
402-
params: shouldShowHighlights
403-
390+
@crossframe?.call('setVisibleHighlights', shouldShowHighlights)
404391
this.toggleHighlightClass(shouldShowHighlights)
405392

406393
toggleHighlightClass: (shouldShowHighlights) ->
@@ -413,11 +400,11 @@ module.exports = class Guest extends Annotator
413400

414401
# Open the sidebar
415402
showFrame: ->
416-
@crossframe?.notify method: 'open'
403+
@crossframe?.call('open')
417404

418405
# Close the sidebar
419406
hideFrame: ->
420-
@crossframe?.notify method: 'back'
407+
@crossframe?.call('back')
421408

422409
onAdderMouseup: (event) ->
423410
event.preventDefault()

h/static/scripts/annotator/plugin/cross-frame.coffee

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ extract = extract = (obj, keys...) ->
1212
# as keeping the annotation state in sync with the sidebar application, this
1313
# frame acts as the bridge client, the sidebar is the server. This plugin
1414
# can also be used to send messages through to the sidebar using the
15-
# notify method.
15+
# call method.
1616
module.exports = class CrossFrame extends Annotator.Plugin
1717
constructor: (elem, options) ->
1818
super
@@ -41,8 +41,8 @@ module.exports = class CrossFrame extends Annotator.Plugin
4141
this.on = (event, fn) ->
4242
bridge.on(event, fn)
4343

44-
this.notify = (message) ->
45-
bridge.notify(message)
44+
this.call = (message, args...) ->
45+
bridge.call(message, args...)
4646

4747
this.onConnect = (fn) ->
4848
bridge.onConnect(fn)

h/static/scripts/annotator/plugin/test/cross-frame-test.coffee

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ describe 'Annotator.Plugin.CrossFrame', ->
2121
fakeBridge =
2222
createChannel: sandbox.stub()
2323
onConnect: sandbox.stub()
24-
notify: sandbox.stub()
24+
call: sandbox.stub()
2525
on: sandbox.stub()
2626

2727
fakeAnnotationSync =
@@ -91,11 +91,11 @@ describe 'Annotator.Plugin.CrossFrame', ->
9191
bridge.on('event', 'arg')
9292
assert.calledWith(fakeBridge.on, 'event', 'arg')
9393

94-
describe '.notify', ->
94+
describe '.call', ->
9595
it 'proxies the call to the bridge', ->
9696
bridge = createCrossFrame()
97-
bridge.notify(method: 'method')
98-
assert.calledWith(fakeBridge.notify, method: 'method')
97+
bridge.call('method', 'arg1', 'arg2')
98+
assert.calledWith(fakeBridge.call, 'method', 'arg1', 'arg2')
9999

100100
describe '.onConnect', ->
101101
it 'proxies the call to the bridge', ->

h/static/scripts/annotator/test/guest-test.coffee

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -144,18 +144,11 @@ describe 'Guest', ->
144144
formatted = options.formatter(ann)
145145
assert.equal(formatted.$$tag, 'tag1')
146146

147-
it 'strips the "anchors" property', ->
147+
it 'strips properties that are not whitelisted', ->
148148
ann = {$$tag: 'tag1', anchors: []}
149149
formatted = options.formatter(ann)
150150
assert.notProperty(formatted, 'anchors')
151151

152-
it 'clones the document.title array if present', ->
153-
title = ['Page Title']
154-
ann = {$$tag: 'tag1', document: {title: title}}
155-
formatted = options.formatter(ann)
156-
assert.notStrictEqual(title, formatted.document.title)
157-
assert.deepEqual(title, formatted.document.title)
158-
159152
describe 'annotation UI events', ->
160153
emitGuestEvent = (event, args...) ->
161154
fn(args...) for [evt, fn] in fakeCrossFrame.on.args when event == evt
@@ -183,7 +176,7 @@ describe 'Guest', ->
183176
{annotation: {$$tag: 'tag1'}, highlights: highlight0.toArray()}
184177
{annotation: {$$tag: 'tag2'}, highlights: highlight1.toArray()}
185178
]
186-
emitGuestEvent('focusAnnotations', 'ctx', ['tag1'])
179+
emitGuestEvent('focusAnnotations', ['tag1'])
187180
assert.isTrue(highlight0.hasClass('annotator-hl-focused'))
188181

189182
it 'unfocuses any annotations without a matching tag', ->
@@ -210,7 +203,7 @@ describe 'Guest', ->
210203
guest.anchors = [
211204
{annotation: {$$tag: 'tag1'}, highlights: highlight.toArray()}
212205
]
213-
emitGuestEvent('scrollToAnnotation', 'ctx', 'tag1')
206+
emitGuestEvent('scrollToAnnotation', 'tag1')
214207
assert.calledOn($.fn.scrollintoview, sinon.match(highlight))
215208

216209
describe 'on "getDocumentInfo" event', ->
@@ -227,44 +220,34 @@ describe 'Guest', ->
227220
sandbox.restore()
228221

229222
it 'calls the callback with the href and pdf metadata', (done) ->
230-
assertComplete = (payload) ->
223+
assertComplete = (err, payload) ->
231224
try
232225
assert.equal(payload.uri, document.location.href)
233226
assert.equal(payload.metadata, metadata)
234227
done()
235228
catch e
236229
done(e)
237230

238-
ctx = {complete: assertComplete, delayReturn: sandbox.stub()}
239231
metadata = {title: 'hi'}
240232
promise = Promise.resolve(metadata)
241233
guest.plugins.PDF.getMetadata.returns(promise)
242234

243-
emitGuestEvent('getDocumentInfo', ctx)
235+
emitGuestEvent('getDocumentInfo', assertComplete)
244236

245237
it 'calls the callback with the href and basic metadata if pdf fails', (done) ->
246-
assertComplete = (payload) ->
238+
assertComplete = (err, payload) ->
247239
try
248240
assert.equal(payload.uri, window.location.href)
249241
assert.deepEqual(payload.metadata, metadata)
250242
done()
251243
catch e
252244
done(e)
253245

254-
ctx = {complete: assertComplete, delayReturn: sandbox.stub()}
255246
metadata = {title: 'hi', link: [{href: window.location.href}]}
256-
257247
promise = Promise.reject(new Error('Not a PDF document'))
258248
guest.plugins.PDF.getMetadata.returns(promise)
259249

260-
emitGuestEvent('getDocumentInfo', ctx)
261-
262-
it 'notifies the channel that the return value is async', ->
263-
delete guest.plugins.PDF
264-
265-
ctx = {complete: sandbox.stub(), delayReturn: sandbox.stub()}
266-
emitGuestEvent('getDocumentInfo', ctx)
267-
assert.calledWith(ctx.delayReturn, true)
250+
emitGuestEvent('getDocumentInfo', assertComplete)
268251

269252
describe 'onAdderMouseUp', ->
270253
it 'it prevents the default browser action when triggered', () ->

h/static/scripts/annotator/test/host-test.coffee

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ describe 'Host', ->
1616
fakeCrossFrame = {}
1717
fakeCrossFrame.onConnect = sandbox.stub().returns(fakeCrossFrame)
1818
fakeCrossFrame.on = sandbox.stub().returns(fakeCrossFrame)
19-
fakeCrossFrame.notify = sandbox.stub().returns(fakeCrossFrame)
19+
fakeCrossFrame.call = sandbox.spy()
2020

2121
Annotator.Plugin.CrossFrame = -> fakeCrossFrame
2222

0 commit comments

Comments
 (0)