autobee 2.0.0-rc.8 → 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
100
+
101
+ this.trusted = new TrustedPeers(handlers)
81
102
 
82
- this.reboot = null
83
- this.rebooting = null
84
- this.rebootTo = null
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,70 +640,148 @@ 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)
584
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
+ }
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) {
593
- const core = this.openCore(key)
594
- await core.ready()
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
+ }
595
680
 
596
- const target = length >= 0 ? length : core.length
597
- if (target === 0) return null
681
+ async _flushWakeup() {
682
+ const hints = await this._applyWakeupHints()
683
+ if (!hints.length) return
598
684
 
599
- const buf = await core.get(target - 1)
600
- let op = encoding.decodeOplog(buf)
685
+ if (!this._ffEnabled) return
601
686
 
602
- if (op.version < 3) {
603
- op = await this._inflateLegacyOplog(buf, core, target - 1)
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
690
+
691
+ try {
692
+ const heads = await this._readCandidateHeads(hints, FastForward.DEFAULT_TIMEOUT)
693
+
694
+ const ff = await FastForward.fromHeads(this, heads, {
695
+ timeout: FastForward.DEFAULT_TIMEOUT
696
+ })
697
+
698
+ if (ff !== null) await this._runFastForward(ff)
699
+ } catch (err) {
700
+ safetyCatch(err)
604
701
  }
702
+ }
605
703
 
606
- await core.close()
704
+ async _readCandidateHeads(hints, timeout) {
705
+ const promises = []
607
706
 
608
- if (buf === null) return null
707
+ for (const { key, length } of hints) {
708
+ if (length === 0) continue
709
+ promises.push(this._resolveOplogHint(key, length, timeout ? { timeout } : null))
710
+ }
609
711
 
610
- return {
611
- key,
612
- length: target,
613
- op
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 })
720
+
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)
726
+ }
614
727
  }
728
+
729
+ return heads
615
730
  }
616
731
 
617
- async _inflateLegacyOplog(buf, core, seq) {
618
- const m = encoding.decodeRawOplog(buf)
619
- const fetches = []
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)
620
738
 
621
- fetches.push(m.digest.pointer ? core.get(seq - m.digest.pointer) : buf)
622
- fetches.push(m.checkpoint.pointer ? core.get(seq - m.checkpoint.system.checkpointer) : buf)
739
+ const block = await this.readOplog(core, max, opts)
740
+ if (max >= core.length) return block // best one
623
741
 
624
- const [digestNode, checkpointNode] = await Promise.all(fetches)
742
+ return await this.readOplog(core, core.length, opts)
743
+ } finally {
744
+ await core.close()
745
+ }
746
+ }
625
747
 
626
- const { digest } = encoding.decodeRawOplog(digestNode)
627
- const { checkpoint } = encoding.decodeRawOplog(checkpointNode)
748
+ async _getOplog(key, length, opts = null) {
749
+ const core = this.openCore(key)
750
+
751
+ try {
752
+ return await this.readOplog(core, length, opts)
753
+ } finally {
754
+ await core.close()
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()
761
+
762
+ const { conservative = false, timeout } = opts === null ? {} : opts
763
+
764
+ const target = length >= 0 ? length : core.length
765
+ if (target === 0) return null
766
+
767
+ // conservative: only proceed when a connected peer can serve the head whole
768
+ if (conservative && core.remoteContiguousLength < target) return null
769
+
770
+ const buf = await core.get(target - 1, { timeout })
771
+ if (buf === null) return null
772
+
773
+ let op = encoding.decodeOplog(buf)
774
+
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)
779
+ }
628
780
 
629
781
  return {
630
- version: m.version,
631
- timestamp: 0,
632
- links: m.node.heads,
633
- batch: { start: 0, end: m.node.batch - 1 },
634
- views: {
635
- system: {
636
- key: digest.key,
637
- length: checkpoint.system.checkpoint.length
638
- },
639
- flushes: seq
640
- },
641
- optimistic: !!m.optimistic,
642
- value: m.node.value
782
+ key: core.key,
783
+ length: target,
784
+ op
643
785
  }
