Suppose a function may be called several times almost simultaneously and it should always return the same result.
Would you like to save computational time and execute that function only once whenever it is possible?
Sure!
Especially if it is a heavy operation like index rebuild or a file reload. Basically this is what this article.

How to save computational resources if repeated calls to function make no difference

Local idempotence

Here I am talking about some special kind of functions.
A formal definition can be:

If function F
* has no arguments
* any number of calls to F overlapping in time give same result
then F can be called locally idempotent

So once again, everything below is for locally idempotent functions only.

We have one more definition to make.

Simultaneous

Next lets define what we mean by simultaneous calls.
In Nodejs a good take is

Two events in same event loop tick can be considered simultaneous

For example simultaneous (in real world) calls to REST server will execute endpoint resolvers in same tick. This is actually the key idea to bear N+1 problem for GraphQL but that’s another story :)

Great, we are ready to write some code!

Regular function

For now we have a function F and we want it to execute its body maximum once in loop tick no matter how many times it is called.
Lets make a wrapper that returns the desired function

function makeIdempotent<Result>(f: () => Result) {
    const idempotentF = function(): ReturnType<typeof f> {
        const promiseKey = '__idempotent_promise__';
        if (idempotentF[promiseKey] == undefined) {
            idempotentF[promiseKey] = f();
            // In next tick we clear cached results
			queueMicrotask(() => {
				idempotentF[promiseKey] = undefined;
			})
        }
        return idempotentF[promiseKey];
    }
    return idempotentF;
}

Done! But what if F returns Promise? Is one tick enough?

Async function

Lets think.

Synchronous function ends in same loop tick when it is called.
So simultaneous for synchronous functions means that they start and end in same tick. Fair enough.

However asynchronous functions may end in different tick they start.
Suppose f runs for N ticks, then should we execute it again until N+1’s tick?
No, if f confirms to my definition of locally idempotent result will be same so why run it again?
Until f resolves or rejects we should receive same Promise if we call f repeatedly.

function makeIdempotent<Result>(f: () => Result) {
    const idempotentF = function(): ReturnType<typeof f> {
        const promiseKey = '__idempotent_promise__';
        if (idempotentF[promiseKey] == undefined) {
            idempotentF[promiseKey] = f();
            if (idempotentF[promiseKey] instanceof Promise) {
                // After promise is resolved we clear cached promise
                idempotentF[promiseKey].then(() => {
                    idempotentF[promiseKey] = undefined;
                }, (error) => {
                    idempotentF[promiseKey] = undefined;
                })
            } else {
                // In next tick we clear cached results
                queueMicrotask(() => {
                    idempotentF[promiseKey] = undefined;
                })
            }
        }
        return idempotentF[promiseKey];
    }
    return idempotentF;
}

Cool.
But…
What if F is a class method?

Class method

We should write a decorator that applies makeIdempotent to class method.

function MakeIdempotent(target: any, propertyName: string, descriptor: PropertyDescriptor) {
    // we use Symbol to hide the key in class instance
    const MakeIdempotentClassKey = Symbol(propertyName);

    const originalMethod = descriptor.value!;

    descriptor.value = function () {
        // We assign cached function to class instance
        // because different class instances should have their own instances of wrapped function
        if (!this[MakeIdempotentClassKey]) {
            this[MakeIdempotentClassKey] = makeIdempotent(() => originalMethod.apply(this))
        }
        return this[MakeIdempotentClassKey]();
    }
}

Finally a real life example that inspired all of this

In my case I had a class that managed search index.

class SearchIndexService {
  getIndex(): Promise<SearchIndex>
  buildIndex(): Promise<SearchIndex>
}

If index doesn’t exist getIndex calls buildIndex but nothing prevents several calls to run in parallel.
So I added a protection with @MakeIdempotent

class SearchIndexService {
  getIndex(): Promise<SearchIndex>

  @MakeIdempotent
  buildIndex(): Promise<SearchIndex>
}

Thanks for reading!