Node.jsTCP

Node In Memory DB

By Jason Murphy
Picture of the author
Published on
Duration
5 days
Role
Developer
Client terminal REPL

Check out the repo

About

This project took inspiration from an in memory database written in python. As a challenge I wanted to see if I could rewrite the program in node.js
Some challenges I faced along the way was not having to deal with socket file like objects like python does. Instead the socket is already a streamable
piece of data called a Buffer. I then had to find a way to keep track of where I was in the buffer, so a cursor property in the protocol handler was made.
I took this a step further and wrote my own restoration function. For every flush in the database a log is generated of the commands that came before the flush. This allows me to recreate the database from a log file by playing back the log.

Code Snippets

Client class

class Client {
  constructor(host = "127.0.0.1", port = 31337) {
    this.protocol = new ProtocolHandler();
    this.partialData = null;
    this.socket = new net.Socket();
    this.socket.connect(port, host, () => {
      console.log("Connected to the server");
    });

Server class

class Server extends EventEmitter {
  constructor(host = "127.0.0.1", port = 31337) {
    super();
    this.host = host;
    this.port = port;
    this.server = net.createServer(this.connectionHandler.bind(this));
    this.kv = new Map();
    this.commands = this.getCommands();
    this.protocolHandler = new ProtocolHandler();
    this.currentLog = `log-${Date.now()}.txt`;
  }

Protocol Handler (Shared by the client and server)

class ProtocolHandler {
  constructor() {
    this.cursor = 0;
    this.handlers = {
      "+": this.handleSimpleString,
      "-": this.handleError,
      ":": this.handleInteger,
      $: this.handleString,
      "*": this.handleArray,
      "%": this.handleDict,
    };
  }

Handling Requests

  handleRequest(buffer) {
    //the first byte will tell us the data type we are dealing with. 
    const firstByte = decoder.write(buffer.slice(this.cursor, this.cursor + 1));
    if (!firstByte) {
      throw new Error("Invalid data");
    }

    const handler = this.handlers[firstByte];
    if (!handler) {
      throw new Error("Bad request");
    }

    this.cursor++;

    return handler.call(this, buffer);
  }

Writing to the socket

Each element in the buffer is terminated by a \r\n similar to redis

write(buf, data) {
    if (typeof data === "string") {
      const strData = Buffer.from(data, "utf-8");
      buf.push(Buffer.from(`$${strData.length}\r\n`));
      buf.push(strData);
      buf.push(Buffer.from("\r\n"));
    } else if (typeof data === "number") {
      buf.push(Buffer.from(`:${data}\r\n`));
    } else if (data instanceof Error) {
      buf.push(Buffer.from(`-${data.message}\r\n`));
    } else if (Array.isArray(data)) {
      buf.push(Buffer.from(`*${data.length}\r\n`));
      for (const item of data) {
        this.write(buf, item);
      }
    } else if (typeof data === "object" && data !== null) {
      // Assuming it's a dictionary
      const keys = Object.keys(data);
      buf.push(Buffer.from(`%${keys.length}\r\n`));
      for (const key of keys) {
        this.write(buf, key);
        this.write(buf, data[key]);
      }
    } else if (data === null) {
      buf.push(Buffer.from("$-1\r\n"));
    } else {
      throw new Error("Unrecognized type: " + typeof data);
    }
  }

In-Memory Database Over TCP

A simple, efficient, and lightweight in-memory database that communicates over TCP using a custom protocol.

🚀 Getting Started

These instructions will get you a copy of the project up and running on your local machine for development and testing purposes.

📋 Prerequisites

- Node.js v14.x.x or later - npm v6.x.x or later

🛠️ Installation

  1. Clone the repository:
git clone git@github.com:jmurphy1196/node-in-memory-database.git
  1. Change to the project directory:
cd node-in-memory-db
  1. Install the dependencies:
npm install
  1. Start the server:
node server.js

You should now have the server running on 127.0.0.1:31337.

🖥️ Usage

For interaction, use the provided client. Here are some basic commands:

const { Client } = require('./client');
const client = new Client();

client.set("key", "value");
client.get("key");

📈 Features

  • In-Memory Storage: Fast data retrieval and storage with no persistence overhead.
  • Custom Protocol: Efficient and simple protocol for communication.
  • TCP Communication: Reliable data transfer over TCP.

🛠️ Built With

📄 License

This project is licensed under the MIT License - see the LICENSE.md file for details.