Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BREAKING: Many fixes; all leaks fixed #762

Merged
merged 12 commits into from Apr 22, 2016
Next

BREAKING: Major cleanup

### Added

- `client.listening` property to signal whether TCP server is listening
for incoming
  connections.

### Changed

- Merged `Swarm` class into `Torrent` object. Properties on
`torrent.swarm` (like
  `torrent.swarm.wires`) now exist on `torrent` (e.g. `torrent.wires`).

- `torrent.addPeer` can no longer be called before the `infoHash` event
has been
  emitted.

- Remove `torrent.on('listening')` event. Use `client.on('listening')`
instead.

- Remove support from `TCPPool` for listening on multiple ports. This
was not used by
  WebTorrent and just added complexity. There is now a single `TCPPool`
instance for the
  whole WebTorrent client.

- Deprecate: Do not use `client.download()` anymore. Use `client.add()`
instead.

- Deprecate: Do not use `torrent.swarm` anymore. Use `torrent` instead.

### Fixed

- When there is a `torrent.on('error')` listener, don't also emit
  `client.on('error')`.

- Do not return existing torrent object when duplicate torrent is
added. Fire an
  `'error'` event instead.

- Memory leak of `Torrent` object caused by `RarityMap`

- Memory leak of `Torrent` object caused by `TCPPool`

- `client.ratio` and `torrent.ratio` are now calculated as `uploaded /
received` instead
  of `uploaded / downloaded`.
  • Loading branch information
feross committed Apr 21, 2016
commit 3daee2c66cbf752b9e6e49b99492b8c1914a4a58
@@ -0,0 +1,41 @@
# WebTorrent Version History

## UNRELEASED

### Added

- `client.listening` property to signal whether TCP server is listening for incoming
connections.

### Changed

- Merged `Swarm` class into `Torrent` object. Properties on `torrent.swarm` (like
`torrent.swarm.wires`) now exist on `torrent` (e.g. `torrent.wires`).

- `torrent.addPeer` can no longer be called before the `infoHash` event has been
emitted.

- Remove `torrent.on('listening')` event. Use `client.on('listening')` instead.

- Remove support from `TCPPool` for listening on multiple ports. This was not used by
WebTorrent and just added complexity. There is now a single `TCPPool` instance for the
whole WebTorrent client.

- Deprecate: Do not use `client.download()` anymore. Use `client.add()` instead.

- Deprecate: Do not use `torrent.swarm` anymore. Use `torrent` instead.

### Fixed

- When there is a `torrent.on('error')` listener, don't also emit
`client.on('error')`.

- Do not return existing torrent object when duplicate torrent is added. Fire an
`'error'` event instead.

- Memory leak of `Torrent` object caused by `RarityMap`

- Memory leak of `Torrent` object caused by `TCPPool`

- `client.ratio` and `torrent.ratio` are now calculated as `uploaded / received` instead
of `uploaded / downloaded`.
@@ -63,7 +63,7 @@ If `opts` is specified, then the default options (shown below) will be overridde

## `client.add(torrentId, [opts], [function ontorrent (torrent) {}])`

Start downloading a new torrent. Aliased as `client.download`.
Start downloading a new torrent.

`torrentId` can be one of:

@@ -251,6 +251,8 @@ Adds a peer to the torrent swarm. Normally, you don't need to call `torrent.addP
WebTorrent will automatically find peers using the tracker servers or DHT. This is just
for manually adding a peer to the client.

This method should not be called until the `infoHash` event has been emitted.

Returns `true` if peer was added, `false` if peer was blocked by the loaded blocklist.

