autobee 1.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.
package/README.md CHANGED
@@ -2,13 +2,253 @@
2
2
 
3
3
  Unstoppable, scalable multiwriter Hyperbee.
4
4
 
5
+ > **Still experimental and under heavy development. Expect breaking changes.**
6
+
5
7
  ```sh
6
- npm i autobee
8
+ npm install autobee
9
+ ```
10
+
11
+ Multiple peers each write to their own local Hypercore. An `apply` function you provide merges those writes into a shared Hyperbee view deterministically. The view is consistent across all peers once they replicate.
12
+
13
+ ## Usage
14
+
15
+ ```js
16
+ const Autobee = require('autobee')
17
+ const Corestore = require('corestore')
18
+
19
+ const store = new Corestore('./my-db')
20
+
21
+ const db = new Autobee(store, null, { apply })
22
+ await db.ready()
23
+
24
+ // append some data
25
+ await db.append(Buffer.from(JSON.stringify({ hello: 'world' })))
26
+
27
+ // read it back from the view
28
+ const node = await db.view.get(Buffer.from('latest'))
29
+ console.log(JSON.parse(node.value))
30
+
31
+ async function apply(nodes, view, host) {
32
+ for (const node of nodes) {
33
+ const op = JSON.parse(node.value)
34
+
35
+ if (op.addWriter) host.addWriter(op.addWriter)
36
+ if (op.removeWriter) host.removeWriter(op.removeWriter)
37
+
38
+ const w = view.write()
39
+ w.tryPut(Buffer.from('latest'), node.value)
40
+ await w.flush()
41
+ }
42
+ }
43
+ ```
44
+
45
+ To add a second writer and replicate:
46
+
47
+ ```js
48
+ const db1 = new Autobee(store1, null, { apply })
49
+ await db1.ready()
50
+
51
+ // share db1.key with others so they can join
52
+ const db2 = new Autobee(store2, db1.key, { apply })
53
+ await db2.ready()
54
+
55
+ // db1 adds db2 as a writer
56
+ await db1.append(Buffer.from(JSON.stringify({ addWriter: db2.local.id })))
57
+
58
+ // replicate using any stream
59
+ const s1 = db1.replicate(true)
60
+ const s2 = db2.replicate(false)
61
+ s1.pipe(s2).pipe(s1)
7
62
  ```
8
63
 
9
64
  ## API
10
65
 