644
786
  }
645
787
 
@@ -700,7 +842,7 @@ module.exports = class Autobee extends ReadyResource {
700
842
  await this.bootstrap.setUserData('autobee/local', this.local.key)
701
843
  await oldLocal.close()
702
844
 
703
- // done, soft reboot
845
+ // done, soft restart
704
846
  this.emit('rotate-local-writer')
705
847
  }
706
848
 
@@ -764,86 +906,152 @@ module.exports = class Autobee extends ReadyResource {
764
906
  }
765
907
 
766
908
  async _bumpMigratedWriters() {
909
+ const opened = new Set()
910
+ let updated = false
911
+
767
912
  for (const batch of this._catchupMigratedNodes) {
768
913
  await this._processBatch(batch)
914
+ updated = true
915
+ for (const node of batch) {
916
+ if (node.from) opened.add(node.from)
917
+ }
769
918
  }
770
- }
771
919
 
772
- // append a null value node to ack writer
773
- _appendAck() {
774
- if (!this._ackRequired) return false
775
- if (!this.writers.writable) return false
920
+ for (const core of opened) await core.close()
921
+ return updated
922
+ }
776
923
 
777
- if (this.writers.localWriter.pending !== null) return false
924
+ async _prefetchApprovals() {
925
+ if (this._prefetchingApprovals) return
926
+ this._prefetchingApprovals = true
778
927
 
779
- const links = this.system.getLinks(this.local.key)
780
- const t = Math.max(this._now(), this.system.timestamp)
928
+ const activeRequests = this._approvalRequests
781
929
 
782
- let unlinked = false
783
- for (const { key, length } of links) {
784
- const hex = b4a.toString(key, 'hex')
785
- const acked = this._ackedHeads.get(hex) || 0
786
- if (length > acked) {
787
- unlinked = true
788
- 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))
789
937
  }
938
+ await Promise.allSettled(prefetch)
939
+ } finally {
940
+ this._prefetchingApprovals = false
790
941
  }
791
- if (!unlinked) {
792
- this._ackRequired = false
793
- return false
794
- }
942
+ }
795
943
 
796
- for (const { key, length } of links) {
797
- this._ackedHeads.set(b4a.toString(key, 'hex'), length)
944
+ async _standing(activeRequests) {
945
+ const rec = await this.system.get(this.local.key, { activeRequests })
946
+ return currentWeight(rec)
947
+ }
948
+
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
798
957
  }
958
+ return false
959
+ }
799
960
 
800
- this.writers.appendLocal(null, t, { start: 0, end: 0 }, links, false, null)
801
- this._ackRequired = false
802
- 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
+ }
803
969
  }
804
970
 
805
- async _bumpPendingWriters() {
806
- if (this._catchupMigratedNodes !== null) {
807
- await this._bumpMigratedWriters()
808
- 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))
809
989
  }
810
990
 
811
- let updated = false
991
+ if (!approving.length) return null
812
992
 
813
- 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
+ }
814
999
 
815
- for (let i = pending.length - 1; i >= 0; i--) {
816
- const w = pending[i]
1000
+ if (!approvals.length) return null
817
1001
 
818
- const batch = await w.next()
819
- if (batch === null) continue
1002
+ for (const a of approvals) {
1003
+ this._approvedPending.set(b4a.toString(a.key, 'hex'), a.weight)
1004
+ }
820
1005
 
821
- if (w.isAdded || (w.isRemoved && w.hasReferrals())) {
822
- await this._processBatch(batch)
823
- w.notify(batch)
824
- updated = true
825
- continue
826
- }
1006
+ return approvals
827
1007
 
828
- if (this.optimistic && !w.isRemoved && batch[0].optimistic) {
829
- if (!(await this._optimisticBatch(batch))) {
830
- w.removePending()
831
- continue
832
- }
833
- w.notify(batch)
834
- updated = true
835
- 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
836
1044
  }
1045
+ w.notify(batch)
1046
+ return true
837
1047
  }
838
1048
 
839
- return updated
1049
+ return true
840
1050
  }
841
1051
 
