Releasing v0.2.0
This commit is contained in:
parent
78cdc82e61
commit
e58f9866e7
|
@ -4,4 +4,5 @@ testBox.js
|
|||
node_modules
|
||||
*.dat
|
||||
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)
|
||||
[![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
|
||||
```
|
||||
|
||||
|
|
|
@ -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 = {};
|
||||
// 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 };
|
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",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
"@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==
|
||||
|
|
Loading…
Reference in New Issue