autobee 2.10.1 → 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -5,6 +5,7 @@ const safetyCatch = require('safety-catch')
5
5
  const Hyperbee = require('hyperbee2')
6
6
  const ID = require('hypercore-id-encoding')
7
7
  const rrp = require('resolve-reject-promise')
8
+ const Signal = require('signal-promise')
8
9
  const { AutobeeEncryption, WriterEncryption, ViewEncryption } = require('autobee-encryption')
9
10
  const AutobeeWakeup = require('autobee-wakeup')
10
11
  const Hypercore = require('hypercore')
@@ -12,6 +13,7 @@ const crypto = require('hypercore-crypto')
12
13
  const c = require('compact-encoding')
13
14
  const asserts = require('./lib/asserts.js')
14
15
  const boot = require('./lib/boot.js')
16
+ const { DEFAULT_MANIFEST_VERSION } = require('./lib/constants.js')
15
17
  const { resolveWeight, currentWeight } = require('./lib/witness.js')
16
18
  const encoding = require('./lib/encoding.js')
17
19
  const FastForward = require('./lib/fast-forward.js')
@@ -26,6 +28,7 @@ const migrations = require('./lib/migrations.js')
26
28
 
27
29
  const EMPTY_HEAD = { length: 0, key: null }
28
30
  const DEFAULT_ACK_THRESHOLD = 32
31
+ const BOOT_NETWORK_TIMEOUT = 5000
29
32
  const INTERRUPT = new Error('Apply interrupted')
30
33
 
