autobee 0.0.0 → 1.0.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/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('../spec/hyperschema')
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,314 @@
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, isIndexer = false } = {}) {
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, isIndexer, 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, false, 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, false, 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, false, 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
+
155
+ if (upd.isAdded) info.isAdded = true
156
+ if (upd.isRemoved) info.isRemoved = true
157
+ if (upd.isIndexer) info.isIndexer = true
158
+ if (upd.isAcked) info.isAcked = true
159
+ if (upd.isOplog) info.isOplog = true
160
+
161
+ info.length = upd.length
162
+
163
+ return info
164
+ }
165
+
166
+ async *list() {
167
+ for await (const data of this.bee.createReadStream()) {
168
+ if (data.key[0] === 1) {
169
+ yield encoding.decodeSystemWriter(data.key, data.value)
170
+ }
171
+ }
172
+ }
173
+
174
+ async has(link) {
175
+ // fast path
176
+ for (let i = 0; i < this.heads.length; i++) {
177
+ const h = this.heads[i]
178
+ if (b4a.equals(h.key, link.key)) return h.length >= link.length
179
+ }
180
+
181
+ const node = await this.get(link.key)
182
+ if (!node || node.length < link.length) return false
183
+ return true
184
+ }
185
+
186
+ update(key, length, isIndexer, isRemoved, isAdded, isAcked) {
187
+ const id = b4a.toString(key, 'hex')
188
+
189
+ let upd = this.updates.get(id)
190
+
191
+ if (isAdded) {
192
+ this.writers.set(id, { key, added: true })
193
+ } else if (isRemoved) {
194
+ this.writers.set(id, { key, added: false })
195
+ }
196
+
197
+ if (!upd) {
198
+ upd = { key, length, isIndexer, isRemoved, isAdded, isAcked, isOplog: false }
199
+ this.updates.set(id, upd)
200
+ return
201
+ }
202
+
203
+ if (isAdded) {
204
+ upd.isIndexer = isIndexer
205
+ upd.isRemoved = false
206
+ }
207
+ if (isRemoved) {
208
+ upd.isIndexer = false
209
+ upd.isRemoved = true
210
+ }
211
+ if (isAcked) {
212
+ upd.isAcked = true
213
+ }
214
+ if (length > upd.length) {
215
+ upd.length = length
216
+ }
217
+ }
218
+
219
+ async _updateWriter(upd, oplog) {
220
+ const changed = upd.isAdded || upd.isRemoved
221
+ const k = encodeWriterKey(upd.key)
222
+
223
+ if (upd.isAcked || !changed) {
224
+ const node = await this.bee.get(k)
225
+ const v = node ? encoding.decodeSystemWriter(node.key, node.value) : null
226
+
227
+ if (v) {
228
+ if (!changed) {
229
+ upd.isIndexer = v.isIndexer
230
+ upd.isRemoved = v.isRemoved
231
+ }
232
+
233
+ if (v.length > upd.length) upd.length = v.length
234
+ }
235
+ }
236
+
237
+ // TODO: we can optimise this a bit, if the above node hasnt changed, do not set this
238
+ // less writes, but future thing
239
+ upd.isOplog = b4a.equals(upd.key, oplog)
240
+
241
+ return [
242
+ k,
243
+ encoding.encodeSystemWriter(upd),
244
+ upd.isIndexer && upd.isAdded ? encodeIndexerWriterKey(upd.key) : null,
245
+ upd.isRemoved ? encodeIndexerWriterKey(upd.key) : null
246
+ ]
247
+ }
248
+
249
+ async flush(batch, bee) {
250
+ if (batch.length === 0) return []
251
+
252
+ const oplog = batch[batch.length - 1].key
253
+
254
+ const promises = []
255
+ for (const upd of this.updates.values()) {
256
+ promises.push(this._updateWriter(upd, oplog))
257
+ }
258
+
259
+ const w = this.bee.write()
260
+
261
+ // migrate
262
+ if (this.indexers) {
263
+ for (const key of this.indexers) w.tryPut(encodeIndexerWriterKey(key), EMPTY)
264
+ this.indexers = null
265
+ }
266
+
267
+ for (const [key, value, add, remove] of await Promise.all(promises)) {
268
+ w.tryPut(key, value)
269
+ if (add) w.tryPut(add, EMPTY)
270
+ if (remove) w.tryDelete(remove)
271
+ }
272
+
273
+ this.flushes++
274
+
275
+ const info = {
276
+ version: AUTOBEE_VERSION,
277
+ view: bee.head(),
278
+ heads: this.heads,
279
+ timestamp: this.timestamp,
280
+ flushes: this.flushes,
281
+ indexers: null // legacy
282
+ }
283
+
284
+ w.tryPut(INFO_KEY, encoding.encodeSystemInfo(info))
285
+
286
+ await w.flush()
287
+
288
+ // Can enable if needed during debuggin
289
+ // asserts.systemFlush(w)
290
+
291
+ this.updates.clear()
292
+
293
+ if (this.writers.size === 0) return []
294
+
295
+ const changes = [...this.writers.values()]
296
+
297
+ this.writers.clear()
298
+ return changes
299
+ }
300
+ }
301
+
302
+ function encodeWriterKey(key) {
303
+ const buf = b4a.allocUnsafe(33)
304
+ buf[0] = 1
305
+ buf.set(key, 1)
306
+ return buf
307
+ }
308
+
309
+ function encodeIndexerWriterKey(key) {
310
+ const buf = b4a.allocUnsafe(33)
311
+ buf[0] = 2
312
+ buf.set(key, 1)
313
+ return buf
314
+ }