842
1052
  async _optimisticBatch(batch) {
843
1053
  const rollbackSystem = this.system.bee.head()
844
1054
  const rollbackView = this._workingBee.head()
845
- const rollbackShared = this.system.shared
846
- const rollbackAttestations = this.writers.attestations.length
847
1055
 
848
1056
  const t = await this.prepareBatch(batch)
849
1057
  if (t.view) this._workingBee.move(t.view)
@@ -865,9 +1073,6 @@ module.exports = class Autobee extends ReadyResource {
865
1073
  this._workingBee.move(rollbackView)
866
1074
  this.system.bee.move(rollbackSystem)
867
1075
  await this.system.reset()
868
- this.system.shared = rollbackShared
869
- // don't attest grants that were just undone
870
- this.writers.attestations.length = rollbackAttestations
871
1076
  return false
872
1077
  }
873
1078
  }
@@ -888,7 +1093,12 @@ module.exports = class Autobee extends ReadyResource {
888
1093
  const t = await topo.sort(this, batch)
889
1094
 
890
1095
  if (t.undo) {
1096
+ this.stats.undos++
1097
+ this.trusted.clear()
891
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
+ }
892
1102
  }
893
1103
 
894
1104
  return t
@@ -908,7 +1118,7 @@ module.exports = class Autobee extends ReadyResource {
908
1118
  }
909
1119
 
910
1120
  if (b4a.equals(batch[0].key, this.key) && batch[0].length === 1) {
911
- await this._host.addWriter(batch[0].key)
1121
+ await this._host.addWriter(batch[0].key, { weight: this.bootstrapWeight })
912
1122
  }
913
1123
 
914
1124
  await this._applyBatch(batch, batch[0].optimistic)
@@ -931,33 +1141,39 @@ module.exports = class Autobee extends ReadyResource {
931
1141
  for (const node of batch) {
932
1142
  this.system.addNode(node)
933
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
+
934
1155
  // compat: autobase nodes may be null (legacy null decodes to 0-length buffer)
935
1156
  if (node.value && node.value.length) userBatch.push(node)
936
1157
  }
937
1158
 
938
1159
  if (this._hasApply && (await this.system.canApply(batch[0].key, optimistic))) {
1160
+ this.stats.applies++
939
1161
  this._host.applying = batch
940
- await this._handlers.apply(userBatch, this._workingView, this._host)
1162
+ await this._workingView.apply(userBatch)
941
1163
  this._host.applying = null
942
1164
  }
943
1165
 
944
- 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)
945
1169
 
946
1170
  if (local) {
947
1171
  this._localSystemLength = this.system.bee.context.local.length - this._localSystemStart
948
1172
  this._localViewLength = this._workingBee.context.local.length - this._localViewStart
1173
+ this._localFlushes = this.system.flushes
949
1174
  }
950
1175
 
