autobee 2.0.0-rc.2 → 2.0.0-rc.21

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,17 @@ 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 FastForward = require('./lib/fast-forward.js')
18
18
  const System = require('./lib/system.js')
19
19
  const ApplyCalls = require('./lib/apply-calls.js')
20
20
  const topo = require('./lib/topo.js')
21
21
  const { ActiveWriters } = require('./lib/writers.js')
22
+ const TrustedPeers = require('./lib/trusted.js')
23
+ const ApplyView = require('./lib/apply-view.js')
22
24
  const UpdateChanges = require('./lib/updates.js')
23
25
 
24
26
  const EMPTY_HEAD = { length: 0, key: null }
25
27
  const INTERRUPT = new Error('Apply interrupted')
26
- const MIN_FF_GAP = 32
27
28
 
28
29
  module.exports = class Autobee extends ReadyResource {
29
30
  constructor(store, key = null, handlers = {}) {
@@ -56,7 +57,8 @@ module.exports = class Autobee extends ReadyResource {
56
57
  this.discoveryKey = null
57
58
  this.id = null
58
59
  this.bootstrap = null
59
- this.handlers = handlers
60
+ this._handlers = handlers
61
+ this.stats = { undos: 0, fastForwards: 0, drains: 0, applies: 0, appends: 0 }
60
62
 
61
63
  this.system = new System(this.store.namespace('system'), this.name, {
62
64
  getEncryptionProvider: this.getSystemEncryption,
@@ -65,7 +67,7 @@ module.exports = class Autobee extends ReadyResource {
65
67
  this.system.auto = this
66
68
 
67
69
  this.bee = bee.snapshot()
68
- this.view = handlers.open ? handlers.open(this.bee, this) : this.bee
70
+ this.view = ApplyView.open(this.bee, this)
69
71
  this.optimistic = handlers.optimistic !== false // TODO: should default to false instead
70
72
 
71
73
  this.name = name // for debugging
@@ -76,23 +78,32 @@ module.exports = class Autobee extends ReadyResource {
76
78
  this.writers = null
77
79
  this.bumping = 0
78
80
 
79
- // system head to boot from: migrates or fast-forwards depending on its version
80
- this.bootFrom = handlers.bootFrom || null
81
+ const fastForward = handlers.fastForward || {}
82
+
83
+ // oplog head to boot from: migrates or fast-forwards depending on its version
84
+ this.bootFrom = fastForward.boot || null
85
+
86
+ // conservative (default on): only fast-forward onto a head someone can serve whole
87
+ this._conservativeFF = fastForward.conservative !== false
88
+
89
+ this.trusted = new TrustedPeers(handlers)
81
90
 
82
- this.reboot = null
83
- this.rebooting = null
84
- this.rebootTo = null
91
+ this.ff = null
92
+ this.fastForwarding = null
93
+ this.fastForwardTo = null
85
94
 
86
95
  this._workingBee = bee
87
- this._workingView = handlers.open ? handlers.open(this._workingBee, this) : this._workingBee
96
+ this._workingView = new ApplyView(this._workingBee, this)
88
97
 
89
98
  this._localSystemStart = 0
90
99
  this._localSystemLength = 0
100
+ this._localFlushes = 0
91
101
  this._localViewStart = 0
92
102
  this._localViewLength = 0
93
103
 
94
104
  this._appending = []
95
105
  this._draining = null
106
+ this._bootWait = null
96
107
 
97
108
  this.legacyViews = handlers.legacyViews || []
98
109
 
@@ -100,11 +111,12 @@ module.exports = class Autobee extends ReadyResource {
100
111
  this._bootingState = null
101
112
  this._bootingAll = null
102
113
 
103
- this._handlers = handlers
104
114
  this._now = handlers.now || Date.now // overridable for clock-drift tests
105
115
  this._hasApply = !!handlers.apply
106
116
  this._hasUpdate = !!handlers.update
107
117
  this._needsUpdate = false
118
+ this._ackRequired = false
119
+ this._ackedHeads = new Map()
108
120
  this._updateLocalCore = null
109
121
  this._host = new ApplyCalls(this)
110
122
  this._notifyHandler = null
@@ -120,6 +132,7 @@ module.exports = class Autobee extends ReadyResource {
120
132
  this.previousDrain = 0
121
133
 
122
134
  this._catchupMigratedNodes = null
135
+ this._migratedHead = null
123
136
 
124
137
  this.ready().catch(noop)
125
138
  }
@@ -161,10 +174,16 @@ module.exports = class Autobee extends ReadyResource {
161
174
 
162
175
  this._localSystemStart = this.system.bee.context.local.length
163
176
  this._localViewStart = this._workingBee.context.local.length
177
+ this._localFlushes = this.system.flushes
164
178
 
165
179
  this.bumpSoon()
166
180
  }
167
181
 
182
+ _requestWakeup() {
183
+ const session = this._wakeup._session
184
+ if (session) session.broadcastLookup()
185
+ }
186
+
168
187
  _registerWakeup() {
169
188
  if (!this.wakeupCapability) return
170
189
  this._wakeup.recouple()
@@ -209,6 +228,7 @@ module.exports = class Autobee extends ReadyResource {
209
228
 
210
229
  async _close() {
211
230
  this._interrupting = true
231
+ if (this._bootWait !== null) this._bootWait.resolve()
212
232
  if (this._notifyHandler) this._notifyHandler.destroy()
213
233
  if (this._draining) {
214
234
  // drain may be waiting on replicated blocks
@@ -216,18 +236,21 @@ module.exports = class Autobee extends ReadyResource {
216
236
  await this._draining
217
237
  }
218
238
 
239
+ if (this._updating) await this._updating
240
+
219
241
  // let in-flight writer adds finish
220
242
  try {
221
243
  await this._bootingAll
222
244
  } catch {}
223
245
 
224
- if (this._handlers.close) await this._handlers.close(this.view)
246
+ await ApplyView.close(this.view, this)
225
247
 
226
248
  if (this.writers) await this.writers.close()
227
249
  await this.local.close()
228
250
  await this.system.close()
229
251
  await this._wakeup.close()
230
- await this._workingBee.close()
252
+ if (this.bootstrap && !this.bootstrap.closed) await this.bootstrap.close()
253
+ await this._workingView.close()
231
254
  await this.bee.close()
232
255
  await this.store.close()
233
256
  }
@@ -239,20 +262,8 @@ module.exports = class Autobee extends ReadyResource {
239
262
  session.clearRequests(session.activeRequests, closingError)
240
263
  }
241
264
  }
242
- }
243
-
244
- async changesFrom({ system } = {}) {
245
- const info = await this.system.getInfo()
246
-
247
- const current = {
248
- flushes: info ? info.flushes : 0,
249
- system: this.system.bee.head(),
250
- view: info ? info.view : EMPTY_HEAD
251
- }
252
-
253
- const shared = await this.system.commonAncestor(system || EMPTY_HEAD)
254
265
 
255
- return UpdateChanges.from(shared, current)
266
+ if (this.fastForwarding) this.fastForwarding.clearRequests(closingError)
256
267
  }
257
268
 
258
269
  replicate(...args) {
@@ -269,6 +280,14 @@ module.exports = class Autobee extends ReadyResource {
269
280
  this._wakeup.hint(wakeup)
270
281
  }
271
282
 
283
+ // the view an oplog head points at, opened and closed through the handlers
284
+ openViewAt(oplog) {
285
+ const v = oplog.op.views.view
286
+ if (!v) return null
287
+
288
+ return new ApplyView(this.bee.checkout({ key: v.key, length: v.start + v.length }), this)
289
+ }
290
+
272
291
  openCore(key) {
273
292
  const encryption = this.encryptionKey ? new WriterEncryption(this) : null
274
293
  return this.store.get({ key, encryption })
@@ -294,7 +313,7 @@ module.exports = class Autobee extends ReadyResource {
294
313
  }
295
314
 
296
315
  if (this.bootFrom) {
297
- this.bootFrom = (await this.bootFrom) || null
316
+ this.bootFrom = getBootOption(await this.bootFrom)
298
317
  }
299
318
  }
300
319
 
@@ -372,15 +391,26 @@ module.exports = class Autobee extends ReadyResource {
372
391
 
373
392
  await this.system.boot(system)
374
393
 
375
- // Use the view position from the system info (authoritative, post-processing)
376
- // rather than from the oplog (stale, captured at append time before _bump)
377
- let view = this.system.view || EMPTY_HEAD
394
+ const migrated = await this.local.getUserData('autobee/migrated-head')
395
+ if (migrated) this._migratedHead = encoding.decodeMigratedHead(migrated)
396
+
397
+ let view = this.system.view
398
+ if (!view) view = (this._migratedHead && this._migratedHead.view) || EMPTY_HEAD
378
399
 
379
400
  // @todo migration
380
401
  if (result.migration) {
381
- if (this.handlers.migrate) {
382
- view = (await this.handlers.migrate(result.migration.views)) || EMPTY_HEAD
402
+ if (this._handlers.migrate) {
403
+ view =
404
+ (await this._handlers.migrate(result.migration.views, result.migration.system)) ||
405
+ EMPTY_HEAD
383
406
  this._catchupMigratedNodes = result.migration.catchup
407
+
408
+ this._migratedHead = {
409
+ system: result.migration.system,
410
+ view: view.length ? view : (this._migratedHead && this._migratedHead.view) || null
411
+ }
412
+
413
+ await this._storeMigratedHead()
384
414
  }
385
415
 
386
416
  // ff boot invalidated by migration
@@ -420,7 +450,7 @@ module.exports = class Autobee extends ReadyResource {
420
450
  async _bump() {
421
451
  if (!(await this._bootReady())) return
422
452
 
423
- await this._flushWakeup()
453
+ if (this._bootWait !== null) this._bootWait.resolve()
424
454
 
425
455
  this.bumping++
426
456
 
@@ -476,13 +506,20 @@ module.exports = class Autobee extends ReadyResource {
476
506
  }
477
507
 
478
508
  async _drain() {
509
+ if (this._updating) await this._updating
510
+
511
+ this.stats.drains++
512
+
479
513
  if (this.bootFrom) {
480
- await this._initFromHead(this.bootFrom)
514
+ const { head = null, legacy = null, bootCondition = null } = this.bootFrom
515
+
481
516
  this.bootFrom = null
517
+
518
+ if (legacy) await this._bootFromSystem(legacy)
519
+ else if (head) await this._bootFromHead(head, bootCondition)
482
520
  }
483
521
 
484
- // tracked across drain iteration
485
- this.system.shared = null
522
+ this._ackRequired = false
486
523
 
487
524
  const changes = this._hasUpdate ? new UpdateChanges(this) : null
488
525
  if (changes) changes.track()
@@ -498,16 +535,26 @@ module.exports = class Autobee extends ReadyResource {
498
535
 
499
536
  try {
500
537
  while (!this._interrupting) {
501
- if (this.rebootTo !== null) {
502
- await this._applyReboot()
538
+ await this._flushWakeup()
539
+ if (this._interrupting) break
540
+
541
+ if (this.fastForwardTo !== null) {
542
+ await this._applyFastForward()
503
543
  break // revaluate conditions...
504
544
  }
505
545
 
506
- if (!(await this._bumpPendingWriters())) break
546
+ if (await this._bumpPendingWriters()) {
547
+ this._needsUpdate = true
548
+ continue
549
+ }
550
+
551
+ if (!this._appendAck()) break
507
552
  this._needsUpdate = true
508
553
  }
509
554
 
510
555
  await this._flushLocal()
556
+
557
+ if (!this._interrupting) await this.writers.refresh()
511
558
  } finally {
512
559
  if (this.bumping === 1) this.bumping = 0
513
560
  else this.bumping = 1
@@ -517,8 +564,15 @@ module.exports = class Autobee extends ReadyResource {
517
564
  this._draining = null
518
565
  if (this._interrupting) return
519
566
 
520
- if (this._needsUpdate) {
521
- await this._update(changes)
567
+ const updating = rrp()
568
+ this._updating = updating.promise
569
+
570
+ try {
571
+ if (this._needsUpdate) await this._update(changes)
572
+ await this._storeBoot()
573
+ } finally {
574
+ this._updating = null
575
+ updating.resolve()
522
576
  }
523
577
  }
524
578
 
@@ -551,71 +605,153 @@ module.exports = class Autobee extends ReadyResource {
551
605
  }
552
606
  }
553
607
 
554
- async _flushWakeup() {
608
+ // wakeup hints keep arriving asynchronously
609
+ async _applyWakeupHints() {
555
610
  const hints = this._wakeup.flush()
611
+ if (!hints.size) return hints
556
612
 
557
613
  this.previousDrain = Date.now()
558
- this.rebootFromHeads(hints).catch(noop)
559
614
 
560
615
  for (const [hex, length] of hints) {
561
616
  const key = b4a.from(hex, 'hex')
562
- if (this.writers.has(hex)) continue
617
+ // wakeup() itself no-ops the add for an already-active writer, but still
618
+ // needs to run so it can hint() the announced length - skipping it here
619
+ // for active writers left gc with no hint to protect them
563
620
  await this.writers.wakeup(key, length)
564
621
  }
622
+
623
+ return hints
624
+ }
625
+
626
+ async _flushWakeup() {
627
+ const hints = await this._applyWakeupHints()
628
+ if (!hints.size) return
629
+
630
+ // a scheduled fast-forward is applied by the drain before we look again
631
+ if (this.fastForwardTo !== null || this.fastForwarding !== null) return
632
+ if (this._interrupting || this.closing || this.bootFrom) return
633
+
634
+ try {
635
+ const heads = await this._readCandidateHeads(hints, FastForward.DEFAULT_TIMEOUT)
636
+
637
+ const ff = await FastForward.fromHeads(this, heads, {
638
+ timeout: FastForward.DEFAULT_TIMEOUT
639
+ })
640
+
641
+ if (ff !== null) await this._runFastForward(ff)
642
+ } catch (err) {
643
+ safetyCatch(err)
644
+ }
565
645
  }
566
646
 
567
- async _getOplog(key, length) {
647
+ async _readCandidateHeads(hints, timeout) {
648
+ const promises = []
649
+
650
+ for (const [hex, length] of hints) {
651
+ if (length === 0) continue
652
+ const key = b4a.from(hex, 'hex')
653
+ promises.push(this._getOplog(key, length, timeout ? { timeout } : null))
654
+ }
655
+
656
+ const ops = await Promise.all(promises)
657
+ const heads = []
658
+
659
+ for (const res of ops) {
660
+ if (res === null) continue
661
+
662
+ // the head we were woken on is a candidate in its own right
663
+ heads.push({ key: res.key, length: res.length })
664
+
665
+ if (!res.op.trusted) continue
666
+
667
+ for (const trusted of res.op.trusted) {
668
+ const head = this.trusted.read(trusted)
669
+ if (head !== null) heads.push(head)
670
+ }
671
+ }
672
+
673
+ return heads
674
+ }
675
+
676
+ async _getOplog(key, length, opts = null) {
568
677
  const core = this.openCore(key)
678
+
679
+ try {
680
+ return await this.readOplog(core, length, opts)
681
+ } finally {
682
+ await core.close()
683
+ }
684
+ }
685
+
686
+ // reads on a core the caller owns, so a walk does not churn a session per step
687
+ async readOplog(core, length, opts = null) {
569
688
  await core.ready()
570
689
 
571
690
  const target = length >= 0 ? length : core.length
572
691
  if (target === 0) return null
573
692
 
574
- const buf = await core.get(target - 1)
693
+ // conservative: only proceed when a connected peer can serve the head whole
694
+ if (opts && opts.conservative && core.remoteContiguousLength < target) return null
695
+
696
+ const buf = await core.get(target - 1, opts)
697
+ if (buf === null) return null
698
+
575
699
  let op = encoding.decodeOplog(buf)
576
700
 
701
+ // legacy nodes always inflate, but only indexers carry views - callers
702
+ // that need system info must check op.views
577
703
  if (op.version < 3) {
578
- op = await this._inflateLegacyOplog(buf, core, target - 1)
704
+ op = await this._inflateLegacyOplog(buf, core, target - 1, opts)
579
705
  }
580
706
 
581
- await core.close()
582
-
583
- if (buf === null) return null
584
-
585
707
  return {
586
- key,
708
+ key: core.key,
587
709
  length: target,
588
710
  op
589
711
  }
590
712
  }
591
713
 
592
- async _inflateLegacyOplog(buf, core, seq) {
714
+ async _inflateLegacyOplog(buf, core, seq, opts = null) {
593
715
  const m = encoding.decodeRawOplog(buf)
716
+
717
+ const op = {
718
+ version: m.version,
719
+ timestamp: 0,
720
+ links: m.node.heads,
721
+ batch: { start: 0, end: m.node.batch - 1 },
722
+ views: null,
723
+ optimistic: !!m.optimistic,
724
+ value: m.node.value
725
+ }
726
+
727
+ if (m.digest === null || m.checkpoint === null || !m.checkpoint.system) return op
728
+
594
729
  const fetches = []
595
730
 
596
- fetches.push(m.digest.pointer ? core.get(seq - m.digest.pointer) : buf)
597
- fetches.push(m.checkpoint.pointer ? core.get(seq - m.checkpoint.system.checkpointer) : buf)
731
+ fetches.push(m.digest.pointer ? core.get(seq - m.digest.pointer, opts) : buf)
732
+ fetches.push(
733
+ m.checkpoint.pointer ? core.get(seq - m.checkpoint.system.checkpointer, opts) : buf
734
+ )
598
735
 
599
736
  const [digestNode, checkpointNode] = await Promise.all(fetches)
737
+ // a wait: false get we could not satisfy locally
738
+ if (digestNode === null || checkpointNode === null) return op
600
739
 
601
740
  const { digest } = encoding.decodeRawOplog(digestNode)
602
741
  const { checkpoint } = encoding.decodeRawOplog(checkpointNode)
603
742
 
604
- return {
605
- version: m.version,
606
- timestamp: 0,
607
- links: m.node.heads,
608
- batch: { start: 0, end: m.node.batch - 1 },
609
- views: {
610
- system: {
611
- key: digest.key,
612
- length: checkpoint.system.checkpoint.length
613
- },
614
- flushes: seq
743
+ if (!checkpoint.system || !checkpoint.system.checkpoint) return op
744
+
745
+ op.views = {
746
+ system: {
747
+ key: digest.key,
748
+ start: 0,
749
+ length: checkpoint.system.checkpoint.length
615
750
  },
616
- optimistic: !!m.optimistic,
617
- value: m.node.value
751
+ flushes: seq
618
752
  }
753
+
754
+ return op
619
755
  }
620
756
 
621
757
  async _update(changes) {
@@ -675,7 +811,7 @@ module.exports = class Autobee extends ReadyResource {
675
811
  await this.bootstrap.setUserData('autobee/local', this.local.key)
676
812
  await oldLocal.close()
677
813
 
678
- // done, soft reboot
814
+ // done, soft restart
679
815
  this.emit('rotate-local-writer')
680
816
  }
681
817
 
@@ -739,9 +875,49 @@ module.exports = class Autobee extends ReadyResource {
739
875
  }
740
876
 
741
877
  async _bumpMigratedWriters() {
878
+ const opened = new Set()
879
+
742
880
  for (const batch of this._catchupMigratedNodes) {
743
881
  await this._processBatch(batch)
882
+ for (const node of batch) {
883
+ if (node.from) opened.add(node.from)
884
+ }
744
885
  }
886
+
887
+ for (const core of opened) await core.close()
888
+ }
889
+
890
+ // append a null value node to ack writer
891
+ _appendAck() {
892
+ if (!this._ackRequired) return false
893
+ if (!this.writers.writable) return false
894
+
895
+ if (this.writers.localWriter.pending !== null) return false
896
+
897
+ const links = this.system.getLinks(this.local.key)
898
+ const t = Math.max(this._now(), this.system.timestamp)
899
+
900
+ let unlinked = false
901
+ for (const { key, length } of links) {
902
+ const hex = b4a.toString(key, 'hex')
903
+ const acked = this._ackedHeads.get(hex) || 0
904
+ if (length > acked) {
905
+ unlinked = true
906
+ break
907
+ }
908
+ }
909
+ if (!unlinked) {
910
+ this._ackRequired = false
911
+ return false
912
+ }
913
+
914
+ for (const { key, length } of links) {
915
+ this._ackedHeads.set(b4a.toString(key, 'hex'), length)
916
+ }
917
+
918
+ this.writers.appendLocal(null, t, { start: 0, end: 0 }, links, false, null)
919
+ this._ackRequired = false
920
+ return true
745
921
  }
746
922
 
747
923
  async _bumpPendingWriters() {
@@ -750,41 +926,33 @@ module.exports = class Autobee extends ReadyResource {
750
926
  this._catchupMigratedNodes = null
751
927
  }
752
928
 
753
- let updated = false
929
+ // apply the best next node to keep the prefix stable
930
+ const next = await this.writers.nextPendingNode()
931
+ if (next === null) return false
754
932
 
755
- const pending = this.writers.pending.slice()
933
+ const { writer: w, batch } = next
756
934
 
757
- for (let i = pending.length - 1; i >= 0; i--) {
758
- const w = pending[i]
759
-
760
- const batch = await w.next()
761
- if (batch === null) continue
762
-
763
- if (w.isAdded || (w.isRemoved && w.hasReferrals())) {
764
- await this._processBatch(batch)
765
- w.notify(batch)
766
- updated = true
767
- continue
768
- }
935
+ if (w.isAdded || (w.isRemoved && w.hasReferrals())) {
936
+ await this._processBatch(batch)
937
+ w.notify(batch)
938
+ return true
939
+ }
769
940
 
770
- if (this.optimistic && !w.isRemoved && batch[0].optimistic) {
771
- if (!(await this._optimisticBatch(batch))) {
772
- w.removePending()
773
- continue
774
- }
775
- w.notify(batch)
776
- updated = true
777
- continue
941
+ if (this.optimistic && !w.isRemoved && batch[0].optimistic) {
942
+ if (!(await this._optimisticBatch(batch))) {
943
+ w.removePending()
944
+ return true
778
945
  }
946
+ w.notify(batch)
947
+ return true
779
948
  }
780
949
 
781
- return updated
950
+ return true
782
951
  }
783
952
 
784
953
  async _optimisticBatch(batch) {
785
954
  const rollbackSystem = this.system.bee.head()
786
955
  const rollbackView = this._workingBee.head()
787
- const rollbackShared = this.system.shared
788
956
  const rollbackAttestations = this.writers.attestations.length
789
957
 
790
958
  const t = await this.prepareBatch(batch)
@@ -807,7 +975,6 @@ module.exports = class Autobee extends ReadyResource {
807
975
  this._workingBee.move(rollbackView)
808
976
  this.system.bee.move(rollbackSystem)
809
977
  await this.system.reset()
810
- this.system.shared = rollbackShared
811
978
  // don't attest grants that were just undone
812
979
  this.writers.attestations.length = rollbackAttestations
813
980
  return false
@@ -830,7 +997,12 @@ module.exports = class Autobee extends ReadyResource {
830
997
  const t = await topo.sort(this, batch)
831
998
 
832
999
  if (t.undo) {
1000
+ this.stats.undos++
1001
+ this.trusted.clear()
833
1002
  t.view = await this.system.undo(t.undo)
1003
+ if (!t.view.length && this._migratedHead && this._migratedHead.view) {
1004
+ t.view = this._migratedHead.view
1005
+ }
834
1006
  }
835
1007
 
836
1008
  return t
@@ -878,8 +1050,9 @@ module.exports = class Autobee extends ReadyResource {
878
1050
  }
879
1051
 
880
1052
  if (this._hasApply && (await this.system.canApply(batch[0].key, optimistic))) {
1053
+ this.stats.applies++
881
1054
  this._host.applying = batch
882
- await this._handlers.apply(userBatch, this._workingView, this._host)
1055
+ await this._workingView.apply(userBatch)
883
1056
  this._host.applying = null
884
1057
  }
885
1058
 
@@ -888,12 +1061,11 @@ module.exports = class Autobee extends ReadyResource {
888
1061
  if (local) {
889
1062
  this._localSystemLength = this.system.bee.context.local.length - this._localSystemStart
890
1063
  this._localViewLength = this._workingBee.context.local.length - this._localViewStart
1064
+ this._localFlushes = this.system.flushes
891
1065
  }
892
1066
 
893
1067
  this.writers.attest(witnessed)
894
1068
 
895
- await this._storeBoot()
896
-
897
1069
  for (const { key, added, isAnchor } of changed) {
898
1070
  if (isAnchor) {
899
1071
  // no Writer/ActiveWriters tracking, but still wake anything linked to it
@@ -922,6 +1094,11 @@ module.exports = class Autobee extends ReadyResource {
922
1094
  return Promise.all(proms)
923
1095
  }
924
1096
 
1097
+ _storeMigratedHead() {
1098
+ const value = this._migratedHead ? encoding.encodeMigratedHead(this._migratedHead) : null
1099
+ return this.local.setUserData('autobee/migrated-head', value)
1100
+ }
1101
+
925
1102
  static decodeValue(buf, opts) {
926
1103
  return encoding.decodeValue(buf, opts)
927
1104
  }
@@ -939,6 +1116,8 @@ module.exports = class Autobee extends ReadyResource {
939
1116
  async append(values, { optimistic = false } = {}) {
940
1117
  if (this.closing) throw new Error('Autobee closed')
941
1118
 
1119
+ this.stats.appends++
1120
+
942
1121
  if (!Array.isArray(values)) values = [values]
943
1122
 
944
1123
  if (!this.opened) await this.ready()
@@ -996,7 +1175,7 @@ module.exports = class Autobee extends ReadyResource {
996
1175
 
997
1176
  async _flushLocal() {
998
1177
  await this.writers.flushLocal({
999
- flushes: this.system.flushes,
1178
+ flushes: this._localFlushes,
1000
1179
  system: {
1001
1180
  key: this.system.bee.context.local.key,
1002
1181
  start: this._localSystemStart,
@@ -1011,133 +1190,135 @@ module.exports = class Autobee extends ReadyResource {
1011
1190
 
1012
1191
  this._localSystemStart = this.system.bee.context.local.length
1013
1192
  this._localSystemLength = 0
1193
+ this._localFlushes = this.system.flushes
1014
1194
  this._localViewStart = this._workingBee.context.local.length
1015
1195
  this._localViewLength = 0
1016
1196
  }
1017
1197
 
1018
- async rebootFromHeads(hints, { force = false } = {}) {
1019
- if (!this._handlers.onwakeup) return false
1020
- if (this._interrupting) return false
1021
- if (!hints.size || this.rebooting || this.rebootTo || this.bootFrom) {
1022
- return false
1023
- }
1198
+ async moveTo(head) {
1199
+ if (!this.opened) await this.ready()
1200
+ if (this.closing) throw new Error('Autobee closed')
1024
1201
 
1025
- const promises = []
1026
- const heads = []
1027
- for (const [hex, length] of hints) {
1028
- if (length === 0) continue
1029
- const key = b4a.from(hex, 'hex')
1030
- heads.push({ key, length })
1031
- promises.push(this._getOplog(key, length))
1032
- }
1202
+ const ff = await FastForward.fromHead(this, head, null, { force: true })
1203
+ if (ff === null) return null
1033
1204
 
1034
- const ops = await Promise.all(promises)
1035
- if (this.rebooting || this.rebootTo) return false
1205
+ if (!(await this._runFastForward(ff))) return null
1036
1206
 
1037
- let best = null
1038
- let bestFlushes = -1
1207
+ return this.ff.promise
1208
+ }
1039
1209
 
1040
- for (let i = 0; i < ops.length; i++) {
1041
- const res = ops[i]
1042
- if (res === null) continue
1210
+ // legacy: ungated boot straight onto a system head
1211
+ async _bootFromSystem(system) {
1212
+ try {
1213
+ const ff = new FastForward(this, system, null, { timeout: FastForward.DEFAULT_TIMEOUT })
1214
+ return await this._runFastForward(ff)
1215
+ } catch (err) {
1216
+ safetyCatch(err)
1217
+ return false
1218
+ }
1219
+ }
1043
1220
 
1044
- if (res.op.views && res.op.views.flushes > bestFlushes) {
1045
- bestFlushes = res.op.views.flushes
1046
- best = res
1221
+ // the boot head seeds the view every trust decision here is made against,
1222
+ // since our own view is still empty
1223
+ async _bootFromHead(head, bootCondition) {
1224
+ // just a head: one attempt, we do not wait around if it cannot be read
1225
+ if (bootCondition === null) {
1226
+ try {
1227
+ const ff = await FastForward.fromHead(this, head, null, {
1228
+ force: true,
1229
+ timeout: FastForward.DEFAULT_TIMEOUT
1230
+ })
1231
+
1232
+ return ff !== null && (await this._runFastForward(ff))
1233
+ } catch (err) {
1234
+ safetyCatch(err)
1235
+ return false
1047
1236
  }
1048
1237
  }
1049
1238
 
1050
- if (best && force) {
1051
- return this._rebootFromHead(best, null, { force: true })
1052
- }
1239
+ // a condition needs the view at head to check against, so park until we
1240
+ // can read it and something acceptable turns up
1241
+ let opened = null
1053
1242
 
1054
- if (best === null || bestFlushes - this.system.flushes < MIN_FF_GAP) return false
1243
+ try {
1244
+ while (!this._interrupting) {
1245
+ this._bootWait = rrp()
1055
1246
 
1056
- const head = { key: best.key, length: best.length }
1247
+ try {
1248
+ if (opened === null) opened = await this._bootReference(head)
1057
1249
 
1058
- const v = best.op.views.view
1059
- const view = this.bee.checkout({ key: v.key, length: v.start + v.length })
1250
+ if (opened !== null && (await this._bootAttempt(head, bootCondition, opened.view))) {
1251
+ return true
1252
+ }
1253
+ } catch (err) {
1254
+ safetyCatch(err)
1255
+ }
1060
1256
 
1061
- let trusted = null
1062
- try {
1063
- trusted = await this._handlers.onwakeup(head, view, this)
1064
- if (!trusted || this.rebooting || this.rebootTo) return false
1257
+ if (this._interrupting) break
1258
+
1259
+ await this._bootWait.promise
1260
+ }
1065
1261
  } finally {
1066
- view.close()
1262
+ this._bootWait = null
1263
+ if (opened !== null) await opened.close()
1067
1264
  }
1068
1265
 
1069
- return this._rebootFromHead(best, trusted)
1266
+ return false
1070
1267
  }
1071
1268
 
1072
- async _rebootFromHead(head, trusted, { force = false, wait = true } = {}) {
1073
- const oplog = await this._getOplog(head.key, head.length)
1074
- if (!oplog) return false
1075
-
1076
- const verified = trusted ? await this._getOplog(trusted.key, trusted.length) : oplog
1077
-
1078
- if (!verified.op.views) return null
1079
-
1080
- if (!force && verified.op.views.flushes - this.system.flushes < MIN_FF_GAP) {
1081
- return false
1082
- }
1269
+ async _bootAttempt(head, bootCondition, reference) {
1270
+ // peek, so the hints are still there for the drain once we have booted
1271
+ const candidates = await this._readCandidateHeads(
1272
+ this._wakeup.hints,
1273
+ FastForward.DEFAULT_TIMEOUT
1274
+ )
1083
1275
 
1084
- const moved = await this._moveTo(batchToHead(verified.op.views.system), {
1085
- system: batchToHead(oplog.op.views.system),
1086
- verified: {
1087
- op: trusted || head,
1088
- flushes: verified.op.views.flushes
1089
- }
1276
+ const ff = await FastForward.fromHeads(this, [head, ...candidates], {
1277
+ force: true,
1278
+ timeout: FastForward.DEFAULT_TIMEOUT,
1279
+ condition: bootCondition,
1280
+ reference
1090
1281
  })
1091
1282
 
1092
- if (moved && wait) return this.reboot.promise
1093
-
1094
- return null
1283
+ return ff !== null && (await this._runFastForward(ff))
1095
1284
  }
1096
1285
 
1097
- // same as moveTo except we don't return the final promise
1098
- _initFromHead(head, tip) {
1099
- if (!head.length) {
1100
- // legacy fastForward boot
1101
- return this._runReboot(new Reboot(this, head, tip))
1102
- }
1286
+ async _bootReference(head) {
1287
+ const oplog = await FastForward.flushHead(this, head, {
1288
+ timeout: FastForward.DEFAULT_TIMEOUT
1289
+ })
1103
1290
 
1104
- return this._rebootFromHead(head, null, { force: true, wait: false })
1291
+ return oplog === null ? null : this.openViewAt(oplog)
1105
1292
  }
1106
1293
 
1107
- // head is a system head; reboots onto it, migrating in place if it's a legacy version
1108
- async _moveTo(systemHead, tip) {
1109
- if (this.rebootTo !== null || this.rebooting !== null) return false
1110
-
1111
- if (await this._runReboot(new Reboot(this, systemHead, tip))) {
1112
- return true
1294
+ async _runFastForward(ff) {
1295
+ if (this.fastForwardTo !== null || this.fastForwarding !== null) {
1296
+ await ff.close()
1297
+ return false
1113
1298
  }
1114
1299
 
1115
- return false
1116
- }
1117
-
1118
- async _runReboot(reboot) {
1119
- this.rebooting = reboot
1300
+ this.fastForwarding = ff
1120
1301
 
1121
- const result = await reboot.run()
1122
- await reboot.close()
1302
+ const result = await ff.run()
1303
+ await ff.close()
1123
1304
 
1124
- if (this.rebooting === reboot) this.rebooting = null
1305
+ if (this.fastForwarding === ff) this.fastForwarding = null
1125
1306
 
1126
1307
  if (!result) return false
1127
1308
 
1128
- this.rebootTo = result
1129
- this.reboot = rrp()
1309
+ this.fastForwardTo = result
1310
+ this.ff = rrp()
1130
1311
 
1131
1312
  this.bumpSoon()
1132
1313
 
1133
1314
  return true
1134
1315
  }
1135
1316
 
1136
- async _applyReboot() {
1317
+ async _applyFastForward() {
1137
1318
  const changes = this._hasUpdate ? new UpdateChanges(this) : null
1138
1319
  if (changes) changes.track()
1139
1320
 
1140
- const { head, tip, migrate } = this.rebootTo
1321
+ const { head, tip, migrate } = this.fastForwardTo
1141
1322
 
1142
1323
  const from = this.system.bee.head()
1143
1324
  const to = head
@@ -1145,15 +1326,16 @@ module.exports = class Autobee extends ReadyResource {
1145
1326
  this.system.bee.move(head)
1146
1327
  await this.system.reset()
1147
1328
 
1148
- this.system.shared = this.rebootTo.shared || {
1149
- flushes: -1,
1150
- view: EMPTY_HEAD,
1151
- system: EMPTY_HEAD
1152
- }
1153
-
1154
1329
  // migrate is set when fast-forwarding from a legacy head
1155
1330
  if (migrate) {
1156
- const view = (await this.handlers.migrate(migrate)) || EMPTY_HEAD
1331
+ const view = (await this._handlers.migrate(migrate, head)) || EMPTY_HEAD
1332
+
1333
+ this._migratedHead = {
1334
+ system: head,
1335
+ view: view.length ? view : (this._migratedHead && this._migratedHead.view) || null
1336
+ }
1337
+
1338
+ await this._storeMigratedHead()
1157
1339
  this.bee.move(view)
1158
1340
  this._workingBee.move(view)
1159
1341
  } else {
@@ -1161,13 +1343,21 @@ module.exports = class Autobee extends ReadyResource {
1161
1343
  this._workingBee.move(this.system.view)
1162
1344
  }
1163
1345
 
1164
- this.rebootTo = null
1346
+ this.fastForwardTo = null
1347
+
1348
+ // process any wakeup while fast-forward itself was in flight
1349
+ await this._applyWakeupHints()
1165
1350
  await this.writers.refresh()
1166
1351
 
1352
+ // we moved, so ask our peers to tell us their heads again
1353
+ this._requestWakeup()
1354
+
1167
1355
  await this._update(changes)
1356
+ await this._storeBoot()
1168
1357
 
1358
+ this.stats.fastForwards++
1169
1359
  this.emit('move-to', to, from)
1170
- this.reboot.resolve({ to, from })
1360
+ this.ff.resolve({ to, from })
1171
1361
 
1172
1362
  // tip is null during boot
1173
1363
  if (!tip) return
@@ -1187,7 +1377,8 @@ module.exports = class Autobee extends ReadyResource {
1187
1377
  const t = await topo.rollback(this, sys, verified)
1188
1378
  await sys.close()
1189
1379
 
1190
- await this.applyBacklog(t.tip)
1380
+ if (t !== null) await this.applyBacklog(t.tip)
1381
+
1191
1382
  return this._update(changes)
1192
1383
  }
1193
1384
 
@@ -1200,6 +1391,17 @@ function isObject(o) {
1200
1391
  return typeof o === 'object' && o && !b4a.isBuffer(o)
1201
1392
  }
1202
1393
 
1394
+ function getBootOption(boot) {
1395
+ if (!boot) return null
1396
+
1397
+ // oldest style, supported for now but will go away: a bare key is legacy
1398
+ if (boot.key) return { legacy: boot, head: null, bootCondition: null }
1399
+
1400
+ asserts.assert(!(boot.head && boot.legacy), 'Boot from either a head or a legacy pointer')
1401
+
1402
+ return boot
1403
+ }
1404
+
1203
1405
  function noop() {}
1204
1406
 
1205
1407
  function createAnchorCore(store, prologue, manifestData) {
@@ -1228,10 +1430,3 @@ function crashSoon(err) {
1228
1430
  })
1229
1431
  throw err
1230
1432
  }
1231
-
1232
- function batchToHead(b) {
1233
- return {
1234
- key: b.key,
1235
- length: b.start + b.length
1236
- }
1237
- }