autobee 1.0.3 → 1.0.4

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 CHANGED
@@ -1,8 +1,10 @@
1
1
  const ReadyResource = require('ready-resource')
2
+ const ReadyGuard = require('ready-guard')
2
3
  const b4a = require('b4a')
3
4
  const safetyCatch = require('safety-catch')
4
5
  const Hyperbee = require('hyperbee2')
5
6
  const ID = require('hypercore-id-encoding')
7
+ const rrp = require('resolve-reject-promise')
6
8
  const { AutobeeEncryption, WriterEncryption } = require('autobee-encryption')
7
9
  const AutobeeWakeup = require('autobee-wakeup')
8
10
  const Hypercore = require('hypercore')
@@ -11,6 +13,7 @@ const c = require('compact-encoding')
11
13
  const asserts = require('./lib/asserts.js')
12
14
  const boot = require('./lib/boot.js')
13
15
  const encoding = require('./lib/encoding.js')
16
+ const FastForward = require('./lib/fast-forward.js')
14
17
  const System = require('./lib/system.js')
15
18
  const ApplyCalls = require('./lib/apply-calls.js')
16
19
  const topo = require('./lib/topo.js')
@@ -19,6 +22,7 @@ const UpdateChanges = require('./lib/updates.js')
19
22
 
20
23
  const EMPTY_HEAD = { length: 0, key: null }
21
24
  const INTERRUPT = new Error('Apply interrupted')
25
+ const MIN_FF_GAP = 32
22
26
 