951
- this.writers.attest(witnessed)
952
-
953
- await this._storeBoot()
954
-
955
- for (const { key, added, isAnchor } of changed) {
956
- if (isAnchor) {
957
- // no Writer/ActiveWriters tracking, but still wake anything linked to it
958
- this.writers.triggers.trigger(b4a.toString(key, 'hex'), 1)
959
- continue
960
- }
1176
+ for (const { key, added } of changed) {
961
1177
  if (added) await this.writers.add(key)
962
1178
  else await this.writers.remove(key)
963
1179
  }
@@ -980,6 +1196,11 @@ module.exports = class Autobee extends ReadyResource {
980
1196
  return Promise.all(proms)
981
1197
  }
982
1198
 
1199
+ _storeMigratedHead() {
1200
+ const value = this._migratedHead ? encoding.encodeMigratedHead(this._migratedHead) : null
1201
+ return this.local.setUserData('autobee/migrated-head', value)
1202
+ }
1203
+
983
1204
  static decodeValue(buf, opts) {
984
1205
  return encoding.decodeValue(buf, opts)
985
1206
  }
@@ -997,6 +1218,8 @@ module.exports = class Autobee extends ReadyResource {
997
1218
  async append(values, { optimistic = false } = {}) {
998
1219
  if (this.closing) throw new Error('Autobee closed')
999
1220
 
1221
+ this.stats.appends++
1222
+
1000
1223
  if (!Array.isArray(values)) values = [values]
1001
1224
 
1002
1225
  if (!this.opened) await this.ready()
@@ -1013,27 +1236,17 @@ module.exports = class Autobee extends ReadyResource {
1013
1236
  const t = Math.max(this._now(), this.system.timestamp)
1014
1237
  const batch = []
1015
1238
 
1016
- // witnesses only ride upgrade windows. witness.weight is the value the backer's
1017
- // snapshot witnesses - verifiers recompute the same read and verify
1018
1239
  const rec = await this.system.get(this.local.key)
1019
1240
  let witness = null
1020
- if (rec && rec.maxWeight > currentWeight(rec)) {
1021
- const sorted = this.writers.witnesses.sort(
1022
- (a, b) => b.attestation.weight - a.attestation.weight
1023
- )
1024
-
1025
- for (const { key, length, attestation, manifest } of sorted) {
1026
- if (attestation.weight > rec.maxWeight) continue
1027
-
1028
- const backer = await this.system.get(key)
1029
- if (!backer || backer.isRemoved || backer.length < length) continue
1030
-
1031
- const { weight, signature } = attestation
1032
- witness = { weight, backer: { key, length, signature, manifest } }
1033
- 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 } }
1034
1245
  }
1035
1246
  }
1036
1247
 
1248
+ const approvals = optimistic ? null : await this._collectApprovals()
1249
+
1037
1250
  for (let i = 0; i < values.length; i++) {
1038
1251
  const value = values[i]
1039
1252
  const buffer = typeof value === 'string' ? b4a.from(value) : value
@@ -1044,7 +1257,8 @@ module.exports = class Autobee extends ReadyResource {
1044
1257
  { start: i, end: values.length - 1 - i },
1045
1258
  lnk,
1046
1259
  optimistic,
1047
- i === 0 ? witness : null
1260
+ i === 0 ? witness : null,
1261
+ i === 0 ? approvals : null
1048
1262
  )
1049
1263
  batch.push(node)
1050
1264
  }
@@ -1053,8 +1267,13 @@ module.exports = class Autobee extends ReadyResource {
1053
1267
  }
1054
1268
 
1055
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
+
1056
1275
  await this.writers.flushLocal({
1057
- flushes: this.system.flushes,
1276
+ flushes: this._localFlushes,
1058
1277
  system: {
1059
1278
  key: this.system.bee.context.local.key,
1060
1279
  start: this._localSystemStart,
@@ -1069,133 +1288,166 @@ module.exports = class Autobee extends ReadyResource {
1069
1288
 
1070
1289
  this._localSystemStart = this.system.bee.context.local.length
1071
1290
  this._localSystemLength = 0
1291
+ this._localFlushes = this.system.flushes
1072
1292
  this._localViewStart = this._workingBee.context.local.length
1073
1293
  this._localViewLength = 0
1074
1294
  }
1075
1295
 
1076
- async rebootFromHeads(hints, { force = false } = {}) {
1077
- if (!this._handlers.onwakeup) return false
1078
- if (this._interrupting) return false
1079
- if (!hints.size || this.rebooting || this.rebootTo || this.bootFrom) {
1080
- return false
1081
- }
1082
-
1083
- const promises = []
1084
- const heads = []
1085
- for (const [hex, length] of hints) {
1086
- if (length === 0) continue
1087
- const key = b4a.from(hex, 'hex')
1088
- heads.push({ key, length })
1089
- promises.push(this._getOplog(key, length))
1090
- }
1296
+ async moveTo(head) {
1297
+ if (!this.opened) await this.ready()
1298
+ if (this.closing) throw new Error('Autobee closed')
1091
1299
 
1092
- const ops = await Promise.all(promises)
1093
- if (this.rebooting || this.rebootTo) return false
1300
+ const ff = await FastForward.fromHead(this, head, null, { force: true })
1301
+ if (ff === null) return null
1094
1302
 
1095
- let best = null
1096
- let bestFlushes = -1
1303
+ if (!(await this._runFastForward(ff))) return null
1097
1304
 
1098
- for (let i = 0; i < ops.length; i++) {
1099
- const res = ops[i]
1100
- if (res === null) continue
1305
+ return this.ff.promise
1306
+ }
1101
1307
 
1102
- if (res.op.views && res.op.views.flushes > bestFlushes) {
1103
- bestFlushes = res.op.views.flushes
1104
- best = res
1105
- }
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
1106
1316
  }
1317
+ }
1107
1318
 
1108
- if (best && force) {
1109
- 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
+ }
1110
1335
  }
1111
1336
 
1112
- 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
1113
1340
 
1114
- 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
+ }
1115
1372
 
1116
- const v = best.op.views.view
1117
- const view = this.bee.checkout({ key: v.key, length: v.start + v.length })
1373
+ if (this._interrupting) break
1118
1374
 
1119
- let trusted = null
1120
- try {
1121
- trusted = await this._handlers.onwakeup(head, view, this)
1122
- if (!trusted || this.rebooting || this.rebootTo) return false
1375
+ await this._bootWait.promise
1376
+ }
1123
1377
  } finally {
1124
- view.close()
1378
+ this._bootWait = null
1379
+ if (opened !== null) await opened.close()
1125
1380
  }
1126
1381
 
1127
- return this._rebootFromHead(best, trusted)
1382
+ return false
1128
1383
  }
1129
1384
 
1130
- async _rebootFromHead(head, trusted, { force = false, wait = true } = {}) {
1131
- const oplog = await this._getOplog(head.key, head.length)
1132
- if (!oplog) return false
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)
1133
1388
 
1134
- const verified = trusted ? await this._getOplog(trusted.key, trusted.length) : oplog
1389
+ const candidates = await this._readCandidateHeads(result, FastForward.DEFAULT_TIMEOUT)
1135
1390
 
1136
- if (!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
+ })
1137
1397
 