11
- #### `const auto = new Autobee(corestore, [key], [options])`
66
+ #### `const db = new Autobee(store, [key], [options])`
67
+
68
+ Create a new Autobee. `store` is a Corestore. `key` is the public key of an existing Autobee to join — omit or pass `null` to create a new one.
69
+
70
+ Options:
71
+
72
+ ```js
73
+ {
74
+ apply (nodes, view, host) {}, // called with batches of new nodes to apply to the view
75
+ open (bee, db) {}, // called to create a custom view, return it
76
+ close (view) {}, // called when the db closes
77
+ update (view, changes) {}, // called after apply when the view has been updated
78
+ encryptionKey: Buffer, // 32-byte key to encrypt all data at rest
79
+ encrypted: false, // set true if using encryptionKey
80
+ keyPair: { publicKey, secretKey }, // custom signing key pair for the local writer
81
+ optimistic: true // allow optimistic writes from unknown writers
82
+ }
83
+ ```
84
+
85
+ #### `db.key`
86
+
87
+ The public key of this Autobee. Share this with peers so they can join.
88
+
89
+ #### `db.discoveryKey`
90
+
91
+ The discovery key. Use this to find peers on the network.
92
+
93
+ #### `db.id`
94
+
95
+ The public key encoded as a hex string.
96
+
97
+ #### `db.local`
98
+
99
+ The local writer Hypercore. Use `db.local.key` or `db.local.id` to identify this writer to others.
100
+
101
+ #### `db.view`
102
+
103
+ A read-only snapshot of the Hyperbee view. Updated after each apply cycle. Use the standard [Hyperbee](https://github.com/holepunks/hyperbee) API to read from it.
104
+
105
+ #### `db.bee`
106
+
107
+ Alias for `db.view`.
108
+
109
+ #### `db.writable`
110
+
111
+ `true` if this instance has been added as a writer.
112
+
113
+ #### `db.isIndexer`
114
+
115
+ `true` if this writer is an indexer.
116
+
117
+ #### `await db.append(value | values)`
118
+
119
+ Append one or more values to the local writer. Triggers an apply cycle.
120
+
121
+ ```js
122
+ await db.append(Buffer.from('hello'))
123
+ await db.append([buf1, buf2, buf3])
124
+ ```
125
+
126
+ Optionally pass `{ optimistic: true }` to write without waiting to be a confirmed writer.
127
+
128
+ ```js
129
+ await db.append(buf, { optimistic: true })
130
+ ```
131
+
132
+ #### `await db.update()`
133
+
134
+ Trigger a new apply cycle. Useful after replication to process new data.
135
+
136
+ #### `await db.updated()`
137
+
138
+ Wait until the current apply cycle has finished.
139
+
140
+ #### `await db.flush()`
141
+
142
+ Wait until all known writers have been fully indexed.
143
+
144
+ #### `stream = db.replicate(isInitiator)`
145
+
146
+ Create a replication stream. Pass `true` for the initiating side, `false` for the other.
147
+
148
+ ```js
149
+ const s1 = db1.replicate(true)
150
+ const s2 = db2.replicate(false)
151
+ s1.pipe(s2).pipe(s1)
152
+ ```
153
+
154
+ #### `db.wakeup({ key, length })`
155
+
156
+ Hint that a new writer core is available at `key` with at least `length` entries. Used to wake up replication when you learn about a peer out of band.
157
+
158
+ #### `await db.setLocal(key, [options])`
159
+
160
+ Rotate the local writer to a different key. The new writer takes over as the active oplog.
161
+
162
+ #### `views = db.views()`
163
+
164
+ Returns the current system and view core positions. Used for replication coordination.
165
+
166
+ #### `Autobee.isAutobee(val)`
167
+
168
+ Returns `true` if `val` is an Autobee instance.
169
+
170
+ ### Apply
171
+
172
+ The `apply` function is called with a batch of nodes from writers, a writable `view` (Hyperbee batch), and a `host` object.
173
+
174
+ ```js
175
+ async function apply(nodes, view, host) {
176
+ for (const node of nodes) {
177
+ // node.key — writer public key (Buffer)
178
+ // node.value — the value appended (Buffer)
179
+ // node.length — position in the writer's core
180
+
181
+ const op = JSON.parse(node.value)
182
+
183
+ // manage writers
184
+ if (op.addWriter) host.addWriter(op.addWriter)
185
+ if (op.removeWriter) host.removeWriter(op.removeWriter)
186
+
187
+ // write to the view
188
+ const w = view.write()
189
+ w.tryPut(Buffer.from('key'), node.value)
190
+ await w.flush()
191
+ }
192
+ }
193
+ ```
194
+
195
+ #### `host.addWriter(key, [options])`
196
+
197
+ Add a writer by public key (Buffer or hex string). Options:
198
+
199
+ ```js
200
+ {
201
+ isIndexer: true // default
202
+ }
203
+ ```
204
+
205
+ #### `host.removeWriter(key)`
206
+
207
+ Remove a writer by public key (Buffer or hex string).
208
+
209
+ #### `host.ackWriter(key)`
210
+
211
+ Acknowledge a writer without changing their permissions.
212
+
213
+ #### `host.interrupt(reason)`
214
+
215
+ Interrupt the current apply cycle. The db emits `'interrupt'` with the reason. Useful for pausing apply while waiting on external data.
216
+
217
+ #### `anchor = await host.createAnchor()`
218
+
219
+ Create an anchor node. Returns `{ key, length }`. Anchors are used to create a verifiable checkpoint in the log that can be used by future writers to prove causal ordering.
220
+
221
+ #### `host.genesis`
222
+
223
+ `true` if the system has not yet processed any nodes. Use this to bootstrap the first writer.
224
+
225
+ ### Encryption
226
+
227
+ Pass an `encryptionKey` to encrypt all writer cores and the view at rest.
228
+
229
+ ```js
230
+ const db = new Autobee(store, null, {
231
+ apply,
232
+ encrypted: true,
233
+ encryptionKey: crypto.randomBytes(32)
234
+ })
235
+ ```
236
+
237
+ All peers must use the same encryption key.
238
+
239
+ ### Static methods
240
+
241
+ #### `buf = Autobee.encodeValue(value, [opts])`
242
+
243
+ Encode a value into an Autobee block with optional metadata.
244
+
245
+ #### `value = Autobee.decodeValue(buf, [opts])`
246
+
247
+ Decode an Autobee block back to its value.
248
+
249
+ #### `Autobee.GENESIS`
250
+
251
+ `{ length: 0, key: null }`. The empty head used to represent the genesis state.
12
252
 
13
253
  ## License
14
254
 
package/index.js CHANGED
@@ -18,6 +18,7 @@ const { ActiveWriters } = require('./lib/writers.js')
18
18
  const UpdateChanges = require('./lib/updates.js')
19
19
 
20
20
  const EMPTY_HEAD = { length: 0, key: null }
21
+ const INTERRUPT = new Error('Apply interrupted')
21
22
 
22
23
  module.exports = class Autobee extends ReadyResource {
23
24
  constructor(store, key = null, handlers = {}) {
@@ -81,6 +82,10 @@ module.exports = class Autobee extends ReadyResource {
81
82
  this._updateLocalCore = null
82
83
  this._host = new ApplyCalls(this)
83
84
 
85
+ this.interrupted = null
86
+ this._interrupting = false
87
+ this._onErrorBound = this._onError.bind(this)
88
+
84
89
  this._wakeup = new AutobeeWakeup(this, handlers)
85
90
  this.wakeupCapability = null
86
91
 
@@ -126,6 +131,22 @@ module.exports = class Autobee extends ReadyResource {
126
131
  this._wakeup.setCapability(this.wakeupCapability.key, this.wakeupCapability.discoveryKey)
127
132
  }
128
133
 
134
+ getExternalWriters() {
135
+ const keys = []
136
+ for (const w of this.writers.active.values()) {
137
+ if (w === this.writers.localWriter) continue
138
+ keys.push(w.core.key)
139
+ }
140
+ return keys
141
+ }
142
+
143
+ async getWriterViews(key) {
144
+ const id = b4a.toString(key, 'hex')
145
+ const w = this.writers.active.get(id)
146
+ if (!w) return []
147
+ return w.views()
148
+ }
149
+
129
150
  views() {
130
151
  const sys = this.system.bee.context.local
131
152
  const view = this._workingBee.context.local
@@ -138,7 +159,8 @@ module.exports = class Autobee extends ReadyResource {
138
159
  }
139
160
 
140
161
  async _close() {
141
- await this.interrupt()
162
+ this._interrupting = true
163
+ if (this._draining) await this._draining
142
164
 
143
165
  if (this._handlers.close) await this._handlers.close(this.view)
144
166
 
@@ -254,8 +276,7 @@ module.exports = class Autobee extends ReadyResource {
254
276
  this.bumping++
255
277
 
256
278
  if (!this._draining) {
257
- this._draining = this._drain()
258
- this._draining.catch(safetyCatch)
279
+ this._draining = this._drain().catch(this._onErrorBound)
259
280
  }
260
281
 
261
282
  return this._draining
@@ -265,11 +286,46 @@ module.exports = class Autobee extends ReadyResource {
265
286
  return this._bump()
266
287
  }
267
288
 
268
- updated() {
289
+ async updated() {
290
+ if (this.opened === false) await this.ready()
269
291
  if (this._draining) return this._draining
270
292
  return Promise.resolve()
271
293
  }
272
294
 
295
+ interrupt(reason) {
296
+ asserts.assert(!!this._host.applying, 'Interrupt is only allowed in apply')
297
+ this._interrupting = true
298
+ if (reason) this.interrupted = reason
299
+ throw INTERRUPT
300
+ }
301
+
302
+ getLastError() {
303
+ return this._lastError
304
+ }
305
+
306
+ _onError(err) {
307
+ if (this.closing) return
308
+
309
+ this._lastError = err
310
+
311
+ if (err === INTERRUPT) {
312
+ this.emit('interrupt', this.interrupted)
313
+ this.emit('update')
314
+ return
315
+ }
316
+
317
+ this.close().catch(safetyCatch)
318
+
319
+ // if no one is listening we should crash! we cannot rely on the EE here
320
+ // as this is wrapped in a promise so instead of nextTick throw it
321
+ if (ReadyResource.listenerCount(this, 'error') === 0) {
322
+ crashSoon(err)
323
+ return
324
+ }
325
+
326
+ this.emit('error', err)
327
+ }
328
+
273
329
  async _drain() {
274
330
  if (this._updateLocalCore !== null) {
275
331
  await this._rotateLocalWriter(this._updateLocalCore)
@@ -376,11 +432,6 @@ module.exports = class Autobee extends ReadyResource {
376
432
  this.emit('rotate-local-writer')
377
433
  }
378
434
 
379
- interrupt() {
380
- this._interrupting = true
381
- if (this._draining) return this._draining
382
- }
383
-
384
435
  async createAnchor() {
385
436
  const node = this._host.applying[this._host.applying.length - 1]
386
437
 
@@ -424,7 +475,7 @@ module.exports = class Autobee extends ReadyResource {
424
475
  await core.append(block, { writable: true, maxLength: 1 })
425
476
  }
426
477
 
427
- await this.system.addWriter(core.key)
478
+ await this.system.addWriter(core.key, { weight: 1 })
428
479
 
429
480
  const anchor = { key: core.key, length: core.length }
430
481
 
@@ -475,8 +526,6 @@ module.exports = class Autobee extends ReadyResource {
475
526
  this._workingBee.move(t.view)
476
527
  }
477
528
 
478
- asserts.assert(batch === t.tip[0], 'Batch must be first part of tip')
479
-
480
529
  let failed = true
481
530
 
482
531
  try {
@@ -594,9 +643,7 @@ module.exports = class Autobee extends ReadyResource {
594
643
  const value = values[i]
595
644
  const buffer = typeof value === 'string' ? b4a.from(value) : value
596
645
  const lnk = i === 0 ? links : []
597
- const b = { start: i, end: values.length - 1 - i }
598
-
599
- const node = this.writers.appendLocal(buffer, t, b, lnk, optimistic)
646
+ const node = this.writers.appendLocal(buffer, t, null, lnk, optimistic)
600
647
  batch.push(node)
601
648
  }
602
649
 
@@ -607,6 +654,10 @@ module.exports = class Autobee extends ReadyResource {
607
654
  // analyze is worth the trade off adding the view here also (technically not needed)
608
655
  await this.writers.flushLocal(this._workingBee.head())
609
656
  }
657
+
658
+ async replay() {
659
+ return topo.replay(this)
660
+ }
610
661
  }
611
662
 
612
663
  function isObject(o) {
@@ -634,3 +685,10 @@ function createAnchorCore(store, prologue, manifestData) {
634
685
 
635
686
  return core
636
687
  }
688
+
689
+ function crashSoon(err) {
690
+ queueMicrotask(() => {
691
+ throw err
692
+ })
693
+ throw err
694
+ }
@@ -22,13 +22,17 @@ class ApplyCalls {
22
22
  return this.auto.name
23
23
  }
24
24
 
25
+ get clock() {
26
+ return this.auto.system.flushes
27
+ }
28
+
25
29
  get genesis() {
26
30
  return this.auto.system.isGenesis()
27
31
  }
28
32
 
29
- addWriter(key, { isIndexer = true } = {}) {
33
+ addWriter(key, { isIndexer = true, weight = isIndexer ? 2 : 1 } = {}) {
30
34
  if (typeof key === 'string') key = ID.decode(key)
31
- return this.auto.system.addWriter(key, { isIndexer })
35
+ return this.auto.system.addWriter(key, { weight })
32
36
  }
33
37
 
34
38
  ackWriter(key) {
@@ -46,8 +50,7 @@ class ApplyCalls {
46
50
  }
47
51
 
48
52
  interrupt(reason) {
49
- throw new Error('TODO')
50
- // this.auto._interrupt(reason)
53
+ this.auto.interrupt(reason)
51
54
  }
52
55
 
53
56
  removeable(key) {
package/lib/encoding.js CHANGED
@@ -3,7 +3,7 @@ const c = require('compact-encoding')
3
3
  const crypto = require('hypercore-crypto')
4
4
  const { AutobeeEncryption } = require('autobee-encryption')
5
5
 
6
- const { getEncoding } = require('../spec/hyperschema')
6
+ const { getEncoding } = require('../encoding/spec/autobee')
7
7
  const { LEGACY_OPLOG_VERSION, OPLOG_VERSION } = require('./constants')
8
8
 
9
9
  const Oplog = getEncoding('@autobee/oplog')
package/lib/system.js CHANGED
@@ -28,12 +28,12 @@ module.exports = class Systembee {
28
28
  this.encrypted = opts.encrypted === true
29
29
  }
30
30
 
31
- async addWriter(key, { length = 0, isIndexer = false } = {}) {
31
+ async addWriter(key, { length = 0, weight = 1 } = {}) {
32
32
  if (length === 0) {
33
33
  const info = await this.get(key, { unflushed: false })
34
34
  length = info ? info.length : 0
35
35
  }
36
- this.update(key, length, isIndexer, false, true, false)
36
+ this.update(key, length, weight, false, true, false)
37
37
  }
38
38
 
39
39
  async ackWriter(key, { length = 0 } = {}) {
@@ -41,7 +41,7 @@ module.exports = class Systembee {
41
41
  const info = await this.get(key, { unflushed: false })
42
42
  length = info ? info.length : 0
43
43
  }
44
- this.update(key, length, false, false, false, true)
44
+ this.update(key, length, 0, false, false, true)
45
45
  }
46
46
 
47
47
  async removeWriter(key, { length = 0 } = {}) {
@@ -49,7 +49,7 @@ module.exports = class Systembee {
49
49
  const info = await this.get(key, { unflushed: true })
50
50
  length = info ? info.length : 0
51
51
  }
52
- this.update(key, length, false, true, false, false)
52
+ this.update(key, length, 0, true, false, false)
53
53
  }
54
54
 
55
55
  isGenesis() {
@@ -133,7 +133,7 @@ module.exports = class Systembee {
133
133
  this.heads.push({ key: node.key, length: node.length })
134
134
  if (node.timestamp > this.timestamp) this.timestamp = node.timestamp // TODO: support smoothing
135
135
 
136
- this.update(node.key, node.length, false, false, false, false)
136
+ this.update(node.key, node.length, 0, false, false, false)
137
137
  }
138
138
 
139
139
  async canApply(key, optimistic) {
@@ -151,6 +151,7 @@ module.exports = class Systembee {
151
151
 
152
152
  const upd = this.updates.get(b4a.toString(key, 'hex'))
153
153
  if (!upd) return info
154
+ if (!info) return upd
154
155
 
155
156
  if (upd.isAdded) info.isAdded = true
156
157
  if (upd.isRemoved) info.isRemoved = true
@@ -183,7 +184,7 @@ module.exports = class Systembee {
183
184
  return true
184
185
  }
185
186
 
186
- update(key, length, isIndexer, isRemoved, isAdded, isAcked) {
187
+ update(key, length, weight, isRemoved, isAdded, isAcked) {
187
188
  const id = b4a.toString(key, 'hex')
188
189
 
189
190
  let upd = this.updates.get(id)
@@ -195,17 +196,27 @@ module.exports = class Systembee {
195
196
  }
196
197
 
197
198
  if (!upd) {
198
- upd = { key, length, isIndexer, isRemoved, isAdded, isAcked, isOplog: false }
199
+ upd = {
200
+ version: 4,
201
+ key,
202
+ length,
203
+ weight,
204
+ clock: 0,
205
+ isRemoved,
206
+ isAdded,
207
+ isAcked,
208
+ isOplog: false
209
+ }
199
210
  this.updates.set(id, upd)
200
211
  return
201
212
  }
202
213
 
203
214
  if (isAdded) {
204
- upd.isIndexer = isIndexer
215
+ upd.weight = weight
205
216
  upd.isRemoved = false
206
217
  }
207
218
  if (isRemoved) {
208
- upd.isIndexer = false
219
+ upd.weight = 0
209
220
  upd.isRemoved = true
210
221
  }
211
222
  if (isAcked) {
@@ -226,7 +237,7 @@ module.exports = class Systembee {
226
237
 
227
238
  if (v) {
228
239
  if (!changed) {
229
- upd.isIndexer = v.isIndexer
240
+ upd.weight = v.weight
230
241
  upd.isRemoved = v.isRemoved
231
242
  }
232
243
 
@@ -234,6 +245,8 @@ module.exports = class Systembee {
234
245
  }
235
246
  }
236
247
 
248
+ upd.clock = this.flushes
249
+
237
250
  // TODO: we can optimise this a bit, if the above node hasnt changed, do not set this
238
251
  // less writes, but future thing
239
252
  upd.isOplog = b4a.equals(upd.key, oplog)