23
27
  module.exports = class Autobee extends ReadyResource {
24
28
  constructor(store, key = null, handlers = {}) {
@@ -32,14 +36,15 @@ module.exports = class Autobee extends ReadyResource {
32
36
  const { name = null, encrypted, encryptionKey } = handlers
33
37
 
34
38
  this.encrypted = encrypted === true || !!encryptionKey
39
+ this._getEncryptionProviderBound = this._getEncryptionProvider.bind(this)
35
40
 
36
41
  const bee = new Hyperbee(store.namespace('view'), {
37
42
  // defer one tick to ensure consistent state, then return state prom
38
43
  preload: async () => {
39
44
  await 1
40
- await this._bootingState
45
+ if (!this._bootGuard.opened) await this._bootGuard.ready()
41
46
  },
42
- getEncryptionProvider: () => this._getEncryptionProvider()
47
+ getEncryptionProvider: this._getEncryptionProviderBound
43
48
  })
44
49
 
45
50
  this.store = store
@@ -50,7 +55,7 @@ module.exports = class Autobee extends ReadyResource {
50
55
  this.bootstrap = null
51
56
 
52
57
  this.system = new System(this.store.namespace('system'), this.name, {
53
- getEncryptionProvider: () => this._getEncryptionProvider(),
58
+ getEncryptionProvider: this._getEncryptionProviderBound,
54
59
  encrypted: this.encrypted
55
60
  })
56
61
 
@@ -66,12 +71,18 @@ module.exports = class Autobee extends ReadyResource {
66
71
  this.writers = null
67
72
  this.bumping = 0
68
73
 
74
+ this.fastForwardBoot = handlers.fastForward || null
75
+ this.fastForward = null
76
+ this.fastForwarding = null
77
+ this.fastForwardTo = null
78
+
69
79
  this._workingBee = bee
70
80
  this._workingView = handlers.open ? handlers.open(this._workingBee, this) : this._workingBee
71
81
 
72
82
  this._appending = []
73
83
  this._draining = null
74
84
 
85
+ this._bootGuard = new ReadyGuard()
75
86
  this._bootingState = null
76
87
  this._bootingAll = null
77
88
 
@@ -81,13 +92,16 @@ module.exports = class Autobee extends ReadyResource {
81
92
  this._needsUpdate = false
82
93
  this._updateLocalCore = null
83
94
  this._host = new ApplyCalls(this)
95
+ this._notifyHandler = null
84
96
 
85
97
  this.interrupted = null
86
98
  this._interrupting = false
87
99
  this._onErrorBound = this._onError.bind(this)
100
+ this._bumpSoonBound = this.bumpSoon.bind(this)
88
101
 
89
- this._wakeup = new AutobeeWakeup(this, handlers)
90
102
  this.wakeupCapability = null
103
+ this._wakeup = new AutobeeWakeup(this, handlers)
104
+ this._previousDrain = 0
91
105
 
92
106
  this.ready().catch(noop)
93
107
  }
@@ -140,7 +154,7 @@ module.exports = class Autobee extends ReadyResource {
140
154
  return keys
141
155
  }
142
156
 
143
- async getWriterViews(key) {
157
+ getWriterViews(key) {
144
158
  const id = b4a.toString(key, 'hex')
145
159
  const w = this.writers.active.get(id)
146
160
  if (!w) return []
@@ -160,6 +174,7 @@ module.exports = class Autobee extends ReadyResource {
160
174
 
161
175
  async _close() {
162
176
  this._interrupting = true
177
+ if (this._notifyHandler) this._notifyHandler.destroy()
163
178
  if (this._draining) await this._draining
164
179
 
165
180
  if (this._handlers.close) await this._handlers.close(this.view)
@@ -215,6 +230,8 @@ module.exports = class Autobee extends ReadyResource {
215
230
  }
216
231
 
217
232
  async _bootState() {
233
+ if (!this._bootGuard.enter()) return this._bootGuard.ready()
234
+
218
235
  const result = await boot(this.store, this.key, {
219
236
  encryptionKey: this.encryptionKey,
220
237
  keyPair: this.keyPair
@@ -244,6 +261,13 @@ module.exports = class Autobee extends ReadyResource {
244
261
 
245
262
  this._registerWakeup()
246
263
 
264
+ if (this.bootstrap !== this.local) {
265
+ await this.bootstrap.setGroup(this.wakeupCapability.discoveryKey)
266
+ }
267
+
268
+ this._notifyHandler = this.store.notifyGroup(this.wakeupCapability.discoveryKey)
269
+ this._notifyHandler.on('update', this._bumpSoonBound)
270
+
247
271
  const system = result.system || EMPTY_HEAD
248
272
 
249
273
  await this.system.boot(system)
@@ -256,10 +280,14 @@ module.exports = class Autobee extends ReadyResource {
256
280
  this.bee.move(view)
257
281
 
258
282
  await this.writers.updateLocalState()
283
+
284
+ this._bootGuard.exit()
285
+
286
+ return this._bootGuard.ready()
259
287
  }
260
288
 
261
289
  async _bootAll() {
262
- await this._bootingState
290
+ if (!this._bootGuard.opened) await this._bootGuard.ready()
263
291
 
264
292
  for await (const node of this.system.list()) {
265
293
  await this.writers.add(node.key)
@@ -272,7 +300,10 @@ module.exports = class Autobee extends ReadyResource {
272
300
  }
273
301
 
274
302
  async _bump() {
303
+ if (!this._bootGuard.opened) await this._bootGuard.ready()
304
+
275
305
  await this._flushWakeup()
306
+
276
307
  this.bumping++
277
308
 
278
309
  if (!this._draining) {
@@ -327,18 +358,30 @@ module.exports = class Autobee extends ReadyResource {
327
358
  }
328
359
 
329
360
  async _drain() {
330
- if (this._updateLocalCore !== null) {
331
- await this._rotateLocalWriter(this._updateLocalCore)
361
+ if (this.fastForwardBoot) {
362
+ this._initFastForward(this.fastForwardBoot)
363
+ this.fastForwardBoot = null
332
364
  }
333
365
 
334
366
  const changes = this._hasUpdate ? new UpdateChanges(this) : null
335
367
  if (changes) changes.track()
336
368
 
369
+ // Anything expecting work to be done during bumpSoon should do it here
337
370
  while (!this._interrupting && this.bumping > 0) {
338
- if (this._interrupting) return
371
+ if (this._interrupting) break
372
+
373
+ // Ensure we catch updates during the drain (i.e. setLocal will bump)
374
+ if (this._updateLocalCore !== null) {
375
+ await this._rotateLocalWriter(this._updateLocalCore)
376
+ }
339
377
 
340
378
  try {
341
379
  while (!this._interrupting) {
380
+ if (this.fastForwardTo !== null) {
381
+ await this._applyFastForward()
382
+ break // revaluate conditions...
383
+ }
384
+
342
385
  if (!(await this._bumpPendingWriters())) break
343
386
  this._needsUpdate = true
344
387
  }
@@ -361,6 +404,14 @@ module.exports = class Autobee extends ReadyResource {
361
404
  async _flushWakeup() {
362
405
  const hints = this._wakeup.flush()
363
406
 
407
+ for await (const key of this._notifyHandler.updates({ since: this.previousDrain })) {
408
+ const hex = b4a.toString(key, 'hex')
409
+ hints.set(hex, -1)
410
+ }
411
+
412
+ this.previousDrain = Date.now()
413
+ this.queueWakeupFastForward(hints).catch(noop)
414
+
364
415
  for (const [hex, length] of hints) {
365
416
  const key = b4a.from(hex, 'hex')
366
417
  if (this.writers.has(hex)) continue
@@ -372,6 +423,25 @@ module.exports = class Autobee extends ReadyResource {
372
423
  }
373
424
  }
374
425
 
426
+ _initFastForward({ key, length }) {
427
+ this.moveTo({ key, length })
428
+ }
429
+
430
+ async _getOplog(key, length) {
431
+ const core = this.openCore(key)
432
+ await core.ready()
433
+
434
+ const target = length >= 0 ? length : core.length
435
+ if (target === 0) return null
436
+
437
+ const buf = await core.get(target - 1)
438
+ await core.close()
439
+
440
+ if (buf === null) return null
441
+
442
+ return encoding.decodeOplog(buf)
443
+ }
444
+
375
445
  _update(changes) {
376
446
  this._needsUpdate = false
377
447
  this.bee.update(this._workingBee.root)
@@ -407,7 +477,7 @@ module.exports = class Autobee extends ReadyResource {
407
477
 
408
478
  let runs = 0
409
479
  while (!this._interrupting && this.appending && runs++ < 16) await this.update()
410
- await this.bumpSoon()
480
+ this.bumpSoon()
411
481
  }
412
482
 
413
483
  async _rotateLocalWriter(newLocal) {
@@ -573,6 +643,10 @@ module.exports = class Autobee extends ReadyResource {
573
643
  this._workingBee.move(t.view)
574
644
  }
575
645
 
646
+ return this._processApplyBatch(t)
647
+ }
648
+
649
+ async _processApplyBatch(t) {
576
650
  // first writer is always added with full permissions
577
651
  if (this.system.isGenesis()) {
578
652
  await this._host.addWriter(t.tip[0][0].key)
@@ -623,7 +697,7 @@ module.exports = class Autobee extends ReadyResource {
623
697
  }
624
698
 
625
699
  async wakeup({ key, length }) {
626
- await this._bootingState
700
+ if (!this._bootGuard.opened) await this._bootGuard.ready()
627
701
  await this.writers.wakeup(key, length)
628
702
  await this._bump()
629
703
  }
@@ -655,8 +729,119 @@ module.exports = class Autobee extends ReadyResource {
655
729
  await this.writers.flushLocal(this._workingBee.head())
656
730
  }
657
731
 
658
- async replay() {
659
- return topo.replay(this)
732
+ moveTo(head, tip) {
733
+ if (this.fastForwardTo !== null || this.fastForwarding !== null) return null
734
+ return this._runFastForward(new FastForward(this, head, tip)).catch(noop)
735
+ }
736
+
737
+ async queueWakeupFastForward(hints) {
738
+ if (!this._handlers.onwakeup) return false
739
+ if (!hints.size || this.fastForwarding || this.fastForwardTo || this.fastForwardBoot) {
740
+ return false
741
+ }
742
+
743
+ const promises = []
744
+ for (const [hex, length] of hints) {
745
+ if (length === 0) continue
746
+ const key = b4a.from(hex, 'hex')
747
+ promises.push(this._getOplog(key, length))
748
+ }
749
+
750
+ const ops = await Promise.all(promises)
751
+ if (this.fastForwarding || this.fastForwardTo) return false
752
+
753
+ let best = null
754
+ let bestFlushes = -1
755
+
756
+ for (const msg of ops) {
757
+ if (msg === null) continue
758
+
759
+ if (msg.views && msg.views.flushes > bestFlushes) {
760
+ bestFlushes = msg.views.flushes
761
+ best = msg.views
762
+ }
763
+ }
764
+
765
+ if (best === null || bestFlushes - this.system.flushes < MIN_FF_GAP) return false
766
+
767
+ const view = this.bee.checkout(best.view)
768
+
769
+ let trusted = null
770
+ try {
771
+ trusted = await this._handlers.onwakeup(view, this)
772
+ if (!trusted || this.fastForwarding || this.fastForwardTo) return false
773
+ } finally {
774
+ view.close()
775
+ }
776
+
777
+ const oplog = await this._getOplog(trusted.key, trusted.length)
778
+
779
+ return this.moveTo(oplog.views.system, {
780
+ system: best.system,
781
+ verified: {
782
+ node: trusted,
783
+ flushes: oplog.views.flushes
784
+ }
785
+ })
786
+ }
787
+
788
+ async _runFastForward(ff) {
789
+ this.fastForwarding = ff
790
+
791
+ const result = await ff.upgrade()
792
+ await ff.close()
793
+
794
+ if (this.fastForwarding === ff) this.fastForwarding = null
795
+
796
+ if (!result) return null
797
+
798
+ this.fastForwardTo = result
799
+ this.fastForward = rrp()
800
+
801
+ this.bumpSoon()
802
+
803
+ return this.fastForward.promise
804
+ }
805
+
806
+ async _applyFastForward() {
807
+ const changes = this._hasUpdate ? new UpdateChanges(this) : null
808
+ if (changes) changes.track(this._applyState)
809
+
810
+ const { head, tip } = this.fastForwardTo
811
+
812
+ const from = this.system.bee.head()
813
+ const to = head
814
+
815
+ this.system.bee.move(head)
816
+ await this.system.reset()
817
+
818
+ this.bee.move(this.system.view)
819
+ this._workingBee.move(this.system.view)
820
+
821
+ this.fastForwardTo = null
822
+
823
+ await this.writers.refresh()
824
+
825
+ if (changes) {
826
+ changes.finalise()
827
+ await this._handlers.update(this.view, changes)
828
+ }
829
+
830
+ this.emit('move-to', to, from)
831
+ this.fastForward.resolve({ to, from })
832
+
833
+ // tip is null when handlers.fastForward set
834
+ if (!tip) return
835
+
836
+ return this._reapply(tip)
837
+ }
838
+
839
+ async _reapply({ system, verified }) {
840
+ const sys = this.system.bee.checkout(system)
841
+ const t = await topo.rollback(this, sys, verified)
842
+ await sys.close()
843
+
844
+ return this._processApplyBatch(t)
660
845
  }
661
846
  }
662
847
 
@@ -0,0 +1,71 @@
1
+ const safetyCatch = require('safety-catch')
2
+
3
+ const System = require('./system.js')
4
+
5
+ const DEFAULT_OP_TIMEOUT = 5_000
6
+
7
+ module.exports = class FastForward {
8
+ constructor(auto, head, tip, { timeout = DEFAULT_OP_TIMEOUT } = {}) {
9
+ this.auto = auto
10
+ this.system = new System(auto.store.namespace('fast-forward'), null, {
11
+ getEncryptionProvider: this.auto._getEncryptionProviderBound,
12
+ encrypted: this.encrypted
13
+ })
14
+
15
+ this.head = head
16
+ this.timeout = timeout
17
+ this.tip = tip || null
18
+ this.destroyed = false
19
+ this.upgrading = null
20
+ this.failed = false
21
+ this.cores = []
22
+ }
23
+
24
+ async upgrade() {
25
+ try {
26
+ if (!this.upgrading) this.upgrading = this._upgrade()
27
+
28
+ if (!(await this.upgrading)) return null
29
+
30
+ return {
31
+ head: this.head,
32
+ tip: this.tip
33
+ }
34
+ } catch (err) {
35
+ safetyCatch(err)
36
+ this.failed = true
37
+ return null
38
+ } finally {
39
+ await this.close()
40
+ }
41
+ }
42
+
43
+ async _upgrade() {
44
+ await this.system.boot(this.head)
45
+
46
+ const promises = []
47
+
48
+ // ensure local key is locally available always
49
+ promises.push(this.system.get(this.auto.local.key, { timeout: this.timeout }))
50
+
51
+ const view = this.auto.store.get({ key: this.system.view.key, active: true })
52
+ this.cores.push(view)
53
+
54
+ promises.push(view.get(this.system.view.length - 1, { timeout: this.timeout }))
55
+
56
+ for (const head of this.system.heads) {
57
+ promises.push(this.system.get(head.key, { timeout: this.timeout }))
58
+ }
59
+
60
+ await Promise.all(promises)
61
+ if (this.destroyed) return false
62
+
63
+ return true
64
+ }
65
+
66
+ async close() {
67
+ this.destroyed = true
68
+ if (this.system) await this.system.close()
69
+ for (const core of this.cores) await core.close()
70
+ }
71
+ }
package/lib/topo.js CHANGED
@@ -5,8 +5,8 @@ const asserts = require('./asserts.js')
5
5
 
6
6
  const EMPTY_HEAD = { length: 0, key: null }
7
7
 
8
- exports.replay = replay
9
8
  exports.sort = sort
9
+ exports.rollback = rollback
10
10
 
11
11
  exports.isLinking = isLinking
12
12
  exports.isLinkingAll = isLinkingAll
@@ -29,17 +29,29 @@ class Clock {
29
29
  }
30
30
  }
31
31
 
32
- async function replay(auto) {
33
- const ch = auto.system.bee.createChangesStream()
34
- const replay = []
32
+ async function rollback(auto, system, verified) {
33
+ const tip = []
34
+ const ch = system.createChangesStream()
35
35
 
36
36
  for await (const data of ch) {
37
37
  const { ref, info } = getOplog(data)
38
- replay.push(ref)
38
+ if (sameNode(ref.oplog, verified.node)) {
39
+ asserts.assert(info.flushes === verified.flushes, 'Expect tip to be continuous')
40
+ break
41
+ }
42
+
43
+ tip.push(ref)
39
44
  }
40
45
 
41
- const inflated = await inflate(auto, replay.reverse())
42
- return inflated.flatMap((b) => b.batch)
46
+ const inflated = await inflate(auto, tip)
47
+
48
+ // @todo: verify sort
49
+
50
+ return {
51
+ undo: null,
52
+ view: null,
53
+ tip: inflated.map(getBatch)
54
+ }
43
55
  }
44
56
 
45
57
  async function sort(auto, batch) {
@@ -269,3 +281,11 @@ function isLinkingAll(node, heads) {
269
281
 
270
282
  return true
271
283
  }
284
+
285
+ function sameNode(a, b) {
286
+ return b4a.equals(a.key, b.key) && a.length === b.length
287
+ }
288
+
289
+ function getBatch(b) {
290
+ return b.batch
291
+ }
package/lib/writers.js CHANGED
@@ -37,7 +37,7 @@ class ActiveWriters {
37
37
  }
38
38
 
39
39
  bump() {
40
- this.auto._bump()
40
+ this.auto.bumpSoon()
41
41
  }
42
42
 
43
43
  async wakeup(key, length) {
@@ -66,6 +66,7 @@ class ActiveWriters {
66
66
 
67
67
  this.localWriter = new Writer(this, this.auto.system, true, core, b4a.toString(core.key, 'hex'))
68
68
  this.localWriter.addPending()
69
+ this.auto.emit('writer', this.localWriter)
69
70
  }
70
71
 
71
72
  clearLocal() {
@@ -76,6 +77,20 @@ class ActiveWriters {
76
77
  return this.localWriter.flush(head)
77
78
  }
78
79
 
80
+ async refresh() {
81
+ if (this.localWriter && !this.localWriter.closed) {
82
+ await this.updateLocalState()
83
+ }
84
+
85
+ for (const writer of this.active.values()) {
86
+ await writer.update()
87
+
88
+ if (writer !== this.localWriter && !writer.isIndexer) {
89
+ await writer.detachAndClose()
90
+ }
91
+ }
92
+ }
93
+
79
94
  async updateLocalState() {
80
95
  const w = this.localWriter
81
96
  const bootstrapping = w.core.length === 0 && b4a.equals(this.auto.key, w.core.key)
@@ -96,7 +111,8 @@ class ActiveWriters {
96
111
  }
97
112
 
98
113
  async add(key) {
99
- if (b4a.equals(key, this.auto.local.key)) await this.updateLocalState()
114
+ const local = b4a.equals(key, this.auto.local.key)
115
+ if (local) await this.updateLocalState()
100
116
 
101
117
  const id = b4a.toString(key, 'hex')
102
118
  let w = this.active.get(id)
@@ -109,8 +125,10 @@ class ActiveWriters {
109
125
 
110
126
  const core = this.auto.store.get({
111
127
  key,
112
- encryption
128
+ encryption,
129
+ group: local ? null : this.auto.wakeupCapability.discoveryKey
113
130
  })
131
+
114
132
  await core.ready()
115
133
  await core.setUserData('referrer', this.auto.key)
116
134
 
@@ -239,6 +257,7 @@ class Writer {
239
257
  if (this.isAttached) return
240
258
  this.isAttached = true
241
259
  this.writers.active.set(this.id, this)
260
+ this.writers.auto.emit('writer', this)
242
261
  this.addPending()
243
262
  }
244
263
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autobee",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "Bee based autobase",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Holepunch Inc.",
@@ -28,15 +28,17 @@
28
28
  "b4a": "^1.8.0",
29
29
  "compact-encoding": "^2.18.0",
30
30
  "hyperbee2": "^2.8.0",
31
- "hypercore": "^11.28.1",
31
+ "hypercore": "^11.33.0",
32
32
  "hypercore-id-encoding": "^1.3.0",
33
33
  "hyperschema": "^1.20.1",
34
+ "ready-guard": "^1.0.0",
34
35
  "ready-resource": "^1.2.0",
36
+ "resolve-reject-promise": "^1.1.0",
35
37
  "safety-catch": "^1.0.3"
36
38
  },
37
39
  "devDependencies": {
38
40
  "brittle": "^3.19.1",
39
- "corestore": "^7.7.0",
41
+ "corestore": "^7.10.0",
40
42
  "hypercore-crypto": "^3.6.1",
41
43
  "lunte": "^1.6.0",
42
44
  "prettier": "^3.8.1",