31
34
  module.exports = class Autobee extends ReadyResource {
@@ -116,6 +119,9 @@ module.exports = class Autobee extends ReadyResource {
116
119
  this.fastForwardTo = null
117
120
  this._ffCandidates = new Map()
118
121
  this._ffSearching = null
122
+ this._bumpSignal = new Signal()
123
+ this._networkBooted = false
124
+ this._networkBooting = false
119
125
 
120
126
  this._workingBee = bee
121
127
  this._workingView = new ApplyView(this._workingBee, this)
@@ -143,6 +149,7 @@ module.exports = class Autobee extends ReadyResource {
143
149
  this._now = handlers.now || Date.now // overridable for clock-drift tests
144
150
  this._preapply = handlers.preapply || null
145
151
  this._preApplied = false
152
+ this._reindexed = false
146
153
  this._warmup = handlers.warmup || null
147
154
  this._hasApply = !!handlers.apply
148
155
  this._hasUpdate = !!handlers.update
@@ -205,7 +212,8 @@ module.exports = class Autobee extends ReadyResource {
205
212
  await 1
206
213
  const result = await this._prebooting
207
214
  return {
208
- name: 'autobee/' + result.local.id + '/' + name,
215
+ name: 'autobee/' + result.local.id + '/view/' + name,
216
+ manifestVersion: DEFAULT_MANIFEST_VERSION,
209
217
  encryption: name === 'system' ? this.getSystemEncryption() : this.getViewEncryption(),
210
218
  inflightRange: [256, 512]
211
219
  }
@@ -380,6 +388,7 @@ module.exports = class Autobee extends ReadyResource {
380
388
 
381
389
  async _close() {
382
390
  this._interrupting = true
391
+ this._bumpSignal.notify()
383
392
 
384
393
  // the local core holds the exclusive lock but the local writer closes it
385
394
  // early in the teardown, so hand the lock to a detached session that
@@ -583,6 +592,11 @@ module.exports = class Autobee extends ReadyResource {
583
592
  this._localFlushes = this.system.flushes
584
593
  }
585
594
 
595
+ async _isReindexing() {
596
+ if (this._networkBooted || !this._ffEnabled || this._migrating) return false
597
+ return !(await this._isReindexed())
598
+ }
599
+
586
600
  async _bootOnline() {
587
601
  if (!this._bootOnlineGuard.enter()) return
588
602
 
@@ -607,6 +621,7 @@ module.exports = class Autobee extends ReadyResource {
607
621
  }
608
622
 
609
623
  bumpSoon() {
624
+ this._bumpSignal.notify()
610
625
  this._bump(false).catch(safetyCatch)
611
626
  }
612
627
 
@@ -668,6 +683,58 @@ module.exports = class Autobee extends ReadyResource {
668
683
  this.emit('error', err)
669
684
  }
670
685
 
686
+ async compactMaybe() {
687
+ if (!this.writers.writable) return 0
688
+
689
+ if (await this._isReindexed()) return 0
690
+
691
+ const view = await this._reindexBee(this._workingBee)
692
+ if (view > 0) this.bee.move(this._workingBee.head())
693
+
694
+ const system = await this._reindexBee(this.system.bee)
695
+
696
+ if (view + system > 0) this._reindexed = true
697
+
698
+ return view + system
699
+ }
700
+
701
+ async _isReindexed() {
702
+ const head = this.system.bee.head()
703
+ if (head === null) return true
704
+ if (await this._shouldReindex(head.key)) return false
705
+
706
+ const view = this.system.view
707
+ if (!view || !view.key) return true
708
+ return !(await this._shouldReindex(view.key))
709
+ }
710
+
711
+ async _reindexBee(bee) {
712
+ const head = bee.head()
713
+ const local = bee.context.local
714
+ if (head === null) return 0
715
+ if ((await this._shouldReindex(local.key)) && local.length > 0) return 0
716
+ if (!(await this._shouldReindex(head.key))) return 0
717
+
718
+ return bee.reindex(async (change) => !(await this._shouldReindex(change.head.key)))
719
+ }
720
+
721
+ async _shouldReindex(key, { unknown = false, length = 0, timeout = 0 } = {}) {
722
+ const core = this.store.get({ key, active: false })
723
+
724
+ try {
725
+ await core.ready()
726
+
727
+ if (core.manifest === null && length > 0) {
728
+ await core.get(length - 1, { raw: true, timeout }).catch(safetyCatch)
729
+ }
730
+
731
+ if (core.manifest === null) return unknown
732
+ return core.manifest.version > 1 && core.manifest.version < DEFAULT_MANIFEST_VERSION
733
+ } finally {
734
+ await core.close()
735
+ }
736
+ }
737
+
671
738
  // one-shot user gate: nothing applies until the host has resolved whatever
672
739
  // state apply depends on (e.g. legacy views recorded by a migration)
673
740
  async _runPreApply() {
@@ -688,6 +755,7 @@ module.exports = class Autobee extends ReadyResource {
688
755
  const { head = null, legacy = null } = this.bootFrom
689
756
 
690
757
  this.bootFrom = null
758
+ this._networkBooted = true
691
759
 
692
760
  if (legacy) {
693
761
  await this._bootFromSystem(legacy)
@@ -695,8 +763,14 @@ module.exports = class Autobee extends ReadyResource {
695
763
  this._wakeup.hint({ key: head.key, length: head.length || 0 })
696
764
  await this._bootFromHead(head)
697
765
  }
766
+ } else if (await this._isReindexing()) {
767
+ this._networkBooted = true
768
+ await this._bootFromNetwork()
698
769
  }
699
770
 
771
+ // preferably get a peer's compacted view during bootFrom
772
+ if (this.fastForwardTo === null) await this.compactMaybe()
773
+
700
774
  const changes = this._hasUpdate ? new UpdateChanges(this) : null
701
775
  if (changes) changes.track()
702
776
 
@@ -865,6 +939,7 @@ module.exports = class Autobee extends ReadyResource {
865
939
  const heads = await this._readCandidateHeads(hints, FastForward.DEFAULT_TIMEOUT)
866
940
 
867
941
  const ff = await FastForward.fromHeads(this, heads, {
942
+ skipGap: this._networkBooting,
868
943
  timeout: FastForward.DEFAULT_TIMEOUT
869
944
  })
870
945
 
@@ -1208,12 +1283,15 @@ module.exports = class Autobee extends ReadyResource {
1208
1283
  }
1209
1284
 
1210
1285
  async _appendAck() {
1211
- if (!this._acking) return false
1286
+ if (!this._acking && !this._reindexed) return false
1212
1287
  if (!this.writers.writable) return false
1213
1288
 
1214
1289
  if (this.writers.localWriter.pending !== null) return false
1215
- if ((await this._flushesBehind()) < this._ackThreshold) return false
1216
- if (await this._allHeadsTrusted()) return false
1290
+
1291
+ if (!this._reindexed) {
1292
+ if ((await this._flushesBehind()) < this._ackThreshold) return false
1293
+ if (await this._allHeadsTrusted()) return false
1294
+ }
1217
1295
 
1218
1296
  const links = this.system.getLinks(this.local.key)
1219
1297
  const now = this._now()
@@ -1371,6 +1449,14 @@ module.exports = class Autobee extends ReadyResource {
1371
1449
 
1372
1450
  const changed = await this.system.flush(batch, this._workingBee)
1373
1451
 
1452
+ if (this._reindexed && (await this._isReindexed())) {
1453
+ this._reindexed = false
1454
+ await this.local.setUserData(
1455
+ 'autobee/head',
1456
+ encoding.encodeBootRecord(this.system.bootRecord())
1457
+ )
1458
+ }
1459
+
1374
1460
  if (this.system.promotions.changed) this._prefetchApprovals().catch(safetyCatch)
1375
1461
 
1376
1462
  if (local) {
@@ -1551,6 +1637,41 @@ module.exports = class Autobee extends ReadyResource {
1551
1637
  }
1552
1638
  }
1553
1639
 
1640
+ async _bootFromNetwork() {
1641
+ this._networkBooting = true
1642
+
1643
+ try {
1644
+ await this._waitForNetworkBoot()
1645
+ } finally {
1646
+ this._networkBooting = false
1647
+ }
1648
+ }
1649
+
1650
+ async _waitForNetworkBoot() {
1651
+ const deadline = Date.now() + BOOT_NETWORK_TIMEOUT
1652
+
1653
+ this._requestWakeup()
1654
+
1655
+ while (!this._interrupting && !this.closing && this.fastForwardTo === null) {
1656
+ const hints = await this._applyWakeupHints()
1657
+ if (hints.length) this._queueFastForward(hints)
1658
+
1659
+ const remaining = deadline - Date.now()
1660
+ if (remaining <= 0 || this.fastForwardTo !== null) return
1661
+
1662
+ if (this._ffSearching !== null) {
1663
+ await Promise.race([this._ffSearching, this._bumpSignal.wait(remaining)])
1664
+ this._bumpSignal.notify()
1665
+ if (this._ffSearching === null) return
1666
+ continue
1667
+ }
1668
+
1669
+ if (hints.length) return
1670
+
1671
+ await this._bumpSignal.wait(remaining)
1672
+ }
1673
+ }
1674
+
1554
1675
  async _runFastForward(ff, { force = false } = {}) {
1555
1676
  if (this.fastForwardTo !== null || this.fastForwarding !== null) {
1556
1677
  await ff.close()
package/lib/constants.js CHANGED
@@ -1,10 +1,12 @@
1
1
  const LEGACY_OPLOG_VERSION = 2
2
2
  const OPLOG_VERSION = 4
3
+ const DEFAULT_MANIFEST_VERSION = 3
3
4
  const LEGACY_AUTOBASE_VERSION = 2
4
5
  const AUTOBEE_VERSION = 3
5
6
  const DEFAULT_OP_TIMEOUT = 5000
6
7
 
7
8
  module.exports = {
9
+ DEFAULT_MANIFEST_VERSION,
8
10
  LEGACY_OPLOG_VERSION,
9
11
  OPLOG_VERSION,
10
12
  LEGACY_AUTOBASE_VERSION,
@@ -7,7 +7,7 @@ const migrations = require('./migrations.js')
7
7
  const { LEGACY_AUTOBASE_VERSION, LEGACY_OPLOG_VERSION } = require('./constants.js')
8
8
 
9
9
  const DEFAULT_OP_TIMEOUT = 5_000
10
- const MIN_FF_GAP = 32
10
+ const MIN_FF_GAP = 16
11
11
 
12
12
  class FastForward {
13
13
  constructor(auto, head, { timeout = DEFAULT_OP_TIMEOUT, conservative = false } = {}) {
@@ -39,7 +39,7 @@ class FastForward {
39
39
 
40
40
  static DEFAULT_TIMEOUT = DEFAULT_OP_TIMEOUT
41
41
 
42
- static async fromHead(auto, head, trusted, { force = false, timeout = 0 } = {}) {
42
+ static async fromHead(auto, head, trusted, { force = false, skipGap = false, timeout = 0 } = {}) {
43
43
  const conservative = !force && auto._conservativeFF
44
44
 
45
45
  const oplog = await FastForward.flushHead(auto, head, { timeout })
@@ -51,7 +51,7 @@ class FastForward {
51
51
  // legacy nodes from non-indexers have no system info to fast-forward from
52
52
  if (!oplog.op.views || !verified.op.views) return null
53
53
 
54
- if (!force && verified.op.views.flushes - auto.system.flushes < MIN_FF_GAP) {
54
+ if (!force && !skipGap && verified.op.views.flushes - auto.system.flushes < MIN_FF_GAP) {
55
55
  return null
56
56
  }
57
57
 
@@ -61,7 +61,7 @@ class FastForward {
61
61
  })
62
62
  }
63
63
 
64
- static async fromHeads(auto, heads, { force = false, timeout = 0 } = {}) {
64
+ static async fromHeads(auto, heads, { force = false, skipGap = false, timeout = 0 } = {}) {
65
65
  const reference = auto._workingView.view
66
66
 
67
67
  const promises = []
@@ -71,14 +71,12 @@ class FastForward {
71
71
  promises.push(FastForward.flushHead(auto, head, { timeout }))
72
72
  }
73
73
 
74
- const ops = await Promise.all(promises)
74
+ const ops = await FastForward.reindexedCandidates(auto, await Promise.all(promises), {
75
+ timeout: timeout || DEFAULT_OP_TIMEOUT
76
+ })
75
77
  if (auto.fastForwarding || auto.fastForwardTo) return null
76
78
 
77
- const trust = await Promise.all(
78
- ops.map((res) => {
79
- return res === null ? false : auto.trusted.isTrusted(res.key, reference)
80
- })
81
- )
79
+ const trust = await Promise.all(ops.map((res) => auto.trusted.isTrusted(res.key, reference)))
82
80
  if (auto.fastForwarding || auto.fastForwardTo) return null
83
81
 
84
82
  const candidates = []
@@ -88,8 +86,7 @@ class FastForward {
88
86
 
89
87
  for (let i = 0; i < ops.length; i++) {
90
88
  const res = ops[i]
91
- if (res === null || !res.op.views) continue
92
- if (!force && res.op.views.flushes - auto.system.flushes < MIN_FF_GAP) continue
89
+ if (!force && !skipGap && res.op.views.flushes - auto.system.flushes < MIN_FF_GAP) continue
93
90
 
94
91
  if (!trust[i]) {
95
92
  candidates.push(res)
@@ -103,7 +100,7 @@ class FastForward {
103
100
  }
104
101
 
105
102
  if (bestTrusted !== null) {
106
- return FastForward.fromHead(auto, bestTrusted, null, { force, timeout })
103
+ return FastForward.fromHead(auto, bestTrusted, null, { force, skipGap, timeout })
107
104
  }
108
105
 
109
106
  candidates.sort((a, b) => b.op.views.flushes - a.op.views.flushes)
@@ -113,13 +110,38 @@ class FastForward {
113
110
  if (trusted === null) continue
114
111
  if (auto.fastForwarding || auto.fastForwardTo) return null
115
112
 
116
- const ff = await FastForward.fromHead(auto, res, trusted, { force, timeout })
113
+ const ff = await FastForward.fromHead(auto, res, trusted, { force, skipGap, timeout })
117
114
  if (ff !== null) return ff
118
115
  }
119
116
 
120
117
  return null
121
118
  }
122
119
 
120
+ static async reindexedCandidates(auto, ops, { timeout }) {
121
+ const withViews = ops.filter((res) => res !== null && res.op.views)
122
+ const reindexed = await Promise.all(
123
+ withViews.map((res) => FastForward.isReindexed(auto, res, timeout))
124
+ )
125
+ return withViews.filter((res, i) => reindexed[i])
126
+ }
127
+
128
+ static async isReindexed(auto, res, timeout) {
129
+ const { system, view } = res.op.views
130
+ const checks = [system, view]
131
+ .filter((v) => !!v)
132
+ .map((range) => {
133
+ const { key, length } = batchToHead(range)
134
+ if (length === 0) return false
135
+ return auto._shouldReindex(key, { unknown: true, length, timeout })
136
+ })
137
+
138
+ for (const shouldReindex of await Promise.all(checks)) {
139
+ if (shouldReindex) return false
140
+ }
141
+
142
+ return true
143
+ }
144
+
123
145
  static async flushHead(auto, head, opts) {
124
146
  const core = auto.openCore(head.key)
125
147
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autobee",
3
- "version": "2.10.1",
3
+ "version": "2.11.0",
4
4
  "description": "Bee based autobase",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Holepunch Inc.",
@@ -27,8 +27,8 @@
27
27
  "autobee-wakeup": "^1.1.3",
28
28
  "b4a": "^1.8.0",
29
29
  "compact-encoding": "^3.3.0",
30
- "hyperbee2": "^2.16.0",
31
- "hypercore": "^11.33.2",
30
+ "hyperbee2": "^2.17.0",
31
+ "hypercore": "^11.37.0",
32
32
  "hypercore-id-encoding": "^1.3.0",
33
33
  "hyperschema": "^1.26.2",
34
34
  "read-write-mutexify": "^2.1.0",
@@ -36,6 +36,7 @@
36
36
  "ready-resource": "^1.2.0",
37
37
  "resolve-reject-promise": "^1.1.0",
38
38
  "safety-catch": "^1.0.3",
39
+ "signal-promise": "^1.0.3",
39
40
  "sodium-native": "^5.1.0"
40
41
  },
41
42
  "devDependencies": {