autobee 2.0.0-rc.9 → 2.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/index.js CHANGED
@@ -14,16 +14,19 @@ const asserts = require('./lib/asserts.js')
14
14
  const boot = require('./lib/boot.js')
15
15
  const { resolveWeight, currentWeight } = require('./lib/witness.js')
16
16
  const encoding = require('./lib/encoding.js')
17
- const Reboot = require('./lib/reboot.js')
17
+ const { LEGACY_OPLOG_VERSION } = require('./lib/constants.js')
18
+ const FastForward = require('./lib/fast-forward.js')
18
19
  const System = require('./lib/system.js')
19
20
  const ApplyCalls = require('./lib/apply-calls.js')
20
21
  const topo = require('./lib/topo.js')
21
22
  const { ActiveWriters } = require('./lib/writers.js')
23
+ const TrustedPeers = require('./lib/trusted.js')
24
+ const ApplyView = require('./lib/apply-view.js')
22
25
  const UpdateChanges = require('./lib/updates.js')
26
+ const migrations = require('./lib/migrations.js')
23
27
 
24
28
  const EMPTY_HEAD = { length: 0, key: null }
25
29
  const INTERRUPT = new Error('Apply interrupted')
26
- const MIN_FF_GAP = 32
27
30
 
28
31
  module.exports = class Autobee extends ReadyResource {
29
32
  constructor(store, key = null, handlers = {}) {
@@ -34,9 +37,16 @@ module.exports = class Autobee extends ReadyResource {
34
37
  key = null
35
38
  }
36
39
 
37
- const { name = null, encrypted, encryptionKey, viewName = 'view' } = handlers
40
+ const {
41
+ name = null,
42
+ encrypted,
43
+ encryptionKey,
44
+ viewName = 'view',
45
+ bootstrapWeight = 2
46
+ } = handlers
38
47
 
39
48
  this.encrypted = encrypted === true || !!encryptionKey
49
+ this.bootstrapWeight = bootstrapWeight
40
50
 
41
51
  this.getSystemEncryption = this._getEncryptionProvider.bind(this, '_system')
42
52
  this.getViewEncryption = this._getEncryptionProvider.bind(this, viewName)
@@ -56,7 +66,8 @@ module.exports = class Autobee extends ReadyResource {
56
66
  this.discoveryKey = null
57
67
  this.id = null
58
68
  this.bootstrap = null
59
- this.handlers = handlers
69
+ this._handlers = handlers
70
+ this.stats = { undos: 0, fastForwards: 0, drains: 0, applies: 0, appends: 0 }
60
71
 
61
72
  this.system = new System(this.store.namespace('system'), this.name, {
62
73
  getEncryptionProvider: this.getSystemEncryption,
@@ -65,7 +76,7 @@ module.exports = class Autobee extends ReadyResource {
65
76
  this.system.auto = this
66
77
 
67
78
  this.bee = bee.snapshot()
68
- this.view = handlers.open ? handlers.open(this.bee, this) : this.bee
79
+ this.view = ApplyView.open(this.bee, this)
69
80
  this.optimistic = handlers.optimistic !== false // TODO: should default to false instead
70
81
 
71
82
  this.name = name // for debugging
@@ -76,23 +87,36 @@ module.exports = class Autobee extends ReadyResource {
76
87
  this.writers = null
77
88
  this.bumping = 0
78
89
 
79
- // system head to boot from: migrates or fast-forwards depending on its version
80
- this.bootFrom = handlers.bootFrom || null
90
+ // fastForward: false disables all fast-forwards - both wakeup and boot
91
+ const fastForward = handlers.fastForward === false ? null : handlers.fastForward || {}
92
+
93
+ this._ffEnabled = fastForward !== null
94
+
95
+ // oplog head to boot from: migrates or fast-forwards depending on its version
96
+ this.bootFrom = (fastForward && fastForward.boot) || null
97
+
98
+ // conservative (default on): only fast-forward onto a head someone can serve whole
99
+ this._conservativeFF = !fastForward || fastForward.conservative !== false
81
100
 
82
- this.reboot = null
83
- this.rebooting = null
84
- this.rebootTo = null
101
+ this.trusted = new TrustedPeers(handlers)
102
+
103
+ this.ff = null
104
+ this.fastForwarding = null
105
+ this.fastForwardTo = null
85
106
 
86
107
  this._workingBee = bee
87
- this._workingView = handlers.open ? handlers.open(this._workingBee, this) : this._workingBee
108
+ this._workingView = new ApplyView(this._workingBee, this)
88
109
 
89
110
  this._localSystemStart = 0
90
111
  this._localSystemLength = 0
112
+ this._localFlushes = 0
91
113
  this._localViewStart = 0
92
114
  this._localViewLength = 0
93
115
 
94
116
  this._appending = []
95
117
  this._draining = null
118
+ this._updating = null
119
+ this._bootWait = null
96
120
 
97
121
  this.legacyViews = handlers.legacyViews || []
98
122
 
@@ -100,13 +124,16 @@ module.exports = class Autobee extends ReadyResource {
100
124
  this._bootingState = null
101
125
  this._bootingAll = null
102
126
 
103
- this._handlers = handlers
104
127
  this._now = handlers.now || Date.now // overridable for clock-drift tests
128
+ this._preapply = handlers.preapply || null
129
+ this._preApplied = false
105
130
  this._hasApply = !!handlers.apply
106
131
  this._hasUpdate = !!handlers.update
107
132
  this._needsUpdate = false
108
- this._ackRequired = false
109
- this._ackedHeads = new Map()
133
+ this._approvalCheck = true
134
+ this._approvedPending = new Map()
135
+ this._approvalRequests = []
136
+ this._prefetchingApprovals = false
110
137
  this._updateLocalCore = null
111
138
  this._host = new ApplyCalls(this)
112
139
  this._notifyHandler = null
@@ -122,6 +149,7 @@ module.exports = class Autobee extends ReadyResource {
122
149
  this.previousDrain = 0
123
150
 
124
151
  this._catchupMigratedNodes = null
152
+ this._migratedHead = null
125
153
 
126
154
  this.ready().catch(noop)
127
155
  }
@@ -149,6 +177,10 @@ module.exports = class Autobee extends ReadyResource {
149
177
  return this.system.flushes
150
178
  }
151
179
 
180
+ get busy() {
181
+ return !!(this._draining || this._updating)
182
+ }
183
+
152
184
  async _open() {
153
185
  await this._preBoot()
154
186
 
@@ -159,14 +191,21 @@ module.exports = class Autobee extends ReadyResource {
159
191
  this._bootingAll.catch(safetyCatch)
160
192
 
161
193
  await this.bee.ready()
194
+ await this._workingBee.ready()
162
195
  await this._bootingState
163
196
 
164
197
  this._localSystemStart = this.system.bee.context.local.length
165
198
  this._localViewStart = this._workingBee.context.local.length
199
+ this._localFlushes = this.system.flushes
166
200
 
167
201
  this.bumpSoon()
168
202
  }
169
203
 
204
+ _requestWakeup() {
205
+ const session = this._wakeup._session
206
+ if (session) session.broadcastLookup()
207
+ }
208
+
170
209
  _registerWakeup() {
171
210
  if (!this.wakeupCapability) return
172
211
  this._wakeup.recouple()
@@ -211,6 +250,8 @@ module.exports = class Autobee extends ReadyResource {
211
250
 
212
251
  async _close() {
213
252
  this._interrupting = true
253
+ Hypercore.destroyRequests(this._approvalRequests, null)
254
+ if (this._bootWait !== null) this._bootWait.resolve()
214
255
  if (this._notifyHandler) this._notifyHandler.destroy()
215
256
  if (this._draining) {
216
257
  // drain may be waiting on replicated blocks
@@ -218,19 +259,21 @@ module.exports = class Autobee extends ReadyResource {
218
259
  await this._draining
219
260
  }
220
261
 
262
+ if (this._updating) await this._updating
263
+
221
264
  // let in-flight writer adds finish
222
265
  try {
223
266
  await this._bootingAll
224
267
  } catch {}
225
268
 
226
- if (this._handlers.close) await this._handlers.close(this.view)
269
+ await ApplyView.close(this.view, this)
227
270
 
228
271
  if (this.writers) await this.writers.close()
229
272
  await this.local.close()
230
273
  await this.system.close()
231
274
  await this._wakeup.close()
232
275
  if (this.bootstrap && !this.bootstrap.closed) await this.bootstrap.close()
233
- await this._workingBee.close()
276
+ await this._workingView.close()
234
277
  await this.bee.close()
235
278
  await this.store.close()
236
279
  }
@@ -242,25 +285,8 @@ module.exports = class Autobee extends ReadyResource {
242
285
  session.clearRequests(session.activeRequests, closingError)
243
286
  }
244
287
  }
245
- }
246
-
247
- async changesFrom({ system } = {}) {
248
- const info = await this.system.getInfo()
249
-
250
- const current = {
251
- flushes: info ? info.flushes : 0,
252
- system: this.system.bee.head(),
253
- view: info ? info.view : EMPTY_HEAD
254
- }
255
288
 
256
- // no common ancestor (null) -> flushes -1 never matches, forcing a full diff
257
- const shared = (await this.system.commonAncestor(system || EMPTY_HEAD)) || {
258
- flushes: -1,
259
- system: EMPTY_HEAD,
260
- view: EMPTY_HEAD
261
- }
262
-
263
- return UpdateChanges.from(shared, current)
289
+ if (this.fastForwarding) this.fastForwarding.clearRequests(closingError)
264
290
  }
265
291
 
266
292
  replicate(...args) {
@@ -277,6 +303,14 @@ module.exports = class Autobee extends ReadyResource {
277
303
  this._wakeup.hint(wakeup)
278
304
  }
279
305
 
306
+ // the view an oplog head points at, opened and closed through the handlers
307
+ openViewAt(oplog) {
308
+ const v = oplog.op.views.view
309
+ if (!v) return null
310
+
311
+ return new ApplyView(this.bee.checkout({ key: v.key, length: v.start + v.length }), this)
312
+ }
313
+
280
314
  openCore(key) {
281
315
  const encryption = this.encryptionKey ? new WriterEncryption(this) : null
282
316
  return this.store.get({ key, encryption })
@@ -302,7 +336,7 @@ module.exports = class Autobee extends ReadyResource {
302
336
  }
303
337
 
304
338
  if (this.bootFrom) {
305
- this.bootFrom = (await this.bootFrom) || null
339
+ this.bootFrom = getBootOption(await this.bootFrom)
306
340
  }
307
341
  }
308
342
 
@@ -354,7 +388,6 @@ module.exports = class Autobee extends ReadyResource {
354
388
  }
355
389
 
356
390
  this.local.setEncryption(this._getEncryptionProvider())
357
- this.local.setActive(true)
358
391
 
359
392
  this.writers = new ActiveWriters(this)
360
393
 
@@ -380,15 +413,26 @@ module.exports = class Autobee extends ReadyResource {
380
413
 
381
414
  await this.system.boot(system)
382
415
 
383
- // Use the view position from the system info (authoritative, post-processing)
384
- // rather than from the oplog (stale, captured at append time before _bump)
385
- let view = this.system.view || EMPTY_HEAD
416
+ const migrated = await this.local.getUserData('autobee/migrated-head')
417
+ if (migrated) this._migratedHead = encoding.decodeMigratedHead(migrated)
418
+
419
+ let view = this.system.view
420
+ if (!view) view = (this._migratedHead && this._migratedHead.view) || EMPTY_HEAD
386
421
 
387
422
  // @todo migration
388
423
  if (result.migration) {
389
- if (this.handlers.migrate) {
390
- view = (await this.handlers.migrate(result.migration.views)) || EMPTY_HEAD
424
+ if (this._handlers.migrate) {
425
+ view =
426
+ (await this._handlers.migrate(result.migration.views, result.migration.system)) ||
427
+ EMPTY_HEAD
391
428
  this._catchupMigratedNodes = result.migration.catchup
429
+
430
+ this._migratedHead = {
431
+ system: result.migration.system,
432
+ view: view.length ? view : (this._migratedHead && this._migratedHead.view) || null
433
+ }
434
+
435
+ await this._storeMigratedHead()
392
436
  }
393
437
 
394
438
  // ff boot invalidated by migration
@@ -405,6 +449,11 @@ module.exports = class Autobee extends ReadyResource {
405
449
  }
406
450
  }
407
451
 
452
+ // since we are doing this DURING the hyperbee2 boot we need to ready the core
453
+ // be good if we had better plumbing for it
454
+ await this._workingBee.core.ready()
455
+ await this.bee.core.ready()
456
+
408
457
  this._workingBee.move(view)
409
458
  this.bee.move(view)
410
459
 
@@ -414,10 +463,14 @@ module.exports = class Autobee extends ReadyResource {
414
463
  async _bootAll() {
415
464
  if (!(await this._bootReady())) return
416
465
 
417
- for await (const node of this.system.list()) {
418
- if (node.isAnchor) continue
419
- await this.writers.add(node.key)
466
+ for (const head of this.system.heads) {
467
+ await this.writers.add(head.key)
468
+ }
469
+
470
+ if (!this.system.heads.length) {
471
+ await this.writers.add(this.bootstrap.key)
420
472
  }
473
+
421
474
  await this._bump()
422
475
  }
423
476
 
@@ -428,7 +481,7 @@ module.exports = class Autobee extends ReadyResource {
428
481
  async _bump() {
429
482
  if (!(await this._bootReady())) return
430
483
 
431
- await this._flushWakeup()
484
+ if (this._bootWait !== null) this._bootWait.resolve()
432
485
 
433
486
  this.bumping++
434
487
 
@@ -483,18 +536,30 @@ module.exports = class Autobee extends ReadyResource {
483
536
  this.emit('error', err)
484
537
  }
485
538
 
539
+ // one-shot user gate: nothing applies until the host has resolved whatever
540
+ // state apply depends on (e.g. legacy views recorded by a migration)
541
+ async _runPreApply() {
542
+ if (this._preapply === null || this._preApplied) return
543
+
544
+ this._preApplied = true
545
+ await this._preapply(this.view)
546
+ }
547
+
486
548
  async _drain() {
487
549
  if (this._updating) await this._updating
488
550
 
551
+ await this._runPreApply()
552
+
553
+ this.stats.drains++
554
+
489
555
  if (this.bootFrom) {
490
- await this._initFromHead(this.bootFrom)
491
- this.bootFrom = null
492
- }
556
+ const { head = null, legacy = null, bootCondition = null, wait = false } = this.bootFrom
493
557
 
494
- // tracked across drain iteration
495
- this.system.shared = null
558
+ this.bootFrom = null
496
559
 
497
- this._ackRequired = false
560
+ if (legacy) await this._bootFromSystem(legacy)
561
+ else if (head) await this._bootFromHead(head, bootCondition, wait)
562
+ }
498
563
 
499
564
  const changes = this._hasUpdate ? new UpdateChanges(this) : null
500
565
  if (changes) changes.track()
@@ -510,21 +575,21 @@ module.exports = class Autobee extends ReadyResource {
510
575
 
511
576
  try {
512
577
  while (!this._interrupting) {
513
- if (this.rebootTo !== null) {
514
- await this._applyReboot()
515
- break // revaluate conditions...
516
- }
578
+ await this._flushWakeup()
579
+ if (this._interrupting) break
517
580
 
518
- if (await this._bumpPendingWriters()) {
519
- this._needsUpdate = true
520
- continue
581
+ if (this.fastForwardTo !== null) {
582
+ await this._applyFastForward()
583
+ break // revaluate conditions...
521
584
  }
522
585
 
523
- if (!this._appendAck()) break
586
+ if (!(await this._bumpPendingWriters())) break
524
587
  this._needsUpdate = true
525
588
  }
526
589
 
527
590
  await this._flushLocal()
591
+
592
+ if (!this._interrupting) await this.writers.refresh()
528
593
  } finally {
529
594
  if (this.bumping === 1) this.bumping = 0
530
595
  else this.bumping = 1
@@ -534,16 +599,15 @@ module.exports = class Autobee extends ReadyResource {
534
599
  this._draining = null
535
600
  if (this._interrupting) return
536
601
 
537
- if (this._needsUpdate) {
538
- const updating = rrp()
539
- this._updating = updating.promise
602
+ const updating = rrp()
603
+ this._updating = updating.promise
540
604
 
541
- try {
542
- await this._update(changes)
543
- } finally {
544
- this._updating = null
545
- updating.resolve()
546
- }
605
+ try {
606
+ if (this._needsUpdate) await this._update(changes)
607
+ await this._storeBoot()
608
+ } finally {
609
+ this._updating = null
610
+ updating.resolve()
547
611
  }
548
612
  }
549
613
 
@@ -576,87 +640,149 @@ module.exports = class Autobee extends ReadyResource {
576
640
  }
577
641
  }
578
642
 
579
- async _flushWakeup() {
643
+ // wakeup hints keep arriving asynchronously
644
+ async _applyWakeupHints() {
580
645
  const hints = this._wakeup.flush()
646
+ if (!hints.size) return []
581
647
 
582
648
  this.previousDrain = Date.now()
583
- this.rebootFromHeads(hints).catch(noop)
649
+ const results = await this._filterHints(hints)
650
+
651
+ for (const { key, length } of results) {
652
+ // wakeup() itself no-ops the add for an already-active writer, but still
653
+ // needs to run so it can hint() the announced length - skipping it here
654
+ // for active writers left gc with no hint to protect them
655
+ await this.writers.wakeup(key, length)
656
+ }
657
+
658
+ return results
659
+ }
584
660
 
661
+ async _filterHints(hints) {
662
+ const results = []
663
+ const promises = []
585
664
  for (const [hex, length] of hints) {
586
665
  const key = b4a.from(hex, 'hex')
587
- if (this.writers.has(hex)) continue
588
- await this.writers.wakeup(key, length)
666
+ promises.push(this._filterTracked(key, length, results))
589
667
  }
668
+ await Promise.all(promises)
669
+ return results
590
670
  }
591
671
 
592
- async _getOplog(key, length, opts = null) {
593
- const core = this.openCore(key)
672
+ async _filterTracked(key, length, results) {
673
+ if (length) {
674
+ const info = await this.system.get(key)
675
+ // if we've seen this already, ignore
676
+ if (info && info.length >= length) return
677
+ }
678
+ results.push({ key, length })
679
+ }
680
+
681
+ async _flushWakeup() {
682
+ const hints = await this._applyWakeupHints()
683
+ if (!hints.length) return
684
+
685
+ if (!this._ffEnabled) return
686
+
687
+ // a scheduled fast-forward is applied by the drain before we look again
688
+ if (this.fastForwardTo !== null || this.fastForwarding !== null) return
689
+ if (this._interrupting || this.closing || this.bootFrom) return
594
690
 
595
691
  try {
596
- await core.ready()
692
+ const heads = await this._readCandidateHeads(hints, FastForward.DEFAULT_TIMEOUT)
597
693
 
598
- const target = length >= 0 ? length : core.length
599
- if (target === 0) return null
694
+ const ff = await FastForward.fromHeads(this, heads, {
695
+ timeout: FastForward.DEFAULT_TIMEOUT
696
+ })
600
697
 
601
- const buf = await core.get(target - 1, opts)
602
- if (buf === null) return null
698
+ if (ff !== null) await this._runFastForward(ff)
699
+ } catch (err) {
700
+ safetyCatch(err)
701
+ }
702
+ }
603
703
 
604
- let op = encoding.decodeOplog(buf)
704
+ async _readCandidateHeads(hints, timeout) {
705
+ const promises = []
605
706
 
606
- // legacy nodes always inflate, but only indexers carry views - callers
607
- // that need system info must check op.views
608
- if (op.version < 3) {
609
- op = await this._inflateLegacyOplog(buf, core, target - 1, opts)
610
- }
707
+ for (const { key, length } of hints) {
708
+ if (length === 0) continue
709
+ promises.push(this._resolveOplogHint(key, length, timeout ? { timeout } : null))
710
+ }
711
+
712
+ const ops = await Promise.all(promises)
713
+ const heads = []
714
+
715
+ for (const res of ops) {
716
+ if (res === null) continue
717
+
718
+ // the head we were woken on is a candidate in its own right
719
+ heads.push({ key: res.key, length: res.length })
611
720
 
612
- return {
613
- key,
614
- length: target,
615
- op
721
+ if (!res.op.trusted) continue
722
+
723
+ for (const trusted of res.op.trusted) {
724
+ const head = this.trusted.read(trusted)
725
+ if (head !== null) heads.push(head)
616
726
  }
727
+ }
728
+
729
+ return heads
730
+ }
731
+
732
+ async _resolveOplogHint(key, length, opts) {
733
+ const core = this.openCore(key)
734
+
735
+ try {
736
+ await core.ready()
737
+ const max = Math.max(length, core.length)
738
+
739
+ const block = await this.readOplog(core, max, opts)
740
+ if (max >= core.length) return block // best one
741
+
742
+ return await this.readOplog(core, core.length, opts)
617
743
  } finally {
618
744
  await core.close()
619
745
  }
620
746
  }
621
747
 
622
- async _inflateLegacyOplog(buf, core, seq, opts = null) {
623
- const m = encoding.decodeRawOplog(buf)
748
+ async _getOplog(key, length, opts = null) {
749
+ const core = this.openCore(key)
624
750
 
625
- const op = {
626
- version: m.version,
627
- timestamp: 0,
628
- links: m.node.heads,
629
- batch: { start: 0, end: m.node.batch - 1 },
630
- views: null,
631
- optimistic: !!m.optimistic,
632
- value: m.node.value
751
+ try {
752
+ return await this.readOplog(core, length, opts)
753
+ } finally {
754
+ await core.close()
633
755
  }
756
+ }
757
+
758
+ // reads on a core the caller owns, so a walk does not churn a session per step
759
+ async readOplog(core, length, opts = null) {
760
+ await core.ready()
634
761
 
635
- if (m.digest === null || m.checkpoint === null) return op
762
+ const { conservative = false, timeout } = opts === null ? {} : opts
636
763
 
637
- const fetches = []
764
+ const target = length >= 0 ? length : core.length
765
+ if (target === 0) return null
638
766
 
639
- fetches.push(m.digest.pointer ? core.get(seq - m.digest.pointer, opts) : buf)
640
- fetches.push(
641
- m.checkpoint.pointer ? core.get(seq - m.checkpoint.system.checkpointer, opts) : buf
642
- )
767
+ // conservative: only proceed when a connected peer can serve the head whole
768
+ if (conservative && core.remoteContiguousLength < target) return null
643
769
 
644
- const [digestNode, checkpointNode] = await Promise.all(fetches)
645
- // a wait: false get we could not satisfy locally
646
- if (digestNode === null || checkpointNode === null) return op
770
+ const buf = await core.get(target - 1, { timeout })
771
+ if (buf === null) return null
647
772
 
648
- const { digest } = encoding.decodeRawOplog(digestNode)
649
- const { checkpoint } = encoding.decodeRawOplog(checkpointNode)
773
+ let op = encoding.decodeOplog(buf)
650
774
 
651
- op.views = {
652
- system: {
653
- key: digest.key,
654
- length: checkpoint.system.checkpoint.length
655
- },
656
- flushes: seq
775
+ // legacy nodes always inflate, but only indexers carry views - callers
776
+ // that need system info must check op.views
777
+ if (op.version < 3) {
778
+ op = await migrations.inflateLegacyOplog(buf, core, target - 1, timeout)
657
779
  }
658
780
 
659
- return op
781
+ return {
782
+ key: core.key,
783
+ length: target,
784
+ op
785
+ }
660
786
  }
661
787
 
662
788
  async _update(changes) {
@@ -716,7 +842,7 @@ module.exports = class Autobee extends ReadyResource {
716
842
  await this.bootstrap.setUserData('autobee/local', this.local.key)
717
843
  await oldLocal.close()
718
844
 
719
- // done, soft reboot
845
+ // done, soft restart
720
846
  this.emit('rotate-local-writer')
721
847
  }
722
848
 
@@ -780,86 +906,152 @@ module.exports = class Autobee extends ReadyResource {
780
906
  }
781
907
 
782
908
  async _bumpMigratedWriters() {
909
+ const opened = new Set()
910
+ let updated = false
911
+
783
912
  for (const batch of this._catchupMigratedNodes) {
784
913
  await this._processBatch(batch)
914
+ updated = true
915
+ for (const node of batch) {
916
+ if (node.from) opened.add(node.from)
917
+ }
785
918
  }
786
- }
787
919
 
788
- // append a null value node to ack writer
789
- _appendAck() {
790
- if (!this._ackRequired) return false
791
- if (!this.writers.writable) return false
920
+ for (const core of opened) await core.close()
921
+ return updated
922
+ }
792
923
 
793
- if (this.writers.localWriter.pending !== null) return false
924
+ async _prefetchApprovals() {
925
+ if (this._prefetchingApprovals) return
926
+ this._prefetchingApprovals = true
794
927
 
795
- const links = this.system.getLinks(this.local.key)
796
- const t = Math.max(this._now(), this.system.timestamp)
928
+ const activeRequests = this._approvalRequests
797
929
 
798
- let unlinked = false
799
- for (const { key, length } of links) {
800
- const hex = b4a.toString(key, 'hex')
801
- const acked = this._ackedHeads.get(hex) || 0
802
- if (length > acked) {
803
- unlinked = true
804
- break
930
+ try {
931
+ const prefetch = []
932
+ const standing = await this._standing(activeRequests)
933
+ if (standing <= 0 || !this._pendingWork(standing)) return
934
+ for await (const p of this._servableRequests(standing, activeRequests)) {
935
+ prefetch.push(this.system.get(p.key, { activeRequests }).catch(safetyCatch))
936
+ prefetch.push(this.system.grantHint(p.key, { activeRequests }).catch(safetyCatch))
805
937
  }
938
+ await Promise.allSettled(prefetch)
939
+ } finally {
940
+ this._prefetchingApprovals = false
806
941
  }
807
- if (!unlinked) {
808
- this._ackRequired = false
809
- return false
810
- }
942
+ }
943
+
944
+ async _standing(activeRequests) {
945
+ const rec = await this.system.get(this.local.key, { activeRequests })
946
+ return currentWeight(rec)
947
+ }
811
948
 
812
- for (const { key, length } of links) {
813
- this._ackedHeads.set(b4a.toString(key, 'hex'), length)
949
+ // only our own tiers: a request whose lower tiers are already anchored is
950
+ // somebody stronger's job, and must not keep every peer at this standing
951
+ // rescanning (or offering) while it waits
952
+ _pendingWork(standing) {
953
+ const digest = this.system.promotions.digest
954
+ const end = Math.min(standing, digest.length)
955
+ for (let w = 1; w <= end; w++) {
956
+ if (digest[w - 1]) return true
814
957
  }
958
+ return false
959
+ }
815
960
 
816
- this.writers.appendLocal(null, t, { start: 0, end: 0 }, links, false, null)
817
- this._ackRequired = false
818
- return true
961
+ // the requests we could serve and by how much - shared by the prefetch and
962
+ // the collector so the two cannot drift on what counts as ours
963
+ async *_servableRequests(standing, activeRequests) {
964
+ for await (const p of this.system.listPendingPromotions({ activeRequests })) {
965
+ const amount = Math.min(standing, p.weight)
966
+ if (amount <= 0) continue
967
+ yield { key: p.key, amount }
968
+ }
819
969
  }
820
970
 
821
- async _bumpPendingWriters() {
822
- if (this._catchupMigratedNodes !== null) {
823
- await this._bumpMigratedWriters()
824
- this._catchupMigratedNodes = null
971
+ async _collectApprovals() {
972
+ if (!this.system.promotions.changed && !this._approvalCheck) return null
973
+ if (!this.writers.writable) return null
974
+
975
+ const activeRequests = this._approvalRequests
976
+ const standing = await this._standing(activeRequests)
977
+ if (standing <= 0) return null
978
+
979
+ this.system.promotions.changed = false
980
+ this._approvalCheck = false
981
+
982
+ if (!this._pendingWork(standing)) return null
983
+
984
+ const approvals = []
985
+ const approving = []
986
+
987
+ for await (const p of this._servableRequests(standing, activeRequests)) {
988
+ approving.push(fetchApproval.call(this, p.key, p.amount, approvals))
825
989
  }
826
990
 
827
- let updated = false
991
+ if (!approving.length) return null
828
992
 
829
- const pending = this.writers.pending.slice()
993
+ try {
994
+ await Promise.all(approving)
995
+ } catch (err) {
996
+ if (this._interrupting) return null
997
+ throw err
998
+ }
830
999
 
831
- for (let i = pending.length - 1; i >= 0; i--) {
832
- const w = pending[i]
1000
+ if (!approvals.length) return null
833
1001
 
834
- const batch = await w.next()
835
- if (batch === null) continue
1002
+ for (const a of approvals) {
1003
+ this._approvedPending.set(b4a.toString(a.key, 'hex'), a.weight)
1004
+ }
836
1005
 
837
- if (w.isAdded || (w.isRemoved && w.hasReferrals())) {
838
- await this._processBatch(batch)
839
- w.notify(batch)
840
- updated = true
841
- continue
842
- }
1006
+ return approvals
843
1007
 
844
- if (this.optimistic && !w.isRemoved && batch[0].optimistic) {
845
- if (!(await this._optimisticBatch(batch))) {
846
- w.removePending()
847
- continue
848
- }
849
- w.notify(batch)
850
- updated = true
851
- continue
1008
+ async function fetchApproval(key, weight, approvals) {
1009
+ const hex = b4a.toString(key, 'hex')
1010
+ if ((this._approvedPending.get(hex) || 0) >= weight) return
1011
+ const [target, hint] = await Promise.all([
1012
+ this.system.get(key, { activeRequests }),
1013
+ this.system.grantHint(key, { activeRequests })
1014
+ ])
1015
+ if (!target || target.isRemoved) return
1016
+ if (hint && hint.weight >= weight) return
1017
+ approvals.push({ key, weight })
1018
+ }
1019
+ }
1020
+
1021
+ async _bumpPendingWriters({ local = false } = {}) {
1022
+ if (!local && this._catchupMigratedNodes !== null) {
1023
+ const updated = await this._bumpMigratedWriters()
1024
+ this._catchupMigratedNodes = null
1025
+ if (updated) return true
1026
+ }
1027
+
1028
+ // apply the best next node to keep the prefix stable
1029
+ const next = await this.writers.nextPendingNode({ local })
1030
+ if (next === null) return false
1031
+
1032
+ const { writer: w, batch } = next
1033
+
1034
+ if (w.isAdded || (w.isRemoved && w.hasReferrals())) {
1035
+ await this._processBatch(batch)
1036
+ w.notify(batch)
1037
+ return true
1038
+ }
1039
+
1040
+ if (this.optimistic && !w.isRemoved && batch[0].optimistic) {
1041
+ if (!(await this._optimisticBatch(batch))) {
1042
+ w.removePending()
1043
+ return true
852
1044
  }
1045
+ w.notify(batch)
1046
+ return true
853
1047
  }
854
1048
 
855
- return updated
1049
+ return true
856
1050
  }
857
1051
 
858
1052
  async _optimisticBatch(batch) {
859
1053
  const rollbackSystem = this.system.bee.head()
860
1054
  const rollbackView = this._workingBee.head()
861
- const rollbackShared = this.system.shared
862
- const rollbackAttestations = this.writers.attestations.length
863
1055
 
864
1056
  const t = await this.prepareBatch(batch)
865
1057
  if (t.view) this._workingBee.move(t.view)
@@ -881,9 +1073,6 @@ module.exports = class Autobee extends ReadyResource {
881
1073
  this._workingBee.move(rollbackView)
882
1074
  this.system.bee.move(rollbackSystem)
883
1075
  await this.system.reset()
884
- this.system.shared = rollbackShared
885
- // don't attest grants that were just undone
886
- this.writers.attestations.length = rollbackAttestations
887
1076
  return false
888
1077
  }
889
1078
  }
@@ -904,7 +1093,12 @@ module.exports = class Autobee extends ReadyResource {
904
1093
  const t = await topo.sort(this, batch)
905
1094
 
906
1095
  if (t.undo) {
1096
+ this.stats.undos++
1097
+ this.trusted.clear()
907
1098
  t.view = await this.system.undo(t.undo)
1099
+ if (!t.view.length && this._migratedHead && this._migratedHead.view) {
1100
+ t.view = this._migratedHead.view
1101
+ }
908
1102
  }
909
1103
 
910
1104
  return t
@@ -924,7 +1118,7 @@ module.exports = class Autobee extends ReadyResource {
924
1118
  }
925
1119
 
926
1120
  if (b4a.equals(batch[0].key, this.key) && batch[0].length === 1) {
927
- await this._host.addWriter(batch[0].key)
1121
+ await this._host.addWriter(batch[0].key, { weight: this.bootstrapWeight })
928
1122
  }
929
1123
 
930
1124
  await this._applyBatch(batch, batch[0].optimistic)
@@ -947,33 +1141,39 @@ module.exports = class Autobee extends ReadyResource {
947
1141
  for (const node of batch) {
948
1142
  this.system.addNode(node)
949
1143
 
1144
+ if (node.approvals) {
1145
+ for (const a of node.approvals) {
1146
+ await this.system.addWriter(a.key, {
1147
+ weight: a.weight,
1148
+ coord: { key: node.key, length: node.length },
1149
+ carrier: node.weight,
1150
+ anchor: true
1151
+ })
1152
+ }
1153
+ }
1154
+
950
1155
  // compat: autobase nodes may be null (legacy null decodes to 0-length buffer)
951
1156
  if (node.value && node.value.length) userBatch.push(node)
952
1157
  }
953
1158
 
954
1159
  if (this._hasApply && (await this.system.canApply(batch[0].key, optimistic))) {
1160
+ this.stats.applies++
955
1161
  this._host.applying = batch
956
- await this._handlers.apply(userBatch, this._workingView, this._host)
1162
+ await this._workingView.apply(userBatch)
957
1163
  this._host.applying = null
958
1164
  }
959
1165
 
960
- const { changed, witnessed } = await this.system.flush(batch, this._workingBee)
1166
+ const changed = await this.system.flush(batch, this._workingBee)
1167
+
1168
+ if (this.system.promotions.changed) this._prefetchApprovals().catch(safetyCatch)
961
1169
 
962
1170
  if (local) {
963
1171
  this._localSystemLength = this.system.bee.context.local.length - this._localSystemStart
964
1172
  this._localViewLength = this._workingBee.context.local.length - this._localViewStart
1173
+ this._localFlushes = this.system.flushes
965
1174
  }
966
1175
 
967
- this.writers.attest(witnessed)
968
-
969
- await this._storeBoot()
970
-
971
- for (const { key, added, isAnchor } of changed) {
972
- if (isAnchor) {
973
- // no Writer/ActiveWriters tracking, but still wake anything linked to it
974
- this.writers.triggers.trigger(b4a.toString(key, 'hex'), 1)
975
- continue
976
- }
1176
+ for (const { key, added } of changed) {
977
1177
  if (added) await this.writers.add(key)
978
1178
  else await this.writers.remove(key)
979
1179
  }
@@ -996,6 +1196,11 @@ module.exports = class Autobee extends ReadyResource {
996
1196
  return Promise.all(proms)
997
1197
  }
998
1198
 
1199
+ _storeMigratedHead() {
1200
+ const value = this._migratedHead ? encoding.encodeMigratedHead(this._migratedHead) : null
1201
+ return this.local.setUserData('autobee/migrated-head', value)
1202
+ }
1203
+
999
1204
  static decodeValue(buf, opts) {
1000
1205
  return encoding.decodeValue(buf, opts)
1001
1206
  }
@@ -1013,6 +1218,8 @@ module.exports = class Autobee extends ReadyResource {
1013
1218
  async append(values, { optimistic = false } = {}) {
1014
1219
  if (this.closing) throw new Error('Autobee closed')
1015
1220
 
1221
+ this.stats.appends++
1222
+
1016
1223
  if (!Array.isArray(values)) values = [values]
1017
1224
 
1018
1225
  if (!this.opened) await this.ready()
@@ -1029,27 +1236,17 @@ module.exports = class Autobee extends ReadyResource {
1029
1236
  const t = Math.max(this._now(), this.system.timestamp)
1030
1237
  const batch = []
1031
1238
 
1032
- // witnesses only ride upgrade windows. witness.weight is the value the backer's
1033
- // snapshot witnesses - verifiers recompute the same read and verify
1034
1239
  const rec = await this.system.get(this.local.key)
1035
1240
  let witness = null
1036
- if (rec && rec.maxWeight > currentWeight(rec)) {
1037
- const sorted = this.writers.witnesses.sort(
1038
- (a, b) => b.attestation.weight - a.attestation.weight
1039
- )
1040
-
1041
- for (const { key, length, attestation, manifest } of sorted) {
1042
- if (attestation.weight > rec.maxWeight) continue
1043
-
1044
- const backer = await this.system.get(key)
1045
- if (!backer || backer.isRemoved || backer.length < length) continue
1046
-
1047
- const { weight, signature } = attestation
1048
- witness = { weight, backer: { key, length, signature, manifest } }
1049
- break
1241
+ if (rec) {
1242
+ const hint = await this.system.grantHint(this.local.key)
1243
+ if (hint && hint.weight > currentWeight(rec)) {
1244
+ witness = { weight: hint.weight, link: { key: hint.key, length: hint.length } }
1050
1245
  }
1051
1246
  }
1052
1247
 
1248
+ const approvals = optimistic ? null : await this._collectApprovals()
1249
+
1053
1250
  for (let i = 0; i < values.length; i++) {
1054
1251
  const value = values[i]
1055
1252
  const buffer = typeof value === 'string' ? b4a.from(value) : value
@@ -1060,7 +1257,8 @@ module.exports = class Autobee extends ReadyResource {
1060
1257
  { start: i, end: values.length - 1 - i },
1061
1258
  lnk,
1062
1259
  optimistic,
1063
- i === 0 ? witness : null
1260
+ i === 0 ? witness : null,
1261
+ i === 0 ? approvals : null
1064
1262
  )
1065
1263
  batch.push(node)
1066
1264
  }
@@ -1069,8 +1267,13 @@ module.exports = class Autobee extends ReadyResource {
1069
1267
  }
1070
1268
 
1071
1269
  async _flushLocal() {
1270
+ // pull any available local nodes in before flushing
1271
+ while (!this._interrupting && (await this._bumpPendingWriters({ local: true }))) {
1272
+ this._needsUpdate = true
1273
+ }
1274
+
1072
1275
  await this.writers.flushLocal({
1073
- flushes: this.system.flushes,
1276
+ flushes: this._localFlushes,
1074
1277
  system: {
1075
1278
  key: this.system.bee.context.local.key,
1076
1279
  start: this._localSystemStart,
@@ -1085,135 +1288,166 @@ module.exports = class Autobee extends ReadyResource {
1085
1288
 
1086
1289
  this._localSystemStart = this.system.bee.context.local.length
1087
1290
  this._localSystemLength = 0
1291
+ this._localFlushes = this.system.flushes
1088
1292
  this._localViewStart = this._workingBee.context.local.length
1089
1293
  this._localViewLength = 0
1090
1294
  }
1091
1295
 
1092
- async rebootFromHeads(hints, { force = false } = {}) {
1093
- if (!this._handlers.onwakeup) return false
1094
- if (this._interrupting) return false
1095
- if (!hints.size || this.rebooting || this.rebootTo || this.bootFrom) {
1096
- return false
1097
- }
1098
-
1099
- const promises = []
1100
- const heads = []
1101
- for (const [hex, length] of hints) {
1102
- if (length === 0) continue
1103
- const key = b4a.from(hex, 'hex')
1104
- heads.push({ key, length })
1105
- promises.push(this._getOplog(key, length))
1106
- }
1296
+ async moveTo(head) {
1297
+ if (!this.opened) await this.ready()
1298
+ if (this.closing) throw new Error('Autobee closed')
1107
1299
 
1108
- const ops = await Promise.all(promises)
1109
- if (this.rebooting || this.rebootTo) return false
1300
+ const ff = await FastForward.fromHead(this, head, null, { force: true })
1301
+ if (ff === null) return null
1110
1302
 
1111
- let best = null
1112
- let bestFlushes = -1
1303
+ if (!(await this._runFastForward(ff))) return null
1113
1304
 
1114
- for (let i = 0; i < ops.length; i++) {
1115
- const res = ops[i]
1116
- if (res === null) continue
1305
+ return this.ff.promise
1306
+ }
1117
1307
 
1118
- if (res.op.views && res.op.views.flushes > bestFlushes) {
1119
- bestFlushes = res.op.views.flushes
1120
- best = res
1121
- }
1308
+ // legacy: ungated boot straight onto a system head
1309
+ async _bootFromSystem(system) {
1310
+ try {
1311
+ const ff = new FastForward(this, system, { timeout: FastForward.DEFAULT_TIMEOUT })
1312
+ return await this._runFastForward(ff)
1313
+ } catch (err) {
1314
+ safetyCatch(err)
1315
+ return false
1122
1316
  }
1317
+ }
1123
1318
 
1124
- if (best && force) {
1125
- return this._rebootFromHead(best, null, { force: true })
1319
+ // the boot head seeds the view every trust decision here is made against,
1320
+ // since our own view is still empty
1321
+ async _bootFromHead(head, bootCondition, wait = false) {
1322
+ // just a head: one attempt, we do not wait around if it cannot be read
1323
+ if (bootCondition === null && !wait) {
1324
+ try {
1325
+ const ff = await FastForward.fromHead(this, head, null, {
1326
+ force: true,
1327
+ timeout: FastForward.DEFAULT_TIMEOUT
1328
+ })
1329
+
1330
+ return ff !== null && (await this._runFastForward(ff))
1331
+ } catch (err) {
1332
+ safetyCatch(err)
1333
+ return false
1334
+ }
1126
1335
  }
1127
1336
 
1128
- if (best === null || bestFlushes - this.system.flushes < MIN_FF_GAP) return false
1337
+ // park until the head can be read and, if a condition is set, something
1338
+ // acceptable turns up
1339
+ let opened = null
1129
1340
 
1130
- const head = { key: best.key, length: best.length }
1341
+ try {
1342
+ while (!this._interrupting) {
1343
+ this._bootWait = rrp()
1344
+
1345
+ try {
1346
+ if (bootCondition === null) {
1347
+ const ff = await FastForward.fromHead(this, head, null, {
1348
+ force: true,
1349
+ timeout: FastForward.DEFAULT_TIMEOUT
1350
+ })
1351
+
1352
+ if (ff !== null && (await this._runFastForward(ff))) return true
1353
+ } else {
1354
+ if (opened === null) opened = await this._bootReference(head)
1355
+
1356
+ if (opened !== null) {
1357
+ if (await this._bootAttempt(head, bootCondition, opened.view)) {
1358
+ return true
1359
+ }
1360
+
1361
+ // the reference may be stale - resolve it afresh next attempt
1362
+ await opened.close()
1363
+ opened = null
1364
+ } else if (await this._bootLegacyHead(head)) {
1365
+ // boot legacy head through the migration pathway
1366
+ return true
1367
+ }
1368
+ }
1369
+ } catch (err) {
1370
+ safetyCatch(err)
1371
+ }
1131
1372
 
1132
- const v = best.op.views.view
1133
- const view = this.bee.checkout({ key: v.key, length: v.start + v.length })
1373
+ if (this._interrupting) break
1134
1374
 
1135
- let trusted = null
1136
- try {
1137
- trusted = await this._handlers.onwakeup(head, view, this)
1138
- if (!trusted || this.rebooting || this.rebootTo) return false
1375
+ await this._bootWait.promise
1376
+ }
1139
1377
  } finally {
1140
- view.close()
1378
+ this._bootWait = null
1379
+ if (opened !== null) await opened.close()
1141
1380
  }
1142
1381
 
1143
- return this._rebootFromHead(best, trusted)
1382
+ return false
1144
1383
  }
1145
1384
 
1146
- async _rebootFromHead(head, trusted, { force = false, wait = true } = {}) {
1147
- const oplog = await this._getOplog(head.key, head.length)
1148
- if (!oplog) return null
1385
+ async _bootAttempt(head, bootCondition, reference) {
1386
+ // peek, so the hints are still there for the drain once we have booted
1387
+ const result = await this._filterHints(this._wakeup.hints)
1149
1388
 
1150
- const verified = trusted ? await this._getOplog(trusted.key, trusted.length) : oplog
1151
- if (!verified) return null
1389
+ const candidates = await this._readCandidateHeads(result, FastForward.DEFAULT_TIMEOUT)
1152
1390
 
1153
- // legacy nodes from non-indexers have no system info to reboot from
1154
- if (!oplog.op.views || !verified.op.views) return null
1391
+ const ff = await FastForward.fromHeads(this, [head, ...candidates], {
1392
+ force: true,
1393
+ timeout: FastForward.DEFAULT_TIMEOUT,
1394
+ condition: bootCondition,
1395
+ reference
1396
+ })
1155
1397
 
1156
- if (!force && verified.op.views.flushes - this.system.flushes < MIN_FF_GAP) {
1157
- return null
1158
- }
1398
+ return ff !== null && (await this._runFastForward(ff))
1399
+ }
1159
1400
 
1160
- const moved = await this._moveTo(batchToHead(verified.op.views.system), {
1161
- system: batchToHead(oplog.op.views.system),
1162
- verified: {
1163
- op: trusted || head,
1164
- flushes: verified.op.views.flushes
1165
- }
1401
+ async _bootLegacyHead(head) {
1402
+ const oplog = await FastForward.flushHead(this, head, {
1403
+ timeout: FastForward.DEFAULT_TIMEOUT
1166
1404
  })
1405
+ if (oplog === null || oplog.op.version > LEGACY_OPLOG_VERSION) return false
1167
1406
 
1168
- if (moved && wait) return this.reboot.promise
1407
+ const ff = await FastForward.fromHead(this, head, null, {
1408
+ force: true,
1409
+ timeout: FastForward.DEFAULT_TIMEOUT
1410
+ })
1169
1411
 
1170
- return null
1412
+ return ff !== null && (await this._runFastForward(ff))
1171
1413
  }
1172
1414
 
1173
- // same as moveTo except we don't return the final promise
1174
- _initFromHead(head, tip) {
1175
- if (!head.length) {
1176
- // legacy fastForward boot
1177
- return this._runReboot(new Reboot(this, head, tip))
1178
- }
1415
+ async _bootReference(head) {
1416
+ const oplog = await FastForward.flushHead(this, head, {
1417
+ timeout: FastForward.DEFAULT_TIMEOUT
1418
+ })
1179
1419
 
1180
- return this._rebootFromHead(head, null, { force: true, wait: false })
1420
+ return oplog === null ? null : this.openViewAt(oplog)
1181
1421
  }
1182
1422
 
1183
- // head is a system head; reboots onto it, migrating in place if it's a legacy version
1184
- async _moveTo(systemHead, tip) {
1185
- if (this.rebootTo !== null || this.rebooting !== null) return false
1186
-
1187
- if (await this._runReboot(new Reboot(this, systemHead, tip))) {
1188
- return true
1423
+ async _runFastForward(ff) {
1424
+ if (this.fastForwardTo !== null || this.fastForwarding !== null) {
1425
+ await ff.close()
1426
+ return false
1189
1427
  }
1190
1428
 
1191
- return false
1192
- }
1193
-
1194
- async _runReboot(reboot) {
1195
- this.rebooting = reboot
1429
+ this.fastForwarding = ff
1196
1430
 
1197
- const result = await reboot.run()
1198
- await reboot.close()
1431
+ const result = await ff.run()
1432
+ await ff.close()
1199
1433
 
1200
- if (this.rebooting === reboot) this.rebooting = null
1434
+ if (this.fastForwarding === ff) this.fastForwarding = null
1201
1435
 
1202
1436
  if (!result) return false
1203
1437
 
1204
- this.rebootTo = result
1205
- this.reboot = rrp()
1438
+ this.fastForwardTo = result
1439
+ this.ff = rrp()
1206
1440
 
1207
1441
  this.bumpSoon()
1208
1442
 
1209
1443
  return true
1210
1444
  }
1211
1445
 
1212
- async _applyReboot() {
1446
+ async _applyFastForward() {
1213
1447
  const changes = this._hasUpdate ? new UpdateChanges(this) : null
1214
1448
  if (changes) changes.track()
1215
1449
 
1216
- const { head, tip, migrate } = this.rebootTo
1450
+ const { head, migrate } = this.fastForwardTo
1217
1451
 
1218
1452
  const from = this.system.bee.head()
1219
1453
  const to = head
@@ -1221,15 +1455,16 @@ module.exports = class Autobee extends ReadyResource {
1221
1455
  this.system.bee.move(head)
1222
1456
  await this.system.reset()
1223
1457
 
1224
- this.system.shared = this.rebootTo.shared || {
1225
- flushes: -1,
1226
- view: EMPTY_HEAD,
1227
- system: EMPTY_HEAD
1228
- }
1229
-
1230
1458
  // migrate is set when fast-forwarding from a legacy head
1231
1459
  if (migrate) {
1232
- const view = (await this.handlers.migrate(migrate)) || EMPTY_HEAD
1460
+ const view = (await this._handlers.migrate(migrate, head)) || EMPTY_HEAD
1461
+
1462
+ this._migratedHead = {
1463
+ system: head,
1464
+ view: view.length ? view : (this._migratedHead && this._migratedHead.view) || null
1465
+ }
1466
+
1467
+ await this._storeMigratedHead()
1233
1468
  this.bee.move(view)
1234
1469
  this._workingBee.move(view)
1235
1470
  } else {
@@ -1237,34 +1472,22 @@ module.exports = class Autobee extends ReadyResource {
1237
1472
  this._workingBee.move(this.system.view)
1238
1473
  }
1239
1474
 
1240
- this.rebootTo = null
1475
+ this._approvalCheck = true
1476
+ this.fastForwardTo = null
1477
+
1478
+ // process any wakeup while fast-forward itself was in flight
1479
+ await this._applyWakeupHints()
1241
1480
  await this.writers.refresh()
1242
1481
 
1482
+ // we moved, so ask our peers to tell us their heads again
1483
+ this._requestWakeup()
1484
+
1243
1485
  await this._update(changes)
1486
+ await this._storeBoot()
1244
1487
 
1488
+ this.stats.fastForwards++
1245
1489
  this.emit('move-to', to, from)
1246
- this.reboot.resolve({ to, from })
1247
-
1248
- // tip is null during boot
1249
- if (!tip) return
1250
-
1251
- try {
1252
- await this._reapply(tip)
1253
- } catch (err) {
1254
- throw err
1255
- }
1256
- }
1257
-
1258
- async _reapply({ system, verified }) {
1259
- const changes = this._hasUpdate ? new UpdateChanges(this) : null
1260
- if (changes) changes.track()
1261
-
1262
- const sys = this.system.bee.checkout(system)
1263
- const t = await topo.rollback(this, sys, verified)
1264
- await sys.close()
1265
-
1266
- await this.applyBacklog(t.tip)
1267
- return this._update(changes)
1490
+ this.ff.resolve({ to, from })
1268
1491
  }
1269
1492
 
1270
1493
  replay() {
@@ -1276,6 +1499,17 @@ function isObject(o) {
1276
1499
  return typeof o === 'object' && o && !b4a.isBuffer(o)
1277
1500
  }
1278
1501
 
1502
+ function getBootOption(boot) {
1503
+ if (!boot) return null
1504
+
1505
+ // oldest style, supported for now but will go away: a bare key is legacy
1506
+ if (boot.key) return { legacy: boot, head: null, bootCondition: null }
1507
+
1508
+ asserts.assert(!(boot.head && boot.legacy), 'Boot from either a head or a legacy pointer')
1509
+
1510
+ return boot
1511
+ }
1512
+
1279
1513
  function noop() {}
1280
1514
 
1281
1515
  function createAnchorCore(store, prologue, manifestData) {
@@ -1304,10 +1538,3 @@ function crashSoon(err) {
1304
1538
  })
1305
1539
  throw err
1306
1540
  }
1307
-
1308
- function batchToHead(b) {
1309
- return {
1310
- key: b.key,
1311
- length: b.start + b.length
1312
- }
1313
- }