Releasing v0.2.0
This commit is contained in:
parent
78cdc82e61
commit
e58f9866e7
|
@ -4,4 +4,5 @@ testBox.js
|
||||||
node_modules
|
node_modules
|
||||||
*.dat
|
*.dat
|
||||||
config.json
|
config.json
|
||||||
stockpile.json
|
stockpile.json
|
||||||
|
dist/
|
31
README.md
31
README.md
|
@ -6,7 +6,7 @@
|
||||||
[![License: MIT](https://img.shields.io/github/license/BRAVO68WEB/stockpile)](https://github.com/BRAVO68WEB/stockpile/blob/master/LICENSE)
|
[![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)
|
[![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.
|
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
|
## Usage
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone https://github.com/BRAVO68WEB/stockpile
|
||||||
|
cd stockpile
|
||||||
|
yarn
|
||||||
|
./spawn_redis_server.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
OR
|
||||||
|
|
||||||
|
>Not test with ts release
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npx @bravo68web/stockpile help
|
npx @bravo68web/stockpile help
|
||||||
```
|
```
|
||||||
|
@ -22,6 +33,22 @@ npx @bravo68web/stockpile help
|
||||||
|
|
||||||
- [Node.js](https://github.com/BRAVO68WEB/stockpile-node-sdk)
|
- [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
|
## Development
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
@ -32,7 +59,7 @@ cd stockpile
|
||||||
## Run tests
|
## Run tests
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
node app/main.js &
|
./spawn_redis_server.sh &
|
||||||
./test_local.sh
|
./test_local.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -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");
|
|
||||||
}
|
|
|
@ -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);
|
||||||
|
}
|
|
@ -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 = {};
|
const data = {};
|
||||||
// Add each map in the array to the data object
|
// Add each map in the array to the data object
|
||||||
for (let i = 0; i < mapData.length; i++) {
|
for (let i = 0; i < mapData.length; i++) {
|
||||||
|
@ -12,25 +15,18 @@ function createBinaryData(mapData) {
|
||||||
|
|
||||||
function readBinaryData(buffer) {
|
function readBinaryData(buffer) {
|
||||||
const data = BSON.deserialize(buffer);
|
const data = BSON.deserialize(buffer);
|
||||||
const mapData = [];
|
const mapData: MapData = [];
|
||||||
|
|
||||||
// Convert each object in the data object to a Map
|
// 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 obj = data[key];
|
||||||
const map = new Map();
|
const map = new Map();
|
||||||
for (const [key, value] of Object.entries(obj)) {
|
for (const [key, value] of Object.entries(obj)) {
|
||||||
if (typeof value === "object") {
|
map.set(key, value);
|
||||||
map.set(key, value);
|
|
||||||
} else {
|
|
||||||
map.set(key, value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
mapData.push(map);
|
mapData.push(map);
|
||||||
}
|
}
|
||||||
|
|
||||||
return mapData;
|
return mapData;
|
||||||
}
|
}
|
||||||
module.exports = {
|
export { createBinaryData, readBinaryData };
|
||||||
createBinaryData,
|
|
||||||
readBinaryData,
|
|
||||||
};
|
|
533
app/main.js
533
app/main.js
|
@ -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);
|
|
||||||
});
|
|
||||||
};
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
};
|
BIN
db/stockpile.db
BIN
db/stockpile.db
Binary file not shown.
20
package.json
20
package.json
|
@ -1,14 +1,20 @@
|
||||||
{
|
{
|
||||||
"name": "@bravo68web/stockpile",
|
"name": "@bravo68web/stockpile",
|
||||||
"version": "0.1.3",
|
"version": "0.2.0",
|
||||||
"description": "A Tiny Redis Server ...",
|
"description": "A Tiny Redis Server ...",
|
||||||
"main": "app/bootstrap.js",
|
"main": "app/bootstrap.ts",
|
||||||
"bin": {
|
"bin": {
|
||||||
"stockpile": "app/bootstrap.js"
|
"stockpile": "dist/app/bootstrap.js"
|
||||||
},
|
},
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"postinstall": "npm run build",
|
||||||
"dev": "node app/bootstrap.js",
|
"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": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -37,6 +43,10 @@
|
||||||
"bson": "^5.1.0"
|
"bson": "^5.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,5 +6,5 @@
|
||||||
#
|
#
|
||||||
# DON'T EDIT THIS!
|
# DON'T EDIT THIS!
|
||||||
currentLoc=$(pwd)
|
currentLoc=$(pwd)
|
||||||
node app/bootstrap.js init --name "stockpile-db" --port 6379 --configpath $currentLoc/stockpile.json --dumppath $currentLoc/stockpile.db
|
yarn app init --name "stockpile-db" --port 6379 --configpath $currentLoc/stockpile.json --dumppath $currentLoc/db/stockpile.db
|
||||||
exec node app/bootstrap.js start --configpath $currentLoc/stockpile.json
|
exec yarn app start --configpath $currentLoc/stockpile.json
|
||||||
|
|
|
@ -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
119
yarn.lock
|
@ -2,12 +2,131 @@
|
||||||
# yarn lockfile v1
|
# 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:
|
bson@^5.1.0:
|
||||||
version "5.1.0"
|
version "5.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/bson/-/bson-5.1.0.tgz#7b15cd9aa012b8bf9d320fbaefe15cc2fb657de2"
|
resolved "https://registry.yarnpkg.com/bson/-/bson-5.1.0.tgz#7b15cd9aa012b8bf9d320fbaefe15cc2fb657de2"
|
||||||
integrity sha512-FEecNHkhYRBe7X9KDkdG12xNuz5VHGeH6mCE0B5sBmYtiR/Ux/9vUH/v4NUoBCDr6NuEhvahjoLiiRogptVW0A==
|
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:
|
prettier@^2.8.4:
|
||||||
version "2.8.4"
|
version "2.8.4"
|
||||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.4.tgz#34dd2595629bfbb79d344ac4a91ff948694463c3"
|
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.4.tgz#34dd2595629bfbb79d344ac4a91ff948694463c3"
|
||||||
integrity sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw==
|
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==
|
||||||
|
|
Loading…
Reference in New Issue