antietcd 1.0.8 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -105,6 +105,9 @@ Specify &lt;ca&gt; = &lt;cert&gt; if your certificate is self-signed.</dd>
105
105
  <dt>--ws_keepalive_interval 30000</dt>
106
106
  <dd>Client websocket ping (keepalive) interval in milliseconds</dd>
107
107
 
108
+ <dt>--use_base64 1</dt>
109
+ <dd>Use base64 encoding of keys and values, like in etcd (enabled by default).</dd>
110
+
108
111
  </dl>
109
112
 
110
113
  ### Persistence
package/anticluster.js CHANGED
@@ -226,7 +226,10 @@ class AntiCluster
226
226
  }
227
227
  else
228
228
  {
229
- this._log('Got dump from '+client.raft_node_id+' with stored term '+res.term);
229
+ this._log(
230
+ 'Got dump from '+client.raft_node_id+' with stored term '+res.term+
231
+ ', mod_revision '+res.mod_revision+', compact_revision '+res.compact_revision
232
+ );
230
233
  }
231
234
  this.resync_state.dumps[client.raft_node_id] = res.error ? null : res;
232
235
  this._continueResync();
@@ -325,16 +328,19 @@ class AntiCluster
325
328
  this.antietcd.stored_term = this.raft.term;
326
329
  this.synced = true;
327
330
  runCallbacks(this, 'wait_sync', []);
328
- this._log('Synchronized with followers, new term is '+this.raft.term);
331
+ this._log(
332
+ 'Synchronized with followers, new term is '+this.raft.term+
333
+ ', mod_revision '+this.antietcd.etctree.mod_revision+', compact_revision '+this.antietcd.etctree.compact_revision
334
+ );
329
335
  }
330
336
 
331
337
  _isWrite(path, data)
332
338
  {
333
339
  if (path == 'kv_txn')
334
340
  {
335
- return ((!data.compare || !data.compare.length) &&
336
- (!data.success || !data.success.filter(f => f.request_put || f.requestPut || f.request_delete_range || f.requestDeleteRange).length) &&
337
- (!data.failure || !data.failure.filter(f => f.request_put || f.requestPut || f.request_delete_range || f.requestDeleteRange).length));
341
+ return (data.compare && data.compare.length ||
342
+ data.success && data.success.filter(f => f.request_put || f.requestPut || f.request_delete_range || f.requestDeleteRange).length ||
343
+ data.failure && data.failure.filter(f => f.request_put || f.requestPut || f.request_delete_range || f.requestDeleteRange).length);
338
344
  }
339
345
  return path != 'kv_range';
340
346
  }
@@ -452,7 +458,10 @@ class AntiCluster
452
458
  this.antietcd.stored_term = msg.term;
453
459
  this.synced = true;
454
460
  runCallbacks(this, 'wait_sync', []);
455
- this._log('Synchronized with leader, new term is '+msg.term);
461
+ this._log(
462
+ 'Synchronized with leader, new term is '+this.raft.term+
463
+ ', mod_revision '+this.antietcd.etctree.mod_revision+', compact_revision '+this.antietcd.etctree.compact_revision
464
+ );
456
465
  client.socket.send(JSON.stringify({ request_id: msg.request_id, reply: {} }));
457
466
  }
458
467
  else
package/antietcd-app.js CHANGED
@@ -12,10 +12,10 @@ License: Mozilla Public License 2.0 or Vitastor Network Public License 1.1
12
12
 
13
13
  Usage:
14
14
 
