tstl-singleton

Asynchronous Singleton generator through TSTL

Usage no npm install needed!

<script type="module">
  import tstlSingleton from 'https://cdn.skypack.dev/tstl-singleton';
</script>

README

TSTL-Singleton

GitHub license npm version Downloads Build Status

1. Outline

Asynchronous Singleton Generator using TSTL.

The tstl-singleton is an Asynchronous Singleton Generator, who guarantees the asynchronous lazy constructor to be called "only one at time". The "only one at time" would always be kepted, even in the race condition, through Mutex and UniqueLock who are imlemented in the TSTL.

2. Installation

Installing tstl-singleton in NodeJS is very easy. Just install with the npm.

npm install --save tstl-singleton

To use the tstl-singleton, import Singleton class and create the Singleton instance with your custom lazy constructor. After the creation, call the Singleton.get() method, then it would return the promised value with lazy construction.

import { Singleton } from "tstl-singleton";
import { Member } from "./Member";

export namespace RemoteAssets
{
    export function getMembers(): Promise<Member[]>
    {
        return members_.get();
    }
    
    const members_: Singleton<Member[]> = new Singleton(() =>
    {
        let response: Response = await fetch("https://some-domain.com/members", {
            method: "GET"
        });
        return await response.json();
    });
}

3. Usage

3.1. Basic Concepts

declare module "tstl-singleton"
{
    export class Singleton<T>
    {
        /**
         * Create singleton generator with the *lazy constructor*.
         */
        public constructor(getter: () => Promise<T>);
        
        /**
         * Get the *lazy constructed* value.
         */
        public get(): Promise<T>;

        /**
         * Reload value by calling the *lazy constructor* forcibly.
         */
        public reload(): Promise<T>;
    }
}

Using the tstl-singleton is also easy, too. Create a Singleton instance with your custom lazy constructor and get the promised value thorugh the Singleton.get() method. The Singleton.get() method would construct the return value following below logics:

  • At the first time: calls the lazy constructor and returns the value.
  • At the lazy construction: returns the pre-constructed value.
  • When race condition:
    • simultaneously call happens during the lazy construction.
    • guarantees the "only one at time" through a mutex.

If you want to reload the promised value, regardless of whether the lazy construction has been completed or not, call the Singleton.reload() method. It would call the lazy constructor forcibly, even if the lazy construction has been completed in sometime.

3.2. Demonstration

As I've mentioned, tstl-singleton always ensures the lazy constructor to be called "only one at time". As you can see from the below example code, even simultaneous call on the Singleton.get() has been occcured, the lazy constructor would be called "only one at time".

import { Singleton } from "tstl-singleton";
import { randint } from "tstl/algorithm/random";
import { sleep_for } from "tstl/thread/global";

async function main(): Promise<void>
{
    // GENERATE SINGLETON WITH LAZY-CONSTRUCTOR 
    let index: number = 0;
    let singleton: Singleton<number> = new Singleton(async () =>
    {
        await sleep_for(randint(50, 500));
        return ++index;
    });

    // RACE CONDITIO: SIMULTANEOUS ACCESS
    let promises: Promise<number>[] = [];
    for (let i: number = 0; i < 5; ++i)
        promises.push( singleton.get() );
    
    // OUTPUT: ALL ELEMENTS MUST BE 1
    console.log(await Promise.all(promises));

    // RELOAD: WOULD BE 2
    console.log(await singleton.reload());
}
1 1 1 1 1
2

3.3. Sample Case

Loading remote data from the external API would be a heavy work for the remote server. Therefore, it would better to call the external API, only when it requires. In such reason, loading remote data from the external API can be the best use case for the tstl-singleton.

With the lazy constructor, you can call the external API only when you need it. Also, you can avoid the vulnerable API callings by using the Singleton.get() method in the race condition, which helps you to call the external API "only one at time".

Look at the below code and feel what the "only one at time" means:

import { Singleton } from "tstl-singleton";

import { Company } from "./Company";
import { Member } from "./Member";

export namespace RemoteAssets
{
    export function getCompanies(): Promise<Company[]> 
    {
        return companies_.get();
    }
    export function getMembers(): Promise<Member[]>
    {
        return members_.get();
    }

    async function fetchItems<Entity>(path: string): Promise<Entity[]>
    {
        let url: string = `https://some-domain.com${path}`;
        let response: Response = await fetch(url, { method: "GET" });
        return await response.json();
    }
    const companies_: Singleton<Company[]> = new Singleton(() => fetchItems("/companies"));
    const members_: Singleton<Member[]> = new Singleton(() => fetchItems("/members"));
}