Compare commits

...

3 Commits

Author SHA1 Message Date
Vitaliy Filippov 49e9fdbfa3 Implement client 2024-05-07 15:37:52 +03:00
Vitaliy Filippov 143456a513 Add ESLint 2024-05-07 15:37:52 +03:00
Vitaliy Filippov 210babaccc Add forgotten package.json 2024-05-07 15:37:52 +03:00
3 changed files with 302 additions and 0 deletions

49
.eslintrc.js Normal file
View File

@ -0,0 +1,49 @@
module.exports = {
"env": {
"es6": true,
"node": true
},
"extends": [
"eslint:recommended",
"plugin:node/recommended"
],
"parserOptions": {
"ecmaVersion": 2020
},
"plugins": [
],
"rules": {
"indent": [
"error",
4
],
"brace-style": [
"error",
"allman",
{ "allowSingleLine": true }
],
"linebreak-style": [
"error",
"unix"
],
"semi": [
"error",
"always"
],
"no-useless-escape": [
"off"
],
"no-control-regex": [
"off"
],
"no-empty": [
"off"
],
"no-process-exit": [
"off"
],
"node/shebang": [
"off"
]
}
};

224
anticli.js Normal file
View File

@ -0,0 +1,224 @@
#!/usr/bin/node
const fsp = require('fs').promises;
const http = require('http');
const https = require('https');
class AntiEtcdCli
{
static parse(args)
{
const cmd = [];
const options = {};
for (let i = 2; i < args.length; i++)
{
const arg = args[i].toLowerCase().replace(/^--(.+)$/, (m, m1) => '--'+m1.replace(/-/g, '_'));
if (arg === '-h' || arg === '--help')
{
console.error(
'USAGE:\n'+
' anticli.js [OPTIONS] put <key> [<value>]\n'+
' anticli.js [OPTIONS] get <key> [-p|--prefix] [-v|--print-value-only] [-k|--keys-only]\n'+
' anticli.js [OPTIONS] del <key> [-p|--prefix]\n'+
'OPTIONS:\n'+
' [--endpoints|-e http://node1:2379,http://node2:2379,http://node3:2379]\n'+
' [--timeout 1000]'
);
process.exit();
}
else if (arg == '-e' || arg == '--endpoints')
{
options['endpoints'] = args[++i].split(/\s*[,\s]+\s*/);
}
else if (arg == '-p' || arg == '--prefix')
{
options['prefix'] = true;
}
else if (arg == '-v' || arg == '--print_value_only')
{
options['print_value_only'] = true;
}
else if (arg == '-k' || arg == '--keys_only')
{
options['keys_only'] = true;
}
else if (arg[0] == '-' && arg[1] !== '-')
{
console.error('Unknown option '+arg);
process.exit(1);
}
else if (arg.substr(0, 2) == '--')
{
options[arg.substr(2)] = args[++i];
}
else
{
cmd.push(arg);
}
}
if (!cmd.length || cmd[0] != 'get' && cmd[0] != 'put' && cmd[0] != 'del')
{
console.log('Supported commands: get, put, del. Use --help to see details');
process.exit(1);
}
return [ cmd, options ];
}
async run(cmd, options)
{
this.options = options;
if (!this.options.endpoints)
{
this.options.endpoints = [ 'http://localhost:2379' ];
}
if (cmd[0] == 'get')
{
await this.get(cmd.slice(1));
}
else if (cmd[0] == 'put')
{
await this.put(cmd[1], cmd.length > 2 ? cmd[2] : undefined);
}
else if (cmd[0] == 'del')
{
await this.del(cmd.slice(1));
}
}
async get(keys)
{
if (this.options.prefix)
{
keys = keys.map(k => k.replace(/\/+$/, ''));
}
const txn = { success: keys.map(key => ({ request_range: this.options.prefix ? { key: b64(key+'/'), range_end: b64(key+'0') } : { key: b64(key) } })) };
const res = await this.request('/v3/kv/txn', txn);
for (const r of (res||{}).responses||[])
{
if (r.response_range)
{
for (const kv of r.response_range.kvs)
{
if (!this.options.print_value_only)
{
console.log(de64(kv.key));
}
if (!this.options.keys_only)
{
console.log(de64(kv.value));
}
}
}
}
}
async put(key, value)
{
if (value === undefined)
{
value = await fsp.readFile(0, { encoding: 'utf-8' });
}
const res = await this.request('/v3/kv/put', { key: b64(key), value: b64(value) });
if (res && res.header)
{
console.log('OK');
}
}
async del(keys)
{
if (this.options.prefix)
{
keys = keys.map(k => k.replace(/\/+$/, ''));
}
const txn = { success: keys.map(key => ({ request_delete_range: this.options.prefix ? { key: b64(key+'/'), range_end: b64(key+'0') } : { key: b64(key) } })) };
const res = await this.request('/v3/kv/txn', txn);
for (const r of (res||{}).responses||[])
{
if (r.response_delete_range)
{
console.log(r.response_delete_range.deleted);
}
}
}
async request(path, body)
{
for (const url of this.options.endpoints)
{
const cur_url = url.replace(/\/+$/, '')+path;
try
{
return (await POST(cur_url, body, this.options.timeout||1000)).json;
}
catch (e)
{
console.error(cur_url+': '+e.message);
}
}
return null;
}
}
function POST(url, body, timeout)
{
return new Promise(ok =>
{
const body_text = Buffer.from(JSON.stringify(body));
let timer_id = timeout > 0 ? setTimeout(() =>
{
if (req)
req.abort();
req = null;
ok({ error: 'timeout' });
}, timeout) : null;
let req = (url.substr(0, 6).toLowerCase() == 'https://' ? https : http).request(url, { method: 'POST', headers: {
'Content-Type': 'application/json',
'Content-Length': body_text.length,
} }, (res) =>
{
if (!req)
{
return;
}
clearTimeout(timer_id);
let res_body = '';
res.setEncoding('utf8');
res.on('error', (error) => ok({ error }));
res.on('data', chunk => { res_body += chunk; });
res.on('end', () =>
{
if (res.statusCode != 200)
{
ok({ error: res_body, code: res.statusCode });
return;
}
try
{
res_body = JSON.parse(res_body);
ok({ response: res, json: res_body });
}
catch (e)
{
ok({ error: e, response: res, body: res_body });
}
});
});
req.on('error', (error) => ok({ error }));
req.on('close', () => ok({ error: new Error('Connection closed prematurely') }));
req.write(body_text);
req.end();
});
}
function b64(str)
{
return Buffer.from(str).toString('base64');
}
function de64(str)
{
return Buffer.from(str, 'base64').toString();
}
new AntiEtcdCli().run(...AntiEtcdCli.parse(process.argv)).catch(console.error);

29
package.json Normal file
View File

@ -0,0 +1,29 @@
{
"name": "tinyraft",
"version": "1.0.0",
"description": "Tiny & abstract Raft leader election algorithm",
"main": "tinyraft.js",
"scripts": {
"lint": "eslint common.js anticli.js antipersistence.js anticluster.js antietcd.js etctree.js etctree.spec.js tinyraft.js tinyraft.spec.js",
"test": "node etctree.spec.js && node tinyraft.spec.js"
},
"repository": {
"type": "git",
"url": "https://git.yourcmc.ru/vitalif/tinyraft"
},
"keywords": [
"raft"
],
"author": "Vitaliy Filippov",
"license": "MPL-2.0",
"engines": {
"node": ">=12.0.0"
},
"devDependencies": {
"eslint": "^8.0.0",
"eslint-plugin-node": "^11.1.0"
},
"dependencies": {
"ws": "^8.17.0"
}
}