TL; DR;
- On web development it is common to have the need for asynchronous initialization
- The singleton pattern allows us to keep only one instance of a class
- Mixing up singleton and promises is a good solution but might be tricky based on how promises behave
- An npm library 'single-promise' take care of the possible pitfalls
What is the singleton pattern and why do we need it
The singleton pattern is a pattern where a class can only have one "single" instance. On web development this is very common as many of the objects that the browser exposes are singleton (e.g.: console or window).
When writing a web application, you'll probably have your own singleton objects for holding credentials, tokens, configurations, etc.
The easiest way to write a singleton class in javascript, is to have a class that exposes only static methods and has only static properties.
class Configuration {
static _configuration = {};
static loadConfiguration() {
// do something
}
static saveConfiguration(newConfig) {
// do another thing
}
}
Working with promises
As in web development a lot happens asynchronously, Promises were introduced in javascript to workaround the need of creating loads of callbacks. In the beginning there was:
const xhr = new XMLHttpRequest();
xhr.open(method, url);
xhr.onload = myLoadFunction;
xhr.onerror = myErrorFunction;
xhr.send();
Then the promises came:
fetch(method, url)
.then((response) => { // do something with the response })
.catch((reason) => { // do something with the reason});
With promises the code is more linear and one can chain promises. But still, there is the need for creating sub-functions. The await/async keywords came to make the code more linear:
try {
await fetch(method, url);
// do something with the response
} catch (reason) {
// do something with the reason
}
It is important to note that the async/await are just helpers and that behind the scenes the code is still asynchronous and fetch is still returning a promise
Singleton promises
Singleton promises come in hand when one needs to call only once an initialization promise. When writing my new connect the dots maker game, I had the need to initialize a game instance to later send the game updates. These were all remote calls. They take long and can fail. Imagine a class that like the one below:
class GameHandler {
async initializeGame(params) {
const response = await fetch(
"example.com/rest/api/startGame",
{ body: JSON.stringify(params), method: "POST" }
);
const obj = await response.json();
this.myGameId = obj.gameId;
}
async updateGame(progress, finished) {
const params = { id: this.myGameId, progress: progress, finished: finished };
const response = await fetch(
"example.com/rest/api/updateGame",
{ body: JSON.stringify(params), method: "POST" }
);
const obj = await response.json();
if (finished) {
this.myScore = obj.score;
}
}
}
Purposely I didn't add any error handler. Depending on how often updateGame is called and on the network conditions, many things can go wrong. The first thing we want to be sure is that any calls to the updateGame can only be done if startGame returned a value.
This is a good candidate for a singleton promise. We could have something like this. For the sake of simplicity I moved the calls to the fetch API to some other method not in the example
class GameHandler {
static startGamePromise;
async static initializeGame(params) {
if (GameHandler.startGamePromise) {
// the game already started
return GameHandler.startGamePromise;
}
// Create a promise and save it on a static variable
GameHandler.startGamePromise =
new Promise((resolve, reject) => async {
try {
GameHandler.myGameId = await GameHandler.callStart(params);
resolve();
}
catch (e) {
reject(e);
}
});
}
async updateGame(progress, finished) {
if (!GameHandler.startGamePromise) {
throw new Error("Game didn't start");
}
// Make sure that the game has been initialized
await GameHandler.startGamePromise;
// Call the update game API
const = await GameHandler.callUpdate(progress, finished);
if (finished) {
this.myScore = obj.score;
}
}
(...)
Now we have only one singleton promise ensuring that the startGame is called once and only once and that calls to updateGame must wait for it to finish.
But, there is something really good and evil about promises. They save their state. Meaning that, if the startGamePromise finished successfully, subsequent calls for
await GameHandler.startGamePromise;
won't generate an API call and won't need to wait.
This is also the biggest pitfall when creating singleton promises. If the promise fails, it will return an error every time it is called.
In the example above, if for some reason the startGame call fails, all the subsequent calls to updateGame will fail and the user won't have any score, even if it was just a glitch on the network.
Summing everything up, a robust singleton promise implementation must:
- Have only one instance (be singleton ;) )
- Be called only once
- Save the status in case of success and do not resolve the promise again
- Retry in case of failure
I put all this conditions in a very simple npm package single-promise. It covers all the above requirements.
You can find the source code here: https://github.com/bmarotta/single-promise
Top comments (0)