Code coverage report for usr/local/google/home/trevj/src/uproxy-lib/build/dev/uproxy-lib/webrtc/datachannel.js

Statements: 85.98% (141 / 164)      Branches: 66.1% (39 / 59)      Functions: 89.47% (34 / 38)      Lines: 85.89% (140 / 163)      Ignored: none     

All files » usr/local/google/home/trevj/src/uproxy-lib/build/dev/uproxy-lib/webrtc/ » datachannel.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324                2 2 2 2       2               2     2       2     1 3 3 3 3   3 3 3   3 3 3 2       3 1 1                               3       1     1     1 1             1 1   1     1     1 1         3     3 1 1 1 1 1           1 1       1 1       3 1 1     1 1 1                       1 1     3 5 1   5         3 5 5 5       5 3 3     1   2     2 1                 2         3 2 2 2 1 1 1         1   2   2 2   3 2 2 2   2 5 5 1   4 3     1       2 2   3 5   3 2   3     3 1     1   3           3 3     3   3 3 3 3 3 3 3         3 2   1             3 3   3 3     3 2 2   3 2         2   3 1 1     2   2     1 1   2     1     3 3 3 3 3       2  
/// <reference path='../../../third_party/typings/es6-promise/es6-promise.d.ts' />
/// <reference path='../../../third_party/freedom-typings/rtcdatachannel.d.ts' />
/// <reference path='../../../third_party/freedom-typings/freedom-common.d.ts' />
// DataPeer - a class that wraps peer connections and data channels.
//
// This class assumes WebRTC is available; this is provided by the cross-
// platform compatibility library webrtc-adaptor.js (from:
// https://code.google.com/p/webrtc/source/browse/stable/samples/js/base/adapter.js)
var handler = require('../handler/queue');
var arraybuffers = require('../arraybuffers/arraybuffers');
var logging = require('../logging/logging');
var log = new logging.Log('DataChannel');
// Messages are limited to a 16KB length by SCTP; we use 15k for safety.
// TODO: test if we can up this to 16k; test the edge-cases!
// http://tools.ietf.org/html/draft-ietf-rtcweb-data-channel-07#section-6.6
var CHUNK_SIZE = 1024 * 15;
// The maximum amount of bytes we should allow to get queued up in
// peerconnection. Any more and we start queueing in JS. Data channels are
// closed by WebRTC when the buffer fills, so we really don't want that happen
// accidentally. More info in this thread (note that 250Kb is well below both
// the 16MB for Chrome 37+ and "100 messages" of previous versions mentioned):
//   https://code.google.com/p/webrtc/issues/detail?id=2866
// CONSIDER: make it 0. There is no size in the spec.
exports.PC_QUEUE_LIMIT = 1024 * 250;
// Javascript has trouble representing integers larger than 2^53. So we simply
// don't support trying to send array's bigger than that.
var MAX_MESSAGE_SIZE = Math.pow(2, 53);
// Wrapper for a WebRtc Data Channels:
// http://dev.w3.org/2011/webrtc/editor/webrtc.html#rtcdatachannel
//
var DataChannelClass = (function () {
    // |rtcDataChannel_| is the freedom rtc data channel.
    // |label_| is the rtcDataChannel_.getLabel() result
    function DataChannelClass(rtcDataChannel_, label_) {
        var _this = this;
        Eif (label_ === void 0) { label_ = ''; }
        this.rtcDataChannel_ = rtcDataChannel_;
        this.label_ = label_;
        // True iff close() has been called.
        this.draining_ = false;
        this.onceDrained_ = new Promise(function (F, R) {
            _this.fulfillDrained_ = F;
        });
        this.overflow_ = false;
        this.overflowListener_ = null;
        this.getLabel = function () {
            return _this.label_;
        };
        // Handle data we get from the peer by putting it, appropriately wrapped, on
        // the queue of data from the peer.
        this.onDataFromPeer_ = function (message) {
            Eif (typeof message.text === 'string') {
                _this.dataFromPeerQueue.handle({ str: message.text });
            }
            else if (message.buffer instanceof ArrayBuffer) {
                _this.dataFromPeerQueue.handle({ buffer: message.buffer });
            }
            else {
                log.error('Unexpected data from peer: %1', message);
            }
        };
        // Promise completes once all the data has been sent. This is async because
        // there may be more data than fits in the buffer; we do chunking so that
        // data larger than the SCTP message size limit (about 16k) can be sent and
        // received reliably, and so that the internal buffer is not over-filled. If
        // data is too big we also fail.
        //
        // CONSIDER: We could support blob data by streaming into array-buffers.
        this.send = function (data) {
            // Note: you cannot just write |if(data.str) ...| because str may be empty
            // which is treated as false. You have to do something more verbose, like
            // |if (typeof data.str === 'string') ...|.
            Iif (!(typeof data.str === 'string' || (typeof data.buffer === 'object') && (data.buffer instanceof ArrayBuffer))) {
                return Promise.reject(new Error('data to send must have at least `str:string` or ' + '`buffer:ArrayBuffer` defined (typeof data.str === ' + typeof data.str + '; typeof data.buffer === ' + typeof data.buffer + '; data.buffer instanceof ArrayBuffer === ' + (data.buffer instanceof ArrayBuffer) + ')'));
            }
            Iif (_this.draining_) {
                return Promise.reject(new Error('send was called after close'));
            }
            var byteLength;
            Iif (typeof data.str === 'string') {
                // This calculation is based on the idea that JS strings are utf-16,
                // but since all strings are converted  to UTF-8 by the data channel
                // this calculation is only an approximate upper bound on the actual
                // message size.
                byteLength = data.str.length * 2;
            }
            else Eif (data.buffer) {
                byteLength = data.buffer.byteLength;
            }
            Iif (byteLength > MAX_MESSAGE_SIZE) {
                return Promise.reject(new Error('Data was too big to send, sorry. ' + 'Need to wait for real Blob support.'));
            }
            Iif (typeof data.str === 'string') {
                return _this.chunkStringOntoQueue_({ str: data.str });
            }
            else Eif (data.buffer) {
                return _this.chunkBufferOntoQueue_({ buffer: data.buffer });
            }
        };
        // TODO: add an issue for chunking strings, write issue number here, then
        // write the code and resolve the issue :-)
        this.chunkStringOntoQueue_ = function (data) {
            return _this.toPeerDataQueue_.handle(data);
        };
        this.chunkBufferOntoQueue_ = function (data) {
            var chunks = arraybuffers.chunk(data.buffer, CHUNK_SIZE);
            var promises = [];
            chunks.forEach(function (chunk) {
                _this.toPeerDataBytes_ += chunk.byteLength;
                promises.push(_this.toPeerDataQueue_.handle({ buffer: chunk }));
            });
            // This check is logically redundant with the check in
            // conjestionControlSendHandler, but it triggers much sooner (synchronously
            // during the call to send), which is valuable to reduce overshoot,
            // especially in fast flows that create web worker scheduling anomalies.
            Eif (_this.toPeerDataBytes_ + _this.lastBrowserBufferedAmount_ > exports.PC_QUEUE_LIMIT) {
                _this.setOverflow_(true);
            }
            // CONSIDER: can we change the interface to support not having the dummy
            // extra return at the end?
            return Promise.all(promises).then(function () {
                return;
            });
        };
        // Assumes data is chunked.
        this.handleSendDataToPeer_ = function (data) {
            try {
                Iif (typeof data.str === 'string') {
                    _this.rtcDataChannel_.send(data.str);
                }
                else Eif (data.buffer) {
                    _this.toPeerDataBytes_ -= data.buffer.byteLength;
                    _this.rtcDataChannel_.sendBuffer(data.buffer);
                }
                else {
                    // Data is good when it meets the type expected of the Data. If type-
                    // saftey is ensured at compile time, this should never happen.
                    return Promise.reject(new Error('Bad data: ' + JSON.stringify(data)));
                }
            }
            catch (e) {
                log.debug('Error in send' + e.toString());
                return Promise.reject(new Error('Error in send: ' + JSON.stringify(e)));
            }
            _this.conjestionControlSendHandler();
            return Promise.resolve();
        };
        // Sets the overflow state, and calls the listener if it has changed.
        this.setOverflow_ = function (overflow) {
            if (_this.overflowListener_ && _this.overflow_ !== overflow) {
                _this.overflowListener_(overflow);
            }
            _this.overflow_ = overflow;
        };
        // TODO: make this timeout adaptive so that we keep the buffer as full as we
        // can without wasting timeout callbacks. When DataChannels correctly has a
        // callback for buffering, we don't need to do this anymore.
        this.conjestionControlSendHandler = function () {
            _this.rtcDataChannel_.getBufferedAmount().then(function (bufferedAmount) {
                _this.lastBrowserBufferedAmount_ = bufferedAmount;
                Iif (_this.toPeerDataQueue_.isHandling()) {
                    log.error('Last packet handler should not still be present');
                    _this.close();
                }
                if (bufferedAmount + CHUNK_SIZE > exports.PC_QUEUE_LIMIT) {
                    _this.setOverflow_(true);
                    if (!_this.isOpen_) {
                        // The remote peer has closed the channel, so we should stop
                        // trying to drain the send buffer.
                        return;
                    }
                    setTimeout(_this.conjestionControlSendHandler, 20);
                }
                else {
                    if (_this.toPeerDataQueue_.getLength() === 0) {
                        _this.setOverflow_(false);
                    }
                    // This processes one block from the queue, which (in Chrome) is
                    // expected to be no larger than 4 KB.  We will then go through the
                    // whole loop again, including checking the buffered amount, before
                    // processing the next block.  This is inefficient because it
                    // introduces an extra IPC call per block; a more efficient
                    // implementation would check the available buffered amount, and
                    // then pull that many bytes off of the queue.
                    _this.toPeerDataQueue_.setNextHandler(_this.handleSendDataToPeer_);
                }
            });
        };
        // Closes asynchronously, after waiting for all outgoing messages.
        this.close = function () {
            log.debug('close requested (%1 messages and %2 bytes to send)', _this.toPeerDataQueue_.getLength(), _this.lastBrowserBufferedAmount_);
            var onceJavascriptBufferDrained = new Promise(function (F, R) {
                if (_this.getJavascriptBufferedAmount() > 0) {
                    _this.setOverflowListener(function (overflow) {
                        Eif (!overflow) {
                            F();
                        }
                    });
                }
                else {
                    F();
                }
                _this.draining_ = true;
            });
            onceJavascriptBufferDrained.then(_this.waitForBrowserToDrain_).then(_this.fulfillDrained_);
            return _this.onceClosed;
        };
        this.waitForBrowserToDrain_ = function () {
            var drained;
            var onceBrowserBufferDrained = new Promise(function (F, R) {
                drained = F;
            });
            var loop = function () {
                _this.getBrowserBufferedAmount().then(function (amount) {
                    if (amount === 0) {
                        drained();
                    }
                    else if (_this.isOpen_) {
                        setTimeout(loop, 20);
                    }
                    else {
                        log.warn('Data channel was closed remotely with %1 bytes buffered', amount);
                    }
                });
            };
            loop();
            return onceBrowserBufferDrained;
        };
        this.getBrowserBufferedAmount = function () {
            return _this.rtcDataChannel_.getBufferedAmount();
        };
        this.getJavascriptBufferedAmount = function () {
            return _this.toPeerDataBytes_;
        };
        this.isInOverflow = function () {
            return _this.overflow_;
        };
        this.setOverflowListener = function (listener) {
            Iif (_this.draining_) {
                throw new Error('Can\'t set overflow listener after close');
            }
            _this.overflowListener_ = listener;
        };
        this.toString = function () {
            var s = _this.getLabel() + ': isOpen_=' + _this.isOpen_;
            return s;
        };
        // This setter is not part of the DataChannel interface, and is only for
        // use by the static constructor.
        this.setLabel = function (label) {
            Iif (_this.label_ !== '') {
                throw new Error('Data Channel label was set twice, to ' + _this.label_ + ' and ' + label);
            }
            _this.label_ = label;
        };
        this.dataFromPeerQueue = new handler.Queue();
        this.toPeerDataQueue_ = new handler.Queue();
        this.toPeerDataBytes_ = 0;
        this.lastBrowserBufferedAmount_ = 0;
        this.onceOpened = new Promise(function (F, R) {
            _this.rejectOpened_ = R;
            _this.rtcDataChannel_.getReadyState().then(function (state) {
                // RTCDataChannels created by a RTCDataChannelEvent have an initial
                // state of open, so the onopen event for the channel will not
                // fire. We need to fire the onOpenDataChannel event here
                // http://www.w3.org/TR/webrtc/#idl-def-RTCDataChannelState
                if (state === 'open') {
                    F();
                }
                else Iif (state === 'connecting') {
                    // Firefox channels do not have an initial state of 'open'
                    // See https://bugzilla.mozilla.org/show_bug.cgi?id=1000478
                    _this.rtcDataChannel_.on('onopen', F);
                }
            });
        });
        this.onceClosed = new Promise(function (F, R) {
            _this.rtcDataChannel_.on('onclose', F);
        });
        this.rtcDataChannel_.on('onmessage', this.onDataFromPeer_);
        this.rtcDataChannel_.on('onerror', function (e) {
            log.error('rtcDataChannel_.onerror: ' + e.toString);
        });
        this.onceOpened.then(function () {
            _this.isOpen_ = true;
            _this.conjestionControlSendHandler();
        });
        this.onceClosed.then(function () {
            Iif (!_this.isOpen_) {
                // Make sure to reject the onceOpened promise if state went from
                // |connecting| to |close|.
                _this.rejectOpened_(new Error('Failed to open; closed while trying to open.'));
            }
            _this.isOpen_ = false;
        });
        this.onceDrained_.then(function () {
            log.debug('all messages sent, closing');
            _this.rtcDataChannel_.close();
        });
    }
    return DataChannelClass;
})();
exports.DataChannelClass = DataChannelClass; // class DataChannelClass
// Static constructor which constructs a core.rtcdatachannel instance
// given a core.rtcdatachannel GUID.
function createFromFreedomId(id) {
    return createFromRtcDataChannel(freedom['core.rtcdatachannel'](id));
}
exports.createFromFreedomId = createFromFreedomId;
// Static constructor which constructs a core.rtcdatachannel instance
// given a core.rtcdatachannel instance.
function createFromRtcDataChannel(rtcDataChannel) {
    // We need to construct the data channel synchronously to avoid missing any
    // early 'onmessage' events.
    var dc = new DataChannelClass(rtcDataChannel);
    return rtcDataChannel.setBinaryType('arraybuffer').then(function () {
        return rtcDataChannel.getLabel().then(function (label) {
            dc.setLabel(label);
            return dc;
        });
    });
}
exports.createFromRtcDataChannel = createFromRtcDataChannel;