1
0
Fork 0
mirror of https://github.com/YunoHost-Apps/rocketchat_ynh.git synced 2024-09-03 20:16:25 +02:00
rocketchat_ynh/sources/programs/server/packages/ddp-server.js
2016-04-29 16:32:48 +02:00

2147 lines
250 KiB
JavaScript

(function () {
/* Imports */
var Meteor = Package.meteor.Meteor;
var check = Package.check.check;
var Match = Package.check.Match;
var Random = Package.random.Random;
var EJSON = Package.ejson.EJSON;
var _ = Package.underscore._;
var Retry = Package.retry.Retry;
var MongoID = Package['mongo-id'].MongoID;
var DiffSequence = Package['diff-sequence'].DiffSequence;
var ECMAScript = Package.ecmascript.ECMAScript;
var DDPCommon = Package['ddp-common'].DDPCommon;
var DDP = Package['ddp-client'].DDP;
var WebApp = Package.webapp.WebApp;
var main = Package.webapp.main;
var WebAppInternals = Package.webapp.WebAppInternals;
var RoutePolicy = Package.routepolicy.RoutePolicy;
var Hook = Package['callback-hook'].Hook;
var LocalCollection = Package.minimongo.LocalCollection;
var Minimongo = Package.minimongo.Minimongo;
var babelHelpers = Package['babel-runtime'].babelHelpers;
var Symbol = Package['ecmascript-runtime'].Symbol;
var Map = Package['ecmascript-runtime'].Map;
var Set = Package['ecmascript-runtime'].Set;
var Promise = Package.promise.Promise;
/* Package-scope variables */
var StreamServer, DDPServer, Server;
(function(){
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //
// packages/ddp-server/stream_server.js //
// //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
var url = Npm.require('url'); // 1
//
// By default, we use the permessage-deflate extension with default //
// configuration. If $SERVER_WEBSOCKET_COMPRESSION is set, then it must be valid //
// JSON. If it represents a falsey value, then we do not use permessage-deflate //
// at all; otherwise, the JSON value is used as an argument to deflate's //
// configure method; see //
// https://github.com/faye/permessage-deflate-node/blob/master/README.md //
// //
// (We do this in an _.once instead of at startup, because we don't want to //
// crash the tool during isopacket load if your JSON doesn't parse. This is only //
// a problem because the tool has to load the DDP server code just in order to //
// be a DDP client; see https://github.com/meteor/meteor/issues/3452 .) //
var websocketExtensions = _.once(function () { // 14
var extensions = []; // 15
//
var websocketCompressionConfig = process.env.SERVER_WEBSOCKET_COMPRESSION ? JSON.parse(process.env.SERVER_WEBSOCKET_COMPRESSION) : {};
if (websocketCompressionConfig) { // 19
extensions.push(Npm.require('permessage-deflate').configure(websocketCompressionConfig)); // 20
} //
//
return extensions; // 25
}); //
//
var pathPrefix = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX || ""; // 28
//
StreamServer = function () { // 30
var self = this; // 31
self.registration_callbacks = []; // 32
self.open_sockets = []; // 33
//
// Because we are installing directly onto WebApp.httpServer instead of using //
// WebApp.app, we have to process the path prefix ourselves. //
self.prefix = pathPrefix + '/sockjs'; // 37
RoutePolicy.declare(self.prefix + '/', 'network'); // 38
//
// set up sockjs //
var sockjs = Npm.require('sockjs'); // 41
var serverOptions = { // 42
prefix: self.prefix, // 43
log: function () {}, // 44
// this is the default, but we code it explicitly because we depend //
// on it in stream_client:HEARTBEAT_TIMEOUT //
heartbeat_delay: 45000, // 47
// The default disconnect_delay is 5 seconds, but if the server ends up CPU //
// bound for that much time, SockJS might not notice that the user has //
// reconnected because the timer (of disconnect_delay ms) can fire before //
// SockJS processes the new connection. Eventually we'll fix this by not //
// combining CPU-heavy processing with SockJS termination (eg a proxy which //
// converts to Unix sockets) but for now, raise the delay. //
disconnect_delay: 60 * 1000, // 54
// Set the USE_JSESSIONID environment variable to enable setting the //
// JSESSIONID cookie. This is useful for setting up proxies with //
// session affinity. //
jsessionid: !!process.env.USE_JSESSIONID // 58
}; //
//
// If you know your server environment (eg, proxies) will prevent websockets //
// from ever working, set $DISABLE_WEBSOCKETS and SockJS clients (ie, //
// browsers) will not waste time attempting to use them. //
// (Your server will still have a /websocket endpoint.) //
if (process.env.DISABLE_WEBSOCKETS) { // 65
serverOptions.websocket = false; // 66
} else { //
serverOptions.faye_server_options = { // 68
extensions: websocketExtensions() // 69
}; //
} //
//
self.server = sockjs.createServer(serverOptions); // 73
//
// Install the sockjs handlers, but we want to keep around our own particular //
// request handler that adjusts idle timeouts while we have an outstanding //
// request. This compensates for the fact that sockjs removes all listeners //
// for "request" to add its own. //
WebApp.httpServer.removeListener('request', WebApp._timeoutAdjustmentRequestCallback); // 79
self.server.installHandlers(WebApp.httpServer); // 81
WebApp.httpServer.addListener('request', WebApp._timeoutAdjustmentRequestCallback); // 82
//
// Support the /websocket endpoint //
self._redirectWebsocketEndpoint(); // 86
//
self.server.on('connection', function (socket) { // 88
socket.send = function (data) { // 89
socket.write(data); // 90
}; //
socket.on('close', function () { // 92
self.open_sockets = _.without(self.open_sockets, socket); // 93
}); //
self.open_sockets.push(socket); // 95
//
// XXX COMPAT WITH 0.6.6. Send the old style welcome message, which //
// will force old clients to reload. Remove this once we're not //
// concerned about people upgrading from a pre-0.7.0 release. Also, //
// remove the clause in the client that ignores the welcome message //
// (livedata_connection.js) //
socket.send(JSON.stringify({ server_id: "0" })); // 102
//
// call all our callbacks when we get a new socket. they will do the //
// work of setting up handlers and such for specific messages. //
_.each(self.registration_callbacks, function (callback) { // 106
callback(socket); // 107
}); //
}); //
}; //
//
_.extend(StreamServer.prototype, { // 113
// call my callback when a new socket connects. //
// also call it for all current connections. //
register: function (callback) { // 116
var self = this; // 117
self.registration_callbacks.push(callback); // 118
_.each(self.all_sockets(), function (socket) { // 119
callback(socket); // 120
}); //
}, //
//
// get a list of all sockets //
all_sockets: function () { // 125
var self = this; // 126
return _.values(self.open_sockets); // 127
}, //
//
// Redirect /websocket to /sockjs/websocket in order to not expose //
// sockjs to clients that want to use raw websockets //
_redirectWebsocketEndpoint: function () { // 132
var self = this; // 133
// Unfortunately we can't use a connect middleware here since //
// sockjs installs itself prior to all existing listeners //
// (meaning prior to any connect middlewares) so we need to take //
// an approach similar to overshadowListeners in //
// https://github.com/sockjs/sockjs-node/blob/cf820c55af6a9953e16558555a31decea554f70e/src/utils.coffee //
_.each(['request', 'upgrade'], function (event) { // 139
var httpServer = WebApp.httpServer; // 140
var oldHttpServerListeners = httpServer.listeners(event).slice(0); // 141
httpServer.removeAllListeners(event); // 142
//
// request and upgrade have different arguments passed but //
// we only care about the first one which is always request //
var newListener = function (request /*, moreArguments */) { // 146
// Store arguments for use within the closure below //
var args = arguments; // 148
//
// Rewrite /websocket and /websocket/ urls to /sockjs/websocket while //
// preserving query string. //
var parsedUrl = url.parse(request.url); // 152
if (parsedUrl.pathname === pathPrefix + '/websocket' || parsedUrl.pathname === pathPrefix + '/websocket/') { // 153
parsedUrl.pathname = self.prefix + '/websocket'; // 155
request.url = url.format(parsedUrl); // 156
} //
_.each(oldHttpServerListeners, function (oldListener) { // 158
oldListener.apply(httpServer, args); // 159
}); //
}; //
httpServer.addListener(event, newListener); // 162
}); //
} //
}); //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
}).call(this);
(function(){
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //
// packages/ddp-server/livedata_server.js //
// //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
DDPServer = {}; // 1
//
var Fiber = Npm.require('fibers'); // 3
//
// This file contains classes: //
// * Session - The server's connection to a single DDP client //
// * Subscription - A single subscription for a single client //
// * Server - An entire server that may talk to > 1 client. A DDP endpoint. //
// //
// Session and Subscription are file scope. For now, until we freeze //
// the interface, Server is package scope (in the future it should be //
// exported.) //
//
// Represents a single document in a SessionCollectionView //
var SessionDocumentView = function () { // 15
var self = this; // 16
self.existsIn = {}; // set of subscriptionHandle // 17
self.dataByKey = {}; // key-> [ {subscriptionHandle, value} by precedence] // 18
}; //
//
_.extend(SessionDocumentView.prototype, { // 21
//
getFields: function () { // 23
var self = this; // 24
var ret = {}; // 25
_.each(self.dataByKey, function (precedenceList, key) { // 26
ret[key] = precedenceList[0].value; // 27
}); //
return ret; // 29
}, //
//
clearField: function (subscriptionHandle, key, changeCollector) { // 32
var self = this; // 33
// Publish API ignores _id if present in fields //
if (key === "_id") return; // 35
var precedenceList = self.dataByKey[key]; // 37
//
// It's okay to clear fields that didn't exist. No need to throw //
// an error. //
if (!precedenceList) return; // 41
//
var removedValue = undefined; // 44
for (var i = 0; i < precedenceList.length; i++) { // 45
var precedence = precedenceList[i]; // 46
if (precedence.subscriptionHandle === subscriptionHandle) { // 47
// The view's value can only change if this subscription is the one that //
// used to have precedence. //
if (i === 0) removedValue = precedence.value; // 50
precedenceList.splice(i, 1); // 52
break; // 53
} //
} //
if (_.isEmpty(precedenceList)) { // 56
delete self.dataByKey[key]; // 57
changeCollector[key] = undefined; // 58
} else if (removedValue !== undefined && !EJSON.equals(removedValue, precedenceList[0].value)) { //
changeCollector[key] = precedenceList[0].value; // 61
} //
}, //
//
changeField: function (subscriptionHandle, key, value, changeCollector, isAdd) { // 65
var self = this; // 67
// Publish API ignores _id if present in fields //
if (key === "_id") return; // 69
//
// Don't share state with the data passed in by the user. //
value = EJSON.clone(value); // 73
//
if (!_.has(self.dataByKey, key)) { // 75
self.dataByKey[key] = [{ subscriptionHandle: subscriptionHandle, // 76
value: value }]; // 77
changeCollector[key] = value; // 78
return; // 79
} //
var precedenceList = self.dataByKey[key]; // 81
var elt; // 82
if (!isAdd) { // 83
elt = _.find(precedenceList, function (precedence) { // 84
return precedence.subscriptionHandle === subscriptionHandle; // 85
}); //
} //
//
if (elt) { // 89
if (elt === precedenceList[0] && !EJSON.equals(value, elt.value)) { // 90
// this subscription is changing the value of this field. //
changeCollector[key] = value; // 92
} //
elt.value = value; // 94
} else { //
// this subscription is newly caring about this field //
precedenceList.push({ subscriptionHandle: subscriptionHandle, value: value }); // 97
} //
} //
}); //
//
/** //
* Represents a client's view of a single collection //
* @param {String} collectionName Name of the collection it represents //
* @param {Object.<String, Function>} sessionCallbacks The callbacks for added, changed, removed //
* @class SessionCollectionView //
*/ //
var SessionCollectionView = function (collectionName, sessionCallbacks) { // 109
var self = this; // 110
self.collectionName = collectionName; // 111
self.documents = {}; // 112
self.callbacks = sessionCallbacks; // 113
}; //
//
DDPServer._SessionCollectionView = SessionCollectionView; // 116
//
_.extend(SessionCollectionView.prototype, { // 119
//
isEmpty: function () { // 121
var self = this; // 122
return _.isEmpty(self.documents); // 123
}, //
//
diff: function (previous) { // 126
var self = this; // 127
DiffSequence.diffObjects(previous.documents, self.documents, { // 128
both: _.bind(self.diffDocument, self), // 129
//
rightOnly: function (id, nowDV) { // 131
self.callbacks.added(self.collectionName, id, nowDV.getFields()); // 132
}, //
//
leftOnly: function (id, prevDV) { // 135
self.callbacks.removed(self.collectionName, id); // 136
} //
}); //
}, //
//
diffDocument: function (id, prevDV, nowDV) { // 141
var self = this; // 142
var fields = {}; // 143
DiffSequence.diffObjects(prevDV.getFields(), nowDV.getFields(), { // 144
both: function (key, prev, now) { // 145
if (!EJSON.equals(prev, now)) fields[key] = now; // 146
}, //
rightOnly: function (key, now) { // 149
fields[key] = now; // 150
}, //
leftOnly: function (key, prev) { // 152
fields[key] = undefined; // 153
} //
}); //
self.callbacks.changed(self.collectionName, id, fields); // 156
}, //
//
added: function (subscriptionHandle, id, fields) { // 159
var self = this; // 160
var docView = self.documents[id]; // 161
var added = false; // 162
if (!docView) { // 163
added = true; // 164
docView = new SessionDocumentView(); // 165
self.documents[id] = docView; // 166
} //
docView.existsIn[subscriptionHandle] = true; // 168
var changeCollector = {}; // 169
_.each(fields, function (value, key) { // 170
docView.changeField(subscriptionHandle, key, value, changeCollector, true); // 171
}); //
if (added) self.callbacks.added(self.collectionName, id, changeCollector);else self.callbacks.changed(self.collectionName, id, changeCollector);
}, //
//
changed: function (subscriptionHandle, id, changed) { // 180
var self = this; // 181
var changedResult = {}; // 182
var docView = self.documents[id]; // 183
if (!docView) throw new Error("Could not find element with id " + id + " to change"); // 184
_.each(changed, function (value, key) { // 186
if (value === undefined) docView.clearField(subscriptionHandle, key, changedResult);else docView.changeField(subscriptionHandle, key, value, changedResult);
}); //
self.callbacks.changed(self.collectionName, id, changedResult); // 192
}, //
//
removed: function (subscriptionHandle, id) { // 195
var self = this; // 196
var docView = self.documents[id]; // 197
if (!docView) { // 198
var err = new Error("Removed nonexistent document " + id); // 199
throw err; // 200
} //
delete docView.existsIn[subscriptionHandle]; // 202
if (_.isEmpty(docView.existsIn)) { // 203
// it is gone from everyone //
self.callbacks.removed(self.collectionName, id); // 205
delete self.documents[id]; // 206
} else { //
var changed = {}; // 208
// remove this subscription from every precedence list //
// and record the changes //
_.each(docView.dataByKey, function (precedenceList, key) { // 211
docView.clearField(subscriptionHandle, key, changed); // 212
}); //
//
self.callbacks.changed(self.collectionName, id, changed); // 215
} //
} //
}); //
//
/******************************************************************************/ //
/* Session */ //
/******************************************************************************/ //
//
var Session = function (server, version, socket, options) { // 224
var self = this; // 225
self.id = Random.id(); // 226
//
self.server = server; // 228
self.version = version; // 229
//
self.initialized = false; // 231
self.socket = socket; // 232
//
// set to null when the session is destroyed. multiple places below //
// use this to determine if the session is alive or not. //
self.inQueue = new Meteor._DoubleEndedQueue(); // 236
//
self.blocked = false; // 238
self.workerRunning = false; // 239
//
// Sub objects for active subscriptions //
self._namedSubs = {}; // 242
self._universalSubs = []; // 243
//
self.userId = null; // 245
//
self.collectionViews = {}; // 247
//
// Set this to false to not send messages when collectionViews are //
// modified. This is done when rerunning subs in _setUserId and those messages //
// are calculated via a diff instead. //
self._isSending = true; // 252
//
// If this is true, don't start a newly-created universal publisher on this //
// session. The session will take care of starting it when appropriate. //
self._dontStartNewUniversalSubs = false; // 256
//
// when we are rerunning subscriptions, any ready messages //
// we want to buffer up for when we are done rerunning subscriptions //
self._pendingReady = []; // 260
//
// List of callbacks to call when this connection is closed. //
self._closeCallbacks = []; // 263
//
// XXX HACK: If a sockjs connection, save off the URL. This is //
// temporary and will go away in the near future. //
self._socketUrl = socket.url; // 268
//
// Allow tests to disable responding to pings. //
self._respondToPings = options.respondToPings; // 271
//
// This object is the public interface to the session. In the public //
// API, it is called the `connection` object. Internally we call it //
// a `connectionHandle` to avoid ambiguity. //
self.connectionHandle = { // 276
id: self.id, // 277
close: function () { // 278
self.close(); // 279
}, //
onClose: function (fn) { // 281
var cb = Meteor.bindEnvironment(fn, "connection onClose callback"); // 282
if (self.inQueue) { // 283
self._closeCallbacks.push(cb); // 284
} else { //
// if we're already closed, call the callback. //
Meteor.defer(cb); // 287
} //
}, //
clientAddress: self._clientAddress(), // 290
httpHeaders: self.socket.headers // 291
}; //
//
socket.send(DDPCommon.stringifyDDP({ msg: 'connected', // 294
session: self.id })); // 295
// On initial connect, spin up all the universal publishers. //
Fiber(function () { // 297
self.startUniversalSubs(); // 298
}).run(); //
//
if (version !== 'pre1' && options.heartbeatInterval !== 0) { // 301
self.heartbeat = new DDPCommon.Heartbeat({ // 302
heartbeatInterval: options.heartbeatInterval, // 303
heartbeatTimeout: options.heartbeatTimeout, // 304
onTimeout: function () { // 305
self.close(); // 306
}, //
sendPing: function () { // 308
self.send({ msg: 'ping' }); // 309
} //
}); //
self.heartbeat.start(); // 312
} //
//
Package.facts && Package.facts.Facts.incrementServerFact("livedata", "sessions", 1); // 315
}; //
//
_.extend(Session.prototype, { // 319
//
sendReady: function (subscriptionIds) { // 321
var self = this; // 322
if (self._isSending) self.send({ msg: "ready", subs: subscriptionIds });else { // 323
_.each(subscriptionIds, function (subscriptionId) { // 326
self._pendingReady.push(subscriptionId); // 327
}); //
} //
}, //
//
sendAdded: function (collectionName, id, fields) { // 332
var self = this; // 333
if (self._isSending) self.send({ msg: "added", collection: collectionName, id: id, fields: fields }); // 334
}, //
//
sendChanged: function (collectionName, id, fields) { // 338
var self = this; // 339
if (_.isEmpty(fields)) return; // 340
//
if (self._isSending) { // 343
self.send({ // 344
msg: "changed", // 345
collection: collectionName, // 346
id: id, // 347
fields: fields // 348
}); //
} //
}, //
//
sendRemoved: function (collectionName, id) { // 353
var self = this; // 354
if (self._isSending) self.send({ msg: "removed", collection: collectionName, id: id }); // 355
}, //
//
getSendCallbacks: function () { // 359
var self = this; // 360
return { // 361
added: _.bind(self.sendAdded, self), // 362
changed: _.bind(self.sendChanged, self), // 363
removed: _.bind(self.sendRemoved, self) // 364
}; //
}, //
//
getCollectionView: function (collectionName) { // 368
var self = this; // 369
if (_.has(self.collectionViews, collectionName)) { // 370
return self.collectionViews[collectionName]; // 371
} //
var ret = new SessionCollectionView(collectionName, self.getSendCallbacks()); // 373
self.collectionViews[collectionName] = ret; // 375
return ret; // 376
}, //
//
added: function (subscriptionHandle, collectionName, id, fields) { // 379
var self = this; // 380
var view = self.getCollectionView(collectionName); // 381
view.added(subscriptionHandle, id, fields); // 382
}, //
//
removed: function (subscriptionHandle, collectionName, id) { // 385
var self = this; // 386
var view = self.getCollectionView(collectionName); // 387
view.removed(subscriptionHandle, id); // 388
if (view.isEmpty()) { // 389
delete self.collectionViews[collectionName]; // 390
} //
}, //
//
changed: function (subscriptionHandle, collectionName, id, fields) { // 394
var self = this; // 395
var view = self.getCollectionView(collectionName); // 396
view.changed(subscriptionHandle, id, fields); // 397
}, //
//
startUniversalSubs: function () { // 400
var self = this; // 401
// Make a shallow copy of the set of universal handlers and start them. If //
// additional universal publishers start while we're running them (due to //
// yielding), they will run separately as part of Server.publish. //
var handlers = _.clone(self.server.universal_publish_handlers); // 405
_.each(handlers, function (handler) { // 406
self._startSubscription(handler); // 407
}); //
}, //
//
// Destroy this session and unregister it at the server. //
close: function () { // 412
var self = this; // 413
//
// Destroy this session, even if it's not registered at the //
// server. Stop all processing and tear everything down. If a socket //
// was attached, close it. //
//
// Already destroyed. //
if (!self.inQueue) return; // 420
//
// Drop the merge box data immediately. //
self.inQueue = null; // 424
self.collectionViews = {}; // 425
//
if (self.heartbeat) { // 427
self.heartbeat.stop(); // 428
self.heartbeat = null; // 429
} //
//
if (self.socket) { // 432
self.socket.close(); // 433
self.socket._meteorSession = null; // 434
} //
//
Package.facts && Package.facts.Facts.incrementServerFact("livedata", "sessions", -1); // 437
//
Meteor.defer(function () { // 440
// stop callbacks can yield, so we defer this on close. //
// sub._isDeactivated() detects that we set inQueue to null and //
// treats it as semi-deactivated (it will ignore incoming callbacks, etc). //
self._deactivateAllSubscriptions(); // 444
//
// Defer calling the close callbacks, so that the caller closing //
// the session isn't waiting for all the callbacks to complete. //
_.each(self._closeCallbacks, function (callback) { // 448
callback(); // 449
}); //
}); //
//
// Unregister the session. //
self.server._removeSession(self); // 454
}, //
//
// Send a message (doing nothing if no socket is connected right now.) //
// It should be a JSON object (it will be stringified.) //
send: function (msg) { // 459
var self = this; // 460
if (self.socket) { // 461
if (Meteor._printSentDDP) Meteor._debug("Sent DDP", DDPCommon.stringifyDDP(msg)); // 462
self.socket.send(DDPCommon.stringifyDDP(msg)); // 464
} //
}, //
//
// Send a connection error. //
sendError: function (reason, offendingMessage) { // 469
var self = this; // 470
var msg = { msg: 'error', reason: reason }; // 471
if (offendingMessage) msg.offendingMessage = offendingMessage; // 472
self.send(msg); // 474
}, //
//
// Process 'msg' as an incoming message. (But as a guard against //
// race conditions during reconnection, ignore the message if //
// 'socket' is not the currently connected socket.) //
// //
// We run the messages from the client one at a time, in the order //
// given by the client. The message handler is passed an idempotent //
// function 'unblock' which it may call to allow other messages to //
// begin running in parallel in another fiber (for example, a method //
// that wants to yield.) Otherwise, it is automatically unblocked //
// when it returns. //
// //
// Actually, we don't have to 'totally order' the messages in this //
// way, but it's the easiest thing that's correct. (unsub needs to //
// be ordered against sub, methods need to be ordered against each //
// other.) //
processMessage: function (msg_in) { // 492
var self = this; // 493
if (!self.inQueue) // we have been destroyed. // 494
return; // 495
//
// Respond to ping and pong messages immediately without queuing. //
// If the negotiated DDP version is "pre1" which didn't support //
// pings, preserve the "pre1" behavior of responding with a "bad //
// request" for the unknown messages. //
// //
// Fibers are needed because heartbeat uses Meteor.setTimeout, which //
// needs a Fiber. We could actually use regular setTimeout and avoid //
// these new fibers, but it is easier to just make everything use //
// Meteor.setTimeout and not think too hard. //
// //
// Any message counts as receiving a pong, as it demonstrates that //
// the client is still alive. //
if (self.heartbeat) { // 509
Fiber(function () { // 510
self.heartbeat.messageReceived(); // 511
}).run(); //
} //
//
if (self.version !== 'pre1' && msg_in.msg === 'ping') { // 515
if (self._respondToPings) self.send({ msg: "pong", id: msg_in.id }); // 516
return; // 518
} //
if (self.version !== 'pre1' && msg_in.msg === 'pong') { // 520
// Since everything is a pong, nothing to do //
return; // 522
} //
//
self.inQueue.push(msg_in); // 525
if (self.workerRunning) return; // 526
self.workerRunning = true; // 528
//
var processNext = function () { // 530
var msg = self.inQueue && self.inQueue.shift(); // 531
if (!msg) { // 532
self.workerRunning = false; // 533
return; // 534
} //
//
Fiber(function () { // 537
var blocked = true; // 538
//
var unblock = function () { // 540
if (!blocked) return; // idempotent // 541
blocked = false; // 543
processNext(); // 544
}; //
//
if (_.has(self.protocol_handlers, msg.msg)) self.protocol_handlers[msg.msg].call(self, msg, unblock);else self.sendError('Bad request', msg);
unblock(); // in case the handler didn't already do it // 551
}).run(); //
}; //
//
processNext(); // 555
}, //
//
protocol_handlers: { // 558
sub: function (msg) { // 559
var self = this; // 560
//
// reject malformed messages //
if (typeof msg.id !== "string" || typeof msg.name !== "string" || 'params' in msg && !(msg.params instanceof Array)) {
self.sendError("Malformed subscription", msg); // 566
return; // 567
} //
//
if (!self.server.publish_handlers[msg.name]) { // 570
self.send({ // 571
msg: 'nosub', id: msg.id, // 572
error: new Meteor.Error(404, "Subscription not found") }); // 573
return; // 574
} //
//
if (_.has(self._namedSubs, msg.id)) // 577
// subs are idempotent, or rather, they are ignored if a sub //
// with that id already exists. this is important during //
// reconnect. //
return; // 581
//
// XXX It'd be much better if we had generic hooks where any package can //
// hook into subscription handling, but in the mean while we special case //
// ddp-rate-limiter package. This is also done for weak requirements to //
// add the ddp-rate-limiter package in case we don't have Accounts. A //
// user trying to use the ddp-rate-limiter must explicitly require it. //
if (Package['ddp-rate-limiter']) { // 588
var DDPRateLimiter = Package['ddp-rate-limiter'].DDPRateLimiter; // 589
var rateLimiterInput = { // 590
userId: self.userId, // 591
clientAddress: self.connectionHandle.clientAddress, // 592
type: "subscription", // 593
name: msg.name, // 594
connectionId: self.id // 595
}; //
//
DDPRateLimiter._increment(rateLimiterInput); // 598
var rateLimitResult = DDPRateLimiter._check(rateLimiterInput); // 599
if (!rateLimitResult.allowed) { // 600
self.send({ // 601
msg: 'nosub', id: msg.id, // 602
error: new Meteor.Error('too-many-requests', DDPRateLimiter.getErrorMessage(rateLimitResult), { timeToReset: rateLimitResult.timeToReset })
}); //
return; // 608
} //
} //
//
var handler = self.server.publish_handlers[msg.name]; // 612
//
self._startSubscription(handler, msg.id, msg.params, msg.name); // 614
}, //
//
unsub: function (msg) { // 618
var self = this; // 619
//
self._stopSubscription(msg.id); // 621
}, //
//
method: function (msg, unblock) { // 624
var self = this; // 625
//
// reject malformed messages //
// For now, we silently ignore unknown attributes, //
// for forwards compatibility. //
if (typeof msg.id !== "string" || typeof msg.method !== "string" || 'params' in msg && !(msg.params instanceof Array) || 'randomSeed' in msg && typeof msg.randomSeed !== "string") {
self.sendError("Malformed method invocation", msg); // 634
return; // 635
} //
//
var randomSeed = msg.randomSeed || null; // 638
//
// set up to mark the method as satisfied once all observers //
// (and subscriptions) have reacted to any writes that were //
// done. //
var fence = new DDPServer._WriteFence(); // 643
fence.onAllCommitted(function () { // 644
// Retire the fence so that future writes are allowed. //
// This means that callbacks like timers are free to use //
// the fence, and if they fire before it's armed (for //
// example, because the method waits for them) their //
// writes will be included in the fence. //
fence.retire(); // 650
self.send({ // 651
msg: 'updated', methods: [msg.id] }); // 652
}); //
//
// find the handler //
var handler = self.server.method_handlers[msg.method]; // 656
if (!handler) { // 657
self.send({ // 658
msg: 'result', id: msg.id, // 659
error: new Meteor.Error(404, "Method not found") }); // 660
fence.arm(); // 661
return; // 662
} //
//
var setUserId = function (userId) { // 665
self._setUserId(userId); // 666
}; //
//
var invocation = new DDPCommon.MethodInvocation({ // 669
isSimulation: false, // 670
userId: self.userId, // 671
setUserId: setUserId, // 672
unblock: unblock, // 673
connection: self.connectionHandle, // 674
randomSeed: randomSeed // 675
}); //
//
var promise = new Promise(function (resolve, reject) { // 678
// XXX It'd be better if we could hook into method handlers better but //
// for now, we need to check if the ddp-rate-limiter exists since we //
// have a weak requirement for the ddp-rate-limiter package to be added //
// to our application. //
if (Package['ddp-rate-limiter']) { // 683
var DDPRateLimiter = Package['ddp-rate-limiter'].DDPRateLimiter; // 684
var rateLimiterInput = { // 685
userId: self.userId, // 686
clientAddress: self.connectionHandle.clientAddress, // 687
type: "method", // 688
name: msg.method, // 689
connectionId: self.id // 690
}; //
DDPRateLimiter._increment(rateLimiterInput); // 692
var rateLimitResult = DDPRateLimiter._check(rateLimiterInput); // 693
if (!rateLimitResult.allowed) { // 694
reject(new Meteor.Error("too-many-requests", DDPRateLimiter.getErrorMessage(rateLimitResult), { timeToReset: rateLimitResult.timeToReset }));
return; // 700
} //
} //
//
resolve(DDPServer._CurrentWriteFence.withValue(fence, function () { // 704
return DDP._CurrentInvocation.withValue(invocation, function () { //
return maybeAuditArgumentChecks(handler, invocation, msg.params, "call to '" + msg.method + "'"); //
}); //
})); //
}); //
//
function finish() { // 716
fence.arm(); // 717
unblock(); // 718
} //
//
var payload = { // 721
msg: "result", // 722
id: msg.id // 723
}; //
//
promise.then(function (result) { // 726
finish(); // 727
if (result !== undefined) { // 728
payload.result = result; // 729
} //
self.send(payload); // 731
}, function (exception) { //
finish(); // 733
payload.error = wrapInternalException(exception, "while invoking method '" + msg.method + "'"); // 734
self.send(payload); // 738
}); //
} //
}, //
//
_eachSub: function (f) { // 743
var self = this; // 744
_.each(self._namedSubs, f); // 745
_.each(self._universalSubs, f); // 746
}, //
//
_diffCollectionViews: function (beforeCVs) { // 749
var self = this; // 750
DiffSequence.diffObjects(beforeCVs, self.collectionViews, { // 751
both: function (collectionName, leftValue, rightValue) { // 752
rightValue.diff(leftValue); // 753
}, //
rightOnly: function (collectionName, rightValue) { // 755
_.each(rightValue.documents, function (docView, id) { // 756
self.sendAdded(collectionName, id, docView.getFields()); // 757
}); //
}, //
leftOnly: function (collectionName, leftValue) { // 760
_.each(leftValue.documents, function (doc, id) { // 761
self.sendRemoved(collectionName, id); // 762
}); //
} //
}); //
}, //
//
// Sets the current user id in all appropriate contexts and reruns //
// all subscriptions //
_setUserId: function (userId) { // 770
var self = this; // 771
//
if (userId !== null && typeof userId !== "string") throw new Error("setUserId must be called on string or null, not " + typeof userId);
//
// Prevent newly-created universal subscriptions from being added to our //
// session; they will be found below when we call startUniversalSubs. //
// //
// (We don't have to worry about named subscriptions, because we only add //
// them when we process a 'sub' message. We are currently processing a //
// 'method' message, and the method did not unblock, because it is illegal //
// to call setUserId after unblock. Thus we cannot be concurrently adding a //
// new named subscription.) //
self._dontStartNewUniversalSubs = true; // 785
//
// Prevent current subs from updating our collectionViews and call their //
// stop callbacks. This may yield. //
self._eachSub(function (sub) { // 789
sub._deactivate(); // 790
}); //
//
// All subs should now be deactivated. Stop sending messages to the client, //
// save the state of the published collections, reset to an empty view, and //
// update the userId. //
self._isSending = false; // 796
var beforeCVs = self.collectionViews; // 797
self.collectionViews = {}; // 798
self.userId = userId; // 799
//
// Save the old named subs, and reset to having no subscriptions. //
var oldNamedSubs = self._namedSubs; // 802
self._namedSubs = {}; // 803
self._universalSubs = []; // 804
//
_.each(oldNamedSubs, function (sub, subscriptionId) { // 806
self._namedSubs[subscriptionId] = sub._recreate(); // 807
// nb: if the handler throws or calls this.error(), it will in fact //
// immediately send its 'nosub'. This is OK, though. //
self._namedSubs[subscriptionId]._runHandler(); // 810
}); //
//
// Allow newly-created universal subs to be started on our connection in //
// parallel with the ones we're spinning up here, and spin up universal //
// subs. //
self._dontStartNewUniversalSubs = false; // 816
self.startUniversalSubs(); // 817
//
// Start sending messages again, beginning with the diff from the previous //
// state of the world to the current state. No yields are allowed during //
// this diff, so that other changes cannot interleave. //
Meteor._noYieldsAllowed(function () { // 822
self._isSending = true; // 823
self._diffCollectionViews(beforeCVs); // 824
if (!_.isEmpty(self._pendingReady)) { // 825
self.sendReady(self._pendingReady); // 826
self._pendingReady = []; // 827
} //
}); //
}, //
//
_startSubscription: function (handler, subId, params, name) { // 832
var self = this; // 833
//
var sub = new Subscription(self, handler, subId, params, name); // 835
if (subId) self._namedSubs[subId] = sub;else self._universalSubs.push(sub); // 837
//
sub._runHandler(); // 842
}, //
//
// tear down specified subscription //
_stopSubscription: function (subId, error) { // 846
var self = this; // 847
//
var subName = null; // 849
//
if (subId && self._namedSubs[subId]) { // 851
subName = self._namedSubs[subId]._name; // 852
self._namedSubs[subId]._removeAllDocuments(); // 853
self._namedSubs[subId]._deactivate(); // 854
delete self._namedSubs[subId]; // 855
} //
//
var response = { msg: 'nosub', id: subId }; // 858
//
if (error) { // 860
response.error = wrapInternalException(error, subName ? "from sub " + subName + " id " + subId : "from sub id " + subId);
} //
//
self.send(response); // 867
}, //
//
// tear down all subscriptions. Note that this does NOT send removed or nosub //
// messages, since we assume the client is gone. //
_deactivateAllSubscriptions: function () { // 872
var self = this; // 873
//
_.each(self._namedSubs, function (sub, id) { // 875
sub._deactivate(); // 876
}); //
self._namedSubs = {}; // 878
//
_.each(self._universalSubs, function (sub) { // 880
sub._deactivate(); // 881
}); //
self._universalSubs = []; // 883
}, //
//
// Determine the remote client's IP address, based on the //
// HTTP_FORWARDED_COUNT environment variable representing how many //
// proxies the server is behind. //
_clientAddress: function () { // 889
var self = this; // 890
//
// For the reported client address for a connection to be correct, //
// the developer must set the HTTP_FORWARDED_COUNT environment //
// variable to an integer representing the number of hops they //
// expect in the `x-forwarded-for` header. E.g., set to "1" if the //
// server is behind one proxy. //
// //
// This could be computed once at startup instead of every time. //
var httpForwardedCount = parseInt(process.env['HTTP_FORWARDED_COUNT']) || 0; // 899
//
if (httpForwardedCount === 0) return self.socket.remoteAddress; // 901
//
var forwardedFor = self.socket.headers["x-forwarded-for"]; // 904
if (!_.isString(forwardedFor)) return null; // 905
forwardedFor = forwardedFor.trim().split(/\s*,\s*/); // 907
//
// Typically the first value in the `x-forwarded-for` header is //
// the original IP address of the client connecting to the first //
// proxy. However, the end user can easily spoof the header, in //
// which case the first value(s) will be the fake IP address from //
// the user pretending to be a proxy reporting the original IP //
// address value. By counting HTTP_FORWARDED_COUNT back from the //
// end of the list, we ensure that we get the IP address being //
// reported by *our* first proxy. //
//
if (httpForwardedCount < 0 || httpForwardedCount > forwardedFor.length) return null; // 918
//
return forwardedFor[forwardedFor.length - httpForwardedCount]; // 921
} //
}); //
//
/******************************************************************************/ //
/* Subscription */ //
/******************************************************************************/ //
//
// ctor for a sub handle: the input to each publish function //
//
// Instance name is this because it's usually referred to as this inside a //
// publish //
/** //
* @summary The server's side of a subscription //
* @class Subscription //
* @instanceName this //
*/ //
var Subscription = function (session, handler, subscriptionId, params, name) { // 938
var self = this; // 940
self._session = session; // type is Session // 941
//
/** //
* @summary Access inside the publish function. The incoming [connection](#meteor_onconnection) for this subscription.
* @locus Server //
* @name connection //
* @memberOf Subscription //
* @instance //
*/ //
self.connection = session.connectionHandle; // public API object // 950
//
self._handler = handler; // 952
//
// my subscription ID (generated by client, undefined for universal subs). //
self._subscriptionId = subscriptionId; // 955
// undefined for universal subs //
self._name = name; // 957
//
self._params = params || []; // 959
//
// Only named subscriptions have IDs, but we need some sort of string //
// internally to keep track of all subscriptions inside //
// SessionDocumentViews. We use this subscriptionHandle for that. //
if (self._subscriptionId) { // 964
self._subscriptionHandle = 'N' + self._subscriptionId; // 965
} else { //
self._subscriptionHandle = 'U' + Random.id(); // 967
} //
//
// has _deactivate been called? //
self._deactivated = false; // 971
//
// stop callbacks to g/c this sub. called w/ zero arguments. //
self._stopCallbacks = []; // 974
//
// the set of (collection, documentid) that this subscription has //
// an opinion about //
self._documents = {}; // 978
//
// remember if we are ready. //
self._ready = false; // 981
//
// Part of the public API: the user of this sub. //
//
/** //
* @summary Access inside the publish function. The id of the logged-in user, or `null` if no user is logged in. //
* @locus Server //
* @memberOf Subscription //
* @name userId //
* @instance //
*/ //
self.userId = session.userId; // 992
//
// For now, the id filter is going to default to //
// the to/from DDP methods on MongoID, to //
// specifically deal with mongo/minimongo ObjectIds. //
//
// Later, you will be able to make this be "raw" //
// if you want to publish a collection that you know //
// just has strings for keys and no funny business, to //
// a ddp consumer that isn't minimongo //
//
self._idFilter = { // 1003
idStringify: MongoID.idStringify, // 1004
idParse: MongoID.idParse // 1005
}; //
//
Package.facts && Package.facts.Facts.incrementServerFact("livedata", "subscriptions", 1); // 1008
}; //
//
_.extend(Subscription.prototype, { // 1012
_runHandler: function () { // 1013
// XXX should we unblock() here? Either before running the publish //
// function, or before running _publishCursor. //
// //
// Right now, each publish function blocks all future publishes and //
// methods waiting on data from Mongo (or whatever else the function //
// blocks on). This probably slows page load in common cases. //
//
var self = this; // 1021
try { // 1022
var res = maybeAuditArgumentChecks(self._handler, self, EJSON.clone(self._params), // 1023
// It's OK that this would look weird for universal subscriptions, //
// because they have no arguments so there can never be an //
// audit-argument-checks failure. //
"publisher '" + self._name + "'"); // 1028
} catch (e) { //
self.error(e); // 1030
return; // 1031
} //
//
// Did the handler call this.error or this.stop? //
if (self._isDeactivated()) return; // 1035
//
self._publishHandlerResult(res); // 1038
}, //
//
_publishHandlerResult: function (res) { // 1041
// SPECIAL CASE: Instead of writing their own callbacks that invoke //
// this.added/changed/ready/etc, the user can just return a collection //
// cursor or array of cursors from the publish function; we call their //
// _publishCursor method which starts observing the cursor and publishes the //
// results. Note that _publishCursor does NOT call ready(). //
// //
// XXX This uses an undocumented interface which only the Mongo cursor //
// interface publishes. Should we make this interface public and encourage //
// users to implement it themselves? Arguably, it's unnecessary; users can //
// already write their own functions like //
// var publishMyReactiveThingy = function (name, handler) { //
// Meteor.publish(name, function () { //
// var reactiveThingy = handler(); //
// reactiveThingy.publishMe(); //
// }); //
// }; //
//
var self = this; // 1059
var isCursor = function (c) { // 1060
return c && c._publishCursor; // 1061
}; //
if (isCursor(res)) { // 1063
try { // 1064
res._publishCursor(self); // 1065
} catch (e) { //
self.error(e); // 1067
return; // 1068
} //
// _publishCursor only returns after the initial added callbacks have run. //
// mark subscription as ready. //
self.ready(); // 1072
} else if (_.isArray(res)) { //
// check all the elements are cursors //
if (!_.all(res, isCursor)) { // 1075
self.error(new Error("Publish function returned an array of non-Cursors")); // 1076
return; // 1077
} //
// find duplicate collection names //
// XXX we should support overlapping cursors, but that would require the //
// merge box to allow overlap within a subscription //
var collectionNames = {}; // 1082
for (var i = 0; i < res.length; ++i) { // 1083
var collectionName = res[i]._getCollectionName(); // 1084
if (_.has(collectionNames, collectionName)) { // 1085
self.error(new Error("Publish function returned multiple cursors for collection " + collectionName)); // 1086
return; // 1089
} //
collectionNames[collectionName] = true; // 1091
}; //
//
try { // 1094
_.each(res, function (cur) { // 1095
cur._publishCursor(self); // 1096
}); //
} catch (e) { //
self.error(e); // 1099
return; // 1100
} //
self.ready(); // 1102
} else if (res) { //
// truthy values other than cursors or arrays are probably a //
// user mistake (possible returning a Mongo document via, say, //
// `coll.findOne()`). //
self.error(new Error("Publish function can only return a Cursor or " + "an array of Cursors")); // 1107
} //
}, //
//
// This calls all stop callbacks and prevents the handler from updating any //
// SessionCollectionViews further. It's used when the user unsubscribes or //
// disconnects, as well as during setUserId re-runs. It does *NOT* send //
// removed messages for the published objects; if that is necessary, call //
// _removeAllDocuments first. //
_deactivate: function () { // 1117
var self = this; // 1118
if (self._deactivated) return; // 1119
self._deactivated = true; // 1121
self._callStopCallbacks(); // 1122
Package.facts && Package.facts.Facts.incrementServerFact("livedata", "subscriptions", -1); // 1123
}, //
//
_callStopCallbacks: function () { // 1127
var self = this; // 1128
// tell listeners, so they can clean up //
var callbacks = self._stopCallbacks; // 1130
self._stopCallbacks = []; // 1131
_.each(callbacks, function (callback) { // 1132
callback(); // 1133
}); //
}, //
//
// Send remove messages for every document. //
_removeAllDocuments: function () { // 1138
var self = this; // 1139
Meteor._noYieldsAllowed(function () { // 1140
_.each(self._documents, function (collectionDocs, collectionName) { // 1141
// Iterate over _.keys instead of the dictionary itself, since we'll be //
// mutating it. //
_.each(_.keys(collectionDocs), function (strId) { // 1144
self.removed(collectionName, self._idFilter.idParse(strId)); // 1145
}); //
}); //
}); //
}, //
//
// Returns a new Subscription for the same session with the same //
// initial creation parameters. This isn't a clone: it doesn't have //
// the same _documents cache, stopped state or callbacks; may have a //
// different _subscriptionHandle, and gets its userId from the //
// session, not from this object. //
_recreate: function () { // 1156
var self = this; // 1157
return new Subscription(self._session, self._handler, self._subscriptionId, self._params, self._name); // 1158
}, //
//
/** //
* @summary Call inside the publish function. Stops this client's subscription, triggering a call on the client to the `onStop` callback passed to [`Meteor.subscribe`](#meteor_subscribe), if any. If `error` is not a [`Meteor.Error`](#meteor_error), it will be [sanitized](#meteor_error).
* @locus Server //
* @param {Error} error The error to pass to the client. //
* @instance //
* @memberOf Subscription //
*/ //
error: function (error) { // 1170
var self = this; // 1171
if (self._isDeactivated()) return; // 1172
self._session._stopSubscription(self._subscriptionId, error); // 1174
}, //
//
// Note that while our DDP client will notice that you've called stop() on the //
// server (and clean up its _subscriptions table) we don't actually provide a //
// mechanism for an app to notice this (the subscribe onError callback only //
// triggers if there is an error). //
//
/** //
* @summary Call inside the publish function. Stops this client's subscription and invokes the client's `onStop` callback with no error.
* @locus Server //
* @instance //
* @memberOf Subscription //
*/ //
stop: function () { // 1188
var self = this; // 1189
if (self._isDeactivated()) return; // 1190
self._session._stopSubscription(self._subscriptionId); // 1192
}, //
//
/** //
* @summary Call inside the publish function. Registers a callback function to run when the subscription is stopped.
* @locus Server //
* @memberOf Subscription //
* @instance //
* @param {Function} func The callback function //
*/ //
onStop: function (callback) { // 1202
var self = this; // 1203
if (self._isDeactivated()) callback();else self._stopCallbacks.push(callback); // 1204
}, //
//
// This returns true if the sub has been deactivated, *OR* if the session was //
// destroyed but the deferred call to _deactivateAllSubscriptions hasn't //
// happened yet. //
_isDeactivated: function () { // 1213
var self = this; // 1214
return self._deactivated || self._session.inQueue === null; // 1215
}, //
//
/** //
* @summary Call inside the publish function. Informs the subscriber that a document has been added to the record set.
* @locus Server //
* @memberOf Subscription //
* @instance //
* @param {String} collection The name of the collection that contains the new document. //
* @param {String} id The new document's ID. //
* @param {Object} fields The fields in the new document. If `_id` is present it is ignored. //
*/ //
added: function (collectionName, id, fields) { // 1227
var self = this; // 1228
if (self._isDeactivated()) return; // 1229
id = self._idFilter.idStringify(id); // 1231
Meteor._ensure(self._documents, collectionName)[id] = true; // 1232
self._session.added(self._subscriptionHandle, collectionName, id, fields); // 1233
}, //
//
/** //
* @summary Call inside the publish function. Informs the subscriber that a document in the record set has been modified.
* @locus Server //
* @memberOf Subscription //
* @instance //
* @param {String} collection The name of the collection that contains the changed document. //
* @param {String} id The changed document's ID. //
* @param {Object} fields The fields in the document that have changed, together with their new values. If a field is not present in `fields` it was left unchanged; if it is present in `fields` and has a value of `undefined` it was removed from the document. If `_id` is present it is ignored.
*/ //
changed: function (collectionName, id, fields) { // 1245
var self = this; // 1246
if (self._isDeactivated()) return; // 1247
id = self._idFilter.idStringify(id); // 1249
self._session.changed(self._subscriptionHandle, collectionName, id, fields); // 1250
}, //
//
/** //
* @summary Call inside the publish function. Informs the subscriber that a document has been removed from the record set.
* @locus Server //
* @memberOf Subscription //
* @instance //
* @param {String} collection The name of the collection that the document has been removed from. //
* @param {String} id The ID of the document that has been removed. //
*/ //
removed: function (collectionName, id) { // 1261
var self = this; // 1262
if (self._isDeactivated()) return; // 1263
id = self._idFilter.idStringify(id); // 1265
// We don't bother to delete sets of things in a collection if the //
// collection is empty. It could break _removeAllDocuments. //
delete self._documents[collectionName][id]; // 1268
self._session.removed(self._subscriptionHandle, collectionName, id); // 1269
}, //
//
/** //
* @summary Call inside the publish function. Informs the subscriber that an initial, complete snapshot of the record set has been sent. This will trigger a call on the client to the `onReady` callback passed to [`Meteor.subscribe`](#meteor_subscribe), if any.
* @locus Server //
* @memberOf Subscription //
* @instance //
*/ //
ready: function () { // 1278
var self = this; // 1279
if (self._isDeactivated()) return; // 1280
if (!self._subscriptionId) return; // unnecessary but ignored for universal sub // 1282
if (!self._ready) { // 1284
self._session.sendReady([self._subscriptionId]); // 1285
self._ready = true; // 1286
} //
} //
}); //
//
/******************************************************************************/ //
/* Server */ //
/******************************************************************************/ //
//
Server = function (options) { // 1295
var self = this; // 1296
//
// The default heartbeat interval is 30 seconds on the server and 35 //
// seconds on the client. Since the client doesn't need to send a //
// ping as long as it is receiving pings, this means that pings //
// normally go from the server to the client. //
// //
// Note: Troposphere depends on the ability to mutate //
// Meteor.server.options.heartbeatTimeout! This is a hack, but it's life. //
self.options = _.defaults(options || {}, { // 1305
heartbeatInterval: 15000, // 1306
heartbeatTimeout: 15000, // 1307
// For testing, allow responding to pings to be disabled. //
respondToPings: true // 1309
}); //
//
// Map of callbacks to call when a new connection comes in to the //
// server and completes DDP version negotiation. Use an object instead //
// of an array so we can safely remove one from the list while //
// iterating over it. //
self.onConnectionHook = new Hook({ // 1316
debugPrintExceptions: "onConnection callback" // 1317
}); //
//
self.publish_handlers = {}; // 1320
self.universal_publish_handlers = []; // 1321
//
self.method_handlers = {}; // 1323
//
self.sessions = {}; // map from id to session // 1325
//
self.stream_server = new StreamServer(); // 1327
//
self.stream_server.register(function (socket) { // 1329
// socket implements the SockJSConnection interface //
socket._meteorSession = null; // 1331
//
var sendError = function (reason, offendingMessage) { // 1333
var msg = { msg: 'error', reason: reason }; // 1334
if (offendingMessage) msg.offendingMessage = offendingMessage; // 1335
socket.send(DDPCommon.stringifyDDP(msg)); // 1337
}; //
//
socket.on('data', function (raw_msg) { // 1340
if (Meteor._printReceivedDDP) { // 1341
Meteor._debug("Received DDP", raw_msg); // 1342
} //
try { // 1344
try { // 1345
var msg = DDPCommon.parseDDP(raw_msg); // 1346
} catch (err) { //
sendError('Parse error'); // 1348
return; // 1349
} //
if (msg === null || !msg.msg) { // 1351
sendError('Bad request', msg); // 1352
return; // 1353
} //
//
if (msg.msg === 'connect') { // 1356
if (socket._meteorSession) { // 1357
sendError("Already connected", msg); // 1358
return; // 1359
} //
Fiber(function () { // 1361
self._handleConnect(socket, msg); // 1362
}).run(); //
return; // 1364
} //
//
if (!socket._meteorSession) { // 1367
sendError('Must connect first', msg); // 1368
return; // 1369
} //
socket._meteorSession.processMessage(msg); // 1371
} catch (e) { //
// XXX print stack nicely //
Meteor._debug("Internal exception while processing message", msg, e.message, e.stack); // 1374
} //
}); //
//
socket.on('close', function () { // 1379
if (socket._meteorSession) { // 1380
Fiber(function () { // 1381
socket._meteorSession.close(); // 1382
}).run(); //
} //
}); //
}); //
}; //
//
_.extend(Server.prototype, { // 1389
//
/** //
* @summary Register a callback to be called when a new DDP connection is made to the server. //
* @locus Server //
* @param {function} callback The function to call when a new DDP connection is established. //
* @memberOf Meteor //
*/ //
onConnection: function (fn) { // 1397
var self = this; // 1398
return self.onConnectionHook.register(fn); // 1399
}, //
//
_handleConnect: function (socket, msg) { // 1402
var self = this; // 1403
//
// The connect message must specify a version and an array of supported //
// versions, and it must claim to support what it is proposing. //
if (!(typeof msg.version === 'string' && _.isArray(msg.support) && _.all(msg.support, _.isString) && _.contains(msg.support, msg.version))) {
socket.send(DDPCommon.stringifyDDP({ msg: 'failed', // 1411
version: DDPCommon.SUPPORTED_DDP_VERSIONS[0] })); // 1412
socket.close(); // 1413
return; // 1414
} //
//
// In the future, handle session resumption: something like: //
// socket._meteorSession = self.sessions[msg.session] //
var version = calculateVersion(msg.support, DDPCommon.SUPPORTED_DDP_VERSIONS); // 1419
//
if (msg.version !== version) { // 1421
// The best version to use (according to the client's stated preferences) //
// is not the one the client is trying to use. Inform them about the best //
// version to use. //
socket.send(DDPCommon.stringifyDDP({ msg: 'failed', version: version })); // 1425
socket.close(); // 1426
return; // 1427
} //
//
// Yay, version matches! Create a new session. //
// Note: Troposphere depends on the ability to mutate //
// Meteor.server.options.heartbeatTimeout! This is a hack, but it's life. //
socket._meteorSession = new Session(self, version, socket, self.options); // 1433
self.sessions[socket._meteorSession.id] = socket._meteorSession; // 1434
self.onConnectionHook.each(function (callback) { // 1435
if (socket._meteorSession) callback(socket._meteorSession.connectionHandle); // 1436
return true; // 1438
}); //
}, //
/** //
* Register a publish handler function. //
* //
* @param name {String} identifier for query //
* @param handler {Function} publish handler //
* @param options {Object} //
* //
* Server will call handler function on each new subscription, //
* either when receiving DDP sub message for a named subscription, or on //
* DDP connect for a universal subscription. //
* //
* If name is null, this will be a subscription that is //
* automatically established and permanently on for all connected //
* client, instead of a subscription that can be turned on and off //
* with subscribe(). //
* //
* options to contain: //
* - (mostly internal) is_auto: true if generated automatically //
* from an autopublish hook. this is for cosmetic purposes only //
* (it lets us determine whether to print a warning suggesting //
* that you turn off autopublish.) //
*/ //
//
/** //
* @summary Publish a record set. //
* @memberOf Meteor //
* @locus Server //
* @param {String} name Name of the record set. If `null`, the set has no name, and the record set is automatically sent to all connected clients.
* @param {Function} func Function called on the server each time a client subscribes. Inside the function, `this` is the publish handler object, described below. If the client passed arguments to `subscribe`, the function is called with the same arguments.
*/ //
publish: function (name, handler, options) { // 1471
var self = this; // 1472
//
options = options || {}; // 1474
//
if (name && name in self.publish_handlers) { // 1476
Meteor._debug("Ignoring duplicate publish named '" + name + "'"); // 1477
return; // 1478
} //
//
if (Package.autopublish && !options.is_auto) { // 1481
// They have autopublish on, yet they're trying to manually //
// picking stuff to publish. They probably should turn off //
// autopublish. (This check isn't perfect -- if you create a //
// publish before you turn on autopublish, it won't catch //
// it. But this will definitely handle the simple case where //
// you've added the autopublish package to your app, and are //
// calling publish from your app code.) //
if (!self.warned_about_autopublish) { // 1489
self.warned_about_autopublish = true; // 1490
Meteor._debug("** You've set up some data subscriptions with Meteor.publish(), but\n" + "** you still have autopublish turned on. Because autopublish is still\n" + "** on, your Meteor.publish() calls won't have much effect. All data\n" + "** will still be sent to all clients.\n" + "**\n" + "** Turn off autopublish by removing the autopublish package:\n" + "**\n" + "** $ meteor remove autopublish\n" + "**\n" + "** .. and make sure you have Meteor.publish() and Meteor.subscribe() calls\n" + "** for each collection that you want clients to see.\n");
} //
} //
//
if (name) self.publish_handlers[name] = handler;else { // 1506
self.universal_publish_handlers.push(handler); // 1509
// Spin up the new publisher on any existing session too. Run each //
// session's subscription in a new Fiber, so that there's no change for //
// self.sessions to change while we're running this loop. //
_.each(self.sessions, function (session) { // 1513
if (!session._dontStartNewUniversalSubs) { // 1514
Fiber(function () { // 1515
session._startSubscription(handler); // 1516
}).run(); //
} //
}); //
} //
}, //
//
_removeSession: function (session) { // 1523
var self = this; // 1524
if (self.sessions[session.id]) { // 1525
delete self.sessions[session.id]; // 1526
} //
}, //
//
/** //
* @summary Defines functions that can be invoked over the network by clients. //
* @locus Anywhere //
* @param {Object} methods Dictionary whose keys are method names and values are functions. //
* @memberOf Meteor //
*/ //
methods: function (methods) { // 1536
var self = this; // 1537
_.each(methods, function (func, name) { // 1538
if (typeof func !== 'function') throw new Error("Method '" + name + "' must be a function"); // 1539
if (self.method_handlers[name]) throw new Error("A method named '" + name + "' is already defined"); // 1541
self.method_handlers[name] = func; // 1543
}); //
}, //
//
call: function (name /*, arguments */) { // 1547
// if it's a function, the last argument is the result callback, //
// not a parameter to the remote method. //
var args = Array.prototype.slice.call(arguments, 1); // 1550
if (args.length && typeof args[args.length - 1] === "function") var callback = args.pop(); // 1551
return this.apply(name, args, callback); // 1553
}, //
//
// @param options {Optional Object} //
// @param callback {Optional Function} //
apply: function (name, args, options, callback) { // 1558
var self = this; // 1559
//
// We were passed 3 arguments. They may be either (name, args, options) //
// or (name, args, callback) //
if (!callback && typeof options === 'function') { // 1563
callback = options; // 1564
options = {}; // 1565
} //
options = options || {}; // 1567
//
if (callback) // 1569
// It's not really necessary to do this, since we immediately //
// run the callback in this fiber before returning, but we do it //
// anyway for regularity. //
// XXX improve error message (and how we report it) //
callback = Meteor.bindEnvironment(callback, "delivering result of invoking '" + name + "'"); // 1574
//
// Run the handler //
var handler = self.method_handlers[name]; // 1580
var exception; // 1581
if (!handler) { // 1582
exception = new Meteor.Error(404, "Method not found"); // 1583
} else { //
// If this is a method call from within another method, get the //
// user state from the outer method, otherwise don't allow //
// setUserId to be called //
var userId = null; // 1588
var setUserId = function () { // 1589
throw new Error("Can't call setUserId on a server initiated method call"); // 1590
}; //
var connection = null; // 1592
var currentInvocation = DDP._CurrentInvocation.get(); // 1593
if (currentInvocation) { // 1594
userId = currentInvocation.userId; // 1595
setUserId = function (userId) { // 1596
currentInvocation.setUserId(userId); // 1597
}; //
connection = currentInvocation.connection; // 1599
} //
//
var invocation = new DDPCommon.MethodInvocation({ // 1602
isSimulation: false, // 1603
userId: userId, // 1604
setUserId: setUserId, // 1605
connection: connection, // 1606
randomSeed: DDPCommon.makeRpcSeed(currentInvocation, name) // 1607
}); //
try { // 1609
var result = DDP._CurrentInvocation.withValue(invocation, function () { // 1610
return maybeAuditArgumentChecks(handler, invocation, EJSON.clone(args), "internal call to '" + name + "'");
}); //
result = EJSON.clone(result); // 1615
} catch (e) { //
exception = e; // 1617
} //
} //
//
// Return the result in whichever way the caller asked for it. Note that we //
// do NOT block on the write fence in an analogous way to how the client //
// blocks on the relevant data being visible, so you are NOT guaranteed that //
// cursor observe callbacks have fired when your callback is invoked. (We //
// can change this if there's a real use case.) //
if (callback) { // 1626
callback(exception, result); // 1627
return undefined; // 1628
} //
if (exception) throw exception; // 1630
return result; // 1632
}, //
//
_urlForSession: function (sessionId) { // 1635
var self = this; // 1636
var session = self.sessions[sessionId]; // 1637
if (session) return session._socketUrl;else return null; // 1638
} //
}); //
//
var calculateVersion = function (clientSupportedVersions, serverSupportedVersions) { // 1645
var correctVersion = _.find(clientSupportedVersions, function (version) { // 1647
return _.contains(serverSupportedVersions, version); // 1648
}); //
if (!correctVersion) { // 1650
correctVersion = serverSupportedVersions[0]; // 1651
} //
return correctVersion; // 1653
}; //
//
DDPServer._calculateVersion = calculateVersion; // 1656
//
// "blind" exceptions other than those that were deliberately thrown to signal //
// errors to the client //
var wrapInternalException = function (exception, context) { // 1661
if (!exception || exception instanceof Meteor.Error) return exception; // 1662
//
// tests can set the 'expected' flag on an exception so it won't go to the //
// server log //
if (!exception.expected) { // 1667
Meteor._debug("Exception " + context, exception.stack); // 1668
if (exception.sanitizedError) { // 1669
Meteor._debug("Sanitized and reported to the client as:", exception.sanitizedError.message); // 1670
Meteor._debug(); // 1671
} //
} //
//
// Did the error contain more details that could have been useful if caught in //
// server code (or if thrown from non-client-originated code), but also //
// provided a "sanitized" version with more context than 500 Internal server //
// error? Use that. //
if (exception.sanitizedError) { // 1679
if (exception.sanitizedError instanceof Meteor.Error) return exception.sanitizedError; // 1680
Meteor._debug("Exception " + context + " provides a sanitizedError that " + "is not a Meteor.Error; ignoring"); // 1682
} //
//
return new Meteor.Error(500, "Internal server error"); // 1686
}; //
//
// Audit argument checks, if the audit-argument-checks package exists (it is a //
// weak dependency of this package). //
var maybeAuditArgumentChecks = function (f, context, args, description) { // 1692
args = args || []; // 1693
if (Package['audit-argument-checks']) { // 1694
return Match._failIfArgumentsAreNotAllChecked(f, context, args, description); // 1695
} //
return f.apply(context, args); // 1698
}; //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
}).call(this);
(function(){
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //
// packages/ddp-server/writefence.js //
// //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
var path = Npm.require('path'); // 1
var Future = Npm.require(path.join('fibers', 'future')); // 2
//
// A write fence collects a group of writes, and provides a callback //
// when all of the writes are fully committed and propagated (all //
// observers have been notified of the write and acknowledged it.) //
// //
DDPServer._WriteFence = function () { // 8
var self = this; // 9
//
self.armed = false; // 11
self.fired = false; // 12
self.retired = false; // 13
self.outstanding_writes = 0; // 14
self.before_fire_callbacks = []; // 15
self.completion_callbacks = []; // 16
}; //
//
// The current write fence. When there is a current write fence, code //
// that writes to databases should register their writes with it using //
// beginWrite(). //
// //
DDPServer._CurrentWriteFence = new Meteor.EnvironmentVariable(); // 23
//
_.extend(DDPServer._WriteFence.prototype, { // 25
// Start tracking a write, and return an object to represent it. The //
// object has a single method, committed(). This method should be //
// called when the write is fully committed and propagated. You can //
// continue to add writes to the WriteFence up until it is triggered //
// (calls its callbacks because all writes have committed.) //
beginWrite: function () { // 31
var self = this; // 32
//
if (self.retired) return { committed: function () {} }; // 34
//
if (self.fired) throw new Error("fence has already activated -- too late to add writes"); // 37
//
self.outstanding_writes++; // 40
var committed = false; // 41
return { // 42
committed: function () { // 43
if (committed) throw new Error("committed called twice on the same write"); // 44
committed = true; // 46
self.outstanding_writes--; // 47
self._maybeFire(); // 48
} //
}; //
}, //
//
// Arm the fence. Once the fence is armed, and there are no more //
// uncommitted writes, it will activate. //
arm: function () { // 55
var self = this; // 56
if (self === DDPServer._CurrentWriteFence.get()) throw Error("Can't arm the current fence"); // 57
self.armed = true; // 59
self._maybeFire(); // 60
}, //
//
// Register a function to be called once before firing the fence. //
// Callback function can add new writes to the fence, in which case //
// it won't fire until those writes are done as well. //
onBeforeFire: function (func) { // 66
var self = this; // 67
if (self.fired) throw new Error("fence has already activated -- too late to " + "add a callback"); // 68
self.before_fire_callbacks.push(func); // 71
}, //
//
// Register a function to be called when the fence fires. //
onAllCommitted: function (func) { // 75
var self = this; // 76
if (self.fired) throw new Error("fence has already activated -- too late to " + "add a callback"); // 77
self.completion_callbacks.push(func); // 80
}, //
//
// Convenience function. Arms the fence, then blocks until it fires. //
armAndWait: function () { // 84
var self = this; // 85
var future = new Future(); // 86
self.onAllCommitted(function () { // 87
future['return'](); // 88
}); //
self.arm(); // 90
future.wait(); // 91
}, //
//
_maybeFire: function () { // 94
var self = this; // 95
if (self.fired) throw new Error("write fence already activated?"); // 96
if (self.armed && !self.outstanding_writes) { // 98
function invokeCallback(func) { // 99
try { // 100
func(self); // 101
} catch (err) { //
Meteor._debug("exception in write fence callback:", err); // 103
} //
} //
//
self.outstanding_writes++; // 107
while (self.before_fire_callbacks.length > 0) { // 108
var callbacks = self.before_fire_callbacks; // 109
self.before_fire_callbacks = []; // 110
_.each(callbacks, invokeCallback); // 111
} //
self.outstanding_writes--; // 113
//
if (!self.outstanding_writes) { // 115
self.fired = true; // 116
var callbacks = self.completion_callbacks; // 117
self.completion_callbacks = []; // 118
_.each(callbacks, invokeCallback); // 119
} //
} //
}, //
//
// Deactivate this fence so that adding more writes has no effect. //
// The fence must have already fired. //
retire: function () { // 126
var self = this; // 127
if (!self.fired) throw new Error("Can't retire a fence that hasn't fired."); // 128
self.retired = true; // 130
} //
}); //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
}).call(this);
(function(){
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //
// packages/ddp-server/crossbar.js //
// //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// A "crossbar" is a class that provides structured notification registration. //
// See _match for the definition of how a notification matches a trigger. //
// All notifications and triggers must have a string key named 'collection'. //
//
DDPServer._Crossbar = function (options) { // 5
var self = this; // 6
options = options || {}; // 7
//
self.nextId = 1; // 9
// map from collection name (string) -> listener id -> object. each object has //
// keys 'trigger', 'callback'. As a hack, the empty string means "no //
// collection". //
self.listenersByCollection = {}; // 13
self.factPackage = options.factPackage || "livedata"; // 14
self.factName = options.factName || null; // 15
}; //
//
_.extend(DDPServer._Crossbar.prototype, { // 18
// msg is a trigger or a notification //
_collectionForMessage: function (msg) { // 20
var self = this; // 21
if (!_.has(msg, 'collection')) { // 22
return ''; // 23
} else if (typeof msg.collection === 'string') { //
if (msg.collection === '') throw Error("Message has empty collection!"); // 25
return msg.collection; // 27
} else { //
throw Error("Message has non-string collection!"); // 29
} //
}, //
//
// Listen for notification that match 'trigger'. A notification //
// matches if it has the key-value pairs in trigger as a //
// subset. When a notification matches, call 'callback', passing //
// the actual notification. //
// //
// Returns a listen handle, which is an object with a method //
// stop(). Call stop() to stop listening. //
// //
// XXX It should be legal to call fire() from inside a listen() //
// callback? //
listen: function (trigger, callback) { // 43
var self = this; // 44
var id = self.nextId++; // 45
//
var collection = self._collectionForMessage(trigger); // 47
var record = { trigger: EJSON.clone(trigger), callback: callback }; // 48
if (!_.has(self.listenersByCollection, collection)) { // 49
self.listenersByCollection[collection] = {}; // 50
} //
self.listenersByCollection[collection][id] = record; // 52
//
if (self.factName && Package.facts) { // 54
Package.facts.Facts.incrementServerFact(self.factPackage, self.factName, 1); // 55
} //
//
return { // 59
stop: function () { // 60
if (self.factName && Package.facts) { // 61
Package.facts.Facts.incrementServerFact(self.factPackage, self.factName, -1); // 62
} //
delete self.listenersByCollection[collection][id]; // 65
if (_.isEmpty(self.listenersByCollection[collection])) { // 66
delete self.listenersByCollection[collection]; // 67
} //
} //
}; //
}, //
//
// Fire the provided 'notification' (an object whose attribute //
// values are all JSON-compatibile) -- inform all matching listeners //
// (registered with listen()). //
// //
// If fire() is called inside a write fence, then each of the //
// listener callbacks will be called inside the write fence as well. //
// //
// The listeners may be invoked in parallel, rather than serially. //
fire: function (notification) { // 81
var self = this; // 82
//
var collection = self._collectionForMessage(notification); // 84
//
if (!_.has(self.listenersByCollection, collection)) { // 86
return; // 87
} //
//
var listenersForCollection = self.listenersByCollection[collection]; // 90
var callbackIds = []; // 91
_.each(listenersForCollection, function (l, id) { // 92
if (self._matches(notification, l.trigger)) { // 93
callbackIds.push(id); // 94
} //
}); //
//
// Listener callbacks can yield, so we need to first find all the ones that //
// match in a single iteration over self.listenersByCollection (which can't //
// be mutated during this iteration), and then invoke the matching //
// callbacks, checking before each call to ensure they haven't stopped. //
// Note that we don't have to check that //
// self.listenersByCollection[collection] still === listenersForCollection, //
// because the only way that stops being true is if listenersForCollection //
// first gets reduced down to the empty object (and then never gets //
// increased again). //
_.each(callbackIds, function (id) { // 107
if (_.has(listenersForCollection, id)) { // 108
listenersForCollection[id].callback(notification); // 109
} //
}); //
}, //
//
// A notification matches a trigger if all keys that exist in both are equal. //
// //
// Examples: //
// N:{collection: "C"} matches T:{collection: "C"} //
// (a non-targeted write to a collection matches a //
// non-targeted query) //
// N:{collection: "C", id: "X"} matches T:{collection: "C"} //
// (a targeted write to a collection matches a non-targeted query) //
// N:{collection: "C"} matches T:{collection: "C", id: "X"} //
// (a non-targeted write to a collection matches a //
// targeted query) //
// N:{collection: "C", id: "X"} matches T:{collection: "C", id: "X"} //
// (a targeted write to a collection matches a targeted query targeted //
// at the same document) //
// N:{collection: "C", id: "X"} does not match T:{collection: "C", id: "Y"} //
// (a targeted write to a collection does not match a targeted query //
// targeted at a different document) //
_matches: function (notification, trigger) { // 131
// Most notifications that use the crossbar have a string `collection` and //
// maybe an `id` that is a string or ObjectID. We're already dividing up //
// triggers by collection, but let's fast-track "nope, different ID" (and //
// avoid the overly generic EJSON.equals). This makes a noticeable //
// performance difference; see https://github.com/meteor/meteor/pull/3697 //
if (typeof notification.id === 'string' && typeof trigger.id === 'string' && notification.id !== trigger.id) { // 137
return false; // 140
} //
if (notification.id instanceof MongoID.ObjectID && trigger.id instanceof MongoID.ObjectID && !notification.id.equals(trigger.id)) {
return false; // 145
} //
//
return _.all(trigger, function (triggerValue, key) { // 148
return !_.has(notification, key) || EJSON.equals(triggerValue, notification[key]); // 149
}); //
} //
}); //
//
// The "invalidation crossbar" is a specific instance used by the DDP server to //
// implement write fence notifications. Listener callbacks on this crossbar //
// should call beginWrite on the current write fence before they return, if they //
// want to delay the write fence from firing (ie, the DDP method-data-updated //
// message from being sent). //
DDPServer._InvalidationCrossbar = new DDPServer._Crossbar({ // 160
factName: "invalidation-crossbar-listeners" // 161
}); //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
}).call(this);
(function(){
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //
// packages/ddp-server/server_convenience.js //
// //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
if (process.env.DDP_DEFAULT_CONNECTION_URL) { // 1
__meteor_runtime_config__.DDP_DEFAULT_CONNECTION_URL = process.env.DDP_DEFAULT_CONNECTION_URL; // 2
} //
//
Meteor.server = new Server(); // 6
//
Meteor.refresh = function (notification) { // 8
DDPServer._InvalidationCrossbar.fire(notification); // 9
}; //
//
// Proxy the public methods of Meteor.server so they can //
// be called directly on Meteor. //
_.each(['publish', 'methods', 'call', 'apply', 'onConnection'], function (name) { // 14
Meteor[name] = _.bind(Meteor.server[name], Meteor.server); // 16
}); //
//
// Meteor.server used to be called Meteor.default_server. Provide //
// backcompat as a courtesy even though it was never documented. //
// XXX COMPAT WITH 0.6.4 //
Meteor.default_server = Meteor.server; // 22
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
}).call(this);
/* Exports */
if (typeof Package === 'undefined') Package = {};
Package['ddp-server'] = {
DDPServer: DDPServer
};
})();
//# sourceMappingURL=ddp-server.js.map