Refactoring Tales - Clusterfun - Don't forget()
In which I describe a refactor in Clusterfun and learn that there is really no substitute for having someone else actually try out your APIs.
By: TheHans255
8/27/2023
I thought it might be nice today to document the lifecycle of a small
refactor I made to the Clusterfun communications protocol
recently. In that article, I mentioned that one of the pain points
I found in the design revolved around the call to forget()
for fire-and-forget messages:
Instead of a separate
forget()
method on requests, there should likely be a different API onSessionHelper
for sending fire and forget messages. Therequest()
-then-forget()
pattern causes a response listener to be set up and then torn down unnecessarily.
As it turns out, I was right about this! And for more reasons than one.
Another developer on Clusterfun recently tried to port in a app from a very
early version of the codebase, before the new messaging system was implemented,
and when he saw that he needed to use forget()
for these fire-and-forget
messages, he expressed that it would end up being something he forgot to do
and would thus end up with a lot of errors.
We discussed a few ideas back and forth, and we ended up landing on adding
a separate API for sending fire-and-forget messages - if you use request()
,
you'll get a reply back, and if you use sendMessage()
, you won't. The code
snippet for sending basic messages now looks like this:
// Send a request with default retry logic, await its result
const oneRequest = sessionHelper.request(myEndpoint, clients[0], data);
const response = await oneRequest;
// Send another request and respond using .then()
const anotherRequest = sessionHelper.request(myEndpoint, clients[1], data);
let shouldResend = true;
anotherRequest.then(response => {
shouldResend = false;
// do stuff with response
})
// ... wait some time, then ...
if (shouldResend) {
anotherRequest.resend();
}
// Send a third request and immediately forget it
sessionHelper.sendMessage(myEndpoint, clients[2], data);
Of course, the TypeScript magic that makes the request methods and endpoints so useful also comes in handy here. The new type signatures for these APIs look like this:
export class SessionHelper {
...
/**
* Makes a request to a given receiver, automatically
* retrying and timing out as needed
* @param endpoint An object describing the route to request on
* @param receiverId The ID of the receiver to send to
* @param request The request data to send
* @returns A Promise-like object resolving to the response
created by the recipient.
*/
request<REQUEST, RESPONSE>(
endpoint: MessageEndpoint<REQUEST, RESPONSE>,
receiverId: string,
request: REQUEST
): ClusterfunRequest<REQUEST, RESPONSE> {
// ...
}
/**
* Sends a fire-and-forget message to a given receiver
* @param endpoint An object describing the route to request on
* @param receiverId The ID of the receiver to send to
* @param message The message data to send
*/
sendMessage<MESSAGE>(
endpoint: MessageEndpoint<MESSAGE, void>,
receiverId: string,
message: MESSAGE
): void {
// ...
}
}
Specifically, fire-and-forget messages use the same MessageEndpoint objects
as requests, but TypeScript will mandate that the endpoints are not returning
any useful information from them. While request()
can still be used for
these void
-returning endpoints (to know when the message has been successfully
processed or to catch any errors), sendMessage()
cannot be used for
endpoints that actually return values.
With those changes, the other dev had a much nicer time porting in the app. Now, RetroSpectro, a sprint retrospective tool, is now available on Clusterfun.tv!