15
- ${process.argv[0]} ${process.argv[1]} \
16
- [--cert ssl.crt] [--key ssl.key] [--port 12379] \
17
- [--data data.gz] [--persist-filter ./filter.js] [--persist_interval 500] \
18
- [--node_id node1 --cluster_key abcdef --cluster node1=http://localhost:12379,node2=http://localhost:12380,node3=http://localhost:12381] \
15
+ ${process.argv[0]} ${process.argv[1]} \n\
16
+ [--cert ssl.crt] [--key ssl.key] [--port 12379] \n\
17
+ [--data data.gz] [--persist-filter ./filter.js] [--persist_interval 500] \n\
18
+ [--node_id node1 --cluster_key abcdef --cluster node1=http://localhost:12379,node2=http://localhost:12380,node3=http://localhost:12381] \n\
19
19
  [other options]
20
20
 
21
21
  Supported etcd REST APIs:
@@ -43,6 +43,11 @@ HTTP:
43
43
  Require TLS client certificates signed by <ca> or by default CA to connect.
44
44
  --ws_keepalive_interval 30000
45
45
  Client websocket ping (keepalive) interval in milliseconds
46
+ --merge_watches 1
47
+ Antietcd merges all watcher events into a single websocket message to provide
48
+ more ordering/transaction guarantees. Set to 0 to disable this behaviour.
49
+ --use_base64 1
50
+ Use base64 encoding of keys and values, like in etcd (enabled by default).
46
51
 
47
52
  Persistence:
48
53
 
@@ -105,7 +110,6 @@ function parse()
105
110
  options[arg.substr(2)] = process.argv[++i];
106
111
  }
107
112
  }
108
- options['stale_read'] = options['stale_read'] === '1' || options['stale_read'] === 'yes' || options['stale_read'] === 'true';
109
113
  if (options['persist_filter'])
