preface

Everyone knows what an asynchronous singleton is.

Asynchronous singleton: it takes a certain amount of time to create an instance. During the creation, the execution right is handed over. After the creation, the execution right is taken back and the result is returned.

Some people might say, that’s it. The rest will be done in a minute. That’s right. No one is irreplaceable.

The main expression here is a programming idea that can change the style of code and solve problems beautifully in certain situations. One more tool, one more choice.

AsyncInsCreator takes 2 seconds to create an object; GetAsyncIns encapsulates the asynchronous object acquisition process. We call getAsyncIns multiple times and we get the same object.

async function asyncInsCreator() {
    await delay(2000).run();
    return new Object(a); }function getAsyncIns() {
    returnfactory(asyncInsCreator); }; (async function test() {
    try {  
        const [ins1, ins2, ins3] = await Promise.all([
            getAsyncIns(),
            getAsyncIns(),
            getAsyncIns()
        ]);

        console.log("ins1:", ins1);  // ins1: {}
        console.log("ins1===ins2", ins1 === ins2); // ins1===ins2 true
        console.log("ins2===ins3", ins2 === ins3); // ins2===ins3 true
        console.log("ins3=== ins1", ins3 === ins1); // ins3=== ins1 true
    } catch (err) {
        console.log("err", err);
    }
})();
Copy the code

Applicable scenario

Asynchronous singleton

Such as initializing the socket. IO client, indexedDB, and so on

It’s just a one-time situation

As an example, we can register multiple Load events

   window.addEventListener("load".function () {
        // other code
        console.log("load 1");
   });

   window.addEventListener("load".function () {
        // other code
        console.log("load 2");
  });
Copy the code

If you were using React or Vue, you’d have to subscribe and unsubscribe, which would be a hassle. Of course, you could use a new layer of subscription publishing ideas:

Is it pleasing to the eye if it’s as follows:

   await loaded();
   // TODO::  
Copy the code

You must say, this I will:

   function loaded() {
        return new Promise((resove, reject) = > {
            window.addEventListener("load", resove)
        });
    }
Copy the code

I’ll give you a test code: Loaded 1, not Loaded 2. The reason: The load event only fires once.

    function loaded() {
        return new Promise((resolve, reject) = > {
            window.addEventListener("load".() = > resolve(null));
        });
    }

    async function test() {
        await loaded();
        console.log("loaded 1");
        
        setTimeout(async() = > {await loaded();
            console.log("loaded 2");
        }, 1000)
    }

   test();
Copy the code

At this point, our asynchronous singleton can show off, even though it doesn’t mean to do this, but it can because it satisfies the condition of only doing it once.

Let’s look at the code that uses the asynchronous singleton pattern: Loaded 1 and Loaded 2 both arrive as expected.

        const factory = asyncFactory();

        function asyncInsCreator() {
            return new Promise((resove, reject) = > {
                window.addEventListener("load")}); }function loaded() {
            return factory(asyncInsCreator)
        }

        async function test() {
            await loaded();
            console.log("loaded 1");  // loaded 1

            setTimeout(async() = > {await loaded();
                console.log("loaded 2"); // loaded 2
            }, 1000)
        }

        test();
Copy the code

Implementation approach

state

For instance creation, there are only two simple states:

  1. In the create
  2. Is created

The difficulty is that while it is being created, there are new requests to get the instance.

Then we need a queue or array to maintain the request queue, wait for the instance to be created, and then notify the requester.

If the instantiation is complete, then simply return the instance.

variable

We need three variables here:

  1. instance

Stores the created instances

  1. initializing

Yes No Creating

  1. requests

To save the requests that are being created

Utility methods

Delay: delays calling the specified function for a certain period of time. For later timeouts, and analog delays.

