Skip to content

Commit

Permalink
Merge branch 'pr-1611' into devel
Browse files Browse the repository at this point in the history
  • Loading branch information
n1mmy committed Dec 5, 2013
2 parents bc8d745 + 6a2c952 commit b5cd66d
Show file tree
Hide file tree
Showing 36 changed files with 714 additions and 217 deletions.
5 changes: 5 additions & 0 deletions History.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
client code changes; server only code changes will not cause the page
to reload.

* Add `Meteor.onConnection` and add `this.connection` to method
invocations and publish functions. These can be used to store data
associated with individual clients between subscriptions and method
calls. See http://docs.meteor.com/#meteor_onconnection for details.

* Bundler failures cause non-zero exit code in `meteor run`. #1515

* Fix `meteor run` with settings files containing non-ASCII characters. #1497
Expand Down
47 changes: 47 additions & 0 deletions docs/client/api.html
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,8 @@ <h2 id="publishandsubscribe"><span>Publish and subscribe</span></h2>
{{> api_box subscription_error}}
{{> api_box subscription_stop}}

{{> api_box subscription_connection}}

{{> api_box subscribe}}

When you subscribe to a record set, it tells the server to send records to the
Expand Down Expand Up @@ -276,6 +278,7 @@ <h2 id="methods_header"><span>Methods</span></h2>
begin running.
* `userId`: the id of the current user.
* `setUserId`: a function that associates the current client with a user.
* `connection`: on the server, the [connection](#meteor_onconnection) this method call was received on.

Calling `methods` on the client defines *stub* functions associated with
server methods of the same name. You don't have to define a stub for
Expand Down Expand Up @@ -328,6 +331,8 @@ <h2 id="methods_header"><span>Methods</span></h2>
returns. However, you can change this by calling `this.unblock`. This
will allow the N+1th invocation to start running in a new fiber.

{{> api_box method_invocation_connection}}

{{> api_box error}}

If you want to return an error from a method, throw an exception. Methods can
Expand Down Expand Up @@ -462,6 +467,47 @@ <h2 id="connections"><span>Server connections</span></h2>
This can be used to save battery on mobile devices when real time
updates are not required.


{{> api_box onConnection}}

`onConnection` returns an object with a single method `stop`. Calling
`stop` unregisters the callback, so that this callback will no longer
be called on new connections.

The callback is called with a single argument, the server-side
`connection` representing the connection from the client. This object
contains the following fields:

<dl class="objdesc">
{{#dtdd name="id" type="String"}}
A globally unique id for this connection.
{{/dtdd}}

{{#dtdd name="close" type="Function"}}
Close this DDP connection. The client is free to reconnect, but will
receive a different connection with a new `id` if it does.
{{/dtdd}}

{{#dtdd name="onClose" type="Function"}}
Register a callback to be called when the connection is closed. If the
connection is already closed, the callback will be called immediately.
{{/dtdd}}
</dl>

{{#note}}
Currently when a client reconnects to the server (such as after
temporarily losing its Internet connection), it will get a new
connection each time. The `onConnection` callbacks will be called
again, and the new connection will have a new connection `id`.

In the future, when client reconnection is fully implemented,
reconnecting from the client will reconnect to the same connection on
the server: the `onConnection` callback won't be called for that
connection again, and the connection will still have the same
connection `id`.
{{/note}}


{{> api_box connect}}

To call methods on another Meteor application or subscribe to its data
Expand Down Expand Up @@ -496,6 +542,7 @@ <h2 id="connections"><span>Server connections</span></h2>
`Meteor.apply`, you are using a connection back to that default
server.


<h2 id="collections"><span>Collections</span></h2>

Meteor stores data in *collections*. To get started, declare a
Expand Down
27 changes: 27 additions & 0 deletions docs/client/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,14 @@ Template.api.subscription_userId = {
};


Template.api.subscription_connection = {
id: "publish_connection",
name: "<i>this</i>.connection",
locus: "Server",
descr: ["Access inside the publish function. The incoming [connection](#meteor_onconnection) for this subscription."]
};


Template.api.subscribe = {
id: "meteor_subscribe",
name: "Meteor.subscribe(name [, arg1, arg2, ... ] [, callbacks])",
Expand Down Expand Up @@ -381,6 +389,13 @@ Template.api.method_invocation_isSimulation = {
descr: ["Access inside a method invocation. Boolean value, true if this invocation is a stub."]
};

Template.api.method_invocation_connection = {
id: "method_connection",
name: "<i>this</i>.connection",
locus: "Server",
descr: ["Access inside a method invocation. The [connection](#meteor_onconnection) this method was received on. `null` if the method is not associated with a connection, eg. a server initiated method call."]
};

Template.api.error = {
id: "meteor_error",
name: "new Meteor.Error(error, reason, details)",
Expand Down Expand Up @@ -479,6 +494,18 @@ Template.api.connect = {
]
};

Template.api.onConnection = {
id: "meteor_onconnection",
name: "Meteor.onConnection(callback)",
locus: "Server",
descr: ["Register a callback to be called when a new DDP connection is made to the server."],
args: [
{name: "callback",
type: "function",
descr: "The function to call when a new DDP connection is established."}
]
};

// onAutopublish

Template.api.meteor_collection = {
Expand Down
7 changes: 5 additions & 2 deletions docs/client/docs.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,8 @@ var toc = [
{instance: "this", name: "ready", id: "publish_ready"},
{instance: "this", name: "onStop", id: "publish_onstop"},
{instance: "this", name: "error", id: "publish_error"},
{instance: "this", name: "stop", id: "publish_stop"}
{instance: "this", name: "stop", id: "publish_stop"},
{instance: "this", name: "connection", id: "publish_connection"}
],
"Meteor.subscribe"
],
Expand All @@ -132,7 +133,8 @@ var toc = [
{instance: "this", name: "userId", id: "method_userId"},
{instance: "this", name: "setUserId", id: "method_setUserId"},
{instance: "this", name: "isSimulation", id: "method_issimulation"},
{instance: "this", name: "unblock", id: "method_unblock"}
{instance: "this", name: "unblock", id: "method_unblock"},
{instance: "this", name: "connection", id: "method_connection"}
],
"Meteor.Error",
"Meteor.call",
Expand All @@ -143,6 +145,7 @@ var toc = [
"Meteor.status",
"Meteor.reconnect",
"Meteor.disconnect",
"Meteor.onConnection",
"DDP.connect"
],

Expand Down
100 changes: 94 additions & 6 deletions packages/accounts-base/accounts_server.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,14 @@ Meteor.methods({
var result = tryAllLoginHandlers(options);
if (result !== null) {
this.setUserId(result.id);
this._setLoginToken(result.token);
Accounts._setLoginToken(this.connection.id, result.token);
}
return result;
},

logout: function() {
var token = this._getLoginToken();
this._setLoginToken(null);
var token = Accounts._getLoginToken(this.connection.id);
Accounts._setLoginToken(this.connection.id, null);
if (token && this.userId)
removeLoginToken(this.userId, token);
this.setUserId(null);
Expand Down Expand Up @@ -139,11 +139,101 @@ Meteor.methods({
}
});

///
/// ACCOUNT DATA
///

// connectionId -> {connection, loginToken, srpChallenge}
var accountData = {};

Accounts._getAccountData = function (connectionId, field) {
var data = accountData[connectionId];
return data && data[field];
};

Accounts._setAccountData = function (connectionId, field, value) {
var data = accountData[connectionId];

// safety belt. shouldn't happen. accountData is set in onConnection,
// we don't have a connectionId until it is set.
if (!data)
return;

if (value === undefined)
delete data[field];
else
data[field] = value;
};

Meteor.server.onConnection(function (connection) {
accountData[connection.id] = {connection: connection};
connection.onClose(function () {
removeConnectionFromToken(connection.id);
delete accountData[connection.id];
});
});


///
/// RECONNECT TOKENS
///
/// support reconnecting using a meteor login token

// token -> list of connection ids
var connectionsByLoginToken = {};

// test hook
Accounts._getTokenConnections = function (token) {
return connectionsByLoginToken[token];
};

// Remove the connection from the list of open connections for the token.
var removeConnectionFromToken = function (connectionId) {
var token = Accounts._getLoginToken(connectionId);
if (token) {
connectionsByLoginToken[token] = _.without(
connectionsByLoginToken[token],
connectionId
);
if (_.isEmpty(connectionsByLoginToken[token]))
delete connectionsByLoginToken[token];
}
};

Accounts._getLoginToken = function (connectionId) {
return Accounts._getAccountData(connectionId, 'loginToken');
};

Accounts._setLoginToken = function (connectionId, newToken) {
removeConnectionFromToken(connectionId);

Accounts._setAccountData(connectionId, 'loginToken', newToken);

if (newToken) {
if (! _.has(connectionsByLoginToken, newToken))
connectionsByLoginToken[newToken] = [];
connectionsByLoginToken[newToken].push(connectionId);
}
};

// Close all open connections associated with any of the tokens in
// `tokens`.
var closeConnectionsForTokens = function (tokens) {
_.each(tokens, function (token) {
if (_.has(connectionsByLoginToken, token)) {
// safety belt. close should defer potentially yielding callbacks.
Meteor._noYieldsAllowed(function () {
_.each(connectionsByLoginToken[token], function (connectionId) {
var connection = Accounts._getAccountData(connectionId, 'connection');
if (connection)
connection.close();
});
});
}
});
};


// Login handler for resume tokens.
Accounts.registerLoginHandler(function(options) {
if (!options.resume)
Expand Down Expand Up @@ -646,9 +736,7 @@ Meteor.startup(function () {
///

var closeTokensForUser = function (userTokens) {
Meteor.server._closeAllForTokens(_.map(userTokens, function (token) {
return token.token;
}));
closeConnectionsForTokens(_.pluck(userTokens, "token"));
};

// Like _.difference, but uses EJSON.equals to compute which values to return.
Expand Down
22 changes: 22 additions & 0 deletions packages/accounts-base/accounts_tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -208,3 +208,25 @@ Tinytest.addAsync('accounts - expire numeric token', function (test, onComplete)
});
Accounts._expireTokens(new Date(), result.id);
});


Tinytest.addAsync(
'accounts - connection data cleaned up',
function (test, onComplete) {
makeTestConnection(
test,
function (clientConn, serverConn) {
// onClose callbacks are called in order, so we run after the
// close callback in accounts.
serverConn.onClose(function () {
test.isFalse(Accounts._getAccountData(serverConn.id, 'connection'));
onComplete();
});

test.isTrue(Accounts._getAccountData(serverConn.id, 'connection'));
serverConn.close();
},
onComplete
);
}
);
1 change: 1 addition & 0 deletions packages/accounts-base/package.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,6 @@ Package.on_test(function (api) {
api.use('accounts-base');
api.use('tinytest');
api.use('random');
api.use('test-helpers');
api.add_files('accounts_tests.js', 'server');
});
Loading

0 comments on commit b5cd66d

Please sign in to comment.