Hi,
That is indeed a clever solution. What I wound up doing was more of a hack, but it works. Adding the audit-argument-checks package causes Match._failIfArgumentsAreNotAllChecked to be called before any server method invocation. So, I redefine this function serverside, like this:
Meteor.startup(function() {
// attempt to avoid multiple identical calls to server functions
var serverCallIds = [];
Match._failIfArgumentsAreNotAllChecked = function(f, context, args, description) {
if (context.isSimulation == true || context.isSimulation == false) {
// this indicates a server call (not a publish call)
if (args.length > 0 && typeof args[0].serverCallId != "undefined") {
var serverCallId = args[0].serverCallId;
if (serverCallIds.includes(serverCallId)) { // check serverCallId
throw new Meteor.Error("already called");
} else {
serverCallIds.push(serverCallId);
while (serverCallIds.length > 1000) {
serverCallIds.splice(0, 1);
}
args.splice(0, 1);
}
}
}
console.log("execute server call");
return f.apply(context, args);
};
});
Then I call all server method through my own serverCall on the client side, like this:
serverCall : function(functionName, args) {
var callback = typeof arguments[arguments.length - 1] == "function" ? arguments[arguments.length - 1] : null;
var functionArgs = [ functionName, {
serverCallId : functionName + "-" + Math.floor(Math.random() * 10000000000000000)
} ];
for ( var i in arguments) {
if (i >= 1 && i < arguments.length - (callback ? 1 : 0)) {
functionArgs.push(arguments[i]);
}
}
functionArgs.push(function(err, result) {
if (err) {
if (err.error == "already called") {
RavenLogger.log("avoided redundant call to " + functionName);
} else {
// show an error message
RavenLogger.log(new Error(err));
Materialize.toast(TAPi18n.__("server_error") + " : " + err, 4000);
}
} else if (callback) {
callback(result);
}
});
Meteor.call.apply(this, functionArgs);
},
The point of all this is to avoid redundant calls to the same function. I had tried by using Meteor.apply with the noRetry flag, like this:
serverCallNoRetry : function(functionName, args) {
var callback = typeof arguments[arguments.length - 1] == "function" ? arguments[arguments.length - 1] : null;
var functionArgs = [ functionName ];
var functionArgsArr = [];
for ( var i in arguments) {
if (i >= 1 && i < arguments.length - (callback ? 1 : 0)) {
functionArgsArr.push(arguments[i]);
}
}
functionArgs.push(functionArgsArr);
functionArgs.push({
noRetry : true
});
functionArgs.push(function(err, result) {
if (err) {
RavenLogger.log(new Error("call to " + functionName + " failed : " + err));
} else if (callback) {
callback(result);
}
});
Meteor.apply.apply(this, functionArgs);
},
… but that rather failed due to the fact that retries are actually quite necessary, it turns out. With noRetry set to true, many server calls just fail completely. So, it seems that what I really need is a way to retry, and yet avoid executing the server code multiple times. In order to accomplish this, I attach a serverCallId to each invocation, which I store and check to make sure that only the first attempt that gets through will be executed.
The issue we are seeing (maybe I should have led with that…) is on mobile devices. When the internet connection is lost, it seems that the client continues trying server calls. When the connection is restored, all these queued requests go through, and they are all executed. If the server call inserts data in Mongo, then we see multiple identical entries, in one case 379 of them!
This effect is hard to produce. Apparently I still don’t understand the circumstances very well, as I can’t make it happen in on the desktop at all, requiring a full mobile build and deployment in order to test… very tedious indeed!
Any thoughts on this dilemma will be much appreciated.