Singleton Pattern, Backend State management and Pub Subs

jay818

Jayant

Posted on August 28, 2024

Singleton Pattern, Backend State management and Pub Subs

Stateless vs Stateful Backends

  1. Stateless - We don't store any state in the backend. We rely on DB for state. Advantages a. User can connect to random server. b. we can scale very easily.
  2. Stateful - We store some state in the server. Mostly used in games, stockes exchanges app,chats app or you want to store some cache in the server for some time. if you building some game app or something that uses ws and needs a room to operate. Then you have to add stickiness to your app. stickiness means user interested in same room gets connected to same server. WebSocket

In the above image we have implemented the stickiness.

Example

export class Cache {
   private inMemoryDb: Map<
    string,
    {
      value: any;
      expiry: number;
    }
   >;
   // Singleton Pattern used, read the blog for more info
   private static instance: Cache;
   // constructor is private so no one is able to access and can't create multiple instance
   private constructor() {
    this.inMemoryDb = new Map<
    string,
    {
    value: any;
    expiry: number;
    } >();
    }

    static getInstance() {
    if (!this.instance) {
    this.instance = new Cache();
    }

        return this.instance;

    }

    set(
    type: string,
    args: string[],
    value: any,
    expirySeconds: number = parseInt(process.env.CACHE_EXPIRE_S || '100', 10),
    ) {
    this.inMemoryDb.set(`${type} ${JSON.stringify(args)}`, {
    value,
    expiry: new Date().getTime() + expirySeconds \* 1000,
    });
    }

    get(type: string, args: string[]) {
    const key = `${type} ${JSON.stringify(args)}`;
    const entry = this.inMemoryDb.get(key);
    if (!entry) {
    return null;
    }
    if (new Date().getTime() > entry.expiry) {
    this.inMemoryDb.delete(key);
    return null;
    }
    return entry.value;
    }

    evict(type: string, args: string[]) {
    const key = "${type} ${JSON.stringify(args)}";
    this.inMemoryDb.delete(key);
    return null;
    }
    }
Enter fullscreen mode Exit fullscreen mode

Best Way to use State in TS

  1. Keep it in a seprate folder called store.ts, acting as an single source of truth.
interface Game {
    whitePlayer: string;
    blackPlayer: string;
    moves: string[];
}

export const games: Game[] = [];
Enter fullscreen mode Exit fullscreen mode

But there is a problem to this, if we need to do any operation on the games object then we have do something like this,

games.push({
    ...data
);

games[0].name="jayant";
games[0].moves.push("E2");
Enter fullscreen mode Exit fullscreen mode

That's not the best way you do this task.

  1. Better way is to use the Classes and user can call the methods to do the operations also, with this we only have to change our code logic in only one place. We will make a GameManager class.
interface Game {
    id: string;
    whitePlayer: string;
    blackPlayer: string;
    moves: string[];
}

export class GameManager {
    private games: Game[] = [];

    public addGame(game: Game) {
        this.games.push(game);
    }

    public getGames() {
        return this.games;
    }

    public addMove(gameId: string, move: string) {
        const game = this.games.find((game) => game.id === gameId);
        if (game) {
            game.moves.push(move);
        }
    }

    public logState() {
        console.log(this.games);
    }
}
Enter fullscreen mode Exit fullscreen mode

But there is also a problem in this, user can create multiple instances of this class and each instance will have different data.
To solve this issue you must create only single instance.
Something like this

// use this at every place.
export const gameManager = new GameManager();
Enter fullscreen mode Exit fullscreen mode

But we can still create an instance of this by calling the class constructor.

  1. To solve this issue we need to use the concept of singleton pattern. a. Make the constructor function private, so that it will only be called inside the Class. b. Use Static keyword. In JavaScript, the keyword static is used in classes to declare static methods or static properties. Static methods and properties belong to the class itself, rather than to any specific instance of the class. Means if we use static keyword then the variable or method now attached to class instead of object.
class Example {
    // count is attached to class instead to any object, so can't be redeclared
    static count = 0;

    constructor() {
        Example.count++; // Increment the static property using the class name
    }
}

let ex1 = new Example();
let ex2 = new Example();
console.log(Example.count); // Outputs: 2
Enter fullscreen mode Exit fullscreen mode

Singleton Pattern

class GameManager {
    private games: Game[];
    // It store the instane and is static, means accessed using GameManager.instance
    private static instance: GameManager;

    // declared as private, can only be called inside the class only
    private constructor() {
        this.games = [];
    }

    // accessed using GameManager.getInstance()
    // gives us instance of the class, but it is created only once.
    static getInstance() {
        if (!this.instance) {
            this.instance = new GameManager();
        }
        return this.instance;
    }

    addGame(game: Game) {
        this.games.push(game);
    }

    removeGame(gameId: string) {
        this.games = this.games.filter((game) => game.id !== gameId);
    }

    log() {
        console.log(this.games);
    }
}

export const gameManager = GameManager.getInstance();
Enter fullscreen mode Exit fullscreen mode

PubSub + Singleton

Let replicate an exchange app, where user can subscribe to the different stockes and gets updated prices.
Pubsub

To Implement PubSub we will use redis.

docker run -d -p 6379:6379 redis
// try Pubsub in terminal
docker exec -it d1da6bcf089f /bin/bash
redis-cli
SUBSCRIBE PAYTM
// in different terminal do this
PUBLISH PAYTM "PRICE 2400
Enter fullscreen mode Exit fullscreen mode
npm i redis
Enter fullscreen mode Exit fullscreen mode

create a PubSubManager class that has

  1. method to subscribe to an channel
  2. method to unsubscribe to a channel
  3. private method that sends message to the user.
import { createClient, RedisClientType } from "redis";
class PubSubManager {
    private static instance: PubSubManager;
    private redisClient: RedisClientType;
    private subscriptions: Map<string, string[]>;

    private constructor() {
        this.redisClient = createClient();
        this.redisClient.connect();
        this.subscriptions = new Map();
    }

    static getInstance(): PubSubManager {
        if (!this.instance) {
            this.instance = new PubSubManager();
        }
        return this.instance;
    }

    public userSubscribe(userId: string, channel: string) {
        // agar phli baar hai to
        if (!this.subscriptions.has(channel)) {
            this.subscriptions.set(channel, []);
        }

        this.subscriptions.get(channel)?.push(userId);

        // If it was our 1st time means we haven't subscribed to the channel
        if (this.subscriptions.get(channel)?.length === 1) {
            // attached an handler agar message aaya to ye run ho jayega.
            this.redisClient.subscribe(channel, (msg) => {
                this.handleMessage(channel, msg);
            });
            console.log(`Subscribed to Redis channel: ${channel}`);
        }
    }

    public userUnSubscribe(userId: string, channel: string) {
        if (this.subscriptions.get(channel)) {
            this.subscriptions.set(
                channel,
                this.subscriptions.get(channel)?.filter((id) => id !== userId) || []
            );
        }

        if (this.subscriptions.get(channel)?.length === 0) {
            this.redisClient.unsubscribe(channel);
            console.log(`Unsubscribed from Redis channel: ${channel}`);
        }
    }

    private handleMessage(channel: string, msg: string) {
        // we need to get all the user.

        this.subscriptions.get(channel)?.forEach((userId) => {
            // send the message to the user
            console.log(`Sending message to user: ${userId}`);
        });
    }

    public async disconnect() {
        await this.redisClient.quit();
    }
}

export const pubSubManager = PubSubManager.getInstance();
Enter fullscreen mode Exit fullscreen mode

Thanks.

💖 💪 🙅 🚩
jay818
Jayant

Posted on August 28, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related