autobee 0.0.0 → 1.0.2

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.
@@ -0,0 +1,65 @@
1
+ const ID = require('hypercore-id-encoding')
2
+
3
+ class ApplyCalls {
4
+ constructor(auto) {
5
+ this.auto = auto
6
+ this.applying = null
7
+ }
8
+
9
+ get id() {
10
+ return this.auto.id
11
+ }
12
+
13
+ get key() {
14
+ return this.auto.key
15
+ }
16
+
17
+ get discoveryKey() {
18
+ return this.auto.discoveryKey
19
+ }
20
+
21
+ get name() {
22
+ return this.auto.name
23
+ }
24
+
25
+ get clock() {
26
+ return this.auto.system.flushes
27
+ }
28
+
29
+ get genesis() {
30
+ return this.auto.system.isGenesis()
31
+ }
32
+
33
+ addWriter(key, { isIndexer = true, weight = isIndexer ? 2 : 1 } = {}) {
34
+ if (typeof key === 'string') key = ID.decode(key)
35
+ return this.auto.system.addWriter(key, { weight })
36
+ }
37
+
38
+ ackWriter(key) {
39
+ if (typeof key === 'string') key = ID.decode(key)
40
+ return this.auto.system.ackWriter(key)
41
+ }
42
+
43
+ removeWriter(key) {
44
+ if (typeof key === 'string') key = ID.decode(key)
45
+ return this.auto.system.removeWriter(key)
46
+ }
47
+
48
+ preferFastForward() {
49
+ // this.auto._preferFastForward()
50
+ }
51
+
52
+ interrupt(reason) {
53
+ this.auto.interrupt(reason)
54
+ }
55
+
56
+ removeable(key) {
57
+ return true
58
+ }
59
+
60
+ createAnchor() {
61
+ return this.auto.createAnchor()
62
+ }
63
+ }
64
+
65
+ module.exports = ApplyCalls
package/lib/asserts.js ADDED
@@ -0,0 +1,25 @@
1
+ exports.heads = function (heads, node) {
2
+ for (const h of this.heads) {
3
+ if (h.key.equals(node.key)) {
4
+ exports.bail('Same node included twice')
5
+ }
6
+ }
7
+ }
8
+
9
+ exports.systemFlush = function (w) {
10
+ for (const op of w.ops) {
11
+ if (op.applied && op.key[0] === 1) return
12
+ }
13
+
14
+ exports.bail('Invalid system batch')
15
+ }
16
+
17
+ exports.assert = function (cond, msg) {
18
+ if (!cond) exports.bail(msg)
19
+ }
20
+
21
+ exports.bail = function (msg) {
22
+ const err = new Error('ERR_ASSERTION: ' + msg)
23
+ err.code = 'ERR_ASSERTION'
24
+ throw err
25
+ }
package/lib/boot.js ADDED
@@ -0,0 +1,117 @@
1
+ const encoding = require('./encoding.js')
2
+
3
+ module.exports = async function boot(
4
+ corestore,
5
+ key,
6
+ { encrypt, encryptionKey, keyPair, exclusive = true } = {}
7
+ ) {
8
+ const result = {
9
+ key: null,
10
+ local: null,
11
+ bootstrap: null,
12
+ encryptionKey: null,
13
+ system: null,
14
+ flushes: null
15
+ }
16
+
17
+ const manifest = keyPair
18
+ ? { version: corestore.manifestVersion, signers: [{ publicKey: keyPair.publicKey }] }
19
+ : null
20
+
21
+ if (key) {
22
+ result.key = key
23
+
24
+ const bootstrap = corestore.get({ key })
25
+ await bootstrap.ready()
26
+
27
+ const localKey = await bootstrap.getUserData('autobee/local')
28
+
29
+ if (keyPair) {
30
+ result.local = corestore.get({
31
+ keyPair,
32
+ active: false,
33
+ exclusive,
34
+ manifest
35
+ })
36
+ } else {
37
+ if (bootstrap.writable && !localKey) {
38
+ result.local = bootstrap.session({
39
+ active: false,
40
+ exclusive
41
+ })
42
+ } else {
43
+ const local = localKey
44
+ ? corestore.get({
45
+ key: localKey,
46
+ active: false,
47
+ exclusive
48
+ })
49
+ : corestore.get({
50
+ name: 'local',
51
+ active: false,
52
+ exclusive
53
+ })
54
+
55
+ await local.ready()
56
+ result.local = local
57
+ }
58
+ }
59
+
60
+ result.bootstrap = bootstrap
61
+ } else {
62
+ result.local = keyPair
63
+ ? corestore.get({
64
+ keyPair,
65
+ manifest,
66
+ active: false,
67
+ exclusive
68
+ })
69
+ : corestore.get({
70
+ name: 'local',
71
+ active: false,
72
+ exclusive
73
+ })
74
+ await result.local.ready()
75
+
76
+ const key = await result.local.getUserData('referrer')
77
+ if (key) {
78
+ result.key = key
79
+ result.bootstrap = corestore.get({ key, active: false })
80
+ await result.bootstrap.ready()
81
+ } else {
82
+ result.key = result.local.key
83
+ result.bootstrap = result.local.session({ active: false })
84
+ await result.bootstrap.setUserData('autobee/local', result.local.key)
85
+ }
86
+ }
87
+
88
+ if (key || keyPair) {
89
+ await result.bootstrap.setUserData('referrer', result.key)
90
+ await result.bootstrap.setUserData('autobee/local', result.local.key)
91
+ await result.local.setUserData('referrer', result.key)
92
+ }
93
+
94
+ const [systemHead, encryptionKeyBuffer] = await Promise.all([
95
+ result.local.getUserData('autobee/head'),
96
+ result.local.getUserData('autobee/encryption')
97
+ ])
98
+
99
+ if (systemHead) result.system = encoding.decodeBootRecord(systemHead)
100
+ if (encryptionKeyBuffer) {
101
+ result.encryptionKey = encryptionKeyBuffer
102
+ }
103
+
104
+ if (!result.encryptionKey && (encryptionKey || encrypt)) {
105
+ if (!encryptionKey) {
106
+ encryptionKey = (await corestore.createKeyPair('autobee/encryption')).secretKey.subarray(
107
+ 0,
108
+ 32
109
+ )
110
+ }
111
+
112
+ await result.local.setUserData('autobee/encryption', encryptionKey)
113
+ result.encryptionKey = encryptionKey
114
+ }
115
+
116
+ return result
117
+ }
@@ -0,0 +1,9 @@
1
+ const LEGACY_OPLOG_VERSION = 2
2
+ const OPLOG_VERSION = 3
3
+ const AUTOBEE_VERSION = 3
4
+
5
+ module.exports = {
6
+ LEGACY_OPLOG_VERSION,
7
+ OPLOG_VERSION,
8
+ AUTOBEE_VERSION
9
+ }
@@ -0,0 +1,150 @@
1
+ const b4a = require('b4a')
2
+ const c = require('compact-encoding')
3
+ const crypto = require('hypercore-crypto')
4
+ const { AutobeeEncryption } = require('autobee-encryption')
5
+
6
+ const { getEncoding } = require('../encoding/spec/autobee')
7
+ const { LEGACY_OPLOG_VERSION, OPLOG_VERSION } = require('./constants')
8
+
9
+ const Oplog = getEncoding('@autobee/oplog')
10
+ const Link = getEncoding('@autobee/link')
11
+ const SystemInfo = getEncoding('@autobee/system-info')
12
+ const SystemWriter = getEncoding('@autobee/system-writer')
13
+ const ManifestData = getEncoding('@autobee/manifest-data')
14
+
15
+ exports.OPLOG_VERSION = OPLOG_VERSION
16
+
17
+ exports.Oplog = Oplog
18
+ exports.ManifestData = ManifestData
19
+ exports.encodeBootRecord = encodeBootRecord
20
+ exports.decodeBootRecord = decodeBootRecord
21
+ exports.encodeSystemWriter = encodeSystemWriter
22
+ exports.decodeSystemWriter = decodeSystemWriter
23
+
24
+ function encodeBootRecord(r) {
25
+ return c.encode(Link, r)
26
+ }
27
+
28
+ function decodeBootRecord(value) {
29
+ return c.decode(Link, value)
30
+ }
31
+
32
+ function encodeSystemWriter(m) {
33
+ return c.encode(SystemWriter, m)
34
+ }
35
+
36
+ function decodeSystemWriter(key, value) {
37
+ return {
38
+ // TODO: add dummy type to schema to preset key without extra alloc
39
+ key: key.subarray(1),
40
+ ...c.decode(SystemWriter, value)
41
+ }
42
+ }
43
+
44
+ exports.encodeSystemInfo = encodeSystemInfo
45
+ exports.decodeSystemInfo = decodeSystemInfo
46
+
47
+ function encodeSystemInfo(m) {
48
+ return c.encode(SystemInfo, m)
49
+ }
50
+
51
+ function decodeSystemInfo(buf) {
52
+ return c.decode(SystemInfo, buf)
53
+ }
54
+
55
+ exports.encodeOplog = encodeOplog
56
+ exports.decodeOplog = decodeOplog
57
+
58
+ function decodeOplog(buf) {
59
+ const m = c.decode(Oplog, buf)
60
+
61
+ if (m.version <= LEGACY_OPLOG_VERSION) {
62
+ return {
63
+ version: m.version,
64
+ timestamp: 0,
65
+ links: m.node.heads,
66
+ batch: { start: 0, end: 0 },
67
+ views: null,
68
+ optimistic: !!m.optimistic,
69
+ value: m.node.value
70
+ }
71
+ }
72
+
73
+ if (m.batch === null) m.batch = { start: 0, end: 0 }
74
+ return m
75
+ }
76
+
77
+ function encodeOplog(m) {
78
+ if (m.batch && m.batch.start === 0 && m.batch.end === 0) m.batch = null
79
+ return c.encode(Oplog, m)
80
+ }
81
+
82
+ exports.encodeValue = encodeValue
83
+ exports.decodeValue = decodeValue
84
+
85
+ function createValue(value, opts = {}) {
86
+ if (opts.legacy) {
87
+ return {
88
+ version: opts.version || LEGACY_OPLOG_VERSION,
89
+ digest: null,
90
+ checkpoint: null,
91
+ optimistic: !!opts.optimistic,
92
+ node: {
93
+ heads: opts.heads || [],
94
+ batch: 1,
95
+ value
96
+ }
97
+ }
98
+ }
99
+
100
+ const {
101
+ version = OPLOG_VERSION,
102
+ optimistic = false,
103
+ timestamp = Date.now(),
104
+ links = [],
105
+ views = null
106
+ } = opts
107
+
108
+ return {
109
+ version,
110
+ timestamp,
111
+ links,
112
+ batch: null,
113
+ views,
114
+ optimistic,
115
+ value
116
+ }
117
+ }
118
+
119
+ function encodeValue(value, opts = {}) {
120
+ const oplog = createValue(value, opts)
121
+
122
+ const state = { start: 0, end: 0, buffer: null }
123
+
124
+ Oplog.preencode(state, oplog)
125
+
126
+ if (opts.padding) {
127
+ state.start = opts.padding
128
+ state.end += opts.padding
129
+ }
130
+
131
+ state.buffer = b4a.alloc(state.end)
132
+
133
+ Oplog.encode(state, oplog)
134
+
135
+ if (!opts.encrypted) return state.buffer
136
+
137
+ if (!opts.optimistic) {
138
+ throw new Error('Encoding an encrypted value is not supported')
139
+ }
140
+
141
+ const padding = b4a.alloc(16) // sodium.crypto_generichash_MINBYTES
142
+ crypto.hash(state.buffer, padding)
143
+ padding[0] = 0
144
+
145
+ return b4a.concat([padding.subarray(0, AutobeeEncryption.PADDING), state.buffer])
146
+ }
147
+
148
+ function decodeValue(buf, opts = {}) {
149
+ return decodeOplog(buf).value
150
+ }
package/lib/system.js ADDED
@@ -0,0 +1,327 @@
1
+ const Hyperbee = require('hyperbee2')
2
+ const b4a = require('b4a')
3
+
4
+ const { AUTOBEE_VERSION } = require('./constants')
5
+ const encoding = require('./encoding.js')
6
+ const topo = require('./topo.js')
7
+
8
+ const INFO_KEY = b4a.from([0])
9
+ const INFO_LEGACY_KEY = b4a.concat([b4a.from([0]), b4a.from('info')])
10
+ const INDEXER_GTE = b4a.from([2])
11
+ const INDEXER_LT = b4a.from([3])
12
+ const EMPTY_HEAD = { length: 0, key: null }
13
+ const EMPTY = b4a.from([])
14
+
15
+ module.exports = class Systembee {
16
+ constructor(store, name, opts = {}) {
17
+ this.name = name // for debuggin
18
+ this.store = store
19
+ this.bee = new Hyperbee(store, opts)
20
+ this.view = null
21
+ this.heads = []
22
+ this.version = 0
23
+ this.timestamp = 0
24
+ this.flushes = 0
25
+ this.indexers = null // legacy
26
+ this.updates = new Map()
27
+ this.writers = new Map()
28
+ this.encrypted = opts.encrypted === true
29
+ }
30
+
31
+ async addWriter(key, { length = 0, weight = 1 } = {}) {
32
+ if (length === 0) {
33
+ const info = await this.get(key, { unflushed: false })
34
+ length = info ? info.length : 0
35
+ }
36
+ this.update(key, length, weight, false, true, false)
37
+ }
38
+
39
+ async ackWriter(key, { length = 0 } = {}) {
40
+ if (length === 0) {
41
+ const info = await this.get(key, { unflushed: false })
42
+ length = info ? info.length : 0
43
+ }
44
+ this.update(key, length, 0, false, false, true)
45
+ }
46
+
47
+ async removeWriter(key, { length = 0 } = {}) {
48
+ if (length === 0) {
49
+ const info = await this.get(key, { unflushed: true })
50
+ length = info ? info.length : 0
51
+ }
52
+ this.update(key, length, 0, true, false, false)
53
+ }
54
+
55
+ isGenesis() {
56
+ return this.bee.head() === null || this.bee.head().length === 0
57
+ }
58
+
59
+ async boot(view) {
60
+ await this.bee.ready()
61
+ this.bee.move(view)
62
+ await this.reset()
63
+ }
64
+
65
+ async getInfo() {
66
+ const node = await this.bee.get(INFO_KEY)
67
+ if (node) return encoding.decodeSystemInfo(node.value)
68
+ const legacy = await this.bee.get(INFO_LEGACY_KEY)
69
+ if (legacy) return encoding.decodeSystemInfo(legacy.value)
70
+ return null
71
+ }
72
+
73
+ async *getIndexers() {
74
+ for await (const data of this.bee.createReadStream({ gte: INDEXER_GTE, lt: INDEXER_LT })) {
75
+ const key = data.key.subarray(1)
76
+ yield key
77
+ }
78
+ }
79
+
80
+ async reset() {
81
+ const info = await this.getInfo()
82
+
83
+ this.version = info ? info.version : 0
84
+ this.view = info ? info.view : EMPTY_HEAD
85
+ this.heads = info ? info.heads : []
86
+ this.timestamp = info ? info.timestamp : 0
87
+ this.flushes = info ? info.flushes : 0
88
+ this.indexers = info ? info.indexers : null
89
+ this.updates.clear()
90
+ this.writers.clear()
91
+
92
+ if (this.version > AUTOBEE_VERSION) {
93
+ throw new Error('Autobee signals newer version than locally supported')
94
+ }
95
+ }
96
+
97
+ bootRecord() {
98
+ const system = this.bee.head()
99
+ return system.length ? system : null
100
+ }
101
+
102
+ async close() {
103
+ await this.bee.close()
104
+ await this.store.close()
105
+ }
106
+
107
+ async undo(head) {
108
+ this.bee.move(head)
109
+ await this.reset()
110
+ return this.view || EMPTY_HEAD
111
+ }
112
+
113
+ getLinks(key) {
114
+ const links = []
115
+ for (const h of this.heads) {
116
+ if (key && b4a.equals(h.key, key)) continue
117
+ links.push(h)
118
+ }
119
+ return links
120
+ }
121
+
122
+ addNode(node) {
123
+ for (let i = 0; i < this.heads.length; i++) {
124
+ const h = this.heads[i]
125
+ if (topo.isLinking(node, h)) {
126
+ this.heads.splice(i--, 1)
127
+ }
128
+ }
129
+
130
+ // Can enable if needed during debuggin
131
+ // asserts.heads(heads, node)
132
+
133
+ this.heads.push({ key: node.key, length: node.length })
134
+ if (node.timestamp > this.timestamp) this.timestamp = node.timestamp // TODO: support smoothing
135
+
136
+ this.update(node.key, node.length, 0, false, false, false)
137
+ }
138
+
139
+ async canApply(key, optimistic) {
140
+ const id = b4a.toString(key, 'hex')
141
+ const w = this.writers.get(id)
142
+ if (w) return w.added
143
+ const info = await this.get(key)
144
+ return info ? !info.isRemoved : !!optimistic
145
+ }
146
+
147
+ async get(key, { unflushed = false } = {}) {
148
+ const node = await this.bee.get(encodeWriterKey(key))
149
+ const info = node !== null ? encoding.decodeSystemWriter(node.key, node.value) : null
150
+ if (!unflushed) return info
151
+
152
+ const upd = this.updates.get(b4a.toString(key, 'hex'))
153
+ if (!upd) return info
154
+ if (!info) return upd
155
+
156
+ if (upd.isAdded) info.isAdded = true
157
+ if (upd.isRemoved) info.isRemoved = true
158
+ if (upd.isIndexer) info.isIndexer = true
159
+ if (upd.isAcked) info.isAcked = true
160
+ if (upd.isOplog) info.isOplog = true
161
+
162
+ info.length = upd.length
163
+
164
+ return info
165
+ }
166
+
167
+ async *list() {
168
+ for await (const data of this.bee.createReadStream()) {
169
+ if (data.key[0] === 1) {
170
+ yield encoding.decodeSystemWriter(data.key, data.value)
171
+ }
172
+ }
173
+ }
174
+
175
+ async has(link) {
176
+ // fast path
177
+ for (let i = 0; i < this.heads.length; i++) {
178
+ const h = this.heads[i]
179
+ if (b4a.equals(h.key, link.key)) return h.length >= link.length
180
+ }
181
+
182
+ const node = await this.get(link.key)
183
+ if (!node || node.length < link.length) return false
184
+ return true
185
+ }
186
+
187
+ update(key, length, weight, isRemoved, isAdded, isAcked) {
188
+ const id = b4a.toString(key, 'hex')
189
+
190
+ let upd = this.updates.get(id)
191
+
192
+ if (isAdded) {
193
+ this.writers.set(id, { key, added: true })
194
+ } else if (isRemoved) {
195
+ this.writers.set(id, { key, added: false })
196
+ }
197
+
198
+ if (!upd) {
199
+ upd = {
200
+ version: 4,
201
+ key,
202
+ length,
203
+ weight,
204
+ clock: 0,
205
+ isRemoved,
206
+ isAdded,
207
+ isAcked,
208
+ isOplog: false
209
+ }
210
+ this.updates.set(id, upd)
211
+ return
212
+ }
213
+
214
+ if (isAdded) {
215
+ upd.weight = weight
216
+ upd.isRemoved = false
217
+ }
218
+ if (isRemoved) {
219
+ upd.weight = 0
220
+ upd.isRemoved = true
221
+ }
222
+ if (isAcked) {
223
+ upd.isAcked = true
224
+ }
225
+ if (length > upd.length) {
226
+ upd.length = length
227
+ }
228
+ }
229
+
230
+ async _updateWriter(upd, oplog) {
231
+ const changed = upd.isAdded || upd.isRemoved
232
+ const k = encodeWriterKey(upd.key)
233
+
234
+ if (upd.isAcked || !changed) {
235
+ const node = await this.bee.get(k)
236
+ const v = node ? encoding.decodeSystemWriter(node.key, node.value) : null
237
+
238
+ if (v) {
239
+ if (!changed) {
240
+ upd.weight = v.weight
241
+ upd.isRemoved = v.isRemoved
242
+ }
243
+
244
+ if (v.length > upd.length) upd.length = v.length
245
+ }
246
+ }
247
+
248
+ upd.clock = this.flushes
249
+
250
+ // TODO: we can optimise this a bit, if the above node hasnt changed, do not set this
251
+ // less writes, but future thing
252
+ upd.isOplog = b4a.equals(upd.key, oplog)
253
+
254
+ return [
255
+ k,
256
+ encoding.encodeSystemWriter(upd),
257
+ upd.isIndexer && upd.isAdded ? encodeIndexerWriterKey(upd.key) : null,
258
+ upd.isRemoved ? encodeIndexerWriterKey(upd.key) : null
259
+ ]
260
+ }
261
+
262
+ async flush(batch, bee) {
263
+ if (batch.length === 0) return []
264
+
265
+ const oplog = batch[batch.length - 1].key
266
+
267
+ const promises = []
268
+ for (const upd of this.updates.values()) {
269
+ promises.push(this._updateWriter(upd, oplog))
270
+ }
271
+
272
+ const w = this.bee.write()
273
+
274
+ // migrate
275
+ if (this.indexers) {
276
+ for (const key of this.indexers) w.tryPut(encodeIndexerWriterKey(key), EMPTY)
277
+ this.indexers = null
278
+ }
279
+
280
+ for (const [key, value, add, remove] of await Promise.all(promises)) {
281
+ w.tryPut(key, value)
282
+ if (add) w.tryPut(add, EMPTY)
283
+ if (remove) w.tryDelete(remove)
284
+ }
285
+
286
+ this.flushes++
287
+
288
+ const info = {
289
+ version: AUTOBEE_VERSION,
290
+ view: bee.head(),
291
+ heads: this.heads,
292
+ timestamp: this.timestamp,
293
+ flushes: this.flushes,
294
+ indexers: null // legacy
295
+ }
296
+
297
+ w.tryPut(INFO_KEY, encoding.encodeSystemInfo(info))
298
+
299
+ await w.flush()
300
+
301
+ // Can enable if needed during debuggin
302
+ // asserts.systemFlush(w)
303
+
304
+ this.updates.clear()
305
+
306
+ if (this.writers.size === 0) return []
307
+
308
+ const changes = [...this.writers.values()]
309
+
310
+ this.writers.clear()
311
+ return changes
312
+ }
313
+ }
314
+
315
+ function encodeWriterKey(key) {
316
+ const buf = b4a.allocUnsafe(33)
317
+ buf[0] = 1
318
+ buf.set(key, 1)
319
+ return buf
320
+ }
321
+
322
+ function encodeIndexerWriterKey(key) {
323
+ const buf = b4a.allocUnsafe(33)
324
+ buf[0] = 2
325
+ buf.set(key, 1)
326
+ return buf
327
+ }