(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.} 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: []}, // 110 // {wait: false, methods: [, // 111 // ]}] // 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