Browse Source

nitial commit

Kieran Gibb 2 years ago
commit
88645cd510
No known key found for this signature in database
7 changed files with 2621 additions and 0 deletions
  1. 15
    0
      README.md
  2. 121
    0
      index.js
  3. 30
    0
      package.json
  4. 61
    0
      test/index.test.js
  5. 60
    0
      test/util.js
  6. 24
    0
      util.js
  7. 2310
    0
      yarn.lock

+ 15
- 0
README.md View File

@@ -0,0 +1,15 @@
1
+# cobox-group-store
2
+
3
+A store for loading into memory multiple `cobox-group``'s for iterating over and operating on...
4
+
5
+## API
6
+
7
+```js
8
+const Store = require('cobox-group-store')
9
+const storage = 'path/to/storage' // defaults to storage path defined in cobox-constants package
10
+const store = Store(storage)
11
+
12
+let groups = await store.all() // ensures groups loaded, then returns an array of cobox-groups
13
+
14
+let group = await store.get(id) // provide a name string, a 32 byte string, or a 64 byte string. ensures groups loaded, then retrieves group from store.
15
+```

+ 121
- 0
index.js View File

@@ -0,0 +1,121 @@
1
+const Group = require('cobox-group')
2
+const Config = require('cobox-config')
3
+const Crypto = require('cobox-crypto')
4
+const path = require('path')
5
+const thunky = require('thunky')
6
+const debug = require('debug')('cobox-server')
7
+const constants = require('cobox-constants')
8
+
9
+const crypto = Crypto()
10
+
11
+const { isString } = require('./util')
12
+
13
+module.exports = (storage, opts) => new GroupStore(storage, opts)
14
+
15
+class GroupStore {
16
+  constructor (storage, opts = {}) {
17
+    this._id = crypto.randomBytes(16).toString('hex')
18
+    this.root = storage || constants.storage
19
+    this.storage = path.join(this.root, 'groups')
20
+    this.config = opts.config
21
+    this.opts = opts
22
+
23
+    this.groups = this.config.groups.list().reduce((collection, groupOpts) => {
24
+      var key
25
+      if (!groupOpts.symmetricKey) key = groupOpts.publicKey
26
+      else key = crypto.pack(groupOpts.publicKey, groupOpts.symmetricKey)
27
+      var group = Group(this.storage, key, this.opts)
28
+      collection[groupOpts.publicKey] = group
29
+      collection[Buffer.from(groupOpts.publicKey, 'hex')] = group
30
+      if (groupOpts.name) collection[groupOpts.name] = group
31
+      return collection
32
+    }, {})
33
+
34
+    this.ready = thunky(this._ready.bind(this))
35
+  }
36
+
37
+  all () {
38
+    var self = this
39
+    return new Promise((resolve, reject) => {
40
+      self.ready(() => {
41
+        return resolve(Array.from(new Set(Object.values(self.groups))))
42
+      })
43
+    })
44
+  }
45
+
46
+  get (id) {
47
+    var self = this
48
+    return new Promise((resolve, reject) => {
49
+      self.ready(() => {
50
+        var key, name, keys
51
+
52
+        if (crypto.isKey(id)) key = id
53
+        if (!crypto.isKey(id) && isString(id)) name = id
54
+
55
+        // Ensure we either have no key, or the key is a valid key
56
+        if (!(!key || crypto.isKey(key))) return reject(new Error('invalid: key format'))
57
+
58
+        if (name) {
59
+          var group = this.groups[name]
60
+          if (group) return resolve(group)
61
+        }
62
+
63
+        if (key) {
64
+          keys = crypto.unpack(key)
65
+          var group = this.groups[keys.publicKey.toString('hex')]
66
+          if (group) return resolve(group)
67
+        } else {
68
+          key = crypto.accessKey()
69
+          keys = crypto.unpack(key)
70
+        }
71
+
72
+        var groupOpts = this.config.groups[keys.publicKey]
73
+        if (groupOpts && name && groupOpts.name !== name) return reject(new Error('invalid: group with this name already exists'))
74
+
75
+        var group = Group(this.storage, key, this.opts)
76
+        this.groups[group.publicKey.toString('hex')] = group
77
+        this.groups[group.publicKey] = group
78
+        if (name) this.groups[name] = group
79
+
80
+        if (!groupOpts) {
81
+          groupOpts = group.keys
82
+          if (name) groupOpts.name = name
83
+          this.config.groups.set(group.publicKey, groupOpts)
84
+          this.config.save()
85
+        }
86
+
87
+        group.ready(() => {
88
+          return resolve(group)
89
+        })
90
+      })
91
+    })
92
+  }
93
+
94
+  // ------------------------------------------------------------------
95
+
96
+  _ready (callback) {
97
+    if (!callback) callback = () => {}
98
+    var pending = 1
99
+    var self = this
100
+
101
+    function next (n) {
102
+      var group = self.groups[n]
103
+      if (!group) return done()
104
+      ++pending
105
+      process.nextTick(next, n + 1)
106
+      group.ready(done)
107
+    }
108
+
109
+    function done (err) {
110
+      if (err) {
111
+        pending = Infinity
112
+        return callback(err)
113
+      }
114
+      if (!--pending) {
115
+        return callback()
116
+      }
117
+    }
118
+
119
+    next(0)
120
+  }
121
+}

+ 30
- 0
package.json View File

@@ -0,0 +1,30 @@
1
+{
2
+  "name": "cobox-group-store",
3
+  "version": "1.0.0",
4
+  "description": "A store for multiple cobox groups",
5
+  "main": "index.js",
6
+  "directories": {
7
+    "test": "test"
8
+  },
9
+  "scripts": {
10
+    "test": "tape test/**/*.test.js | tap-spec"
11
+  },
12
+  "author": "magma-collective",
13
+  "license": "AGPL-3.0-or-later",
14
+  "dependencies": {
15
+    "cobox-config": "git+ssh://git@ledger-git.dyne.org:2240/CoBox/cobox-config.git",
16
+    "cobox-crypto": "git+ssh://git@ledger-git.dyne.org:2240/CoBox/cobox-crypto.git",
17
+    "cobox-group": "git+ssh://git@ledger-git.dyne.org:2240/CoBox/cobox-group.git",
18
+    "cobox-constants": "git+ssh://git@ledger-git.dyne.org:2240/CoBox/cobox-constants.git",
19
+    "debug": "^4.1.1",
20
+    "thunky": "^1.1.0"
21
+  },
22
+  "devDependencies": {
23
+    "mkdirp": "^0.5.1",
24
+    "rimraf": "^3.0.0",
25
+    "tap-spec": "^5.0.0",
26
+    "tape": "^4.11.0",
27
+    "tape-plus": "^1.0.0",
28
+    "tmp": "^0.1.0"
29
+  }
30
+}

+ 61
- 0
test/index.test.js View File

@@ -0,0 +1,61 @@
1
+const { describe } = require('tape-plus')
2
+const Config = require('cobox-config')
3
+const Group = require('cobox-group')
4
+const crypto = require('cobox-crypto')()
5
+const Store = require('../')
6
+
7
+const { tmp, cleanup } = require('./util')
8
+
9
+describe('group store: basic', (context) => {
10
+  let storage, config
11
+
12
+  context.beforeEach((c) => {
13
+    storage = tmp()
14
+    config = Config(storage)
15
+  })
16
+
17
+  context.afterEach((c) => {
18
+    cleanup(storage)
19
+  })
20
+
21
+  context('on load it caches existing groups from the config', function (assert, next) {
22
+    var group = crypto.keySet()
23
+    config.groups.set(group.publicKey, group)
24
+    const store = Store(storage, { config })
25
+    store.ready(() => {
26
+      assert.ok(Object.values(store.groups).length, 'builds then caches groups from the config')
27
+      assert.ok(store.groups[group.publicKey], 'accessible using buffer')
28
+      assert.ok(store.groups[group.publicKey.toString('hex')], 'accessible using hex')
29
+      next()
30
+    })
31
+  })
32
+
33
+  context('create a single group', async function (assert, next) {
34
+    const store = Store(storage, { config })
35
+    var a = await store.get()
36
+    assert.ok(a, 'creates a new group')
37
+    next()
38
+  })
39
+
40
+  context('get a group by key', async function (assert, next) {
41
+    const store = Store(storage, { config })
42
+    var a = await store.get()
43
+    assert.ok(a, 'creates a new group')
44
+
45
+    var b = await store.get(a.key)
46
+    assert.same(a.key, b.key, 'getes the same group by key')
47
+    assert.same(a._id, b._id, 'is the same group instance')
48
+    next()
49
+  })
50
+
51
+  context('get a group by name', async function (assert, next) {
52
+    const store = Store(storage, { config })
53
+    var a = await store.get("Silly String")
54
+    assert.ok(a, 'creates a new group')
55
+
56
+    var b = await store.get("Silly String")
57
+    assert.same(a.key, b.key, 'getes the same group by key')
58
+    assert.same(a._id, b._id, 'is the same group instance')
59
+    next()
60
+  })
61
+})

+ 60
- 0
test/util.js View File

@@ -0,0 +1,60 @@
1
+const rimraf = require('rimraf')
2
+const debug = require('debug')('cleanup')
3
+const tmpdir = require('tmp').dirSync
4
+const mkdirp = require('mkdirp')
5
+
6
+function cleanup (dirs, cb) {
7
+  if (!cb) cb = noop
8
+  if (!Array.isArray(dirs)) dirs = [dirs]
9
+  var pending = 1
10
+
11
+  function next (n) {
12
+    var dir = dirs[n]
13
+    if (!dir) return done()
14
+    ++pending
15
+    process.nextTick(next, n + 1)
16
+
17
+    rimraf(dir, (err) => {
18
+      if (err) return done(err)
19
+      done()
20
+    })
21
+  }
22
+
23
+  function done (err) {
24
+    if (err) {
25
+      pending = Infinity
26
+      return cb(err)
27
+    }
28
+    if (!--pending) return cb()
29
+  }
30
+
31
+  next(0)
32
+}
33
+
34
+function tmp () {
35
+  var path = tmpdir().name
36
+  mkdirp.sync(path)
37
+  return path
38
+}
39
+
40
+function replicate (a, b, opts, cb) {
41
+  if (typeof opts === 'function') return replicate(a, b, {}, opts)
42
+  if (!cb) cb = noop
43
+
44
+  var s = a.replicate(Object.assign({ live: false }, opts))
45
+  var d = b.replicate(Object.assign({ live: false }, opts))
46
+
47
+  s.pipe(d).pipe(s)
48
+
49
+  s.on('error', cb)
50
+  s.on('end', cb)
51
+}
52
+
53
+function uniq (array) {
54
+  if (!Array.isArray(array)) array = [array]
55
+  return Array.from(new Set(array))
56
+}
57
+
58
+function noop () {}
59
+
60
+module.exports = { cleanup, tmp, replicate, uniq }

+ 24
- 0
util.js View File

@@ -0,0 +1,24 @@
1
+function isString (variable) {
2
+  return typeof variable === 'string'
3
+}
4
+
5
+function isFunction (variable) {
6
+  return variable && typeof variable === 'function'
7
+}
8
+
9
+function removeEmpty (obj) {
10
+  return Object.keys(obj)
11
+    .filter(k => obj[k] != null)
12
+    .reduce((newObj, k) => {
13
+      return typeof obj[k] === "object"
14
+        ? { ...newObj, [k]: removeEmpty(obj[k]) }
15
+        : { ...newObj, [k]: obj[k] }
16
+    }, {})
17
+}
18
+
19
+module.exports = {
20
+  isString,
21
+  isFunction,
22
+  removeEmpty
23
+}
24
+

+ 2310
- 0
yarn.lock
File diff suppressed because it is too large
View File