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/README.md ADDED
@@ -0,0 +1,15 @@
1
+ # autobee
2
+
3
+ Unstoppable, scalable multiwriter Hyperbee.
4
+
5
+ ```sh
6
+ npm i autobee
7
+ ```
8
+
9
+ ## API
10
+
11
+ #### `const auto = new Autobee(corestore, [key], [options])`
12
+
13
+ ## License
14
+
15
+ Apache-2.0
package/index.js ADDED
@@ -0,0 +1,636 @@
1
+ const ReadyResource = require('ready-resource')
2
+ const b4a = require('b4a')
3
+ const safetyCatch = require('safety-catch')
4
+ const Hyperbee = require('hyperbee2')
5
+ const ID = require('hypercore-id-encoding')
6
+ const { AutobeeEncryption, WriterEncryption } = require('autobee-encryption')
7
+ const AutobeeWakeup = require('autobee-wakeup')
8
+ const Hypercore = require('hypercore')
9
+ const crypto = require('hypercore-crypto')
10
+ const c = require('compact-encoding')
11
+ const asserts = require('./lib/asserts.js')
12
+ const boot = require('./lib/boot.js')
13
+ const encoding = require('./lib/encoding.js')
14
+ const System = require('./lib/system.js')
15
+ const ApplyCalls = require('./lib/apply-calls.js')
16
+ const topo = require('./lib/topo.js')
17
+ const { ActiveWriters } = require('./lib/writers.js')
18
+ const UpdateChanges = require('./lib/updates.js')
19
+
20
+ const EMPTY_HEAD = { length: 0, key: null }
21
+
22
+ module.exports = class Autobee extends ReadyResource {
23
+ constructor(store, key = null, handlers = {}) {
24
+ super()
25
+
26
+ if (isObject(key)) {
27
+ handlers = key
28
+ key = null
29
+ }
30
+
31
+ const { name = null, encrypted, encryptionKey } = handlers
32
+
33
+ this.encrypted = encrypted === true || !!encryptionKey
34
+
35
+ const bee = new Hyperbee(store.namespace('view'), {
36
+ // defer one tick to ensure consistent state, then return state prom
37
+ preload: async () => {
38
+ await 1
39
+ await this._bootingState
40
+ },
41
+ getEncryptionProvider: () => this._getEncryptionProvider()
42
+ })
43
+
44
+ this.store = store
45
+
46
+ this.key = key ? ID.decode(key) : null
47
+ this.discoveryKey = null
48
+ this.id = null
49
+ this.bootstrap = null
50
+
51
+ this.system = new System(this.store.namespace('system'), this.name, {
52
+ getEncryptionProvider: () => this._getEncryptionProvider(),
53
+ encrypted: this.encrypted
54
+ })
55
+
56
+ this.bee = bee.snapshot()
57
+ this.view = handlers.open ? handlers.open(this.bee, this) : this.bee
58
+ this.optimistic = handlers.optimistic !== false // TODO: should default to false instead
59
+
60
+ this.name = name // for debugging
61
+
62
+ this.local = null
63
+ this.encryptionKey = null
64
+ this.keyPair = null
65
+ this.writers = null
66
+ this.bumping = 0
67
+
68
+ this._workingBee = bee
69
+ this._workingView = handlers.open ? handlers.open(this._workingBee, this) : this._workingBee
70
+
71
+ this._appending = []
72
+ this._draining = null
73
+
74
+ this._bootingState = null
75
+ this._bootingAll = null
76
+
77
+ this._handlers = handlers
78
+ this._hasApply = !!handlers.apply
79
+ this._hasUpdate = !!handlers.update
80
+ this._needsUpdate = false
81
+ this._updateLocalCore = null
82
+ this._host = new ApplyCalls(this)
83
+
84
+ this._wakeup = new AutobeeWakeup(this, handlers)
85
+ this.wakeupCapability = null
86
+
87
+ this.ready().catch(noop)
88
+ }
89
+
90
+ static GENESIS = EMPTY_HEAD
91
+
92
+ static isAutobee(auto) {
93
+ return auto instanceof Autobee
94
+ }
95
+
96
+ get isIndexer() {
97
+ return this.writers.localWriter.isIndexer
98
+ }
99
+
100
+ get writable() {
101
+ return this.writers.writable
102
+ }
103
+
104
+ // autobase compat
105
+ get activeWriters() {
106
+ return this.writers
107
+ }
108
+
109
+ async _open() {
110
+ await this._preBoot()
111
+
112
+ this._bootingState = this._bootState()
113
+ this._bootingAll = this._bootAll()
114
+
115
+ this._bootingState.catch(safetyCatch)
116
+ this._bootingAll.catch(safetyCatch)
117
+
118
+ await this.bee.ready()
119
+ await this._bootingState
120
+
121
+ this.bumpSoon()
122
+ }
123
+
124
+ _registerWakeup() {
125
+ this._wakeup.recouple()
126
+ this._wakeup.setCapability(this.wakeupCapability.key, this.wakeupCapability.discoveryKey)
127
+ }
128
+
129
+ views() {
130
+ const sys = this.system.bee.context.local
131
+ const view = this._workingBee.context.local
132
+
133
+ // signedLength for autobase compat
134
+ return [
135
+ { key: sys.key, length: sys.length, signedLength: sys.length },
136
+ { key: view.key, length: view.length, signedLength: view.length }
137
+ ]
138
+ }
139
+
140
+ async _close() {
141
+ await this.interrupt()
142
+
143
+ if (this._handlers.close) await this._handlers.close(this.view)
144
+
145
+ await this.local.close()
146
+ await this.system.close()
147
+ await this._wakeup.close()
148
+ await this._workingBee.close()
149
+ await this.bee.close()
150
+ await this.store.close()
151
+
152
+ try {
153
+ await this._bootingAll
154
+ } catch {}
155
+ }
156
+
157
+ replicate(...args) {
158
+ const stream = this.store.replicate(...args)
159
+ this._wakeup.addStream(stream)
160
+ return stream
161
+ }
162
+
163
+ async flush() {
164
+ await this._bootingAll
165
+ }
166
+
167
+ hintWakeup(wakeup) {
168
+ this._wakeup.hint(wakeup)
169
+ }
170
+
171
+ openCore(key) {
172
+ const encryption = this.encryptionKey ? new WriterEncryption(this) : null
173
+ return this.store.get({ key, encryption })
174
+ }
175
+
176
+ _getEncryptionProvider() {
177
+ if (!this.encrypted) return null
178
+ return new WriterEncryption(this)
179
+ }
180
+
181
+ async _preBoot() {
182
+ if (this._handlers.wait) await this._handlers.wait()
183
+
184
+ await this.store.ready()
185
+
186
+ if (this._handlers.encryptionKey) {
187
+ this.encryptionKey = await this._handlers.encryptionKey
188
+ }
189
+
190
+ if (this._handlers.keyPair) {
191
+ this.keyPair = await this._handlers.keyPair
192
+ }
193
+ }
194
+
195
+ async _bootState() {
196
+ const result = await boot(this.store, this.key, {
197
+ encryptionKey: this.encryptionKey,
198
+ keyPair: this.keyPair
199
+ })
200
+
201
+ this.key = result.key
202
+ this.bootstrap = result.bootstrap
203
+ this.discoveryKey = result.bootstrap.core.discoveryKey
204
+ this.id = result.bootstrap.core.id
205
+ this.encryptionKey = result.encryptionKey
206
+
207
+ if (this.encrypted) {
208
+ asserts.assert(this.encryptionKey !== null, 'Encryption key is expected')
209
+ }
210
+
211
+ this.local = result.local
212
+ this.local.setEncryption(this._getEncryptionProvider())
213
+ this.local.setActive(true)
214
+
215
+ this.writers = new ActiveWriters(this)
216
+
217
+ if (this._handlers.wakeupCapability) {
218
+ this.wakeupCapability = await this._handlers.wakeupCapability
219
+ } else {
220
+ this.wakeupCapability = { key: this.key, discoveryKey: this.discoveryKey }
221
+ }
222
+
223
+ this._registerWakeup()
224
+
225
+ const system = result.system || EMPTY_HEAD
226
+
227
+ await this.system.boot(system)
228
+
229
+ // Use the view position from the system info (authoritative, post-processing)
230
+ // rather than from the oplog (stale, captured at append time before _bump)
231
+ const view = this.system.view || EMPTY_HEAD
232
+
233
+ this._workingBee.move(view)
234
+ this.bee.move(view)
235
+
236
+ await this.writers.updateLocalState()
237
+ }
238
+
239
+ async _bootAll() {
240
+ await this._bootingState
241
+
242
+ for await (const node of this.system.list()) {
243
+ await this.writers.add(node.key)
244
+ }
245
+ await this._bump()
246
+ }
247
+
248
+ bumpSoon() {
249
+ this._bump().catch(safetyCatch)
250
+ }
251
+
252
+ async _bump() {
253
+ await this._flushWakeup()
254
+ this.bumping++
255
+
256
+ if (!this._draining) {
257
+ this._draining = this._drain()
258
+ this._draining.catch(safetyCatch)
259
+ }
260
+
261
+ return this._draining
262
+ }
263
+
264
+ update() {
265
+ return this._bump()
266
+ }
267
+
268
+ updated() {
269
+ if (this._draining) return this._draining
270
+ return Promise.resolve()
271
+ }
272
+
273
+ async _drain() {
274
+ if (this._updateLocalCore !== null) {
275
+ await this._rotateLocalWriter(this._updateLocalCore)
276
+ }
277
+
278
+ const changes = this._hasUpdate ? new UpdateChanges(this) : null
279
+ if (changes) changes.track()
280
+
281
+ while (!this._interrupting && this.bumping > 0) {
282
+ if (this._interrupting) return
283
+
284
+ try {
285
+ while (!this._interrupting) {
286
+ if (!(await this._bumpPendingWriters())) break
287
+ this._needsUpdate = true
288
+ }
289
+
290
+ await this._flushLocal()
291
+ } finally {
292
+ if (this.bumping === 1) this.bumping = 0
293
+ else this.bumping = 1
294
+ }
295
+ }
296
+
297
+ this._draining = null
298
+ if (this._interrupting) return
299
+
300
+ if (this._needsUpdate) {
301
+ this._update(changes)
302
+ }
303
+ }
304
+
305
+ async _flushWakeup() {
306
+ const hints = this._wakeup.flush()
307
+
308
+ for (const [hex, length] of hints) {
309
+ const key = b4a.from(hex, 'hex')
310
+ if (this.writers.has(hex)) continue
311
+ if (length !== -1) {
312
+ const info = await this.system.get(key)
313
+ if (info && length <= info.length) continue // stale hint
314
+ }
315
+ await this.writers.wakeup(key, length === -1 ? 0 : length)
316
+ }
317
+ }
318
+
319
+ _update(changes) {
320
+ this._needsUpdate = false
321
+ this.bee.update(this._workingBee.root)
322
+
323
+ if (!changes) return
324
+
325
+ changes.finalise()
326
+ this._handlers.update(this.view, changes)
327
+ }
328
+
329
+ async setLocal(key, { keyPair } = {}) {
330
+ if (!this.opened) await this.ready()
331
+
332
+ const manifest = keyPair
333
+ ? { version: this.store.manifestVersion, signers: [{ publicKey: keyPair.publicKey }] }
334
+ : null
335
+ if (!key) key = Hypercore.key(manifest)
336
+ // If the keys are the same, no need to rotate
337
+ if (b4a.equals(key, this.local.key)) return
338
+
339
+ const encryption = this.encryptionKey ? this._getEncryptionProvider() : null
340
+
341
+ const local = this.store.get({
342
+ key,
343
+ manifest,
344
+ active: false,
345
+ exclusive: true,
346
+ encryption
347
+ })
348
+ await local.ready()
349
+
350
+ this._updateLocalCore = local
351
+
352
+ let runs = 0
353
+ while (!this._interrupting && this.appending && runs++ < 16) await this.update()
354
+ await this.bumpSoon()
355
+ }
356
+
357
+ async _rotateLocalWriter(newLocal) {
358
+ asserts.assert(!this.appending, 'Cannot rotate a newLocal writer if an append is in progress')
359
+
360
+ const oldLocal = this.local
361
+
362
+ this.local = newLocal
363
+ this.writers.rotateLocalWriter(this.local)
364
+
365
+ this._updateLocalCore = null
366
+
367
+ this.local.setUserData('referrer', this.key)
368
+ if (this.encryptionKey) {
369
+ await this.local.setUserData('autobase/encryption', this.encryptionKey)
370
+ }
371
+
372
+ await this.bootstrap.setUserData('autobase/local', this.local.key)
373
+ await oldLocal.close()
374
+
375
+ // done, soft reboot
376
+ this.emit('rotate-local-writer')
377
+ }
378
+
379
+ interrupt() {
380
+ this._interrupting = true
381
+ if (this._draining) return this._draining
382
+ }
383
+
384
+ async createAnchor() {
385
+ const node = this._host.applying[this._host.applying.length - 1]
386
+
387
+ const key = node.key
388
+ const length = node.length
389
+ const legacy = node.version <= 2
390
+
391
+ const info = await this.system.get(key, { unflushed: true })
392
+ if (!info || info.length < length) throw new Error('Anchor node is not in system')
393
+
394
+ const state = { start: 0, end: 40, buffer: b4a.alloc(40) }
395
+ c.fixed32.encode(state, key)
396
+ c.uint64.encode(state, length)
397
+
398
+ const namespace = crypto.hash(state.buffer)
399
+ const manifestData = c.encode(encoding.ManifestData, { version: 0, legacyBlocks: 0, namespace })
400
+
401
+ const padding = this.encryptionKey ? AutobeeEncryption.PADDING : 0
402
+ const links = [{ key, length }]
403
+
404
+ const block = Autobee.encodeValue(null, {
405
+ legacy,
406
+ timestamp: 0,
407
+ links,
408
+ heads: links, // legacy compat
409
+ padding
410
+ })
411
+
412
+ if (this.encryptionKey) {
413
+ AutobeeEncryption.encryptAnchor(block, this.key, this.encryptionKey, namespace)
414
+ }
415
+
416
+ const root = { index: 0, size: block.byteLength, hash: crypto.data(block) }
417
+ const hash = crypto.tree([root])
418
+ const prologue = { hash, length: 1 }
419
+
420
+ const core = createAnchorCore(this.store, prologue, manifestData)
421
+ await core.ready()
422
+
423
+ if (core.length === 0) {
424
+ await core.append(block, { writable: true, maxLength: 1 })
425
+ }
426
+
427
+ await this.system.addWriter(core.key)
428
+
429
+ const anchor = { key: core.key, length: core.length }
430
+
431
+ await core.close()
432
+
433
+ return anchor
434
+ }
435
+
436
+ async _bumpPendingWriters() {
437
+ let updated = false
438
+
439
+ const pending = this.writers.pending.slice()
440
+
441
+ for (let i = pending.length - 1; i >= 0; i--) {
442
+ const w = pending[i]
443
+
444
+ const batch = await w.next()
445
+ if (batch === null) continue
446
+
447
+ if (w.isAdded || (w.isRemoved && w.hasReferrals())) {
448
+ await this._processBatch(batch)
449
+ w.notify(batch)
450
+ updated = true
451
+ continue
452
+ }
453
+
454
+ if (this.optimistic && !w.isRemoved && batch[0].optimistic) {
455
+ if (!(await this._optimisticBatch(batch))) {
456
+ w.removePending()
457
+ continue
458
+ }
459
+ w.notify(batch)
460
+ updated = true
461
+ continue
462
+ }
463
+ }
464
+
465
+ return updated
466
+ }
467
+
468
+ async _optimisticBatch(batch) {
469
+ const rollbackSystem = this.system.bee.head()
470
+ const rollbackView = this._workingBee.head()
471
+
472
+ const t = await this.prepareBatch(batch)
473
+
474
+ if (t.view) {
475
+ this._workingBee.move(t.view)
476
+ }
477
+
478
+ asserts.assert(batch === t.tip[0], 'Batch must be first part of tip')
479
+
480
+ let failed = true
481
+
482
+ try {
483
+ if (await this.system.canApply(batch[0].key, true)) {
484
+ await this._applyBatch(batch, true)
485
+ failed = false
486
+ }
487
+ } catch {}
488
+
489
+ const w = failed ? null : await this.system.get(batch[0].key)
490
+ if (!w || w.length < batch[0].length) {
491
+ this._workingBee.move(rollbackView)
492
+ this.system.bee.move(rollbackSystem)
493
+ await this.system.reset()
494
+ return false
495
+ }
496
+
497
+ for (let i = 1; i < t.tip.length; i++) {
498
+ await this._applyBatch(t.tip[i], t.tip[i][0].optimistic)
499
+ }
500
+
501
+ return true
502
+ }
503
+
504
+ async prepareBatch(batch) {
505
+ const node = batch[0]
506
+
507
+ if (topo.isLinkingAll(node, this.system.heads)) {
508
+ return { undo: null, view: null, tip: [batch] }
509
+ }
510
+
511
+ const t = await topo.sort(this, batch)
512
+
513
+ if (t.undo) {
514
+ t.view = await this.system.undo(t.undo)
515
+ }
516
+
517
+ return t
518
+ }
519
+
520
+ async _processBatch(batch) {
521
+ const t = await this.prepareBatch(batch)
522
+
523
+ if (t.view) {
524
+ this._workingBee.move(t.view)
525
+ }
526
+
527
+ // first writer is always added with full permissions
528
+ if (this.system.isGenesis()) {
529
+ await this._host.addWriter(t.tip[0][0].key)
530
+ }
531
+
532
+ for (let i = 0; i < t.tip.length; i++) {
533
+ await this._applyBatch(t.tip[i], t.tip[i][0].optimistic)
534
+ }
535
+ }
536
+
537
+ async _applyBatch(batch, optimistic) {
538
+ const userBatch = []
539
+ for (const node of batch) {
540
+ this.system.addNode(node)
541
+
542
+ // compat: autobase nodes may be null
543
+ if (node.value) userBatch.push(node)
544
+ }
545
+
546
+ if (this._hasApply && (await this.system.canApply(batch[0].key, optimistic))) {
547
+ this._host.applying = batch
548
+ await this._handlers.apply(userBatch, this._workingView, this._host)
549
+ this._host.applying = null
550
+ }
551
+
552
+ const changed = await this.system.flush(batch, this._workingBee)
553
+
554
+ await this._storeBoot()
555
+
556
+ for (const { key, added } of changed) {
557
+ if (added) await this.writers.add(key)
558
+ else await this.writers.remove(key)
559
+ }
560
+ }
561
+
562
+ _storeBoot() {
563
+ const boot = this.system.bootRecord()
564
+ if (!boot) return
565
+ return this.local.setUserData('autobee/head', encoding.encodeBootRecord(boot))
566
+ }
567
+
568
+ static decodeValue(buf, opts) {
569
+ return encoding.decodeValue(buf, opts)
570
+ }
571
+
572
+ static encodeValue(value, opts) {
573
+ return encoding.encodeValue(value, opts)
574
+ }
575
+
576
+ async wakeup({ key, length }) {
577
+ await this._bootingState
578
+ await this.writers.wakeup(key, length)
579
+ await this._bump()
580
+ }
581
+
582
+ async append(values, { optimistic = false } = {}) {
583
+ if (!Array.isArray(values)) values = [values]
584
+
585
+ if (!this.opened) await this.ready()
586
+
587
+ await this.local.ready()
588
+
589
+ const links = this.system.getLinks(this.local.key)
590
+ const t = Date.now()
591
+ const batch = []
592
+
593
+ for (let i = 0; i < values.length; i++) {
594
+ const value = values[i]
595
+ const buffer = typeof value === 'string' ? b4a.from(value) : value
596
+ 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)
600
+ batch.push(node)
601
+ }
602
+
603
+ return this._bump()
604
+ }
605
+
606
+ async _flushLocal() {
607
+ // analyze is worth the trade off adding the view here also (technically not needed)
608
+ await this.writers.flushLocal(this._workingBee.head())
609
+ }
610
+ }
611
+
612
+ function isObject(o) {
613
+ return typeof o === 'object' && o && !b4a.isBuffer(o)
614
+ }
615
+
616
+ function noop() {}
617
+
618
+ function createAnchorCore(store, prologue, manifestData) {
619
+ const manifest = {
620
+ version: 2,
621
+ hash: 'blake2b',
622
+ prologue,
623
+ allowPatch: false,
624
+ quorum: 0,
625
+ signers: [],
626
+ userData: manifestData,
627
+ linked: null
628
+ }
629
+
630
+ const core = store.get({
631
+ manifest,
632
+ active: false
633
+ })
634
+
635
+ return core
636
+ }
@@ -0,0 +1,62 @@
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 genesis() {
26
+ return this.auto.system.isGenesis()
27
+ }
28
+
29
+ addWriter(key, { isIndexer = true } = {}) {
30
+ if (typeof key === 'string') key = ID.decode(key)
31
+ return this.auto.system.addWriter(key, { isIndexer })
32
+ }
33
+
34
+ ackWriter(key) {
35
+ if (typeof key === 'string') key = ID.decode(key)
36
+ return this.auto.system.ackWriter(key)
37
+ }
38
+
39
+ removeWriter(key) {
40
+ if (typeof key === 'string') key = ID.decode(key)
41
+ return this.auto.system.removeWriter(key)
42
+ }
43
+
44
+ preferFastForward() {
45
+ // this.auto._preferFastForward()
46
+ }
47
+
48
+ interrupt(reason) {
49
+ throw new Error('TODO')
50
+ // this.auto._interrupt(reason)
51
+ }
52
+
53
+ removeable(key) {
54
+ return true
55
+ }
56
+
57
+ createAnchor() {
58
+ return this.auto.createAnchor()
59
+ }
60
+ }
61
+
62
+ module.exports = ApplyCalls