Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
C
coopwire-hypothesis
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
孙灵跃 Leon Sun
coopwire-hypothesis
Commits
00c59e0e
Commit
00c59e0e
authored
Nov 09, 2021
by
Eduardo Sanz García
Committed by
Eduardo
Nov 15, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Apply suggestions from code review
Co-authored-by:
Robert Knight
<
robertknight@gmail.com
>
parent
e78ada77
Changes
5
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
296 additions
and
321 deletions
+296
-321
port-finder.js
src/shared/port-finder.js
+48
-102
port-provider.js
src/shared/port-provider.js
+127
-128
port-util.js
src/shared/port-util.js
+3
-1
port-finder-test.js
src/shared/test/port-finder-test.js
+80
-77
port-provider-test.js
src/shared/test/port-provider-test.js
+38
-13
No files found.
src/shared/port-finder.js
View file @
00c59e0e
...
@@ -2,7 +2,7 @@ import { ListenerCollection } from './listener-collection';
...
@@ -2,7 +2,7 @@ import { ListenerCollection } from './listener-collection';
import
{
isMessageEqual
,
SOURCE
as
source
}
from
'./port-util'
;
import
{
isMessageEqual
,
SOURCE
as
source
}
from
'./port-util'
;
const
MAX_WAIT_FOR_PORT
=
1000
*
30
;
const
MAX_WAIT_FOR_PORT
=
1000
*
30
;
const
POLLING_INTERVAL_FOR_PORT
=
50
0
;
const
POLLING_INTERVAL_FOR_PORT
=
25
0
;
/**
/**
* @typedef {import('../types/annotator').Destroyable} Destroyable
* @typedef {import('../types/annotator').Destroyable} Destroyable
...
@@ -12,8 +12,7 @@ const POLLING_INTERVAL_FOR_PORT = 500;
...
@@ -12,8 +12,7 @@ const POLLING_INTERVAL_FOR_PORT = 500;
*/
*/
/**
/**
* PortFinder class should be used in frames that are not the `host` frame. It
* PortFinder helps to discover `MessagePort` on a specific channel.
* helps to discover `MessagePort` on a specific channel.
*
*
* Channel nomenclature is `[frame1]-[frame2]` so that:
* Channel nomenclature is `[frame1]-[frame2]` so that:
* - `port1` should be owned by/transferred to `frame1`, and
* - `port1` should be owned by/transferred to `frame1`, and
...
@@ -28,118 +27,65 @@ export class PortFinder {
...
@@ -28,118 +27,65 @@ export class PortFinder {
this
.
_listeners
=
new
ListenerCollection
();
this
.
_listeners
=
new
ListenerCollection
();
}
}
// Two important characteristics of `MessagePort`:
destroy
()
{
// - it can only be used by one frame; the port is neutered if, after started to
this
.
_listeners
.
removeAll
();
// be used to receive messages, the port is transferred to a different frame.
}
// - messages are queued until the other port is ready to listen (`port.start()`)
/**
/**
* `guest-host` communication
* Polls the hostFrame for a specific port and returns a Promise of the port.
* @typedef {{channel: 'guest-host', hostFrame: Window, port: 'guest'}} options0
*
* `guest-sidebar` communication
* @typedef {{channel: 'guest-sidebar', hostFrame: Window, port: 'guest'}} options1
*
*
* `host-sidebar` communication
* @param {object} options
* @typedef {{channel: 'host-sidebar', hostFrame: Window, port: 'sidebar'}} options2
* @param {Channel} options.channel - requested channel
*
* @param {Window} options.hostFrame - frame where the hypothesis client is
* `notebook-sidebar` communication
* loaded and `PortProvider` is listening for messages
* @typedef {{channel: 'notebook-sidebar', hostFrame: Window, port: 'notebook'}} options3
* @param {Port} options.port - requested port
*
* @param {options0|options1|options2|options3} options
* @return {Promise<MessagePort>}
* @return {Promise<MessagePort>}
*/
*/
discover
(
options
)
{
discover
({
channel
,
hostFrame
,
port
})
{
const
{
channel
,
port
}
=
options
;
let
isValidRequest
=
false
;
if
(
(
channel
===
'guest-host'
&&
port
===
'guest'
)
||
(
channel
===
'guest-sidebar'
&&
port
===
'guest'
)
||
(
channel
===
'host-sidebar'
&&
port
===
'sidebar'
)
||
(
channel
===
'notebook-sidebar'
&&
port
===
'notebook'
)
)
{
isValidRequest
=
true
;
}
return
new
Promise
((
resolve
,
reject
)
=>
{
return
new
Promise
((
resolve
,
reject
)
=>
{
if
(
if
(
!
isValidRequest
)
{
(
channel
===
'guest-host'
&&
port
===
'guest'
)
||
reject
(
new
Error
(
'Invalid request of channel/port'
));
(
channel
===
'guest-sidebar'
&&
port
===
'guest'
)
||
(
channel
===
'host-sidebar'
&&
port
===
'sidebar'
)
||
(
channel
===
'notebook-sidebar'
&&
port
===
'notebook'
)
)
{
this
.
_requestPort
({
...
options
,
reject
,
resolve
,
});
return
;
return
;
}
}
reject
(
new
Error
(
'Invalid request of channel/port'
));
function
postRequest
()
{
});
hostFrame
.
postMessage
({
channel
,
port
,
source
,
type
:
'request'
},
'*'
);
}
}
/**
* @typedef RequestPortOptions
* @prop {Channel} channel - requested channel
* @prop {Window} hostFrame - the frame where the hypothesis client is loaded.
* It is used to send a `window.postMessage`.
* @prop {Port} port - requested port
* @prop {(reason: Error) => void} reject - execute the `Promise.reject` in case
* the `host` frame takes too long to answer the request.
* @prop {(port: MessagePort) => void} resolve - execute the `Promise.resolve`
* when `host` frame successfully answers the request.
*/
/**
* Register a listener for the port `offer` and sends a request for one port.
*
* @param {RequestPortOptions} options
*/
_requestPort
({
channel
,
hostFrame
,
port
,
reject
,
resolve
})
{
function
postRequest
()
{
hostFrame
.
postMessage
({
channel
,
port
,
source
,
type
:
'request'
},
'*'
);
}
const
intervalId
=
window
.
setInterval
(
()
=>
postRequest
(),
POLLING_INTERVAL_FOR_PORT
);
// The `host` frame maybe busy, that's why we should wait.
const
intervalId
=
setInterval
(
const
timeoutId
=
window
.
setTimeout
(()
=>
{
()
=>
postRequest
(),
clearInterval
(
intervalId
);
POLLING_INTERVAL_FOR_PORT
reject
(
new
Error
(
`Unable to find '
${
port
}
' port on '
${
channel
}
' channel`
)
);
);
},
MAX_WAIT_FOR_PORT
);
// TODO: It would be nice to remove the listener after receiving the port.
this
.
_listeners
.
add
(
window
,
'message'
,
event
=>
this
.
_handlePortOffer
(
/** @type {MessageEvent} */
(
event
),
{
intervalId
,
message
:
{
channel
,
port
,
source
,
type
:
'offer'
},
resolve
,
timeoutId
,
})
);
postRequest
();
// The `host` frame maybe busy, that's why we should wait.
}
const
timeoutId
=
window
.
setTimeout
(()
=>
{
clearInterval
(
intervalId
);
reject
(
new
Error
(
`Unable to find '
${
port
}
' port on '
${
channel
}
' channel`
)
);
},
MAX_WAIT_FOR_PORT
);
/**
// TODO: It would be nice to remove the listener after receiving the port.
* Resolve with a MessagePort when the `offer` message matches.
this
.
_listeners
.
add
(
window
,
'message'
,
event
=>
{
*
const
{
data
,
ports
}
=
/** @type {MessageEvent} */
(
event
);
* @param {MessageEvent} event
if
(
isMessageEqual
(
data
,
{
channel
,
port
,
source
,
type
:
'offer'
}))
{
* @param {object} options
clearInterval
(
intervalId
);
* @param {Message} options.message
clearTimeout
(
timeoutId
);
* @param {(port: MessagePort) => void} options.resolve
resolve
(
ports
[
0
]);
* @param {number} options.timeoutId
}
* @param {number} [options.intervalId]
});
*/
_handlePortOffer
(
{
data
,
ports
},
{
message
,
resolve
,
timeoutId
,
intervalId
}
)
{
if
(
isMessageEqual
(
data
,
message
))
{
clearInterval
(
intervalId
);
clearTimeout
(
timeoutId
);
resolve
(
ports
[
0
]);
}
}
destroy
()
{
postRequest
();
this
.
_listeners
.
removeAll
(
);
}
);
}
}
}
}
src/shared/port-provider.js
View file @
00c59e0e
This diff is collapsed.
Click to expand it.
src/shared/port-util.js
View file @
00c59e0e
// Because there are many `postMessages` on the `host` frame, the SOURCE property
// Because there are many `postMessages` on the `host` frame, the SOURCE property
// is added to the hypothesis `postMessages` to identify the provenance of the
// is added to the hypothesis `postMessages` to identify the provenance of the
// message and avoid listening to messages that could have the same properties
// message and avoid listening to messages that could have the same properties
// but different source. This is not a
is not a
security feature but an
// but different source. This is not a security feature but an
// anti-collision mechanism.
// anti-collision mechanism.
export
const
SOURCE
=
'hypothesis'
;
export
const
SOURCE
=
'hypothesis'
;
/**
/**
* These types are the used in by `PortProvider` and `PortFinder` for the
* inter-frame discovery and communication processes.
* @typedef {'guest-host'|'guest-sidebar'|'host-sidebar'|'notebook-sidebar'} Channel
* @typedef {'guest-host'|'guest-sidebar'|'host-sidebar'|'notebook-sidebar'} Channel
* @typedef {'guest'|'host'|'notebook'|'sidebar'} Port
* @typedef {'guest'|'host'|'notebook'|'sidebar'} Port
*
*
...
...
src/shared/test/port-finder-test.js
View file @
00c59e0e
...
@@ -26,92 +26,93 @@ describe('PortFinder', () => {
...
@@ -26,92 +26,93 @@ describe('PortFinder', () => {
portFinder
.
destroy
();
portFinder
.
destroy
();
});
});
[
describe
(
'#destroy'
,
()
=>
{
{
channel
:
'invalid'
,
port
:
'guest'
},
it
(
'ignores `offer` messages of ports'
,
async
()
=>
{
{
channel
:
'guest-host'
,
port
:
'invalid'
},
].
forEach
(({
channel
,
port
})
=>
it
(
'rejects if requesting an invalid port'
,
async
()
=>
{
let
error
;
let
error
;
const
channel
=
'host-sidebar'
;
const
port
=
'sidebar'
;
const
{
port1
}
=
new
MessageChannel
();
const
clock
=
sinon
.
useFakeTimers
();
try
{
try
{
await
portFinder
.
discover
({
portFinder
channel
,
.
discover
({
hostFrame
:
window
,
channel
,
port
,
hostFrame
:
window
,
port
,
})
.
catch
(
e
=>
(
error
=
e
));
portFinder
.
destroy
();
sendMessage
({
data
:
{
channel
,
port
,
source
,
type
:
'offer'
},
ports
:
[
port1
],
});
});
}
catch
(
e
)
{
clock
.
tick
(
30000
);
error
=
e
;
}
finally
{
clock
.
restore
();
}
}
assert
.
equal
(
error
.
message
,
'Invalid request of channel/port'
);
})
);
[
{
channel
:
'guest-host'
,
port
:
'guest'
},
{
channel
:
'guest-sidebar'
,
port
:
'guest'
},
{
channel
:
'host-sidebar'
,
port
:
'sidebar'
},
{
channel
:
'notebook-sidebar'
,
port
:
'notebook'
},
].
forEach
(({
channel
,
port
})
=>
it
(
'resolves if requesting a valid port'
,
async
()
=>
{
const
{
port1
}
=
new
MessageChannel
();
let
resolvedPort
;
portFinder
.
discover
({
channel
,
hostFrame
:
window
,
port
,
})
.
then
(
port
=>
(
resolvedPort
=
port
));
sendMessage
({
data
:
{
channel
,
port
,
source
,
type
:
'offer'
},
ports
:
[
port1
],
});
await
delay
(
0
);
await
delay
(
0
);
assert
.
instanceOf
(
resolvedPort
,
MessagePort
);
assert
.
equal
(
})
error
.
message
,
);
"Unable to find 'sidebar' port on 'host-sidebar' channel"
);
it
(
"timeouts if host doesn't respond"
,
async
()
=>
{
});
let
error
;
});
const
channel
=
'host-sidebar'
;
const
port
=
'sidebar'
;
describe
(
'#discover'
,
()
=>
{
const
clock
=
sinon
.
useFakeTimers
();
[
{
channel
:
'invalid'
,
port
:
'guest'
},
try
{
{
channel
:
'guest-host'
,
port
:
'invalid'
},
portFinder
{
channel
:
'guest-host'
,
port
:
'host'
},
.
discover
({
].
forEach
(({
channel
,
port
})
=>
channel
,
it
(
'rejects if requesting an invalid port'
,
async
()
=>
{
hostFrame
:
window
,
let
error
;
port
,
try
{
})
await
portFinder
.
discover
({
.
catch
(
e
=>
(
error
=
e
));
channel
,
clock
.
tick
(
30000
);
hostFrame
:
window
,
}
finally
{
port
,
clock
.
restore
();
});
}
}
catch
(
e
)
{
error
=
e
;
assert
.
callCount
(
window
.
postMessage
,
61
);
}
assert
.
alwaysCalledWithExactly
(
assert
.
equal
(
error
.
message
,
'Invalid request of channel/port'
);
window
.
postMessage
,
})
{
channel
,
port
,
source
,
type
:
'request'
},
'*'
);
);
await
delay
(
0
);
[
{
channel
:
'guest-host'
,
port
:
'guest'
},
{
channel
:
'guest-sidebar'
,
port
:
'guest'
},
{
channel
:
'host-sidebar'
,
port
:
'sidebar'
},
{
channel
:
'notebook-sidebar'
,
port
:
'notebook'
},
].
forEach
(({
channel
,
port
})
=>
it
(
'resolves if requesting a valid port'
,
async
()
=>
{
const
{
port1
}
=
new
MessageChannel
();
let
resolvedPort
;
assert
.
equal
(
portFinder
error
.
message
,
.
discover
({
"Unable to find 'sidebar' port on 'host-sidebar' channel"
channel
,
hostFrame
:
window
,
port
,
})
.
then
(
port
=>
(
resolvedPort
=
port
));
sendMessage
({
data
:
{
channel
,
port
,
source
,
type
:
'offer'
},
ports
:
[
port1
],
});
await
delay
(
0
);
assert
.
instanceOf
(
resolvedPort
,
MessagePort
);
})
);
);
});
describe
(
'#destroy'
,
()
=>
{
it
(
"timeouts if host doesn't respond"
,
async
()
=>
{
it
(
'ignores `offer` messages of ports'
,
async
()
=>
{
let
error
;
let
error
;
const
channel
=
'host-sidebar'
;
const
channel
=
'host-sidebar'
;
const
port
=
'sidebar'
;
const
port
=
'sidebar'
;
const
{
port1
}
=
new
MessageChannel
();
const
clock
=
sinon
.
useFakeTimers
();
const
clock
=
sinon
.
useFakeTimers
();
try
{
try
{
...
@@ -122,16 +123,18 @@ describe('PortFinder', () => {
...
@@ -122,16 +123,18 @@ describe('PortFinder', () => {
port
,
port
,
})
})
.
catch
(
e
=>
(
error
=
e
));
.
catch
(
e
=>
(
error
=
e
));
portFinder
.
destroy
();
sendMessage
({
data
:
{
channel
,
port
,
source
,
type
:
'offer'
},
ports
:
[
port1
],
});
clock
.
tick
(
30000
);
clock
.
tick
(
30000
);
}
finally
{
}
finally
{
clock
.
restore
();
clock
.
restore
();
}
}
assert
.
callCount
(
window
.
postMessage
,
121
);
assert
.
alwaysCalledWithExactly
(
window
.
postMessage
,
{
channel
,
port
,
source
,
type
:
'request'
},
'*'
);
await
delay
(
0
);
await
delay
(
0
);
assert
.
equal
(
assert
.
equal
(
...
...
src/shared/test/port-provider-test.js
View file @
00c59e0e
...
@@ -23,7 +23,8 @@ describe('PortProvider', () => {
...
@@ -23,7 +23,8 @@ describe('PortProvider', () => {
beforeEach
(()
=>
{
beforeEach
(()
=>
{
sinon
.
stub
(
window
,
'postMessage'
);
sinon
.
stub
(
window
,
'postMessage'
);
portProvider
=
new
PortProvider
(
window
.
location
.
href
);
portProvider
=
new
PortProvider
(
window
.
location
.
origin
);
portProvider
.
listen
();
});
});
afterEach
(()
=>
{
afterEach
(()
=>
{
...
@@ -31,6 +32,23 @@ describe('PortProvider', () => {
...
@@ -31,6 +32,23 @@ describe('PortProvider', () => {
portProvider
.
destroy
();
portProvider
.
destroy
();
});
});
describe
(
'#destroy'
,
()
=>
{
it
(
'ignores valid port request if `PortFinder` has been destroyed'
,
async
()
=>
{
portProvider
.
destroy
();
sendMessage
({
data
:
{
channel
:
'host-sidebar'
,
port
:
'sidebar'
,
source
,
type
:
'request'
,
},
});
await
delay
(
0
);
assert
.
notCalled
(
window
.
postMessage
);
});
});
describe
(
'#getPort'
,
()
=>
{
describe
(
'#getPort'
,
()
=>
{
it
(
'returns `null` if called with wrong arguments'
,
()
=>
{
it
(
'returns `null` if called with wrong arguments'
,
()
=>
{
let
hostPort
;
let
hostPort
;
...
@@ -59,20 +77,30 @@ describe('PortProvider', () => {
...
@@ -59,20 +77,30 @@ describe('PortProvider', () => {
});
});
});
});
describe
(
'#
destroy
'
,
()
=>
{
describe
(
'#
listen
'
,
()
=>
{
it
(
'ignores
valid port request if `PortFinder` has been destroyed
'
,
async
()
=>
{
it
(
'ignores
all port request until start listening
'
,
async
()
=>
{
portProvider
.
destroy
();
portProvider
.
destroy
();
portProvider
=
new
PortProvider
(
window
.
location
.
origin
);
const
data
=
{
channel
:
'host-sidebar'
,
port
:
'sidebar'
,
source
,
type
:
'request'
,
};
sendMessage
({
sendMessage
({
data
:
{
data
,
channel
:
'host-sidebar'
,
port
:
'sidebar'
,
source
,
type
:
'request'
,
},
});
});
await
delay
(
0
);
await
delay
(
0
);
assert
.
notCalled
(
window
.
postMessage
);
assert
.
notCalled
(
window
.
postMessage
);
portProvider
.
listen
();
sendMessage
({
data
,
});
await
delay
(
0
);
assert
.
calledOnce
(
window
.
postMessage
);
});
});
});
});
...
@@ -94,7 +122,6 @@ describe('PortProvider', () => {
...
@@ -94,7 +122,6 @@ describe('PortProvider', () => {
data
,
data
,
source
:
new
MessageChannel
().
port1
,
source
:
new
MessageChannel
().
port1
,
});
});
await
delay
(
0
);
await
delay
(
0
);
assert
.
notCalled
(
window
.
postMessage
);
assert
.
notCalled
(
window
.
postMessage
);
...
@@ -132,7 +159,6 @@ describe('PortProvider', () => {
...
@@ -132,7 +159,6 @@ describe('PortProvider', () => {
data
,
data
,
origin
:
'https://dummy.com'
,
origin
:
'https://dummy.com'
,
});
});
await
delay
(
0
);
await
delay
(
0
);
assert
.
notCalled
(
window
.
postMessage
);
assert
.
notCalled
(
window
.
postMessage
);
...
@@ -148,7 +174,6 @@ describe('PortProvider', () => {
...
@@ -148,7 +174,6 @@ describe('PortProvider', () => {
sendMessage
({
sendMessage
({
data
,
data
,
});
});
await
delay
(
0
);
await
delay
(
0
);
assert
.
calledWith
(
assert
.
calledWith
(
...
@@ -217,7 +242,7 @@ describe('PortProvider', () => {
...
@@ -217,7 +242,7 @@ describe('PortProvider', () => {
it
(
'sends the reciprocal port of the `guest-host` channel (via listener)'
,
async
()
=>
{
it
(
'sends the reciprocal port of the `guest-host` channel (via listener)'
,
async
()
=>
{
const
handler
=
sinon
.
stub
();
const
handler
=
sinon
.
stub
();
portProvider
.
addEventListener
(
'onH
ostPortRequest'
,
handler
);
portProvider
.
on
(
'h
ostPortRequest'
,
handler
);
const
data
=
{
const
data
=
{
channel
:
'guest-host'
,
channel
:
'guest-host'
,
port
:
'guest'
,
port
:
'guest'
,
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment