Releasing v0.2.0

This commit is contained in:
Jyotirmoy Bandyopadhayaya 2023-05-09 10:48:37 +05:30
parent 78cdc82e61
commit e58f9866e7
Signed by: bravo68web
GPG Key ID: F5671FD7BCB9917A
12 changed files with 986 additions and 658 deletions

3
.gitignore vendored
View File

@ -4,4 +4,5 @@ testBox.js
node_modules
*.dat
config.json
stockpile.json
stockpile.json
dist/

View File

@ -6,7 +6,7 @@
[![License: MIT](https://img.shields.io/github/license/BRAVO68WEB/stockpile)](https://github.com/BRAVO68WEB/stockpile/blob/master/LICENSE)
[![Twitter: BRAVO68WEB](https://img.shields.io/twitter/follow/BRAVO68WEB.svg?style=social)](https://twitter.com/BRAVO68WEB)
> A Tiny Redis Server ...
> A Tiny Redis Server with no external dependencies ...
Stockpile is a Tiny Redis Server, created from scratch in Node.js with only few dependencies. It is a work in progress, and is not yet ready for production use. It was initially a part of [CodeCrafter's - Create Redis Challenge](https://app.codecrafters.io/courses/redis/). But, I decided to make it a standalone project. I will be adding more features to it, and will be using it in my future projects. I will also be adding more tests to it. If you want to contribute, please feel free to do so.
@ -14,6 +14,17 @@ Stockpile is a Tiny Redis Server, created from scratch in Node.js with only few
## Usage
```sh
git clone https://github.com/BRAVO68WEB/stockpile
cd stockpile
yarn
./spawn_redis_server.sh
```
OR
>Not test with ts release
```sh
npx @bravo68web/stockpile help
```
@ -22,6 +33,22 @@ npx @bravo68web/stockpile help
- [Node.js](https://github.com/BRAVO68WEB/stockpile-node-sdk)
## ChangeLog
- **Mar 11, 2023** - Initial Commit
- **Mar 11, 2023** - Added `SET`, `GET`, `DEL` and `PING` commands
- **Mar 12, 2023** - Added `ECHO`, `EXISTS`, `KEYS`, `APPEND`, `STRLEN` commands
- **Mar 12, 2023** - Added `INCR`, `DECR`, `INCRBY`, `DECRBY` commands
- **MAR 14, 2023** - Added `SETNX`, `SETRANGE`, `GETRANGE`, `MSET`, `MGET`, `FLUSHDB`, `FLUSHALL`, `DBSIZE`, `RANDOMKEY` commands
- **MAR 15, 2023** - Added `EXPIRE`, `TTL`, `PERSIST`, `EXPIREAT`, `PEXPIREAT`, `PERSIST`, `MOVE`, `SELECT` commands
- **MAR 17, 2023** - Added `Added feature to dump/restore data and `AUH` command support
- **MAR 18, 2023** - Release v0.1.0 and Added npx support
- **MAR 19, 2023** - Added `BSON` for buffer support and Added `/health` endpoint for HTTP health check
- **MAR 19, 2023** - Dockerizing application
- **MAY 8, 2023** - Added Github Actions to Build Image to `GitHub Container Registry`
- **MAY 9, 2023** - Rewrite code to Typescript
- *... more to come*
## Development
```sh
@ -32,7 +59,7 @@ cd stockpile
## Run tests
```sh
node app/main.js &
./spawn_redis_server.sh &
./test_local.sh
```

102
app/bootstrap.js vendored
View File

@ -1,102 +0,0 @@
#!/usr/bin/env node
// This is the main entry point for the application.
// CLI arguments are passed to this file, and then passed to the main application.
// Do not use any CLI framework here, as it will be difficult to maintain.
const commandName = process.argv[2];
const commandArgs = process.argv.slice(3);
const fs = require("fs");
const os = require("os");
const path = require("path");
const pkg = require("../package.json");
if (commandName === "help") {
console.log("Stockpile CLI");
console.log("Version " + pkg.version);
console.log("");
console.log("Commands:");
console.log(
" init --name <name> --configpath <path> --auth <password> --port <port> --dumppath <path> | Create a new config file"
);
console.log(" start --configpath <path> | Start Stockpile");
console.log(" help Display this help message");
console.log("");
console.log("Github : ", pkg.repository.url.split("+")[1]);
console.log("Author : ", pkg.author.name);
process.exit(0);
} else if (commandName === "init") {
try {
console.log("Initializing new Stockpile config file");
let config = {
name: "",
configpath: os.homedir() + "/.stockpile.config.json",
auth: "",
port: 6379,
dumppath: os.homedir() + "/.stockpile.dump",
};
if (commandArgs.length % 2 !== 0) {
console.log("Invalid init syntax");
return;
}
for (const i in commandArgs) {
if (commandArgs[i] === "--name") {
config.name = commandArgs[parseInt(i) + 1];
} else if (commandArgs[i] === "--configpath") {
config.configpath = commandArgs[parseInt(i) + 1];
} else if (commandArgs[i] === "--auth") {
config.auth = commandArgs[parseInt(i) + 1];
} else if (commandArgs[i] === "--port") {
config.port = commandArgs[parseInt(i) + 1];
} else if (commandArgs[i] === "--dumppath") {
config.dumppath = commandArgs[parseInt(i) + 1];
}
}
fs.writeFileSync(
path.resolve(config.configpath),
JSON.stringify(config)
);
console.log("Config file created at " + config.configpath);
process.exit(0);
} catch (err) {
console.log(err);
process.exit(1);
}
} else if (commandName === "start") {
console.log("Starting Stockpile");
let configpath = "";
for (const i in commandArgs) {
if (commandArgs[i] === "--configpath") {
configpath = commandArgs[parseInt(i) + 1];
}
}
if (configpath === "") {
console.log("No config file specified");
return;
}
if (!fs.existsSync(configpath)) {
console.log("Config file does not exist");
return;
}
const config = JSON.parse(fs.readFileSync(configpath));
["name", "configpath", "port", "dumppath"].forEach((key) => {
if (!config[key]) {
console.log("Invalid config file");
process.exit(1);
}
});
const Stockpile = require("./main.js");
Stockpile(config);
} else {
console.log("Invalid command");
}

114
app/bootstrap.ts Normal file
View File

@ -0,0 +1,114 @@
import fs from "fs";
import os from "os";
import path from "path";
import Stockpile from "./main";
const [commandName, ...commandArgs] = process.argv.slice(2);
const version = "v0.2.0";
if (commandName === "help") {
console.log("Stockpile CLI");
console.log(`Version : ${version}`);
console.log("");
console.log("Commands:");
console.log(
" init --name <name> --configpath <path> --auth <password> --port <port> --dumppath <path> | Create a new config file"
);
console.log(" start --configpath <path> | Start Stockpile");
console.log(" help Display this help message");
console.log("");
console.log("Github : ", "https://github.com/BRAVO68WEB/stockpile");
console.log("Author : ", "https://github.com/BRAVO68WEB/");
process.exit(0);
} else if (commandName === "init") {
try {
console.log("Initializing new Stockpile config file");
const config = {
name: "",
configpath: path.resolve(os.homedir(), ".stockpile.config.json"),
auth: "",
port: 6379,
dumppath: path.resolve(os.homedir(), ".stockpile.dump"),
};
if (commandArgs.length % 2 !== 0) {
console.log("Invalid init syntax");
process.exit(1);
}
for (let i = 0; i < commandArgs.length; i += 2) {
const option = commandArgs[i];
const value = commandArgs[i + 1];
switch (option) {
case "--name":
config.name = value;
break;
case "--configpath":
config.configpath = value;
break;
case "--auth":
config.auth = value;
break;
case "--port":
config.port = parseInt(value);
break;
case "--dumppath":
config.dumppath = value;
break;
default:
console.log("Invalid init syntax");
process.exit(1);
}
}
fs.writeFileSync(config.configpath, JSON.stringify(config));
console.log(`Config file created at ${config.configpath}`);
process.exit(0);
} catch (err) {
console.log(err);
process.exit(1);
}
} else if (commandName === "start") {
console.log(`Stockpile ${version}`);
let configpath = "";
for (let i = 0; i < commandArgs.length; i += 2) {
const option = commandArgs[i];
const value = commandArgs[i + 1];
if (option === "--configpath") {
configpath = value;
break;
}
}
if (configpath === "") {
console.log("No config file specified");
process.exit(1);
}
if (!fs.existsSync(configpath)) {
console.log("Config file does not exist");
process.exit(1);
}
const config = JSON.parse(fs.readFileSync(configpath, "utf-8"));
if (
!config.name ||
!config.configpath ||
!config.port ||
!config.dumppath
) {
console.log("Invalid config file");
process.exit(1);
}
Stockpile(config);
} else {
console.log("Invalid command");
process.exit(1);
}

View File

@ -1,6 +1,9 @@
const { BSON } = require("bson");
import { BSON } from "bson";
function createBinaryData(mapData) {
type MapData = Map<MapEntry, any>[];
type MapEntry = [string, any];
function createBinaryData(mapData: MapData) {
const data = {};
// Add each map in the array to the data object
for (let i = 0; i < mapData.length; i++) {
@ -12,25 +15,18 @@ function createBinaryData(mapData) {
function readBinaryData(buffer) {
const data = BSON.deserialize(buffer);
const mapData = [];
const mapData: MapData = [];
// Convert each object in the data object to a Map
for (const key in data) {
for (const key in data as MapEntry) {
const obj = data[key];
const map = new Map();
for (const [key, value] of Object.entries(obj)) {
if (typeof value === "object") {
map.set(key, value);
} else {
map.set(key, value);
}
map.set(key, value);
}
mapData.push(map);
}
return mapData;
}
module.exports = {
createBinaryData,
readBinaryData,
};
export { createBinaryData, readBinaryData };

View File

@ -1,533 +0,0 @@
const net = require("net");
const fs = require("fs");
const path = require("path");
const pkg = require("../package.json");
const { createBinaryData, readBinaryData } = require("./buffer");
module.exports = (config) => {
const MAX_DATABASES = 16;
const DEFAULT_DATABASE = 0;
const authConfig = {
enabled: config.auth ? true : false,
isAuthenticated: false,
password: config.auth ? config.auth : "",
};
let dataStore = new Array(MAX_DATABASES);
for (let i = 0; i < MAX_DATABASES; i++) {
dataStore[i] = new Map();
}
if (fs.existsSync(path.join(config.dumppath))) {
dataStore = fs.readFileSync(path.join(config.dumppath));
dataStore = readBinaryData(dataStore);
}
let currentDatabase = DEFAULT_DATABASE;
const parseIncomingData = (data) => {
const lines = data.toString().split("\r\n");
return lines;
};
const assignHandler = (command, connection) => {
command[2] = command[2].toUpperCase();
console.log(command);
console.log(dataStore[currentDatabase]);
if (
authConfig.enabled &&
!authConfig.isAuthenticated &&
command[2] !== "AUTH"
) {
connection.write("-ERR NOAUTH Authentication required.\r\n");
return;
}
if (command[2] == "AUTH") {
if (authConfig.enabled) {
if (authConfig.isAuthenticated) {
connection.write("-ERR Already authenticated\r\n");
} else if (command[4] == authConfig.password) {
authConfig.isAuthenticated = true;
connection.write("+OK Auth Successfull\r\n");
} else if (command[4] != authConfig.password) {
connection.write("-ERR Invalid Password\r\n");
}
} else {
connection.write("-ERR Authentication is not enabled\r\n");
}
} else if (command[2] == "PING") {
connection.write("+PONG\r\n");
} else if (command[2] == "SET") {
dataStore[currentDatabase].set(command[4], command[6]);
let expireTime = null;
for (let i = 8; i < command.length; i += 2) {
if (command[i].toUpperCase() === "PX") {
expireTime = Date.now() + parseInt(command[i + 2]);
} else if (command[i].toUpperCase() === "EX") {
expireTime = Date.now() + parseInt(command[i + 2]) * 1000;
}
}
if (expireTime) {
dataStore[currentDatabase].set(
command[4] + "_expire",
expireTime
);
}
connection.write("+OK\r\n");
} else if (command[2] == "GET") {
if (
dataStore[currentDatabase].get(command[4] + "_expire") &&
dataStore[currentDatabase].get(command[4] + "_expire") <
Date.now()
) {
connection.write("$-1\r\n");
dataStore[currentDatabase].delete(command[4]);
dataStore[currentDatabase].delete(command[4] + "_expire");
} else if (dataStore[currentDatabase].get(command[4]))
connection.write(
"+" + dataStore[currentDatabase].get(command[4]) + "\r\n"
);
else connection.write("$-1\r\n");
} else if (command[2] == "DEL") {
dataStore[currentDatabase].delete(command[4]);
connection.write(":1\r\n");
} else if (command[2] == "ECHO") {
connection.write(
"$" + command[4].length + "\r\n" + command[4] + "\r\n"
);
} else if (command[2] == "EXISTS") {
if (dataStore[currentDatabase].has(command[4]))
connection.write(":1\r\n");
else connection.write(":0\r\n");
} else if (command[2] == "KEYS") {
let pattern = command[4];
let keys = Array.from(dataStore[currentDatabase].keys());
let matchingKeys = keys.filter((key) => {
const regex = new RegExp(pattern.replace("*", ".*"));
return regex.test(key);
});
let response = "*" + matchingKeys.length + "\r\n";
for (let key of matchingKeys) {
response += "$" + key.length + "\r\n" + key + "\r\n";
}
connection.write(response);
} else if (command[2] == "APPEND") {
let key = command[4];
let value = command[6];
if (dataStore[currentDatabase].has(key)) {
let newValue = dataStore[currentDatabase].get(key) + value;
dataStore[currentDatabase].set(key, newValue);
connection.write(":" + newValue.length + "\r\n");
} else {
dataStore[currentDatabase].set(key, value);
connection.write(":" + value.length + "\r\n");
}
} else if (command[2] == "STRLEN") {
let key = command[4];
if (dataStore[currentDatabase].has(key)) {
let value = dataStore[currentDatabase].get(key);
connection.write(":" + value.length + "\r\n");
} else {
connection.write(":0\r\n");
}
} else if (command[2] == "SETNX") {
let key = command[4];
let value = command[6];
if (dataStore[currentDatabase].has(key)) {
connection.write(":0\r\n");
} else {
dataStore[currentDatabase].set(key, value);
connection.write(":1\r\n");
}
} else if (command[2] == "SETRANGE") {
let key = command[4];
let offset = command[6];
let value = command[8];
if (dataStore[currentDatabase].has(key)) {
let oldValue = dataStore[currentDatabase].get(key);
let newValue =
oldValue.substring(0, offset) +
value +
oldValue.substring(offset + value.length);
dataStore[currentDatabase].set(key, newValue);
connection.write(":" + newValue.length + "\r\n");
} else {
dataStore[currentDatabase].set(key, value);
connection.write(":" + value.length + "\r\n");
}
} else if (command[2] == "GETRANGE") {
let key = command[4];
let start = command[6];
let end = command[8];
if (dataStore[currentDatabase].has(key)) {
let value = dataStore[currentDatabase].get(key);
let newValue = value.substring(start, end + 1);
connection.write(
"$" + newValue.length + "\r\n" + newValue + "\r\n"
);
} else {
connection.write("$-1\r\n");
}
} else if (command[2] == "MSET") {
const keyValuePairs = command.slice(4);
for (let i = 0; i < keyValuePairs.length; ) {
dataStore[currentDatabase].set(
keyValuePairs[i],
keyValuePairs[i + 2]
);
i += 4;
}
connection.write("+OK\r\n");
} else if (command[2] == "MGET") {
const keys = command.slice(1);
const values = keys.map(
(key) => dataStore[currentDatabase].get(key) ?? null
);
if (values.includes(null)) {
connection.write("$-1\r\n");
} else {
const response = values.reduce((acc, value) => {
return `${acc}\r\n$${value.length}\r\n${value}`;
}, `*${values.length}`);
connection.write(response);
}
} else if (command[2] == "FLUSHDB") {
dataStore[currentDatabase].clear();
connection.write("+OK\r\n");
} else if (command[2] == "FLUSHALL") {
dataStore = [];
for (let i = 0; i < 16; i++) {
dataStore.push(new Map());
}
connection.write("+OK\r\n");
} else if (command[2] == "DBSIZE") {
let size = dataStore[currentDatabase].size;
connection.write(":" + size + "\r\n");
} else if (command[2] == "SELECT") {
let database = command[4];
currentDatabase = database;
connection.write("+OK\r\n");
} else if (command[2] == "RANDOMKEY") {
let keys = Array.from(dataStore[currentDatabase].keys());
if (keys.length == 0) {
connection.write("$-1\r\n");
return;
}
let randomKey = keys[Math.floor(Math.random() * keys.length)];
connection.write(
"$" + randomKey.length + "\r\n" + randomKey + "\r\n"
);
} else if (command[2] == "INCR") {
let key = command[4];
if (dataStore[currentDatabase].has(key)) {
let value = dataStore[currentDatabase].get(key);
let newValue = parseInt(value) + 1;
dataStore[currentDatabase].set(key, newValue.toString());
connection.write(":" + newValue + "\r\n");
} else {
dataStore[currentDatabase].set(key, "1");
connection.write(":1\r\n");
}
} else if (command[2] == "INCRBY") {
let key = command[4];
let increment = command[6];
if (dataStore[currentDatabase].has(key)) {
let value = dataStore[currentDatabase].get(key);
let newValue = parseInt(value) + parseInt(increment);
dataStore[currentDatabase].set(key, newValue.toString());
connection.write(":" + newValue + "\r\n");
} else {
dataStore[currentDatabase].set(key, increment);
connection.write(":" + increment + "\r\n");
}
} else if (command[2] == "DECR") {
let key = command[4];
if (dataStore[currentDatabase].has(key)) {
let value = dataStore[currentDatabase].get(key);
let newValue = parseInt(value) - 1;
dataStore[currentDatabase].set(key, newValue.toString());
connection.write(":" + newValue + "\r\n");
} else {
dataStore[currentDatabase].set(key, "-1");
connection.write(":-1\r\n");
}
} else if (command[2] == "DECRBY") {
let key = command[4];
let decrement = command[6];
if (dataStore[currentDatabase].has(key)) {
let value = dataStore[currentDatabase].get(key);
let newValue = parseInt(value) - parseInt(decrement);
dataStore[currentDatabase].set(key, newValue.toString());
connection.write(":" + newValue + "\r\n");
} else {
dataStore[currentDatabase].set(key, "-" + decrement);
connection.write(":-" + decrement + "\r\n");
}
} else if (command[2] == "EXPIRE") {
const key = command[4];
const seconds = parseInt(command[6]);
if (dataStore[currentDatabase].has(key)) {
const timestamp = Math.floor(Date.now() / 1000) + seconds;
dataStore[currentDatabase].set(`${key}:expire`, timestamp);
connection.write(":1\r\n");
setTimeout(() => {
if (dataStore[currentDatabase].has(key)) {
dataStore[currentDatabase].delete(key);
dataStore[currentDatabase].delete(`${key}:expire`);
}
}, seconds * 1000);
} else {
connection.write(":0\r\n");
}
} else if (command[2] == "TTL") {
const key = command[4];
if (dataStore[currentDatabase].has(key)) {
// Get the expiration time of the key
const expireKey = `${key}:expire`;
if (dataStore[currentDatabase].has(expireKey)) {
const timestamp = dataStore[currentDatabase].get(expireKey);
const ttl = Math.max(
timestamp - Math.floor(Date.now() / 1000),
0
);
connection.write(`:${ttl}\r\n`);
} else {
connection.write(":-1\r\n");
}
} else {
connection.write(":-2\r\n");
}
} else if (command[2] == "EXPIREAT") {
const key = command[4];
const timestamp = parseInt(command[6]);
if (dataStore[currentDatabase].has(key)) {
// Set the expiration time of the key
dataStore[currentDatabase].set(`${key}:expire`, timestamp);
connection.write(":1\r\n");
} else {
connection.write(":0\r\n");
}
} else if (command[2] == "PERSIST") {
const key = command[4];
if (dataStore[currentDatabase].has(`${key}:expire`)) {
dataStore[currentDatabase].delete(`${key}:expire`);
connection.write(":1\r\n");
} else {
connection.write(":0\r\n");
}
} else if (command[2] == "HGET") {
const key = command[4];
const field = command[6];
if (dataStore[currentDatabase].has(key)) {
const value = dataStore[currentDatabase].get(key)[field];
if (value !== undefined) {
connection.write(`$${value.length}\r\n${value}\r\n`);
} else {
connection.write("$-1\r\n");
}
} else {
connection.write("$-1\r\n");
}
} else if (command[2] == "HSET") {
const key = command[4];
const field = command[6];
const value = command[8];
if (!dataStore[currentDatabase].has(key)) {
dataStore[currentDatabase].set(key, {});
}
dataStore[currentDatabase].get(key)[field] = value;
connection.write(":1\r\n");
} else if (command[2] == "HGETALL") {
const key = command[4];
if (dataStore[currentDatabase].has(key)) {
const obj = dataStore[currentDatabase].get(key);
let response = "*" + Object.keys(obj).length * 2 + "\r\n";
for (const [field, value] of Object.entries(obj)) {
response += `$${field.length}\r\n${field}\r\n`;
response += `$${value.length}\r\n${value}\r\n`;
}
connection.write(response);
} else {
connection.write("*0\r\n");
}
} else if (command[2] == "HSETNX") {
const key = command[4];
const field = command[6];
const value = command[8];
if (!dataStore[currentDatabase].has(key)) {
dataStore[currentDatabase].set(key, {});
}
const obj = dataStore[currentDatabase].get(key);
if (obj[field] === undefined) {
obj[field] = value;
connection.write(":1\r\n");
} else {
connection.write(":0\r\n");
}
} else if (command[2] == "HMSET") {
const hash = command[1];
const fieldsAndValues = command.slice(2);
const hashExists = dataStore[currentDatabase].hasOwnProperty(hash);
if (!hashExists) {
dataStore[currentDatabase][hash] = {};
}
for (let i = 0; i < fieldsAndValues.length; i += 2) {
const field = fieldsAndValues[i];
const value = fieldsAndValues[i + 1];
dataStore[currentDatabase][hash][field] = value;
}
connection.write("+OK\r\n");
} else if (command[2] == "HINCRBY") {
const key = command[4];
const field = command[6];
const increment = Number(command[8]);
if (!dataStore[currentDatabase].has(key)) {
dataStore[currentDatabase].set(key, {});
}
const obj = dataStore[currentDatabase].get(key);
if (obj[field] === undefined) {
obj[field] = "0";
}
obj[field] = String(Number(obj[field]) + increment);
connection.write(
":" + obj[field].length + "\r\n" + obj[field] + "\r\n"
);
} else if (command[2] == "HDEL") {
const key = command[4];
const fields = command.slice(6);
if (!dataStore[currentDatabase].has(key)) {
connection.write(":0\r\n");
} else {
const obj = dataStore[currentDatabase].get(key);
let count = 0;
for (const field of fields) {
if (obj.hasOwnProperty(field)) {
delete obj[field];
count += 1;
}
}
if (Object.keys(obj).length === 0) {
dataStore[currentDatabase].delete(key);
}
connection.write(":" + count + "\r\n");
}
} else if (command[2] == "HEXISTS") {
const key = command[4];
const field = command[6];
if (!dataStore[currentDatabase].has(key)) {
connection.write(":0\r\n");
} else {
const obj = dataStore[currentDatabase].get(key);
if (obj.hasOwnProperty(field)) {
connection.write(":1\r\n");
} else {
connection.write(":0\r\n");
}
}
} else if (command[2] == "HKEYS") {
const key = command[4];
if (!dataStore[currentDatabase].has(key)) {
connection.write("*0\r\n");
} else {
const obj = dataStore[currentDatabase].get(key);
const fields = Object.keys(obj);
let response = "*" + fields.length + "\r\n";
for (const field of fields) {
response += "$" + field.length + "\r\n" + field + "\r\n";
}
connection.write(response);
}
} else if (command[2] == "HLEN") {
const key = command[4];
if (!dataStore[currentDatabase].has(key)) {
connection.write(":0\r\n");
} else {
const obj = dataStore[currentDatabase].get(key);
const count = Object.keys(obj).length;
connection.write(":" + count + "\r\n");
}
} else if (command[2] == "HSTRLEN") {
const key = command[4];
const field = command[6];
if (!dataStore[currentDatabase].has(key)) {
connection.write(":0\r\n");
} else {
const obj = dataStore[currentDatabase].get(key);
if (obj.hasOwnProperty(field)) {
const value = obj[field];
connection.write(":" + value.length + "\r\n");
} else {
connection.write(":0\r\n");
}
}
} else if (command[2] == "HVALS") {
const key = command[4];
if (!dataStore[currentDatabase].has(key)) {
connection.write("*0\r\n");
} else {
const obj = dataStore[currentDatabase].get(key);
const values = Object.values(obj);
let response = "*" + values.length + "\r\n";
for (const value of values) {
response += "$" + value.length + "\r\n" + value + "\r\n";
}
connection.write(response);
}
} else if (command[2] == "DUMP") {
saveDataToFile(dataStore);
connection.write("+OK Saving Current State\r\n");
} else if (command[0].includes("health")) {
connection.write("OK !!\r\n");
connection.end();
} else if (command[2].includes("INFO")) {
connection.write(`+# Stockpile Server ${pkg.version}\r\n`);
} else {
connection.write("-ERR unknown command " + command[2] + "\r\n");
}
};
let id = 0;
const server = net.createServer((connection) => {
connection.id = id;
id += 1;
connection.on("data", (data) => {
let command = parseIncomingData(data);
assignHandler(command, connection);
});
connection.on("end", (something) => {
console.log("Client disconnected");
});
connection.on("error", (err) => {
console.log("Error in connection");
console.log(err);
});
});
server.listen(config.port, "0.0.0.0", () => {
console.log("Starting server");
console.log("Stockpile server listening on port 6379");
});
function saveDataToFile(data) {
const binaryData = createBinaryData(data);
fs.writeFileSync(config.dumppath, binaryData);
}
process.on("SIGINT", () => {
console.log("\nTaking Snapshot !!");
saveDataToFile(dataStore);
console.log("Shutting down server");
process.exit(0);
});
};

673
app/main.ts Normal file
View File

@ -0,0 +1,673 @@
import net, { Socket } from "net";
import fs from "fs";
import path from "path";
import pkg from "../package.json" assert { type: "json" };
import { createBinaryData, readBinaryData } from "./buffer";
interface IAuthConfig {
enabled: boolean;
isAuthenticated: boolean;
password: string;
}
export interface IConfig {
port: number;
auth?: string;
dumppath: string;
configpath: string;
name: string;
logpath: string;
}
interface IConnection extends Socket {
id: number;
}
export default (config: IConfig) => {
const MAX_DATABASES = 16;
const DEFAULT_DATABASE = 0;
const authConfig: IAuthConfig = {
enabled: config.auth ? true : false,
isAuthenticated: false,
password: config.auth ? config.auth : "",
};
let dataStore: any[] | Buffer = new Array(MAX_DATABASES);
for (let i = 0; i < MAX_DATABASES; i++) {
dataStore[i] = new Map();
}
if (fs.existsSync(path.join(config.dumppath))) {
dataStore = fs.readFileSync(path.join(config.dumppath));
dataStore = readBinaryData(dataStore);
}
let currentDatabase = DEFAULT_DATABASE;
const parseIncomingData = (data) => {
const lines: string[] = data.toString().split("\r\n");
return lines;
};
const assignHandler = (command: string[], connection: IConnection) => {
command[2] = command[2].toUpperCase();
if (
authConfig.enabled &&
!authConfig.isAuthenticated &&
command[2] !== "AUTH"
) {
connection.write("-ERR NOAUTH Authentication required.\r\n");
return;
}
switch (command[2] as string) {
case "INFO": {
connection.write(`+# Stockpile Server ${pkg.version}\r\n`);
break;
}
case "AUTH": {
if (authConfig.enabled) {
if (authConfig.isAuthenticated) {
connection.write("-ERR Already authenticated\r\n");
} else if (command[4] == authConfig.password) {
authConfig.isAuthenticated = true;
connection.write("+OK Auth Successfull\r\n");
} else if (command[4] != authConfig.password) {
connection.write("-ERR Invalid Password\r\n");
}
} else {
connection.write("-ERR Authentication is not enabled\r\n");
}
break;
}
case "PING": {
connection.write("+PONG\r\n");
break;
}
case "SET": {
dataStore[currentDatabase].set(command[4], command[6]);
let expireTime: number | null = null;
for (let i = 8; i < command.length; i += 2) {
if (command[i].toUpperCase() === "PX") {
expireTime = Date.now() + parseInt(command[i + 2]);
} else if (command[i].toUpperCase() === "EX") {
expireTime =
Date.now() + parseInt(command[i + 2]) * 1000;
}
}
if (expireTime) {
dataStore[currentDatabase].set(
command[4] + "_expire",
expireTime
);
}
connection.write("+OK\r\n");
break;
}
case "GET": {
if (
dataStore[currentDatabase].get(command[4] + "_expire") &&
dataStore[currentDatabase].get(command[4] + "_expire") <
Date.now()
) {
connection.write("$-1\r\n");
dataStore[currentDatabase].delete(command[4]);
dataStore[currentDatabase].delete(command[4] + "_expire");
} else if (dataStore[currentDatabase].get(command[4]))
connection.write(
"+" +
dataStore[currentDatabase].get(command[4]) +
"\r\n"
);
else connection.write("$-1\r\n");
break;
}
case "DEL": {
dataStore[currentDatabase].delete(command[4]);
connection.write(":1\r\n");
break;
}
case "ECHO": {
connection.write(
"$" + command[4].length + "\r\n" + command[4] + "\r\n"
);
break;
}
case "EXISTS": {
if (dataStore[currentDatabase].has(command[4]))
connection.write(":1\r\n");
else connection.write(":0\r\n");
break;
}
case "KEYS": {
let pattern = command[4];
let keys = Array.from(dataStore[currentDatabase].keys());
let matchingKeys = keys.filter((key: any) => {
const regex = new RegExp(pattern.replace("*", ".*"));
return regex.test(key);
}) as any[];
let response = "*" + matchingKeys.length + "\r\n";
for (let key of matchingKeys) {
response += "$" + key.length + "\r\n" + key + "\r\n";
}
connection.write(response);
break;
}
case "APPEND": {
let key = command[4];
let value = command[6];
if (dataStore[currentDatabase].has(key)) {
let newValue = dataStore[currentDatabase].get(key) + value;
dataStore[currentDatabase].set(key, newValue);
connection.write(":" + newValue.length + "\r\n");
} else {
dataStore[currentDatabase].set(key, value);
connection.write(":" + value.length + "\r\n");
}
break;
}
case "STRLEN": {
let key = command[4];
if (dataStore[currentDatabase].has(key)) {
let value = dataStore[currentDatabase].get(key);
connection.write(":" + value.length + "\r\n");
} else {
connection.write(":0\r\n");
}
break;
}
case "SETNX": {
let key = command[4];
let value = command[6];
if (dataStore[currentDatabase].has(key)) {
connection.write(":0\r\n");
} else {
dataStore[currentDatabase].set(key, value);
connection.write(":1\r\n");
}
break;
}
case "SETRANGE": {
let key = command[4];
let offset = command[6];
let value = command[8];
if (dataStore[currentDatabase].has(key)) {
let oldValue = dataStore[currentDatabase].get(key);
let newValue =
oldValue.substring(0, offset) +
value +
oldValue.substring(offset + value.length);
dataStore[currentDatabase].set(key, newValue);
connection.write(":" + newValue.length + "\r\n");
} else {
dataStore[currentDatabase].set(key, value);
connection.write(":" + value.length + "\r\n");
}
break;
}
case "GETRANGE": {
let key = command[4];
let start = command[6];
let end = command[8];
if (dataStore[currentDatabase].has(key)) {
let value = dataStore[currentDatabase].get(key);
let newValue = value.substring(start, end + 1);
connection.write(
"$" + newValue.length + "\r\n" + newValue + "\r\n"
);
} else {
connection.write("$-1\r\n");
}
break;
}
case "MSET": {
const keyValuePairs = command.slice(4);
for (let i = 0; i < keyValuePairs.length; ) {
dataStore[currentDatabase].set(
keyValuePairs[i],
keyValuePairs[i + 2]
);
i += 4;
}
connection.write("+OK\r\n");
break;
}
case "MGET": {
const keys = command.slice(1);
const values = keys.map(
(key) => dataStore[currentDatabase].get(key) ?? null
);
if (values.includes(null)) {
connection.write("$-1\r\n");
} else {
const response = values.reduce((acc, value) => {
return `${acc}\r\n$${value.length}\r\n${value}`;
}, `*${values.length}`);
connection.write(response);
}
break;
}
case "FLUSHDB": {
dataStore[currentDatabase].clear();
connection.write("+OK\r\n");
break;
}
case "FLUSHALL": {
dataStore = [];
for (let i = 0; i < 16; i++) {
dataStore.push(new Map());
}
connection.write("+OK\r\n");
break;
}
case "DBSIZE": {
let size = dataStore[currentDatabase].size;
connection.write(":" + size + "\r\n");
break;
}
case "SELECT": {
let database = Number(command[4]);
currentDatabase = database;
connection.write("+OK\r\n");
break;
}
case "RANDOMKEY": {
let keys = Array.from(dataStore[currentDatabase].keys());
if (keys.length == 0) {
connection.write("$-1\r\n");
return;
}
let randomKey: string[] = keys[
Math.floor(Math.random() * keys.length)
] as string[];
connection.write(
"$" + randomKey.length + "\r\n" + randomKey + "\r\n"
);
break;
}
case "INCR": {
let key = command[4];
if (dataStore[currentDatabase].has(key)) {
let value = dataStore[currentDatabase].get(key);
let newValue = parseInt(value) + 1;
dataStore[currentDatabase].set(key, newValue.toString());
connection.write(":" + newValue + "\r\n");
} else {
dataStore[currentDatabase].set(key, "1");
connection.write(":1\r\n");
}
break;
}
case "INCRBY": {
let key = command[4];
let increment = command[6];
if (dataStore[currentDatabase].has(key)) {
let value = dataStore[currentDatabase].get(key);
let newValue = parseInt(value) + parseInt(increment);
dataStore[currentDatabase].set(key, newValue.toString());
connection.write(":" + newValue + "\r\n");
} else {
dataStore[currentDatabase].set(key, increment);
connection.write(":" + increment + "\r\n");
}
break;
}
case "DECR": {
let key = command[4];
if (dataStore[currentDatabase].has(key)) {
let value = dataStore[currentDatabase].get(key);
let newValue = parseInt(value) - 1;
dataStore[currentDatabase].set(key, newValue.toString());
connection.write(":" + newValue + "\r\n");
} else {
dataStore[currentDatabase].set(key, "-1");
connection.write(":-1\r\n");
}
break;
}
case "DECRBY": {
let key = command[4];
let decrement = command[6];
if (dataStore[currentDatabase].has(key)) {
let value = dataStore[currentDatabase].get(key);
let newValue = parseInt(value) - parseInt(decrement);
dataStore[currentDatabase].set(key, newValue.toString());
connection.write(":" + newValue + "\r\n");
} else {
dataStore[currentDatabase].set(key, "-" + decrement);
connection.write(":-" + decrement + "\r\n");
}
break;
}
case "EXPIRE": {
const key = command[4];
const seconds = parseInt(command[6]);
if (dataStore[currentDatabase].has(key)) {
const timestamp = Math.floor(Date.now() / 1000) + seconds;
dataStore[currentDatabase].set(`${key}:expire`, timestamp);
connection.write(":1\r\n");
setTimeout(() => {
if (dataStore[currentDatabase].has(key)) {
dataStore[currentDatabase].delete(key);
dataStore[currentDatabase].delete(`${key}:expire`);
}
}, seconds * 1000);
} else {
connection.write(":0\r\n");
}
break;
}
case "TTL": {
const key = command[4];
if (dataStore[currentDatabase].has(key)) {
// Get the expiration time of the key
const expireKey = `${key}:expire`;
if (dataStore[currentDatabase].has(expireKey)) {
const timestamp =
dataStore[currentDatabase].get(expireKey);
const ttl = Math.max(
timestamp - Math.floor(Date.now() / 1000),
0
);
connection.write(`:${ttl}\r\n`);
} else {
connection.write(":-1\r\n");
}
} else {
connection.write(":-2\r\n");
}
break;
}
case "PERSIST": {
const key = command[4];
if (dataStore[currentDatabase].has(`${key}:expire`)) {
dataStore[currentDatabase].delete(`${key}:expire`);
connection.write(":1\r\n");
} else {
connection.write(":0\r\n");
}
break;
}
case "HGET": {
const key = command[4];
const field = command[6];
if (dataStore[currentDatabase].has(key)) {
const value = dataStore[currentDatabase].get(key)[field];
if (value !== undefined) {
connection.write(`$${value.length}\r\n${value}\r\n`);
} else {
connection.write("$-1\r\n");
}
} else {
connection.write("$-1\r\n");
}
break;
}
case "HSET": {
const key = command[4];
const field = command[6];
const value = command[8];
if (!dataStore[currentDatabase].has(key)) {
dataStore[currentDatabase].set(key, {});
}
dataStore[currentDatabase].get(key)[field] = value;
connection.write(":1\r\n");
break;
}
case "HGETALL": {
const key = command[4];
if (dataStore[currentDatabase].has(key)) {
const obj = dataStore[currentDatabase].get(key);
let response = "*" + Object.keys(obj).length * 2 + "\r\n";
for (const [field, value] of Object.entries(obj) as any) {
response += `$${field.length}\r\n${field}\r\n`;
response += `$${value.length}\r\n${value}\r\n`;
}
connection.write(response);
} else {
connection.write("*0\r\n");
}
break;
}
case "HSETNX": {
const key = command[4];
const field = command[6];
const value = command[8];
if (!dataStore[currentDatabase].has(key)) {
dataStore[currentDatabase].set(key, {});
}
const obj = dataStore[currentDatabase].get(key);
if (obj[field] === undefined) {
obj[field] = value;
connection.write(":1\r\n");
} else {
connection.write(":0\r\n");
}
break;
}
case "HMSET": {
const hash = command[1];
const fieldsAndValues = command.slice(2);
const hashExists =
dataStore[currentDatabase].hasOwnProperty(hash);
if (!hashExists) {
dataStore[currentDatabase][hash] = {};
}
for (let i = 0; i < fieldsAndValues.length; i += 2) {
const field = fieldsAndValues[i];
const value = fieldsAndValues[i + 1];
dataStore[currentDatabase][hash][field] = value;
}
connection.write("+OK\r\n");
break;
}
case "HINCRBY": {
const key = command[4];
const field = command[6];
const increment = Number(command[8]);
if (!dataStore[currentDatabase].has(key)) {
dataStore[currentDatabase].set(key, {});
}
const obj = dataStore[currentDatabase].get(key);
if (obj[field] === undefined) {
obj[field] = "0";
}
obj[field] = String(Number(obj[field]) + increment);
connection.write(
":" + obj[field].length + "\r\n" + obj[field] + "\r\n"
);
break;
}
case "HDEL": {
const key = command[4];
const fields = command.slice(6);
if (!dataStore[currentDatabase].has(key)) {
connection.write(":0\r\n");
} else {
const obj = dataStore[currentDatabase].get(key);
let count = 0;
for (const field of fields) {
if (obj.hasOwnProperty(field)) {
delete obj[field];
count += 1;
}
}
if (Object.keys(obj).length === 0) {
dataStore[currentDatabase].delete(key);
}
connection.write(":" + count + "\r\n");
}
break;
}
case "HEXISTS": {
const key = command[4];
const field = command[6];
if (!dataStore[currentDatabase].has(key)) {
connection.write(":0\r\n");
} else {
const obj = dataStore[currentDatabase].get(key);
if (obj.hasOwnProperty(field)) {
connection.write(":1\r\n");
} else {
connection.write(":0\r\n");
}
}
break;
}
case "HKEYS": {
const key = command[4];
if (!dataStore[currentDatabase].has(key)) {
connection.write("*0\r\n");
} else {
const obj = dataStore[currentDatabase].get(key);
const fields = Object.keys(obj);
let response = "*" + fields.length + "\r\n";
for (const field of fields) {
response +=
"$" + field.length + "\r\n" + field + "\r\n";
}
connection.write(response);
}
break;
}
case "HLEN": {
const key = command[4];
if (!dataStore[currentDatabase].has(key)) {
connection.write(":0\r\n");
} else {
const obj = dataStore[currentDatabase].get(key);
const count = Object.keys(obj).length;
connection.write(":" + count + "\r\n");
}
break;
}
case "HSTRLEN": {
const key = command[4];
const field = command[6];
if (!dataStore[currentDatabase].has(key)) {
connection.write(":0\r\n");
} else {
const obj = dataStore[currentDatabase].get(key);
if (obj.hasOwnProperty(field)) {
const value = obj[field];
connection.write(":" + value.length + "\r\n");
} else {
connection.write(":0\r\n");
}
}
break;
}
case "HVALS": {
const key = command[4];
if (!dataStore[currentDatabase].has(key)) {
connection.write("*0\r\n");
} else {
const obj = dataStore[currentDatabase].get(key);
const values = Object.values(obj);
let response = "*" + values.length + "\r\n";
for (const value of values as string[]) {
response +=
"$" + value.length + "\r\n" + value + "\r\n";
}
connection.write(response);
}
break;
}
case "DUMP": {
saveDataToFile(dataStore);
connection.write("+OK Saving Current State\r\n");
break;
}
default: {
connection.write("-ERR unknown command " + command[2] + "\r\n");
break;
}
}
};
let id = 0;
const handleConnection = (connection: IConnection) => {
connection.id = id;
id += 1;
connection.on("data", (data) => {
let command = parseIncomingData(data);
assignHandler(command, connection);
});
connection.on("end", () => {
console.log(`Client ${connection.id} disconnected`);
});
connection.on("error", (err) => {
console.log(`Error in connection ${connection.id}`);
console.log(err);
});
};
const server: net.Server = net.createServer(handleConnection as any);
server.listen(config.port, "0.0.0.0", () => {
console.log("Listening on port " + config.port);
});
function saveDataToFile(data) {
const binaryData = createBinaryData(data);
fs.writeFileSync(config.dumppath, binaryData);
}
process.on("SIGINT", () => {
console.log("\nTaking Snapshot !!");
saveDataToFile(dataStore);
console.log("Shutting down server ...");
process.exit(0);
});
};

Binary file not shown.

View File

@ -1,14 +1,20 @@
{
"name": "@bravo68web/stockpile",
"version": "0.1.3",
"version": "0.2.0",
"description": "A Tiny Redis Server ...",
"main": "app/bootstrap.js",
"main": "app/bootstrap.ts",
"bin": {
"stockpile": "app/bootstrap.js"
"stockpile": "dist/app/bootstrap.js"
},
"type": "module",
"scripts": {
"postinstall": "npm run build",
"dev": "node app/bootstrap.js",
"test": "./test_local.sh"
"build": "rm -rf dist && tsc",
"dev:ts": "ts-node app/bootstrap.ts",
"prettier": "prettier --write \"**/*.{js,ts,json,md}\"",
"test": "./test_local.sh",
"app": "NODE_NO_WARNINGS=1 node --es-module-specifier-resolution=node --loader ts-node/esm ./dist/app/bootstrap.js"
},
"repository": {
"type": "git",
@ -37,6 +43,10 @@
"bson": "^5.1.0"
},
"devDependencies": {
"prettier": "^2.8.4"
"@swc/wasm": "^1.3.57",
"@types/node": "^20.1.1",
"prettier": "^2.8.4",
"ts-node": "^10.9.1",
"typescript": "^5.0.4"
}
}

View File

@ -6,5 +6,5 @@
#
# DON'T EDIT THIS!
currentLoc=$(pwd)
node app/bootstrap.js init --name "stockpile-db" --port 6379 --configpath $currentLoc/stockpile.json --dumppath $currentLoc/stockpile.db
exec node app/bootstrap.js start --configpath $currentLoc/stockpile.json
yarn app init --name "stockpile-db" --port 6379 --configpath $currentLoc/stockpile.json --dumppath $currentLoc/db/stockpile.db
exec yarn app start --configpath $currentLoc/stockpile.json

23
tsconfig.json Normal file
View File

@ -0,0 +1,23 @@
{
"compilerOptions": {
"lib": ["es2018", "es5", "dom"],
"typeRoots": ["node_modules/@types", "./types"],
"resolveJsonModule": true,
"esModuleInterop": true,
"target": "ES2017",
"strict": true,
"module": "ESNext",
"moduleResolution": "node",
"outDir": "./dist",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"declaration": true,
"sourceMap": false,
"noImplicitAny": false
},
"exclude": ["./node_modules/**/*", "./build/**/*"],
"include": ["./**/*.ts", "./**/*.tsx", "./**/*.json", "./**/*.js"],
"ts-node": {
"swc": true
}
}

119
yarn.lock
View File

@ -2,12 +2,131 @@
# yarn lockfile v1
"@cspotcode/source-map-support@^0.8.0":
version "0.8.1"
resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1"
integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==
dependencies:
"@jridgewell/trace-mapping" "0.3.9"
"@jridgewell/resolve-uri@^3.0.3":
version "3.1.1"
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721"
integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==
"@jridgewell/sourcemap-codec@^1.4.10":
version "1.4.15"
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
"@jridgewell/trace-mapping@0.3.9":
version "0.3.9"
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9"
integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==
dependencies:
"@jridgewell/resolve-uri" "^3.0.3"
"@jridgewell/sourcemap-codec" "^1.4.10"
"@swc/wasm@^1.3.57":
version "1.3.57"
resolved "https://registry.yarnpkg.com/@swc/wasm/-/wasm-1.3.57.tgz#385e6fa1e36f238e78f8731b8bed6d2e4b07e304"
integrity sha512-K5E9cD+ZRDCPZoUIcem6xOEqyb6XxEepv6qm8Ck6l9KgWmvnsPc2ijMWCnGQlUYKl1Ff1WcJ+c54EFgaBHDbwQ==
"@tsconfig/node10@^1.0.7":
version "1.0.9"
resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2"
integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==
"@tsconfig/node12@^1.0.7":
version "1.0.11"
resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d"
integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==
"@tsconfig/node14@^1.0.0":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1"
integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==
"@tsconfig/node16@^1.0.2":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e"
integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==
"@types/node@^20.1.1":
version "20.1.1"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.1.1.tgz#afc492e8dbe7f672dd3a13674823522b467a45ad"
integrity sha512-uKBEevTNb+l6/aCQaKVnUModfEMjAl98lw2Si9P5y4hLu9tm6AlX2ZIoXZX6Wh9lJueYPrGPKk5WMCNHg/u6/A==
acorn-walk@^8.1.1:
version "8.2.0"
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1"
integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==
acorn@^8.4.1:
version "8.8.2"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a"
integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==
arg@^4.1.0:
version "4.1.3"
resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==
bson@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/bson/-/bson-5.1.0.tgz#7b15cd9aa012b8bf9d320fbaefe15cc2fb657de2"
integrity sha512-FEecNHkhYRBe7X9KDkdG12xNuz5VHGeH6mCE0B5sBmYtiR/Ux/9vUH/v4NUoBCDr6NuEhvahjoLiiRogptVW0A==
create-require@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
diff@^4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
make-error@^1.1.1:
version "1.3.6"
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
prettier@^2.8.4:
version "2.8.4"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.4.tgz#34dd2595629bfbb79d344ac4a91ff948694463c3"
integrity sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw==
ts-node@^10.9.1:
version "10.9.1"
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b"
integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==
dependencies:
"@cspotcode/source-map-support" "^0.8.0"
"@tsconfig/node10" "^1.0.7"
"@tsconfig/node12" "^1.0.7"
"@tsconfig/node14" "^1.0.0"
"@tsconfig/node16" "^1.0.2"
acorn "^8.4.1"
acorn-walk "^8.1.1"
arg "^4.1.0"
create-require "^1.1.0"
diff "^4.0.1"
make-error "^1.1.1"
v8-compile-cache-lib "^3.0.1"
yn "3.1.1"
typescript@^5.0.4:
version "5.0.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.0.4.tgz#b217fd20119bd61a94d4011274e0ab369058da3b"
integrity sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==
v8-compile-cache-lib@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf"
integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==
yn@3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==