Idiomatic method client simulation opt-out

Sometimes the client simulation of Meteor methods just does not work: required data is not available locally to correctly compute the outcome, or some libraries are only available on the server (MongoInternals when implementing transactions for instance).

When such a method is called, the client gets an unhelpful error in the browser console. Even though the method eventually successfully completes.

What is the idiomatic way of short-circuiting these simulations? The goal is to minimize UI glitches.

You can have two definitions of the same method, one on the client side and other on the server side. Personally, I prefer to only define my methods server side for security/privacy and also you get the added benefit of only reloading the server whenever you make changes to your methods while your client side remains as is. Honestly, the whole methods simulation thing is not that beneficial. :man_shrugging:

The method simulation (“stub method”) is only meaningful if the method mutates subscribed data:

If you do define a stub, when a client invokes a server method it will also run its stub in parallel. On the client, the return value of a stub is ignored. Stubs are run for their side-effects: they are intended to simulate the result of what the server’s method will do, but without waiting for the round trip delay. If a stub throws an exception it will be logged to the console.

You use methods all the time, because the database mutators ( insert , update , remove ) are implemented as methods. When you call any of these functions on the client, you’re invoking their stub version that update the local cache, and sending the same write request to the server. When the server responds, the client updates the local cache with the writes that actually occurred on the server.

See Methods | Meteor API Docs

Yep. That’s the abstract idea. Running one thing on the client and another on the server. Here is my first shot at it

import {Meteor} from 'meteor/meteor';
import {EJSONable, EJSONableProperty} from 'meteor/ejson';

interface Options<
	Result extends
		| EJSONable
		| EJSONable[]
		| EJSONableProperty
		| EJSONableProperty[],
> {
	wait?: boolean | undefined;
	onResultReceived?:
		| ((error: Error | Meteor.Error | undefined, result?: Result) => void)
		| undefined;
	returnStubValue?: boolean | undefined;
	throwStubExceptions?: boolean | undefined;
}

interface Params<T> {
	name: string;
	validate: (...args: any[]) => void;
	run: (...args: any[]) => any;
	simulate?: (...args: any[]) => any;
	options?: Options<T>;
}

type Endpoint<T> = Params<T>;

const invoke = <T>(
	endpoint: Endpoint<T>,
	invocation: Partial<Meteor.MethodThisType>,
	args: any[],
) => {
	Reflect.apply(endpoint.validate, invocation, args);
	const body = (invocation.isSimulation && endpoint.simulate) || endpoint.run;
	return Reflect.apply(body, invocation, args);
};

const define = <T>(params: Params<T>): Endpoint<T> => {
	Meteor.methods({
		[params.name](...args: any[]) {
			return invoke(params, this, args);
		},
	});

	return params;
};

Not sure what you mean.

That makes perfect sense. So I guess on the client I should be optimistic about all access/rights/authorization conditionals and execute whatever would make a change in the UI.

Yes, that’s how it works.

Not sure what you mean.

I just import my methods on the server side only so there’re basically no stubs and this yields certain benefits as I illustrated above.

Personally, I prefer to only define my methods server side for security/privacy and also you get the added benefit of only reloading the server whenever you make changes to your methods while your client side remains as is

OK. I can see the benefits. Do you mean you get security by making the server code unavailable to clients? What do you mean by privacy? I can see this also prevents bloating the client bundle.

What if I want something in between? I have instances were it is easy to make optimistic UI work. How would I define server and client code in the same file for coding convenience and at the same time hint the bundler at the fact that some of the code only needs to be bundled on the client and the rest on the server. I guess tree-shaking would achieve that provided I name-export each part and name-import only the parts necessary in each bundle.

Would this branching code be split at bundling time? Are we doomed to use two distinct files?

const define = ({name, serverImplementation, clientImplementation}) => {
	if (Meteor.isServer) {
		Meteor.methods({
			[name]: serverImplementation,
		});
	}
	else {
		Meteor.methods({
			[name]: clientImplementation,
		});
	}
};

and later

define({
	name: 'myMethod',
	serverImplementation: async (...) => executeTransaction(async () => {...}),
	clientImplementation: (...) => {
		Collection.insert({...});
	},
});

OK. I can see the benefits. Do you mean you get security by making the server code unavailable to clients? What do you mean by privacy? I can see this also prevents bloating the client bundle.

Exactly what you describe. By imports your methods server side you avoid shipping them to client.

Would this branching code be split at bundling time? Are we doomed to use two distinct files?

Seems like it’d be split if tree shaking PR gets merged.