autobee 2.0.0-rc.9 → 2.0.1

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