thali
Version:
334 lines (311 loc) • 16.3 kB
JavaScript
var Promise = require("lie");
/** @module thaliMobileNativeWrapper */
/**
* @file
*
* This is the primary interface for those wishing to talk directly to the native layer. All the methods defined
* in this file are asynchronous. However, with the exception of {@link module:thaliMobileNativeWrapper.emitter} and
* {@link module:thaliMobileNativeWrapper.connect}, any time a method is called the invocation will immediately return
* but the request will actually be put on a queue and all incoming requests will be run out of that queue. This means
* that if one calls two start methods on say advertising or discovery then the first start method will execute, call
* back its promise and only then will the second start method start running. This restriction is in place to simplify
* the state model and reduce testing.
*
* ## Why not just call {@link module:thaliMobileNative} directly?
* Our contract in {@link module:thaliMobileNative} stipulates some behaviors that have to be enforced
* at the node.js layer. For example, we can only issue a single outstanding non-connect Mobile().callNative
* command at a time. We also want to change the callbacks from callbacks to promises as well as change
* registerToNative calls into Node.js events. All of that is handled here. So as a general rule nobody
* but this module should ever call {@link module:thaliMobileNative}. Anyone who wants to use the
* {@link module:thaliMobileNative} functionality should be calling this module.
*/
/*
METHODS
*/
/**
* This method MUST be called before any other method here other than registering for events on the emitter. This
* method will cause us to:
* - create a TCP server on a random port and host the router on that server.
* - create a {@link module:TCPServersManager}.
* - listen for the {@link module:TCPServersManager~failedConnection} event and then repeat it.
* - listen for the {@link module:TCPServersManager~routerPortConnectionFailed} event which we will then cause us to
* fire a {@link event:incomingConnectionToPortNumberFailed}.
* - call start on the {@link module:TCPServersManager} object and record the returned port.
*
* We MUST register for the native layer handlers exactly once.
*
* If the start fails then the object is not in start state and vice versa.
*
* This method is not idempotent. If called two times in a row without an intervening stop a "Call Stop!" Error
* MUST be returned.
*
* This method can be called after stop since this is a singleton object.
*
* @public
* @param {Object} router This is an Express Router object (for example, express-pouchdb is a router object) that the
* caller wants the non-TCP connections to be terminated with. This code will put that router at '/' so make sure your
* paths are set up appropriately.
* @returns {Promise<null|Error>}
*/
module.exports.start = function(router) {
return Promise.resolve();
};
/**
* This method will call all the stop methods, call stop on the
* {@link module:TCPServersManager} object and close the TCP server hosting the router.
*
* Once called the object is in stop state.
*
* This method is idempotent and so MUST be able to be called multiple times in a row without changing state.
*
* @returns {Promise<null|Error>}
*/
module.exports.stop = function() {
return Promise.resolve();
};
/**
* This method instructs the native layer to discover what other devices are within range using the platform's non-TCP
* P2P capabilities. When a device is discovered its information will be published via
* {@link event:nonTCPPeerAvailabilityChangedEvent}.
*
* This method is idempotent so multiple consecutive calls without an intervening call to stop will not cause a state
* change.
*
* This method MUST NOT be called if the object is not in start state or a "Call Start!" error MUST be returned.
*
* | Error String | Description |
* |--------------|-------------|
* | No Native Non-TCP Support | There are no non-TCP radios on this platform. |
* | Radio Turned Off | The radio(s) needed for this method are not turned on. |
* | Unspecified Error with Radio infrastructure | Something went wrong with the radios. Check the logs. |
* | Call Start! | The object is not in start state. |
*
* @public
* @returns {Promise<null|Error>}
* @throws {Error}
*/
module.exports.startListeningForAdvertisements = function() {
return Promise.resolve();
};
/**
* This method instructs the native layer to stop listening for discovery advertisements. Note that so long as
* discovery isn't occurring (because, for example, the radio needed isn't on) this method will return success.
*
* This method is idempotent and MAY be called even if startListeningForAdvertisements has not been called.
*
* This method MUST NOT terminate any existing connections created locally using
* {@link module:thaliMobileNativeWrapper.connect}.
*
* | Error String | Description |
* |--------------|-------------|
* | Failed | Somehow the stop method couldn't do its job. Check the logs. |
*
* @public
* @returns {Promise<null|Error>}
*/
module.exports.stopListeningForAdvertisements = function() {
return Promise.resolve();
};
/**
* This method has two separate but related functions. It's first function is to begin advertising the Thali peer's
* presence to other peers. The second purpose is to accept incoming non-TCP/IP connections (that will then be bridged
* to TCP/IP) from other peers.
*
* In Android these functions can be separated but with iOS the multi-peer connectivity framework is designed such
* that it is only possible for remote peers to connect to the current peer if and only if the current peer is
* advertising its presence. So we therefore have put the two functions together into a single method.
*
* This method MUST NOT be called unless in the start state otherwise a "Call Start!" error MUST be returned.
*
* ## Discovery
* Thali currently handles discovery by announcing over the discovery channel that the Thali peer has had a
* state change without providing any additional information, such as who the peer is or who the state changes
* are relevant to. The remote peers, when they get the state change notification, will have to connect to this
* peer in order to retrieve information about the state change.
*
* Therefore the purpose of this method is just to raise the "state changed" flag. Each time it is called a new
* event will be generated that will tell listeners that the system has changed state since the last call. Therefore
* this method is not idempotent since each call causes a state change.
*
* Once an advertisement is sent out as a result of calling this method typically any new peers who come in range
* will be able to retrieve the existing advertisement. So this is not a one time event but rather more of a case
* of publishing an ongoing advertisement regarding the peer's state.
*
* ## Incoming Connections
* By default all incoming TCP connections generated by
* {@link external:"Mobile('StartUpdateAdvertisingAndListenForIncomingConnections')".callNative} MUST be passed
* through a multiplex layer. The details of how this layer works are given in {@link module:TCPServersManager}.
* This method will pass the port from {@link module:TCPServersManager.start} output to
* {@link external:"Mobile('StartUpdateAdvertisingAndListenForIncomingConnections')".callNative}.
*
* ## Repeated calls
* By design this method is intended to be called multiple times without calling stop as each call causes the
* currently notification flag to change.
*
* | Error String | Description |
* |--------------|-------------|
* | No Native Non-TCP Support | There are no non-TCP radios on this platform. |
* | Radio Turned Off | The radio(s) needed for this method are not turned on. |
* | Unspecified Error with Radio infrastructure | Something went wrong with the radios. Check the logs. |
* | Call Start! | The object is not in start state. |
*
* @public
* @returns {Promise<null|Error>}
*/
module.exports.startUpdateAdvertisingAndListenForIncomingConnections = function() {
return Promise.resolve();
};
/**
* This method tells the native layer to stop advertising the presence of the peer, stop accepting incoming
* connections over the non-TCP/IP transport and to disconnect all existing non-TCP/IP transport incoming connections.
*
* Note that so long as advertising has stopped and there are no incoming connections or the ability to accept them
* then this method will return success. So, for example, if advertising was never started then this method will
* return success.
*
* | Error String | Description |
* |--------------|-------------|
* | Failed | Somehow the stop method couldn't do its job. Check the logs. |
*
* @public
* @returns {Promise<null|Error>}
*/
module.exports.stopAdvertisingAndListeningForIncomingConnections = function() {
return Promise.resolve();
};
/**
* # WARNING: This method is intended for internal Thali testing only. DO NOT USE!
*
* This method is only intended for iOS. It's job is to terminate all incoming and outgoing multipeer connectivity
* framework browser, advertiser, MCSession and stream connections immediately without using the normal stop and
* start interfaces or TCP/IP level connections. The goal is to simulate what would happen if we switched the
* phone to something like airplane mode. This simulates what would happen if peers went out of range.
*
* This method MUST return "Not Supported" if called on Android. On Android we can get this functionality by using
* JXCore's ability to disable the local radios.
*
* | Error String | Description |
* |--------------|-------------|
* | Failed | Somehow the stop method couldn't do its job. Check the logs. |
* | Not Supported | This method is not support on this platform. |
*
* @private
* @returns {Promise<null|Error>}
*/
module.exports.killConnections = function() {
return Promise.resolve();
};
/*
EVENTS
*/
/**
* When a {@link module:thaliMobileNative~peerAvailabilityChangedCallback} occurs each peer MUST be placed into a queue.
* Each peer in the queue MUST be processed as given below and only once all processing related to that peer has
* completed MAY the next peer be taken from the queue.
*
* If a peer's peerAvailable is set to false then we MUST use platform specific heuristics to decide how to process
* this. For example, on Android it is possible for a peer to go into the background at which point their BLE radio
* will go to low power even though their Bluetooth radio may still be reachable. So the system might decide to
* wait for a bit before issuing a peerAvailable = false for that peer. But when the system decides to issue a
* peer not available event it MUST issue a {@link event:nonTCPPeerAvailabilityChangedEvent} with
* peerIdentifier set to the value in the peer object, portNumber set to null and suggestedTCPTimeout not set.
*
* If a peer's peerAvailable is set to true then we MUST call {@link module:TCPServersManager.createPeerListener}. If
* an error is returned then the error MUST be logged and we MUST treat this as if we received the value with
* peerAvailable equal to false. If the call is a success then we
* MUST issue a {@link event:nonTCPPeerAvailabilityChangedEvent} with peerIdentifier set to the value in the peer
* object, portNumber set to the returned value and suggestedTCPTimeout set based on the behavior we have seen on the
* platform. That is, some non-TCP technologies can take longer to set up a connection than others so we need to warn
* those upstream of that.
*
* @public
* @typedef {Object} nonTCPPeerAvailabilityChanged
* @property {String} peerIdentifier See {@link module:thaliMobileNative~peer.peerIdentifier}.
* @property {number|null} portNumber If this value is null then the system is advertising that it no longer believes
* this peer is available. If this value is non-null then it is a port on 127.0.0.1 at which the local peer can
* connect in order to establish a TCP/IP connection to the remote peer.
* @property {number} [suggestedTCPTimeout] Based on the characteristics of the underlying non-TCP transport how long
* the system suggests that the caller be prepared to wait before the TCP/IP connection to the remote peer can be
* set up. This is measured in milliseconds.
*/
/**
* This event MAY start firing as soon as either of the start methods is called. Start listening for advertisements
* obviously looks for new peers but in some cases so does start advertising. This is because in some cases
* it's possible for peer A to discover peer B but not vice versa. This can result in peer A connecting to peer B
* who previously didn't know peer A exists. When that happens we will fire a discovery event.
*
* This event MUST stop firing when both stop methods have been called.
*
* The native layer does not guarantee that it will filter out duplicate peerAvailabilityChanged callbacks. This means
* it is possible to receive multiple announcements about the same peer in the same state.
*
* While the native layer can return multiple peers in a single callback the wrapper breaks them into
* individual events. See {@link module:thaliMobileNativeWrapper~nonTCPPeerAvailabilityChanged} for details on how to
* process each peer.
*
* If we receive a {@link module:TCPServersManager~failedConnection} then we MUST treat that as the equivalent of having
* received a peer for nonTCPPeerAvailabilityChanged with peerAvailable set to false.
*
* @public
* @event nonTCPPeerAvailabilityChangedEvent
* @type {Object}
* @property {module:thaliMobileNativeWrapper~nonTCPPeerAvailabilityChanged} peer
*/
/**
* This is used whenever discovery or advertising starts or stops. Since it's possible for these to
* be stopped (in particular) due to events outside of node.js's control (for example, someone turned off a radio)
* we provide a callback to track these changes. Note that there is no guarantee that the same callback value couldn't
* be sent multiple times in a row.
*
* But the general rule we will only fire this event in response to receiving the event from the native layer. That is,
* we won't fire it ourselves when someone calls start or stop advertising/incoming on the wrapper.
*
* @public
* @event discoveryAdvertisingStateUpdateNonTCPEvent
* @type {object}
* @property {module:thaliMobileNative~discoveryAdvertisingStateUpdate} discoveryAdvertisingStateUpdateValue
*/
/**
* Provides a notification when the network's state changes as well as when our use of the network changes,
* specifically when discovery or advertising/listening starts and stops. This event can start firing as soon
* as the system starts.
*
* @public
* @event networkChangedNonTCP
* @type {Object}
* @property {module:thaliMobileNative~NetworkChanged} networkChangedValue
*/
/**
* This event specifies that our internal TCP servers are no longer accepting connections so we are in serious
* trouble. Stopping and restarting is almost certainly necessary at this point. We can discover this either because
* of an error in {@link module:TCPServersManager} or because of
* {@link external:"Mobile('IncomingConnectionToPortNumberFailed')".registerToNative}.
*
* @public
* @event incomingConnectionToPortNumberFailed
* @property {number} portNumber the 127.0.0.1 port that the TCP/IP bridge tried to connect to.
*/
/**
* Use this emitter to subscribe to events.
*
* @public
* @fires event:nonTCPPeerAvailabilityChangedEvent
* @fires event:networkChangedNonTCP
* @fires event:incomingConnectionToPortNumberFailed
* @fires event:discoveryAdvertisingStateUpdateNonTCPEvent
* @fires module:TCPServersManager~failedConnection We repeat these events
*/
module.exports.emitter = new EventEmitter();
Mobile('PeerAvailabilityChange').registerToNative(function(peers) {
// do stuff!
});
Mobile('DiscoveryAdvertisingStateUpdateNonTCP').registerToNative(function(discoveryAdvertisingStateUpdateValue) {
// do stuff!
});
Mobile('NetworkChanged').registerToNative(function(networkChanged) {
// do stuff!
});
Mobile('IncomingConnectionToPortNumberFailed').registerToNative(function(portNumber) {
// do stuff!
});
;