/// // This module provides a pool for managing data channels. It mimics the // interface for creating data channels in uproxy-lib's PeerConnection, // but the channel objects it produces are actually "virtual channels", // which wrap an actual DataChannel. The key difference is that when the // virtual channel is closed, the underlying DataChannel remains open, and // is returned to the pool, to be released again upon a later call to // CreateDataChannel. // This class was written principally as a workaround for bugs related to // RTCDataChannel.close behavior, such as https://crbug.com/474688. // However, it should also help to reduce startup latency, by removing a // roundtrip from each connection request (waiting for the "open ack"). // It may therefore be worth preserving even after any platform bugs are // resolved. import peerconnection = require('../../../third_party/uproxy-lib/webrtc/peerconnection'); import datachannel = require('../../../third_party/uproxy-lib/webrtc/datachannel'); import handler = require('../../../third_party/uproxy-lib/handler/queue'); import queue = require('../../../third_party/uproxy-lib/queue/queue'); import logging = require('../../../third_party/uproxy-lib/logging/logging'); var log :logging.Log = new logging.Log('pool'); // This is the only exported class in this module. It mimics the data channel // aspects of the PeerConnection interface. Internally, it provides a pool // of channels that keeps old channels for reuse instead of closing them, and // makes new channels as needed when the pool runs dry. Crucially, the local // and remote pools do not interfere with each other, even if they use the // same labels, because the browser ensures that data channels created by each // peer are drawn from separate ID spaces (odd vs. even). class Pool { public peerOpenedChannelQueue :handler.QueueHandler; private localPool_ :LocalPool; constructor( pc:peerconnection.PeerConnection, name_:string) { this.localPool_ = new LocalPool(pc, name_); var remotePool = new RemotePool(pc, name_); this.peerOpenedChannelQueue = remotePool.peerOpenedChannelQueue; } public openDataChannel = () : Promise => { return this.localPool_.openDataChannel(); } } // Manages a pool of data channels opened by this peer. The only public method // is openDataChannel. class LocalPool { private numChannels_ = 0; // Channels which have been closed, and may be re-opened. private pool_ = new queue.Queue(); constructor( private pc_:peerconnection.PeerConnection, private name_:string) {} public openDataChannel = () : Promise => { return this.reuseOrCreate_().then((channel:PoolChannel) => { return channel.open().then(() => { // When this channel closes, reset it and return it to the pool. channel.onceClosed.then(() => { this.onChannelClosed_(channel); }); return channel; }); }); } private reuseOrCreate_ = () : Promise => { // If there are no channels available right now, open a new one. // TODO: limit the number of channels (probably should be <=256). if (this.pool_.length > 0) { var channel = this.pool_.shift(); log.debug('%1: channel requested, pulled %2 from pool (%3 remaining)', this.name_, channel.getLabel(), this.pool_.length); return Promise.resolve(channel); } else { log.debug('%1: channel requested, creating new', this.name_); return this.openNewChannel_(); } } // Creates and returns a new channel, wrapping it. private openNewChannel_ = () : Promise => { return this.pc_.openDataChannel('p' + this.numChannels_++). then((dc:datachannel.DataChannel) => { return dc.onceOpened.then(() => { return new PoolChannel(dc); }); }); } // Resets the channel, making it ready for use again, and adds it // to the pool. private onChannelClosed_ = (poolChannel:PoolChannel) : void => { if (!poolChannel.reset()) { return; } this.pool_.push(poolChannel); log.debug('%1: returned channel %2 to the pool (new size: %3)', this.name_, poolChannel.getLabel(), this.pool_.length); } } // Tracks a pool of channels that were opened by the remote peer. class RemotePool { public peerOpenedChannelQueue = new handler.Queue(); constructor( private pc_:peerconnection.PeerConnection, private name_:string) { this.pc_.peerOpenedChannelQueue.setSyncHandler(this.onNewChannel_); } private onNewChannel_ = (dc:datachannel.DataChannel) => { log.debug('%1: remote side created new channel: %2', this.name_, dc.getLabel()); dc.onceOpened.then(() => { var poolChannel = new PoolChannel(dc); this.listenForOpenAndClose_(poolChannel); }); } private listenForOpenAndClose_ = (poolChannel:PoolChannel) : void => { poolChannel.onceOpened.then(() => { this.peerOpenedChannelQueue.handle(poolChannel); }); poolChannel.onceClosed.then(() => { if (!poolChannel.reset()) { return; } this.listenForOpenAndClose_(poolChannel); }); } } // These are the two control messages used. To help debugging, and // improve forward-compatibility, we send the string name on the wire, // not the numerical enum value. Therefore, these names are part of // the normative protocol, and will break compatibility if changed. enum ControlMessage { OPEN, CLOSE } enum State { OPEN, CLOSING, // Waiting for CLOSE ack CLOSED, PERMANENTLY_CLOSED } // Each PoolChannel wraps an actual DataChannel, and provides behavior // that is intended to be indistinguishable to the caller. However, // close() does not actually close the underlying channel. Instead, // it sends an in-band control message indicating the close, and the // channel is returned to the pool of inactive channels, ready for // reuse when the client asks for a new channel. class PoolChannel implements datachannel.DataChannel { private fulfillOpened_ :() => void; public onceOpened : Promise; private fulfillClosed_ :() => void; public onceClosed : Promise; // Every call to dataFromPeerQueue.handle() must also set // lastDataFromPeerHandled_ to the new return value, so that we can // tell when all pending data from the peer has been drained. public dataFromPeerQueue :handler.Queue; private lastDataFromPeerHandled_ : Promise; private state_ :State = State.CLOSED; // dc_.onceOpened must already have resolved constructor(private dc_:datachannel.DataChannel) { this.reset(); this.dc_.dataFromPeerQueue.setSyncHandler(this.onDataFromPeer_); this.dc_.onceClosed.then(() => { this.state_ = State.PERMANENTLY_CLOSED; this.fulfillClosed_(); }); } public reset = () : boolean => { if (this.state_ !== State.CLOSED) { return false; } this.dataFromPeerQueue = new handler.Queue(); this.lastDataFromPeerHandled_ = Promise.resolve(); this.onceOpened = new Promise((F, R) => { this.fulfillOpened_ = F; }); this.onceClosed = new Promise((F, R) => { this.fulfillClosed_ = F; }); this.onceOpened.then(() => { if (this.state_ === State.CLOSED) { this.state_ = State.OPEN; } }); this.onceClosed.then(() => { if (this.state_ !== State.PERMANENTLY_CLOSED) { this.state_ = State.CLOSED; } }); return true; } public getLabel = () : string => { return this.dc_.getLabel(); } public send = (data:datachannel.Data) : Promise => { if (this.state_ !== State.OPEN) { return Promise.reject(new Error('Can\'t send while closed')); } // To distinguish control messages from application data, all string // messages are encapsulated in a JSON layer. Binary messages are unaffected. if (data.str) { return this.dc_.send({ str: JSON.stringify({ data: data.str }) }); } return this.dc_.send(data); } private sendControlMessage_ = (controlMessage:ControlMessage) : Promise => { log.debug('%1: sending control message: %2', this.getLabel(), ControlMessage[controlMessage]); return this.dc_.send({ str: JSON.stringify({ control: ControlMessage[controlMessage] }) }); } private onDataFromPeer_ = (data:datachannel.Data) : void => { if (data.str) { try { var msg = JSON.parse(data.str); } catch (e) { log.error('%1: Got non-JSON string: %2', this.getLabel(), data.str); return; } if (typeof msg.data === 'string') { this.lastDataFromPeerHandled_ = this.dataFromPeerQueue.handle({str: msg.data}); } else if (typeof msg.control === 'string') { this.onControlMessage_(msg.control); } else { log.error('No data or control message found'); } return; } this.lastDataFromPeerHandled_ = this.dataFromPeerQueue.handle(data); } private onControlMessage_ = (controlMessage:string) : void => { log.debug('%1: received control message: %2', this.getLabel(), controlMessage); if (controlMessage === ControlMessage[ControlMessage.OPEN]) { if (this.state_ === State.OPEN) { log.warn('%1: Got redundant open message', this.getLabel()); } this.fulfillOpened_(); } else if (controlMessage === ControlMessage[ControlMessage.CLOSE]) { if (this.state_ === State.OPEN) { this.state_ = State.CLOSING; // Drain messages, then ack the close. this.lastDataFromPeerHandled_.then(() => { return this.sendControlMessage_(ControlMessage.CLOSE); }).then(this.fulfillClosed_); } else if (this.state_ === State.CLOSING) { // We both sent a "close" command at the same time. this.fulfillClosed_(); } else if (this.state_ === State.CLOSED) { log.warn('%1: Got redundant close message', this.getLabel()); } } else { log.error('%1: unknown control message: %2', this.getLabel(), controlMessage); } } public getBrowserBufferedAmount = () : Promise => { return this.dc_.getBrowserBufferedAmount(); } public getJavascriptBufferedAmount = () : number => { return this.dc_.getJavascriptBufferedAmount(); } public isInOverflow = () : boolean => { return this.dc_.isInOverflow(); } public setOverflowListener = (listener:(overflow:boolean) => void) : void => { this.dc_.setOverflowListener(listener); } // New method for PoolChannel, not present in the DataChannel interface. public open = () : Promise => { log.debug(this.getLabel() + ': open'); if (this.state_ === State.OPEN) { return Promise.reject(new Error('channel is already open')); } this.sendControlMessage_(ControlMessage.OPEN); // Immediate open; there is no open-ack this.fulfillOpened_(); return this.onceOpened; } public close = () : Promise => { log.debug('%1: close', this.getLabel()); if (this.state_ !== State.OPEN) { return; } this.state_ = State.CLOSING; this.sendControlMessage_(ControlMessage.CLOSE); return this.onceClosed; } public toString = () : string => { return 'PoolChannel(' + this.dc_.toString() + ')'; } } export = Pool;