Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Async Methods with Callback #12677

Closed
wants to merge 8 commits into from
2 changes: 1 addition & 1 deletion packages/accounts-base/accounts_client_tests.js
Expand Up @@ -53,7 +53,7 @@ const removeTestUser = done => {
};

const forceEnableUser2fa = done => {
Meteor.callAsync('forceEnableUser2fa', {returnServerPromise:true}, { username }, secret2fa).then((token) => {
Meteor.callAsync("forceEnableUser2fa", { username }, secret2fa, (err, token) => {
done(token);
});
};
Expand Down
32 changes: 16 additions & 16 deletions packages/accounts-base/accounts_tests.js
Expand Up @@ -238,7 +238,7 @@ Tinytest.addAsync('accounts - login token', async (test) => {
connection = DDP.connect(Meteor.absoluteUrl());
// evil plan foiled
await test.throwsAsync(
async () => await connection.callAsync('login', { returnStubValue: true }, { resume: stolenToken2 }),
async () => await connection.callAsync('login', { resume: stolenToken2 }),
/You\'ve been logged out by the server/
);
connection.disconnect();
Expand All @@ -251,7 +251,7 @@ Tinytest.addAsync('accounts - login token', async (test) => {
const stampedToken2 = Accounts._generateStampedLoginToken();
await insertUnhashedLoginToken(userId4, stampedToken2);
connection = DDP.connect(Meteor.absoluteUrl());
await connection.callAsync('login', { returnStubValue: true }, { resume: stampedToken2.token });
await connection.callAsync('login', { resume: stampedToken2.token });
connection.disconnect();

// The token is no longer available to be stolen.
Expand Down Expand Up @@ -296,15 +296,15 @@ Tinytest.addAsync('accounts - get new token', async test => {
await Accounts._insertLoginToken(userId, stampedToken);

const conn = DDP.connect(Meteor.absoluteUrl());
await conn.callAsync('login', { returnStubValue: true }, { resume: stampedToken.token });
test.equal(await conn.callAsync('getCurrentLoginToken', { returnServerPromise: true }),
await conn.callAsync('login', { resume: stampedToken.token });
test.equal(await conn.callAsync('getCurrentLoginToken'),
Accounts._hashLoginToken(stampedToken.token));

const newTokenResult = await conn.callAsync('getNewToken', { returnServerPromise: true });
const newTokenResult = await conn.callAsync('getNewToken');
test.equal(newTokenResult.tokenExpires,
Accounts._tokenExpiration(stampedToken.when));
const token = await conn.callAsync('getCurrentLoginToken', { returnServerPromise: true });
test.equal(await conn.callAsync('getCurrentLoginToken', { returnServerPromise: true }),
const token = await conn.callAsync('getCurrentLoginToken');
test.equal(await conn.callAsync('getCurrentLoginToken'),
Accounts._hashLoginToken(newTokenResult.token));
conn.disconnect();

Expand All @@ -329,7 +329,7 @@ Tinytest.addAsync('accounts - remove other tokens', async (test) => {
await Accounts._insertLoginToken(userId, stampedTokens[i]);
const conn = DDP.connect(Meteor.absoluteUrl());
await conn.callAsync('login', { resume: stampedTokens[i].token });
test.equal(await conn.callAsync('getCurrentLoginToken', { returnServerPromise: true }),
test.equal(await conn.callAsync('getCurrentLoginToken'),
Accounts._hashLoginToken(stampedTokens[i].token));
conns.push(conn);
}
Expand All @@ -339,7 +339,7 @@ Tinytest.addAsync('accounts - remove other tokens', async (test) => {
simplePoll(async () => {
let tokens = [];
for (const conn of conns) {
tokens.push(await conn.callAsync('getCurrentLoginToken', { returnServerPromise: true }));
tokens.push(await conn.callAsync('getCurrentLoginToken'));
}
return !tokens[1] &&
tokens[0] === Accounts._hashLoginToken(stampedTokens[0].token);
Expand Down Expand Up @@ -381,18 +381,18 @@ Tinytest.addAsync(
// On a new connection, Meteor.userId() should be null until logged in.
let validateAttemptExpectedUserId = null;
const onLoginExpectedUserId = userId;
await conn.callAsync('login', { returnServerPromise: true }, { resume: stampedToken.token });
await conn.callAsync('login', { resume: stampedToken.token });

// Now that the user is logged in on the connection, Meteor.userId() should
// return that user.
validateAttemptExpectedUserId = userId;
await conn.callAsync('login', { returnServerPromise: true }, { resume: stampedToken.token });
await conn.callAsync('login', { resume: stampedToken.token });

// Trigger onLoginFailure callbacks
const onLoginFailureExpectedUserId = userId;
await test.throwsAsync(
async () =>
await conn.callAsync('login', { returnServerPromise: true }, { resume: "bogus" }), '403');
await conn.callAsync('login', { resume: "bogus" }), '403');

// Trigger onLogout callbacks
const onLogoutExpectedUserId = userId;
Expand Down Expand Up @@ -437,23 +437,23 @@ Tinytest.addAsync(

// test a new connection
let allowLogin = true;
await conn.callAsync('login', { returnServerPromise: true }, { resume: stampedToken.token });
await conn.callAsync('login', { resume: stampedToken.token });

// Now that the user is logged in on the connection, Meteor.userId() should
// return that user.
await conn.callAsync('login', { returnServerPromise: true }, { resume: stampedToken.token });
await conn.callAsync('login', { resume: stampedToken.token });

// Trigger onLoginFailure callbacks, this will not include the user object
allowLogin = 'bogus';
await test.throwsAsync(
async () =>
await conn.callAsync('login', { returnServerPromise: true }, { resume: "bogus" }), '403');
await conn.callAsync('login', { resume: "bogus" }), '403');

// test a forced login fail which WILL include the user object
allowLogin = false;
await test.throwsAsync(
async () =>
await conn.callAsync('login', { returnServerPromise: true }, { resume: stampedToken.token }), '403');
await conn.callAsync('login', { resume: stampedToken.token }), '403');

// Trigger onLogout callbacks
const onLogoutExpectedUserId = userId;
Expand Down
70 changes: 40 additions & 30 deletions packages/accounts-password/password_tests.js
Expand Up @@ -291,11 +291,14 @@ if (Meteor.isClient) (() => {
expect));
},
// Make sure the new user has not been inserted
async function (test) {
const result = await Meteor.callAsync('countUsersOnServer', { returnServerPromise: true }, {
username: this.newUsername,
});
test.equal(result, 0);
async function (test, expect) {
Meteor.callAsync(
"countUsersOnServer",
{ username: this.newUsername },
expect(function (error, result) {
test.equal(result, 0);
})
);
}
]);

Expand Down Expand Up @@ -399,11 +402,16 @@ if (Meteor.isClient) (() => {
expect));
},
// Make sure the new user has not been inserted
async function (test) {
const result = await Meteor.callAsync('countUsersOnServer', { returnServerPromise: true }, {
'emails.address': this.newEmail,
});
test.equal(result, 0);
async function (test, expect) {
Meteor.callAsync(
"countUsersOnServer",
{
"emails.address": this.newEmail,
},
expect(function (error, result) {
test.equal(result, 0);
})
);
}
]);

Expand All @@ -427,10 +435,12 @@ if (Meteor.isClient) (() => {
test.isFalse(error);
}));
},
async function (test) {
const token = await Meteor.callAsync("getResetToken", { returnServerPromise: true });
test.isTrue(token);
this.token = token;
async function (test, expect) {
Meteor.callAsync("getResetToken", expect((err, token) => {
test.isFalse(err);
test.isTrue(token);
this.token = token;
}));
},
// change password with bad old password. we stay logged in.
function (test, expect) {
Expand All @@ -452,7 +462,7 @@ if (Meteor.isClient) (() => {
loggedInAs(this.username, test, expect));
},
async function (test) {
const token = await Meteor.callAsync("getResetToken", { returnServerPromise: true });
const token = await Meteor.callAsync("getResetToken");
test.isFalse(token);
},
logoutStep,
Expand Down Expand Up @@ -771,11 +781,10 @@ if (Meteor.isClient) (() => {
test.notEqual(this.userId, null);
test.notEqual(this.userId, this.otherUserId);
// Can't update fields other than profile.
await Meteor.users
.updateAsync(this.userId, {
$set: { disallowed: true, "profile.updated": 42 },
})
.catch((err) => {
Meteor.users.updateAsync(
this.userId,
{ $set: { disallowed: true, "profile.updated": 42 } },
expect((err) => {
test.isTrue(err);
test.equal(err.error, 403);
test.isFalse(
Expand All @@ -787,18 +796,19 @@ if (Meteor.isClient) (() => {
"updated"
)
);
});
})
);
},
async function (test, expect) {
// Can't update another user.
await Meteor.users
.updateAsync(this.otherUserId, { $set: { "profile.updated": 42 } })
.catch(
expect((err) => {
test.isTrue(err);
test.equal(err.error, 403);
})
);
Meteor.users.updateAsync(
this.otherUserId,
{ $set: { "profile.updated": 42 } },
expect((err) => {
test.isTrue(err);
test.equal(err.error, 403);
})
);
},
async function (test, expect) {
// Can't update using a non-ID selector. (This one is thrown client-side.)
Expand Down Expand Up @@ -1253,7 +1263,7 @@ if (Meteor.isServer) (() => {
test.fail('observe should be gone');
})

const result = await clientConn.callAsync('login', { returnServerPromise: true }, {
const result = await clientConn.callAsync('login', {
user: { username: username },
password: hashPassword('password')
});
Expand Down
13 changes: 8 additions & 5 deletions packages/allow-deny/allow-deny.js
Expand Up @@ -596,8 +596,13 @@ CollectionPrototype._validatedRemove = function(userId, selector) {
return self._collection.remove.call(self._collection, selector);
};

CollectionPrototype._callMutatorMethodAsync = async function _callMutatorMethodAsync(name, args, options = {}) {

CollectionPrototype._callMutatorMethodAsync = async function _callMutatorMethodAsync(name, args, callback) {
if (Meteor.isClient && !callback && !alreadyInSimulation()) {
callback = function (err) {
if (err)
Meteor._debug(name + " failed", err);
};
}
// For two out of three mutator methods, the first argument is a selector
const firstArgIsSelector = name === "updateAsync" || name === "removeAsync";
if (firstArgIsSelector && !alreadyInSimulation()) {
Expand All @@ -610,9 +615,7 @@ CollectionPrototype._callMutatorMethodAsync = async function _callMutatorMethodA
const mutatorMethodName = this._prefix + name;
return this._connection.applyAsync(mutatorMethodName, args, {
returnStubValue: true,
returnServerResultPromise: true,
...options,
});
}, callback);
}

CollectionPrototype._callMutatorMethod = function _callMutatorMethod(name, args, callback) {
Expand Down
86 changes: 9 additions & 77 deletions packages/ddp-client/common/livedata_connection.js
Expand Up @@ -572,73 +572,16 @@ export class Connection {
* @returns {Promise}
*/
async callAsync(name /* .. [arguments] .. */) {

// if it's a function, the last argument is the result callback,
// not a parameter to the remote method.
const args = slice.call(arguments, 1);
let callback;
if (args.length && typeof args[args.length - 1] === 'function') {
throw new Error(
"Meteor.callAsync() does not accept a callback. You should 'await' the result, or use .then()."
);
}

const applyOptions = ['returnStubValue', 'returnServerResultPromise', 'returnServerPromise'];
const defaultOptions = {
returnServerResultPromise: true,
};
const options = {
...defaultOptions,
...(applyOptions.some(o => args[0]?.hasOwnProperty(o))
? args.shift()
: {}),
};

const invocation = DDP._CurrentCallAsyncInvocation.get();

if (invocation?.hasCallAsyncParent) {
return this.applyAsync(name, args, { ...options, isFromCallAsync: true });
callback = args.pop();
}
return this.apply(name, args, {isFromCallAsync: true, returnStubValue: true}, callback);

/*
* This is necessary because when you call a Promise.then, you're actually calling a bound function by Meteor.
*
* This is done by this code https://github.com/meteor/meteor/blob/17673c66878d3f7b1d564a4215eb0633fa679017/npm-packages/meteor-promise/promise_client.js#L1-L16. (All the logic below can be removed in the future, when we stop overwriting the
* Promise.)
*
* When you call a ".then()", like "Meteor.callAsync().then()", the global context (inside currentValues)
* will be from the call of Meteor.callAsync(), and not the context after the promise is done.
*
* This means that without this code if you call a stub inside the ".then()", this stub will act as a simulation
* and won't reach the server.
*
* Inside the function _getIsSimulation(), if isFromCallAsync is false, we continue to consider just the
* alreadyInSimulation, otherwise, isFromCallAsync is true, we also check the value of callAsyncMethodRunning (by
* calling DDP._CurrentMethodInvocation._isCallAsyncMethodRunning()).
*
* With this, if a stub is running inside a ".then()", it'll know it's not a simulation, because callAsyncMethodRunning
* will be false.
*
* DDP._CurrentMethodInvocation._set() is important because without it, if you have a code like:
*
* Meteor.callAsync("m1").then(() => {
* Meteor.callAsync("m2")
* })
*
* The call the method m2 will act as a simulation and won't reach the server. That's why we reset the context here
* before calling everything else.
*
* */
DDP._CurrentMethodInvocation._set();
DDP._CurrentMethodInvocation._setCallAsyncMethodRunning(true);
const promise = new Promise((resolve, reject) => {
DDP._CurrentCallAsyncInvocation._set({ name, hasCallAsyncParent: true });
this.applyAsync(name, args, { isFromCallAsync: true, ...options })
.then(resolve)
.catch(reject)
.finally(() => {
DDP._CurrentCallAsyncInvocation._set();
});
});
return promise.finally(() =>
DDP._CurrentMethodInvocation._setCallAsyncMethodRunning(false)
);
}

/**
Expand Down Expand Up @@ -820,11 +763,7 @@ export class Connection {
// If the caller didn't give a callback, decide what to do.
let future;
if (!callback) {
if (
Meteor.isClient &&
!options.returnServerResultPromise &&
(!options.isFromCallAsync || options.returnStubValue)
) {
if (Meteor.isClient) {
// On the client, we don't have fibers, so we can't block. The
// only thing we can do is to return undefined and discard the
// result of the RPC. If an error occurred then print the error
Expand Down Expand Up @@ -890,16 +829,9 @@ export class Connection {
// If we're using the default callback on the server,
// block waiting for the result.
if (future) {
if (options.returnServerPromise) {
return future;
}
return options.returnStubValue
? future.then(() => stubReturnValue)
: {
stubValuePromise: future,
};
return future;
}
return options.returnStubValue ? stubReturnValue : undefined;
return options.returnStubValue || options.isFromCallAsync ? stubReturnValue : undefined;
}


Expand Down