export function delay(delay: number = 5000, fn = () => { }, context = null) {
    let ticket = null;
    return {
        run(. args: any[]) {
            return new Promise((resolve, reject) = > {
                ticket = setTimeout(async() = > {try {
                        const res = await fn.apply(context, args);
                        resolve(res);
                    } catch (err) {
                        reject(err);
                    }
                }, delay);
            });
        },
        cancel: () = > {
            clearTimeout(ticket); }}; };Copy the code

Basic version

The implementation code

Note:

  1. instance ! == undefined

This is best used in a one-time scenario to determine whether or not it is instantiated, that is, it can be null. So this is also a limitation, what if I just return undefined? I’m going to keep silent. Some people might tease me, you said earlier that undefined is unreliable, I smiled, do you find it charming?

  1. After the failureinitializing = false

The intent is that when an initialization fails, all previous requests will be notified that they failed. In subsequent requests, initialization will also be attempted.

import { delay } from ".. /util";

function asyncFactory() {
    let requests = [];
    let instance;
    let initializing = false;

    return function initiator(fn: (... args: any) =>Promise<any>) {
         // The instance is already instantiated
         if(instance ! = =undefined) {return Promise.resolve(instance);
        }
        // Initialization
        if (initializing) {
            return new Promise((resolve, reject) = > {
                // Save the request
                requests.push({
                    resolve,
                    reject
                });
            })
        }
        initializing = true;
        return new Promise((resolve, reject) = > {
            // Save the request
            requests.push({
                resolve,
                reject
            });

            fn()
                .then(result= > {
                    instance = result;
                    initializing = false;
                    processRequests('resolve', instance);
                })
                .catch(error= > {
                    initializing = false;
                    processRequests('reject', error);
                });
        });
    }
    function processRequests(type: "resolve" | "reject", value: any) {
        / / each resolve
        requests.forEach(q= > {
            q[type](value);
        });
        // Set the request to null, and then use instance directlyrequests = []; }}Copy the code

The test code

const factory = asyncFactory();

async function asyncInsCreator() {
    await delay(2000).run();
    return new Object(a); }function getAsyncIns() {
    returnfactory(asyncInsCreator); }; (async function test() {
    try {  

        const [ins1, ins2, ins3] = await Promise.all([
            getAsyncIns(),
            getAsyncIns(),
            getAsyncIns()
        ]);

        console.log("ins1:", ins1);  // ins1: {}
        console.log("ins1===ins2", ins1 === ins2); // ins1===ins2 true
        console.log("ins2===ins3", ins2 === ins3); // ins2===ins3 true
        console.log("ins3=== ins1", ins3 === ins1); // ins3=== ins1 true
    } catch (err) {
        console.log("err", err);
    }

})();
Copy the code

Existing problems:

There’s no way to pass arguments, there’s no way to set the context for this.

Pass parameter version

Implementation ideas:

  1. Increase the parametercontextAs well asargsparameter
  2. Function.prototype.appy

The implementation code

import { delay } from ".. /util";

interface AVFunction<T = unknown> {
    (value: T): void
}

function asyncFactory<R = unknown.RR = unknown> () {
    let requests: { reject: AVFunction<RR>, resolve: AVFunction<R> }[] = [];
    let instance: R;
    let initializing = false;

    return function initiator(fn: (... args: any) =>Promise<R>, context: unknown, ... args: unknown[]) :Promise<R> {
        // The instance is already instantiated
        if(instance ! = =undefined) {return Promise.resolve(instance);
        }
        // Initialization
        if (initializing) {
            return new Promise((resolve, reject) = > {
                requests.push({
                    resolve,
                    reject
                })
            })
        }
        initializing = true
        return new Promise((resolve, reject) = > {
            requests.push({
                resolve,
                reject
            })

            fn.apply(context, args)
                .then(res= > {
                    instance = res;
                    initializing = false;
                    processRequests('resolve', instance);
                })
                .catch(error= > {
                    initializing = false;
                    processRequests('reject', error); })})}function processRequests(type: "resolve" | "reject", value: any) {
        / / each resolve
        requests.forEach(q= > {
            q[type](value);
        });
        // Set the request to null, and then use instance directlyrequests = []; }}Copy the code

The test code

interface RES {
    p1: number
}

const factory = asyncFactory<RES>();

async function asyncInsCreator(opitons: unknown = {}) {
    await delay(2000).run();
    console.log("context.name".this.name);
    const result = new Object(opitons) as RES;
    return result;
}

function getAsyncIns(context: unknown, options: unknown = {}) {
    returnfactory(asyncInsCreator, context, options); }; (async function test() {

    try {
        const context = {
            name: "context"
        };

        const [ins1, ins2, ins3] = await Promise.all([
            getAsyncIns(context, { p1: 1 }),
            getAsyncIns(context, { p1: 2 }),
            getAsyncIns(context, { p1: 3})]);console.log("ins1:", ins1, ins1.p1);
        console.log("ins1=== ins2", ins1 === ins2);
        console.log("ins2=== ins3", ins2 === ins3);
        console.log("ins3=== ins1", ins3 === ins1);
    } catch (err) {
        console.log("err", err);
    }

})();
Copy the code

Existing problems

It looks perfect, but what if it runs out of time?

People who think of this problem, article section post, I give you a thumbs up.

Timeout version

Here we need to borrow our tool method delay:

  • If the timeout does not succeed, all requests are notified of failure.
  • Otherwise, all requests are notified of success.

The implementation code

import { delay } from ".. /util";

interface AVFunction<T = unknown> {
    (value: T): void
}

function asyncFactory<R = unknown.RR = unknown> (timeout: number = 5 * 1000) {
    let requests: { reject: AVFunction<RR>, resolve: AVFunction<R> }[] = [];
    let instance: R;
    let initializing = false;

    return function initiator(fn: (... args: any) =>Promise<R>, context: unknown, ... args: unknown[]) :Promise<R> {

        // The instance is already instantiated
        if(instance ! = =undefined) {return Promise.resolve(instance);
        }

        // Initialization
        if (initializing) {
            return new Promise((resolve, reject) = > {
                requests.push({
                    resolve,
                    reject
                })
            })
        }

        initializing = true
        return new Promise((resolve, reject) = > {

            requests.push({
                resolve,
                reject
            })

            const { run, cancel } = delay(timeout);

            run().then(() = > {
                const error = new Error("Operation timeout");
                processRequests("reject", error);
            });

            fn.apply(context, args)
                .then(res= > {
                    // Initialization succeeded
                    cancel();
                    instance = res;
                    initializing = false;
                    processRequests('resolve', instance);
                })
                .catch(error= > {
                    // Initialization failed
                    cancel();
                    initializing = false;
                    processRequests('reject', error); })})}function processRequests(type: "resolve" | "reject", value: any) {
        / / each resolve
        requests.forEach(q= > {
            q[type](value);
        });
        // Set the request to null, and then use instance directly
        requests = [];
    }
}

interface RES {
    p1: number
}
const factory = asyncFactory<RES>();

async function asyncInsCreator(opitons: unknown = {}) {
    await delay(1000).run();
    console.log("context.name".this.name);
    const result = new Object(opitons) as RES;
    return result;
}

function getAsyncIns(context: unknown, options: unknown = {}) {
    returnfactory(asyncInsCreator, context, options); }; (async function test() {

    try {
        const context = {
            name: "context"
        };

        const [instance1, instance2, instance3] = await Promise.all([
            getAsyncIns(context, { p1: 1 }),
            getAsyncIns(context, { p1: 2 }),
            getAsyncIns(context, { p1: 3})]);console.log("instance1:", instance1, instance1.p1);
        console.log("instance1=== instance2", instance1 === instance2);
        console.log("instance2=== instance3", instance2 === instance3);
        console.log("instance3=== instance1", instance3 === instance1);
    } catch (err) {
        console.log("err", err);
    }
})();

Copy the code

The test code

When asyncInsCreator delay(1000) is changed to delay(6000), it creates an event 6000ms larger than the asyncFactory default of 5000ms, it throws the following exception.

At c:\projects-github\juejinBlogs\ asynchronous singletons \queue\args_timeout.ts:40:31Copy the code
interface RES {
    p1: number
}

const factory = asyncFactory<RES>();


async function asyncInsCreator(opitons: unknown = {}) {
    await delay(1000).run();
    console.log("context.name".this.name);
    const result = new Object(opitons) as RES;
    return result;
}

function getAsyncIns(context: unknown, options: unknown = {}) {
    returnfactory(asyncInsCreator, context, options); }; (async function test() {
    try {
        const context = {
            name: "context"
        };
        const [ins1, ins2, ins3] = await Promise.all([
            getAsyncIns(context, { p1: 1 }),
            getAsyncIns(context, { p1: 2 }),
            getAsyncIns(context, { p1: 3})]);console.log("ins1:", ins1, ins1.p1);
        console.log("ins1=== ins2", ins1 === ins2);
        console.log("ins2=== ins3", ins2 === ins3);
        console.log("ins3=== ins1", ins3 === ins1);
    } catch (err) {
        console.log("err", err);
    }
})();
Copy the code

Existing problems

Existing problems:

  1. Error is raisedNew Error(" operation timeout ")

We simply throw this exception, and when the enclosing try/catch catches it, there is no way to tell the source of the error. We can also block an AsyncFactoryError, or the asyncInsCreator can throw a certain value and let the try/catch recognize it for itself.

  1. No judgment parameterfn

If fn is not a valid function, does it return a Promise? It’s easy to tell if it’s a valid function. After execution is not return a Promise, borrow the giant P-IS-promise shoulder to lean on.

// The core code
function isPromise(value) {
   return value instanceof Promise ||
   	(
   		isObject(value) &&
   		typeof value.then === 'function' &&
   		typeof value.catch === 'function'
   	);
}
Copy the code

There’s a problem, and you don’t solve it? Don’t solve, wait for you to do it.

A version of the subscription-based publishing pattern

Here’s another way of thinking about implementation, using subscription publishers.

The main points of

By listening on the EventEmitter event on the Promise, here, because you only need to listen once, once shines.

new Promise((resolve, reject) = > {
    emitter.once("initialized".() = > {
        resolve(instance);
    });
    emitter.once("error".(error) = > {
        reject(error);
    });
});
Copy the code

The implementation code

Here is a basic version of the implementation, as for the context, parameters, timeout version, you can try their own implementation.

import { EventEmitter } from "events";
import { delay } from "./util";

function asyncFactory<R = any> () {
    let emitter = new EventEmitter();
    let instance: any = null;
    let initializing = false;

    return function getAsyncInstance(factory: () => Promise<R>) :Promise<R> {
        // Initialization is complete
        if(instance ! = =undefined) {return Promise.resolve(instance);
        }
        // Initialization
        if (initializing === true) {
            return new Promise((resolve, reject) = > {
                emitter.once("initialized".() = > {
                    resolve(instance);
                });
                emitter.once("error".(error) = > {
                    reject(error);
                });
            });
        }

        initializing = true;
        return new Promise((resolve, reject) = > {
            emitter.once("initialized".() = > {
                resolve(instance);
            });
            emitter.once("error".(error) = > {
                reject(error);
            });
            factory()
                .then(ins= > {
                    instance = ins;
                    initializing = false;
                    emitter.emit("initialized");
                    emitter = null;
                })
                .catch((error) = > {
                    initializing = false;
                    emitter.emit("error", error); }); }}})Copy the code

conclusion

Asynchronous singletons are rare, but here’s the idea of turning event-based programming into promise-based programming. There are also some design patterns that you can apply, that you can put into actual code, that you can solve problems, that you can make money, that’s what we’re looking for.

Write in the last

Writing is not easy, your praise and comment is my motivation.

async-init

Is it impossible to create a reliable async singleton pattern in JavaScript?

Creating an async singletone in javascript