Refactoring Tales - Clusterfun - Don't forget()

by: TheHans255

August 28, 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 on SessionHelper for sending fire and forget messages. The request()-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) {

// 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
        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!

Copyright © 2022-2023, TheHans255. All rights reserved.