Async Singletons: A Web3 JS Story

Jeremy Tong
3 min readJul 27, 2023

While building the frontend of a DEX with one of our crypto partners, we encountered an issue trying to optimize performance using their SDK library. The SDK relied on a global DEX client class instance to make smart-contract calls, read data from the API, and more.

We would use react-query to handle many of these function calls, as these effectively composed the server-side state of the application. However, the instantiation of the global DEX client class required not only a constructor (passed user wallet info), but an initial async function loadState() to handle loading an initial state for both the DEX and the user’s info (a heavy, costly function). Working around possible anti-patterns in libraries not under one’s direct control is a common occurrence in software development.

The following code provides a simple solution to applying a singleton pattern to retrieve the same instance returned by an async function on any subsequent calls; it does so by singleton-izing both the client and the promise to retrieve it.

type MockClient = {
description: string
}

let loadedClient: MockClient | undefined;
let clientLoadingPromise: Promise<MockClient> | undefined;

async function loadClient () {
//wait 4 seconds
await new Promise((resolve) => setTimeout(resolve, 4_000))
const mockClient = { description: `I am a loaded client for key` };
return mockClient;
}

export async function getSingletonClientAsync(resetToNewClient = false): Promise<any> {
let client: any
if (!resetToNewClient && loadedClient) {
client = loadedClient
} else {
const waitingOnExistingLoad = !!clientLoadingPromise;
if (!waitingOnExistingLoad) {
clientLoadingPromise = loadClient()
}
client = await clientLoadingPromise
}
//reset
clientLoadingPromise = undefined

return { client }
}

Suppose you needed multiple clients, each with its own instantiation key (e.g. a unique wallet address per client). The following code adds a unique key as a param to getSingletonClientAsync(), and uses a hash-map pattern to retrieve the correct singleton promise for the requested key.


type MockClient = {
description: string
}

const storedLoadedClients: {
[clientConstructionKey: string]: MockClient
} = {}

const clientLoadingPromises: {
[clientConstructionKey: string]: Promise<MockClient> | undefined
} = {}

async function loadClient (clientConstructionKey: string) {
//wait 4 seconds
await new Promise((resolve) => setTimeout(resolve, 4_000))
const mockClient = { description: `I am a loaded client for key: ${clientConstructionKey}` };
return mockClient;
}

export async function getSingletonClientAsync(clientConstructionKey: string, resetToNewClient = false): Promise<any> {
let client: any
const storedLoadedClient = storedLoadedClients[clientConstructionKey]
if (!resetToNewClient && storedLoadedClient) {
client = storedLoadedClient
} else {
const waitingOnExistingLoad = clientLoadingPromises[clientConstructionKey];
if (!waitingOnExistingLoad) {
clientLoadingPromises[clientConstructionKey] = loadClient(clientConstructionKey)
}
client = await clientLoadingPromises[clientConstructionKey]
}
//reset
clientLoadingPromises[clientConstructionKey] = undefined

return { client }
}

This pattern is most useful in cases where you need to eagerly load a client, and use it across an application (especially if the loading is expensive). I suspect it to have use cases in other Web3 JS SDKs, given the need to often overclock front-end requirements for these projects.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Jeremy Tong
Jeremy Tong

Written by Jeremy Tong

Startup-Focused Software Developer

No responses yet

Write a response