ethstats-server/web-app/.meteor/local/build/programs/server/packages/ddp.js

4782 lines
572 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 Tracker = Package.tracker.Tracker;
var Deps = Package.tracker.Deps;
var Log = Package.logging.Log;
var Retry = Package.retry.Retry;
var Hook = Package['callback-hook'].Hook;
var LocalCollection = Package.minimongo.LocalCollection;
var Minimongo = Package.minimongo.Minimongo;
/* Package-scope variables */
var DDP, DDPServer, LivedataTest, toSockjsUrl, toWebsocketUrl, StreamServer, Heartbeat, Server, SUPPORTED_DDP_VERSIONS, MethodInvocation, parseDDP, stringifyDDP, RandomStream, makeRpcSeed, allConnections;
(function () {
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //
// packages/ddp/common.js //
// //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
/** // 1
* @namespace DDP // 2
* @summary The namespace for DDP-related methods. // 3
*/ // 4
DDP = {}; // 5
LivedataTest = {}; // 6
// 7
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
}).call(this);
(function () {
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //
// packages/ddp/stream_client_nodejs.js //
// //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// @param endpoint {String} URL to Meteor app // 1
// "http://subdomain.meteor.com/" or "/" or // 2
// "ddp+sockjs://foo-**.meteor.com/sockjs" // 3
// // 4
// We do some rewriting of the URL to eventually make it "ws://" or "wss://", // 5
// whatever was passed in. At the very least, what Meteor.absoluteUrl() returns // 6
// us should work. // 7
// // 8
// We don't do any heartbeating. (The logic that did this in sockjs was removed, // 9
// because it used a built-in sockjs mechanism. We could do it with WebSocket // 10
// ping frames or with DDP-level messages.) // 11
LivedataTest.ClientStream = function (endpoint, options) { // 12
var self = this; // 13
options = options || {}; // 14
// 15
self.options = _.extend({ // 16
retry: true // 17
}, options); // 18
// 19
self.client = null; // created in _launchConnection // 20
self.endpoint = endpoint; // 21
// 22
self.headers = self.options.headers || {}; // 23
// 24
self._initCommon(self.options); // 25
// 26
//// Kickoff! // 27
self._launchConnection(); // 28
}; // 29
// 30
_.extend(LivedataTest.ClientStream.prototype, { // 31
// 32
// data is a utf8 string. Data sent while not connected is dropped on // 33
// the floor, and it is up the user of this API to retransmit lost // 34
// messages on 'reset' // 35
send: function (data) { // 36
var self = this; // 37
if (self.currentStatus.connected) { // 38
self.client.send(data); // 39
} // 40
}, // 41
// 42
// Changes where this connection points // 43
_changeUrl: function (url) { // 44
var self = this; // 45
self.endpoint = url; // 46
}, // 47
// 48
_onConnect: function (client) { // 49
var self = this; // 50
// 51
if (client !== self.client) { // 52
// This connection is not from the last call to _launchConnection. // 53
// But _launchConnection calls _cleanup which closes previous connections. // 54
// It's our belief that this stifles future 'open' events, but maybe // 55
// we are wrong? // 56
throw new Error("Got open from inactive client " + !!self.client); // 57
} // 58
// 59
if (self._forcedToDisconnect) { // 60
// We were asked to disconnect between trying to open the connection and // 61
// actually opening it. Let's just pretend this never happened. // 62
self.client.close(); // 63
self.client = null; // 64
return; // 65
} // 66
// 67
if (self.currentStatus.connected) { // 68
// We already have a connection. It must have been the case that we // 69
// started two parallel connection attempts (because we wanted to // 70
// 'reconnect now' on a hanging connection and we had no way to cancel the // 71
// connection attempt.) But this shouldn't happen (similarly to the client // 72
// !== self.client check above). // 73
throw new Error("Two parallel connections?"); // 74
} // 75
// 76
self._clearConnectionTimer(); // 77
// 78
// update status // 79
self.currentStatus.status = "connected"; // 80
self.currentStatus.connected = true; // 81
self.currentStatus.retryCount = 0; // 82
self.statusChanged(); // 83
// 84
// fire resets. This must come after status change so that clients // 85
// can call send from within a reset callback. // 86
_.each(self.eventCallbacks.reset, function (callback) { callback(); }); // 87
}, // 88
// 89
_cleanup: function (maybeError) { // 90
var self = this; // 91
// 92
self._clearConnectionTimer(); // 93
if (self.client) { // 94
var client = self.client; // 95
self.client = null; // 96
client.close(); // 97
// 98
_.each(self.eventCallbacks.disconnect, function (callback) { // 99
callback(maybeError); // 100
}); // 101
} // 102
}, // 103
// 104
_clearConnectionTimer: function () { // 105
var self = this; // 106
// 107
if (self.connectionTimer) { // 108
clearTimeout(self.connectionTimer); // 109
self.connectionTimer = null; // 110
} // 111
}, // 112
// 113
_getProxyUrl: function (targetUrl) { // 114
var self = this; // 115
// Similar to code in tools/http-helpers.js. // 116
var proxy = process.env.HTTP_PROXY || process.env.http_proxy || null; // 117
// if we're going to a secure url, try the https_proxy env variable first. // 118
if (targetUrl.match(/^wss:/)) { // 119
proxy = process.env.HTTPS_PROXY || process.env.https_proxy || proxy; // 120
} // 121
return proxy; // 122
}, // 123
// 124
_launchConnection: function () { // 125
var self = this; // 126
self._cleanup(); // cleanup the old socket, if there was one. // 127
// 128
// Since server-to-server DDP is still an experimental feature, we only // 129
// require the module if we actually create a server-to-server // 130
// connection. // 131
var FayeWebSocket = Npm.require('faye-websocket'); // 132
// 133
var targetUrl = toWebsocketUrl(self.endpoint); // 134
var fayeOptions = { headers: self.headers }; // 135
var proxyUrl = self._getProxyUrl(targetUrl); // 136
if (proxyUrl) { // 137
fayeOptions.proxy = { origin: proxyUrl }; // 138
}; // 139
// 140
// We would like to specify 'ddp' as the subprotocol here. The npm module we // 141
// used to use as a client would fail the handshake if we ask for a // 142
// subprotocol and the server doesn't send one back (and sockjs doesn't). // 143
// Faye doesn't have that behavior; it's unclear from reading RFC 6455 if // 144
// Faye is erroneous or not. So for now, we don't specify protocols. // 145
var subprotocols = []; // 146
// 147
var client = self.client = new FayeWebSocket.Client( // 148
targetUrl, subprotocols, fayeOptions); // 149
// 150
self._clearConnectionTimer(); // 151
self.connectionTimer = Meteor.setTimeout( // 152
function () { // 153
self._lostConnection( // 154
new DDP.ConnectionError("DDP connection timed out")); // 155
}, // 156
self.CONNECT_TIMEOUT); // 157
// 158
self.client.on('open', Meteor.bindEnvironment(function () { // 159
return self._onConnect(client); // 160
}, "stream connect callback")); // 161
// 162
var clientOnIfCurrent = function (event, description, f) { // 163
self.client.on(event, Meteor.bindEnvironment(function () { // 164
// Ignore events from any connection we've already cleaned up. // 165
if (client !== self.client) // 166
return; // 167
f.apply(this, arguments); // 168
}, description)); // 169
}; // 170
// 171
clientOnIfCurrent('error', 'stream error callback', function (error) { // 172
if (!self.options._dontPrintErrors) // 173
Meteor._debug("stream error", error.message); // 174
// 175
// Faye's 'error' object is not a JS error (and among other things, // 176
// doesn't stringify well). Convert it to one. // 177
self._lostConnection(new DDP.ConnectionError(error.message)); // 178
}); // 179
// 180
// 181
clientOnIfCurrent('close', 'stream close callback', function () { // 182
self._lostConnection(); // 183
}); // 184
// 185
// 186
clientOnIfCurrent('message', 'stream message callback', function (message) { // 187
// Ignore binary frames, where message.data is a Buffer // 188
if (typeof message.data !== "string") // 189
return; // 190
// 191
_.each(self.eventCallbacks.message, function (callback) { // 192
callback(message.data); // 193
}); // 194
}); // 195
} // 196
}); // 197
// 198
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
}).call(this);
(function () {
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //
// packages/ddp/stream_client_common.js //
// //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// XXX from Underscore.String (http://epeli.github.com/underscore.string/) // 1
var startsWith = function(str, starts) { // 2
return str.length >= starts.length && // 3
str.substring(0, starts.length) === starts; // 4
}; // 5
var endsWith = function(str, ends) { // 6
return str.length >= ends.length && // 7
str.substring(str.length - ends.length) === ends; // 8
}; // 9
// 10
// @param url {String} URL to Meteor app, eg: // 11
// "/" or "madewith.meteor.com" or "https://foo.meteor.com" // 12
// or "ddp+sockjs://ddp--****-foo.meteor.com/sockjs" // 13
// @returns {String} URL to the endpoint with the specific scheme and subPath, e.g. // 14
// for scheme "http" and subPath "sockjs" // 15
// "http://subdomain.meteor.com/sockjs" or "/sockjs" // 16
// or "https://ddp--1234-foo.meteor.com/sockjs" // 17
var translateUrl = function(url, newSchemeBase, subPath) { // 18
if (! newSchemeBase) { // 19
newSchemeBase = "http"; // 20
} // 21
// 22
var ddpUrlMatch = url.match(/^ddp(i?)\+sockjs:\/\//); // 23
var httpUrlMatch = url.match(/^http(s?):\/\//); // 24
var newScheme; // 25
if (ddpUrlMatch) { // 26
// Remove scheme and split off the host. // 27
var urlAfterDDP = url.substr(ddpUrlMatch[0].length); // 28
newScheme = ddpUrlMatch[1] === "i" ? newSchemeBase : newSchemeBase + "s"; // 29
var slashPos = urlAfterDDP.indexOf('/'); // 30
var host = // 31
slashPos === -1 ? urlAfterDDP : urlAfterDDP.substr(0, slashPos); // 32
var rest = slashPos === -1 ? '' : urlAfterDDP.substr(slashPos); // 33
// 34
// In the host (ONLY!), change '*' characters into random digits. This // 35
// allows different stream connections to connect to different hostnames // 36
// and avoid browser per-hostname connection limits. // 37
host = host.replace(/\*/g, function () { // 38
return Math.floor(Random.fraction()*10); // 39
}); // 40
// 41
return newScheme + '://' + host + rest; // 42
} else if (httpUrlMatch) { // 43
newScheme = !httpUrlMatch[1] ? newSchemeBase : newSchemeBase + "s"; // 44
var urlAfterHttp = url.substr(httpUrlMatch[0].length); // 45
url = newScheme + "://" + urlAfterHttp; // 46
} // 47
// 48
// Prefix FQDNs but not relative URLs // 49
if (url.indexOf("://") === -1 && !startsWith(url, "/")) { // 50
url = newSchemeBase + "://" + url; // 51
} // 52
// 53
// XXX This is not what we should be doing: if I have a site // 54
// deployed at "/foo", then DDP.connect("/") should actually connect // 55
// to "/", not to "/foo". "/" is an absolute path. (Contrast: if // 56
// deployed at "/foo", it would be reasonable for DDP.connect("bar") // 57
// to connect to "/foo/bar"). // 58
// // 59
// We should make this properly honor absolute paths rather than // 60
// forcing the path to be relative to the site root. Simultaneously, // 61
// we should set DDP_DEFAULT_CONNECTION_URL to include the site // 62
// root. See also client_convenience.js #RationalizingRelativeDDPURLs // 63
url = Meteor._relativeToSiteRootUrl(url); // 64
// 65
if (endsWith(url, "/")) // 66
return url + subPath; // 67
else // 68
return url + "/" + subPath; // 69
}; // 70
// 71
toSockjsUrl = function (url) { // 72
return translateUrl(url, "http", "sockjs"); // 73
}; // 74
// 75
toWebsocketUrl = function (url) { // 76
var ret = translateUrl(url, "ws", "websocket"); // 77
return ret; // 78
}; // 79
// 80
LivedataTest.toSockjsUrl = toSockjsUrl; // 81
// 82
// 83
_.extend(LivedataTest.ClientStream.prototype, { // 84
// 85
// Register for callbacks. // 86
on: function (name, callback) { // 87
var self = this; // 88
// 89
if (name !== 'message' && name !== 'reset' && name !== 'disconnect') // 90
throw new Error("unknown event type: " + name); // 91
// 92
if (!self.eventCallbacks[name]) // 93
self.eventCallbacks[name] = []; // 94
self.eventCallbacks[name].push(callback); // 95
}, // 96
// 97
// 98
_initCommon: function (options) { // 99
var self = this; // 100
options = options || {}; // 101
// 102
//// Constants // 103
// 104
// how long to wait until we declare the connection attempt // 105
// failed. // 106
self.CONNECT_TIMEOUT = options.connectTimeoutMs || 10000; // 107
// 108
self.eventCallbacks = {}; // name -> [callback] // 109
// 110
self._forcedToDisconnect = false; // 111
// 112
//// Reactive status // 113
self.currentStatus = { // 114
status: "connecting", // 115
connected: false, // 116
retryCount: 0 // 117
}; // 118
// 119
// 120
self.statusListeners = typeof Tracker !== 'undefined' && new Tracker.Dependency; // 121
self.statusChanged = function () { // 122
if (self.statusListeners) // 123
self.statusListeners.changed(); // 124
}; // 125
// 126
//// Retry logic // 127
self._retry = new Retry; // 128
self.connectionTimer = null; // 129
// 130
}, // 131
// 132
// Trigger a reconnect. // 133
reconnect: function (options) { // 134
var self = this; // 135
options = options || {}; // 136
// 137
if (options.url) { // 138
self._changeUrl(options.url); // 139
} // 140
// 141
if (options._sockjsOptions) { // 142
self.options._sockjsOptions = options._sockjsOptions; // 143
} // 144
// 145
if (self.currentStatus.connected) { // 146
if (options._force || options.url) { // 147
// force reconnect. // 148
self._lostConnection(new DDP.ForcedReconnectError); // 149
} // else, noop. // 150
return; // 151
} // 152
// 153
// if we're mid-connection, stop it. // 154
if (self.currentStatus.status === "connecting") { // 155
// Pretend it's a clean close. // 156
self._lostConnection(); // 157
} // 158
// 159
self._retry.clear(); // 160
self.currentStatus.retryCount -= 1; // don't count manual retries // 161
self._retryNow(); // 162
}, // 163
// 164
disconnect: function (options) { // 165
var self = this; // 166
options = options || {}; // 167
// 168
// Failed is permanent. If we're failed, don't let people go back // 169
// online by calling 'disconnect' then 'reconnect'. // 170
if (self._forcedToDisconnect) // 171
return; // 172
// 173
// If _permanent is set, permanently disconnect a stream. Once a stream // 174
// is forced to disconnect, it can never reconnect. This is for // 175
// error cases such as ddp version mismatch, where trying again // 176
// won't fix the problem. // 177
if (options._permanent) { // 178
self._forcedToDisconnect = true; // 179
} // 180
// 181
self._cleanup(); // 182
self._retry.clear(); // 183
// 184
self.currentStatus = { // 185
status: (options._permanent ? "failed" : "offline"), // 186
connected: false, // 187
retryCount: 0 // 188
}; // 189
// 190
if (options._permanent && options._error) // 191
self.currentStatus.reason = options._error; // 192
// 193
self.statusChanged(); // 194
}, // 195
// 196
// maybeError is set unless it's a clean protocol-level close. // 197
_lostConnection: function (maybeError) { // 198
var self = this; // 199
// 200
self._cleanup(maybeError); // 201
self._retryLater(maybeError); // sets status. no need to do it here. // 202
}, // 203
// 204
// fired when we detect that we've gone online. try to reconnect // 205
// immediately. // 206
_online: function () { // 207
// if we've requested to be offline by disconnecting, don't reconnect. // 208
if (this.currentStatus.status != "offline") // 209
this.reconnect(); // 210
}, // 211
// 212
_retryLater: function (maybeError) { // 213
var self = this; // 214
// 215
var timeout = 0; // 216
if (self.options.retry || // 217
(maybeError && maybeError.errorType === "DDP.ForcedReconnectError")) { // 218
timeout = self._retry.retryLater( // 219
self.currentStatus.retryCount, // 220
_.bind(self._retryNow, self) // 221
); // 222
self.currentStatus.status = "waiting"; // 223
self.currentStatus.retryTime = (new Date()).getTime() + timeout; // 224
} else { // 225
self.currentStatus.status = "failed"; // 226
delete self.currentStatus.retryTime; // 227
} // 228
// 229
self.currentStatus.connected = false; // 230
self.statusChanged(); // 231
}, // 232
// 233
_retryNow: function () { // 234
var self = this; // 235
// 236
if (self._forcedToDisconnect) // 237
return; // 238
// 239
self.currentStatus.retryCount += 1; // 240
self.currentStatus.status = "connecting"; // 241
self.currentStatus.connected = false; // 242
delete self.currentStatus.retryTime; // 243
self.statusChanged(); // 244
// 245
self._launchConnection(); // 246
}, // 247
// 248
// 249
// Get current status. Reactive. // 250
status: function () { // 251
var self = this; // 252
if (self.statusListeners) // 253
self.statusListeners.depend(); // 254
return self.currentStatus; // 255
} // 256
}); // 257
// 258
DDP.ConnectionError = Meteor.makeErrorType( // 259
"DDP.ConnectionError", function (message) { // 260
var self = this; // 261
self.message = message; // 262
}); // 263
// 264
DDP.ForcedReconnectError = Meteor.makeErrorType( // 265
"DDP.ForcedReconnectError", function () {}); // 266
// 267
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
}).call(this);
(function () {
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //
// packages/ddp/stream_server.js //
// //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
var url = Npm.require('url'); // 1
// 2
var pathPrefix = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX || ""; // 3
// 4
StreamServer = function () { // 5
var self = this; // 6
self.registration_callbacks = []; // 7
self.open_sockets = []; // 8
// 9
// Because we are installing directly onto WebApp.httpServer instead of using // 10
// WebApp.app, we have to process the path prefix ourselves. // 11
self.prefix = pathPrefix + '/sockjs'; // 12
// routepolicy is only a weak dependency, because we don't need it if we're // 13
// just doing server-to-server DDP as a client. // 14
if (Package.routepolicy) { // 15
Package.routepolicy.RoutePolicy.declare(self.prefix + '/', 'network'); // 16
} // 17
// 18
// set up sockjs // 19
var sockjs = Npm.require('sockjs'); // 20
var serverOptions = { // 21
prefix: self.prefix, // 22
log: function() {}, // 23
// this is the default, but we code it explicitly because we depend // 24
// on it in stream_client:HEARTBEAT_TIMEOUT // 25
heartbeat_delay: 45000, // 26
// The default disconnect_delay is 5 seconds, but if the server ends up CPU // 27
// bound for that much time, SockJS might not notice that the user has // 28
// reconnected because the timer (of disconnect_delay ms) can fire before // 29
// SockJS processes the new connection. Eventually we'll fix this by not // 30
// combining CPU-heavy processing with SockJS termination (eg a proxy which // 31
// converts to Unix sockets) but for now, raise the delay. // 32
disconnect_delay: 60 * 1000, // 33
// Set the USE_JSESSIONID environment variable to enable setting the // 34
// JSESSIONID cookie. This is useful for setting up proxies with // 35
// session affinity. // 36
jsessionid: !!process.env.USE_JSESSIONID // 37
}; // 38
// 39
// If you know your server environment (eg, proxies) will prevent websockets // 40
// from ever working, set $DISABLE_WEBSOCKETS and SockJS clients (ie, // 41
// browsers) will not waste time attempting to use them. // 42
// (Your server will still have a /websocket endpoint.) // 43
if (process.env.DISABLE_WEBSOCKETS) // 44
serverOptions.websocket = false; // 45
// 46
self.server = sockjs.createServer(serverOptions); // 47
if (!Package.webapp) { // 48
throw new Error("Cannot create a DDP server without the webapp package"); // 49
} // 50
// Install the sockjs handlers, but we want to keep around our own particular // 51
// request handler that adjusts idle timeouts while we have an outstanding // 52
// request. This compensates for the fact that sockjs removes all listeners // 53
// for "request" to add its own. // 54
Package.webapp.WebApp.httpServer.removeListener('request', Package.webapp.WebApp._timeoutAdjustmentRequestCallback); // 55
self.server.installHandlers(Package.webapp.WebApp.httpServer); // 56
Package.webapp.WebApp.httpServer.addListener('request', Package.webapp.WebApp._timeoutAdjustmentRequestCallback); // 57
// 58
// Support the /websocket endpoint // 59
self._redirectWebsocketEndpoint(); // 60
// 61
self.server.on('connection', function (socket) { // 62
socket.send = function (data) { // 63
socket.write(data); // 64
}; // 65
socket.on('close', function () { // 66
self.open_sockets = _.without(self.open_sockets, socket); // 67
}); // 68
self.open_sockets.push(socket); // 69
// 70
// XXX COMPAT WITH 0.6.6. Send the old style welcome message, which // 71
// will force old clients to reload. Remove this once we're not // 72
// concerned about people upgrading from a pre-0.7.0 release. Also, // 73
// remove the clause in the client that ignores the welcome message // 74
// (livedata_connection.js) // 75
socket.send(JSON.stringify({server_id: "0"})); // 76
// 77
// call all our callbacks when we get a new socket. they will do the // 78
// work of setting up handlers and such for specific messages. // 79
_.each(self.registration_callbacks, function (callback) { // 80
callback(socket); // 81
}); // 82
}); // 83
// 84
}; // 85
// 86
_.extend(StreamServer.prototype, { // 87
// call my callback when a new socket connects. // 88
// also call it for all current connections. // 89
register: function (callback) { // 90
var self = this; // 91
self.registration_callbacks.push(callback); // 92
_.each(self.all_sockets(), function (socket) { // 93
callback(socket); // 94
}); // 95
}, // 96
// 97
// get a list of all sockets // 98
all_sockets: function () { // 99
var self = this; // 100
return _.values(self.open_sockets); // 101
}, // 102
// 103
// Redirect /websocket to /sockjs/websocket in order to not expose // 104
// sockjs to clients that want to use raw websockets // 105
_redirectWebsocketEndpoint: function() { // 106
var self = this; // 107
// Unfortunately we can't use a connect middleware here since // 108
// sockjs installs itself prior to all existing listeners // 109
// (meaning prior to any connect middlewares) so we need to take // 110
// an approach similar to overshadowListeners in // 111
// https://github.com/sockjs/sockjs-node/blob/cf820c55af6a9953e16558555a31decea554f70e/src/utils.coffee // 112
_.each(['request', 'upgrade'], function(event) { // 113
var httpServer = Package.webapp.WebApp.httpServer; // 114
var oldHttpServerListeners = httpServer.listeners(event).slice(0); // 115
httpServer.removeAllListeners(event); // 116
// 117
// request and upgrade have different arguments passed but // 118
// we only care about the first one which is always request // 119
var newListener = function(request /*, moreArguments */) { // 120
// Store arguments for use within the closure below // 121
var args = arguments; // 122
// 123
// Rewrite /websocket and /websocket/ urls to /sockjs/websocket while // 124
// preserving query string. // 125
var parsedUrl = url.parse(request.url); // 126
if (parsedUrl.pathname === pathPrefix + '/websocket' || // 127
parsedUrl.pathname === pathPrefix + '/websocket/') { // 128
parsedUrl.pathname = self.prefix + '/websocket'; // 129
request.url = url.format(parsedUrl); // 130
} // 131
_.each(oldHttpServerListeners, function(oldListener) { // 132
oldListener.apply(httpServer, args); // 133
}); // 134
}; // 135
httpServer.addListener(event, newListener); // 136
}); // 137
} // 138
}); // 139
// 140
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
}).call(this);
(function () {
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //
// packages/ddp/heartbeat.js //
// //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Heartbeat options: // 1
// heartbeatInterval: interval to send pings, in milliseconds. // 2
// heartbeatTimeout: timeout to close the connection if a reply isn't // 3
// received, in milliseconds. // 4
// sendPing: function to call to send a ping on the connection. // 5
// onTimeout: function to call to close the connection. // 6
// 7
Heartbeat = function (options) { // 8
var self = this; // 9
// 10
self.heartbeatInterval = options.heartbeatInterval; // 11
self.heartbeatTimeout = options.heartbeatTimeout; // 12
self._sendPing = options.sendPing; // 13
self._onTimeout = options.onTimeout; // 14
// 15
self._heartbeatIntervalHandle = null; // 16
self._heartbeatTimeoutHandle = null; // 17
}; // 18
// 19
_.extend(Heartbeat.prototype, { // 20
stop: function () { // 21
var self = this; // 22
self._clearHeartbeatIntervalTimer(); // 23
self._clearHeartbeatTimeoutTimer(); // 24
}, // 25
// 26
start: function () { // 27
var self = this; // 28
self.stop(); // 29
self._startHeartbeatIntervalTimer(); // 30
}, // 31
// 32
_startHeartbeatIntervalTimer: function () { // 33
var self = this; // 34
self._heartbeatIntervalHandle = Meteor.setTimeout( // 35
_.bind(self._heartbeatIntervalFired, self), // 36
self.heartbeatInterval // 37
); // 38
}, // 39
// 40
_startHeartbeatTimeoutTimer: function () { // 41
var self = this; // 42
self._heartbeatTimeoutHandle = Meteor.setTimeout( // 43
_.bind(self._heartbeatTimeoutFired, self), // 44
self.heartbeatTimeout // 45
); // 46
}, // 47
// 48
_clearHeartbeatIntervalTimer: function () { // 49
var self = this; // 50
if (self._heartbeatIntervalHandle) { // 51
Meteor.clearTimeout(self._heartbeatIntervalHandle); // 52
self._heartbeatIntervalHandle = null; // 53
} // 54
}, // 55
// 56
_clearHeartbeatTimeoutTimer: function () { // 57
var self = this; // 58
if (self._heartbeatTimeoutHandle) { // 59
Meteor.clearTimeout(self._heartbeatTimeoutHandle); // 60
self._heartbeatTimeoutHandle = null; // 61
} // 62
}, // 63
// 64
// The heartbeat interval timer is fired when we should send a ping. // 65
_heartbeatIntervalFired: function () { // 66
var self = this; // 67
self._heartbeatIntervalHandle = null; // 68
self._sendPing(); // 69
// Wait for a pong. // 70
self._startHeartbeatTimeoutTimer(); // 71
}, // 72
// 73
// The heartbeat timeout timer is fired when we sent a ping, but we // 74
// timed out waiting for the pong. // 75
_heartbeatTimeoutFired: function () { // 76
var self = this; // 77
self._heartbeatTimeoutHandle = null; // 78
self._onTimeout(); // 79
}, // 80
// 81
pingReceived: function () { // 82
var self = this; // 83
// We know the connection is alive if we receive a ping, so we // 84
// don't need to send a ping ourselves. Reset the interval timer. // 85
if (self._heartbeatIntervalHandle) { // 86
self._clearHeartbeatIntervalTimer(); // 87
self._startHeartbeatIntervalTimer(); // 88
} // 89
}, // 90
// 91
pongReceived: function () { // 92
var self = this; // 93
// 94
// Receiving a pong means we won't timeout, so clear the timeout // 95
// timer and start the interval again. // 96
if (self._heartbeatTimeoutHandle) { // 97
self._clearHeartbeatTimeoutTimer(); // 98
self._startHeartbeatIntervalTimer(); // 99
} // 100
} // 101
}); // 102
// 103
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
}).call(this);
(function () {
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //
// packages/ddp/livedata_server.js //
// //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
DDPServer = {}; // 1
// 2
var Fiber = Npm.require('fibers'); // 3
// 4
// This file contains classes: // 5
// * Session - The server's connection to a single DDP client // 6
// * Subscription - A single subscription for a single client // 7
// * Server - An entire server that may talk to > 1 client. A DDP endpoint. // 8
// // 9
// Session and Subscription are file scope. For now, until we freeze // 10
// the interface, Server is package scope (in the future it should be // 11
// exported.) // 12
// 13
// Represents a single document in a SessionCollectionView // 14
var SessionDocumentView = function () { // 15
var self = this; // 16
self.existsIn = {}; // set of subscriptionHandle // 17
self.dataByKey = {}; // key-> [ {subscriptionHandle, value} by precedence] // 18
}; // 19
// 20
_.extend(SessionDocumentView.prototype, { // 21
// 22
getFields: function () { // 23
var self = this; // 24
var ret = {}; // 25
_.each(self.dataByKey, function (precedenceList, key) { // 26
ret[key] = precedenceList[0].value; // 27
}); // 28
return ret; // 29
}, // 30
// 31
clearField: function (subscriptionHandle, key, changeCollector) { // 32
var self = this; // 33
// Publish API ignores _id if present in fields // 34
if (key === "_id") // 35
return; // 36
var precedenceList = self.dataByKey[key]; // 37
// 38
// It's okay to clear fields that didn't exist. No need to throw // 39
// an error. // 40
if (!precedenceList) // 41
return; // 42
// 43
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 // 48
// used to have precedence. // 49
if (i === 0) // 50
removedValue = precedence.value; // 51
precedenceList.splice(i, 1); // 52
break; // 53
} // 54
} // 55
if (_.isEmpty(precedenceList)) { // 56
delete self.dataByKey[key]; // 57
changeCollector[key] = undefined; // 58
} else if (removedValue !== undefined && // 59
!EJSON.equals(removedValue, precedenceList[0].value)) { // 60
changeCollector[key] = precedenceList[0].value; // 61
} // 62
}, // 63
// 64
changeField: function (subscriptionHandle, key, value, // 65
changeCollector, isAdd) { // 66
var self = this; // 67
// Publish API ignores _id if present in fields // 68
if (key === "_id") // 69
return; // 70
// 71
// Don't share state with the data passed in by the user. // 72
value = EJSON.clone(value); // 73
// 74
if (!_.has(self.dataByKey, key)) { // 75
self.dataByKey[key] = [{subscriptionHandle: subscriptionHandle, // 76
value: value}]; // 77
changeCollector[key] = value; // 78
return; // 79
} // 80
var precedenceList = self.dataByKey[key]; // 81
var elt; // 82
if (!isAdd) { // 83
elt = _.find(precedenceList, function (precedence) { // 84
return precedence.subscriptionHandle === subscriptionHandle; // 85
}); // 86
} // 87
// 88
if (elt) { // 89
if (elt === precedenceList[0] && !EJSON.equals(value, elt.value)) { // 90
// this subscription is changing the value of this field. // 91
changeCollector[key] = value; // 92
} // 93
elt.value = value; // 94
} else { // 95
// this subscription is newly caring about this field // 96
precedenceList.push({subscriptionHandle: subscriptionHandle, value: value}); // 97
} // 98
// 99
} // 100
}); // 101
// 102
/** // 103
* Represents a client's view of a single collection // 104
* @param {String} collectionName Name of the collection it represents // 105
* @param {Object.<String, Function>} sessionCallbacks The callbacks for added, changed, removed // 106
* @class SessionCollectionView // 107
*/ // 108
var SessionCollectionView = function (collectionName, sessionCallbacks) { // 109
var self = this; // 110
self.collectionName = collectionName; // 111
self.documents = {}; // 112
self.callbacks = sessionCallbacks; // 113
}; // 114
// 115
LivedataTest.SessionCollectionView = SessionCollectionView; // 116
// 117
// 118
_.extend(SessionCollectionView.prototype, { // 119
// 120
isEmpty: function () { // 121
var self = this; // 122
return _.isEmpty(self.documents); // 123
}, // 124
// 125
diff: function (previous) { // 126
var self = this; // 127
LocalCollection._diffObjects(previous.documents, self.documents, { // 128
both: _.bind(self.diffDocument, self), // 129
// 130
rightOnly: function (id, nowDV) { // 131
self.callbacks.added(self.collectionName, id, nowDV.getFields()); // 132
}, // 133
// 134
leftOnly: function (id, prevDV) { // 135
self.callbacks.removed(self.collectionName, id); // 136
} // 137
}); // 138
}, // 139
// 140
diffDocument: function (id, prevDV, nowDV) { // 141
var self = this; // 142
var fields = {}; // 143
LocalCollection._diffObjects(prevDV.getFields(), nowDV.getFields(), { // 144
both: function (key, prev, now) { // 145
if (!EJSON.equals(prev, now)) // 146
fields[key] = now; // 147
}, // 148
rightOnly: function (key, now) { // 149
fields[key] = now; // 150
}, // 151
leftOnly: function(key, prev) { // 152
fields[key] = undefined; // 153
} // 154
}); // 155
self.callbacks.changed(self.collectionName, id, fields); // 156
}, // 157
// 158
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
} // 167
docView.existsIn[subscriptionHandle] = true; // 168
var changeCollector = {}; // 169
_.each(fields, function (value, key) { // 170
docView.changeField( // 171
subscriptionHandle, key, value, changeCollector, true); // 172
}); // 173
if (added) // 174
self.callbacks.added(self.collectionName, id, changeCollector); // 175
else // 176
self.callbacks.changed(self.collectionName, id, changeCollector); // 177
}, // 178
// 179
changed: function (subscriptionHandle, id, changed) { // 180
var self = this; // 181
var changedResult = {}; // 182
var docView = self.documents[id]; // 183
if (!docView) // 184
throw new Error("Could not find element with id " + id + " to change"); // 185
_.each(changed, function (value, key) { // 186
if (value === undefined) // 187
docView.clearField(subscriptionHandle, key, changedResult); // 188
else // 189
docView.changeField(subscriptionHandle, key, value, changedResult); // 190
}); // 191
self.callbacks.changed(self.collectionName, id, changedResult); // 192
}, // 193
// 194
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
} // 201
delete docView.existsIn[subscriptionHandle]; // 202
if (_.isEmpty(docView.existsIn)) { // 203
// it is gone from everyone // 204
self.callbacks.removed(self.collectionName, id); // 205
delete self.documents[id]; // 206
} else { // 207
var changed = {}; // 208
// remove this subscription from every precedence list // 209
// and record the changes // 210
_.each(docView.dataByKey, function (precedenceList, key) { // 211
docView.clearField(subscriptionHandle, key, changed); // 212
}); // 213
// 214
self.callbacks.changed(self.collectionName, id, changed); // 215
} // 216
} // 217
}); // 218
// 219
/******************************************************************************/ // 220
/* Session */ // 221
/******************************************************************************/ // 222
// 223
var Session = function (server, version, socket, options) { // 224
var self = this; // 225
self.id = Random.id(); // 226
// 227
self.server = server; // 228
self.version = version; // 229
// 230
self.initialized = false; // 231
self.socket = socket; // 232
// 233
// set to null when the session is destroyed. multiple places below // 234
// use this to determine if the session is alive or not. // 235
self.inQueue = new Meteor._DoubleEndedQueue(); // 236
// 237
self.blocked = false; // 238
self.workerRunning = false; // 239
// 240
// Sub objects for active subscriptions // 241
self._namedSubs = {}; // 242
self._universalSubs = []; // 243
// 244
self.userId = null; // 245
// 246
self.collectionViews = {}; // 247
// 248
// Set this to false to not send messages when collectionViews are // 249
// modified. This is done when rerunning subs in _setUserId and those messages // 250
// are calculated via a diff instead. // 251
self._isSending = true; // 252
// 253
// If this is true, don't start a newly-created universal publisher on this // 254
// session. The session will take care of starting it when appropriate. // 255
self._dontStartNewUniversalSubs = false; // 256
// 257
// when we are rerunning subscriptions, any ready messages // 258
// we want to buffer up for when we are done rerunning subscriptions // 259
self._pendingReady = []; // 260
// 261
// List of callbacks to call when this connection is closed. // 262
self._closeCallbacks = []; // 263
// 264
// 265
// XXX HACK: If a sockjs connection, save off the URL. This is // 266
// temporary and will go away in the near future. // 267
self._socketUrl = socket.url; // 268
// 269
// Allow tests to disable responding to pings. // 270
self._respondToPings = options.respondToPings; // 271
// 272
// This object is the public interface to the session. In the public // 273
// API, it is called the `connection` object. Internally we call it // 274
// a `connectionHandle` to avoid ambiguity. // 275
self.connectionHandle = { // 276
id: self.id, // 277
close: function () { // 278
self.close(); // 279
}, // 280
onClose: function (fn) { // 281
var cb = Meteor.bindEnvironment(fn, "connection onClose callback"); // 282
if (self.inQueue) { // 283
self._closeCallbacks.push(cb); // 284
} else { // 285
// if we're already closed, call the callback. // 286
Meteor.defer(cb); // 287
} // 288
}, // 289
clientAddress: self._clientAddress(), // 290
httpHeaders: self.socket.headers // 291
}; // 292
// 293
socket.send(stringifyDDP({msg: 'connected', // 294
session: self.id})); // 295
// On initial connect, spin up all the universal publishers. // 296
Fiber(function () { // 297
self.startUniversalSubs(); // 298
}).run(); // 299
// 300
if (version !== 'pre1' && options.heartbeatInterval !== 0) { // 301
self.heartbeat = new Heartbeat({ // 302
heartbeatInterval: options.heartbeatInterval, // 303
heartbeatTimeout: options.heartbeatTimeout, // 304
onTimeout: function () { // 305
self.close(); // 306
}, // 307
sendPing: function () { // 308
self.send({msg: 'ping'}); // 309
} // 310
}); // 311
self.heartbeat.start(); // 312
} // 313
// 314
Package.facts && Package.facts.Facts.incrementServerFact( // 315
"livedata", "sessions", 1); // 316
}; // 317
// 318
_.extend(Session.prototype, { // 319
// 320
sendReady: function (subscriptionIds) { // 321
var self = this; // 322
if (self._isSending) // 323
self.send({msg: "ready", subs: subscriptionIds}); // 324
else { // 325
_.each(subscriptionIds, function (subscriptionId) { // 326
self._pendingReady.push(subscriptionId); // 327
}); // 328
} // 329
}, // 330
// 331
sendAdded: function (collectionName, id, fields) { // 332
var self = this; // 333
if (self._isSending) // 334
self.send({msg: "added", collection: collectionName, id: id, fields: fields}); // 335
}, // 336
// 337
sendChanged: function (collectionName, id, fields) { // 338
var self = this; // 339
if (_.isEmpty(fields)) // 340
return; // 341
// 342
if (self._isSending) { // 343
self.send({ // 344
msg: "changed", // 345
collection: collectionName, // 346
id: id, // 347
fields: fields // 348
}); // 349
} // 350
}, // 351
// 352
sendRemoved: function (collectionName, id) { // 353
var self = this; // 354
if (self._isSending) // 355
self.send({msg: "removed", collection: collectionName, id: id}); // 356
}, // 357
// 358
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
}; // 365
}, // 366
// 367
getCollectionView: function (collectionName) { // 368
var self = this; // 369
if (_.has(self.collectionViews, collectionName)) { // 370
return self.collectionViews[collectionName]; // 371
} // 372
var ret = new SessionCollectionView(collectionName, // 373
self.getSendCallbacks()); // 374
self.collectionViews[collectionName] = ret; // 375
return ret; // 376
}, // 377
// 378
added: function (subscriptionHandle, collectionName, id, fields) { // 379
var self = this; // 380
var view = self.getCollectionView(collectionName); // 381
view.added(subscriptionHandle, id, fields); // 382
}, // 383
// 384
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
} // 391
}, // 392
// 393
changed: function (subscriptionHandle, collectionName, id, fields) { // 394
var self = this; // 395
var view = self.getCollectionView(collectionName); // 396
view.changed(subscriptionHandle, id, fields); // 397
}, // 398
// 399
startUniversalSubs: function () { // 400
var self = this; // 401
// Make a shallow copy of the set of universal handlers and start them. If // 402
// additional universal publishers start while we're running them (due to // 403
// yielding), they will run separately as part of Server.publish. // 404
var handlers = _.clone(self.server.universal_publish_handlers); // 405
_.each(handlers, function (handler) { // 406
self._startSubscription(handler); // 407
}); // 408
}, // 409
// 410
// Destroy this session and unregister it at the server. // 411
close: function () { // 412
var self = this; // 413
// 414
// Destroy this session, even if it's not registered at the // 415
// server. Stop all processing and tear everything down. If a socket // 416
// was attached, close it. // 417
// 418
// Already destroyed. // 419
if (! self.inQueue) // 420
return; // 421
// 422
// Drop the merge box data immediately. // 423
self.inQueue = null; // 424
self.collectionViews = {}; // 425
// 426
if (self.heartbeat) { // 427
self.heartbeat.stop(); // 428
self.heartbeat = null; // 429
} // 430
// 431
if (self.socket) { // 432
self.socket.close(); // 433
self.socket._meteorSession = null; // 434
} // 435
// 436
Package.facts && Package.facts.Facts.incrementServerFact( // 437
"livedata", "sessions", -1); // 438
// 439
Meteor.defer(function () { // 440
// stop callbacks can yield, so we defer this on close. // 441
// sub._isDeactivated() detects that we set inQueue to null and // 442
// treats it as semi-deactivated (it will ignore incoming callbacks, etc). // 443
self._deactivateAllSubscriptions(); // 444
// 445
// Defer calling the close callbacks, so that the caller closing // 446
// the session isn't waiting for all the callbacks to complete. // 447
_.each(self._closeCallbacks, function (callback) { // 448
callback(); // 449
}); // 450
}); // 451
// 452
// Unregister the session. // 453
self.server._removeSession(self); // 454
}, // 455
// 456
// Send a message (doing nothing if no socket is connected right now.) // 457
// It should be a JSON object (it will be stringified.) // 458
send: function (msg) { // 459
var self = this; // 460
if (self.socket) { // 461
if (Meteor._printSentDDP) // 462
Meteor._debug("Sent DDP", stringifyDDP(msg)); // 463
self.socket.send(stringifyDDP(msg)); // 464
} // 465
}, // 466
// 467
// Send a connection error. // 468
sendError: function (reason, offendingMessage) { // 469
var self = this; // 470
var msg = {msg: 'error', reason: reason}; // 471
if (offendingMessage) // 472
msg.offendingMessage = offendingMessage; // 473
self.send(msg); // 474
}, // 475
// 476
// Process 'msg' as an incoming message. (But as a guard against // 477
// race conditions during reconnection, ignore the message if // 478
// 'socket' is not the currently connected socket.) // 479
// // 480
// We run the messages from the client one at a time, in the order // 481
// given by the client. The message handler is passed an idempotent // 482
// function 'unblock' which it may call to allow other messages to // 483
// begin running in parallel in another fiber (for example, a method // 484
// that wants to yield.) Otherwise, it is automatically unblocked // 485
// when it returns. // 486
// // 487
// Actually, we don't have to 'totally order' the messages in this // 488
// way, but it's the easiest thing that's correct. (unsub needs to // 489
// be ordered against sub, methods need to be ordered against each // 490
// other.) // 491
processMessage: function (msg_in) { // 492
var self = this; // 493
if (!self.inQueue) // we have been destroyed. // 494
return; // 495
// 496
// Respond to ping and pong messages immediately without queuing. // 497
// If the negotiated DDP version is "pre1" which didn't support // 498
// pings, preserve the "pre1" behavior of responding with a "bad // 499
// request" for the unknown messages. // 500
// // 501
// Fibers are needed because heartbeat uses Meteor.setTimeout, which // 502
// needs a Fiber. We could actually use regular setTimeout and avoid // 503
// these new fibers, but it is easier to just make everything use // 504
// Meteor.setTimeout and not think too hard. // 505
if (self.version !== 'pre1' && msg_in.msg === 'ping') { // 506
if (self._respondToPings) // 507
self.send({msg: "pong", id: msg_in.id}); // 508
if (self.heartbeat) // 509
Fiber(function () { // 510
self.heartbeat.pingReceived(); // 511
}).run(); // 512
return; // 513
} // 514
if (self.version !== 'pre1' && msg_in.msg === 'pong') { // 515
if (self.heartbeat) // 516
Fiber(function () { // 517
self.heartbeat.pongReceived(); // 518
}).run(); // 519
return; // 520
} // 521
// 522
self.inQueue.push(msg_in); // 523
if (self.workerRunning) // 524
return; // 525
self.workerRunning = true; // 526
// 527
var processNext = function () { // 528
var msg = self.inQueue && self.inQueue.shift(); // 529
if (!msg) { // 530
self.workerRunning = false; // 531
return; // 532
} // 533
// 534
Fiber(function () { // 535
var blocked = true; // 536
// 537
var unblock = function () { // 538
if (!blocked) // 539
return; // idempotent // 540
blocked = false; // 541
processNext(); // 542
}; // 543
// 544
if (_.has(self.protocol_handlers, msg.msg)) // 545
self.protocol_handlers[msg.msg].call(self, msg, unblock); // 546
else // 547
self.sendError('Bad request', msg); // 548
unblock(); // in case the handler didn't already do it // 549
}).run(); // 550
}; // 551
// 552
processNext(); // 553
}, // 554
// 555
protocol_handlers: { // 556
sub: function (msg) { // 557
var self = this; // 558
// 559
// reject malformed messages // 560
if (typeof (msg.id) !== "string" || // 561
typeof (msg.name) !== "string" || // 562
(('params' in msg) && !(msg.params instanceof Array))) { // 563
self.sendError("Malformed subscription", msg); // 564
return; // 565
} // 566
// 567
if (!self.server.publish_handlers[msg.name]) { // 568
self.send({ // 569
msg: 'nosub', id: msg.id, // 570
error: new Meteor.Error(404, "Subscription not found")}); // 571
return; // 572
} // 573
// 574
if (_.has(self._namedSubs, msg.id)) // 575
// subs are idempotent, or rather, they are ignored if a sub // 576
// with that id already exists. this is important during // 577
// reconnect. // 578
return; // 579
// 580
var handler = self.server.publish_handlers[msg.name]; // 581
self._startSubscription(handler, msg.id, msg.params, msg.name); // 582
// 583
}, // 584
// 585
unsub: function (msg) { // 586
var self = this; // 587
// 588
self._stopSubscription(msg.id); // 589
}, // 590
// 591
method: function (msg, unblock) { // 592
var self = this; // 593
// 594
// reject malformed messages // 595
// For now, we silently ignore unknown attributes, // 596
// for forwards compatibility. // 597
if (typeof (msg.id) !== "string" || // 598
typeof (msg.method) !== "string" || // 599
(('params' in msg) && !(msg.params instanceof Array)) || // 600
(('randomSeed' in msg) && (typeof msg.randomSeed !== "string"))) { // 601
self.sendError("Malformed method invocation", msg); // 602
return; // 603
} // 604
// 605
var randomSeed = msg.randomSeed || null; // 606
// 607
// set up to mark the method as satisfied once all observers // 608
// (and subscriptions) have reacted to any writes that were // 609
// done. // 610
var fence = new DDPServer._WriteFence; // 611
fence.onAllCommitted(function () { // 612
// Retire the fence so that future writes are allowed. // 613
// This means that callbacks like timers are free to use // 614
// the fence, and if they fire before it's armed (for // 615
// example, because the method waits for them) their // 616
// writes will be included in the fence. // 617
fence.retire(); // 618
self.send({ // 619
msg: 'updated', methods: [msg.id]}); // 620
}); // 621
// 622
// find the handler // 623
var handler = self.server.method_handlers[msg.method]; // 624
if (!handler) { // 625
self.send({ // 626
msg: 'result', id: msg.id, // 627
error: new Meteor.Error(404, "Method not found")}); // 628
fence.arm(); // 629
return; // 630
} // 631
// 632
var setUserId = function(userId) { // 633
self._setUserId(userId); // 634
}; // 635
// 636
var invocation = new MethodInvocation({ // 637
isSimulation: false, // 638
userId: self.userId, // 639
setUserId: setUserId, // 640
unblock: unblock, // 641
connection: self.connectionHandle, // 642
randomSeed: randomSeed // 643
}); // 644
try { // 645
var result = DDPServer._CurrentWriteFence.withValue(fence, function () { // 646
return DDP._CurrentInvocation.withValue(invocation, function () { // 647
return maybeAuditArgumentChecks( // 648
handler, invocation, msg.params, "call to '" + msg.method + "'"); // 649
}); // 650
}); // 651
} catch (e) { // 652
var exception = e; // 653
} // 654
// 655
fence.arm(); // we're done adding writes to the fence // 656
unblock(); // unblock, if the method hasn't done it already // 657
// 658
exception = wrapInternalException( // 659
exception, "while invoking method '" + msg.method + "'"); // 660
// 661
// send response and add to cache // 662
var payload = // 663
exception ? {error: exception} : (result !== undefined ? // 664
{result: result} : {}); // 665
self.send(_.extend({msg: 'result', id: msg.id}, payload)); // 666
} // 667
}, // 668
// 669
_eachSub: function (f) { // 670
var self = this; // 671
_.each(self._namedSubs, f); // 672
_.each(self._universalSubs, f); // 673
}, // 674
// 675
_diffCollectionViews: function (beforeCVs) { // 676
var self = this; // 677
LocalCollection._diffObjects(beforeCVs, self.collectionViews, { // 678
both: function (collectionName, leftValue, rightValue) { // 679
rightValue.diff(leftValue); // 680
}, // 681
rightOnly: function (collectionName, rightValue) { // 682
_.each(rightValue.documents, function (docView, id) { // 683
self.sendAdded(collectionName, id, docView.getFields()); // 684
}); // 685
}, // 686
leftOnly: function (collectionName, leftValue) { // 687
_.each(leftValue.documents, function (doc, id) { // 688
self.sendRemoved(collectionName, id); // 689
}); // 690
} // 691
}); // 692
}, // 693
// 694
// Sets the current user id in all appropriate contexts and reruns // 695
// all subscriptions // 696
_setUserId: function(userId) { // 697
var self = this; // 698
// 699
if (userId !== null && typeof userId !== "string") // 700
throw new Error("setUserId must be called on string or null, not " + // 701
typeof userId); // 702
// 703
// Prevent newly-created universal subscriptions from being added to our // 704
// session; they will be found below when we call startUniversalSubs. // 705
// // 706
// (We don't have to worry about named subscriptions, because we only add // 707
// them when we process a 'sub' message. We are currently processing a // 708
// 'method' message, and the method did not unblock, because it is illegal // 709
// to call setUserId after unblock. Thus we cannot be concurrently adding a // 710
// new named subscription.) // 711
self._dontStartNewUniversalSubs = true; // 712
// 713
// Prevent current subs from updating our collectionViews and call their // 714
// stop callbacks. This may yield. // 715
self._eachSub(function (sub) { // 716
sub._deactivate(); // 717
}); // 718
// 719
// All subs should now be deactivated. Stop sending messages to the client, // 720
// save the state of the published collections, reset to an empty view, and // 721
// update the userId. // 722
self._isSending = false; // 723
var beforeCVs = self.collectionViews; // 724
self.collectionViews = {}; // 725
self.userId = userId; // 726
// 727
// Save the old named subs, and reset to having no subscriptions. // 728
var oldNamedSubs = self._namedSubs; // 729
self._namedSubs = {}; // 730
self._universalSubs = []; // 731
// 732
_.each(oldNamedSubs, function (sub, subscriptionId) { // 733
self._namedSubs[subscriptionId] = sub._recreate(); // 734
// nb: if the handler throws or calls this.error(), it will in fact // 735
// immediately send its 'nosub'. This is OK, though. // 736
self._namedSubs[subscriptionId]._runHandler(); // 737
}); // 738
// 739
// Allow newly-created universal subs to be started on our connection in // 740
// parallel with the ones we're spinning up here, and spin up universal // 741
// subs. // 742
self._dontStartNewUniversalSubs = false; // 743
self.startUniversalSubs(); // 744
// 745
// Start sending messages again, beginning with the diff from the previous // 746
// state of the world to the current state. No yields are allowed during // 747
// this diff, so that other changes cannot interleave. // 748
Meteor._noYieldsAllowed(function () { // 749
self._isSending = true; // 750
self._diffCollectionViews(beforeCVs); // 751
if (!_.isEmpty(self._pendingReady)) { // 752
self.sendReady(self._pendingReady); // 753
self._pendingReady = []; // 754
} // 755
}); // 756
}, // 757
// 758
_startSubscription: function (handler, subId, params, name) { // 759
var self = this; // 760
// 761
var sub = new Subscription( // 762
self, handler, subId, params, name); // 763
if (subId) // 764
self._namedSubs[subId] = sub; // 765
else // 766
self._universalSubs.push(sub); // 767
// 768
sub._runHandler(); // 769
}, // 770
// 771
// tear down specified subscription // 772
_stopSubscription: function (subId, error) { // 773
var self = this; // 774
// 775
var subName = null; // 776
// 777
if (subId && self._namedSubs[subId]) { // 778
subName = self._namedSubs[subId]._name; // 779
self._namedSubs[subId]._removeAllDocuments(); // 780
self._namedSubs[subId]._deactivate(); // 781
delete self._namedSubs[subId]; // 782
} // 783
// 784
var response = {msg: 'nosub', id: subId}; // 785
// 786
if (error) { // 787
response.error = wrapInternalException( // 788
error, // 789
subName ? ("from sub " + subName + " id " + subId) // 790
: ("from sub id " + subId)); // 791
} // 792
// 793
self.send(response); // 794
}, // 795
// 796
// tear down all subscriptions. Note that this does NOT send removed or nosub // 797
// messages, since we assume the client is gone. // 798
_deactivateAllSubscriptions: function () { // 799
var self = this; // 800
// 801
_.each(self._namedSubs, function (sub, id) { // 802
sub._deactivate(); // 803
}); // 804
self._namedSubs = {}; // 805
// 806
_.each(self._universalSubs, function (sub) { // 807
sub._deactivate(); // 808
}); // 809
self._universalSubs = []; // 810
}, // 811
// 812
// Determine the remote client's IP address, based on the // 813
// HTTP_FORWARDED_COUNT environment variable representing how many // 814
// proxies the server is behind. // 815
_clientAddress: function () { // 816
var self = this; // 817
// 818
// For the reported client address for a connection to be correct, // 819
// the developer must set the HTTP_FORWARDED_COUNT environment // 820
// variable to an integer representing the number of hops they // 821
// expect in the `x-forwarded-for` header. E.g., set to "1" if the // 822
// server is behind one proxy. // 823
// // 824
// This could be computed once at startup instead of every time. // 825
var httpForwardedCount = parseInt(process.env['HTTP_FORWARDED_COUNT']) || 0; // 826
// 827
if (httpForwardedCount === 0) // 828
return self.socket.remoteAddress; // 829
// 830
var forwardedFor = self.socket.headers["x-forwarded-for"]; // 831
if (! _.isString(forwardedFor)) // 832
return null; // 833
forwardedFor = forwardedFor.trim().split(/\s*,\s*/); // 834
// 835
// Typically the first value in the `x-forwarded-for` header is // 836
// the original IP address of the client connecting to the first // 837
// proxy. However, the end user can easily spoof the header, in // 838
// which case the first value(s) will be the fake IP address from // 839
// the user pretending to be a proxy reporting the original IP // 840
// address value. By counting HTTP_FORWARDED_COUNT back from the // 841
// end of the list, we ensure that we get the IP address being // 842
// reported by *our* first proxy. // 843
// 844
if (httpForwardedCount < 0 || httpForwardedCount > forwardedFor.length) // 845
return null; // 846
// 847
return forwardedFor[forwardedFor.length - httpForwardedCount]; // 848
} // 849
}); // 850
// 851
/******************************************************************************/ // 852
/* Subscription */ // 853
/******************************************************************************/ // 854
// 855
// ctor for a sub handle: the input to each publish function // 856
// 857
// Instance name is this because it's usually referred to as this inside a // 858
// publish // 859
/** // 860
* @summary The server's side of a subscription // 861
* @class Subscription // 862
* @instanceName this // 863
*/ // 864
var Subscription = function ( // 865
session, handler, subscriptionId, params, name) { // 866
var self = this; // 867
self._session = session; // type is Session // 868
// 869
/** // 870
* @summary Access inside the publish function. The incoming [connection](#meteor_onconnection) for this subscription.
* @locus Server // 872
* @name connection // 873
* @memberOf Subscription // 874
* @instance // 875
*/ // 876
self.connection = session.connectionHandle; // public API object // 877
// 878
self._handler = handler; // 879
// 880
// my subscription ID (generated by client, undefined for universal subs). // 881
self._subscriptionId = subscriptionId; // 882
// undefined for universal subs // 883
self._name = name; // 884
// 885
self._params = params || []; // 886
// 887
// Only named subscriptions have IDs, but we need some sort of string // 888
// internally to keep track of all subscriptions inside // 889
// SessionDocumentViews. We use this subscriptionHandle for that. // 890
if (self._subscriptionId) { // 891
self._subscriptionHandle = 'N' + self._subscriptionId; // 892
} else { // 893
self._subscriptionHandle = 'U' + Random.id(); // 894
} // 895
// 896
// has _deactivate been called? // 897
self._deactivated = false; // 898
// 899
// stop callbacks to g/c this sub. called w/ zero arguments. // 900
self._stopCallbacks = []; // 901
// 902
// the set of (collection, documentid) that this subscription has // 903
// an opinion about // 904
self._documents = {}; // 905
// 906
// remember if we are ready. // 907
self._ready = false; // 908
// 909
// Part of the public API: the user of this sub. // 910
// 911
/** // 912
* @summary Access inside the publish function. The id of the logged-in user, or `null` if no user is logged in. // 913
* @locus Server // 914
* @memberOf Subscription // 915
* @name userId // 916
* @instance // 917
*/ // 918
self.userId = session.userId; // 919
// 920
// For now, the id filter is going to default to // 921
// the to/from DDP methods on LocalCollection, to // 922
// specifically deal with mongo/minimongo ObjectIds. // 923
// 924
// Later, you will be able to make this be "raw" // 925
// if you want to publish a collection that you know // 926
// just has strings for keys and no funny business, to // 927
// a ddp consumer that isn't minimongo // 928
// 929
self._idFilter = { // 930
idStringify: LocalCollection._idStringify, // 931
idParse: LocalCollection._idParse // 932
}; // 933
// 934
Package.facts && Package.facts.Facts.incrementServerFact( // 935
"livedata", "subscriptions", 1); // 936
}; // 937
// 938
_.extend(Subscription.prototype, { // 939
_runHandler: function () { // 940
// XXX should we unblock() here? Either before running the publish // 941
// function, or before running _publishCursor. // 942
// // 943
// Right now, each publish function blocks all future publishes and // 944
// methods waiting on data from Mongo (or whatever else the function // 945
// blocks on). This probably slows page load in common cases. // 946
// 947
var self = this; // 948
try { // 949
var res = maybeAuditArgumentChecks( // 950
self._handler, self, EJSON.clone(self._params), // 951
// It's OK that this would look weird for universal subscriptions, // 952
// because they have no arguments so there can never be an // 953
// audit-argument-checks failure. // 954
"publisher '" + self._name + "'"); // 955
} catch (e) { // 956
self.error(e); // 957
return; // 958
} // 959
// 960
// Did the handler call this.error or this.stop? // 961
if (self._isDeactivated()) // 962
return; // 963
// 964
// SPECIAL CASE: Instead of writing their own callbacks that invoke // 965
// this.added/changed/ready/etc, the user can just return a collection // 966
// cursor or array of cursors from the publish function; we call their // 967
// _publishCursor method which starts observing the cursor and publishes the // 968
// results. Note that _publishCursor does NOT call ready(). // 969
// // 970
// XXX This uses an undocumented interface which only the Mongo cursor // 971
// interface publishes. Should we make this interface public and encourage // 972
// users to implement it themselves? Arguably, it's unnecessary; users can // 973
// already write their own functions like // 974
// var publishMyReactiveThingy = function (name, handler) { // 975
// Meteor.publish(name, function () { // 976
// var reactiveThingy = handler(); // 977
// reactiveThingy.publishMe(); // 978
// }); // 979
// }; // 980
var isCursor = function (c) { // 981
return c && c._publishCursor; // 982
}; // 983
if (isCursor(res)) { // 984
try { // 985
res._publishCursor(self); // 986
} catch (e) { // 987
self.error(e); // 988
return; // 989
} // 990
// _publishCursor only returns after the initial added callbacks have run. // 991
// mark subscription as ready. // 992
self.ready(); // 993
} else if (_.isArray(res)) { // 994
// check all the elements are cursors // 995
if (! _.all(res, isCursor)) { // 996
self.error(new Error("Publish function returned an array of non-Cursors")); // 997
return; // 998
} // 999
// find duplicate collection names // 1000
// XXX we should support overlapping cursors, but that would require the // 1001
// merge box to allow overlap within a subscription // 1002
var collectionNames = {}; // 1003
for (var i = 0; i < res.length; ++i) { // 1004
var collectionName = res[i]._getCollectionName(); // 1005
if (_.has(collectionNames, collectionName)) { // 1006
self.error(new Error( // 1007
"Publish function returned multiple cursors for collection " + // 1008
collectionName)); // 1009
return; // 1010
} // 1011
collectionNames[collectionName] = true; // 1012
}; // 1013
// 1014
try { // 1015
_.each(res, function (cur) { // 1016
cur._publishCursor(self); // 1017
}); // 1018
} catch (e) { // 1019
self.error(e); // 1020
return; // 1021
} // 1022
self.ready(); // 1023
} else if (res) { // 1024
// truthy values other than cursors or arrays are probably a // 1025
// user mistake (possible returning a Mongo document via, say, // 1026
// `coll.findOne()`). // 1027
self.error(new Error("Publish function can only return a Cursor or " // 1028
+ "an array of Cursors")); // 1029
} // 1030
}, // 1031
// 1032
// This calls all stop callbacks and prevents the handler from updating any // 1033
// SessionCollectionViews further. It's used when the user unsubscribes or // 1034
// disconnects, as well as during setUserId re-runs. It does *NOT* send // 1035
// removed messages for the published objects; if that is necessary, call // 1036
// _removeAllDocuments first. // 1037
_deactivate: function() { // 1038
var self = this; // 1039
if (self._deactivated) // 1040
return; // 1041
self._deactivated = true; // 1042
self._callStopCallbacks(); // 1043
Package.facts && Package.facts.Facts.incrementServerFact( // 1044
"livedata", "subscriptions", -1); // 1045
}, // 1046
// 1047
_callStopCallbacks: function () { // 1048
var self = this; // 1049
// tell listeners, so they can clean up // 1050
var callbacks = self._stopCallbacks; // 1051
self._stopCallbacks = []; // 1052
_.each(callbacks, function (callback) { // 1053
callback(); // 1054
}); // 1055
}, // 1056
// 1057
// Send remove messages for every document. // 1058
_removeAllDocuments: function () { // 1059
var self = this; // 1060
Meteor._noYieldsAllowed(function () { // 1061
_.each(self._documents, function(collectionDocs, collectionName) { // 1062
// Iterate over _.keys instead of the dictionary itself, since we'll be // 1063
// mutating it. // 1064
_.each(_.keys(collectionDocs), function (strId) { // 1065
self.removed(collectionName, self._idFilter.idParse(strId)); // 1066
}); // 1067
}); // 1068
}); // 1069
}, // 1070
// 1071
// Returns a new Subscription for the same session with the same // 1072
// initial creation parameters. This isn't a clone: it doesn't have // 1073
// the same _documents cache, stopped state or callbacks; may have a // 1074
// different _subscriptionHandle, and gets its userId from the // 1075
// session, not from this object. // 1076
_recreate: function () { // 1077
var self = this; // 1078
return new Subscription( // 1079
self._session, self._handler, self._subscriptionId, self._params, // 1080
self._name); // 1081
}, // 1082
// 1083
/** // 1084
* @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 // 1086
* @param {Error} error The error to pass to the client. // 1087
* @instance // 1088
* @memberOf Subscription // 1089
*/ // 1090
error: function (error) { // 1091
var self = this; // 1092
if (self._isDeactivated()) // 1093
return; // 1094
self._session._stopSubscription(self._subscriptionId, error); // 1095
}, // 1096
// 1097
// Note that while our DDP client will notice that you've called stop() on the // 1098
// server (and clean up its _subscriptions table) we don't actually provide a // 1099
// mechanism for an app to notice this (the subscribe onError callback only // 1100
// triggers if there is an error). // 1101
// 1102
/** // 1103
* @summary Call inside the publish function. Stops this client's subscription and invokes the client's `onStop` callback with no error.
* @locus Server // 1105
* @instance // 1106
* @memberOf Subscription // 1107
*/ // 1108
stop: function () { // 1109
var self = this; // 1110
if (self._isDeactivated()) // 1111
return; // 1112
self._session._stopSubscription(self._subscriptionId); // 1113
}, // 1114
// 1115
/** // 1116
* @summary Call inside the publish function. Registers a callback function to run when the subscription is stopped.
* @locus Server // 1118
* @memberOf Subscription // 1119
* @instance // 1120
* @param {Function} func The callback function // 1121
*/ // 1122
onStop: function (callback) { // 1123
var self = this; // 1124
if (self._isDeactivated()) // 1125
callback(); // 1126
else // 1127
self._stopCallbacks.push(callback); // 1128
}, // 1129
// 1130
// This returns true if the sub has been deactivated, *OR* if the session was // 1131
// destroyed but the deferred call to _deactivateAllSubscriptions hasn't // 1132
// happened yet. // 1133
_isDeactivated: function () { // 1134
var self = this; // 1135
return self._deactivated || self._session.inQueue === null; // 1136
}, // 1137
// 1138
/** // 1139
* @summary Call inside the publish function. Informs the subscriber that a document has been added to the record set.
* @locus Server // 1141
* @memberOf Subscription // 1142
* @instance // 1143
* @param {String} collection The name of the collection that contains the new document. // 1144
* @param {String} id The new document's ID. // 1145
* @param {Object} fields The fields in the new document. If `_id` is present it is ignored. // 1146
*/ // 1147
added: function (collectionName, id, fields) { // 1148
var self = this; // 1149
if (self._isDeactivated()) // 1150
return; // 1151
id = self._idFilter.idStringify(id); // 1152
Meteor._ensure(self._documents, collectionName)[id] = true; // 1153
self._session.added(self._subscriptionHandle, collectionName, id, fields); // 1154
}, // 1155
// 1156
/** // 1157
* @summary Call inside the publish function. Informs the subscriber that a document in the record set has been modified.
* @locus Server // 1159
* @memberOf Subscription // 1160
* @instance // 1161
* @param {String} collection The name of the collection that contains the changed document. // 1162
* @param {String} id The changed document's ID. // 1163
* @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.
*/ // 1165
changed: function (collectionName, id, fields) { // 1166
var self = this; // 1167
if (self._isDeactivated()) // 1168
return; // 1169
id = self._idFilter.idStringify(id); // 1170
self._session.changed(self._subscriptionHandle, collectionName, id, fields); // 1171
}, // 1172
// 1173
/** // 1174
* @summary Call inside the publish function. Informs the subscriber that a document has been removed from the record set.
* @locus Server // 1176
* @memberOf Subscription // 1177
* @instance // 1178
* @param {String} collection The name of the collection that the document has been removed from. // 1179
* @param {String} id The ID of the document that has been removed. // 1180
*/ // 1181
removed: function (collectionName, id) { // 1182
var self = this; // 1183
if (self._isDeactivated()) // 1184
return; // 1185
id = self._idFilter.idStringify(id); // 1186
// We don't bother to delete sets of things in a collection if the // 1187
// collection is empty. It could break _removeAllDocuments. // 1188
delete self._documents[collectionName][id]; // 1189
self._session.removed(self._subscriptionHandle, collectionName, id); // 1190
}, // 1191
// 1192
/** // 1193
* @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 // 1195
* @memberOf Subscription // 1196
* @instance // 1197
*/ // 1198
ready: function () { // 1199
var self = this; // 1200
if (self._isDeactivated()) // 1201
return; // 1202
if (!self._subscriptionId) // 1203
return; // unnecessary but ignored for universal sub // 1204
if (!self._ready) { // 1205
self._session.sendReady([self._subscriptionId]); // 1206
self._ready = true; // 1207
} // 1208
} // 1209
}); // 1210
// 1211
/******************************************************************************/ // 1212
/* Server */ // 1213
/******************************************************************************/ // 1214
// 1215
Server = function (options) { // 1216
var self = this; // 1217
// 1218
// The default heartbeat interval is 30 seconds on the server and 35 // 1219
// seconds on the client. Since the client doesn't need to send a // 1220
// ping as long as it is receiving pings, this means that pings // 1221
// normally go from the server to the client. // 1222
// // 1223
// Note: Troposphere depends on the ability to mutate // 1224
// Meteor.server.options.heartbeatTimeout! This is a hack, but it's life. // 1225
self.options = _.defaults(options || {}, { // 1226
heartbeatInterval: 30000, // 1227
heartbeatTimeout: 15000, // 1228
// For testing, allow responding to pings to be disabled. // 1229
respondToPings: true // 1230
}); // 1231
// 1232
// Map of callbacks to call when a new connection comes in to the // 1233
// server and completes DDP version negotiation. Use an object instead // 1234
// of an array so we can safely remove one from the list while // 1235
// iterating over it. // 1236
self.onConnectionHook = new Hook({ // 1237
debugPrintExceptions: "onConnection callback" // 1238
}); // 1239
// 1240
self.publish_handlers = {}; // 1241
self.universal_publish_handlers = []; // 1242
// 1243
self.method_handlers = {}; // 1244
// 1245
self.sessions = {}; // map from id to session // 1246
// 1247
self.stream_server = new StreamServer; // 1248
// 1249
self.stream_server.register(function (socket) { // 1250
// socket implements the SockJSConnection interface // 1251
socket._meteorSession = null; // 1252
// 1253
var sendError = function (reason, offendingMessage) { // 1254
var msg = {msg: 'error', reason: reason}; // 1255
if (offendingMessage) // 1256
msg.offendingMessage = offendingMessage; // 1257
socket.send(stringifyDDP(msg)); // 1258
}; // 1259
// 1260
socket.on('data', function (raw_msg) { // 1261
if (Meteor._printReceivedDDP) { // 1262
Meteor._debug("Received DDP", raw_msg); // 1263
} // 1264
try { // 1265
try { // 1266
var msg = parseDDP(raw_msg); // 1267
} catch (err) { // 1268
sendError('Parse error'); // 1269
return; // 1270
} // 1271
if (msg === null || !msg.msg) { // 1272
sendError('Bad request', msg); // 1273
return; // 1274
} // 1275
// 1276
if (msg.msg === 'connect') { // 1277
if (socket._meteorSession) { // 1278
sendError("Already connected", msg); // 1279
return; // 1280
} // 1281
Fiber(function () { // 1282
self._handleConnect(socket, msg); // 1283
}).run(); // 1284
return; // 1285
} // 1286
// 1287
if (!socket._meteorSession) { // 1288
sendError('Must connect first', msg); // 1289
return; // 1290
} // 1291
socket._meteorSession.processMessage(msg); // 1292
} catch (e) { // 1293
// XXX print stack nicely // 1294
Meteor._debug("Internal exception while processing message", msg, // 1295
e.message, e.stack); // 1296
} // 1297
}); // 1298
// 1299
socket.on('close', function () { // 1300
if (socket._meteorSession) { // 1301
Fiber(function () { // 1302
socket._meteorSession.close(); // 1303
}).run(); // 1304
} // 1305
}); // 1306
}); // 1307
}; // 1308
// 1309
_.extend(Server.prototype, { // 1310
// 1311
/** // 1312
* @summary Register a callback to be called when a new DDP connection is made to the server. // 1313
* @locus Server // 1314
* @param {function} callback The function to call when a new DDP connection is established. // 1315
* @memberOf Meteor // 1316
*/ // 1317
onConnection: function (fn) { // 1318
var self = this; // 1319
return self.onConnectionHook.register(fn); // 1320
}, // 1321
// 1322
_handleConnect: function (socket, msg) { // 1323
var self = this; // 1324
// 1325
// The connect message must specify a version and an array of supported // 1326
// versions, and it must claim to support what it is proposing. // 1327
if (!(typeof (msg.version) === 'string' && // 1328
_.isArray(msg.support) && // 1329
_.all(msg.support, _.isString) && // 1330
_.contains(msg.support, msg.version))) { // 1331
socket.send(stringifyDDP({msg: 'failed', // 1332
version: SUPPORTED_DDP_VERSIONS[0]})); // 1333
socket.close(); // 1334
return; // 1335
} // 1336
// 1337
// In the future, handle session resumption: something like: // 1338
// socket._meteorSession = self.sessions[msg.session] // 1339
var version = calculateVersion(msg.support, SUPPORTED_DDP_VERSIONS); // 1340
// 1341
if (msg.version !== version) { // 1342
// The best version to use (according to the client's stated preferences) // 1343
// is not the one the client is trying to use. Inform them about the best // 1344
// version to use. // 1345
socket.send(stringifyDDP({msg: 'failed', version: version})); // 1346
socket.close(); // 1347
return; // 1348
} // 1349
// 1350
// Yay, version matches! Create a new session. // 1351
// Note: Troposphere depends on the ability to mutate // 1352
// Meteor.server.options.heartbeatTimeout! This is a hack, but it's life. // 1353
socket._meteorSession = new Session(self, version, socket, self.options); // 1354
self.sessions[socket._meteorSession.id] = socket._meteorSession; // 1355
self.onConnectionHook.each(function (callback) { // 1356
if (socket._meteorSession) // 1357
callback(socket._meteorSession.connectionHandle); // 1358
return true; // 1359
}); // 1360
}, // 1361
/** // 1362
* Register a publish handler function. // 1363
* // 1364
* @param name {String} identifier for query // 1365
* @param handler {Function} publish handler // 1366
* @param options {Object} // 1367
* // 1368
* Server will call handler function on each new subscription, // 1369
* either when receiving DDP sub message for a named subscription, or on // 1370
* DDP connect for a universal subscription. // 1371
* // 1372
* If name is null, this will be a subscription that is // 1373
* automatically established and permanently on for all connected // 1374
* client, instead of a subscription that can be turned on and off // 1375
* with subscribe(). // 1376
* // 1377
* options to contain: // 1378
* - (mostly internal) is_auto: true if generated automatically // 1379
* from an autopublish hook. this is for cosmetic purposes only // 1380
* (it lets us determine whether to print a warning suggesting // 1381
* that you turn off autopublish.) // 1382
*/ // 1383
// 1384
/** // 1385
* @summary Publish a record set. // 1386
* @memberOf Meteor // 1387
* @locus Server // 1388
* @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.
*/ // 1391
publish: function (name, handler, options) { // 1392
var self = this; // 1393
// 1394
options = options || {}; // 1395
// 1396
if (name && name in self.publish_handlers) { // 1397
Meteor._debug("Ignoring duplicate publish named '" + name + "'"); // 1398
return; // 1399
} // 1400
// 1401
if (Package.autopublish && !options.is_auto) { // 1402
// They have autopublish on, yet they're trying to manually // 1403
// picking stuff to publish. They probably should turn off // 1404
// autopublish. (This check isn't perfect -- if you create a // 1405
// publish before you turn on autopublish, it won't catch // 1406
// it. But this will definitely handle the simple case where // 1407
// you've added the autopublish package to your app, and are // 1408
// calling publish from your app code.) // 1409
if (!self.warned_about_autopublish) { // 1410
self.warned_about_autopublish = true; // 1411
Meteor._debug( // 1412
"** You've set up some data subscriptions with Meteor.publish(), but\n" + // 1413
"** you still have autopublish turned on. Because autopublish is still\n" + // 1414
"** on, your Meteor.publish() calls won't have much effect. All data\n" + // 1415
"** will still be sent to all clients.\n" + // 1416
"**\n" + // 1417
"** Turn off autopublish by removing the autopublish package:\n" + // 1418
"**\n" + // 1419
"** $ meteor remove autopublish\n" + // 1420
"**\n" + // 1421
"** .. and make sure you have Meteor.publish() and Meteor.subscribe() calls\n" + // 1422
"** for each collection that you want clients to see.\n"); // 1423
} // 1424
} // 1425
// 1426
if (name) // 1427
self.publish_handlers[name] = handler; // 1428
else { // 1429
self.universal_publish_handlers.push(handler); // 1430
// Spin up the new publisher on any existing session too. Run each // 1431
// session's subscription in a new Fiber, so that there's no change for // 1432
// self.sessions to change while we're running this loop. // 1433
_.each(self.sessions, function (session) { // 1434
if (!session._dontStartNewUniversalSubs) { // 1435
Fiber(function() { // 1436
session._startSubscription(handler); // 1437
}).run(); // 1438
} // 1439
}); // 1440
} // 1441
}, // 1442
// 1443
_removeSession: function (session) { // 1444
var self = this; // 1445
if (self.sessions[session.id]) { // 1446
delete self.sessions[session.id]; // 1447
} // 1448
}, // 1449
// 1450
/** // 1451
* @summary Defines functions that can be invoked over the network by clients. // 1452
* @locus Anywhere // 1453
* @param {Object} methods Dictionary whose keys are method names and values are functions. // 1454
* @memberOf Meteor // 1455
*/ // 1456
methods: function (methods) { // 1457
var self = this; // 1458
_.each(methods, function (func, name) { // 1459
if (self.method_handlers[name]) // 1460
throw new Error("A method named '" + name + "' is already defined"); // 1461
self.method_handlers[name] = func; // 1462
}); // 1463
}, // 1464
// 1465
call: function (name /*, arguments */) { // 1466
// if it's a function, the last argument is the result callback, // 1467
// not a parameter to the remote method. // 1468
var args = Array.prototype.slice.call(arguments, 1); // 1469
if (args.length && typeof args[args.length - 1] === "function") // 1470
var callback = args.pop(); // 1471
return this.apply(name, args, callback); // 1472
}, // 1473
// 1474
// @param options {Optional Object} // 1475
// @param callback {Optional Function} // 1476
apply: function (name, args, options, callback) { // 1477
var self = this; // 1478
// 1479
// We were passed 3 arguments. They may be either (name, args, options) // 1480
// or (name, args, callback) // 1481
if (!callback && typeof options === 'function') { // 1482
callback = options; // 1483
options = {}; // 1484
} // 1485
options = options || {}; // 1486
// 1487
if (callback) // 1488
// It's not really necessary to do this, since we immediately // 1489
// run the callback in this fiber before returning, but we do it // 1490
// anyway for regularity. // 1491
// XXX improve error message (and how we report it) // 1492
callback = Meteor.bindEnvironment( // 1493
callback, // 1494
"delivering result of invoking '" + name + "'" // 1495
); // 1496
// 1497
// Run the handler // 1498
var handler = self.method_handlers[name]; // 1499
var exception; // 1500
if (!handler) { // 1501
exception = new Meteor.Error(404, "Method not found"); // 1502
} else { // 1503
// If this is a method call from within another method, get the // 1504
// user state from the outer method, otherwise don't allow // 1505
// setUserId to be called // 1506
var userId = null; // 1507
var setUserId = function() { // 1508
throw new Error("Can't call setUserId on a server initiated method call"); // 1509
}; // 1510
var connection = null; // 1511
var currentInvocation = DDP._CurrentInvocation.get(); // 1512
if (currentInvocation) { // 1513
userId = currentInvocation.userId; // 1514
setUserId = function(userId) { // 1515
currentInvocation.setUserId(userId); // 1516
}; // 1517
connection = currentInvocation.connection; // 1518
} // 1519
// 1520
var invocation = new MethodInvocation({ // 1521
isSimulation: false, // 1522
userId: userId, // 1523
setUserId: setUserId, // 1524
connection: connection, // 1525
randomSeed: makeRpcSeed(currentInvocation, name) // 1526
}); // 1527
try { // 1528
var result = DDP._CurrentInvocation.withValue(invocation, function () { // 1529
return maybeAuditArgumentChecks( // 1530
handler, invocation, EJSON.clone(args), "internal call to '" + // 1531
name + "'"); // 1532
}); // 1533
result = EJSON.clone(result); // 1534
} catch (e) { // 1535
exception = e; // 1536
} // 1537
} // 1538
// 1539
// Return the result in whichever way the caller asked for it. Note that we // 1540
// do NOT block on the write fence in an analogous way to how the client // 1541
// blocks on the relevant data being visible, so you are NOT guaranteed that // 1542
// cursor observe callbacks have fired when your callback is invoked. (We // 1543
// can change this if there's a real use case.) // 1544
if (callback) { // 1545
callback(exception, result); // 1546
return undefined; // 1547
} // 1548
if (exception) // 1549
throw exception; // 1550
return result; // 1551
}, // 1552
// 1553
_urlForSession: function (sessionId) { // 1554
var self = this; // 1555
var session = self.sessions[sessionId]; // 1556
if (session) // 1557
return session._socketUrl; // 1558
else // 1559
return null; // 1560
} // 1561
}); // 1562
// 1563
var calculateVersion = function (clientSupportedVersions, // 1564
serverSupportedVersions) { // 1565
var correctVersion = _.find(clientSupportedVersions, function (version) { // 1566
return _.contains(serverSupportedVersions, version); // 1567
}); // 1568
if (!correctVersion) { // 1569
correctVersion = serverSupportedVersions[0]; // 1570
} // 1571
return correctVersion; // 1572
}; // 1573
// 1574
LivedataTest.calculateVersion = calculateVersion; // 1575
// 1576
// 1577
// "blind" exceptions other than those that were deliberately thrown to signal // 1578
// errors to the client // 1579
var wrapInternalException = function (exception, context) { // 1580
if (!exception || exception instanceof Meteor.Error) // 1581
return exception; // 1582
// 1583
// tests can set the 'expected' flag on an exception so it won't go to the // 1584
// server log // 1585
if (!exception.expected) { // 1586
Meteor._debug("Exception " + context, exception.stack); // 1587
if (exception.sanitizedError) { // 1588
Meteor._debug("Sanitized and reported to the client as:", exception.sanitizedError.message); // 1589
Meteor._debug(); // 1590
} // 1591
} // 1592
// 1593
// Did the error contain more details that could have been useful if caught in // 1594
// server code (or if thrown from non-client-originated code), but also // 1595
// provided a "sanitized" version with more context than 500 Internal server // 1596
// error? Use that. // 1597
if (exception.sanitizedError) { // 1598
if (exception.sanitizedError instanceof Meteor.Error) // 1599
return exception.sanitizedError; // 1600
Meteor._debug("Exception " + context + " provides a sanitizedError that " + // 1601
"is not a Meteor.Error; ignoring"); // 1602
} // 1603
// 1604
return new Meteor.Error(500, "Internal server error"); // 1605
}; // 1606
// 1607
// 1608
// Audit argument checks, if the audit-argument-checks package exists (it is a // 1609
// weak dependency of this package). // 1610
var maybeAuditArgumentChecks = function (f, context, args, description) { // 1611
args = args || []; // 1612
if (Package['audit-argument-checks']) { // 1613
return Match._failIfArgumentsAreNotAllChecked( // 1614
f, context, args, description); // 1615
} // 1616
return f.apply(context, args); // 1617
}; // 1618
// 1619
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
}).call(this);
(function () {
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //
// packages/ddp/writefence.js //
// //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
var path = Npm.require('path'); // 1
var Future = Npm.require(path.join('fibers', 'future')); // 2
// 3
// A write fence collects a group of writes, and provides a callback // 4
// when all of the writes are fully committed and propagated (all // 5
// observers have been notified of the write and acknowledged it.) // 6
// // 7
DDPServer._WriteFence = function () { // 8
var self = this; // 9
// 10
self.armed = false; // 11
self.fired = false; // 12
self.retired = false; // 13
self.outstanding_writes = 0; // 14
self.completion_callbacks = []; // 15
}; // 16
// 17
// The current write fence. When there is a current write fence, code // 18
// that writes to databases should register their writes with it using // 19
// beginWrite(). // 20
// // 21
DDPServer._CurrentWriteFence = new Meteor.EnvironmentVariable; // 22
// 23
_.extend(DDPServer._WriteFence.prototype, { // 24
// Start tracking a write, and return an object to represent it. The // 25
// object has a single method, committed(). This method should be // 26
// called when the write is fully committed and propagated. You can // 27
// continue to add writes to the WriteFence up until it is triggered // 28
// (calls its callbacks because all writes have committed.) // 29
beginWrite: function () { // 30
var self = this; // 31
// 32
if (self.retired) // 33
return { committed: function () {} }; // 34
// 35
if (self.fired) // 36
throw new Error("fence has already activated -- too late to add writes"); // 37
// 38
self.outstanding_writes++; // 39
var committed = false; // 40
return { // 41
committed: function () { // 42
if (committed) // 43
throw new Error("committed called twice on the same write"); // 44
committed = true; // 45
self.outstanding_writes--; // 46
self._maybeFire(); // 47
} // 48
}; // 49
}, // 50
// 51
// Arm the fence. Once the fence is armed, and there are no more // 52
// uncommitted writes, it will activate. // 53
arm: function () { // 54
var self = this; // 55
if (self === DDPServer._CurrentWriteFence.get()) // 56
throw Error("Can't arm the current fence"); // 57
self.armed = true; // 58
self._maybeFire(); // 59
}, // 60
// 61
// Register a function to be called when the fence fires. // 62
onAllCommitted: function (func) { // 63
var self = this; // 64
if (self.fired) // 65
throw new Error("fence has already activated -- too late to " + // 66
"add a callback"); // 67
self.completion_callbacks.push(func); // 68
}, // 69
// 70
// Convenience function. Arms the fence, then blocks until it fires. // 71
armAndWait: function () { // 72
var self = this; // 73
var future = new Future; // 74
self.onAllCommitted(function () { // 75
future['return'](); // 76
}); // 77
self.arm(); // 78
future.wait(); // 79
}, // 80
// 81
_maybeFire: function () { // 82
var self = this; // 83
if (self.fired) // 84
throw new Error("write fence already activated?"); // 85
if (self.armed && !self.outstanding_writes) { // 86
self.fired = true; // 87
_.each(self.completion_callbacks, function (f) {f(self);}); // 88
self.completion_callbacks = []; // 89
} // 90
}, // 91
// 92
// Deactivate this fence so that adding more writes has no effect. // 93
// The fence must have already fired. // 94
retire: function () { // 95
var self = this; // 96
if (! self.fired) // 97
throw new Error("Can't retire a fence that hasn't fired."); // 98
self.retired = true; // 99
} // 100
}); // 101
// 102
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
}).call(this);
(function () {
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //
// packages/ddp/crossbar.js //
// //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// A "crossbar" is a class that provides structured notification registration. // 1
// See _match for the definition of how a notification matches a trigger. // 2
// All notifications and triggers must have a string key named 'collection'. // 3
// 4
DDPServer._Crossbar = function (options) { // 5
var self = this; // 6
options = options || {}; // 7
// 8
self.nextId = 1; // 9
// map from collection name (string) -> listener id -> object. each object has // 10
// keys 'trigger', 'callback'. // 11
self.listenersByCollection = {}; // 12
self.factPackage = options.factPackage || "livedata"; // 13
self.factName = options.factName || null; // 14
}; // 15
// 16
_.extend(DDPServer._Crossbar.prototype, { // 17
// Listen for notification that match 'trigger'. A notification // 18
// matches if it has the key-value pairs in trigger as a // 19
// subset. When a notification matches, call 'callback', passing // 20
// the actual notification. // 21
// // 22
// Returns a listen handle, which is an object with a method // 23
// stop(). Call stop() to stop listening. // 24
// // 25
// XXX It should be legal to call fire() from inside a listen() // 26
// callback? // 27
listen: function (trigger, callback) { // 28
var self = this; // 29
var id = self.nextId++; // 30
// 31
if (typeof(trigger.collection) !== 'string') { // 32
throw Error("Trigger lacks collection!"); // 33
} // 34
// 35
var collection = trigger.collection; // save in case trigger is mutated // 36
var record = {trigger: EJSON.clone(trigger), callback: callback}; // 37
if (! _.has(self.listenersByCollection, collection)) { // 38
self.listenersByCollection[collection] = {}; // 39
} // 40
self.listenersByCollection[collection][id] = record; // 41
// 42
if (self.factName && Package.facts) { // 43
Package.facts.Facts.incrementServerFact( // 44
self.factPackage, self.factName, 1); // 45
} // 46
// 47
return { // 48
stop: function () { // 49
if (self.factName && Package.facts) { // 50
Package.facts.Facts.incrementServerFact( // 51
self.factPackage, self.factName, -1); // 52
} // 53
delete self.listenersByCollection[collection][id]; // 54
if (_.isEmpty(self.listenersByCollection[collection])) { // 55
delete self.listenersByCollection[collection]; // 56
} // 57
} // 58
}; // 59
}, // 60
// 61
// Fire the provided 'notification' (an object whose attribute // 62
// values are all JSON-compatibile) -- inform all matching listeners // 63
// (registered with listen()). // 64
// // 65
// If fire() is called inside a write fence, then each of the // 66
// listener callbacks will be called inside the write fence as well. // 67
// // 68
// The listeners may be invoked in parallel, rather than serially. // 69
fire: function (notification) { // 70
var self = this; // 71
// 72
if (typeof(notification.collection) !== 'string') { // 73
throw Error("Notification lacks collection!"); // 74
} // 75
// 76
if (! _.has(self.listenersByCollection, notification.collection)) // 77
return; // 78
// 79
var listenersForCollection = // 80
self.listenersByCollection[notification.collection]; // 81
var callbackIds = []; // 82
_.each(listenersForCollection, function (l, id) { // 83
if (self._matches(notification, l.trigger)) { // 84
callbackIds.push(id); // 85
} // 86
}); // 87
// 88
// Listener callbacks can yield, so we need to first find all the ones that // 89
// match in a single iteration over self.listenersByCollection (which can't // 90
// be mutated during this iteration), and then invoke the matching // 91
// callbacks, checking before each call to ensure they haven't stopped. // 92
// Note that we don't have to check that // 93
// self.listenersByCollection[notification.collection] still === // 94
// listenersForCollection, because the only way that stops being true is if // 95
// listenersForCollection first gets reduced down to the empty object (and // 96
// then never gets increased again). // 97
_.each(callbackIds, function (id) { // 98
if (_.has(listenersForCollection, id)) { // 99
listenersForCollection[id].callback(notification); // 100
} // 101
}); // 102
}, // 103
// 104
// A notification matches a trigger if all keys that exist in both are equal. // 105
// // 106
// Examples: // 107
// N:{collection: "C"} matches T:{collection: "C"} // 108
// (a non-targeted write to a collection matches a // 109
// non-targeted query) // 110
// N:{collection: "C", id: "X"} matches T:{collection: "C"} // 111
// (a targeted write to a collection matches a non-targeted query) // 112
// N:{collection: "C"} matches T:{collection: "C", id: "X"} // 113
// (a non-targeted write to a collection matches a // 114
// targeted query) // 115
// N:{collection: "C", id: "X"} matches T:{collection: "C", id: "X"} // 116
// (a targeted write to a collection matches a targeted query targeted // 117
// at the same document) // 118
// N:{collection: "C", id: "X"} does not match T:{collection: "C", id: "Y"} // 119
// (a targeted write to a collection does not match a targeted query // 120
// targeted at a different document) // 121
_matches: function (notification, trigger) { // 122
// Most notifications that use the crossbar have a string `collection` and // 123
// maybe an `id` that is a string or ObjectID. We're already dividing up // 124
// triggers by collection, but let's fast-track "nope, different ID" (and // 125
// avoid the overly generic EJSON.equals). This makes a noticeable // 126
// performance difference; see https://github.com/meteor/meteor/pull/3697 // 127
if (typeof(notification.id) === 'string' && // 128
typeof(trigger.id) === 'string' && // 129
notification.id !== trigger.id) { // 130
return false; // 131
} // 132
if (notification.id instanceof LocalCollection._ObjectID && // 133
trigger.id instanceof LocalCollection._ObjectID && // 134
! notification.id.equals(trigger.id)) { // 135
return false; // 136
} // 137
// 138
return _.all(trigger, function (triggerValue, key) { // 139
return !_.has(notification, key) || // 140
EJSON.equals(triggerValue, notification[key]); // 141
}); // 142
} // 143
}); // 144
// 145
// The "invalidation crossbar" is a specific instance used by the DDP server to // 146
// implement write fence notifications. Listener callbacks on this crossbar // 147
// should call beginWrite on the current write fence before they return, if they // 148
// want to delay the write fence from firing (ie, the DDP method-data-updated // 149
// message from being sent). // 150
DDPServer._InvalidationCrossbar = new DDPServer._Crossbar({ // 151
factName: "invalidation-crossbar-listeners" // 152
}); // 153
// 154
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
}).call(this);
(function () {
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //
// packages/ddp/livedata_common.js //
// //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// All the supported versions (for both the client and server) // 1
// These must be in order of preference; most favored-first // 2
SUPPORTED_DDP_VERSIONS = [ '1', 'pre2', 'pre1' ]; // 3
// 4
LivedataTest.SUPPORTED_DDP_VERSIONS = SUPPORTED_DDP_VERSIONS; // 5
// 6
// Instance name is this because it is usually referred to as this inside a // 7
// method definition // 8
/** // 9
* @summary The state for a single invocation of a method, referenced by this // 10
* inside a method definition. // 11
* @param {Object} options // 12
* @instanceName this // 13
*/ // 14
MethodInvocation = function (options) { // 15
var self = this; // 16
// 17
// true if we're running not the actual method, but a stub (that is, // 18
// if we're on a client (which may be a browser, or in the future a // 19
// server connecting to another server) and presently running a // 20
// simulation of a server-side method for latency compensation // 21
// purposes). not currently true except in a client such as a browser, // 22
// since there's usually no point in running stubs unless you have a // 23
// zero-latency connection to the user. // 24
// 25
/** // 26
* @summary Access inside a method invocation. Boolean value, true if this invocation is a stub. // 27
* @locus Anywhere // 28
* @name isSimulation // 29
* @memberOf MethodInvocation // 30
* @instance // 31
* @type {Boolean} // 32
*/ // 33
this.isSimulation = options.isSimulation; // 34
// 35
// call this function to allow other method invocations (from the // 36
// same client) to continue running without waiting for this one to // 37
// complete. // 38
this._unblock = options.unblock || function () {}; // 39
this._calledUnblock = false; // 40
// 41
// current user id // 42
// 43
/** // 44
* @summary The id of the user that made this method call, or `null` if no user was logged in. // 45
* @locus Anywhere // 46
* @name userId // 47
* @memberOf MethodInvocation // 48
* @instance // 49
*/ // 50
this.userId = options.userId; // 51
// 52
// sets current user id in all appropriate server contexts and // 53
// reruns subscriptions // 54
this._setUserId = options.setUserId || function () {}; // 55
// 56
// On the server, the connection this method call came in on. // 57
// 58
/** // 59
* @summary Access inside a method invocation. The [connection](#meteor_onconnection) that this method was received on. `null` if the method is not associated with a connection, eg. a server initiated method call.
* @locus Server // 61
* @name connection // 62
* @memberOf MethodInvocation // 63
* @instance // 64
*/ // 65
this.connection = options.connection; // 66
// 67
// The seed for randomStream value generation // 68
this.randomSeed = options.randomSeed; // 69
// 70
// This is set by RandomStream.get; and holds the random stream state // 71
this.randomStream = null; // 72
}; // 73
// 74
_.extend(MethodInvocation.prototype, { // 75
/** // 76
* @summary Call inside a method invocation. Allow subsequent method from this client to begin running in a new fiber.
* @locus Server // 78
* @memberOf MethodInvocation // 79
* @instance // 80
*/ // 81
unblock: function () { // 82
var self = this; // 83
self._calledUnblock = true; // 84
self._unblock(); // 85
}, // 86
// 87
/** // 88
* @summary Set the logged in user. // 89
* @locus Server // 90
* @memberOf MethodInvocation // 91
* @instance // 92
* @param {String | null} userId The value that should be returned by `userId` on this connection. // 93
*/ // 94
setUserId: function(userId) { // 95
var self = this; // 96
if (self._calledUnblock) // 97
throw new Error("Can't call setUserId in a method after calling unblock"); // 98
self.userId = userId; // 99
self._setUserId(userId); // 100
} // 101
}); // 102
// 103
parseDDP = function (stringMessage) { // 104
try { // 105
var msg = JSON.parse(stringMessage); // 106
} catch (e) { // 107
Meteor._debug("Discarding message with invalid JSON", stringMessage); // 108
return null; // 109
} // 110
// DDP messages must be objects. // 111
if (msg === null || typeof msg !== 'object') { // 112
Meteor._debug("Discarding non-object DDP message", stringMessage); // 113
return null; // 114
} // 115
// 116
// massage msg to get it into "abstract ddp" rather than "wire ddp" format. // 117
// 118
// switch between "cleared" rep of unsetting fields and "undefined" // 119
// rep of same // 120
if (_.has(msg, 'cleared')) { // 121
if (!_.has(msg, 'fields')) // 122
msg.fields = {}; // 123
_.each(msg.cleared, function (clearKey) { // 124
msg.fields[clearKey] = undefined; // 125
}); // 126
delete msg.cleared; // 127
} // 128
// 129
_.each(['fields', 'params', 'result'], function (field) { // 130
if (_.has(msg, field)) // 131
msg[field] = EJSON._adjustTypesFromJSONValue(msg[field]); // 132
}); // 133
// 134
return msg; // 135
}; // 136
// 137
stringifyDDP = function (msg) { // 138
var copy = EJSON.clone(msg); // 139
// swizzle 'changed' messages from 'fields undefined' rep to 'fields // 140
// and cleared' rep // 141
if (_.has(msg, 'fields')) { // 142
var cleared = []; // 143
_.each(msg.fields, function (value, key) { // 144
if (value === undefined) { // 145
cleared.push(key); // 146
delete copy.fields[key]; // 147
} // 148
}); // 149
if (!_.isEmpty(cleared)) // 150
copy.cleared = cleared; // 151
if (_.isEmpty(copy.fields)) // 152
delete copy.fields; // 153
} // 154
// adjust types to basic // 155
_.each(['fields', 'params', 'result'], function (field) { // 156
if (_.has(copy, field)) // 157
copy[field] = EJSON._adjustTypesToJSONValue(copy[field]); // 158
}); // 159
if (msg.id && typeof msg.id !== 'string') { // 160
throw new Error("Message id is not a string"); // 161
} // 162
return JSON.stringify(copy); // 163
}; // 164
// 165
// This is private but it's used in a few places. accounts-base uses // 166
// it to get the current user. accounts-password uses it to stash SRP // 167
// state in the DDP session. Meteor.setTimeout and friends clear // 168
// it. We can probably find a better way to factor this. // 169
DDP._CurrentInvocation = new Meteor.EnvironmentVariable; // 170
// 171
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
}).call(this);
(function () {
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //
// packages/ddp/random_stream.js //
// //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// RandomStream allows for generation of pseudo-random values, from a seed. // 1
// // 2
// We use this for consistent 'random' numbers across the client and server. // 3
// We want to generate probably-unique IDs on the client, and we ideally want // 4
// the server to generate the same IDs when it executes the method. // 5
// // 6
// For generated values to be the same, we must seed ourselves the same way, // 7
// and we must keep track of the current state of our pseudo-random generators. // 8
// We call this state the scope. By default, we use the current DDP method // 9
// invocation as our scope. DDP now allows the client to specify a randomSeed. // 10
// If a randomSeed is provided it will be used to seed our random sequences. // 11
// In this way, client and server method calls will generate the same values. // 12
// // 13
// We expose multiple named streams; each stream is independent // 14
// and is seeded differently (but predictably from the name). // 15
// By using multiple streams, we support reordering of requests, // 16
// as long as they occur on different streams. // 17
// // 18
// @param options {Optional Object} // 19
// seed: Array or value - Seed value(s) for the generator. // 20
// If an array, will be used as-is // 21
// If a value, will be converted to a single-value array // 22
// If omitted, a random array will be used as the seed. // 23
RandomStream = function (options) { // 24
var self = this; // 25
// 26
this.seed = [].concat(options.seed || randomToken()); // 27
// 28
this.sequences = {}; // 29
}; // 30
// 31
// Returns a random string of sufficient length for a random seed. // 32
// This is a placeholder function; a similar function is planned // 33
// for Random itself; when that is added we should remove this function, // 34
// and call Random's randomToken instead. // 35
function randomToken() { // 36
return Random.hexString(20); // 37
}; // 38
// 39
// Returns the random stream with the specified name, in the specified scope. // 40
// If scope is null (or otherwise falsey) then we will use Random, which will // 41
// give us as random numbers as possible, but won't produce the same // 42
// values across client and server. // 43
// However, scope will normally be the current DDP method invocation, so // 44
// we'll use the stream with the specified name, and we should get consistent // 45
// values on the client and server sides of a method call. // 46
RandomStream.get = function (scope, name) { // 47
if (!name) { // 48
name = "default"; // 49
} // 50
if (!scope) { // 51
// There was no scope passed in; // 52
// the sequence won't actually be reproducible. // 53
return Random; // 54
} // 55
var randomStream = scope.randomStream; // 56
if (!randomStream) { // 57
scope.randomStream = randomStream = new RandomStream({ // 58
seed: scope.randomSeed // 59
}); // 60
} // 61
return randomStream._sequence(name); // 62
}; // 63
// 64
// Returns the named sequence of pseudo-random values. // 65
// The scope will be DDP._CurrentInvocation.get(), so the stream will produce // 66
// consistent values for method calls on the client and server. // 67
DDP.randomStream = function (name) { // 68
var scope = DDP._CurrentInvocation.get(); // 69
return RandomStream.get(scope, name); // 70
}; // 71
// 72
// Creates a randomSeed for passing to a method call. // 73
// Note that we take enclosing as an argument, // 74
// though we expect it to be DDP._CurrentInvocation.get() // 75
// However, we often evaluate makeRpcSeed lazily, and thus the relevant // 76
// invocation may not be the one currently in scope. // 77
// If enclosing is null, we'll use Random and values won't be repeatable. // 78
makeRpcSeed = function (enclosing, methodName) { // 79
var stream = RandomStream.get(enclosing, '/rpc/' + methodName); // 80
return stream.hexString(20); // 81
}; // 82
// 83
_.extend(RandomStream.prototype, { // 84
// Get a random sequence with the specified name, creating it if does not exist. // 85
// New sequences are seeded with the seed concatenated with the name. // 86
// By passing a seed into Random.create, we use the Alea generator. // 87
_sequence: function (name) { // 88
var self = this; // 89
// 90
var sequence = self.sequences[name] || null; // 91
if (sequence === null) { // 92
var sequenceSeed = self.seed.concat(name); // 93
for (var i = 0; i < sequenceSeed.length; i++) { // 94
if (_.isFunction(sequenceSeed[i])) { // 95
sequenceSeed[i] = sequenceSeed[i](); // 96
} // 97
} // 98
self.sequences[name] = sequence = Random.createWithSeeds.apply(null, sequenceSeed); // 99
} // 100
return sequence; // 101
} // 102
}); // 103
// 104
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
}).call(this);
(function () {
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //
// packages/ddp/livedata_connection.js //
// //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
if (Meteor.isServer) { // 1
var path = Npm.require('path'); // 2
var Fiber = Npm.require('fibers'); // 3
var Future = Npm.require(path.join('fibers', 'future')); // 4
} // 5
// 6
// @param url {String|Object} URL to Meteor app, // 7
// or an object as a test hook (see code) // 8
// Options: // 9
// reloadWithOutstanding: is it OK to reload if there are outstanding methods? // 10
// headers: extra headers to send on the websockets connection, for // 11
// server-to-server DDP only // 12
// _sockjsOptions: Specifies options to pass through to the sockjs client // 13
// onDDPNegotiationVersionFailure: callback when version negotiation fails. // 14
// // 15
// XXX There should be a way to destroy a DDP connection, causing all // 16
// outstanding method calls to fail. // 17
// // 18
// XXX Our current way of handling failure and reconnection is great // 19
// for an app (where we want to tolerate being disconnected as an // 20
// expect state, and keep trying forever to reconnect) but cumbersome // 21
// for something like a command line tool that wants to make a // 22
// connection, call a method, and print an error if connection // 23
// fails. We should have better usability in the latter case (while // 24
// still transparently reconnecting if it's just a transient failure // 25
// or the server migrating us). // 26
var Connection = function (url, options) { // 27
var self = this; // 28
options = _.extend({ // 29
onConnected: function () {}, // 30
onDDPVersionNegotiationFailure: function (description) { // 31
Meteor._debug(description); // 32
}, // 33
heartbeatInterval: 35000, // 34
heartbeatTimeout: 15000, // 35
// These options are only for testing. // 36
reloadWithOutstanding: false, // 37
supportedDDPVersions: SUPPORTED_DDP_VERSIONS, // 38
retry: true, // 39
respondToPings: true // 40
}, options); // 41
// 42
// If set, called when we reconnect, queuing method calls _before_ the // 43
// existing outstanding ones. This is the only data member that is part of the // 44
// public API! // 45
self.onReconnect = null; // 46
// 47
// as a test hook, allow passing a stream instead of a url. // 48
if (typeof url === "object") { // 49
self._stream = url; // 50
} else { // 51
self._stream = new LivedataTest.ClientStream(url, { // 52
retry: options.retry, // 53
headers: options.headers, // 54
_sockjsOptions: options._sockjsOptions, // 55
// Used to keep some tests quiet, or for other cases in which // 56
// the right thing to do with connection errors is to silently // 57
// fail (e.g. sending package usage stats). At some point we // 58
// should have a real API for handling client-stream-level // 59
// errors. // 60
_dontPrintErrors: options._dontPrintErrors, // 61
connectTimeoutMs: options.connectTimeoutMs // 62
}); // 63
} // 64
// 65
self._lastSessionId = null; // 66
self._versionSuggestion = null; // The last proposed DDP version. // 67
self._version = null; // The DDP version agreed on by client and server. // 68
self._stores = {}; // name -> object with methods // 69
self._methodHandlers = {}; // name -> func // 70
self._nextMethodId = 1; // 71
self._supportedDDPVersions = options.supportedDDPVersions; // 72
// 73
self._heartbeatInterval = options.heartbeatInterval; // 74
self._heartbeatTimeout = options.heartbeatTimeout; // 75
// 76
// Tracks methods which the user has tried to call but which have not yet // 77
// called their user callback (ie, they are waiting on their result or for all // 78
// of their writes to be written to the local cache). Map from method ID to // 79
// MethodInvoker object. // 80
self._methodInvokers = {}; // 81
// 82
// Tracks methods which the user has called but whose result messages have not // 83
// arrived yet. // 84
// // 85
// _outstandingMethodBlocks is an array of blocks of methods. Each block // 86
// represents a set of methods that can run at the same time. The first block // 87
// represents the methods which are currently in flight; subsequent blocks // 88
// must wait for previous blocks to be fully finished before they can be sent // 89
// to the server. // 90
// // 91
// Each block is an object with the following fields: // 92
// - methods: a list of MethodInvoker objects // 93
// - wait: a boolean; if true, this block had a single method invoked with // 94
// the "wait" option // 95
// // 96
// There will never be adjacent blocks with wait=false, because the only thing // 97
// that makes methods need to be serialized is a wait method. // 98
// // 99
// Methods are removed from the first block when their "result" is // 100
// received. The entire first block is only removed when all of the in-flight // 101
// methods have received their results (so the "methods" list is empty) *AND* // 102
// all of the data written by those methods are visible in the local cache. So // 103
// it is possible for the first block's methods list to be empty, if we are // 104
// still waiting for some objects to quiesce. // 105
// // 106
// Example: // 107
// _outstandingMethodBlocks = [ // 108
// {wait: false, methods: []}, // 109
// {wait: true, methods: [<MethodInvoker for 'login'>]}, // 110
// {wait: false, methods: [<MethodInvoker for 'foo'>, // 111
// <MethodInvoker for 'bar'>]}] // 112
// This means that there were some methods which were sent to the server and // 113
// which have returned their results, but some of the data written by // 114
// the methods may not be visible in the local cache. Once all that data is // 115
// visible, we will send a 'login' method. Once the login method has returned // 116
// and all the data is visible (including re-running subs if userId changes), // 117
// we will send the 'foo' and 'bar' methods in parallel. // 118
self._outstandingMethodBlocks = []; // 119
// 120
// method ID -> array of objects with keys 'collection' and 'id', listing // 121
// documents written by a given method's stub. keys are associated with // 122
// methods whose stub wrote at least one document, and whose data-done message // 123
// has not yet been received. // 124
self._documentsWrittenByStub = {}; // 125
// collection -> IdMap of "server document" object. A "server document" has: // 126
// - "document": the version of the document according the // 127
// server (ie, the snapshot before a stub wrote it, amended by any changes // 128
// received from the server) // 129
// It is undefined if we think the document does not exist // 130
// - "writtenByStubs": a set of method IDs whose stubs wrote to the document // 131
// whose "data done" messages have not yet been processed // 132
self._serverDocuments = {}; // 133
// 134
// Array of callbacks to be called after the next update of the local // 135
// cache. Used for: // 136
// - Calling methodInvoker.dataVisible and sub ready callbacks after // 137
// the relevant data is flushed. // 138
// - Invoking the callbacks of "half-finished" methods after reconnect // 139
// quiescence. Specifically, methods whose result was received over the old // 140
// connection (so we don't re-send it) but whose data had not been made // 141
// visible. // 142
self._afterUpdateCallbacks = []; // 143
// 144
// In two contexts, we buffer all incoming data messages and then process them // 145
// all at once in a single update: // 146
// - During reconnect, we buffer all data messages until all subs that had // 147
// been ready before reconnect are ready again, and all methods that are // 148
// active have returned their "data done message"; then // 149
// - During the execution of a "wait" method, we buffer all data messages // 150
// until the wait method gets its "data done" message. (If the wait method // 151
// occurs during reconnect, it doesn't get any special handling.) // 152
// all data messages are processed in one update. // 153
// // 154
// The following fields are used for this "quiescence" process. // 155
// 156
// This buffers the messages that aren't being processed yet. // 157
self._messagesBufferedUntilQuiescence = []; // 158
// Map from method ID -> true. Methods are removed from this when their // 159
// "data done" message is received, and we will not quiesce until it is // 160
// empty. // 161
self._methodsBlockingQuiescence = {}; // 162
// map from sub ID -> true for subs that were ready (ie, called the sub // 163
// ready callback) before reconnect but haven't become ready again yet // 164
self._subsBeingRevived = {}; // map from sub._id -> true // 165
// if true, the next data update should reset all stores. (set during // 166
// reconnect.) // 167
self._resetStores = false; // 168
// 169
// name -> array of updates for (yet to be created) collections // 170
self._updatesForUnknownStores = {}; // 171
// if we're blocking a migration, the retry func // 172
self._retryMigrate = null; // 173
// 174
// metadata for subscriptions. Map from sub ID to object with keys: // 175
// - id // 176
// - name // 177
// - params // 178
// - inactive (if true, will be cleaned up if not reused in re-run) // 179
// - ready (has the 'ready' message been received?) // 180
// - readyCallback (an optional callback to call when ready) // 181
// - errorCallback (an optional callback to call if the sub terminates with // 182
// an error, XXX COMPAT WITH 1.0.3.1) // 183
// - stopCallback (an optional callback to call when the sub terminates // 184
// for any reason, with an error argument if an error triggered the stop) // 185
self._subscriptions = {}; // 186
// 187
// Reactive userId. // 188
self._userId = null; // 189
self._userIdDeps = new Tracker.Dependency; // 190
// 191
// Block auto-reload while we're waiting for method responses. // 192
if (Meteor.isClient && Package.reload && !options.reloadWithOutstanding) { // 193
Package.reload.Reload._onMigrate(function (retry) { // 194
if (!self._readyToMigrate()) { // 195
if (self._retryMigrate) // 196
throw new Error("Two migrations in progress?"); // 197
self._retryMigrate = retry; // 198
return false; // 199
} else { // 200
return [true]; // 201
} // 202
}); // 203
} // 204
// 205
var onMessage = function (raw_msg) { // 206
try { // 207
var msg = parseDDP(raw_msg); // 208
} catch (e) { // 209
Meteor._debug("Exception while parsing DDP", e); // 210
return; // 211
} // 212
// 213
if (msg === null || !msg.msg) { // 214
// XXX COMPAT WITH 0.6.6. ignore the old welcome message for back // 215
// compat. Remove this 'if' once the server stops sending welcome // 216
// messages (stream_server.js). // 217
if (! (msg && msg.server_id)) // 218
Meteor._debug("discarding invalid livedata message", msg); // 219
return; // 220
} // 221
// 222
if (msg.msg === 'connected') { // 223
self._version = self._versionSuggestion; // 224
self._livedata_connected(msg); // 225
options.onConnected(); // 226
} // 227
else if (msg.msg == 'failed') { // 228
if (_.contains(self._supportedDDPVersions, msg.version)) { // 229
self._versionSuggestion = msg.version; // 230
self._stream.reconnect({_force: true}); // 231
} else { // 232
var description = // 233
"DDP version negotiation failed; server requested version " + msg.version; // 234
self._stream.disconnect({_permanent: true, _error: description}); // 235
options.onDDPVersionNegotiationFailure(description); // 236
} // 237
} // 238
else if (msg.msg === 'ping') { // 239
if (options.respondToPings) // 240
self._send({msg: "pong", id: msg.id}); // 241
if (self._heartbeat) // 242
self._heartbeat.pingReceived(); // 243
} // 244
else if (msg.msg === 'pong') { // 245
if (self._heartbeat) { // 246
self._heartbeat.pongReceived(); // 247
} // 248
} // 249
else if (_.include(['added', 'changed', 'removed', 'ready', 'updated'], msg.msg)) // 250
self._livedata_data(msg); // 251
else if (msg.msg === 'nosub') // 252
self._livedata_nosub(msg); // 253
else if (msg.msg === 'result') // 254
self._livedata_result(msg); // 255
else if (msg.msg === 'error') // 256
self._livedata_error(msg); // 257
else // 258
Meteor._debug("discarding unknown livedata message type", msg); // 259
}; // 260
// 261
var onReset = function () { // 262
// Send a connect message at the beginning of the stream. // 263
// NOTE: reset is called even on the first connection, so this is // 264
// the only place we send this message. // 265
var msg = {msg: 'connect'}; // 266
if (self._lastSessionId) // 267
msg.session = self._lastSessionId; // 268
msg.version = self._versionSuggestion || self._supportedDDPVersions[0]; // 269
self._versionSuggestion = msg.version; // 270
msg.support = self._supportedDDPVersions; // 271
self._send(msg); // 272
// 273
// Now, to minimize setup latency, go ahead and blast out all of // 274
// our pending methods ands subscriptions before we've even taken // 275
// the necessary RTT to know if we successfully reconnected. (1) // 276
// They're supposed to be idempotent; (2) even if we did // 277
// reconnect, we're not sure what messages might have gotten lost // 278
// (in either direction) since we were disconnected (TCP being // 279
// sloppy about that.) // 280
// 281
// If the current block of methods all got their results (but didn't all get // 282
// their data visible), discard the empty block now. // 283
if (! _.isEmpty(self._outstandingMethodBlocks) && // 284
_.isEmpty(self._outstandingMethodBlocks[0].methods)) { // 285
self._outstandingMethodBlocks.shift(); // 286
} // 287
// 288
// Mark all messages as unsent, they have not yet been sent on this // 289
// connection. // 290
_.each(self._methodInvokers, function (m) { // 291
m.sentMessage = false; // 292
}); // 293
// 294
// If an `onReconnect` handler is set, call it first. Go through // 295
// some hoops to ensure that methods that are called from within // 296
// `onReconnect` get executed _before_ ones that were originally // 297
// outstanding (since `onReconnect` is used to re-establish auth // 298
// certificates) // 299
if (self.onReconnect) // 300
self._callOnReconnectAndSendAppropriateOutstandingMethods(); // 301
else // 302
self._sendOutstandingMethods(); // 303
// 304
// add new subscriptions at the end. this way they take effect after // 305
// the handlers and we don't see flicker. // 306
_.each(self._subscriptions, function (sub, id) { // 307
self._send({ // 308
msg: 'sub', // 309
id: id, // 310
name: sub.name, // 311
params: sub.params // 312
}); // 313
}); // 314
}; // 315
// 316
var onDisconnect = function () { // 317
if (self._heartbeat) { // 318
self._heartbeat.stop(); // 319
self._heartbeat = null; // 320
} // 321
}; // 322
// 323
if (Meteor.isServer) { // 324
self._stream.on('message', Meteor.bindEnvironment(onMessage, Meteor._debug)); // 325
self._stream.on('reset', Meteor.bindEnvironment(onReset, Meteor._debug)); // 326
self._stream.on('disconnect', Meteor.bindEnvironment(onDisconnect, Meteor._debug)); // 327
} else { // 328
self._stream.on('message', onMessage); // 329
self._stream.on('reset', onReset); // 330
self._stream.on('disconnect', onDisconnect); // 331
} // 332
}; // 333
// 334
// A MethodInvoker manages sending a method to the server and calling the user's // 335
// callbacks. On construction, it registers itself in the connection's // 336
// _methodInvokers map; it removes itself once the method is fully finished and // 337
// the callback is invoked. This occurs when it has both received a result, // 338
// and the data written by it is fully visible. // 339
var MethodInvoker = function (options) { // 340
var self = this; // 341
// 342
// Public (within this file) fields. // 343
self.methodId = options.methodId; // 344
self.sentMessage = false; // 345
// 346
self._callback = options.callback; // 347
self._connection = options.connection; // 348
self._message = options.message; // 349
self._onResultReceived = options.onResultReceived || function () {}; // 350
self._wait = options.wait; // 351
self._methodResult = null; // 352
self._dataVisible = false; // 353
// 354
// Register with the connection. // 355
self._connection._methodInvokers[self.methodId] = self; // 356
}; // 357
_.extend(MethodInvoker.prototype, { // 358
// Sends the method message to the server. May be called additional times if // 359
// we lose the connection and reconnect before receiving a result. // 360
sendMessage: function () { // 361
var self = this; // 362
// This function is called before sending a method (including resending on // 363
// reconnect). We should only (re)send methods where we don't already have a // 364
// result! // 365
if (self.gotResult()) // 366
throw new Error("sendingMethod is called on method with result"); // 367
// 368
// If we're re-sending it, it doesn't matter if data was written the first // 369
// time. // 370
self._dataVisible = false; // 371
// 372
self.sentMessage = true; // 373
// 374
// If this is a wait method, make all data messages be buffered until it is // 375
// done. // 376
if (self._wait) // 377
self._connection._methodsBlockingQuiescence[self.methodId] = true; // 378
// 379
// Actually send the message. // 380
self._connection._send(self._message); // 381
}, // 382
// Invoke the callback, if we have both a result and know that all data has // 383
// been written to the local cache. // 384
_maybeInvokeCallback: function () { // 385
var self = this; // 386
if (self._methodResult && self._dataVisible) { // 387
// Call the callback. (This won't throw: the callback was wrapped with // 388
// bindEnvironment.) // 389
self._callback(self._methodResult[0], self._methodResult[1]); // 390
// 391
// Forget about this method. // 392
delete self._connection._methodInvokers[self.methodId]; // 393
// 394
// Let the connection know that this method is finished, so it can try to // 395
// move on to the next block of methods. // 396
self._connection._outstandingMethodFinished(); // 397
} // 398
}, // 399
// Call with the result of the method from the server. Only may be called // 400
// once; once it is called, you should not call sendMessage again. // 401
// If the user provided an onResultReceived callback, call it immediately. // 402
// Then invoke the main callback if data is also visible. // 403
receiveResult: function (err, result) { // 404
var self = this; // 405
if (self.gotResult()) // 406
throw new Error("Methods should only receive results once"); // 407
self._methodResult = [err, result]; // 408
self._onResultReceived(err, result); // 409
self._maybeInvokeCallback(); // 410
}, // 411
// Call this when all data written by the method is visible. This means that // 412
// the method has returns its "data is done" message *AND* all server // 413
// documents that are buffered at that time have been written to the local // 414
// cache. Invokes the main callback if the result has been received. // 415
dataVisible: function () { // 416
var self = this; // 417
self._dataVisible = true; // 418
self._maybeInvokeCallback(); // 419
}, // 420
// True if receiveResult has been called. // 421
gotResult: function () { // 422
var self = this; // 423
return !!self._methodResult; // 424
} // 425
}); // 426
// 427
_.extend(Connection.prototype, { // 428
// 'name' is the name of the data on the wire that should go in the // 429
// store. 'wrappedStore' should be an object with methods beginUpdate, update, // 430
// endUpdate, saveOriginals, retrieveOriginals. see Collection for an example. // 431
registerStore: function (name, wrappedStore) { // 432
var self = this; // 433
// 434
if (name in self._stores) // 435
return false; // 436
// 437
// Wrap the input object in an object which makes any store method not // 438
// implemented by 'store' into a no-op. // 439
var store = {}; // 440
_.each(['update', 'beginUpdate', 'endUpdate', 'saveOriginals', // 441
'retrieveOriginals'], function (method) { // 442
store[method] = function () { // 443
return (wrappedStore[method] // 444
? wrappedStore[method].apply(wrappedStore, arguments) // 445
: undefined); // 446
}; // 447
}); // 448
// 449
self._stores[name] = store; // 450
// 451
var queued = self._updatesForUnknownStores[name]; // 452
if (queued) { // 453
store.beginUpdate(queued.length, false); // 454
_.each(queued, function (msg) { // 455
store.update(msg); // 456
}); // 457
store.endUpdate(); // 458
delete self._updatesForUnknownStores[name]; // 459
} // 460
// 461
return true; // 462
}, // 463
// 464
/** // 465
* @memberOf Meteor // 466
* @summary Subscribe to a record set. Returns a handle that provides // 467
* `stop()` and `ready()` methods. // 468
* @locus Client // 469
* @param {String} name Name of the subscription. Matches the name of the // 470
* server's `publish()` call. // 471
* @param {Any} [arg1,arg2...] Optional arguments passed to publisher // 472
* function on server. // 473
* @param {Function|Object} [callbacks] Optional. May include `onStop` // 474
* and `onReady` callbacks. If there is an error, it is passed as an // 475
* argument to `onStop`. If a function is passed instead of an object, it // 476
* is interpreted as an `onReady` callback. // 477
*/ // 478
subscribe: function (name /* .. [arguments] .. (callback|callbacks) */) { // 479
var self = this; // 480
// 481
var params = Array.prototype.slice.call(arguments, 1); // 482
var callbacks = {}; // 483
if (params.length) { // 484
var lastParam = params[params.length - 1]; // 485
if (_.isFunction(lastParam)) { // 486
callbacks.onReady = params.pop(); // 487
} else if (lastParam && // 488
// XXX COMPAT WITH 1.0.3.1 onError used to exist, but now we use // 489
// onStop with an error callback instead. // 490
_.any([lastParam.onReady, lastParam.onError, lastParam.onStop], // 491
_.isFunction)) { // 492
callbacks = params.pop(); // 493
} // 494
} // 495
// 496
// Is there an existing sub with the same name and param, run in an // 497
// invalidated Computation? This will happen if we are rerunning an // 498
// existing computation. // 499
// // 500
// For example, consider a rerun of: // 501
// // 502
// Tracker.autorun(function () { // 503
// Meteor.subscribe("foo", Session.get("foo")); // 504
// Meteor.subscribe("bar", Session.get("bar")); // 505
// }); // 506
// // 507
// If "foo" has changed but "bar" has not, we will match the "bar" // 508
// subcribe to an existing inactive subscription in order to not // 509
// unsub and resub the subscription unnecessarily. // 510
// // 511
// We only look for one such sub; if there are N apparently-identical subs // 512
// being invalidated, we will require N matching subscribe calls to keep // 513
// them all active. // 514
var existing = _.find(self._subscriptions, function (sub) { // 515
return sub.inactive && sub.name === name && // 516
EJSON.equals(sub.params, params); // 517
}); // 518
// 519
var id; // 520
if (existing) { // 521
id = existing.id; // 522
existing.inactive = false; // reactivate // 523
// 524
if (callbacks.onReady) { // 525
// If the sub is not already ready, replace any ready callback with the // 526
// one provided now. (It's not really clear what users would expect for // 527
// an onReady callback inside an autorun; the semantics we provide is // 528
// that at the time the sub first becomes ready, we call the last // 529
// onReady callback provided, if any.) // 530
if (!existing.ready) // 531
existing.readyCallback = callbacks.onReady; // 532
} // 533
// 534
// XXX COMPAT WITH 1.0.3.1 we used to have onError but now we call // 535
// onStop with an optional error argument // 536
if (callbacks.onError) { // 537
// Replace existing callback if any, so that errors aren't // 538
// double-reported. // 539
existing.errorCallback = callbacks.onError; // 540
} // 541
// 542
if (callbacks.onStop) { // 543
existing.stopCallback = callbacks.onStop; // 544
} // 545
} else { // 546
// New sub! Generate an id, save it locally, and send message. // 547
id = Random.id(); // 548
self._subscriptions[id] = { // 549
id: id, // 550
name: name, // 551
params: EJSON.clone(params), // 552
inactive: false, // 553
ready: false, // 554
readyDeps: new Tracker.Dependency, // 555
readyCallback: callbacks.onReady, // 556
// XXX COMPAT WITH 1.0.3.1 #errorCallback // 557
errorCallback: callbacks.onError, // 558
stopCallback: callbacks.onStop, // 559
connection: self, // 560
remove: function() { // 561
delete this.connection._subscriptions[this.id]; // 562
this.ready && this.readyDeps.changed(); // 563
}, // 564
stop: function() { // 565
this.connection._send({msg: 'unsub', id: id}); // 566
this.remove(); // 567
// 568
if (callbacks.onStop) { // 569
callbacks.onStop(); // 570
} // 571
} // 572
}; // 573
self._send({msg: 'sub', id: id, name: name, params: params}); // 574
} // 575
// 576
// return a handle to the application. // 577
var handle = { // 578
stop: function () { // 579
if (!_.has(self._subscriptions, id)) // 580
return; // 581
// 582
self._subscriptions[id].stop(); // 583
}, // 584
ready: function () { // 585
// return false if we've unsubscribed. // 586
if (!_.has(self._subscriptions, id)) // 587
return false; // 588
var record = self._subscriptions[id]; // 589
record.readyDeps.depend(); // 590
return record.ready; // 591
}, // 592
subscriptionId: id // 593
}; // 594
// 595
if (Tracker.active) { // 596
// We're in a reactive computation, so we'd like to unsubscribe when the // 597
// computation is invalidated... but not if the rerun just re-subscribes // 598
// to the same subscription! When a rerun happens, we use onInvalidate // 599
// as a change to mark the subscription "inactive" so that it can // 600
// be reused from the rerun. If it isn't reused, it's killed from // 601
// an afterFlush. // 602
Tracker.onInvalidate(function (c) { // 603
if (_.has(self._subscriptions, id)) // 604
self._subscriptions[id].inactive = true; // 605
// 606
Tracker.afterFlush(function () { // 607
if (_.has(self._subscriptions, id) && // 608
self._subscriptions[id].inactive) // 609
handle.stop(); // 610
}); // 611
}); // 612
} // 613
// 614
return handle; // 615
}, // 616
// 617
// options: // 618
// - onLateError {Function(error)} called if an error was received after the ready event. // 619
// (errors received before ready cause an error to be thrown) // 620
_subscribeAndWait: function (name, args, options) { // 621
var self = this; // 622
var f = new Future(); // 623
var ready = false; // 624
var handle; // 625
args = args || []; // 626
args.push({ // 627
onReady: function () { // 628
ready = true; // 629
f['return'](); // 630
}, // 631
onError: function (e) { // 632
if (!ready) // 633
f['throw'](e); // 634
else // 635
options && options.onLateError && options.onLateError(e); // 636
} // 637
}); // 638
// 639
handle = self.subscribe.apply(self, [name].concat(args)); // 640
f.wait(); // 641
return handle; // 642
}, // 643
// 644
methods: function (methods) { // 645
var self = this; // 646
_.each(methods, function (func, name) { // 647
if (self._methodHandlers[name]) // 648
throw new Error("A method named '" + name + "' is already defined"); // 649
self._methodHandlers[name] = func; // 650
}); // 651
}, // 652
// 653
/** // 654
* @memberOf Meteor // 655
* @summary Invokes a method passing any number of arguments. // 656
* @locus Anywhere // 657
* @param {String} name Name of method to invoke // 658
* @param {EJSONable} [arg1,arg2...] Optional method arguments // 659
* @param {Function} [asyncCallback] Optional callback, which is called asynchronously with the error or result after the method is complete. If not provided, the method runs synchronously if possible (see below).
*/ // 661
call: function (name /* .. [arguments] .. callback */) { // 662
// if it's a function, the last argument is the result callback, // 663
// not a parameter to the remote method. // 664
var args = Array.prototype.slice.call(arguments, 1); // 665
if (args.length && typeof args[args.length - 1] === "function") // 666
var callback = args.pop(); // 667
return this.apply(name, args, callback); // 668
}, // 669
// 670
// @param options {Optional Object} // 671
// wait: Boolean - Should we wait to call this until all current methods // 672
// are fully finished, and block subsequent method calls // 673
// until this method is fully finished? // 674
// (does not affect methods called from within this method) // 675
// onResultReceived: Function - a callback to call as soon as the method // 676
// result is received. the data written by // 677
// the method may not yet be in the cache! // 678
// returnStubValue: Boolean - If true then in cases where we would have // 679
// otherwise discarded the stub's return value // 680
// and returned undefined, instead we go ahead // 681
// and return it. Specifically, this is any // 682
// time other than when (a) we are already // 683
// inside a stub or (b) we are in Node and no // 684
// callback was provided. Currently we require // 685
// this flag to be explicitly passed to reduce // 686
// the likelihood that stub return values will // 687
// be confused with server return values; we // 688
// may improve this in future. // 689
// @param callback {Optional Function} // 690
// 691
/** // 692
* @memberOf Meteor // 693
* @summary Invoke a method passing an array of arguments. // 694
* @locus Anywhere // 695
* @param {String} name Name of method to invoke // 696
* @param {EJSONable[]} args Method arguments // 697
* @param {Object} [options] // 698
* @param {Boolean} options.wait (Client only) If true, don't send this method until all previous method calls have completed, and don't send any subsequent method calls until this one is completed.
* @param {Function} options.onResultReceived (Client only) This callback is invoked with the error or result of the method (just like `asyncCallback`) as soon as the error or result is available. The local cache may not yet reflect the writes performed by the method.
* @param {Function} [asyncCallback] Optional callback; same semantics as in [`Meteor.call`](#meteor_call). // 701
*/ // 702
apply: function (name, args, options, callback) { // 703
var self = this; // 704
// 705
// We were passed 3 arguments. They may be either (name, args, options) // 706
// or (name, args, callback) // 707
if (!callback && typeof options === 'function') { // 708
callback = options; // 709
options = {}; // 710
} // 711
options = options || {}; // 712
// 713
if (callback) { // 714
// XXX would it be better form to do the binding in stream.on, // 715
// or caller, instead of here? // 716
// XXX improve error message (and how we report it) // 717
callback = Meteor.bindEnvironment( // 718
callback, // 719
"delivering result of invoking '" + name + "'" // 720
); // 721
} // 722
// 723
// Keep our args safe from mutation (eg if we don't send the message for a // 724
// while because of a wait method). // 725
args = EJSON.clone(args); // 726
// 727
// Lazily allocate method ID once we know that it'll be needed. // 728
var methodId = (function () { // 729
var id; // 730
return function () { // 731
if (id === undefined) // 732
id = '' + (self._nextMethodId++); // 733
return id; // 734
}; // 735
})(); // 736
// 737
var enclosing = DDP._CurrentInvocation.get(); // 738
var alreadyInSimulation = enclosing && enclosing.isSimulation; // 739
// 740
// Lazily generate a randomSeed, only if it is requested by the stub. // 741
// The random streams only have utility if they're used on both the client // 742
// and the server; if the client doesn't generate any 'random' values // 743
// then we don't expect the server to generate any either. // 744
// Less commonly, the server may perform different actions from the client, // 745
// and may in fact generate values where the client did not, but we don't // 746
// have any client-side values to match, so even here we may as well just // 747
// use a random seed on the server. In that case, we don't pass the // 748
// randomSeed to save bandwidth, and we don't even generate it to save a // 749
// bit of CPU and to avoid consuming entropy. // 750
var randomSeed = null; // 751
var randomSeedGenerator = function () { // 752
if (randomSeed === null) { // 753
randomSeed = makeRpcSeed(enclosing, name); // 754
} // 755
return randomSeed; // 756
}; // 757
// 758
// Run the stub, if we have one. The stub is supposed to make some // 759
// temporary writes to the database to give the user a smooth experience // 760
// until the actual result of executing the method comes back from the // 761
// server (whereupon the temporary writes to the database will be reversed // 762
// during the beginUpdate/endUpdate process.) // 763
// // 764
// Normally, we ignore the return value of the stub (even if it is an // 765
// exception), in favor of the real return value from the server. The // 766
// exception is if the *caller* is a stub. In that case, we're not going // 767
// to do a RPC, so we use the return value of the stub as our return // 768
// value. // 769
// 770
var stub = self._methodHandlers[name]; // 771
if (stub) { // 772
var setUserId = function(userId) { // 773
self.setUserId(userId); // 774
}; // 775
// 776
var invocation = new MethodInvocation({ // 777
isSimulation: true, // 778
userId: self.userId(), // 779
setUserId: setUserId, // 780
randomSeed: function () { return randomSeedGenerator(); } // 781
}); // 782
// 783
if (!alreadyInSimulation) // 784
self._saveOriginals(); // 785
// 786
try { // 787
// Note that unlike in the corresponding server code, we never audit // 788
// that stubs check() their arguments. // 789
var stubReturnValue = DDP._CurrentInvocation.withValue(invocation, function () { // 790
if (Meteor.isServer) { // 791
// Because saveOriginals and retrieveOriginals aren't reentrant, // 792
// don't allow stubs to yield. // 793
return Meteor._noYieldsAllowed(function () { // 794
// re-clone, so that the stub can't affect our caller's values // 795
return stub.apply(invocation, EJSON.clone(args)); // 796
}); // 797
} else { // 798
return stub.apply(invocation, EJSON.clone(args)); // 799
} // 800
}); // 801
} // 802
catch (e) { // 803
var exception = e; // 804
} // 805
// 806
if (!alreadyInSimulation) // 807
self._retrieveAndStoreOriginals(methodId()); // 808
} // 809
// 810
// If we're in a simulation, stop and return the result we have, // 811
// rather than going on to do an RPC. If there was no stub, // 812
// we'll end up returning undefined. // 813
if (alreadyInSimulation) { // 814
if (callback) { // 815
callback(exception, stubReturnValue); // 816
return undefined; // 817
} // 818
if (exception) // 819
throw exception; // 820
return stubReturnValue; // 821
} // 822
// 823
// If an exception occurred in a stub, and we're ignoring it // 824
// because we're doing an RPC and want to use what the server // 825
// returns instead, log it so the developer knows. // 826
// // 827
// Tests can set the 'expected' flag on an exception so it won't // 828
// go to log. // 829
if (exception && !exception.expected) { // 830
Meteor._debug("Exception while simulating the effect of invoking '" + // 831
name + "'", exception, exception.stack); // 832
} // 833
// 834
// 835
// At this point we're definitely doing an RPC, and we're going to // 836
// return the value of the RPC to the caller. // 837
// 838
// If the caller didn't give a callback, decide what to do. // 839
if (!callback) { // 840
if (Meteor.isClient) { // 841
// On the client, we don't have fibers, so we can't block. The // 842
// only thing we can do is to return undefined and discard the // 843
// result of the RPC. If an error occurred then print the error // 844
// to the console. // 845
callback = function (err) { // 846
err && Meteor._debug("Error invoking Method '" + name + "':", // 847
err.message); // 848
}; // 849
} else { // 850
// On the server, make the function synchronous. Throw on // 851
// errors, return on success. // 852
var future = new Future; // 853
callback = future.resolver(); // 854
} // 855
} // 856
// Send the RPC. Note that on the client, it is important that the // 857
// stub have finished before we send the RPC, so that we know we have // 858
// a complete list of which local documents the stub wrote. // 859
var message = { // 860
msg: 'method', // 861
method: name, // 862
params: args, // 863
id: methodId() // 864
}; // 865
// 866
// Send the randomSeed only if we used it // 867
if (randomSeed !== null) { // 868
message.randomSeed = randomSeed; // 869
} // 870
// 871
var methodInvoker = new MethodInvoker({ // 872
methodId: methodId(), // 873
callback: callback, // 874
connection: self, // 875
onResultReceived: options.onResultReceived, // 876
wait: !!options.wait, // 877
message: message // 878
}); // 879
// 880
if (options.wait) { // 881
// It's a wait method! Wait methods go in their own block. // 882
self._outstandingMethodBlocks.push( // 883
{wait: true, methods: [methodInvoker]}); // 884
} else { // 885
// Not a wait method. Start a new block if the previous block was a wait // 886
// block, and add it to the last block of methods. // 887
if (_.isEmpty(self._outstandingMethodBlocks) || // 888
_.last(self._outstandingMethodBlocks).wait) // 889
self._outstandingMethodBlocks.push({wait: false, methods: []}); // 890
_.last(self._outstandingMethodBlocks).methods.push(methodInvoker); // 891
} // 892
// 893
// If we added it to the first block, send it out now. // 894
if (self._outstandingMethodBlocks.length === 1) // 895
methodInvoker.sendMessage(); // 896
// 897
// If we're using the default callback on the server, // 898
// block waiting for the result. // 899
if (future) { // 900
return future.wait(); // 901
} // 902
return options.returnStubValue ? stubReturnValue : undefined; // 903
}, // 904
// 905
// Before calling a method stub, prepare all stores to track changes and allow // 906
// _retrieveAndStoreOriginals to get the original versions of changed // 907
// documents. // 908
_saveOriginals: function () { // 909
var self = this; // 910
_.each(self._stores, function (s) { // 911
s.saveOriginals(); // 912
}); // 913
}, // 914
// Retrieves the original versions of all documents modified by the stub for // 915
// method 'methodId' from all stores and saves them to _serverDocuments (keyed // 916
// by document) and _documentsWrittenByStub (keyed by method ID). // 917
_retrieveAndStoreOriginals: function (methodId) { // 918
var self = this; // 919
if (self._documentsWrittenByStub[methodId]) // 920
throw new Error("Duplicate methodId in _retrieveAndStoreOriginals"); // 921
// 922
var docsWritten = []; // 923
_.each(self._stores, function (s, collection) { // 924
var originals = s.retrieveOriginals(); // 925
// not all stores define retrieveOriginals // 926
if (!originals) // 927
return; // 928
originals.forEach(function (doc, id) { // 929
docsWritten.push({collection: collection, id: id}); // 930
if (!_.has(self._serverDocuments, collection)) // 931
self._serverDocuments[collection] = new LocalCollection._IdMap; // 932
var serverDoc = self._serverDocuments[collection].setDefault(id, {}); // 933
if (serverDoc.writtenByStubs) { // 934
// We're not the first stub to write this doc. Just add our method ID // 935
// to the record. // 936
serverDoc.writtenByStubs[methodId] = true; // 937
} else { // 938
// First stub! Save the original value and our method ID. // 939
serverDoc.document = doc; // 940
serverDoc.flushCallbacks = []; // 941
serverDoc.writtenByStubs = {}; // 942
serverDoc.writtenByStubs[methodId] = true; // 943
} // 944
}); // 945
}); // 946
if (!_.isEmpty(docsWritten)) { // 947
self._documentsWrittenByStub[methodId] = docsWritten; // 948
} // 949
}, // 950
// 951
// This is very much a private function we use to make the tests // 952
// take up fewer server resources after they complete. // 953
_unsubscribeAll: function () { // 954
var self = this; // 955
_.each(_.clone(self._subscriptions), function (sub, id) { // 956
// Avoid killing the autoupdate subscription so that developers // 957
// still get hot code pushes when writing tests. // 958
// // 959
// XXX it's a hack to encode knowledge about autoupdate here, // 960
// but it doesn't seem worth it yet to have a special API for // 961
// subscriptions to preserve after unit tests. // 962
if (sub.name !== 'meteor_autoupdate_clientVersions') { // 963
self._subscriptions[id].stop(); // 964
} // 965
}); // 966
}, // 967
// 968
// Sends the DDP stringification of the given message object // 969
_send: function (obj) { // 970
var self = this; // 971
self._stream.send(stringifyDDP(obj)); // 972
}, // 973
// 974
// We detected via DDP-level heartbeats that we've lost the // 975
// connection. Unlike `disconnect` or `close`, a lost connection // 976
// will be automatically retried. // 977
_lostConnection: function (error) { // 978
var self = this; // 979
self._stream._lostConnection(error); // 980
}, // 981
// 982
/** // 983
* @summary Get the current connection status. A reactive data source. // 984
* @locus Client // 985
* @memberOf Meteor // 986
*/ // 987
status: function (/*passthrough args*/) { // 988
var self = this; // 989
return self._stream.status.apply(self._stream, arguments); // 990
}, // 991
// 992
/** // 993
* @summary Force an immediate reconnection attempt if the client is not connected to the server. // 994
// 995
This method does nothing if the client is already connected. // 996
* @locus Client // 997
* @memberOf Meteor // 998
*/ // 999
reconnect: function (/*passthrough args*/) { // 1000
var self = this; // 1001
return self._stream.reconnect.apply(self._stream, arguments); // 1002
}, // 1003
// 1004
/** // 1005
* @summary Disconnect the client from the server. // 1006
* @locus Client // 1007
* @memberOf Meteor // 1008
*/ // 1009
disconnect: function (/*passthrough args*/) { // 1010
var self = this; // 1011
return self._stream.disconnect.apply(self._stream, arguments); // 1012
}, // 1013
// 1014
close: function () { // 1015
var self = this; // 1016
return self._stream.disconnect({_permanent: true}); // 1017
}, // 1018
// 1019
/// // 1020
/// Reactive user system // 1021
/// // 1022
userId: function () { // 1023
var self = this; // 1024
if (self._userIdDeps) // 1025
self._userIdDeps.depend(); // 1026
return self._userId; // 1027
}, // 1028
// 1029
setUserId: function (userId) { // 1030
var self = this; // 1031
// Avoid invalidating dependents if setUserId is called with current value. // 1032
if (self._userId === userId) // 1033
return; // 1034
self._userId = userId; // 1035
if (self._userIdDeps) // 1036
self._userIdDeps.changed(); // 1037
}, // 1038
// 1039
// Returns true if we are in a state after reconnect of waiting for subs to be // 1040
// revived or early methods to finish their data, or we are waiting for a // 1041
// "wait" method to finish. // 1042
_waitingForQuiescence: function () { // 1043
var self = this; // 1044
return (! _.isEmpty(self._subsBeingRevived) || // 1045
! _.isEmpty(self._methodsBlockingQuiescence)); // 1046
}, // 1047
// 1048
// Returns true if any method whose message has been sent to the server has // 1049
// not yet invoked its user callback. // 1050
_anyMethodsAreOutstanding: function () { // 1051
var self = this; // 1052
return _.any(_.pluck(self._methodInvokers, 'sentMessage')); // 1053
}, // 1054
// 1055
_livedata_connected: function (msg) { // 1056
var self = this; // 1057
// 1058
if (self._version !== 'pre1' && self._heartbeatInterval !== 0) { // 1059
self._heartbeat = new Heartbeat({ // 1060
heartbeatInterval: self._heartbeatInterval, // 1061
heartbeatTimeout: self._heartbeatTimeout, // 1062
onTimeout: function () { // 1063
self._lostConnection( // 1064
new DDP.ConnectionError("DDP heartbeat timed out")); // 1065
}, // 1066
sendPing: function () { // 1067
self._send({msg: 'ping'}); // 1068
} // 1069
}); // 1070
self._heartbeat.start(); // 1071
} // 1072
// 1073
// If this is a reconnect, we'll have to reset all stores. // 1074
if (self._lastSessionId) // 1075
self._resetStores = true; // 1076
// 1077
if (typeof (msg.session) === "string") { // 1078
var reconnectedToPreviousSession = (self._lastSessionId === msg.session); // 1079
self._lastSessionId = msg.session; // 1080
} // 1081
// 1082
if (reconnectedToPreviousSession) { // 1083
// Successful reconnection -- pick up where we left off. Note that right // 1084
// now, this never happens: the server never connects us to a previous // 1085
// session, because DDP doesn't provide enough data for the server to know // 1086
// what messages the client has processed. We need to improve DDP to make // 1087
// this possible, at which point we'll probably need more code here. // 1088
return; // 1089
} // 1090
// 1091
// Server doesn't have our data any more. Re-sync a new session. // 1092
// 1093
// Forget about messages we were buffering for unknown collections. They'll // 1094
// be resent if still relevant. // 1095
self._updatesForUnknownStores = {}; // 1096
// 1097
if (self._resetStores) { // 1098
// Forget about the effects of stubs. We'll be resetting all collections // 1099
// anyway. // 1100
self._documentsWrittenByStub = {}; // 1101
self._serverDocuments = {}; // 1102
} // 1103
// 1104
// Clear _afterUpdateCallbacks. // 1105
self._afterUpdateCallbacks = []; // 1106
// 1107
// Mark all named subscriptions which are ready (ie, we already called the // 1108
// ready callback) as needing to be revived. // 1109
// XXX We should also block reconnect quiescence until unnamed subscriptions // 1110
// (eg, autopublish) are done re-publishing to avoid flicker! // 1111
self._subsBeingRevived = {}; // 1112
_.each(self._subscriptions, function (sub, id) { // 1113
if (sub.ready) // 1114
self._subsBeingRevived[id] = true; // 1115
}); // 1116
// 1117
// Arrange for "half-finished" methods to have their callbacks run, and // 1118
// track methods that were sent on this connection so that we don't // 1119
// quiesce until they are all done. // 1120
// // 1121
// Start by clearing _methodsBlockingQuiescence: methods sent before // 1122
// reconnect don't matter, and any "wait" methods sent on the new connection // 1123
// that we drop here will be restored by the loop below. // 1124
self._methodsBlockingQuiescence = {}; // 1125
if (self._resetStores) { // 1126
_.each(self._methodInvokers, function (invoker) { // 1127
if (invoker.gotResult()) { // 1128
// This method already got its result, but it didn't call its callback // 1129
// because its data didn't become visible. We did not resend the // 1130
// method RPC. We'll call its callback when we get a full quiesce, // 1131
// since that's as close as we'll get to "data must be visible". // 1132
self._afterUpdateCallbacks.push(_.bind(invoker.dataVisible, invoker)); // 1133
} else if (invoker.sentMessage) { // 1134
// This method has been sent on this connection (maybe as a resend // 1135
// from the last connection, maybe from onReconnect, maybe just very // 1136
// quickly before processing the connected message). // 1137
// // 1138
// We don't need to do anything special to ensure its callbacks get // 1139
// called, but we'll count it as a method which is preventing // 1140
// reconnect quiescence. (eg, it might be a login method that was run // 1141
// from onReconnect, and we don't want to see flicker by seeing a // 1142
// logged-out state.) // 1143
self._methodsBlockingQuiescence[invoker.methodId] = true; // 1144
} // 1145
}); // 1146
} // 1147
// 1148
self._messagesBufferedUntilQuiescence = []; // 1149
// 1150
// If we're not waiting on any methods or subs, we can reset the stores and // 1151
// call the callbacks immediately. // 1152
if (!self._waitingForQuiescence()) { // 1153
if (self._resetStores) { // 1154
_.each(self._stores, function (s) { // 1155
s.beginUpdate(0, true); // 1156
s.endUpdate(); // 1157
}); // 1158
self._resetStores = false; // 1159
} // 1160
self._runAfterUpdateCallbacks(); // 1161
} // 1162
}, // 1163
// 1164
// 1165
_processOneDataMessage: function (msg, updates) { // 1166
var self = this; // 1167
// Using underscore here so as not to need to capitalize. // 1168
self['_process_' + msg.msg](msg, updates); // 1169
}, // 1170
// 1171
// 1172
_livedata_data: function (msg) { // 1173
var self = this; // 1174
// 1175
// collection name -> array of messages // 1176
var updates = {}; // 1177
// 1178
if (self._waitingForQuiescence()) { // 1179
self._messagesBufferedUntilQuiescence.push(msg); // 1180
// 1181
if (msg.msg === "nosub") // 1182
delete self._subsBeingRevived[msg.id]; // 1183
// 1184
_.each(msg.subs || [], function (subId) { // 1185
delete self._subsBeingRevived[subId]; // 1186
}); // 1187
_.each(msg.methods || [], function (methodId) { // 1188
delete self._methodsBlockingQuiescence[methodId]; // 1189
}); // 1190
// 1191
if (self._waitingForQuiescence()) // 1192
return; // 1193
// 1194
// No methods or subs are blocking quiescence! // 1195
// We'll now process and all of our buffered messages, reset all stores, // 1196
// and apply them all at once. // 1197
_.each(self._messagesBufferedUntilQuiescence, function (bufferedMsg) { // 1198
self._processOneDataMessage(bufferedMsg, updates); // 1199
}); // 1200
self._messagesBufferedUntilQuiescence = []; // 1201
} else { // 1202
self._processOneDataMessage(msg, updates); // 1203
} // 1204
// 1205
if (self._resetStores || !_.isEmpty(updates)) { // 1206
// Begin a transactional update of each store. // 1207
_.each(self._stores, function (s, storeName) { // 1208
s.beginUpdate(_.has(updates, storeName) ? updates[storeName].length : 0, // 1209
self._resetStores); // 1210
}); // 1211
self._resetStores = false; // 1212
// 1213
_.each(updates, function (updateMessages, storeName) { // 1214
var store = self._stores[storeName]; // 1215
if (store) { // 1216
_.each(updateMessages, function (updateMessage) { // 1217
store.update(updateMessage); // 1218
}); // 1219
} else { // 1220
// Nobody's listening for this data. Queue it up until // 1221
// someone wants it. // 1222
// XXX memory use will grow without bound if you forget to // 1223
// create a collection or just don't care about it... going // 1224
// to have to do something about that. // 1225
if (!_.has(self._updatesForUnknownStores, storeName)) // 1226
self._updatesForUnknownStores[storeName] = []; // 1227
Array.prototype.push.apply(self._updatesForUnknownStores[storeName], // 1228
updateMessages); // 1229
} // 1230
}); // 1231
// 1232
// End update transaction. // 1233
_.each(self._stores, function (s) { s.endUpdate(); }); // 1234
} // 1235
// 1236
self._runAfterUpdateCallbacks(); // 1237
}, // 1238
// 1239
// Call any callbacks deferred with _runWhenAllServerDocsAreFlushed whose // 1240
// relevant docs have been flushed, as well as dataVisible callbacks at // 1241
// reconnect-quiescence time. // 1242
_runAfterUpdateCallbacks: function () { // 1243
var self = this; // 1244
var callbacks = self._afterUpdateCallbacks; // 1245
self._afterUpdateCallbacks = []; // 1246
_.each(callbacks, function (c) { // 1247
c(); // 1248
}); // 1249
}, // 1250
// 1251
_pushUpdate: function (updates, collection, msg) { // 1252
var self = this; // 1253
if (!_.has(updates, collection)) { // 1254
updates[collection] = []; // 1255
} // 1256
updates[collection].push(msg); // 1257
}, // 1258
// 1259
_getServerDoc: function (collection, id) { // 1260
var self = this; // 1261
if (!_.has(self._serverDocuments, collection)) // 1262
return null; // 1263
var serverDocsForCollection = self._serverDocuments[collection]; // 1264
return serverDocsForCollection.get(id) || null; // 1265
}, // 1266
// 1267
_process_added: function (msg, updates) { // 1268
var self = this; // 1269
var id = LocalCollection._idParse(msg.id); // 1270
var serverDoc = self._getServerDoc(msg.collection, id); // 1271
if (serverDoc) { // 1272
// Some outstanding stub wrote here. // 1273
if (serverDoc.document !== undefined) // 1274
throw new Error("Server sent add for existing id: " + msg.id); // 1275
serverDoc.document = msg.fields || {}; // 1276
serverDoc.document._id = id; // 1277
} else { // 1278
self._pushUpdate(updates, msg.collection, msg); // 1279
} // 1280
}, // 1281
// 1282
_process_changed: function (msg, updates) { // 1283
var self = this; // 1284
var serverDoc = self._getServerDoc( // 1285
msg.collection, LocalCollection._idParse(msg.id)); // 1286
if (serverDoc) { // 1287
if (serverDoc.document === undefined) // 1288
throw new Error("Server sent changed for nonexisting id: " + msg.id); // 1289
LocalCollection._applyChanges(serverDoc.document, msg.fields); // 1290
} else { // 1291
self._pushUpdate(updates, msg.collection, msg); // 1292
} // 1293
}, // 1294
// 1295
_process_removed: function (msg, updates) { // 1296
var self = this; // 1297
var serverDoc = self._getServerDoc( // 1298
msg.collection, LocalCollection._idParse(msg.id)); // 1299
if (serverDoc) { // 1300
// Some outstanding stub wrote here. // 1301
if (serverDoc.document === undefined) // 1302
throw new Error("Server sent removed for nonexisting id:" + msg.id); // 1303
serverDoc.document = undefined; // 1304
} else { // 1305
self._pushUpdate(updates, msg.collection, { // 1306
msg: 'removed', // 1307
collection: msg.collection, // 1308
id: msg.id // 1309
}); // 1310
} // 1311
}, // 1312
// 1313
_process_updated: function (msg, updates) { // 1314
var self = this; // 1315
// Process "method done" messages. // 1316
_.each(msg.methods, function (methodId) { // 1317
_.each(self._documentsWrittenByStub[methodId], function (written) { // 1318
var serverDoc = self._getServerDoc(written.collection, written.id); // 1319
if (!serverDoc) // 1320
throw new Error("Lost serverDoc for " + JSON.stringify(written)); // 1321
if (!serverDoc.writtenByStubs[methodId]) // 1322
throw new Error("Doc " + JSON.stringify(written) + // 1323
" not written by method " + methodId); // 1324
delete serverDoc.writtenByStubs[methodId]; // 1325
if (_.isEmpty(serverDoc.writtenByStubs)) { // 1326
// All methods whose stubs wrote this method have completed! We can // 1327
// now copy the saved document to the database (reverting the stub's // 1328
// change if the server did not write to this object, or applying the // 1329
// server's writes if it did). // 1330
// 1331
// This is a fake ddp 'replace' message. It's just for talking // 1332
// between livedata connections and minimongo. (We have to stringify // 1333
// the ID because it's supposed to look like a wire message.) // 1334
self._pushUpdate(updates, written.collection, { // 1335
msg: 'replace', // 1336
id: LocalCollection._idStringify(written.id), // 1337
replace: serverDoc.document // 1338
}); // 1339
// Call all flush callbacks. // 1340
_.each(serverDoc.flushCallbacks, function (c) { // 1341
c(); // 1342
}); // 1343
// 1344
// Delete this completed serverDocument. Don't bother to GC empty // 1345
// IdMaps inside self._serverDocuments, since there probably aren't // 1346
// many collections and they'll be written repeatedly. // 1347
self._serverDocuments[written.collection].remove(written.id); // 1348
} // 1349
}); // 1350
delete self._documentsWrittenByStub[methodId]; // 1351
// 1352
// We want to call the data-written callback, but we can't do so until all // 1353
// currently buffered messages are flushed. // 1354
var callbackInvoker = self._methodInvokers[methodId]; // 1355
if (!callbackInvoker) // 1356
throw new Error("No callback invoker for method " + methodId); // 1357
self._runWhenAllServerDocsAreFlushed( // 1358
_.bind(callbackInvoker.dataVisible, callbackInvoker)); // 1359
}); // 1360
}, // 1361
// 1362
_process_ready: function (msg, updates) { // 1363
var self = this; // 1364
// Process "sub ready" messages. "sub ready" messages don't take effect // 1365
// until all current server documents have been flushed to the local // 1366
// database. We can use a write fence to implement this. // 1367
_.each(msg.subs, function (subId) { // 1368
self._runWhenAllServerDocsAreFlushed(function () { // 1369
var subRecord = self._subscriptions[subId]; // 1370
// Did we already unsubscribe? // 1371
if (!subRecord) // 1372
return; // 1373
// Did we already receive a ready message? (Oops!) // 1374
if (subRecord.ready) // 1375
return; // 1376
subRecord.readyCallback && subRecord.readyCallback(); // 1377
subRecord.ready = true; // 1378
subRecord.readyDeps.changed(); // 1379
}); // 1380
}); // 1381
}, // 1382
// 1383
// Ensures that "f" will be called after all documents currently in // 1384
// _serverDocuments have been written to the local cache. f will not be called // 1385
// if the connection is lost before then! // 1386
_runWhenAllServerDocsAreFlushed: function (f) { // 1387
var self = this; // 1388
var runFAfterUpdates = function () { // 1389
self._afterUpdateCallbacks.push(f); // 1390
}; // 1391
var unflushedServerDocCount = 0; // 1392
var onServerDocFlush = function () { // 1393
--unflushedServerDocCount; // 1394
if (unflushedServerDocCount === 0) { // 1395
// This was the last doc to flush! Arrange to run f after the updates // 1396
// have been applied. // 1397
runFAfterUpdates(); // 1398
} // 1399
}; // 1400
_.each(self._serverDocuments, function (collectionDocs) { // 1401
collectionDocs.forEach(function (serverDoc) { // 1402
var writtenByStubForAMethodWithSentMessage = _.any( // 1403
serverDoc.writtenByStubs, function (dummy, methodId) { // 1404
var invoker = self._methodInvokers[methodId]; // 1405
return invoker && invoker.sentMessage; // 1406
}); // 1407
if (writtenByStubForAMethodWithSentMessage) { // 1408
++unflushedServerDocCount; // 1409
serverDoc.flushCallbacks.push(onServerDocFlush); // 1410
} // 1411
}); // 1412
}); // 1413
if (unflushedServerDocCount === 0) { // 1414
// There aren't any buffered docs --- we can call f as soon as the current // 1415
// round of updates is applied! // 1416
runFAfterUpdates(); // 1417
} // 1418
}, // 1419
// 1420
_livedata_nosub: function (msg) { // 1421
var self = this; // 1422
// 1423
// First pass it through _livedata_data, which only uses it to help get // 1424
// towards quiescence. // 1425
self._livedata_data(msg); // 1426
// 1427
// Do the rest of our processing immediately, with no // 1428
// buffering-until-quiescence. // 1429
// 1430
// we weren't subbed anyway, or we initiated the unsub. // 1431
if (!_.has(self._subscriptions, msg.id)) // 1432
return; // 1433
// 1434
// XXX COMPAT WITH 1.0.3.1 #errorCallback // 1435
var errorCallback = self._subscriptions[msg.id].errorCallback; // 1436
var stopCallback = self._subscriptions[msg.id].stopCallback; // 1437
// 1438
self._subscriptions[msg.id].remove(); // 1439
// 1440
var meteorErrorFromMsg = function (msgArg) { // 1441
return msgArg && msgArg.error && new Meteor.Error( // 1442
msgArg.error.error, msgArg.error.reason, msgArg.error.details); // 1443
} // 1444
// 1445
// XXX COMPAT WITH 1.0.3.1 #errorCallback // 1446
if (errorCallback && msg.error) { // 1447
errorCallback(meteorErrorFromMsg(msg)); // 1448
} // 1449
// 1450
if (stopCallback) { // 1451
stopCallback(meteorErrorFromMsg(msg)); // 1452
} // 1453
}, // 1454
// 1455
_process_nosub: function () { // 1456
// This is called as part of the "buffer until quiescence" process, but // 1457
// nosub's effect is always immediate. It only goes in the buffer at all // 1458
// because it's possible for a nosub to be the thing that triggers // 1459
// quiescence, if we were waiting for a sub to be revived and it dies // 1460
// instead. // 1461
}, // 1462
// 1463
_livedata_result: function (msg) { // 1464
// id, result or error. error has error (code), reason, details // 1465
// 1466
var self = this; // 1467
// 1468
// find the outstanding request // 1469
// should be O(1) in nearly all realistic use cases // 1470
if (_.isEmpty(self._outstandingMethodBlocks)) { // 1471
Meteor._debug("Received method result but no methods outstanding"); // 1472
return; // 1473
} // 1474
var currentMethodBlock = self._outstandingMethodBlocks[0].methods; // 1475
var m; // 1476
for (var i = 0; i < currentMethodBlock.length; i++) { // 1477
m = currentMethodBlock[i]; // 1478
if (m.methodId === msg.id) // 1479
break; // 1480
} // 1481
// 1482
if (!m) { // 1483
Meteor._debug("Can't match method response to original method call", msg); // 1484
return; // 1485
} // 1486
// 1487
// Remove from current method block. This may leave the block empty, but we // 1488
// don't move on to the next block until the callback has been delivered, in // 1489
// _outstandingMethodFinished. // 1490
currentMethodBlock.splice(i, 1); // 1491
// 1492
if (_.has(msg, 'error')) { // 1493
m.receiveResult(new Meteor.Error( // 1494
msg.error.error, msg.error.reason, // 1495
msg.error.details)); // 1496
} else { // 1497
// msg.result may be undefined if the method didn't return a // 1498
// value // 1499
m.receiveResult(undefined, msg.result); // 1500
} // 1501
}, // 1502
// 1503
// Called by MethodInvoker after a method's callback is invoked. If this was // 1504
// the last outstanding method in the current block, runs the next block. If // 1505
// there are no more methods, consider accepting a hot code push. // 1506
_outstandingMethodFinished: function () { // 1507
var self = this; // 1508
if (self._anyMethodsAreOutstanding()) // 1509
return; // 1510
// 1511
// No methods are outstanding. This should mean that the first block of // 1512
// methods is empty. (Or it might not exist, if this was a method that // 1513
// half-finished before disconnect/reconnect.) // 1514
if (! _.isEmpty(self._outstandingMethodBlocks)) { // 1515
var firstBlock = self._outstandingMethodBlocks.shift(); // 1516
if (! _.isEmpty(firstBlock.methods)) // 1517
throw new Error("No methods outstanding but nonempty block: " + // 1518
JSON.stringify(firstBlock)); // 1519
// 1520
// Send the outstanding methods now in the first block. // 1521
if (!_.isEmpty(self._outstandingMethodBlocks)) // 1522
self._sendOutstandingMethods(); // 1523
} // 1524
// 1525
// Maybe accept a hot code push. // 1526
self._maybeMigrate(); // 1527
}, // 1528
// 1529
// Sends messages for all the methods in the first block in // 1530
// _outstandingMethodBlocks. // 1531
_sendOutstandingMethods: function() { // 1532
var self = this; // 1533
if (_.isEmpty(self._outstandingMethodBlocks)) // 1534
return; // 1535
_.each(self._outstandingMethodBlocks[0].methods, function (m) { // 1536
m.sendMessage(); // 1537
}); // 1538
}, // 1539
// 1540
_livedata_error: function (msg) { // 1541
Meteor._debug("Received error from server: ", msg.reason); // 1542
if (msg.offendingMessage) // 1543
Meteor._debug("For: ", msg.offendingMessage); // 1544
}, // 1545
// 1546
_callOnReconnectAndSendAppropriateOutstandingMethods: function() { // 1547
var self = this; // 1548
var oldOutstandingMethodBlocks = self._outstandingMethodBlocks; // 1549
self._outstandingMethodBlocks = []; // 1550
// 1551
self.onReconnect(); // 1552
// 1553
if (_.isEmpty(oldOutstandingMethodBlocks)) // 1554
return; // 1555
// 1556
// We have at least one block worth of old outstanding methods to try // 1557
// again. First: did onReconnect actually send anything? If not, we just // 1558
// restore all outstanding methods and run the first block. // 1559
if (_.isEmpty(self._outstandingMethodBlocks)) { // 1560
self._outstandingMethodBlocks = oldOutstandingMethodBlocks; // 1561
self._sendOutstandingMethods(); // 1562
return; // 1563
} // 1564
// 1565
// OK, there are blocks on both sides. Special case: merge the last block of // 1566
// the reconnect methods with the first block of the original methods, if // 1567
// neither of them are "wait" blocks. // 1568
if (!_.last(self._outstandingMethodBlocks).wait && // 1569
!oldOutstandingMethodBlocks[0].wait) { // 1570
_.each(oldOutstandingMethodBlocks[0].methods, function (m) { // 1571
_.last(self._outstandingMethodBlocks).methods.push(m); // 1572
// 1573
// If this "last block" is also the first block, send the message. // 1574
if (self._outstandingMethodBlocks.length === 1) // 1575
m.sendMessage(); // 1576
}); // 1577
// 1578
oldOutstandingMethodBlocks.shift(); // 1579
} // 1580
// 1581
// Now add the rest of the original blocks on. // 1582
_.each(oldOutstandingMethodBlocks, function (block) { // 1583
self._outstandingMethodBlocks.push(block); // 1584
}); // 1585
}, // 1586
// 1587
// We can accept a hot code push if there are no methods in flight. // 1588
_readyToMigrate: function() { // 1589
var self = this; // 1590
return _.isEmpty(self._methodInvokers); // 1591
}, // 1592
// 1593
// If we were blocking a migration, see if it's now possible to continue. // 1594
// Call whenever the set of outstanding/blocked methods shrinks. // 1595
_maybeMigrate: function () { // 1596
var self = this; // 1597
if (self._retryMigrate && self._readyToMigrate()) { // 1598
self._retryMigrate(); // 1599
self._retryMigrate = null; // 1600
} // 1601
} // 1602
}); // 1603
// 1604
LivedataTest.Connection = Connection; // 1605
// 1606
// @param url {String} URL to Meteor app, // 1607
// e.g.: // 1608
// "subdomain.meteor.com", // 1609
// "http://subdomain.meteor.com", // 1610
// "/", // 1611
// "ddp+sockjs://ddp--****-foo.meteor.com/sockjs" // 1612
// 1613
/** // 1614
* @summary Connect to the server of a different Meteor application to subscribe to its document sets and invoke its remote methods.
* @locus Anywhere // 1616
* @param {String} url The URL of another Meteor application. // 1617
*/ // 1618
DDP.connect = function (url, options) { // 1619
var ret = new Connection(url, options); // 1620
allConnections.push(ret); // hack. see below. // 1621
return ret; // 1622
}; // 1623
// 1624
// Hack for `spiderable` package: a way to see if the page is done // 1625
// loading all the data it needs. // 1626
// // 1627
allConnections = []; // 1628
DDP._allSubscriptionsReady = function () { // 1629
return _.all(allConnections, function (conn) { // 1630
return _.all(conn._subscriptions, function (sub) { // 1631
return sub.ready; // 1632
}); // 1633
}); // 1634
}; // 1635
// 1636
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
}).call(this);
(function () {
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //
// packages/ddp/server_convenience.js //
// //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Only create a server if we are in an environment with a HTTP server // 1
// (as opposed to, eg, a command-line tool). // 2
// // 3
// Note: this whole conditional is a total hack to get around the fact that this // 4
// package logically should be split into a ddp-client and ddp-server package; // 5
// see https://github.com/meteor/meteor/issues/3452 // 6
// // 7
// Until we do that, this conditional (and the weak dependency on webapp that // 8
// should really be a strong dependency of the ddp-server package) allows you to // 9
// build projects which use `ddp` in Node without wanting to run a DDP server // 10
// (ie, allows you to act as if you were using the nonexistent `ddp-client` // 11
// server package). // 12
if (Package.webapp) { // 13
if (process.env.DDP_DEFAULT_CONNECTION_URL) { // 14
__meteor_runtime_config__.DDP_DEFAULT_CONNECTION_URL = // 15
process.env.DDP_DEFAULT_CONNECTION_URL; // 16
} // 17
// 18
Meteor.server = new Server; // 19
// 20
Meteor.refresh = function (notification) { // 21
DDPServer._InvalidationCrossbar.fire(notification); // 22
}; // 23
// 24
// Proxy the public methods of Meteor.server so they can // 25
// be called directly on Meteor. // 26
_.each(['publish', 'methods', 'call', 'apply', 'onConnection'], // 27
function (name) { // 28
Meteor[name] = _.bind(Meteor.server[name], Meteor.server); // 29
}); // 30
} else { // 31
// No server? Make these empty/no-ops. // 32
Meteor.server = null; // 33
Meteor.refresh = function (notification) { // 34
}; // 35
// 36
// Make these empty/no-ops too, so that non-webapp apps can still // 37
// depend on/use packages that use those functions. // 38
_.each(['publish', 'methods', 'onConnection'], // 39
function (name) { // 40
Meteor[name] = function () { }; // 41
}); // 42
} // 43
// 44
// Meteor.server used to be called Meteor.default_server. Provide // 45
// backcompat as a courtesy even though it was never documented. // 46
// XXX COMPAT WITH 0.6.4 // 47
Meteor.default_server = Meteor.server; // 48
// 49
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
}).call(this);
/* Exports */
if (typeof Package === 'undefined') Package = {};
Package.ddp = {
DDP: DDP,
DDPServer: DDPServer,
LivedataTest: LivedataTest
};
})();
//# sourceMappingURL=ddp.js.map