110
114
  {
111
115
  options['persist_filter'] = require(options['persist_filter'])(options);
package/antietcd.js CHANGED
@@ -15,15 +15,18 @@ const ws = require('ws');
15
15
  const EtcTree = require('./etctree.js');
16
16
  const AntiPersistence = require('./antipersistence.js');
17
17
  const AntiCluster = require('./anticluster.js');
18
- const { runCallbacks, RequestError } = require('./common.js');
18
+ const { runCallbacks, de64, b64, RequestError } = require('./common.js');
19
19
 
20
- const VERSION = '1.0.8';
20
+ const VERSION = '1.1.0';
21
21
 
22
22
  class AntiEtcd extends EventEmitter
23
23
  {
24
24
  constructor(cfg)
25
25
  {
26
26
  super();
27
+ cfg['merge_watches'] = !('merge_watches' in cfg) || is_true(cfg['merge_watches']);
28
+ cfg['stale_read'] = !('stale_read' in cfg) || is_true(cfg['stale_read']);
29
+ cfg['use_base64'] = !('use_base64' in cfg) || is_true(cfg['use_base64']);
27
30
  this.clients = {};
28
31
  this.client_id = 1;
29
32
  this.etctree = new EtcTree(true);
@@ -74,7 +77,7 @@ class AntiEtcd extends EventEmitter
74
77
  {
75
78
  this.server = http.createServer((req, res) => this._handleRequest(req, res));
76
79
  }
77
- this.wss = new ws.WebSocketServer({ server: this.server });
80
+ this.wss = new ws.Server({ server: this.server });
78
81
  // eslint-disable-next-line no-unused-vars
79
82
  this.wss.on('connection', (conn, req) => this._startWebsocket(conn, null));
80
83
  this.server.listen(this.cfg.port || 2379, this.cfg.ip || undefined);
@@ -120,7 +123,7 @@ class AntiEtcd extends EventEmitter
120
123
  let done = 0;
121
124
  await new Promise((allOk, allNo) =>
122
125
  {
123
- res.map(promise => promise.then(res =>
126
+ res.map(promise => promise.then(r =>
124
127
  {
125
128
  if ((++done) == res.length)
126
129
  allOk();
@@ -411,9 +414,9 @@ class AntiEtcd extends EventEmitter
411
414
  }
412
415
 
413
416
  // public watch API
414
- async create_watch(params, callback)
417
+ async create_watch(params, callback, stream_id)
415
418
  {
416
- const watch = this.etctree.api_create_watch({ ...params, watch_id: null }, callback);
419
+ const watch = this.etctree.api_create_watch(this._encodeWatch(params), (msg) => callback(this._encodeMsg(msg)), stream_id);
417
420
  if (!watch.created)
418
421
  {
419
422
  throw new RequestError(400, 'Requested watch revision is compacted', { compact_revision: watch.compact_revision });
@@ -437,24 +440,64 @@ class AntiEtcd extends EventEmitter
437
440
  // internal handlers
438
441
  async _handle_kv_txn(data)
439
442
  {
440
- return await this.etctree.api_txn(data);
443
+ if (this.cfg.use_base64)
444
+ {
445
+ for (const item of data.compare||[])
446
+ {
447
+ if (item.key != null)
448
+ item.key = de64(item.key);
449
+ }
450
+ for (const items of [ data.success, data.failure ])
451
+ {
452
+ for (const item of items||[])
453
+ {
454
+ const req = item.request_range || item.requestRange ||
455
+ item.request_put || item.requestPut ||
456
+ item.request_delete_range || item.requestDeleteRange;
457
+ if (req.key != null)
458
+ req.key = de64(req.key);
459
+ if (req.range_end != null)
460
+ req.range_end = de64(req.range_end);
461
+ if (req.value != null)
462
+ req.value = de64(req.value);
463
+ }
464
+ }
465
+ }
466
+ const result = await this.etctree.api_txn(data);
467
+ if (this.cfg.use_base64)
468
+ {
469
+ for (const item of result.responses||[])
470
+ {
471
+ if (item.response_range)
472
+ {
473
+ for (const kv of item.response_range.kvs)
474
+ {
475
+ if (kv.key != null)
476
+ kv.key = b64(kv.key);
477
+ if (kv.value != null)
478
+ kv.value = b64(kv.value);
479
+ }
480
+ }
481
+ }
482
+ }
483
+ return result;
441
484
  }
442
485
 
443
486
  async _handle_kv_range(data)
444
487
  {
445
- const r = await this.etctree.api_txn({ success: [ { request_range: data } ] });
488
+ const r = await this._handle_kv_txn({ success: [ { request_range: data } ] });
446
489
  return { header: r.header, ...r.responses[0].response_range };
447
490
  }
448
491
 
449
492
  async _handle_kv_put(data)
450
493
  {
451
- const r = await this.etctree.api_txn({ success: [ { request_put: data } ] });
494
+ const r = await this._handle_kv_txn({ success: [ { request_put: data } ] });
452
495
  return { header: r.header, ...r.responses[0].response_put };
453
496
  }
454
497
 
455
498
  async _handle_kv_deleterange(data)
456
499
  {
457
- const r = await this.etctree.api_txn({ success: [ { request_delete_range: data } ] });
500
+ const r = await this._handle_kv_txn({ success: [ { request_delete_range: data } ] });
458
501
  return { header: r.header, ...r.responses[0].response_delete_range };
459
502
  }
460
503
 
@@ -478,7 +521,7 @@ class AntiEtcd extends EventEmitter
478
521
  return this.etctree.api_keepalive_lease(data);
479
522
  }
480
523
 
481
- _handle_maintenance_status(data)
524
+ _handle_maintenance_status(/*data*/)
482
525
  {
483
526
  const raft = this.cluster && this.cluster.raft;
484
527
  return {
@@ -516,8 +559,9 @@ class AntiEtcd extends EventEmitter
516
559
  const create_request = msg.create_request;
517
560
  if (!create_request.watch_id || !client.watches[create_request.watch_id])
518
561
  {
562
+ client.send_cb = client.send_cb || (msg => socket.send(JSON.stringify(this._encodeMsg(msg))));
519
563
  const watch = this.etctree.api_create_watch(
520
- { ...create_request, watch_id: null }, (msg) => socket.send(JSON.stringify(msg))
564
+ this._encodeWatch(create_request), client.send_cb, (this.cfg.merge_watches ? 'C'+client_id : null)
521
565
  );
522
566
  if (!watch.created)
523
567
  {
@@ -555,6 +599,35 @@ class AntiEtcd extends EventEmitter
555
599
  }
556
600
  }
557
601
 
602
+ _encodeWatch(create_request)
603
+ {
604
+ const req = { ...create_request, watch_id: null };
605
+ if (this.cfg.use_base64)
606
+ {
607
+ if (req.key != null)
608
+ req.key = de64(req.key);
609
+ if (req.range_end != null)
610
+ req.range_end = de64(req.range_end);
611
+ }
612
+ return req;
613
+ }
614
+
615
+ _encodeMsg(msg)
616
+ {
617
+ if (this.cfg.use_base64 && msg.result && msg.result.events)
618
+ {
619
+ return { ...msg, result: { ...msg.result, events: msg.result.events.map(ev => ({
620
+ ...ev,
621
+ kv: !ev.kv ? ev.kv : {
622
+ ...ev.kv,
623
+ key: b64(ev.kv.key),
624
+ value: b64(ev.kv.value),
625
+ },
626
+ })) } };
627
+ }
628
+ return msg;
629
+ }
630
+
558
631
  _unsubscribeClient(client_id)
559
632
  {
560
633
  if (!this.clients[client_id])
@@ -569,6 +642,11 @@ class AntiEtcd extends EventEmitter
569
642
  }
570
643
  }
571
644
 
645
+ function is_true(s)
646
+ {
647
+ return s === true || s === 1 || s === '1' || s === 'yes' || s === 'true' || s === 'on';
648
+ }
649
+
572
650
  AntiEtcd.RequestError = RequestError;
573
651
 
574
652
  AntiEtcd.VERSION = VERSION;
@@ -8,7 +8,7 @@ const zlib = require('zlib');
8
8
 
9
9
  const stableStringify = require('./stable-stringify.js');
10
10
  const EtcTree = require('./etctree.js');
11
- const { de64, runCallbacks } = require('./common.js');
11
+ const { runCallbacks } = require('./common.js');
12
12
 
13
13
  class AntiPersistence
14
14
  {
@@ -60,20 +60,18 @@ class AntiPersistence
60
60
  if (ev.lease)
61
61
  {
62
62
  // Values with lease are never persisted
63
- const key = de64(ev.key);
64
- if (this.prev_value[key] !== undefined)
63
+ if (this.prev_value[ev.key] !== undefined)
65
64
  {
66
- delete this.prev_value[key];
65
+ delete this.prev_value[ev.key];
67
66
  changed = true;
68
67
  }
69
68
  }
70
69
  else
71
70
  {
72
- const key = de64(ev.key);
73
- const filtered = this.cfg.persist_filter(key, ev.value == null ? undefined : de64(ev.value));
74
- if (!EtcTree.eq(filtered, this.prev_value[key]))
71
+ const filtered = this.cfg.persist_filter(ev.key, ev.value == null ? undefined : ev.value);
72
+ if (!EtcTree.eq(filtered, this.prev_value[ev.key]))
75
73
  {
76
- this.prev_value[key] = filtered;
74
+ this.prev_value[ev.key] = filtered;
77
75
  changed = true;
78
76
  }
79
77
  }
@@ -116,7 +114,7 @@ class AntiPersistence
116
114
  this.wait_persist = [];
117
115
  try
118
116
  {
119
- let dump = this.antietcd.etctree.dump(true);
117
+ let dump = this.antietcd.etctree.dump(true, this.cfg.persist_filter);
120
118
  dump['term'] = this.antietcd.stored_term;
121
119
  dump = stableStringify(dump);
122
120
  dump = await new Promise((ok, no) => zlib.gzip(dump, (err, res) => err ? no(err) : ok(res)));
package/common.js CHANGED
@@ -19,6 +19,13 @@ function de64(k)
19
19
  return Buffer.from(k, 'base64').toString();
20
20
  }
21
21
 
22
+ function b64(k)
23
+ {
24
+ if (k == null) // null or undefined
25
+ return k;
26
+ return Buffer.from(k).toString('base64');
27
+ }
28
+
22
29
  function runCallbacks(obj, key, new_value)
23
30
  {
24
31
  const cbs = obj[key];
@@ -35,5 +42,6 @@ function runCallbacks(obj, key, new_value)
35
42
  module.exports = {
36
43
  RequestError,
37
44
  de64,
45
+ b64,
38
46
  runCallbacks,
39
47
  };
package/etctree.js CHANGED
@@ -19,7 +19,7 @@ const { RequestError } = require('./common.js');
19
19
 
20
20
  class EtcTree
21
21
  {
22
- constructor(use_base64)
22
+ constructor()
23
23
  {
24
24
  this.state = {};
25
25
  this.leases = {};
@@ -27,7 +27,6 @@ class EtcTree
27
27
  this.watcher_id = 0;
28
28
  this.mod_revision = 0;
29
29
  this.compact_revision = 0;
30
- this.use_base64 = use_base64;
31
30
  this.replicate = null;
32
31
  this.paused = false;
33
32
  this.active_immediate = [];
@@ -51,23 +50,9 @@ class EtcTree
51
50
  this.replicate = replicate;
52
51
  }
53
52
 
54
- de64(k)
55
- {
56
- if (k == null) // null or undefined
57
- return k;
58
- return this.use_base64 ? Buffer.from(k, 'base64').toString() : k;
59
- }
60
-
61
- b64(k)
62
- {
63
- if (k == null) // null or undefined
64
- return k;
65
- return this.use_base64 ? Buffer.from(k).toString('base64') : k;
66
- }
67
-
68
53
  _check(chk)
69
54
  {
70
- const parts = this._key_parts(this.de64(chk.key));
55
+ const parts = this._key_parts(chk.key);
71
56
  const { cur } = this._get_subtree(parts, false, false);
72
57
  let check_value, ref_value;
73
58
  if (chk.target === 'MOD')
@@ -118,8 +103,8 @@ class EtcTree
118
103
 
119
104
  _get_range(req)
120
105
  {
121
- const key = this.de64(req.key);
122
- const end = this.de64(req.range_end);
106
+ const key = req.key;
107
+ const end = req.range_end;
123
108
  if (end != null && (key !== '' && end !== '') && (key[key.length-1] != '/' || end[end.length-1] != '0' ||
124
109
  end.substr(0, end.length-1) !== key.substr(0, key.length-1)))
125
110
  {
@@ -168,7 +153,7 @@ class EtcTree
168
153
  dump(persistent_only, value_filter)
169
154
  {
170
155
  const snapshot = {
171
- state: this._copy_tree(this.state, persistent_only, value_filter) || {},
156
+ state: this._copy_tree(this.state, null, persistent_only, value_filter) || {},
172
157
  mod_revision: this.mod_revision,
173
158
  compact_revision: this.compact_revision,
174
159
  };
@@ -184,13 +169,13 @@ class EtcTree
184
169
  return snapshot;
185
170
  }
186
171
 
187
- _copy_tree(cur, no_lease, value_filter)
172
+ _copy_tree(cur, key, no_lease, value_filter)
188
173
  {
189
174
  let nonempty = cur.value != null && (!no_lease || !cur.lease);
190
175
  let filtered;
191
176
  if (nonempty && value_filter)
192
177
  {
193
- filtered = value_filter(cur.value);
178
+ filtered = value_filter(key === null ? '' : key, cur.value);
194
179
  nonempty = nonempty && filtered != null;
195
180
  }
196
181
  const copy = (nonempty ? { ...cur } : {});
@@ -204,7 +189,7 @@ class EtcTree
204
189
  let has_children = false;
205
190
  for (const k in cur.children)
206
191
  {
207
- const child = this._copy_tree(cur.children[k], no_lease, value_filter);
192
+ const child = this._copy_tree(cur.children[k], key === null ? k : key+'/'+k, no_lease, value_filter);
208
193
  if (child)
209
194
  {
210
195
  copy.children[k] = child;
@@ -279,7 +264,7 @@ class EtcTree
279
264
  {
280
265
  cur_old.value = cur_new.value;
281
266
  const key_watchers = (cur_old.key_watchers ? [ ...watchers, ...(cur_old.key_watchers||[]) ] : watchers);
282
- const notify = { watchers: key_watchers, key: this.b64(key), value: this.b64(cur_new.value), mod_revision: cur_new.mod_revision };
267
+ const notify = { watchers: key_watchers, key, value: cur_new.value, mod_revision: cur_new.mod_revision };
283
268
  if (cur_new.lease)
284
269
  {
285
270
  notify.lease = cur_new.lease;
@@ -444,7 +429,7 @@ class EtcTree
444
429
  }
445
430
  for (const key in this.leases[id].keys)
446
431
  {
447
- this._delete_range({ key: this.use_base64 ? this.b64(key) : key }, next_revision, notifications);
432
+ this._delete_range({ key }, next_revision, notifications);
448
433
  }
449
434
  if (this.leases[id].timer_id)
450
435
  {
@@ -544,7 +529,7 @@ class EtcTree
544
529
  }
545
530
  }
546
531
 
547
- api_create_watch(req, send)
532
+ api_create_watch(req, send, stream_id)
548
533
  {
549
534
  const { parts, all } = this._get_range(req);
550
535
  if (req.start_revision && this.compact_revision && this.compact_revision > req.start_revision)
@@ -566,6 +551,7 @@ class EtcTree
566
551
  this.watchers[watch_id] = {
567
552
  paths: [],
568
553
  send,
554
+ stream_id,
569
555
  };
570
556
  }
571
557
  this.watchers[watch_id].paths.push(parts);
@@ -602,9 +588,9 @@ class EtcTree
602
588
  {
603
589
  const ev = {
604
590
  type: cur.value == null ? 'DELETE' : 'PUT',
605
- kv: cur.value == null ? { key: this.b64(prefix === null ? '' : prefix) } : {
606
- key: this.b64(prefix),
607
- value: this.b64(cur.value),
591
+ kv: cur.value == null ? { key: (prefix === null ? '' : prefix) } : {
592
+ key: prefix,
593
+ value: cur.value,
608
594
  mod_revision: cur.mod_revision,
609
595
  },
610
596
  };
@@ -667,15 +653,15 @@ class EtcTree
667
653
  {
668
654
  if (this.watchers[wid])
669
655
  {
670
- by_watcher[wid] = by_watcher[wid] || { header: { revision: this.mod_revision }, events: {} };
671
- by_watcher[wid].events[notif.key] = conv;
656
+ const stream_id = this.watchers[wid].stream_id || wid;
657
+ by_watcher[stream_id] = by_watcher[stream_id] || { send: this.watchers[wid].send, events: {} };
658
+ by_watcher[stream_id].events[notif.key] = conv;
672
659
  }
673
660
  }
674
661
  }
675
- for (const wid in by_watcher)
662
+ for (const stream_id in by_watcher)
676
663
  {
677
- by_watcher[wid].events = Object.values(by_watcher[wid].events);
678
- this.watchers[wid].send({ result: by_watcher[wid] });
664
+ by_watcher[stream_id].send({ result: { header: { revision: this.mod_revision }, events: Object.values(by_watcher[stream_id].events) } });
679
665
  }
680
666
  }
681
667
 
@@ -732,9 +718,9 @@ class EtcTree
732
718
  _put(request_put, cur_revision, notifications)
733
719
  {
734
720
  // FIXME: prev_kv, ignore_value(?), ignore_lease(?)
735
- const parts = this._key_parts(this.de64(request_put.key));
721
+ const parts = this._key_parts(request_put.key);
736
722
  const key = parts.join('/');
737
- const value = this.de64(request_put.value);
723
+ const value = request_put.value;
738
724
  const { cur, watchers } = this._get_subtree(parts, true, true);
739
725
  if (cur.key_watchers)
740
726
  {
@@ -767,7 +753,7 @@ class EtcTree
767
753
  cur.create_revision = cur_revision;
768
754
  }
769
755
  cur.value = value;
770
- const notify = { watchers, key: this.b64(key), value: this.b64(value), mod_revision: cur.mod_revision };
756
+ const notify = { watchers, key, value, mod_revision: cur.mod_revision };
771
757
  if (cur.lease)
772
758
  {
773
759
  notify.lease = cur.lease;
@@ -798,10 +784,10 @@ class EtcTree
798
784
  }
799
785
  if (cur.value != null)
800
786
  {
801
- const item = { key: this.b64(prefix === null ? '' : prefix) };
787
+ const item = { key: (prefix === null ? '' : prefix) };
802
788
  if (!req.keys_only)
803
789
  {
804
- item.value = this.b64(cur.value);
790
+ item.value = cur.value;
805
791
  item.mod_revision = cur.mod_revision;
806
792
  //item.create_revision = cur.create_revision;
807
793
  //item.version = cur.version;
@@ -838,7 +824,7 @@ class EtcTree
838
824
  this.mod_revision = cur_revision;
839
825
  notifications.push({
840
826
  watchers: cur.key_watchers ? [ ...watchers, ...cur.key_watchers ] : watchers,
841
- key: this.b64(prefix === null ? '' : prefix),
827
+ key: (prefix === null ? '' : prefix),
842
828
  mod_revision: cur_revision,
843
829
  });
844
830
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antietcd",
3
- "version": "1.0.8",
3
+ "version": "1.1.0",
4
4
  "description": "Simplistic etcd replacement based on TinyRaft",
5
5
  "main": "antietcd.js",
6
6
  "scripts": {