1138
- if (!force && verified.op.views.flushes - this.system.flushes < MIN_FF_GAP) {
1139
- return false
1140
- }
1398
+ return ff !== null && (await this._runFastForward(ff))
1399
+ }
1141
1400
 
1142
- const moved = await this._moveTo(batchToHead(verified.op.views.system), {
1143
- system: batchToHead(oplog.op.views.system),
1144
- verified: {
1145
- op: trusted || head,
1146
- flushes: verified.op.views.flushes
1147
- }
1401
+ async _bootLegacyHead(head) {
1402
+ const oplog = await FastForward.flushHead(this, head, {
1403
+ timeout: FastForward.DEFAULT_TIMEOUT
1148
1404
  })
1405
+ if (oplog === null || oplog.op.version > LEGACY_OPLOG_VERSION) return false
1149
1406
 
1150
- 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
+ })
1151
1411
 
1152
- return null
1412
+ return ff !== null && (await this._runFastForward(ff))
1153
1413
  }
1154
1414
 
1155
- // same as moveTo except we don't return the final promise
1156
- _initFromHead(head, tip) {
1157
- if (!head.length) {
1158
- // legacy fastForward boot
1159
- return this._runReboot(new Reboot(this, head, tip))
1160
- }
1415
+ async _bootReference(head) {
1416
+ const oplog = await FastForward.flushHead(this, head, {
1417
+ timeout: FastForward.DEFAULT_TIMEOUT
1418
+ })
1161
1419
 
1162
- return this._rebootFromHead(head, null, { force: true, wait: false })
1420
+ return oplog === null ? null : this.openViewAt(oplog)
1163
1421
  }
1164
1422
 
1165
- // head is a system head; reboots onto it, migrating in place if it's a legacy version
1166
- async _moveTo(systemHead, tip) {
1167
- if (this.rebootTo !== null || this.rebooting !== null) return false
1168
-
1169
- if (await this._runReboot(new Reboot(this, systemHead, tip))) {
1170
- return true
1423
+ async _runFastForward(ff) {
1424
+ if (this.fastForwardTo !== null || this.fastForwarding !== null) {
1425
+ await ff.close()
1426
+ return false
1171
1427
  }
1172
1428
 
1173
- return false
1174
- }
1175
-
1176
- async _runReboot(reboot) {
1177
- this.rebooting = reboot
1429
+ this.fastForwarding = ff
1178
1430
 
1179
- const result = await reboot.run()
1180
- await reboot.close()
1431
+ const result = await ff.run()
1432
+ await ff.close()
1181
1433
 
1182
- if (this.rebooting === reboot) this.rebooting = null
1434
+ if (this.fastForwarding === ff) this.fastForwarding = null
1183
1435
 
1184
1436
  if (!result) return false
1185
1437
 
1186
- this.rebootTo = result
1187
- this.reboot = rrp()
1438
+ this.fastForwardTo = result
1439
+ this.ff = rrp()
1188
1440
 
1189
1441
  this.bumpSoon()
1190
1442
 
1191
1443
  return true
1192
1444
  }
1193
1445
 
1194
- async _applyReboot() {
1446
+ async _applyFastForward() {
1195
1447
  const changes = this._hasUpdate ? new UpdateChanges(this) : null
1196
1448
  if (changes) changes.track()
1197
1449
 
1198
- const { head, tip, migrate } = this.rebootTo
1450
+ const { head, migrate } = this.fastForwardTo
1199
1451
 
1200
1452
  const from = this.system.bee.head()
1201
1453
  const to = head
@@ -1203,15 +1455,16 @@ module.exports = class Autobee extends ReadyResource {
1203
1455
  this.system.bee.move(head)
1204
1456
  await this.system.reset()
1205
1457
 
1206
- this.system.shared = this.rebootTo.shared || {
1207
- flushes: -1,
1208
- view: EMPTY_HEAD,
1209
- system: EMPTY_HEAD
1210
- }
1211
-
1212
1458
  // migrate is set when fast-forwarding from a legacy head
1213
1459
  if (migrate) {
1214
- 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()
1215
1468
  this.bee.move(view)
1216
1469
  this._workingBee.move(view)
1217
1470
  } else {
@@ -1219,34 +1472,22 @@ module.exports = class Autobee extends ReadyResource {
1219
1472
  this._workingBee.move(this.system.view)
1220
1473
  }
1221
1474
 
1222
- 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()
1223
1480
  await this.writers.refresh()
1224
1481
 
1482
+ // we moved, so ask our peers to tell us their heads again
1483
+ this._requestWakeup()
1484
+
1225
1485
  await this._update(changes)
1486
+ await this._storeBoot()
1226
1487
 
1488
+ this.stats.fastForwards++
1227
1489
  this.emit('move-to', to, from)
1228
- this.reboot.resolve({ to, from })
1229
-
1230
- // tip is null during boot
1231
- if (!tip) return
1232
-
1233
- try {
1234
- await this._reapply(tip)
1235
- } catch (err) {
1236
- throw err
1237
- }
1238
- }
1239
-
1240
- async _reapply({ system, verified }) {
1241
- const changes = this._hasUpdate ? new UpdateChanges(this) : null
1242
- if (changes) changes.track()
1243
-
1244
- const sys = this.system.bee.checkout(system)
1245
- const t = await topo.rollback(this, sys, verified)
1246
- await sys.close()
1247
-
1248
- await this.applyBacklog(t.tip)
1249
- return this._update(changes)
1490
+ this.ff.resolve({ to, from })
1250
1491
  }
1251
1492
 
1252
1493
  replay() {
@@ -1258,6 +1499,17 @@ function isObject(o) {
1258
1499
  return typeof o === 'object' && o && !b4a.isBuffer(o)
1259
1500
  }
1260
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
+
1261
1513
  function noop() {}
1262
1514
 
1263
1515
  function createAnchorCore(store, prologue, manifestData) {
@@ -1286,10 +1538,3 @@ function crashSoon(err) {
1286
1538
  })
1287
1539
  throw err
1288
1540
  }
1289
-
1290
- function batchToHead(b) {
1291
- return {
1292
- key: b.key,
1293
- length: b.start + b.length
1294
- }
1295
- }