diff options
author | Timothy J Fontaine <tjfontaine@gmail.com> | 2015-03-22 12:33:12 -0700 |
---|---|---|
committer | Timothy J Fontaine <tjfontaine@gmail.com> | 2015-03-24 14:09:16 -0700 |
commit | fa79367e9873930fc69c52f8f52a67e6c4428d06 (patch) | |
tree | d657b005e199d1dd0409e9309059e3ca9336d62d | |
parent | a93087007fce8466ef58e37af76521468295ccec (diff) | |
download | node-fa79367e9873930fc69c52f8f52a67e6c4428d06.tar.gz |
events: add StrictEE
-rw-r--r-- | lib/_http_client.js | 23 | ||||
-rw-r--r-- | lib/_http_incoming.js | 9 | ||||
-rw-r--r-- | lib/_http_outgoing.js | 15 | ||||
-rw-r--r-- | lib/_http_server.js | 30 | ||||
-rw-r--r-- | lib/_stream_readable.js | 20 | ||||
-rw-r--r-- | lib/_stream_writable.js | 21 | ||||
-rw-r--r-- | lib/_tls_wrap.js | 18 | ||||
-rw-r--r-- | lib/events.js | 162 | ||||
-rw-r--r-- | lib/fs.js | 22 | ||||
-rw-r--r-- | lib/https.js | 20 | ||||
-rw-r--r-- | lib/net.js | 63 | ||||
-rw-r--r-- | lib/stream.js | 14 |
12 files changed, 407 insertions, 10 deletions
diff --git a/lib/_http_client.js b/lib/_http_client.js index 5f873d65d..5e004ce8c 100644 --- a/lib/_http_client.js +++ b/lib/_http_client.js @@ -42,6 +42,29 @@ var Agent = require('_http_agent'); function ClientRequest(options, cb) { var self = this; OutgoingMessage.call(self); + this.strictEERegister({ + connect: { + maxCount: 1, + after: ['socket'], + }, + close: { + maxCount: 1, + after: ['error', 'upgrade', 'aborted', 'finish', 'end'], + }, + error: { + maxCount: 1, + notAfter: ['close'], + }, + continue: { + maxCount: 1, + notAfter: ['error', 'close'], + after: ['socket'], + }, + response: { + notAfter: ['error', 'close'], + maxCount: 1, + }, + }, true); if (util.isString(options)) { options = url.parse(options); diff --git a/lib/_http_incoming.js b/lib/_http_incoming.js index 69d3d86ec..30297ebe1 100644 --- a/lib/_http_incoming.js +++ b/lib/_http_incoming.js @@ -38,6 +38,15 @@ exports.readStop = readStop; /* Abstract base class for ServerRequest and ClientResponse. */ function IncomingMessage(socket) { Stream.Readable.call(this); + this.strictEERegister({ + aborted: { + maxCount: 1, + notAfter: ['error', 'close', 'end'], + }, + timeout: { + notAfter: ['error', 'close'], + }, + }); // XXX This implementation is kind of all over the place // When the parser emits body chunks, they go in this list. diff --git a/lib/_http_outgoing.js b/lib/_http_outgoing.js index d02bdf7f2..39cc2ef48 100644 --- a/lib/_http_outgoing.js +++ b/lib/_http_outgoing.js @@ -62,7 +62,18 @@ utcDate._onTimeout = function() { function OutgoingMessage() { - Stream.call(this); + Stream.Writable.call(this); + this.strictEERegister({ + socket: { + notAfter: ['close', 'error', 'finish'], + }, + upgrade: { + maxCount: 1, + }, + timeout: { + notAfter: ['error', 'close'], + }, + }); this.output = []; this.outputEncodings = []; @@ -90,7 +101,7 @@ function OutgoingMessage() { this._headers = null; this._headerNames = {}; } -util.inherits(OutgoingMessage, Stream); +util.inherits(OutgoingMessage, Stream.Writable); exports.OutgoingMessage = OutgoingMessage; diff --git a/lib/_http_server.js b/lib/_http_server.js index 787fc27bb..7ca307ca2 100644 --- a/lib/_http_server.js +++ b/lib/_http_server.js @@ -100,6 +100,15 @@ var STATUS_CODES = exports.STATUS_CODES = { function ServerResponse(req) { OutgoingMessage.call(this); + this.strictEERegister({ + close: { + maxCount: 1, + after: ['abort', 'error', '_socketClose'], + }, + _socketClose: { + maxCount: 1, + }, + }, true); if (req.method === 'HEAD') this._hasBody = false; @@ -238,6 +247,27 @@ function Server(requestListener) { if (!(this instanceof Server)) return new Server(requestListener); net.Server.call(this, { allowHalfOpen: true }); + this.strictEERegister({ + checkContinue: { + notAfter: ['close', 'error'], + after: ['listening', 'connection'], + }, + request: { + notAfter: ['close', 'error'], + }, + socket: { + notAfter: ['close', 'error'], + }, + timeout: { + after: ['listening', 'connection'], + notAfter: ['error', 'close'], + }, + upgrade: { + after: ['listening', 'connection'], + notAfter: ['close', 'error'], + }, + }); + if (requestListener) { this.addListener('request', requestListener); } diff --git a/lib/_stream_readable.js b/lib/_stream_readable.js index 5f280b773..e87c9f293 100644 --- a/lib/_stream_readable.js +++ b/lib/_stream_readable.js @@ -22,7 +22,7 @@ module.exports = Readable; Readable.ReadableState = ReadableState; -var EE = require('events').EventEmitter; +var EE = require('events').StrictEE; var Stream = require('stream'); var Buffer = require('buffer').Buffer; var util = require('util'); @@ -106,6 +106,24 @@ function Readable(options) { this.readable = true; Stream.call(this); + this.strictEERegister({ + data: { + notAfter: ['close', 'end', 'error'], + }, + end: { + maxCount: 1, + notAfter: ['error', 'close'], + }, + pause: { + notAfter: ['end', 'error', 'close'], + }, + readable: { + notAfter: ['end', 'error', 'close'], + }, + resume: { + notAfter: ['end', 'error', 'close'], + }, + }, true); } // Manually shove something into the read() buffer. diff --git a/lib/_stream_writable.js b/lib/_stream_writable.js index f39393b06..8f5730706 100644 --- a/lib/_stream_writable.js +++ b/lib/_stream_writable.js @@ -155,6 +155,27 @@ function Writable(options) { this.writable = true; Stream.call(this); + this.strictEERegister({ + drain: { + notAfter: ['finish', 'close', 'error'], + }, + finish: { + maxCount: 1, + after: ['prefinish'], + notAfter: ['close', 'error'], + }, + pipe: { + notAfter: ['close', 'error', 'finish'], + }, + prefinish: { + maxCount: 1, + notAfter: ['close', 'error', 'finish'], + }, + unpipe: { + after: ['pipe'], + notAfter: ['close', 'error'], + }, + }, true); } // Otherwise people can pipe Writable streams, which is just wrong. diff --git a/lib/_tls_wrap.js b/lib/_tls_wrap.js index 6e2a43084..ac4873fa3 100644 --- a/lib/_tls_wrap.js +++ b/lib/_tls_wrap.js @@ -235,6 +235,18 @@ function TLSSocket(socket, options) { readable: false, writable: false }); + this.strictEERegister({ + secure: { + maxCount: 1, + }, + secureConnect: { + maxCount: 1, + after: ['connect', 'secure'], + }, + _tlsError: { + maxCount: 1, + }, + }); if (socket) { this._parent = socket; @@ -674,6 +686,12 @@ function Server(/* [options], listener */) { } }); }); + this.strictEERegister({ + secureConnection: { + after: ['listening', 'connection'], + notAfter: ['close', 'error'], + }, + }); if (listener) { this.on('secureConnection', listener); diff --git a/lib/events.js b/lib/events.js index 0cd841b84..b6bc9a866 100644 --- a/lib/events.js +++ b/lib/events.js @@ -19,7 +19,7 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. -var domain; +var assert, domain; var util = require('util'); function EventEmitter() { @@ -315,3 +315,163 @@ EventEmitter.listenerCount = function(emitter, type) { ret = emitter._events[type].length; return ret; }; + +// StrictEE +var strictID = 0; +var expectedEvents = {}; + +function StrictEE(rules, override) { + if (EventEmitter.TrackMinCount === undefined) { + EventEmitter.TrackMinCount = process.NODE_STRICT_EVENTS || false; + process.on('exit', function StrictEEOnExit() { + if (EventEmitter.TrackMinCount && Object.keys(expectedEvents).length > 0) { + (function MinCountAssertionFailure(ee) { + process.abort(); + })(expectedEvents); + } + }); + } + + if (EventEmitter.RequireEventDef === undefined) { + EventEmitter.RequireEventDef = process.NODE_REQUIRE_EVENTS || true; + } + + if (assert === undefined) + assert = require('assert'); + + if (!(this instanceof StrictEE)) + return new StrictEE(rules); + + this._strictID = undefined; + this.strictEERegister(rules, override); + + EventEmitter.call(this); +} +util.inherits(StrictEE, EventEmitter); + + +EventEmitter.StrictEE = StrictEE; +EventEmitter.TrackMinCount = undefined; +EventEmitter.RequireEventDef = undefined; + +StrictEE.prototype.strictEERegister = function strictRegister(rules, override) { + var oldRules = this._strictEERules = this._strictEERules || {}; + this._strictEvents = this._strictEvents || {}; + + var i, e; + + for (i in rules) { + if (!override) { + var r = oldRules[i]; + assert.strictEqual(r, undefined, + util.format('Event "%s" already registered, %j', i, r)); + } + + e = rules[i]; + + if (util.isArray(e.notAfter) && e.notAfter.indexOf('*') > -1) { + assert.strictEqual(e.notAfter.length, 1, + util.format( + 'Event "%s" notAfter wild card must only have one entry, not %j', + i, e.notAfter)); + } + + if (EventEmitter.TrackMinCount && util.isNumber(e.minCount)) { + if (!this._strictID) + this._strictID = ++strictID; + + var ee = expectedEvents[strictID] = expectedEvents[strictID] || { __: this }; + + ee[i] = e.minCount; + } + } + + this._strictEERules = util._extend(oldRules, rules); +}; + +StrictEE.prototype.emit = function strictEmit(type) { + var strictRule = this._strictEERules[type]; + + if (!strictRule) { + if (EventEmitter.RequireEventDef) + assert.ok(false, util.format('Event "%s" was not defined, fired %j', + type, Object.keys(this._strictEvents))); + else + return EventEmitter.prototype.emit.apply(this, arguments); + } + + var keys; + + if (util.isArray(strictRule.notAfter) && strictRule.notAfter.length === 1 && + strictRule.notAfter[0] === '*') { + keys = Object.keys(this._strictEvents); + assert.strictEqual(keys.length, 0, + util.format('Event "%s" must come first but came after %j', type, keys)); + } + + var strictEvent = this._strictEvents[type] || {}; + + strictEvent.count = 1 + (strictEvent.count || 0); + + if (util.isNumber(strictRule.maxCount)) { + assert.ok(strictEvent.count <= strictRule.maxCount, + util.format('Event "%s" fired %d times, more than %d', + type, strictEvent.count, strictRule.maxCount)); + } + + if (util.isNumber(strictRule.minCount)) { + assert.ok(this._strictID, 'Object must have a strictID'); + var oee = expectedEvents[this._strictID]; + if (oee) { + var ee = oee[type]; + if (ee) { + if (--ee === 0) + delete oee[type]; + } + + if (Object.keys(oee) === 1) + delete expectedEvents[this._strictID]; + } + } + + var found, i, e; + + if (util.isArray(strictRule.notAfter)) { + found = false; + + for (i in strictRule.notAfter) { + if (i === '*') continue; + + e = strictRule.notAfter[i]; + if (this._strictEvents[e]) { + found = e; + break; + } + } + + assert.strictEqual(found, false, + util.format('Event "%s" must **not** fire after "%s", fired %j', + type, found, Object.keys(this._strictEvents))); + } + + if (util.isArray(strictRule.after)) { + found = false; + + for (i in strictRule.after) { + e = strictRule.after[i]; + if (this._strictEvents[e]) { + found = true; + break; + } + } + + console.error(this); + assert.strictEqual(found, true, + util.format('Event "%s" must fire after at least one of %j, fired %j', + type, strictRule.after, Object.keys(this._strictEvents))); + } + + this._strictEvents[type] = strictEvent; + + return EventEmitter.prototype.emit.apply(this, arguments); +}; @@ -1567,6 +1567,12 @@ function ReadStream(path, options) { }, options || {}); Readable.call(this, options); + this.strictEERegister({ + open: { + maxCount: 1, + notAfter: ['close', 'error'], + }, + }); this.path = path; this.fd = options.hasOwnProperty('fd') ? options.fd : null; @@ -1733,6 +1739,12 @@ function WriteStream(path, options) { options = options || {}; Writable.call(this, options); + this.strictEERegister({ + open: { + maxCount: 1, + notAfter: ['error', 'close'], + }, + }); this.path = path; this.fd = null; @@ -1814,7 +1826,13 @@ WriteStream.prototype.destroySoon = WriteStream.prototype.end; // SyncWriteStream is internal. DO NOT USE. // Temporary hack for process.stdout and process.stderr when piped to files. function SyncWriteStream(fd, options) { - Stream.call(this); + Stream.Writable.call(this); + this.strictEERegister({ + open: { + maxOpen: 1, + notAfter: ['error', 'close'], + }, + }); options = options || {}; @@ -1825,7 +1843,7 @@ function SyncWriteStream(fd, options) { options.autoClose : true; } -util.inherits(SyncWriteStream, Stream); +util.inherits(SyncWriteStream, Stream.Writable); // Export diff --git a/lib/https.js b/lib/https.js index f9011648f..f723278f5 100644 --- a/lib/https.js +++ b/lib/https.js @@ -34,6 +34,26 @@ function Server(opts, requestListener) { } tls.Server.call(this, opts, http._connectionListener); + this.strictEERegister({ + checkContinue: { + notAfter: ['close', 'error'], + after: ['listening', 'connection'], + }, + request: { + notAfter: ['close', 'error'], + }, + socket: { + notAfter: ['close', 'error'], + }, + timeout: { + after: ['listening', 'connection'], + notAfter: ['error', 'close'], + }, + upgrade: { + after: ['listening', 'connection'], + notAfter: ['close', 'error'], + }, + }); this.httpAllowHalfOpen = false; diff --git a/lib/net.js b/lib/net.js index 70baab3bd..d16bba141 100644 --- a/lib/net.js +++ b/lib/net.js @@ -197,6 +197,38 @@ function Socket(options) { this.read(0); } } + + this.strictEERegister({ + agentRemove: { + notAfter: ['close', 'error'], + }, + // aborted comes from http + connect: { + maxCount: 1, + notAfter: ['data', 'error', 'end', 'finish', 'close'], + }, + destroy: { + //maxCount: 1, + notAfter: ['close'], + }, + // XXX + // used by httpAgent, and inexplicably we can't actually say it doesn't + // happen after any of the events you might consider being final. + free: { + //notAfter: ['error', 'finish', 'end', 'close'], + }, + lookup: { + maxCount: 1, + notAfter: ['connect', 'data', 'error', 'end', 'finish', 'close'], + }, + timeout: { + notAfter: ['error', 'end', 'close'], + }, + _socketEnd: { + maxCount: 1, + notAfter: ['error', 'end', 'close'], + }, + }); } util.inherits(Socket, stream.Duplex); @@ -856,6 +888,7 @@ Socket.prototype.connect = function(options, cb) { return Socket.prototype.connect.apply(this, args); } + // XXX this logic is suspect if (this.destroyed) { this._readableState.reading = false; this._readableState.ended = false; @@ -866,6 +899,7 @@ Socket.prototype.connect = function(options, cb) { this._writableState.errorEmitted = false; this.destroyed = false; this._handle = null; + this._strictEvents = {}; } var self = this; @@ -1004,7 +1038,32 @@ function afterConnect(status, handle, req, readable, writable) { function Server(/* [ options, ] listener */) { if (!(this instanceof Server)) return new Server(arguments[0], arguments[1]); - events.EventEmitter.call(this); + events.StrictEE.call(this, { + clientError: { + after: ['listening'], + notAfter: ['error', 'close'], + }, + close: { + after: ['listening', 'error'], + maxCount: 1, + }, + connect: { + after: ['listening'], + notAfter: ['error', 'close'], + }, + connection: { + after: ['listening'], + notAfter: ['close', 'error'], + }, + error: { + notAfter: ['close'], + maxCount: 1, + }, + listening: { + notAfter: ['close', 'error'], + maxCount: 1, + }, + }); var self = this; @@ -1044,7 +1103,7 @@ function Server(/* [ options, ] listener */) { this.allowHalfOpen = options.allowHalfOpen || false; this.pauseOnConnect = !!options.pauseOnConnect; } -util.inherits(Server, events.EventEmitter); +util.inherits(Server, events.StrictEE); exports.Server = Server; diff --git a/lib/stream.js b/lib/stream.js index 073c6ed33..298994305 100644 --- a/lib/stream.js +++ b/lib/stream.js @@ -21,7 +21,7 @@ module.exports = Stream; -var EE = require('events').EventEmitter; +var EE = require('events').StrictEE; var util = require('util'); util.inherits(Stream, EE); @@ -40,7 +40,17 @@ Stream.Stream = Stream; // part of this class) is overridden in the Readable class. function Stream() { - EE.call(this); + EE.call(this, { + close: { + maxCount: 1, + //aborted comes from http semantics + after: ['error', 'end', 'finish', 'destroy', 'aborted'], + }, + error: { + maxCount: 1, + notAfter: ['close', 'end', 'finish'], + }, + }, true); } Stream.prototype.pipe = function(dest, options) { |