autobee 2.0.0-rc.15 → 2.0.0-rc.16

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/README.md CHANGED
@@ -78,7 +78,10 @@ Options:
78
78
  encryptionKey: Buffer, // 32-byte key to encrypt all data at rest
79
79
  encrypted: false, // set true if using encryptionKey
80
80
  keyPair: { publicKey, secretKey }, // custom signing key pair for the local writer
81
- optimistic: true // allow optimistic writes from unknown writers
81
+ optimistic: true, // allow optimistic writes from unknown writers
82
+ isTrusted (key, reference) {}, // do we trust this writer, see Fast-forward
83
+ mostRecentTrusted (target, reference) {}, // the head we vouch for, see Fast-forward
84
+ fastForward: {} // see Fast-forward
82
85
  }
83
86
  ```
84
87
 
@@ -222,6 +225,50 @@ Create an anchor node. Returns `{ key, length }`. Anchors are used to create a v
222
225
 
223
226
  `true` if the system has not yet processed any nodes. Use this to bootstrap the first writer.
224
227
 
228
+ ### Fast-forward
229
+
230
+ Fast-forward only ever deals in oplog heads — `{ key, length }` of a writer's core, never a system head.
231
+
232
+ Each flush stamps the head you vouch for into your own oplog, and peers read those stamps out of the writers they wake up on, so trust travels with the log.
233
+
234
+ #### `isTrusted(key, reference)`
235
+
236
+ Return whether `key` is a writer you trust, judged against the `reference` view.
237
+
238
+ Positive answers are cached until an undo rewinds the view, and the default is `true` unless you supply `mostRecentTrusted`, in which case it is `false`.
239
+
240
+ #### `mostRecentTrusted(target, reference)`
241
+
242
+ Return the oplog head you most recently vouched for, given the `target` view being considered.
243
+
244
+ Called at flush time to stamp your own oplog (with your view as `target` and a `null` reference), and again per candidate during discovery.
245
+
246
+ #### `fastForward.boot`
247
+
248
+ ```js
249
+ {
250
+ head: { key, length }, // oplog head to boot from
251
+ legacy: { key, length }, // pre-2.0 pointer, see below
252
+ bootCondition (target, reference) {} // optional gate on the view we would land on
253
+ }
254
+ ```
255
+
256
+ Pass one of `head` or `legacy`, not both.
257
+
258
+ Without `bootCondition` this is a single attempt that gives up if the head cannot be read; with one it parks until the condition is satisfied, which today means indefinitely if it never is.
259
+
260
+ `legacy` is for records written before boot heads were oplog heads: the key is a _system_ head and a `0` length is resolved from the core. It boots ungated - `bootCondition` does not apply - and will be removed, so don't reach for it. A bare `{ key, length }` in place of the whole struct means the same thing, older still.
261
+
262
+ #### `fastForward.conservative`
263
+
264
+ Defaults to `true`: only fast-forward onto a head a connected peer can serve whole.
265
+
266
+ The check covers the oplog head only, not the system and view cores the fast-forward then reads.
267
+
268
+ #### `await db.moveTo(head)`
269
+
270
+ Fast-forward onto an oplog head, ignoring the usual distance and conservative checks. Resolves `{ to, from }`.
271
+
225
272
  ### Encryption
226
273
 
227
274
  Pass an `encryptionKey` to encrypt all writer cores and the view at rest.
@@ -856,15 +856,28 @@ const encoding29_7 = c.array(c.frame(encoding28))
856
856
  // @autobee/oplog-message-v3
857
857
  const encoding29 = {
858
858
  preencode(state, m) {
859
+ let flags =
860
+ (m.batch ? 1 : 0) |
861
+ (m.views ? 2 : 0) |
862
+ (m.optimistic ? 8 : 0) |
863
+ (m.value ? 16 : 0) |
864
+ (m.witness ? 32 : 0) |
865
+ (m.attestations ? 64 : 0) |
866
+ (m.trusted ? 128 : 0)
867
+ if (m.views) {
868
+ flags |= m.views.view ? 4 : 0
869
+ }
870
+
859
871
  c.uint.preencode(state, m.timestamp)
860
872
  encoding29_1.preencode(state, m.links)
861
- state.end++ // max flag is 64 so always one byte
873
+ c.uint.preencode(state, flags)
862
874
 
863
875
  if (m.batch) encoding24.preencode(state, m.batch)
864
876
  if (m.views) encoding25_inline.preencode(state, m.views)
865
877
  if (m.value) c.buffer.preencode(state, m.value)
866
878
  if (m.witness) encoding29_6.preencode(state, m.witness)
867
879
  if (m.attestations) encoding29_7.preencode(state, m.attestations)
880
+ if (m.trusted) encoding29_8.preencode(state, m.trusted)
868
881
  },
869
882
  encode(state, m) {
870
883
  let flags =
@@ -873,7 +886,8 @@ const encoding29 = {
873
886
  (m.optimistic ? 8 : 0) |
874
887
  (m.value ? 16 : 0) |
875
888
  (m.witness ? 32 : 0) |
876
- (m.attestations ? 64 : 0)
889
+ (m.attestations ? 64 : 0) |
890
+ (m.trusted ? 128 : 0)
877
891
  if (m.views) {
878
892
  flags |= m.views.view ? 4 : 0
879
893
  }
@@ -887,6 +901,7 @@ const encoding29 = {
887
901
  if (m.value) c.buffer.encode(state, m.value)
888
902
  if (m.witness) encoding29_6.encode(state, m.witness)
889
903
  if (m.attestations) encoding29_7.encode(state, m.attestations)
904
+ if (m.trusted) encoding29_8.encode(state, m.trusted)
890
905
  },
891
906
  decode(state) {
892
907
  const v = c.uint.decode(state)
@@ -903,7 +918,8 @@ const encoding29 = {
903
918
  optimistic: (flags & 8) !== 0,
904
919
  value: (flags & 16) !== 0 ? c.buffer.decode(state) : null,
905
920
  witness: (flags & 32) !== 0 ? encoding29_6.decode(state) : null,
906
- attestations: (flags & 64) !== 0 ? encoding29_7.decode(state) : null
921
+ attestations: (flags & 64) !== 0 ? encoding29_7.decode(state) : null,
922
+ trusted: (flags & 128) !== 0 ? encoding29_8.decode(state) : null
907
923
  }
908
924
  }
909
925
  }
@@ -1007,12 +1023,39 @@ const encoding31 = {
1007
1023
  }
1008
1024
  }
1009
1025
 
1026
+ // @autobee/trusted-head
1027
+ const encoding32 = {
1028
+ preencode(state, m) {
1029
+ c.fixed32.preencode(state, m.key)
1030
+ c.uint.preencode(state, m.length)
1031
+ c.uint.preencode(state, m.flushes)
1032
+ },
1033
+ encode(state, m) {
1034
+ c.fixed32.encode(state, m.key)
1035
+ c.uint.encode(state, m.length)
1036
+ c.uint.encode(state, m.flushes)
1037
+ },
1038
+ decode(state) {
1039
+ const r0 = c.fixed32.decode(state)
1040
+ const r1 = c.uint.decode(state)
1041
+ const r2 = c.uint.decode(state)
1042
+
1043
+ return {
1044
+ key: r0,
1045
+ length: r1,
1046
+ flushes: r2
1047
+ }
1048
+ }
1049
+ }
1050
+
1010
1051
  // @autobee/system-info-v3.heads, deferred due to recusive use
1011
1052
  const encoding18_3 = c.array(encoding22)
1012
1053
  // @autobee/system-info-v3.indexers, deferred due to recusive use
1013
1054
  const encoding18_4 = encoding18_3
1014
1055
  // @autobee/oplog-message-v3.links, deferred due to recusive use
1015
1056
  const encoding29_1 = encoding18_3
1057
+ // @autobee/oplog-message-v3.trusted, deferred due to recusive use
1058
+ const encoding29_8 = c.array(c.frame(encoding32))
1016
1059
 
1017
1060
  function setVersion(v) {
1018
1061
  version = v
@@ -1101,6 +1144,8 @@ function getEncoding(name) {
1101
1144
  return encoding30
1102
1145
  case '@autobee/manifest-data':
1103
1146
  return encoding31
1147
+ case '@autobee/trusted-head':
1148
+ return encoding32
1104
1149
  default:
1105
1150
  throw new Error('Encoder not found ' + name)
1106
1151
  }
@@ -750,6 +750,12 @@
750
750
  "array": true,
751
751
  "type": "@autobee/attestation",
752
752
  "version": 1
753
+ },
754
+ {
755
+ "name": "trusted",
756
+ "array": true,
757
+ "type": "@autobee/trusted-head",
758
+ "version": 1
753
759
  }
754
760
  ]
755
761
  },
@@ -804,6 +810,32 @@
804
810
  "version": 1
805
811
  }
806
812
  ]
813
+ },
814
+ {
815
+ "name": "trusted-head",
816
+ "namespace": "autobee",
817
+ "compact": false,
818
+ "flagsPosition": -1,
819
+ "fields": [
820
+ {
821
+ "name": "key",
822
+ "required": true,
823
+ "type": "fixed32",
824
+ "version": 1
825
+ },
826
+ {
827
+ "name": "length",
828
+ "required": true,
829
+ "type": "uint",
830
+ "version": 1
831
+ },
832
+ {
833
+ "name": "flushes",
834
+ "required": true,
835
+ "type": "uint",
836
+ "version": 1
837
+ }
838
+ ]
807
839
  }
808
840
  ],
809
841
  "namespaces": [
package/index.js CHANGED
@@ -19,11 +19,12 @@ 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,7 @@ 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
60
61
  this.stats = { undos: 0, fastForwards: 0, drains: 0, applies: 0, appends: 0 }
61
62
 
62
63
  this.system = new System(this.store.namespace('system'), this.name, {
@@ -66,7 +67,7 @@ module.exports = class Autobee extends ReadyResource {
66
67
  this.system.auto = this
67
68
 
68
69
  this.bee = bee.snapshot()
69
- this.view = handlers.open ? handlers.open(this.bee, this) : this.bee
70
+ this.view = ApplyView.open(this.bee, this)
70
71
  this.optimistic = handlers.optimistic !== false // TODO: should default to false instead
71
72
 
72
73
  this.name = name // for debugging
@@ -79,18 +80,20 @@ module.exports = class Autobee extends ReadyResource {
79
80
 
80
81
  const fastForward = handlers.fastForward || {}
81
82
 
82
- // system head to boot from: migrates or fast-forwards depending on its version
83
+ // oplog head to boot from: migrates or fast-forwards depending on its version
83
84
  this.bootFrom = fastForward.boot || null
84
85
 
85
86
  // conservative (default on): only fast-forward onto a head someone can serve whole
86
87
  this._conservativeFF = fastForward.conservative !== false
87
88
 
89
+ this.trusted = new TrustedPeers(handlers)
90
+
88
91
  this.ff = null
89
92
  this.fastForwarding = null
90
93
  this.fastForwardTo = null
91
94
 
92
95
  this._workingBee = bee
93
- this._workingView = handlers.open ? handlers.open(this._workingBee, this) : this._workingBee
96
+ this._workingView = new ApplyView(this._workingBee, this)
94
97
 
95
98
  this._localSystemStart = 0
96
99
  this._localSystemLength = 0
@@ -100,6 +103,7 @@ module.exports = class Autobee extends ReadyResource {
100
103
 
101
104
  this._appending = []
102
105
  this._draining = null
106
+ this._bootWait = null
103
107
 
104
108
  this.legacyViews = handlers.legacyViews || []
105
109
 
@@ -107,7 +111,6 @@ module.exports = class Autobee extends ReadyResource {
107
111
  this._bootingState = null
108
112
  this._bootingAll = null
109
113
 
110
- this._handlers = handlers
111
114
  this._now = handlers.now || Date.now // overridable for clock-drift tests
112
115
  this._hasApply = !!handlers.apply
113
116
  this._hasUpdate = !!handlers.update
@@ -175,6 +178,11 @@ module.exports = class Autobee extends ReadyResource {
175
178
  this.bumpSoon()
176
179
  }
177
180
 
181
+ _requestWakeup() {
182
+ const session = this._wakeup._session
183
+ if (session) session.broadcastLookup()
184
+ }
185
+
178
186
  _registerWakeup() {
179
187
  if (!this.wakeupCapability) return
180
188
  this._wakeup.recouple()
@@ -219,6 +227,7 @@ module.exports = class Autobee extends ReadyResource {
219
227
 
220
228
  async _close() {
221
229
  this._interrupting = true
230
+ if (this._bootWait !== null) this._bootWait.resolve()
222
231
  if (this._notifyHandler) this._notifyHandler.destroy()
223
232
  if (this._draining) {
224
233
  // drain may be waiting on replicated blocks
@@ -231,14 +240,14 @@ module.exports = class Autobee extends ReadyResource {
231
240
  await this._bootingAll
232
241
  } catch {}
233
242
 
234
- if (this._handlers.close) await this._handlers.close(this.view)
243
+ await ApplyView.close(this.view, this)
235
244
 
236
245
  if (this.writers) await this.writers.close()
237
246
  await this.local.close()
238
247
  await this.system.close()
239
248
  await this._wakeup.close()
240
249
  if (this.bootstrap && !this.bootstrap.closed) await this.bootstrap.close()
241
- await this._workingBee.close()
250
+ await this._workingView.close()
242
251
  await this.bee.close()
243
252
  await this.store.close()
244
253
  }
@@ -287,6 +296,14 @@ module.exports = class Autobee extends ReadyResource {
287
296
  this._wakeup.hint(wakeup)
288
297
  }
289
298
 
299
+ // the view an oplog head points at, opened and closed through the handlers
300
+ openViewAt(oplog) {
301
+ const v = oplog.op.views.view
302
+ if (!v) return null
303
+
304
+ return new ApplyView(this.bee.checkout({ key: v.key, length: v.start + v.length }), this)
305
+ }
306
+
290
307
  openCore(key) {
291
308
  const encryption = this.encryptionKey ? new WriterEncryption(this) : null
292
309
  return this.store.get({ key, encryption })
@@ -312,7 +329,7 @@ module.exports = class Autobee extends ReadyResource {
312
329
  }
313
330
 
314
331
  if (this.bootFrom) {
315
- this.bootFrom = (await this.bootFrom) || null
332
+ this.bootFrom = getBootOption(await this.bootFrom)
316
333
  }
317
334
  }
318
335
 
@@ -400,8 +417,8 @@ module.exports = class Autobee extends ReadyResource {
400
417
 
401
418
  // @todo migration
402
419
  if (result.migration) {
403
- if (this.handlers.migrate) {
404
- view = (await this.handlers.migrate(result.migration.views)) || EMPTY_HEAD
420
+ if (this._handlers.migrate) {
421
+ view = (await this._handlers.migrate(result.migration.views)) || EMPTY_HEAD
405
422
  this._catchupMigratedNodes = result.migration.catchup
406
423
  await this._storeMigratedView(view)
407
424
  }
@@ -443,6 +460,8 @@ module.exports = class Autobee extends ReadyResource {
443
460
  async _bump() {
444
461
  if (!(await this._bootReady())) return
445
462
 
463
+ if (this._bootWait !== null) this._bootWait.resolve()
464
+
446
465
  this.bumping++
447
466
 
448
467
  if (!this._draining) {
@@ -502,8 +521,12 @@ module.exports = class Autobee extends ReadyResource {
502
521
  this.stats.drains++
503
522
 
504
523
  if (this.bootFrom) {
505
- await this._initFromHead(this.bootFrom)
524
+ const { head = null, legacy = null, bootCondition = null } = this.bootFrom
525
+
506
526
  this.bootFrom = null
527
+
528
+ if (legacy) await this._bootFromSystem(legacy)
529
+ else if (head) await this._bootFromHead(head, bootCondition)
507
530
  }
508
531
 
509
532
  // tracked across drain iteration
@@ -607,49 +630,86 @@ module.exports = class Autobee extends ReadyResource {
607
630
  }
608
631
 
609
632
  // a scheduled fast-forward is applied by the drain before we look again
610
- if (this.fastForwardTo !== null) return
611
- if (this._interrupting || this.closing) return
633
+ if (this.fastForwardTo !== null || this.fastForwarding !== null) return
634
+ if (this._interrupting || this.closing || this.bootFrom) return
612
635
 
613
636
  try {
614
- await this.fastForwardFromHeads(hints, { wait: false, timeout: FastForward.DEFAULT_TIMEOUT })
637
+ const heads = await this._readCandidateHeads(hints, FastForward.DEFAULT_TIMEOUT)
638
+
639
+ const ff = await FastForward.fromHeads(this, heads, {
640
+ timeout: FastForward.DEFAULT_TIMEOUT
641
+ })
642
+
643
+ if (ff !== null) await this._runFastForward(ff)
615
644
  } catch (err) {
616
645
  safetyCatch(err)
617
- return false
618
646
  }
647
+ }
619
648
 
620
- return true
649
+ async _readCandidateHeads(hints, timeout) {
650
+ const promises = []
651
+
652
+ for (const [hex, length] of hints) {
653
+ if (length === 0) continue
654
+ const key = b4a.from(hex, 'hex')
655
+ promises.push(this._getOplog(key, length, timeout ? { timeout } : null))
656
+ }
657
+
658
+ const ops = await Promise.all(promises)
659
+ const heads = []
660
+
661
+ for (const res of ops) {
662
+ if (res === null) continue
663
+
664
+ // the head we were woken on is a candidate in its own right
665
+ heads.push({ key: res.key, length: res.length })
666
+
667
+ if (!res.op.trusted) continue
668
+
669
+ for (const trusted of res.op.trusted) {
670
+ const head = this.trusted.read(trusted)
671
+ if (head !== null) heads.push(head)
672
+ }
673
+ }
674
+
675
+ return heads
621
676
  }
622
677
 
623
678
  async _getOplog(key, length, opts = null) {
624
679
  const core = this.openCore(key)
625
680
 
626
681
  try {
627
- await core.ready()
682
+ return await this.readOplog(core, length, opts)
683
+ } finally {
684
+ await core.close()
685
+ }
686
+ }
687
+
688
+ // reads on a core the caller owns, so a walk does not churn a session per step
689
+ async readOplog(core, length, opts = null) {
690
+ await core.ready()
628
691
 
629
- const target = length >= 0 ? length : core.length
630
- if (target === 0) return null
692
+ const target = length >= 0 ? length : core.length
693
+ if (target === 0) return null
631
694
 
632
- // conservative: only proceed when a connected peer can serve the head whole
633
- if (opts && opts.conservative && core.remoteContiguousLength < target) return null
695
+ // conservative: only proceed when a connected peer can serve the head whole
696
+ if (opts && opts.conservative && core.remoteContiguousLength < target) return null
634
697
 
635
- const buf = await core.get(target - 1, opts)
636
- if (buf === null) return null
698
+ const buf = await core.get(target - 1, opts)
699
+ if (buf === null) return null
637
700
 
638
- let op = encoding.decodeOplog(buf)
701
+ let op = encoding.decodeOplog(buf)
639
702
 
640
- // legacy nodes always inflate, but only indexers carry views - callers
641
- // that need system info must check op.views
642
- if (op.version < 3) {
643
- op = await this._inflateLegacyOplog(buf, core, target - 1, opts)
644
- }
703
+ // legacy nodes always inflate, but only indexers carry views - callers
704
+ // that need system info must check op.views
705
+ if (op.version < 3) {
706
+ op = await this._inflateLegacyOplog(buf, core, target - 1, opts)
707
+ }
645
708
 
646
- return {
647
- key,
648
- length: target,
649
- op
650
- }
651
- } finally {
652
- await core.close()
709
+ return {
710
+ key: core.key,
711
+ length: target,
712
+ op
653
713
  }
654
714
  }
655
715
 
@@ -939,6 +999,7 @@ module.exports = class Autobee extends ReadyResource {
939
999
 
940
1000
  if (t.undo) {
941
1001
  this.stats.undos++
1002
+ this.trusted.clear()
942
1003
  t.view = await this.system.undo(t.undo)
943
1004
  }
944
1005
 
@@ -989,7 +1050,7 @@ module.exports = class Autobee extends ReadyResource {
989
1050
  if (this._hasApply && (await this.system.canApply(batch[0].key, optimistic))) {
990
1051
  this.stats.applies++
991
1052
  this._host.applying = batch
992
- await this._handlers.apply(userBatch, this._workingView, this._host)
1053
+ await this._workingView.apply(userBatch)
993
1054
  this._host.applying = null
994
1055
  }
995
1056
 
@@ -1135,115 +1196,108 @@ module.exports = class Autobee extends ReadyResource {
1135
1196
  this._localViewLength = 0
1136
1197
  }
1137
1198
 
1138
- async fastForwardFromHeads(hints, { wait = true, timeout = 0 } = {}) {
1139
- if (!this._handlers.onwakeup) return false
1140
- if (this._interrupting) return false
1141
- if (!hints.size || this.fastForwarding || this.fastForwardTo || this.bootFrom) {
1142
- return false
1143
- }
1199
+ async moveTo(head) {
1200
+ if (!this.opened) await this.ready()
1201
+ if (this.closing) throw new Error('Autobee closed')
1144
1202
 
1145
- const promises = []
1146
- const heads = []
1147
- for (const [hex, length] of hints) {
1148
- if (length === 0) continue
1149
- const key = b4a.from(hex, 'hex')
1150
- heads.push({ key, length })
1151
- promises.push(this._getOplog(key, length, timeout ? { timeout } : null))
1152
- }
1203
+ const ff = await FastForward.fromHead(this, head, null, { force: true })
1204
+ if (ff === null) return null
1153
1205
 
1154
- const ops = await Promise.all(promises)
1155
- if (this.fastForwarding || this.fastForwardTo) return false
1206
+ if (!(await this._runFastForward(ff))) return null
1156
1207
 
1157
- let best = null
1158
- let bestFlushes = -1
1208
+ return this.ff.promise
1209
+ }
1159
1210
 
1160
- for (let i = 0; i < ops.length; i++) {
1161
- const res = ops[i]
1162
- if (res === null) continue
1211
+ // legacy: ungated boot straight onto a system head
1212
+ async _bootFromSystem(system) {
1213
+ try {
1214
+ const ff = new FastForward(this, system, null, { timeout: FastForward.DEFAULT_TIMEOUT })
1215
+ return await this._runFastForward(ff)
1216
+ } catch (err) {
1217
+ safetyCatch(err)
1218
+ return false
1219
+ }
1220
+ }
1163
1221
 
1164
- if (res.op.views && res.op.views.flushes > bestFlushes) {
1165
- bestFlushes = res.op.views.flushes
1166
- best = res
1222
+ // the boot head seeds the view every trust decision here is made against,
1223
+ // since our own view is still empty
1224
+ async _bootFromHead(head, bootCondition) {
1225
+ // just a head: one attempt, we do not wait around if it cannot be read
1226
+ if (bootCondition === null) {
1227
+ try {
1228
+ const ff = await FastForward.fromHead(this, head, null, {
1229
+ force: true,
1230
+ timeout: FastForward.DEFAULT_TIMEOUT
1231
+ })
1232
+
1233
+ return ff !== null && (await this._runFastForward(ff))
1234
+ } catch (err) {
1235
+ safetyCatch(err)
1236
+ return false
1167
1237
  }
1168
1238
  }
1169
1239
 
1170
- if (best === null || bestFlushes - this.system.flushes < MIN_FF_GAP) return false
1171
-
1172
- const head = { key: best.key, length: best.length }
1173
-
1174
- const v = best.op.views.view
1175
- const view = this.bee.checkout({ key: v.key, length: v.start + v.length })
1240
+ // a condition needs the view at head to check against, so park until we
1241
+ // can read it and something acceptable turns up
1242
+ let opened = null
1176
1243
 
1177
- let trusted = null
1178
1244
  try {
1179
- trusted = await this._handlers.onwakeup(head, view, this)
1180
- if (!trusted || this.fastForwarding || this.fastForwardTo) return false
1181
- } finally {
1182
- view.close()
1183
- }
1245
+ while (!this._interrupting) {
1246
+ this._bootWait = rrp()
1184
1247
 
1185
- return this._fastForwardFromHead(best, trusted, { wait, timeout })
1186
- }
1248
+ try {
1249
+ if (opened === null) opened = await this._bootReference(head)
1187
1250
 
1188
- async _fastForwardFromHead(head, trusted, { force = false, wait = true, timeout = 0 } = {}) {
1189
- const conservative = !force && this._conservativeFF
1190
- const timeoutOpts = timeout ? { timeout } : null
1191
-
1192
- const oplog = await this._getOplog(head.key, head.length, {
1193
- conservative,
1194
- ...timeoutOpts
1195
- })
1196
- if (!oplog) return null
1197
-
1198
- const verified = trusted
1199
- ? await this._getOplog(trusted.key, trusted.length, timeoutOpts)
1200
- : oplog
1201
- if (!verified) return null
1202
-
1203
- // legacy nodes from non-indexers have no system info to fast-forward from
1204
- if (!oplog.op.views || !verified.op.views) return null
1251
+ if (opened !== null && (await this._bootAttempt(head, bootCondition, opened.view))) {
1252
+ return true
1253
+ }
1254
+ } catch (err) {
1255
+ safetyCatch(err)
1256
+ }
1205
1257
 
1206
- if (!force && verified.op.views.flushes - this.system.flushes < MIN_FF_GAP) {
1207
- return null
1208
- }
1258
+ if (this._interrupting) break
1209
1259
 
1210
- const tip = {
1211
- system: batchToHead(oplog.op.views.system),
1212
- verified: {
1213
- op: trusted || head,
1214
- flushes: verified.op.views.flushes
1260
+ await this._bootWait.promise
1215
1261
  }
1262
+ } finally {
1263
+ this._bootWait = null
1264
+ if (opened !== null) await opened.close()
1216
1265
  }
1217
1266
 
1218
- const moved = await this._moveTo(batchToHead(verified.op.views.system), tip)
1219
-
1220
- if (moved && wait) return this.ff.promise
1221
-
1222
- return null
1267
+ return false
1223
1268
  }
1224
1269
 
1225
- // same as moveTo except we don't return the final promise
1226
- _initFromHead(head, tip) {
1227
- if (!head.length) {
1228
- // legacy fastForward boot
1229
- return this._runFastForward(new FastForward(this, head, tip))
1230
- }
1270
+ async _bootAttempt(head, bootCondition, reference) {
1271
+ // peek, so the hints are still there for the drain once we have booted
1272
+ const candidates = await this._readCandidateHeads(
1273
+ this._wakeup.hints,
1274
+ FastForward.DEFAULT_TIMEOUT
1275
+ )
1231
1276
 
1232
- return this._fastForwardFromHead(head, null, { force: true, wait: false })
1233
- }
1277
+ const ff = await FastForward.fromHeads(this, [head, ...candidates], {
1278
+ force: true,
1279
+ timeout: FastForward.DEFAULT_TIMEOUT,
1280
+ condition: bootCondition,
1281
+ reference
1282
+ })
1234
1283
 
1235
- // head is a system head; fast-forwards onto it, migrating in place if it's a legacy version
1236
- async _moveTo(systemHead, tip) {
1237
- if (this.fastForwardTo !== null || this.fastForwarding !== null) return false
1284
+ return ff !== null && (await this._runFastForward(ff))
1285
+ }
1238
1286
 
1239
- if (await this._runFastForward(new FastForward(this, systemHead, tip))) {
1240
- return true
1241
- }
1287
+ async _bootReference(head) {
1288
+ const oplog = await FastForward.flushHead(this, head, {
1289
+ timeout: FastForward.DEFAULT_TIMEOUT
1290
+ })
1242
1291
 
1243
- return false
1292
+ return oplog === null ? null : this.openViewAt(oplog)
1244
1293
  }
1245
1294
 
1246
1295
  async _runFastForward(ff) {
1296
+ if (this.fastForwardTo !== null || this.fastForwarding !== null) {
1297
+ await ff.close()
1298
+ return false
1299
+ }
1300
+
1247
1301
  this.fastForwarding = ff
1248
1302
 
1249
1303
  const result = await ff.run()
@@ -1281,7 +1335,7 @@ module.exports = class Autobee extends ReadyResource {
1281
1335
 
1282
1336
  // migrate is set when fast-forwarding from a legacy head
1283
1337
  if (migrate) {
1284
- const view = (await this.handlers.migrate(migrate)) || EMPTY_HEAD
1338
+ const view = (await this._handlers.migrate(migrate)) || EMPTY_HEAD
1285
1339
  await this._storeMigratedView(view)
1286
1340
  this.bee.move(view)
1287
1341
  this._workingBee.move(view)
@@ -1293,6 +1347,9 @@ module.exports = class Autobee extends ReadyResource {
1293
1347
  this.fastForwardTo = null
1294
1348
  await this.writers.refresh()
1295
1349
 
1350
+ // we moved, so ask our peers to tell us their heads again
1351
+ this._requestWakeup()
1352
+
1296
1353
  await this._storeBoot()
1297
1354
 
1298
1355
  await this._update(changes)
@@ -1333,6 +1390,17 @@ function isObject(o) {
1333
1390
  return typeof o === 'object' && o && !b4a.isBuffer(o)
1334
1391
  }
1335
1392
 
1393
+ function getBootOption(boot) {
1394
+ if (!boot) return null
1395
+
1396
+ // oldest style, supported for now but will go away: a bare key is legacy
1397
+ if (boot.key) return { legacy: boot, head: null, bootCondition: null }
1398
+
1399
+ asserts.assert(!(boot.head && boot.legacy), 'Boot from either a head or a legacy pointer')
1400
+
1401
+ return boot
1402
+ }
1403
+
1336
1404
  function noop() {}
1337
1405
 
1338
1406
  function createAnchorCore(store, prologue, manifestData) {
@@ -1361,10 +1429,3 @@ function crashSoon(err) {
1361
1429
  })
1362
1430
  throw err
1363
1431
  }
1364
-
1365
- function batchToHead(b) {
1366
- return {
1367
- key: b.key,
1368
- length: b.start + b.length
1369
- }
1370
- }
@@ -0,0 +1,25 @@
1
+ module.exports = class ApplyView {
2
+ constructor(bee, auto) {
3
+ this.bee = bee
4
+ this.auto = auto
5
+ this.view = ApplyView.open(bee, auto)
6
+ }
7
+
8
+ static open(bee, auto) {
9
+ return auto._handlers.open ? auto._handlers.open(bee, auto) : bee
10
+ }
11
+
12
+ static close(view, auto) {
13
+ if (!auto._handlers.close) return Promise.resolve()
14
+ return auto._handlers.close(view)
15
+ }
16
+
17
+ apply(userBatch) {
18
+ return this.auto._handlers.apply(userBatch, this.view, this.auto._host)
19
+ }
20
+
21
+ async close() {
22
+ await ApplyView.close(this.view, this.auto)
23
+ await this.bee.close()
24
+ }
25
+ }
@@ -19,6 +19,11 @@ const [NS_SIGNER_NAMESPACE] = crypto.namespace('autobase', 1)
19
19
 
20
20
  const EMPTY = b4a.alloc(0)
21
21
  const DEFAULT_OP_TIMEOUT = 5_000
22
+ const MIN_FF_GAP = 32
23
+
24
+ // views are only stamped on the last node of a flush, so a head may sit above
25
+ // the most recent one - how far back we look for it
26
+ const MAX_FLUSH_SEARCH = 64
22
27
 
23
28
  module.exports = class FastForward {
24
29
  constructor(auto, head, tip, { timeout = DEFAULT_OP_TIMEOUT } = {}) {
@@ -46,6 +51,162 @@ module.exports = class FastForward {
46
51
 
47
52
  static DEFAULT_TIMEOUT = DEFAULT_OP_TIMEOUT
48
53
 
54
+ static async fromHead(
55
+ auto,
56
+ head,
57
+ trusted,
58
+ { force = false, timeout = 0, condition = null, reference = null } = {}
59
+ ) {
60
+ const conservative = !force && auto._conservativeFF
61
+ const timeoutOpts = timeout ? { timeout } : null
62
+
63
+ const oplog = await FastForward.flushHead(auto, head, {
64
+ conservative,
65
+ ...timeoutOpts
66
+ })
67
+ if (!oplog) return null
68
+
69
+ const verified = trusted ? await FastForward.flushHead(auto, trusted, timeoutOpts) : oplog
70
+ if (!verified) return null
71
+
72
+ // legacy nodes from non-indexers have no system info to fast-forward from
73
+ if (!oplog.op.views || !verified.op.views) return null
74
+
75
+ if (!force && verified.op.views.flushes - auto.system.flushes < MIN_FF_GAP) {
76
+ return null
77
+ }
78
+
79
+ if (condition !== null && !(await FastForward.accepts(auto, verified, condition, reference))) {
80
+ return null
81
+ }
82
+
83
+ const tip = {
84
+ system: batchToHead(oplog.op.views.system),
85
+ verified: {
86
+ op: { key: verified.key, length: verified.length },
87
+ flushes: verified.op.views.flushes
88
+ }
89
+ }
90
+
91
+ return new FastForward(auto, batchToHead(verified.op.views.system), tip)
92
+ }
93
+
94
+ static async fromHeads(
95
+ auto,
96
+ heads,
97
+ { force = false, timeout = 0, condition = null, reference = null } = {}
98
+ ) {
99
+ if (reference === null) reference = auto._workingView.view
100
+
101
+ const promises = []
102
+
103
+ for (const head of heads) {
104
+ if (head.length === 0) continue
105
+ promises.push(FastForward.flushHead(auto, head, timeout ? { timeout } : null))
106
+ }
107
+
108
+ const ops = await Promise.all(promises)
109
+ if (auto.fastForwarding || auto.fastForwardTo) return null
110
+
111
+ const trust = await Promise.all(
112
+ ops.map((res) => {
113
+ return res === null ? false : auto.trusted.isTrusted(res.key, reference)
114
+ })
115
+ )
116
+ if (auto.fastForwarding || auto.fastForwardTo) return null
117
+
118
+ const candidates = []
119
+
120
+ let bestTrusted = null
121
+ let bestTrustedFlushes = -1
122
+
123
+ for (let i = 0; i < ops.length; i++) {
124
+ const res = ops[i]
125
+ if (res === null || !res.op.views) continue
126
+ if (!force && res.op.views.flushes - auto.system.flushes < MIN_FF_GAP) continue
127
+
128
+ if (!trust[i]) {
129
+ candidates.push(res)
130
+ continue
131
+ }
132
+
133
+ if (res.op.views.flushes > bestTrustedFlushes) {
134
+ bestTrustedFlushes = res.op.views.flushes
135
+ bestTrusted = res
136
+ }
137
+ }
138
+
139
+ if (bestTrusted !== null) {
140
+ return FastForward.fromHead(auto, bestTrusted, null, {
141
+ force,
142
+ timeout,
143
+ condition,
144
+ reference
145
+ })
146
+ }
147
+
148
+ candidates.sort((a, b) => b.op.views.flushes - a.op.views.flushes)
149
+
150
+ for (const res of candidates) {
151
+ const trusted = await FastForward.mostRecentTrusted(auto, res, reference)
152
+ if (trusted === null) continue
153
+ if (auto.fastForwarding || auto.fastForwardTo) return null
154
+
155
+ const ff = await FastForward.fromHead(auto, res, trusted, {
156
+ force,
157
+ timeout,
158
+ condition,
159
+ reference
160
+ })
161
+ if (ff !== null) return ff
162
+ }
163
+
164
+ return null
165
+ }
166
+
167
+ static async flushHead(auto, head, opts) {
168
+ const core = auto.openCore(head.key)
169
+
170
+ try {
171
+ let length = head.length
172
+
173
+ for (let i = 0; i < MAX_FLUSH_SEARCH; i++) {
174
+ const oplog = await auto.readOplog(core, length, opts)
175
+ if (oplog === null) return null
176
+ if (oplog.op.views) return oplog
177
+
178
+ length = oplog.length - 1
179
+ if (length <= 0) return null
180
+ }
181
+
182
+ return null
183
+ } finally {
184
+ await core.close()
185
+ }
186
+ }
187
+
188
+ static async accepts(auto, oplog, condition, reference) {
189
+ const opened = auto.openViewAt(oplog)
190
+ if (opened === null) return false
191
+
192
+ try {
193
+ return !!(await condition(opened.view, reference))
194
+ } finally {
195
+ await opened.close()
196
+ }
197
+ }
198
+
199
+ static async mostRecentTrusted(auto, head, reference) {
200
+ const opened = auto.openViewAt(head)
201
+ if (opened === null) return null
202
+
203
+ try {
204
+ return (await auto.trusted.mostRecentTrusted(opened.view, reference)) || null
205
+ } finally {
206
+ await opened.close()
207
+ }
208
+ }
209
+
49
210
  async run() {
50
211
  try {
51
212
  if (!this.running) this.running = this._run()
@@ -228,6 +389,13 @@ module.exports = class FastForward {
228
389
  }
229
390
  }
230
391
 
392
+ function batchToHead(b) {
393
+ return {
394
+ key: b.key,
395
+ length: b.start + b.length
396
+ }
397
+ }
398
+
231
399
  function decodeLegacySystemInfo(buffer) {
232
400
  const state = { start: 0, end: buffer.length, buffer }
233
401
  const version = c.uint.decode(state)
package/lib/topo.js CHANGED
@@ -308,7 +308,7 @@ function getLocalBatch(undo, writer, length, weight, index) {
308
308
 
309
309
  if (length > writer.core.length) {
310
310
  if (!writer.pending) {
311
- throw new Error(`No local nodes: looking for ${length}, found ${writer.core.length}`)
311
+ throw new Error(`No local nodes: looking for ${length}, found ${writer.core.length}@${writer.core.id}`)
312
312
  }
313
313
 
314
314
  let i = 0
package/lib/trusted.js ADDED
@@ -0,0 +1,44 @@
1
+ const b4a = require('b4a')
2
+
3
+ class TrustedPeers {
4
+ constructor(hooks) {
5
+ this.trusted = new Map()
6
+ this.pending = new Map()
7
+ this.hooks = hooks
8
+ }
9
+
10
+ async isTrusted(key, reference) {
11
+ const hex = b4a.toString(key, 'hex')
12
+ if (this.trusted.get(hex)) return true
13
+
14
+ let promise = this.pending.get(hex)
15
+ if (promise) return promise
16
+
17
+ promise = this.hooks.isTrusted
18
+ ? this.hooks.isTrusted(key, reference)
19
+ : Promise.resolve(!this.hooks.mostRecentTrusted)
20
+ this.pending.set(hex, promise)
21
+
22
+ const trusted = await promise
23
+ if (trusted) this.trusted.set(hex, trusted)
24
+
25
+ if (this.pending.get(hex) === promise) this.pending.delete(hex)
26
+ return trusted
27
+ }
28
+
29
+ read(trusted) {
30
+ return trusted || null
31
+ }
32
+
33
+ mostRecentTrusted(target, reference) {
34
+ if (!this.hooks.mostRecentTrusted) return Promise.resolve(null)
35
+ return this.hooks.mostRecentTrusted(target, reference)
36
+ }
37
+
38
+ clear() {
39
+ this.pending.clear()
40
+ this.trusted.clear()
41
+ }
42
+ }
43
+
44
+ module.exports = TrustedPeers
package/lib/writers.js CHANGED
@@ -64,6 +64,9 @@ class ActiveWriters {
64
64
  w = await this.add(key)
65
65
  }
66
66
 
67
+ // prefetch the oplog head for the fast-forward that runs right after us
68
+ if (w && length > 0) w.core.get(length - 1).catch(noop)
69
+
67
70
  // TODO: signal that we've seen length on w for gc
68
71
 
69
72
  this.bump()
@@ -95,16 +98,18 @@ class ActiveWriters {
95
98
  return this.localWriter.clear()
96
99
  }
97
100
 
98
- flushLocal(views) {
101
+ async flushLocal(views) {
99
102
  const w = this.localWriter
100
103
 
101
104
  // only consume the queue when a flush will actually write
102
- if (w.pending === null || w.processed === 0) return Promise.resolve()
105
+ if (w.pending === null || w.processed === 0) return
103
106
 
104
107
  const queued = this.attestations
105
108
  this.attestations = []
106
109
 
107
- return w.flush(views, queued)
110
+ const trusted = await this.auto.trusted.mostRecentTrusted(this.auto._workingView.view, null)
111
+
112
+ return w.flush(views, queued, trusted)
108
113
  }
109
114
 
110
115
  async refresh() {
@@ -623,7 +628,8 @@ class Writer {
623
628
  batch,
624
629
  views: null,
625
630
  optimistic: optimistic && !this.writers.writable,
626
- value
631
+ value,
632
+ trusted: null
627
633
  }
628
634
 
629
635
  const node = createNode(this.core, this.appendLength++, -1, oplog)
@@ -646,12 +652,16 @@ class Writer {
646
652
  this.processed = 0
647
653
  }
648
654
 
649
- flush(views, queued) {
655
+ flush(views, queued, trusted) {
650
656
  if (this.pending === null || this.processed === 0) return Promise.resolve()
651
657
 
652
658
  const head = this.pending[this.processed - 1]
653
659
  head.views = views
654
660
 
661
+ head.trusted = trusted
662
+ ? [{ key: trusted.key, length: trusted.length, flushes: views.flushes }]
663
+ : null
664
+
655
665
  if (queued && queued.length) {
656
666
  const attestations = []
657
667
  for (const { key, weight } of queued) {
@@ -695,7 +705,8 @@ function createNode(core, length, weight, oplog) {
695
705
  batch: oplog.batch,
696
706
  views: oplog.views,
697
707
  optimistic: oplog.optimistic,
698
- value: oplog.value
708
+ value: oplog.value,
709
+ trusted: oplog.trusted || []
699
710
  }
700
711
  }
701
712
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autobee",
3
- "version": "2.0.0-rc.15",
3
+ "version": "2.0.0-rc.16",
4
4
  "description": "Bee based autobase",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Holepunch Inc.",