Permalink
Browse files

Make a copy of `listeners` for hijacking events.

Node.js v0.7+ `event.listeners()` returns a reference
that continues to be used internally after calling
`removeAllListeners()`. Make a copy before calling
the original functions to prevent recursion on v0.7+.

Since this pattern is common in Yeti, move this
event hijacking into its own module.
  • Loading branch information...
1 parent ab10c71 commit 51aa577fbebf63d1a707ce3de652decdd1f12bba @reid reid committed Jun 12, 2012
Showing with 71 additions and 63 deletions.
  1. +6 −44 lib/blizzard/index.js
  2. +58 −0 lib/hijack.js
  3. +7 −19 lib/hub/listener.js
View
50 lib/blizzard/index.js
@@ -4,6 +4,8 @@ var http = require("http");
var util = require("util");
var urlParser = require("url");
+var hijack = require("../hijack");
+
var BlizzardSession = require("./session");
var EventEmitter2 = require("eventemitter2").EventEmitter2;
@@ -177,50 +179,10 @@ Blizzard.prototype.connect = function (url, cb) {
* @param {HTTPServer} httpServer
*/
Blizzard.prototype.listen = function (httpServer) {
- var self = this,
- listeners = httpServer.listeners("upgrade");
-
- httpServer.removeAllListeners("upgrade");
-
- httpServer.on("upgrade", function (req, socket, head) {
-
- // First, attempt to upgrade to Blizzard.
- var success = self.serverUpgrade(req, socket, head),
- written = false,
- originalWrite;
-
- if (!success) {
- // Blizzard was not the protocol asked for.
-
- // If data is written to the socket,
- // the connection was upgraded.
- originalWrite = socket.write;
- socket.write = function (string, encoding, fd) {
- written = true;
- originalWrite.call(this, string, encoding, fd);
- };
-
- // Try other upgrade listeners, e.g. Socket.io.
- listeners.forEach(function (fn) {
- fn(req, socket, head);
- });
-
- // Restore original write.
- socket.write = originalWrite;
-
- // No listener wrote to the socket.
- // Destroy the connection.
- if (!written && socket.writable) {
- socket.write([
- "HTTP/1.1 400 Bad Request",
- "X-Reason: Protocol not supported",
- "Connection: close",
- "Content-Length: 0",
- "", ""
- ].join("\r\n"));
- socket.end();
- }
- }
+ var self = this;
+
+ hijack(httpServer, "upgrade", function (req, socket, head) {
+ return self.serverUpgrade(req, socket, head);
});
return this;
View
58 lib/hijack.js
@@ -0,0 +1,58 @@
+"use strict";
+
+/**
+ * @module hijack
+ */
+
+/**
+ * Attach the provided function as the first
+ * listener of the given EventEmitter event.
+ *
+ * All listeners of the event will be removed
+ * and replaced with a listener that will run
+ * the provided function first, followed by
+ * the original listeners if the function
+ * returned false.
+ *
+ * Works with Node.js v0.7+ where the array
+ * returned by `ee.listeners()` is a reference.
+ *
+ * @method hijack
+ * @param {EventEmitter} ee Emitter.
+ * @param {String} event Event name.
+ * @param {Function} firstFn Function to run first.
+ */
+module.exports = function hijack(ee, event, firstFn) {
+ var listeners = ee.listeners(event),
+ i = 0,
+ length = listeners.length,
+ originalListeners = [];
+
+ // Note: listeners is a reference in Node v0.7.
+ // Calling `removeAllListeners` no longer destroys
+ // the listener array, which causes it survive
+ // as a reference. See joyent/node commits:
+ // - 78dc13fbf97e2e3003e6f3baacdd5ff60e8de3f7
+ // - 928ea564d16da47e615ddac627e0b4d4a40d8196
+ //
+ // Make a copy first.
+ for (; i < length; i += 1) {
+ originalListeners[i] = listeners[i];
+ }
+
+ ee.removeAllListeners(event);
+
+ ee.on(event, function () {
+ var args = Array.prototype.slice.call(arguments),
+ stack = [firstFn].concat(originalListeners),
+ handled;
+
+ handled = firstFn.apply(ee, args);
+
+ if (!handled) {
+ originalListeners.forEach(function (fn) {
+ fn.apply(ee, args);
+ });
+ }
+ });
+};
View
26 lib/hub/listener.js
@@ -5,7 +5,8 @@
*/
var util = require("util"),
- url = require("url");
+ url = require("url"),
+ hijack = require("../hijack");
/**
* A **HubListener** augments an existing `http.Server`,
@@ -39,14 +40,9 @@ var proto = HubListener.prototype;
* @private
*/
proto._setupServer = function () {
- var self = this,
- upgradeListeners = this.server.listeners("upgrade"),
- requestListeners = this.server.listeners("request");
+ var self = this;
- this.server.removeAllListeners("upgrade");
- this.server.removeAllListeners("request");
-
- this.server.on("upgrade", function (req, socket, head) {
+ hijack(this.server, "upgrade", function (req, socket, head) {
var server = this,
parsedUrl = url.parse(req.url),
resolvedUrl,
@@ -70,22 +66,14 @@ proto._setupServer = function () {
if (yetiUpgrade) {
self.hub.server.emit("upgrade", req, socket, head);
- return;
+ return true;
}
-
- upgradeListeners.forEach(function (listener) {
- listener.call(server, req, socket, head);
- });
});
- this.server.on("request", function (req, res) {
+ hijack(this.server, "request", function faxd (req, res) {
if (self.match(req, res)) {
- return;
+ return true;
}
- var server = this;
- requestListeners.forEach(function (listener) {
- listener.call(server, req, res);
- });
});
};

0 comments on commit 51aa577

Please sign in to comment.