mirror of
https://github.com/YunoHost-Apps/rocketchat_ynh.git
synced 2024-09-03 20:16:25 +02:00
2303 lines
254 KiB
JavaScript
2303 lines
254 KiB
JavaScript
(function () {
|
|
|
|
/* Imports */
|
|
var Meteor = Package.meteor.Meteor;
|
|
var check = Package.check.check;
|
|
var Match = Package.check.Match;
|
|
var Random = Package.random.Random;
|
|
var EJSON = Package.ejson.EJSON;
|
|
var _ = Package.underscore._;
|
|
var Tracker = Package.tracker.Tracker;
|
|
var Deps = Package.tracker.Deps;
|
|
var Retry = Package.retry.Retry;
|
|
var IdMap = Package['id-map'].IdMap;
|
|
var DDPCommon = Package['ddp-common'].DDPCommon;
|
|
var DiffSequence = Package['diff-sequence'].DiffSequence;
|
|
var MongoID = Package['mongo-id'].MongoID;
|
|
|
|
/* Package-scope variables */
|
|
var DDP, LivedataTest, MongoIDMap, toSockjsUrl, toWebsocketUrl, allConnections;
|
|
|
|
(function(){
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// //
|
|
// packages/ddp-client/namespace.js //
|
|
// //
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
/** // 1
|
|
* @namespace DDP // 2
|
|
* @summary Namespace for DDP-related methods/classes. // 3
|
|
*/ // 4
|
|
DDP = {}; // 5
|
|
LivedataTest = {}; // 6
|
|
// 7
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
}).call(this);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
(function(){
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// //
|
|
// packages/ddp-client/id_map.js //
|
|
// //
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
MongoIDMap = function () { // 1
|
|
var self = this; // 2
|
|
IdMap.call(self, MongoID.idStringify, MongoID.idParse); // 3
|
|
}; // 4
|
|
// 5
|
|
Meteor._inherits(MongoIDMap, IdMap); // 6
|
|
// 7
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
}).call(this);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
(function(){
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// //
|
|
// packages/ddp-client/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
|
|
var deflate = Npm.require('permessage-deflate'); // 133
|
|
// 134
|
|
var targetUrl = toWebsocketUrl(self.endpoint); // 135
|
|
var fayeOptions = { // 136
|
|
headers: self.headers, // 137
|
|
extensions: [deflate] // 138
|
|
}; // 139
|
|
var proxyUrl = self._getProxyUrl(targetUrl); // 140
|
|
if (proxyUrl) { // 141
|
|
fayeOptions.proxy = { origin: proxyUrl }; // 142
|
|
}; // 143
|
|
// 144
|
|
// We would like to specify 'ddp' as the subprotocol here. The npm module we // 145
|
|
// used to use as a client would fail the handshake if we ask for a // 146
|
|
// subprotocol and the server doesn't send one back (and sockjs doesn't). // 147
|
|
// Faye doesn't have that behavior; it's unclear from reading RFC 6455 if // 148
|
|
// Faye is erroneous or not. So for now, we don't specify protocols. // 149
|
|
var subprotocols = []; // 150
|
|
// 151
|
|
var client = self.client = new FayeWebSocket.Client( // 152
|
|
targetUrl, subprotocols, fayeOptions); // 153
|
|
// 154
|
|
self._clearConnectionTimer(); // 155
|
|
self.connectionTimer = Meteor.setTimeout( // 156
|
|
function () { // 157
|
|
self._lostConnection( // 158
|
|
new DDP.ConnectionError("DDP connection timed out")); // 159
|
|
}, // 160
|
|
self.CONNECT_TIMEOUT); // 161
|
|
// 162
|
|
self.client.on('open', Meteor.bindEnvironment(function () { // 163
|
|
return self._onConnect(client); // 164
|
|
}, "stream connect callback")); // 165
|
|
// 166
|
|
var clientOnIfCurrent = function (event, description, f) { // 167
|
|
self.client.on(event, Meteor.bindEnvironment(function () { // 168
|
|
// Ignore events from any connection we've already cleaned up. // 169
|
|
if (client !== self.client) // 170
|
|
return; // 171
|
|
f.apply(this, arguments); // 172
|
|
}, description)); // 173
|
|
}; // 174
|
|
// 175
|
|
clientOnIfCurrent('error', 'stream error callback', function (error) { // 176
|
|
if (!self.options._dontPrintErrors) // 177
|
|
Meteor._debug("stream error", error.message); // 178
|
|
// 179
|
|
// Faye's 'error' object is not a JS error (and among other things, // 180
|
|
// doesn't stringify well). Convert it to one. // 181
|
|
self._lostConnection(new DDP.ConnectionError(error.message)); // 182
|
|
}); // 183
|
|
// 184
|
|
// 185
|
|
clientOnIfCurrent('close', 'stream close callback', function () { // 186
|
|
self._lostConnection(); // 187
|
|
}); // 188
|
|
// 189
|
|
// 190
|
|
clientOnIfCurrent('message', 'stream message callback', function (message) { // 191
|
|
// Ignore binary frames, where message.data is a Buffer // 192
|
|
if (typeof message.data !== "string") // 193
|
|
return; // 194
|
|
// 195
|
|
_.each(self.eventCallbacks.message, function (callback) { // 196
|
|
callback(message.data); // 197
|
|
}); // 198
|
|
}); // 199
|
|
} // 200
|
|
}); // 201
|
|
// 202
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
}).call(this);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
(function(){
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// //
|
|
// packages/ddp-client/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-client/livedata_common.js //
|
|
// //
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
LivedataTest.SUPPORTED_DDP_VERSIONS = DDPCommon.SUPPORTED_DDP_VERSIONS; // 1
|
|
// 2
|
|
// This is private but it's used in a few places. accounts-base uses // 3
|
|
// it to get the current user. Meteor.setTimeout and friends clear // 4
|
|
// it. We can probably find a better way to factor this. // 5
|
|
DDP._CurrentInvocation = new Meteor.EnvironmentVariable; // 6
|
|
// 7
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
}).call(this);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
(function(){
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// //
|
|
// packages/ddp-client/random_stream.js //
|
|
// //
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// Returns the named sequence of pseudo-random values. // 1
|
|
// The scope will be DDP._CurrentInvocation.get(), so the stream will produce // 2
|
|
// consistent values for method calls on the client and server. // 3
|
|
DDP.randomStream = function (name) { // 4
|
|
var scope = DDP._CurrentInvocation.get(); // 5
|
|
return DDPCommon.RandomStream.get(scope, name); // 6
|
|
}; // 7
|
|
// 8
|
|
// 9
|
|
// 10
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
}).call(this);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
(function(){
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// //
|
|
// packages/ddp-client/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: 17500, // 34
|
|
heartbeatTimeout: 15000, // 35
|
|
// These options are only for testing. // 36
|
|
reloadWithOutstanding: false, // 37
|
|
supportedDDPVersions: DDPCommon.SUPPORTED_DDP_VERSIONS, // 38
|
|
retry: true, // 39
|
|
respondToPings: true // 40
|
|
}, options); // 41
|
|
// 42
|
|
// If set, called when we reconnect, queuing method calls _before_ the // 43
|
|
// existing outstanding ones. This is the only data member that is part of the // 44
|
|
// public API! // 45
|
|
self.onReconnect = null; // 46
|
|
// 47
|
|
// as a test hook, allow passing a stream instead of a url. // 48
|
|
if (typeof url === "object") { // 49
|
|
self._stream = url; // 50
|
|
} else { // 51
|
|
self._stream = new LivedataTest.ClientStream(url, { // 52
|
|
retry: options.retry, // 53
|
|
headers: options.headers, // 54
|
|
_sockjsOptions: options._sockjsOptions, // 55
|
|
// Used to keep some tests quiet, or for other cases in which // 56
|
|
// the right thing to do with connection errors is to silently // 57
|
|
// fail (e.g. sending package usage stats). At some point we // 58
|
|
// should have a real API for handling client-stream-level // 59
|
|
// errors. // 60
|
|
_dontPrintErrors: options._dontPrintErrors, // 61
|
|
connectTimeoutMs: options.connectTimeoutMs // 62
|
|
}); // 63
|
|
} // 64
|
|
// 65
|
|
self._lastSessionId = null; // 66
|
|
self._versionSuggestion = null; // The last proposed DDP version. // 67
|
|
self._version = null; // The DDP version agreed on by client and server. // 68
|
|
self._stores = {}; // name -> object with methods // 69
|
|
self._methodHandlers = {}; // name -> func // 70
|
|
self._nextMethodId = 1; // 71
|
|
self._supportedDDPVersions = options.supportedDDPVersions; // 72
|
|
// 73
|
|
self._heartbeatInterval = options.heartbeatInterval; // 74
|
|
self._heartbeatTimeout = options.heartbeatTimeout; // 75
|
|
// 76
|
|
// Tracks methods which the user has tried to call but which have not yet // 77
|
|
// called their user callback (ie, they are waiting on their result or for all // 78
|
|
// of their writes to be written to the local cache). Map from method ID to // 79
|
|
// MethodInvoker object. // 80
|
|
self._methodInvokers = {}; // 81
|
|
// 82
|
|
// Tracks methods which the user has called but whose result messages have not // 83
|
|
// arrived yet. // 84
|
|
// // 85
|
|
// _outstandingMethodBlocks is an array of blocks of methods. Each block // 86
|
|
// represents a set of methods that can run at the same time. The first block // 87
|
|
// represents the methods which are currently in flight; subsequent blocks // 88
|
|
// must wait for previous blocks to be fully finished before they can be sent // 89
|
|
// to the server. // 90
|
|
// // 91
|
|
// Each block is an object with the following fields: // 92
|
|
// - methods: a list of MethodInvoker objects // 93
|
|
// - wait: a boolean; if true, this block had a single method invoked with // 94
|
|
// the "wait" option // 95
|
|
// // 96
|
|
// There will never be adjacent blocks with wait=false, because the only thing // 97
|
|
// that makes methods need to be serialized is a wait method. // 98
|
|
// // 99
|
|
// Methods are removed from the first block when their "result" is // 100
|
|
// received. The entire first block is only removed when all of the in-flight // 101
|
|
// methods have received their results (so the "methods" list is empty) *AND* // 102
|
|
// all of the data written by those methods are visible in the local cache. So // 103
|
|
// it is possible for the first block's methods list to be empty, if we are // 104
|
|
// still waiting for some objects to quiesce. // 105
|
|
// // 106
|
|
// Example: // 107
|
|
// _outstandingMethodBlocks = [ // 108
|
|
// {wait: false, methods: []}, // 109
|
|
// {wait: true, methods: [<MethodInvoker for 'login'>]}, // 110
|
|
// {wait: false, methods: [<MethodInvoker for 'foo'>, // 111
|
|
// <MethodInvoker for 'bar'>]}] // 112
|
|
// This means that there were some methods which were sent to the server and // 113
|
|
// which have returned their results, but some of the data written by // 114
|
|
// the methods may not be visible in the local cache. Once all that data is // 115
|
|
// visible, we will send a 'login' method. Once the login method has returned // 116
|
|
// and all the data is visible (including re-running subs if userId changes), // 117
|
|
// we will send the 'foo' and 'bar' methods in parallel. // 118
|
|
self._outstandingMethodBlocks = []; // 119
|
|
// 120
|
|
// method ID -> array of objects with keys 'collection' and 'id', listing // 121
|
|
// documents written by a given method's stub. keys are associated with // 122
|
|
// methods whose stub wrote at least one document, and whose data-done message // 123
|
|
// has not yet been received. // 124
|
|
self._documentsWrittenByStub = {}; // 125
|
|
// collection -> IdMap of "server document" object. A "server document" has: // 126
|
|
// - "document": the version of the document according the // 127
|
|
// server (ie, the snapshot before a stub wrote it, amended by any changes // 128
|
|
// received from the server) // 129
|
|
// It is undefined if we think the document does not exist // 130
|
|
// - "writtenByStubs": a set of method IDs whose stubs wrote to the document // 131
|
|
// whose "data done" messages have not yet been processed // 132
|
|
self._serverDocuments = {}; // 133
|
|
// 134
|
|
// Array of callbacks to be called after the next update of the local // 135
|
|
// cache. Used for: // 136
|
|
// - Calling methodInvoker.dataVisible and sub ready callbacks after // 137
|
|
// the relevant data is flushed. // 138
|
|
// - Invoking the callbacks of "half-finished" methods after reconnect // 139
|
|
// quiescence. Specifically, methods whose result was received over the old // 140
|
|
// connection (so we don't re-send it) but whose data had not been made // 141
|
|
// visible. // 142
|
|
self._afterUpdateCallbacks = []; // 143
|
|
// 144
|
|
// In two contexts, we buffer all incoming data messages and then process them // 145
|
|
// all at once in a single update: // 146
|
|
// - During reconnect, we buffer all data messages until all subs that had // 147
|
|
// been ready before reconnect are ready again, and all methods that are // 148
|
|
// active have returned their "data done message"; then // 149
|
|
// - During the execution of a "wait" method, we buffer all data messages // 150
|
|
// until the wait method gets its "data done" message. (If the wait method // 151
|
|
// occurs during reconnect, it doesn't get any special handling.) // 152
|
|
// all data messages are processed in one update. // 153
|
|
// // 154
|
|
// The following fields are used for this "quiescence" process. // 155
|
|
// 156
|
|
// This buffers the messages that aren't being processed yet. // 157
|
|
self._messagesBufferedUntilQuiescence = []; // 158
|
|
// Map from method ID -> true. Methods are removed from this when their // 159
|
|
// "data done" message is received, and we will not quiesce until it is // 160
|
|
// empty. // 161
|
|
self._methodsBlockingQuiescence = {}; // 162
|
|
// map from sub ID -> true for subs that were ready (ie, called the sub // 163
|
|
// ready callback) before reconnect but haven't become ready again yet // 164
|
|
self._subsBeingRevived = {}; // map from sub._id -> true // 165
|
|
// if true, the next data update should reset all stores. (set during // 166
|
|
// reconnect.) // 167
|
|
self._resetStores = false; // 168
|
|
// 169
|
|
// name -> array of updates for (yet to be created) collections // 170
|
|
self._updatesForUnknownStores = {}; // 171
|
|
// if we're blocking a migration, the retry func // 172
|
|
self._retryMigrate = null; // 173
|
|
// 174
|
|
// metadata for subscriptions. Map from sub ID to object with keys: // 175
|
|
// - id // 176
|
|
// - name // 177
|
|
// - params // 178
|
|
// - inactive (if true, will be cleaned up if not reused in re-run) // 179
|
|
// - ready (has the 'ready' message been received?) // 180
|
|
// - readyCallback (an optional callback to call when ready) // 181
|
|
// - errorCallback (an optional callback to call if the sub terminates with // 182
|
|
// an error, XXX COMPAT WITH 1.0.3.1) // 183
|
|
// - stopCallback (an optional callback to call when the sub terminates // 184
|
|
// for any reason, with an error argument if an error triggered the stop) // 185
|
|
self._subscriptions = {}; // 186
|
|
// 187
|
|
// Reactive userId. // 188
|
|
self._userId = null; // 189
|
|
self._userIdDeps = new Tracker.Dependency; // 190
|
|
// 191
|
|
// Block auto-reload while we're waiting for method responses. // 192
|
|
if (Meteor.isClient && Package.reload && !options.reloadWithOutstanding) { // 193
|
|
Package.reload.Reload._onMigrate(function (retry) { // 194
|
|
if (!self._readyToMigrate()) { // 195
|
|
if (self._retryMigrate) // 196
|
|
throw new Error("Two migrations in progress?"); // 197
|
|
self._retryMigrate = retry; // 198
|
|
return false; // 199
|
|
} else { // 200
|
|
return [true]; // 201
|
|
} // 202
|
|
}); // 203
|
|
} // 204
|
|
// 205
|
|
var onMessage = function (raw_msg) { // 206
|
|
try { // 207
|
|
var msg = DDPCommon.parseDDP(raw_msg); // 208
|
|
} catch (e) { // 209
|
|
Meteor._debug("Exception while parsing DDP", e); // 210
|
|
return; // 211
|
|
} // 212
|
|
// 213
|
|
// Any message counts as receiving a pong, as it demonstrates that // 214
|
|
// the server is still alive. // 215
|
|
if (self._heartbeat) { // 216
|
|
self._heartbeat.messageReceived(); // 217
|
|
} // 218
|
|
// 219
|
|
if (msg === null || !msg.msg) { // 220
|
|
// XXX COMPAT WITH 0.6.6. ignore the old welcome message for back // 221
|
|
// compat. Remove this 'if' once the server stops sending welcome // 222
|
|
// messages (stream_server.js). // 223
|
|
if (! (msg && msg.server_id)) // 224
|
|
Meteor._debug("discarding invalid livedata message", msg); // 225
|
|
return; // 226
|
|
} // 227
|
|
// 228
|
|
if (msg.msg === 'connected') { // 229
|
|
self._version = self._versionSuggestion; // 230
|
|
self._livedata_connected(msg); // 231
|
|
options.onConnected(); // 232
|
|
} // 233
|
|
else if (msg.msg === 'failed') { // 234
|
|
if (_.contains(self._supportedDDPVersions, msg.version)) { // 235
|
|
self._versionSuggestion = msg.version; // 236
|
|
self._stream.reconnect({_force: true}); // 237
|
|
} else { // 238
|
|
var description = // 239
|
|
"DDP version negotiation failed; server requested version " + msg.version; // 240
|
|
self._stream.disconnect({_permanent: true, _error: description}); // 241
|
|
options.onDDPVersionNegotiationFailure(description); // 242
|
|
} // 243
|
|
} // 244
|
|
else if (msg.msg === 'ping' && options.respondToPings) { // 245
|
|
self._send({msg: "pong", id: msg.id}); // 246
|
|
} // 247
|
|
else if (msg.msg === 'pong') { // 248
|
|
// noop, as we assume everything's a pong // 249
|
|
} // 250
|
|
else if (_.include(['added', 'changed', 'removed', 'ready', 'updated'], msg.msg)) // 251
|
|
self._livedata_data(msg); // 252
|
|
else if (msg.msg === 'nosub') // 253
|
|
self._livedata_nosub(msg); // 254
|
|
else if (msg.msg === 'result') // 255
|
|
self._livedata_result(msg); // 256
|
|
else if (msg.msg === 'error') // 257
|
|
self._livedata_error(msg); // 258
|
|
else // 259
|
|
Meteor._debug("discarding unknown livedata message type", msg); // 260
|
|
}; // 261
|
|
// 262
|
|
var onReset = function () { // 263
|
|
// Send a connect message at the beginning of the stream. // 264
|
|
// NOTE: reset is called even on the first connection, so this is // 265
|
|
// the only place we send this message. // 266
|
|
var msg = {msg: 'connect'}; // 267
|
|
if (self._lastSessionId) // 268
|
|
msg.session = self._lastSessionId; // 269
|
|
msg.version = self._versionSuggestion || self._supportedDDPVersions[0]; // 270
|
|
self._versionSuggestion = msg.version; // 271
|
|
msg.support = self._supportedDDPVersions; // 272
|
|
self._send(msg); // 273
|
|
// 274
|
|
// Now, to minimize setup latency, go ahead and blast out all of // 275
|
|
// our pending methods ands subscriptions before we've even taken // 276
|
|
// the necessary RTT to know if we successfully reconnected. (1) // 277
|
|
// They're supposed to be idempotent; (2) even if we did // 278
|
|
// reconnect, we're not sure what messages might have gotten lost // 279
|
|
// (in either direction) since we were disconnected (TCP being // 280
|
|
// sloppy about that.) // 281
|
|
// 282
|
|
// If the current block of methods all got their results (but didn't all get // 283
|
|
// their data visible), discard the empty block now. // 284
|
|
if (! _.isEmpty(self._outstandingMethodBlocks) && // 285
|
|
_.isEmpty(self._outstandingMethodBlocks[0].methods)) { // 286
|
|
self._outstandingMethodBlocks.shift(); // 287
|
|
} // 288
|
|
// 289
|
|
// Mark all messages as unsent, they have not yet been sent on this // 290
|
|
// connection. // 291
|
|
_.each(self._methodInvokers, function (m) { // 292
|
|
m.sentMessage = false; // 293
|
|
}); // 294
|
|
// 295
|
|
// If an `onReconnect` handler is set, call it first. Go through // 296
|
|
// some hoops to ensure that methods that are called from within // 297
|
|
// `onReconnect` get executed _before_ ones that were originally // 298
|
|
// outstanding (since `onReconnect` is used to re-establish auth // 299
|
|
// certificates) // 300
|
|
if (self.onReconnect) // 301
|
|
self._callOnReconnectAndSendAppropriateOutstandingMethods(); // 302
|
|
else // 303
|
|
self._sendOutstandingMethods(); // 304
|
|
// 305
|
|
// add new subscriptions at the end. this way they take effect after // 306
|
|
// the handlers and we don't see flicker. // 307
|
|
_.each(self._subscriptions, function (sub, id) { // 308
|
|
self._send({ // 309
|
|
msg: 'sub', // 310
|
|
id: id, // 311
|
|
name: sub.name, // 312
|
|
params: sub.params // 313
|
|
}); // 314
|
|
}); // 315
|
|
}; // 316
|
|
// 317
|
|
var onDisconnect = function () { // 318
|
|
if (self._heartbeat) { // 319
|
|
self._heartbeat.stop(); // 320
|
|
self._heartbeat = null; // 321
|
|
} // 322
|
|
}; // 323
|
|
// 324
|
|
if (Meteor.isServer) { // 325
|
|
self._stream.on('message', Meteor.bindEnvironment(onMessage, "handling DDP message")); // 326
|
|
self._stream.on('reset', Meteor.bindEnvironment(onReset, "handling DDP reset")); // 327
|
|
self._stream.on('disconnect', Meteor.bindEnvironment(onDisconnect, "handling DDP disconnect")); // 328
|
|
} else { // 329
|
|
self._stream.on('message', onMessage); // 330
|
|
self._stream.on('reset', onReset); // 331
|
|
self._stream.on('disconnect', onDisconnect); // 332
|
|
} // 333
|
|
}; // 334
|
|
// 335
|
|
// A MethodInvoker manages sending a method to the server and calling the user's // 336
|
|
// callbacks. On construction, it registers itself in the connection's // 337
|
|
// _methodInvokers map; it removes itself once the method is fully finished and // 338
|
|
// the callback is invoked. This occurs when it has both received a result, // 339
|
|
// and the data written by it is fully visible. // 340
|
|
var MethodInvoker = function (options) { // 341
|
|
var self = this; // 342
|
|
// 343
|
|
// Public (within this file) fields. // 344
|
|
self.methodId = options.methodId; // 345
|
|
self.sentMessage = false; // 346
|
|
// 347
|
|
self._callback = options.callback; // 348
|
|
self._connection = options.connection; // 349
|
|
self._message = options.message; // 350
|
|
self._onResultReceived = options.onResultReceived || function () {}; // 351
|
|
self._wait = options.wait; // 352
|
|
self._methodResult = null; // 353
|
|
self._dataVisible = false; // 354
|
|
// 355
|
|
// Register with the connection. // 356
|
|
self._connection._methodInvokers[self.methodId] = self; // 357
|
|
}; // 358
|
|
_.extend(MethodInvoker.prototype, { // 359
|
|
// Sends the method message to the server. May be called additional times if // 360
|
|
// we lose the connection and reconnect before receiving a result. // 361
|
|
sendMessage: function () { // 362
|
|
var self = this; // 363
|
|
// This function is called before sending a method (including resending on // 364
|
|
// reconnect). We should only (re)send methods where we don't already have a // 365
|
|
// result! // 366
|
|
if (self.gotResult()) // 367
|
|
throw new Error("sendingMethod is called on method with result"); // 368
|
|
// 369
|
|
// If we're re-sending it, it doesn't matter if data was written the first // 370
|
|
// time. // 371
|
|
self._dataVisible = false; // 372
|
|
// 373
|
|
self.sentMessage = true; // 374
|
|
// 375
|
|
// If this is a wait method, make all data messages be buffered until it is // 376
|
|
// done. // 377
|
|
if (self._wait) // 378
|
|
self._connection._methodsBlockingQuiescence[self.methodId] = true; // 379
|
|
// 380
|
|
// Actually send the message. // 381
|
|
self._connection._send(self._message); // 382
|
|
}, // 383
|
|
// Invoke the callback, if we have both a result and know that all data has // 384
|
|
// been written to the local cache. // 385
|
|
_maybeInvokeCallback: function () { // 386
|
|
var self = this; // 387
|
|
if (self._methodResult && self._dataVisible) { // 388
|
|
// Call the callback. (This won't throw: the callback was wrapped with // 389
|
|
// bindEnvironment.) // 390
|
|
self._callback(self._methodResult[0], self._methodResult[1]); // 391
|
|
// 392
|
|
// Forget about this method. // 393
|
|
delete self._connection._methodInvokers[self.methodId]; // 394
|
|
// 395
|
|
// Let the connection know that this method is finished, so it can try to // 396
|
|
// move on to the next block of methods. // 397
|
|
self._connection._outstandingMethodFinished(); // 398
|
|
} // 399
|
|
}, // 400
|
|
// Call with the result of the method from the server. Only may be called // 401
|
|
// once; once it is called, you should not call sendMessage again. // 402
|
|
// If the user provided an onResultReceived callback, call it immediately. // 403
|
|
// Then invoke the main callback if data is also visible. // 404
|
|
receiveResult: function (err, result) { // 405
|
|
var self = this; // 406
|
|
if (self.gotResult()) // 407
|
|
throw new Error("Methods should only receive results once"); // 408
|
|
self._methodResult = [err, result]; // 409
|
|
self._onResultReceived(err, result); // 410
|
|
self._maybeInvokeCallback(); // 411
|
|
}, // 412
|
|
// Call this when all data written by the method is visible. This means that // 413
|
|
// the method has returns its "data is done" message *AND* all server // 414
|
|
// documents that are buffered at that time have been written to the local // 415
|
|
// cache. Invokes the main callback if the result has been received. // 416
|
|
dataVisible: function () { // 417
|
|
var self = this; // 418
|
|
self._dataVisible = true; // 419
|
|
self._maybeInvokeCallback(); // 420
|
|
}, // 421
|
|
// True if receiveResult has been called. // 422
|
|
gotResult: function () { // 423
|
|
var self = this; // 424
|
|
return !!self._methodResult; // 425
|
|
} // 426
|
|
}); // 427
|
|
// 428
|
|
_.extend(Connection.prototype, { // 429
|
|
// 'name' is the name of the data on the wire that should go in the // 430
|
|
// store. 'wrappedStore' should be an object with methods beginUpdate, update, // 431
|
|
// endUpdate, saveOriginals, retrieveOriginals. see Collection for an example. // 432
|
|
registerStore: function (name, wrappedStore) { // 433
|
|
var self = this; // 434
|
|
// 435
|
|
if (name in self._stores) // 436
|
|
return false; // 437
|
|
// 438
|
|
// Wrap the input object in an object which makes any store method not // 439
|
|
// implemented by 'store' into a no-op. // 440
|
|
var store = {}; // 441
|
|
_.each(['update', 'beginUpdate', 'endUpdate', 'saveOriginals', // 442
|
|
'retrieveOriginals', 'getDoc'], function (method) { // 443
|
|
store[method] = function () { // 444
|
|
return (wrappedStore[method] // 445
|
|
? wrappedStore[method].apply(wrappedStore, arguments) // 446
|
|
: undefined); // 447
|
|
}; // 448
|
|
}); // 449
|
|
// 450
|
|
self._stores[name] = store; // 451
|
|
// 452
|
|
var queued = self._updatesForUnknownStores[name]; // 453
|
|
if (queued) { // 454
|
|
store.beginUpdate(queued.length, false); // 455
|
|
_.each(queued, function (msg) { // 456
|
|
store.update(msg); // 457
|
|
}); // 458
|
|
store.endUpdate(); // 459
|
|
delete self._updatesForUnknownStores[name]; // 460
|
|
} // 461
|
|
// 462
|
|
return true; // 463
|
|
}, // 464
|
|
// 465
|
|
/** // 466
|
|
* @memberOf Meteor // 467
|
|
* @summary Subscribe to a record set. Returns a handle that provides // 468
|
|
* `stop()` and `ready()` methods. // 469
|
|
* @locus Client // 470
|
|
* @param {String} name Name of the subscription. Matches the name of the // 471
|
|
* server's `publish()` call. // 472
|
|
* @param {Any} [arg1,arg2...] Optional arguments passed to publisher // 473
|
|
* function on server. // 474
|
|
* @param {Function|Object} [callbacks] Optional. May include `onStop` // 475
|
|
* and `onReady` callbacks. If there is an error, it is passed as an // 476
|
|
* argument to `onStop`. If a function is passed instead of an object, it // 477
|
|
* is interpreted as an `onReady` callback. // 478
|
|
*/ // 479
|
|
subscribe: function (name /* .. [arguments] .. (callback|callbacks) */) { // 480
|
|
var self = this; // 481
|
|
// 482
|
|
var params = Array.prototype.slice.call(arguments, 1); // 483
|
|
var callbacks = {}; // 484
|
|
if (params.length) { // 485
|
|
var lastParam = params[params.length - 1]; // 486
|
|
if (_.isFunction(lastParam)) { // 487
|
|
callbacks.onReady = params.pop(); // 488
|
|
} else if (lastParam && // 489
|
|
// XXX COMPAT WITH 1.0.3.1 onError used to exist, but now we use // 490
|
|
// onStop with an error callback instead. // 491
|
|
_.any([lastParam.onReady, lastParam.onError, lastParam.onStop], // 492
|
|
_.isFunction)) { // 493
|
|
callbacks = params.pop(); // 494
|
|
} // 495
|
|
} // 496
|
|
// 497
|
|
// Is there an existing sub with the same name and param, run in an // 498
|
|
// invalidated Computation? This will happen if we are rerunning an // 499
|
|
// existing computation. // 500
|
|
// // 501
|
|
// For example, consider a rerun of: // 502
|
|
// // 503
|
|
// Tracker.autorun(function () { // 504
|
|
// Meteor.subscribe("foo", Session.get("foo")); // 505
|
|
// Meteor.subscribe("bar", Session.get("bar")); // 506
|
|
// }); // 507
|
|
// // 508
|
|
// If "foo" has changed but "bar" has not, we will match the "bar" // 509
|
|
// subcribe to an existing inactive subscription in order to not // 510
|
|
// unsub and resub the subscription unnecessarily. // 511
|
|
// // 512
|
|
// We only look for one such sub; if there are N apparently-identical subs // 513
|
|
// being invalidated, we will require N matching subscribe calls to keep // 514
|
|
// them all active. // 515
|
|
var existing = _.find(self._subscriptions, function (sub) { // 516
|
|
return sub.inactive && sub.name === name && // 517
|
|
EJSON.equals(sub.params, params); // 518
|
|
}); // 519
|
|
// 520
|
|
var id; // 521
|
|
if (existing) { // 522
|
|
id = existing.id; // 523
|
|
existing.inactive = false; // reactivate // 524
|
|
// 525
|
|
if (callbacks.onReady) { // 526
|
|
// If the sub is not already ready, replace any ready callback with the // 527
|
|
// one provided now. (It's not really clear what users would expect for // 528
|
|
// an onReady callback inside an autorun; the semantics we provide is // 529
|
|
// that at the time the sub first becomes ready, we call the last // 530
|
|
// onReady callback provided, if any.) // 531
|
|
if (!existing.ready) // 532
|
|
existing.readyCallback = callbacks.onReady; // 533
|
|
} // 534
|
|
// 535
|
|
// XXX COMPAT WITH 1.0.3.1 we used to have onError but now we call // 536
|
|
// onStop with an optional error argument // 537
|
|
if (callbacks.onError) { // 538
|
|
// Replace existing callback if any, so that errors aren't // 539
|
|
// double-reported. // 540
|
|
existing.errorCallback = callbacks.onError; // 541
|
|
} // 542
|
|
// 543
|
|
if (callbacks.onStop) { // 544
|
|
existing.stopCallback = callbacks.onStop; // 545
|
|
} // 546
|
|
} else { // 547
|
|
// New sub! Generate an id, save it locally, and send message. // 548
|
|
id = Random.id(); // 549
|
|
self._subscriptions[id] = { // 550
|
|
id: id, // 551
|
|
name: name, // 552
|
|
params: EJSON.clone(params), // 553
|
|
inactive: false, // 554
|
|
ready: false, // 555
|
|
readyDeps: new Tracker.Dependency, // 556
|
|
readyCallback: callbacks.onReady, // 557
|
|
// XXX COMPAT WITH 1.0.3.1 #errorCallback // 558
|
|
errorCallback: callbacks.onError, // 559
|
|
stopCallback: callbacks.onStop, // 560
|
|
connection: self, // 561
|
|
remove: function() { // 562
|
|
delete this.connection._subscriptions[this.id]; // 563
|
|
this.ready && this.readyDeps.changed(); // 564
|
|
}, // 565
|
|
stop: function() { // 566
|
|
this.connection._send({msg: 'unsub', id: id}); // 567
|
|
this.remove(); // 568
|
|
// 569
|
|
if (callbacks.onStop) { // 570
|
|
callbacks.onStop(); // 571
|
|
} // 572
|
|
} // 573
|
|
}; // 574
|
|
self._send({msg: 'sub', id: id, name: name, params: params}); // 575
|
|
} // 576
|
|
// 577
|
|
// return a handle to the application. // 578
|
|
var handle = { // 579
|
|
stop: function () { // 580
|
|
if (!_.has(self._subscriptions, id)) // 581
|
|
return; // 582
|
|
// 583
|
|
self._subscriptions[id].stop(); // 584
|
|
}, // 585
|
|
ready: function () { // 586
|
|
// return false if we've unsubscribed. // 587
|
|
if (!_.has(self._subscriptions, id)) // 588
|
|
return false; // 589
|
|
var record = self._subscriptions[id]; // 590
|
|
record.readyDeps.depend(); // 591
|
|
return record.ready; // 592
|
|
}, // 593
|
|
subscriptionId: id // 594
|
|
}; // 595
|
|
// 596
|
|
if (Tracker.active) { // 597
|
|
// We're in a reactive computation, so we'd like to unsubscribe when the // 598
|
|
// computation is invalidated... but not if the rerun just re-subscribes // 599
|
|
// to the same subscription! When a rerun happens, we use onInvalidate // 600
|
|
// as a change to mark the subscription "inactive" so that it can // 601
|
|
// be reused from the rerun. If it isn't reused, it's killed from // 602
|
|
// an afterFlush. // 603
|
|
Tracker.onInvalidate(function (c) { // 604
|
|
if (_.has(self._subscriptions, id)) // 605
|
|
self._subscriptions[id].inactive = true; // 606
|
|
// 607
|
|
Tracker.afterFlush(function () { // 608
|
|
if (_.has(self._subscriptions, id) && // 609
|
|
self._subscriptions[id].inactive) // 610
|
|
handle.stop(); // 611
|
|
}); // 612
|
|
}); // 613
|
|
} // 614
|
|
// 615
|
|
return handle; // 616
|
|
}, // 617
|
|
// 618
|
|
// options: // 619
|
|
// - onLateError {Function(error)} called if an error was received after the ready event. // 620
|
|
// (errors received before ready cause an error to be thrown) // 621
|
|
_subscribeAndWait: function (name, args, options) { // 622
|
|
var self = this; // 623
|
|
var f = new Future(); // 624
|
|
var ready = false; // 625
|
|
var handle; // 626
|
|
args = args || []; // 627
|
|
args.push({ // 628
|
|
onReady: function () { // 629
|
|
ready = true; // 630
|
|
f['return'](); // 631
|
|
}, // 632
|
|
onError: function (e) { // 633
|
|
if (!ready) // 634
|
|
f['throw'](e); // 635
|
|
else // 636
|
|
options && options.onLateError && options.onLateError(e); // 637
|
|
} // 638
|
|
}); // 639
|
|
// 640
|
|
handle = self.subscribe.apply(self, [name].concat(args)); // 641
|
|
f.wait(); // 642
|
|
return handle; // 643
|
|
}, // 644
|
|
// 645
|
|
methods: function (methods) { // 646
|
|
var self = this; // 647
|
|
_.each(methods, function (func, name) { // 648
|
|
if (typeof func !== 'function') // 649
|
|
throw new Error("Method '" + name + "' must be a function"); // 650
|
|
if (self._methodHandlers[name]) // 651
|
|
throw new Error("A method named '" + name + "' is already defined"); // 652
|
|
self._methodHandlers[name] = func; // 653
|
|
}); // 654
|
|
}, // 655
|
|
// 656
|
|
/** // 657
|
|
* @memberOf Meteor // 658
|
|
* @summary Invokes a method passing any number of arguments. // 659
|
|
* @locus Anywhere // 660
|
|
* @param {String} name Name of method to invoke // 661
|
|
* @param {EJSONable} [arg1,arg2...] Optional method arguments // 662
|
|
* @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).
|
|
*/ // 664
|
|
call: function (name /* .. [arguments] .. callback */) { // 665
|
|
// if it's a function, the last argument is the result callback, // 666
|
|
// not a parameter to the remote method. // 667
|
|
var args = Array.prototype.slice.call(arguments, 1); // 668
|
|
if (args.length && typeof args[args.length - 1] === "function") // 669
|
|
var callback = args.pop(); // 670
|
|
return this.apply(name, args, callback); // 671
|
|
}, // 672
|
|
// 673
|
|
// @param options {Optional Object} // 674
|
|
// wait: Boolean - Should we wait to call this until all current methods // 675
|
|
// are fully finished, and block subsequent method calls // 676
|
|
// until this method is fully finished? // 677
|
|
// (does not affect methods called from within this method) // 678
|
|
// onResultReceived: Function - a callback to call as soon as the method // 679
|
|
// result is received. the data written by // 680
|
|
// the method may not yet be in the cache! // 681
|
|
// returnStubValue: Boolean - If true then in cases where we would have // 682
|
|
// otherwise discarded the stub's return value // 683
|
|
// and returned undefined, instead we go ahead // 684
|
|
// and return it. Specifically, this is any // 685
|
|
// time other than when (a) we are already // 686
|
|
// inside a stub or (b) we are in Node and no // 687
|
|
// callback was provided. Currently we require // 688
|
|
// this flag to be explicitly passed to reduce // 689
|
|
// the likelihood that stub return values will // 690
|
|
// be confused with server return values; we // 691
|
|
// may improve this in future. // 692
|
|
// @param callback {Optional Function} // 693
|
|
// 694
|
|
/** // 695
|
|
* @memberOf Meteor // 696
|
|
* @summary Invoke a method passing an array of arguments. // 697
|
|
* @locus Anywhere // 698
|
|
* @param {String} name Name of method to invoke // 699
|
|
* @param {EJSONable[]} args Method arguments // 700
|
|
* @param {Object} [options] // 701
|
|
* @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).
|
|
*/ // 705
|
|
apply: function (name, args, options, callback) { // 706
|
|
var self = this; // 707
|
|
// 708
|
|
// We were passed 3 arguments. They may be either (name, args, options) // 709
|
|
// or (name, args, callback) // 710
|
|
if (!callback && typeof options === 'function') { // 711
|
|
callback = options; // 712
|
|
options = {}; // 713
|
|
} // 714
|
|
options = options || {}; // 715
|
|
// 716
|
|
if (callback) { // 717
|
|
// XXX would it be better form to do the binding in stream.on, // 718
|
|
// or caller, instead of here? // 719
|
|
// XXX improve error message (and how we report it) // 720
|
|
callback = Meteor.bindEnvironment( // 721
|
|
callback, // 722
|
|
"delivering result of invoking '" + name + "'" // 723
|
|
); // 724
|
|
} // 725
|
|
// 726
|
|
// Keep our args safe from mutation (eg if we don't send the message for a // 727
|
|
// while because of a wait method). // 728
|
|
args = EJSON.clone(args); // 729
|
|
// 730
|
|
// Lazily allocate method ID once we know that it'll be needed. // 731
|
|
var methodId = (function () { // 732
|
|
var id; // 733
|
|
return function () { // 734
|
|
if (id === undefined) // 735
|
|
id = '' + (self._nextMethodId++); // 736
|
|
return id; // 737
|
|
}; // 738
|
|
})(); // 739
|
|
// 740
|
|
var enclosing = DDP._CurrentInvocation.get(); // 741
|
|
var alreadyInSimulation = enclosing && enclosing.isSimulation; // 742
|
|
// 743
|
|
// Lazily generate a randomSeed, only if it is requested by the stub. // 744
|
|
// The random streams only have utility if they're used on both the client // 745
|
|
// and the server; if the client doesn't generate any 'random' values // 746
|
|
// then we don't expect the server to generate any either. // 747
|
|
// Less commonly, the server may perform different actions from the client, // 748
|
|
// and may in fact generate values where the client did not, but we don't // 749
|
|
// have any client-side values to match, so even here we may as well just // 750
|
|
// use a random seed on the server. In that case, we don't pass the // 751
|
|
// randomSeed to save bandwidth, and we don't even generate it to save a // 752
|
|
// bit of CPU and to avoid consuming entropy. // 753
|
|
var randomSeed = null; // 754
|
|
var randomSeedGenerator = function () { // 755
|
|
if (randomSeed === null) { // 756
|
|
randomSeed = DDPCommon.makeRpcSeed(enclosing, name); // 757
|
|
} // 758
|
|
return randomSeed; // 759
|
|
}; // 760
|
|
// 761
|
|
// Run the stub, if we have one. The stub is supposed to make some // 762
|
|
// temporary writes to the database to give the user a smooth experience // 763
|
|
// until the actual result of executing the method comes back from the // 764
|
|
// server (whereupon the temporary writes to the database will be reversed // 765
|
|
// during the beginUpdate/endUpdate process.) // 766
|
|
// // 767
|
|
// Normally, we ignore the return value of the stub (even if it is an // 768
|
|
// exception), in favor of the real return value from the server. The // 769
|
|
// exception is if the *caller* is a stub. In that case, we're not going // 770
|
|
// to do a RPC, so we use the return value of the stub as our return // 771
|
|
// value. // 772
|
|
// 773
|
|
var stub = self._methodHandlers[name]; // 774
|
|
if (stub) { // 775
|
|
var setUserId = function(userId) { // 776
|
|
self.setUserId(userId); // 777
|
|
}; // 778
|
|
// 779
|
|
var invocation = new DDPCommon.MethodInvocation({ // 780
|
|
isSimulation: true, // 781
|
|
userId: self.userId(), // 782
|
|
setUserId: setUserId, // 783
|
|
randomSeed: function () { return randomSeedGenerator(); } // 784
|
|
}); // 785
|
|
// 786
|
|
if (!alreadyInSimulation) // 787
|
|
self._saveOriginals(); // 788
|
|
// 789
|
|
try { // 790
|
|
// Note that unlike in the corresponding server code, we never audit // 791
|
|
// that stubs check() their arguments. // 792
|
|
var stubReturnValue = DDP._CurrentInvocation.withValue(invocation, function () { // 793
|
|
if (Meteor.isServer) { // 794
|
|
// Because saveOriginals and retrieveOriginals aren't reentrant, // 795
|
|
// don't allow stubs to yield. // 796
|
|
return Meteor._noYieldsAllowed(function () { // 797
|
|
// re-clone, so that the stub can't affect our caller's values // 798
|
|
return stub.apply(invocation, EJSON.clone(args)); // 799
|
|
}); // 800
|
|
} else { // 801
|
|
return stub.apply(invocation, EJSON.clone(args)); // 802
|
|
} // 803
|
|
}); // 804
|
|
} // 805
|
|
catch (e) { // 806
|
|
var exception = e; // 807
|
|
} // 808
|
|
// 809
|
|
if (!alreadyInSimulation) // 810
|
|
self._retrieveAndStoreOriginals(methodId()); // 811
|
|
} // 812
|
|
// 813
|
|
// If we're in a simulation, stop and return the result we have, // 814
|
|
// rather than going on to do an RPC. If there was no stub, // 815
|
|
// we'll end up returning undefined. // 816
|
|
if (alreadyInSimulation) { // 817
|
|
if (callback) { // 818
|
|
callback(exception, stubReturnValue); // 819
|
|
return undefined; // 820
|
|
} // 821
|
|
if (exception) // 822
|
|
throw exception; // 823
|
|
return stubReturnValue; // 824
|
|
} // 825
|
|
// 826
|
|
// If an exception occurred in a stub, and we're ignoring it // 827
|
|
// because we're doing an RPC and want to use what the server // 828
|
|
// returns instead, log it so the developer knows // 829
|
|
// (unless they explicitly ask to see the error). // 830
|
|
// // 831
|
|
// Tests can set the 'expected' flag on an exception so it won't // 832
|
|
// go to log. // 833
|
|
if (exception) { // 834
|
|
if (options.throwStubExceptions) { // 835
|
|
throw exception; // 836
|
|
} else if (!exception.expected) { // 837
|
|
Meteor._debug("Exception while simulating the effect of invoking '" + // 838
|
|
name + "'", exception, exception.stack); // 839
|
|
} // 840
|
|
} // 841
|
|
// 842
|
|
// 843
|
|
// At this point we're definitely doing an RPC, and we're going to // 844
|
|
// return the value of the RPC to the caller. // 845
|
|
// 846
|
|
// If the caller didn't give a callback, decide what to do. // 847
|
|
if (!callback) { // 848
|
|
if (Meteor.isClient) { // 849
|
|
// On the client, we don't have fibers, so we can't block. The // 850
|
|
// only thing we can do is to return undefined and discard the // 851
|
|
// result of the RPC. If an error occurred then print the error // 852
|
|
// to the console. // 853
|
|
callback = function (err) { // 854
|
|
err && Meteor._debug("Error invoking Method '" + name + "':", // 855
|
|
err.message); // 856
|
|
}; // 857
|
|
} else { // 858
|
|
// On the server, make the function synchronous. Throw on // 859
|
|
// errors, return on success. // 860
|
|
var future = new Future; // 861
|
|
callback = future.resolver(); // 862
|
|
} // 863
|
|
} // 864
|
|
// Send the RPC. Note that on the client, it is important that the // 865
|
|
// stub have finished before we send the RPC, so that we know we have // 866
|
|
// a complete list of which local documents the stub wrote. // 867
|
|
var message = { // 868
|
|
msg: 'method', // 869
|
|
method: name, // 870
|
|
params: args, // 871
|
|
id: methodId() // 872
|
|
}; // 873
|
|
// 874
|
|
// Send the randomSeed only if we used it // 875
|
|
if (randomSeed !== null) { // 876
|
|
message.randomSeed = randomSeed; // 877
|
|
} // 878
|
|
// 879
|
|
var methodInvoker = new MethodInvoker({ // 880
|
|
methodId: methodId(), // 881
|
|
callback: callback, // 882
|
|
connection: self, // 883
|
|
onResultReceived: options.onResultReceived, // 884
|
|
wait: !!options.wait, // 885
|
|
message: message // 886
|
|
}); // 887
|
|
// 888
|
|
if (options.wait) { // 889
|
|
// It's a wait method! Wait methods go in their own block. // 890
|
|
self._outstandingMethodBlocks.push( // 891
|
|
{wait: true, methods: [methodInvoker]}); // 892
|
|
} else { // 893
|
|
// Not a wait method. Start a new block if the previous block was a wait // 894
|
|
// block, and add it to the last block of methods. // 895
|
|
if (_.isEmpty(self._outstandingMethodBlocks) || // 896
|
|
_.last(self._outstandingMethodBlocks).wait) // 897
|
|
self._outstandingMethodBlocks.push({wait: false, methods: []}); // 898
|
|
_.last(self._outstandingMethodBlocks).methods.push(methodInvoker); // 899
|
|
} // 900
|
|
// 901
|
|
// If we added it to the first block, send it out now. // 902
|
|
if (self._outstandingMethodBlocks.length === 1) // 903
|
|
methodInvoker.sendMessage(); // 904
|
|
// 905
|
|
// If we're using the default callback on the server, // 906
|
|
// block waiting for the result. // 907
|
|
if (future) { // 908
|
|
return future.wait(); // 909
|
|
} // 910
|
|
return options.returnStubValue ? stubReturnValue : undefined; // 911
|
|
}, // 912
|
|
// 913
|
|
// Before calling a method stub, prepare all stores to track changes and allow // 914
|
|
// _retrieveAndStoreOriginals to get the original versions of changed // 915
|
|
// documents. // 916
|
|
_saveOriginals: function () { // 917
|
|
var self = this; // 918
|
|
_.each(self._stores, function (s) { // 919
|
|
s.saveOriginals(); // 920
|
|
}); // 921
|
|
}, // 922
|
|
// Retrieves the original versions of all documents modified by the stub for // 923
|
|
// method 'methodId' from all stores and saves them to _serverDocuments (keyed // 924
|
|
// by document) and _documentsWrittenByStub (keyed by method ID). // 925
|
|
_retrieveAndStoreOriginals: function (methodId) { // 926
|
|
var self = this; // 927
|
|
if (self._documentsWrittenByStub[methodId]) // 928
|
|
throw new Error("Duplicate methodId in _retrieveAndStoreOriginals"); // 929
|
|
// 930
|
|
var docsWritten = []; // 931
|
|
_.each(self._stores, function (s, collection) { // 932
|
|
var originals = s.retrieveOriginals(); // 933
|
|
// not all stores define retrieveOriginals // 934
|
|
if (!originals) // 935
|
|
return; // 936
|
|
originals.forEach(function (doc, id) { // 937
|
|
docsWritten.push({collection: collection, id: id}); // 938
|
|
if (!_.has(self._serverDocuments, collection)) // 939
|
|
self._serverDocuments[collection] = new MongoIDMap; // 940
|
|
var serverDoc = self._serverDocuments[collection].setDefault(id, {}); // 941
|
|
if (serverDoc.writtenByStubs) { // 942
|
|
// We're not the first stub to write this doc. Just add our method ID // 943
|
|
// to the record. // 944
|
|
serverDoc.writtenByStubs[methodId] = true; // 945
|
|
} else { // 946
|
|
// First stub! Save the original value and our method ID. // 947
|
|
serverDoc.document = doc; // 948
|
|
serverDoc.flushCallbacks = []; // 949
|
|
serverDoc.writtenByStubs = {}; // 950
|
|
serverDoc.writtenByStubs[methodId] = true; // 951
|
|
} // 952
|
|
}); // 953
|
|
}); // 954
|
|
if (!_.isEmpty(docsWritten)) { // 955
|
|
self._documentsWrittenByStub[methodId] = docsWritten; // 956
|
|
} // 957
|
|
}, // 958
|
|
// 959
|
|
// This is very much a private function we use to make the tests // 960
|
|
// take up fewer server resources after they complete. // 961
|
|
_unsubscribeAll: function () { // 962
|
|
var self = this; // 963
|
|
_.each(_.clone(self._subscriptions), function (sub, id) { // 964
|
|
// Avoid killing the autoupdate subscription so that developers // 965
|
|
// still get hot code pushes when writing tests. // 966
|
|
// // 967
|
|
// XXX it's a hack to encode knowledge about autoupdate here, // 968
|
|
// but it doesn't seem worth it yet to have a special API for // 969
|
|
// subscriptions to preserve after unit tests. // 970
|
|
if (sub.name !== 'meteor_autoupdate_clientVersions') { // 971
|
|
self._subscriptions[id].stop(); // 972
|
|
} // 973
|
|
}); // 974
|
|
}, // 975
|
|
// 976
|
|
// Sends the DDP stringification of the given message object // 977
|
|
_send: function (obj) { // 978
|
|
var self = this; // 979
|
|
self._stream.send(DDPCommon.stringifyDDP(obj)); // 980
|
|
}, // 981
|
|
// 982
|
|
// We detected via DDP-level heartbeats that we've lost the // 983
|
|
// connection. Unlike `disconnect` or `close`, a lost connection // 984
|
|
// will be automatically retried. // 985
|
|
_lostConnection: function (error) { // 986
|
|
var self = this; // 987
|
|
self._stream._lostConnection(error); // 988
|
|
}, // 989
|
|
// 990
|
|
/** // 991
|
|
* @summary Get the current connection status. A reactive data source. // 992
|
|
* @locus Client // 993
|
|
* @memberOf Meteor // 994
|
|
*/ // 995
|
|
status: function (/*passthrough args*/) { // 996
|
|
var self = this; // 997
|
|
return self._stream.status.apply(self._stream, arguments); // 998
|
|
}, // 999
|
|
// 1000
|
|
/** // 1001
|
|
* @summary Force an immediate reconnection attempt if the client is not connected to the server. // 1002
|
|
// 1003
|
|
This method does nothing if the client is already connected. // 1004
|
|
* @locus Client // 1005
|
|
* @memberOf Meteor // 1006
|
|
*/ // 1007
|
|
reconnect: function (/*passthrough args*/) { // 1008
|
|
var self = this; // 1009
|
|
return self._stream.reconnect.apply(self._stream, arguments); // 1010
|
|
}, // 1011
|
|
// 1012
|
|
/** // 1013
|
|
* @summary Disconnect the client from the server. // 1014
|
|
* @locus Client // 1015
|
|
* @memberOf Meteor // 1016
|
|
*/ // 1017
|
|
disconnect: function (/*passthrough args*/) { // 1018
|
|
var self = this; // 1019
|
|
return self._stream.disconnect.apply(self._stream, arguments); // 1020
|
|
}, // 1021
|
|
// 1022
|
|
close: function () { // 1023
|
|
var self = this; // 1024
|
|
return self._stream.disconnect({_permanent: true}); // 1025
|
|
}, // 1026
|
|
// 1027
|
|
/// // 1028
|
|
/// Reactive user system // 1029
|
|
/// // 1030
|
|
userId: function () { // 1031
|
|
var self = this; // 1032
|
|
if (self._userIdDeps) // 1033
|
|
self._userIdDeps.depend(); // 1034
|
|
return self._userId; // 1035
|
|
}, // 1036
|
|
// 1037
|
|
setUserId: function (userId) { // 1038
|
|
var self = this; // 1039
|
|
// Avoid invalidating dependents if setUserId is called with current value. // 1040
|
|
if (self._userId === userId) // 1041
|
|
return; // 1042
|
|
self._userId = userId; // 1043
|
|
if (self._userIdDeps) // 1044
|
|
self._userIdDeps.changed(); // 1045
|
|
}, // 1046
|
|
// 1047
|
|
// Returns true if we are in a state after reconnect of waiting for subs to be // 1048
|
|
// revived or early methods to finish their data, or we are waiting for a // 1049
|
|
// "wait" method to finish. // 1050
|
|
_waitingForQuiescence: function () { // 1051
|
|
var self = this; // 1052
|
|
return (! _.isEmpty(self._subsBeingRevived) || // 1053
|
|
! _.isEmpty(self._methodsBlockingQuiescence)); // 1054
|
|
}, // 1055
|
|
// 1056
|
|
// Returns true if any method whose message has been sent to the server has // 1057
|
|
// not yet invoked its user callback. // 1058
|
|
_anyMethodsAreOutstanding: function () { // 1059
|
|
var self = this; // 1060
|
|
return _.any(_.pluck(self._methodInvokers, 'sentMessage')); // 1061
|
|
}, // 1062
|
|
// 1063
|
|
_livedata_connected: function (msg) { // 1064
|
|
var self = this; // 1065
|
|
// 1066
|
|
if (self._version !== 'pre1' && self._heartbeatInterval !== 0) { // 1067
|
|
self._heartbeat = new DDPCommon.Heartbeat({ // 1068
|
|
heartbeatInterval: self._heartbeatInterval, // 1069
|
|
heartbeatTimeout: self._heartbeatTimeout, // 1070
|
|
onTimeout: function () { // 1071
|
|
self._lostConnection( // 1072
|
|
new DDP.ConnectionError("DDP heartbeat timed out")); // 1073
|
|
}, // 1074
|
|
sendPing: function () { // 1075
|
|
self._send({msg: 'ping'}); // 1076
|
|
} // 1077
|
|
}); // 1078
|
|
self._heartbeat.start(); // 1079
|
|
} // 1080
|
|
// 1081
|
|
// If this is a reconnect, we'll have to reset all stores. // 1082
|
|
if (self._lastSessionId) // 1083
|
|
self._resetStores = true; // 1084
|
|
// 1085
|
|
if (typeof (msg.session) === "string") { // 1086
|
|
var reconnectedToPreviousSession = (self._lastSessionId === msg.session); // 1087
|
|
self._lastSessionId = msg.session; // 1088
|
|
} // 1089
|
|
// 1090
|
|
if (reconnectedToPreviousSession) { // 1091
|
|
// Successful reconnection -- pick up where we left off. Note that right // 1092
|
|
// now, this never happens: the server never connects us to a previous // 1093
|
|
// session, because DDP doesn't provide enough data for the server to know // 1094
|
|
// what messages the client has processed. We need to improve DDP to make // 1095
|
|
// this possible, at which point we'll probably need more code here. // 1096
|
|
return; // 1097
|
|
} // 1098
|
|
// 1099
|
|
// Server doesn't have our data any more. Re-sync a new session. // 1100
|
|
// 1101
|
|
// Forget about messages we were buffering for unknown collections. They'll // 1102
|
|
// be resent if still relevant. // 1103
|
|
self._updatesForUnknownStores = {}; // 1104
|
|
// 1105
|
|
if (self._resetStores) { // 1106
|
|
// Forget about the effects of stubs. We'll be resetting all collections // 1107
|
|
// anyway. // 1108
|
|
self._documentsWrittenByStub = {}; // 1109
|
|
self._serverDocuments = {}; // 1110
|
|
} // 1111
|
|
// 1112
|
|
// Clear _afterUpdateCallbacks. // 1113
|
|
self._afterUpdateCallbacks = []; // 1114
|
|
// 1115
|
|
// Mark all named subscriptions which are ready (ie, we already called the // 1116
|
|
// ready callback) as needing to be revived. // 1117
|
|
// XXX We should also block reconnect quiescence until unnamed subscriptions // 1118
|
|
// (eg, autopublish) are done re-publishing to avoid flicker! // 1119
|
|
self._subsBeingRevived = {}; // 1120
|
|
_.each(self._subscriptions, function (sub, id) { // 1121
|
|
if (sub.ready) // 1122
|
|
self._subsBeingRevived[id] = true; // 1123
|
|
}); // 1124
|
|
// 1125
|
|
// Arrange for "half-finished" methods to have their callbacks run, and // 1126
|
|
// track methods that were sent on this connection so that we don't // 1127
|
|
// quiesce until they are all done. // 1128
|
|
// // 1129
|
|
// Start by clearing _methodsBlockingQuiescence: methods sent before // 1130
|
|
// reconnect don't matter, and any "wait" methods sent on the new connection // 1131
|
|
// that we drop here will be restored by the loop below. // 1132
|
|
self._methodsBlockingQuiescence = {}; // 1133
|
|
if (self._resetStores) { // 1134
|
|
_.each(self._methodInvokers, function (invoker) { // 1135
|
|
if (invoker.gotResult()) { // 1136
|
|
// This method already got its result, but it didn't call its callback // 1137
|
|
// because its data didn't become visible. We did not resend the // 1138
|
|
// method RPC. We'll call its callback when we get a full quiesce, // 1139
|
|
// since that's as close as we'll get to "data must be visible". // 1140
|
|
self._afterUpdateCallbacks.push(_.bind(invoker.dataVisible, invoker)); // 1141
|
|
} else if (invoker.sentMessage) { // 1142
|
|
// This method has been sent on this connection (maybe as a resend // 1143
|
|
// from the last connection, maybe from onReconnect, maybe just very // 1144
|
|
// quickly before processing the connected message). // 1145
|
|
// // 1146
|
|
// We don't need to do anything special to ensure its callbacks get // 1147
|
|
// called, but we'll count it as a method which is preventing // 1148
|
|
// reconnect quiescence. (eg, it might be a login method that was run // 1149
|
|
// from onReconnect, and we don't want to see flicker by seeing a // 1150
|
|
// logged-out state.) // 1151
|
|
self._methodsBlockingQuiescence[invoker.methodId] = true; // 1152
|
|
} // 1153
|
|
}); // 1154
|
|
} // 1155
|
|
// 1156
|
|
self._messagesBufferedUntilQuiescence = []; // 1157
|
|
// 1158
|
|
// If we're not waiting on any methods or subs, we can reset the stores and // 1159
|
|
// call the callbacks immediately. // 1160
|
|
if (!self._waitingForQuiescence()) { // 1161
|
|
if (self._resetStores) { // 1162
|
|
_.each(self._stores, function (s) { // 1163
|
|
s.beginUpdate(0, true); // 1164
|
|
s.endUpdate(); // 1165
|
|
}); // 1166
|
|
self._resetStores = false; // 1167
|
|
} // 1168
|
|
self._runAfterUpdateCallbacks(); // 1169
|
|
} // 1170
|
|
}, // 1171
|
|
// 1172
|
|
// 1173
|
|
_processOneDataMessage: function (msg, updates) { // 1174
|
|
var self = this; // 1175
|
|
// Using underscore here so as not to need to capitalize. // 1176
|
|
self['_process_' + msg.msg](msg, updates); // 1177
|
|
}, // 1178
|
|
// 1179
|
|
// 1180
|
|
_livedata_data: function (msg) { // 1181
|
|
var self = this; // 1182
|
|
// 1183
|
|
// collection name -> array of messages // 1184
|
|
var updates = {}; // 1185
|
|
// 1186
|
|
if (self._waitingForQuiescence()) { // 1187
|
|
self._messagesBufferedUntilQuiescence.push(msg); // 1188
|
|
// 1189
|
|
if (msg.msg === "nosub") // 1190
|
|
delete self._subsBeingRevived[msg.id]; // 1191
|
|
// 1192
|
|
_.each(msg.subs || [], function (subId) { // 1193
|
|
delete self._subsBeingRevived[subId]; // 1194
|
|
}); // 1195
|
|
_.each(msg.methods || [], function (methodId) { // 1196
|
|
delete self._methodsBlockingQuiescence[methodId]; // 1197
|
|
}); // 1198
|
|
// 1199
|
|
if (self._waitingForQuiescence()) // 1200
|
|
return; // 1201
|
|
// 1202
|
|
// No methods or subs are blocking quiescence! // 1203
|
|
// We'll now process and all of our buffered messages, reset all stores, // 1204
|
|
// and apply them all at once. // 1205
|
|
_.each(self._messagesBufferedUntilQuiescence, function (bufferedMsg) { // 1206
|
|
self._processOneDataMessage(bufferedMsg, updates); // 1207
|
|
}); // 1208
|
|
self._messagesBufferedUntilQuiescence = []; // 1209
|
|
} else { // 1210
|
|
self._processOneDataMessage(msg, updates); // 1211
|
|
} // 1212
|
|
// 1213
|
|
if (self._resetStores || !_.isEmpty(updates)) { // 1214
|
|
// Begin a transactional update of each store. // 1215
|
|
_.each(self._stores, function (s, storeName) { // 1216
|
|
s.beginUpdate(_.has(updates, storeName) ? updates[storeName].length : 0, // 1217
|
|
self._resetStores); // 1218
|
|
}); // 1219
|
|
self._resetStores = false; // 1220
|
|
// 1221
|
|
_.each(updates, function (updateMessages, storeName) { // 1222
|
|
var store = self._stores[storeName]; // 1223
|
|
if (store) { // 1224
|
|
_.each(updateMessages, function (updateMessage) { // 1225
|
|
store.update(updateMessage); // 1226
|
|
}); // 1227
|
|
} else { // 1228
|
|
// Nobody's listening for this data. Queue it up until // 1229
|
|
// someone wants it. // 1230
|
|
// XXX memory use will grow without bound if you forget to // 1231
|
|
// create a collection or just don't care about it... going // 1232
|
|
// to have to do something about that. // 1233
|
|
if (!_.has(self._updatesForUnknownStores, storeName)) // 1234
|
|
self._updatesForUnknownStores[storeName] = []; // 1235
|
|
Array.prototype.push.apply(self._updatesForUnknownStores[storeName], // 1236
|
|
updateMessages); // 1237
|
|
} // 1238
|
|
}); // 1239
|
|
// 1240
|
|
// End update transaction. // 1241
|
|
_.each(self._stores, function (s) { s.endUpdate(); }); // 1242
|
|
} // 1243
|
|
// 1244
|
|
self._runAfterUpdateCallbacks(); // 1245
|
|
}, // 1246
|
|
// 1247
|
|
// Call any callbacks deferred with _runWhenAllServerDocsAreFlushed whose // 1248
|
|
// relevant docs have been flushed, as well as dataVisible callbacks at // 1249
|
|
// reconnect-quiescence time. // 1250
|
|
_runAfterUpdateCallbacks: function () { // 1251
|
|
var self = this; // 1252
|
|
var callbacks = self._afterUpdateCallbacks; // 1253
|
|
self._afterUpdateCallbacks = []; // 1254
|
|
_.each(callbacks, function (c) { // 1255
|
|
c(); // 1256
|
|
}); // 1257
|
|
}, // 1258
|
|
// 1259
|
|
_pushUpdate: function (updates, collection, msg) { // 1260
|
|
var self = this; // 1261
|
|
if (!_.has(updates, collection)) { // 1262
|
|
updates[collection] = []; // 1263
|
|
} // 1264
|
|
updates[collection].push(msg); // 1265
|
|
}, // 1266
|
|
// 1267
|
|
_getServerDoc: function (collection, id) { // 1268
|
|
var self = this; // 1269
|
|
if (!_.has(self._serverDocuments, collection)) // 1270
|
|
return null; // 1271
|
|
var serverDocsForCollection = self._serverDocuments[collection]; // 1272
|
|
return serverDocsForCollection.get(id) || null; // 1273
|
|
}, // 1274
|
|
// 1275
|
|
_process_added: function (msg, updates) { // 1276
|
|
var self = this; // 1277
|
|
var id = MongoID.idParse(msg.id); // 1278
|
|
var serverDoc = self._getServerDoc(msg.collection, id); // 1279
|
|
if (serverDoc) { // 1280
|
|
// Some outstanding stub wrote here. // 1281
|
|
var isExisting = (serverDoc.document !== undefined); // 1282
|
|
// 1283
|
|
serverDoc.document = msg.fields || {}; // 1284
|
|
serverDoc.document._id = id; // 1285
|
|
// 1286
|
|
if (self._resetStores) { // 1287
|
|
// During reconnect the server is sending adds for existing ids. // 1288
|
|
// Always push an update so that document stays in the store after // 1289
|
|
// reset. Use current version of the document for this update, so // 1290
|
|
// that stub-written values are preserved. // 1291
|
|
var currentDoc = self._stores[msg.collection].getDoc(msg.id); // 1292
|
|
if (currentDoc !== undefined) // 1293
|
|
msg.fields = currentDoc; // 1294
|
|
// 1295
|
|
self._pushUpdate(updates, msg.collection, msg); // 1296
|
|
} else if (isExisting) { // 1297
|
|
throw new Error("Server sent add for existing id: " + msg.id); // 1298
|
|
} // 1299
|
|
} else { // 1300
|
|
self._pushUpdate(updates, msg.collection, msg); // 1301
|
|
} // 1302
|
|
}, // 1303
|
|
// 1304
|
|
_process_changed: function (msg, updates) { // 1305
|
|
var self = this; // 1306
|
|
var serverDoc = self._getServerDoc( // 1307
|
|
msg.collection, MongoID.idParse(msg.id)); // 1308
|
|
if (serverDoc) { // 1309
|
|
if (serverDoc.document === undefined) // 1310
|
|
throw new Error("Server sent changed for nonexisting id: " + msg.id); // 1311
|
|
DiffSequence.applyChanges(serverDoc.document, msg.fields); // 1312
|
|
} else { // 1313
|
|
self._pushUpdate(updates, msg.collection, msg); // 1314
|
|
} // 1315
|
|
}, // 1316
|
|
// 1317
|
|
_process_removed: function (msg, updates) { // 1318
|
|
var self = this; // 1319
|
|
var serverDoc = self._getServerDoc( // 1320
|
|
msg.collection, MongoID.idParse(msg.id)); // 1321
|
|
if (serverDoc) { // 1322
|
|
// Some outstanding stub wrote here. // 1323
|
|
if (serverDoc.document === undefined) // 1324
|
|
throw new Error("Server sent removed for nonexisting id:" + msg.id); // 1325
|
|
serverDoc.document = undefined; // 1326
|
|
} else { // 1327
|
|
self._pushUpdate(updates, msg.collection, { // 1328
|
|
msg: 'removed', // 1329
|
|
collection: msg.collection, // 1330
|
|
id: msg.id // 1331
|
|
}); // 1332
|
|
} // 1333
|
|
}, // 1334
|
|
// 1335
|
|
_process_updated: function (msg, updates) { // 1336
|
|
var self = this; // 1337
|
|
// Process "method done" messages. // 1338
|
|
_.each(msg.methods, function (methodId) { // 1339
|
|
_.each(self._documentsWrittenByStub[methodId], function (written) { // 1340
|
|
var serverDoc = self._getServerDoc(written.collection, written.id); // 1341
|
|
if (!serverDoc) // 1342
|
|
throw new Error("Lost serverDoc for " + JSON.stringify(written)); // 1343
|
|
if (!serverDoc.writtenByStubs[methodId]) // 1344
|
|
throw new Error("Doc " + JSON.stringify(written) + // 1345
|
|
" not written by method " + methodId); // 1346
|
|
delete serverDoc.writtenByStubs[methodId]; // 1347
|
|
if (_.isEmpty(serverDoc.writtenByStubs)) { // 1348
|
|
// All methods whose stubs wrote this method have completed! We can // 1349
|
|
// now copy the saved document to the database (reverting the stub's // 1350
|
|
// change if the server did not write to this object, or applying the // 1351
|
|
// server's writes if it did). // 1352
|
|
// 1353
|
|
// This is a fake ddp 'replace' message. It's just for talking // 1354
|
|
// between livedata connections and minimongo. (We have to stringify // 1355
|
|
// the ID because it's supposed to look like a wire message.) // 1356
|
|
self._pushUpdate(updates, written.collection, { // 1357
|
|
msg: 'replace', // 1358
|
|
id: MongoID.idStringify(written.id), // 1359
|
|
replace: serverDoc.document // 1360
|
|
}); // 1361
|
|
// Call all flush callbacks. // 1362
|
|
_.each(serverDoc.flushCallbacks, function (c) { // 1363
|
|
c(); // 1364
|
|
}); // 1365
|
|
// 1366
|
|
// Delete this completed serverDocument. Don't bother to GC empty // 1367
|
|
// IdMaps inside self._serverDocuments, since there probably aren't // 1368
|
|
// many collections and they'll be written repeatedly. // 1369
|
|
self._serverDocuments[written.collection].remove(written.id); // 1370
|
|
} // 1371
|
|
}); // 1372
|
|
delete self._documentsWrittenByStub[methodId]; // 1373
|
|
// 1374
|
|
// We want to call the data-written callback, but we can't do so until all // 1375
|
|
// currently buffered messages are flushed. // 1376
|
|
var callbackInvoker = self._methodInvokers[methodId]; // 1377
|
|
if (!callbackInvoker) // 1378
|
|
throw new Error("No callback invoker for method " + methodId); // 1379
|
|
self._runWhenAllServerDocsAreFlushed( // 1380
|
|
_.bind(callbackInvoker.dataVisible, callbackInvoker)); // 1381
|
|
}); // 1382
|
|
}, // 1383
|
|
// 1384
|
|
_process_ready: function (msg, updates) { // 1385
|
|
var self = this; // 1386
|
|
// Process "sub ready" messages. "sub ready" messages don't take effect // 1387
|
|
// until all current server documents have been flushed to the local // 1388
|
|
// database. We can use a write fence to implement this. // 1389
|
|
_.each(msg.subs, function (subId) { // 1390
|
|
self._runWhenAllServerDocsAreFlushed(function () { // 1391
|
|
var subRecord = self._subscriptions[subId]; // 1392
|
|
// Did we already unsubscribe? // 1393
|
|
if (!subRecord) // 1394
|
|
return; // 1395
|
|
// Did we already receive a ready message? (Oops!) // 1396
|
|
if (subRecord.ready) // 1397
|
|
return; // 1398
|
|
subRecord.ready = true; // 1399
|
|
subRecord.readyCallback && subRecord.readyCallback(); // 1400
|
|
subRecord.readyDeps.changed(); // 1401
|
|
}); // 1402
|
|
}); // 1403
|
|
}, // 1404
|
|
// 1405
|
|
// Ensures that "f" will be called after all documents currently in // 1406
|
|
// _serverDocuments have been written to the local cache. f will not be called // 1407
|
|
// if the connection is lost before then! // 1408
|
|
_runWhenAllServerDocsAreFlushed: function (f) { // 1409
|
|
var self = this; // 1410
|
|
var runFAfterUpdates = function () { // 1411
|
|
self._afterUpdateCallbacks.push(f); // 1412
|
|
}; // 1413
|
|
var unflushedServerDocCount = 0; // 1414
|
|
var onServerDocFlush = function () { // 1415
|
|
--unflushedServerDocCount; // 1416
|
|
if (unflushedServerDocCount === 0) { // 1417
|
|
// This was the last doc to flush! Arrange to run f after the updates // 1418
|
|
// have been applied. // 1419
|
|
runFAfterUpdates(); // 1420
|
|
} // 1421
|
|
}; // 1422
|
|
_.each(self._serverDocuments, function (collectionDocs) { // 1423
|
|
collectionDocs.forEach(function (serverDoc) { // 1424
|
|
var writtenByStubForAMethodWithSentMessage = _.any( // 1425
|
|
serverDoc.writtenByStubs, function (dummy, methodId) { // 1426
|
|
var invoker = self._methodInvokers[methodId]; // 1427
|
|
return invoker && invoker.sentMessage; // 1428
|
|
}); // 1429
|
|
if (writtenByStubForAMethodWithSentMessage) { // 1430
|
|
++unflushedServerDocCount; // 1431
|
|
serverDoc.flushCallbacks.push(onServerDocFlush); // 1432
|
|
} // 1433
|
|
}); // 1434
|
|
}); // 1435
|
|
if (unflushedServerDocCount === 0) { // 1436
|
|
// There aren't any buffered docs --- we can call f as soon as the current // 1437
|
|
// round of updates is applied! // 1438
|
|
runFAfterUpdates(); // 1439
|
|
} // 1440
|
|
}, // 1441
|
|
// 1442
|
|
_livedata_nosub: function (msg) { // 1443
|
|
var self = this; // 1444
|
|
// 1445
|
|
// First pass it through _livedata_data, which only uses it to help get // 1446
|
|
// towards quiescence. // 1447
|
|
self._livedata_data(msg); // 1448
|
|
// 1449
|
|
// Do the rest of our processing immediately, with no // 1450
|
|
// buffering-until-quiescence. // 1451
|
|
// 1452
|
|
// we weren't subbed anyway, or we initiated the unsub. // 1453
|
|
if (!_.has(self._subscriptions, msg.id)) // 1454
|
|
return; // 1455
|
|
// 1456
|
|
// XXX COMPAT WITH 1.0.3.1 #errorCallback // 1457
|
|
var errorCallback = self._subscriptions[msg.id].errorCallback; // 1458
|
|
var stopCallback = self._subscriptions[msg.id].stopCallback; // 1459
|
|
// 1460
|
|
self._subscriptions[msg.id].remove(); // 1461
|
|
// 1462
|
|
var meteorErrorFromMsg = function (msgArg) { // 1463
|
|
return msgArg && msgArg.error && new Meteor.Error( // 1464
|
|
msgArg.error.error, msgArg.error.reason, msgArg.error.details); // 1465
|
|
} // 1466
|
|
// 1467
|
|
// XXX COMPAT WITH 1.0.3.1 #errorCallback // 1468
|
|
if (errorCallback && msg.error) { // 1469
|
|
errorCallback(meteorErrorFromMsg(msg)); // 1470
|
|
} // 1471
|
|
// 1472
|
|
if (stopCallback) { // 1473
|
|
stopCallback(meteorErrorFromMsg(msg)); // 1474
|
|
} // 1475
|
|
}, // 1476
|
|
// 1477
|
|
_process_nosub: function () { // 1478
|
|
// This is called as part of the "buffer until quiescence" process, but // 1479
|
|
// nosub's effect is always immediate. It only goes in the buffer at all // 1480
|
|
// because it's possible for a nosub to be the thing that triggers // 1481
|
|
// quiescence, if we were waiting for a sub to be revived and it dies // 1482
|
|
// instead. // 1483
|
|
}, // 1484
|
|
// 1485
|
|
_livedata_result: function (msg) { // 1486
|
|
// id, result or error. error has error (code), reason, details // 1487
|
|
// 1488
|
|
var self = this; // 1489
|
|
// 1490
|
|
// find the outstanding request // 1491
|
|
// should be O(1) in nearly all realistic use cases // 1492
|
|
if (_.isEmpty(self._outstandingMethodBlocks)) { // 1493
|
|
Meteor._debug("Received method result but no methods outstanding"); // 1494
|
|
return; // 1495
|
|
} // 1496
|
|
var currentMethodBlock = self._outstandingMethodBlocks[0].methods; // 1497
|
|
var m; // 1498
|
|
for (var i = 0; i < currentMethodBlock.length; i++) { // 1499
|
|
m = currentMethodBlock[i]; // 1500
|
|
if (m.methodId === msg.id) // 1501
|
|
break; // 1502
|
|
} // 1503
|
|
// 1504
|
|
if (!m) { // 1505
|
|
Meteor._debug("Can't match method response to original method call", msg); // 1506
|
|
return; // 1507
|
|
} // 1508
|
|
// 1509
|
|
// Remove from current method block. This may leave the block empty, but we // 1510
|
|
// don't move on to the next block until the callback has been delivered, in // 1511
|
|
// _outstandingMethodFinished. // 1512
|
|
currentMethodBlock.splice(i, 1); // 1513
|
|
// 1514
|
|
if (_.has(msg, 'error')) { // 1515
|
|
m.receiveResult(new Meteor.Error( // 1516
|
|
msg.error.error, msg.error.reason, // 1517
|
|
msg.error.details)); // 1518
|
|
} else { // 1519
|
|
// msg.result may be undefined if the method didn't return a // 1520
|
|
// value // 1521
|
|
m.receiveResult(undefined, msg.result); // 1522
|
|
} // 1523
|
|
}, // 1524
|
|
// 1525
|
|
// Called by MethodInvoker after a method's callback is invoked. If this was // 1526
|
|
// the last outstanding method in the current block, runs the next block. If // 1527
|
|
// there are no more methods, consider accepting a hot code push. // 1528
|
|
_outstandingMethodFinished: function () { // 1529
|
|
var self = this; // 1530
|
|
if (self._anyMethodsAreOutstanding()) // 1531
|
|
return; // 1532
|
|
// 1533
|
|
// No methods are outstanding. This should mean that the first block of // 1534
|
|
// methods is empty. (Or it might not exist, if this was a method that // 1535
|
|
// half-finished before disconnect/reconnect.) // 1536
|
|
if (! _.isEmpty(self._outstandingMethodBlocks)) { // 1537
|
|
var firstBlock = self._outstandingMethodBlocks.shift(); // 1538
|
|
if (! _.isEmpty(firstBlock.methods)) // 1539
|
|
throw new Error("No methods outstanding but nonempty block: " + // 1540
|
|
JSON.stringify(firstBlock)); // 1541
|
|
// 1542
|
|
// Send the outstanding methods now in the first block. // 1543
|
|
if (!_.isEmpty(self._outstandingMethodBlocks)) // 1544
|
|
self._sendOutstandingMethods(); // 1545
|
|
} // 1546
|
|
// 1547
|
|
// Maybe accept a hot code push. // 1548
|
|
self._maybeMigrate(); // 1549
|
|
}, // 1550
|
|
// 1551
|
|
// Sends messages for all the methods in the first block in // 1552
|
|
// _outstandingMethodBlocks. // 1553
|
|
_sendOutstandingMethods: function() { // 1554
|
|
var self = this; // 1555
|
|
if (_.isEmpty(self._outstandingMethodBlocks)) // 1556
|
|
return; // 1557
|
|
_.each(self._outstandingMethodBlocks[0].methods, function (m) { // 1558
|
|
m.sendMessage(); // 1559
|
|
}); // 1560
|
|
}, // 1561
|
|
// 1562
|
|
_livedata_error: function (msg) { // 1563
|
|
Meteor._debug("Received error from server: ", msg.reason); // 1564
|
|
if (msg.offendingMessage) // 1565
|
|
Meteor._debug("For: ", msg.offendingMessage); // 1566
|
|
}, // 1567
|
|
// 1568
|
|
_callOnReconnectAndSendAppropriateOutstandingMethods: function() { // 1569
|
|
var self = this; // 1570
|
|
var oldOutstandingMethodBlocks = self._outstandingMethodBlocks; // 1571
|
|
self._outstandingMethodBlocks = []; // 1572
|
|
// 1573
|
|
self.onReconnect(); // 1574
|
|
// 1575
|
|
if (_.isEmpty(oldOutstandingMethodBlocks)) // 1576
|
|
return; // 1577
|
|
// 1578
|
|
// We have at least one block worth of old outstanding methods to try // 1579
|
|
// again. First: did onReconnect actually send anything? If not, we just // 1580
|
|
// restore all outstanding methods and run the first block. // 1581
|
|
if (_.isEmpty(self._outstandingMethodBlocks)) { // 1582
|
|
self._outstandingMethodBlocks = oldOutstandingMethodBlocks; // 1583
|
|
self._sendOutstandingMethods(); // 1584
|
|
return; // 1585
|
|
} // 1586
|
|
// 1587
|
|
// OK, there are blocks on both sides. Special case: merge the last block of // 1588
|
|
// the reconnect methods with the first block of the original methods, if // 1589
|
|
// neither of them are "wait" blocks. // 1590
|
|
if (!_.last(self._outstandingMethodBlocks).wait && // 1591
|
|
!oldOutstandingMethodBlocks[0].wait) { // 1592
|
|
_.each(oldOutstandingMethodBlocks[0].methods, function (m) { // 1593
|
|
_.last(self._outstandingMethodBlocks).methods.push(m); // 1594
|
|
// 1595
|
|
// If this "last block" is also the first block, send the message. // 1596
|
|
if (self._outstandingMethodBlocks.length === 1) // 1597
|
|
m.sendMessage(); // 1598
|
|
}); // 1599
|
|
// 1600
|
|
oldOutstandingMethodBlocks.shift(); // 1601
|
|
} // 1602
|
|
// 1603
|
|
// Now add the rest of the original blocks on. // 1604
|
|
_.each(oldOutstandingMethodBlocks, function (block) { // 1605
|
|
self._outstandingMethodBlocks.push(block); // 1606
|
|
}); // 1607
|
|
}, // 1608
|
|
// 1609
|
|
// We can accept a hot code push if there are no methods in flight. // 1610
|
|
_readyToMigrate: function() { // 1611
|
|
var self = this; // 1612
|
|
return _.isEmpty(self._methodInvokers); // 1613
|
|
}, // 1614
|
|
// 1615
|
|
// If we were blocking a migration, see if it's now possible to continue. // 1616
|
|
// Call whenever the set of outstanding/blocked methods shrinks. // 1617
|
|
_maybeMigrate: function () { // 1618
|
|
var self = this; // 1619
|
|
if (self._retryMigrate && self._readyToMigrate()) { // 1620
|
|
self._retryMigrate(); // 1621
|
|
self._retryMigrate = null; // 1622
|
|
} // 1623
|
|
} // 1624
|
|
}); // 1625
|
|
// 1626
|
|
LivedataTest.Connection = Connection; // 1627
|
|
// 1628
|
|
// @param url {String} URL to Meteor app, // 1629
|
|
// e.g.: // 1630
|
|
// "subdomain.meteor.com", // 1631
|
|
// "http://subdomain.meteor.com", // 1632
|
|
// "/", // 1633
|
|
// "ddp+sockjs://ddp--****-foo.meteor.com/sockjs" // 1634
|
|
// 1635
|
|
/** // 1636
|
|
* @summary Connect to the server of a different Meteor application to subscribe to its document sets and invoke its remote methods.
|
|
* @locus Anywhere // 1638
|
|
* @param {String} url The URL of another Meteor application. // 1639
|
|
*/ // 1640
|
|
DDP.connect = function (url, options) { // 1641
|
|
var ret = new Connection(url, options); // 1642
|
|
allConnections.push(ret); // hack. see below. // 1643
|
|
return ret; // 1644
|
|
}; // 1645
|
|
// 1646
|
|
// Hack for `spiderable` package: a way to see if the page is done // 1647
|
|
// loading all the data it needs. // 1648
|
|
// // 1649
|
|
allConnections = []; // 1650
|
|
DDP._allSubscriptionsReady = function () { // 1651
|
|
return _.all(allConnections, function (conn) { // 1652
|
|
return _.all(conn._subscriptions, function (sub) { // 1653
|
|
return sub.ready; // 1654
|
|
}); // 1655
|
|
}); // 1656
|
|
}; // 1657
|
|
// 1658
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
}).call(this);
|
|
|
|
|
|
/* Exports */
|
|
if (typeof Package === 'undefined') Package = {};
|
|
Package['ddp-client'] = {
|
|
DDP: DDP,
|
|
LivedataTest: LivedataTest
|
|
};
|
|
|
|
})();
|
|
|
|
//# sourceMappingURL=ddp-client.js.map
|