The `peer` argument must be an address string in the format `12.34.56.78:4444` (for
@@ -452,7 +454,7 @@ called once the file is ready. `callback` must be specified, and will be called
```js
file.getBuffer(function (err, buffer) {
if (err) throw err
console.log(buffer) // <Buffer 00 98 00 01 01 00 00 00 50 ae 07 04 01 00 00 00 0a 00 00 00 00 00 00 00 78 ae 07 04 01 00 00 00 05 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ...>
console.log(buffer) // <Buffer 00 98 00 01 ...>
})
```

@@ -463,9 +465,10 @@ that handles many file types like video (.mp4, .webm, .m4v, etc.), audio (.m4a,
.wav, etc.), images (.jpg, .gif, .png, etc.), and other file formats (.pdf, .md, .txt,
etc.).

The file will be fetched from the network with highest priority and streamed into the
page (if it's video or audio). In some cases, video or audio files will not be streamable
because they're not in a format that the browser can stream so the file will be fully downloaded before being played. For other non-streamable file types like images and PDFs,
The file will be fetched from the network with highest priority and streamed into the page
(if it's video or audio). In some cases, video or audio files will not be streamable
because they're not in a format that the browser can stream so the file will be fully
downloaded before being played. For other non-streamable file types like images and PDFs,
the file will be downloaded then displayed.

`rootElem` is a container element (CSS selector or reference to DOM node) that the content
130 index.js
@@ -1,3 +1,6 @@
// TODO: cleanup event listeners
// TODO: set dhtPort to correct port

module.exports = WebTorrent

var createTorrent = require('create-torrent')
@@ -16,10 +19,9 @@ var speedometer = require('speedometer')
var zeroFill = require('zero-fill')

var concatStream = require('./lib/concat-stream')
var TCPPool = require('./lib/tcp-pool') // browser exclude
var Torrent = require('./lib/torrent')

module.exports.WEBRTC_SUPPORT = Peer.WEBRTC_SUPPORT

/**
* WebTorrent version.
*/
@@ -55,36 +57,44 @@ function WebTorrent (opts) {

if (!opts) opts = {}

self.peerId = typeof opts.peerId === 'string'
? opts.peerId
: (opts.peerId || new Buffer(VERSION_PREFIX + hat(48))).toString('hex')
self.peerIdBuffer = new Buffer(self.peerId, 'hex')

self.nodeId = typeof opts.nodeId === 'string'
? opts.nodeId
: (opts.nodeId && opts.nodeId.toString('hex')) || hat(160)
self.nodeIdBuffer = new Buffer(self.nodeId, 'hex')

self.destroyed = false
self.listening = false
self.torrentPort = opts.torrentPort || 0
self.tracker = opts.tracker !== undefined ? opts.tracker : true
self.torrents = []
self.maxConns = Number(opts.maxConns) || 55

self._rtcConfig = opts.rtcConfig
self._wrtc = opts.wrtc || global.WRTC // to support `webtorrent-hybrid` package

self.torrents = []
if (typeof TCPPool === 'function') {
self._tcpPool = new TCPPool(self)
} else {
process.nextTick(function () {
self._onListening()
})
}

// stats
self._downloadSpeed = speedometer()
self._uploadSpeed = speedometer()

self.maxConns = opts.maxConns

self.peerId = typeof opts.peerId === 'string'
? opts.peerId
: (opts.peerId || new Buffer(VERSION_PREFIX + hat(48))).toString('hex')
self.peerIdBuffer = new Buffer(self.peerId, 'hex')

self.nodeId = typeof opts.nodeId === 'string'
? opts.nodeId
: (opts.nodeId && opts.nodeId.toString('hex')) || hat(160)
self.nodeIdBuffer = new Buffer(self.nodeId, 'hex')

if (opts.dht !== false && typeof DHT === 'function' /* browser exclude */) {
// use a single DHT instance for all torrents, so the routing table can be reused
self.dht = new DHT(extend({ nodeId: self.nodeId }, opts.dht))
self.dht.once('error', function (err) {
self.emit('error', err)
self.destroy()
self._destroy()
})

// Ignore warning when there are > 10 torrents in the client
@@ -114,6 +124,8 @@ function WebTorrent (opts) {
}
}

WebTorrent.WEBRTC_SUPPORT = Peer.WEBRTC_SUPPORT

Object.defineProperty(WebTorrent.prototype, 'downloadSpeed', {
get: function () { return this._downloadSpeed() }
})
@@ -142,10 +154,10 @@ Object.defineProperty(WebTorrent.prototype, 'ratio', {
var uploaded = this.torrents.reduce(function (total, torrent) {
return total + torrent.uploaded
}, 0)
var downloaded = this.torrents.reduce(function (total, torrent) {
return total + torrent.downloaded
var received = this.torrents.reduce(function (total, torrent) {
return total + torrent.received
}, 0) || 1
return uploaded / downloaded
return uploaded / received
}
})

@@ -174,44 +186,31 @@ WebTorrent.prototype.get = function (torrentId) {
return null
}

WebTorrent.prototype.download = function (torrentId, opts, ontorrent) {
console.warn('WebTorrent: client.download() is deprecated. Use client.add() instead')
return this.add(torrentId, opts, ontorrent)
}

/**
* Start downloading a new torrent. Aliased as `client.download`.
* @param {string|Buffer|Object} torrentId
* @param {Object} opts torrent-specific options
* @param {function=} ontorrent called when the torrent is ready (has metadata)
*/
WebTorrent.prototype.add =
WebTorrent.prototype.download = function (torrentId, opts, ontorrent) {
WebTorrent.prototype.add = function (torrentId, opts, ontorrent) {
var self = this
if (self.destroyed) throw new Error('client is destroyed')
if (typeof opts === 'function') return self.add(torrentId, null, opts)

debug('add')
opts = opts ? extend(opts) : {}

var torrent = self.get(torrentId)

if (torrent) {
if (torrent.ready) process.nextTick(onReady)
else torrent.once('ready', onReady)
} else {
torrent = new Torrent(torrentId, self, opts)
self.torrents.push(torrent)
var torrent = new Torrent(torrentId, self, opts)
self.torrents.push(torrent)

torrent.once('error', function (err) {
self.emit('error', err, torrent)
self.remove(torrent)
})

torrent.once('listening', function (port) {
self.emit('listening', port, torrent)
})

torrent.once('ready', onReady)
}
torrent.once('ready', onReady)

function onReady () {
debug('on torrent')
if (typeof ontorrent === 'function') ontorrent(torrent)
self.emit('torrent', torrent)
}
@@ -303,38 +302,69 @@ WebTorrent.prototype.seed = function (input, opts, onseed) {
* @param {function} cb
*/
WebTorrent.prototype.remove = function (torrentId, cb) {
var self = this
debug('remove')

var torrent = self.get(torrentId)
var torrent = this.get(torrentId)
if (!torrent) throw new Error('No torrent with id ' + torrentId)

self.torrents.splice(self.torrents.indexOf(torrent), 1)
this.torrents.splice(this.torrents.indexOf(torrent), 1)
torrent.destroy(cb)
}

WebTorrent.prototype.address = function () {
var self = this
return { address: '0.0.0.0', family: 'IPv4', port: self.torrentPort }
if (!this.listening) return null
return this._tcpPool
? this._tcpPool.server.address()
: { address: '0.0.0.0', family: 'IPv4', port: 0 }
}

/**
* Destroy the client, including all torrents and connections to peers.
* @param {function} cb
*/
WebTorrent.prototype.destroy = function (cb) {
if (this.destroyed) throw new Error('client already destroyed')
this._destroy(null, cb)
}

WebTorrent.prototype._destroy = function (err, cb) {
var self = this
if (self.destroyed) throw new Error('client already destroyed')
debug('client destroy')
self.destroyed = true
debug('destroy')

var tasks = self.torrents.map(function (torrent) {
return function (cb) { self.remove(torrent, cb) }
return function (cb) {
torrent.destroy(cb)
}
})

if (self.dht) tasks.push(function (cb) { self.dht.destroy(cb) })
if (self._tcpPool) {
tasks.push(function (cb) {
self._tcpPool.destroy(cb)
})
}

if (self.dht) {
tasks.push(function (cb) {
self.dht.destroy(cb)
})
}

parallel(tasks, cb)

if (err) self.emit('error', err)
}

WebTorrent.prototype._onListening = function () {
this.listening = true

if (this._tcpPool) {
// Sometimes server.address() returns `null` in Docker.
// WebTorrent issue: https://github.com/feross/bittorrent-swarm/pull/18
this.torrentPort = (this._tcpPool.server.address() || { port: 0 }).port
}

this.emit('listening')
}

/**
@@ -60,18 +60,18 @@ exports.createTCPOutgoingPeer = function (addr, swarm) {
/**
* Peer that represents a Web Seed (BEP17 / BEP19).
*/
exports.createWebSeedPeer = function (url, parsedTorrent, swarm) {
exports.createWebSeedPeer = function (url, swarm) {
var peer = new Peer(url, 'webSeed')
peer.swarm = swarm
peer.conn = new WebConn(url, parsedTorrent)
peer.conn = new WebConn(url, swarm)

peer.onConnect()

return peer
}

/**
* Peer. Represents a peer in the Swarm.
* Peer. Represents a peer in the torrent swarm.
*
* @param {string} id "ip:port" string, peer id (for WebRTC peers), or url (for Web Seeds)
* @param {string} type the type of the peer
@@ -161,7 +161,7 @@ Peer.prototype.onHandshake = function (infoHash, peerId) {
return self.destroy(new Error('unexpected handshake info hash for this swarm'))
}
if (peerId === self.swarm.peerId) {
return self.destroy(new Error('refusing to handshake with self'))
return self.destroy(new Error('refusing to connect to ourselves'))
}

debug('Peer %s got handshake %s', self.id, infoHash)
@@ -170,27 +170,12 @@ Peer.prototype.onHandshake = function (infoHash, peerId) {

self.retries = 0

self.wire.on('download', function (downloaded) {
if (self.destroyed) return
self.swarm.downloaded += downloaded
self.swarm.downloadSpeed(downloaded)
self.swarm.emit('download', downloaded)
})

self.wire.on('upload', function (uploaded) {
if (self.destroyed) return
self.swarm.uploaded += uploaded
self.swarm.uploadSpeed(uploaded)
self.swarm.emit('upload', uploaded)
})

self.swarm.wires.push(self.wire)

var addr = self.addr
if (!addr && self.conn.remoteAddress) {
addr = self.conn.remoteAddress + ':' + self.conn.remotePort
}
self.swarm.emit('wire', self.wire, addr)
self.swarm._onWire(self.wire, addr)

// swarm could be destroyed in user's 'wire' event handler
if (!self.swarm || self.swarm.destroyed) return

@@ -199,7 +184,10 @@ Peer.prototype.onHandshake = function (infoHash, peerId) {

Peer.prototype.handshake = function () {
var self = this
self.wire.handshake(self.swarm.infoHash, self.swarm.peerId, self.swarm.handshakeOpts)
var opts = {
dht: self.swarm.private ? false : !!self.swarm.client.dht
}
self.wire.handshake(self.swarm.infoHash, self.swarm.client.peerId, opts)
self.sentHandshake = true
}

@@ -236,8 +224,8 @@ Peer.prototype.destroy = function (err) {
var conn = self.conn
var wire = self.wire

self.conn = null
self.swarm = null
self.conn = null
self.wire = null

if (swarm && wire) {
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.