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