I’ve been pondering separating out some of the heavier processes in an application I’ve built into a submodule (or series of submodules) - 90% of what I want to do is directly expose the methods/publications of the submodule to the client, without requiring they connect to (or even know about the existance of) a submodule.
I looked at some existing implementations, primarily https://medium.com/@rkstar/m-m-meteor-microservices-world-tour-part-2-3-a-m-2260df4f72d7. However, apart from not looking quite correct, it also looks like it would be exceptionally memory heavy for the main server - as it would maintain a copy of any document sent to the client.
The approach I’m working on (for publications at least) is to create a connection on the main server to the submodule, one per client connection that requires access to the submodule (created lazily upon first request), and then basically just forwarding all subscription related messages from that connection directly back to the client.
I’m interested in seeing if anyone has worked with a similar (or better?) approach before, and what the potential problems are here. I’m aware that the number of connections from server -> microservice could be large, but I’m not particularly worried about that - particularly if the only other solution is having the main server maintain copies of documents for a mergebox.
A cutdown example is here:
//service.js
export default class DDPService {
constructor(ddpAddress) {
this.ddpAddress = ddpAddress;
...
this.connections = {};
}
registerConsumerConnection(connection) {
const service = this;
if (!this.connections[connection.id]) {
service.connections[connection.id] = {
upstream: DDP.connect(this.ddpAddress),
subscriptions: {},
methods: {}
};
this.connections[connection.id].upstream.onMessage = function onMessage(rawMessage) {
const serviceInstance = service.connections[connection.id];
if (!serviceInstance) {
Meteor._debug("downstream connection doesn't exist any more");
this.close();
}
try {
const msg = DDPCommon.parseDDP(rawMessage);
if (msg.msg === "added" || msg.msg === "removed" || msg.msg === "changed" || msg.msg === "nosub") {
Meteor.default_server.sessions[connection.id].send(msg);
}
else if (serviceInstance.subscriptions[msg.id]) {
msg.id = serviceInstance.subscriptions[msg.id];
Meteor.default_server.sessions[connection.id].send(msg);
}
else {//just for debugging
console.log(msg);
}
}
catch (e) {
Meteor._debug("Exception while parsing DDP", e);
}
};
delete this.connections[connection.id].upstream._stream.eventCallbacks.messsage;
this.connections[connection.id].upstream._stream.on(
"message",
Meteor.bindEnvironment(this.connections[connection.id].upstream.onMessage.bind(this.connections[connection.id].upstream))
);
connection.onClose(() => {
this.connections[connection.id].upstream.close();
delete this.connections[connection.id];
});
}
return this.connections[connection.id];
}
methods(methodDefs) {
...
}
publish(localName, remoteName, beforeCall) {
const service = this;
Meteor.publish(localName, function publishHandler(...args) {
const publishContext = this;
if (beforeCall) {
beforeCall.call(publishContext, ...args);
}
const serviceInstance = service.registerConsumerConnection(publishContext.connection, publishContext.userId);
if (!serviceInstance) {
throw new Meteor.Error(500, "No upstream connection");
}
const sub = serviceInstance.upstream.subscribe.apply(serviceInstance.upstream, [remoteName, ...args, {
onReady() {
publishContext.ready();
},
onStop() {
publishContext.stop();
}
}]);
serviceInstance.subscriptions[sub.subscriptionId] = this._subscriptionId;
publishContext.onStop(() => {
delete serviceInstance.subscriptions[sub.subscriptionId];
sub.stop();
});
});
}
}
It would then be used as such, assuming a remotePubName
publication is available in the microservice:
//my-service.js
const MyService = new DDPService("someurl");
MyService.publish("localPubName","remotePubName", function checkBeforeCalling(...args) {
//am I logged in/validate args
});