(function () { /* Imports */ var Meteor = Package.meteor.Meteor; var NpmModuleMongodb = Package['npm-mongo'].NpmModuleMongodb; var NpmModuleMongodbVersion = Package['npm-mongo'].NpmModuleMongodbVersion; var Random = Package.random.Random; var EJSON = Package.ejson.EJSON; var _ = Package.underscore._; var LocalCollection = Package.minimongo.LocalCollection; var Minimongo = Package.minimongo.Minimongo; var DDP = Package['ddp-client'].DDP; var DDPServer = Package['ddp-server'].DDPServer; var Tracker = Package.tracker.Tracker; var Deps = Package.tracker.Deps; var DiffSequence = Package['diff-sequence'].DiffSequence; var MongoID = Package['mongo-id'].MongoID; var check = Package.check.check; var Match = Package.check.Match; var MaxHeap = Package['binary-heap'].MaxHeap; var MinHeap = Package['binary-heap'].MinHeap; var MinMaxHeap = Package['binary-heap'].MinMaxHeap; var Hook = Package['callback-hook'].Hook; /* Package-scope variables */ var MongoInternals, MongoTest, MongoConnection, CursorDescription, Cursor, listenAll, forEachTrigger, OPLOG_COLLECTION, idForOp, OplogHandle, ObserveMultiplexer, ObserveHandle, DocFetcher, PollingObserveDriver, OplogObserveDriver, LocalCollectionDriver, Mongo; (function(){ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // // packages/mongo/mongo_driver.js // // // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // /** // 1 * Provide a synchronous Collection API using fibers, backed by // 2 * MongoDB. This is only for use on the server, and mostly identical // 3 * to the client API. // 4 * // 5 * NOTE: the public API methods must be run within a fiber. If you call // 6 * these outside of a fiber they will explode! // 7 */ // 8 // 9 var path = Npm.require('path'); // 10 var MongoDB = NpmModuleMongodb; // 11 var Fiber = Npm.require('fibers'); // 12 var Future = Npm.require(path.join('fibers', 'future')); // 13 // 14 MongoInternals = {}; // 15 MongoTest = {}; // 16 // 17 MongoInternals.NpmModules = { // 18 mongodb: { // 19 version: NpmModuleMongodbVersion, // 20 module: MongoDB // 21 } // 22 }; // 23 // 24 // Older version of what is now available via // 25 // MongoInternals.NpmModules.mongodb.module. It was never documented, but // 26 // people do use it. // 27 // XXX COMPAT WITH 1.0.3.2 // 28 MongoInternals.NpmModule = MongoDB; // 29 // 30 // This is used to add or remove EJSON from the beginning of everything nested // 31 // inside an EJSON custom type. It should only be called on pure JSON! // 32 var replaceNames = function (filter, thing) { // 33 if (typeof thing === "object") { // 34 if (_.isArray(thing)) { // 35 return _.map(thing, _.bind(replaceNames, null, filter)); // 36 } // 37 var ret = {}; // 38 _.each(thing, function (value, key) { // 39 ret[filter(key)] = replaceNames(filter, value); // 40 }); // 41 return ret; // 42 } // 43 return thing; // 44 }; // 45 // 46 // Ensure that EJSON.clone keeps a Timestamp as a Timestamp (instead of just // 47 // doing a structural clone). // 48 // XXX how ok is this? what if there are multiple copies of MongoDB loaded? // 49 MongoDB.Timestamp.prototype.clone = function () { // 50 // Timestamps should be immutable. // 51 return this; // 52 }; // 53 // 54 var makeMongoLegal = function (name) { return "EJSON" + name; }; // 55 var unmakeMongoLegal = function (name) { return name.substr(5); }; // 56 // 57 var replaceMongoAtomWithMeteor = function (document) { // 58 if (document instanceof MongoDB.Binary) { // 59 var buffer = document.value(true); // 60 return new Uint8Array(buffer); // 61 } // 62 if (document instanceof MongoDB.ObjectID) { // 63 return new Mongo.ObjectID(document.toHexString()); // 64 } // 65 if (document["EJSON$type"] && document["EJSON$value"] // 66 && _.size(document) === 2) { // 67 return EJSON.fromJSONValue(replaceNames(unmakeMongoLegal, document)); // 68 } // 69 if (document instanceof MongoDB.Timestamp) { // 70 // For now, the Meteor representation of a Mongo timestamp type (not a date! // 71 // this is a weird internal thing used in the oplog!) is the same as the // 72 // Mongo representation. We need to do this explicitly or else we would do a // 73 // structural clone and lose the prototype. // 74 return document; // 75 } // 76 return undefined; // 77 }; // 78 // 79 var replaceMeteorAtomWithMongo = function (document) { // 80 if (EJSON.isBinary(document)) { // 81 // This does more copies than we'd like, but is necessary because // 82 // MongoDB.BSON only looks like it takes a Uint8Array (and doesn't actually // 83 // serialize it correctly). // 84 return new MongoDB.Binary(new Buffer(document)); // 85 } // 86 if (document instanceof Mongo.ObjectID) { // 87 return new MongoDB.ObjectID(document.toHexString()); // 88 } // 89 if (document instanceof MongoDB.Timestamp) { // 90 // For now, the Meteor representation of a Mongo timestamp type (not a date! // 91 // this is a weird internal thing used in the oplog!) is the same as the // 92 // Mongo representation. We need to do this explicitly or else we would do a // 93 // structural clone and lose the prototype. // 94 return document; // 95 } // 96 if (EJSON._isCustomType(document)) { // 97 return replaceNames(makeMongoLegal, EJSON.toJSONValue(document)); // 98 } // 99 // It is not ordinarily possible to stick dollar-sign keys into mongo // 100 // so we don't bother checking for things that need escaping at this time. // 101 return undefined; // 102 }; // 103 // 104 var replaceTypes = function (document, atomTransformer) { // 105 if (typeof document !== 'object' || document === null) // 106 return document; // 107 // 108 var replacedTopLevelAtom = atomTransformer(document); // 109 if (replacedTopLevelAtom !== undefined) // 110 return replacedTopLevelAtom; // 111 // 112 var ret = document; // 113 _.each(document, function (val, key) { // 114 var valReplaced = replaceTypes(val, atomTransformer); // 115 if (val !== valReplaced) { // 116 // Lazy clone. Shallow copy. // 117 if (ret === document) // 118 ret = _.clone(document); // 119 ret[key] = valReplaced; // 120 } // 121 }); // 122 return ret; // 123 }; // 124 // 125 // 126 MongoConnection = function (url, options) { // 127 var self = this; // 128 options = options || {}; // 129 self._observeMultiplexers = {}; // 130 self._onFailoverHook = new Hook; // 131 // 132 var mongoOptions = {db: {safe: true}, server: {}, replSet: {}}; // 133 // 134 // Set autoReconnect to true, unless passed on the URL. Why someone // 135 // would want to set autoReconnect to false, I'm not really sure, but // 136 // keeping this for backwards compatibility for now. // 137 if (!(/[\?&]auto_?[rR]econnect=/.test(url))) { // 138 mongoOptions.server.auto_reconnect = true; // 139 } // 140 // 141 // Disable the native parser by default, unless specifically enabled // 142 // in the mongo URL. // 143 // - The native driver can cause errors which normally would be // 144 // thrown, caught, and handled into segfaults that take down the // 145 // whole app. // 146 // - Binary modules don't yet work when you bundle and move the bundle // 147 // to a different platform (aka deploy) // 148 // We should revisit this after binary npm module support lands. // 149 if (!(/[\?&]native_?[pP]arser=/.test(url))) { // 150 mongoOptions.db.native_parser = false; // 151 } // 152 // 153 // XXX maybe we should have a better way of allowing users to configure the // 154 // underlying Mongo driver // 155 if (_.has(options, 'poolSize')) { // 156 // If we just set this for "server", replSet will override it. If we just // 157 // set it for replSet, it will be ignored if we're not using a replSet. // 158 mongoOptions.server.poolSize = options.poolSize; // 159 mongoOptions.replSet.poolSize = options.poolSize; // 160 } // 161 // 162 self.db = null; // 163 // We keep track of the ReplSet's primary, so that we can trigger hooks when // 164 // it changes. The Node driver's joined callback seems to fire way too // 165 // often, which is why we need to track it ourselves. // 166 self._primary = null; // 167 self._oplogHandle = null; // 168 self._docFetcher = null; // 169 // 170 // 171 var connectFuture = new Future; // 172 MongoDB.connect( // 173 url, // 174 mongoOptions, // 175 Meteor.bindEnvironment( // 176 function (err, db) { // 177 if (err) { // 178 throw err; // 179 } // 180 // 181 // First, figure out what the current primary is, if any. // 182 if (db.serverConfig._state.master) // 183 self._primary = db.serverConfig._state.master.name; // 184 db.serverConfig.on( // 185 'joined', Meteor.bindEnvironment(function (kind, doc) { // 186 if (kind === 'primary') { // 187 if (doc.primary !== self._primary) { // 188 self._primary = doc.primary; // 189 self._onFailoverHook.each(function (callback) { // 190 callback(); // 191 return true; // 192 }); // 193 } // 194 } else if (doc.me === self._primary) { // 195 // The thing we thought was primary is now something other than // 196 // primary. Forget that we thought it was primary. (This means // 197 // that if a server stops being primary and then starts being // 198 // primary again without another server becoming primary in the // 199 // middle, we'll correctly count it as a failover.) // 200 self._primary = null; // 201 } // 202 })); // 203 // 204 // Allow the constructor to return. // 205 connectFuture['return'](db); // 206 }, // 207 connectFuture.resolver() // onException // 208 ) // 209 ); // 210 // 211 // Wait for the connection to be successful; throws on failure. // 212 self.db = connectFuture.wait(); // 213 // 214 if (options.oplogUrl && ! Package['disable-oplog']) { // 215 self._oplogHandle = new OplogHandle(options.oplogUrl, self.db.databaseName); // 216 self._docFetcher = new DocFetcher(self); // 217 } // 218 }; // 219 // 220 MongoConnection.prototype.close = function() { // 221 var self = this; // 222 // 223 if (! self.db) // 224 throw Error("close called before Connection created?"); // 225 // 226 // XXX probably untested // 227 var oplogHandle = self._oplogHandle; // 228 self._oplogHandle = null; // 229 if (oplogHandle) // 230 oplogHandle.stop(); // 231 // 232 // Use Future.wrap so that errors get thrown. This happens to // 233 // work even outside a fiber since the 'close' method is not // 234 // actually asynchronous. // 235 Future.wrap(_.bind(self.db.close, self.db))(true).wait(); // 236 }; // 237 // 238 // Returns the Mongo Collection object; may yield. // 239 MongoConnection.prototype.rawCollection = function (collectionName) { // 240 var self = this; // 241 // 242 if (! self.db) // 243 throw Error("rawCollection called before Connection created?"); // 244 // 245 var future = new Future; // 246 self.db.collection(collectionName, future.resolver()); // 247 return future.wait(); // 248 }; // 249 // 250 MongoConnection.prototype._createCappedCollection = function ( // 251 collectionName, byteSize, maxDocuments) { // 252 var self = this; // 253 // 254 if (! self.db) // 255 throw Error("_createCappedCollection called before Connection created?"); // 256 // 257 var future = new Future(); // 258 self.db.createCollection( // 259 collectionName, // 260 { capped: true, size: byteSize, max: maxDocuments }, // 261 future.resolver()); // 262 future.wait(); // 263 }; // 264 // 265 // This should be called synchronously with a write, to create a // 266 // transaction on the current write fence, if any. After we can read // 267 // the write, and after observers have been notified (or at least, // 268 // after the observer notifiers have added themselves to the write // 269 // fence), you should call 'committed()' on the object returned. // 270 MongoConnection.prototype._maybeBeginWrite = function () { // 271 var self = this; // 272 var fence = DDPServer._CurrentWriteFence.get(); // 273 if (fence) // 274 return fence.beginWrite(); // 275 else // 276 return {committed: function () {}}; // 277 }; // 278 // 279 // Internal interface: adds a callback which is called when the Mongo primary // 280 // changes. Returns a stop handle. // 281 MongoConnection.prototype._onFailover = function (callback) { // 282 return this._onFailoverHook.register(callback); // 283 }; // 284 // 285 // 286 //////////// Public API ////////// // 287 // 288 // The write methods block until the database has confirmed the write (it may // 289 // not be replicated or stable on disk, but one server has confirmed it) if no // 290 // callback is provided. If a callback is provided, then they call the callback // 291 // when the write is confirmed. They return nothing on success, and raise an // 292 // exception on failure. // 293 // // 294 // After making a write (with insert, update, remove), observers are // 295 // notified asynchronously. If you want to receive a callback once all // 296 // of the observer notifications have landed for your write, do the // 297 // writes inside a write fence (set DDPServer._CurrentWriteFence to a new // 298 // _WriteFence, and then set a callback on the write fence.) // 299 // // 300 // Since our execution environment is single-threaded, this is // 301 // well-defined -- a write "has been made" if it's returned, and an // 302 // observer "has been notified" if its callback has returned. // 303 // 304 var writeCallback = function (write, refresh, callback) { // 305 return function (err, result) { // 306 if (! err) { // 307 // XXX We don't have to run this on error, right? // 308 try { // 309 refresh(); // 310 } catch (refreshErr) { // 311 if (callback) { // 312 callback(refreshErr); // 313 return; // 314 } else { // 315 throw refreshErr; // 316 } // 317 } // 318 } // 319 write.committed(); // 320 if (callback) // 321 callback(err, result); // 322 else if (err) // 323 throw err; // 324 }; // 325 }; // 326 // 327 var bindEnvironmentForWrite = function (callback) { // 328 return Meteor.bindEnvironment(callback, "Mongo write"); // 329 }; // 330 // 331 MongoConnection.prototype._insert = function (collection_name, document, // 332 callback) { // 333 var self = this; // 334 // 335 var sendError = function (e) { // 336 if (callback) // 337 return callback(e); // 338 throw e; // 339 }; // 340 // 341 if (collection_name === "___meteor_failure_test_collection") { // 342 var e = new Error("Failure test"); // 343 e.expected = true; // 344 sendError(e); // 345 return; // 346 } // 347 // 348 if (!(LocalCollection._isPlainObject(document) && // 349 !EJSON._isCustomType(document))) { // 350 sendError(new Error( // 351 "Only plain objects may be inserted into MongoDB")); // 352 return; // 353 } // 354 // 355 var write = self._maybeBeginWrite(); // 356 var refresh = function () { // 357 Meteor.refresh({collection: collection_name, id: document._id }); // 358 }; // 359 callback = bindEnvironmentForWrite(writeCallback(write, refresh, callback)); // 360 try { // 361 var collection = self.rawCollection(collection_name); // 362 collection.insert(replaceTypes(document, replaceMeteorAtomWithMongo), // 363 {safe: true}, callback); // 364 } catch (e) { // 365 write.committed(); // 366 throw e; // 367 } // 368 }; // 369 // 370 // Cause queries that may be affected by the selector to poll in this write // 371 // fence. // 372 MongoConnection.prototype._refresh = function (collectionName, selector) { // 373 var self = this; // 374 var refreshKey = {collection: collectionName}; // 375 // If we know which documents we're removing, don't poll queries that are // 376 // specific to other documents. (Note that multiple notifications here should // 377 // not cause multiple polls, since all our listener is doing is enqueueing a // 378 // poll.) // 379 var specificIds = LocalCollection._idsMatchedBySelector(selector); // 380 if (specificIds) { // 381 _.each(specificIds, function (id) { // 382 Meteor.refresh(_.extend({id: id}, refreshKey)); // 383 }); // 384 } else { // 385 Meteor.refresh(refreshKey); // 386 } // 387 }; // 388 // 389 MongoConnection.prototype._remove = function (collection_name, selector, // 390 callback) { // 391 var self = this; // 392 // 393 if (collection_name === "___meteor_failure_test_collection") { // 394 var e = new Error("Failure test"); // 395 e.expected = true; // 396 if (callback) // 397 return callback(e); // 398 else // 399 throw e; // 400 } // 401 // 402 var write = self._maybeBeginWrite(); // 403 var refresh = function () { // 404 self._refresh(collection_name, selector); // 405 }; // 406 callback = bindEnvironmentForWrite(writeCallback(write, refresh, callback)); // 407 // 408 try { // 409 var collection = self.rawCollection(collection_name); // 410 collection.remove(replaceTypes(selector, replaceMeteorAtomWithMongo), // 411 {safe: true}, callback); // 412 } catch (e) { // 413 write.committed(); // 414 throw e; // 415 } // 416 }; // 417 // 418 MongoConnection.prototype._dropCollection = function (collectionName, cb) { // 419 var self = this; // 420 // 421 var write = self._maybeBeginWrite(); // 422 var refresh = function () { // 423 Meteor.refresh({collection: collectionName, id: null, // 424 dropCollection: true}); // 425 }; // 426 cb = bindEnvironmentForWrite(writeCallback(write, refresh, cb)); // 427 // 428 try { // 429 var collection = self.rawCollection(collectionName); // 430 collection.drop(cb); // 431 } catch (e) { // 432 write.committed(); // 433 throw e; // 434 } // 435 }; // 436 // 437 // For testing only. Slightly better than `c.rawDatabase().dropDatabase()` // 438 // because it lets the test's fence wait for it to be complete. // 439 MongoConnection.prototype._dropDatabase = function (cb) { // 440 var self = this; // 441 // 442 var write = self._maybeBeginWrite(); // 443 var refresh = function () { // 444 Meteor.refresh({ dropDatabase: true }); // 445 }; // 446 cb = bindEnvironmentForWrite(writeCallback(write, refresh, cb)); // 447 // 448 try { // 449 self.db.dropDatabase(cb); // 450 } catch (e) { // 451 write.committed(); // 452 throw e; // 453 } // 454 }; // 455 // 456 MongoConnection.prototype._update = function (collection_name, selector, mod, // 457 options, callback) { // 458 var self = this; // 459 // 460 if (! callback && options instanceof Function) { // 461 callback = options; // 462 options = null; // 463 } // 464 // 465 if (collection_name === "___meteor_failure_test_collection") { // 466 var e = new Error("Failure test"); // 467 e.expected = true; // 468 if (callback) // 469 return callback(e); // 470 else // 471 throw e; // 472 } // 473 // 474 // explicit safety check. null and undefined can crash the mongo // 475 // driver. Although the node driver and minimongo do 'support' // 476 // non-object modifier in that they don't crash, they are not // 477 // meaningful operations and do not do anything. Defensively throw an // 478 // error here. // 479 if (!mod || typeof mod !== 'object') // 480 throw new Error("Invalid modifier. Modifier must be an object."); // 481 // 482 if (!(LocalCollection._isPlainObject(mod) && // 483 !EJSON._isCustomType(mod))) { // 484 throw new Error( // 485 "Only plain objects may be used as replacement" + // 486 " documents in MongoDB"); // 487 return; // 488 } // 489 // 490 if (!options) options = {}; // 491 // 492 var write = self._maybeBeginWrite(); // 493 var refresh = function () { // 494 self._refresh(collection_name, selector); // 495 }; // 496 callback = writeCallback(write, refresh, callback); // 497 try { // 498 var collection = self.rawCollection(collection_name); // 499 var mongoOpts = {safe: true}; // 500 // explictly enumerate options that minimongo supports // 501 if (options.upsert) mongoOpts.upsert = true; // 502 if (options.multi) mongoOpts.multi = true; // 503 // Lets you get a more more full result from MongoDB. Use with caution: // 504 // might not work with C.upsert (as opposed to C.update({upsert:true}) or // 505 // with simulated upsert. // 506 if (options.fullResult) mongoOpts.fullResult = true; // 507 // 508 var mongoSelector = replaceTypes(selector, replaceMeteorAtomWithMongo); // 509 var mongoMod = replaceTypes(mod, replaceMeteorAtomWithMongo); // 510 // 511 var isModify = isModificationMod(mongoMod); // 512 var knownId = selector._id || mod._id; // 513 // 514 if (options._forbidReplace && ! isModify) { // 515 var e = new Error("Invalid modifier. Replacements are forbidden."); // 516 if (callback) { // 517 return callback(e); // 518 } else { // 519 throw e; // 520 } // 521 } // 522 // 523 if (options.upsert && (! knownId) && options.insertedId) { // 524 // XXX If we know we're using Mongo 2.6 (and this isn't a replacement) // 525 // we should be able to just use $setOnInsert instead of this // 526 // simulated upsert thing. (We can't use $setOnInsert with // 527 // replacements because there's nowhere to write it, and $setOnInsert // 528 // can't set _id on Mongo 2.4.) // 529 // // 530 // Also, in the future we could do a real upsert for the mongo id // 531 // generation case, if the the node mongo driver gives us back the id // 532 // of the upserted doc (which our current version does not). // 533 // // 534 // For more context, see // 535 // https://github.com/meteor/meteor/issues/2278#issuecomment-64252706 // 536 simulateUpsertWithInsertedId( // 537 collection, mongoSelector, mongoMod, // 538 isModify, options, // 539 // This callback does not need to be bindEnvironment'ed because // 540 // simulateUpsertWithInsertedId() wraps it and then passes it through // 541 // bindEnvironmentForWrite. // 542 function (err, result) { // 543 // If we got here via a upsert() call, then options._returnObject will // 544 // be set and we should return the whole object. Otherwise, we should // 545 // just return the number of affected docs to match the mongo API. // 546 if (result && ! options._returnObject) // 547 callback(err, result.numberAffected); // 548 else // 549 callback(err, result); // 550 } // 551 ); // 552 } else { // 553 collection.update( // 554 mongoSelector, mongoMod, mongoOpts, // 555 bindEnvironmentForWrite(function (err, result, extra) { // 556 if (! err) { // 557 if (result && options._returnObject) { // 558 result = { numberAffected: result }; // 559 // If this was an upsert() call, and we ended up // 560 // inserting a new doc and we know its id, then // 561 // return that id as well. // 562 if (options.upsert && knownId && // 563 ! extra.updatedExisting) // 564 result.insertedId = knownId; // 565 } // 566 } // 567 callback(err, result); // 568 })); // 569 } // 570 } catch (e) { // 571 write.committed(); // 572 throw e; // 573 } // 574 }; // 575 // 576 var isModificationMod = function (mod) { // 577 var isReplace = false; // 578 var isModify = false; // 579 for (var k in mod) { // 580 if (k.substr(0, 1) === '$') { // 581 isModify = true; // 582 } else { // 583 isReplace = true; // 584 } // 585 } // 586 if (isModify && isReplace) { // 587 throw new Error( // 588 "Update parameter cannot have both modifier and non-modifier fields."); // 589 } // 590 return isModify; // 591 }; // 592 // 593 var NUM_OPTIMISTIC_TRIES = 3; // 594 // 595 // exposed for testing // 596 MongoConnection._isCannotChangeIdError = function (err) { // 597 // First check for what this error looked like in Mongo 2.4. Either of these // 598 // checks should work, but just to be safe... // 599 if (err.code === 13596) // 600 return true; // 601 if (err.err.indexOf("cannot change _id of a document") === 0) // 602 return true; // 603 // 604 // Now look for what it looks like in Mongo 2.6. We don't use the error code // 605 // here, because the error code we observed it producing (16837) appears to be // 606 // a far more generic error code based on examining the source. // 607 if (err.err.indexOf("The _id field cannot be changed") === 0) // 608 return true; // 609 // 610 return false; // 611 }; // 612 // 613 var simulateUpsertWithInsertedId = function (collection, selector, mod, // 614 isModify, options, callback) { // 615 // STRATEGY: First try doing a plain update. If it affected 0 documents, // 616 // then without affecting the database, we know we should probably do an // 617 // insert. We then do a *conditional* insert that will fail in the case // 618 // of a race condition. This conditional insert is actually an // 619 // upsert-replace with an _id, which will never successfully update an // 620 // existing document. If this upsert fails with an error saying it // 621 // couldn't change an existing _id, then we know an intervening write has // 622 // caused the query to match something. We go back to step one and repeat. // 623 // Like all "optimistic write" schemes, we rely on the fact that it's // 624 // unlikely our writes will continue to be interfered with under normal // 625 // circumstances (though sufficiently heavy contention with writers // 626 // disagreeing on the existence of an object will cause writes to fail // 627 // in theory). // 628 // 629 var newDoc; // 630 // Run this code up front so that it fails fast if someone uses // 631 // a Mongo update operator we don't support. // 632 if (isModify) { // 633 // We've already run replaceTypes/replaceMeteorAtomWithMongo on // 634 // selector and mod. We assume it doesn't matter, as far as // 635 // the behavior of modifiers is concerned, whether `_modify` // 636 // is run on EJSON or on mongo-converted EJSON. // 637 var selectorDoc = LocalCollection._removeDollarOperators(selector); // 638 // 639 newDoc = selectorDoc; // 640 // 641 // Convert dotted keys into objects. (Resolves issue #4522). // 642 _.each(newDoc, function (value, key) { // 643 var trail = key.split("."); // 644 // 645 if (trail.length > 1) { // 646 //Key is dotted. Convert it into an object. // 647 delete newDoc[key]; // 648 // 649 var obj = newDoc, // 650 leaf = trail.pop(); // 651 // 652 // XXX It is not quite certain what should be done if there are clashing // 653 // keys on the trail of the dotted key. For now we will just override it // 654 // It wouldn't be a very sane query in the first place, but should look // 655 // up what mongo does in this case. // 656 // 657 while ((key = trail.shift())) { // 658 if (typeof obj[key] !== "object") { // 659 obj[key] = {}; // 660 } // 661 // 662 obj = obj[key]; // 663 } // 664 // 665 obj[leaf] = value; // 666 } // 667 }); // 668 // 669 LocalCollection._modify(newDoc, mod, {isInsert: true}); // 670 } else { // 671 newDoc = mod; // 672 } // 673 // 674 var insertedId = options.insertedId; // must exist // 675 var mongoOptsForUpdate = { // 676 safe: true, // 677 multi: options.multi // 678 }; // 679 var mongoOptsForInsert = { // 680 safe: true, // 681 upsert: true // 682 }; // 683 // 684 var tries = NUM_OPTIMISTIC_TRIES; // 685 // 686 var doUpdate = function () { // 687 tries--; // 688 if (! tries) { // 689 callback(new Error("Upsert failed after " + NUM_OPTIMISTIC_TRIES + " tries.")); // 690 } else { // 691 collection.update(selector, mod, mongoOptsForUpdate, // 692 bindEnvironmentForWrite(function (err, result) { // 693 if (err) // 694 callback(err); // 695 else if (result) // 696 callback(null, { // 697 numberAffected: result // 698 }); // 699 else // 700 doConditionalInsert(); // 701 })); // 702 } // 703 }; // 704 // 705 var doConditionalInsert = function () { // 706 var replacementWithId = _.extend( // 707 replaceTypes({_id: insertedId}, replaceMeteorAtomWithMongo), // 708 newDoc); // 709 collection.update(selector, replacementWithId, mongoOptsForInsert, // 710 bindEnvironmentForWrite(function (err, result) { // 711 if (err) { // 712 // figure out if this is a // 713 // "cannot change _id of document" error, and // 714 // if so, try doUpdate() again, up to 3 times. // 715 if (MongoConnection._isCannotChangeIdError(err)) { // 716 doUpdate(); // 717 } else { // 718 callback(err); // 719 } // 720 } else { // 721 callback(null, { // 722 numberAffected: result, // 723 insertedId: insertedId // 724 }); // 725 } // 726 })); // 727 }; // 728 // 729 doUpdate(); // 730 }; // 731 // 732 _.each(["insert", "update", "remove", "dropCollection", "dropDatabase"], function (method) { // 733 MongoConnection.prototype[method] = function (/* arguments */) { // 734 var self = this; // 735 return Meteor.wrapAsync(self["_" + method]).apply(self, arguments); // 736 }; // 737 }); // 738 // 739 // XXX MongoConnection.upsert() does not return the id of the inserted document // 740 // unless you set it explicitly in the selector or modifier (as a replacement // 741 // doc). // 742 MongoConnection.prototype.upsert = function (collectionName, selector, mod, // 743 options, callback) { // 744 var self = this; // 745 if (typeof options === "function" && ! callback) { // 746 callback = options; // 747 options = {}; // 748 } // 749 // 750 return self.update(collectionName, selector, mod, // 751 _.extend({}, options, { // 752 upsert: true, // 753 _returnObject: true // 754 }), callback); // 755 }; // 756 // 757 MongoConnection.prototype.find = function (collectionName, selector, options) { // 758 var self = this; // 759 // 760 if (arguments.length === 1) // 761 selector = {}; // 762 // 763 return new Cursor( // 764 self, new CursorDescription(collectionName, selector, options)); // 765 }; // 766 // 767 MongoConnection.prototype.findOne = function (collection_name, selector, // 768 options) { // 769 var self = this; // 770 if (arguments.length === 1) // 771 selector = {}; // 772 // 773 options = options || {}; // 774 options.limit = 1; // 775 return self.find(collection_name, selector, options).fetch()[0]; // 776 }; // 777 // 778 // We'll actually design an index API later. For now, we just pass through to // 779 // Mongo's, but make it synchronous. // 780 MongoConnection.prototype._ensureIndex = function (collectionName, index, // 781 options) { // 782 var self = this; // 783 // 784 // We expect this function to be called at startup, not from within a method, // 785 // so we don't interact with the write fence. // 786 var collection = self.rawCollection(collectionName); // 787 var future = new Future; // 788 var indexName = collection.ensureIndex(index, options, future.resolver()); // 789 future.wait(); // 790 }; // 791 MongoConnection.prototype._dropIndex = function (collectionName, index) { // 792 var self = this; // 793 // 794 // This function is only used by test code, not within a method, so we don't // 795 // interact with the write fence. // 796 var collection = self.rawCollection(collectionName); // 797 var future = new Future; // 798 var indexName = collection.dropIndex(index, future.resolver()); // 799 future.wait(); // 800 }; // 801 // 802 // CURSORS // 803 // 804 // There are several classes which relate to cursors: // 805 // // 806 // CursorDescription represents the arguments used to construct a cursor: // 807 // collectionName, selector, and (find) options. Because it is used as a key // 808 // for cursor de-dup, everything in it should either be JSON-stringifiable or // 809 // not affect observeChanges output (eg, options.transform functions are not // 810 // stringifiable but do not affect observeChanges). // 811 // // 812 // SynchronousCursor is a wrapper around a MongoDB cursor // 813 // which includes fully-synchronous versions of forEach, etc. // 814 // // 815 // Cursor is the cursor object returned from find(), which implements the // 816 // documented Mongo.Collection cursor API. It wraps a CursorDescription and a // 817 // SynchronousCursor (lazily: it doesn't contact Mongo until you call a method // 818 // like fetch or forEach on it). // 819 // // 820 // ObserveHandle is the "observe handle" returned from observeChanges. It has a // 821 // reference to an ObserveMultiplexer. // 822 // // 823 // ObserveMultiplexer allows multiple identical ObserveHandles to be driven by a // 824 // single observe driver. // 825 // // 826 // There are two "observe drivers" which drive ObserveMultiplexers: // 827 // - PollingObserveDriver caches the results of a query and reruns it when // 828 // necessary. // 829 // - OplogObserveDriver follows the Mongo operation log to directly observe // 830 // database changes. // 831 // Both implementations follow the same simple interface: when you create them, // 832 // they start sending observeChanges callbacks (and a ready() invocation) to // 833 // their ObserveMultiplexer, and you stop them by calling their stop() method. // 834 // 835 CursorDescription = function (collectionName, selector, options) { // 836 var self = this; // 837 self.collectionName = collectionName; // 838 self.selector = Mongo.Collection._rewriteSelector(selector); // 839 self.options = options || {}; // 840 }; // 841 // 842 Cursor = function (mongo, cursorDescription) { // 843 var self = this; // 844 // 845 self._mongo = mongo; // 846 self._cursorDescription = cursorDescription; // 847 self._synchronousCursor = null; // 848 }; // 849 // 850 _.each(['forEach', 'map', 'fetch', 'count'], function (method) { // 851 Cursor.prototype[method] = function () { // 852 var self = this; // 853 // 854 // You can only observe a tailable cursor. // 855 if (self._cursorDescription.options.tailable) // 856 throw new Error("Cannot call " + method + " on a tailable cursor"); // 857 // 858 if (!self._synchronousCursor) { // 859 self._synchronousCursor = self._mongo._createSynchronousCursor( // 860 self._cursorDescription, { // 861 // Make sure that the "self" argument to forEach/map callbacks is the // 862 // Cursor, not the SynchronousCursor. // 863 selfForIteration: self, // 864 useTransform: true // 865 }); // 866 } // 867 // 868 return self._synchronousCursor[method].apply( // 869 self._synchronousCursor, arguments); // 870 }; // 871 }); // 872 // 873 // Since we don't actually have a "nextObject" interface, there's really no // 874 // reason to have a "rewind" interface. All it did was make multiple calls // 875 // to fetch/map/forEach return nothing the second time. // 876 // XXX COMPAT WITH 0.8.1 // 877 Cursor.prototype.rewind = function () { // 878 }; // 879 // 880 Cursor.prototype.getTransform = function () { // 881 return this._cursorDescription.options.transform; // 882 }; // 883 // 884 // When you call Meteor.publish() with a function that returns a Cursor, we need // 885 // to transmute it into the equivalent subscription. This is the function that // 886 // does that. // 887 // 888 Cursor.prototype._publishCursor = function (sub) { // 889 var self = this; // 890 var collection = self._cursorDescription.collectionName; // 891 return Mongo.Collection._publishCursor(self, sub, collection); // 892 }; // 893 // 894 // Used to guarantee that publish functions return at most one cursor per // 895 // collection. Private, because we might later have cursors that include // 896 // documents from multiple collections somehow. // 897 Cursor.prototype._getCollectionName = function () { // 898 var self = this; // 899 return self._cursorDescription.collectionName; // 900 } // 901 // 902 Cursor.prototype.observe = function (callbacks) { // 903 var self = this; // 904 return LocalCollection._observeFromObserveChanges(self, callbacks); // 905 }; // 906 // 907 Cursor.prototype.observeChanges = function (callbacks) { // 908 var self = this; // 909 var ordered = LocalCollection._observeChangesCallbacksAreOrdered(callbacks); // 910 return self._mongo._observeChanges( // 911 self._cursorDescription, ordered, callbacks); // 912 }; // 913 // 914 MongoConnection.prototype._createSynchronousCursor = function( // 915 cursorDescription, options) { // 916 var self = this; // 917 options = _.pick(options || {}, 'selfForIteration', 'useTransform'); // 918 // 919 var collection = self.rawCollection(cursorDescription.collectionName); // 920 var cursorOptions = cursorDescription.options; // 921 var mongoOptions = { // 922 sort: cursorOptions.sort, // 923 limit: cursorOptions.limit, // 924 skip: cursorOptions.skip // 925 }; // 926 // 927 // Do we want a tailable cursor (which only works on capped collections)? // 928 if (cursorOptions.tailable) { // 929 // We want a tailable cursor... // 930 mongoOptions.tailable = true; // 931 // ... and for the server to wait a bit if any getMore has no data (rather // 932 // than making us put the relevant sleeps in the client)... // 933 mongoOptions.awaitdata = true; // 934 // ... and to keep querying the server indefinitely rather than just 5 times // 935 // if there's no more data. // 936 mongoOptions.numberOfRetries = -1; // 937 // And if this is on the oplog collection and the cursor specifies a 'ts', // 938 // then set the undocumented oplog replay flag, which does a special scan to // 939 // find the first document (instead of creating an index on ts). This is a // 940 // very hard-coded Mongo flag which only works on the oplog collection and // 941 // only works with the ts field. // 942 if (cursorDescription.collectionName === OPLOG_COLLECTION && // 943 cursorDescription.selector.ts) { // 944 mongoOptions.oplogReplay = true; // 945 } // 946 } // 947 // 948 var dbCursor = collection.find( // 949 replaceTypes(cursorDescription.selector, replaceMeteorAtomWithMongo), // 950 cursorOptions.fields, mongoOptions); // 951 // 952 return new SynchronousCursor(dbCursor, cursorDescription, options); // 953 }; // 954 // 955 var SynchronousCursor = function (dbCursor, cursorDescription, options) { // 956 var self = this; // 957 options = _.pick(options || {}, 'selfForIteration', 'useTransform'); // 958 // 959 self._dbCursor = dbCursor; // 960 self._cursorDescription = cursorDescription; // 961 // The "self" argument passed to forEach/map callbacks. If we're wrapped // 962 // inside a user-visible Cursor, we want to provide the outer cursor! // 963 self._selfForIteration = options.selfForIteration || self; // 964 if (options.useTransform && cursorDescription.options.transform) { // 965 self._transform = LocalCollection.wrapTransform( // 966 cursorDescription.options.transform); // 967 } else { // 968 self._transform = null; // 969 } // 970 // 971 // Need to specify that the callback is the first argument to nextObject, // 972 // since otherwise when we try to call it with no args the driver will // 973 // interpret "undefined" first arg as an options hash and crash. // 974 self._synchronousNextObject = Future.wrap( // 975 dbCursor.nextObject.bind(dbCursor), 0); // 976 self._synchronousCount = Future.wrap(dbCursor.count.bind(dbCursor)); // 977 self._visitedIds = new LocalCollection._IdMap; // 978 }; // 979 // 980 _.extend(SynchronousCursor.prototype, { // 981 _nextObject: function () { // 982 var self = this; // 983 // 984 while (true) { // 985 var doc = self._synchronousNextObject().wait(); // 986 // 987 if (!doc) return null; // 988 doc = replaceTypes(doc, replaceMongoAtomWithMeteor); // 989 // 990 if (!self._cursorDescription.options.tailable && _.has(doc, '_id')) { // 991 // Did Mongo give us duplicate documents in the same cursor? If so, // 992 // ignore this one. (Do this before the transform, since transform might // 993 // return some unrelated value.) We don't do this for tailable cursors, // 994 // because we want to maintain O(1) memory usage. And if there isn't _id // 995 // for some reason (maybe it's the oplog), then we don't do this either. // 996 // (Be careful to do this for falsey but existing _id, though.) // 997 if (self._visitedIds.has(doc._id)) continue; // 998 self._visitedIds.set(doc._id, true); // 999 } // 1000 // 1001 if (self._transform) // 1002 doc = self._transform(doc); // 1003 // 1004 return doc; // 1005 } // 1006 }, // 1007 // 1008 forEach: function (callback, thisArg) { // 1009 var self = this; // 1010 // 1011 // Get back to the beginning. // 1012 self._rewind(); // 1013 // 1014 // We implement the loop ourself instead of using self._dbCursor.each, // 1015 // because "each" will call its callback outside of a fiber which makes it // 1016 // much more complex to make this function synchronous. // 1017 var index = 0; // 1018 while (true) { // 1019 var doc = self._nextObject(); // 1020 if (!doc) return; // 1021 callback.call(thisArg, doc, index++, self._selfForIteration); // 1022 } // 1023 }, // 1024 // 1025 // XXX Allow overlapping callback executions if callback yields. // 1026 map: function (callback, thisArg) { // 1027 var self = this; // 1028 var res = []; // 1029 self.forEach(function (doc, index) { // 1030 res.push(callback.call(thisArg, doc, index, self._selfForIteration)); // 1031 }); // 1032 return res; // 1033 }, // 1034 // 1035 _rewind: function () { // 1036 var self = this; // 1037 // 1038 // known to be synchronous // 1039 self._dbCursor.rewind(); // 1040 // 1041 self._visitedIds = new LocalCollection._IdMap; // 1042 }, // 1043 // 1044 // Mostly usable for tailable cursors. // 1045 close: function () { // 1046 var self = this; // 1047 // 1048 self._dbCursor.close(); // 1049 }, // 1050 // 1051 fetch: function () { // 1052 var self = this; // 1053 return self.map(_.identity); // 1054 }, // 1055 // 1056 count: function () { // 1057 var self = this; // 1058 return self._synchronousCount().wait(); // 1059 }, // 1060 // 1061 // This method is NOT wrapped in Cursor. // 1062 getRawObjects: function (ordered) { // 1063 var self = this; // 1064 if (ordered) { // 1065 return self.fetch(); // 1066 } else { // 1067 var results = new LocalCollection._IdMap; // 1068 self.forEach(function (doc) { // 1069 results.set(doc._id, doc); // 1070 }); // 1071 return results; // 1072 } // 1073 } // 1074 }); // 1075 // 1076 MongoConnection.prototype.tail = function (cursorDescription, docCallback) { // 1077 var self = this; // 1078 if (!cursorDescription.options.tailable) // 1079 throw new Error("Can only tail a tailable cursor"); // 1080 // 1081 var cursor = self._createSynchronousCursor(cursorDescription); // 1082 // 1083 var stopped = false; // 1084 var lastTS = undefined; // 1085 var loop = function () { // 1086 while (true) { // 1087 if (stopped) // 1088 return; // 1089 try { // 1090 var doc = cursor._nextObject(); // 1091 } catch (err) { // 1092 // There's no good way to figure out if this was actually an error // 1093 // from Mongo. Ah well. But either way, we need to retry the cursor // 1094 // (unless the failure was because the observe got stopped). // 1095 doc = null; // 1096 } // 1097 // Since cursor._nextObject can yield, we need to check again to see if // 1098 // we've been stopped before calling the callback. // 1099 if (stopped) // 1100 return; // 1101 if (doc) { // 1102 // If a tailable cursor contains a "ts" field, use it to recreate the // 1103 // cursor on error. ("ts" is a standard that Mongo uses internally for // 1104 // the oplog, and there's a special flag that lets you do binary search // 1105 // on it instead of needing to use an index.) // 1106 lastTS = doc.ts; // 1107 docCallback(doc); // 1108 } else { // 1109 var newSelector = _.clone(cursorDescription.selector); // 1110 if (lastTS) { // 1111 newSelector.ts = {$gt: lastTS}; // 1112 } // 1113 cursor = self._createSynchronousCursor(new CursorDescription( // 1114 cursorDescription.collectionName, // 1115 newSelector, // 1116 cursorDescription.options)); // 1117 // Mongo failover takes many seconds. Retry in a bit. (Without this // 1118 // setTimeout, we peg the CPU at 100% and never notice the actual // 1119 // failover. // 1120 Meteor.setTimeout(loop, 100); // 1121 break; // 1122 } // 1123 } // 1124 }; // 1125 // 1126 Meteor.defer(loop); // 1127 // 1128 return { // 1129 stop: function () { // 1130 stopped = true; // 1131 cursor.close(); // 1132 } // 1133 }; // 1134 }; // 1135 // 1136 MongoConnection.prototype._observeChanges = function ( // 1137 cursorDescription, ordered, callbacks) { // 1138 var self = this; // 1139 // 1140 if (cursorDescription.options.tailable) { // 1141 return self._observeChangesTailable(cursorDescription, ordered, callbacks); // 1142 } // 1143 // 1144 // You may not filter out _id when observing changes, because the id is a core // 1145 // part of the observeChanges API. // 1146 if (cursorDescription.options.fields && // 1147 (cursorDescription.options.fields._id === 0 || // 1148 cursorDescription.options.fields._id === false)) { // 1149 throw Error("You may not observe a cursor with {fields: {_id: 0}}"); // 1150 } // 1151 // 1152 var observeKey = JSON.stringify( // 1153 _.extend({ordered: ordered}, cursorDescription)); // 1154 // 1155 var multiplexer, observeDriver; // 1156 var firstHandle = false; // 1157 // 1158 // Find a matching ObserveMultiplexer, or create a new one. This next block is // 1159 // guaranteed to not yield (and it doesn't call anything that can observe a // 1160 // new query), so no other calls to this function can interleave with it. // 1161 Meteor._noYieldsAllowed(function () { // 1162 if (_.has(self._observeMultiplexers, observeKey)) { // 1163 multiplexer = self._observeMultiplexers[observeKey]; // 1164 } else { // 1165 firstHandle = true; // 1166 // Create a new ObserveMultiplexer. // 1167 multiplexer = new ObserveMultiplexer({ // 1168 ordered: ordered, // 1169 onStop: function () { // 1170 delete self._observeMultiplexers[observeKey]; // 1171 observeDriver.stop(); // 1172 } // 1173 }); // 1174 self._observeMultiplexers[observeKey] = multiplexer; // 1175 } // 1176 }); // 1177 // 1178 var observeHandle = new ObserveHandle(multiplexer, callbacks); // 1179 // 1180 if (firstHandle) { // 1181 var matcher, sorter; // 1182 var canUseOplog = _.all([ // 1183 function () { // 1184 // At a bare minimum, using the oplog requires us to have an oplog, to // 1185 // want unordered callbacks, and to not want a callback on the polls // 1186 // that won't happen. // 1187 return self._oplogHandle && !ordered && // 1188 !callbacks._testOnlyPollCallback; // 1189 }, function () { // 1190 // We need to be able to compile the selector. Fall back to polling for // 1191 // some newfangled $selector that minimongo doesn't support yet. // 1192 try { // 1193 matcher = new Minimongo.Matcher(cursorDescription.selector); // 1194 return true; // 1195 } catch (e) { // 1196 // XXX make all compilation errors MinimongoError or something // 1197 // so that this doesn't ignore unrelated exceptions // 1198 return false; // 1199 } // 1200 }, function () { // 1201 // ... and the selector itself needs to support oplog. // 1202 return OplogObserveDriver.cursorSupported(cursorDescription, matcher); // 1203 }, function () { // 1204 // And we need to be able to compile the sort, if any. eg, can't be // 1205 // {$natural: 1}. // 1206 if (!cursorDescription.options.sort) // 1207 return true; // 1208 try { // 1209 sorter = new Minimongo.Sorter(cursorDescription.options.sort, // 1210 { matcher: matcher }); // 1211 return true; // 1212 } catch (e) { // 1213 // XXX make all compilation errors MinimongoError or something // 1214 // so that this doesn't ignore unrelated exceptions // 1215 return false; // 1216 } // 1217 }], function (f) { return f(); }); // invoke each function // 1218 // 1219 var driverClass = canUseOplog ? OplogObserveDriver : PollingObserveDriver; // 1220 observeDriver = new driverClass({ // 1221 cursorDescription: cursorDescription, // 1222 mongoHandle: self, // 1223 multiplexer: multiplexer, // 1224 ordered: ordered, // 1225 matcher: matcher, // ignored by polling // 1226 sorter: sorter, // ignored by polling // 1227 _testOnlyPollCallback: callbacks._testOnlyPollCallback // 1228 }); // 1229 // 1230 // This field is only set for use in tests. // 1231 multiplexer._observeDriver = observeDriver; // 1232 } // 1233 // 1234 // Blocks until the initial adds have been sent. // 1235 multiplexer.addHandleAndSendInitialAdds(observeHandle); // 1236 // 1237 return observeHandle; // 1238 }; // 1239 // 1240 // Listen for the invalidation messages that will trigger us to poll the // 1241 // database for changes. If this selector specifies specific IDs, specify them // 1242 // here, so that updates to different specific IDs don't cause us to poll. // 1243 // listenCallback is the same kind of (notification, complete) callback passed // 1244 // to InvalidationCrossbar.listen. // 1245 // 1246 listenAll = function (cursorDescription, listenCallback) { // 1247 var listeners = []; // 1248 forEachTrigger(cursorDescription, function (trigger) { // 1249 listeners.push(DDPServer._InvalidationCrossbar.listen( // 1250 trigger, listenCallback)); // 1251 }); // 1252 // 1253 return { // 1254 stop: function () { // 1255 _.each(listeners, function (listener) { // 1256 listener.stop(); // 1257 }); // 1258 } // 1259 }; // 1260 }; // 1261 // 1262 forEachTrigger = function (cursorDescription, triggerCallback) { // 1263 var key = {collection: cursorDescription.collectionName}; // 1264 var specificIds = LocalCollection._idsMatchedBySelector( // 1265 cursorDescription.selector); // 1266 if (specificIds) { // 1267 _.each(specificIds, function (id) { // 1268 triggerCallback(_.extend({id: id}, key)); // 1269 }); // 1270 triggerCallback(_.extend({dropCollection: true, id: null}, key)); // 1271 } else { // 1272 triggerCallback(key); // 1273 } // 1274 // Everyone cares about the database being dropped. // 1275 triggerCallback({ dropDatabase: true }); // 1276 }; // 1277 // 1278 // observeChanges for tailable cursors on capped collections. // 1279 // // 1280 // Some differences from normal cursors: // 1281 // - Will never produce anything other than 'added' or 'addedBefore'. If you // 1282 // do update a document that has already been produced, this will not notice // 1283 // it. // 1284 // - If you disconnect and reconnect from Mongo, it will essentially restart // 1285 // the query, which will lead to duplicate results. This is pretty bad, // 1286 // but if you include a field called 'ts' which is inserted as // 1287 // new MongoInternals.MongoTimestamp(0, 0) (which is initialized to the // 1288 // current Mongo-style timestamp), we'll be able to find the place to // 1289 // restart properly. (This field is specifically understood by Mongo with an // 1290 // optimization which allows it to find the right place to start without // 1291 // an index on ts. It's how the oplog works.) // 1292 // - No callbacks are triggered synchronously with the call (there's no // 1293 // differentiation between "initial data" and "later changes"; everything // 1294 // that matches the query gets sent asynchronously). // 1295 // - De-duplication is not implemented. // 1296 // - Does not yet interact with the write fence. Probably, this should work by // 1297 // ignoring removes (which don't work on capped collections) and updates // 1298 // (which don't affect tailable cursors), and just keeping track of the ID // 1299 // of the inserted object, and closing the write fence once you get to that // 1300 // ID (or timestamp?). This doesn't work well if the document doesn't match // 1301 // the query, though. On the other hand, the write fence can close // 1302 // immediately if it does not match the query. So if we trust minimongo // 1303 // enough to accurately evaluate the query against the write fence, we // 1304 // should be able to do this... Of course, minimongo doesn't even support // 1305 // Mongo Timestamps yet. // 1306 MongoConnection.prototype._observeChangesTailable = function ( // 1307 cursorDescription, ordered, callbacks) { // 1308 var self = this; // 1309 // 1310 // Tailable cursors only ever call added/addedBefore callbacks, so it's an // 1311 // error if you didn't provide them. // 1312 if ((ordered && !callbacks.addedBefore) || // 1313 (!ordered && !callbacks.added)) { // 1314 throw new Error("Can't observe an " + (ordered ? "ordered" : "unordered") // 1315 + " tailable cursor without a " // 1316 + (ordered ? "addedBefore" : "added") + " callback"); // 1317 } // 1318 // 1319 return self.tail(cursorDescription, function (doc) { // 1320 var id = doc._id; // 1321 delete doc._id; // 1322 // The ts is an implementation detail. Hide it. // 1323 delete doc.ts; // 1324 if (ordered) { // 1325 callbacks.addedBefore(id, doc, null); // 1326 } else { // 1327 callbacks.added(id, doc); // 1328 } // 1329 }); // 1330 }; // 1331 // 1332 // XXX We probably need to find a better way to expose this. Right now // 1333 // it's only used by tests, but in fact you need it in normal // 1334 // operation to interact with capped collections. // 1335 MongoInternals.MongoTimestamp = MongoDB.Timestamp; // 1336 // 1337 MongoInternals.Connection = MongoConnection; // 1338 // 1339 //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// }).call(this); (function(){ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // // packages/mongo/oplog_tailing.js // // // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // var Future = Npm.require('fibers/future'); // 1 // 2 OPLOG_COLLECTION = 'oplog.rs'; // 3 // 4 var TOO_FAR_BEHIND = process.env.METEOR_OPLOG_TOO_FAR_BEHIND || 2000; // 5 // 6 var showTS = function (ts) { // 7 return "Timestamp(" + ts.getHighBits() + ", " + ts.getLowBits() + ")"; // 8 }; // 9 // 10 idForOp = function (op) { // 11 if (op.op === 'd') // 12 return op.o._id; // 13 else if (op.op === 'i') // 14 return op.o._id; // 15 else if (op.op === 'u') // 16 return op.o2._id; // 17 else if (op.op === 'c') // 18 throw Error("Operator 'c' doesn't supply an object with id: " + // 19 EJSON.stringify(op)); // 20 else // 21 throw Error("Unknown op: " + EJSON.stringify(op)); // 22 }; // 23 // 24 OplogHandle = function (oplogUrl, dbName) { // 25 var self = this; // 26 self._oplogUrl = oplogUrl; // 27 self._dbName = dbName; // 28 // 29 self._oplogLastEntryConnection = null; // 30 self._oplogTailConnection = null; // 31 self._stopped = false; // 32 self._tailHandle = null; // 33 self._readyFuture = new Future(); // 34 self._crossbar = new DDPServer._Crossbar({ // 35 factPackage: "mongo-livedata", factName: "oplog-watchers" // 36 }); // 37 self._baseOplogSelector = { // 38 ns: new RegExp('^' + Meteor._escapeRegExp(self._dbName) + '\\.'), // 39 $or: [ // 40 { op: {$in: ['i', 'u', 'd']} }, // 41 // drop collection // 42 { op: 'c', 'o.drop': { $exists: true } }, // 43 { op: 'c', 'o.dropDatabase': 1 }, // 44 ] // 45 }; // 46 // 47 // Data structures to support waitUntilCaughtUp(). Each oplog entry has a // 48 // MongoTimestamp object on it (which is not the same as a Date --- it's a // 49 // combination of time and an incrementing counter; see // 50 // http://docs.mongodb.org/manual/reference/bson-types/#timestamps). // 51 // // 52 // _catchingUpFutures is an array of {ts: MongoTimestamp, future: Future} // 53 // objects, sorted by ascending timestamp. _lastProcessedTS is the // 54 // MongoTimestamp of the last oplog entry we've processed. // 55 // // 56 // Each time we call waitUntilCaughtUp, we take a peek at the final oplog // 57 // entry in the db. If we've already processed it (ie, it is not greater than // 58 // _lastProcessedTS), waitUntilCaughtUp immediately returns. Otherwise, // 59 // waitUntilCaughtUp makes a new Future and inserts it along with the final // 60 // timestamp entry that it read, into _catchingUpFutures. waitUntilCaughtUp // 61 // then waits on that future, which is resolved once _lastProcessedTS is // 62 // incremented to be past its timestamp by the worker fiber. // 63 // // 64 // XXX use a priority queue or something else that's faster than an array // 65 self._catchingUpFutures = []; // 66 self._lastProcessedTS = null; // 67 // 68 self._onSkippedEntriesHook = new Hook({ // 69 debugPrintExceptions: "onSkippedEntries callback" // 70 }); // 71 // 72 self._entryQueue = new Meteor._DoubleEndedQueue(); // 73 self._workerActive = false; // 74 // 75 self._startTailing(); // 76 }; // 77 // 78 _.extend(OplogHandle.prototype, { // 79 stop: function () { // 80 var self = this; // 81 if (self._stopped) // 82 return; // 83 self._stopped = true; // 84 if (self._tailHandle) // 85 self._tailHandle.stop(); // 86 // XXX should close connections too // 87 }, // 88 onOplogEntry: function (trigger, callback) { // 89 var self = this; // 90 if (self._stopped) // 91 throw new Error("Called onOplogEntry on stopped handle!"); // 92 // 93 // Calling onOplogEntry requires us to wait for the tailing to be ready. // 94 self._readyFuture.wait(); // 95 // 96 var originalCallback = callback; // 97 callback = Meteor.bindEnvironment(function (notification) { // 98 // XXX can we avoid this clone by making oplog.js careful? // 99 originalCallback(EJSON.clone(notification)); // 100 }, function (err) { // 101 Meteor._debug("Error in oplog callback", err.stack); // 102 }); // 103 var listenHandle = self._crossbar.listen(trigger, callback); // 104 return { // 105 stop: function () { // 106 listenHandle.stop(); // 107 } // 108 }; // 109 }, // 110 // Register a callback to be invoked any time we skip oplog entries (eg, // 111 // because we are too far behind). // 112 onSkippedEntries: function (callback) { // 113 var self = this; // 114 if (self._stopped) // 115 throw new Error("Called onSkippedEntries on stopped handle!"); // 116 return self._onSkippedEntriesHook.register(callback); // 117 }, // 118 // Calls `callback` once the oplog has been processed up to a point that is // 119 // roughly "now": specifically, once we've processed all ops that are // 120 // currently visible. // 121 // XXX become convinced that this is actually safe even if oplogConnection // 122 // is some kind of pool // 123 waitUntilCaughtUp: function () { // 124 var self = this; // 125 if (self._stopped) // 126 throw new Error("Called waitUntilCaughtUp on stopped handle!"); // 127 // 128 // Calling waitUntilCaughtUp requries us to wait for the oplog connection to // 129 // be ready. // 130 self._readyFuture.wait(); // 131 // 132 while (!self._stopped) { // 133 // We need to make the selector at least as restrictive as the actual // 134 // tailing selector (ie, we need to specify the DB name) or else we might // 135 // find a TS that won't show up in the actual tail stream. // 136 try { // 137 var lastEntry = self._oplogLastEntryConnection.findOne( // 138 OPLOG_COLLECTION, self._baseOplogSelector, // 139 {fields: {ts: 1}, sort: {$natural: -1}}); // 140 break; // 141 } catch (e) { // 142 // During failover (eg) if we get an exception we should log and retry // 143 // instead of crashing. // 144 Meteor._debug("Got exception while reading last entry: " + e); // 145 Meteor._sleepForMs(100); // 146 } // 147 } // 148 // 149 if (self._stopped) // 150 return; // 151 // 152 if (!lastEntry) { // 153 // Really, nothing in the oplog? Well, we've processed everything. // 154 return; // 155 } // 156 // 157 var ts = lastEntry.ts; // 158 if (!ts) // 159 throw Error("oplog entry without ts: " + EJSON.stringify(lastEntry)); // 160 // 161 if (self._lastProcessedTS && ts.lessThanOrEqual(self._lastProcessedTS)) { // 162 // We've already caught up to here. // 163 return; // 164 } // 165 // 166 // 167 // Insert the future into our list. Almost always, this will be at the end, // 168 // but it's conceivable that if we fail over from one primary to another, // 169 // the oplog entries we see will go backwards. // 170 var insertAfter = self._catchingUpFutures.length; // 171 while (insertAfter - 1 > 0 // 172 && self._catchingUpFutures[insertAfter - 1].ts.greaterThan(ts)) { // 173 insertAfter--; // 174 } // 175 var f = new Future; // 176 self._catchingUpFutures.splice(insertAfter, 0, {ts: ts, future: f}); // 177 f.wait(); // 178 }, // 179 _startTailing: function () { // 180 var self = this; // 181 // First, make sure that we're talking to the local database. // 182 var mongodbUri = Npm.require('mongodb-uri'); // 183 if (mongodbUri.parse(self._oplogUrl).database !== 'local') { // 184 throw Error("$MONGO_OPLOG_URL must be set to the 'local' database of " + // 185 "a Mongo replica set"); // 186 } // 187 // 188 // We make two separate connections to Mongo. The Node Mongo driver // 189 // implements a naive round-robin connection pool: each "connection" is a // 190 // pool of several (5 by default) TCP connections, and each request is // 191 // rotated through the pools. Tailable cursor queries block on the server // 192 // until there is some data to return (or until a few seconds have // 193 // passed). So if the connection pool used for tailing cursors is the same // 194 // pool used for other queries, the other queries will be delayed by seconds // 195 // 1/5 of the time. // 196 // // 197 // The tail connection will only ever be running a single tail command, so // 198 // it only needs to make one underlying TCP connection. // 199 self._oplogTailConnection = new MongoConnection( // 200 self._oplogUrl, {poolSize: 1}); // 201 // XXX better docs, but: it's to get monotonic results // 202 // XXX is it safe to say "if there's an in flight query, just use its // 203 // results"? I don't think so but should consider that // 204 self._oplogLastEntryConnection = new MongoConnection( // 205 self._oplogUrl, {poolSize: 1}); // 206 // 207 // Now, make sure that there actually is a repl set here. If not, oplog // 208 // tailing won't ever find anything! // 209 var f = new Future; // 210 self._oplogLastEntryConnection.db.admin().command( // 211 { ismaster: 1 }, f.resolver()); // 212 var isMasterDoc = f.wait(); // 213 if (!(isMasterDoc && isMasterDoc.documents && isMasterDoc.documents[0] && // 214 isMasterDoc.documents[0].setName)) { // 215 throw Error("$MONGO_OPLOG_URL must be set to the 'local' database of " + // 216 "a Mongo replica set"); // 217 } // 218 // 219 // Find the last oplog entry. // 220 var lastOplogEntry = self._oplogLastEntryConnection.findOne( // 221 OPLOG_COLLECTION, {}, {sort: {$natural: -1}, fields: {ts: 1}}); // 222 // 223 var oplogSelector = _.clone(self._baseOplogSelector); // 224 if (lastOplogEntry) { // 225 // Start after the last entry that currently exists. // 226 oplogSelector.ts = {$gt: lastOplogEntry.ts}; // 227 // If there are any calls to callWhenProcessedLatest before any other // 228 // oplog entries show up, allow callWhenProcessedLatest to call its // 229 // callback immediately. // 230 self._lastProcessedTS = lastOplogEntry.ts; // 231 } // 232 // 233 var cursorDescription = new CursorDescription( // 234 OPLOG_COLLECTION, oplogSelector, {tailable: true}); // 235 // 236 self._tailHandle = self._oplogTailConnection.tail( // 237 cursorDescription, function (doc) { // 238 self._entryQueue.push(doc); // 239 self._maybeStartWorker(); // 240 } // 241 ); // 242 self._readyFuture.return(); // 243 }, // 244 // 245 _maybeStartWorker: function () { // 246 var self = this; // 247 if (self._workerActive) // 248 return; // 249 self._workerActive = true; // 250 Meteor.defer(function () { // 251 try { // 252 while (! self._stopped && ! self._entryQueue.isEmpty()) { // 253 // Are we too far behind? Just tell our observers that they need to // 254 // repoll, and drop our queue. // 255 if (self._entryQueue.length > TOO_FAR_BEHIND) { // 256 var lastEntry = self._entryQueue.pop(); // 257 self._entryQueue.clear(); // 258 // 259 self._onSkippedEntriesHook.each(function (callback) { // 260 callback(); // 261 return true; // 262 }); // 263 // 264 // Free any waitUntilCaughtUp() calls that were waiting for us to // 265 // pass something that we just skipped. // 266 self._setLastProcessedTS(lastEntry.ts); // 267 continue; // 268 } // 269 // 270 var doc = self._entryQueue.shift(); // 271 // 272 if (!(doc.ns && doc.ns.length > self._dbName.length + 1 && // 273 doc.ns.substr(0, self._dbName.length + 1) === // 274 (self._dbName + '.'))) { // 275 throw new Error("Unexpected ns"); // 276 } // 277 // 278 var trigger = {collection: doc.ns.substr(self._dbName.length + 1), // 279 dropCollection: false, // 280 dropDatabase: false, // 281 op: doc}; // 282 // 283 // Is it a special command and the collection name is hidden somewhere // 284 // in operator? // 285 if (trigger.collection === "$cmd") { // 286 if (doc.o.dropDatabase) { // 287 delete trigger.collection; // 288 trigger.dropDatabase = true; // 289 } else if (_.has(doc.o, 'drop')) { // 290 trigger.collection = doc.o.drop; // 291 trigger.dropCollection = true; // 292 trigger.id = null; // 293 } else { // 294 throw Error("Unknown command " + JSON.stringify(doc)); // 295 } // 296 } else { // 297 // All other ops have an id. // 298 trigger.id = idForOp(doc); // 299 } // 300 // 301 self._crossbar.fire(trigger); // 302 // 303 // Now that we've processed this operation, process pending // 304 // sequencers. // 305 if (!doc.ts) // 306 throw Error("oplog entry without ts: " + EJSON.stringify(doc)); // 307 self._setLastProcessedTS(doc.ts); // 308 } // 309 } finally { // 310 self._workerActive = false; // 311 } // 312 }); // 313 }, // 314 _setLastProcessedTS: function (ts) { // 315 var self = this; // 316 self._lastProcessedTS = ts; // 317 while (!_.isEmpty(self._catchingUpFutures) // 318 && self._catchingUpFutures[0].ts.lessThanOrEqual( // 319 self._lastProcessedTS)) { // 320 var sequencer = self._catchingUpFutures.shift(); // 321 sequencer.future.return(); // 322 } // 323 } // 324 }); // 325 // 326 //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// }).call(this); (function(){ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // // packages/mongo/observe_multiplex.js // // // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // var Future = Npm.require('fibers/future'); // 1 // 2 ObserveMultiplexer = function (options) { // 3 var self = this; // 4 // 5 if (!options || !_.has(options, 'ordered')) // 6 throw Error("must specified ordered"); // 7 // 8 Package.facts && Package.facts.Facts.incrementServerFact( // 9 "mongo-livedata", "observe-multiplexers", 1); // 10 // 11 self._ordered = options.ordered; // 12 self._onStop = options.onStop || function () {}; // 13 self._queue = new Meteor._SynchronousQueue(); // 14 self._handles = {}; // 15 self._readyFuture = new Future; // 16 self._cache = new LocalCollection._CachingChangeObserver({ // 17 ordered: options.ordered}); // 18 // Number of addHandleAndSendInitialAdds tasks scheduled but not yet // 19 // running. removeHandle uses this to know if it's time to call the onStop // 20 // callback. // 21 self._addHandleTasksScheduledButNotPerformed = 0; // 22 // 23 _.each(self.callbackNames(), function (callbackName) { // 24 self[callbackName] = function (/* ... */) { // 25 self._applyCallback(callbackName, _.toArray(arguments)); // 26 }; // 27 }); // 28 }; // 29 // 30 _.extend(ObserveMultiplexer.prototype, { // 31 addHandleAndSendInitialAdds: function (handle) { // 32 var self = this; // 33 // 34 // Check this before calling runTask (even though runTask does the same // 35 // check) so that we don't leak an ObserveMultiplexer on error by // 36 // incrementing _addHandleTasksScheduledButNotPerformed and never // 37 // decrementing it. // 38 if (!self._queue.safeToRunTask()) // 39 throw new Error( // 40 "Can't call observeChanges from an observe callback on the same query"); // 41 ++self._addHandleTasksScheduledButNotPerformed; // 42 // 43 Package.facts && Package.facts.Facts.incrementServerFact( // 44 "mongo-livedata", "observe-handles", 1); // 45 // 46 self._queue.runTask(function () { // 47 self._handles[handle._id] = handle; // 48 // Send out whatever adds we have so far (whether or not we the // 49 // multiplexer is ready). // 50 self._sendAdds(handle); // 51 --self._addHandleTasksScheduledButNotPerformed; // 52 }); // 53 // *outside* the task, since otherwise we'd deadlock // 54 self._readyFuture.wait(); // 55 }, // 56 // 57 // Remove an observe handle. If it was the last observe handle, call the // 58 // onStop callback; you cannot add any more observe handles after this. // 59 // // 60 // This is not synchronized with polls and handle additions: this means that // 61 // you can safely call it from within an observe callback, but it also means // 62 // that we have to be careful when we iterate over _handles. // 63 removeHandle: function (id) { // 64 var self = this; // 65 // 66 // This should not be possible: you can only call removeHandle by having // 67 // access to the ObserveHandle, which isn't returned to user code until the // 68 // multiplex is ready. // 69 if (!self._ready()) // 70 throw new Error("Can't remove handles until the multiplex is ready"); // 71 // 72 delete self._handles[id]; // 73 // 74 Package.facts && Package.facts.Facts.incrementServerFact( // 75 "mongo-livedata", "observe-handles", -1); // 76 // 77 if (_.isEmpty(self._handles) && // 78 self._addHandleTasksScheduledButNotPerformed === 0) { // 79 self._stop(); // 80 } // 81 }, // 82 _stop: function (options) { // 83 var self = this; // 84 options = options || {}; // 85 // 86 // It shouldn't be possible for us to stop when all our handles still // 87 // haven't been returned from observeChanges! // 88 if (! self._ready() && ! options.fromQueryError) // 89 throw Error("surprising _stop: not ready"); // 90 // 91 // Call stop callback (which kills the underlying process which sends us // 92 // callbacks and removes us from the connection's dictionary). // 93 self._onStop(); // 94 Package.facts && Package.facts.Facts.incrementServerFact( // 95 "mongo-livedata", "observe-multiplexers", -1); // 96 // 97 // Cause future addHandleAndSendInitialAdds calls to throw (but the onStop // 98 // callback should make our connection forget about us). // 99 self._handles = null; // 100 }, // 101 // 102 // Allows all addHandleAndSendInitialAdds calls to return, once all preceding // 103 // adds have been processed. Does not block. // 104 ready: function () { // 105 var self = this; // 106 self._queue.queueTask(function () { // 107 if (self._ready()) // 108 throw Error("can't make ObserveMultiplex ready twice!"); // 109 self._readyFuture.return(); // 110 }); // 111 }, // 112 // 113 // If trying to execute the query results in an error, call this. This is // 114 // intended for permanent errors, not transient network errors that could be // 115 // fixed. It should only be called before ready(), because if you called ready // 116 // that meant that you managed to run the query once. It will stop this // 117 // ObserveMultiplex and cause addHandleAndSendInitialAdds calls (and thus // 118 // observeChanges calls) to throw the error. // 119 queryError: function (err) { // 120 var self = this; // 121 self._queue.runTask(function () { // 122 if (self._ready()) // 123 throw Error("can't claim query has an error after it worked!"); // 124 self._stop({fromQueryError: true}); // 125 self._readyFuture.throw(err); // 126 }); // 127 }, // 128 // 129 // Calls "cb" once the effects of all "ready", "addHandleAndSendInitialAdds" // 130 // and observe callbacks which came before this call have been propagated to // 131 // all handles. "ready" must have already been called on this multiplexer. // 132 onFlush: function (cb) { // 133 var self = this; // 134 self._queue.queueTask(function () { // 135 if (!self._ready()) // 136 throw Error("only call onFlush on a multiplexer that will be ready"); // 137 cb(); // 138 }); // 139 }, // 140 callbackNames: function () { // 141 var self = this; // 142 if (self._ordered) // 143 return ["addedBefore", "changed", "movedBefore", "removed"]; // 144 else // 145 return ["added", "changed", "removed"]; // 146 }, // 147 _ready: function () { // 148 return this._readyFuture.isResolved(); // 149 }, // 150 _applyCallback: function (callbackName, args) { // 151 var self = this; // 152 self._queue.queueTask(function () { // 153 // If we stopped in the meantime, do nothing. // 154 if (!self._handles) // 155 return; // 156 // 157 // First, apply the change to the cache. // 158 // XXX We could make applyChange callbacks promise not to hang on to any // 159 // state from their arguments (assuming that their supplied callbacks // 160 // don't) and skip this clone. Currently 'changed' hangs on to state // 161 // though. // 162 self._cache.applyChange[callbackName].apply(null, EJSON.clone(args)); // 163 // 164 // If we haven't finished the initial adds, then we should only be getting // 165 // adds. // 166 if (!self._ready() && // 167 (callbackName !== 'added' && callbackName !== 'addedBefore')) { // 168 throw new Error("Got " + callbackName + " during initial adds"); // 169 } // 170 // 171 // Now multiplex the callbacks out to all observe handles. It's OK if // 172 // these calls yield; since we're inside a task, no other use of our queue // 173 // can continue until these are done. (But we do have to be careful to not // 174 // use a handle that got removed, because removeHandle does not use the // 175 // queue; thus, we iterate over an array of keys that we control.) // 176 _.each(_.keys(self._handles), function (handleId) { // 177 var handle = self._handles && self._handles[handleId]; // 178 if (!handle) // 179 return; // 180 var callback = handle['_' + callbackName]; // 181 // clone arguments so that callbacks can mutate their arguments // 182 callback && callback.apply(null, EJSON.clone(args)); // 183 }); // 184 }); // 185 }, // 186 // 187 // Sends initial adds to a handle. It should only be called from within a task // 188 // (the task that is processing the addHandleAndSendInitialAdds call). It // 189 // synchronously invokes the handle's added or addedBefore; there's no need to // 190 // flush the queue afterwards to ensure that the callbacks get out. // 191 _sendAdds: function (handle) { // 192 var self = this; // 193 if (self._queue.safeToRunTask()) // 194 throw Error("_sendAdds may only be called from within a task!"); // 195 var add = self._ordered ? handle._addedBefore : handle._added; // 196 if (!add) // 197 return; // 198 // note: docs may be an _IdMap or an OrderedDict // 199 self._cache.docs.forEach(function (doc, id) { // 200 if (!_.has(self._handles, handle._id)) // 201 throw Error("handle got removed before sending initial adds!"); // 202 var fields = EJSON.clone(doc); // 203 delete fields._id; // 204 if (self._ordered) // 205 add(id, fields, null); // we're going in order, so add at end // 206 else // 207 add(id, fields); // 208 }); // 209 } // 210 }); // 211 // 212 // 213 var nextObserveHandleId = 1; // 214 ObserveHandle = function (multiplexer, callbacks) { // 215 var self = this; // 216 // The end user is only supposed to call stop(). The other fields are // 217 // accessible to the multiplexer, though. // 218 self._multiplexer = multiplexer; // 219 _.each(multiplexer.callbackNames(), function (name) { // 220 if (callbacks[name]) { // 221 self['_' + name] = callbacks[name]; // 222 } else if (name === "addedBefore" && callbacks.added) { // 223 // Special case: if you specify "added" and "movedBefore", you get an // 224 // ordered observe where for some reason you don't get ordering data on // 225 // the adds. I dunno, we wrote tests for it, there must have been a // 226 // reason. // 227 self._addedBefore = function (id, fields, before) { // 228 callbacks.added(id, fields); // 229 }; // 230 } // 231 }); // 232 self._stopped = false; // 233 self._id = nextObserveHandleId++; // 234 }; // 235 ObserveHandle.prototype.stop = function () { // 236 var self = this; // 237 if (self._stopped) // 238 return; // 239 self._stopped = true; // 240 self._multiplexer.removeHandle(self._id); // 241 }; // 242 // 243 //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// }).call(this); (function(){ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // // packages/mongo/doc_fetcher.js // // // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // var Fiber = Npm.require('fibers'); // 1 var Future = Npm.require('fibers/future'); // 2 // 3 DocFetcher = function (mongoConnection) { // 4 var self = this; // 5 self._mongoConnection = mongoConnection; // 6 // Map from cache key -> [callback] // 7 self._callbacksForCacheKey = {}; // 8 }; // 9 // 10 _.extend(DocFetcher.prototype, { // 11 // Fetches document "id" from collectionName, returning it or null if not // 12 // found. // 13 // // 14 // If you make multiple calls to fetch() with the same cacheKey (a string), // 15 // DocFetcher may assume that they all return the same document. (It does // 16 // not check to see if collectionName/id match.) // 17 // // 18 // You may assume that callback is never called synchronously (and in fact // 19 // OplogObserveDriver does so). // 20 fetch: function (collectionName, id, cacheKey, callback) { // 21 var self = this; // 22 // 23 check(collectionName, String); // 24 // id is some sort of scalar // 25 check(cacheKey, String); // 26 // 27 // If there's already an in-progress fetch for this cache key, yield until // 28 // it's done and return whatever it returns. // 29 if (_.has(self._callbacksForCacheKey, cacheKey)) { // 30 self._callbacksForCacheKey[cacheKey].push(callback); // 31 return; // 32 } // 33 // 34 var callbacks = self._callbacksForCacheKey[cacheKey] = [callback]; // 35 // 36 Fiber(function () { // 37 try { // 38 var doc = self._mongoConnection.findOne( // 39 collectionName, {_id: id}) || null; // 40 // Return doc to all relevant callbacks. Note that this array can // 41 // continue to grow during callback excecution. // 42 while (!_.isEmpty(callbacks)) { // 43 // Clone the document so that the various calls to fetch don't return // 44 // objects that are intertwingled with each other. Clone before // 45 // popping the future, so that if clone throws, the error gets passed // 46 // to the next callback. // 47 var clonedDoc = EJSON.clone(doc); // 48 callbacks.pop()(null, clonedDoc); // 49 } // 50 } catch (e) { // 51 while (!_.isEmpty(callbacks)) { // 52 callbacks.pop()(e); // 53 } // 54 } finally { // 55 // XXX consider keeping the doc around for a period of time before // 56 // removing from the cache // 57 delete self._callbacksForCacheKey[cacheKey]; // 58 } // 59 }).run(); // 60 } // 61 }); // 62 // 63 MongoTest.DocFetcher = DocFetcher; // 64 // 65 //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// }).call(this); (function(){ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // // packages/mongo/polling_observe_driver.js // // // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // PollingObserveDriver = function (options) { // 1 var self = this; // 2 // 3 self._cursorDescription = options.cursorDescription; // 4 self._mongoHandle = options.mongoHandle; // 5 self._ordered = options.ordered; // 6 self._multiplexer = options.multiplexer; // 7 self._stopCallbacks = []; // 8 self._stopped = false; // 9 // 10 self._synchronousCursor = self._mongoHandle._createSynchronousCursor( // 11 self._cursorDescription); // 12 // 13 // previous results snapshot. on each poll cycle, diffs against // 14 // results drives the callbacks. // 15 self._results = null; // 16 // 17 // The number of _pollMongo calls that have been added to self._taskQueue but // 18 // have not started running. Used to make sure we never schedule more than one // 19 // _pollMongo (other than possibly the one that is currently running). It's // 20 // also used by _suspendPolling to pretend there's a poll scheduled. Usually, // 21 // it's either 0 (for "no polls scheduled other than maybe one currently // 22 // running") or 1 (for "a poll scheduled that isn't running yet"), but it can // 23 // also be 2 if incremented by _suspendPolling. // 24 self._pollsScheduledButNotStarted = 0; // 25 self._pendingWrites = []; // people to notify when polling completes // 26 // 27 // Make sure to create a separately throttled function for each // 28 // PollingObserveDriver object. // 29 self._ensurePollIsScheduled = _.throttle( // 30 self._unthrottledEnsurePollIsScheduled, 50 /* ms */); // 31 // 32 // XXX figure out if we still need a queue // 33 self._taskQueue = new Meteor._SynchronousQueue(); // 34 // 35 var listenersHandle = listenAll( // 36 self._cursorDescription, function (notification) { // 37 // When someone does a transaction that might affect us, schedule a poll // 38 // of the database. If that transaction happens inside of a write fence, // 39 // block the fence until we've polled and notified observers. // 40 var fence = DDPServer._CurrentWriteFence.get(); // 41 if (fence) // 42 self._pendingWrites.push(fence.beginWrite()); // 43 // Ensure a poll is scheduled... but if we already know that one is, // 44 // don't hit the throttled _ensurePollIsScheduled function (which might // 45 // lead to us calling it unnecessarily in 50ms). // 46 if (self._pollsScheduledButNotStarted === 0) // 47 self._ensurePollIsScheduled(); // 48 } // 49 ); // 50 self._stopCallbacks.push(function () { listenersHandle.stop(); }); // 51 // 52 // every once and a while, poll even if we don't think we're dirty, for // 53 // eventual consistency with database writes from outside the Meteor // 54 // universe. // 55 // // 56 // For testing, there's an undocumented callback argument to observeChanges // 57 // which disables time-based polling and gets called at the beginning of each // 58 // poll. // 59 if (options._testOnlyPollCallback) { // 60 self._testOnlyPollCallback = options._testOnlyPollCallback; // 61 } else { // 62 var intervalHandle = Meteor.setInterval( // 63 _.bind(self._ensurePollIsScheduled, self), 10 * 1000); // 64 self._stopCallbacks.push(function () { // 65 Meteor.clearInterval(intervalHandle); // 66 }); // 67 } // 68 // 69 // Make sure we actually poll soon! // 70 self._unthrottledEnsurePollIsScheduled(); // 71 // 72 Package.facts && Package.facts.Facts.incrementServerFact( // 73 "mongo-livedata", "observe-drivers-polling", 1); // 74 }; // 75 // 76 _.extend(PollingObserveDriver.prototype, { // 77 // This is always called through _.throttle (except once at startup). // 78 _unthrottledEnsurePollIsScheduled: function () { // 79 var self = this; // 80 if (self._pollsScheduledButNotStarted > 0) // 81 return; // 82 ++self._pollsScheduledButNotStarted; // 83 self._taskQueue.queueTask(function () { // 84 self._pollMongo(); // 85 }); // 86 }, // 87 // 88 // test-only interface for controlling polling. // 89 // // 90 // _suspendPolling blocks until any currently running and scheduled polls are // 91 // done, and prevents any further polls from being scheduled. (new // 92 // ObserveHandles can be added and receive their initial added callbacks, // 93 // though.) // 94 // // 95 // _resumePolling immediately polls, and allows further polls to occur. // 96 _suspendPolling: function() { // 97 var self = this; // 98 // Pretend that there's another poll scheduled (which will prevent // 99 // _ensurePollIsScheduled from queueing any more polls). // 100 ++self._pollsScheduledButNotStarted; // 101 // Now block until all currently running or scheduled polls are done. // 102 self._taskQueue.runTask(function() {}); // 103 // 104 // Confirm that there is only one "poll" (the fake one we're pretending to // 105 // have) scheduled. // 106 if (self._pollsScheduledButNotStarted !== 1) // 107 throw new Error("_pollsScheduledButNotStarted is " + // 108 self._pollsScheduledButNotStarted); // 109 }, // 110 _resumePolling: function() { // 111 var self = this; // 112 // We should be in the same state as in the end of _suspendPolling. // 113 if (self._pollsScheduledButNotStarted !== 1) // 114 throw new Error("_pollsScheduledButNotStarted is " + // 115 self._pollsScheduledButNotStarted); // 116 // Run a poll synchronously (which will counteract the // 117 // ++_pollsScheduledButNotStarted from _suspendPolling). // 118 self._taskQueue.runTask(function () { // 119 self._pollMongo(); // 120 }); // 121 }, // 122 // 123 _pollMongo: function () { // 124 var self = this; // 125 --self._pollsScheduledButNotStarted; // 126 // 127 if (self._stopped) // 128 return; // 129 // 130 var first = false; // 131 var oldResults = self._results; // 132 if (!oldResults) { // 133 first = true; // 134 // XXX maybe use OrderedDict instead? // 135 oldResults = self._ordered ? [] : new LocalCollection._IdMap; // 136 } // 137 // 138 self._testOnlyPollCallback && self._testOnlyPollCallback(); // 139 // 140 // Save the list of pending writes which this round will commit. // 141 var writesForCycle = self._pendingWrites; // 142 self._pendingWrites = []; // 143 // 144 // Get the new query results. (This yields.) // 145 try { // 146 var newResults = self._synchronousCursor.getRawObjects(self._ordered); // 147 } catch (e) { // 148 if (first && typeof(e.code) === 'number') { // 149 // This is an error document sent to us by mongod, not a connection // 150 // error generated by the client. And we've never seen this query work // 151 // successfully. Probably it's a bad selector or something, so we should // 152 // NOT retry. Instead, we should halt the observe (which ends up calling // 153 // `stop` on us). // 154 self._multiplexer.queryError( // 155 new Error( // 156 "Exception while polling query " + // 157 JSON.stringify(self._cursorDescription) + ": " + e.message)); // 158 return; // 159 } // 160 // 161 // getRawObjects can throw if we're having trouble talking to the // 162 // database. That's fine --- we will repoll later anyway. But we should // 163 // make sure not to lose track of this cycle's writes. // 164 // (It also can throw if there's just something invalid about this query; // 165 // unfortunately the ObserveDriver API doesn't provide a good way to // 166 // "cancel" the observe from the inside in this case. // 167 Array.prototype.push.apply(self._pendingWrites, writesForCycle); // 168 Meteor._debug("Exception while polling query " + // 169 JSON.stringify(self._cursorDescription) + ": " + e.stack); // 170 return; // 171 } // 172 // 173 // Run diffs. // 174 if (!self._stopped) { // 175 LocalCollection._diffQueryChanges( // 176 self._ordered, oldResults, newResults, self._multiplexer); // 177 } // 178 // 179 // Signals the multiplexer to allow all observeChanges calls that share this // 180 // multiplexer to return. (This happens asynchronously, via the // 181 // multiplexer's queue.) // 182 if (first) // 183 self._multiplexer.ready(); // 184 // 185 // Replace self._results atomically. (This assignment is what makes `first` // 186 // stay through on the next cycle, so we've waited until after we've // 187 // committed to ready-ing the multiplexer.) // 188 self._results = newResults; // 189 // 190 // Once the ObserveMultiplexer has processed everything we've done in this // 191 // round, mark all the writes which existed before this call as // 192 // commmitted. (If new writes have shown up in the meantime, there'll // 193 // already be another _pollMongo task scheduled.) // 194 self._multiplexer.onFlush(function () { // 195 _.each(writesForCycle, function (w) { // 196 w.committed(); // 197 }); // 198 }); // 199 }, // 200 // 201 stop: function () { // 202 var self = this; // 203 self._stopped = true; // 204 _.each(self._stopCallbacks, function (c) { c(); }); // 205 // Release any write fences that are waiting on us. // 206 _.each(self._pendingWrites, function (w) { // 207 w.committed(); // 208 }); // 209 Package.facts && Package.facts.Facts.incrementServerFact( // 210 "mongo-livedata", "observe-drivers-polling", -1); // 211 } // 212 }); // 213 // 214 //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// }).call(this); (function(){ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // // packages/mongo/oplog_observe_driver.js // // // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // var Fiber = Npm.require('fibers'); // 1 var Future = Npm.require('fibers/future'); // 2 // 3 var PHASE = { // 4 QUERYING: "QUERYING", // 5 FETCHING: "FETCHING", // 6 STEADY: "STEADY" // 7 }; // 8 // 9 // Exception thrown by _needToPollQuery which unrolls the stack up to the // 10 // enclosing call to finishIfNeedToPollQuery. // 11 var SwitchedToQuery = function () {}; // 12 var finishIfNeedToPollQuery = function (f) { // 13 return function () { // 14 try { // 15 f.apply(this, arguments); // 16 } catch (e) { // 17 if (!(e instanceof SwitchedToQuery)) // 18 throw e; // 19 } // 20 }; // 21 }; // 22 // 23 var currentId = 0; // 24 // 25 // OplogObserveDriver is an alternative to PollingObserveDriver which follows // 26 // the Mongo operation log instead of just re-polling the query. It obeys the // 27 // same simple interface: constructing it starts sending observeChanges // 28 // callbacks (and a ready() invocation) to the ObserveMultiplexer, and you stop // 29 // it by calling the stop() method. // 30 OplogObserveDriver = function (options) { // 31 var self = this; // 32 self._usesOplog = true; // tests look at this // 33 // 34 self._id = currentId; // 35 currentId++; // 36 // 37 self._cursorDescription = options.cursorDescription; // 38 self._mongoHandle = options.mongoHandle; // 39 self._multiplexer = options.multiplexer; // 40 // 41 if (options.ordered) { // 42 throw Error("OplogObserveDriver only supports unordered observeChanges"); // 43 } // 44 // 45 var sorter = options.sorter; // 46 // We don't support $near and other geo-queries so it's OK to initialize the // 47 // comparator only once in the constructor. // 48 var comparator = sorter && sorter.getComparator(); // 49 // 50 if (options.cursorDescription.options.limit) { // 51 // There are several properties ordered driver implements: // 52 // - _limit is a positive number // 53 // - _comparator is a function-comparator by which the query is ordered // 54 // - _unpublishedBuffer is non-null Min/Max Heap, // 55 // the empty buffer in STEADY phase implies that the // 56 // everything that matches the queries selector fits // 57 // into published set. // 58 // - _published - Min Heap (also implements IdMap methods) // 59 // 60 var heapOptions = { IdMap: LocalCollection._IdMap }; // 61 self._limit = self._cursorDescription.options.limit; // 62 self._comparator = comparator; // 63 self._sorter = sorter; // 64 self._unpublishedBuffer = new MinMaxHeap(comparator, heapOptions); // 65 // We need something that can find Max value in addition to IdMap interface // 66 self._published = new MaxHeap(comparator, heapOptions); // 67 } else { // 68 self._limit = 0; // 69 self._comparator = null; // 70 self._sorter = null; // 71 self._unpublishedBuffer = null; // 72 self._published = new LocalCollection._IdMap; // 73 } // 74 // 75 // Indicates if it is safe to insert a new document at the end of the buffer // 76 // for this query. i.e. it is known that there are no documents matching the // 77 // selector those are not in published or buffer. // 78 self._safeAppendToBuffer = false; // 79 // 80 self._stopped = false; // 81 self._stopHandles = []; // 82 // 83 Package.facts && Package.facts.Facts.incrementServerFact( // 84 "mongo-livedata", "observe-drivers-oplog", 1); // 85 // 86 self._registerPhaseChange(PHASE.QUERYING); // 87 // 88 var selector = self._cursorDescription.selector; // 89 self._matcher = options.matcher; // 90 var projection = self._cursorDescription.options.fields || {}; // 91 self._projectionFn = LocalCollection._compileProjection(projection); // 92 // Projection function, result of combining important fields for selector and // 93 // existing fields projection // 94 self._sharedProjection = self._matcher.combineIntoProjection(projection); // 95 if (sorter) // 96 self._sharedProjection = sorter.combineIntoProjection(self._sharedProjection); // 97 self._sharedProjectionFn = LocalCollection._compileProjection( // 98 self._sharedProjection); // 99 // 100 self._needToFetch = new LocalCollection._IdMap; // 101 self._currentlyFetching = null; // 102 self._fetchGeneration = 0; // 103 // 104 self._requeryWhenDoneThisQuery = false; // 105 self._writesToCommitWhenWeReachSteady = []; // 106 // 107 // If the oplog handle tells us that it skipped some entries (because it got // 108 // behind, say), re-poll. // 109 self._stopHandles.push(self._mongoHandle._oplogHandle.onSkippedEntries( // 110 finishIfNeedToPollQuery(function () { // 111 self._needToPollQuery(); // 112 }) // 113 )); // 114 // 115 forEachTrigger(self._cursorDescription, function (trigger) { // 116 self._stopHandles.push(self._mongoHandle._oplogHandle.onOplogEntry( // 117 trigger, function (notification) { // 118 Meteor._noYieldsAllowed(finishIfNeedToPollQuery(function () { // 119 var op = notification.op; // 120 if (notification.dropCollection || notification.dropDatabase) { // 121 // Note: this call is not allowed to block on anything (especially // 122 // on waiting for oplog entries to catch up) because that will block // 123 // onOplogEntry! // 124 self._needToPollQuery(); // 125 } else { // 126 // All other operators should be handled depending on phase // 127 if (self._phase === PHASE.QUERYING) // 128 self._handleOplogEntryQuerying(op); // 129 else // 130 self._handleOplogEntrySteadyOrFetching(op); // 131 } // 132 })); // 133 } // 134 )); // 135 }); // 136 // 137 // XXX ordering w.r.t. everything else? // 138 self._stopHandles.push(listenAll( // 139 self._cursorDescription, function (notification) { // 140 // If we're not in a pre-fire write fence, we don't have to do anything. // 141 var fence = DDPServer._CurrentWriteFence.get(); // 142 if (!fence || fence.fired) // 143 return; // 144 // 145 if (fence._oplogObserveDrivers) { // 146 fence._oplogObserveDrivers[self._id] = self; // 147 return; // 148 } // 149 // 150 fence._oplogObserveDrivers = {}; // 151 fence._oplogObserveDrivers[self._id] = self; // 152 // 153 fence.onBeforeFire(function () { // 154 var drivers = fence._oplogObserveDrivers; // 155 delete fence._oplogObserveDrivers; // 156 // 157 // This fence cannot fire until we've caught up to "this point" in the // 158 // oplog, and all observers made it back to the steady state. // 159 self._mongoHandle._oplogHandle.waitUntilCaughtUp(); // 160 // 161 _.each(drivers, function (driver) { // 162 if (driver._stopped) // 163 return; // 164 // 165 var write = fence.beginWrite(); // 166 if (driver._phase === PHASE.STEADY) { // 167 // Make sure that all of the callbacks have made it through the // 168 // multiplexer and been delivered to ObserveHandles before committing // 169 // writes. // 170 driver._multiplexer.onFlush(function () { // 171 write.committed(); // 172 }); // 173 } else { // 174 driver._writesToCommitWhenWeReachSteady.push(write); // 175 } // 176 }); // 177 }); // 178 } // 179 )); // 180 // 181 // When Mongo fails over, we need to repoll the query, in case we processed an // 182 // oplog entry that got rolled back. // 183 self._stopHandles.push(self._mongoHandle._onFailover(finishIfNeedToPollQuery( // 184 function () { // 185 self._needToPollQuery(); // 186 }))); // 187 // 188 // Give _observeChanges a chance to add the new ObserveHandle to our // 189 // multiplexer, so that the added calls get streamed. // 190 Meteor.defer(finishIfNeedToPollQuery(function () { // 191 self._runInitialQuery(); // 192 })); // 193 }; // 194 // 195 _.extend(OplogObserveDriver.prototype, { // 196 _addPublished: function (id, doc) { // 197 var self = this; // 198 Meteor._noYieldsAllowed(function () { // 199 var fields = _.clone(doc); // 200 delete fields._id; // 201 self._published.set(id, self._sharedProjectionFn(doc)); // 202 self._multiplexer.added(id, self._projectionFn(fields)); // 203 // 204 // After adding this document, the published set might be overflowed // 205 // (exceeding capacity specified by limit). If so, push the maximum // 206 // element to the buffer, we might want to save it in memory to reduce the // 207 // amount of Mongo lookups in the future. // 208 if (self._limit && self._published.size() > self._limit) { // 209 // XXX in theory the size of published is no more than limit+1 // 210 if (self._published.size() !== self._limit + 1) { // 211 throw new Error("After adding to published, " + // 212 (self._published.size() - self._limit) + // 213 " documents are overflowing the set"); // 214 } // 215 // 216 var overflowingDocId = self._published.maxElementId(); // 217 var overflowingDoc = self._published.get(overflowingDocId); // 218 // 219 if (EJSON.equals(overflowingDocId, id)) { // 220 throw new Error("The document just added is overflowing the published set"); // 221 } // 222 // 223 self._published.remove(overflowingDocId); // 224 self._multiplexer.removed(overflowingDocId); // 225 self._addBuffered(overflowingDocId, overflowingDoc); // 226 } // 227 }); // 228 }, // 229 _removePublished: function (id) { // 230 var self = this; // 231 Meteor._noYieldsAllowed(function () { // 232 self._published.remove(id); // 233 self._multiplexer.removed(id); // 234 if (! self._limit || self._published.size() === self._limit) // 235 return; // 236 // 237 if (self._published.size() > self._limit) // 238 throw Error("self._published got too big"); // 239 // 240 // OK, we are publishing less than the limit. Maybe we should look in the // 241 // buffer to find the next element past what we were publishing before. // 242 // 243 if (!self._unpublishedBuffer.empty()) { // 244 // There's something in the buffer; move the first thing in it to // 245 // _published. // 246 var newDocId = self._unpublishedBuffer.minElementId(); // 247 var newDoc = self._unpublishedBuffer.get(newDocId); // 248 self._removeBuffered(newDocId); // 249 self._addPublished(newDocId, newDoc); // 250 return; // 251 } // 252 // 253 // There's nothing in the buffer. This could mean one of a few things. // 254 // 255 // (a) We could be in the middle of re-running the query (specifically, we // 256 // could be in _publishNewResults). In that case, _unpublishedBuffer is // 257 // empty because we clear it at the beginning of _publishNewResults. In // 258 // this case, our caller already knows the entire answer to the query and // 259 // we don't need to do anything fancy here. Just return. // 260 if (self._phase === PHASE.QUERYING) // 261 return; // 262 // 263 // (b) We're pretty confident that the union of _published and // 264 // _unpublishedBuffer contain all documents that match selector. Because // 265 // _unpublishedBuffer is empty, that means we're confident that _published // 266 // contains all documents that match selector. So we have nothing to do. // 267 if (self._safeAppendToBuffer) // 268 return; // 269 // 270 // (c) Maybe there are other documents out there that should be in our // 271 // buffer. But in that case, when we emptied _unpublishedBuffer in // 272 // _removeBuffered, we should have called _needToPollQuery, which will // 273 // either put something in _unpublishedBuffer or set _safeAppendToBuffer // 274 // (or both), and it will put us in QUERYING for that whole time. So in // 275 // fact, we shouldn't be able to get here. // 276 // 277 throw new Error("Buffer inexplicably empty"); // 278 }); // 279 }, // 280 _changePublished: function (id, oldDoc, newDoc) { // 281 var self = this; // 282 Meteor._noYieldsAllowed(function () { // 283 self._published.set(id, self._sharedProjectionFn(newDoc)); // 284 var projectedNew = self._projectionFn(newDoc); // 285 var projectedOld = self._projectionFn(oldDoc); // 286 var changed = DiffSequence.makeChangedFields( // 287 projectedNew, projectedOld); // 288 if (!_.isEmpty(changed)) // 289 self._multiplexer.changed(id, changed); // 290 }); // 291 }, // 292 _addBuffered: function (id, doc) { // 293 var self = this; // 294 Meteor._noYieldsAllowed(function () { // 295 self._unpublishedBuffer.set(id, self._sharedProjectionFn(doc)); // 296 // 297 // If something is overflowing the buffer, we just remove it from cache // 298 if (self._unpublishedBuffer.size() > self._limit) { // 299 var maxBufferedId = self._unpublishedBuffer.maxElementId(); // 300 // 301 self._unpublishedBuffer.remove(maxBufferedId); // 302 // 303 // Since something matching is removed from cache (both published set and // 304 // buffer), set flag to false // 305 self._safeAppendToBuffer = false; // 306 } // 307 }); // 308 }, // 309 // Is called either to remove the doc completely from matching set or to move // 310 // it to the published set later. // 311 _removeBuffered: function (id) { // 312 var self = this; // 313 Meteor._noYieldsAllowed(function () { // 314 self._unpublishedBuffer.remove(id); // 315 // To keep the contract "buffer is never empty in STEADY phase unless the // 316 // everything matching fits into published" true, we poll everything as // 317 // soon as we see the buffer becoming empty. // 318 if (! self._unpublishedBuffer.size() && ! self._safeAppendToBuffer) // 319 self._needToPollQuery(); // 320 }); // 321 }, // 322 // Called when a document has joined the "Matching" results set. // 323 // Takes responsibility of keeping _unpublishedBuffer in sync with _published // 324 // and the effect of limit enforced. // 325 _addMatching: function (doc) { // 326 var self = this; // 327 Meteor._noYieldsAllowed(function () { // 328 var id = doc._id; // 329 if (self._published.has(id)) // 330 throw Error("tried to add something already published " + id); // 331 if (self._limit && self._unpublishedBuffer.has(id)) // 332 throw Error("tried to add something already existed in buffer " + id); // 333 // 334 var limit = self._limit; // 335 var comparator = self._comparator; // 336 var maxPublished = (limit && self._published.size() > 0) ? // 337 self._published.get(self._published.maxElementId()) : null; // 338 var maxBuffered = (limit && self._unpublishedBuffer.size() > 0) // 339 ? self._unpublishedBuffer.get(self._unpublishedBuffer.maxElementId()) // 340 : null; // 341 // The query is unlimited or didn't publish enough documents yet or the // 342 // new document would fit into published set pushing the maximum element // 343 // out, then we need to publish the doc. // 344 var toPublish = ! limit || self._published.size() < limit || // 345 comparator(doc, maxPublished) < 0; // 346 // 347 // Otherwise we might need to buffer it (only in case of limited query). // 348 // Buffering is allowed if the buffer is not filled up yet and all // 349 // matching docs are either in the published set or in the buffer. // 350 var canAppendToBuffer = !toPublish && self._safeAppendToBuffer && // 351 self._unpublishedBuffer.size() < limit; // 352 // 353 // Or if it is small enough to be safely inserted to the middle or the // 354 // beginning of the buffer. // 355 var canInsertIntoBuffer = !toPublish && maxBuffered && // 356 comparator(doc, maxBuffered) <= 0; // 357 // 358 var toBuffer = canAppendToBuffer || canInsertIntoBuffer; // 359 // 360 if (toPublish) { // 361 self._addPublished(id, doc); // 362 } else if (toBuffer) { // 363 self._addBuffered(id, doc); // 364 } else { // 365 // dropping it and not saving to the cache // 366 self._safeAppendToBuffer = false; // 367 } // 368 }); // 369 }, // 370 // Called when a document leaves the "Matching" results set. // 371 // Takes responsibility of keeping _unpublishedBuffer in sync with _published // 372 // and the effect of limit enforced. // 373 _removeMatching: function (id) { // 374 var self = this; // 375 Meteor._noYieldsAllowed(function () { // 376 if (! self._published.has(id) && ! self._limit) // 377 throw Error("tried to remove something matching but not cached " + id); // 378 // 379 if (self._published.has(id)) { // 380 self._removePublished(id); // 381 } else if (self._unpublishedBuffer.has(id)) { // 382 self._removeBuffered(id); // 383 } // 384 }); // 385 }, // 386 _handleDoc: function (id, newDoc) { // 387 var self = this; // 388 Meteor._noYieldsAllowed(function () { // 389 var matchesNow = newDoc && self._matcher.documentMatches(newDoc).result; // 390 // 391 var publishedBefore = self._published.has(id); // 392 var bufferedBefore = self._limit && self._unpublishedBuffer.has(id); // 393 var cachedBefore = publishedBefore || bufferedBefore; // 394 // 395 if (matchesNow && !cachedBefore) { // 396 self._addMatching(newDoc); // 397 } else if (cachedBefore && !matchesNow) { // 398 self._removeMatching(id); // 399 } else if (cachedBefore && matchesNow) { // 400 var oldDoc = self._published.get(id); // 401 var comparator = self._comparator; // 402 var minBuffered = self._limit && self._unpublishedBuffer.size() && // 403 self._unpublishedBuffer.get(self._unpublishedBuffer.minElementId()); // 404 // 405 if (publishedBefore) { // 406 // Unlimited case where the document stays in published once it // 407 // matches or the case when we don't have enough matching docs to // 408 // publish or the changed but matching doc will stay in published // 409 // anyways. // 410 // // 411 // XXX: We rely on the emptiness of buffer. Be sure to maintain the // 412 // fact that buffer can't be empty if there are matching documents not // 413 // published. Notably, we don't want to schedule repoll and continue // 414 // relying on this property. // 415 var staysInPublished = ! self._limit || // 416 self._unpublishedBuffer.size() === 0 || // 417 comparator(newDoc, minBuffered) <= 0; // 418 // 419 if (staysInPublished) { // 420 self._changePublished(id, oldDoc, newDoc); // 421 } else { // 422 // after the change doc doesn't stay in the published, remove it // 423 self._removePublished(id); // 424 // but it can move into buffered now, check it // 425 var maxBuffered = self._unpublishedBuffer.get( // 426 self._unpublishedBuffer.maxElementId()); // 427 // 428 var toBuffer = self._safeAppendToBuffer || // 429 (maxBuffered && comparator(newDoc, maxBuffered) <= 0); // 430 // 431 if (toBuffer) { // 432 self._addBuffered(id, newDoc); // 433 } else { // 434 // Throw away from both published set and buffer // 435 self._safeAppendToBuffer = false; // 436 } // 437 } // 438 } else if (bufferedBefore) { // 439 oldDoc = self._unpublishedBuffer.get(id); // 440 // remove the old version manually instead of using _removeBuffered so // 441 // we don't trigger the querying immediately. if we end this block // 442 // with the buffer empty, we will need to trigger the query poll // 443 // manually too. // 444 self._unpublishedBuffer.remove(id); // 445 // 446 var maxPublished = self._published.get( // 447 self._published.maxElementId()); // 448 var maxBuffered = self._unpublishedBuffer.size() && // 449 self._unpublishedBuffer.get( // 450 self._unpublishedBuffer.maxElementId()); // 451 // 452 // the buffered doc was updated, it could move to published // 453 var toPublish = comparator(newDoc, maxPublished) < 0; // 454 // 455 // or stays in buffer even after the change // 456 var staysInBuffer = (! toPublish && self._safeAppendToBuffer) || // 457 (!toPublish && maxBuffered && // 458 comparator(newDoc, maxBuffered) <= 0); // 459 // 460 if (toPublish) { // 461 self._addPublished(id, newDoc); // 462 } else if (staysInBuffer) { // 463 // stays in buffer but changes // 464 self._unpublishedBuffer.set(id, newDoc); // 465 } else { // 466 // Throw away from both published set and buffer // 467 self._safeAppendToBuffer = false; // 468 // Normally this check would have been done in _removeBuffered but // 469 // we didn't use it, so we need to do it ourself now. // 470 if (! self._unpublishedBuffer.size()) { // 471 self._needToPollQuery(); // 472 } // 473 } // 474 } else { // 475 throw new Error("cachedBefore implies either of publishedBefore or bufferedBefore is true."); // 476 } // 477 } // 478 }); // 479 }, // 480 _fetchModifiedDocuments: function () { // 481 var self = this; // 482 Meteor._noYieldsAllowed(function () { // 483 self._registerPhaseChange(PHASE.FETCHING); // 484 // Defer, because nothing called from the oplog entry handler may yield, // 485 // but fetch() yields. // 486 Meteor.defer(finishIfNeedToPollQuery(function () { // 487 while (!self._stopped && !self._needToFetch.empty()) { // 488 if (self._phase === PHASE.QUERYING) { // 489 // While fetching, we decided to go into QUERYING mode, and then we // 490 // saw another oplog entry, so _needToFetch is not empty. But we // 491 // shouldn't fetch these documents until AFTER the query is done. // 492 break; // 493 } // 494 // 495 // Being in steady phase here would be surprising. // 496 if (self._phase !== PHASE.FETCHING) // 497 throw new Error("phase in fetchModifiedDocuments: " + self._phase); // 498 // 499 self._currentlyFetching = self._needToFetch; // 500 var thisGeneration = ++self._fetchGeneration; // 501 self._needToFetch = new LocalCollection._IdMap; // 502 var waiting = 0; // 503 var fut = new Future; // 504 // This loop is safe, because _currentlyFetching will not be updated // 505 // during this loop (in fact, it is never mutated). // 506 self._currentlyFetching.forEach(function (cacheKey, id) { // 507 waiting++; // 508 self._mongoHandle._docFetcher.fetch( // 509 self._cursorDescription.collectionName, id, cacheKey, // 510 finishIfNeedToPollQuery(function (err, doc) { // 511 try { // 512 if (err) { // 513 Meteor._debug("Got exception while fetching documents: " + // 514 err); // 515 // If we get an error from the fetcher (eg, trouble // 516 // connecting to Mongo), let's just abandon the fetch phase // 517 // altogether and fall back to polling. It's not like we're // 518 // getting live updates anyway. // 519 if (self._phase !== PHASE.QUERYING) { // 520 self._needToPollQuery(); // 521 } // 522 } else if (!self._stopped && self._phase === PHASE.FETCHING // 523 && self._fetchGeneration === thisGeneration) { // 524 // We re-check the generation in case we've had an explicit // 525 // _pollQuery call (eg, in another fiber) which should // 526 // effectively cancel this round of fetches. (_pollQuery // 527 // increments the generation.) // 528 self._handleDoc(id, doc); // 529 } // 530 } finally { // 531 waiting--; // 532 // Because fetch() never calls its callback synchronously, // 533 // this is safe (ie, we won't call fut.return() before the // 534 // forEach is done). // 535 if (waiting === 0) // 536 fut.return(); // 537 } // 538 })); // 539 }); // 540 fut.wait(); // 541 // Exit now if we've had a _pollQuery call (here or in another fiber). // 542 if (self._phase === PHASE.QUERYING) // 543 return; // 544 self._currentlyFetching = null; // 545 } // 546 // We're done fetching, so we can be steady, unless we've had a // 547 // _pollQuery call (here or in another fiber). // 548 if (self._phase !== PHASE.QUERYING) // 549 self._beSteady(); // 550 })); // 551 }); // 552 }, // 553 _beSteady: function () { // 554 var self = this; // 555 Meteor._noYieldsAllowed(function () { // 556 self._registerPhaseChange(PHASE.STEADY); // 557 var writes = self._writesToCommitWhenWeReachSteady; // 558 self._writesToCommitWhenWeReachSteady = []; // 559 self._multiplexer.onFlush(function () { // 560 _.each(writes, function (w) { // 561 w.committed(); // 562 }); // 563 }); // 564 }); // 565 }, // 566 _handleOplogEntryQuerying: function (op) { // 567 var self = this; // 568 Meteor._noYieldsAllowed(function () { // 569 self._needToFetch.set(idForOp(op), op.ts.toString()); // 570 }); // 571 }, // 572 _handleOplogEntrySteadyOrFetching: function (op) { // 573 var self = this; // 574 Meteor._noYieldsAllowed(function () { // 575 var id = idForOp(op); // 576 // If we're already fetching this one, or about to, we can't optimize; // 577 // make sure that we fetch it again if necessary. // 578 if (self._phase === PHASE.FETCHING && // 579 ((self._currentlyFetching && self._currentlyFetching.has(id)) || // 580 self._needToFetch.has(id))) { // 581 self._needToFetch.set(id, op.ts.toString()); // 582 return; // 583 } // 584 // 585 if (op.op === 'd') { // 586 if (self._published.has(id) || // 587 (self._limit && self._unpublishedBuffer.has(id))) // 588 self._removeMatching(id); // 589 } else if (op.op === 'i') { // 590 if (self._published.has(id)) // 591 throw new Error("insert found for already-existing ID in published"); // 592 if (self._unpublishedBuffer && self._unpublishedBuffer.has(id)) // 593 throw new Error("insert found for already-existing ID in buffer"); // 594 // 595 // XXX what if selector yields? for now it can't but later it could // 596 // have $where // 597 if (self._matcher.documentMatches(op.o).result) // 598 self._addMatching(op.o); // 599 } else if (op.op === 'u') { // 600 // Is this a modifier ($set/$unset, which may require us to poll the // 601 // database to figure out if the whole document matches the selector) or // 602 // a replacement (in which case we can just directly re-evaluate the // 603 // selector)? // 604 var isReplace = !_.has(op.o, '$set') && !_.has(op.o, '$unset'); // 605 // If this modifier modifies something inside an EJSON custom type (ie, // 606 // anything with EJSON$), then we can't try to use // 607 // LocalCollection._modify, since that just mutates the EJSON encoding, // 608 // not the actual object. // 609 var canDirectlyModifyDoc = // 610 !isReplace && modifierCanBeDirectlyApplied(op.o); // 611 // 612 var publishedBefore = self._published.has(id); // 613 var bufferedBefore = self._limit && self._unpublishedBuffer.has(id); // 614 // 615 if (isReplace) { // 616 self._handleDoc(id, _.extend({_id: id}, op.o)); // 617 } else if ((publishedBefore || bufferedBefore) && // 618 canDirectlyModifyDoc) { // 619 // Oh great, we actually know what the document is, so we can apply // 620 // this directly. // 621 var newDoc = self._published.has(id) // 622 ? self._published.get(id) : self._unpublishedBuffer.get(id); // 623 newDoc = EJSON.clone(newDoc); // 624 // 625 newDoc._id = id; // 626 try { // 627 LocalCollection._modify(newDoc, op.o); // 628 } catch (e) { // 629 if (e.name !== "MinimongoError") // 630 throw e; // 631 // We didn't understand the modifier. Re-fetch. // 632 self._needToFetch.set(id, op.ts.toString()); // 633 if (self._phase === PHASE.STEADY) { // 634 self._fetchModifiedDocuments(); // 635 } // 636 return; // 637 } // 638 self._handleDoc(id, self._sharedProjectionFn(newDoc)); // 639 } else if (!canDirectlyModifyDoc || // 640 self._matcher.canBecomeTrueByModifier(op.o) || // 641 (self._sorter && self._sorter.affectedByModifier(op.o))) { // 642 self._needToFetch.set(id, op.ts.toString()); // 643 if (self._phase === PHASE.STEADY) // 644 self._fetchModifiedDocuments(); // 645 } // 646 } else { // 647 throw Error("XXX SURPRISING OPERATION: " + op); // 648 } // 649 }); // 650 }, // 651 // Yields! // 652 _runInitialQuery: function () { // 653 var self = this; // 654 if (self._stopped) // 655 throw new Error("oplog stopped surprisingly early"); // 656 // 657 self._runQuery({initial: true}); // yields // 658 // 659 if (self._stopped) // 660 return; // can happen on queryError // 661 // 662 // Allow observeChanges calls to return. (After this, it's possible for // 663 // stop() to be called.) // 664 self._multiplexer.ready(); // 665 // 666 self._doneQuerying(); // yields // 667 }, // 668 // 669 // In various circumstances, we may just want to stop processing the oplog and // 670 // re-run the initial query, just as if we were a PollingObserveDriver. // 671 // // 672 // This function may not block, because it is called from an oplog entry // 673 // handler. // 674 // // 675 // XXX We should call this when we detect that we've been in FETCHING for "too // 676 // long". // 677 // // 678 // XXX We should call this when we detect Mongo failover (since that might // 679 // mean that some of the oplog entries we have processed have been rolled // 680 // back). The Node Mongo driver is in the middle of a bunch of huge // 681 // refactorings, including the way that it notifies you when primary // 682 // changes. Will put off implementing this until driver 1.4 is out. // 683 _pollQuery: function () { // 684 var self = this; // 685 Meteor._noYieldsAllowed(function () { // 686 if (self._stopped) // 687 return; // 688 // 689 // Yay, we get to forget about all the things we thought we had to fetch. // 690 self._needToFetch = new LocalCollection._IdMap; // 691 self._currentlyFetching = null; // 692 ++self._fetchGeneration; // ignore any in-flight fetches // 693 self._registerPhaseChange(PHASE.QUERYING); // 694 // 695 // Defer so that we don't yield. We don't need finishIfNeedToPollQuery // 696 // here because SwitchedToQuery is not thrown in QUERYING mode. // 697 Meteor.defer(function () { // 698 self._runQuery(); // 699 self._doneQuerying(); // 700 }); // 701 }); // 702 }, // 703 // 704 // Yields! // 705 _runQuery: function (options) { // 706 var self = this; // 707 options = options || {}; // 708 var newResults, newBuffer; // 709 // 710 // This while loop is just to retry failures. // 711 while (true) { // 712 // If we've been stopped, we don't have to run anything any more. // 713 if (self._stopped) // 714 return; // 715 // 716 newResults = new LocalCollection._IdMap; // 717 newBuffer = new LocalCollection._IdMap; // 718 // 719 // Query 2x documents as the half excluded from the original query will go // 720 // into unpublished buffer to reduce additional Mongo lookups in cases // 721 // when documents are removed from the published set and need a // 722 // replacement. // 723 // XXX needs more thought on non-zero skip // 724 // XXX 2 is a "magic number" meaning there is an extra chunk of docs for // 725 // buffer if such is needed. // 726 var cursor = self._cursorForQuery({ limit: self._limit * 2 }); // 727 try { // 728 cursor.forEach(function (doc, i) { // yields // 729 if (!self._limit || i < self._limit) // 730 newResults.set(doc._id, doc); // 731 else // 732 newBuffer.set(doc._id, doc); // 733 }); // 734 break; // 735 } catch (e) { // 736 if (options.initial && typeof(e.code) === 'number') { // 737 // This is an error document sent to us by mongod, not a connection // 738 // error generated by the client. And we've never seen this query work // 739 // successfully. Probably it's a bad selector or something, so we // 740 // should NOT retry. Instead, we should halt the observe (which ends // 741 // up calling `stop` on us). // 742 self._multiplexer.queryError(e); // 743 return; // 744 } // 745 // 746 // During failover (eg) if we get an exception we should log and retry // 747 // instead of crashing. // 748 Meteor._debug("Got exception while polling query: " + e); // 749 Meteor._sleepForMs(100); // 750 } // 751 } // 752 // 753 if (self._stopped) // 754 return; // 755 // 756 self._publishNewResults(newResults, newBuffer); // 757 }, // 758 // 759 // Transitions to QUERYING and runs another query, or (if already in QUERYING) // 760 // ensures that we will query again later. // 761 // // 762 // This function may not block, because it is called from an oplog entry // 763 // handler. However, if we were not already in the QUERYING phase, it throws // 764 // an exception that is caught by the closest surrounding // 765 // finishIfNeedToPollQuery call; this ensures that we don't continue running // 766 // close that was designed for another phase inside PHASE.QUERYING. // 767 // // 768 // (It's also necessary whenever logic in this file yields to check that other // 769 // phases haven't put us into QUERYING mode, though; eg, // 770 // _fetchModifiedDocuments does this.) // 771 _needToPollQuery: function () { // 772 var self = this; // 773 Meteor._noYieldsAllowed(function () { // 774 if (self._stopped) // 775 return; // 776 // 777 // If we're not already in the middle of a query, we can query now // 778 // (possibly pausing FETCHING). // 779 if (self._phase !== PHASE.QUERYING) { // 780 self._pollQuery(); // 781 throw new SwitchedToQuery; // 782 } // 783 // 784 // We're currently in QUERYING. Set a flag to ensure that we run another // 785 // query when we're done. // 786 self._requeryWhenDoneThisQuery = true; // 787 }); // 788 }, // 789 // 790 // Yields! // 791 _doneQuerying: function () { // 792 var self = this; // 793 // 794 if (self._stopped) // 795 return; // 796 self._mongoHandle._oplogHandle.waitUntilCaughtUp(); // yields // 797 if (self._stopped) // 798 return; // 799 if (self._phase !== PHASE.QUERYING) // 800 throw Error("Phase unexpectedly " + self._phase); // 801 // 802 Meteor._noYieldsAllowed(function () { // 803 if (self._requeryWhenDoneThisQuery) { // 804 self._requeryWhenDoneThisQuery = false; // 805 self._pollQuery(); // 806 } else if (self._needToFetch.empty()) { // 807 self._beSteady(); // 808 } else { // 809 self._fetchModifiedDocuments(); // 810 } // 811 }); // 812 }, // 813 // 814 _cursorForQuery: function (optionsOverwrite) { // 815 var self = this; // 816 return Meteor._noYieldsAllowed(function () { // 817 // The query we run is almost the same as the cursor we are observing, // 818 // with a few changes. We need to read all the fields that are relevant to // 819 // the selector, not just the fields we are going to publish (that's the // 820 // "shared" projection). And we don't want to apply any transform in the // 821 // cursor, because observeChanges shouldn't use the transform. // 822 var options = _.clone(self._cursorDescription.options); // 823 // 824 // Allow the caller to modify the options. Useful to specify different // 825 // skip and limit values. // 826 _.extend(options, optionsOverwrite); // 827 // 828 options.fields = self._sharedProjection; // 829 delete options.transform; // 830 // We are NOT deep cloning fields or selector here, which should be OK. // 831 var description = new CursorDescription( // 832 self._cursorDescription.collectionName, // 833 self._cursorDescription.selector, // 834 options); // 835 return new Cursor(self._mongoHandle, description); // 836 }); // 837 }, // 838 // 839 // 840 // Replace self._published with newResults (both are IdMaps), invoking observe // 841 // callbacks on the multiplexer. // 842 // Replace self._unpublishedBuffer with newBuffer. // 843 // // 844 // XXX This is very similar to LocalCollection._diffQueryUnorderedChanges. We // 845 // should really: (a) Unify IdMap and OrderedDict into Unordered/OrderedDict // 846 // (b) Rewrite diff.js to use these classes instead of arrays and objects. // 847 _publishNewResults: function (newResults, newBuffer) { // 848 var self = this; // 849 Meteor._noYieldsAllowed(function () { // 850 // 851 // If the query is limited and there is a buffer, shut down so it doesn't // 852 // stay in a way. // 853 if (self._limit) { // 854 self._unpublishedBuffer.clear(); // 855 } // 856 // 857 // First remove anything that's gone. Be careful not to modify // 858 // self._published while iterating over it. // 859 var idsToRemove = []; // 860 self._published.forEach(function (doc, id) { // 861 if (!newResults.has(id)) // 862 idsToRemove.push(id); // 863 }); // 864 _.each(idsToRemove, function (id) { // 865 self._removePublished(id); // 866 }); // 867 // 868 // Now do adds and changes. // 869 // If self has a buffer and limit, the new fetched result will be // 870 // limited correctly as the query has sort specifier. // 871 newResults.forEach(function (doc, id) { // 872 self._handleDoc(id, doc); // 873 }); // 874 // 875 // Sanity-check that everything we tried to put into _published ended up // 876 // there. // 877 // XXX if this is slow, remove it later // 878 if (self._published.size() !== newResults.size()) { // 879 throw Error( // 880 "The Mongo server and the Meteor query disagree on how " + // 881 "many documents match your query. Maybe it is hitting a Mongo " + // 882 "edge case? The query is: " + // 883 EJSON.stringify(self._cursorDescription.selector)); // 884 } // 885 self._published.forEach(function (doc, id) { // 886 if (!newResults.has(id)) // 887 throw Error("_published has a doc that newResults doesn't; " + id); // 888 }); // 889 // 890 // Finally, replace the buffer // 891 newBuffer.forEach(function (doc, id) { // 892 self._addBuffered(id, doc); // 893 }); // 894 // 895 self._safeAppendToBuffer = newBuffer.size() < self._limit; // 896 }); // 897 }, // 898 // 899 // This stop function is invoked from the onStop of the ObserveMultiplexer, so // 900 // it shouldn't actually be possible to call it until the multiplexer is // 901 // ready. // 902 // // 903 // It's important to check self._stopped after every call in this file that // 904 // can yield! // 905 stop: function () { // 906 var self = this; // 907 if (self._stopped) // 908 return; // 909 self._stopped = true; // 910 _.each(self._stopHandles, function (handle) { // 911 handle.stop(); // 912 }); // 913 // 914 // Note: we *don't* use multiplexer.onFlush here because this stop // 915 // callback is actually invoked by the multiplexer itself when it has // 916 // determined that there are no handles left. So nothing is actually going // 917 // to get flushed (and it's probably not valid to call methods on the // 918 // dying multiplexer). // 919 _.each(self._writesToCommitWhenWeReachSteady, function (w) { // 920 w.committed(); // maybe yields? // 921 }); // 922 self._writesToCommitWhenWeReachSteady = null; // 923 // 924 // Proactively drop references to potentially big things. // 925 self._published = null; // 926 self._unpublishedBuffer = null; // 927 self._needToFetch = null; // 928 self._currentlyFetching = null; // 929 self._oplogEntryHandle = null; // 930 self._listenersHandle = null; // 931 // 932 Package.facts && Package.facts.Facts.incrementServerFact( // 933 "mongo-livedata", "observe-drivers-oplog", -1); // 934 }, // 935 // 936 _registerPhaseChange: function (phase) { // 937 var self = this; // 938 Meteor._noYieldsAllowed(function () { // 939 var now = new Date; // 940 // 941 if (self._phase) { // 942 var timeDiff = now - self._phaseStartTime; // 943 Package.facts && Package.facts.Facts.incrementServerFact( // 944 "mongo-livedata", "time-spent-in-" + self._phase + "-phase", timeDiff); // 945 } // 946 // 947 self._phase = phase; // 948 self._phaseStartTime = now; // 949 }); // 950 } // 951 }); // 952 // 953 // Does our oplog tailing code support this cursor? For now, we are being very // 954 // conservative and allowing only simple queries with simple options. // 955 // (This is a "static method".) // 956 OplogObserveDriver.cursorSupported = function (cursorDescription, matcher) { // 957 // First, check the options. // 958 var options = cursorDescription.options; // 959 // 960 // Did the user say no explicitly? // 961 if (options._disableOplog) // 962 return false; // 963 // 964 // skip is not supported: to support it we would need to keep track of all // 965 // "skipped" documents or at least their ids. // 966 // limit w/o a sort specifier is not supported: current implementation needs a // 967 // deterministic way to order documents. // 968 if (options.skip || (options.limit && !options.sort)) return false; // 969 // 970 // If a fields projection option is given check if it is supported by // 971 // minimongo (some operators are not supported). // 972 if (options.fields) { // 973 try { // 974 LocalCollection._checkSupportedProjection(options.fields); // 975 } catch (e) { // 976 if (e.name === "MinimongoError") // 977 return false; // 978 else // 979 throw e; // 980 } // 981 } // 982 // 983 // We don't allow the following selectors: // 984 // - $where (not confident that we provide the same JS environment // 985 // as Mongo, and can yield!) // 986 // - $near (has "interesting" properties in MongoDB, like the possibility // 987 // of returning an ID multiple times, though even polling maybe // 988 // have a bug there) // 989 // XXX: once we support it, we would need to think more on how we // 990 // initialize the comparators when we create the driver. // 991 return !matcher.hasWhere() && !matcher.hasGeoQuery(); // 992 }; // 993 // 994 var modifierCanBeDirectlyApplied = function (modifier) { // 995 return _.all(modifier, function (fields, operation) { // 996 return _.all(fields, function (value, field) { // 997 return !/EJSON\$/.test(field); // 998 }); // 999 }); // 1000 }; // 1001 // 1002 MongoInternals.OplogObserveDriver = OplogObserveDriver; // 1003 // 1004 //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// }).call(this); (function(){ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // // packages/mongo/local_collection_driver.js // // // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // LocalCollectionDriver = function () { // 1 var self = this; // 2 self.noConnCollections = {}; // 3 }; // 4 // 5 var ensureCollection = function (name, collections) { // 6 if (!(name in collections)) // 7 collections[name] = new LocalCollection(name); // 8 return collections[name]; // 9 }; // 10 // 11 _.extend(LocalCollectionDriver.prototype, { // 12 open: function (name, conn) { // 13 var self = this; // 14 if (!name) // 15 return new LocalCollection; // 16 if (! conn) { // 17 return ensureCollection(name, self.noConnCollections); // 18 } // 19 if (! conn._mongo_livedata_collections) // 20 conn._mongo_livedata_collections = {}; // 21 // XXX is there a way to keep track of a connection's collections without // 22 // dangling it off the connection object? // 23 return ensureCollection(name, conn._mongo_livedata_collections); // 24 } // 25 }); // 26 // 27 // singleton // 28 LocalCollectionDriver = new LocalCollectionDriver; // 29 // 30 //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// }).call(this); (function(){ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // // packages/mongo/remote_collection_driver.js // // // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // MongoInternals.RemoteCollectionDriver = function ( // 1 mongo_url, options) { // 2 var self = this; // 3 self.mongo = new MongoConnection(mongo_url, options); // 4 }; // 5 // 6 _.extend(MongoInternals.RemoteCollectionDriver.prototype, { // 7 open: function (name) { // 8 var self = this; // 9 var ret = {}; // 10 _.each( // 11 ['find', 'findOne', 'insert', 'update', 'upsert', // 12 'remove', '_ensureIndex', '_dropIndex', '_createCappedCollection', // 13 'dropCollection', 'rawCollection'], // 14 function (m) { // 15 ret[m] = _.bind(self.mongo[m], self.mongo, name); // 16 }); // 17 return ret; // 18 } // 19 }); // 20 // 21 // 22 // Create the singleton RemoteCollectionDriver only on demand, so we // 23 // only require Mongo configuration if it's actually used (eg, not if // 24 // you're only trying to receive data from a remote DDP server.) // 25 MongoInternals.defaultRemoteCollectionDriver = _.once(function () { // 26 var connectionOptions = {}; // 27 // 28 var mongoUrl = process.env.MONGO_URL; // 29 // 30 if (process.env.MONGO_OPLOG_URL) { // 31 connectionOptions.oplogUrl = process.env.MONGO_OPLOG_URL; // 32 } // 33 // 34 if (! mongoUrl) // 35 throw new Error("MONGO_URL must be set in environment"); // 36 // 37 return new MongoInternals.RemoteCollectionDriver(mongoUrl, connectionOptions); // 38 }); // 39 // 40 //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// }).call(this); (function(){ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // // packages/mongo/collection.js // // // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // options.connection, if given, is a LivedataClient or LivedataServer // 1 // XXX presently there is no way to destroy/clean up a Collection // 2 // 3 /** // 4 * @summary Namespace for MongoDB-related items // 5 * @namespace // 6 */ // 7 Mongo = {}; // 8 // 9 /** // 10 * @summary Constructor for a Collection // 11 * @locus Anywhere // 12 * @instancename collection // 13 * @class // 14 * @param {String} name The name of the collection. If null, creates an unmanaged (unsynchronized) local collection. * @param {Object} [options] // 16 * @param {Object} options.connection The server connection that will manage this collection. Uses the default connection if not specified. Pass the return value of calling [`DDP.connect`](#ddp_connect) to specify a different server. Pass `null` to specify no connection. Unmanaged (`name` is null) collections cannot specify a connection. * @param {String} options.idGeneration The method of generating the `_id` fields of new documents in this collection. Possible values: // 19 - **`'STRING'`**: random strings // 20 - **`'MONGO'`**: random [`Mongo.ObjectID`](#mongo_object_id) values // 21 // 22 The default id generation technique is `'STRING'`. // 23 * @param {Function} options.transform An optional transformation function. Documents will be passed through this function before being returned from `fetch` or `findOne`, and before being passed to callbacks of `observe`, `map`, `forEach`, `allow`, and `deny`. Transforms are *not* applied for the callbacks of `observeChanges` or to cursors returned from publish functions. */ // 25 Mongo.Collection = function (name, options) { // 26 var self = this; // 27 if (! (self instanceof Mongo.Collection)) // 28 throw new Error('use "new" to construct a Mongo.Collection'); // 29 // 30 if (!name && (name !== null)) { // 31 Meteor._debug("Warning: creating anonymous collection. It will not be " + // 32 "saved or synchronized over the network. (Pass null for " + // 33 "the collection name to turn off this warning.)"); // 34 name = null; // 35 } // 36 // 37 if (name !== null && typeof name !== "string") { // 38 throw new Error( // 39 "First argument to new Mongo.Collection must be a string or null"); // 40 } // 41 // 42 if (options && options.methods) { // 43 // Backwards compatibility hack with original signature (which passed // 44 // "connection" directly instead of in options. (Connections must have a "methods" // 45 // method.) // 46 // XXX remove before 1.0 // 47 options = {connection: options}; // 48 } // 49 // Backwards compatibility: "connection" used to be called "manager". // 50 if (options && options.manager && !options.connection) { // 51 options.connection = options.manager; // 52 } // 53 options = _.extend({ // 54 connection: undefined, // 55 idGeneration: 'STRING', // 56 transform: null, // 57 _driver: undefined, // 58 _preventAutopublish: false // 59 }, options); // 60 // 61 switch (options.idGeneration) { // 62 case 'MONGO': // 63 self._makeNewID = function () { // 64 var src = name // 65 ? DDP.randomStream('/collection/' + name) // 66 : Random.insecure; // 67 return new Mongo.ObjectID(src.hexString(24)); // 68 }; // 69 break; // 70 case 'STRING': // 71 default: // 72 self._makeNewID = function () { // 73 var src = name // 74 ? DDP.randomStream('/collection/' + name) // 75 : Random.insecure; // 76 return src.id(); // 77 }; // 78 break; // 79 } // 80 // 81 self._transform = LocalCollection.wrapTransform(options.transform); // 82 // 83 if (! name || options.connection === null) // 84 // note: nameless collections never have a connection // 85 self._connection = null; // 86 else if (options.connection) // 87 self._connection = options.connection; // 88 else if (Meteor.isClient) // 89 self._connection = Meteor.connection; // 90 else // 91 self._connection = Meteor.server; // 92 // 93 if (!options._driver) { // 94 // XXX This check assumes that webapp is loaded so that Meteor.server !== // 95 // null. We should fully support the case of "want to use a Mongo-backed // 96 // collection from Node code without webapp", but we don't yet. // 97 // #MeteorServerNull // 98 if (name && self._connection === Meteor.server && // 99 typeof MongoInternals !== "undefined" && // 100 MongoInternals.defaultRemoteCollectionDriver) { // 101 options._driver = MongoInternals.defaultRemoteCollectionDriver(); // 102 } else { // 103 options._driver = LocalCollectionDriver; // 104 } // 105 } // 106 // 107 self._collection = options._driver.open(name, self._connection); // 108 self._name = name; // 109 self._driver = options._driver; // 110 // 111 if (self._connection && self._connection.registerStore) { // 112 // OK, we're going to be a slave, replicating some remote // 113 // database, except possibly with some temporary divergence while // 114 // we have unacknowledged RPC's. // 115 var ok = self._connection.registerStore(name, { // 116 // Called at the beginning of a batch of updates. batchSize is the number // 117 // of update calls to expect. // 118 // // 119 // XXX This interface is pretty janky. reset probably ought to go back to // 120 // being its own function, and callers shouldn't have to calculate // 121 // batchSize. The optimization of not calling pause/remove should be // 122 // delayed until later: the first call to update() should buffer its // 123 // message, and then we can either directly apply it at endUpdate time if // 124 // it was the only update, or do pauseObservers/apply/apply at the next // 125 // update() if there's another one. // 126 beginUpdate: function (batchSize, reset) { // 127 // pause observers so users don't see flicker when updating several // 128 // objects at once (including the post-reconnect reset-and-reapply // 129 // stage), and so that a re-sorting of a query can take advantage of the // 130 // full _diffQuery moved calculation instead of applying change one at a // 131 // time. // 132 if (batchSize > 1 || reset) // 133 self._collection.pauseObservers(); // 134 // 135 if (reset) // 136 self._collection.remove({}); // 137 }, // 138 // 139 // Apply an update. // 140 // XXX better specify this interface (not in terms of a wire message)? // 141 update: function (msg) { // 142 var mongoId = MongoID.idParse(msg.id); // 143 var doc = self._collection.findOne(mongoId); // 144 // 145 // Is this a "replace the whole doc" message coming from the quiescence // 146 // of method writes to an object? (Note that 'undefined' is a valid // 147 // value meaning "remove it".) // 148 if (msg.msg === 'replace') { // 149 var replace = msg.replace; // 150 if (!replace) { // 151 if (doc) // 152 self._collection.remove(mongoId); // 153 } else if (!doc) { // 154 self._collection.insert(replace); // 155 } else { // 156 // XXX check that replace has no $ ops // 157 self._collection.update(mongoId, replace); // 158 } // 159 return; // 160 } else if (msg.msg === 'added') { // 161 if (doc) { // 162 throw new Error("Expected not to find a document already present for an add"); // 163 } // 164 self._collection.insert(_.extend({_id: mongoId}, msg.fields)); // 165 } else if (msg.msg === 'removed') { // 166 if (!doc) // 167 throw new Error("Expected to find a document already present for removed"); // 168 self._collection.remove(mongoId); // 169 } else if (msg.msg === 'changed') { // 170 if (!doc) // 171 throw new Error("Expected to find a document to change"); // 172 if (!_.isEmpty(msg.fields)) { // 173 var modifier = {}; // 174 _.each(msg.fields, function (value, key) { // 175 if (value === undefined) { // 176 if (!modifier.$unset) // 177 modifier.$unset = {}; // 178 modifier.$unset[key] = 1; // 179 } else { // 180 if (!modifier.$set) // 181 modifier.$set = {}; // 182 modifier.$set[key] = value; // 183 } // 184 }); // 185 self._collection.update(mongoId, modifier); // 186 } // 187 } else { // 188 throw new Error("I don't know how to deal with this message"); // 189 } // 190 // 191 }, // 192 // 193 // Called at the end of a batch of updates. // 194 endUpdate: function () { // 195 self._collection.resumeObservers(); // 196 }, // 197 // 198 // Called around method stub invocations to capture the original versions // 199 // of modified documents. // 200 saveOriginals: function () { // 201 self._collection.saveOriginals(); // 202 }, // 203 retrieveOriginals: function () { // 204 return self._collection.retrieveOriginals(); // 205 }, // 206 // 207 // Used to preserve current versions of documents across a store reset. // 208 getDoc: function(id) { // 209 return self.findOne(id); // 210 }, // 211 }); // 212 // 213 if (!ok) // 214 throw new Error("There is already a collection named '" + name + "'"); // 215 } // 216 // 217 self._defineMutationMethods(); // 218 // 219 // autopublish // 220 if (Package.autopublish && !options._preventAutopublish && self._connection // 221 && self._connection.publish) { // 222 self._connection.publish(null, function () { // 223 return self.find(); // 224 }, {is_auto: true}); // 225 } // 226 }; // 227 // 228 /// // 229 /// Main collection API // 230 /// // 231 // 232 // 233 _.extend(Mongo.Collection.prototype, { // 234 // 235 _getFindSelector: function (args) { // 236 if (args.length == 0) // 237 return {}; // 238 else // 239 return args[0]; // 240 }, // 241 // 242 _getFindOptions: function (args) { // 243 var self = this; // 244 if (args.length < 2) { // 245 return { transform: self._transform }; // 246 } else { // 247 check(args[1], Match.Optional(Match.ObjectIncluding({ // 248 fields: Match.Optional(Match.OneOf(Object, undefined)), // 249 sort: Match.Optional(Match.OneOf(Object, Array, undefined)), // 250 limit: Match.Optional(Match.OneOf(Number, undefined)), // 251 skip: Match.Optional(Match.OneOf(Number, undefined)) // 252 }))); // 253 // 254 return _.extend({ // 255 transform: self._transform // 256 }, args[1]); // 257 } // 258 }, // 259 // 260 /** // 261 * @summary Find the documents in a collection that match the selector. // 262 * @locus Anywhere // 263 * @method find // 264 * @memberOf Mongo.Collection // 265 * @instance // 266 * @param {MongoSelector} [selector] A query describing the documents to find // 267 * @param {Object} [options] // 268 * @param {MongoSortSpecifier} options.sort Sort order (default: natural order) // 269 * @param {Number} options.skip Number of results to skip at the beginning // 270 * @param {Number} options.limit Maximum number of results to return // 271 * @param {MongoFieldSpecifier} options.fields Dictionary of fields to return or exclude. // 272 * @param {Boolean} options.reactive (Client only) Default `true`; pass `false` to disable reactivity // 273 * @param {Function} options.transform Overrides `transform` on the [`Collection`](#collections) for this cursor. Pass `null` to disable transformation. * @returns {Mongo.Cursor} // 275 */ // 276 find: function (/* selector, options */) { // 277 // Collection.find() (return all docs) behaves differently // 278 // from Collection.find(undefined) (return 0 docs). so be // 279 // careful about the length of arguments. // 280 var self = this; // 281 var argArray = _.toArray(arguments); // 282 return self._collection.find(self._getFindSelector(argArray), // 283 self._getFindOptions(argArray)); // 284 }, // 285 // 286 /** // 287 * @summary Finds the first document that matches the selector, as ordered by sort and skip options. // 288 * @locus Anywhere // 289 * @method findOne // 290 * @memberOf Mongo.Collection // 291 * @instance // 292 * @param {MongoSelector} [selector] A query describing the documents to find // 293 * @param {Object} [options] // 294 * @param {MongoSortSpecifier} options.sort Sort order (default: natural order) // 295 * @param {Number} options.skip Number of results to skip at the beginning // 296 * @param {MongoFieldSpecifier} options.fields Dictionary of fields to return or exclude. // 297 * @param {Boolean} options.reactive (Client only) Default true; pass false to disable reactivity // 298 * @param {Function} options.transform Overrides `transform` on the [`Collection`](#collections) for this cursor. Pass `null` to disable transformation. * @returns {Object} // 300 */ // 301 findOne: function (/* selector, options */) { // 302 var self = this; // 303 var argArray = _.toArray(arguments); // 304 return self._collection.findOne(self._getFindSelector(argArray), // 305 self._getFindOptions(argArray)); // 306 } // 307 // 308 }); // 309 // 310 Mongo.Collection._publishCursor = function (cursor, sub, collection) { // 311 var observeHandle = cursor.observeChanges({ // 312 added: function (id, fields) { // 313 sub.added(collection, id, fields); // 314 }, // 315 changed: function (id, fields) { // 316 sub.changed(collection, id, fields); // 317 }, // 318 removed: function (id) { // 319 sub.removed(collection, id); // 320 } // 321 }); // 322 // 323 // We don't call sub.ready() here: it gets called in livedata_server, after // 324 // possibly calling _publishCursor on multiple returned cursors. // 325 // 326 // register stop callback (expects lambda w/ no args). // 327 sub.onStop(function () {observeHandle.stop();}); // 328 }; // 329 // 330 // protect against dangerous selectors. falsey and {_id: falsey} are both // 331 // likely programmer error, and not what you want, particularly for destructive // 332 // operations. JS regexps don't serialize over DDP but can be trivially // 333 // replaced by $regex. // 334 Mongo.Collection._rewriteSelector = function (selector) { // 335 // shorthand -- scalars match _id // 336 if (LocalCollection._selectorIsId(selector)) // 337 selector = {_id: selector}; // 338 // 339 if (_.isArray(selector)) { // 340 // This is consistent with the Mongo console itself; if we don't do this // 341 // check passing an empty array ends up selecting all items // 342 throw new Error("Mongo selector can't be an array."); // 343 } // 344 // 345 if (!selector || (('_id' in selector) && !selector._id)) // 346 // can't match anything // 347 return {_id: Random.id()}; // 348 // 349 var ret = {}; // 350 _.each(selector, function (value, key) { // 351 // Mongo supports both {field: /foo/} and {field: {$regex: /foo/}} // 352 if (value instanceof RegExp) { // 353 ret[key] = convertRegexpToMongoSelector(value); // 354 } else if (value && value.$regex instanceof RegExp) { // 355 ret[key] = convertRegexpToMongoSelector(value.$regex); // 356 // if value is {$regex: /foo/, $options: ...} then $options // 357 // override the ones set on $regex. // 358 if (value.$options !== undefined) // 359 ret[key].$options = value.$options; // 360 } // 361 else if (_.contains(['$or','$and','$nor'], key)) { // 362 // Translate lower levels of $and/$or/$nor // 363 ret[key] = _.map(value, function (v) { // 364 return Mongo.Collection._rewriteSelector(v); // 365 }); // 366 } else { // 367 ret[key] = value; // 368 } // 369 }); // 370 return ret; // 371 }; // 372 // 373 // convert a JS RegExp object to a Mongo {$regex: ..., $options: ...} // 374 // selector // 375 var convertRegexpToMongoSelector = function (regexp) { // 376 check(regexp, RegExp); // safety belt // 377 // 378 var selector = {$regex: regexp.source}; // 379 var regexOptions = ''; // 380 // JS RegExp objects support 'i', 'm', and 'g'. Mongo regex $options // 381 // support 'i', 'm', 'x', and 's'. So we support 'i' and 'm' here. // 382 if (regexp.ignoreCase) // 383 regexOptions += 'i'; // 384 if (regexp.multiline) // 385 regexOptions += 'm'; // 386 if (regexOptions) // 387 selector.$options = regexOptions; // 388 // 389 return selector; // 390 }; // 391 // 392 var throwIfSelectorIsNotId = function (selector, methodName) { // 393 if (!LocalCollection._selectorIsIdPerhapsAsObject(selector)) { // 394 throw new Meteor.Error( // 395 403, "Not permitted. Untrusted code may only " + methodName + // 396 " documents by ID."); // 397 } // 398 }; // 399 // 400 // 'insert' immediately returns the inserted document's new _id. // 401 // The others return values immediately if you are in a stub, an in-memory // 402 // unmanaged collection, or a mongo-backed collection and you don't pass a // 403 // callback. 'update' and 'remove' return the number of affected // 404 // documents. 'upsert' returns an object with keys 'numberAffected' and, if an // 405 // insert happened, 'insertedId'. // 406 // // 407 // Otherwise, the semantics are exactly like other methods: they take // 408 // a callback as an optional last argument; if no callback is // 409 // provided, they block until the operation is complete, and throw an // 410 // exception if it fails; if a callback is provided, then they don't // 411 // necessarily block, and they call the callback when they finish with error and // 412 // result arguments. (The insert method provides the document ID as its result; // 413 // update and remove provide the number of affected docs as the result; upsert // 414 // provides an object with numberAffected and maybe insertedId.) // 415 // // 416 // On the client, blocking is impossible, so if a callback // 417 // isn't provided, they just return immediately and any error // 418 // information is lost. // 419 // // 420 // There's one more tweak. On the client, if you don't provide a // 421 // callback, then if there is an error, a message will be logged with // 422 // Meteor._debug. // 423 // // 424 // The intent (though this is actually determined by the underlying // 425 // drivers) is that the operations should be done synchronously, not // 426 // generating their result until the database has acknowledged // 427 // them. In the future maybe we should provide a flag to turn this // 428 // off. // 429 // 430 /** // 431 * @summary Insert a document in the collection. Returns its unique _id. // 432 * @locus Anywhere // 433 * @method insert // 434 * @memberOf Mongo.Collection // 435 * @instance // 436 * @param {Object} doc The document to insert. May not yet have an _id attribute, in which case Meteor will generate one for you. * @param {Function} [callback] Optional. If present, called with an error object as the first argument and, if no error, the _id as the second. */ // 439 // 440 /** // 441 * @summary Modify one or more documents in the collection. Returns the number of affected documents. // 442 * @locus Anywhere // 443 * @method update // 444 * @memberOf Mongo.Collection // 445 * @instance // 446 * @param {MongoSelector} selector Specifies which documents to modify // 447 * @param {MongoModifier} modifier Specifies how to modify the documents // 448 * @param {Object} [options] // 449 * @param {Boolean} options.multi True to modify all matching documents; false to only modify one of the matching documents (the default). * @param {Boolean} options.upsert True to insert a document if no matching documents are found. // 451 * @param {Function} [callback] Optional. If present, called with an error object as the first argument and, if no error, the number of affected documents as the second. */ // 453 // 454 /** // 455 * @summary Remove documents from the collection // 456 * @locus Anywhere // 457 * @method remove // 458 * @memberOf Mongo.Collection // 459 * @instance // 460 * @param {MongoSelector} selector Specifies which documents to remove // 461 * @param {Function} [callback] Optional. If present, called with an error object as its argument. // 462 */ // 463 // 464 _.each(["insert", "update", "remove"], function (name) { // 465 Mongo.Collection.prototype[name] = function (/* arguments */) { // 466 var self = this; // 467 var args = _.toArray(arguments); // 468 var callback; // 469 var insertId; // 470 var ret; // 471 // 472 // Pull off any callback (or perhaps a 'callback' variable that was passed // 473 // in undefined, like how 'upsert' does it). // 474 if (args.length && // 475 (args[args.length - 1] === undefined || // 476 args[args.length - 1] instanceof Function)) { // 477 callback = args.pop(); // 478 } // 479 // 480 if (name === "insert") { // 481 if (!args.length) // 482 throw new Error("insert requires an argument"); // 483 // shallow-copy the document and generate an ID // 484 args[0] = _.extend({}, args[0]); // 485 if ('_id' in args[0]) { // 486 insertId = args[0]._id; // 487 if (!insertId || !(typeof insertId === 'string' // 488 || insertId instanceof Mongo.ObjectID)) // 489 throw new Error("Meteor requires document _id fields to be non-empty strings or ObjectIDs"); // 490 } else { // 491 var generateId = true; // 492 // Don't generate the id if we're the client and the 'outermost' call // 493 // This optimization saves us passing both the randomSeed and the id // 494 // Passing both is redundant. // 495 if (self._connection && self._connection !== Meteor.server) { // 496 var enclosing = DDP._CurrentInvocation.get(); // 497 if (!enclosing) { // 498 generateId = false; // 499 } // 500 } // 501 if (generateId) { // 502 insertId = args[0]._id = self._makeNewID(); // 503 } // 504 } // 505 } else { // 506 args[0] = Mongo.Collection._rewriteSelector(args[0]); // 507 // 508 if (name === "update") { // 509 // Mutate args but copy the original options object. We need to add // 510 // insertedId to options, but don't want to mutate the caller's options // 511 // object. We need to mutate `args` because we pass `args` into the // 512 // driver below. // 513 var options = args[2] = _.clone(args[2]) || {}; // 514 if (options && typeof options !== "function" && options.upsert) { // 515 // set `insertedId` if absent. `insertedId` is a Meteor extension. // 516 if (options.insertedId) { // 517 if (!(typeof options.insertedId === 'string' // 518 || options.insertedId instanceof Mongo.ObjectID)) // 519 throw new Error("insertedId must be string or ObjectID"); // 520 } else if (! args[0]._id) { // 521 options.insertedId = self._makeNewID(); // 522 } // 523 } // 524 } // 525 } // 526 // 527 // On inserts, always return the id that we generated; on all other // 528 // operations, just return the result from the collection. // 529 var chooseReturnValueFromCollectionResult = function (result) { // 530 if (name === "insert") { // 531 if (!insertId && result) { // 532 insertId = result; // 533 } // 534 return insertId; // 535 } else { // 536 return result; // 537 } // 538 }; // 539 // 540 var wrappedCallback; // 541 if (callback) { // 542 wrappedCallback = function (error, result) { // 543 callback(error, ! error && chooseReturnValueFromCollectionResult(result)); // 544 }; // 545 } // 546 // 547 // XXX see #MeteorServerNull // 548 if (self._connection && self._connection !== Meteor.server) { // 549 // just remote to another endpoint, propagate return value or // 550 // exception. // 551 // 552 var enclosing = DDP._CurrentInvocation.get(); // 553 var alreadyInSimulation = enclosing && enclosing.isSimulation; // 554 // 555 if (Meteor.isClient && !wrappedCallback && ! alreadyInSimulation) { // 556 // Client can't block, so it can't report errors by exception, // 557 // only by callback. If they forget the callback, give them a // 558 // default one that logs the error, so they aren't totally // 559 // baffled if their writes don't work because their database is // 560 // down. // 561 // Don't give a default callback in simulation, because inside stubs we // 562 // want to return the results from the local collection immediately and // 563 // not force a callback. // 564 wrappedCallback = function (err) { // 565 if (err) // 566 Meteor._debug(name + " failed: " + (err.reason || err.stack)); // 567 }; // 568 } // 569 // 570 if (!alreadyInSimulation && name !== "insert") { // 571 // If we're about to actually send an RPC, we should throw an error if // 572 // this is a non-ID selector, because the mutation methods only allow // 573 // single-ID selectors. (If we don't throw here, we'll see flicker.) // 574 throwIfSelectorIsNotId(args[0], name); // 575 } // 576 // 577 ret = chooseReturnValueFromCollectionResult( // 578 self._connection.apply(self._prefix + name, args, {returnStubValue: true}, wrappedCallback) // 579 ); // 580 // 581 } else { // 582 // it's my collection. descend into the collection object // 583 // and propagate any exception. // 584 args.push(wrappedCallback); // 585 try { // 586 // If the user provided a callback and the collection implements this // 587 // operation asynchronously, then queryRet will be undefined, and the // 588 // result will be returned through the callback instead. // 589 var queryRet = self._collection[name].apply(self._collection, args); // 590 ret = chooseReturnValueFromCollectionResult(queryRet); // 591 } catch (e) { // 592 if (callback) { // 593 callback(e); // 594 return null; // 595 } // 596 throw e; // 597 } // 598 } // 599 // 600 // both sync and async, unless we threw an exception, return ret // 601 // (new document ID for insert, num affected for update/remove, object with // 602 // numberAffected and maybe insertedId for upsert). // 603 return ret; // 604 }; // 605 }); // 606 // 607 /** // 608 * @summary Modify one or more documents in the collection, or insert one if no matching documents were found. Returns an object with keys `numberAffected` (the number of documents modified) and `insertedId` (the unique _id of the document that was inserted, if any). * @locus Anywhere // 610 * @param {MongoSelector} selector Specifies which documents to modify // 611 * @param {MongoModifier} modifier Specifies how to modify the documents // 612 * @param {Object} [options] // 613 * @param {Boolean} options.multi True to modify all matching documents; false to only modify one of the matching documents (the default). * @param {Function} [callback] Optional. If present, called with an error object as the first argument and, if no error, the number of affected documents as the second. */ // 616 Mongo.Collection.prototype.upsert = function (selector, modifier, // 617 options, callback) { // 618 var self = this; // 619 if (! callback && typeof options === "function") { // 620 callback = options; // 621 options = {}; // 622 } // 623 return self.update(selector, modifier, // 624 _.extend({}, options, { _returnObject: true, upsert: true }), // 625 callback); // 626 }; // 627 // 628 // We'll actually design an index API later. For now, we just pass through to // 629 // Mongo's, but make it synchronous. // 630 Mongo.Collection.prototype._ensureIndex = function (index, options) { // 631 var self = this; // 632 if (!self._collection._ensureIndex) // 633 throw new Error("Can only call _ensureIndex on server collections"); // 634 self._collection._ensureIndex(index, options); // 635 }; // 636 Mongo.Collection.prototype._dropIndex = function (index) { // 637 var self = this; // 638 if (!self._collection._dropIndex) // 639 throw new Error("Can only call _dropIndex on server collections"); // 640 self._collection._dropIndex(index); // 641 }; // 642 Mongo.Collection.prototype._dropCollection = function () { // 643 var self = this; // 644 if (!self._collection.dropCollection) // 645 throw new Error("Can only call _dropCollection on server collections"); // 646 self._collection.dropCollection(); // 647 }; // 648 Mongo.Collection.prototype._createCappedCollection = function (byteSize, maxDocuments) { // 649 var self = this; // 650 if (!self._collection._createCappedCollection) // 651 throw new Error("Can only call _createCappedCollection on server collections"); // 652 self._collection._createCappedCollection(byteSize, maxDocuments); // 653 }; // 654 // 655 /** // 656 * @summary Returns the [`Collection`](http://mongodb.github.io/node-mongodb-native/1.4/api-generated/collection.html) object corresponding to this collection from the [npm `mongodb` driver module](https://www.npmjs.com/package/mongodb) which is wrapped by `Mongo.Collection`. * @locus Server // 658 */ // 659 Mongo.Collection.prototype.rawCollection = function () { // 660 var self = this; // 661 if (! self._collection.rawCollection) { // 662 throw new Error("Can only call rawCollection on server collections"); // 663 } // 664 return self._collection.rawCollection(); // 665 }; // 666 // 667 /** // 668 * @summary Returns the [`Db`](http://mongodb.github.io/node-mongodb-native/1.4/api-generated/db.html) object corresponding to this collection's database connection from the [npm `mongodb` driver module](https://www.npmjs.com/package/mongodb) which is wrapped by `Mongo.Collection`. * @locus Server // 670 */ // 671 Mongo.Collection.prototype.rawDatabase = function () { // 672 var self = this; // 673 if (! (self._driver.mongo && self._driver.mongo.db)) { // 674 throw new Error("Can only call rawDatabase on server collections"); // 675 } // 676 return self._driver.mongo.db; // 677 }; // 678 // 679 // 680 /** // 681 * @summary Create a Mongo-style `ObjectID`. If you don't specify a `hexString`, the `ObjectID` will generated randomly (not using MongoDB's ID construction rules). * @locus Anywhere // 683 * @class // 684 * @param {String} [hexString] Optional. The 24-character hexadecimal contents of the ObjectID to create // 685 */ // 686 Mongo.ObjectID = MongoID.ObjectID; // 687 // 688 /** // 689 * @summary To create a cursor, use find. To access the documents in a cursor, use forEach, map, or fetch. // 690 * @class // 691 * @instanceName cursor // 692 */ // 693 Mongo.Cursor = LocalCollection.Cursor; // 694 // 695 /** // 696 * @deprecated in 0.9.1 // 697 */ // 698 Mongo.Collection.Cursor = Mongo.Cursor; // 699 // 700 /** // 701 * @deprecated in 0.9.1 // 702 */ // 703 Mongo.Collection.ObjectID = Mongo.ObjectID; // 704 // 705 /// // 706 /// Remote methods and access control. // 707 /// // 708 // 709 // Restrict default mutators on collection. allow() and deny() take the // 710 // same options: // 711 // // 712 // options.insert {Function(userId, doc)} // 713 // return true to allow/deny adding this document // 714 // // 715 // options.update {Function(userId, docs, fields, modifier)} // 716 // return true to allow/deny updating these documents. // 717 // `fields` is passed as an array of fields that are to be modified // 718 // // 719 // options.remove {Function(userId, docs)} // 720 // return true to allow/deny removing these documents // 721 // // 722 // options.fetch {Array} // 723 // Fields to fetch for these validators. If any call to allow or deny // 724 // does not have this option then all fields are loaded. // 725 // // 726 // allow and deny can be called multiple times. The validators are // 727 // evaluated as follows: // 728 // - If neither deny() nor allow() has been called on the collection, // 729 // then the request is allowed if and only if the "insecure" smart // 730 // package is in use. // 731 // - Otherwise, if any deny() function returns true, the request is denied. // 732 // - Otherwise, if any allow() function returns true, the request is allowed. // 733 // - Otherwise, the request is denied. // 734 // // 735 // Meteor may call your deny() and allow() functions in any order, and may not // 736 // call all of them if it is able to make a decision without calling them all // 737 // (so don't include side effects). // 738 // 739 (function () { // 740 var addValidator = function(allowOrDeny, options) { // 741 // validate keys // 742 var VALID_KEYS = ['insert', 'update', 'remove', 'fetch', 'transform']; // 743 _.each(_.keys(options), function (key) { // 744 if (!_.contains(VALID_KEYS, key)) // 745 throw new Error(allowOrDeny + ": Invalid key: " + key); // 746 }); // 747 // 748 var self = this; // 749 self._restricted = true; // 750 // 751 _.each(['insert', 'update', 'remove'], function (name) { // 752 if (options.hasOwnProperty(name)) { // 753 if (!(options[name] instanceof Function)) { // 754 throw new Error(allowOrDeny + ": Value for `" + name + "` must be a function"); // 755 } // 756 // 757 // If the transform is specified at all (including as 'null') in this // 758 // call, then take that; otherwise, take the transform from the // 759 // collection. // 760 if (options.transform === undefined) { // 761 options[name].transform = self._transform; // already wrapped // 762 } else { // 763 options[name].transform = LocalCollection.wrapTransform( // 764 options.transform); // 765 } // 766 // 767 self._validators[name][allowOrDeny].push(options[name]); // 768 } // 769 }); // 770 // 771 // Only update the fetch fields if we're passed things that affect // 772 // fetching. This way allow({}) and allow({insert: f}) don't result in // 773 // setting fetchAllFields // 774 if (options.update || options.remove || options.fetch) { // 775 if (options.fetch && !(options.fetch instanceof Array)) { // 776 throw new Error(allowOrDeny + ": Value for `fetch` must be an array"); // 777 } // 778 self._updateFetch(options.fetch); // 779 } // 780 }; // 781 // 782 /** // 783 * @summary Allow users to write directly to this collection from client code, subject to limitations you define. // 784 * @locus Server // 785 * @param {Object} options // 786 * @param {Function} options.insert,update,remove Functions that look at a proposed modification to the database and return true if it should be allowed. * @param {String[]} options.fetch Optional performance enhancement. Limits the fields that will be fetched from the database for inspection by your `update` and `remove` functions. * @param {Function} options.transform Overrides `transform` on the [`Collection`](#collections). Pass `null` to disable transformation. */ // 790 Mongo.Collection.prototype.allow = function(options) { // 791 addValidator.call(this, 'allow', options); // 792 }; // 793 // 794 /** // 795 * @summary Override `allow` rules. // 796 * @locus Server // 797 * @param {Object} options // 798 * @param {Function} options.insert,update,remove Functions that look at a proposed modification to the database and return true if it should be denied, even if an [allow](#allow) rule says otherwise. * @param {String[]} options.fetch Optional performance enhancement. Limits the fields that will be fetched from the database for inspection by your `update` and `remove` functions. * @param {Function} options.transform Overrides `transform` on the [`Collection`](#collections). Pass `null` to disable transformation. */ // 802 Mongo.Collection.prototype.deny = function(options) { // 803 addValidator.call(this, 'deny', options); // 804 }; // 805 })(); // 806 // 807 // 808 Mongo.Collection.prototype._defineMutationMethods = function() { // 809 var self = this; // 810 // 811 // set to true once we call any allow or deny methods. If true, use // 812 // allow/deny semantics. If false, use insecure mode semantics. // 813 self._restricted = false; // 814 // 815 // Insecure mode (default to allowing writes). Defaults to 'undefined' which // 816 // means insecure iff the insecure package is loaded. This property can be // 817 // overriden by tests or packages wishing to change insecure mode behavior of // 818 // their collections. // 819 self._insecure = undefined; // 820 // 821 self._validators = { // 822 insert: {allow: [], deny: []}, // 823 update: {allow: [], deny: []}, // 824 remove: {allow: [], deny: []}, // 825 upsert: {allow: [], deny: []}, // dummy arrays; can't set these! // 826 fetch: [], // 827 fetchAllFields: false // 828 }; // 829 // 830 if (!self._name) // 831 return; // anonymous collection // 832 // 833 // XXX Think about method namespacing. Maybe methods should be // 834 // "Meteor:Mongo:insert/NAME"? // 835 self._prefix = '/' + self._name + '/'; // 836 // 837 // mutation methods // 838 if (self._connection) { // 839 var m = {}; // 840 // 841 _.each(['insert', 'update', 'remove'], function (method) { // 842 m[self._prefix + method] = function (/* ... */) { // 843 // All the methods do their own validation, instead of using check(). // 844 check(arguments, [Match.Any]); // 845 var args = _.toArray(arguments); // 846 try { // 847 // For an insert, if the client didn't specify an _id, generate one // 848 // now; because this uses DDP.randomStream, it will be consistent with // 849 // what the client generated. We generate it now rather than later so // 850 // that if (eg) an allow/deny rule does an insert to the same // 851 // collection (not that it really should), the generated _id will // 852 // still be the first use of the stream and will be consistent. // 853 // // 854 // However, we don't actually stick the _id onto the document yet, // 855 // because we want allow/deny rules to be able to differentiate // 856 // between arbitrary client-specified _id fields and merely // 857 // client-controlled-via-randomSeed fields. // 858 var generatedId = null; // 859 if (method === "insert" && !_.has(args[0], '_id')) { // 860 generatedId = self._makeNewID(); // 861 } // 862 // 863 if (this.isSimulation) { // 864 // In a client simulation, you can do any mutation (even with a // 865 // complex selector). // 866 if (generatedId !== null) // 867 args[0]._id = generatedId; // 868 return self._collection[method].apply( // 869 self._collection, args); // 870 } // 871 // 872 // This is the server receiving a method call from the client. // 873 // 874 // We don't allow arbitrary selectors in mutations from the client: only // 875 // single-ID selectors. // 876 if (method !== 'insert') // 877 throwIfSelectorIsNotId(args[0], method); // 878 // 879 if (self._restricted) { // 880 // short circuit if there is no way it will pass. // 881 if (self._validators[method].allow.length === 0) { // 882 throw new Meteor.Error( // 883 403, "Access denied. No allow validators set on restricted " + // 884 "collection for method '" + method + "'."); // 885 } // 886 // 887 var validatedMethodName = // 888 '_validated' + method.charAt(0).toUpperCase() + method.slice(1); // 889 args.unshift(this.userId); // 890 method === 'insert' && args.push(generatedId); // 891 return self[validatedMethodName].apply(self, args); // 892 } else if (self._isInsecure()) { // 893 if (generatedId !== null) // 894 args[0]._id = generatedId; // 895 // In insecure mode, allow any mutation (with a simple selector). // 896 // XXX This is kind of bogus. Instead of blindly passing whatever // 897 // we get from the network to this function, we should actually // 898 // know the correct arguments for the function and pass just // 899 // them. For example, if you have an extraneous extra null // 900 // argument and this is Mongo on the server, the .wrapAsync'd // 901 // functions like update will get confused and pass the // 902 // "fut.resolver()" in the wrong slot, where _update will never // 903 // invoke it. Bam, broken DDP connection. Probably should just // 904 // take this whole method and write it three times, invoking // 905 // helpers for the common code. // 906 return self._collection[method].apply(self._collection, args); // 907 } else { // 908 // In secure mode, if we haven't called allow or deny, then nothing // 909 // is permitted. // 910 throw new Meteor.Error(403, "Access denied"); // 911 } // 912 } catch (e) { // 913 if (e.name === 'MongoError' || e.name === 'MinimongoError') { // 914 throw new Meteor.Error(409, e.toString()); // 915 } else { // 916 throw e; // 917 } // 918 } // 919 }; // 920 }); // 921 // Minimongo on the server gets no stubs; instead, by default // 922 // it wait()s until its result is ready, yielding. // 923 // This matches the behavior of macromongo on the server better. // 924 // XXX see #MeteorServerNull // 925 if (Meteor.isClient || self._connection === Meteor.server) // 926 self._connection.methods(m); // 927 } // 928 }; // 929 // 930 // 931 Mongo.Collection.prototype._updateFetch = function (fields) { // 932 var self = this; // 933 // 934 if (!self._validators.fetchAllFields) { // 935 if (fields) { // 936 self._validators.fetch = _.union(self._validators.fetch, fields); // 937 } else { // 938 self._validators.fetchAllFields = true; // 939 // clear fetch just to make sure we don't accidentally read it // 940 self._validators.fetch = null; // 941 } // 942 } // 943 }; // 944 // 945 Mongo.Collection.prototype._isInsecure = function () { // 946 var self = this; // 947 if (self._insecure === undefined) // 948 return !!Package.insecure; // 949 return self._insecure; // 950 }; // 951 // 952 var docToValidate = function (validator, doc, generatedId) { // 953 var ret = doc; // 954 if (validator.transform) { // 955 ret = EJSON.clone(doc); // 956 // If you set a server-side transform on your collection, then you don't get // 957 // to tell the difference between "client specified the ID" and "server // 958 // generated the ID", because transforms expect to get _id. If you want to // 959 // do that check, you can do it with a specific // 960 // `C.allow({insert: f, transform: null})` validator. // 961 if (generatedId !== null) { // 962 ret._id = generatedId; // 963 } // 964 ret = validator.transform(ret); // 965 } // 966 return ret; // 967 }; // 968 // 969 Mongo.Collection.prototype._validatedInsert = function (userId, doc, // 970 generatedId) { // 971 var self = this; // 972 // 973 // call user validators. // 974 // Any deny returns true means denied. // 975 if (_.any(self._validators.insert.deny, function(validator) { // 976 return validator(userId, docToValidate(validator, doc, generatedId)); // 977 })) { // 978 throw new Meteor.Error(403, "Access denied"); // 979 } // 980 // Any allow returns true means proceed. Throw error if they all fail. // 981 if (_.all(self._validators.insert.allow, function(validator) { // 982 return !validator(userId, docToValidate(validator, doc, generatedId)); // 983 })) { // 984 throw new Meteor.Error(403, "Access denied"); // 985 } // 986 // 987 // If we generated an ID above, insert it now: after the validation, but // 988 // before actually inserting. // 989 if (generatedId !== null) // 990 doc._id = generatedId; // 991 // 992 self._collection.insert.call(self._collection, doc); // 993 }; // 994 // 995 var transformDoc = function (validator, doc) { // 996 if (validator.transform) // 997 return validator.transform(doc); // 998 return doc; // 999 }; // 1000 // 1001 // Simulate a mongo `update` operation while validating that the access // 1002 // control rules set by calls to `allow/deny` are satisfied. If all // 1003 // pass, rewrite the mongo operation to use $in to set the list of // 1004 // document ids to change ##ValidatedChange // 1005 Mongo.Collection.prototype._validatedUpdate = function( // 1006 userId, selector, mutator, options) { // 1007 var self = this; // 1008 // 1009 check(mutator, Object); // 1010 // 1011 options = _.clone(options) || {}; // 1012 // 1013 if (!LocalCollection._selectorIsIdPerhapsAsObject(selector)) // 1014 throw new Error("validated update should be of a single ID"); // 1015 // 1016 // We don't support upserts because they don't fit nicely into allow/deny // 1017 // rules. // 1018 if (options.upsert) // 1019 throw new Meteor.Error(403, "Access denied. Upserts not " + // 1020 "allowed in a restricted collection."); // 1021 // 1022 var noReplaceError = "Access denied. In a restricted collection you can only" + // 1023 " update documents, not replace them. Use a Mongo update operator, such " + // 1024 "as '$set'."; // 1025 // 1026 // compute modified fields // 1027 var fields = []; // 1028 if (_.isEmpty(mutator)) { // 1029 throw new Meteor.Error(403, noReplaceError); // 1030 } // 1031 _.each(mutator, function (params, op) { // 1032 if (op.charAt(0) !== '$') { // 1033 throw new Meteor.Error(403, noReplaceError); // 1034 } else if (!_.has(ALLOWED_UPDATE_OPERATIONS, op)) { // 1035 throw new Meteor.Error( // 1036 403, "Access denied. Operator " + op + " not allowed in a restricted collection."); // 1037 } else { // 1038 _.each(_.keys(params), function (field) { // 1039 // treat dotted fields as if they are replacing their // 1040 // top-level part // 1041 if (field.indexOf('.') !== -1) // 1042 field = field.substring(0, field.indexOf('.')); // 1043 // 1044 // record the field we are trying to change // 1045 if (!_.contains(fields, field)) // 1046 fields.push(field); // 1047 }); // 1048 } // 1049 }); // 1050 // 1051 var findOptions = {transform: null}; // 1052 if (!self._validators.fetchAllFields) { // 1053 findOptions.fields = {}; // 1054 _.each(self._validators.fetch, function(fieldName) { // 1055 findOptions.fields[fieldName] = 1; // 1056 }); // 1057 } // 1058 // 1059 var doc = self._collection.findOne(selector, findOptions); // 1060 if (!doc) // none satisfied! // 1061 return 0; // 1062 // 1063 // call user validators. // 1064 // Any deny returns true means denied. // 1065 if (_.any(self._validators.update.deny, function(validator) { // 1066 var factoriedDoc = transformDoc(validator, doc); // 1067 return validator(userId, // 1068 factoriedDoc, // 1069 fields, // 1070 mutator); // 1071 })) { // 1072 throw new Meteor.Error(403, "Access denied"); // 1073 } // 1074 // Any allow returns true means proceed. Throw error if they all fail. // 1075 if (_.all(self._validators.update.allow, function(validator) { // 1076 var factoriedDoc = transformDoc(validator, doc); // 1077 return !validator(userId, // 1078 factoriedDoc, // 1079 fields, // 1080 mutator); // 1081 })) { // 1082 throw new Meteor.Error(403, "Access denied"); // 1083 } // 1084 // 1085 options._forbidReplace = true; // 1086 // 1087 // Back when we supported arbitrary client-provided selectors, we actually // 1088 // rewrote the selector to include an _id clause before passing to Mongo to // 1089 // avoid races, but since selector is guaranteed to already just be an ID, we // 1090 // don't have to any more. // 1091 // 1092 return self._collection.update.call( // 1093 self._collection, selector, mutator, options); // 1094 }; // 1095 // 1096 // Only allow these operations in validated updates. Specifically // 1097 // whitelist operations, rather than blacklist, so new complex // 1098 // operations that are added aren't automatically allowed. A complex // 1099 // operation is one that does more than just modify its target // 1100 // field. For now this contains all update operations except '$rename'. // 1101 // http://docs.mongodb.org/manual/reference/operators/#update // 1102 var ALLOWED_UPDATE_OPERATIONS = { // 1103 $inc:1, $set:1, $unset:1, $addToSet:1, $pop:1, $pullAll:1, $pull:1, // 1104 $pushAll:1, $push:1, $bit:1 // 1105 }; // 1106 // 1107 // Simulate a mongo `remove` operation while validating access control // 1108 // rules. See #ValidatedChange // 1109 Mongo.Collection.prototype._validatedRemove = function(userId, selector) { // 1110 var self = this; // 1111 // 1112 var findOptions = {transform: null}; // 1113 if (!self._validators.fetchAllFields) { // 1114 findOptions.fields = {}; // 1115 _.each(self._validators.fetch, function(fieldName) { // 1116 findOptions.fields[fieldName] = 1; // 1117 }); // 1118 } // 1119 // 1120 var doc = self._collection.findOne(selector, findOptions); // 1121 if (!doc) // 1122 return 0; // 1123 // 1124 // call user validators. // 1125 // Any deny returns true means denied. // 1126 if (_.any(self._validators.remove.deny, function(validator) { // 1127 return validator(userId, transformDoc(validator, doc)); // 1128 })) { // 1129 throw new Meteor.Error(403, "Access denied"); // 1130 } // 1131 // Any allow returns true means proceed. Throw error if they all fail. // 1132 if (_.all(self._validators.remove.allow, function(validator) { // 1133 return !validator(userId, transformDoc(validator, doc)); // 1134 })) { // 1135 throw new Meteor.Error(403, "Access denied"); // 1136 } // 1137 // 1138 // Back when we supported arbitrary client-provided selectors, we actually // 1139 // rewrote the selector to {_id: {$in: [ids that we found]}} before passing to // 1140 // Mongo to avoid races, but since selector is guaranteed to already just be // 1141 // an ID, we don't have to any more. // 1142 // 1143 return self._collection.remove.call(self._collection, selector); // 1144 }; // 1145 // 1146 /** // 1147 * @deprecated in 0.9.1 // 1148 */ // 1149 Meteor.Collection = Mongo.Collection; // 1150 // 1151 //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// }).call(this); /* Exports */ if (typeof Package === 'undefined') Package = {}; Package.mongo = { MongoInternals: MongoInternals, MongoTest: MongoTest, Mongo: Mongo }; })(); //# sourceMappingURL=mongo.js.map