Jayant
Posted on August 28, 2024
Stateless vs Stateful Backends
-
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. -
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 addstickiness
to your app.stickiness
means user interested in same room gets connected to same server.
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;
}
}
Best Way to use State in TS
- 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[] = [];
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");
That's not the best way you do this task.
- 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);
}
}
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();
But we can still create an instance of this by calling the class constructor.
- 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. UseStatic
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
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();
PubSub + Singleton
Let replicate an exchange app, where user can subscribe to the different stockes and gets updated prices.
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
npm i redis
create a PubSubManager class that has
- method to subscribe to an channel
- method to unsubscribe to a channel
- 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();
Thanks.
Posted on August 28, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.