autobee 2.0.0-rc.14 → 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 +48 -1
- package/encoding/spec/autobee/index.js +48 -3
- package/encoding/spec/autobee/schema.json +32 -0
- package/index.js +221 -142
- package/lib/apply-view.js +25 -0
- package/lib/boot.js +1 -0
- package/lib/{reboot.js → fast-forward.js} +203 -5
- package/lib/topo.js +1 -1
- package/lib/trusted.js +44 -0
- package/lib/writers.js +17 -6
- package/package.json +1 -1
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
|
|
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
|
|
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
|
@@ -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
|
|
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,7 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
56
57
|
this.discoveryKey = null
|
|
57
58
|
this.id = null
|
|
58
59
|
this.bootstrap = null
|
|
59
|
-
this.
|
|
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 =
|
|
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
|
-
//
|
|
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
|
|
|
88
|
-
this.
|
|
89
|
-
|
|
90
|
-
this.
|
|
89
|
+
this.trusted = new TrustedPeers(handlers)
|
|
90
|
+
|
|
91
|
+
this.ff = null
|
|
92
|
+
this.fastForwarding = null
|
|
93
|
+
this.fastForwardTo = null
|
|
91
94
|
|
|
92
95
|
this._workingBee = bee
|
|
93
|
-
this._workingView =
|
|
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
|
-
|
|
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.
|
|
250
|
+
await this._workingView.close()
|
|
242
251
|
await this.bee.close()
|
|
243
252
|
await this.store.close()
|
|
244
253
|
}
|
|
@@ -250,6 +259,8 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
250
259
|
session.clearRequests(session.activeRequests, closingError)
|
|
251
260
|
}
|
|
252
261
|
}
|
|
262
|
+
|
|
263
|
+
if (this.fastForwarding) this.fastForwarding.clearRequests(closingError)
|
|
253
264
|
}
|
|
254
265
|
|
|
255
266
|
async changesFrom({ system } = {}) {
|
|
@@ -285,6 +296,14 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
285
296
|
this._wakeup.hint(wakeup)
|
|
286
297
|
}
|
|
287
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
|
+
|
|
288
307
|
openCore(key) {
|
|
289
308
|
const encryption = this.encryptionKey ? new WriterEncryption(this) : null
|
|
290
309
|
return this.store.get({ key, encryption })
|
|
@@ -310,7 +329,7 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
310
329
|
}
|
|
311
330
|
|
|
312
331
|
if (this.bootFrom) {
|
|
313
|
-
this.bootFrom = (await this.bootFrom)
|
|
332
|
+
this.bootFrom = getBootOption(await this.bootFrom)
|
|
314
333
|
}
|
|
315
334
|
}
|
|
316
335
|
|
|
@@ -398,8 +417,8 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
398
417
|
|
|
399
418
|
// @todo migration
|
|
400
419
|
if (result.migration) {
|
|
401
|
-
if (this.
|
|
402
|
-
view = (await this.
|
|
420
|
+
if (this._handlers.migrate) {
|
|
421
|
+
view = (await this._handlers.migrate(result.migration.views)) || EMPTY_HEAD
|
|
403
422
|
this._catchupMigratedNodes = result.migration.catchup
|
|
404
423
|
await this._storeMigratedView(view)
|
|
405
424
|
}
|
|
@@ -441,7 +460,7 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
441
460
|
async _bump() {
|
|
442
461
|
if (!(await this._bootReady())) return
|
|
443
462
|
|
|
444
|
-
|
|
463
|
+
if (this._bootWait !== null) this._bootWait.resolve()
|
|
445
464
|
|
|
446
465
|
this.bumping++
|
|
447
466
|
|
|
@@ -502,8 +521,12 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
502
521
|
this.stats.drains++
|
|
503
522
|
|
|
504
523
|
if (this.bootFrom) {
|
|
505
|
-
|
|
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
|
|
@@ -525,8 +548,11 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
525
548
|
|
|
526
549
|
try {
|
|
527
550
|
while (!this._interrupting) {
|
|
528
|
-
|
|
529
|
-
|
|
551
|
+
await this._flushWakeup()
|
|
552
|
+
if (this._interrupting) break
|
|
553
|
+
|
|
554
|
+
if (this.fastForwardTo !== null) {
|
|
555
|
+
await this._applyFastForward()
|
|
530
556
|
break // revaluate conditions...
|
|
531
557
|
}
|
|
532
558
|
|
|
@@ -593,50 +619,100 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
593
619
|
|
|
594
620
|
async _flushWakeup() {
|
|
595
621
|
const hints = this._wakeup.flush()
|
|
622
|
+
if (!hints.size) return
|
|
596
623
|
|
|
597
624
|
this.previousDrain = Date.now()
|
|
598
|
-
this.rebootFromHeads(hints).catch(noop)
|
|
599
625
|
|
|
600
626
|
for (const [hex, length] of hints) {
|
|
601
627
|
const key = b4a.from(hex, 'hex')
|
|
602
628
|
if (this.writers.has(hex)) continue
|
|
603
629
|
await this.writers.wakeup(key, length)
|
|
604
630
|
}
|
|
605
|
-
}
|
|
606
631
|
|
|
607
|
-
|
|
608
|
-
|
|
632
|
+
// a scheduled fast-forward is applied by the drain before we look again
|
|
633
|
+
if (this.fastForwardTo !== null || this.fastForwarding !== null) return
|
|
634
|
+
if (this._interrupting || this.closing || this.bootFrom) return
|
|
609
635
|
|
|
610
636
|
try {
|
|
611
|
-
await
|
|
637
|
+
const heads = await this._readCandidateHeads(hints, FastForward.DEFAULT_TIMEOUT)
|
|
612
638
|
|
|
613
|
-
const
|
|
614
|
-
|
|
639
|
+
const ff = await FastForward.fromHeads(this, heads, {
|
|
640
|
+
timeout: FastForward.DEFAULT_TIMEOUT
|
|
641
|
+
})
|
|
615
642
|
|
|
616
|
-
|
|
617
|
-
|
|
643
|
+
if (ff !== null) await this._runFastForward(ff)
|
|
644
|
+
} catch (err) {
|
|
645
|
+
safetyCatch(err)
|
|
646
|
+
}
|
|
647
|
+
}
|
|
618
648
|
|
|
619
|
-
|
|
620
|
-
|
|
649
|
+
async _readCandidateHeads(hints, timeout) {
|
|
650
|
+
const promises = []
|
|
621
651
|
|
|
622
|
-
|
|
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
|
+
}
|
|
623
657
|
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
if (op.version < 3) {
|
|
627
|
-
op = await this._inflateLegacyOplog(buf, core, target - 1, opts)
|
|
628
|
-
}
|
|
658
|
+
const ops = await Promise.all(promises)
|
|
659
|
+
const heads = []
|
|
629
660
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
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)
|
|
634
672
|
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
return heads
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
async _getOplog(key, length, opts = null) {
|
|
679
|
+
const core = this.openCore(key)
|
|
680
|
+
|
|
681
|
+
try {
|
|
682
|
+
return await this.readOplog(core, length, opts)
|
|
635
683
|
} finally {
|
|
636
684
|
await core.close()
|
|
637
685
|
}
|
|
638
686
|
}
|
|
639
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()
|
|
691
|
+
|
|
692
|
+
const target = length >= 0 ? length : core.length
|
|
693
|
+
if (target === 0) return null
|
|
694
|
+
|
|
695
|
+
// conservative: only proceed when a connected peer can serve the head whole
|
|
696
|
+
if (opts && opts.conservative && core.remoteContiguousLength < target) return null
|
|
697
|
+
|
|
698
|
+
const buf = await core.get(target - 1, opts)
|
|
699
|
+
if (buf === null) return null
|
|
700
|
+
|
|
701
|
+
let op = encoding.decodeOplog(buf)
|
|
702
|
+
|
|
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
|
+
}
|
|
708
|
+
|
|
709
|
+
return {
|
|
710
|
+
key: core.key,
|
|
711
|
+
length: target,
|
|
712
|
+
op
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
640
716
|
async _inflateLegacyOplog(buf, core, seq, opts = null) {
|
|
641
717
|
const m = encoding.decodeRawOplog(buf)
|
|
642
718
|
|
|
@@ -734,7 +810,7 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
734
810
|
await this.bootstrap.setUserData('autobee/local', this.local.key)
|
|
735
811
|
await oldLocal.close()
|
|
736
812
|
|
|
737
|
-
// done, soft
|
|
813
|
+
// done, soft restart
|
|
738
814
|
this.emit('rotate-local-writer')
|
|
739
815
|
}
|
|
740
816
|
|
|
@@ -923,6 +999,7 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
923
999
|
|
|
924
1000
|
if (t.undo) {
|
|
925
1001
|
this.stats.undos++
|
|
1002
|
+
this.trusted.clear()
|
|
926
1003
|
t.view = await this.system.undo(t.undo)
|
|
927
1004
|
}
|
|
928
1005
|
|
|
@@ -973,7 +1050,7 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
973
1050
|
if (this._hasApply && (await this.system.canApply(batch[0].key, optimistic))) {
|
|
974
1051
|
this.stats.applies++
|
|
975
1052
|
this._host.applying = batch
|
|
976
|
-
await this.
|
|
1053
|
+
await this._workingView.apply(userBatch)
|
|
977
1054
|
this._host.applying = null
|
|
978
1055
|
}
|
|
979
1056
|
|
|
@@ -1119,135 +1196,130 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
1119
1196
|
this._localViewLength = 0
|
|
1120
1197
|
}
|
|
1121
1198
|
|
|
1122
|
-
async
|
|
1123
|
-
if (!this.
|
|
1124
|
-
if (this.
|
|
1125
|
-
if (!hints.size || this.rebooting || this.rebootTo || this.bootFrom) {
|
|
1126
|
-
return false
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
const promises = []
|
|
1130
|
-
const heads = []
|
|
1131
|
-
for (const [hex, length] of hints) {
|
|
1132
|
-
if (length === 0) continue
|
|
1133
|
-
const key = b4a.from(hex, 'hex')
|
|
1134
|
-
heads.push({ key, length })
|
|
1135
|
-
promises.push(this._getOplog(key, length))
|
|
1136
|
-
}
|
|
1199
|
+
async moveTo(head) {
|
|
1200
|
+
if (!this.opened) await this.ready()
|
|
1201
|
+
if (this.closing) throw new Error('Autobee closed')
|
|
1137
1202
|
|
|
1138
|
-
const
|
|
1139
|
-
if (
|
|
1203
|
+
const ff = await FastForward.fromHead(this, head, null, { force: true })
|
|
1204
|
+
if (ff === null) return null
|
|
1140
1205
|
|
|
1141
|
-
|
|
1142
|
-
let bestFlushes = -1
|
|
1206
|
+
if (!(await this._runFastForward(ff))) return null
|
|
1143
1207
|
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
if (res === null) continue
|
|
1208
|
+
return this.ff.promise
|
|
1209
|
+
}
|
|
1147
1210
|
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
}
|
|
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
|
|
1152
1219
|
}
|
|
1220
|
+
}
|
|
1153
1221
|
|
|
1154
|
-
|
|
1155
|
-
|
|
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
|
|
1237
|
+
}
|
|
1156
1238
|
}
|
|
1157
1239
|
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
const v = best.op.views.view
|
|
1163
|
-
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
|
|
1164
1243
|
|
|
1165
|
-
let trusted = null
|
|
1166
1244
|
try {
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
} finally {
|
|
1170
|
-
view.close()
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
return this._rebootFromHead(best, trusted)
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
async _rebootFromHead(head, trusted, { force = false, wait = true } = {}) {
|
|
1177
|
-
const conservative = !force && this._conservativeFF
|
|
1178
|
-
|
|
1179
|
-
const oplog = await this._getOplog(head.key, head.length, { conservative })
|
|
1180
|
-
if (!oplog) return null
|
|
1245
|
+
while (!this._interrupting) {
|
|
1246
|
+
this._bootWait = rrp()
|
|
1181
1247
|
|
|
1182
|
-
|
|
1183
|
-
|
|
1248
|
+
try {
|
|
1249
|
+
if (opened === null) opened = await this._bootReference(head)
|
|
1184
1250
|
|
|
1185
|
-
|
|
1186
|
-
|
|
1251
|
+
if (opened !== null && (await this._bootAttempt(head, bootCondition, opened.view))) {
|
|
1252
|
+
return true
|
|
1253
|
+
}
|
|
1254
|
+
} catch (err) {
|
|
1255
|
+
safetyCatch(err)
|
|
1256
|
+
}
|
|
1187
1257
|
|
|
1188
|
-
|
|
1189
|
-
return null
|
|
1190
|
-
}
|
|
1258
|
+
if (this._interrupting) break
|
|
1191
1259
|
|
|
1192
|
-
|
|
1193
|
-
system: batchToHead(oplog.op.views.system),
|
|
1194
|
-
verified: {
|
|
1195
|
-
op: trusted || head,
|
|
1196
|
-
flushes: verified.op.views.flushes
|
|
1260
|
+
await this._bootWait.promise
|
|
1197
1261
|
}
|
|
1262
|
+
} finally {
|
|
1263
|
+
this._bootWait = null
|
|
1264
|
+
if (opened !== null) await opened.close()
|
|
1198
1265
|
}
|
|
1199
1266
|
|
|
1200
|
-
|
|
1267
|
+
return false
|
|
1268
|
+
}
|
|
1201
1269
|
|
|
1202
|
-
|
|
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
|
+
)
|
|
1203
1276
|
|
|
1204
|
-
|
|
1277
|
+
const ff = await FastForward.fromHeads(this, [head, ...candidates], {
|
|
1278
|
+
force: true,
|
|
1279
|
+
timeout: FastForward.DEFAULT_TIMEOUT,
|
|
1280
|
+
condition: bootCondition,
|
|
1281
|
+
reference
|
|
1282
|
+
})
|
|
1283
|
+
|
|
1284
|
+
return ff !== null && (await this._runFastForward(ff))
|
|
1205
1285
|
}
|
|
1206
1286
|
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
return this._runReboot(new Reboot(this, head, tip))
|
|
1212
|
-
}
|
|
1287
|
+
async _bootReference(head) {
|
|
1288
|
+
const oplog = await FastForward.flushHead(this, head, {
|
|
1289
|
+
timeout: FastForward.DEFAULT_TIMEOUT
|
|
1290
|
+
})
|
|
1213
1291
|
|
|
1214
|
-
return
|
|
1292
|
+
return oplog === null ? null : this.openViewAt(oplog)
|
|
1215
1293
|
}
|
|
1216
1294
|
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
if (await this._runReboot(new Reboot(this, systemHead, tip))) {
|
|
1222
|
-
return true
|
|
1295
|
+
async _runFastForward(ff) {
|
|
1296
|
+
if (this.fastForwardTo !== null || this.fastForwarding !== null) {
|
|
1297
|
+
await ff.close()
|
|
1298
|
+
return false
|
|
1223
1299
|
}
|
|
1224
1300
|
|
|
1225
|
-
|
|
1226
|
-
}
|
|
1227
|
-
|
|
1228
|
-
async _runReboot(reboot) {
|
|
1229
|
-
this.rebooting = reboot
|
|
1301
|
+
this.fastForwarding = ff
|
|
1230
1302
|
|
|
1231
|
-
const result = await
|
|
1232
|
-
await
|
|
1303
|
+
const result = await ff.run()
|
|
1304
|
+
await ff.close()
|
|
1233
1305
|
|
|
1234
|
-
if (this.
|
|
1306
|
+
if (this.fastForwarding === ff) this.fastForwarding = null
|
|
1235
1307
|
|
|
1236
1308
|
if (!result) return false
|
|
1237
1309
|
|
|
1238
|
-
this.
|
|
1239
|
-
this.
|
|
1310
|
+
this.fastForwardTo = result
|
|
1311
|
+
this.ff = rrp()
|
|
1240
1312
|
|
|
1241
1313
|
this.bumpSoon()
|
|
1242
1314
|
|
|
1243
1315
|
return true
|
|
1244
1316
|
}
|
|
1245
1317
|
|
|
1246
|
-
async
|
|
1318
|
+
async _applyFastForward() {
|
|
1247
1319
|
const changes = this._hasUpdate ? new UpdateChanges(this) : null
|
|
1248
1320
|
if (changes) changes.track()
|
|
1249
1321
|
|
|
1250
|
-
const { head, tip, migrate } = this.
|
|
1322
|
+
const { head, tip, migrate } = this.fastForwardTo
|
|
1251
1323
|
|
|
1252
1324
|
const from = this.system.bee.head()
|
|
1253
1325
|
const to = head
|
|
@@ -1255,7 +1327,7 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
1255
1327
|
this.system.bee.move(head)
|
|
1256
1328
|
await this.system.reset()
|
|
1257
1329
|
|
|
1258
|
-
this.system.shared = this.
|
|
1330
|
+
this.system.shared = this.fastForwardTo.shared || {
|
|
1259
1331
|
flushes: -1,
|
|
1260
1332
|
view: EMPTY_HEAD,
|
|
1261
1333
|
system: EMPTY_HEAD
|
|
@@ -1263,7 +1335,7 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
1263
1335
|
|
|
1264
1336
|
// migrate is set when fast-forwarding from a legacy head
|
|
1265
1337
|
if (migrate) {
|
|
1266
|
-
const view = (await this.
|
|
1338
|
+
const view = (await this._handlers.migrate(migrate)) || EMPTY_HEAD
|
|
1267
1339
|
await this._storeMigratedView(view)
|
|
1268
1340
|
this.bee.move(view)
|
|
1269
1341
|
this._workingBee.move(view)
|
|
@@ -1272,16 +1344,19 @@ module.exports = class Autobee extends ReadyResource {
|
|
|
1272
1344
|
this._workingBee.move(this.system.view)
|
|
1273
1345
|
}
|
|
1274
1346
|
|
|
1275
|
-
this.
|
|
1347
|
+
this.fastForwardTo = null
|
|
1276
1348
|
await this.writers.refresh()
|
|
1277
1349
|
|
|
1350
|
+
// we moved, so ask our peers to tell us their heads again
|
|
1351
|
+
this._requestWakeup()
|
|
1352
|
+
|
|
1278
1353
|
await this._storeBoot()
|
|
1279
1354
|
|
|
1280
1355
|
await this._update(changes)
|
|
1281
1356
|
|
|
1282
1357
|
this.stats.fastForwards++
|
|
1283
1358
|
this.emit('move-to', to, from)
|
|
1284
|
-
this.
|
|
1359
|
+
this.ff.resolve({ to, from })
|
|
1285
1360
|
|
|
1286
1361
|
// tip is null during boot
|
|
1287
1362
|
if (!tip) return
|
|
@@ -1315,6 +1390,17 @@ function isObject(o) {
|
|
|
1315
1390
|
return typeof o === 'object' && o && !b4a.isBuffer(o)
|
|
1316
1391
|
}
|
|
1317
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
|
+
|
|
1318
1404
|
function noop() {}
|
|
1319
1405
|
|
|
1320
1406
|
function createAnchorCore(store, prologue, manifestData) {
|
|
@@ -1343,10 +1429,3 @@ function crashSoon(err) {
|
|
|
1343
1429
|
})
|
|
1344
1430
|
throw err
|
|
1345
1431
|
}
|
|
1346
|
-
|
|
1347
|
-
function batchToHead(b) {
|
|
1348
|
-
return {
|
|
1349
|
-
key: b.key,
|
|
1350
|
-
length: b.start + b.length
|
|
1351
|
-
}
|
|
1352
|
-
}
|
|
@@ -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
|
+
}
|
package/lib/boot.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const safetyCatch = require('safety-catch')
|
|
2
2
|
const b4a = require('b4a')
|
|
3
|
+
const Hypercore = require('hypercore')
|
|
3
4
|
const c = require('compact-encoding')
|
|
4
5
|
const crypto = require('hypercore-crypto')
|
|
5
6
|
|
|
@@ -18,13 +19,24 @@ const [NS_SIGNER_NAMESPACE] = crypto.namespace('autobase', 1)
|
|
|
18
19
|
|
|
19
20
|
const EMPTY = b4a.alloc(0)
|
|
20
21
|
const DEFAULT_OP_TIMEOUT = 5_000
|
|
22
|
+
const MIN_FF_GAP = 32
|
|
21
23
|
|
|
22
|
-
|
|
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
|
|
27
|
+
|
|
28
|
+
module.exports = class FastForward {
|
|
23
29
|
constructor(auto, head, tip, { timeout = DEFAULT_OP_TIMEOUT } = {}) {
|
|
24
30
|
this.auto = auto
|
|
31
|
+
|
|
32
|
+
this.activeRequests = []
|
|
33
|
+
|
|
34
|
+
// 'reboot' is a storage namespace: renaming it would derive a different
|
|
35
|
+
// scratch core on disk, so it stays as is
|
|
25
36
|
this.system = new System(auto.store.namespace('reboot'), null, {
|
|
26
37
|
getEncryptionProvider: this.auto.getSystemEncryption,
|
|
27
|
-
encrypted: this.encrypted
|
|
38
|
+
encrypted: this.encrypted,
|
|
39
|
+
activeRequests: this.activeRequests
|
|
28
40
|
})
|
|
29
41
|
|
|
30
42
|
// length -1 marks an uninitialised head: we know the key but not the length
|
|
@@ -37,6 +49,164 @@ module.exports = class Reboot {
|
|
|
37
49
|
this.cores = []
|
|
38
50
|
}
|
|
39
51
|
|
|
52
|
+
static DEFAULT_TIMEOUT = DEFAULT_OP_TIMEOUT
|
|
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
|
+
|
|
40
210
|
async run() {
|
|
41
211
|
try {
|
|
42
212
|
if (!this.running) this.running = this._run()
|
|
@@ -77,7 +247,12 @@ module.exports = class Reboot {
|
|
|
77
247
|
const view = this.auto.store.get({ key: this.system.view.key, active: true })
|
|
78
248
|
this.cores.push(view)
|
|
79
249
|
|
|
80
|
-
promises.push(
|
|
250
|
+
promises.push(
|
|
251
|
+
view.get(this.system.view.length - 1, {
|
|
252
|
+
timeout: this.timeout,
|
|
253
|
+
activeRequests: this.activeRequests
|
|
254
|
+
})
|
|
255
|
+
)
|
|
81
256
|
|
|
82
257
|
for (const head of this.system.heads) {
|
|
83
258
|
promises.push(this.system.get(head.key, { timeout: this.timeout }))
|
|
@@ -118,7 +293,12 @@ module.exports = class Reboot {
|
|
|
118
293
|
await core.setEncryption(this.auto.getSystemEncryption())
|
|
119
294
|
|
|
120
295
|
// Decode hyperbee block using hyperbee2 compat
|
|
121
|
-
const node = decodeBlock(
|
|
296
|
+
const node = decodeBlock(
|
|
297
|
+
await core.get(this.head.length - 1, {
|
|
298
|
+
timeout: this.timeout,
|
|
299
|
+
activeRequests: this.activeRequests
|
|
300
|
+
})
|
|
301
|
+
)
|
|
122
302
|
assert(node.keys.length > 0, 'bad system block')
|
|
123
303
|
|
|
124
304
|
// Decode system info from hyperbee block using autobee compat
|
|
@@ -163,7 +343,12 @@ module.exports = class Reboot {
|
|
|
163
343
|
await core.ready()
|
|
164
344
|
|
|
165
345
|
try {
|
|
166
|
-
if (!core.manifest)
|
|
346
|
+
if (!core.manifest) {
|
|
347
|
+
await core.get(length - 1, {
|
|
348
|
+
timeout: this.timeout,
|
|
349
|
+
activeRequests: this.activeRequests
|
|
350
|
+
})
|
|
351
|
+
}
|
|
167
352
|
} catch {
|
|
168
353
|
return null
|
|
169
354
|
}
|
|
@@ -191,13 +376,26 @@ module.exports = class Reboot {
|
|
|
191
376
|
}
|
|
192
377
|
}
|
|
193
378
|
|
|
379
|
+
// cancels every read in flight and fails any that come after
|
|
380
|
+
clearRequests(err = null) {
|
|
381
|
+
Hypercore.destroyRequests(this.activeRequests, err)
|
|
382
|
+
}
|
|
383
|
+
|
|
194
384
|
async close() {
|
|
195
385
|
this.destroyed = true
|
|
386
|
+
this.clearRequests()
|
|
196
387
|
if (this.system) await this.system.close()
|
|
197
388
|
for (const core of this.cores) await core.close()
|
|
198
389
|
}
|
|
199
390
|
}
|
|
200
391
|
|
|
392
|
+
function batchToHead(b) {
|
|
393
|
+
return {
|
|
394
|
+
key: b.key,
|
|
395
|
+
length: b.start + b.length
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
201
399
|
function decodeLegacySystemInfo(buffer) {
|
|
202
400
|
const state = { start: 0, end: buffer.length, buffer }
|
|
203
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
|
|
105
|
+
if (w.pending === null || w.processed === 0) return
|
|
103
106
|
|
|
104
107
|
const queued = this.attestations
|
|
105
108
|
this.attestations = []
|
|
106
109
|
|
|
107
|
-
|
|
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
|
|