From 390dafea01381d375be21f0bd126ddc7fd0a0582 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Mon, 27 Jan 2014 21:18:24 -0800 Subject: [PATCH 1/7] Move _SynchronousQueue into another file This commit does not build but exists so that the next commit shows diffs better. --- packages/meteor/fiber_stubs_client.js | 68 --------------------------- packages/meteor/unyielding_queue.js | 67 ++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 68 deletions(-) create mode 100644 packages/meteor/unyielding_queue.js diff --git a/packages/meteor/fiber_stubs_client.js b/packages/meteor/fiber_stubs_client.js index 3babd0e08f..46163a26e1 100644 --- a/packages/meteor/fiber_stubs_client.js +++ b/packages/meteor/fiber_stubs_client.js @@ -6,71 +6,3 @@ Meteor._noYieldsAllowed = function (f) { return f(); }; - -// An even simpler queue of tasks than the fiber-enabled one. This one just -// runs all the tasks when you call runTask or flush, synchronously. -// -Meteor._SynchronousQueue = function () { - var self = this; - self._tasks = []; - self._running = false; -}; - -_.extend(Meteor._SynchronousQueue.prototype, { - runTask: function (task) { - var self = this; - if (!self.safeToRunTask()) - throw new Error("Could not synchronously run a task from a running task"); - self._tasks.push(task); - var tasks = self._tasks; - self._tasks = []; - self._running = true; - try { - while (!_.isEmpty(tasks)) { - var t = tasks.shift(); - try { - t(); - } catch (e) { - if (_.isEmpty(tasks)) { - // this was the last task, that is, the one we're calling runTask - // for. - throw e; - } else { - Meteor._debug("Exception in queued task: " + e.stack); - } - } - } - } finally { - self._running = false; - } - }, - - queueTask: function (task) { - var self = this; - var wasEmpty = _.isEmpty(self._tasks); - self._tasks.push(task); - // Intentionally not using Meteor.setTimeout, because it doesn't like runing - // in stubs for now. - if (wasEmpty) - setTimeout(_.bind(self.flush, self), 0); - }, - - flush: function () { - var self = this; - self.runTask(function () {}); - }, - - drain: function () { - var self = this; - if (!self.safeToRunTask()) - return; - while (!_.isEmpty(self._tasks)) { - self.flush(); - } - }, - - safeToRunTask: function () { - var self = this; - return !self._running; - } -}); diff --git a/packages/meteor/unyielding_queue.js b/packages/meteor/unyielding_queue.js new file mode 100644 index 0000000000..71819b460b --- /dev/null +++ b/packages/meteor/unyielding_queue.js @@ -0,0 +1,67 @@ +// An even simpler queue of tasks than the fiber-enabled one. This one just +// runs all the tasks when you call runTask or flush, synchronously. +// +Meteor._SynchronousQueue = function () { + var self = this; + self._tasks = []; + self._running = false; +}; + +_.extend(Meteor._SynchronousQueue.prototype, { + runTask: function (task) { + var self = this; + if (!self.safeToRunTask()) + throw new Error("Could not synchronously run a task from a running task"); + self._tasks.push(task); + var tasks = self._tasks; + self._tasks = []; + self._running = true; + try { + while (!_.isEmpty(tasks)) { + var t = tasks.shift(); + try { + t(); + } catch (e) { + if (_.isEmpty(tasks)) { + // this was the last task, that is, the one we're calling runTask + // for. + throw e; + } else { + Meteor._debug("Exception in queued task: " + e.stack); + } + } + } + } finally { + self._running = false; + } + }, + + queueTask: function (task) { + var self = this; + var wasEmpty = _.isEmpty(self._tasks); + self._tasks.push(task); + // Intentionally not using Meteor.setTimeout, because it doesn't like runing + // in stubs for now. + if (wasEmpty) + setTimeout(_.bind(self.flush, self), 0); + }, + + flush: function () { + var self = this; + self.runTask(function () {}); + }, + + drain: function () { + var self = this; + if (!self.safeToRunTask()) + return; + while (!_.isEmpty(self._tasks)) { + self.flush(); + } + }, + + safeToRunTask: function () { + var self = this; + return !self._running; + } +}); From ed83f00fc108b1c61ddd06c1b4362b3ffc6fd055 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Mon, 27 Jan 2014 21:19:32 -0800 Subject: [PATCH 2/7] Rename _SynchronousQueue to _UnyieldingQueue Allow server uses of LocalCollection to prefer _UnyieldingQueue. This will mean that, as long as their observeChanges callbacks do not yield, calls to insert/update/remove will not yield either. --- packages/meteor/package.js | 1 + packages/meteor/unyielding_queue.js | 15 ++++++++++----- packages/minimongo/minimongo.js | 29 +++++++++++++++++++++++++---- 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/packages/meteor/package.js b/packages/meteor/package.js index 439d80e6f2..9e2db6e68e 100644 --- a/packages/meteor/package.js +++ b/packages/meteor/package.js @@ -23,6 +23,7 @@ Package.on_use(function (api) { api.add_files('errors.js', ['client', 'server']); api.add_files('fiber_helpers.js', 'server'); api.add_files('fiber_stubs_client.js', 'client'); + api.add_files('unyielding_queue.js'); api.add_files('startup_client.js', ['client']); api.add_files('startup_server.js', ['server']); api.add_files('debug.js', ['client', 'server']); diff --git a/packages/meteor/unyielding_queue.js b/packages/meteor/unyielding_queue.js index 71819b460b..95c62b23a4 100644 --- a/packages/meteor/unyielding_queue.js +++ b/packages/meteor/unyielding_queue.js @@ -1,13 +1,16 @@ -// An even simpler queue of tasks than the fiber-enabled one. This one just -// runs all the tasks when you call runTask or flush, synchronously. +// A simpler version of Meteor._SynchronousQueue with the same external +// interface. It runs on both client and server, unlike _SynchronousQueue which +// only runs on the server. When used on the server, tasks may not yield. This +// one just runs all the tasks when you call runTask or flush, synchronously. +// It itself also does not yield. // -Meteor._SynchronousQueue = function () { +Meteor._UnyieldingQueue = function () { var self = this; self._tasks = []; self._running = false; }; -_.extend(Meteor._SynchronousQueue.prototype, { +_.extend(Meteor._UnyieldingQueue.prototype, { runTask: function (task) { var self = this; if (!self.safeToRunTask()) @@ -20,7 +23,9 @@ _.extend(Meteor._SynchronousQueue.prototype, { while (!_.isEmpty(tasks)) { var t = tasks.shift(); try { - t(); + Meteor._noYieldsAllowed(function () { + t(); + }); } catch (e) { if (_.isEmpty(tasks)) { // this was the last task, that is, the one we're calling runTask diff --git a/packages/minimongo/minimongo.js b/packages/minimongo/minimongo.js index 8362c95a78..539a0ec704 100644 --- a/packages/minimongo/minimongo.js +++ b/packages/minimongo/minimongo.js @@ -7,13 +7,34 @@ // ObserveHandle: the return value of a live query. -LocalCollection = function (name) { +LocalCollection = function (name, options) { var self = this; + options = options || {}; + self.name = name; // _id -> document (also containing id) self._docs = new LocalCollection._IdMap; - self._observeQueue = new Meteor._SynchronousQueue(); + // When writing to this collection, we batch all observeChanges callbacks + // until the end of the write, and run them at this point. On the server, we + // use a single SynchronousQueue to do so, so that we never deliver callbacks + // out of order even if other writes occur during a yield. On the client, or + // on the server if we promise that our callbacks will never yield via an + // undocumented option, we use the simpler UnyieldingQueue. + // + // (What is the _observeCallbacksWillNeverYield option for? In some cases, it + // can be nice (on the server) to be able to write to a LocalCollection + // without yielding (eg, in a _noYieldsAllowed block). It's necessary to + // provide non-yielding allow callbacks in that case, but just doing that + // wouldn't be good enough if we always used SynchronousQueue on the server, + // since it tends to yield in order to run even non-yielding callbacks.) + var queueClass; + if (Meteor._SynchronousQueue && !options._observeCallbacksWillNeverYield) { + queueClass = Meteor._SynchronousQueue; + } else { + queueClass = Meteor._UnyieldingQueue; + } + self._observeQueue = new queueClass(); self.next_qid = 1; // live query id generator @@ -26,8 +47,8 @@ LocalCollection = function (name) { // selector, sorter, (callbacks): functions self.queries = {}; - // null if not saving originals; an IdMap from id to original document value if - // saving originals. See comments before saveOriginals(). + // null if not saving originals; an IdMap from id to original document value + // if saving originals. See comments before saveOriginals(). self._savedOriginals = null; // True when observers are paused and we should not send callbacks. From 782ff1bcaee22dc2cd8e1dda6bdb7b2932d632a1 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Mon, 27 Jan 2014 21:23:07 -0800 Subject: [PATCH 3/7] Optionify the barely-used LocalCollection name arg --- packages/minimongo/minimongo.js | 4 ++-- packages/mongo-livedata/local_collection_driver.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/minimongo/minimongo.js b/packages/minimongo/minimongo.js index 539a0ec704..34b5efa035 100644 --- a/packages/minimongo/minimongo.js +++ b/packages/minimongo/minimongo.js @@ -7,11 +7,11 @@ // ObserveHandle: the return value of a live query. -LocalCollection = function (name, options) { +LocalCollection = function (options) { var self = this; options = options || {}; - self.name = name; + self.name = options.name; // _id -> document (also containing id) self._docs = new LocalCollection._IdMap; diff --git a/packages/mongo-livedata/local_collection_driver.js b/packages/mongo-livedata/local_collection_driver.js index ec78544154..aa325979e5 100644 --- a/packages/mongo-livedata/local_collection_driver.js +++ b/packages/mongo-livedata/local_collection_driver.js @@ -5,7 +5,7 @@ LocalCollectionDriver = function () { var ensureCollection = function (name, collections) { if (!(name in collections)) - collections[name] = new LocalCollection(name); + collections[name] = new LocalCollection({name: name}); return collections[name]; }; From 980b70596d323f818156b0749f5998268b384e2e Mon Sep 17 00:00:00 2001 From: David Glasser Date: Mon, 27 Jan 2014 21:55:36 -0800 Subject: [PATCH 4/7] More comments about our sort inconsistency --- packages/minimongo/sort.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/minimongo/sort.js b/packages/minimongo/sort.js index 718ec1537e..02218593ec 100644 --- a/packages/minimongo/sort.js +++ b/packages/minimongo/sort.js @@ -48,11 +48,19 @@ Sorter = function (spec) { // min/max.) // // XXX This is actually wrong! In fact, the whole attempt to compile sort - // functions independently of selectors is wrong. In MongoDB, if you have - // documents {_id: 'x', a: [1, 10]} and {_id: 'y', a: [5, 15]}, - // then C.find({}, {sort: {a: 1}}) puts x before y (1 comes before 5). - // But C.find({a: {$gt: 3}}, {sort: {a: 1}}) puts y before x (1 does not match - // the selector, and 5 comes before 10). + // functions independently of selectors is wrong. In MongoDB, if you have + // documents {_id: 'x', a: [1, 10]} and {_id: 'y', a: [5, 15]}, then + // C.find({}, {sort: {a: 1}}) puts x before y (1 comes before 5). But + // C.find({a: {$gt: 3}}, {sort: {a: 1}}) puts y before x (1 does not match + // the selector, and 5 comes before 10). + // + // The way this works is pretty subtle! For example, if the documents are + // instead {_id: 'x', a: [{x: 1}, {x: 10}]}) and + // {_id: 'y', a: [{x: 5}, {x: 15}]}), + // then C.find({'a.x': {$gt: 3}}, {sort: {'a.x': 1}}) and + // C.find({a: {$elemMatch: {x: {$gt: 3}}}}, {sort: {'a.x': 1}}) + // both follow this rule (y before x). ie, you do have to apply this + // through $elemMatch. var reduceValue = function (branchValues, findMin) { // Expand any leaf arrays that we find, and ignore those arrays themselves. branchValues = expandArraysInBranches(branchValues, true); From a65b978972d6f2111254c19fc5b6a1b6b53b43e6 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Fri, 24 Jan 2014 17:04:13 -0800 Subject: [PATCH 5/7] Refactor OplogObserveDriver to use LocalCollection This should be a good starting point for implementing limit/sort/skip. --- .../mongo-livedata/oplog_observe_driver.js | 151 +++++++++--------- 1 file changed, 72 insertions(+), 79 deletions(-) diff --git a/packages/mongo-livedata/oplog_observe_driver.js b/packages/mongo-livedata/oplog_observe_driver.js index 2cfa2a7775..d3d9796eda 100644 --- a/packages/mongo-livedata/oplog_observe_driver.js +++ b/packages/mongo-livedata/oplog_observe_driver.js @@ -30,13 +30,24 @@ OplogObserveDriver = function (options) { self._registerPhaseChange(PHASE.QUERYING); - self._published = new LocalCollection._IdMap; + // A minimongo LocalCollection containing the docs that match the selector, + // and maybe more. It is guaranteed to contain all the fields needed for the + // selector and the projection, and may have other fields too. (In the future + // we may try to make this collection be shared between multiple + // OplogObserveDrivers, but not currently.) + self._collection = + new LocalCollection({_observeCallbacksWillNeverYield: true}); + // XXX think about what all the options are + var minimongoCursor = self._collection.find( + self._cursorDescription.selector, self._cursorDescription.options); + self._stopHandles.push(minimongoCursor.observeChanges(self._multiplexer)); + var selector = self._cursorDescription.selector; self._matcher = options.matcher; - var projection = self._cursorDescription.options.fields || {}; - self._projectionFn = LocalCollection._compileProjection(projection); + // Projection function, result of combining important fields for selector and // existing fields projection + var projection = self._cursorDescription.options.fields || {}; self._sharedProjection = self._matcher.combineIntoProjection(projection); self._sharedProjectionFn = LocalCollection._compileProjection( self._sharedProjection); @@ -109,47 +120,51 @@ OplogObserveDriver = function (options) { _.extend(OplogObserveDriver.prototype, { _add: function (doc) { var self = this; - var id = doc._id; - var fields = _.clone(doc); - delete fields._id; - if (self._published.has(id)) - throw Error("tried to add something already published " + id); - self._published.set(id, self._sharedProjectionFn(fields)); - self._multiplexer.added(id, self._projectionFn(fields)); + doc = self._sharedProjectionFn(doc); + // XXX does _sharedProjection always preserve id? + if (!_.has(doc, '_id')) + throw Error("Can't add doc without _id"); + self._collection.insert(doc); }, - _remove: function (id) { + _remove: function (id, options) { var self = this; - if (!self._published.has(id)) + options = options || {}; + var removed = self._collection.remove({_id: id}); + if (options.mustExist && removed !== 1) throw Error("tried to remove something unpublished " + id); - self._published.remove(id); - self._multiplexer.removed(id); }, _handleDoc: function (id, newDoc, mustMatchNow) { var self = this; - newDoc = _.clone(newDoc); + newDoc = _.clone(newDoc); // *shallow* clone + // XXX this is just about "matching selector", not about skip/limit var matchesNow = newDoc && self._matcher.documentMatches(newDoc).result; if (mustMatchNow && !matchesNow) { throw Error("expected " + EJSON.stringify(newDoc) + " to match " + EJSON.stringify(self._cursorDescription)); } - var matchedBefore = self._published.has(id); + var inCollection = !!self._collection.findOne(id); - if (matchesNow && !matchedBefore) { + if (matchesNow && !inCollection) { + // It matches the selector and it isn't in our collection, so add it. + // XXX once we add skip/limit, this may not always send an added, and + // we may need to do some GC self._add(newDoc); - } else if (matchedBefore && !matchesNow) { - self._remove(id); + } else if (inCollection && !matchesNow) { + // We remove this from the collection to achieve two goals: (a) causing + // the observeChanges to fire removed() and (b) saving memory. That said, + // it would be legitimate (if !!newDoc) to update the collection instead + // of removing, if we thought we might need this doc again soon. + self._remove(id, {mustExist: true}); } else if (matchesNow) { - var oldDoc = self._published.get(id); - if (!oldDoc) - throw Error("thought that " + id + " was there!"); - delete newDoc._id; - self._published.set(id, self._sharedProjectionFn(newDoc)); - var changed = LocalCollection._makeChangedFields(_.clone(newDoc), oldDoc); - changed = self._projectionFn(changed); - if (!_.isEmpty(changed)) - self._multiplexer.changed(id, changed); + // Replace the doc inside our collection, which may trigger a changed + // callback. + newDoc = self._sharedProjectionFn(newDoc); + // XXX does _sharedProjection always preserve id? + if (!_.has(newDoc, '_id')) + throw Error("Can't add newDoc without _id"); + self._collection.update(id, newDoc); } }, _fetchModifiedDocuments: function () { @@ -233,10 +248,9 @@ _.extend(OplogObserveDriver.prototype, { } if (op.op === 'd') { - if (self._published.has(id)) - self._remove(id); + self._remove(id); } else if (op.op === 'i') { - if (self._published.has(id)) + if (self._collection.findOne(id)) throw new Error("insert found for already-existing ID"); // XXX what if selector yields? for now it can't but later it could have @@ -258,18 +272,22 @@ _.extend(OplogObserveDriver.prototype, { if (isReplace) { self._handleDoc(id, _.extend({_id: id}, op.o)); - } else if (self._published.has(id) && canDirectlyModifyDoc) { - // Oh great, we actually know what the document is, so we can apply - // this directly. - var newDoc = EJSON.clone(self._published.get(id)); - newDoc._id = id; - LocalCollection._modify(newDoc, op.o); - self._handleDoc(id, self._sharedProjectionFn(newDoc)); - } else if (!canDirectlyModifyDoc || - self._matcher.canBecomeTrueByModifier(op.o)) { - self._needToFetch.set(id, op.ts.toString()); - if (self._phase === PHASE.STEADY) - self._fetchModifiedDocuments(); + } else { + var newDoc = self._collection.findOne(id); + if (newDoc && canDirectlyModifyDoc) { + // Oh great, we actually know what the document is, so we can apply + // this directly. + // XXX just send the modifier to _collection.update? but then + // we don't necessarily get to GC + newDoc = EJSON.clone(newDoc); + LocalCollection._modify(newDoc, op.o); + self._handleDoc(id, newDoc); + } else if (!canDirectlyModifyDoc || + self._matcher.canBecomeTrueByModifier(op.o)) { + self._needToFetch.set(id, op.ts.toString()); + if (self._phase === PHASE.STEADY) + self._fetchModifiedDocuments(); + } } } else { throw Error("XXX SURPRISING OPERATION: " + op); @@ -318,18 +336,19 @@ _.extend(OplogObserveDriver.prototype, { self._currentlyFetching = null; ++self._fetchGeneration; // ignore any in-flight fetches self._registerPhaseChange(PHASE.QUERYING); + self._collection.pauseObservers(); + // XXX this won't be quite correct for skip/limit + self._collection.remove({}); // Defer so that we don't block. Meteor.defer(function () { - // subtle note: _published does not contain _id fields, but newResults - // does - var newResults = new LocalCollection._IdMap; - var cursor = self._cursorForQuery(); - cursor.forEach(function (doc) { - newResults.set(doc._id, doc); + // Insert all the documents currently found by the query. + self._cursorForQuery().forEach(function (doc) { + self._collection.insert(doc); }); - self._publishNewResults(newResults); + // Allow observe callbacks (ie multiplexer invocations) to fire. + self._collection.resumeObservers(); self._doneQuerying(); }); @@ -399,34 +418,6 @@ _.extend(OplogObserveDriver.prototype, { }, - // Replace self._published with newResults (both are IdMaps), invoking observe - // callbacks on the multiplexer. - // - // XXX This is very similar to LocalCollection._diffQueryUnorderedChanges. We - // should really: (a) Unify IdMap and OrderedDict into Unordered/OrderedDict (b) - // Rewrite diff.js to use these classes instead of arrays and objects. - _publishNewResults: function (newResults) { - var self = this; - - // First remove anything that's gone. Be careful not to modify - // self._published while iterating over it. - var idsToRemove = []; - self._published.forEach(function (doc, id) { - if (!newResults.has(id)) - idsToRemove.push(id); - }); - _.each(idsToRemove, function (id) { - self._remove(id); - }); - - // Now do adds and changes. - newResults.forEach(function (doc, id) { - // "true" here means to throw if we think this doc doesn't match the - // selector. - self._handleDoc(id, doc, true); - }); - }, - // This stop function is invoked from the onStop of the ObserveMultiplexer, so // it shouldn't actually be possible to call it until the multiplexer is // ready. @@ -450,7 +441,6 @@ _.extend(OplogObserveDriver.prototype, { self._writesToCommitWhenWeReachSteady = null; // Proactively drop references to potentially big things. - self._published = null; self._needToFetch = null; self._currentlyFetching = null; self._oplogEntryHandle = null; @@ -464,6 +454,9 @@ _.extend(OplogObserveDriver.prototype, { var self = this; var now = new Date; + if (phase === self._phase) + return; + if (self._phase) { var timeDiff = now - self._phaseStartTime; Package.facts && Package.facts.Facts.incrementServerFact( From cb2f2adb1bd1a88a9fd651fe797592e2cd725941 Mon Sep 17 00:00:00 2001 From: Slava Kim Date: Tue, 28 Jan 2014 10:11:27 -0800 Subject: [PATCH 6/7] Do less deep copies --- packages/mongo-livedata/oplog_observe_driver.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/mongo-livedata/oplog_observe_driver.js b/packages/mongo-livedata/oplog_observe_driver.js index d3d9796eda..face62ccbb 100644 --- a/packages/mongo-livedata/oplog_observe_driver.js +++ b/packages/mongo-livedata/oplog_observe_driver.js @@ -144,7 +144,7 @@ _.extend(OplogObserveDriver.prototype, { + EJSON.stringify(self._cursorDescription)); } - var inCollection = !!self._collection.findOne(id); + var inCollection = !!self._collection.find(id).count(); if (matchesNow && !inCollection) { // It matches the selector and it isn't in our collection, so add it. @@ -250,7 +250,7 @@ _.extend(OplogObserveDriver.prototype, { if (op.op === 'd') { self._remove(id); } else if (op.op === 'i') { - if (self._collection.findOne(id)) + if (self._collection.find(id).count()) throw new Error("insert found for already-existing ID"); // XXX what if selector yields? for now it can't but later it could have @@ -279,7 +279,9 @@ _.extend(OplogObserveDriver.prototype, { // this directly. // XXX just send the modifier to _collection.update? but then // we don't necessarily get to GC - newDoc = EJSON.clone(newDoc); + + // We can avoid another deep clone here since the findOne above would + // return a copy anyways LocalCollection._modify(newDoc, op.o); self._handleDoc(id, newDoc); } else if (!canDirectlyModifyDoc || From 604cd853f3c63b4f64161742bbc4767a310bbba9 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Tue, 28 Jan 2014 11:57:36 -0800 Subject: [PATCH 7/7] Update http-proxy to 1.0.2 from our fork of 1.0.1 Only real change is changing a bad console.log into invoking our error handler, which we need for not printing too much junk in the runner https://github.com/nodejitsu/node-http-proxy/commit/daad4703 --- meteor | 4 +++- scripts/generate-dev-bundle.sh | 6 +----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/meteor b/meteor index 95a9676e78..3b6fe46738 100755 --- a/meteor +++ b/meteor @@ -1,6 +1,8 @@ #!/bin/bash -BUNDLE_VERSION=0.3.28 +# danger will robinson! mother:config/download-dev-bundles.sh only goes up to +# 0.3.30! +BUNDLE_VERSION=0.3.29 # OS Check. Put here because here is where we download the precompiled # bundles that are arch specific. diff --git a/scripts/generate-dev-bundle.sh b/scripts/generate-dev-bundle.sh index bbee7da6e6..8270888a5b 100755 --- a/scripts/generate-dev-bundle.sh +++ b/scripts/generate-dev-bundle.sh @@ -109,11 +109,7 @@ npm install eachline@2.4.0 npm install source-map@0.1.31 npm install source-map-support@0.2.5 npm install bcrypt@0.7.7 - -# Based on 1.0.1; includes our PRs -# https://github.com/nodejitsu/node-http-proxy/pull/561 and -# https://github.com/nodejitsu/node-http-proxy/pull/560 -npm install https://github.com/meteor/node-http-proxy/tarball/d8ea687936d6bed0f3e99849695cab2dcdccd6f4 +npm install http-proxy@1.0.2 # Using the unreleased 1.1 branch. We can probably switch to a built NPM version # when it gets released.