Skip to main content

NodeJS 101 - Lazy Initialization

· 2 min read
Lucas Weis Polesello

One of the main challenges when dealing w/ the async nature of NodeJS is initializing classes/clients that requires some sort of side effect - such as database connection, disk reads or whatsoever. Even the simple idea of waiting for the first use-case to connect/initialize a resource.

Besides Dependency Injection - I like to use two approaches for this:

1) Leaving it up to the client to call connect or any other synonym - easy as creating an async function as the example below

const redis = require('redis');
const crypto = require('crypto');
//PROS: Damn easy, simple and straight-forward

//CONS: This leaves the entire responsibility to the client
class DistributedDataStructure {
constructor(){
this.client = redis.createClient();
}

async connect(){
return this.client.connect();
}

async add(staffName, reviewId){
//Do some business here - idk,
const accountName = await this.client.get(key);
return this.client.sAdd(`v1:${accountName}:pending-reviews`, reviewId);
}
}

(async () => {
const ds = new DistributedDataStructure();
await ds.connect();
ds.add('Jerome', crypto.randomBytes(12).toString('hex'));
})()

2) Proxying the access

In the real and wild-world we know that we have to deal w/ legacy code, legacy initialization methods and much more unexpected stuff - for this we have a second use-case which leverages the (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy])[Proxy API for JS]

Using Proxy it would look poorly-like

const redis = require('redis');
const { once } = require('events');
const crypto = require('crypto');

//PROS: No client responsibility - makes it easy for the client
//CONS: More complex and error prone
class ProxiedDistributedDataStructure {
constructor(){
this.client = redis.createClient();
this.client.connect();
return new Proxy(this, {
get(target, property){
const descriptor = target[property];
if(!descriptor){
return;
}
if(target.isReady){
return descriptor;
}
return async function(){
await once(target.client, 'ready');
return descriptor.apply(target, arguments);
}
}
});
}

async add(staffName, reviewId){
//Do some business here - idk - like below
const accountName = await this.client.get(staffName);
return this.client.sAdd(`v1:${accountName}:pending-reviews`, reviewId);
}
}

const client = new ProxiedDistributedDataStructure();
client.add('Jerome', crypto.randomBytes(12).toString('hex'));

The main benefit for the second approach is that we can instantiate the objects in sync contexts and only treat the method calls as async - instead of needing to play around some dirty gimmicks to call connect and chain promises - even worse, callbackifying.

NOTES: AFAIC from Redis V3^ we have an option legacyMode whenever creating the client which we can keep this lazy nature of Redis - doing client buffering of calls.